diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index dadb65d427..d38af3fb6d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,25 +1,102 @@ { "name": "retina", - "image": "mcr.microsoft.com/devcontainers/base:jammy", + "image": "mcr.microsoft.com/devcontainers/base:noble", "features": { - "ghcr.io/devcontainers/features/common-utils:2": {}, - "ghcr.io/devcontainers/features/docker-in-docker:2": {}, - "ghcr.io/devcontainers/features/github-cli:1": {}, - "ghcr.io/devcontainers/features/go:1": {}, - "ghcr.io/devcontainers/features/kubectl-helm-minikube:1": {}, - "ghcr.io/devcontainers-contrib/features/kind:1": {}, - "ghcr.io/devcontainers/features/azure-cli:1": {} + "ghcr.io/devcontainers/features/docker-in-docker:2.16.1": {}, + "ghcr.io/devcontainers/features/github-cli:1.1.0": {}, + "ghcr.io/devcontainers/features/go:1.3.4": { + "version": "1.24.11" + }, + "ghcr.io/devcontainers/features/kubectl-helm-minikube:1.3.1": {}, + "ghcr.io/devcontainers-extra/features/kind:1.0.15": {}, + "ghcr.io/devcontainers/features/azure-cli:1.2.9": {}, + // LLVM 17 is the minimum version available for Ubuntu Noble on apt.llvm.org. + // Provides clang and llvm-strip needed for eBPF compilation. + "ghcr.io/devcontainers-community/features/llvm:3.2.0": { + "version": "17" + } + }, + "hostRequirements": { + "cpus": 4, + "memory": "8gb", + "storage": "32gb" + }, + // Persist Go module and build caches across container rebuilds. + "mounts": [ + { + "type": "volume", + "source": "retina-gomodcache", + "target": "/go/pkg/mod" + }, + { + "type": "volume", + "source": "retina-gobuildcache", + "target": "/home/vscode/.cache/go-build" + } + ], + // These commands run in parallel during container creation. + "onCreateCommand": { + // The LLVM feature installs versioned binaries (clang-17, llvm-strip-17). + // Create unversioned symlinks so the build system can find them. + "symlinks": "sudo ln -sf /usr/bin/clang-17 /usr/bin/clang && sudo ln -sf /usr/bin/llvm-strip-17 /usr/bin/llvm-strip", + // Fix ownership of volume mounts (created as root) and cache dirs, + // then download Go modules. + "go-setup": "sudo chown -R vscode:vscode /go /home/vscode/.cache && go mod download", + // Install jq (needed by some Makefile targets and scripts). + "apt-deps": "sudo apt-get update && sudo apt-get install -y --no-install-recommends jq && sudo rm -rf /var/lib/apt/lists/*" + }, + // Wait for Docker-in-Docker to be ready, then create a Kind cluster for local testing. + "postStartCommand": { + "kind": "while ! docker info >/dev/null 2>&1; do sleep 1; done && kind create cluster 2>/dev/null || true" + }, + "waitFor": "onCreateCommand", + "forwardPorts": [ + 9965, + 4244, + 10093 + ], + "portsAttributes": { + "9965": { + "label": "Hubble Metrics", + "onAutoForward": "silent" + }, + "4244": { + "label": "Hubble Relay", + "onAutoForward": "silent" + }, + "10093": { + "label": "Retina Metrics", + "onAutoForward": "silent" + } }, - "postCreateCommand": "bash .devcontainer/installMoreTools.sh && kind create cluster", "customizations": { "vscode": { "extensions": [ - "esbenp.prettier-vscode", "golang.go", - "mutantdino.resourcemonitor", "ms-vscode.makefile-tools", - "ms-kubernetes-tools.vscode-kubernetes-tools" - ] + "ms-kubernetes-tools.vscode-kubernetes-tools", + "ms-azuretools.vscode-docker", + "redhat.vscode-yaml", + "mutantdino.resourcemonitor", + "DavidAnson.vscode-markdownlint" + ], + "settings": { + "go.lintTool": "golangci-lint", + "go.lintFlags": [ + "--config=.golangci.yaml", + "--timeout=10m" + ], + "go.formatTool": "gofumpt", + "files.insertFinalNewline": true, + "markdownlint.config": { + "extends": ".github/.markdownlint.json" + }, + "[markdown]": { + "editor.codeActionsOnSave": { + "source.fixAll.markdownlint": "explicit" + } + } + } } } } diff --git a/.devcontainer/installMoreTools.sh b/.devcontainer/installMoreTools.sh deleted file mode 100755 index 8d8fc3b369..0000000000 --- a/.devcontainer/installMoreTools.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -# Install the required tools and dependencies -sudo apt-get update && sudo apt-get install -y lsb-release wget software-properties-common gnupg clang-14 lldb-14 lld-14 clangd-14 man-db - -# Install LLVM 14 -export LLVM_VERSION=14 -curl -sL https://apt.llvm.org/llvm.sh | sudo bash -s "$LLVM_VERSION" diff --git a/.github/.markdownlint.json b/.github/.markdownlint.json index e0d691f1ee..8693de0cdf 100644 --- a/.github/.markdownlint.json +++ b/.github/.markdownlint.json @@ -2,6 +2,9 @@ "MD013": false, "MD010": false, "MD033": false, + "MD058": false, + "MD059": false, + "MD060": false, "MD024": { "siblings_only": true } diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 3e89fa2f7b..1e471c9bcd 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -1,15 +1,36 @@ version: 2 updates: + # Docker base images across all Dockerfiles - package-ecosystem: "docker" - directory: "/" + directories: + - "/controller" + - "/shell" + - "/cli" + - "/operator" + - "/test/image" + - "/hack/tools/kapinger" + - "/hack/tools/toolbox" schedule: - interval: "daily" + interval: "weekly" reviewers: - "microsoft/retina" commit-message: prefix: "deps" labels: ["area/infra", "area/dependencies"] open-pull-requests-limit: 10 + cooldown: + default-days: 7 + groups: + golang-base: + patterns: ["*golang*"] + azurelinux-base: + patterns: ["*azurelinux*"] + windows-base: + patterns: ["*windows*", "*nanoserver*", "*servercore*"] + ubuntu-base: + patterns: ["*ubuntu*"] + + # GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: @@ -20,6 +41,34 @@ updates: prefix: "deps" labels: ["area/infra", "area/dependencies"] open-pull-requests-limit: 10 + cooldown: + default-days: 3 + groups: + actions-patch: + update-types: ["patch"] + + # npm (Docusaurus site) + - package-ecosystem: "npm" + directory: "/site" + schedule: + interval: "weekly" + reviewers: + - "microsoft/retina" + commit-message: + prefix: "deps" + labels: ["area/docs", "area/dependencies"] + open-pull-requests-limit: 5 + cooldown: + default-days: 7 + semver-major-days: 30 + groups: + docusaurus: + patterns: + - "@docusaurus/*" + - "@mdx-js/*" + update-types: ["patch"] + + # Go modules - package-ecosystem: "gomod" directory: "/" schedule: @@ -29,6 +78,32 @@ updates: commit-message: prefix: "deps" labels: ["lang/go", "area/dependencies"] - ignore: - - dependency-name: "github.com/inspektor-gadget/inspektor-gadget" open-pull-requests-limit: 10 + cooldown: + default-days: 7 + semver-major-days: 30 + groups: + k8s: + patterns: + - "k8s.io/*" + - "sigs.k8s.io/*" + exclude-patterns: + - "sigs.k8s.io/cloud-provider-azure/*" + update-types: ["patch"] + cilium: + patterns: + - "github.com/cilium/*" + update-types: ["patch"] + aws-sdk: + patterns: + - "github.com/aws/aws-sdk-go-v2/*" + update-types: ["patch"] + azure-sdk: + patterns: + - "github.com/Azure/*" + - "sigs.k8s.io/cloud-provider-azure/*" + update-types: ["patch"] + otel: + patterns: + - "go.opentelemetry.io/*" + update-types: ["patch"] diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml index bc94b24d7b..519a76ca9e 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -6,6 +6,14 @@ on: branches: [main] pull_request: branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: analyze: name: Analyze @@ -17,7 +25,6 @@ jobs: language: [go] runs-on: ubuntu-latest env: - IS_NOT_MERGE_GROUP: ${{ github.event_name != 'merge_group' }} GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} timeout-minutes: 90 @@ -27,23 +34,18 @@ jobs: security-events: write steps: - name: Checkout repository - if: env.IS_NOT_MERGE_GROUP - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup go - if: env.IS_NOT_MERGE_GROUP - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod - name: Initialize CodeQL - if: env.IS_NOT_MERGE_GROUP - uses: github/codeql-action/init@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 + uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 with: languages: ${{ matrix.language }} - name: Autobuild - if: env.IS_NOT_MERGE_GROUP - uses: github/codeql-action/autobuild@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 + uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 - name: Perform CodeQL Analysis - if: env.IS_NOT_MERGE_GROUP - uses: github/codeql-action/analyze@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 + uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/commit-message.yaml b/.github/workflows/commit-message.yaml index ea7e1a1128..3c9d30a17d 100644 --- a/.github/workflows/commit-message.yaml +++ b/.github/workflows/commit-message.yaml @@ -8,10 +8,15 @@ on: - synchronize - edited - reopened + +permissions: + contents: read + jobs: commit-message: if: ${{ github.event_name != 'merge_group' }} runs-on: ubuntu-24.04 + timeout-minutes: 5 steps: - name: verify_commit_message env: diff --git a/.github/workflows/devcontainer.yaml b/.github/workflows/devcontainer.yaml new file mode 100644 index 0000000000..77161f5e39 --- /dev/null +++ b/.github/workflows/devcontainer.yaml @@ -0,0 +1,46 @@ +name: DevContainer +on: + merge_group: + push: + branches: [main] + paths: + - ".devcontainer/**" + - "go.mod" + pull_request: + branches: [main] + paths: + - ".devcontainer/**" + - "go.mod" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build DevContainer + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Build and validate devcontainer + uses: devcontainers/ci@b63b30de439b47a52267f241112c5b453b673db5 # v0.3.1900000449 + with: + runCmd: | + clang --version + llvm-strip --version + go version + EXPECTED_GO=$(grep '^go ' go.mod | awk '{print $2}') + ACTUAL_GO=$(go version | grep -oP '\d+\.\d+\.\d+') + if [ "$EXPECTED_GO" != "$ACTUAL_GO" ]; then + echo "::error::Go version mismatch: devcontainer has $ACTUAL_GO but go.mod requires $EXPECTED_GO" + exit 1 + fi + kubectl version --client + helm version + kind version + grep -rl 'go:generate.*bpf2go' pkg/plugin/ | xargs -I{} go generate {} diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 6b35313530..12073387ac 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -3,6 +3,14 @@ name: Build and Deploy Retina.sh on: push: branches: ["main"] + paths: + - 'site/**' + - 'docs/**' + pull_request: + branches: ["main"] + paths: + - 'site/**' + - 'docs/**' workflow_dispatch: merge_group: permissions: @@ -13,28 +21,36 @@ concurrency: group: "pages" cancel-in-progress: false jobs: - deploy: - if: ${{ github.event_name != 'merge_group' }} - environment: - name: retina.sh - url: ${{ steps.deployment.outputs.page_url }} + build: runs-on: ubuntu-latest + timeout-minutes: 15 steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Setup Pages - uses: actions/configure-pages@v5 - - uses: actions/setup-node@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 20 - name: build run: | npm install --prefix site/ npm run build --prefix site/ - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + - name: Upload build artifact + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: path: "./site/build" + + deploy: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: build + environment: + name: retina.sh + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Setup Pages + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 diff --git a/.github/workflows/e2e-test-event-writer.yml b/.github/workflows/e2e-test-event-writer.yml index cbd9c811da..f1ace0a753 100644 --- a/.github/workflows/e2e-test-event-writer.yml +++ b/.github/workflows/e2e-test-event-writer.yml @@ -9,7 +9,6 @@ on: types: [checks_requested] workflow_dispatch: - permissions: contents: read id-token: write @@ -24,6 +23,7 @@ jobs: retina-win-e2e-bpf-images: name: Build E2E Test Event Writer runs-on: windows-2022 + timeout-minutes: 30 env: IS_MERGE_GROUP: ${{ (github.event_name == 'merge_group') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev/v0.0.33-windows' && github.repository == 'microsoft/retina') }} @@ -35,7 +35,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Azure Login uses: azure/login@v2 @@ -140,4 +140,4 @@ jobs: docker push "${image}:latest" } else { Write-Host "Skipping image push for pull_request runs" - } \ No newline at end of file + } diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 8d1028594b..5339875dbe 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -19,6 +19,21 @@ on: required: true type: string description: The Azure location to use for the E2E tests. + azure_agent_linux_sku: + required: false + default: '' + type: string + description: VM SKU for the Linux agent pool. Falls back to the in-code default when empty. + azure_agent_windows_sku: + required: false + default: '' + type: string + description: VM SKU for the Windows agent pool. Falls back to the in-code default when empty. + azure_agent_linux_arm_sku: + required: false + default: '' + type: string + description: VM SKU for the ARM64 agent pool. Falls back to the in-code default when empty. use_existing_infra: required: false default: false @@ -61,6 +76,21 @@ on: required: false type: string description: The Azure location to use for the E2E tests. + azure_agent_linux_sku: + required: false + default: '' + type: string + description: VM SKU for the Linux agent pool. Falls back to the in-code default when empty. + azure_agent_windows_sku: + required: false + default: '' + type: string + description: VM SKU for the Windows agent pool. Falls back to the in-code default when empty. + azure_agent_linux_arm_sku: + required: false + default: '' + type: string + description: VM SKU for the ARM64 agent pool. Falls back to the in-code default when empty. use_existing_infra: required: false default: false @@ -83,19 +113,22 @@ jobs: e2e: name: E2E runs-on: ubuntu-latest + timeout-minutes: 120 + env: + CLUSTER_NAME: retina-e2e-${{ github.run_id }}-${{ github.run_attempt }} steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup go - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod - run: go version - name: Az CLI login - uses: azure/login@v2 + uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -105,14 +138,36 @@ jobs: env: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION }} AZURE_LOCATION: ${{ inputs.azure_location }} + AZURE_AGENT_LINUX_SKU: ${{ inputs.azure_agent_linux_sku }} + AZURE_AGENT_WINDOWS_SKU: ${{ inputs.azure_agent_windows_sku }} + AZURE_AGENT_LINUX_ARM_SKU: ${{ inputs.azure_agent_linux_arm_sku }} + IMAGE_TAG: ${{ inputs.image_tag }} + IMAGE_REGISTRY: ${{ inputs.image_registry }} + IMAGE_NAMESPACE: ${{ inputs.image_namespace }} + USE_EXISTING_INFRA: ${{ inputs.use_existing_infra }} + INPUT_CLUSTER_NAME: ${{ inputs.cluster_name }} + INPUT_RESOURCE_GROUP: ${{ inputs.resource_group }} shell: bash run: | set -euo pipefail - if [ "${{ inputs.use_existing_infra }}" = "true" ]; then - export CLUSTER_NAME=${{ inputs.cluster_name }} - export AZURE_RESOURCE_GROUP=${{ inputs.resource_group }} + if [ "$USE_EXISTING_INFRA" = "true" ]; then + export CLUSTER_NAME="$INPUT_CLUSTER_NAME" + export AZURE_RESOURCE_GROUP="$INPUT_RESOURCE_GROUP" + CREATE_INFRA=false + DELETE_INFRA=false + else + CREATE_INFRA=true + DELETE_INFRA=true fi - go test -v ./test/e2e/. -timeout 60m -tags=e2e -count=1 -args -image-tag=${{ inputs.image_tag }} -image-registry=${{ inputs.image_registry }} -image-namespace=${{ inputs.image_namespace }} -create-infra=${{ !inputs.use_existing_infra }} -delete-infra=${{ !inputs.use_existing_infra }} - \ No newline at end of file + go test -v ./test/e2e/. -timeout 60m -tags=e2e -count=1 -args -image-tag="$IMAGE_TAG" -image-registry="$IMAGE_REGISTRY" -image-namespace="$IMAGE_NAMESPACE" -create-infra="$CREATE_INFRA" -delete-infra="$DELETE_INFRA" + + - name: Cleanup resource group + if: always() + shell: bash + run: | + if az group exists --name "$CLUSTER_NAME" 2>/dev/null | grep -q true; then + echo "Deleting resource group $CLUSTER_NAME..." + az group delete --name "$CLUSTER_NAME" --yes --no-wait || true + fi diff --git a/.github/workflows/generate-check.yaml b/.github/workflows/generate-check.yaml new file mode 100644 index 0000000000..d337b2e9fc --- /dev/null +++ b/.github/workflows/generate-check.yaml @@ -0,0 +1,113 @@ +name: Check Generated Code +on: + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + generate-check: + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runner: ubuntu-latest + - arch: arm64 + runner: ubuntu-24.04-arm + name: Generate (${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 15 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version-file: go.mod + + - name: Check that committed .o files are empty stubs + run: | + RED='\033[0;31m' + YELLOW='\033[1;33m' + CYAN='\033[0;36m' + NC='\033[0m' + # Tracked .o files must be empty (0 bytes) in the committed tree. + # They exist so Go source with bpf2go references compiles without + # running go generate. Real BPF objects are built at image build time. + # This check runs BEFORE generate since generate populates them. + non_empty=$(git ls-files '*.o' | while read -r f; do + size=$(git cat-file -s "HEAD:$f" 2>/dev/null || echo 0) + if [ "$size" -gt 0 ]; then echo "$f ($size bytes)"; fi + done || true) + if [ -n "$non_empty" ]; then + echo "" + echo -e "${RED}============================================================${NC}" + echo -e "${RED}ERROR: The following .o files must be empty stubs (0 bytes).${NC}" + echo "" + echo -e "${YELLOW}${non_empty}${NC}" + echo "" + echo -e "${CYAN}Run 'make empty-bpf-objects' to truncate them, then commit.${NC}" + echo -e "${RED}============================================================${NC}" + echo "::error::Non-empty .o files committed. Run 'make empty-bpf-objects' and commit the result." + exit 1 + fi + + - name: Install BPF build dependencies + run: | + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends clang llvm lld libbpf-dev linux-headers-$(uname -r) + sudo apt-get install -y --no-install-recommends linux-tools-$(uname -r) linux-tools-common || true + + - name: Run make generate for ${{ matrix.arch }} + run: | + # Generate BPF objects and Go bindings for this runner's native arch only, + # then run the remaining (non-BPF) generators. + GOARCH=${{ matrix.arch }} go generate ./pkg/plugin/... + go generate ./... + + - name: Check for uncommitted changes + run: | + RED='\033[0;31m' + YELLOW='\033[1;33m' + CYAN='\033[0;36m' + NC='\033[0m' + failed=0 + + # 1. Check generated .go files match committed code. + # Ignore .o files — they are empty stubs in the repo and get + # populated with real BPF objects during generate. + if ! git diff --quiet -- ':!*.o'; then + echo "" + echo -e "${RED}============================================================${NC}" + echo -e "${RED}ERROR: Generated code is out of date.${NC}" + echo "" + echo -e "${YELLOW}The following files differ after running 'make generate':${NC}" + git diff --name-only -- ':!*.o' + echo "" + echo -e "${CYAN}Please run 'make generate' locally and commit the changes.${NC}" + echo -e "${RED}============================================================${NC}" + echo "::error::Generated code is out of date. Run 'make generate' locally and commit the changes." + failed=1 + fi + + # 2. Check for new generated files that weren't committed. + untracked=$(git ls-files --others --exclude-standard -- '*.go' | head -20) + if [ -n "$untracked" ]; then + echo "" + echo -e "${RED}============================================================${NC}" + echo -e "${RED}ERROR: New generated files are not committed.${NC}" + echo "" + echo -e "${YELLOW}${untracked}${NC}" + echo "" + echo -e "${CYAN}Please run 'make generate' locally and commit the new files.${NC}" + echo -e "${RED}============================================================${NC}" + echo "::error::New generated files are not committed. Run 'make generate' locally and commit the new files." + failed=1 + fi + + exit $failed diff --git a/.github/workflows/golangci-lint.yaml b/.github/workflows/golangci-lint.yaml index 95cb3cf847..a1d2202fb8 100644 --- a/.github/workflows/golangci-lint.yaml +++ b/.github/workflows/golangci-lint.yaml @@ -6,6 +6,14 @@ on: branches: [main] pull_request: branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: golangci: strategy: @@ -15,24 +23,19 @@ jobs: goarch: ["amd64", "arm64"] name: Lint runs-on: ubuntu-latest + timeout-minutes: 30 env: - IS_NOT_MERGE_GROUP: ${{ github.event_name != 'merge_group' }} GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - if: env.IS_NOT_MERGE_GROUP + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 - if: env.IS_NOT_MERGE_GROUP + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod + - name: Build golangci-lint + run: GOOS=linux GOARCH=amd64 go build -o "$RUNNER_TEMP/golangci-lint" github.com/golangci/golangci-lint/v2/cmd/golangci-lint - name: golangci-lint - if: env.IS_NOT_MERGE_GROUP - uses: golangci/golangci-lint-action@v6 - with: - version: latest - args: --concurrency 4 --verbose --config=.golangci.yaml --timeout=25m - only-new-issues: true - skip-cache: true + run: | + "$RUNNER_TEMP/golangci-lint" run --new-from-rev=${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before || 'HEAD~1' }} --concurrency 4 --verbose --config=.golangci.yaml --timeout=25m diff --git a/.github/workflows/goreleaser.yaml b/.github/workflows/goreleaser.yaml index 65ef2df6e9..fa2ab1c3b1 100644 --- a/.github/workflows/goreleaser.yaml +++ b/.github/workflows/goreleaser.yaml @@ -4,25 +4,36 @@ on: branches: [main] push: tags: ["v*"] + permissions: contents: write packages: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: name: Build kubectl-retina if: github.ref_type == 'branch' runs-on: ubuntu-latest + timeout-minutes: 30 steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod + - name: Install Syft + uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 - name: Run GoReleaser build - uses: goreleaser/goreleaser-action@v6 + uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2 + env: + MCR_AGENT_IMAGE_NAME: mcr.microsoft.com/containernetworking/retina-agent with: distribution: goreleaser version: latest @@ -30,21 +41,36 @@ jobs: release: name: Release kubectl-retina runs-on: ubuntu-latest + timeout-minutes: 30 if: github.ref_type == 'tag' steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod + - name: Install Syft + uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 - name: Run GoReleaser release - uses: goreleaser/goreleaser-action@v6 + uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2 with: distribution: goreleaser version: latest args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MCR_AGENT_IMAGE_NAME: mcr.microsoft.com/containernetworking/retina-agent + - name: Check if release tag + id: check-tag + run: | + if [[ "${{ github.ref_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "is_release=true" >> $GITHUB_OUTPUT + else + echo "is_release=false" >> $GITHUB_OUTPUT + fi + - name: Update new version in krew-index + if: github.repository_owner == 'microsoft' && steps.check-tag.outputs.is_release == 'true' + uses: rajatjindal/krew-release-bot@c970b8a8f6dbc2f2285a26e3ae160903b87002c3 # v0.0.51 diff --git a/.github/workflows/images.yaml b/.github/workflows/images.yaml index 83d3835608..6f20504761 100644 --- a/.github/workflows/images.yaml +++ b/.github/workflows/images.yaml @@ -13,30 +13,50 @@ permissions: contents: read id-token: write +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: - retina-images: - name: Build Images and Run E2E + get-tag: + name: Get Image Tag runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + tag: ${{ steps.get_tag.outputs.tag }} + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Get tag + id: get_tag + run: echo "tag=$(make version)" >> $GITHUB_OUTPUT + + retina-images: + name: Build Agent Images - Linux + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 strategy: matrix: platform: ["linux"] arch: ["amd64", "arm64"] + include: + - arch: amd64 + runner: ubuntu-latest + - arch: arm64 + runner: ubuntu-24.04-arm steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod - run: go version - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Az CLI login - uses: azure/login@v2 + uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 if: ${{ (github.event_name == 'merge_group') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev/v0.0.33-windows' && github.repository == 'microsoft/retina') }} with: client-id: ${{ secrets.AZURE_CLIENT_ID }} @@ -64,30 +84,84 @@ jobs: env: SHOULD_PUSH_IMAGE: ${{ (github.event_name == 'merge_group') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev/v0.0.33-windows' && github.repository == 'microsoft/retina') }} - retina-win-images: - name: Build Agent Windows Images + build-windows-binaries: + name: Build Windows Binaries runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version-file: go.mod + - run: go version + + - name: Build Windows Binaries + shell: bash + run: | + TAG=$(make version) + echo "TAG=$TAG" >> $GITHUB_ENV + make build-windows-binaries \ + GOOS=windows \ + GOARCH=amd64 \ + TAG=$TAG \ + APP_INSIGHTS_ID=${{ secrets.AZURE_APP_INSIGHTS_KEY }} + env: + APP_INSIGHTS_ID: ${{ secrets.AZURE_APP_INSIGHTS_KEY }} + + - name: Upload Windows Binaries + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: windows-binaries + path: output/windows_amd64/ + retention-days: 1 + + retina-image-win: + name: Build Agent Image - Windows ${{ matrix.year }} + needs: build-windows-binaries + runs-on: windows-${{ matrix.year }} + timeout-minutes: 60 strategy: matrix: + year: ["2022"] platform: ["windows"] arch: ["amd64"] - year: ["2019", "2022"] steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + - name: Download Windows Binaries + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - go-version-file: go.mod - - run: go version + name: windows-binaries + path: output/windows_amd64/ - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + - name: Ensure Docker daemon is running + shell: pwsh + run: | + $timeout = 120 + $timer = [Diagnostics.Stopwatch]::StartNew() + while ($timer.Elapsed.TotalSeconds -lt $timeout) { + $svc = Get-Service docker -ErrorAction SilentlyContinue + if ($svc -and $svc.Status -ne 'Running') { + Start-Service docker -ErrorAction SilentlyContinue + } + $result = docker info 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Host "Docker daemon is ready" + return + } + Write-Host "Waiting for Docker daemon to start..." + Start-Sleep -Seconds 5 + } + throw "Docker daemon failed to start within $timeout seconds" - name: Az CLI login - uses: azure/login@v2 + uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 if: ${{ (github.event_name == 'merge_group') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev/v0.0.33-windows' && github.repository == 'microsoft/retina') }} with: client-id: ${{ secrets.AZURE_CLIENT_ID }} @@ -106,48 +180,56 @@ jobs: shell: bash run: | set -euo pipefail - echo "TAG=$(make version)" >> $GITHUB_ENV + TAG=$(make version) + echo "TAG=$TAG" >> "$GITHUB_ENV" if [ "$SHOULD_PUSH_IMAGE" == "true" ]; then az acr login -n ${{ vars.ACR_NAME }} make retina-image-win \ IMAGE_NAMESPACE=${{ github.repository }} \ PLATFORM=${{ matrix.platform }}/${{ matrix.arch }} \ + WINDOWS_YEARS=${{ matrix.year }} \ IMAGE_REGISTRY=${{ vars.ACR_NAME }} \ APP_INSIGHTS_ID=${{ secrets.AZURE_APP_INSIGHTS_KEY }} \ - WINDOWS_YEARS=${{ matrix.year }} \ - BUILDX_ACTION=--push + BINARIES_PATH=output/windows_${{ matrix.arch }} \ + REPO_PATH=. + docker push ${{ vars.ACR_NAME }}/${{ github.repository }}/retina-agent:${TAG}-windows-ltsc${{ matrix.year }}-${{ matrix.arch }} else make retina-image-win \ IMAGE_NAMESPACE=${{ github.repository }} \ PLATFORM=${{ matrix.platform }}/${{ matrix.arch }} \ - WINDOWS_YEARS=${{ matrix.year }} + WINDOWS_YEARS=${{ matrix.year }} \ + BINARIES_PATH=output/windows_${{ matrix.arch }} \ + REPO_PATH=. fi env: SHOULD_PUSH_IMAGE: ${{ (github.event_name == 'merge_group') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev/v0.0.33-windows' && github.repository == 'microsoft/retina') }} operator-images: name: Build Operator Images - runs-on: ubuntu-latest + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 strategy: matrix: platform: ["linux"] arch: ["amd64", "arm64"] + include: + - arch: amd64 + runner: ubuntu-latest + - arch: arm64 + runner: ubuntu-24.04-arm steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod - run: go version - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Az CLI login - uses: azure/login@v2 + uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 if: ${{ (github.event_name == 'merge_group') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev/v0.0.33-windows' && github.repository == 'microsoft/retina') }} with: client-id: ${{ secrets.AZURE_CLIENT_ID }} @@ -176,28 +258,31 @@ jobs: SHOULD_PUSH_IMAGE: ${{ (github.event_name == 'merge_group') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev/v0.0.33-windows' && github.repository == 'microsoft/retina') }} retina-shell-images: - name: Build Retina Shell Images - runs-on: ubuntu-latest + name: Build Retina Shell Images (${{ matrix.platform }}, ${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 strategy: matrix: - platform: ["linux"] - arch: ["amd64", "arm64"] + include: + - platform: linux + arch: amd64 + runner: ubuntu-latest + - platform: linux + arch: arm64 + runner: ubuntu-24.04-arm steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod - run: go version - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Az CLI login - uses: azure/login@v2 + uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 if: ${{ (github.event_name == 'merge_group') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev/v0.0.33-windows' && github.repository == 'microsoft/retina') }} with: client-id: ${{ secrets.AZURE_CLIENT_ID }} @@ -226,27 +311,30 @@ jobs: kubectl-retina-images: name: Build Kubectl Retina Images - runs-on: ubuntu-latest + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 strategy: matrix: platform: ["linux"] arch: ["amd64", "arm64"] + include: + - arch: amd64 + runner: ubuntu-latest + - arch: arm64 + runner: ubuntu-24.04-arm steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod - run: go version - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Az CLI login - uses: azure/login@v2 + uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 if: ${{ (github.event_name == 'merge_group') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev/v0.0.33-windows' && github.repository == 'microsoft/retina') }} with: client-id: ${{ secrets.AZURE_CLIENT_ID }} @@ -277,10 +365,11 @@ jobs: name: Generate Manifests if: ${{ (github.event_name == 'merge_group') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev/v0.0.33-windows' && github.repository == 'microsoft/retina') }} runs-on: ubuntu-latest + timeout-minutes: 30 needs: [ retina-images, - retina-win-images, + retina-image-win, operator-images, retina-shell-images, kubectl-retina-images, @@ -292,13 +381,13 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - name: Azure CLI login - uses: azure/login@v2 + uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -317,19 +406,22 @@ jobs: if: ${{ (github.event_name == 'merge_group') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev/v0.0.33-windows' && github.repository == 'microsoft/retina') }} needs: [manifests] runs-on: ubuntu-latest + timeout-minutes: 90 + env: + CLUSTER_NAME: retina-e2e-${{ github.run_id }}-${{ github.run_attempt }} steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod - run: go version - name: Az CLI login - uses: azure/login@v2 + uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 if: ${{ (github.event_name == 'merge_group') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev/v0.0.33-windows' && github.repository == 'microsoft/retina') }} with: client-id: ${{ secrets.AZURE_CLIENT_ID }} @@ -340,37 +432,55 @@ jobs: env: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION }} AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + AZURE_AGENT_LINUX_SKU: ${{ vars.AZURE_AGENT_LINUX_SKU }} + AZURE_AGENT_WINDOWS_SKU: ${{ vars.AZURE_AGENT_WINDOWS_SKU }} + AZURE_AGENT_LINUX_ARM_SKU: ${{ vars.AZURE_AGENT_LINUX_ARM_SKU }} shell: bash run: | set -euo pipefail go test -v ./test/e2e/. -timeout 60m -tags=e2e -count=1 -args -image-tag=$(make version) -image-registry=${{ vars.ACR_NAME }} -image-namespace=${{ github.repository}} + - name: Cleanup resource group + if: always() + shell: bash + run: | + if az group exists --name "$CLUSTER_NAME" 2>/dev/null | grep -q true; then + echo "Deleting resource group $CLUSTER_NAME..." + az group delete --name "$CLUSTER_NAME" --yes --no-wait || true + fi + perf-test-basic: if: ${{ github.event_name == 'merge_group'}} - needs: [manifests] + needs: [manifests, get-tag, e2e] uses: ./.github/workflows/perf-template.yaml with: image-registry: ${{ vars.ACR_NAME }} - tag: $(make version) + tag: ${{ needs.get-tag.outputs.tag }} image-namespace: ${{ github.repository }} retina-mode: basic azure-location: ${{ vars.AZURE_LOCATION }} + azure-agent-linux-sku: ${{ vars.AZURE_AGENT_LINUX_SKU }} + azure-agent-windows-sku: ${{ vars.AZURE_AGENT_WINDOWS_SKU }} + azure-agent-linux-arm-sku: ${{ vars.AZURE_AGENT_LINUX_ARM_SKU }} secrets: azure-subscription: ${{ secrets.AZURE_SUBSCRIPTION }} azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} azure-app-insights-key: ${{ secrets.AZURE_APP_INSIGHTS_KEY }} - + perf-test-advanced: if: ${{ github.event_name == 'merge_group'}} - needs: [manifests] + needs: [manifests, get-tag, e2e] uses: ./.github/workflows/perf-template.yaml with: image-registry: ${{ vars.ACR_NAME }} - tag: $(make version) + tag: ${{ needs.get-tag.outputs.tag }} image-namespace: ${{ github.repository }} retina-mode: advanced azure-location: ${{ vars.AZURE_LOCATION }} + azure-agent-linux-sku: ${{ vars.AZURE_AGENT_LINUX_SKU }} + azure-agent-windows-sku: ${{ vars.AZURE_AGENT_WINDOWS_SKU }} + azure-agent-linux-arm-sku: ${{ vars.AZURE_AGENT_LINUX_ARM_SKU }} secrets: azure-subscription: ${{ secrets.AZURE_SUBSCRIPTION }} azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} diff --git a/.github/workflows/kapinger.yaml b/.github/workflows/kapinger.yaml index 6a329412c0..ad9fff855f 100644 --- a/.github/workflows/kapinger.yaml +++ b/.github/workflows/kapinger.yaml @@ -4,62 +4,173 @@ on: push: branches: - main + paths: + - "hack/tools/kapinger/**" + - ".github/workflows/kapinger.yaml" + tags: + - "v*" + pull_request: + branches: + - main + paths: + - "hack/tools/kapinger/**" + - ".github/workflows/kapinger.yaml" + merge_group: + types: [checks_requested] + +permissions: + contents: read + packages: write jobs: - build: - runs-on: ubuntu-latest + build-linux: + name: Build Linux Kapinger Image (${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + + strategy: + matrix: + arch: ["amd64", "arm64"] + include: + - arch: amd64 + runner: ubuntu-latest + - arch: arm64 + runner: ubuntu-24.04-arm steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set outputs id: vars run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - - name: Check outputs - run: echo ${{ steps.vars.outputs.sha_short }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - # - name: Login to GitHub Container Registry - # uses: docker/login-action@v1 - # with: - # registry: ghcr.io - # username: ${{ github.repository_owner }} - # password: ${{ secrets.GITHUB_TOKEN }} + - name: Log in to GHCR + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin - - name: Build Windows Kapinger Image - uses: docker/build-push-action@v6 + - name: Build Linux Kapinger Image + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: hack/tools/kapinger file: hack/tools/kapinger/Dockerfile - platforms: windows/amd64 + platforms: linux/${{ matrix.arch }} + target: linux + load: true push: false provenance: false - tags: ghcr.io/${{ github.repository }}/kapinger:${{ steps.vars.outputs.sha_short }}-windows + tags: ghcr.io/${{ github.repository }}/kapinger:${{ steps.vars.outputs.sha_short }}-linux-${{ matrix.arch }} - - name: Build Linux Kapinger Image - uses: docker/build-push-action@v6 + - name: Smoke test kapinger binary + run: | + IMAGE=ghcr.io/${{ github.repository }}/kapinger:${{ steps.vars.outputs.sha_short }}-linux-${{ matrix.arch }} + # Verify the binary starts and prints the expected startup log. + # It will exit with an error because there's no K8s cluster, but + # that confirms the binary is correctly linked and executable. + docker run --rm "$IMAGE" 2>&1 | grep -q "starting kapinger" + + - name: Push Linux Kapinger Image + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: hack/tools/kapinger file: hack/tools/kapinger/Dockerfile - platforms: linux/amd64 - push: false + platforms: linux/${{ matrix.arch }} + target: linux + push: true provenance: false - tags: ghcr.io/${{ github.repository }}/kapinger:${{ steps.vars.outputs.sha_short }}-linux + tags: ghcr.io/${{ github.repository }}/kapinger:${{ steps.vars.outputs.sha_short }}-linux-${{ matrix.arch }} + + build-windows: + name: Build Windows Kapinger Image + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set outputs + id: vars + run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + + - name: Log in to GHCR + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin + + - name: Build Windows Kapinger Image + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + with: + context: hack/tools/kapinger + file: hack/tools/kapinger/Dockerfile + platforms: windows/amd64 + target: windows + push: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) }} + provenance: false + tags: ghcr.io/${{ github.repository }}/kapinger:${{ steps.vars.outputs.sha_short }}-windows + + manifest: + name: Create and Push Manifest + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) + needs: [build-linux, build-windows] + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set outputs + id: vars + run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + + - name: Log in to GHCR + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin + + - name: Create and push manifest + run: | + TAG=${{ steps.vars.outputs.sha_short }} + REPO=ghcr.io/${{ github.repository }}/kapinger + TAGS="-t $REPO:$TAG" + if [[ "${{ github.ref }}" == refs/tags/v* ]]; then + TAGS="$TAGS -t $REPO:${{ github.ref_name }}" + fi + docker buildx imagetools create $TAGS \ + "$REPO:$TAG-linux-amd64" \ + "$REPO:$TAG-linux-arm64" \ + "$REPO:$TAG-windows" + + build-toolbox: + name: Build Linux Toolbox Image + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set outputs + id: vars + run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + + - name: Log in to GHCR + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin - name: Build Linux Toolbox Image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: hack/tools file: hack/tools/toolbox/Dockerfile platforms: linux/amd64 - push: false + push: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) }} provenance: false tags: ghcr.io/${{ github.repository }}/toolbox:${{ steps.vars.outputs.sha_short }}-linux -# - name: Create and push manifest -# id: docker_manifest -# run: | -# docker manifest create ghcr.io/${{ github.repository }}/kapinger:latest ghcr.io/${{ github.repository }}/kapinger:${{ steps.vars.outputs.sha_short }}-windows ghcr.io/${{ github.repository }}/kapinger:${{ steps.vars.outputs.sha_short }}-linux -# docker manifest push ghcr.io/${{ github.repository }}/kapinger:latest diff --git a/.github/workflows/markdownlint.yaml b/.github/workflows/markdownlint.yaml index d11291efaf..1fb9fcca1d 100644 --- a/.github/workflows/markdownlint.yaml +++ b/.github/workflows/markdownlint.yaml @@ -3,17 +3,22 @@ on: merge_group: pull_request: branches: [main] + paths: + - '**/*.md' + +permissions: + contents: read + jobs: markdownlint: if: ${{ github.event_name != 'merge_group' }} name: markdownlint runs-on: ubuntu-latest + timeout-minutes: 10 steps: - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: DavidAnson/markdownlint-cli2-action@v9 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: DavidAnson/markdownlint-cli2-action@07035fd053f7be764496c0f8d8f9f41f98305101 # v22.0.0 with: - command: config - globs: | - .github/.markdownlint.json - **/*.md + config: .github/.markdownlint.json + globs: '**/*.md' diff --git a/.github/workflows/perf-manual.yaml b/.github/workflows/perf-manual.yaml index 1660987969..0aabe614fd 100644 --- a/.github/workflows/perf-manual.yaml +++ b/.github/workflows/perf-manual.yaml @@ -35,6 +35,9 @@ jobs: image-namespace: ${{ inputs.image-namespace || github.repository }} retina-mode: ${{ inputs.retina-mode }} azure-location: ${{ vars.AZURE_LOCATION }} + azure-agent-linux-sku: ${{ vars.AZURE_AGENT_LINUX_SKU }} + azure-agent-windows-sku: ${{ vars.AZURE_AGENT_WINDOWS_SKU }} + azure-agent-linux-arm-sku: ${{ vars.AZURE_AGENT_LINUX_ARM_SKU }} secrets: azure-subscription: ${{ secrets.AZURE_SUBSCRIPTION }} azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} diff --git a/.github/workflows/perf-schedule.yaml b/.github/workflows/perf-schedule.yaml index ee0886626c..851dd1105e 100644 --- a/.github/workflows/perf-schedule.yaml +++ b/.github/workflows/perf-schedule.yaml @@ -10,14 +10,33 @@ permissions: id-token: write jobs: + get-tag: + name: Get Latest Release Tag + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + tag: ${{ steps.get_tag.outputs.tag }} + steps: + - name: Get latest release tag + id: get_tag + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG=$(gh release view --repo ${{ github.repository }} --json tagName -q .tagName) + echo "tag=$TAG" >> $GITHUB_OUTPUT + perf-test-basic: + needs: get-tag uses: ./.github/workflows/perf-template.yaml with: image-registry: ghcr.io - tag: $(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r .tag_name) + tag: ${{ needs.get-tag.outputs.tag }} image-namespace: ${{ github.repository }} retina-mode: basic azure-location: ${{ vars.AZURE_LOCATION }} + azure-agent-linux-sku: ${{ vars.AZURE_AGENT_LINUX_SKU }} + azure-agent-windows-sku: ${{ vars.AZURE_AGENT_WINDOWS_SKU }} + azure-agent-linux-arm-sku: ${{ vars.AZURE_AGENT_LINUX_ARM_SKU }} secrets: azure-subscription: ${{ secrets.AZURE_SUBSCRIPTION }} azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -25,13 +44,17 @@ jobs: azure-app-insights-key: ${{ secrets.AZURE_APP_INSIGHTS_KEY }} perf-test-advanced: + needs: get-tag uses: ./.github/workflows/perf-template.yaml with: image-registry: ghcr.io - tag: $(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r .tag_name) + tag: ${{ needs.get-tag.outputs.tag }} image-namespace: ${{ github.repository }} retina-mode: advanced azure-location: ${{ vars.AZURE_LOCATION }} + azure-agent-linux-sku: ${{ vars.AZURE_AGENT_LINUX_SKU }} + azure-agent-windows-sku: ${{ vars.AZURE_AGENT_WINDOWS_SKU }} + azure-agent-linux-arm-sku: ${{ vars.AZURE_AGENT_LINUX_ARM_SKU }} secrets: azure-subscription: ${{ secrets.AZURE_SUBSCRIPTION }} azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} diff --git a/.github/workflows/perf-template.yaml b/.github/workflows/perf-template.yaml index d202b45148..f952c83b76 100644 --- a/.github/workflows/perf-template.yaml +++ b/.github/workflows/perf-template.yaml @@ -24,6 +24,21 @@ on: description: 'Azure location for the performance test' required: true type: string + azure-agent-linux-sku: + description: 'VM SKU for the Linux agent pool' + required: false + default: '' + type: string + azure-agent-windows-sku: + description: 'VM SKU for the Windows agent pool' + required: false + default: '' + type: string + azure-agent-linux-arm-sku: + description: 'VM SKU for the ARM64 agent pool' + required: false + default: '' + type: string secrets: azure-subscription: description: 'Azure subscription ID' @@ -46,18 +61,21 @@ jobs: perf-test: name: Retina ${{ inputs.retina-mode }} Performance Test runs-on: ubuntu-latest + timeout-minutes: 150 + env: + CLUSTER_NAME: retina-perf-${{ inputs.retina-mode }}-${{ github.run_id }}-${{ github.run_attempt }} steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup go - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod - run: go version - name: Az CLI login - uses: azure/login@v2 + uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 with: client-id: ${{ secrets.azure-client-id }} tenant-id: ${{ secrets.azure-tenant-id }} @@ -68,14 +86,25 @@ jobs: AZURE_APP_INSIGHTS_KEY: ${{ secrets.azure-app-insights-key }} AZURE_SUBSCRIPTION_ID: ${{ secrets.azure-subscription }} AZURE_LOCATION: ${{ inputs.azure-location }} + AZURE_AGENT_LINUX_SKU: ${{ inputs.azure-agent-linux-sku }} + AZURE_AGENT_WINDOWS_SKU: ${{ inputs.azure-agent-windows-sku }} + AZURE_AGENT_LINUX_ARM_SKU: ${{ inputs.azure-agent-linux-arm-sku }} + TAG: ${{ inputs.tag }} + REGISTRY: ${{ inputs.image-registry }} + NAMESPACE: ${{ inputs.image-namespace }} + MODE: ${{ inputs.retina-mode }} shell: bash run: | set -euo pipefail - - TAG="${{ inputs.tag }}" - REGISTRY="${{ inputs.image-registry }}" - NAMESPACE="${{ inputs.image-namespace }}" - MODE="${{ inputs.retina-mode }}" - + echo "Running in $MODE mode..." - go test -v ./test/e2e/. -timeout 2h -tags=perf -count=1 -args -image-tag=$TAG -image-registry=$REGISTRY -image-namespace=$NAMESPACE -retina-mode=$MODE + go test -v ./test/e2e/. -timeout 2h -tags=perf -count=1 -args -image-tag="$TAG" -image-registry="$REGISTRY" -image-namespace="$NAMESPACE" -retina-mode="$MODE" + + - name: Cleanup resource group + if: always() + shell: bash + run: | + if az group exists --name "$CLUSTER_NAME" 2>/dev/null | grep -q true; then + echo "Deleting resource group $CLUSTER_NAME..." + az group delete --name "$CLUSTER_NAME" --yes --no-wait || true + fi diff --git a/.github/workflows/release-charts.yaml b/.github/workflows/release-charts.yaml index 6ce60f8894..73c9e088a3 100644 --- a/.github/workflows/release-charts.yaml +++ b/.github/workflows/release-charts.yaml @@ -16,24 +16,25 @@ jobs: push-retina-charts: name: Publish Retina Helm Charts runs-on: ubuntu-latest + timeout-minutes: 30 if: github.ref_type == 'tag' steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - - uses: azure/setup-helm@v4.3.0 + - uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0 id: install - + - name: Install Cosign - uses: sigstore/cosign-installer@v3.8.2 + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: Log in to registry (Helm for pushing chart, Docker for signing and push signature) run: | echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u $ --password-stdin echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin - + - name: Build, Push and Sign chart id: build_chart shell: bash diff --git a/.github/workflows/release-images.yaml b/.github/workflows/release-images.yaml index 302a334590..09086407a6 100644 --- a/.github/workflows/release-images.yaml +++ b/.github/workflows/release-images.yaml @@ -15,27 +15,30 @@ permissions: jobs: retina-images: name: Build Agent Images - runs-on: ubuntu-latest + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 strategy: matrix: platform: ["linux"] arch: ["amd64", "arm64"] + include: + - arch: amd64 + runner: ubuntu-latest + - arch: arm64 + runner: ubuntu-24.04-arm steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod - run: go version - name: Install Cosign - uses: sigstore/cosign-installer@v3.8.2 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: Log in to registry run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin @@ -58,30 +61,81 @@ jobs: cosign sign --yes ${IMAGE_PATH}@${DIGEST} done - retina-win-images: - name: Build Agent Windows Images + build-windows-binaries: + name: Build Windows Binaries runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version-file: go.mod + - run: go version + + - name: Build Windows Binaries + shell: bash + run: | + TAG=$(make version) + echo "TAG=$TAG" >> $GITHUB_ENV + make build-windows-binaries \ + GOOS=windows \ + GOARCH=amd64 \ + TAG=$TAG + + - name: Upload Windows Binaries + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: windows-binaries + path: output/windows_amd64/ + retention-days: 1 + + retina-win-images: + name: Build Agent Image - Windows ${{ matrix.year }} + needs: build-windows-binaries + runs-on: windows-${{ matrix.year }} + timeout-minutes: 60 strategy: matrix: + year: ["2022"] platform: ["windows"] arch: ["amd64"] - year: ["2019", "2022"] steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + - name: Download Windows Binaries + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - go-version-file: go.mod - - run: go version + name: windows-binaries + path: output/windows_amd64/ - - name: Install Cosign - uses: sigstore/cosign-installer@v3.8.2 + - name: Ensure Docker daemon is running + shell: pwsh + run: | + $timeout = 120 + $timer = [Diagnostics.Stopwatch]::StartNew() + while ($timer.Elapsed.TotalSeconds -lt $timeout) { + $svc = Get-Service docker -ErrorAction SilentlyContinue + if ($svc -and $svc.Status -ne 'Running') { + Start-Service docker -ErrorAction SilentlyContinue + } + $result = docker info 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Host "Docker daemon is ready" + return + } + Write-Host "Waiting for Docker daemon to start..." + Start-Sleep -Seconds 5 + } + throw "Docker daemon failed to start within $timeout seconds" - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + - name: Install Cosign + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: Log in to registry run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin @@ -90,44 +144,51 @@ jobs: shell: bash run: | set -euo pipefail - echo "TAG=$(make version)" >> $GITHUB_ENV + TAG=$(make version) + echo "TAG=$TAG" >> "$GITHUB_ENV" make retina-image-win \ - IMAGE_NAMESPACE=${{ github.repository }} \ - PLATFORM=${{ matrix.platform }}/${{ matrix.arch }} \ - WINDOWS_YEARS=${{ matrix.year }} \ - BUILDX_ACTION=--push - + IMAGE_NAMESPACE=${{ github.repository }} \ + PLATFORM=${{ matrix.platform }}/${{ matrix.arch }} \ + WINDOWS_YEARS=${{ matrix.year }} \ + BINARIES_PATH=output/windows_${{ matrix.arch }} \ + REPO_PATH=. + docker push ghcr.io/${{ github.repository }}/retina-agent:${TAG}-windows-ltsc${{ matrix.year }}-${{ matrix.arch }} + - name: Sign container image + shell: bash run: | for image in retina-agent ; do IMAGE_PATH="ghcr.io/${{ github.repository }}/$image:$TAG-windows-ltsc${{ matrix.year }}-${{ matrix.arch }}" - DIGEST=$(jq -r '.["containerimage.digest"]' image-metadata-$image-$TAG-windows-ltsc${{ matrix.year }}-${{ matrix.arch }}.json) + DIGEST=$(docker manifest inspect $IMAGE_PATH -v | jq -r '.Descriptor.digest') cosign sign --yes ${IMAGE_PATH}@${DIGEST} done operator-images: name: Build Operator Images - runs-on: ubuntu-latest + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 strategy: matrix: platform: ["linux"] - arch: ["amd64"] + arch: ["amd64", "arm64"] + include: + - arch: amd64 + runner: ubuntu-latest + - arch: arm64 + runner: ubuntu-24.04-arm steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod - run: go version - name: Install Cosign - uses: sigstore/cosign-installer@v3.8.2 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: Log in to registry run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin @@ -151,28 +212,31 @@ jobs: done retina-shell-images: - name: Build Retina Shell Images - runs-on: ubuntu-latest + name: Build Retina Shell Images (${{ matrix.platform }}, ${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 strategy: matrix: - platform: ["linux"] - arch: ["amd64", "arm64"] + include: + - platform: linux + arch: amd64 + runner: ubuntu-latest + - platform: linux + arch: arm64 + runner: ubuntu-24.04-arm steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod - run: go version - name: Install Cosign - uses: sigstore/cosign-installer@v3.8.2 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: Log in to registry run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin @@ -195,27 +259,30 @@ jobs: kubectl-retina-images: name: Build Kubectl Retina Images - runs-on: ubuntu-latest + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 strategy: matrix: platform: ["linux"] arch: ["amd64", "arm64"] + include: + - arch: amd64 + runner: ubuntu-latest + - arch: arm64 + runner: ubuntu-24.04-arm steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod - run: go version - name: Install Cosign - uses: sigstore/cosign-installer@v3.8.2 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: Log in to registry run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin @@ -239,6 +306,7 @@ jobs: manifests: name: Generate Manifests runs-on: ubuntu-latest + timeout-minutes: 30 needs: [ retina-images, @@ -254,13 +322,13 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - name: Install Cosign - uses: sigstore/cosign-installer@v3.8.2 + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: Log in to registry run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin diff --git a/.github/workflows/release-validation.yaml b/.github/workflows/release-validation.yaml index 3adb4a6abf..7211e0ee0a 100644 --- a/.github/workflows/release-validation.yaml +++ b/.github/workflows/release-validation.yaml @@ -6,13 +6,18 @@ on: types: - completed +permissions: + contents: read + jobs: release_validation: + if: ${{ github.event.workflow_run.conclusion == 'success' }} name: Release Validation runs-on: ubuntu-latest + timeout-minutes: 30 steps: - name: Checkout repository - uses: actions/checkout@v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Get latest tag id: get_latest_tag @@ -40,7 +45,7 @@ jobs: helm pull oci://ghcr.io/microsoft/retina/charts/retina --version ${{ env.TAG }} - name: Setup kind cluster - uses: helm/kind-action@v1.12.0 + uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0 - name: Check Go package version run: | diff --git a/.github/workflows/scale-test.yaml b/.github/workflows/scale-test.yaml index f46db14bae..36f0eefd18 100644 --- a/.github/workflows/scale-test.yaml +++ b/.github/workflows/scale-test.yaml @@ -65,19 +65,20 @@ jobs: scale-test: name: Scale Test runs-on: ubuntu-latest + timeout-minutes: 360 steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup go - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod - run: go version - name: Az CLI login - uses: azure/login@v2 + uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 12eebe7143..f70e5acc90 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -7,12 +7,13 @@ on: jobs: stale: runs-on: ubuntu-latest + timeout-minutes: 15 permissions: contents: write issues: write pull-requests: write steps: - - uses: actions/stale@main + - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 id: stale with: ascending: true @@ -31,4 +32,4 @@ jobs: stale-issue-message: "This issue will be closed in 7 days due to inactivity." stale-pr-message: "This PR will be closed in 7 days due to inactivity." - name: Print outputs - run: echo ${{ join(steps.stale.outputs.*, ',') }} + run: echo "${{ join(steps.stale.outputs.*, ',') }}" diff --git a/.github/workflows/test-ebpf.yaml b/.github/workflows/test-ebpf.yaml new file mode 100644 index 0000000000..234e78ccf3 --- /dev/null +++ b/.github/workflows/test-ebpf.yaml @@ -0,0 +1,49 @@ +name: eBPF Program Tests +on: + push: + branches: [main] + paths: + - 'pkg/plugin/**/_cprog/**' + - 'pkg/plugin/**/*_ebpf_test.go' + - 'pkg/plugin/ebpftest/**' + pull_request: + branches: [main] + paths: + - 'pkg/plugin/**/_cprog/**' + - 'pkg/plugin/**/*_ebpf_test.go' + - 'pkg/plugin/ebpftest/**' + workflow_dispatch: + +permissions: + contents: read + +jobs: + ebpf-tests: + strategy: + matrix: + include: + - arch: amd64 + runner: ubuntu-latest + - arch: arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version-file: go.mod + + - name: Install clang and llvm-strip + run: | + sudo apt-get update + sudo apt-get install -y clang llvm + + - name: Compile eBPF programs + run: | + GOARCH=${{ matrix.arch }} go generate ./pkg/plugin/... + + - name: Run eBPF program tests + run: | + sudo $(which go) test -tags=ebpf -v -count=1 ./pkg/plugin/... diff --git a/.github/workflows/test-multicloud.yml b/.github/workflows/test-multicloud.yml index 98c40277d7..3719d1bc8b 100644 --- a/.github/workflows/test-multicloud.yml +++ b/.github/workflows/test-multicloud.yml @@ -5,20 +5,28 @@ on: paths: - 'test/multicloud/**' +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: multicloud-test: runs-on: ubuntu-latest + timeout-minutes: 30 steps: - - uses: opentofu/setup-opentofu@v1 + - uses: opentofu/setup-opentofu@847eaa4afeb791b06daa46e8eafa8b1b68d7cfb4 # v2.0.1 with: tofu_version: 1.8.3 - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Go - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod @@ -28,4 +36,4 @@ jobs: - name: Run tests run: make test - working-directory: test/multicloud/ \ No newline at end of file + working-directory: test/multicloud/ diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9144fa6c03..1d8d2fd454 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -9,22 +9,23 @@ on: workflow_dispatch: permissions: - actions: read contents: read - deployments: read - packages: none pull-requests: write - security-events: write - issues: write + actions: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: test-image: runs-on: ubuntu-latest + timeout-minutes: 30 steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod @@ -36,7 +37,86 @@ jobs: make test-image IMAGE_NAMESPACE=${{ github.repository }} PLATFORM=linux/amd64 - name: Upload Artifacts - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: coverage-files path: ./artifacts/coverage* + + - name: Generate coverage summary + run: | + COVERAGE_FILE="./artifacts/coverage.out" + if [ ! -s "$COVERAGE_FILE" ]; then + echo "::warning::Coverage file is empty or missing" + echo "## Test Coverage" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo ":warning: No coverage data available." >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + # Filter out generated files (eBPF, mocks) + grep -Ev '_bpf\.go|_bpfel_x86\.go|_bpfel_arm64\.go|_generated\.go|mock_' "$COVERAGE_FILE" > coverage_filtered.out + + # Generate function-level coverage report + go tool cover -func=coverage_filtered.out > coverage_func.out + + # Extract total coverage percentage + TOTAL_COVERAGE=$(grep '^total:' coverage_func.out | awk '{print $NF}') + + # Write step summary + { + echo "## Test Coverage" + echo "" + echo ":white_check_mark: **Tests passed**" + echo "" + echo "| Metric | Value |" + echo "| --- | --- |" + echo "| Total Coverage | \`${TOTAL_COVERAGE}\` |" + echo "" + echo "
" + echo "Coverage by package (top 20 lowest)" + echo "" + echo '```' + grep -v '^total:' coverage_func.out | sort -t'%' -k1 -n | head -20 + echo '```' + echo "
" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Compare coverage with main branch + if: github.event_name == 'pull_request' + continue-on-error: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + run: | + COVERAGE_FILE="./artifacts/coverage.out" + if [ ! -s "$COVERAGE_FILE" ]; then + echo "Coverage file is empty or missing, skipping comparison" + exit 0 + fi + + # Filter generated files and prepare current branch coverage + grep -Ev '_bpf\.go|_bpfel_x86\.go|_bpfel_arm64\.go|_generated\.go|mock_' "$COVERAGE_FILE" > coveragenew.out + cp coveragenew.out coverage.out + go tool cover -func=coveragenew.out -o coverageexpanded.out + + # Install Python dependency for GitHub API calls + pip install --quiet requests + + # Fetch main branch coverage + python3 scripts/coverage/get_coverage.py + + # Check if main branch coverage was downloaded + if [ ! -f mainbranchcoverage/coverage.out ]; then + echo "No main branch coverage found, skipping comparison" + exit 0 + fi + + # Filter main branch coverage + grep -Ev '_bpf\.go|_bpfel_x86\.go|_bpfel_arm64\.go|_generated\.go|mock_' mainbranchcoverage/coverage.out > mainbranchcoverage/coverage_filtered.out + mv mainbranchcoverage/coverage_filtered.out mainbranchcoverage/coverage.out + + # Generate expanded coverage for main branch + go tool cover -func=mainbranchcoverage/coverage.out -o maincoverageexpanded.out + + # Post PR comment with coverage diff + python3 scripts/coverage/compare_cov.py diff --git a/.github/workflows/trivy.yaml b/.github/workflows/trivy.yaml index 39c6eba598..c9df0bbc9f 100644 --- a/.github/workflows/trivy.yaml +++ b/.github/workflows/trivy.yaml @@ -12,6 +12,7 @@ permissions: contents: read jobs: scan: + if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} permissions: contents: read security-events: write @@ -21,16 +22,24 @@ jobs: matrix: image: ["retina-agent", "retina-init", "retina-operator", "kubectl-retina", "retina-shell"] runs-on: ubuntu-latest # trivy only supports running on Linux + timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Get Tag + env: + GH_TOKEN: ${{ github.token }} run: | - echo "TAG=$(make version)" >> $GITHUB_ENV + if [ "${{ github.event_name }}" = "schedule" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + TAG=$(gh release view --repo ${{ github.repository }} --json tagName -q .tagName 2>/dev/null || make version) + else + TAG=$(make version) + fi + echo "TAG=$TAG" >> $GITHUB_ENV - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@6c175e9c4083a92bbca2f9724c8a5e33bc2d97a5 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # 0.36.0 with: image-ref: "ghcr.io/${{ github.repository }}/${{ matrix.image }}:${{ env.TAG }}" format: "template" @@ -39,6 +48,6 @@ jobs: severity: "CRITICAL,HIGH" - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 with: sarif_file: "trivy-results.sarif" diff --git a/.github/workflows/update-hubble.yaml b/.github/workflows/update-hubble.yaml index dc65eb5083..2e01141c4e 100644 --- a/.github/workflows/update-hubble.yaml +++ b/.github/workflows/update-hubble.yaml @@ -13,10 +13,11 @@ jobs: update-hubble: name: Update Hubble to latest version runs-on: ubuntu-latest + timeout-minutes: 15 steps: - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Get latest Hubble version id: get_version @@ -51,7 +52,7 @@ jobs: - name: Create pull request if: env.update_needed == 'true' - uses: peter-evans/create-pull-request@v7 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ secrets.GITHUB_TOKEN }} branch: deps/update-hubble-to-${{ env.version }} diff --git a/.gitignore b/.gitignore index 0fbcc3c9c5..1171f198e4 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,9 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +# logs +*.log + # Dependency directories (remove the comment below to include it) # vendor/ @@ -48,3 +51,5 @@ netperf-*.csv .certs/ artifacts/ + +test-summary \ No newline at end of file diff --git a/.golangci.yaml b/.golangci.yaml index ea6e2cf9f0..eeb577c46a 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,49 +1,95 @@ +version: "2" +linters: + enable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - contextcheck + - copyloopvar + - durationcheck + - err113 + - errchkjson + - errorlint + - exhaustive + - fatcontext + - gocheckcompilerdirectives + - gochecksumtype + - goconst + - gocritic + - gocyclo + - goprintffuncname + - gosec + - gosmopolitan + - lll + - loggercheck + - makezero + - misspell + - musttag + - nakedret + - nilerr + - nilnesserr + - noctx + - perfsprint + - prealloc + - promlinter + - protogetter + - reassign + - recvcheck + - revive + - rowserrcheck + - spancheck + - sqlclosecheck + - testifylint + - unparam + - wrapcheck + - zerologlint + settings: + gocritic: + disabled-checks: + - hugeParam + enabled-tags: + - diagnostic + - style + - performance + govet: + enable: + - shadow + lll: + line-length: 200 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + # some type names are caps/underscore to map OS primitive types + - linters: + - revive + - var-naming + path: pkg/metrics/types_windows.go + - linters: + - revive + - var-naming + path: pkg/metrics/types_linux.go + paths: + - third_party$ + - builtin$ + - examples$ issues: - max-same-issues: 0 max-issues-per-linter: 0 + max-same-issues: 0 new-from-rev: origin/main - exclude-rules: - # some type names are caps/underscore to map OS primitive types - - path: pkg/metrics/types_windows.go - linters: - - revive - - var-naming - - path: pkg/metrics/types_linux.go - linters: - - revive - - var-naming -linters: - presets: - - bugs - - error - - format - - performance - - unused - disable: - - gci +formatters: enable: - - copyloopvar - - goconst - - gocritic - - gocyclo - - gofmt - - goprintffuncname - - gosimple - - lll - - misspell - - nakedret - - promlinter - - revive -linters-settings: - gocritic: - enabled-tags: - - "diagnostic" - - "style" - - "performance" - disabled-checks: - - "hugeParam" - govet: - enable: - - shadow - lll: - line-length: 200 + - gofmt + - gofumpt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/.goreleaser.yaml b/.goreleaser.yaml index e7a395b868..db214b01b1 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -10,6 +10,7 @@ before: builds: - binary: kubectl-retina-{{ .Os }}-{{ .Arch }} + id: retina env: - CGO_ENABLED=0 goarch: @@ -24,13 +25,47 @@ builds: ldflags: - -X github.com/microsoft/retina/internal/buildinfo.Version=v{{.Version}} main: cli/main.go + - binary: kubectl-retina-mcr-{{ .Os }}-{{ .Arch }} + id: retina-mcr + env: + - CGO_ENABLED=0 + goarch: + - amd64 + - arm64 + gcflags: + - -dwarflocationlists=true + goos: + - linux + - windows + - darwin + ldflags: + - -X github.com/microsoft/retina/internal/buildinfo.Version=v{{.Version}} + - -X github.com/microsoft/retina/internal/buildinfo.RetinaAgentImageName={{.Env.MCR_AGENT_IMAGE_NAME}} + main: cli/main.go archives: - name_template: "{{ .Binary }}-v{{ .Version }}" + id: retina-kubectl + ids: + - retina + wrap_in_directory: false + format_overrides: + - goos: windows + formats: [ 'zip' ] + - name_template: "{{ .Binary }}-v{{ .Version }}" + id: retina-kubectl-mcr + ids: + - retina-mcr wrap_in_directory: false format_overrides: - goos: windows - format: zip + formats: [ 'zip' ] + +checksum: + name_template: 'checksums.txt' + +sboms: + - artifacts: archive changelog: sort: asc diff --git a/Makefile b/Makefile index 42e8b9eefc..458b42e9f9 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ ifndef TAG endif OUTPUT_DIR = $(REPO_ROOT)/output ARTIFACTS_DIR = $(REPO_ROOT)/artifacts +OUTPUT_LOCAL ?= --output type=local,dest=$(ARTIFACTS_DIR) BUILD_DIR = $(OUTPUT_DIR)/$(GOOS)_$(GOARCH) RETINA_BUILD_DIR = $(BUILD_DIR)/retina RETINA_DIR = $(REPO_ROOT)/controller @@ -21,11 +22,16 @@ CAPTURE_WORKLOAD_DIR = $(REPO_ROOT)/captureworkload KIND = /usr/local/bin/kind KIND_CLUSTER = retina-cluster WINVER2022 ?= "10.0.20348.1906" -WINVER2019 ?= "10.0.17763.4737" APP_INSIGHTS_ID ?= "" +AGENT_IMAGE_NAME ?= "" GENERATE_TARGET_DIRS = \ ./pkg/plugin/linuxutil +# Set agent registry to get image from when using retina-kubectl +ifneq ($(AGENT_IMAGE_NAME), "") + EXTRA_BUILD_ARGS := "--build-arg AGENT_IMAGE_NAME=$(AGENT_IMAGE_NAME)" +endif + # Default platform is linux/amd64 GOOS ?= linux GOARCH ?= amd64 @@ -33,9 +39,9 @@ OS ?= $(GOOS) ARCH ?= $(GOARCH) PLATFORM ?= $(OS)/$(ARCH) PLATFORMS ?= linux/amd64 linux/arm64 windows/amd64 -OS_VERSION ?= ltsc2019 +OS_VERSION ?= ltsc2022 -HUBBLE_VERSION ?= v1.17.3 +HUBBLE_VERSION ?= v1.19.3 CONTAINER_BUILDER ?= docker CONTAINER_RUNTIME ?= docker @@ -61,7 +67,7 @@ RETINA_PLATFORM_TAG ?= $(TAG)-$(subst /,-,$(PLATFORM)) # used for looping through components in container build AGENT_TARGETS ?= init agent -WINDOWS_YEARS ?= "2019 2022" +WINDOWS_YEARS ?= 2022 # for windows os, add year to the platform tag ifeq ($(OS),windows) @@ -89,7 +95,7 @@ help: ## Display this help ##@ Tools GOFUMPT = go tool mvdan.cc/gofumpt -GOLANGCI_LINT = go tool github.com/golangci/golangci-lint/cmd/golangci-lint +GOLANGCI_LINT = go tool github.com/golangci/golangci-lint/v2/cmd/golangci-lint GORELEASER = go tool github.com/goreleaser/goreleaser CONTROLLER_GEN = go tool sigs.k8s.io/controller-tools/cmd/controller-gen GINKGO = go tool github.com/onsi/ginkgo @@ -130,6 +136,9 @@ generate-bpf-go: ## generate ebpf wrappers for plugins for all archs FMT_PKG ?= . LINT_PKG ?= . +empty-bpf-objects: ## truncate all tracked .o files to 0 bytes + git ls-files '*.o' | xargs truncate -s 0 + fmt: ## run gofumpt on $FMT_PKG (default "retina"). $(GOFUMPT) -w $(FMT_PKG) @@ -142,6 +151,9 @@ lint-existing: ## Lint the current branch in entirety. clean: ## clean build artifacts $(RMDIR) $(OUTPUT_DIR) +bump-images: ## update all Dockerfile base image digests to latest + @./scripts/bump-images.sh + ##@ Build Binaries retina: ## builds retina binary @@ -166,6 +178,7 @@ RETINA_INIT_IMAGE = $(IMAGE_NAMESPACE)/retina-init RETINA_OPERATOR_IMAGE = $(IMAGE_NAMESPACE)/retina-operator RETINA_SHELL_IMAGE = $(IMAGE_NAMESPACE)/retina-shell KUBECTL_RETINA_IMAGE = $(IMAGE_NAMESPACE)/kubectl-retina +KUBECTL_RETINA_SHELL_IMAGE = $(IMAGE_NAMESPACE)/kubectl-retina-shell RETINA_INTEGRATION_TEST_IMAGE = $(IMAGE_NAMESPACE)/retina-integration-test RETINA_PROTO_IMAGE = $(IMAGE_NAMESPACE)/retina-proto-gen RETINA_GO_GEN_IMAGE = $(IMAGE_NAMESPACE)/retina-go-gen @@ -207,7 +220,6 @@ buildx: fi; - container-docker: buildx # util target to build container images using docker buildx. do not invoke directly. os=$$(echo $(PLATFORM) | cut -d'/' -f1); \ arch=$$(echo $(PLATFORM) | cut -d'/' -f2); \ @@ -228,10 +240,31 @@ container-docker: buildx # util target to build container images using docker bu --build-arg VERSION=$(VERSION) $(EXTRA_BUILD_ARGS) \ --target=$(TARGET) \ -t $(IMAGE_REGISTRY)/$(IMAGE):$(TAG) \ - --output type=local,dest=$(ARTIFACTS_DIR) \ + $(OUTPUT_LOCAL) \ $(BUILDX_ACTION) \ $(CONTEXT_DIR) +container-docker-windows: # util target to build Windows container images without buildx. do not invoke directly. + os=$$(echo $(PLATFORM) | cut -d'/' -f1); \ + arch=$$(echo $(PLATFORM) | cut -d'/' -f2); \ + image_name=$$(basename $(IMAGE)); \ + echo "Building $$image_name for $$os/$$arch "; \ + docker build \ + --platform $(PLATFORM) \ + -f $(DOCKERFILE) \ + --build-arg BUILDPLATFORM=$(PLATFORM) \ + --build-arg APP_INSIGHTS_ID=$(APP_INSIGHTS_ID) \ + --build-arg GOARCH=$$arch \ + --build-arg GOOS=$$os \ + --build-arg OS_VERSION=$(OS_VERSION) \ + --build-arg HUBBLE_VERSION=$(HUBBLE_VERSION) \ + --build-arg VERSION=$(VERSION) \ + --build-arg REPO_PATH=$(REPO_PATH) \ + --build-arg BINARIES_PATH=$(BINARIES_PATH) \ + $(EXTRA_BUILD_ARGS) \ + --target=$(TARGET) \ + -t $(IMAGE_REGISTRY)/$(IMAGE):$(TAG) \ + $(CONTEXT_DIR) retina-image: ## build the retina linux container image. echo "Building for $(PLATFORM)" @@ -254,20 +287,24 @@ retina-image: ## build the retina linux container image. TARGET=$$target; \ done -retina-image-win: ## build the retina Windows container image. +retina-image-win: build-windows-binaries ## build the retina Windows container image. +# The Windows images are built on a corresponding Windows host without buildx. +# This is done to mitigate CVE-2013-3900. for year in $(WINDOWS_YEARS); do \ tag=$(TAG)-windows-ltsc$$year-amd64; \ - echo "Building $(RETINA_PLATFORM_TAG)"; \ + echo "Building $$tag"; \ set -e ; \ - $(MAKE) container-$(CONTAINER_BUILDER) \ + $(MAKE) container-docker-windows \ PLATFORM=windows/amd64 \ - DOCKERFILE=controller/Dockerfile \ + DOCKERFILE=controller/Dockerfile.windows-$$year \ REGISTRY=$(IMAGE_REGISTRY) \ IMAGE=$(RETINA_IMAGE) \ OS_VERSION=ltsc$$year \ VERSION=$(TAG) \ TAG=$$tag \ TARGET=agent-win \ + REPO_PATH=$(REPO_PATH) \ + BINARIES_PATH=$(BINARIES_PATH) \ CONTEXT_DIR=$(REPO_ROOT); \ done @@ -294,6 +331,7 @@ retina-shell-image: IMAGE=$(RETINA_SHELL_IMAGE) \ VERSION=$(TAG) \ TAG=$(RETINA_PLATFORM_TAG) \ + OUTPUT_LOCAL= \ CONTEXT_DIR=$(REPO_ROOT) kubectl-retina-image: @@ -306,7 +344,22 @@ kubectl-retina-image: IMAGE=$(KUBECTL_RETINA_IMAGE) \ VERSION=$(TAG) \ TAG=$(RETINA_PLATFORM_TAG) \ - CONTEXT_DIR=$(REPO_ROOT) + CONTEXT_DIR=$(REPO_ROOT) \ + EXTRA_BUILD_ARGS=$(EXTRA_BUILD_ARGS) + +kubectl-retina-shell-image: + echo "Building shell-enabled kubectl-retina for $(PLATFORM)" + set -e ; \ + $(MAKE) container-$(CONTAINER_BUILDER) \ + PLATFORM=$(PLATFORM) \ + DOCKERFILE=cli/Dockerfile \ + REGISTRY=$(IMAGE_REGISTRY) \ + IMAGE=$(KUBECTL_RETINA_SHELL_IMAGE) \ + VERSION=$(TAG) \ + TAG=$(RETINA_PLATFORM_TAG) \ + CONTEXT_DIR=$(REPO_ROOT) \ + TARGET=shell-target \ + EXTRA_BUILD_ARGS=$(EXTRA_BUILD_ARGS) kapinger-image: docker buildx build --builder retina --platform windows/amd64 --target windows-amd64 -t $(IMAGE_REGISTRY)/$(KAPINGER_IMAGE):$(TAG)-windows-amd64 ./hack/tools/kapinger/ --push @@ -339,21 +392,32 @@ all-gen: ## generate all code $(MAKE) proto-gen $(MAKE) go-gen -build-windows-binaries: - GOOS=windows GOARCH=$(GOARCH) go build -v -o /go/bin/retina/captureworkload -ldflags "-X github.com/microsoft/retina/internal/buildinfo.Version=$(TAG) -X github.com/microsoft/retina/internal/buildinfo.ApplicationInsightsID=$(APP_INSIGHTS_ID)" captureworkload/main.go - GOOS=windows GOARCH=$(GOARCH) go build -x -v -o /go/bin/retina/controller -ldflags "-X github.com/microsoft/retina/internal/buildinfo.Version=$(TAG) -X github.com/microsoft/retina/internal/buildinfo.ApplicationInsightsID=$(APP_INSIGHTS_ID)" controller/main.go - +build-windows-binaries: ## Build Windows binaries + @echo "Building Windows binaries for $(GOARCH)..." + @mkdir -p $(BUILD_DIR) + CGO_ENABLED=0 GOOS=windows GOARCH=$(GOARCH) go build -v \ + -o $(BUILD_DIR)/captureworkload.exe \ + -ldflags "-X github.com/microsoft/retina/internal/buildinfo.Version=$(TAG) \ + -X github.com/microsoft/retina/internal/buildinfo.ApplicationInsightsID=$(APP_INSIGHTS_ID)" \ + $(CAPTURE_WORKLOAD_DIR)/main.go + CGO_ENABLED=0 GOOS=windows GOARCH=$(GOARCH) go build -v \ + -o $(BUILD_DIR)/controller.exe \ + -ldflags "-X github.com/microsoft/retina/internal/buildinfo.Version=$(TAG) \ + -X github.com/microsoft/retina/internal/buildinfo.ApplicationInsightsID=$(APP_INSIGHTS_ID)" \ + $(RETINA_DIR)/main.go + @echo "Windows binaries built successfully in $(BUILD_DIR)" + ##@ Multiplatform manifest-retina-image: ## create a multiplatform manifest for the retina image $(eval FULL_IMAGE_NAME=$(IMAGE_REGISTRY)/$(RETINA_IMAGE):$(TAG)) $(eval FULL_INIT_IMAGE_NAME=$(IMAGE_REGISTRY)/$(RETINA_INIT_IMAGE):$(TAG)) - docker buildx imagetools create -t $(FULL_IMAGE_NAME) $(foreach platform,linux/amd64 linux/arm64 windows-ltsc2019-amd64 windows-ltsc2022-amd64, $(FULL_IMAGE_NAME)-$(subst /,-,$(platform))) + docker buildx imagetools create -t $(FULL_IMAGE_NAME) $(foreach platform,linux/amd64 linux/arm64 windows-ltsc2022-amd64, $(FULL_IMAGE_NAME)-$(subst /,-,$(platform))) docker buildx imagetools create -t $(FULL_INIT_IMAGE_NAME) $(foreach platform,linux/amd64 linux/arm64, $(FULL_INIT_IMAGE_NAME)-$(subst /,-,$(platform))) manifest-operator-image: ## create a multiplatform manifest for the operator image $(eval FULL_IMAGE_NAME=$(IMAGE_REGISTRY)/$(RETINA_OPERATOR_IMAGE):$(TAG)) - docker buildx imagetools create -t $(FULL_IMAGE_NAME) $(foreach platform,linux/amd64, $(FULL_IMAGE_NAME)-$(subst /,-,$(platform))) + docker buildx imagetools create -t $(FULL_IMAGE_NAME) $(foreach platform,linux/amd64 linux/arm64, $(FULL_IMAGE_NAME)-$(subst /,-,$(platform))) manifest-shell-image: $(eval FULL_IMAGE_NAME=$(IMAGE_REGISTRY)/$(RETINA_SHELL_IMAGE):$(TAG)) @@ -363,6 +427,10 @@ manifest-kubectl-retina-image: $(eval FULL_IMAGE_NAME=$(IMAGE_REGISTRY)/$(KUBECTL_RETINA_IMAGE):$(TAG)) docker buildx imagetools create -t $(FULL_IMAGE_NAME) $(foreach platform,linux/amd64 linux/arm64, $(FULL_IMAGE_NAME)-$(subst /,-,$(platform))) +manifest-kubectl-retina-shell-image: + $(eval FULL_IMAGE_NAME=$(IMAGE_REGISTRY)/$(KUBECTL_RETINA_SHELL_IMAGE):$(TAG)) + docker buildx imagetools create -t $(FULL_IMAGE_NAME) $(foreach platform,linux/amd64 linux/arm64, $(FULL_IMAGE_NAME)-$(subst /,-,$(platform))) + manifest: echo "Building for $(COMPONENT)" if [ "$(COMPONENT)" = "retina" ]; then \ @@ -373,6 +441,8 @@ manifest: $(MAKE) manifest-shell-image; \ elif [ "$(COMPONENT)" = "kubectl-retina" ]; then \ $(MAKE) manifest-kubectl-retina-image; \ + elif [ "$(COMPONENT)" = "kubectl-retina-shell" ]; then \ + $(MAKE) manifest-kubectl-retina-shell-image; \ fi ##@ Tests @@ -393,11 +463,15 @@ COVER_PKG ?= . .PHONY: test test: # Run unit tests. go build -o test-summary ./test/utsummary/main.go - KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use -p path)" go test -tags=unit,dashboard -skip=TestE2E* -coverprofile=coverage.out -v -json ./... | ./test-summary --progress --verbose + bash -o pipefail -c 'KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use -p path)" go test -tags=unit,dashboard -skip=TestE2E* -coverprofile=coverage.out -v -json ./... | ./test-summary --progress --verbose' + +.PHONY: test-ebpf +test-ebpf: # Run eBPF program tests (requires root/CAP_BPF). + sudo $$(which go) test -tags=ebpf -v -count=1 ./pkg/plugin/... coverage: # Code coverage. # go generate ./... && go test -tags=unit -coverprofile=coverage.out.tmp ./... - cat coverage.out | grep -v "_bpf.go\|_bpfel_x86.go\|_bpfel_arm64.go|_generated.go|mock_" | grep -v mock > coveragenew.out + cat coverage.out | grep -Ev '_bpf\.go|_bpfel_x86\.go|_bpfel_arm64\.go|_generated\.go|mock_' > coveragenew.out go tool cover -html coveragenew.out -o coverage.html go tool cover -func=coveragenew.out -o coverageexpanded.out ls -al @@ -416,7 +490,7 @@ manifests: cd crd && make manifests && make generate # Fetch the latest tag from the GitHub -LATEST_TAG := $(shell curl -s https://api.github.com/repos/microsoft/retina/releases | jq -r '.[0].name') +LATEST_TAG := $(shell curl -s https://api.github.com/repos/microsoft/retina/releases/latest | jq -r '.name') HELM_IMAGE_TAG ?= $(LATEST_TAG) @@ -573,6 +647,9 @@ simplify-dashboards: run-perf-test: go test -v ./test/e2e/retina_perf_test.go -timeout 2h -tags=perf -count=1 -args -image-tag=${TAG} -image-registry=${IMAGE_REGISTRY} -image-namespace=${IMAGE_NAMESPACE} +run-e2e-test: + go test -v ./test/e2e/ -timeout 1h -tags=e2e -count=1 -args -image-tag=${TAG} -image-registry=${IMAGE_REGISTRY} -image-namespace=${IMAGE_NAMESPACE} + .PHONY: update-hubble update-hubble: @echo "Checking for Hubble updates..." diff --git a/README.md b/README.md index 7a28679dd0..541b5f930f 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,12 @@ Retina lets you **investigate network issues on-demand** and **continuously moni See [retina.sh](http://retina.sh) for documentation and examples. +## Known Limitations + +⚠️ **Performance on High-Core-Count Systems**: Community users have reported performance considerations when using Advanced metrics (with `packetparser` plugin) on nodes with 32+ CPU cores under high network load. Consider starting with Basic metrics mode on large node types. See [Known Limitations](https://retina.sh/docs/Introduction/intro#known-limitations) for details. + +⚠️ **Windows Server 2019**: Retina no longer supports Windows Server 2019 nodes. Use Windows Server 2022 for Windows workloads. + ## Capabilities Retina has two major features: diff --git a/cli/Dockerfile b/cli/Dockerfile index c187323479..784859528e 100644 --- a/cli/Dockerfile +++ b/cli/Dockerfile @@ -1,8 +1,9 @@ -# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.24.4-azurelinux3.0 --format "{{.Name}}@{{.Digest}}" -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/oss/go/microsoft/golang:1.24.4-azurelinux3.0@sha256:250d01e55a37bd79d7014ae83f9f50aa6fa5570ca910e7f19faeff4bb0132ae1 AS builder +# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.26.3-azurelinux3.0 --format "{{.Name}}@{{.Digest}}" +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/oss/go/microsoft/golang:1.26.3-1-azurelinux3.0@sha256:ef480755a4126131197d7311ab1e24d55600407194b45349c4975b7ed0d176e6 AS builder ARG VERSION ARG APP_INSIGHTS_ID +ARG AGENT_IMAGE_NAME="ghcr.io/microsoft/retina/retina-agent" WORKDIR /workspace COPY . . @@ -15,13 +16,33 @@ ARG GOARCH=amd64 ENV GOARCH=${GOARCH} RUN --mount=type=cache,target="/root/.cache/go-build" \ - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + go build \ -ldflags "-X github.com/microsoft/retina/internal/buildinfo.Version="$VERSION" \ - -X "github.com/microsoft/retina/internal/buildinfo.ApplicationInsightsID"="$APP_INSIGHTS_ID"" \ + -X "github.com/microsoft/retina/internal/buildinfo.ApplicationInsightsID"="$APP_INSIGHTS_ID" \ + -X "github.com/microsoft/retina/internal/buildinfo.RetinaAgentImageName"="$AGENT_IMAGE_NAME"" \ -a -o kubectl-retina cli/main.go +# minimal libs stage — provides only the system libraries needed by the CGO-enabled binary, +# without the bloat of the full Go SDK image (python, gcc, systemd, etc.) +# skopeo inspect docker://mcr.microsoft.com/azurelinux/base/core:3.0 --format "{{.Name}}@{{.Digest}}" +FROM mcr.microsoft.com/azurelinux/base/core:3.0.20260517@sha256:f5e224c47997aa4a5d3d8addfcc3866e175e7026368a71ce1be2c0eed1876f04 AS libs + +# Target 1: Distroless (secure, minimal) # skopeo inspect docker://mcr.microsoft.com/azurelinux/distroless/minimal:3.0 --format "{{.Name}}@{{.Digest}}" -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/azurelinux/distroless/minimal:3.0@sha256:5a66f9f16ac675db2a8229dac72d83811b73b502d6ad192d8b374c7f3be498af +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/azurelinux/distroless/minimal:3.0.20260517@sha256:0c64ab9cfc44d4f100c0590bd59ead9afedda6cc54f14bb7465b5f9c35ddc037 AS distroless-target +COPY --from=libs /lib/ /lib +COPY --from=libs /usr/lib/ /usr/lib +COPY --from=libs /etc/pki/tls/ /etc/pki/tls/ WORKDIR / COPY --from=builder /workspace/kubectl-retina . +# Target 2: Shell-enabled (operational, init container support) +# skopeo inspect docker://mcr.microsoft.com/azurelinux/base/core:3.0 --format "{{.Name}}@{{.Digest}}" +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/azurelinux/base/core:3.0.20260517@sha256:f5e224c47997aa4a5d3d8addfcc3866e175e7026368a71ce1be2c0eed1876f04 AS shell-target +WORKDIR / +COPY --from=builder /workspace/kubectl-retina /bin/kubectl-retina +RUN chmod +x /bin/kubectl-retina + +# Default target (distroless for backward compatibility) +FROM distroless-target + diff --git a/cli/README.md b/cli/README.md index d638fa6ad9..3a2fedb7df 100644 --- a/cli/README.md +++ b/cli/README.md @@ -8,6 +8,11 @@ This directory serves as the entrypoint to Retina CLI Trace subcommand retrieve status or results from Retina. +### bpftrace + +Bpftrace subcommand allows real-time tracing of network issues on Kubernetes nodes using eBPF/bpftrace. +Captures packet drops, TCP RST events, socket errors, and retransmissions. + ### Config Config subcommand configures retina CLI. diff --git a/cli/cmd/bpftrace.go b/cli/cmd/bpftrace.go new file mode 100644 index 0000000000..e59c31d6e6 --- /dev/null +++ b/cli/cmd/bpftrace.go @@ -0,0 +1,350 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package cmd + +import ( + "context" + "errors" + "fmt" + "net" + "os" + "os/signal" + "syscall" + "time" + + "github.com/microsoft/retina/shell" + "github.com/spf13/cobra" + v1 "k8s.io/api/core/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/resource" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/scheme" + "k8s.io/kubectl/pkg/util/templates" +) + +// Trace command flags - use separate variables to avoid conflicts with shell command +var ( + traceConfigFlags *genericclioptions.ConfigFlags + traceMatchVersionFlags *cmdutil.MatchVersionFlags + + // Image settings + traceRetinaShellImageRepo string + traceRetinaShellImageVersion string + + // Filter settings (raw strings from CLI, validated before use) + traceFilterIP string + traceFilterCIDR string + + // Output settings + traceOutputFormat string + traceDuration time.Duration + traceStartupTimeout time.Duration + + // Event selection flags + traceAll bool + traceDrops bool + traceRST bool + traceErrors bool + traceRetransmits bool + traceNfqueueDrops bool +) + +// TraceOutputFormat represents validated output format options +type TraceOutputFormat string + +const ( + TraceOutputTable TraceOutputFormat = "table" + TraceOutputJSON TraceOutputFormat = "json" +) + +// Validation errors +var ( + errInvalidIP = errors.New("invalid IP address") + errIPv6NotSupported = errors.New("IPv6 is not supported, please provide an IPv4 address") + errInvalidCIDR = errors.New("invalid CIDR notation") + errInvalidOutputFormat = errors.New("invalid output format: must be 'table' or 'json'") + errNodeOnly = errors.New("bpftrace command only supports nodes, not pods") +) + +// ValidateFilterIP validates an IP address string and returns the parsed IP. +// Returns nil IP and no error if input is empty (no filter). +// Returns error if input is non-empty but invalid. +func ValidateFilterIP(input string) (net.IP, error) { + if input == "" { + return nil, nil + } + ip := net.ParseIP(input) + if ip == nil { + return nil, fmt.Errorf("%w: %q", errInvalidIP, input) + } + if ip.To4() == nil { + return nil, fmt.Errorf("%w: %q", errIPv6NotSupported, input) + } + return ip, nil +} + +// ValidateFilterCIDR validates a CIDR string and returns the parsed IPNet. +// Returns nil and no error if input is empty (no filter). +// Returns error if input is non-empty but invalid. +func ValidateFilterCIDR(input string) (*net.IPNet, error) { + if input == "" { + return nil, nil + } + _, ipnet, err := net.ParseCIDR(input) + if err != nil { + return nil, fmt.Errorf("%w: %q: %w", errInvalidCIDR, input, err) + } + return ipnet, nil +} + +// ValidateOutputFormat validates the output format string. +func ValidateOutputFormat(input string) (TraceOutputFormat, error) { + switch input { + case "table", "": + return TraceOutputTable, nil + case "json": + return TraceOutputJSON, nil + default: + return "", fmt.Errorf("%w: got %q", errInvalidOutputFormat, input) + } +} + +var bpftraceCmd = &cobra.Command{ + Use: "bpftrace NODE", + Short: "[EXPERIMENTAL] Trace network issues on a node using bpftrace", + Long: templates.LongDesc(` + [EXPERIMENTAL] This is an experimental command. The flags and behavior may change in the future. + + Trace network issues (packet drops, TCP resets, connection errors) on a node in real-time + using bpftrace. + + This creates a privileged pod on the target node that runs bpftrace to capture: + * Packet drops (with drop reason: NETFILTER_DROP, NO_SOCKET, etc.) [--drops] + * TCP RST sent/received (connection refused, reset by peer) [--rst] + * Socket errors (ECONNREFUSED, ETIMEDOUT, etc.) [--errors] + * TCP retransmissions (packet loss indicators) [--retransmits] + * NFQUEUE drops (no consumer on iptables NFQUEUE target) [--nfqueue-drops] + + By default, all event types are traced. Use individual flags to trace specific events only. + + Use --ip or --cidr to focus on specific endpoints. + The filter matches both source AND destination addresses. + + Note: Currently supports IPv4 only. +`), + + Example: templates.Examples(` + # trace all network issues on a node (default) + kubectl retina bpftrace node0001 + + # trace only packet drops + kubectl retina bpftrace node0001 --drops + + # trace drops and RSTs for a specific IP + kubectl retina bpftrace node0001 --drops --rst --ip 10.244.1.15 + + # trace retransmits for a subnet + kubectl retina bpftrace node0001 --retransmits --cidr 10.244.0.0/16 + + # trace for 60 seconds and exit + kubectl retina bpftrace node0001 --duration 60s + + # output in JSON format (for scripting) + kubectl retina bpftrace node0001 --output json + + # trace only socket errors + kubectl retina bpftrace node0001 --errors + + # trace NFQUEUE drops (packets hitting iptables NFQUEUE with no consumer) + kubectl retina bpftrace node0001 --nfqueue-drops + + # combine options + kubectl retina bpftrace node0001 --ip 10.244.1.15 --duration 30s --output json +`), + Args: cobra.ExactArgs(1), + RunE: runBpftrace, +} + +func runBpftrace(_ *cobra.Command, args []string) error { + // Validate image version + if traceRetinaShellImageVersion == "" { + return errMissingRequiredRetinaShellImageVersionArg + } + + // === SECURITY: Validate all user inputs BEFORE any use === + + // Validate IP filter (strict parsing) + filterIP, err := ValidateFilterIP(traceFilterIP) + if err != nil { + return fmt.Errorf("invalid --ip: %w", err) + } + + // Validate CIDR filter (strict parsing) + filterCIDR, err := ValidateFilterCIDR(traceFilterCIDR) + if err != nil { + return fmt.Errorf("invalid --cidr: %w", err) + } + + // Validate output format (whitelist) + outputFormat, err := ValidateOutputFormat(traceOutputFormat) + if err != nil { + return err + } + + // Get namespace + namespace, explicitNamespace, err := traceMatchVersionFlags.ToRawKubeConfigLoader().Namespace() + if err != nil { + return fmt.Errorf("error retrieving namespace arg: %w", err) + } + + // Parse node argument (only nodes supported, not pods) + r := resource.NewBuilder(traceConfigFlags). + WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). + FilenameParam(explicitNamespace, &resource.FilenameOptions{}). + NamespaceParam(namespace).DefaultNamespace().ResourceNames("nodes", args[0]). + Do() + if rerr := r.Err(); rerr != nil { + return fmt.Errorf("error constructing resource builder: %w", rerr) + } + + // Get REST config + restConfig, err := traceMatchVersionFlags.ToRESTConfig() + if err != nil { + return fmt.Errorf("error constructing REST config: %w", err) + } + + // Visit the resource (should be a node) + return r.Visit(func(info *resource.Info, err error) error { //nolint:wrapcheck // visitor pattern returns errors as-is + if err != nil { + return err + } + + switch obj := info.Object.(type) { + case *v1.Node: + nodeName := obj.Name + podNamespace := namespace + + // Determine which events to trace + // If no individual flags set, or --all is set, enable all events + enableAll := traceAll || (!traceDrops && !traceRST && !traceErrors && !traceRetransmits && !traceNfqueueDrops) + + // Build TraceConfig with validated, typed values only + traceConfig := shell.TraceConfig{ + RestConfig: restConfig, + RetinaShellImage: fmt.Sprintf("%s:%s", traceRetinaShellImageRepo, traceRetinaShellImageVersion), + FilterIPs: nil, + FilterCIDRs: nil, + OutputJSON: outputFormat == TraceOutputJSON, + TraceDuration: traceDuration, + Timeout: traceStartupTimeout, + EnableDrops: enableAll || traceDrops, + EnableRST: enableAll || traceRST, + EnableErrors: enableAll || traceErrors, + EnableRetransmits: enableAll || traceRetransmits, + EnableNfqueueDrops: enableAll || traceNfqueueDrops, + } + + // Add validated IP filter (already typed as net.IP) + if filterIP != nil { + traceConfig.FilterIPs = append(traceConfig.FilterIPs, filterIP) + } + + // Add validated CIDR filter (already typed as *net.IPNet) + if filterCIDR != nil { + traceConfig.FilterCIDRs = append(traceConfig.FilterCIDRs, filterCIDR) + } + + // Create context with cancellation for Ctrl-C handling + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Handle Ctrl-C gracefully + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + defer signal.Stop(sigCh) + go func() { + <-sigCh + fmt.Fprintln(os.Stderr, "\nReceived interrupt, cleaning up...") + cancel() + }() + + // Apply duration timeout if specified + if traceDuration > 0 { + var timeoutCancel context.CancelFunc + ctx, timeoutCancel = context.WithTimeout(ctx, traceDuration) + defer timeoutCancel() + } + + return shell.RunTrace(ctx, traceConfig, nodeName, podNamespace) + + case *v1.Pod: + return errNodeOnly + + default: + gvk := obj.GetObjectKind().GroupVersionKind() + return fmt.Errorf("unsupported resource %s/%s: %w", gvk.GroupVersion(), gvk.Kind, errUnsupportedResourceType) + } + }) +} + +func init() { + Retina.AddCommand(bpftraceCmd) + + bpftraceCmd.PersistentPreRun = func(cmd *cobra.Command, _ []string) { + cmd.SilenceUsage = true + cmd.SilenceErrors = true + + // Allow setting image repo and version via environment variables + if !cmd.Flags().Changed("retina-shell-image-repo") { + if envRepo := os.Getenv("RETINA_SHELL_IMAGE_REPO"); envRepo != "" { + traceRetinaShellImageRepo = envRepo + } + } + if !cmd.Flags().Changed("retina-shell-image-version") { + if envVersion := os.Getenv("RETINA_SHELL_IMAGE_VERSION"); envVersion != "" { + traceRetinaShellImageVersion = envVersion + } + } + } + + // Image flags (same as shell command) + bpftraceCmd.Flags().StringVar(&traceRetinaShellImageRepo, "retina-shell-image-repo", + defaultRetinaShellImageRepo, "The container registry repository for the retina-shell image") + bpftraceCmd.Flags().StringVar(&traceRetinaShellImageVersion, "retina-shell-image-version", + defaultRetinaShellImageVersion, "The version (tag) of the retina-shell image") + + // Filter flags + bpftraceCmd.Flags().StringVar(&traceFilterIP, "ip", "", + "Filter by IP address (matches source OR destination)") + bpftraceCmd.Flags().StringVar(&traceFilterCIDR, "cidr", "", + "Filter by CIDR (matches source OR destination)") + + // Event selection flags + bpftraceCmd.Flags().BoolVar(&traceAll, "all", false, + "Enable all event types (default behavior when no event flags specified)") + bpftraceCmd.Flags().BoolVar(&traceDrops, "drops", false, + "Enable packet drop events (kfree_skb tracepoint)") + bpftraceCmd.Flags().BoolVar(&traceRST, "rst", false, + "Enable TCP RST events (tcp_send_reset/tcp_receive_reset)") + bpftraceCmd.Flags().BoolVar(&traceErrors, "errors", false, + "Enable socket error events (inet_sk_error_report)") + bpftraceCmd.Flags().BoolVar(&traceRetransmits, "retransmits", false, + "Enable TCP retransmit events (tcp_retransmit_skb)") + bpftraceCmd.Flags().BoolVar(&traceNfqueueDrops, "nfqueue-drops", false, + "Enable NFQUEUE drop events (fexit:vmlinux:__nf_queue, requires BTF)") + + // Output flags + bpftraceCmd.Flags().StringVarP(&traceOutputFormat, "output", "o", "table", + "Output format: 'table' (human-readable) or 'json' (machine-readable)") + bpftraceCmd.Flags().DurationVar(&traceDuration, "duration", 0, + "How long to trace (e.g., 30s, 5m). 0 means until Ctrl-C.") + bpftraceCmd.Flags().DurationVar(&traceStartupTimeout, "startup-timeout", defaultTimeout, + "Timeout for starting the trace pod") + + // Kubernetes config flags + traceConfigFlags = genericclioptions.NewConfigFlags(true) + traceConfigFlags.AddFlags(bpftraceCmd.PersistentFlags()) + traceMatchVersionFlags = cmdutil.NewMatchVersionFlags(traceConfigFlags) + traceMatchVersionFlags.AddFlags(bpftraceCmd.PersistentFlags()) +} diff --git a/cli/cmd/bpftrace_test.go b/cli/cmd/bpftrace_test.go new file mode 100644 index 0000000000..5bc7558e6a --- /dev/null +++ b/cli/cmd/bpftrace_test.go @@ -0,0 +1,393 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package cmd + +import ( + "net" + "testing" +) + +func TestValidateFilterIP(t *testing.T) { + tests := []struct { + name string + input string + wantIP net.IP + wantErr bool + }{ + // Valid cases + { + name: "valid IPv4", + input: "10.0.0.1", + wantIP: net.ParseIP("10.0.0.1"), + wantErr: false, + }, + { + name: "valid IPv4 192.168", + input: "192.168.1.1", + wantIP: net.ParseIP("192.168.1.1"), + wantErr: false, + }, + { + name: "IPv6 loopback rejected", + input: "::1", + wantIP: nil, + wantErr: true, + }, + { + name: "IPv6 full rejected", + input: "2001:db8::1", + wantIP: nil, + wantErr: true, + }, + { + name: "empty string - no filter", + input: "", + wantIP: nil, + wantErr: false, + }, + // Security: Injection attempts + { + name: "injection attempt - semicolon command", + input: "10.0.0.1; rm -rf /", + wantIP: nil, + wantErr: true, + }, + { + name: "injection attempt - backtick command", + input: "10.0.0.1`whoami`", + wantIP: nil, + wantErr: true, + }, + { + name: "injection attempt - dollar command", + input: "10.0.0.1$(whoami)", + wantIP: nil, + wantErr: true, + }, + { + name: "injection attempt - pipe", + input: "10.0.0.1 | cat /etc/passwd", + wantIP: nil, + wantErr: true, + }, + { + name: "injection attempt - newline", + input: "10.0.0.1\nrm -rf /", + wantIP: nil, + wantErr: true, + }, + // Invalid IPs + { + name: "not an IP - text", + input: "not-an-ip", + wantIP: nil, + wantErr: true, + }, + { + name: "invalid octet - 256", + input: "10.0.0.256", + wantIP: nil, + wantErr: true, + }, + { + name: "invalid format - too many octets", + input: "10.0.0.1.5", + wantIP: nil, + wantErr: true, + }, + { + name: "invalid - CIDR notation", + input: "10.0.0.0/24", + wantIP: nil, + wantErr: true, + }, + { + name: "invalid - hostname", + input: "example.com", + wantIP: nil, + wantErr: true, + }, + { + name: "invalid - negative number", + input: "-1.0.0.1", + wantIP: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotIP, err := ValidateFilterIP(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("ValidateFilterIP(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) + return + } + if tt.wantErr { + return + } + // Compare IPs + if tt.wantIP == nil && gotIP == nil { + return // Both nil, OK + } + if tt.wantIP == nil || gotIP == nil { + t.Errorf("ValidateFilterIP(%q) = %v, want %v", tt.input, gotIP, tt.wantIP) + return + } + if !tt.wantIP.Equal(gotIP) { + t.Errorf("ValidateFilterIP(%q) = %v, want %v", tt.input, gotIP, tt.wantIP) + } + }) + } +} + +func TestValidateFilterCIDR(t *testing.T) { + tests := []struct { + name string + input string + wantCIDR string // Expected CIDR string representation + wantErr bool + }{ + // Valid cases + { + name: "valid /24", + input: "10.0.0.0/24", + wantCIDR: "10.0.0.0/24", + wantErr: false, + }, + { + name: "valid /16", + input: "192.168.0.0/16", + wantCIDR: "192.168.0.0/16", + wantErr: false, + }, + { + name: "valid /8", + input: "10.0.0.0/8", + wantCIDR: "10.0.0.0/8", + wantErr: false, + }, + { + name: "valid /32 single host", + input: "10.0.0.1/32", + wantCIDR: "10.0.0.1/32", + wantErr: false, + }, + { + name: "valid - normalizes to network address", + input: "10.0.0.5/24", + wantCIDR: "10.0.0.0/24", // Normalized + wantErr: false, + }, + { + name: "empty string - no filter", + input: "", + wantCIDR: "", + wantErr: false, + }, + // Security: Injection attempts + { + name: "injection attempt - semicolon", + input: "10.0.0.0/24; rm -rf /", + wantCIDR: "", + wantErr: true, + }, + { + name: "injection attempt - backtick", + input: "10.0.0.0/24`whoami`", + wantCIDR: "", + wantErr: true, + }, + { + name: "injection attempt - newline", + input: "10.0.0.0/24\ncat /etc/passwd", + wantCIDR: "", + wantErr: true, + }, + // Invalid CIDRs + { + name: "invalid - no mask", + input: "10.0.0.0", + wantCIDR: "", + wantErr: true, + }, + { + name: "invalid - mask too large", + input: "10.0.0.0/33", + wantCIDR: "", + wantErr: true, + }, + { + name: "invalid - negative mask", + input: "10.0.0.0/-1", + wantCIDR: "", + wantErr: true, + }, + { + name: "invalid - text", + input: "not-a-cidr", + wantCIDR: "", + wantErr: true, + }, + { + name: "invalid - hostname", + input: "example.com/24", + wantCIDR: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotCIDR, err := ValidateFilterCIDR(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("ValidateFilterCIDR(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) + return + } + if tt.wantErr { + return + } + // Compare CIDRs + if tt.wantCIDR == "" && gotCIDR == nil { + return // Both empty, OK + } + if tt.wantCIDR == "" || gotCIDR == nil { + t.Errorf("ValidateFilterCIDR(%q) = %v, want %v", tt.input, gotCIDR, tt.wantCIDR) + return + } + if gotCIDR.String() != tt.wantCIDR { + t.Errorf("ValidateFilterCIDR(%q) = %v, want %v", tt.input, gotCIDR.String(), tt.wantCIDR) + } + }) + } +} + +func TestValidateOutputFormat(t *testing.T) { + tests := []struct { + name string + input string + wantFormat TraceOutputFormat + wantErr bool + }{ + // Valid cases + { + name: "table", + input: "table", + wantFormat: TraceOutputTable, + wantErr: false, + }, + { + name: "json", + input: "json", + wantFormat: TraceOutputJSON, + wantErr: false, + }, + { + name: "empty defaults to table", + input: "", + wantFormat: TraceOutputTable, + wantErr: false, + }, + // Invalid cases + { + name: "invalid - yaml", + input: "yaml", + wantFormat: "", + wantErr: true, + }, + { + name: "invalid - xml", + input: "xml", + wantFormat: "", + wantErr: true, + }, + { + name: "invalid - random", + input: "notaformat", + wantFormat: "", + wantErr: true, + }, + { + name: "invalid - TABLE uppercase", + input: "TABLE", + wantFormat: "", + wantErr: true, + }, + { + name: "invalid - JSON uppercase", + input: "JSON", + wantFormat: "", + wantErr: true, + }, + // Security: injection attempts + { + name: "injection - semicolon", + input: "table; rm -rf /", + wantFormat: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotFormat, err := ValidateOutputFormat(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("ValidateOutputFormat(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) + return + } + if gotFormat != tt.wantFormat { + t.Errorf("ValidateOutputFormat(%q) = %v, want %v", tt.input, gotFormat, tt.wantFormat) + } + }) + } +} + +// TestValidationRejectsAllInjectionPatterns is a comprehensive security test +// that ensures common injection patterns are rejected by all validators. +func TestValidationRejectsAllInjectionPatterns(t *testing.T) { + injectionPatterns := []string{ + "; rm -rf /", + "| cat /etc/passwd", + "` whoami `", + "`whoami`", + "$(whoami)", + "$((1+1))", + "\n rm -rf /", + "\r\n del *.*", + "&& ls", + "|| ls", + "> /tmp/file", + "< /etc/passwd", + "' OR '1'='1", + "\" OR \"1\"=\"1", + } + + validIP := "10.0.0.1" + validCIDR := "10.0.0.0/24" + + for _, pattern := range injectionPatterns { + t.Run("IP_"+pattern, func(t *testing.T) { + input := validIP + pattern + _, err := ValidateFilterIP(input) + if err == nil { + t.Errorf("ValidateFilterIP(%q) should have returned error for injection pattern", input) + } + }) + + t.Run("CIDR_"+pattern, func(t *testing.T) { + input := validCIDR + pattern + _, err := ValidateFilterCIDR(input) + if err == nil { + t.Errorf("ValidateFilterCIDR(%q) should have returned error for injection pattern", input) + } + }) + + t.Run("Output_"+pattern, func(t *testing.T) { + input := "table" + pattern + _, err := ValidateOutputFormat(input) + if err == nil { + t.Errorf("ValidateOutputFormat(%q) should have returned error for injection pattern", input) + } + }) + } +} diff --git a/cli/cmd/capture/capture.go b/cli/cmd/capture/capture.go index c7c6861b12..83017feb80 100644 --- a/cli/cmd/capture/capture.go +++ b/cli/cmd/capture/capture.go @@ -4,28 +4,150 @@ package capture import ( - "github.com/microsoft/retina/cli/cmd" + "errors" + "fmt" + "time" + "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/kubernetes" +) + +var ( + ErrInvalidVerbosityLevel = errors.New("invalid verbosity level") + ErrInvalidTimestampFormat = errors.New("invalid timestamp format") + ErrInvalidPrintDataFormat = errors.New("invalid print data format") + ErrBPFFilterEmpty = errors.New("BPF filter cannot be empty or whitespace-only") + ErrBPFFilterContainsFlag = errors.New("BPF filter contains flag which is not allowed") +) + +// VerbosityLevel represents the verbosity level for packet capture output +type VerbosityLevel string + +const ( + VerbosityNormal VerbosityLevel = "" // Default, no extra verbosity + VerbosityVerbose VerbosityLevel = "verbose" // tcpdump -v + VerbosityExtra VerbosityLevel = "extra" // tcpdump -vv + VerbosityMax VerbosityLevel = "max" // tcpdump -vvv ) -var opts = struct { +func (v VerbosityLevel) Validate() error { + switch v { + case VerbosityNormal, VerbosityVerbose, VerbosityExtra, VerbosityMax: + return nil + default: + return fmt.Errorf("%w: %s (valid: verbose, extra, max)", ErrInvalidVerbosityLevel, v) + } +} + +// TimestampFormat represents the timestamp format for packet capture output +type TimestampFormat string + +const ( + TimestampDefault TimestampFormat = "" // Default formatted timestamp + TimestampNone TimestampFormat = "none" // tcpdump -t + TimestampUnformatted TimestampFormat = "unformatted" // tcpdump -tt (Unix epoch) + TimestampDelta TimestampFormat = "delta" // tcpdump -ttt (delta between packets) + TimestampDate TimestampFormat = "date" // tcpdump -tttt (with date) + TimestampDeltaSinceFirst TimestampFormat = "delta-since-first" // tcpdump -ttttt (delta since first) +) + +func (t TimestampFormat) Validate() error { + switch t { + case TimestampDefault, TimestampNone, TimestampUnformatted, TimestampDelta, TimestampDate, TimestampDeltaSinceFirst: + return nil + default: + return fmt.Errorf("%w: %s (valid: none, unformatted, delta, date, delta-since-first)", ErrInvalidTimestampFormat, t) + } +} + +// PrintDataFormat represents the format for printing packet data +type PrintDataFormat string + +const ( + PrintDataNone PrintDataFormat = "" // Default, no data printing + PrintDataHex PrintDataFormat = "hex" // tcpdump -x (hex only) + PrintDataHexWithLink PrintDataFormat = "hex-with-link" // tcpdump -xx (hex with link header) + PrintDataASCII PrintDataFormat = "ascii" // tcpdump -A (ASCII only) + PrintDataASCIIWithLink PrintDataFormat = "ascii-with-link" // tcpdump -AA (ASCII with link header) +) + +func (p PrintDataFormat) Validate() error { + switch p { + case PrintDataNone, PrintDataHex, PrintDataHexWithLink, PrintDataASCII, PrintDataASCIIWithLink: + return nil + default: + return fmt.Errorf("%w: %s (valid: hex, hex-with-link, ascii, ascii-with-link)", ErrInvalidPrintDataFormat, p) + } +} + +type Opts struct { genericclioptions.ConfigFlags - Name *string -}{ + Name *string + blobUpload string + cleanUpAfterUpload bool + debug bool + duration time.Duration + excludeFilter string + hostPath string + hostPathBaseDir string + includeFilter string + includeMetadata bool + interfaces string + jobNumLimit int + maxSize int + namespaceSelectors string + nodeNames string + nodeSelectors string + nowait bool + packetSize int + podNames string + podSelectors string + pvc string + s3AccessKeyID string + s3Bucket string + s3Endpoint string + s3Path string + s3Region string + s3SecretAccessKey string + // tcpdumpFilter is deprecated and will be removed. Use captureOption.pcapFilter and captureOption boolean flags for display options. + tcpdumpFilter string + pcapFilter string + noPromiscuous bool + packetBuffered bool + immediateMode bool + noResolveDNS bool + noResolvePort bool + verbosityLevel VerbosityLevel + timestampFormat TimestampFormat + printDataFormat PrintDataFormat + printLinkHeader bool + quietOutput bool + absoluteSeq bool + dontVerifyChecksum bool +} + +var opts = Opts{ Name: new(string), } -const defaultName = "retina-capture" +const DefaultName = "retina-capture" -var capture = &cobra.Command{ - Use: "capture", - Short: "Capture network traffic", -} +func NewCommand(kubeClient kubernetes.Interface) *cobra.Command { + capture := &cobra.Command{ + Use: "capture", + Short: "Capture network traffic", + Long: "Capture network traffic from pods in a Kubernetes cluster.", + } -func init() { - cmd.Retina.AddCommand(capture) opts.ConfigFlags = *genericclioptions.NewConfigFlags(true) opts.AddFlags(capture.PersistentFlags()) - capture.PersistentFlags().StringVar(opts.Name, "name", defaultName, "The name of the Retina Capture") + capture.PersistentFlags().StringVar(opts.Name, "name", DefaultName, "The name of the Retina Capture") + + capture.AddCommand(NewCreateSubCommand(kubeClient)) + capture.AddCommand(NewDeleteSubCommand(kubeClient)) + capture.AddCommand(NewDownloadSubCommand()) + capture.AddCommand(NewListSubCommand()) + + return capture } diff --git a/cli/cmd/capture/cleanup_after_upload_test.go b/cli/cmd/capture/cleanup_after_upload_test.go new file mode 100644 index 0000000000..ace7b15e5c --- /dev/null +++ b/cli/cmd/capture/cleanup_after_upload_test.go @@ -0,0 +1,289 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package capture + +import ( + "bytes" + "context" + "fmt" + "testing" + + "github.com/microsoft/retina/pkg/label" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + clienttesting "k8s.io/client-go/testing" +) + +func newFakeClientForCleanupTests() *fake.Clientset { + objects := []runtime.Object{ + NewNode("node1"), + NewNamespace("default"), + } + + kubeClient := fake.NewClientset(objects...) + + // Handle job creation to set job name and quickly mark as completed + kubeClient.PrependReactor("create", "jobs", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + createAction, ok := action.(clienttesting.CreateAction) + if !ok { + return false, nil, fmt.Errorf("expected CreateAction, got %T", action) //nolint:err113 // test code + } + job := createAction.GetObject().(*batchv1.Job) + + // Set job name if unset + if job.Name == "" { + job.Name = job.GenerateName + randomString(5) + } + // Mark job as completed immediately for cleanup tests + now := metav1.Now() + job.Status.CompletionTime = &now + job.Status.Conditions = []batchv1.JobCondition{ + { + Type: batchv1.JobComplete, + Status: corev1.ConditionTrue, + }, + } + return false, job, nil + }) + + // Handle secret creation to set name from GenerateName + kubeClient.PrependReactor("create", "secrets", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + createAction, ok := action.(clienttesting.CreateAction) + if !ok { + return false, nil, fmt.Errorf("expected CreateAction, got %T", action) //nolint:err113 // test code + } + secret := createAction.GetObject().(*corev1.Secret) + + // Set secret name if unset (mimics real k8s behavior with GenerateName) + if secret.Name == "" { + secret.Name = secret.GenerateName + randomString(5) + } + return false, secret, nil + }) + + return kubeClient +} + +func TestCleanupAfterUpload_RequiresRemoteStorage(t *testing.T) { + // When --cleanup-after-upload is set without remote storage, it should fail + kubeClient := newFakeClientForCleanupTests() + cmd := NewCommand(kubeClient) + + cmd.SetArgs([]string{ + "create", + "--name=test-cleanup", + "--namespace=default", + "--node-names=node1", + "--host-path=captures", + "--cleanup-after-upload", + "--duration=5s", + }) + + buf := new(bytes.Buffer) + cmd.SetOut(buf) + + err := cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "--cleanup-after-upload requires remote storage (--blob-upload, --s3-bucket, or --pvc)") +} + +func TestCleanupAfterUpload_WithBlobUpload(t *testing.T) { + // When --cleanup-after-upload is set with blob upload, command should succeed + // and jobs should be created (controller handles cleanup after upload) + kubeClient := newFakeClientForCleanupTests() + cmd := NewCommand(kubeClient) + + cmd.SetArgs([]string{ + "create", + "--name=test-cleanup-blob", + "--namespace=default", + "--node-names=node1", + "--blob-upload=https://testaccount.blob.core.windows.net/container?sv=2021-06-08&ss=b&srt=co&sp=rwdlacitfx&se=2099-01-01", + "--cleanup-after-upload", + "--duration=5s", + }) + + buf := new(bytes.Buffer) + cmd.SetOut(buf) + + err := cmd.Execute() + require.NoError(t, err) + + // Verify jobs were created (controller handles cleanup after upload completes) + jobs, err := kubeClient.BatchV1().Jobs("default").List(context.TODO(), metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s", label.CaptureNameLabel, "test-cleanup-blob"), + }) + require.NoError(t, err) + assert.NotEmpty(t, jobs.Items, "jobs should be created for capture") +} + +func TestCleanupAfterUpload_WithS3Upload(t *testing.T) { + // When --cleanup-after-upload is set with S3 upload, command should succeed + kubeClient := newFakeClientForCleanupTests() + cmd := NewCommand(kubeClient) + + cmd.SetArgs([]string{ + "create", + "--name=test-cleanup-s3", + "--namespace=default", + "--node-names=node1", + "--s3-bucket=test-bucket", + "--s3-region=us-east-1", + "--s3-access-key-id=AKIAIOSFODNN7EXAMPLE", + "--s3-secret-access-key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + "--cleanup-after-upload", + "--duration=5s", + }) + + buf := new(bytes.Buffer) + cmd.SetOut(buf) + + err := cmd.Execute() + require.NoError(t, err) + + // Verify jobs were created (controller handles cleanup after upload completes) + jobs, err := kubeClient.BatchV1().Jobs("default").List(context.TODO(), metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s", label.CaptureNameLabel, "test-cleanup-s3"), + }) + require.NoError(t, err) + assert.NotEmpty(t, jobs.Items, "jobs should be created for capture") +} + +func TestCleanupAfterUpload_RespectsNoWait(t *testing.T) { + // When --cleanup-after-upload is set with --no-wait=true (default), + // the CLI should not block. TTL + owner refs handle cleanup. + kubeClient := newFakeClientForCleanupTests() + cmd := NewCommand(kubeClient) + + cmd.SetArgs([]string{ + "create", + "--name=test-cleanup-nowait", + "--namespace=default", + "--node-names=node1", + "--blob-upload=https://testaccount.blob.core.windows.net/container?sv=2021-06-08&ss=b&srt=co&sp=rwdlacitfx&se=2099-01-01", + "--cleanup-after-upload", + "--no-wait=true", + "--duration=5s", + }) + + buf := new(bytes.Buffer) + cmd.SetOut(buf) + + err := cmd.Execute() + require.NoError(t, err) + + // Verify jobs still exist (CLI returns immediately in no-wait mode, + // TTL and owner references handle automatic cleanup) + jobs, err := kubeClient.BatchV1().Jobs("default").List(context.TODO(), metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s", label.CaptureNameLabel, "test-cleanup-nowait"), + }) + require.NoError(t, err) + assert.NotEmpty(t, jobs.Items, "jobs should exist because CLI returned immediately in no-wait mode") +} + +func TestCleanupAfterUpload_DefaultIsFalse(t *testing.T) { + // Without --cleanup-after-upload flag, the default should be false + assert.False(t, DefaultCleanUpAfterUpload) +} + +func TestCleanupAfterUpload_FlagRegistered(t *testing.T) { + // Verify the flag is properly registered on the create subcommand + kubeClient := fake.NewClientset() + cmd := NewCommand(kubeClient) + + // Find the create subcommand + var createCmd *cobra.Command + for _, sub := range cmd.Commands() { + if sub.Name() == "create" { + createCmd = sub + break + } + } + require.NotNil(t, createCmd, "create subcommand should exist") + + flag := createCmd.Flags().Lookup("cleanup-after-upload") + require.NotNil(t, flag, "cleanup-after-upload flag should be registered") + assert.Equal(t, "false", flag.DefValue) + assert.Contains(t, flag.Usage, "clean up capture jobs") +} + +func TestCleanupAfterUpload_TTLSetInNoWaitMode(t *testing.T) { + // When --cleanup-after-upload with --no-wait=true and remote destination, + // jobs should have TTLSecondsAfterFinished set. + kubeClient := newFakeClientForCleanupTests() + + var createdJob *batchv1.Job + kubeClient.PrependReactor("create", "jobs", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + createAction := action.(clienttesting.CreateAction) + job := createAction.GetObject().(*batchv1.Job) + if job.Name == "" { + job.Name = job.GenerateName + "test3" + } + now := metav1.Now() + job.Status.CompletionTime = &now + createdJob = job + return false, job, nil + }) + + cmd := NewCommand(kubeClient) + cmd.SetArgs([]string{ + "create", + "--name=test-ttl", + "--namespace=default", + "--node-names=node1", + "--blob-upload=https://testaccount.blob.core.windows.net/container?sv=2021-06-08", + "--cleanup-after-upload", + "--no-wait=true", + "--duration=5s", + }) + + err := cmd.Execute() + require.NoError(t, err) + require.NotNil(t, createdJob) + + require.NotNil(t, createdJob.Spec.TTLSecondsAfterFinished, "TTL should be set in no-wait mode with remote destination") + assert.Equal(t, JobTTLSecondsAfterFinished, *createdJob.Spec.TTLSecondsAfterFinished) +} + +func TestCleanupAfterUpload_NoTTLWhenHostPathOnly(t *testing.T) { + // When only host-path is configured (no remote), TTL should NOT be set + // even in no-wait mode, because the user needs the job to find their capture file. + kubeClient := newFakeClientForCleanupTests() + + var createdJob *batchv1.Job + kubeClient.PrependReactor("create", "jobs", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + createAction := action.(clienttesting.CreateAction) + job := createAction.GetObject().(*batchv1.Job) + if job.Name == "" { + job.Name = job.GenerateName + "test4" + } + now := metav1.Now() + job.Status.CompletionTime = &now + createdJob = job + return false, job, nil + }) + + cmd := NewCommand(kubeClient) + cmd.SetArgs([]string{ + "create", + "--name=test-no-ttl", + "--namespace=default", + "--node-names=node1", + "--host-path=captures", + "--no-wait=true", + "--duration=5s", + }) + + err := cmd.Execute() + require.NoError(t, err) + require.NotNil(t, createdJob) + + assert.Nil(t, createdJob.Spec.TTLSecondsAfterFinished, "TTL should NOT be set when only host-path is configured") +} diff --git a/cli/cmd/capture/create.go b/cli/cmd/capture/create.go index 16b0f61fb0..bb1cbfafb4 100644 --- a/cli/cmd/capture/create.go +++ b/cli/cmd/capture/create.go @@ -16,8 +16,10 @@ import ( "go.uber.org/zap" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes" "k8s.io/kubectl/pkg/util/i18n" @@ -28,41 +30,22 @@ import ( "github.com/microsoft/retina/internal/buildinfo" pkgcapture "github.com/microsoft/retina/pkg/capture" captureConstants "github.com/microsoft/retina/pkg/capture/constants" + "github.com/microsoft/retina/pkg/capture/file" captureUtils "github.com/microsoft/retina/pkg/capture/utils" "github.com/microsoft/retina/pkg/config" ) -var ( - blobUpload string - debug bool - duration time.Duration - excludeFilter string - hostPath string - includeFilter string - includeMetadata bool - jobNumLimit int - maxSize int - namespace string - namespaceSelectors string - nodeNames string - nodeSelectors string - nowait bool - packetSize int - podSelectors string - pvc string - s3AccessKeyID string - s3Bucket string - s3Endpoint string - s3Path string - s3Region string - s3SecretAccessKey string - tcpdumpFilter string -) - const ( - DefaultDebug bool = false - DefaultDuration time.Duration = 1 * time.Minute - DefaultHostPath string = "/mnt/retina/captures" + DefaultCleanUpAfterUpload bool = false + DefaultDebug bool = false + DefaultDuration time.Duration = 1 * time.Minute + // DefaultHostPath is the default subpath (joined under DefaultHostPathBaseDir on + // the node) where capture artifacts are stored. It is a bare name, not an + // absolute path: absolute paths are rejected by the operator. + DefaultHostPath string = "retina" + // DefaultHostPathBaseDir is the default node-side parent directory for capture + // artifacts; the user-supplied --host-path subpath is joined under it. + DefaultHostPathBaseDir string = "/var/log/retina/captures" DefaultIncludeMetadata bool = true DefaultJobNumLimit int = 0 DefaultMaxSize int = 100 @@ -72,21 +55,56 @@ const ( DefaultS3Path string = "retina/captures" DefaultWaitPeriod time.Duration = 1 * time.Minute DefaultWaitTimeout time.Duration = 5 * time.Minute + + // JobTTLSecondsAfterFinished is how long completed/failed jobs and their + // pods are kept before Kubernetes garbage-collects them (no-wait mode). + JobTTLSecondsAfterFinished int32 = 300 // 5 minutes + + // JobActiveDeadlineBufferSeconds is added to the capture duration to form + // the Job's activeDeadlineSeconds. This buffer accounts for output upload + // and cleanup time after the capture itself finishes. + JobActiveDeadlineBufferSeconds int64 = 1800 // 30 minutes + + // WaitDeadlineBuffer is extra time added to the capture duration when + // computing the wait deadline, to account for upload and scheduling overhead. + WaitDeadlineBuffer time.Duration = 5 * time.Minute + + // MinPollAttempts is the minimum number of polling iterations before the + // wait deadline expires. Used to compute a fallback poll period. + MinPollAttempts = 4 ) +var errCleanupRequiresRemoteStorage = errors.New("--cleanup-after-upload requires remote storage (--blob-upload, --s3-bucket, or --pvc)") + +// hasRemoteDestination returns true if the capture options specify a remote +// storage output (blob upload or S3 upload). +func hasRemoteDestination(o *Opts) bool { + return o.blobUpload != "" || o.s3Bucket != "" || o.pvc != "" +} + var createExample = templates.Examples(i18n.T(` # Select nodes by node name and copy the artifacts to the node host path - kubectl retina capture create --host-path /mnt/retina/testcapture --node-names "," + # (joined under the operator-configured base directory; default /var/log/retina/captures/testcapture) + kubectl retina capture create --host-path testcapture --node-names "," # Select pods determined by pod-selectors and namespace-selectors kubectl retina capture create --namespace capture --pod-selectors="k8s-app=kube-dns" --namespace-selectors="kubernetes.io/metadata.name=kube-system" + # Select specific pods by name + kubectl retina capture create --namespace default --pod-names "pod1,pod2" --duration 30s + + # Select a single pod by name + kubectl retina capture create --namespace myapp --pod-names "my-app-pod-abc123" --duration 60s + # Select nodes with label "agentpool=agentpool" and "version:v20" kubectl retina capture create --node-selectors="agentpool=agentpool,version:v20" # Select nodes using node-selector and set duration to 10s kubectl retina capture create --node-selectors="agentpool=agentpool" --duration=10s + # Capture on specific network interfaces (instead of all interfaces) + kubectl retina capture create --node-selectors="agentpool=agentpool" --interfaces="eth0,eth1" + # Select nodes using node-selector and upload the artifacts to blob storage with SAS URL https://testaccount.blob.core.windows.net/ kubectl retina capture create --node-selectors="agentpool=agentpool" --blob-upload=https://testaccount.blob.core.windows.net/ @@ -105,89 +123,198 @@ var createExample = templates.Examples(i18n.T(` --s3-secret-access-key "your-secret-access-key" `)) -var createCapture = &cobra.Command{ - Use: "create", - Short: "Create a Retina Capture", - Example: createExample, - RunE: func(*cobra.Command, []string) error { - kubeConfig, err := opts.ToRESTConfig() - if err != nil { - return errors.Wrap(err, "failed to compose k8s rest config") - } +func create(kubeClient kubernetes.Interface) error { + // Set namespace. If --namespace is not set, use namespace on user's context + ns, _, err := opts.ConfigFlags.ToRawKubeConfigLoader().Namespace() + if err != nil { + return errors.Wrap(err, "failed to get namespace from kubeconfig") + } - kubeClient, err := kubernetes.NewForConfig(kubeConfig) - if err != nil { - return errors.Wrap(err, "failed to initialize kubernetes client") + if opts.Namespace == nil || *opts.Namespace == "" { + opts.Namespace = &ns + } + + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM) + defer cancel() + + capture, err := createCaptureF(ctx, kubeClient) + if err != nil { + return err + } + + if opts.cleanUpAfterUpload { + if !hasRemoteDestination(&opts) { + return errCleanupRequiresRemoteStorage } + } - ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM) - defer cancel() + jobsCreated, err := createJobs(ctx, kubeClient, capture) + if err != nil { + retinacmd.Logger.Error("Failed to create job", zap.Error(err)) + return err + } - capture, err := createCaptureF(ctx, kubeClient) - if err != nil { - return err + // Set owner references on secrets so they are garbage-collected when the + // jobs are removed (by TTL, explicit deletion, or manual kubectl delete). + if ownerRefErr := setSecretOwnerReferences(ctx, kubeClient, capture, jobsCreated); ownerRefErr != nil { + retinacmd.Logger.Error("Failed to set owner references on capture secrets", zap.Error(ownerRefErr)) + } + + if opts.nowait { + if opts.cleanUpAfterUpload && hasRemoteDestination(&opts) { + retinacmd.Logger.Info("Capture jobs will be automatically cleaned up after upload (TTL-based)") + } else { + retinacmd.Logger.Info("Please manually delete all capture jobs (associated secrets will be garbage-collected automatically)") } + printCaptureResult(jobsCreated) + return nil + } - jobsCreated, err := createJobs(ctx, kubeClient, capture) - if err != nil { - retinacmd.Logger.Error("Failed to create job", zap.Error(err)) - return err + // Wait until all jobs finish then delete the jobs before the timeout, otherwise print jobs created to + // let the customer recycle them. + retinacmd.Logger.Info("Waiting for capture jobs to finish") + + allJobsCompleted := waitUntilJobsComplete(ctx, kubeClient, jobsCreated) + + // Delete all jobs created only if they all completed, otherwise keep the jobs for debugging. + if allJobsCompleted { + retinacmd.Logger.Info("Deleting jobs as all jobs are completed") + jobsFailedToDelete := deleteJobs(ctx, kubeClient, jobsCreated) + if len(jobsFailedToDelete) != 0 { + retinacmd.Logger.Info("Please manually delete capture jobs failed to delete", zap.String("namespace", *opts.Namespace), zap.String("job list", strings.Join(jobsFailedToDelete, ","))) } - if nowait { - retinacmd.Logger.Info("Please manually delete all capture jobs") - if capture.Spec.OutputConfiguration.BlobUpload != nil { - retinacmd.Logger.Info("Please manually delete capture secret", zap.String("namespace", *opts.Namespace), zap.String("secret name", *capture.Spec.OutputConfiguration.BlobUpload)) + + if capture.Spec.OutputConfiguration.BlobUpload != nil { + err = deleteSecret(ctx, kubeClient, capture.Spec.OutputConfiguration.BlobUpload) + if err != nil { + retinacmd.Logger.Error("Failed to delete capture secret, please manually delete it", + zap.String("namespace", *opts.Namespace), zap.String("secret name", *capture.Spec.OutputConfiguration.BlobUpload), zap.Error(err)) } - if capture.Spec.OutputConfiguration.S3Upload != nil && capture.Spec.OutputConfiguration.S3Upload.SecretName != "" { - retinacmd.Logger.Info("Please manually delete capture secret", zap.String("namespace", *opts.Namespace), zap.String("secret name", capture.Spec.OutputConfiguration.S3Upload.SecretName)) + } + + if capture.Spec.OutputConfiguration.S3Upload != nil && capture.Spec.OutputConfiguration.S3Upload.SecretName != "" { + err = deleteSecret(ctx, kubeClient, &capture.Spec.OutputConfiguration.S3Upload.SecretName) + if err != nil { + retinacmd.Logger.Error("Failed to delete capture secret, please manually delete it", + zap.String("namespace", *opts.Namespace), + zap.String("secret name", capture.Spec.OutputConfiguration.S3Upload.SecretName), + zap.Error(err), + ) } - printCaptureResult(jobsCreated) - return nil } - // Wait until all jobs finish then delete the jobs before the timeout, otherwise print jobs created to - // let the customer recycle them. - retinacmd.Logger.Info("Waiting for capture jobs to finish") + if len(jobsFailedToDelete) == 0 && err == nil { + retinacmd.Logger.Info("Done for deleting jobs") + } + return nil + } - allJobsCompleted := waitUntilJobsComplete(ctx, kubeClient, jobsCreated) + retinacmd.Logger.Info("Not all job are completed in the given time") + retinacmd.Logger.Info("Please manually delete the Capture") - // Delete all jobs created only if they all completed, otherwise keep the jobs for debugging. - if allJobsCompleted { - retinacmd.Logger.Info("Deleting jobs as all jobs are completed") - jobsFailedToDelete := deleteJobs(ctx, kubeClient, jobsCreated) - if len(jobsFailedToDelete) != 0 { - retinacmd.Logger.Info("Please manually delete capture jobs failed to delete", zap.String("namespace", *opts.Namespace), zap.String("job list", strings.Join(jobsFailedToDelete, ","))) - } + return getCaptureAndPrintCaptureResult(ctx, kubeClient, capture.Name, *opts.Namespace) +} - if capture.Spec.OutputConfiguration.BlobUpload != nil { - err = deleteSecret(ctx, kubeClient, capture.Spec.OutputConfiguration.BlobUpload) - if err != nil { - retinacmd.Logger.Error("Failed to delete capture secret, please manually delete it", - zap.String("namespace", *opts.Namespace), zap.String("secret name", *capture.Spec.OutputConfiguration.BlobUpload), zap.Error(err)) - } - } +func GetClientset() (*kubernetes.Clientset, error) { + kubeConfig, err := opts.ToRESTConfig() + if err != nil { + return nil, errors.Wrap(err, "failed to compose k8s rest config") + } - if capture.Spec.OutputConfiguration.S3Upload != nil && capture.Spec.OutputConfiguration.S3Upload.SecretName != "" { - err = deleteSecret(ctx, kubeClient, &capture.Spec.OutputConfiguration.S3Upload.SecretName) - if err != nil { - retinacmd.Logger.Error("Failed to delete capture secret, please manually delete it", - zap.String("namespace", *opts.Namespace), - zap.String("secret name", capture.Spec.OutputConfiguration.S3Upload.SecretName), - zap.Error(err), - ) - } - } + kubeClient, err := kubernetes.NewForConfig(kubeConfig) + if err != nil { + return nil, errors.Wrap(err, "failed to initialize kubernetes client") + } - if len(jobsFailedToDelete) == 0 && err == nil { - retinacmd.Logger.Info("Done for deleting jobs") - } - return nil + return kubeClient, nil +} + +func NewCreateSubCommand(kubeClient kubernetes.Interface) *cobra.Command { + createCapture := &cobra.Command{ + Use: "create", + Short: "Create a Retina Capture", + Example: createExample, + } + + var verbosityStr, timestampStr, printDataStr string + + createCapture.RunE = func(*cobra.Command, []string) error { + // Validate enum flags + opts.verbosityLevel = VerbosityLevel(verbosityStr) + if err := opts.verbosityLevel.Validate(); err != nil { + return err + } + opts.timestampFormat = TimestampFormat(timestampStr) + if err := opts.timestampFormat.Validate(); err != nil { + return err } + opts.printDataFormat = PrintDataFormat(printDataStr) + if err := opts.printDataFormat.Validate(); err != nil { + return err + } + return create(kubeClient) + } + + createCapture.Flags().DurationVar(&opts.duration, "duration", DefaultDuration, "Duration of capturing packets") + createCapture.Flags().IntVar(&opts.maxSize, "max-size", DefaultMaxSize, "Limit the capture file to MB in size which works only for Linux") //nolint:gomnd // default + createCapture.Flags().IntVar(&opts.packetSize, "packet-size", DefaultPacketSize, "Limits the each packet to bytes in size which works only for Linux") + createCapture.Flags().StringVar(&opts.nodeNames, "node-names", "", "A comma-separated list of node names to select nodes on which the network capture will be performed") + createCapture.Flags().StringVar(&opts.nodeSelectors, "node-selectors", DefaultNodeSelectors, "A comma-separated list of node labels to select nodes on which the network capture will be performed") + createCapture.Flags().StringVar(&opts.podNames, "pod-names", "", + "A comma-separated list of pod names to select specific pods on which the network capture will be performed (must be in the specified namespace)") + createCapture.Flags().StringVar(&opts.podSelectors, "pod-selectors", "", + "A comma-separated list of pod labels to select pods on which the network capture will be performed") + createCapture.Flags().StringVar(&opts.namespaceSelectors, "namespace-selectors", "", + "A comma-separated list of namespace labels to filter which namespaces will be targeted for packet capture (used with --pod-selectors)") + createCapture.Flags().StringVar(&opts.hostPath, "host-path", DefaultHostPath, + "Subpath name (joined under --host-path-base-dir) for capture artifacts on the node. Must be a relative subpath and must not contain '..'.") + createCapture.Flags().StringVar(&opts.hostPathBaseDir, "host-path-base-dir", DefaultHostPathBaseDir, "Absolute base directory on the node under which --host-path is joined") + createCapture.Flags().StringVar(&opts.pvc, "pvc", "", "PersistentVolumeClaim under the specified or default namespace to store capture files") + createCapture.Flags().StringVar(&opts.blobUpload, "blob-upload", "", "Blob SAS URL with write permission to upload capture files") + createCapture.Flags().StringVar(&opts.s3Region, "s3-region", "", "Region where the S3 compatible bucket is located") + createCapture.Flags().StringVar(&opts.s3Endpoint, "s3-endpoint", "", + "Endpoint for an S3 compatible storage service. Use this if you are using a custom or private S3 service that requires a specific endpoint") + createCapture.Flags().StringVar(&opts.s3Bucket, "s3-bucket", "", "Bucket in which to store capture files") + createCapture.Flags().StringVar(&opts.s3Path, "s3-path", DefaultS3Path, "Prefix path within the S3 bucket where captures will be stored") + createCapture.Flags().StringVar(&opts.s3AccessKeyID, "s3-access-key-id", "", "S3 access key id to upload capture files") + createCapture.Flags().StringVar(&opts.s3SecretAccessKey, "s3-secret-access-key", "", "S3 access secret key to upload capture files") + createCapture.Flags().StringVar(&opts.tcpdumpFilter, "tcpdump-filter", "", + "DEPRECATED and will be removed: Use --pcap-filter for BPF expressions. BPF filter expression without flags (e.g., 'host 10.0.0.1', 'tcp port 443')") + createCapture.Flags().StringVar(&opts.pcapFilter, "pcap-filter", "", + "BPF filter expression for packet filtering (e.g., 'host 10.0.0.1', 'tcp port 443'). See https://www.tcpdump.org/manpages/pcap-filter.7.html") + createCapture.Flags().StringVar(&opts.interfaces, "interfaces", "", "Comma-separated list of network interfaces to capture on (e.g., eth0,eth1)") + + // Tcpdump boolean flags for capture behavior and display options + createCapture.Flags().BoolVar(&opts.noPromiscuous, "no-promiscuous", false, "Disable promiscuous mode (tcpdump -p flag)") + createCapture.Flags().BoolVar(&opts.packetBuffered, "packet-buffered", false, "Enable packet-buffered output (tcpdump -U flag)") + createCapture.Flags().BoolVar(&opts.immediateMode, "immediate-mode", false, "Enable immediate mode for packet capture (tcpdump --immediate-mode)") + createCapture.Flags().BoolVar(&opts.noResolveDNS, "no-resolve-dns", false, "Don't resolve hostnames (tcpdump -n flag)") + createCapture.Flags().BoolVar(&opts.noResolvePort, "no-resolve-port", false, "Don't resolve hostnames or port names (tcpdump -nn flag)") + createCapture.Flags().BoolVar(&opts.printLinkHeader, "print-link-header", false, "Print link-level headers (tcpdump -e flag)") + createCapture.Flags().BoolVar(&opts.quietOutput, "quiet-output", false, "Quick/quiet output mode (tcpdump -q flag)") + createCapture.Flags().BoolVar(&opts.absoluteSeq, "absolute-seq", false, "Print absolute TCP sequence numbers (tcpdump -S flag)") + createCapture.Flags().BoolVar(&opts.dontVerifyChecksum, "dont-verify-checksum", false, "Don't verify TCP checksums (tcpdump -K flag)") + + // Enum-based flags for mutually exclusive options + createCapture.Flags().StringVar(&verbosityStr, "verbosity", "", "Verbosity level: verbose, extra, max (tcpdump -v/-vv/-vvv)") + createCapture.Flags().StringVar(×tampStr, "timestamp-format", "", "Timestamp format: none, unformatted, delta, date, delta-since-first (tcpdump -t/-tt/-ttt/-tttt/-ttttt)") + createCapture.Flags().StringVar(&printDataStr, "print-data", "", "Print packet data: hex, hex-with-link, ascii, ascii-with-link (tcpdump -x/-xx/-A/-AA)") + + // Filters + createCapture.Flags().StringVar(&opts.excludeFilter, "exclude-filter", "", "A comma-separated list of IP:Port pairs that are "+ + "excluded from capturing network packets. Supported formats are IP:Port, IP, Port, *:Port, IP:*") + createCapture.Flags().StringVar(&opts.includeFilter, "include-filter", "", "A comma-separated list of IP:Port pairs that are "+ + "used to filter capture network packets. Supported formats are IP:Port, IP, Port, *:Port, IP:*") + + // Capture options + createCapture.Flags().BoolVar(&opts.includeMetadata, "include-metadata", DefaultIncludeMetadata, "If true, collect static network metadata into capture file") + createCapture.Flags().IntVar(&opts.jobNumLimit, "job-num-limit", DefaultJobNumLimit, "The maximum number of jobs can be created for each capture. 0 means no limit") + createCapture.Flags().BoolVar(&opts.nowait, "no-wait", DefaultNowait, "Do not wait for the long-running capture job to finish") + createCapture.Flags().BoolVar(&opts.cleanUpAfterUpload, "cleanup-after-upload", DefaultCleanUpAfterUpload, + "Automatically clean up capture jobs and resources after successful upload to remote storage") + createCapture.Flags().BoolVar(&opts.debug, "debug", DefaultDebug, "When debug is true, a customized retina-agent image, determined by the environment variable RETINA_AGENT_IMAGE, is set") - retinacmd.Logger.Info("Not all job are completed in the given time") - retinacmd.Logger.Info("Please manually delete the Capture") - return getCaptureAndPrintCaptureResult(ctx, kubeClient, capture.Name, *opts.Namespace) - }, + return createCapture } func createSecretFromBlobUpload(ctx context.Context, kubeClient kubernetes.Interface, blobUpload, captureName string) (string, error) { @@ -236,10 +363,47 @@ func deleteSecret(ctx context.Context, kubeClient kubernetes.Interface, secretNa return nil } - return kubeClient.CoreV1().Secrets(*opts.Namespace).Delete(ctx, *secretName, metav1.DeleteOptions{}) //nolint:wrapcheck //internal return + err := kubeClient.CoreV1().Secrets(*opts.Namespace).Delete(ctx, *secretName, metav1.DeleteOptions{}) + if k8serrors.IsNotFound(err) { + return nil + } + return err //nolint:wrapcheck //internal return +} + +// validateBPFFilter checks that a BPF filter string doesn't contain flags (tokens starting with '-'). +// This prevents command injection attacks by ensuring only BPF expressions are provided. +func validateBPFFilter(filter, filterName string) error { + if filter == "" { + return nil + } + + trimmed := strings.TrimSpace(filter) + if trimmed == "" { + return fmt.Errorf("%s: %w", filterName, ErrBPFFilterEmpty) + } + + // Check for flags (tokens starting with '-') + tokens := strings.Fields(trimmed) + for _, token := range tokens { + if strings.HasPrefix(token, "-") { + return fmt.Errorf("%s contains flag %q: %w", filterName, token, ErrBPFFilterContainsFlag) + } + } + + return nil } func createCaptureF(ctx context.Context, kubeClient kubernetes.Interface) (*retinav1alpha1.Capture, error) { + // Validate filters early to provide immediate feedback + if err := validateBPFFilter(opts.tcpdumpFilter, "--tcpdump-filter"); err != nil { + return nil, err + } + if err := validateBPFFilter(opts.pcapFilter, "--pcap-filter"); err != nil { + return nil, err + } + + timestamp := file.Now() + capture := &retinav1alpha1.Capture{ ObjectMeta: metav1.ObjectMeta{ Name: *opts.Name, @@ -247,48 +411,55 @@ func createCaptureF(ctx context.Context, kubeClient kubernetes.Interface) (*reti }, Spec: retinav1alpha1.CaptureSpec{ CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ - TcpdumpFilter: &tcpdumpFilter, + TcpdumpFilter: &opts.tcpdumpFilter, CaptureTarget: retinav1alpha1.CaptureTarget{}, - IncludeMetadata: includeMetadata, + IncludeMetadata: opts.includeMetadata, CaptureOption: retinav1alpha1.CaptureOption{}, }, }, + Status: retinav1alpha1.CaptureStatus{ + StartTime: timestamp, + }, } - if duration != 0 { - retinacmd.Logger.Info(fmt.Sprintf("The capture duration is set to %s", duration)) - capture.Spec.CaptureConfiguration.CaptureOption.Duration = &metav1.Duration{Duration: duration} + retinacmd.Logger.Info(fmt.Sprintf("Capture timestamp: %s", timestamp)) + + if opts.duration != 0 { + retinacmd.Logger.Info(fmt.Sprintf("The capture duration is set to %s", opts.duration)) + capture.Spec.CaptureConfiguration.CaptureOption.Duration = &metav1.Duration{Duration: opts.duration} } - if namespaceSelectors != "" || podSelectors != "" { - // if node selector is using the default value (aka hasn't been set by user), set it to nil to prevent clash with namespace and pod selector - if nodeSelectors == DefaultNodeSelectors { - retinacmd.Logger.Info("Overriding default node selectors value and setting it to nil. Using namespace and pod selectors. To use node selector, please remove namespace and pod selectors.") - nodeSelectors = "" + if opts.namespaceSelectors != "" || opts.podSelectors != "" || opts.podNames != "" || opts.nodeNames != "" { + // if node selector is using the default value (aka hasn't been set by user), set it to nil to prevent clash + // with namespace/pod selectors, pod names, or explicit node names + if opts.nodeSelectors == DefaultNodeSelectors { + retinacmd.Logger.Info("Overriding default node selectors value and setting it to nil. Using namespace, pod selectors, pod names, or node names. " + + "To use node selector, please remove namespace and pod selectors.") + opts.nodeSelectors = "" } } - nodeSelectorLabelsMap, err := labels.ConvertSelectorToLabelsMap(nodeSelectors) + nodeSelectorLabelsMap, err := labels.ConvertSelectorToLabelsMap(opts.nodeSelectors) if err != nil { return nil, err } - podSelectorLabelsMap, err := labels.ConvertSelectorToLabelsMap(podSelectors) + podSelectorLabelsMap, err := labels.ConvertSelectorToLabelsMap(opts.podSelectors) if err != nil { return nil, err } - namespaceSelectorLabelsMap, err := labels.ConvertSelectorToLabelsMap(namespaceSelectors) + namespaceSelectorLabelsMap, err := labels.ConvertSelectorToLabelsMap(opts.namespaceSelectors) if err != nil { return nil, err } - if len(nodeSelectorLabelsMap) != 0 || len(nodeNames) != 0 { + if len(nodeSelectorLabelsMap) != 0 || opts.nodeNames != "" { capture.Spec.CaptureConfiguration.CaptureTarget.NodeSelector = &metav1.LabelSelector{} } if len(nodeSelectorLabelsMap) != 0 { capture.Spec.CaptureConfiguration.CaptureTarget.NodeSelector.MatchLabels = nodeSelectorLabelsMap } - if len(nodeNames) != 0 { - nodeNameSlice := strings.Split(nodeNames, ",") + if opts.nodeNames != "" { + nodeNameSlice := strings.Split(opts.nodeNames, ",") if len(nodeNameSlice) != 0 { capture.Spec.CaptureConfiguration.CaptureTarget.NodeSelector.MatchExpressions = []metav1.LabelSelectorRequirement{{ Key: corev1.LabelHostname, @@ -298,81 +469,165 @@ func createCaptureF(ctx context.Context, kubeClient kubernetes.Interface) (*reti } } + // Add namespace selectors if provided, regardless of other selectors if len(namespaceSelectorLabelsMap) != 0 { capture.Spec.CaptureConfiguration.CaptureTarget.NamespaceSelector = &metav1.LabelSelector{ MatchLabels: namespaceSelectorLabelsMap, } } + + // Add pod selectors if provided if len(podSelectorLabelsMap) != 0 { capture.Spec.CaptureConfiguration.CaptureTarget.PodSelector = &metav1.LabelSelector{ MatchLabels: podSelectorLabelsMap, } } - if maxSize != 0 { - retinacmd.Logger.Info(fmt.Sprintf("The capture file max size is set to %dMB", maxSize)) - capture.Spec.CaptureConfiguration.CaptureOption.MaxCaptureSize = &maxSize + // Add pod names if provided + if opts.podNames != "" { + podNameSlice := strings.Split(opts.podNames, ",") + for i := range podNameSlice { + podNameSlice[i] = strings.TrimSpace(podNameSlice[i]) + } + retinacmd.Logger.Info(fmt.Sprintf("Capturing on specific pods: %v", podNameSlice)) + capture.Spec.CaptureConfiguration.CaptureTarget.PodNames = podNameSlice + } + + if opts.maxSize != 0 { + retinacmd.Logger.Info(fmt.Sprintf("The capture file max size is set to %dMB", opts.maxSize)) + capture.Spec.CaptureConfiguration.CaptureOption.MaxCaptureSize = &opts.maxSize + } + + if opts.packetSize != 0 { + retinacmd.Logger.Info(fmt.Sprintf("The capture packet size is set to %d bytes", opts.packetSize)) + capture.Spec.CaptureConfiguration.CaptureOption.PacketSize = &opts.packetSize + } + + if opts.interfaces != "" { + interfaceSlice := strings.Split(opts.interfaces, ",") + for i := range interfaceSlice { + interfaceSlice[i] = strings.TrimSpace(interfaceSlice[i]) + } + retinacmd.Logger.Info(fmt.Sprintf("Capturing on specific interfaces: %v", interfaceSlice)) + capture.Spec.CaptureConfiguration.CaptureOption.Interfaces = interfaceSlice + } + + // Set pcap-filter if provided + if opts.pcapFilter != "" { + capture.Spec.CaptureConfiguration.CaptureOption.PcapFilter = &opts.pcapFilter } - if packetSize != 0 { - retinacmd.Logger.Info(fmt.Sprintf("The capture packet size is set to %d bytes", packetSize)) - capture.Spec.CaptureConfiguration.CaptureOption.PacketSize = &packetSize + // Set boolean capture and display flags + if opts.noPromiscuous { + capture.Spec.CaptureConfiguration.CaptureOption.NoPromiscuous = &opts.noPromiscuous } - if len(hostPath) != 0 { - capture.Spec.OutputConfiguration.HostPath = &hostPath + if opts.packetBuffered { + capture.Spec.CaptureConfiguration.CaptureOption.PacketBuffered = &opts.packetBuffered } - if len(pvc) != 0 { - capture.Spec.OutputConfiguration.PersistentVolumeClaim = &pvc + + if opts.immediateMode { + capture.Spec.CaptureConfiguration.CaptureOption.ImmediateMode = &opts.immediateMode } - if len(blobUpload) != 0 { + if opts.noResolveDNS { + capture.Spec.CaptureConfiguration.CaptureOption.NoResolveDNS = &opts.noResolveDNS + } + if opts.noResolvePort { + capture.Spec.CaptureConfiguration.CaptureOption.NoResolvePort = &opts.noResolvePort + } + + // Set verbosity level enum field based on CLI value + if opts.verbosityLevel != VerbosityNormal { + verbosityStr := string(opts.verbosityLevel) + capture.Spec.CaptureConfiguration.CaptureOption.Verbosity = &verbosityStr + } + + // Set print data format enum field based on CLI value + if opts.printDataFormat != PrintDataNone { + printDataStr := string(opts.printDataFormat) + capture.Spec.CaptureConfiguration.CaptureOption.PrintDataFormat = &printDataStr + } + + if opts.printLinkHeader { + capture.Spec.CaptureConfiguration.CaptureOption.PrintLinkHeader = &opts.printLinkHeader + } + + if opts.quietOutput { + capture.Spec.CaptureConfiguration.CaptureOption.QuietOutput = &opts.quietOutput + } + + if opts.absoluteSeq { + capture.Spec.CaptureConfiguration.CaptureOption.AbsoluteSeq = &opts.absoluteSeq + } + + // Set timestamp format enum field based on CLI value + if opts.timestampFormat != TimestampDefault { + timestampStr := string(opts.timestampFormat) + capture.Spec.CaptureConfiguration.CaptureOption.TimestampFormat = ×tampStr + } + + if opts.dontVerifyChecksum { + capture.Spec.CaptureConfiguration.CaptureOption.DontVerifyChecksum = &opts.dontVerifyChecksum + } + + if opts.hostPath != "" { + capture.Spec.OutputConfiguration.HostPath = &opts.hostPath + } + if opts.pvc != "" { + capture.Spec.OutputConfiguration.PersistentVolumeClaim = &opts.pvc + } + + if opts.blobUpload != "" { // Mount blob url as secret onto the capture pod for security concern if blob url is not empty. - secretName, err := createSecretFromBlobUpload(ctx, kubeClient, blobUpload, *opts.Name) + secretName, err := createSecretFromBlobUpload(ctx, kubeClient, opts.blobUpload, *opts.Name) if err != nil { return nil, err } capture.Spec.OutputConfiguration.BlobUpload = &secretName } - if s3Bucket != "" { - secretName, err := createSecretFromS3Upload(ctx, kubeClient, s3AccessKeyID, s3SecretAccessKey, *opts.Name) + if opts.s3Bucket != "" { + secretName, err := createSecretFromS3Upload(ctx, kubeClient, opts.s3AccessKeyID, opts.s3SecretAccessKey, *opts.Name) if err != nil { return nil, fmt.Errorf("failed to create s3 upload secret: %w", err) } capture.Spec.OutputConfiguration.S3Upload = &retinav1alpha1.S3Upload{ - Endpoint: s3Endpoint, - Bucket: s3Bucket, + Endpoint: opts.s3Endpoint, + Bucket: opts.s3Bucket, SecretName: secretName, - Region: s3Region, - Path: s3Path, + Region: opts.s3Region, + Path: opts.s3Path, } } - if len(excludeFilter) != 0 { + if opts.excludeFilter != "" { if capture.Spec.CaptureConfiguration.Filters == nil { capture.Spec.CaptureConfiguration.Filters = &retinav1alpha1.CaptureConfigurationFilters{} } - excludeFilterSlice := strings.Split(excludeFilter, ",") + excludeFilterSlice := strings.Split(opts.excludeFilter, ",") capture.Spec.CaptureConfiguration.Filters.Exclude = excludeFilterSlice } - if len(includeFilter) != 0 { + if opts.includeFilter != "" { if capture.Spec.CaptureConfiguration.Filters == nil { capture.Spec.CaptureConfiguration.Filters = &retinav1alpha1.CaptureConfigurationFilters{} } - includeFilterSlice := strings.Split(includeFilter, ",") + includeFilterSlice := strings.Split(opts.includeFilter, ",") capture.Spec.CaptureConfiguration.Filters.Include = includeFilterSlice } + + capture.Spec.CleanUpAfterUpload = opts.cleanUpAfterUpload return capture, nil } func getCLICaptureConfig() config.CaptureConfig { return config.CaptureConfig{ CaptureImageVersion: buildinfo.Version, - CaptureDebug: debug, + CaptureDebug: opts.debug, CaptureImageVersionSource: captureUtils.VersionSourceCLIVersion, - CaptureJobNumLimit: jobNumLimit, + CaptureJobNumLimit: opts.jobNumLimit, + CaptureHostPathBaseDir: opts.hostPathBaseDir, } } @@ -385,6 +640,21 @@ func createJobs(ctx context.Context, kubeClient kubernetes.Interface, capture *r jobsCreated := []batchv1.Job{} for _, job := range jobs { + // In no-wait mode with a remote destination, set TTL so Kubernetes + // auto-deletes completed jobs and their pods. When host-path only, + // the job metadata is needed to locate the capture file. + if opts.nowait && opts.cleanUpAfterUpload && hasRemoteDestination(&opts) { + ttl := JobTTLSecondsAfterFinished + job.Spec.TTLSecondsAfterFinished = &ttl + } + + // Set a hard deadline to prevent jobs from running indefinitely if the + // capture process hangs. Deadline = capture duration + upload buffer. + if opts.duration > 0 { + deadline := int64(opts.duration.Seconds()) + JobActiveDeadlineBufferSeconds + job.Spec.ActiveDeadlineSeconds = &deadline + } + jobCreated, err := kubeClient.BatchV1().Jobs(*opts.Namespace).Create(ctx, job, metav1.CreateOptions{}) if err != nil { return nil, err @@ -399,15 +669,24 @@ func waitUntilJobsComplete(ctx context.Context, kubeClient kubernetes.Interface, allJobsCompleted := false // TODO: let's make the timeout and period to wait for all job to finish configurable. - var deadline time.Duration = DefaultWaitTimeout - if duration != 0 { - deadline = duration * 2 + deadline := DefaultWaitTimeout + if opts.duration != 0 { + // Allow time for capture + upload + scheduling overhead (Windows + // nodes in particular need extra time for image pull and pod start). + deadline = opts.duration + WaitDeadlineBuffer + if deadline < DefaultWaitTimeout { + deadline = DefaultWaitTimeout + } } - var period time.Duration = DefaultWaitPeriod + period := DefaultWaitPeriod // To print less noisy messages, we rely on duration to decide the wait period. - if period < duration/10 { - period = duration / 10 + if period < opts.duration/10 { + period = opts.duration / 10 + } + // Ensure poll period is less than the deadline so we get multiple checks. + if period >= deadline { + period = deadline / MinPollAttempts } retinacmd.Logger.Info(fmt.Sprintf("Waiting timeout is set to %s", deadline)) @@ -465,34 +744,50 @@ func deleteJobs(ctx context.Context, kubeClient kubernetes.Interface, jobs []bat return jobsFailedtoDelete } -func init() { - capture.AddCommand(createCapture) - createCapture.Flags().DurationVar(&duration, "duration", DefaultDuration, "Duration of capturing packets") - createCapture.Flags().IntVar(&maxSize, "max-size", DefaultMaxSize, "Limit the capture file to MB in size which works only for Linux") //nolint:gomnd // default - createCapture.Flags().IntVar(&packetSize, "packet-size", DefaultPacketSize, "Limits the each packet to bytes in size which works only for Linux") - createCapture.Flags().StringVar(&nodeNames, "node-names", "", "A comma-separated list of node names to select nodes on which the network capture will be performed") - createCapture.Flags().StringVar(&nodeSelectors, "node-selectors", DefaultNodeSelectors, "A comma-separated list of node labels to select nodes on which the network capture will be performed") - createCapture.Flags().StringVar(&podSelectors, "pod-selectors", "", - "A comma-separated list of pod labels to select pods on which the network capture will be performed") - createCapture.Flags().StringVar(&namespaceSelectors, "namespace-selectors", "", - "A comma-separated list of namespace labels in which to apply the pod-selectors. By default, the pod namespace is specified by the flag namespace") - createCapture.Flags().StringVar(&hostPath, "host-path", DefaultHostPath, "HostPath of the node to store the capture files") - createCapture.Flags().StringVar(&pvc, "pvc", "", "PersistentVolumeClaim under the specified or default namespace to store capture files") - createCapture.Flags().StringVar(&blobUpload, "blob-upload", "", "Blob SAS URL with write permission to upload capture files") - createCapture.Flags().StringVar(&s3Region, "s3-region", "", "Region where the S3 compatible bucket is located") - createCapture.Flags().StringVar(&s3Endpoint, "s3-endpoint", "", - "Endpoint for an S3 compatible storage service. Use this if you are using a custom or private S3 service that requires a specific endpoint") - createCapture.Flags().StringVar(&s3Bucket, "s3-bucket", "", "Bucket in which to store capture files") - createCapture.Flags().StringVar(&s3Path, "s3-path", DefaultS3Path, "Prefix path within the S3 bucket where captures will be stored") - createCapture.Flags().StringVar(&s3AccessKeyID, "s3-access-key-id", "", "S3 access key id to upload capture files") - createCapture.Flags().StringVar(&s3SecretAccessKey, "s3-secret-access-key", "", "S3 access secret key to upload capture files") - createCapture.Flags().StringVar(&tcpdumpFilter, "tcpdump-filter", "", "Raw tcpdump flags which works only for Linux") - createCapture.Flags().StringVar(&excludeFilter, "exclude-filter", "", "A comma-separated list of IP:Port pairs that are "+ - "excluded from capturing network packets. Supported formats are IP:Port, IP, Port, *:Port, IP:*") - createCapture.Flags().StringVar(&includeFilter, "include-filter", "", "A comma-separated list of IP:Port pairs that are "+ - "used to filter capture network packets. Supported formats are IP:Port, IP, Port, *:Port, IP:*") - createCapture.Flags().BoolVar(&includeMetadata, "include-metadata", DefaultIncludeMetadata, "If true, collect static network metadata into capture file") - createCapture.Flags().IntVar(&jobNumLimit, "job-num-limit", DefaultJobNumLimit, "The maximum number of jobs can be created for each capture. 0 means no limit") - createCapture.Flags().BoolVar(&nowait, "no-wait", DefaultNowait, "Do not wait for the long-running capture job to finish") - createCapture.Flags().BoolVar(&debug, "debug", DefaultDebug, "When debug is true, a customized retina-agent image, determined by the environment variable RETINA_AGENT_IMAGE, is set") +// setSecretOwnerReferences adds ownerReferences from the given jobs to the +// secrets used by the capture (blob or S3). This ensures secrets are garbage- +// collected when the owning jobs are deleted (by TTL or explicitly). +func setSecretOwnerReferences(ctx context.Context, kubeClient kubernetes.Interface, capture *retinav1alpha1.Capture, jobs []batchv1.Job) error { + var secretNames []string + if capture.Spec.OutputConfiguration.BlobUpload != nil { + secretNames = append(secretNames, *capture.Spec.OutputConfiguration.BlobUpload) + } + if capture.Spec.OutputConfiguration.S3Upload != nil && capture.Spec.OutputConfiguration.S3Upload.SecretName != "" { + secretNames = append(secretNames, capture.Spec.OutputConfiguration.S3Upload.SecretName) + } + + if len(secretNames) == 0 || len(jobs) == 0 { + return nil + } + + ownerRefs := make([]metav1.OwnerReference, 0, len(jobs)) + for i := range jobs { + ownerRefs = append(ownerRefs, metav1.OwnerReference{ + APIVersion: batchv1.SchemeGroupVersion.String(), + Kind: "Job", + Name: jobs[i].Name, + UID: jobs[i].UID, + }) + } + + for _, secretName := range secretNames { + secret, err := kubeClient.CoreV1().Secrets(*opts.Namespace).Get(ctx, secretName, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to get secret %s: %w", secretName, err) + } + // Deduplicate owner references to avoid appending the same refs on retries. + existing := make(map[types.UID]struct{}, len(secret.OwnerReferences)) + for _, ref := range secret.OwnerReferences { + existing[ref.UID] = struct{}{} + } + for _, ref := range ownerRefs { + if _, found := existing[ref.UID]; !found { + secret.OwnerReferences = append(secret.OwnerReferences, ref) + } + } + if _, err := kubeClient.CoreV1().Secrets(*opts.Namespace).Update(ctx, secret, metav1.UpdateOptions{}); err != nil { + return fmt.Errorf("failed to update secret %s with owner references: %w", secretName, err) + } + } + return nil } diff --git a/cli/cmd/capture/create_test.go b/cli/cmd/capture/create_test.go new file mode 100644 index 0000000000..dff8bab2b9 --- /dev/null +++ b/cli/cmd/capture/create_test.go @@ -0,0 +1,1058 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package capture + +import ( + "bytes" + "context" + "fmt" + "math/rand" + "strings" + "testing" + + retinav1alpha1 "github.com/microsoft/retina/crd/api/v1alpha1" + "github.com/microsoft/retina/internal/buildinfo" + captureUtils "github.com/microsoft/retina/pkg/capture/utils" + "github.com/microsoft/retina/pkg/label" + "github.com/stretchr/testify/require" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + clienttesting "k8s.io/client-go/testing" +) + +const testNamespace = "test-ns" + +type testcase struct { + name string + inputName string + wantName string + inputNamespace string + wantNamespace string + inputPodSelector string + inputNsSelector string + inputNodeSelector string + wantNodes []string + wantErr bool +} + +func randomString(length int) string { + const charset = "abcdefghijklmnopqrstuvwxyz0123456789" + result := make([]byte, length) + for i := range result { + result[i] = charset[rand.Intn(len(charset))] //nolint:gosec // this random number generator is fine + } + return string(result) +} + +func argsFromTestCase(tc testcase) []string { + args := []string{"create", "--pod-selectors", tc.inputPodSelector} + if tc.inputNsSelector != "" { + args = append(args, "--namespace-selectors="+tc.inputNsSelector) + } + if tc.inputNamespace != "" { + args = append(args, "--namespace", tc.inputNamespace) + } + if tc.inputName != "" { + args = append(args, "--name", tc.inputName) + } + if tc.inputNodeSelector != "" { + args = append(args, "--node-selectors="+tc.inputNodeSelector) + } + return args +} + +func NewNode(name string) *corev1.Node { + return &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{ + "kubernetes.io/hostname": name, + "kubernetes.io/os": "linux", + }, + }, + } +} + +func NewNamespace(name string) *corev1.Namespace { + return &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{ + "name": name, + }, + }, + } +} + +func NewClientServerPods(service, namespace string) []*corev1.Pod { + return []*corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "client" + service, + Namespace: namespace, + Labels: map[string]string{ + "service": service, + }, + }, + Spec: corev1.PodSpec{ + NodeName: service + "1", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "server" + service, + Namespace: namespace, + Labels: map[string]string{ + "service": service, + }, + }, + Spec: corev1.PodSpec{ + NodeName: service + "2", + }, + }, + } +} + +func TestCreateJobsWithNamespace(t *testing.T) { + // Create a fake Kubernetes client with workload and capture namespaces + newKubeclient := func() *fake.Clientset { + objects := []runtime.Object{ + NewNode("A1"), + NewNode("A2"), + NewNode("B1"), + NewNode("B2"), + NewNamespace("workload"), + NewNamespace("capture"), + } + for _, pod := range NewClientServerPods("A", "workload") { + objects = append(objects, pod) + } + for _, pod := range NewClientServerPods("B", "workload") { + objects = append(objects, pod) + } + + kubeClient := fake.NewClientset(objects...) + + // Handle job creation to set job name if not provided, which is done automatically in a real k8s cluster + kubeClient.PrependReactor("create", "jobs", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + createAction, ok := action.(clienttesting.CreateAction) + if !ok { + return false, nil, fmt.Errorf("expected CreateAction, got %T", action) //nolint:err113 // test code + } + job := createAction.GetObject().(*batchv1.Job) + + // Set job name if unset + if job.Name == "" { + job.Name = job.GenerateName + randomString(5) + } + return false, job, nil + }) + + return kubeClient + } + + // Setup test cases + testCases := []testcase{ + { + name: "create --name=test --podSelector=service=A --namespace-selectors=name=workload", + inputName: "test", + wantName: "test", + inputNamespace: "", + wantNamespace: "default", + inputPodSelector: "service=A", + inputNsSelector: "name=workload", + wantNodes: []string{"A1", "A2"}, + wantErr: false, + }, + { + name: "create --namespace=workload --podSelector=service=A --namespace-selectors=name=workload", + inputName: "", + wantName: DefaultName, + inputNamespace: "workload", + wantNamespace: "workload", + inputPodSelector: "service=A", + inputNsSelector: "name=workload", + wantNodes: []string{"A1", "A2"}, + wantErr: false, + }, + { + name: "create --namespace=workload --podSelector=service=A", + inputName: "", + wantName: DefaultName, + inputNamespace: "workload", + wantNamespace: "workload", + inputPodSelector: "service=A", + inputNsSelector: "", + wantNodes: []string{}, + wantErr: true, + }, + { + name: "create --namespace=workload --namespace-selectors=name=workload", + inputName: "", + wantName: DefaultName, + inputNamespace: "workload", + wantNamespace: "workload", + inputPodSelector: "", + inputNsSelector: "name=workload", + wantNodes: []string{}, + wantErr: true, + }, + { + name: "create --namespace=workload --podSelector=service=B --namespace-selectors=name=workload", + inputName: "", + wantName: DefaultName, + inputNamespace: "workload", + wantNamespace: "workload", + inputPodSelector: "service=B", + inputNsSelector: "name=workload", + wantNodes: []string{"B1", "B2"}, + wantErr: false, + }, + { + name: "create --namespace=workload --node-selectors=kubernetes.io/hostname=B1", + inputName: "", + wantName: DefaultName, + inputNamespace: "workload", + wantNamespace: "workload", + inputNodeSelector: "kubernetes.io/hostname=B1", + wantNodes: []string{"B1"}, + wantErr: false, + }, + { + name: "create --namespace=workload --podSelector=service=B --namespace-selectors=name=workload --node-selectors=kubernetes.io/hostname=B1", + inputName: "", + wantName: DefaultName, + inputNamespace: "workload", + wantNamespace: "workload", + inputPodSelector: "service=B", + inputNsSelector: "name=workload", + inputNodeSelector: "kubernetes.io/hostname=B1", + wantNodes: []string{"B1"}, + wantErr: true, + }, + } + for _, tc := range testCases { + fmt.Println("\n### Running test case:", tc.name) + // Given + kubeClient := newKubeclient() + cmd := NewCommand(kubeClient) + cmd.SetArgs(argsFromTestCase(tc)) + buf := new(bytes.Buffer) + cmd.SetOut(buf) + + t.Run(tc.name, func(t *testing.T) { + // When + err := cmd.Execute() + + // Then + + // Check the command execution + AssertError(t, err, tc) + + // Validate that jobs are created correctly + JobsCreatedCorrectly(t, kubeClient, tc) + }) + } +} + +// AssertError checks if the error matches the expected outcome based on the testcase +func AssertError(t *testing.T, err error, tc testcase) { + t.Helper() + if tc.wantErr { + if err == nil { + t.Fatalf("Expected error for test case %s, but got none", tc.name) + } + t.Skip("Successfully got expected error") + } + if err != nil { + t.Fatalf("Failed to execute command: %v", err) + } +} + +// JobsCreatedCorrectly validates that Kubernetes jobs were created correctly based on the namespace and pod selector flags +// provided to the command. It verifies that jobs are created in the right namespace and with the correct node affinity. +func JobsCreatedCorrectly(t *testing.T, kubeClient *fake.Clientset, tc testcase) { + t.Helper() + // Get created jobs + jobs, err := kubeClient.BatchV1().Jobs(tc.wantNamespace).List(context.TODO(), metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s", label.CaptureNameLabel, tc.wantName), + }) + // Execution should not return an error + if err != nil { + t.Fatalf("Failed to list jobs in namespace %s: %v", tc.wantNamespace, err) + } + + // Jobs should not be nil or empty + if jobs == nil || len(jobs.Items) == 0 { + t.Fatalf("No jobs found for capture %s in namespace %s", tc.wantName, tc.wantNamespace) + } + + // Number of jobs should match expected number of nodes + if len(jobs.Items) != len(tc.wantNodes) { + t.Fatalf("Expected %d jobs, but found %d", len(tc.wantNodes), len(jobs.Items)) + } + + // Create a map of expected nodes for easier comparison + expectedNodes := make(map[string]bool) + for _, node := range tc.wantNodes { + expectedNodes[node] = true + } + matchCount := 0 + + // Validate node affinity for each job + for idx := range jobs.Items { + job := jobs.Items[idx] + + // Validate node affinity based on pod selector and namespace selector + if len(tc.wantNodes) > 0 { + if job.Spec.Template.Spec.Affinity == nil || job.Spec.Template.Spec.Affinity.NodeAffinity == nil { + t.Fatalf("Expected job to have node affinity, but none found") + } + + nodeAffinity := job.Spec.Template.Spec.Affinity.NodeAffinity + if nodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution == nil { + t.Fatalf("Expected job to have required node affinity, but none found") + } + + // Look for hostname match expression + for _, nodeSelectorTerm := range nodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms { + for _, expression := range nodeSelectorTerm.MatchExpressions { + if expression.Key == "kubernetes.io/hostname" { + // Check if all values are in expected nodes and count matches + for _, value := range expression.Values { + if expectedNodes[value] { + matchCount++ + } else { + t.Errorf("Unexpected node %s in job %s, expected nodes: %v", value, job.Name, tc.wantNodes) + } + } + } + } + } + } + } + + // Check if all expected nodes are present + if matchCount != len(tc.wantNodes) { + t.Errorf("Job's node affinity doesn't match expected nodes. Expected: %v", tc.wantNodes) + } +} + +// Pod Names Tests - Tests for CLI pod name selection functionality + +func TestCreateCaptureCommand_PodNamesClearsDefaultNodeSelector(t *testing.T) { + // When --pod-names is set together with the default --node-selectors, + // the default selector must be cleared so pod names take precedence. + savedNodeSelectors := opts.nodeSelectors + savedPodNames := opts.podNames + savedNamespace := opts.Namespace + savedName := opts.Name + t.Cleanup(func() { + opts.nodeSelectors = savedNodeSelectors + opts.podNames = savedPodNames + opts.Namespace = savedNamespace + opts.Name = savedName + }) + + name := "test-capture" + namespace := "default" + + opts.nodeSelectors = DefaultNodeSelectors + opts.podNames = "nonexistent-pod" + opts.Namespace = &namespace + opts.Name = &name + + capture, err := createCaptureF(context.Background(), fake.NewClientset()) + require.NoError(t, err) + + require.Equal(t, []string{"nonexistent-pod"}, capture.Spec.CaptureConfiguration.CaptureTarget.PodNames) + require.Nil(t, capture.Spec.CaptureConfiguration.CaptureTarget.NodeSelector) +} + +func TestCreateCaptureWithPodNames_CRDStructure(t *testing.T) { + // Table-driven test for pod names CRD structure generation + cases := []struct { + name string + podNames []string + namespace string + wantSelector bool + }{ + { + name: "single pod name", + podNames: []string{"test-pod"}, + namespace: "default", + wantSelector: true, + }, + { + name: "multiple pod names", + podNames: []string{"pod1", "pod2", "pod3"}, + namespace: "myapp", + wantSelector: true, + }, + { + name: "no pod names", + podNames: nil, + namespace: "default", + wantSelector: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + capture := &retinav1alpha1.Capture{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-capture", + Namespace: tc.namespace, + }, + Spec: retinav1alpha1.CaptureSpec{ + CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ + CaptureTarget: retinav1alpha1.CaptureTarget{ + PodNames: tc.podNames, + }, + }, + }, + } + + require.NotNil(t, capture) + require.Equal(t, tc.namespace, capture.Namespace) + + if tc.wantSelector { + require.NotNil(t, capture.Spec.CaptureConfiguration.CaptureTarget.PodNames) + require.Equal(t, tc.podNames, capture.Spec.CaptureConfiguration.CaptureTarget.PodNames) + require.Nil(t, capture.Spec.CaptureConfiguration.CaptureTarget.NodeSelector) + } else { + require.Nil(t, capture.Spec.CaptureConfiguration.CaptureTarget.PodNames) + } + }) + } +} + +func TestCaptureTarget_PodNames_MutualExclusivity(t *testing.T) { + // Comprehensive test for mutual exclusivity constraints between pod names and other selectors + cases := []struct { + name string + target retinav1alpha1.CaptureTarget + isValid bool + }{ + { + name: "pod names only", + target: retinav1alpha1.CaptureTarget{ + PodNames: []string{"pod1", "pod2"}, + }, + isValid: true, + }, + { + name: "pod names with node selector", + target: retinav1alpha1.CaptureTarget{ + PodNames: []string{"pod1"}, + NodeSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"kubernetes.io/os": "linux"}, + }, + }, + isValid: false, + }, + { + name: "pod names with pod selector", + target: retinav1alpha1.CaptureTarget{ + PodNames: []string{"pod1"}, + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "test"}, + }, + }, + isValid: false, + }, + { + name: "pod names with namespace selector", + target: retinav1alpha1.CaptureTarget{ + PodNames: []string{"pod1"}, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"name": "default"}, + }, + }, + isValid: false, + }, + { + name: "node selector only", + target: retinav1alpha1.CaptureTarget{ + NodeSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"kubernetes.io/os": "linux"}, + }, + }, + isValid: true, + }, + { + name: "pod and namespace selectors", + target: retinav1alpha1.CaptureTarget{ + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "test"}, + }, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"name": "default"}, + }, + }, + isValid: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + capture := &retinav1alpha1.Capture{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-capture", + Namespace: "default", + }, + Spec: retinav1alpha1.CaptureSpec{ + CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ + CaptureTarget: tc.target, + }, + }, + } + + require.NotNil(t, capture) + if tc.isValid { + t.Logf("✓ Valid selector combination: %s", tc.name) + } else { + t.Logf("✓ Invalid selector combination (will be caught by validation): %s", tc.name) + } + }) + } +} + +func TestNodeNamesClearsDefaultNodeSelector(t *testing.T) { + // When --node-names is specified, the default kubernetes.io/os=linux node-selector + // must be cleared so that Windows nodes can be targeted by name. + winNode := NewWindowsNode("win-node-1") + linNode := NewNode("lin-node-1") + + kubeClient := fake.NewClientset(winNode, linNode) + kubeClient.PrependReactor("create", "jobs", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + createAction, ok := action.(clienttesting.CreateAction) + if !ok { + return false, nil, fmt.Errorf("expected CreateAction, got %T", action) //nolint:err113 // test code + } + job := createAction.GetObject().(*batchv1.Job) + if job.Name == "" { + job.Name = job.GenerateName + randomString(5) + } + return false, job, nil + }) + + cases := []struct { + name string + args []string + wantNodes []string + wantErr bool + }{ + { + name: "node-names targets a Windows node without explicit node-selectors", + args: []string{ + "create", + "--name=test-win", + "--namespace=default", + "--node-names=win-node-1", + "--duration=10s", + "--host-path=capture", + }, + wantNodes: []string{"win-node-1"}, + wantErr: false, + }, + { + name: "node-names targets a Linux node without explicit node-selectors", + args: []string{ + "create", + "--name=test-lin", + "--namespace=default", + "--node-names=lin-node-1", + "--duration=10s", + "--host-path=capture", + }, + wantNodes: []string{"lin-node-1"}, + wantErr: false, + }, + { + name: "node-names targets both Linux and Windows nodes", + args: []string{ + "create", + "--name=test-both", + "--namespace=default", + "--node-names=lin-node-1,win-node-1", + "--duration=10s", + "--host-path=capture", + }, + wantNodes: []string{"lin-node-1", "win-node-1"}, + wantErr: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + cmd := NewCommand(kubeClient) + cmd.SetArgs(tc.args) + buf := new(bytes.Buffer) + cmd.SetOut(buf) + + err := cmd.Execute() + + if tc.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err, "capture create should succeed for node-names targeting %v", tc.wantNodes) + + // Verify jobs were created for the expected nodes + jobs, err := kubeClient.BatchV1().Jobs("default").List(context.TODO(), metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s", label.CaptureNameLabel, strings.TrimPrefix(tc.args[1], "--name=")), + }) + require.NoError(t, err) + require.Len(t, jobs.Items, len(tc.wantNodes), "should create one job per target node") + + gotNodes := map[string]bool{} + for _, job := range jobs.Items { + nodeAffinity := job.Spec.Template.Spec.Affinity.NodeAffinity + require.NotNil(t, nodeAffinity) + for _, term := range nodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms { + for _, expr := range term.MatchExpressions { + if expr.Key == "kubernetes.io/hostname" { + for _, v := range expr.Values { + gotNodes[v] = true + } + } + } + } + } + + for _, wantNode := range tc.wantNodes { + require.True(t, gotNodes[wantNode], "expected job targeting node %s", wantNode) + } + }) + } +} + +func TestGetCLICaptureConfig(t *testing.T) { + savedDebug, savedJobNumLimit, savedHostPathBaseDir := opts.debug, opts.jobNumLimit, opts.hostPathBaseDir + t.Cleanup(func() { + opts.debug = savedDebug + opts.jobNumLimit = savedJobNumLimit + opts.hostPathBaseDir = savedHostPathBaseDir + }) + + opts.debug = true + opts.jobNumLimit = 7 + opts.hostPathBaseDir = "/mnt/captures" + + got := getCLICaptureConfig() + + require.Equal(t, buildinfo.Version, got.CaptureImageVersion) + require.Equal(t, captureUtils.VersionSourceCLIVersion, got.CaptureImageVersionSource) + require.True(t, got.CaptureDebug) + require.Equal(t, 7, got.CaptureJobNumLimit) + require.Equal(t, "/mnt/captures", got.CaptureHostPathBaseDir) +} + +func TestCreateCaptureCommand_AbsoluteHostPath_ShouldFail(t *testing.T) { + // --host-path must be a bare subpath; absolute paths are rejected by the + // shared validateHostPath helper used by both the operator and the CLI. + cmd := NewCommand(fake.NewClientset()) + + cmd.SetArgs([]string{ + "create", + "--name=hp-absolute", + "--namespace=default", + "--node-selectors=kubernetes.io/os=linux", + "--duration=10s", + "--host-path=/tmp/foo", + }) + + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetErr(buf) + + err := cmd.Execute() + require.Error(t, err, "command should fail when --host-path is absolute") + require.Contains(t, err.Error(), "OutputConfiguration.HostPath", + "error should reference the rejected HostPath field; got: %v", err) +} +func TestHasRemoteDestination(t *testing.T) { + tests := []struct { + name string + opts Opts + want bool + }{ + { + name: "blob upload is remote", + opts: Opts{blobUpload: "https://account.blob.core.windows.net/container"}, + want: true, + }, + { + name: "s3 bucket is remote", + opts: Opts{s3Bucket: "my-bucket"}, + want: true, + }, + { + name: "pvc is remote", + opts: Opts{pvc: "my-pvc"}, + want: true, + }, + { + name: "host-path only is not remote", + opts: Opts{hostPath: "/mnt/captures"}, + want: false, + }, + { + name: "empty opts is not remote", + opts: Opts{}, + want: false, + }, + { + name: "multiple remote destinations", + opts: Opts{blobUpload: "https://x.blob.core.windows.net/c", s3Bucket: "bucket"}, + want: true, + }, + } + + for i := range tests { + t.Run(tests[i].name, func(t *testing.T) { + require.Equal(t, tests[i].want, hasRemoteDestination(&tests[i].opts)) + }) + } +} + +func TestSetSecretOwnerReferences(t *testing.T) { + ns := testNamespace + secretName := "blob-secret-abc" + + kubeClient := fake.NewClientset( + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: ns, + }, + }, + ) + + origNs := opts.Namespace + opts.Namespace = &ns + defer func() { opts.Namespace = origNs }() + + blobUpload := secretName + capture := &retinav1alpha1.Capture{ + Spec: retinav1alpha1.CaptureSpec{ + OutputConfiguration: retinav1alpha1.OutputConfiguration{ + BlobUpload: &blobUpload, + }, + }, + } + + jobs := []batchv1.Job{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "job-1", + Namespace: ns, + UID: "uid-1", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "job-2", + Namespace: ns, + UID: "uid-2", + }, + }, + } + + err := setSecretOwnerReferences(context.Background(), kubeClient, capture, jobs) + require.NoError(t, err) + + secret, err := kubeClient.CoreV1().Secrets(ns).Get(context.Background(), secretName, metav1.GetOptions{}) + require.NoError(t, err) + require.Len(t, secret.OwnerReferences, 2, "secret should have owner references for both jobs") + require.Equal(t, "job-1", secret.OwnerReferences[0].Name) + require.Equal(t, "job-2", secret.OwnerReferences[1].Name) + require.Equal(t, "batch/v1", secret.OwnerReferences[0].APIVersion) + require.Equal(t, "Job", secret.OwnerReferences[0].Kind) +} + +func TestSetSecretOwnerReferences_Idempotent(t *testing.T) { + ns := testNamespace + secretName := "blob-secret-idem" + + kubeClient := fake.NewClientset( + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: ns, + }, + }, + ) + + origNs := opts.Namespace + opts.Namespace = &ns + defer func() { opts.Namespace = origNs }() + + blobUpload := secretName + capture := &retinav1alpha1.Capture{ + Spec: retinav1alpha1.CaptureSpec{ + OutputConfiguration: retinav1alpha1.OutputConfiguration{ + BlobUpload: &blobUpload, + }, + }, + } + + jobs := []batchv1.Job{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "job-1", + Namespace: ns, + UID: "uid-1", + }, + }, + } + + // Call twice to simulate retry/re-reconcile. + err := setSecretOwnerReferences(context.Background(), kubeClient, capture, jobs) + require.NoError(t, err) + err = setSecretOwnerReferences(context.Background(), kubeClient, capture, jobs) + require.NoError(t, err) + + secret, err := kubeClient.CoreV1().Secrets(ns).Get(context.Background(), secretName, metav1.GetOptions{}) + require.NoError(t, err) + require.Len(t, secret.OwnerReferences, 1, "duplicate owner references should not be added") +} + +func TestSetSecretOwnerReferences_NoSecrets(t *testing.T) { + kubeClient := fake.NewClientset() + + capture := &retinav1alpha1.Capture{ + Spec: retinav1alpha1.CaptureSpec{ + OutputConfiguration: retinav1alpha1.OutputConfiguration{}, + }, + } + + err := setSecretOwnerReferences(context.Background(), kubeClient, capture, []batchv1.Job{}) + require.NoError(t, err) +} + +func TestSetSecretOwnerReferences_S3Secret(t *testing.T) { + ns := testNamespace + secretName := "s3-secret-xyz" + + kubeClient := fake.NewClientset( + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: ns, + }, + }, + ) + + origNs := opts.Namespace + opts.Namespace = &ns + defer func() { opts.Namespace = origNs }() + + capture := &retinav1alpha1.Capture{ + Spec: retinav1alpha1.CaptureSpec{ + OutputConfiguration: retinav1alpha1.OutputConfiguration{ + S3Upload: &retinav1alpha1.S3Upload{ + SecretName: secretName, + Bucket: "my-bucket", + }, + }, + }, + } + + jobs := []batchv1.Job{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "job-a", + Namespace: ns, + UID: "uid-a", + }, + }, + } + + err := setSecretOwnerReferences(context.Background(), kubeClient, capture, jobs) + require.NoError(t, err) + + secret, err := kubeClient.CoreV1().Secrets(ns).Get(context.Background(), secretName, metav1.GetOptions{}) + require.NoError(t, err) + require.Len(t, secret.OwnerReferences, 1, "secret should have owner reference for the job") + require.Equal(t, "job-a", secret.OwnerReferences[0].Name) + require.Equal(t, "batch/v1", secret.OwnerReferences[0].APIVersion) + require.Equal(t, "Job", secret.OwnerReferences[0].Kind) +} + +func TestDeleteSecret_NotFound(t *testing.T) { + // deleteSecret should return nil when the secret doesn't exist + ns := testNamespace + kubeClient := fake.NewClientset() // no secrets pre-created + + origNs := opts.Namespace + opts.Namespace = &ns + defer func() { opts.Namespace = origNs }() + + name := "nonexistent-secret" + err := deleteSecret(context.Background(), kubeClient, &name) + require.NoError(t, err, "deleteSecret should not error on NotFound") +} + +func TestDeleteSecret_NilName(t *testing.T) { + ns := testNamespace + kubeClient := fake.NewClientset() + + origNs := opts.Namespace + opts.Namespace = &ns + defer func() { opts.Namespace = origNs }() + + err := deleteSecret(context.Background(), kubeClient, nil) + require.NoError(t, err, "deleteSecret should return nil for nil secretName") +} + +func TestDeleteSecret_ExistingSecret(t *testing.T) { + // deleteSecret should succeed when the secret exists + ns := testNamespace + secretName := "my-secret" + kubeClient := fake.NewClientset( + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: ns, + }, + }, + ) + + origNs := opts.Namespace + opts.Namespace = &ns + defer func() { opts.Namespace = origNs }() + + err := deleteSecret(context.Background(), kubeClient, &secretName) + require.NoError(t, err) + + // Verify secret is deleted + _, err = kubeClient.CoreV1().Secrets(ns).Get(context.Background(), secretName, metav1.GetOptions{}) + require.Error(t, err, "secret should be gone after deleteSecret") +} + +func TestCreateJobs_ActiveDeadlineSeconds(t *testing.T) { + // When duration > 0, jobs should have ActiveDeadlineSeconds set + kubeClient := newFakeClientForCleanupTests() + + var createdJob *batchv1.Job + kubeClient.PrependReactor("create", "jobs", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + createAction := action.(clienttesting.CreateAction) + job := createAction.GetObject().(*batchv1.Job) + if job.Name == "" { + job.Name = job.GenerateName + "test" + } + now := metav1.Now() + job.Status.CompletionTime = &now + createdJob = job + return false, job, nil + }) + + cmd := NewCommand(kubeClient) + cmd.SetArgs([]string{ + "create", + "--name=test-deadline", + "--namespace=default", + "--node-names=node1", + "--host-path=captures", + "--no-wait=true", + "--duration=60s", + }) + + err := cmd.Execute() + require.NoError(t, err) + require.NotNil(t, createdJob) + require.NotNil(t, createdJob.Spec.ActiveDeadlineSeconds, + "ActiveDeadlineSeconds should be set when duration > 0") + // 60s + 1800s buffer = 1860 + require.Equal(t, int64(1860), *createdJob.Spec.ActiveDeadlineSeconds) +} + +func TestCreateJobs_NoTTLWithoutCleanupFlag(t *testing.T) { + // When --cleanup-after-upload is NOT set, TTL should NOT be set + // even with remote + no-wait. + kubeClient := newFakeClientForCleanupTests() + + var createdJob *batchv1.Job + kubeClient.PrependReactor("create", "jobs", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + createAction := action.(clienttesting.CreateAction) + job := createAction.GetObject().(*batchv1.Job) + if job.Name == "" { + job.Name = job.GenerateName + "test" + } + now := metav1.Now() + job.Status.CompletionTime = &now + createdJob = job + return false, job, nil + }) + + cmd := NewCommand(kubeClient) + cmd.SetArgs([]string{ + "create", + "--name=test-no-cleanup", + "--namespace=default", + "--node-names=node1", + "--blob-upload=https://testaccount.blob.core.windows.net/container?sv=2021-06-08", + "--no-wait=true", + "--duration=5s", + // NOTE: --cleanup-after-upload is NOT set + }) + + err := cmd.Execute() + require.NoError(t, err) + require.NotNil(t, createdJob) + require.Nil(t, createdJob.Spec.TTLSecondsAfterFinished, + "TTL should NOT be set without --cleanup-after-upload") +} + +func TestWaitUntilJobsComplete_ShortDuration(t *testing.T) { + // Verifies that waitUntilJobsComplete uses a deadline of at least + // DefaultWaitTimeout (5min), and completes quickly when jobs are already done. + // This exercises the deadline calculation (duration + 5min, floored at DefaultWaitTimeout) + // and the period clamping logic. + kubeClient := newFakeClientForCleanupTests() + + // The fake client's reactor already marks jobs as completed on create. + // For the "get" call inside waitUntilJobsComplete, we need the job to appear completed. + kubeClient.PrependReactor("get", "jobs", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + getAction := action.(clienttesting.GetAction) + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: getAction.GetName(), + Namespace: getAction.GetNamespace(), + }, + Status: batchv1.JobStatus{ + CompletionTime: &metav1.Time{}, + Conditions: []batchv1.JobCondition{ + { + Type: batchv1.JobComplete, + Status: corev1.ConditionTrue, + }, + }, + }, + } + return true, job, nil + }) + + cmd := NewCommand(kubeClient) + cmd.SetArgs([]string{ + "create", + "--name=test-wait-short", + "--namespace=default", + "--node-names=node1", + "--host-path=captures", + "--no-wait=false", + "--duration=2s", + }) + + // This should complete quickly (within seconds) because the fake client + // reports jobs as already completed. If the deadline/period logic is broken, + // this would hang until the test timeout. + err := cmd.Execute() + require.NoError(t, err) +} diff --git a/cli/cmd/capture/delete.go b/cli/cmd/capture/delete.go index 7cc6cb8a4c..4e90147a54 100644 --- a/cli/cmd/capture/delete.go +++ b/cli/cmd/capture/delete.go @@ -15,6 +15,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" "go.uber.org/zap" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/kubernetes" @@ -27,66 +28,68 @@ var deleteExample = templates.Examples(i18n.T(` kubectl retina capture delete --name retina-capture-8v6wd --namespace capture `)) -var deleteCapture = &cobra.Command{ - Use: "delete", - Short: "Delete a Retina capture", - Example: deleteExample, - RunE: func(*cobra.Command, []string) error { - kubeConfig, err := opts.ToRESTConfig() - if err != nil { - return errors.Wrap(err, "") - } +func NewDeleteSubCommand(kubeClient kubernetes.Interface) *cobra.Command { + deleteCapture := &cobra.Command{ + Use: "delete", + Short: "Delete a Retina capture", + Example: deleteExample, + RunE: func(*cobra.Command, []string) error { + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM) + defer cancel() - kubeClient, err := kubernetes.NewForConfig(kubeConfig) - if err != nil { - return errors.Wrap(err, "") - } + // Set namespace. If --namespace is not set, use namespace on user's context + ns, _, err := opts.ConfigFlags.ToRawKubeConfigLoader().Namespace() + if err != nil { + return errors.Wrap(err, "failed to get namespace from kubeconfig") + } - ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM) - defer cancel() + if opts.Namespace == nil || *opts.Namespace == "" { + opts.Namespace = &ns + } - captureJobSelector := &metav1.LabelSelector{ - MatchLabels: map[string]string{ - label.CaptureNameLabel: *opts.Name, - label.AppLabel: captureConstants.CaptureAppname, - }, - } - labelSelector, _ := labels.Parse(metav1.FormatLabelSelector(captureJobSelector)) - jobListOpt := metav1.ListOptions{ - LabelSelector: labelSelector.String(), - } + captureJobSelector := &metav1.LabelSelector{ + MatchLabels: map[string]string{ + label.CaptureNameLabel: *opts.Name, + label.AppLabel: captureConstants.CaptureAppname, + }, + } + labelSelector, _ := labels.Parse(metav1.FormatLabelSelector(captureJobSelector)) + jobListOpt := metav1.ListOptions{ + LabelSelector: labelSelector.String(), + } - jobList, err := kubeClient.BatchV1().Jobs(*opts.Namespace).List(ctx, jobListOpt) - if err != nil { - return errors.Wrap(err, "failed to list capture jobs") - } - if len(jobList.Items) == 0 { - return errors.Errorf("capture %s in namespace %s was not found", *opts.Name, *opts.Namespace) - } + jobList, err := kubeClient.BatchV1().Jobs(*opts.Namespace).List(ctx, jobListOpt) + if err != nil { + return errors.Wrap(err, "failed to list capture jobs") + } + if len(jobList.Items) == 0 { + return errors.Errorf("capture %s in namespace %s was not found", *opts.Name, *opts.Namespace) + } - for _, job := range jobList.Items { - deletePropagationBackground := metav1.DeletePropagationBackground - if err := kubeClient.BatchV1().Jobs(job.Namespace).Delete(ctx, job.Name, metav1.DeleteOptions{ - PropagationPolicy: &deletePropagationBackground, - }); err != nil { - retinacmd.Logger.Info("Failed to delete job", zap.String("job name", job.Name), zap.Error(err)) + for idx := range jobList.Items { + deletePropagationBackground := metav1.DeletePropagationBackground + if err := kubeClient.BatchV1().Jobs(jobList.Items[idx].Namespace).Delete(ctx, jobList.Items[idx].Name, metav1.DeleteOptions{ + PropagationPolicy: &deletePropagationBackground, + }); err != nil { + retinacmd.Logger.Info("Failed to delete job", zap.String("job name", jobList.Items[idx].Name), zap.Error(err)) + } } - } - for _, volume := range jobList.Items[0].Spec.Template.Spec.Volumes { - if volume.Secret != nil { - if err := kubeClient.CoreV1().Secrets(*opts.Namespace).Delete(ctx, volume.Secret.SecretName, metav1.DeleteOptions{}); err != nil { - return errors.Wrap(err, "failed to delete capture secret") + for idx := range jobList.Items[0].Spec.Template.Spec.Volumes { + if jobList.Items[0].Spec.Template.Spec.Volumes[idx].Secret != nil { + if err := kubeClient.CoreV1().Secrets(*opts.Namespace).Delete(ctx, jobList.Items[0].Spec.Template.Spec.Volumes[idx].Secret.SecretName, metav1.DeleteOptions{}); err != nil { + if !k8serrors.IsNotFound(err) { + return errors.Wrap(err, "failed to delete capture secret") + } + } + break } - break } - } - retinacmd.Logger.Info(fmt.Sprintf("Retina Capture %q delete", *opts.Name)) + retinacmd.Logger.Info(fmt.Sprintf("Retina Capture %q delete", *opts.Name)) - return nil - }, -} + return nil + }, + } -func init() { - capture.AddCommand(deleteCapture) + return deleteCapture } diff --git a/cli/cmd/capture/delete_test.go b/cli/cmd/capture/delete_test.go new file mode 100644 index 0000000000..f346758b26 --- /dev/null +++ b/cli/cmd/capture/delete_test.go @@ -0,0 +1,254 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package capture + +import ( + "bytes" + "context" + "fmt" + "testing" + + "github.com/microsoft/retina/pkg/label" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" + clienttesting "k8s.io/client-go/testing" +) + +const ( + // Constants for creating test capture jobs + defaultCaptureJobName = "retina-capture-test" + defaultCaptureJobNamespace = "workload" +) + +type deleteTestCase struct { + name string + inputName string // name flag for delete + inputNamespace string // namespace flag for delete + jobNamespace string // namespace where the job is expected to be found + jobPodSelectors string // pod selectors for the job +} + +// newKubeclient creates a consistent fake Kubernetes client for all tests +func newKubeclient() *fake.Clientset { + objects := []runtime.Object{ + NewNode("A1"), + NewNode("A2"), + NewNode("B1"), + NewNode("B2"), + NewNamespace("default"), + NewNamespace("workload"), + } + + // Add pods to the client + for _, pod := range NewClientServerPods("A", "default") { + objects = append(objects, pod) + } + for _, pod := range NewClientServerPods("B", "workload") { + objects = append(objects, pod) + } + + kubeClient := fake.NewClientset(objects...) + + // Handle job creation to set job name if not provided, which is done automatically in a real k8s cluster + kubeClient.PrependReactor("create", "jobs", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + createAction, ok := action.(clienttesting.CreateAction) + if !ok { + return false, nil, fmt.Errorf("expected CreateAction, got %T", action) //nolint:err113 // test code + } + job := createAction.GetObject().(*batchv1.Job) + + // Set job name if unset + if job.Name == "" { + job.Name = job.GenerateName + randomString(5) + } + return false, job, nil + }) + + return kubeClient +} + +func deleteArgs(tc deleteTestCase) []string { + args := []string{"delete"} + if tc.inputNamespace != "" { + args = append(args, "--namespace", tc.inputNamespace) + } + if tc.inputName != "" { + args = append(args, "--name", tc.inputName) + } + return args +} + +func createArgs(name, namespace, podSelectors string) []string { + // Create a default set of arguments for the create command + return []string{ + "create", + "--name=" + name, + "--namespace=" + namespace, + "--pod-selectors=" + podSelectors, + "--namespace-selectors=name=" + namespace, + } +} + +func createCapture(t *testing.T, kubeClient kubernetes.Interface, name, namespace, podSelectors string) { + createCmd := NewCommand(kubeClient) + + createCmd.SetArgs(createArgs(name, namespace, podSelectors)) + + buf := new(bytes.Buffer) + createCmd.SetOut(buf) + + err := createCmd.Execute() + if err != nil { + t.Fatalf("Failed to create capture jobs: %v", err) + } +} + +// setupDeleteTest prepares the test environment and creates jobs if needed +func setupDeleteTest(t *testing.T, tc deleteTestCase) *fake.Clientset { + // Create a Kubernetes client with test resources + kubeClient := newKubeclient() + + createCapture(t, kubeClient, tc.inputName, tc.jobNamespace, tc.jobPodSelectors) + createCapture(t, kubeClient, "do-not-delete", tc.jobNamespace, tc.jobPodSelectors) + + return kubeClient +} + +func jobExists(t *testing.T, kubeClient kubernetes.Interface, name, namespace string) bool { + jobs, err := kubeClient.BatchV1().Jobs(namespace).List( + context.TODO(), + metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s", label.CaptureNameLabel, name), + }, + ) + if err != nil { + t.Fatalf("Failed to list jobs in namespace %s: %v", namespace, err) + } + + return len(jobs.Items) > 0 +} + +// jobDeletedCorrectly validates that Kubernetes jobs were deleted correctly +func jobDeletedCorrectly(t *testing.T, kubeClient *fake.Clientset, tc deleteTestCase) { + t.Helper() + + // Check if the job was deleted as expected + if jobExists(t, kubeClient, tc.inputName, tc.inputNamespace) { + t.Errorf("Expected job %s to be deleted from namespace %s, but it still exists", + tc.inputName, tc.inputNamespace) + } + + // Check if the other job in namespace is still present + if !jobExists(t, kubeClient, "do-not-delete", tc.inputNamespace) { + t.Errorf("Expected job do-not-delete in namespace %s, but it was deleted", + tc.inputNamespace) + } +} + +func TestDeleteCaptureJobs(t *testing.T) { + testCases := []deleteTestCase{ + { + name: "delete providing name only", + inputName: defaultCaptureJobName, + inputNamespace: "", + jobNamespace: "default", + jobPodSelectors: "service=A", + }, + { + name: "delete providing name and namespace", + inputName: defaultCaptureJobName, + inputNamespace: defaultCaptureJobNamespace, + jobNamespace: defaultCaptureJobNamespace, + jobPodSelectors: "service=B", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + kubeClient := setupDeleteTest(t, tc) + + // Create a delete command + deleteCmd := NewCommand(kubeClient) + + // Set command args + deleteCmd.SetArgs(deleteArgs(tc)) + buf := new(bytes.Buffer) + deleteCmd.SetOut(buf) + + // Execute the delete command + err := deleteCmd.Execute() + if err != nil { + t.Fatalf("Failed to delete capture job: %v", err) + } + + // Validate job is deleted correctly + jobDeletedCorrectly(t, kubeClient, tc) + }) + } +} + +func TestDeleteCaptureJobs_SecretAlreadyDeleted(t *testing.T) { + // When a secret referenced by a job's volume has already been deleted + // (e.g. via ownerRef GC), the delete command should still succeed. + deleteTestCapName := "test-secret-gone" + ns := "default" + + kubeClient := newKubeclient() + + // Create a job that references a secret volume, but don't create the secret itself. + secretName := "already-deleted-secret" + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: deleteTestCapName + "-node1-abcde", + Namespace: ns, + Labels: map[string]string{ + label.CaptureNameLabel: deleteTestCapName, + label.AppLabel: "capture", + }, + }, + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "capture", Image: "test"}}, + Volumes: []corev1.Volume{ + { + Name: "capture-secret", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: secretName, + }, + }, + }, + }, + RestartPolicy: corev1.RestartPolicyNever, + }, + }, + }, + } + + _, err := kubeClient.BatchV1().Jobs(ns).Create(context.Background(), job, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create test job: %v", err) + } + + // Run delete — the secret doesn't exist, so the NotFound check is exercised + deleteCmd := NewCommand(kubeClient) + deleteCmd.SetArgs([]string{"delete", "--name", deleteTestCapName, "--namespace", ns}) + buf := new(bytes.Buffer) + deleteCmd.SetOut(buf) + + err = deleteCmd.Execute() + if err != nil { + t.Fatalf("Delete should succeed even when secret is already gone, got: %v", err) + } + + // Verify job was deleted + if jobExists(t, kubeClient, deleteTestCapName, ns) { + t.Error("Expected job to be deleted") + } +} diff --git a/cli/cmd/capture/download.go b/cli/cmd/capture/download.go index c5add7ec85..910608c51a 100644 --- a/cli/cmd/capture/download.go +++ b/cli/cmd/capture/download.go @@ -4,82 +4,794 @@ package capture import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "errors" "fmt" "io" "net/url" "os" + "os/signal" + "path/filepath" "strings" + "syscall" + "time" "github.com/Azure/azure-sdk-for-go/storage" - "github.com/pkg/errors" + retinacmd "github.com/microsoft/retina/cli/cmd" + captureConstants "github.com/microsoft/retina/pkg/capture/constants" + captureFile "github.com/microsoft/retina/pkg/capture/file" + captureUtils "github.com/microsoft/retina/pkg/capture/utils" + captureLabels "github.com/microsoft/retina/pkg/label" "github.com/spf13/cobra" "github.com/spf13/viper" + "go.uber.org/zap" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/rand" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/remotecommand" + "k8s.io/kubectl/pkg/scheme" + "k8s.io/kubectl/pkg/util/i18n" + "k8s.io/kubectl/pkg/util/templates" ) -const BlobURL = "BLOB_URL" +// NodeOS represents the operating system of a Kubernetes node +type NodeOS *int -var ErrEmptyBlobURL = errors.Errorf("%s environment variable is empty. It must be set/exported", BlobURL) +const ( + LinuxOS = 0 + WindowsOS = 1 +) + +var ( + Linux NodeOS = &[]int{LinuxOS}[0] + Windows NodeOS = &[]int{WindowsOS}[0] +) + +const ( + DefaultOutputPath = "./" +) + +var ( + blobURL string + ErrCreateDirectory = errors.New("failed to create directory") + ErrGetNodeInfo = errors.New("failed to get node information") + ErrWriteFileToHost = errors.New("failed to write file to host") + ErrObtainPodList = errors.New("failed to obtain list of pods") + ErrExecFileDownload = errors.New("failed to exec file download in container") + ErrCreateDownloadPod = errors.New("failed to create download pod") + ErrGetDownloadPod = errors.New("failed to get download pod") + ErrCheckFileExistence = errors.New("failed to check file existence") + ErrCreateExecutor = errors.New("failed to create executor") + ErrExecCommand = errors.New("failed to exec command") + ErrCreateOutputDir = errors.New("failed to create output directory") + ErrNoBlobsFound = errors.New("no blobs found with prefix") + captureName string + outputPath string + downloadAll bool + downloadAllNamespaces bool +) + +var ( + ErrNoPodFound = errors.New("no pod found for job") + ErrManyPodsFound = errors.New("more than one pod found for job; expected exactly one") + ErrCaptureContainerNotFound = errors.New("capture container not found in pod") + ErrFileNotAccessible = errors.New("file does not exist or is not readable") + ErrEmptyDownloadOutput = errors.New("download command produced no output") + ErrFailedToCreateDownloadPod = errors.New("failed to create download pod") + ErrUnsupportedNodeOS = errors.New("unsupported node operating system") + ErrMissingRequiredFlags = errors.New("either --name, --blob-url, or --all must be specified") + ErrAllNamespacesRequiresAll = errors.New("--all-namespaces flag can only be used with --all flag") +) + +// DownloadCmd holds all OS-specific commands and configurations +type DownloadCmd struct { + ContainerImage string + SrcFilePath string + MountPath string + KeepAliveCommand []string + FileCheckCommand []string + FileReadCommand []string +} + +// DownloadService encapsulates the download functionality and shared dependencies +type DownloadService struct { + kubeClient kubernetes.Interface + config *rest.Config + namespace string +} + +// Key represents a unique capture identifier +type Key struct { + Name string + Namespace string +} + +// NewDownloadService creates a new download service with shared dependencies +func NewDownloadService(kubeClient kubernetes.Interface, config *rest.Config, namespace string) *DownloadService { + return &DownloadService{ + kubeClient: kubeClient, + config: config, + namespace: namespace, + } +} + +func getDownloadCmd(node *corev1.Node, hostPath, fileName string) (*DownloadCmd, error) { + nodeOS, err := getNodeOS(node) + if err != nil { + return nil, err + } + + if nodeOS == nil { + return nil, ErrUnsupportedNodeOS + } + + switch *nodeOS { + case WindowsOS: + srcFilePath := "C:\\host" + strings.ReplaceAll(hostPath, "/", "\\") + "\\" + fileName + ".tar.gz" + mountPath := "C:\\host" + strings.ReplaceAll(hostPath, "/", "\\") + return &DownloadCmd{ + ContainerImage: getWindowsContainerImage(node), + SrcFilePath: srcFilePath, + MountPath: mountPath, + KeepAliveCommand: []string{"cmd", "/c", "echo Download pod ready & ping -n 3601 127.0.0.1 > nul"}, + FileCheckCommand: []string{"cmd", "/c", fmt.Sprintf("if exist %s echo FILE_EXISTS", srcFilePath)}, + FileReadCommand: []string{"cmd", "/c", "type", srcFilePath}, + }, nil + case LinuxOS: + srcFilePath := "/" + filepath.Join("host", hostPath, fileName) + ".tar.gz" + mountPath := "/" + filepath.Join("host", hostPath) + return &DownloadCmd{ + ContainerImage: "mcr.microsoft.com/azurelinux/busybox:1.36", + SrcFilePath: srcFilePath, + MountPath: mountPath, + KeepAliveCommand: []string{"sh", "-c", "echo 'Download pod ready'; sleep 3600"}, + FileCheckCommand: []string{"sh", "-c", fmt.Sprintf("if [ -r %q ]; then echo 'FILE_EXISTS'; fi", srcFilePath)}, + FileReadCommand: []string{"cat", srcFilePath}, + }, nil + default: + return nil, ErrUnsupportedNodeOS + } +} + +func getNodeOS(node *corev1.Node) (NodeOS, error) { + nodeOS := strings.ToLower(node.Status.NodeInfo.OperatingSystem) + + if strings.Contains(nodeOS, "windows") { + retinacmd.Logger.Info("Detected node OS: Windows", zap.String("node", node.Name), zap.String("os", node.Status.NodeInfo.OperatingSystem)) + return Windows, nil + } + + if strings.Contains(nodeOS, "linux") { + retinacmd.Logger.Info("Detected node OS: Linux", zap.String("node", node.Name), zap.String("os", node.Status.NodeInfo.OperatingSystem)) + return Linux, nil + } + + return nil, fmt.Errorf("unsupported operating system: %s: %w", node.Status.NodeInfo.OperatingSystem, ErrUnsupportedNodeOS) +} + +// Detects the Windows LTSC version and returns the appropriate nanoserver image +func getWindowsContainerImage(node *corev1.Node) string { + osImage := strings.ToLower(node.Status.NodeInfo.OSImage) + + var suffix string + switch { + case strings.Contains(osImage, "2025"): + suffix = "ltsc2025" + case strings.Contains(osImage, "2022"): + suffix = "ltsc2022" + case strings.Contains(osImage, "2016"): + suffix = "ltsc2016" + default: + retinacmd.Logger.Warn("Could not determine Windows LTSC version, defaulting to ltsc2022", + zap.String("node", node.Name), + zap.String("osImage", osImage)) + suffix = "ltsc2022" + } + + containerImage := "mcr.microsoft.com/windows/nanoserver:" + suffix + retinacmd.Logger.Info("Selected Windows container image", zap.String("image", containerImage)) + + return containerImage +} + +var downloadExample = templates.Examples(i18n.T(` + # List Retina capture jobs + kubectl retina capture list + + # Download the capture file(s) created using the capture name + kubectl retina capture download --name + + # Download the capture file(s) created using the capture name and define output location + kubectl retina capture download --name -o + + # Download all available captures + kubectl retina capture download --all + + # Download all available captures from all namespaces + kubectl retina capture download --all --all-namespaces + + # Download capture file(s) from Blob Storage via Blob URL (Blob URL requires Read/List permissions) + kubectl retina capture download --blob-url "" +`)) + +func downloadFromCluster(ctx context.Context, config *rest.Config, namespace string) error { + fmt.Println("Downloading capture: ", captureName) + kubeClient, err := kubernetes.NewForConfig(config) + if err != nil { + return fmt.Errorf("failed to initialize k8s client: %w", err) + } + + downloadService := NewDownloadService(kubeClient, config, namespace) + + pods, err := getCapturePods(ctx, kubeClient, captureName, namespace) + if err != nil { + return fmt.Errorf("failed to obtain capture pod: %w", err) + } + + err = os.MkdirAll(filepath.Join(outputPath, captureName), 0o775) + if err != nil { + return errors.Join(ErrCreateDirectory, err) + } + + for i := range pods.Items { + pod := pods.Items[i] + if pod.Status.Phase != corev1.PodSucceeded { + return fmt.Errorf("%s: %w", captureName, ErrNoPodFound) + } + + nodeName := pod.Spec.NodeName + hostPath, ok := pod.Annotations[captureConstants.CaptureHostPathAnnotationKey] + if !ok { + return errors.New("cannot obtain host path from pod annotations") + } + fileName, ok := pod.Annotations[captureConstants.CaptureFilenameAnnotationKey] + if !ok { + return errors.New("cannot obtain capture file name from pod annotations") + } + + err = downloadService.DownloadFile(ctx, nodeName, hostPath, fileName, captureName) + if err != nil { + return err + } + } + + return nil +} + +// DownloadFile downloads a capture file from a specific node +func (ds *DownloadService) DownloadFile(ctx context.Context, nodeName, hostPath, fileName, captureName string) error { + content, err := ds.DownloadFileContent(ctx, nodeName, hostPath, fileName, captureName) + if err != nil { + return err + } + + outputFile := filepath.Join(outputPath, captureName, fileName+".tar.gz") + fmt.Printf("Bytes retrieved: %d\n", len(content)) + + err = os.WriteFile(outputFile, content, 0o600) + if err != nil { + return errors.Join(ErrWriteFileToHost, err) + } + + fmt.Printf("File written to: %s\n", outputFile) + return nil +} + +// DownloadFileContent downloads a capture file from a specific node and returns the content +func (ds *DownloadService) DownloadFileContent(ctx context.Context, nodeName, hostPath, fileName, captureName string) ([]byte, error) { + node, err := ds.kubeClient.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) + if err != nil { + return nil, errors.Join(ErrGetNodeInfo, err) + } + + downloadCmd, err := getDownloadCmd(node, hostPath, fileName) + if err != nil { + return nil, err + } + + fmt.Println("File to be downloaded: ", downloadCmd.SrcFilePath) + downloadPod, err := ds.createDownloadPod(ctx, nodeName, hostPath, captureName, downloadCmd) + if err != nil { + return nil, err + } + + // Ensure cleanup + defer func() { + cleanupErr := ds.kubeClient.CoreV1().Pods(ds.namespace).Delete(ctx, downloadPod.Name, metav1.DeleteOptions{}) + if cleanupErr != nil { + retinacmd.Logger.Warn("Failed to clean up debug pod", zap.String("name", downloadPod.Name), zap.Error(cleanupErr)) + } + }() + + fileExists, err := ds.verifyFileExists(ctx, downloadPod, downloadCmd) + if err != nil || !fileExists { + return nil, err + } + + fmt.Println("Obtaining file...") + fileContent, err := ds.executeFileDownload(ctx, downloadPod, downloadCmd) + if err != nil { + return nil, err + } + + return fileContent, nil +} + +func getCapturePods(ctx context.Context, kubeClient kubernetes.Interface, captureName, namespace string) (*corev1.PodList, error) { + pods, err := kubeClient.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ + LabelSelector: captureLabels.CaptureNameLabel + "=" + captureName, + }) + if err != nil { + return &corev1.PodList{}, errors.Join(ErrObtainPodList, err) + } + if len(pods.Items) == 0 { + return &corev1.PodList{}, fmt.Errorf("%s: %w", captureName, ErrNoPodFound) + } + + return pods, nil +} + +// executeFileDownload downloads the file content from the pod +func (ds *DownloadService) executeFileDownload(ctx context.Context, pod *corev1.Pod, downloadCmd *DownloadCmd) ([]byte, error) { + content, err := ds.createDownloadExec(ctx, pod, downloadCmd.FileReadCommand) + if err != nil { + return nil, errors.Join(ErrExecFileDownload, err) + } + + if content == "" { + return nil, ErrEmptyDownloadOutput + } + + return []byte(content), nil +} + +// createDownloadPod creates a pod for downloading files from the host +func (ds *DownloadService) createDownloadPod(ctx context.Context, nodeName, hostPath, captureName string, downloadCmd *DownloadCmd) (*corev1.Pod, error) { + podName := captureName + "-download-" + rand.String(5) + + podSpec := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName, + Namespace: ds.namespace, + Labels: captureUtils.GetDownloadLabelsFromCaptureName(captureName), + }, + Spec: corev1.PodSpec{ + NodeName: nodeName, + Containers: []corev1.Container{ + { + Name: captureConstants.DownloadContainerName, + Image: downloadCmd.ContainerImage, + Command: downloadCmd.KeepAliveCommand, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "host-mount", + MountPath: downloadCmd.MountPath, + }, + }, + }, + }, + RestartPolicy: corev1.RestartPolicyNever, + Volumes: []corev1.Volume{ + { + Name: "host-mount", + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{ + Path: hostPath, + }, + }, + }, + }, + }, + } + + fmt.Printf("Creating download pod: %s\n", podName) + _, err := ds.kubeClient.CoreV1().Pods(ds.namespace).Create(ctx, podSpec, metav1.CreateOptions{}) + if err != nil { + return nil, errors.Join(ErrCreateDownloadPod, err) + } + + return ds.waitForPodReady(ctx, podName) +} + +// waitForPodReady waits for the pod to be in running state +func (ds *DownloadService) waitForPodReady(ctx context.Context, podName string) (*corev1.Pod, error) { + timeout := time.After(30 * time.Second) + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-timeout: + return nil, fmt.Errorf("timeout waiting for download pod to become ready: %w", ErrFailedToCreateDownloadPod) + case <-ticker.C: + pod, err := ds.kubeClient.CoreV1().Pods(ds.namespace).Get(ctx, podName, metav1.GetOptions{}) + if err != nil { + return nil, errors.Join(ErrGetDownloadPod, err) + } + if pod.Status.Phase == corev1.PodRunning { + return pod, nil + } + if pod.Status.Phase == corev1.PodFailed { + return nil, fmt.Errorf("download pod failed to spin up successfully: %w", ErrFailedToCreateDownloadPod) + } + } + } +} + +// verifyFileExists checks if the target file exists and is accessible +func (ds *DownloadService) verifyFileExists(ctx context.Context, pod *corev1.Pod, downloadCmd *DownloadCmd) (bool, error) { + maxAttempts := 3 -var downloadCapture = &cobra.Command{ - Use: "download", - Short: "Download Retina Captures", - RunE: func(*cobra.Command, []string) error { - viper.AutomaticEnv() - blobURL := viper.GetString(BlobURL) - if blobURL == "" { - return ErrEmptyBlobURL + for attempt := 1; attempt <= maxAttempts; attempt++ { + checkOutput, err := ds.createDownloadExec(ctx, pod, downloadCmd.FileCheckCommand) + if err != nil { + if attempt == maxAttempts { + return false, fmt.Errorf("failed to check file existence after %d attempts: %w", attempt, err) + } + time.Sleep(time.Duration(attempt*2) * time.Second) + continue + } + + if strings.Contains(checkOutput, "FILE_EXISTS") { + return true, nil } - u, err := url.Parse(blobURL) + time.Sleep(time.Duration(attempt*2) * time.Second) + } + + return false, fmt.Errorf("%s: %w", downloadCmd.SrcFilePath, ErrFileNotAccessible) +} + +// createDownloadExec executes a command in the pod and returns the output +func (ds *DownloadService) createDownloadExec(ctx context.Context, pod *corev1.Pod, command []string) (string, error) { + req := ds.kubeClient.CoreV1().RESTClient().Post(). + Resource("pods"). + Name(pod.Name). + Namespace(pod.Namespace). + SubResource("exec"). + VersionedParams(&corev1.PodExecOptions{ + Container: captureConstants.DownloadContainerName, + Command: command, + Stdout: true, + Stderr: true, + }, scheme.ParameterCodec) + + exec, err := remotecommand.NewSPDYExecutor(ds.config, "POST", req.URL()) + if err != nil { + return "", errors.Join(ErrCreateExecutor, err) + } + + var outBuf, errBuf bytes.Buffer + streamOpts := remotecommand.StreamOptions{ + Stdout: &outBuf, + Stderr: &errBuf, + } + + if err = exec.StreamWithContext(ctx, streamOpts); err != nil { + return "", fmt.Errorf("failed to exec command (stderr: %s): %w", errBuf.String(), err) + } + + return outBuf.String(), nil +} + +func downloadFromBlob() error { + u, err := url.Parse(blobURL) + if err != nil { + retinacmd.Logger.Error("err: ", zap.Error(err)) + return fmt.Errorf("failed to parse SAS URL %s: %w", blobURL, err) + } + + b, err := storage.NewAccountSASClientFromEndpointToken(u.String(), u.Query().Encode()) + if err != nil { + retinacmd.Logger.Error("err: ", zap.Error(err)) + return fmt.Errorf("failed to create storage account client: %w", err) + } + + blobService := b.GetBlobService() + containerPath := strings.TrimLeft(u.Path, "/") + splitPath := strings.SplitN(containerPath, "/", 2) + containerName := splitPath[0] + + params := storage.ListBlobsParameters{Prefix: *opts.Name} + blobList, err := blobService.GetContainerReference(containerName).ListBlobs(params) + if err != nil { + retinacmd.Logger.Error("err: ", zap.Error(err)) + return fmt.Errorf("failed to list blobstore: %w", err) + } + + if len(blobList.Blobs) == 0 { + retinacmd.Logger.Error("err: ", zap.Error(err)) + return fmt.Errorf("%w: %s", ErrNoBlobsFound, *opts.Name) + } + + err = os.MkdirAll(outputPath, 0o775) + if err != nil { + return errors.Join(ErrCreateOutputDir, err) + } + + for i := range blobList.Blobs { + blob := blobList.Blobs[i] + blobRef := blobService.GetContainerReference(containerName).GetBlobReference(blob.Name) + readCloser, err := blobRef.Get(&storage.GetBlobOptions{}) + if err != nil { + retinacmd.Logger.Error("err: ", zap.Error(err)) + return fmt.Errorf("failed to read from blobstore: %w", err) + } + + blobData, err := io.ReadAll(readCloser) + readCloser.Close() if err != nil { - return errors.Wrapf(err, "failed to parse SAS URL %s", blobURL) + retinacmd.Logger.Error("err: ", zap.Error(err)) + return fmt.Errorf("failed to obtain blob from blobstore: %w", err) } - // blobService, err := storage.NewAccountSASClientFromEndpointToken(u.String(), u.Query().Encode()).GetBlobService() - b, err := storage.NewAccountSASClientFromEndpointToken(u.String(), u.Query().Encode()) + outputFile := filepath.Join(outputPath, blob.Name) + err = os.WriteFile(outputFile, blobData, 0o600) if err != nil { - return errors.Wrap(err, "failed to create storage account client") + retinacmd.Logger.Error("err: ", zap.Error(err)) + return fmt.Errorf("failed to write file: %w", err) } - blobService := b.GetBlobService() - containerPath := strings.TrimLeft(u.Path, "/") - splitPath := strings.SplitN(containerPath, "/", 2) //nolint:gomnd // TODO string splitting probably isn't the right way to parse this URL? - containerName := splitPath[0] + fmt.Println("Downloaded: ", outputFile) + } + return nil +} + +func downloadAllCaptures(ctx context.Context, config *rest.Config, namespace string) error { + if downloadAllNamespaces { + fmt.Println("Downloading all captures from all namespaces...") + } else { + fmt.Printf("Downloading all captures for namespace %s...\n", namespace) + } + kubeClient, err := kubernetes.NewForConfig(config) + if err != nil { + return fmt.Errorf("failed to initialize k8s client: %w", err) + } - params := storage.ListBlobsParameters{Prefix: *opts.Name} - blobList, err := blobService.GetContainerReference(containerName).ListBlobs(params) + // List all capture jobs with the capture app label + captureJobSelector := &metav1.LabelSelector{ + MatchLabels: map[string]string{ + captureLabels.AppLabel: captureConstants.CaptureAppname, + }, + } + labelSelector, err := metav1.LabelSelectorAsSelector(captureJobSelector) + if err != nil { + return fmt.Errorf("failed to parse label selector: %w", err) + } + + var jobList *batchv1.JobList + if downloadAllNamespaces { + // Search across all namespaces + jobList, err = kubeClient.BatchV1().Jobs("").List(ctx, metav1.ListOptions{ + LabelSelector: labelSelector.String(), + }) + if err != nil { + return fmt.Errorf("failed to list capture jobs across all namespaces: %w", err) + } + } else { + // Search in specified namespace only + jobList, err = kubeClient.BatchV1().Jobs(namespace).List(ctx, metav1.ListOptions{ + LabelSelector: labelSelector.String(), + }) if err != nil { - return errors.Wrap(err, "failed to list blobstore ") + return fmt.Errorf("failed to list capture jobs: %w", err) } + } - if len(blobList.Blobs) == 0 { - return errors.Errorf("no blobs found with prefix: %s", *opts.Name) + if len(jobList.Items) == 0 { + if downloadAllNamespaces { + fmt.Printf("No captures found across all namespaces\n") + } else { + fmt.Printf("No captures found in namespace %s\n", namespace) } + return nil + } - for _, v := range blobList.Blobs { - blob := blobService.GetContainerReference(containerName).GetBlobReference(v.Name) - readCloser, err := blob.Get(&storage.GetBlobOptions{}) - if err != nil { - return errors.Wrap(err, "failed to read from blobstore") + // Group jobs by capture name and namespace + captureToJobs := make(map[Key][]batchv1.Job) + for i := range jobList.Items { + job := &jobList.Items[i] + captureNameFromLabel, ok := job.Labels[captureLabels.CaptureNameLabel] + if !ok { + continue + } + key := Key{Name: captureNameFromLabel, Namespace: job.Namespace} + captureToJobs[key] = append(captureToJobs[key], *job) + } + + fmt.Printf("Found %d capture(s) to download\n", len(captureToJobs)) + + // Create the final archive using streaming approach to avoid memory issues + timestamp := captureFile.TimeToString(captureFile.Now()) + finalArchivePath := filepath.Join(outputPath, fmt.Sprintf("all-captures-%s.tar.gz", timestamp)) + + fmt.Printf("Creating final archive: %s\n", finalArchivePath) + err = createStreamingTarGzArchive(ctx, finalArchivePath, captureToJobs, kubeClient, config) + if err != nil { + return fmt.Errorf("failed to create final archive: %w", err) + } + + fmt.Printf("Successfully created archive: %s\n", finalArchivePath) + return nil +} + +// createStreamingTarGzArchive creates a tar.gz archive by streaming files one at a time to avoid memory issues +func createStreamingTarGzArchive(ctx context.Context, outputPath string, captureToJobs map[Key][]batchv1.Job, kubeClient kubernetes.Interface, config *rest.Config) error { + // Create the output file + outFile, err := os.Create(outputPath) + if err != nil { + return fmt.Errorf("failed to create archive file: %w", err) + } + defer outFile.Close() + + // Create gzip writer + gzipWriter := gzip.NewWriter(outFile) + defer gzipWriter.Close() + + // Create tar writer + tarWriter := tar.NewWriter(gzipWriter) + defer tarWriter.Close() + + // We'll create download services per namespace as needed + downloadServices := make(map[string]*DownloadService) + fileCount := 0 + + // Process each capture and stream files directly to archive + for captureKey := range captureToJobs { + currentCaptureName := captureKey.Name + currentNamespace := captureKey.Namespace + fmt.Printf("Processing capture: %s in namespace: %s\n", currentCaptureName, currentNamespace) + + // Get or create download service for this namespace + downloadService, exists := downloadServices[currentNamespace] + if !exists { + downloadService = NewDownloadService(kubeClient, config, currentNamespace) + downloadServices[currentNamespace] = downloadService + } + + // Get pods for this capture and download files + pods, podsErr := getCapturePods(ctx, kubeClient, currentCaptureName, currentNamespace) + if podsErr != nil { + fmt.Printf("Warning: Failed to get pods for capture %s in namespace %s: %v\n", currentCaptureName, currentNamespace, podsErr) + continue + } + + for i := range pods.Items { + pod := pods.Items[i] + if pod.Status.Phase != corev1.PodSucceeded { + fmt.Printf("Warning: Pod %s is not in Succeeded phase (status: %s), skipping\n", pod.Name, pod.Status.Phase) + continue } - defer readCloser.Close() + nodeName := pod.Spec.NodeName + hostPath, ok := pod.Annotations[captureConstants.CaptureHostPathAnnotationKey] + if !ok { + fmt.Printf("Warning: Cannot obtain host path from pod annotations for %s\n", pod.Name) + continue + } + fileName, ok := pod.Annotations[captureConstants.CaptureFilenameAnnotationKey] + if !ok { + fmt.Printf("Warning: Cannot obtain capture file name from pod annotations for %s\n", pod.Name) + continue + } - blobData, err := io.ReadAll(readCloser) + // Download file content (this is still done in memory per file, but not all files at once) + content, err := downloadService.DownloadFileContent(ctx, nodeName, hostPath, fileName, currentCaptureName) if err != nil { - return errors.Wrap(err, "failed to obtain blob from blobstore") + fmt.Printf("Warning: Failed to download file from pod %s: %v\n", pod.Name, err) + continue } - err = os.WriteFile(v.Name, blobData, 0o644) //nolint:gosec,gomnd // intentionally permissive bitmask - if err != nil { - return errors.Wrap(err, "failed to write file") + // Determine archive path based on whether we're using all namespaces + var archivePath string + if downloadAllNamespaces { + // Include namespace in path: namespace/captureName/fileName.tar.gz + archivePath = filepath.Join(currentNamespace, currentCaptureName, fileName+".tar.gz") + } else { + // Original path: captureName/fileName.tar.gz + archivePath = filepath.Join(currentCaptureName, fileName+".tar.gz") + } + + // Stream file directly to archive + header := &tar.Header{ + Name: archivePath, + Mode: 0o600, + Size: int64(len(content)), + } + + // Write header + if err := tarWriter.WriteHeader(header); err != nil { + return fmt.Errorf("failed to write header for %s: %w", archivePath, err) + } + + // Write content + if _, err := tarWriter.Write(content); err != nil { + return fmt.Errorf("failed to write content for %s: %w", archivePath, err) } - fmt.Println("Downloaded blob: ", v.Name) + + fileCount++ + fmt.Printf("Added %s (%d bytes) to archive\n", archivePath, len(content)) } + } + + if fileCount == 0 { + // Remove the empty archive file + outFile.Close() + os.Remove(outputPath) + fmt.Println("No capture files were successfully downloaded") return nil - }, + } + + fmt.Printf("Successfully added %d files to archive\n", fileCount) + return nil } -func init() { - capture.AddCommand(downloadCapture) +func NewDownloadSubCommand() *cobra.Command { + downloadCapture := &cobra.Command{ + Use: "download", + Short: "Download Retina Captures", + Example: downloadExample, + RunE: func(*cobra.Command, []string) error { + viper.AutomaticEnv() + + kubeConfig, err := opts.ToRESTConfig() + if err != nil { + return fmt.Errorf("failed to compose k8s rest config: %w", err) + } + + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM) + defer cancel() + + captureNamespace := *opts.Namespace + if captureNamespace == "" { + captureNamespace = "default" + } + + if captureName == "" && blobURL == "" && !downloadAll { + return ErrMissingRequiredFlags + } + + // Validate all-namespaces flag usage + if downloadAllNamespaces && !downloadAll { + return ErrAllNamespacesRequiresAll + } + + if captureName != "" { + err = downloadFromCluster(ctx, kubeConfig, captureNamespace) + if err != nil { + return err + } + } + + if blobURL != "" { + err = downloadFromBlob() + if err != nil { + return err + } + } + + if downloadAll { + err = downloadAllCaptures(ctx, kubeConfig, captureNamespace) + if err != nil { + return err + } + } + + return nil + }, + } + + downloadCapture.Flags().StringVar(&blobURL, "blob-url", "", "Blob URL from which to download") + downloadCapture.Flags().StringVar(&captureName, "name", "", "The name of a the capture") + downloadCapture.Flags().BoolVar(&downloadAll, "all", false, "Download all available captures for the specified namespace (or all namespaces if --all-namespaces flag is set)") + downloadCapture.Flags().BoolVar(&downloadAllNamespaces, "all-namespaces", false, "Download captures from all namespaces (only works with --all flag)") + downloadCapture.Flags().StringVarP(&outputPath, "output", "o", DefaultOutputPath, "Path to save the downloaded capture") + + return downloadCapture } diff --git a/cli/cmd/capture/download_test.go b/cli/cmd/capture/download_test.go new file mode 100644 index 0000000000..77e3ffcad4 --- /dev/null +++ b/cli/cmd/capture/download_test.go @@ -0,0 +1,1048 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package capture + +import ( + "context" + "errors" + "fmt" + "strings" + "testing" + + "github.com/spf13/cobra" + + captureConstants "github.com/microsoft/retina/pkg/capture/constants" + "github.com/microsoft/retina/pkg/label" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/rest" + clienttesting "k8s.io/client-go/testing" +) + +const ( + testCapture = "test-capture" + testFile = "test-file" + cmdCommand = "cmd" + shellCommand = "sh" +) + +func NewLinuxNode(name string) *corev1.Node { + return &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{ + "kubernetes.io/hostname": name, + }, + }, + Status: corev1.NodeStatus{ + NodeInfo: corev1.NodeSystemInfo{ + OperatingSystem: "linux", + OSImage: "Ubuntu 20.04 LTS", + }, + }, + } +} + +func NewWindowsNode(name string) *corev1.Node { + return &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{ + "kubernetes.io/hostname": name, + "kubernetes.io/os": "windows", + }, + }, + Status: corev1.NodeStatus{ + NodeInfo: corev1.NodeSystemInfo{ + OperatingSystem: "windows", + OSImage: "Windows Server 2022 Datacenter", + }, + }, + } +} + +func NewDownloadNamespace(name string) *corev1.Namespace { + return &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } +} + +func NewCapturePodsWithStatus(captureName, namespace, nodeName string, status corev1.PodPhase) *corev1.Pod { + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: captureName + "-" + nodeName, + Namespace: namespace, + Labels: map[string]string{ + label.CaptureNameLabel: captureName, + }, + Annotations: map[string]string{ + captureConstants.CaptureHostPathAnnotationKey: "/tmp/captures", + captureConstants.CaptureFilenameAnnotationKey: "capture-" + nodeName, + }, + }, + Spec: corev1.PodSpec{ + NodeName: nodeName, + }, + Status: corev1.PodStatus{ + Phase: status, + }, + } +} + +func newDownloadKubeClient(objects []runtime.Object) *fake.Clientset { + if objects == nil { + objects = []runtime.Object{ + NewLinuxNode("linux-node-1"), + NewWindowsNode("windows-node-1"), + NewDownloadNamespace("default"), + NewDownloadNamespace("capture-test"), + } + } + + kubeClient := fake.NewClientset(objects...) + + // Mock pod creation for download pods + kubeClient.PrependReactor("create", "pods", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + createAction, ok := action.(clienttesting.CreateAction) + if !ok { + return false, nil, fmt.Errorf("%w: expected CreateAction", ErrCreateDownloadPod) + } + pod := createAction.GetObject().(*corev1.Pod) + + // Simulate pod running after creation + pod.Status.Phase = corev1.PodRunning + return false, pod, nil + }) + + return kubeClient +} + +func TestDownloadFromCluster(t *testing.T) { + tempDir := t.TempDir() + + // Set global variables for testing + originalCaptureName := captureName + originalOutputPath := outputPath + captureName = testCapture + outputPath = tempDir + defer func() { + captureName = originalCaptureName + outputPath = originalOutputPath + }() + + testCases := []struct { + name string + namespace string + setupObjects func() []runtime.Object + wantErr bool + expectedError string + }{ + { + name: "successful capture pods found", + namespace: "default", + setupObjects: func() []runtime.Object { + return []runtime.Object{ + NewLinuxNode("linux-node-1"), + NewWindowsNode("windows-node-1"), + NewDownloadNamespace("default"), + NewCapturePodsWithStatus(testCapture, "default", "linux-node-1", corev1.PodSucceeded), + NewCapturePodsWithStatus(testCapture, "default", "windows-node-1", corev1.PodSucceeded), + } + }, + wantErr: false, + expectedError: "", + }, + { + name: "no capture pods found", + namespace: "default", + setupObjects: func() []runtime.Object { + return []runtime.Object{ + NewLinuxNode("linux-node-1"), + NewDownloadNamespace("default"), + } + }, + wantErr: true, + expectedError: "no pod found for job", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + objects := tc.setupObjects() + kubeClient := newDownloadKubeClient(objects) + + ctx := context.Background() + + // We can't easily test the full downloadFromCluster function due to + // its dependency on creating actual Kubernetes clients, so we'll focus + // on testing the service methods and individual functions + pods, err := getCapturePods(ctx, kubeClient, captureName, tc.namespace) + + if tc.wantErr && err == nil { + t.Errorf("Expected error for test case %s, but got none", tc.name) + } + if tc.wantErr && err != nil && tc.expectedError != "" && !strings.Contains(err.Error(), tc.expectedError) { + t.Errorf("Expected error message to contain %q, but got %q", tc.expectedError, err.Error()) + } + if !tc.wantErr && err != nil { + t.Errorf("Unexpected error for test case %s: %v", tc.name, err) + } + if !tc.wantErr && (pods == nil || len(pods.Items) == 0) { + t.Errorf("Expected to find capture pods for %s, but got none", tc.name) + } + + // Validate pod properties + for _, pod := range pods.Items { + if pod.Status.Phase != corev1.PodSucceeded { + t.Errorf("Expected pod phase to be Succeeded, got %s", pod.Status.Phase) + } + + if pod.Labels[label.CaptureNameLabel] != captureName { + t.Errorf("Expected pod to have capture name label %s, got %s", + captureName, pod.Labels[label.CaptureNameLabel]) + } + + // Validate required annotations exist + if _, ok := pod.Annotations[captureConstants.CaptureHostPathAnnotationKey]; !ok { + t.Errorf("Expected pod to have host path annotation") + } + + if _, ok := pod.Annotations[captureConstants.CaptureFilenameAnnotationKey]; !ok { + t.Errorf("Expected pod to have filename annotation") + } + } + + t.Logf("Successfully found %d capture pods for %s", len(pods.Items), tc.name) + }) + } +} + +func TestGetNodeOS(t *testing.T) { + testCases := []struct { + name string + node *corev1.Node + expected NodeOS + wantErr bool + }{ + { + name: "Linux node", + node: NewLinuxNode("linux-test"), + expected: Linux, + wantErr: false, + }, + { + name: "Windows node", + node: NewWindowsNode("windows-test"), + expected: Windows, + wantErr: false, + }, + { + name: "Unknown OS node", + node: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: "unknown-test"}, + Status: corev1.NodeStatus{ + NodeInfo: corev1.NodeSystemInfo{ + OperatingSystem: "darwin", + }, + }, + }, + expected: nil, // nil for unknown OS + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := getNodeOS(tc.node) + + if tc.wantErr && err == nil { + t.Errorf("Expected error for %s, but got none", tc.name) + } + if !tc.wantErr && err != nil { + t.Errorf("Unexpected error for %s: %v", tc.name, err) + } + if result != tc.expected { + t.Errorf("Expected %v, got %v for %s", tc.expected, result, tc.name) + } + }) + } +} + +func TestGetDownloadCmd(t *testing.T) { + testCases := []struct { + name string + node *corev1.Node + hostPath string + fileName string + wantErr bool + validate func(*testing.T, *DownloadCmd, error) + }{ + { + name: "Linux node download cmd", + node: NewLinuxNode("linux-test"), + hostPath: "/tmp/captures", + fileName: testCapture, + wantErr: false, + validate: func(t *testing.T, cmd *DownloadCmd, err error) { + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + if cmd == nil { + t.Fatal("Expected DownloadCmd, got nil") + } + if !strings.Contains(cmd.ContainerImage, "busybox") { + t.Errorf("Expected Linux container image to contain 'busybox', got %s", cmd.ContainerImage) + } + if !strings.Contains(cmd.SrcFilePath, "/host/tmp/captures/"+testCapture+".tar.gz") { + t.Errorf("Expected Linux source file path to match pattern, got %s", cmd.SrcFilePath) + } + if cmd.MountPath != "/host/tmp/captures" { + t.Errorf("Expected Linux mount path '/host/tmp/captures', got %s", cmd.MountPath) + } + if len(cmd.KeepAliveCommand) == 0 || cmd.KeepAliveCommand[0] != shellCommand { + t.Errorf("Expected Linux keep alive command to start with 'sh', got %v", cmd.KeepAliveCommand) + } + if len(cmd.FileCheckCommand) == 0 || cmd.FileCheckCommand[0] != shellCommand { + t.Errorf("Expected Linux file check command to start with 'sh', got %v", cmd.FileCheckCommand) + } + if len(cmd.FileReadCommand) == 0 || cmd.FileReadCommand[0] != "cat" { + t.Errorf("Expected Linux file read command to start with 'cat', got %v", cmd.FileReadCommand) + } + }, + }, + { + name: "Windows node download cmd", + node: NewWindowsNode("windows-test"), + hostPath: "/tmp/captures", + fileName: testCapture, + wantErr: false, + validate: func(t *testing.T, cmd *DownloadCmd, err error) { + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + if cmd == nil { + t.Fatal("Expected DownloadCmd, got nil") + } + if !strings.Contains(cmd.ContainerImage, "nanoserver") { + t.Errorf("Expected Windows container image to contain 'nanoserver', got %s", cmd.ContainerImage) + } + if !strings.Contains(cmd.SrcFilePath, "C:\\host\\tmp\\captures\\"+testCapture+".tar.gz") { + t.Errorf("Expected Windows source file path to match pattern, got %s", cmd.SrcFilePath) + } + if cmd.MountPath != "C:\\host\\tmp\\captures" { + t.Errorf("Expected Windows mount path 'C:\\host\\tmp\\captures', got %s", cmd.MountPath) + } + if len(cmd.KeepAliveCommand) == 0 || cmd.KeepAliveCommand[0] != cmdCommand { + t.Errorf("Expected Windows keep alive command to start with 'cmd', got %v", cmd.KeepAliveCommand) + } + if len(cmd.FileCheckCommand) == 0 || cmd.FileCheckCommand[0] != cmdCommand { + t.Errorf("Expected Windows file check command to start with 'cmd', got %v", cmd.FileCheckCommand) + } + if len(cmd.FileReadCommand) == 0 || cmd.FileReadCommand[0] != cmdCommand { + t.Errorf("Expected Windows file read command to start with 'cmd', got %v", cmd.FileReadCommand) + } + }, + }, + { + name: "Unsupported node OS", + node: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: "unsupported-test"}, + Status: corev1.NodeStatus{ + NodeInfo: corev1.NodeSystemInfo{ + OperatingSystem: "darwin", + }, + }, + }, + hostPath: "/tmp/captures", + fileName: testCapture, + wantErr: true, + validate: func(t *testing.T, cmd *DownloadCmd, err error) { + if err == nil { + t.Fatal("Expected error for unsupported OS, got nil") + } + if cmd != nil { + t.Errorf("Expected nil DownloadCmd for unsupported OS, got %v", cmd) + } + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := getDownloadCmd(tc.node, tc.hostPath, tc.fileName) + tc.validate(t, result, err) + }) + } +} + +func TestGetWindowsContainerImage(t *testing.T) { + testCases := []struct { + name string + osImage string + expectedSuffix string + }{ + { + name: "Windows Server 2022", + osImage: "Windows Server 2022 Datacenter", + expectedSuffix: "ltsc2022", + }, + { + name: "Windows Server 2025", + osImage: "Windows Server 2025 Datacenter", + expectedSuffix: "ltsc2025", + }, + { + name: "Unknown Windows version", + osImage: "Windows Server Unknown", + expectedSuffix: "ltsc2022", // Default + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + node := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: "test-node"}, + Status: corev1.NodeStatus{ + NodeInfo: corev1.NodeSystemInfo{ + OSImage: tc.osImage, + }, + }, + } + + result := getWindowsContainerImage(node) + expectedImage := "mcr.microsoft.com/windows/nanoserver:" + tc.expectedSuffix + + if result != expectedImage { + t.Errorf("Expected %s, got %s", expectedImage, result) + } + }) + } +} + +func TestDownloadService(t *testing.T) { + kubeClient := newDownloadKubeClient(nil) + config := &rest.Config{} + namespace := "test-namespace" + + service := NewDownloadService(kubeClient, config, namespace) + + // Test service creation + if service == nil { + t.Fatal("Expected service to be created, got nil") + } + + if service.config != config { + t.Error("Expected config to match") + } + if service.namespace != namespace { + t.Error("Expected namespace to match") + } +} + +func TestGetCapturePods(t *testing.T) { + testCases := []struct { + name string + captureName string + namespace string + setupPods func() []runtime.Object + expectedCount int + wantErr bool + }{ + { + name: "find capture pods successfully", + captureName: testCapture, + namespace: "default", + setupPods: func() []runtime.Object { + return []runtime.Object{ + NewCapturePodsWithStatus(testCapture, "default", "node1", corev1.PodSucceeded), + NewCapturePodsWithStatus(testCapture, "default", "node2", corev1.PodSucceeded), + } + }, + expectedCount: 2, + wantErr: false, + }, + { + name: "no capture pods found", + captureName: "nonexistent-capture", + namespace: "default", + setupPods: func() []runtime.Object { + return []runtime.Object{} + }, + expectedCount: 0, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + objects := tc.setupPods() + kubeClient := newDownloadKubeClient(objects) + + ctx := context.Background() + pods, err := getCapturePods(ctx, kubeClient, tc.captureName, tc.namespace) + + if tc.wantErr && err == nil { + t.Errorf("Expected error for %s, but got none", tc.name) + } + if !tc.wantErr && err != nil { + t.Errorf("Unexpected error for %s: %v", tc.name, err) + } + if !tc.wantErr && len(pods.Items) != tc.expectedCount { + t.Errorf("Expected %d pods, got %d", tc.expectedCount, len(pods.Items)) + } + }) + } +} + +// Mock test for blob download functionality +func TestDownloadFromBlobValidation(t *testing.T) { + // Test URL parsing validation + testCases := []struct { + name string + blobURL string + wantErr bool + }{ + { + name: "invalid URL", + blobURL: "not-a-valid-url", + wantErr: true, + }, + { + name: "valid https URL but no authentication", + blobURL: "https://storageaccount.blob.core.windows.net/container/blob", + wantErr: true, // Expect error due to missing authentication in test environment + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Set the global variable for the test + originalBlobURL := blobURL + blobURL = tc.blobURL + defer func() { blobURL = originalBlobURL }() + + err := downloadFromBlob() + + if tc.wantErr && err == nil { + t.Errorf("Expected error for %s, but got none", tc.name) + } + if !tc.wantErr && err != nil { + t.Errorf("Unexpected error for %s: %v", tc.name, err) + } + if tc.wantErr && err != nil { + t.Logf("Expected error occurred for %s: %v", tc.name, err) + } + if !tc.wantErr && err == nil { + t.Logf("Successfully completed %s", tc.name) + } + }) + } +} + +func TestDownloadServiceMethods(t *testing.T) { + ctx := context.Background() + + // Setup test objects + objects := []runtime.Object{ + NewLinuxNode("test-node"), + NewDownloadNamespace("test-namespace"), + } + + kubeClient := newDownloadKubeClient(objects) + config := &rest.Config{} + service := NewDownloadService(kubeClient, config, "test-namespace") + + t.Run("createDownloadPod creates pod correctly", func(t *testing.T) { + downloadCmd := &DownloadCmd{ + ContainerImage: "mcr.microsoft.com/azurelinux/busybox:1.36", + MountPath: "/host/tmp/captures", + KeepAliveCommand: []string{"sh", "-c", "echo 'Download pod ready'; sleep 3600"}, + } + + pod, err := service.createDownloadPod(ctx, "test-node", "/tmp/captures", testCapture, downloadCmd) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if pod == nil { + t.Fatal("Expected pod to be created, got nil") + } + + if pod.Spec.NodeName != "test-node" { + t.Errorf("Expected NodeName to be 'test-node', got: %s", pod.Spec.NodeName) + } + + if len(pod.Spec.Containers) != 1 { + t.Errorf("Expected 1 container, got: %d", len(pod.Spec.Containers)) + } + + container := pod.Spec.Containers[0] + if container.Image != downloadCmd.ContainerImage { + t.Errorf("Expected container image %s, got: %s", downloadCmd.ContainerImage, container.Image) + } + }) + + t.Run("waitForPodReady handles pod states correctly", func(t *testing.T) { + // This test is limited due to the fake client behavior + // In a real scenario, we would test timeout and different pod phases + podName := "test-pod-ready" + + // Create a pod that should be running + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName, + Namespace: service.namespace, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + }, + } + + // Add pod to client + _, err := service.kubeClient.CoreV1().Pods(service.namespace).Create(ctx, pod, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create test pod: %v", err) + } + + readyPod, err := service.waitForPodReady(ctx, podName) + if err != nil { + t.Fatalf("Expected no error waiting for pod, got: %v", err) + } + + if readyPod.Status.Phase != corev1.PodRunning { + t.Errorf("Expected pod phase Running, got: %s", readyPod.Status.Phase) + } + }) +} + +func TestDownloadServiceErrorHandling(t *testing.T) { + ctx := context.Background() + kubeClient := newDownloadKubeClient(nil) + config := &rest.Config{} + service := NewDownloadService(kubeClient, config, "test-namespace") + + t.Run("DownloadFile handles unsupported node OS", func(t *testing.T) { + // Create a node with unsupported OS + unsupportedNode := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: "unsupported-node"}, + Status: corev1.NodeStatus{ + NodeInfo: corev1.NodeSystemInfo{ + OperatingSystem: "darwin", // Unsupported OS + }, + }, + } + + // Add the node to the client + _, err := service.kubeClient.CoreV1().Nodes().Create(ctx, unsupportedNode, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create test node: %v", err) + } + + err = service.DownloadFile(ctx, "unsupported-node", "/tmp", testFile, testCapture) + if err == nil { + t.Error("Expected error for unsupported node OS, got nil") + } + + if !errors.Is(err, ErrUnsupportedNodeOS) { + t.Errorf("Expected ErrUnsupportedNodeOS, got: %v", err) + } + }) + + t.Run("DownloadFile handles missing node", func(t *testing.T) { + err := service.DownloadFile(ctx, "nonexistent-node", "/tmp", testFile, testCapture) + if err == nil { + t.Error("Expected error for missing node, got nil") + } + + if !strings.Contains(err.Error(), "failed to get node information") { + t.Errorf("Expected error about missing node, got: %v", err) + } + }) +} + +func TestDownloadCommandFlags(t *testing.T) { + testCases := []struct { + name string + args []string + validate func(*testing.T, *cobra.Command) + }{ + { + name: "valid name flag provided", + args: []string{"--name", testCapture}, + validate: func(t *testing.T, cmd *cobra.Command) { + nameFlag := cmd.Flag("name") + if nameFlag == nil { + t.Error("Expected name flag to exist") + return + } + + if nameFlag.Value.String() != testCapture { + t.Errorf("Expected name flag value '%s', got '%s'", testCapture, nameFlag.Value.String()) + } + }, + }, + { + name: "valid blob-url flag provided", + args: []string{"--blob-url", "https://storageaccount.blob.core.windows.net/container/blob?sastoken"}, + validate: func(t *testing.T, cmd *cobra.Command) { + blobURLFlag := cmd.Flag("blob-url") + if blobURLFlag == nil { + t.Error("Expected blob-url flag to exist") + return + } + + expectedURL := "https://storageaccount.blob.core.windows.net/container/blob?sastoken" + if blobURLFlag.Value.String() != expectedURL { + t.Errorf("Expected blob-url flag value '%s', got '%s'", expectedURL, blobURLFlag.Value.String()) + } + }, + }, + { + name: "name with custom output path", + args: []string{"--name", testCapture, "--output", "/tmp/downloads"}, + validate: func(t *testing.T, cmd *cobra.Command) { + nameFlag := cmd.Flag("name") + outputFlag := cmd.Flag("output") + + if nameFlag == nil { + t.Error("Expected name flag to exist") + return + } + if outputFlag == nil { + t.Error("Expected output flag to exist") + return + } + + if nameFlag.Value.String() != testCapture { + t.Errorf("Expected name flag value '%s', got '%s'", testCapture, nameFlag.Value.String()) + } + + if outputFlag.Value.String() != "/tmp/downloads" { + t.Errorf("Expected output flag value '/tmp/downloads', got '%s'", outputFlag.Value.String()) + } + }, + }, + { + name: "both name and blob-url flags", + args: []string{"--name", testCapture, "--blob-url", "https://example.com/blob"}, + validate: func(t *testing.T, cmd *cobra.Command) { + nameFlag := cmd.Flag("name") + blobURLFlag := cmd.Flag("blob-url") + + if nameFlag == nil { + t.Error("Expected name flag to exist") + return + } + if blobURLFlag == nil { + t.Error("Expected blob-url flag to exist") + return + } + + if nameFlag.Value.String() != testCapture { + t.Errorf("Expected name flag value '%s', got '%s'", testCapture, nameFlag.Value.String()) + } + + if blobURLFlag.Value.String() != "https://example.com/blob" { + t.Errorf("Expected blob-url flag value 'https://example.com/blob', got '%s'", blobURLFlag.Value.String()) + } + }, + }, + { + name: "missing required flags", + args: []string{}, + validate: func(t *testing.T, cmd *cobra.Command) { + // Test the validation logic directly by checking global variables + // Save original values + originalCaptureName := captureName + originalBlobURL := blobURL + originalDownloadAll := downloadAll + captureName = "" + blobURL = "" + downloadAll = false + defer func() { + captureName = originalCaptureName + blobURL = originalBlobURL + downloadAll = originalDownloadAll + }() + + // Test the validation condition directly + if captureName == "" && blobURL == "" && !downloadAll { + t.Log("Correctly identified missing required flags") + } else { + t.Error("Should have identified missing required flags") + } + + // Verify the command has the expected flags defined + nameFlag := cmd.Flag("name") + blobURLFlag := cmd.Flag("blob-url") + allFlag := cmd.Flag("all") + outputFlag := cmd.Flag("output") + + if nameFlag == nil { + t.Error("Expected name flag to be defined") + } + if blobURLFlag == nil { + t.Error("Expected blob-url flag to be defined") + } + if allFlag == nil { + t.Error("Expected all flag to be defined") + } + if outputFlag == nil { + t.Error("Expected output flag to be defined") + } + + // Test that flags have expected default values + if nameFlag != nil && nameFlag.DefValue != "" { + t.Errorf("Expected name flag default to be empty, got '%s'", nameFlag.DefValue) + } + if blobURLFlag != nil && blobURLFlag.DefValue != "" { + t.Errorf("Expected blob-url flag default to be empty, got '%s'", blobURLFlag.DefValue) + } + if allFlag != nil && allFlag.DefValue != "false" { + t.Errorf("Expected all flag default to be false, got '%s'", allFlag.DefValue) + } + }, + }, + { + name: "valid all flag provided", + args: []string{"--all"}, + validate: func(t *testing.T, cmd *cobra.Command) { + allFlag := cmd.Flag("all") + if allFlag == nil { + t.Error("Expected all flag to be defined") + return + } + + if allFlag.Value.String() != "true" { + t.Errorf("Expected all flag value 'true', got '%s'", allFlag.Value.String()) + } + }, + }, + { + name: "all flag with custom output path", + args: []string{"--all", "-o", "/custom/path"}, + validate: func(t *testing.T, cmd *cobra.Command) { + allFlag := cmd.Flag("all") + outputFlag := cmd.Flag("output") + + if allFlag == nil { + t.Error("Expected all flag to be defined") + return + } + if outputFlag == nil { + t.Error("Expected output flag to be defined") + return + } + + if allFlag.Value.String() != "true" { + t.Errorf("Expected all flag value 'true', got '%s'", allFlag.Value.String()) + } + + if outputFlag.Value.String() != "/custom/path" { + t.Errorf("Expected output flag value '/custom/path', got '%s'", outputFlag.Value.String()) + } + }, + }, + { + name: "all-namespaces flag with all flag", + args: []string{"--all", "--all-namespaces"}, + validate: func(t *testing.T, _ *cobra.Command) { + if !downloadAll { + t.Error("Expected downloadAll to be true") + } + if !downloadAllNamespaces { + t.Error("Expected downloadAllNamespaces to be true") + } + if captureName != "" { + t.Error("Expected captureName to be empty") + } + if blobURL != "" { + t.Error("Expected blobURL to be empty") + } + }, + }, + { + name: "all-namespaces flag without all flag (should fail validation)", + args: []string{"--all-namespaces"}, + validate: func(t *testing.T, _ *cobra.Command) { + if downloadAll { + t.Error("Expected downloadAll to be false") + } + if !downloadAllNamespaces { + t.Error("Expected downloadAllNamespaces to be true") + } + // This should fail validation in the actual command execution + // but we can't test that here since we're only parsing flags + }, + }, + { + name: "all-namespaces with name flag (should fail validation)", + args: []string{"--name", "test", "--all-namespaces"}, + validate: func(t *testing.T, _ *cobra.Command) { + if downloadAll { + t.Error("Expected downloadAll to be false") + } + if !downloadAllNamespaces { + t.Error("Expected downloadAllNamespaces to be true") + } + if captureName != "test" { + t.Error("Expected captureName to be 'test'") + } + // This should fail validation in the actual command execution + }, + }, + } + + // Test all cases with unified approach + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resetDownloadGlobals(t) + + cmd := NewDownloadSubCommand() + + // Parse flags without executing the command + err := cmd.ParseFlags(tc.args) + if err != nil { + t.Fatalf("Failed to parse flags: %v", err) + } + + tc.validate(t, cmd) + }) + } +} + +// resetDownloadGlobals zeros the package-level globals bound to the download +// command's flags and restores them on test cleanup. Cobra binds flag values +// to these globals, so without resetting between subtests one subtest's flag +// values leak into the next, making tests order-dependent. +func resetDownloadGlobals(t *testing.T) { + t.Helper() + origCaptureName := captureName + origBlobURL := blobURL + origDownloadAll := downloadAll + origDownloadAllNamespaces := downloadAllNamespaces + origOutputPath := outputPath + + captureName = "" + blobURL = "" + downloadAll = false + downloadAllNamespaces = false + outputPath = "" + + t.Cleanup(func() { + captureName = origCaptureName + blobURL = origBlobURL + downloadAll = origDownloadAll + downloadAllNamespaces = origDownloadAllNamespaces + outputPath = origOutputPath + }) +} + +func TestDownloadAllCapturesGracefulErrorHandling(t *testing.T) { + resetDownloadGlobals(t) + + // Test the graceful error handling by testing individual components + // that are used in downloadAllCaptures + + testCases := []struct { + name string + pod *corev1.Pod + expectSkip bool + description string + }{ + { + name: "successful pod", + pod: NewCapturePodsWithStatus("test-capture", "default", "node1", corev1.PodSucceeded), + expectSkip: false, + description: "Pod with Succeeded status should be processed", + }, + { + name: "failed pod", + pod: NewCapturePodsWithStatus("test-capture", "default", "node1", corev1.PodFailed), + expectSkip: true, + description: "Pod with Failed status should be skipped with warning", + }, + { + name: "running pod", + pod: NewCapturePodsWithStatus("test-capture", "default", "node1", corev1.PodRunning), + expectSkip: true, + description: "Pod with Running status should be skipped with warning", + }, + { + name: "pod missing host path annotation", + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + Labels: map[string]string{ + label.CaptureNameLabel: "test-capture", + }, + Annotations: map[string]string{ + // Missing CaptureHostPathAnnotationKey + captureConstants.CaptureFilenameAnnotationKey: "test-file", + }, + }, + Spec: corev1.PodSpec{NodeName: "node1"}, + Status: corev1.PodStatus{Phase: corev1.PodSucceeded}, + }, + expectSkip: true, + description: "Pod missing host path annotation should be skipped with warning", + }, + { + name: "pod missing filename annotation", + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + Labels: map[string]string{ + label.CaptureNameLabel: "test-capture", + }, + Annotations: map[string]string{ + captureConstants.CaptureHostPathAnnotationKey: "/tmp/captures", + // Missing CaptureFilenameAnnotationKey + }, + }, + Spec: corev1.PodSpec{NodeName: "node1"}, + Status: corev1.PodStatus{Phase: corev1.PodSucceeded}, + }, + expectSkip: true, + description: "Pod missing filename annotation should be skipped with warning", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Test the logic that would be used in downloadAllCaptures + shouldSkip := false + + // Check pod status + if tc.pod.Status.Phase != corev1.PodSucceeded { + shouldSkip = true + t.Logf("Pod %s would be skipped due to status: %s", tc.pod.Name, tc.pod.Status.Phase) + } + + // Check annotations if pod status is good + if !shouldSkip { + if _, ok := tc.pod.Annotations[captureConstants.CaptureHostPathAnnotationKey]; !ok { + shouldSkip = true + t.Logf("Pod %s would be skipped due to missing host path annotation", tc.pod.Name) + } + if _, ok := tc.pod.Annotations[captureConstants.CaptureFilenameAnnotationKey]; !ok { + shouldSkip = true + t.Logf("Pod %s would be skipped due to missing filename annotation", tc.pod.Name) + } + } + + if shouldSkip != tc.expectSkip { + t.Errorf("Expected skip=%v, got skip=%v for %s", tc.expectSkip, shouldSkip, tc.description) + } + + t.Logf("Test case '%s' completed: %s", tc.name, tc.description) + }) + } +} diff --git a/cli/cmd/capture/enums_test.go b/cli/cmd/capture/enums_test.go new file mode 100644 index 0000000000..5064432c04 --- /dev/null +++ b/cli/cmd/capture/enums_test.go @@ -0,0 +1,470 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package capture + +import ( + "testing" +) + +func TestVerbosityLevel_Validate(t *testing.T) { + tests := []struct { + name string + level VerbosityLevel + wantErr bool + }{ + { + name: "empty string (normal) is valid", + level: VerbosityNormal, + wantErr: false, + }, + { + name: "verbose is valid", + level: VerbosityVerbose, + wantErr: false, + }, + { + name: "extra is valid", + level: VerbosityExtra, + wantErr: false, + }, + { + name: "max is valid", + level: VerbosityMax, + wantErr: false, + }, + { + name: "invalid value", + level: VerbosityLevel("invalid"), + wantErr: true, + }, + { + name: "v is invalid (should use verbose)", + level: VerbosityLevel("v"), + wantErr: true, + }, + { + name: "vvv is invalid (should use max)", + level: VerbosityLevel("vvv"), + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.level.Validate() + if (err != nil) != tt.wantErr { + t.Errorf("VerbosityLevel.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestVerbosityLevel_Constants(t *testing.T) { + tests := []struct { + name string + level VerbosityLevel + expected string + }{ + { + name: "normal is empty string", + level: VerbosityNormal, + expected: "", + }, + { + name: "verbose equals 'verbose'", + level: VerbosityVerbose, + expected: "verbose", + }, + { + name: "extra equals 'extra'", + level: VerbosityExtra, + expected: "extra", + }, + { + name: "max equals 'max'", + level: VerbosityMax, + expected: "max", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if string(tt.level) != tt.expected { + t.Errorf("VerbosityLevel constant = %q, want %q", tt.level, tt.expected) + } + }) + } +} + +func TestTimestampFormat_Validate(t *testing.T) { + tests := []struct { + name string + format TimestampFormat + wantErr bool + }{ + { + name: "empty string (default) is valid", + format: TimestampDefault, + wantErr: false, + }, + { + name: "none is valid", + format: TimestampNone, + wantErr: false, + }, + { + name: "unformatted is valid", + format: TimestampUnformatted, + wantErr: false, + }, + { + name: "delta is valid", + format: TimestampDelta, + wantErr: false, + }, + { + name: "date is valid", + format: TimestampDate, + wantErr: false, + }, + { + name: "delta-since-first is valid", + format: TimestampDeltaSinceFirst, + wantErr: false, + }, + { + name: "invalid value", + format: TimestampFormat("invalid"), + wantErr: true, + }, + { + name: "epoch is invalid (should use unformatted)", + format: TimestampFormat("epoch"), + wantErr: true, + }, + { + name: "default as string is invalid (should be empty)", + format: TimestampFormat("default"), + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.format.Validate() + if (err != nil) != tt.wantErr { + t.Errorf("TimestampFormat.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestTimestampFormat_Constants(t *testing.T) { + tests := []struct { + name string + format TimestampFormat + expected string + }{ + { + name: "default is empty string", + format: TimestampDefault, + expected: "", + }, + { + name: "none equals 'none'", + format: TimestampNone, + expected: "none", + }, + { + name: "unformatted equals 'unformatted'", + format: TimestampUnformatted, + expected: "unformatted", + }, + { + name: "delta equals 'delta'", + format: TimestampDelta, + expected: "delta", + }, + { + name: "date equals 'date'", + format: TimestampDate, + expected: "date", + }, + { + name: "delta-since-first equals 'delta-since-first'", + format: TimestampDeltaSinceFirst, + expected: "delta-since-first", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if string(tt.format) != tt.expected { + t.Errorf("TimestampFormat constant = %q, want %q", tt.format, tt.expected) + } + }) + } +} + +func TestPrintDataFormat_Validate(t *testing.T) { + tests := []struct { + name string + format PrintDataFormat + wantErr bool + }{ + { + name: "empty string (none) is valid", + format: PrintDataNone, + wantErr: false, + }, + { + name: "hex is valid", + format: PrintDataHex, + wantErr: false, + }, + { + name: "hex-with-link is valid", + format: PrintDataHexWithLink, + wantErr: false, + }, + { + name: "ascii is valid", + format: PrintDataASCII, + wantErr: false, + }, + { + name: "ascii-with-link is valid", + format: PrintDataASCIIWithLink, + wantErr: false, + }, + { + name: "invalid value", + format: PrintDataFormat("invalid"), + wantErr: true, + }, + { + name: "X is invalid (should use hex)", + format: PrintDataFormat("X"), + wantErr: true, + }, + { + name: "none as string is invalid (should be empty)", + format: PrintDataFormat("none"), + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.format.Validate() + if (err != nil) != tt.wantErr { + t.Errorf("PrintDataFormat.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPrintDataFormat_Constants(t *testing.T) { + tests := []struct { + name string + format PrintDataFormat + expected string + }{ + { + name: "none is empty string", + format: PrintDataNone, + expected: "", + }, + { + name: "hex equals 'hex'", + format: PrintDataHex, + expected: "hex", + }, + { + name: "hex-with-link equals 'hex-with-link'", + format: PrintDataHexWithLink, + expected: "hex-with-link", + }, + { + name: "ascii equals 'ascii'", + format: PrintDataASCII, + expected: "ascii", + }, + { + name: "ascii-with-link equals 'ascii-with-link'", + format: PrintDataASCIIWithLink, + expected: "ascii-with-link", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if string(tt.format) != tt.expected { + t.Errorf("PrintDataFormat constant = %q, want %q", tt.format, tt.expected) + } + }) + } +} + +// TestEnumMapping_Verbosity tests that the enum values correctly map to their intended usage +func TestEnumMapping_Verbosity(t *testing.T) { + tests := []struct { + name string + cliFlag string + expectedEnum VerbosityLevel + tcpdumpFlag string // what tcpdump flag this should produce + }{ + { + name: "no flag means normal (no tcpdump verbosity)", + cliFlag: "", + expectedEnum: VerbosityNormal, + tcpdumpFlag: "(none)", + }, + { + name: "verbose produces -v", + cliFlag: "verbose", + expectedEnum: VerbosityVerbose, + tcpdumpFlag: "-v", + }, + { + name: "extra produces -vv", + cliFlag: "extra", + expectedEnum: VerbosityExtra, + tcpdumpFlag: "-vv", + }, + { + name: "max produces -vvv", + cliFlag: "max", + expectedEnum: VerbosityMax, + tcpdumpFlag: "-vvv", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + level := VerbosityLevel(tt.cliFlag) + if level != tt.expectedEnum { + t.Errorf("VerbosityLevel(%q) = %q, want %q", tt.cliFlag, level, tt.expectedEnum) + } + if err := level.Validate(); err != nil { + t.Errorf("VerbosityLevel(%q).Validate() error = %v", tt.cliFlag, err) + } + t.Logf("✓ --verbosity=%s → %s → tcpdump %s", tt.cliFlag, tt.expectedEnum, tt.tcpdumpFlag) + }) + } +} + +// TestEnumMapping_Timestamp tests that the enum values correctly map to their intended usage +func TestEnumMapping_Timestamp(t *testing.T) { + tests := []struct { + name string + cliFlag string + expectedEnum TimestampFormat + tcpdumpFlag string + }{ + { + name: "default means normal timestamps", + cliFlag: "", + expectedEnum: TimestampDefault, + tcpdumpFlag: "(default)", + }, + { + name: "none produces -t", + cliFlag: "none", + expectedEnum: TimestampNone, + tcpdumpFlag: "-t", + }, + { + name: "unformatted produces -tt", + cliFlag: "unformatted", + expectedEnum: TimestampUnformatted, + tcpdumpFlag: "-tt", + }, + { + name: "delta produces -ttt", + cliFlag: "delta", + expectedEnum: TimestampDelta, + tcpdumpFlag: "-ttt", + }, + { + name: "date produces -tttt", + cliFlag: "date", + expectedEnum: TimestampDate, + tcpdumpFlag: "-tttt", + }, + { + name: "delta-since-first produces -ttttt", + cliFlag: "delta-since-first", + expectedEnum: TimestampDeltaSinceFirst, + tcpdumpFlag: "-ttttt", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + format := TimestampFormat(tt.cliFlag) + if format != tt.expectedEnum { + t.Errorf("TimestampFormat(%q) = %q, want %q", tt.cliFlag, format, tt.expectedEnum) + } + if err := format.Validate(); err != nil { + t.Errorf("TimestampFormat(%q).Validate() error = %v", tt.cliFlag, err) + } + t.Logf("✓ --timestamp-format=%s → %s → tcpdump %s", tt.cliFlag, tt.expectedEnum, tt.tcpdumpFlag) + }) + } +} + +// TestEnumMapping_PrintData tests that the enum values correctly map to their intended usage +func TestEnumMapping_PrintData(t *testing.T) { + tests := []struct { + name string + cliFlag string + expectedEnum PrintDataFormat + tcpdumpFlag string + }{ + { + name: "default means no data printing", + cliFlag: "", + expectedEnum: PrintDataNone, + tcpdumpFlag: "(none)", + }, + { + name: "hex produces -x", + cliFlag: "hex", + expectedEnum: PrintDataHex, + tcpdumpFlag: "-x", + }, + { + name: "hex-with-link produces -xx", + cliFlag: "hex-with-link", + expectedEnum: PrintDataHexWithLink, + tcpdumpFlag: "-xx", + }, + { + name: "ascii produces -A", + cliFlag: "ascii", + expectedEnum: PrintDataASCII, + tcpdumpFlag: "-A", + }, + { + name: "ascii-with-link produces -AA", + cliFlag: "ascii-with-link", + expectedEnum: PrintDataASCIIWithLink, + tcpdumpFlag: "-AA", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + format := PrintDataFormat(tt.cliFlag) + if format != tt.expectedEnum { + t.Errorf("PrintDataFormat(%q) = %q, want %q", tt.cliFlag, format, tt.expectedEnum) + } + if err := format.Validate(); err != nil { + t.Errorf("PrintDataFormat(%q).Validate() error = %v", tt.cliFlag, err) + } + t.Logf("✓ --print-data=%s → %s → tcpdump %s", tt.cliFlag, tt.expectedEnum, tt.tcpdumpFlag) + }) + } +} diff --git a/cli/cmd/capture/list.go b/cli/cmd/capture/list.go index 43e5667f05..8beda63b06 100644 --- a/cli/cmd/capture/list.go +++ b/cli/cmd/capture/list.go @@ -25,35 +25,35 @@ var listExample = templates.Examples(i18n.T(` kubectl retina capture list --all-namespaces `)) -var listCaptures = &cobra.Command{ - Use: "list", - Short: "List Retina Captures", - Example: listExample, - RunE: func(*cobra.Command, []string) error { - kubeConfig, err := opts.ToRESTConfig() - if err != nil { - return errors.Wrap(err, "failed to compose k8s rest config") - } - - kubeClient, err := kubernetes.NewForConfig(kubeConfig) - if err != nil { - return errors.Wrap(err, "failed to initialize kubernetes client") - } - - // Create a context that is canceled when a termination signal is received - ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM) - defer cancel() - - captureNamespace := *opts.Namespace - if allNamespaces { - captureNamespace = "" - } - return listCapturesInNamespaceAndPrintCaptureResults(ctx, kubeClient, captureNamespace) - }, -} +func NewListSubCommand() *cobra.Command { + listCaptures := &cobra.Command{ + Use: "list", + Short: "List Retina Captures", + Example: listExample, + RunE: func(*cobra.Command, []string) error { + kubeConfig, err := opts.ToRESTConfig() + if err != nil { + return errors.Wrap(err, "failed to compose k8s rest config") + } + + kubeClient, err := kubernetes.NewForConfig(kubeConfig) + if err != nil { + return errors.Wrap(err, "failed to initialize kubernetes client") + } + + // Create a context that is canceled when a termination signal is received + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM) + defer cancel() + + captureNamespace := *opts.Namespace + if allNamespaces { + captureNamespace = "" + } + return listCapturesInNamespaceAndPrintCaptureResults(ctx, kubeClient, captureNamespace) + }, + } -func init() { - capture.AddCommand(listCaptures) listCaptures.Flags().BoolVarP(&allNamespaces, "all-namespaces", "A", allNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.") + return listCaptures } diff --git a/cli/cmd/capture/table_util.go b/cli/cmd/capture/table_util.go index 3f3c5389a4..65fc343bc4 100644 --- a/cli/cmd/capture/table_util.go +++ b/cli/cmd/capture/table_util.go @@ -21,7 +21,7 @@ import ( "k8s.io/client-go/kubernetes" ) -func getCaptureAndPrintCaptureResult(ctx context.Context, kubeClient *kubernetes.Clientset, name, namespace string) error { +func getCaptureAndPrintCaptureResult(ctx context.Context, kubeClient kubernetes.Interface, name, namespace string) error { return listCapturesAndPrintCaptureResults(ctx, kubeClient, name, namespace) } @@ -30,7 +30,7 @@ func listCapturesInNamespaceAndPrintCaptureResults(ctx context.Context, kubeClie } // listCapturesAndPrintCaptureResults list captures and print the running jobs into properly aligned text. -func listCapturesAndPrintCaptureResults(ctx context.Context, kubeClient *kubernetes.Clientset, name, namespace string) error { +func listCapturesAndPrintCaptureResults(ctx context.Context, kubeClient kubernetes.Interface, name, namespace string) error { jobListOpt := metav1.ListOptions{} if len(name) != 0 { captureJobSelector := &metav1.LabelSelector{ @@ -74,32 +74,26 @@ func printCaptureResult(captureJobs []batchv1.Job) { w := new(tabwriter.Writer) w.Init(os.Stdout, 0, 8, 3, ' ', 0) - fmt.Fprintln(w, "NAMESPACE\tCAPTURE NAME\tJOBS\tCOMPLETIONS\tAGE") - for captureRef, jobs := range captureToJobs { - captureRef := strings.Split(captureRef, "/") - captureNamespace, captureName := captureRef[0], captureRef[1] - jobNames := []string{} - completedJobNum := 0 - age := "" - totalJobNum := len(jobs) - for _, job := range jobs { - jobNames = append(jobNames, job.Name) - if job.Status.CompletionTime != nil { - completedJobNum += 1 - } - } - sort.SliceStable(jobNames, func(i, j int) bool { - return jobNames[i] < jobNames[j] - }) - if len(jobs) > 0 { - age = durationUtil.HumanDuration(time.Since(jobs[0].CreationTimestamp.Time)) - } + fmt.Fprintln(w, "NAMESPACE\tCAPTURE NAME\tJOB\tCOMPLETIONS\tAGE") - jobsNameJoined := strings.Join(jobNames, ",") + for captureRef := range captureToJobs { + jobs := captureToJobs[captureRef] + captureParts := strings.Split(captureRef, "/") + captureNamespace, captureName := captureParts[0], captureParts[1] - completions := fmt.Sprintf("%d/%d", completedJobNum, totalJobNum) - rr := fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t", captureNamespace, captureName, jobsNameJoined, completions, age) - fmt.Fprintln(w, rr) + sort.SliceStable(jobs, func(i, j int) bool { + return jobs[i].Name < jobs[j].Name + }) + + for i := range jobs { + job := &jobs[i] + var completions string + if job.Spec.Completions != nil { + completions = fmt.Sprintf("%d/%d", job.Status.Succeeded, *job.Spec.Completions) + } + age := durationUtil.HumanDuration(time.Since(job.CreationTimestamp.Time)) + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", captureNamespace, captureName, job.Name, completions, age) + } } w.Flush() fmt.Println() diff --git a/cli/cmd/shell.go b/cli/cmd/shell.go index b388c720eb..d8f5c101c4 100644 --- a/cli/cmd/shell.go +++ b/cli/cmd/shell.go @@ -24,6 +24,8 @@ var ( retinaShellImageVersion string mountHostFilesystem bool allowHostFilesystemWrite bool + appArmorUnconfined bool + seccompUnconfined bool hostPID bool capabilities []string timeout time.Duration @@ -113,6 +115,8 @@ var shellCmd = &cobra.Command{ AllowHostFilesystemWrite: allowHostFilesystemWrite, HostPID: hostPID, Capabilities: capabilities, + AppArmorUnconfined: appArmorUnconfined, + SeccompUnconfined: seccompUnconfined, Timeout: timeout, } @@ -163,6 +167,8 @@ func init() { shellCmd.Flags().BoolVar(&hostPID, "host-pid", false, "Set HostPID on the shell container. Applies only to nodes, not pods.") shellCmd.Flags().StringSliceVarP(&capabilities, "capabilities", "c", []string{}, "Add capabilities to the shell container") shellCmd.Flags().DurationVar(&timeout, "timeout", defaultTimeout, "The maximum time to wait for the shell container to start") + shellCmd.Flags().BoolVar(&appArmorUnconfined, "apparmor-unconfined", false, "Set AppArmor profile type to unconfined. Applies only to nodes, not pods.") + shellCmd.Flags().BoolVar(&seccompUnconfined, "seccomp-unconfined", false, "Set Seccomp profile type to unconfined. Applies only to nodes, not pods.") // configFlags and matchVersion flags are used to load kubeconfig. // This uses the same mechanism as `kubectl debug` to connect to apiserver and attach to containers. diff --git a/cli/main.go b/cli/main.go index b6aa648ea0..c32c138aae 100644 --- a/cli/main.go +++ b/cli/main.go @@ -7,10 +7,16 @@ import ( "os" "github.com/microsoft/retina/cli/cmd" - _ "github.com/microsoft/retina/cli/cmd/capture" + "github.com/microsoft/retina/cli/cmd/capture" ) func main() { + kubeClient, err := capture.GetClientset() + if err != nil { + fmt.Printf("Failed to get Kubernetes client: %v\n", err) + os.Exit(1) + } + cmd.Retina.AddCommand(capture.NewCommand(kubeClient)) if err := cmd.Retina.Execute(); err != nil { fmt.Println(err) os.Exit(1) diff --git a/cmd/hubble/cells_linux.go b/cmd/hubble/cells_linux.go index e6777640a1..dd05e51aed 100644 --- a/cmd/hubble/cells_linux.go +++ b/cmd/hubble/cells_linux.go @@ -5,35 +5,69 @@ package hubble import ( + "log/slog" + "sync" + + "github.com/cilium/cilium/pkg/datapath/link" "github.com/cilium/cilium/pkg/defaults" "github.com/cilium/cilium/pkg/gops" hubblecell "github.com/cilium/cilium/pkg/hubble/cell" - exportercell "github.com/cilium/cilium/pkg/hubble/exporter/cell" - hubbleParser "github.com/cilium/cilium/pkg/hubble/parser" - "github.com/cilium/cilium/pkg/ipcache" - "github.com/cilium/cilium/pkg/k8s" + metricscell "github.com/cilium/cilium/pkg/hubble/metrics/cell" + ciliumparser "github.com/cilium/cilium/pkg/hubble/parser" k8sClient "github.com/cilium/cilium/pkg/k8s/client" - "github.com/cilium/cilium/pkg/logging" + "github.com/cilium/cilium/pkg/kpr" + "github.com/cilium/cilium/pkg/kvstore" "github.com/cilium/cilium/pkg/logging/logfields" "github.com/cilium/cilium/pkg/node/manager" "github.com/cilium/cilium/pkg/option" "github.com/cilium/cilium/pkg/pprof" - "github.com/cilium/cilium/pkg/recorder" "github.com/cilium/hive/cell" - "github.com/sirupsen/logrus" + "github.com/cilium/statedb" "k8s.io/client-go/rest" "github.com/microsoft/retina/internal/buildinfo" "github.com/microsoft/retina/pkg/config" rnode "github.com/microsoft/retina/pkg/controllers/daemon/nodereconciler" "github.com/microsoft/retina/pkg/hubble/parser" + "github.com/microsoft/retina/pkg/hubble/resources" retinak8s "github.com/microsoft/retina/pkg/k8s" + retinalog "github.com/microsoft/retina/pkg/log" "github.com/microsoft/retina/pkg/managers/pluginmanager" "github.com/microsoft/retina/pkg/monitoragent" "github.com/microsoft/retina/pkg/servermanager" "github.com/microsoft/retina/pkg/shared/telemetry" ) +// disabledKVStoreClient wraps a kvstore.Client but returns IsEnabled() = false. +// This is needed because K8sCiliumEndpointsWatcher only initializes if kvstore is disabled. +// When kvstore is enabled, Cilium expects CiliumEndpoint data to come from kvstore, +// but Retina watches CiliumEndpoint CRDs directly and needs the watcher to populate IPCache. +type disabledKVStoreClient struct { + kvstore.Client +} + +// IsEnabled returns false to indicate kvstore is not being used for CiliumEndpoint sync. +// This allows the K8sCiliumEndpointsWatcher to initialize and populate IPCache with K8sMetadata. +func (d *disabledKVStoreClient) IsEnabled() bool { + return false +} + +const daemonSubsys = "daemon" + +var ( + loggerOnce sync.Once + cachedLogger *slog.Logger +) + +// logger returns a zap-backed slog logger. Resolved lazily so it reaches +// Application Insights once SetupZapLogger has run. +func logger() *slog.Logger { + loggerOnce.Do(func() { + cachedLogger = retinalog.SlogLogger().With(logfields.LogSubsys, daemonSubsys) + }) + return cachedLogger +} + var ( Agent = cell.Module( "agent", @@ -41,8 +75,6 @@ var ( Infrastructure, ControlPlane, ) - daemonSubsys = "daemon" - logger = logging.DefaultLogger.WithField(logfields.LogSubsys, daemonSubsys) Infrastructure = cell.Module( "infrastructure", @@ -64,6 +96,19 @@ var ( // Kubernetes client k8sClient.Cell, + // Kube proxy replacement config (needed by loadbalancer cells) + kpr.Cell, + + // Provide a disabled kvstore client for Retina. + // This is important: the K8sCiliumEndpointsWatcher only initializes + // if kvstore.IsEnabled() returns false (because with a real kvstore, + // CiliumEndpoint data would come from kvstore instead of watching CRDs). + // Since Retina doesn't use etcd/consul and relies on watching CiliumEndpoint CRDs, + // we need IsEnabled() to return false so the watcher populates IPCache with K8sMetadata. + cell.Provide(func(db *statedb.DB) kvstore.Client { + return &disabledKVStoreClient{Client: kvstore.NewInMemoryClient(db, "default")} + }), + cell.Provide(func(cfg config.Config, k8sCfg *rest.Config) telemetry.Config { return telemetry.Config{ Component: "retina-agent", @@ -95,13 +140,11 @@ var ( retinak8s.Cell, - recorder.Cell, + // Provides resources for hubble + resources.Cell, - cell.Provide( - func(l logrus.FieldLogger, ipc *ipcache.IPCache, sc *k8s.ServiceCacheImpl) hubbleParser.Decoder { - return parser.New(l.WithField("decoder", nil), sc, ipc) - }, - ), + // Provides link cache needed by hubble parser + link.Cell, // Provides the node reconciler as node manager rnode.Cell, @@ -111,9 +154,17 @@ var ( }, ), - exportercell.Cell, - // Provides the hubble agent - hubblecell.Core, + // Provides the full hubble agent (includes parser, exporter, metrics, and TLS) + hubblecell.Cell, + + // Force the Hubble metrics server to start. Without this, the DI system + // prunes newMetricsServer because nothing in Retina consumes metricscell.Server. + cell.Invoke(func(_ metricscell.Server) {}), + + // Override Cilium's parser with Retina's parser that understands v1.Event from plugins + cell.DecorateAll(func(_ ciliumparser.Decoder, params parser.Params) ciliumparser.Decoder { + return parser.New(params) + }), telemetry.Heartbeat, ) diff --git a/cmd/hubble/daemon_linux.go b/cmd/hubble/daemon_linux.go index 58636e761d..58c281a4a5 100644 --- a/cmd/hubble/daemon_linux.go +++ b/cmd/hubble/daemon_linux.go @@ -7,11 +7,12 @@ package hubble import ( "context" "fmt" + "log/slog" "github.com/pkg/errors" - "github.com/sirupsen/logrus" "github.com/microsoft/retina/pkg/config" + "github.com/microsoft/retina/pkg/log" "github.com/microsoft/retina/pkg/managers/pluginmanager" "github.com/microsoft/retina/pkg/managers/servermanager" @@ -20,7 +21,6 @@ import ( v1 "github.com/cilium/cilium/pkg/hubble/api/v1" hubblecell "github.com/cilium/cilium/pkg/hubble/cell" "github.com/cilium/cilium/pkg/ipcache" - "github.com/cilium/cilium/pkg/k8s" k8sClient "github.com/cilium/cilium/pkg/k8s/client" "github.com/cilium/cilium/pkg/k8s/watchers" monitoragent "github.com/cilium/cilium/pkg/monitor/agent" @@ -34,7 +34,6 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/log" - zapf "sigs.k8s.io/controller-runtime/pkg/log/zap" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" ) @@ -44,7 +43,9 @@ var ( "daemon", "Retina-Agent Daemon", // Create the controller manager, provides the hive with the controller manager and its client - cell.Provide(func(k8sCfg *rest.Config, logger logrus.FieldLogger, rcfg config.RetinaHubbleConfig) (ctrl.Manager, client.Client, error) { + cell.Provide(func( + k8sCfg *rest.Config, logger *slog.Logger, rcfg config.RetinaHubbleConfig, + ) (ctrl.Manager, client.Client, error) { if err := corev1.AddToScheme(scheme); err != nil { //nolint:govet // intentional shadow logger.Error("failed to add corev1 to scheme") return nil, nil, errors.Wrap(err, "failed to add corev1 to scheme") @@ -60,7 +61,7 @@ var ( LeaderElectionID: "ecaf1259.retina.io", } - logf.SetLogger(zapf.New()) + logf.SetLogger(log.LogrLogger()) ctrlManager, err := ctrl.NewManager(k8sCfg, mgrOption) if err != nil { logger.Error("failed to create manager") @@ -71,7 +72,7 @@ var ( }), // Start the controller manager - cell.Invoke(func(l logrus.FieldLogger, lifecycle cell.Lifecycle, ctrlManager ctrl.Manager) { + cell.Invoke(func(l *slog.Logger, lifecycle cell.Lifecycle, ctrlManager ctrl.Manager) { var wp *workerpool.WorkerPool lifecycle.Append( cell.Hook{ @@ -99,7 +100,7 @@ var ( type Daemon struct { clientset k8sClient.Clientset - log logrus.FieldLogger + log *slog.Logger monitorAgent monitoragent.Agent pluginManager *pluginmanager.PluginManager HTTPServer *servermanager.HTTPServer @@ -108,7 +109,6 @@ type Daemon struct { k8swatcher *watchers.K8sWatcher localNodeStore *node.LocalNodeStore ipc *ipcache.IPCache - svcCache k8s.ServiceCache hubble hubblecell.HubbleIntegration } @@ -124,17 +124,12 @@ func newDaemon(params *daemonParams) *Daemon { k8swatcher: params.K8sWatcher, localNodeStore: params.Lnds, ipc: params.IPC, - svcCache: params.SvcCache, hubble: params.Hubble, } } func (d *Daemon) Run(ctx context.Context) error { - // Start K8s watcher - d.log.WithField("localNodeStore", d.localNodeStore).Info("Starting local node store") - - // Start K8s watcher. Will block till sync is complete or timeout. - // If sync doesn't complete within timeout (3 minutes), causes fatal error. + // Start K8s watcher. Blocks until sync completes or times out (3 min). retinak8s.Start(ctx, d.k8swatcher) go d.generateEvents(ctx) @@ -147,10 +142,8 @@ func (d *Daemon) generateEvents(ctx context.Context) { case <-ctx.Done(): return case event := <-d.eventChan: - d.log.WithField("event", event).Debug("Sending event to monitor agent") - err := d.monitorAgent.SendEvent(0, event) - if err != nil { - d.log.WithError(err).Error("Unable to send event to monitor agent") + if err := d.monitorAgent.SendEvent(0, event); err != nil { + d.log.Error("Unable to send event to monitor agent", "error", err) } } } diff --git a/cmd/hubble/daemon_main_linux.go b/cmd/hubble/daemon_main_linux.go index c4a609e3b5..ba240a2722 100644 --- a/cmd/hubble/daemon_main_linux.go +++ b/cmd/hubble/daemon_main_linux.go @@ -8,20 +8,17 @@ package hubble import ( "context" "fmt" - "io" "log/slog" - "os" "path/filepath" - zaphook "github.com/Sytten/logrus-zap-hook" "github.com/cilium/cilium/pkg/hive" v1 "github.com/cilium/cilium/pkg/hubble/api/v1" hubblecell "github.com/cilium/cilium/pkg/hubble/cell" "github.com/cilium/cilium/pkg/ipcache" - "github.com/cilium/cilium/pkg/k8s" k8sClient "github.com/cilium/cilium/pkg/k8s/client" "github.com/cilium/cilium/pkg/k8s/watchers" "github.com/cilium/cilium/pkg/logging" + "github.com/cilium/cilium/pkg/logging/logfields" "github.com/cilium/cilium/pkg/metrics" monitorAgent "github.com/cilium/cilium/pkg/monitor/agent" "github.com/cilium/cilium/pkg/node" @@ -38,7 +35,6 @@ import ( "github.com/microsoft/retina/pkg/managers/servermanager" sharedconfig "github.com/microsoft/retina/pkg/shared/config" "github.com/microsoft/retina/pkg/telemetry" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" "go.uber.org/zap" @@ -60,8 +56,11 @@ func InitGlobalFlags(cmd *cobra.Command, vp *viper.Viper) { flags.String(option.ConfigDir, "/retina/config", `Configuration directory that contains a file for each option`) option.BindEnv(vp, option.ConfigDir) + // Set the default value for the endpoint GC interval to 0, which disables it. + vp.Set(option.EndpointGCInterval, 0) + if err := vp.BindPFlags(flags); err != nil { - logger.Fatalf("BindPFlags failed: %s", err) + logging.Fatal(logger(), fmt.Sprintf("BindPFlags failed: %s", err)) } } @@ -73,13 +72,12 @@ type daemonParams struct { MonitorAgent monitorAgent.Agent PluginManager *pluginmanager.PluginManager HTTPServer *servermanager.HTTPServer - Log logrus.FieldLogger + Log *slog.Logger Client client.Client EventChan chan *v1.Event K8sWatcher *watchers.K8sWatcher Lnds *node.LocalNodeStore IPC *ipcache.IPCache - SvcCache k8s.ServiceCache Telemetry telemetry.Telemetry Hubble hubblecell.HubbleIntegration Config config.Config @@ -98,7 +96,7 @@ func newDaemonPromise(params daemonParams) promise.Promise[*Daemon] { daemon = d daemonResolver.Resolve(daemon) - d.log.Info("starting Retina Enterprise version: ", buildinfo.Version) + d.log.Info("starting Retina", "version", buildinfo.Version) err := d.Run(daemonCtx) if err != nil { return fmt.Errorf("daemon run failed: %w", err) @@ -114,27 +112,32 @@ func newDaemonPromise(params daemonParams) promise.Promise[*Daemon] { return daemonPromise } -func initLogging() { - logger := setupDefaultLogger() - retinaConfig, _ := getRetinaConfig(logger) +// initZap wires Retina's zap logger (including the Application Insights sink) +// and redirects Go's stdlib slog default at it. The Cilium-side tee must wait +// until *after* logging.SetupLogging runs, because SetupLogging calls +// MultiSlogHandler.SetHandler which replaces the handler list. +func initZap() { + retinaConfig, _ := getRetinaConfig() k8sCfg, _ := sharedconfig.GetK8sConfig() - zapLogger := setupZapLogger(retinaConfig, k8sCfg) - setupLoggingHooks(logger, zapLogger) - bootstrapLogging(retinaConfig, logger) + setupZapLogger(retinaConfig, k8sCfg) + + // Route Go's stdlib slog default through zap so any bare slog call hits AI. + log.SetDefaultSlog() } -func setupDefaultLogger() *logrus.Logger { - logger := logging.DefaultLogger - logger.ReportCaller = true - logger.SetOutput(io.Discard) - return logger +func initLogging() { + bootstrapLogging() + // Register zap + metrics hook AFTER SetupLogging so they survive its + // defaultMultiSlogHandler.SetHandler replace. From this point every + // logging.DefaultSlogLogger emission fans out to zap → Application Insights. + logging.AddHandlers(log.SlogHandler(), metrics.NewLoggingHook()) } -func getRetinaConfig(logger *logrus.Logger) (*config.Config, error) { +func getRetinaConfig() (*config.Config, error) { retinaConfigFile := filepath.Join(option.Config.ConfigDir, configFileName) conf, err := config.GetConfig(retinaConfigFile) if err != nil { - logger.WithError(err).Error("Failed to get config file") + logging.DefaultSlogLogger.Error("Failed to get config file", "error", err) return nil, fmt.Errorf("getting config from file %q: %w", configFileName, err) } return conf, nil @@ -161,7 +164,7 @@ func setupZapLogger(retinaConfig *config.Config, k8sCfg *rest.Config) *log.ZapLo _, err := log.SetupZapLogger(logOpts, persistentFields...) if err != nil { - logger.Fatalf("Failed to setup zap logger: %v", err) + logging.Fatal(logger(), fmt.Sprintf("Failed to setup zap logger: %v", err)) } namedLogger := log.Logger().Named("retina-with-hubble") @@ -170,52 +173,42 @@ func setupZapLogger(retinaConfig *config.Config, k8sCfg *rest.Config) *log.ZapLo return namedLogger } -func setupLoggingHooks(logger *logrus.Logger, zapLogger *log.ZapLogger) { - logger.Hooks.Add(metrics.NewLoggingHook()) - - zapHook, err := zaphook.NewZapHook(zapLogger.Logger) - if err != nil { - logger.WithError(err).Error("Failed to create zap hook") - } else { - logger.Hooks.Add(zapHook) - } -} - -func bootstrapLogging(retinaConfig *config.Config, logger *logrus.Logger) { +func bootstrapLogging() { if err := logging.SetupLogging(option.Config.LogDriver, logging.LogOptions(option.Config.LogOpt), "retina-agent", option.Config.Debug); err != nil { - logger.Fatal(err) - } - - logLevel, err := logrus.ParseLevel(retinaConfig.LogLevel) - if err != nil { - logLevel = logrus.InfoLevel + logging.Fatal(logging.DefaultSlogLogger, err.Error()) } - logger.SetLevel(logLevel) } func initDaemonConfig(vp *viper.Viper) { - option.Config.Populate(vp) + // slogloggercheck: using default logger for configuration initialization + option.Config.Populate(logging.DefaultSlogLogger, vp) time.MaxInternalTimerDelay = vp.GetDuration(option.MaxInternalTimerDelay) } func Execute(cobraCmd *cobra.Command, h *hive.Hive) { - fn := option.InitConfig(cobraCmd, "retina-agent", "retina", h.Viper()) + // slogloggercheck: using default logger for configuration initialization + fn := option.InitConfig(logging.DefaultSlogLogger, cobraCmd, "retina-agent", "retina", h.Viper()) fn() initDaemonConfig(h.Viper()) + + // Bring up zap + Application Insights first, then tee Cilium's slog into it. + // Any subsequent log that flows through Cilium's DefaultSlogLogger (including + // SetupLogging output below) now reaches AI. + initZap() initLogging() - hiveLogger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + hiveLogger := log.SlogLogger() // Allow the current process to lock memory for eBPF resources. if err := rlimit.RemoveMemlock(); err != nil { - logger.Fatal("failed to remove memlock", zap.Error(err)) + logging.Fatal(logger(), "failed to remove memlock", logfields.Error, err) } //nolint:gocritic // without granular commits this commented-out code may be lost // initEnv(h.Viper()) if err := h.Run(hiveLogger); err != nil { - logger.Fatal(err) + logging.Fatal(logger(), "Hive Run failed", logfields.Error, err) } } diff --git a/cmd/standard/daemon.go b/cmd/standard/daemon.go index 121bf70645..b93763054e 100644 --- a/cmd/standard/daemon.go +++ b/cmd/standard/daemon.go @@ -4,6 +4,7 @@ package standard import ( "fmt" + "log/slog" "os" "strings" @@ -24,6 +25,7 @@ import ( crmgr "sigs.k8s.io/controller-runtime/pkg/manager" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "github.com/cilium/cilium/pkg/logging" "github.com/go-logr/zapr" retinav1alpha1 "github.com/microsoft/retina/crd/api/v1alpha1" "github.com/microsoft/retina/internal/buildinfo" @@ -128,6 +130,10 @@ func (d *Daemon) Start() error { panic(err) } defer zl.Close() + // Tee Cilium's MultiSlogHandler into zap so any DefaultSlogLogger call + // (including package-var captures) reaches Application Insights. + logging.AddHandlers(log.SlogHandler()) + log.SetDefaultSlog() // Set Go's global slog to use zap-backed handler mainLogger := zl.Named("main").Sugar() // Allow the current process to lock memory for eBPF resources. @@ -137,7 +143,7 @@ func (d *Daemon) Start() error { mainLogger.Fatal("failed to remove memlock", zap.Error(err)) } - metrics.InitializeMetrics() + metrics.InitializeMetrics(slog.Default()) mainLogger.Info(zap.String("data aggregation level", daemonConfig.DataAggregationLevel.String())) @@ -241,7 +247,7 @@ func (d *Daemon) Start() error { controllerCache := controllercache.New(pubSub) enrich := enricher.New(ctx, controllerCache) //nolint:govet // shadowing this err is fine - fm, err := filtermanager.Init(5) //nolint:gomnd // defaults + fm, err := filtermanager.Init(5, daemonConfig.FilterMapMaxEntries) //nolint:gomnd // defaults if err != nil { mainLogger.Fatal("unable to create filter manager", zap.Error(err)) } @@ -294,7 +300,7 @@ func (d *Daemon) Start() error { } } - controllerMgr, err := cm.NewControllerManager(daemonConfig, cl, tel) + controllerMgr, err := cm.NewControllerManager(daemonConfig, cl, tel, slog.Default()) if err != nil { mainLogger.Fatal("Failed to create controller manager", zap.Error(err)) } diff --git a/controller/Dockerfile b/controller/Dockerfile index b3cd8c5c89..1dd3b35583 100644 --- a/controller/Dockerfile +++ b/controller/Dockerfile @@ -1,22 +1,13 @@ -# Only applicable for windows images -ARG OS_VERSION=ltsc2022 # pinned base images -# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.24.4-azurelinux3.0 --format "{{.Name}}@{{.Digest}}" -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/oss/go/microsoft/golang:1.24.4-azurelinux3.0@sha256:250d01e55a37bd79d7014ae83f9f50aa6fa5570ca910e7f19faeff4bb0132ae1 AS golang +# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.26.3-azurelinux3.0 --format "{{.Name}}@{{.Digest}}" +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/oss/go/microsoft/golang:1.26.3-1-azurelinux3.0@sha256:ef480755a4126131197d7311ab1e24d55600407194b45349c4975b7ed0d176e6 AS golang # skopeo inspect docker://mcr.microsoft.com/azurelinux/base/core:3.0 --format "{{.Name}}@{{.Digest}}" -FROM mcr.microsoft.com/azurelinux/base/core:3.0@sha256:35149ae8dd179684f969944f54a337c665a64e702486154eb44253fb39c2505b AS azurelinux-core +FROM mcr.microsoft.com/azurelinux/base/core:3.0.20260517@sha256:f5e224c47997aa4a5d3d8addfcc3866e175e7026368a71ce1be2c0eed1876f04 AS azurelinux-core # skopeo inspect docker://mcr.microsoft.com/azurelinux/distroless/minimal:3.0 --format "{{.Name}}@{{.Digest}}" -FROM mcr.microsoft.com/azurelinux/distroless/minimal:3.0@sha256:5a66f9f16ac675db2a8229dac72d83811b73b502d6ad192d8b374c7f3be498af AS azurelinux-distroless - -# skopeo inspect docker://mcr.microsoft.com/windows/servercore:ltsc2019 --override-os windows --format "{{.Name}}@{{.Digest}}" -FROM mcr.microsoft.com/windows/servercore@sha256:cd96c9b4873aba2f63716934ed5e535ec21c8d3dc32d29d9bc778be28f19cbfa AS ltsc2019 - -# skopeo inspect docker://mcr.microsoft.com/windows/servercore:ltsc2022 --override-os windows --format "{{.Name}}@{{.Digest}}" -FROM mcr.microsoft.com/windows/servercore@sha256:86da395cfd2b35dbfc2e9d08719550c51b0570c394bff8f92622a19234766185 AS ltsc2022 - +FROM mcr.microsoft.com/azurelinux/distroless/minimal:3.0.20260517@sha256:0c64ab9cfc44d4f100c0590bd59ead9afedda6cc54f14bb7465b5f9c35ddc037 AS azurelinux-distroless # build stages @@ -28,21 +19,21 @@ ARG GOOS=linux # default to linux ENV GOARCH=${GOARCH} ENV GOOS=${GOOS} RUN if [ "$GOOS" = "linux" ] ; then \ - tdnf install -y clang lld bpftool libbpf-devel; \ + tdnf install -y clang lld bpftool libbpf-devel; \ fi COPY ./pkg/plugin /go/src/github.com/microsoft/retina/pkg/plugin WORKDIR /go/src/github.com/microsoft/retina RUN if [ "$GOOS" = "linux" ] ; then \ - go mod init github.com/microsoft/retina; \ - go generate -skip "mockgen" -x /go/src/github.com/microsoft/retina/pkg/plugin/...; \ - tar czf /gen.tar.gz ./pkg/plugin; \ - rm go.mod; \ + go mod init github.com/microsoft/retina; \ + go generate -skip "mockgen" -x /go/src/github.com/microsoft/retina/pkg/plugin/...; \ + tar czf /gen.tar.gz ./pkg/plugin; \ + rm go.mod; \ fi COPY ./go.mod ./go.sum ./ RUN go mod download COPY . . RUN if [ "$GOOS" = "linux" ] ; then \ - rm -rf ./pkg/plugin && tar xvf /gen.tar.gz ./pkg/plugin; \ + rm -rf ./pkg/plugin && tar xvf /gen.tar.gz ./pkg/plugin; \ fi @@ -56,7 +47,6 @@ ENV GOARCH=${GOARCH} ENV GOOS=${GOOS} RUN --mount=type=cache,target="/root/.cache/go-build" go build -v -o /go/bin/retina/captureworkload -ldflags "-X github.com/microsoft/retina/internal/buildinfo.Version="$VERSION" -X github.com/microsoft/retina/internal/buildinfo.ApplicationInsightsID="$APP_INSIGHTS_ID"" captureworkload/main.go - # controller binary FROM intermediate AS controller-bin ARG APP_INSIGHTS_ID # set to enable AI telemetry @@ -67,7 +57,6 @@ ENV GOARCH=${GOARCH} ENV GOOS=${GOOS} RUN --mount=type=cache,target="/root/.cache/go-build" go build -x -v -o /go/bin/retina/controller -ldflags "-X github.com/microsoft/retina/internal/buildinfo.Version="$VERSION" -X github.com/microsoft/retina/internal/buildinfo.ApplicationInsightsID="$APP_INSIGHTS_ID"" controller/main.go - # init binary FROM intermediate AS init-bin ARG APP_INSIGHTS_ID # set to enable AI telemetry @@ -78,11 +67,11 @@ ENV GOARCH=${GOARCH} ENV GOOS=${GOOS} RUN --mount=type=cache,target="/root/.cache/go-build" go build -v -o /go/bin/retina/initretina -ldflags "-X github.com/microsoft/retina/internal/buildinfo.Version="$VERSION" -X github.com/microsoft/retina/internal/buildinfo.ApplicationInsightsID="$APP_INSIGHTS_ID"" init/retina/main_linux.go - # tools image FROM azurelinux-core AS tools RUN tdnf install -y \ clang \ + bpftool \ iproute \ iptables \ tcpdump \ @@ -96,11 +85,13 @@ RUN arr="clang tcpdump ip ss iptables-legacy iptables-legacy-save iptables-nft i for i in $arr; do \ cp $(which $i) /tmp/bin; \ done +RUN mkdir -p /tmp/init-bin +RUN cp $(which bpftool) /tmp/init-bin # Download Hubble ARG GOARCH=amd64 ENV HUBBLE_ARCH=${GOARCH} # ARG HUBBLE_VERSION may be modified via the update-hubble GitHub Action -ARG HUBBLE_VERSION=v1.17.3 +ARG HUBBLE_VERSION=v1.19.3 ENV HUBBLE_VERSION=${HUBBLE_VERSION} RUN echo "Hubble version: $HUBBLE_VERSION" && \ wget --no-check-certificate https://github.com/cilium/hubble/releases/download/$HUBBLE_VERSION/hubble-linux-${HUBBLE_ARCH}.tar.gz && \ @@ -114,15 +105,17 @@ FROM azurelinux-distroless AS init COPY --from=init-bin /go/bin/retina/initretina /retina/initretina COPY --from=tools /lib/ /lib COPY --from=tools /usr/lib/ /usr/lib +COPY --from=tools /etc/pki/tls/ /etc/pki/tls/ +COPY --from=tools /tmp/init-bin/ /bin/ ENTRYPOINT ["./retina/initretina"] - # agent final image # mcr.microsoft.com/azurelinux/distroless/minimal:3.0 -# mcr.microsoft.com/azurelinux/distroless/minimal@sha256:0801b80a0927309572b9adc99bd1813bc680473175f6e8175cd4124d95dbd50c +# mcr.microsoft.com/azurelinux/distroless/minimal@sha256:5a66f9f16ac675db2a8229dac72d83811b73b502d6ad192d8b374c7f3be498af FROM azurelinux-distroless AS agent COPY --from=tools /lib/ /lib COPY --from=tools /usr/lib/ /usr/lib +COPY --from=tools /etc/pki/tls/ /etc/pki/tls/ COPY --from=tools /tmp/bin/ /bin COPY --from=controller-bin /go/bin/retina/controller /retina/controller COPY --from=controller-bin /go/src/github.com/microsoft/retina/pkg/plugin /go/src/github.com/microsoft/retina/pkg/plugin @@ -132,13 +125,3 @@ COPY --from=tools /usr/local/hubble /bin/hubble # Set Hubble server. ENV HUBBLE_SERVER=unix:///var/run/cilium/hubble.sock ENTRYPOINT ["./retina/controller"] - - -# agent final image for windows -FROM ${OS_VERSION} AS agent-win -COPY --from=controller-bin /go/src/github.com/microsoft/retina/windows/kubeconfigtemplate.yaml kubeconfigtemplate.yaml -COPY --from=controller-bin /go/src/github.com/microsoft/retina/windows/setkubeconfigpath.ps1 setkubeconfigpath.ps1 -COPY --from=controller-bin /go/bin/retina/controller controller.exe -COPY --from=capture-bin /go/bin/retina/captureworkload captureworkload.exe -ADD https://github.com/microsoft/etl2pcapng/releases/download/v1.10.0/etl2pcapng.exe /etl2pcapng.exe -CMD ["controller.exe", "start", "--kubeconfig=.\\kubeconfig"] diff --git a/controller/Dockerfile.gogen b/controller/Dockerfile.gogen index 5059c92256..d0bfe348bc 100644 --- a/controller/Dockerfile.gogen +++ b/controller/Dockerfile.gogen @@ -1,5 +1,5 @@ -# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.24.4-azurelinux3.0 --format "{{.Name}}@{{.Digest}}" -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/oss/go/microsoft/golang:1.24.4-azurelinux3.0@sha256:250d01e55a37bd79d7014ae83f9f50aa6fa5570ca910e7f19faeff4bb0132ae1 +# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.26.3-azurelinux3.0 --format "{{.Name}}@{{.Digest}}" +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/oss/go/microsoft/golang:1.26.3-1-azurelinux3.0@sha256:ef480755a4126131197d7311ab1e24d55600407194b45349c4975b7ed0d176e6 # Default linux/architecture. ARG GOOS=linux @@ -24,4 +24,4 @@ RUN ln -s /usr/bin/clang-14 /usr/bin/clang WORKDIR /app # Generate go code. -ENTRYPOINT mkdir /tmp/.cache && export GOCACHE=/tmp/.cache && CGO_ENABLED=0 go generate ./... +ENTRYPOINT mkdir /tmp/.cache && export GOCACHE=/tmp/.cache && go generate ./... diff --git a/controller/Dockerfile.proto b/controller/Dockerfile.proto index c261a2d965..cc7f706d39 100644 --- a/controller/Dockerfile.proto +++ b/controller/Dockerfile.proto @@ -1,5 +1,5 @@ -# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.24.4-azurelinux3.0 --format "{{.Name}}@{{.Digest}}" -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/oss/go/microsoft/golang:1.24.4-azurelinux3.0@sha256:250d01e55a37bd79d7014ae83f9f50aa6fa5570ca910e7f19faeff4bb0132ae1 +# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.26.3-azurelinux3.0 --format "{{.Name}}@{{.Digest}}" +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/oss/go/microsoft/golang:1.26.3-1-azurelinux3.0@sha256:ef480755a4126131197d7311ab1e24d55600407194b45349c4975b7ed0d176e6 LABEL Name=retina-builder Version=0.0.1 @@ -13,4 +13,6 @@ RUN unzip protoc-24.2-linux-x86_64.zip -d protoc RUN mv protoc/bin/protoc /usr/bin/protoc WORKDIR /app -ENTRYPOINT protoc -I=. --go_out=paths=source_relative:. ./pkg/utils/metadata_linux.proto +ENTRYPOINT /bin/sh -c "protoc -I=. --go_out=paths=source_relative:. ./pkg/utils/metadata_linux.proto && \ + protoc -I=. --go_out=paths=source_relative:. ./pkg/utils/metadata_darwin.proto && \ + protoc -I=. --go_out=paths=source_relative:. ./pkg/utils/metadata_windows.proto" diff --git a/controller/Dockerfile.windows-2019 b/controller/Dockerfile.windows-2019 deleted file mode 100644 index 882cac36e7..0000000000 --- a/controller/Dockerfile.windows-2019 +++ /dev/null @@ -1,27 +0,0 @@ -# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.24.2-2-cbl-mariner2.0 --format "{{.Name}}@{{.Digest}}" -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/oss/go/microsoft/golang@sha256:5341a0010ecff114ee2f11f5eaa4f73b721b54142954041523f3e785d5c4b978 AS builder - -# Build args -ARG VERSION -ARG APP_INSIGHTS_ID - -ENV GOOS=windows -ENV GOARCH=amd64 - -WORKDIR /usr/src/retina -# Copy the source -COPY . . - -RUN --mount=type=cache,target="/root/.cache/go-build" go build -v -o /usr/bin/controller.exe -ldflags "-X github.com/microsoft/retina/internal/buildinfo.Version="$VERSION" -X "github.com/microsoft/retina/internal/buildinfo.ApplicationInsightsID"="$APP_INSIGHTS_ID"" ./controller/ -RUN --mount=type=cache,target="/root/.cache/go-build" go build -v -o /usr/bin/captureworkload.exe ./captureworkload/ - -# Copy into final image -FROM mcr.microsoft.com/windows/servercore:ltsc2019 as final -COPY --from=builder /usr/src/retina/windows/kubeconfigtemplate.yaml kubeconfigtemplate.yaml -COPY --from=builder /usr/src/retina/windows/setkubeconfigpath.ps1 setkubeconfigpath.ps1 -COPY --from=builder /usr/bin/controller.exe controller.exe -COPY --from=builder /usr/bin/captureworkload.exe captureworkload.exe - -ADD https://github.com/microsoft/etl2pcapng/releases/download/v1.10.0/etl2pcapng.exe /etl2pcapng.exe - -CMD ["controller.exe", "start", "--kubeconfig=.\\kubeconfig"] diff --git a/controller/Dockerfile.windows-2022 b/controller/Dockerfile.windows-2022 index 0697cb69d5..4872556a03 100644 --- a/controller/Dockerfile.windows-2022 +++ b/controller/Dockerfile.windows-2022 @@ -1,27 +1,24 @@ -# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.24.2-2-cbl-mariner2.0 --format "{{.Name}}@{{.Digest}}" -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/oss/go/microsoft/golang@sha256:5341a0010ecff114ee2f11f5eaa4f73b721b54142954041523f3e785d5c4b978 AS builder - -# Build args -ARG VERSION -ARG APP_INSIGHTS_ID - -ENV GOOS=windows -ENV GOARCH=amd64 - -WORKDIR /usr/src/retina -# Copy the source -COPY . . - -RUN --mount=type=cache,target="/root/.cache/go-build" go build -v -o /usr/bin/controller.exe -ldflags "-X github.com/microsoft/retina/internal/buildinfo.Version="$VERSION" -X "github.com/microsoft/retina/internal/buildinfo.ApplicationInsightsID"="$APP_INSIGHTS_ID"" ./controller/ -RUN --mount=type=cache,target="/root/.cache/go-build" go build -v -o /usr/bin/captureworkload.exe ./captureworkload/ - -# Copy into final image -FROM --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2022 as final -COPY --from=builder /usr/src/retina/windows/kubeconfigtemplate.yaml kubeconfigtemplate.yaml -COPY --from=builder /usr/src/retina/windows/setkubeconfigpath.ps1 setkubeconfigpath.ps1 -COPY --from=builder /usr/bin/controller.exe controller.exe -COPY --from=builder /usr/bin/captureworkload.exe captureworkload.exe +# pinned base image +# skopeo inspect docker://mcr.microsoft.com/windows/servercore:ltsc2022 --override-os windows --format "{{.Name}}@{{.Digest}}" +FROM mcr.microsoft.com/windows/servercore:ltsc2022@sha256:86da395cfd2b35dbfc2e9d08719550c51b0570c394bff8f92622a19234766185 AS ltsc2022 +FROM ltsc2022 AS agent-win +ARG GOARCH=amd64 # default to amd64 +ARG GOOS=windows # default to windows +ARG OS_VERSION=ltsc2022 +ARG REPO_PATH +ARG BINARIES_PATH +ENV GOARCH=${GOARCH} +ENV GOOS=${GOOS} +ENV OS_VERSION=${OS_VERSION} +ENV BINARIES_PATH=${BINARIES_PATH} +ENV REPO_PATH=${REPO_PATH} +# CVE-2013-3900 Mitigation +RUN reg add "HKEY_LOCAL_MACHINE\Software\Microsoft\Cryptography\Wintrust\Config" /v "EnableCertPaddingCheck" /t REG_DWORD /d "1" /f +RUN reg add "HKEY_LOCAL_MACHINE\Software\Wow6432Node\Microsoft\Cryptography\Wintrust\Config" /v "EnableCertPaddingCheck" /t REG_DWORD /d "1" /f +COPY ${REPO_PATH}/windows/kubeconfigtemplate.yaml kubeconfigtemplate.yaml +COPY ${REPO_PATH}/windows/setkubeconfigpath.ps1 setkubeconfigpath.ps1 +COPY ${BINARIES_PATH}/captureworkload.exe captureworkload.exe +COPY ${BINARIES_PATH}/controller.exe controller.exe ADD https://github.com/microsoft/etl2pcapng/releases/download/v1.10.0/etl2pcapng.exe /etl2pcapng.exe - CMD ["controller.exe", "start", "--kubeconfig=.\\kubeconfig"] diff --git a/controller/Dockerfile.windows-cgo b/controller/Dockerfile.windows-cgo index ad35301c04..d5e322ba84 100644 --- a/controller/Dockerfile.windows-cgo +++ b/controller/Dockerfile.windows-cgo @@ -1,5 +1,5 @@ -# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.24.2-2-windowsservercore-ltsc2022 --format "{{.Name}}@{{.Digest}}" -FROM --platform=windows/amd64 mcr.microsoft.com/oss/go/microsoft/golang@sha256:0fb2afcf9c877c90ca609984303da41846c447ca62c0f9e42046be113cae117e AS cgo +# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.26.3-windowsservercore-ltsc2022 --override-os windows --format "{{.Name}}@{{.Digest}}" +FROM --platform=windows/amd64 mcr.microsoft.com/oss/go/microsoft/golang:1.26.3-1-windowsservercore-ltsc2022@sha256:7ebc4adb9d8a8359d3d0fb6d1479962fdab5b275045e78d70e4b7cdf8f76455b AS cgo SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] diff --git a/controller/Dockerfile.windows-native b/controller/Dockerfile.windows-native index e90cae6267..9eea0159b8 100644 --- a/controller/Dockerfile.windows-native +++ b/controller/Dockerfile.windows-native @@ -3,8 +3,8 @@ # buildx targets, and this one requires legacy build. # Maybe one day: https://github.com/moby/buildkit/issues/616 ARG BUILDER_IMAGE -# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.24.2-2-windowsservercore-ltsc2022 --format "{{.Name}}@{{.Digest}}" -FROM --platform=windows/amd64 mcr.microsoft.com/oss/go/microsoft/golang@sha256:0fb2afcf9c877c90ca609984303da41846c447ca62c0f9e42046be113cae117e AS builder +# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.26.3-windowsservercore-ltsc2022 --override-os windows --format "{{.Name}}@{{.Digest}}" +FROM --platform=windows/amd64 mcr.microsoft.com/oss/go/microsoft/golang:1.26.3-1-windowsservercore-ltsc2022@sha256:7ebc4adb9d8a8359d3d0fb6d1479962fdab5b275045e78d70e4b7cdf8f76455b AS builder WORKDIR C:\\retina COPY go.mod . COPY go.sum . @@ -23,7 +23,8 @@ RUN go build -v -o captureworkload.exe -ldflags="-X github.com/microsoft/retina/ FROM --platform=windows/amd64 ${BUILDER_IMAGE} as pktmon-builder WORKDIR C:\\retina -FROM --platform=windows/amd64 mcr.microsoft.com/windows/nanoserver:ltsc2022 AS final +# skopeo inspect docker://mcr.microsoft.com/windows/nanoserver:ltsc2022 --override-os windows --format "{{.Name}}@{{.Digest}}" +FROM --platform=windows/amd64 mcr.microsoft.com/windows/nanoserver:ltsc2022@sha256:3ca7daac9e971bd440920e408a29d46545b717775794c71561d57ac2f9354a48 AS final ADD https://github.com/microsoft/etl2pcapng/releases/download/v1.10.0/etl2pcapng.exe /etl2pcapng.exe SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'Continue';"] COPY --from=builder C:\\retina\\windows\\kubeconfigtemplate.yaml kubeconfigtemplate.yaml diff --git a/controller/Dockerfile.windows-retina-oss-build b/controller/Dockerfile.windows-retina-oss-build index 1be0c27847..a6c6346883 100644 --- a/controller/Dockerfile.windows-retina-oss-build +++ b/controller/Dockerfile.windows-retina-oss-build @@ -2,12 +2,8 @@ ARG OS_VERSION=ltsc2022 # pinned base images -# mcr.microsoft.com/windows/servercore:ltsc2019 -FROM mcr.microsoft.com/windows/servercore@sha256:cd96c9b4873aba2f63716934ed5e535ec21c8d3dc32d29d9bc778be28f19cbfa AS ltsc2019 - -# mcr.microsoft.com/windows/servercore:ltsc2022 -FROM mcr.microsoft.com/windows/servercore@sha256:86da395cfd2b35dbfc2e9d08719550c51b0570c394bff8f92622a19234766185 AS ltsc2022 - +# skopeo inspect docker://mcr.microsoft.com/windows/servercore:ltsc2022 --override-os windows --format "{{.Name}}@{{.Digest}}" +FROM mcr.microsoft.com/windows/servercore:ltsc2022@sha256:86da395cfd2b35dbfc2e9d08719550c51b0570c394bff8f92622a19234766185 AS ltsc2022 FROM ${OS_VERSION} AS agent-win ARG GOARCH=amd64 # default to amd64 @@ -20,9 +16,12 @@ ENV GOOS=${GOOS} ENV OS_VERSION=${OS_VERSION} ENV BINARIES_PATH=${BINARIES_PATH} ENV REPO_PATH=${REPO_PATH} -COPY ${REPO_PATH}/retina/windows/kubeconfigtemplate.yaml kubeconfigtemplate.yaml -COPY ${REPO_PATH}/retina/windows/setkubeconfigpath.ps1 setkubeconfigpath.ps1 +# CVE-2013-3900 Mitigation +RUN reg add "HKEY_LOCAL_MACHINE\Software\Microsoft\Cryptography\Wintrust\Config" /v "EnableCertPaddingCheck" /t REG_DWORD /d "1" /f +RUN reg add "HKEY_LOCAL_MACHINE\Software\Wow6432Node\Microsoft\Cryptography\Wintrust\Config" /v "EnableCertPaddingCheck" /t REG_DWORD /d "1" /f +COPY ${REPO_PATH}/windows/kubeconfigtemplate.yaml kubeconfigtemplate.yaml +COPY ${REPO_PATH}/windows/setkubeconfigpath.ps1 setkubeconfigpath.ps1 COPY ${BINARIES_PATH}/captureworkload.exe captureworkload.exe COPY ${BINARIES_PATH}/controller.exe controller.exe ADD https://github.com/microsoft/etl2pcapng/releases/download/v1.10.0/etl2pcapng.exe /etl2pcapng.exe -CMD ["controller.exe", "start", "--kubeconfig=.\\kubeconfig"] \ No newline at end of file +CMD ["controller.exe", "start", "--kubeconfig=.\\kubeconfig"] diff --git a/crd/api/v1alpha1/capture_types.go b/crd/api/v1alpha1/capture_types.go index 70fab30ace..c90ecf7958 100644 --- a/crd/api/v1alpha1/capture_types.go +++ b/crd/api/v1alpha1/capture_types.go @@ -65,6 +65,91 @@ type CaptureOption struct { // +kubebuilder:default=100 // +optional MaxCaptureSize *int `json:"maxCaptureSize,omitempty"` + + // Interfaces specifies the network interfaces on which to capture packets. + // If specified, captures only on the listed interfaces (e.g., ["eth0", "eth1"]). + // If empty, captures on all interfaces by default. + // Use this field to select specific interfaces, NOT the tcpdumpFilter field. + // +optional + Interfaces []string `json:"interfaces,omitempty"` + + // PcapFilter specifies a BPF filter expression for packet filtering (e.g., "tcp port 443", "host 10.0.0.1"). + // Only BPF expressions are allowed, no flags. See https://www.tcpdump.org/manpages/pcap-filter.7.html + // +optional + // +kubebuilder:validation:MaxLength=1024 + // +kubebuilder:validation:Pattern="^[^-]*$" + PcapFilter *string `json:"pcapFilter,omitempty"` + + // NoPromiscuous disables promiscuous mode for packet capture. + // When true, only packets destined for this host are captured (equivalent to tcpdump -p flag). + // When false or unset, captures all packets on the network segment (default behavior). + // +optional + NoPromiscuous *bool `json:"noPromiscuous,omitempty"` + + // PacketBuffered enables packet-buffered output mode (equivalent to tcpdump -U flag). + // When true, packets are written to output as soon as they're captured rather than being buffered. + // Useful for real-time monitoring but may impact performance. + // +optional + PacketBuffered *bool `json:"packetBuffered,omitempty"` + + // ImmediateMode enables immediate mode for packet capture (equivalent to tcpdump --immediate-mode). + // When true, packets are delivered to the application immediately rather than being buffered. + // This can reduce latency but may increase CPU usage. + // +optional + ImmediateMode *bool `json:"immediateMode,omitempty"` + + // NoResolveDNS disables DNS resolution for captured addresses (equivalent to tcpdump -n flag). + // When true, IP addresses are displayed numerically without resolving hostnames. + // This speeds up capture processing and avoids DNS lookup overhead. + // +optional + NoResolveDNS *bool `json:"noResolveDNS,omitempty"` + + // NoResolvePort disables port name resolution (equivalent to tcpdump -nn flag). + // When true, both IP addresses and port numbers are displayed numerically. + // This prevents service name lookups for port numbers. + // +optional + NoResolvePort *bool `json:"noResolvePort,omitempty"` + + // Verbosity controls the verbosity level of packet capture output. + // Valid values: "" (normal/default), "verbose" (tcpdump -v), "extra" (tcpdump -vv), "max" (tcpdump -vvv). + // Empty string means normal verbosity with no extra verbose flags. + // +optional + // +kubebuilder:validation:Enum="";verbose;extra;max + Verbosity *string `json:"verbosity,omitempty"` + + // PrintDataFormat controls how packet data is printed in the output. + // Valid values: "" (none), "hex" (tcpdump -x), "hex-with-link" (tcpdump -xx), "ascii" (tcpdump -A), "ascii-with-link" (tcpdump -AA). + // Empty string means no packet data printing. + // +optional + // +kubebuilder:validation:Enum="";hex;hex-with-link;ascii;ascii-with-link + PrintDataFormat *string `json:"printDataFormat,omitempty"` + + // PrintLinkHeader prints link-level (Ethernet) headers (equivalent to tcpdump -e flag). + // Shows MAC addresses and other link-layer information. + // +optional + PrintLinkHeader *bool `json:"printLinkHeader,omitempty"` + + // QuietOutput enables quiet/quick output mode (equivalent to tcpdump -q flag). + // Prints less protocol information for shorter output lines. + // +optional + QuietOutput *bool `json:"quietOutput,omitempty"` + + // AbsoluteSeq prints absolute TCP sequence numbers (equivalent to tcpdump -S flag). + // Shows actual sequence numbers instead of relative numbers. + // +optional + AbsoluteSeq *bool `json:"absoluteSeq,omitempty"` + + // TimestampFormat controls the timestamp format in packet capture output. + // Valid values: "" (default), "none" (tcpdump -t), "unformatted" (tcpdump -tt), "delta" (tcpdump -ttt), "date" (tcpdump -tttt), "delta-since-first" (tcpdump -ttttt). + // Empty string means default timestamp format. + // +optional + // +kubebuilder:validation:Enum="";none;unformatted;delta;date;delta-since-first + TimestampFormat *string `json:"timestampFormat,omitempty"` + + // DontVerifyChecksum disables TCP checksum verification (equivalent to tcpdump -K flag). + // Skips TCP checksum validation for captured packets. + // +optional + DontVerifyChecksum *bool `json:"dontVerifyChecksum,omitempty"` } // CaptureTarget indicates the target on which the network packets capture will be performed. @@ -85,6 +170,12 @@ type CaptureTarget struct { // selector semantics. // +optional PodSelector *metav1.LabelSelector `json:"podSelector,omitempty"` + + // PodNames allows selecting specific pods by their names. + // If specified, the capture will be performed on the pods with matching names in the specified namespace. + // PodNames is incompatible with NodeSelector, NamespaceSelector, and PodSelector. + // +optional + PodNames []string `json:"podNames,omitempty"` } // CaptureConfiguration indicates the configurations of the network capture. @@ -94,8 +185,13 @@ type CaptureConfiguration struct { // +optional Filters *CaptureConfigurationFilters `json:"filters,omitempty"` - // TcpdumpFilter is a raw tcpdump filter string. + // TcpdumpFilter accepts BPF filter expressions only (no flags). + // + // DEPRECATED and will be removed: Currently functional but scheduled for removal. + // Use captureOption.pcapFilter for BPF expressions and captureOption boolean flags for tcpdump display options instead. // +optional + // +kubebuilder:validation:MaxLength=1024 + // +kubebuilder:validation:Pattern="^[^-]*$" TcpdumpFilter *string `json:"tcpdumpFilter,omitempty"` // IncludeMetadata represents whether or not networking metadata should be captured. @@ -129,8 +225,16 @@ type CaptureConfigurationFilters struct { // OutputConfiguration indicates the location capture will be stored. type OutputConfiguration struct { - // HostPath stores the capture files into the specified host filesystem. - // If nothing exists at the given path of the host, an empty directory will be created there. + // HostPath is a relative subpath name (e.g. "my-capture") joined under the + // operator-configured host base directory (default /var/log/retina/captures) + // on every node that runs a capture pod. The capture files are written to + // that joined directory, and an empty directory is created there if it does + // not already exist. + // + // HostPath must be a relative subpath: absolute paths (e.g. "/tmp/foo", + // "C:\\foo") and any value containing ".." segments are rejected by the + // operator. CR authors cannot influence the base directory, which is + // controlled by the cluster operator via the operator config. // +optional HostPath *string `json:"hostPath,omitempty"` // PersistentVolumeClaim mounts the supplied PVC into the pod on `/capture` and write the capture files there. @@ -168,6 +272,14 @@ type CaptureSpec struct { CaptureConfiguration CaptureConfiguration `json:"captureConfiguration"` // +kubebuilder:validation:Required OutputConfiguration OutputConfiguration `json:"outputConfiguration,omitempty"` + // CleanUpAfterUpload indicates whether the capture jobs and associated resources + // should be automatically cleaned up after a successful upload to remote storage + // (BlobUpload or S3Upload). When set to true, completed capture jobs, secrets, + // and the Capture resource itself will be deleted once all jobs have succeeded + // and uploads are confirmed. + // +optional + // +kubebuilder:default=false + CleanUpAfterUpload bool `json:"cleanUpAfterUpload,omitempty"` } // +kubebuilder:object:root=true diff --git a/crd/api/v1alpha1/metricsconfiguration_types.go b/crd/api/v1alpha1/metricsconfiguration_types.go index 18902c6fa6..0044493a4d 100644 --- a/crd/api/v1alpha1/metricsconfiguration_types.go +++ b/crd/api/v1alpha1/metricsconfiguration_types.go @@ -41,6 +41,10 @@ type MetricsContextOptions struct { // +optional // +listType=set AdditionalLabels []string `json:"additionalLabels,omitempty"` + // TTL represents the time-to-live of the metrics collected + // Metrics which have not been updated within the TTL will be removed from export + // +optional + TTL string `json:"ttl,omitempty"` } // MetricsNamespaces indicates the namespaces to include or exclude in metric collection diff --git a/crd/api/v1alpha1/validations/validate_metricconfiguration.go b/crd/api/v1alpha1/validations/validate_metricconfiguration.go index 0a7a456d40..4945b40726 100644 --- a/crd/api/v1alpha1/validations/validate_metricconfiguration.go +++ b/crd/api/v1alpha1/validations/validate_metricconfiguration.go @@ -6,12 +6,16 @@ Licensed under the MIT license. package validations import ( + "errors" "fmt" + "time" "github.com/microsoft/retina/crd/api/v1alpha1" "github.com/microsoft/retina/pkg/utils" ) +var ErrNegativeTTL = errors.New("TTL cannot be negative") + // MetricsConfiguration validates the metrics configuration func MetricsCRD(metricsConfig *v1alpha1.MetricsConfiguration) error { if metricsConfig == nil { @@ -40,6 +44,15 @@ func MetricsSpec(metricsSpec v1alpha1.MetricsSpec) error { if !utils.IsAdvancedMetric(contextOption.MetricName) { return fmt.Errorf("%s is not a valid metric", contextOption.MetricName) } + if contextOption.TTL != "" { + ttl, err := time.ParseDuration(contextOption.TTL) + if err != nil { + return fmt.Errorf("invalid TTL format for metric %s: %w", contextOption.MetricName, err) + } + if ttl < 0 { + return fmt.Errorf("%w for metric %s", ErrNegativeTTL, contextOption.MetricName) + } + } } err := MetricsNamespaces(metricsSpec.Namespaces) @@ -152,10 +165,13 @@ func MetricsContextOptionsCompare(old, new []v1alpha1.MetricsContextOptions) boo return false } - if !utils.CompareStringSlice(oldContextOption.AdditionalLabels, newContextOption.AdditionalLabels) { + if oldContextOption.TTL != newContextOption.TTL { return false } + if !utils.CompareStringSlice(oldContextOption.AdditionalLabels, newContextOption.AdditionalLabels) { + return false + } } return true diff --git a/crd/api/v1alpha1/validations/validate_metricconfiguration_test.go b/crd/api/v1alpha1/validations/validate_metricconfiguration_test.go index d5197facb0..93626f29a3 100644 --- a/crd/api/v1alpha1/validations/validate_metricconfiguration_test.go +++ b/crd/api/v1alpha1/validations/validate_metricconfiguration_test.go @@ -98,6 +98,86 @@ func TestMetricsConfiguration(t *testing.T) { }, wantErr: false, }, + { + name: "valid metrics crd with TTL", + obj: &v1alpha1.MetricsConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "metricsconfig", + }, + Spec: v1alpha1.MetricsSpec{ + ContextOptions: []v1alpha1.MetricsContextOptions{ + { + MetricName: "drop_count", + TTL: "24h", + }, + }, + Namespaces: v1alpha1.MetricsNamespaces{ + Exclude: []string{"kube-system"}, + }, + }, + }, + wantErr: false, + }, + { + name: "valid metrics crd with zero TTL", + obj: &v1alpha1.MetricsConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "metricsconfig", + }, + Spec: v1alpha1.MetricsSpec{ + ContextOptions: []v1alpha1.MetricsContextOptions{ + { + MetricName: "drop_count", + TTL: "0", + }, + }, + Namespaces: v1alpha1.MetricsNamespaces{ + Exclude: []string{"kube-system"}, + }, + }, + }, + wantErr: false, + }, + { + name: "invalid metrics crd with TTL", + obj: &v1alpha1.MetricsConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "metricsconfig", + }, + Spec: v1alpha1.MetricsSpec{ + ContextOptions: []v1alpha1.MetricsContextOptions{ + { + MetricName: "drop_count", + TTL: "24", + }, + }, + Namespaces: v1alpha1.MetricsNamespaces{ + Exclude: []string{"kube-system"}, + }, + }, + }, + wantErr: true, + }, + { + name: "invalid metrics crd with negative TTL", + obj: &v1alpha1.MetricsConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "metricsconfig", + }, + Spec: v1alpha1.MetricsSpec{ + ContextOptions: []v1alpha1.MetricsContextOptions{ + { + MetricName: "drop_count", + TTL: "-24h", + }, + }, + Namespaces: v1alpha1.MetricsNamespaces{ + Exclude: []string{"kube-system"}, + }, + }, + }, + wantErr: true, + }, { name: "invalid metrics crd with random metric name", obj: &v1alpha1.MetricsConfiguration{ @@ -348,6 +428,125 @@ func TestCompare(t *testing.T) { }, equal: true, }, + { + name: "valid test 6", + old: &v1alpha1.MetricsConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "metricsconfig", + }, + Spec: v1alpha1.MetricsSpec{ + ContextOptions: []v1alpha1.MetricsContextOptions{ + { + MetricName: "drop_count", + SourceLabels: []string{"ns", "ip", "port"}, + TTL: "24h", + }, + }, + Namespaces: v1alpha1.MetricsNamespaces{ + Include: []string{"default", "test"}, + Exclude: []string{"kube-system"}, + }, + }, + }, + new: &v1alpha1.MetricsConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "metricsconfig", + }, + Spec: v1alpha1.MetricsSpec{ + ContextOptions: []v1alpha1.MetricsContextOptions{ + { + MetricName: "drop_count", + SourceLabels: []string{"ip", "port", "ns"}, + }, + }, + Namespaces: v1alpha1.MetricsNamespaces{ + Include: []string{"default", "test"}, + Exclude: []string{"kube-system"}, + }, + }, + }, + equal: false, + }, + { + name: "valid test 7", + old: &v1alpha1.MetricsConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "metricsconfig", + }, + Spec: v1alpha1.MetricsSpec{ + ContextOptions: []v1alpha1.MetricsContextOptions{ + { + MetricName: "drop_count", + SourceLabels: []string{"ns", "ip", "port"}, + TTL: "24h", + }, + }, + Namespaces: v1alpha1.MetricsNamespaces{ + Include: []string{"default", "test"}, + Exclude: []string{"kube-system"}, + }, + }, + }, + new: &v1alpha1.MetricsConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "metricsconfig", + }, + Spec: v1alpha1.MetricsSpec{ + ContextOptions: []v1alpha1.MetricsContextOptions{ + { + MetricName: "drop_count", + SourceLabels: []string{"ip", "port", "ns"}, + TTL: "24h", + }, + }, + Namespaces: v1alpha1.MetricsNamespaces{ + Include: []string{"default", "test"}, + Exclude: []string{"kube-system"}, + }, + }, + }, + equal: true, + }, + { + name: "valid test 8", + old: &v1alpha1.MetricsConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "metricsconfig", + }, + Spec: v1alpha1.MetricsSpec{ + ContextOptions: []v1alpha1.MetricsContextOptions{ + { + MetricName: "drop_count", + SourceLabels: []string{"ns", "ip", "port"}, + TTL: "24h", + }, + }, + Namespaces: v1alpha1.MetricsNamespaces{ + Include: []string{"default", "test"}, + Exclude: []string{"kube-system"}, + }, + }, + }, + new: &v1alpha1.MetricsConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "metricsconfig", + }, + Spec: v1alpha1.MetricsSpec{ + ContextOptions: []v1alpha1.MetricsContextOptions{ + { + MetricName: "drop_count", + SourceLabels: []string{"ip", "port", "ns"}, + TTL: "12h", + }, + }, + Namespaces: v1alpha1.MetricsNamespaces{ + Include: []string{"default", "test"}, + Exclude: []string{"kube-system"}, + }, + }, + }, + equal: false, + }, } for _, tt := range tests { diff --git a/crd/api/v1alpha1/zz_generated.deepcopy.go b/crd/api/v1alpha1/zz_generated.deepcopy.go index 48b9c3bdfb..9ce7d26d4b 100644 --- a/crd/api/v1alpha1/zz_generated.deepcopy.go +++ b/crd/api/v1alpha1/zz_generated.deepcopy.go @@ -138,6 +138,76 @@ func (in *CaptureOption) DeepCopyInto(out *CaptureOption) { *out = new(int) **out = **in } + if in.Interfaces != nil { + in, out := &in.Interfaces, &out.Interfaces + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.PcapFilter != nil { + in, out := &in.PcapFilter, &out.PcapFilter + *out = new(string) + **out = **in + } + if in.NoPromiscuous != nil { + in, out := &in.NoPromiscuous, &out.NoPromiscuous + *out = new(bool) + **out = **in + } + if in.PacketBuffered != nil { + in, out := &in.PacketBuffered, &out.PacketBuffered + *out = new(bool) + **out = **in + } + if in.ImmediateMode != nil { + in, out := &in.ImmediateMode, &out.ImmediateMode + *out = new(bool) + **out = **in + } + if in.NoResolveDNS != nil { + in, out := &in.NoResolveDNS, &out.NoResolveDNS + *out = new(bool) + **out = **in + } + if in.NoResolvePort != nil { + in, out := &in.NoResolvePort, &out.NoResolvePort + *out = new(bool) + **out = **in + } + if in.Verbosity != nil { + in, out := &in.Verbosity, &out.Verbosity + *out = new(string) + **out = **in + } + if in.PrintDataFormat != nil { + in, out := &in.PrintDataFormat, &out.PrintDataFormat + *out = new(string) + **out = **in + } + if in.PrintLinkHeader != nil { + in, out := &in.PrintLinkHeader, &out.PrintLinkHeader + *out = new(bool) + **out = **in + } + if in.QuietOutput != nil { + in, out := &in.QuietOutput, &out.QuietOutput + *out = new(bool) + **out = **in + } + if in.AbsoluteSeq != nil { + in, out := &in.AbsoluteSeq, &out.AbsoluteSeq + *out = new(bool) + **out = **in + } + if in.TimestampFormat != nil { + in, out := &in.TimestampFormat, &out.TimestampFormat + *out = new(string) + **out = **in + } + if in.DontVerifyChecksum != nil { + in, out := &in.DontVerifyChecksum, &out.DontVerifyChecksum + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CaptureOption. @@ -215,6 +285,11 @@ func (in *CaptureTarget) DeepCopyInto(out *CaptureTarget) { *out = new(v1.LabelSelector) (*in).DeepCopyInto(*out) } + if in.PodNames != nil { + in, out := &in.PodNames, &out.PodNames + *out = make([]string, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CaptureTarget. diff --git a/deploy/grafana-dashboards/standard-windows-ebpf/windows-ebpf-drops-dashboard.json b/deploy/grafana-dashboards/standard-windows-ebpf/windows-ebpf-drops-dashboard.json new file mode 100644 index 0000000000..04310e7e49 --- /dev/null +++ b/deploy/grafana-dashboards/standard-windows-ebpf/windows-ebpf-drops-dashboard.json @@ -0,0 +1,1233 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 239082, + "links": [ + { + "asDropdown": true, + "icon": "external link", + "includeVars": true, + "keepTime": true, + "tags": [ + "k8s:retina-network-observability" + ], + "targetBlank": false, + "title": "Dashboards: Retina Network Observability", + "tooltip": "", + "type": "dashboards", + "url": "" + }, + { + "asDropdown": false, + "icon": "info", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Documentation", + "tooltip": "", + "type": "link", + "url": "https://retina.sh/docs/Introduction/intro" + } + ], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "yellow", + "value": 100 + }, + { + "color": "red", + "value": 1000 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "inverted", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(networkobservability_adv_drop_count{namespace=~\"$namespace\", cluster=\"$cluster\"})", + "legendFormat": "Total Drops", + "range": true, + "refId": "A" + } + ], + "title": "Total Drops", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "yellow", + "value": 100 + }, + { + "color": "red", + "value": 1000 + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 0 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "inverted", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": true, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(rate(networkobservability_adv_drop_count{namespace=~\"$namespace\", cluster=\"$cluster\"}[$__rate_interval]))", + "legendFormat": "Drop Rate", + "range": true, + "refId": "A" + } + ], + "title": "Drop Rate (pps)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "yellow", + "value": 5 + }, + { + "color": "red", + "value": 15 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 0 + }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "inverted", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": true, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "(sum(rate(networkobservability_adv_drop_count{namespace=~\"$namespace\", cluster=\"$cluster\"}[$__rate_interval])) / (sum(rate(networkobservability_adv_drop_count{namespace=~\"$namespace\", cluster=\"$cluster\"}[$__rate_interval])) + sum(rate(networkobservability_adv_forward_count{namespace=~\"$namespace\", cluster=\"$cluster\"}[$__rate_interval])))) * 100", + "legendFormat": "Drop %", + "range": true, + "refId": "A" + } + ], + "title": "Drop Percentage", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 0 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "topk(1, sum by(reason) (rate(networkobservability_adv_drop_count{namespace=~\"$namespace\", cluster=\"$cluster\"}[$__rate_interval])))", + "legendFormat": "{{reason}}", + "range": true, + "refId": "A" + } + ], + "title": "Top Drop Reason", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 4 + }, + "id": 5, + "options": { + "legend": { + "calcs": [ + "last", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "topk(10, sum by(reason) (rate(networkobservability_adv_drop_count{namespace=~\"$namespace\", cluster=\"$cluster\"}[$__rate_interval])))", + "legendFormat": "{{reason}}", + "range": true, + "refId": "A" + } + ], + "title": "Drop Rate by Reason (Top 10)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "egress" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "ingress" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 4 + }, + "id": 6, + "options": { + "legend": { + "calcs": [ + "last", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(direction) (rate(networkobservability_adv_drop_count{namespace=~\"$namespace\", cluster=\"$cluster\"}[$__rate_interval]))", + "legendFormat": "{{direction}}", + "range": true, + "refId": "A" + } + ], + "title": "Drops by Direction (Egress vs Ingress)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "yellow", + "value": 100 + }, + { + "color": "red", + "value": 1000 + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 12 + }, + "id": 7, + "options": { + "displayMode": "gradient", + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "maxVizHeight": 300, + "minVizHeight": 16, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "auto", + "valueMode": "color" + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "topk(10, sum by(podname, namespace) (rate(networkobservability_adv_drop_count{namespace=~\"$namespace\", cluster=\"$cluster\", podname!=\"\"}[$__rate_interval])))", + "legendFormat": "{{namespace}}/{{podname}}", + "range": true, + "refId": "A" + } + ], + "title": "Top 10 Pods by Drop Rate", + "type": "bargauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "links": [ + { + "targetBlank": true, + "title": "Drop Reason Search", + "url": "https://www.bing.com/copilotsearch?q=what does ${__field.labels.reason} mean in the context of network drops" + } + ], + "mappings": [], + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 12 + }, + "id": 8, + "options": { + "displayLabels": [ + "percent" + ], + "legend": { + "displayMode": "table", + "placement": "right", + "showLegend": true, + "values": [ + "value", + "percent" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "sort": "desc", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "topk(10, sum by(reason) (rate(networkobservability_adv_drop_count{namespace=~\"$namespace\", cluster=\"$cluster\"}[$__rate_interval])))", + "legendFormat": "{{reason}}", + "range": true, + "refId": "A" + } + ], + "title": "Top 10 Drop Reasons Distribution", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Drop Count" + }, + "properties": [ + { + "id": "unit", + "value": "pps" + }, + { + "id": "custom.axisPlacement", + "value": "left" + }, + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Drop Bytes" + }, + "properties": [ + { + "id": "unit", + "value": "Bps" + }, + { + "id": "custom.axisPlacement", + "value": "right" + }, + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 20 + }, + "id": 9, + "options": { + "legend": { + "calcs": [ + "last", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(rate(networkobservability_adv_drop_count{namespace=~\"$namespace\", cluster=\"$cluster\"}[$__rate_interval]))", + "legendFormat": "Drop Count", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(rate(networkobservability_adv_drop_bytes{namespace=~\"$namespace\", cluster=\"$cluster\"}[$__rate_interval]))", + "legendFormat": "Drop Bytes", + "range": true, + "refId": "B" + } + ], + "title": "Drop Bytes vs Count Correlation", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto", + "wrapText": false + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "red", + "value": 500 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "namespace" + }, + "properties": [ + { + "id": "custom.width", + "value": 200 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Packets Per Second (pps)" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "color-text", + "wrapText": false + } + }, + { + "id": "custom.width", + "value": 221 + }, + { + "id": "custom.align", + "value": "center" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Drop Rate" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "drawStyle": "line", + "gradientMode": "hue", + "hideValue": true, + "lineStyle": { + "dash": [ + 10, + 10 + ], + "fill": "solid" + }, + "type": "sparkline" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 20 + }, + "id": 10, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Packets Per Second (pps)" + } + ] + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by(namespace) (rate(networkobservability_adv_drop_count{namespace=~\"$namespace\", namespace!=\"\", cluster=\"$cluster\"}[$__rate_interval]))", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "{{namespace}}", + "range": false, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by(namespace) (rate(networkobservability_adv_drop_count{namespace=~\"$namespace\", namespace!=\"\", cluster=\"$cluster\"}[$__rate_interval]))", + "format": "time_series", + "hide": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "B" + } + ], + "title": "Drops by Namespace", + "transformations": [ + { + "id": "timeSeriesTable", + "options": { + "B": { + "timeField": "Time" + } + } + }, + { + "id": "merge", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "Trend #A": "Drop Rate", + "Trend #B": "Drop Rate", + "Value": "Packets Per Second (pps)", + "Value #A": "Drop Rate", + "cosmic-networking": "", + "namespace": "" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 28 + }, + "id": 11, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-red", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Reds", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(podname, namespace, cluster) (rate(networkobservability_adv_drop_count{namespace=~\"$namespace\", podname!=\"\", cluster=~\"$cluster\"}[$__rate_interval]))", + "legendFormat": "{{namespace}}/{{podname}}", + "range": true, + "refId": "A" + } + ], + "title": "Drop Patterns Heatmap (Pod x Time)", + "type": "heatmap" + } + ], + "preload": false, + "refresh": "30s", + "schemaVersion": 41, + "tags": [ + "k8s:retina-network-observability", + "windows-ebpf", + "drops" + ], + "templating": { + "list": [ + { + "current": { + "text": "CosmicMdmPreProd", + "value": "f48d17e6-07ac-4efc-97ad-b347b565c3f4" + }, + "includeAll": false, + "label": "Data Source", + "name": "datasource", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "current": { + "text": "", + "value": "" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(networkobservability_adv_drop_count,cluster)", + "includeAll": false, + "label": "Cluster", + "name": "cluster", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(networkobservability_adv_drop_count,cluster)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "", + "sort": 1, + "type": "query" + }, + { + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(networkobservability_adv_drop_count, namespace)", + "includeAll": true, + "label": "Namespace", + "multi": true, + "name": "namespace", + "options": [], + "query": { + "query": "label_values(networkobservability_adv_drop_count, namespace)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "sort": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "America/Los_Angeles", + "title": "Kubernetes / Networking / Retina (Standard) / Windows eBPF Drops", + "uid": "RetinaWindowsEBPFDropsClusterWIP", + "version": 22 +} \ No newline at end of file diff --git a/deploy/grafana-dashboards/standard-windows-ebpf/windows-ebpf-namespace-dashboard.json b/deploy/grafana-dashboards/standard-windows-ebpf/windows-ebpf-namespace-dashboard.json new file mode 100644 index 0000000000..8893b0533e --- /dev/null +++ b/deploy/grafana-dashboards/standard-windows-ebpf/windows-ebpf-namespace-dashboard.json @@ -0,0 +1,1398 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 239083, + "links": [ + { + "asDropdown": true, + "icon": "external link", + "includeVars": true, + "keepTime": true, + "tags": [ + "k8s:retina-network-observability" + ], + "targetBlank": false, + "title": "Dashboards: Retina Network Observability", + "tooltip": "", + "type": "dashboards", + "url": "" + }, + { + "asDropdown": false, + "icon": "info", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Documentation", + "tooltip": "", + "type": "link", + "url": "https://retina.sh/docs/Introduction/intro" + } + ], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "titleSize": 18, + "valueSize": 28 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "vector(1)", + "legendFormat": "Cluster: $cluster | Namespace: $namespace", + "range": true, + "refId": "A" + } + ], + "title": "Current Namespace Selection", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 3 + }, + "id": 2, + "options": { + "legend": { + "calcs": [ + "last", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(podname) (rate(networkobservability_adv_forward_count{namespace=~\"$namespace\", cluster=\"$cluster\", podname!=\"\"}[$__rate_interval]))", + "legendFormat": "{{podname}}", + "range": true, + "refId": "A" + } + ], + "title": "Pod-Level Forward Traffic", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "yellow", + "value": 100 + }, + { + "color": "red", + "value": 1000 + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 3 + }, + "id": 3, + "options": { + "legend": { + "calcs": [ + "last", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(podname) (rate(networkobservability_adv_drop_count{namespace=~\"$namespace\", podname!=\"\", cluster=\"$cluster\"}[$__rate_interval]))", + "legendFormat": "{{podname}}", + "range": true, + "refId": "A" + } + ], + "title": "Pod-Level Drop Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "yellow", + "value": 100 + }, + { + "color": "red", + "value": 1000 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Pod" + }, + "properties": [ + { + "id": "custom.width", + "value": 305 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Forward Rate" + }, + "properties": [ + { + "id": "unit", + "value": "pps" + }, + { + "id": "custom.cellOptions", + "value": { + "type": "auto" + } + }, + { + "id": "custom.width", + "value": 200 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Drop Rate" + }, + "properties": [ + { + "id": "unit", + "value": "pps" + }, + { + "id": "custom.cellOptions", + "value": { + "type": "auto" + } + }, + { + "id": "custom.width", + "value": 200 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Drop %" + }, + "properties": [ + { + "id": "unit", + "value": "percent" + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "yellow", + "value": 5 + }, + { + "color": "red", + "value": 15 + } + ] + } + }, + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "gauge" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 11 + }, + "id": 4, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Forward Rate" + } + ] + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(podname) (rate(networkobservability_adv_forward_count{namespace=~\"$namespace\", podname!=\"\", cluster=\"$cluster\"}[$__rate_interval]))", + "format": "table", + "legendFormat": "__auto", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(podname) (rate(networkobservability_adv_drop_count{namespace=~\"$namespace\", podname!=\"\", cluster=\"$cluster\"}[$__rate_interval]))", + "format": "table", + "hide": false, + "legendFormat": "__auto", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "(sum by(podname) (rate(networkobservability_adv_drop_count{namespace=~\"$namespace\", podname!=\"\", cluster=\"$cluster\"}[$__rate_interval])) / (sum by(podname) (rate(networkobservability_adv_drop_count{namespace=~\"$namespace\", podname!=\"\", cluster=\"$cluster\"}[$__rate_interval])) + sum by(podname) (rate(networkobservability_adv_forward_count{namespace=~\"$namespace\", podname!=\"\", cluster=\"$cluster\"}[$__rate_interval])))) * 100", + "format": "table", + "hide": false, + "legendFormat": "__auto", + "range": true, + "refId": "C" + } + ], + "title": "Pod Traffic Summary", + "transformations": [ + { + "id": "merge", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "includeByName": {}, + "indexByName": { + "Time": 0, + "Value #A": 2, + "Value #B": 3, + "Value #C": 4, + "podname": 1 + }, + "renameByName": { + "Value #A": "Forward Rate", + "Value #B": "Drop Rate", + "Value #C": "Drop %", + "podname": "Pod" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 19 + }, + "id": 5, + "options": { + "legend": { + "calcs": [ + "last", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(podname, reason) (rate(networkobservability_adv_drop_count{namespace=~\"$namespace\", podname!=\"\", cluster=\"$cluster\"}[$__rate_interval]))", + "legendFormat": "{{podname}} - {{reason}}", + "range": true, + "refId": "A" + } + ], + "title": "Drop Reasons by Pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 19 + }, + "id": 6, + "options": { + "legend": { + "calcs": [ + "last", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(podname, flag) (rate(networkobservability_adv_tcpflags_count{namespace=~\"$namespace\", podname!=\"\", cluster=\"$cluster\"}[$__rate_interval]))", + "legendFormat": "{{podname}} - {{flag}}", + "range": true, + "refId": "A" + } + ], + "title": "TCP Flags by Pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 27 + }, + "id": 7, + "options": { + "legend": { + "calcs": [ + "last", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(workload_kind, workload_name) (rate(networkobservability_adv_forward_count{namespace=~\"$namespace\", workload_name!=\"\", cluster=\"$cluster\"}[$__rate_interval]))", + "legendFormat": "{{workload_kind}}: {{workload_name}}", + "range": true, + "refId": "A" + } + ], + "title": "Forward Traffic by Workload Type", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "yellow", + "value": 100 + }, + { + "color": "red", + "value": 1000 + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 27 + }, + "id": 8, + "options": { + "legend": { + "calcs": [ + "last", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(workload_kind, workload_name) (rate(networkobservability_adv_drop_count{namespace=~\"$namespace\", workload_name!=\"\", cluster=\"$cluster\"}[$__rate_interval]))", + "legendFormat": "{{workload_kind}}: {{workload_name}}", + "range": true, + "refId": "A" + } + ], + "title": "Drop Rate by Workload Type", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 35 + }, + "id": 9, + "options": { + "legend": { + "calcs": [ + "last", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(podname, direction) (rate(networkobservability_adv_forward_count{namespace=~\"$namespace\", podname!=\"\",cluster=\"$cluster\"}[$__rate_interval]))", + "legendFormat": "{{podname}} - {{direction}}", + "range": true, + "refId": "A" + } + ], + "title": "Forward Traffic by Pod and Direction", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "Bps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 35 + }, + "id": 10, + "options": { + "legend": { + "calcs": [ + "last", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(podname) (rate(networkobservability_adv_forward_bytes{namespace=~\"$namespace\", podname!=\"\",cluster=\"$cluster\"}[$__rate_interval]))", + "legendFormat": "{{podname}}", + "range": true, + "refId": "A" + } + ], + "title": "Forward Bytes by Pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 43 + }, + "id": 11, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-green", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Greens", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(podname) (rate(networkobservability_adv_forward_count{namespace=~\"$namespace\", podname!=\"\", cluster=\"$cluster\"}[$__rate_interval]))", + "legendFormat": "{{podname}}", + "range": true, + "refId": "A" + } + ], + "title": "Pod Forward Traffic Heatmap", + "type": "heatmap" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 43 + }, + "id": 12, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-red", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Reds", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(podname) (rate(networkobservability_adv_drop_count{namespace=~\"$namespace\", podname!=\"\", cluster=\"$cluster\"}[$__rate_interval]))", + "legendFormat": "{{podname}}", + "range": true, + "refId": "A" + } + ], + "title": "Pod Drop Rate Heatmap", + "type": "heatmap" + } + ], + "preload": false, + "refresh": "30s", + "schemaVersion": 41, + "tags": [ + "k8s:retina-network-observability", + "windows-ebpf", + "namespace-deep-dive" + ], + "templating": { + "list": [ + { + "current": { + "text": "CosmicMdmPreProd", + "value": "f48d17e6-07ac-4efc-97ad-b347b565c3f4" + }, + "includeAll": false, + "label": "Data Source", + "name": "datasource", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "current": { + "text": "", + "value": "" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(networkobservability_adv_drop_count,cluster)", + "includeAll": false, + "label": "Cluster", + "name": "cluster", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(networkobservability_adv_drop_count,cluster)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "", + "sort": 1, + "type": "query" + }, + { + "current": { + "text": "", + "value": "" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(networkobservability_adv_forward_count, namespace)", + "includeAll": false, + "label": "Namespace", + "name": "namespace", + "options": [], + "query": { + "query": "label_values(networkobservability_adv_forward_count, namespace)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "sort": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "America/Los_Angeles", + "title": "Kubernetes / Networking / Retina (Standard) / Windows eBPF Namespace Deep Dive", + "uid": "RetinaWindowsEBPFNamespaceClusterWIP", + "version": 1 +} \ No newline at end of file diff --git a/deploy/grafana-dashboards/standard-windows-ebpf/windows-ebpf-overview-dashboard.json b/deploy/grafana-dashboards/standard-windows-ebpf/windows-ebpf-overview-dashboard.json new file mode 100644 index 0000000000..ce8947926e --- /dev/null +++ b/deploy/grafana-dashboards/standard-windows-ebpf/windows-ebpf-overview-dashboard.json @@ -0,0 +1,1517 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 239084, + "links": [ + { + "asDropdown": true, + "icon": "external link", + "includeVars": true, + "keepTime": true, + "tags": [ + "k8s:retina-network-observability" + ], + "targetBlank": false, + "title": "Dashboards: Retina Network Observability", + "tooltip": "", + "type": "dashboards", + "url": "" + }, + { + "asDropdown": false, + "icon": "info", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Documentation", + "tooltip": "", + "type": "link", + "url": "https://retina.sh/docs/Introduction/intro" + } + ], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": true, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(networkobservability_adv_forward_count{namespace=~\"$namespace\", cluster=\"$cluster\"})", + "legendFormat": "Total Forwards", + "range": true, + "refId": "A" + } + ], + "title": "Total Forward Packets", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 0 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": true, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(networkobservability_adv_forward_bytes{namespace=~\"$namespace\", cluster=\"$cluster\"})", + "legendFormat": "Total Bytes", + "range": true, + "refId": "A" + } + ], + "title": "Total Forward Bytes", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "yellow", + "value": 100 + }, + { + "color": "red", + "value": 1000 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 0 + }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "inverted", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": true, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(networkobservability_adv_drop_count{namespace=~\"$namespace\", cluster=\"$cluster\"})", + "legendFormat": "Total Drops", + "range": true, + "refId": "A" + } + ], + "title": "Total Drops", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "yellow", + "value": 5 + }, + { + "color": "red", + "value": 15 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 0 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "inverted", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": true, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "(sum(rate(networkobservability_adv_drop_count{namespace=~\"$namespace\", cluster=\"$cluster\"}[$__rate_interval])) / (sum(rate(networkobservability_adv_drop_count{namespace=~\"$namespace\", cluster=\"$cluster\"}[$__rate_interval])) + sum(rate(networkobservability_adv_forward_count{namespace=~\"$namespace\", cluster=\"$cluster\"}[$__rate_interval])))) * 100", + "legendFormat": "Drop %", + "range": true, + "refId": "A" + } + ], + "title": "Drop Rate %", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "purple" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 0 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "count(count by(podname) (networkobservability_adv_forward_count{namespace=~\"$namespace\", podname!=\"\", cluster=\"$cluster\"}))", + "legendFormat": "Active Pods", + "range": true, + "refId": "A" + } + ], + "title": "Active Pods", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "orange" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 0 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "count(count by(namespace) (networkobservability_adv_forward_count{namespace=~\"$namespace\", namespace!=\"\", cluster=\"$cluster\"}))", + "legendFormat": "Active Namespaces", + "range": true, + "refId": "A" + } + ], + "title": "Active Namespaces", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 4 + }, + "id": 7, + "options": { + "legend": { + "calcs": [ + "last", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(rate(networkobservability_adv_forward_count{namespace=~\"$namespace\", cluster=\"$cluster\"}[$__rate_interval]))", + "legendFormat": "Total Forward Rate", + "range": true, + "refId": "A" + } + ], + "title": "Total Forward Rate (pps)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "Bps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 4 + }, + "id": 8, + "options": { + "legend": { + "calcs": [ + "last", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(rate(networkobservability_adv_forward_bytes{namespace=~\"$namespace\", cluster=\"$cluster\"}[$__rate_interval]))", + "legendFormat": "Total Forward Bytes", + "range": true, + "refId": "A" + } + ], + "title": "Total Forward Bytes (Bps)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 12 + }, + "id": 9, + "options": { + "legend": { + "calcs": [ + "last", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "topk(10, sum by(podname, namespace) (rate(networkobservability_adv_forward_count{namespace=~\"$namespace\", podname!=\"\", cluster=\"$cluster\", direction=\"egress\"}[$__rate_interval])))", + "legendFormat": "{{namespace}}/{{podname}}", + "range": true, + "refId": "A" + } + ], + "title": "Top 10 Pods - Outgoing Traffic", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 12 + }, + "id": 10, + "options": { + "legend": { + "calcs": [ + "last", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "topk(10, sum by(podname, namespace) (rate(networkobservability_adv_forward_count{namespace=~\"$namespace\", podname!=\"\", cluster=\"$cluster\", direction=\"ingress\"}[$__rate_interval])))", + "legendFormat": "{{namespace}}/{{podname}}", + "range": true, + "refId": "A" + } + ], + "title": "Top 10 Pods - Incoming Traffic", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 20 + }, + "id": 11, + "options": { + "legend": { + "calcs": [ + "last", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(flag) (rate(networkobservability_adv_tcpflags_count{namespace=~\"$namespace\", cluster=\"$cluster\"}[$__rate_interval]))", + "legendFormat": "{{flag}}", + "range": true, + "refId": "A" + } + ], + "title": "TCP Flags Distribution Over Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 20 + }, + "id": 12, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-blue", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Blues", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(podname, namespace) (rate(networkobservability_adv_tcpflags_count{namespace=~\"$namespace\", podname!=\"\", cluster=\"$cluster\"}[$__rate_interval]))", + "legendFormat": "{{namespace}}/{{podname}}", + "range": true, + "refId": "A" + } + ], + "title": "TCP Flags by Pod (Heatmap)", + "type": "heatmap" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Forwards" + }, + "properties": [ + { + "id": "unit", + "value": "pps" + }, + { + "id": "custom.axisPlacement", + "value": "left" + }, + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Drops" + }, + "properties": [ + { + "id": "unit", + "value": "pps" + }, + { + "id": "custom.axisPlacement", + "value": "right" + }, + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 28 + }, + "id": 13, + "options": { + "legend": { + "calcs": [ + "last", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(rate(networkobservability_adv_forward_count{namespace=~\"$namespace\", cluster=\"$cluster\"}[$__rate_interval]))", + "legendFormat": "Forwards", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(rate(networkobservability_adv_drop_count{namespace=~\"$namespace\", cluster=\"$cluster\"}[$__rate_interval]))", + "legendFormat": "Drops", + "range": true, + "refId": "B" + } + ], + "title": "Forward vs Drop Comparison", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 28 + }, + "id": 14, + "options": { + "legend": { + "calcs": [ + "last" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "topk(10, sum by(namespace) (rate(networkobservability_adv_forward_count{namespace=~\"$namespace\", namespace!=\"\", cluster=\"$cluster\"}[$__rate_interval])))", + "legendFormat": "{{namespace}}", + "range": true, + "refId": "A" + } + ], + "title": "Namespace Traffic Breakdown (Top 10)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 36 + }, + "id": 15, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Oranges", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "topk(10, sum by(podname, namespace) (rate(networkobservability_adv_forward_count{namespace=~\"$namespace\", podname!=\"\", cluster=\"$cluster\", direction=\"egress\"}[$__rate_interval])))", + "legendFormat": "{{namespace}}/{{podname}}", + "range": true, + "refId": "A" + } + ], + "title": "Top 10 Pods Outgoing Traffic (Heatmap)", + "type": "heatmap" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 36 + }, + "id": 16, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-purple", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Purples", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false + } + }, + "pluginVersion": "11.6.9", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "topk(10, sum by(podname, namespace) (rate(networkobservability_adv_forward_count{namespace=~\"$namespace\", podname!=\"\", cluster=\"$cluster\", direction=\"ingress\"}[$__rate_interval])))", + "legendFormat": "{{namespace}}/{{podname}}", + "range": true, + "refId": "A" + } + ], + "title": "Top 10 Pods Incoming Traffic (Heatmap)", + "type": "heatmap" + } + ], + "preload": false, + "refresh": "30s", + "schemaVersion": 41, + "tags": [ + "k8s:retina-network-observability", + "windows-ebpf", + "overview" + ], + "templating": { + "list": [ + { + "current": { + "text": "CosmicMdmPreProd", + "value": "f48d17e6-07ac-4efc-97ad-b347b565c3f4" + }, + "includeAll": false, + "label": "Data Source", + "name": "datasource", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "current": { + "text": "", + "value": "" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(networkobservability_adv_drop_count,cluster)", + "includeAll": false, + "label": "Cluster", + "name": "cluster", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(networkobservability_adv_drop_count,cluster)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "", + "type": "query" + }, + { + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(networkobservability_adv_forward_count, namespace)", + "includeAll": true, + "label": "Namespace", + "multi": true, + "name": "namespace", + "options": [], + "query": { + "query": "label_values(networkobservability_adv_forward_count, namespace)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "sort": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Kubernetes / Networking / Retina (Standard) / Windows eBPF Overview", + "uid": "RetinaWindowsEBPFOverviewClusterWIP", + "version": 1 +} \ No newline at end of file diff --git a/deploy/hubble/manifests/controller/helm/retina/templates/_helpers.tpl b/deploy/hubble/manifests/controller/helm/retina/templates/_helpers.tpl index ce25734672..36834bb3cd 100644 --- a/deploy/hubble/manifests/controller/helm/retina/templates/_helpers.tpl +++ b/deploy/hubble/manifests/controller/helm/retina/templates/_helpers.tpl @@ -80,3 +80,21 @@ Return the appropriate apiVersion for cronjob. {{- print "batch/v1beta1" -}} {{- end -}} {{- end -}} + +{{/* +Return the appropriate backend for Hubble UI ingress. +*/}} +{{- define "ingress.paths" -}} +{{ if semverCompare ">=1.4-0, <1.19-0" .Capabilities.KubeVersion.Version -}} +backend: + serviceName: hubble-ui + servicePort: http +{{- else if semverCompare "^1.19-0" .Capabilities.KubeVersion.Version -}} +pathType: Prefix +backend: + service: + name: hubble-ui + port: + name: http +{{- end -}} +{{- end -}} \ No newline at end of file diff --git a/deploy/hubble/manifests/controller/helm/retina/templates/agent/clusterrole.yaml b/deploy/hubble/manifests/controller/helm/retina/templates/agent/clusterrole.yaml index bd60f37cd0..8ab864a33a 100644 --- a/deploy/hubble/manifests/controller/helm/retina/templates/agent/clusterrole.yaml +++ b/deploy/hubble/manifests/controller/helm/retina/templates/agent/clusterrole.yaml @@ -15,25 +15,34 @@ rules: resources: ["clusterobservers"] verbs: ["get", "list", "watch"] - apiGroups: - - retina.io + - retina.sh resources: - retinaendpoints verbs: - get - list - watch - {{- if .Values.operator.enabled }} - apiGroups: - - "" + - cilium.io resources: - - namespaces - - endpoints + - ciliumnodes + - ciliumidentities + - ciliumendpoints verbs: - - get - - list - - watch + - get + - list + - watch + - apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - get + - list + - watch + {{- if .Values.operator.enabled }} - apiGroups: - - retina.io + - retina.sh resources: - retinaendpoints verbs: @@ -45,7 +54,7 @@ rules: - update - watch - apiGroups: - - retina.io + - retina.sh resources: - metricsconfigurations verbs: @@ -57,37 +66,19 @@ rules: - update - watch - apiGroups: - - retina.io + - retina.sh resources: - retinaendpoints/finalizers verbs: - update - apiGroups: - - retina.io + - retina.sh resources: - retinaendpoints/status verbs: - get - patch - update - - apiGroups: - - discovery.k8s.io - resources: - - endpointslices - verbs: - - get - - list - - watch - - apiGroups: - - cilium.io - resources: - - ciliumnodes - - ciliumidentities - - ciliumendpoints - verbs: - - get - - list - - watch {{- end }} {{- end}} diff --git a/deploy/hubble/manifests/controller/helm/retina/templates/agent/configmap.yaml b/deploy/hubble/manifests/controller/helm/retina/templates/agent/configmap.yaml index 0dd67890c4..6b1dd072a9 100644 --- a/deploy/hubble/manifests/controller/helm/retina/templates/agent/configmap.yaml +++ b/deploy/hubble/manifests/controller/helm/retina/templates/agent/configmap.yaml @@ -114,6 +114,8 @@ data: bypassLookupIPOfInterest: {{ .Values.bypassLookupIPOfInterest }} dataAggregationLevel: {{ .Values.dataAggregationLevel }} monitorSockPath: {{ .Values.monitorSockPath }} + packetParserRingBuffer: {{ .Values.packetParserRingBuffer }} + packetParserRingBufferSize: {{ .Values.packetParserRingBufferSize }} {{- end}} --- {{- if .Values.os.windows}} diff --git a/deploy/hubble/manifests/controller/helm/retina/templates/agent/daemonset.yaml b/deploy/hubble/manifests/controller/helm/retina/templates/agent/daemonset.yaml index 5dd90331a4..99eec33c6d 100644 --- a/deploy/hubble/manifests/controller/helm/retina/templates/agent/daemonset.yaml +++ b/deploy/hubble/manifests/controller/helm/retina/templates/agent/daemonset.yaml @@ -54,6 +54,8 @@ spec: mountPath: /var/run/cilium - name: config mountPath: /retina/config + - name: tmp + mountPath: /tmp containers: - name: {{ include "retina.name" . }} image: {{ .Values.agent.repository }}:{{ .Values.agent.tag }} @@ -75,6 +77,9 @@ spec: ports: - containerPort: {{ .Values.agent.container.retina.ports.containerPort }} resources: + requests: + memory: {{ .Values.resources.requests.memory | quote }} + cpu: {{ .Values.resources.requests.cpu | quote }} limits: memory: {{ .Values.resources.limits.memory | quote }} cpu: {{ .Values.resources.limits.cpu | quote }} @@ -120,6 +125,10 @@ spec: mountPath: /sys/kernel/tracing - name: cilium mountPath: /var/run/cilium + - name: host-os-release + mountPath: /etc/os-release + - name: tmp + mountPath: /tmp {{- if .Values.hubble.tls.enabled }} - name: tls mountPath: /var/lib/cilium/tls/hubble @@ -146,6 +155,12 @@ spec: hostPath: path: /var/run/cilium type: DirectoryOrCreate + - name: host-os-release + hostPath: + path: /etc/os-release + type: FileOrCreate + - name: tmp + emptyDir: {} {{- if .Values.hubble.tls.enabled }} - name: tls projected: diff --git a/deploy/hubble/manifests/controller/helm/retina/templates/hubble-relay/service.yaml b/deploy/hubble/manifests/controller/helm/retina/templates/hubble-relay/service.yaml index fc13c90165..cb82b4b97d 100644 --- a/deploy/hubble/manifests/controller/helm/retina/templates/hubble-relay/service.yaml +++ b/deploy/hubble/manifests/controller/helm/retina/templates/hubble-relay/service.yaml @@ -14,6 +14,9 @@ metadata: app.kubernetes.io/part-of: retina spec: type: {{ .Values.hubble.relay.service.type | quote }} + {{- if and .Values.hubble.relay.service.trafficDistribution (semverCompare ">=1.31-0" .Capabilities.KubeVersion.GitVersion) }} + trafficDistribution: {{ .Values.hubble.relay.service.trafficDistribution }} + {{- end }} selector: k8s-app: hubble-relay ports: diff --git a/deploy/hubble/manifests/controller/helm/retina/templates/hubble-ui/httproute.yaml b/deploy/hubble/manifests/controller/helm/retina/templates/hubble-ui/httproute.yaml new file mode 100644 index 0000000000..28899ca991 --- /dev/null +++ b/deploy/hubble/manifests/controller/helm/retina/templates/hubble-ui/httproute.yaml @@ -0,0 +1,44 @@ +{{- if and (or .Values.hubble.enabled .Values.hubble.ui.standalone.enabled) .Values.hubble.ui.enabled .Values.hubble.ui.httpRoute.enabled }} +{{- $baseUrl := .Values.hubble.ui.baseUrl -}} +{{- $port := .Values.hubble.ui.httpRoute.port | default .Values.hubble.ui.service.port | default 80 -}} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: hubble-ui + namespace: {{ .Release.Namespace }} + labels: + k8s-app: hubble-ui + app.kubernetes.io/name: hubble-ui + app.kubernetes.io/part-of: cilium + {{- with .Values.hubble.ui.httpRoute.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- if or .Values.hubble.ui.httpRoute.annotations .Values.hubble.ui.annotations }} + annotations: + {{- with .Values.hubble.ui.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.hubble.ui.httpRoute.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} +spec: + {{- if .Values.hubble.ui.httpRoute.parentRefs }} + parentRefs: + {{- toYaml .Values.hubble.ui.httpRoute.parentRefs | nindent 4 }} + {{- end }} + {{- if .Values.hubble.ui.httpRoute.hostnames }} + hostnames: + {{- range .Values.hubble.ui.httpRoute.hostnames }} + - {{ . }} + {{- end }} + {{- end }} + rules: + - matches: + - path: + type: PathPrefix + value: {{ $baseUrl | quote }} + backendRefs: + - name: hubble-ui + port: {{ $port }} +{{- end }} diff --git a/deploy/hubble/manifests/controller/helm/retina/templates/hubble-ui/service.yaml b/deploy/hubble/manifests/controller/helm/retina/templates/hubble-ui/service.yaml index a820b3420b..251415e689 100644 --- a/deploy/hubble/manifests/controller/helm/retina/templates/hubble-ui/service.yaml +++ b/deploy/hubble/manifests/controller/helm/retina/templates/hubble-ui/service.yaml @@ -19,6 +19,9 @@ metadata: app.kubernetes.io/part-of: cilium spec: type: {{ .Values.hubble.ui.service.type | quote }} + {{- if and .Values.hubble.ui.service.trafficDistribution (semverCompare ">=1.31-0" .Capabilities.KubeVersion.GitVersion) }} + trafficDistribution: {{ .Values.hubble.ui.service.trafficDistribution }} + {{- end }} selector: k8s-app: hubble-ui ports: diff --git a/deploy/hubble/manifests/controller/helm/retina/templates/hubble/tls-cronjob/clusterrole.yaml b/deploy/hubble/manifests/controller/helm/retina/templates/hubble/tls-cronjob/role.yaml similarity index 94% rename from deploy/hubble/manifests/controller/helm/retina/templates/hubble/tls-cronjob/clusterrole.yaml rename to deploy/hubble/manifests/controller/helm/retina/templates/hubble/tls-cronjob/role.yaml index 74d0783173..cf36be7d19 100644 --- a/deploy/hubble/manifests/controller/helm/retina/templates/hubble/tls-cronjob/clusterrole.yaml +++ b/deploy/hubble/manifests/controller/helm/retina/templates/hubble/tls-cronjob/role.yaml @@ -1,8 +1,9 @@ {{- if and .Values.hubble.enabled .Values.hubble.tls.enabled .Values.hubble.tls.auto.enabled (eq .Values.hubble.tls.auto.method "cronJob") .Values.serviceAccounts.hubblecertgen.create }} apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole +kind: Role metadata: name: hubble-generate-certs + namespace: {{ .Release.Namespace }} {{- with .Values.hubble.annotations }} annotations: {{- toYaml . | nindent 4 }} diff --git a/deploy/hubble/manifests/controller/helm/retina/templates/hubble/tls-cronjob/rolebinding.yaml b/deploy/hubble/manifests/controller/helm/retina/templates/hubble/tls-cronjob/rolebinding.yaml index eca15c6f7d..f25156969d 100644 --- a/deploy/hubble/manifests/controller/helm/retina/templates/hubble/tls-cronjob/rolebinding.yaml +++ b/deploy/hubble/manifests/controller/helm/retina/templates/hubble/tls-cronjob/rolebinding.yaml @@ -12,7 +12,7 @@ metadata: app.kubernetes.io/part-of: cilium roleRef: apiGroup: rbac.authorization.k8s.io - kind: ClusterRole + kind: Role name: hubble-generate-certs subjects: - kind: ServiceAccount diff --git a/deploy/hubble/manifests/controller/helm/retina/templates/operator/clusterrole.yaml b/deploy/hubble/manifests/controller/helm/retina/templates/operator/clusterrole.yaml index 3e436490f9..d2e12e7c9e 100644 --- a/deploy/hubble/manifests/controller/helm/retina/templates/operator/clusterrole.yaml +++ b/deploy/hubble/manifests/controller/helm/retina/templates/operator/clusterrole.yaml @@ -3,7 +3,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: creationTimestamp: null - name: retina-operator-role + name: retina-operator-clusterrole rules: - apiGroups: - "apiextensions.k8s.io" @@ -52,7 +52,7 @@ rules: verbs: - get - apiGroups: - - retina.io + - retina.sh resources: - captures verbs: @@ -64,13 +64,13 @@ rules: - update - watch - apiGroups: - - retina.io + - retina.sh resources: - captures/finalizers verbs: - update - apiGroups: - - retina.io + - retina.sh resources: - captures/status verbs: @@ -100,19 +100,4 @@ rules: - get - list - watch - # For cilium-operator running in HA mode. - # - # Cilium operator running in HA mode requires the use of ResourceLock for Leader Election - # between multiple running instances. - # The preferred way of doing this is to use LeasesResourceLock as edits to Leases are less - # common and fewer objects in the cluster watch "all Leases". - - apiGroups: - - coordination.k8s.io - resources: - - leases - verbs: - - create - - get - - update - {{- end -}} diff --git a/deploy/hubble/manifests/controller/helm/retina/templates/operator/clusterrolebinding.yaml b/deploy/hubble/manifests/controller/helm/retina/templates/operator/clusterrolebinding.yaml index 3138f41fbf..a67b2a1c99 100644 --- a/deploy/hubble/manifests/controller/helm/retina/templates/operator/clusterrolebinding.yaml +++ b/deploy/hubble/manifests/controller/helm/retina/templates/operator/clusterrolebinding.yaml @@ -13,7 +13,7 @@ metadata: roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: retina-operator-role + name: retina-operator-clusterrole subjects: - kind: ServiceAccount name: retina-operator diff --git a/deploy/hubble/manifests/controller/helm/retina/templates/operator/deployment.yaml b/deploy/hubble/manifests/controller/helm/retina/templates/operator/deployment.yaml index e44a535c81..acea3cb711 100644 --- a/deploy/hubble/manifests/controller/helm/retina/templates/operator/deployment.yaml +++ b/deploy/hubble/manifests/controller/helm/retina/templates/operator/deployment.yaml @@ -17,7 +17,7 @@ spec: selector: matchLabels: control-plane: retina-operator - replicas: 1 + replicas: {{ .Values.operator.replicas }} template: metadata: annotations: @@ -56,16 +56,18 @@ spec: {{- end }} {{ else }} command: - - /retina-operator + - /retina-operator {{- end }} {{- if .Values.operator.container.args}} args: {{- range $.Values.operator.container.args}} - {{ . | quote }} {{- end}} - {{ else }} + - --leader-election-namespace={{ $.Values.operator.leaderElectionNamespace | default $.Release.Namespace }} + {{- else }} args: - --config-dir=/retina + - --leader-election-namespace={{ .Values.operator.leaderElectionNamespace | default .Release.Namespace }} {{- end}} env: # this env var is used by retina OSS telemetry and zap diff --git a/deploy/hubble/manifests/controller/helm/retina/templates/operator/role.yaml b/deploy/hubble/manifests/controller/helm/retina/templates/operator/role.yaml new file mode 100644 index 0000000000..6ffecf4a26 --- /dev/null +++ b/deploy/hubble/manifests/controller/helm/retina/templates/operator/role.yaml @@ -0,0 +1,30 @@ +{{- if .Values.operator.enabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: retina-operator-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: operator + app.kubernetes.io/part-of: operator + app.kubernetes.io/managed-by: kustomize + name: retina-operator-role + namespace: {{ .Values.operator.leaderElectionNamespace | default .Release.Namespace }} +rules: + # For cilium-operator running in HA mode. + # + # Cilium operator running in HA mode requires the use of ResourceLock for Leader Election + # between multiple running instances. + # The preferred way of doing this is to use LeasesResourceLock as edits to Leases are less + # common and fewer objects in the cluster watch "all Leases". + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - get + - update + +{{- end -}} diff --git a/deploy/hubble/manifests/controller/helm/retina/templates/operator/rolebinding.yaml b/deploy/hubble/manifests/controller/helm/retina/templates/operator/rolebinding.yaml new file mode 100644 index 0000000000..911e9cbaee --- /dev/null +++ b/deploy/hubble/manifests/controller/helm/retina/templates/operator/rolebinding.yaml @@ -0,0 +1,23 @@ +{{- if .Values.operator.enabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: retina-operator-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: operator + app.kubernetes.io/part-of: operator + app.kubernetes.io/managed-by: kustomize + name: retina-operator-rolebinding + namespace: {{ .Values.operator.leaderElectionNamespace | default .Release.Namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: retina-operator-role +subjects: +- kind: ServiceAccount + name: retina-operator + namespace: {{ .Values.namespace }} + +{{- end -}} diff --git a/deploy/hubble/manifests/controller/helm/retina/values.yaml b/deploy/hubble/manifests/controller/helm/retina/values.yaml index e68d8c82b0..d7258dac61 100644 --- a/deploy/hubble/manifests/controller/helm/retina/values.yaml +++ b/deploy/hubble/manifests/controller/helm/retina/values.yaml @@ -16,6 +16,7 @@ os: # FIXME: remove unnecessary pieces, etc. operator: enabled: true + replicas: 1 repository: acndev.azurecr.io/retina-operator pullPolicy: Always tag: "latest" @@ -32,6 +33,9 @@ operator: # -- Node tolerations for pod assignment on nodes with taints # ref: https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/ tolerations: [] + # Namespace used for operator leader election lease. + # Defaults to .Release.Namespace when empty. + leaderElectionNamespace: "" agent: leaderElection: false @@ -68,6 +72,17 @@ enableAnnotations: false bypassLookupIPOfInterest: true dataAggregationLevel: "high" monitorSockPath: "/var/run/cilium/monitor1_2.sock" +# Use BPF ring buffers (BPF_MAP_TYPE_RINGBUF) instead of BPF_PERF_EVENT_ARRAY. +# Pros: lower per-event overhead at high event rates, simpler variable-sized records, more consistent latency. +# Cons: fixed-size pre-allocated (locked) memory that can be wasted on low traffic; events are dropped when the buffer fills (reserve fails). +# Possible values: "enabled", "disabled". +packetParserRingBuffer: "disabled" +# Ring buffer size in bytes (only used when packetParserRingBuffer="enabled"). +# Default: 8MiB (8388608). +# Valid range: power of two between the kernel page size and 1GiB (inclusive). +# Invalid values cause startup to fail with a validation error. +# This value is not applied when ring buffers are disabled. +packetParserRingBufferSize: 8388608 imagePullSecrets: [] nameOverride: "retina" @@ -90,9 +105,16 @@ logLevel: info enabledPlugin_linux: '["linuxutil","packetforward","packetparser","dns", "dropreason"]' enabledPlugin_win: '["hnsstats"]' +# TCX Support +# Supported Values: "auto", "off" +# Default: auto +enableTCX: "auto" + enableTelemetry: false # Interval, in duration, to scrape/publish metrics. +# Default when unset or invalid (used by pluginmanager and all plugins). +metricsInterval: "10s" metricsIntervalDuration: "10s" azure: @@ -108,7 +130,7 @@ securityContext: capabilities: add: - SYS_ADMIN - - SYS_RESOURCE + - SYS_RESOURCE # for setting rlimit - NET_ADMIN # for packetparser plugin - IPC_LOCK # for mmap() calls made by NewReader(), ref: https://man7.org/linux/man-pages/man2/mmap.2.html windowsOptions: @@ -130,6 +152,9 @@ serviceAccount: name: "retina-agent" resources: + requests: + memory: "500Mi" + cpu: "500m" limits: memory: "500Mi" cpu: "500m" @@ -501,6 +526,12 @@ hubble: type: ClusterIP # --- The port to use when the service type is set to NodePort. nodePort: 31234 + # --- trafficDistribution field for service load balancing behavior. + # Valid values: PreferClose + # Provides better performance and reliability than annotation-based topologyAwareRouting + # by being built into the Service API and offering more predictable routing behavior. + # See https://kubernetes.io/docs/concepts/services-networking/service/#traffic-distribution + trafficDistribution: "" # -- Host to listen to. Specify an empty string to bind to all the interfaces. listenHost: "" @@ -776,6 +807,12 @@ hubble: type: ClusterIP # --- The port to use when the service type is set to NodePort. nodePort: 31235 + # --- trafficDistribution field for service load balancing behavior. + # Valid values: PreferClose + # Provides better performance and reliability than annotation-based topologyAwareRouting + # by being built into the Service API and offering more predictable routing behavior. + # See https://kubernetes.io/docs/concepts/services-networking/service/#traffic-distribution + trafficDistribution: "" # -- Defines base url prefix for all hubble-ui http requests. # It needs to be changed in case if ingress for hubble-ui is configured under some sub-path. @@ -798,6 +835,23 @@ hubble: # hosts: # - chart-example.local + # -- hubble-ui HTTPRoute configuration (Gateway API). + # Requires Kubernetes 1.26+ or Gateway API CRDs to be pre-installed. + httpRoute: + enabled: false + annotations: {} + labels: {} + # -- Parent Gateways that this route is attached to. + parentRefs: + - name: example-gateway + namespace: default + sectionName: example-listener + # -- Hostnames for this HTTPRoute. + hostnames: + - chart-example.local + # -- Backend port number. Defaults to the service port (80). + port: ~ + # -- Hubble flows export. export: # --- Defines max file size of output file before it gets rotated. diff --git a/deploy/standard/manifests/controller/helm/retina/crds/retina.sh_captures.yaml b/deploy/standard/manifests/controller/helm/retina/crds/retina.sh_captures.yaml index 749d2f3cfb..c38fd357df 100644 --- a/deploy/standard/manifests/controller/helm/retina/crds/retina.sh_captures.yaml +++ b/deploy/standard/manifests/controller/helm/retina/crds/retina.sh_captures.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.19.0 name: captures.retina.sh spec: group: retina.sh @@ -48,20 +48,122 @@ spec: captureOption: description: CaptureOption lists the options of the capture. properties: + absoluteSeq: + description: |- + AbsoluteSeq prints absolute TCP sequence numbers (equivalent to tcpdump -S flag). + Shows actual sequence numbers instead of relative numbers. + type: boolean + dontVerifyChecksum: + description: |- + DontVerifyChecksum disables TCP checksum verification (equivalent to tcpdump -K flag). + Skips TCP checksum validation for captured packets. + type: boolean duration: description: Duration indicates length of time that the capture should continue for. pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ type: string + immediateMode: + description: |- + ImmediateMode enables immediate mode for packet capture (equivalent to tcpdump --immediate-mode). + When true, packets are delivered to the application immediately rather than being buffered. + This can reduce latency but may increase CPU usage. + type: boolean + interfaces: + description: |- + Interfaces specifies the network interfaces on which to capture packets. + If specified, captures only on the listed interfaces (e.g., ["eth0", "eth1"]). + If empty, captures on all interfaces by default. + Use this field to select specific interfaces, NOT the tcpdumpFilter field. + items: + type: string + type: array maxCaptureSize: default: 100 description: MaxCaptureSize limits the capture file to MB in size. type: integer + noPromiscuous: + description: |- + NoPromiscuous disables promiscuous mode for packet capture. + When true, only packets destined for this host are captured (equivalent to tcpdump -p flag). + When false or unset, captures all packets on the network segment (default behavior). + type: boolean + noResolveDNS: + description: |- + NoResolveDNS disables DNS resolution for captured addresses (equivalent to tcpdump -n flag). + When true, IP addresses are displayed numerically without resolving hostnames. + This speeds up capture processing and avoids DNS lookup overhead. + type: boolean + noResolvePort: + description: |- + NoResolvePort disables port name resolution (equivalent to tcpdump -nn flag). + When true, both IP addresses and port numbers are displayed numerically. + This prevents service name lookups for port numbers. + type: boolean + packetBuffered: + description: |- + PacketBuffered enables packet-buffered output mode (equivalent to tcpdump -U flag). + When true, packets are written to output as soon as they're captured rather than being buffered. + Useful for real-time monitoring but may impact performance. + type: boolean packetSize: description: PacketSize limits the each packet to bytes in size and packets longer than PacketSize will be truncated. type: integer + pcapFilter: + description: |- + PcapFilter specifies a BPF filter expression for packet filtering (e.g., "tcp port 443", "host 10.0.0.1"). + Only BPF expressions are allowed, no flags. See https://www.tcpdump.org/manpages/pcap-filter.7.html + maxLength: 1024 + pattern: ^[^-]*$ + type: string + printDataFormat: + description: |- + PrintDataFormat controls how packet data is printed in the output. + Valid values: "" (none), "hex" (tcpdump -x), "hex-with-link" (tcpdump -xx), "ascii" (tcpdump -A), "ascii-with-link" (tcpdump -AA). + Empty string means no packet data printing. + enum: + - "" + - hex + - hex-with-link + - ascii + - ascii-with-link + type: string + printLinkHeader: + description: |- + PrintLinkHeader prints link-level (Ethernet) headers (equivalent to tcpdump -e flag). + Shows MAC addresses and other link-layer information. + type: boolean + quietOutput: + description: |- + QuietOutput enables quiet/quick output mode (equivalent to tcpdump -q flag). + Prints less protocol information for shorter output lines. + type: boolean + timestampFormat: + description: |- + TimestampFormat controls the timestamp format in packet capture output. + Valid values: "" (default), "none" (tcpdump -t), "unformatted" (tcpdump -tt), "delta" (tcpdump -ttt), "date" (tcpdump -tttt), "delta-since-first" (tcpdump -ttttt). + Empty string means default timestamp format. + enum: + - "" + - none + - unformatted + - delta + - date + - delta-since-first + type: string + verbosity: + description: |- + Verbosity controls the verbosity level of packet capture output. + Valid values: "" (normal/default), "verbose" (tcpdump -v), "extra" (tcpdump -vv), "max" (tcpdump -vvv). + Empty string means normal verbosity with no extra verbose flags. + enum: + - "" + - verbose + - extra + - max + type: string type: object captureTarget: description: CaptureTarget indicates the target on which the network @@ -165,6 +267,14 @@ spec: type: object type: object x-kubernetes-map-type: atomic + podNames: + description: |- + PodNames allows selecting specific pods by their names. + If specified, the capture will be performed on the pods with matching names in the specified namespace. + PodNames is incompatible with NodeSelector, NamespaceSelector, and PodSelector. + items: + type: string + type: array podSelector: description: |- This is a label selector which selects Pods. This field follows standard label @@ -247,11 +357,26 @@ spec: - Network statistics information type: boolean tcpdumpFilter: - description: TcpdumpFilter is a raw tcpdump filter string. + description: |- + TcpdumpFilter accepts BPF filter expressions only (no flags). + + DEPRECATED and will be removed: Currently functional but scheduled for removal. + Use captureOption.pcapFilter for BPF expressions and captureOption boolean flags for tcpdump display options instead. + maxLength: 1024 + pattern: ^[^-]*$ type: string required: - captureTarget type: object + cleanUpAfterUpload: + default: false + description: |- + CleanUpAfterUpload indicates whether the capture jobs and associated resources + should be automatically cleaned up after a successful upload to remote storage + (BlobUpload or S3Upload). When set to true, completed capture jobs, secrets, + and the Capture resource itself will be deleted once all jobs have succeeded + and uploads are confirmed. + type: boolean outputConfiguration: description: OutputConfiguration indicates the location capture will be stored. @@ -262,8 +387,16 @@ spec: type: string hostPath: description: |- - HostPath stores the capture files into the specified host filesystem. - If nothing exists at the given path of the host, an empty directory will be created there. + HostPath is a relative subpath name (e.g. "my-capture") joined under the + operator-configured host base directory (default /var/log/retina/captures) + on every node that runs a capture pod. The capture files are written to + that joined directory, and an empty directory is created there if it does + not already exist. + + HostPath must be a relative subpath: absolute paths (e.g. "/tmp/foo", + "C:\\foo") and any value containing ".." segments are rejected by the + operator. CR authors cannot influence the base directory, which is + controlled by the cluster operator via the operator config. type: string persistentVolumeClaim: description: PersistentVolumeClaim mounts the supplied PVC into @@ -290,10 +423,14 @@ spec: description: SecretName is the name of secret which stores S3 compliant storage access key and secret key. type: string + required: + - bucket + - secretName type: object type: object required: - captureConfiguration + - outputConfiguration type: object status: description: CaptureStatus describes the status of the capture. @@ -310,16 +447,8 @@ spec: type: string conditions: items: - description: "Condition contains details for one aspect of the current - state of this API Resource.\n---\nThis struct is intended for - direct use as an array at the field path .status.conditions. For - example,\n\n\n\ttype FooStatus struct{\n\t // Represents the - observations of a foo's current state.\n\t // Known .status.conditions.type - are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // - +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t - \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" - patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t - \ // other fields\n\t}" + description: Condition contains details for one aspect of the current + state of this API Resource. properties: lastTransitionTime: description: |- @@ -360,12 +489,7 @@ spec: - Unknown type: string type: - description: |- - type of condition in CamelCase or in foo.example.com/CamelCase. - --- - Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be - useful (see .node.status.conditions), the ability to deconflict is important. - The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + description: type of condition in CamelCase or in foo.example.com/CamelCase. maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string diff --git a/deploy/standard/manifests/controller/helm/retina/crds/retina.sh_metricsconfigurations.yaml b/deploy/standard/manifests/controller/helm/retina/crds/retina.sh_metricsconfigurations.yaml index b0453ebf81..1179c0092a 100644 --- a/deploy/standard/manifests/controller/helm/retina/crds/retina.sh_metricsconfigurations.yaml +++ b/deploy/standard/manifests/controller/helm/retina/crds/retina.sh_metricsconfigurations.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.19.0 name: metricsconfigurations.retina.sh spec: group: retina.sh @@ -75,6 +75,11 @@ spec: type: string type: array x-kubernetes-list-type: set + ttl: + description: |- + TTL represents the time-to-live of the metrics collected + Metrics which have not been updated within the TTL will be removed from export + type: string required: - metricName type: object @@ -136,6 +141,11 @@ spec: type: string type: array x-kubernetes-list-type: set + ttl: + description: |- + TTL represents the time-to-live of the metrics collected + Metrics which have not been updated within the TTL will be removed from export + type: string required: - metricName type: object diff --git a/deploy/standard/manifests/controller/helm/retina/crds/retina.sh_retinaendpoints.yaml b/deploy/standard/manifests/controller/helm/retina/crds/retina.sh_retinaendpoints.yaml index a9b94e5999..f091618a15 100644 --- a/deploy/standard/manifests/controller/helm/retina/crds/retina.sh_retinaendpoints.yaml +++ b/deploy/standard/manifests/controller/helm/retina/crds/retina.sh_retinaendpoints.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.19.0 name: retinaendpoints.retina.sh spec: group: retina.sh diff --git a/deploy/standard/manifests/controller/helm/retina/crds/retina.sh_tracesconfigurations.yaml b/deploy/standard/manifests/controller/helm/retina/crds/retina.sh_tracesconfigurations.yaml index ee00af15ae..1f6bce86b6 100644 --- a/deploy/standard/manifests/controller/helm/retina/crds/retina.sh_tracesconfigurations.yaml +++ b/deploy/standard/manifests/controller/helm/retina/crds/retina.sh_tracesconfigurations.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.19.0 name: tracesconfigurations.retina.sh spec: group: retina.sh diff --git a/deploy/standard/manifests/controller/helm/retina/templates/configmap.yaml b/deploy/standard/manifests/controller/helm/retina/templates/configmap.yaml index dc0f74eea2..e4fdf524b0 100644 --- a/deploy/standard/manifests/controller/helm/retina/templates/configmap.yaml +++ b/deploy/standard/manifests/controller/helm/retina/templates/configmap.yaml @@ -17,6 +17,7 @@ data: enabledPlugin: {{ .Values.enabledPlugin_linux }} metricsInterval: {{ .Values.metricsInterval }} metricsIntervalDuration: {{ .Values.metricsIntervalDuration }} + enableTCX: {{ .Values.enableTCX }} enableTelemetry: {{ .Values.enableTelemetry }} enablePodLevel: {{ .Values.enablePodLevel }} enableConntrackMetrics: {{ .Values.enableConntrackMetrics }} @@ -25,6 +26,10 @@ data: bypassLookupIPOfInterest: {{ .Values.bypassLookupIPOfInterest }} dataAggregationLevel: {{ .Values.dataAggregationLevel }} telemetryInterval: {{ .Values.daemonset.telemetryInterval }} + dataSamplingRate: {{ .Values.dataSamplingRate }} + packetParserRingBuffer: {{ .Values.packetParserRingBuffer }} + packetParserRingBufferSize: {{ .Values.packetParserRingBufferSize }} + filterMapMaxEntries: {{ .Values.filterMapMaxEntries }} {{- end}} --- {{- if .Values.os.windows}} @@ -50,6 +55,7 @@ data: enableAnnotations: {{ .Values.enableAnnotations }} enablePodLevel: {{ .Values.enablePodLevel }} remoteContext: {{ .Values.remoteContext }} + enableAnnotations: {{ .Values.enableAnnotations }} telemetryInterval: {{ .Values.daemonset.telemetryInterval }} {{- end}} diff --git a/deploy/standard/manifests/controller/helm/retina/templates/daemonset.yaml b/deploy/standard/manifests/controller/helm/retina/templates/daemonset.yaml index 2b7e929421..fece02726d 100644 --- a/deploy/standard/manifests/controller/helm/retina/templates/daemonset.yaml +++ b/deploy/standard/manifests/controller/helm/retina/templates/daemonset.yaml @@ -45,6 +45,8 @@ spec: mountPropagation: Bidirectional - name: config mountPath: /retina/config + - name: tmp + mountPath: /tmp containers: - name: {{ include "retina.name" . }} readinessProbe: @@ -87,6 +89,9 @@ spec: resources: {{- toYaml .Values.resources | nindent 12 }} env: + {{- if .Values.daemonset.container.retina.env -}} + {{ toYaml .Values.daemonset.container.retina.env | nindent 10 }} + {{- end }} - name: POD_NAME valueFrom: fieldRef: @@ -218,6 +223,9 @@ spec: - .\setkubeconfigpath.ps1; ./controller.exe --config ./retina/config.yaml --kubeconfig ./kubeconfig {{- end }} env: + {{- if .Values.daemonset.container.retina.env -}} + {{ toYaml .Values.daemonset.container.retina.env | nindent 10 }} + {{- end }} - name: POD_NAME valueFrom: fieldRef: diff --git a/deploy/standard/manifests/controller/helm/retina/templates/operator-configmap.yaml b/deploy/standard/manifests/controller/helm/retina/templates/operator-configmap.yaml new file mode 100644 index 0000000000..ab82bc5ae1 --- /dev/null +++ b/deploy/standard/manifests/controller/helm/retina/templates/operator-configmap.yaml @@ -0,0 +1,22 @@ +{{- if .Values.operator.enabled -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: "{{ .Values.operator.name }}-config" + namespace: {{ .Values.namespace }} +data: + operator-config.yaml: |- + installCRDs: {{ .Values.operator.installCRDs }} + enableTelemetry: {{ .Values.enableTelemetry }} + remoteContext: {{ .Values.remoteContext }} + captureDebug: {{ .Values.capture.debug }} + captureJobNumLimit: {{ .Values.capture.jobNumLimit }} +{{- with .Values.capture.hostPathBaseDir }} + captureHostPathBaseDir: {{ . | quote }} +{{- end }} + enableManagedStorageAccount: {{ .Values.capture.enableManagedStorageAccount }} + telemetryInterval: {{ .Values.operator.telemetryInterval }} +{{- if .Values.capture.enableManagedStorageAccount }} + azureCredentialConfig: /etc/cloud-config/azure.json +{{- end }} +{{- end }} diff --git a/deploy/standard/manifests/controller/helm/retina/templates/operator.yaml b/deploy/standard/manifests/controller/helm/retina/templates/operator.yaml index c63c07e8fb..693538e4c1 100644 --- a/deploy/standard/manifests/controller/helm/retina/templates/operator.yaml +++ b/deploy/standard/manifests/controller/helm/retina/templates/operator.yaml @@ -24,6 +24,10 @@ spec: kubectl.kubernetes.io/default-container: {{ .Values.operator.name }} prometheus.io/port: "{{ .Values.operatorService.port }}" prometheus.io/scrape: "true" + # Roll the operator pod whenever its ConfigMap rendering changes, + # so `helm upgrade` applies new config values (e.g. `remoteContext`) + # without requiring a manual `kubectl rollout restart`. + checksum/config: {{ include (print $.Template.BasePath "/operator-configmap.yaml") . | sha256sum }} labels: app: {{ .Values.operator.name }} control-plane: {{ .Values.operator.name }} @@ -283,24 +287,6 @@ subjects: name: {{ .Values.operator.name }} namespace: {{ .Values.namespace }} --- -apiVersion: v1 -kind: ConfigMap -metadata: - name: "{{ .Values.operator.name }}-config" - namespace: {{ .Values.namespace }} -data: - operator-config.yaml: |- - installCRDs: {{ .Values.operator.installCRDs }} - enableTelemetry: {{ .Values.enableTelemetry }} - remoteContext: {{ .Values.remoteContext }} - captureDebug: {{ .Values.capture.debug }} - captureJobNumLimit: {{ .Values.capture.jobNumLimit }} - enableManagedStorageAccount: {{ .Values.capture.enableManagedStorageAccount }} - telemetryInterval: {{ .Values.operator.telemetryInterval }} -{{- if .Values.capture.enableManagedStorageAccount }} - azureCredentialConfig: /etc/cloud-config/azure.json -{{- end }} ---- {{- if .Values.capture.enableManagedStorageAccount }} apiVersion: v1 kind: Secret diff --git a/deploy/standard/manifests/controller/helm/retina/templates/rbac.yaml b/deploy/standard/manifests/controller/helm/retina/templates/rbac.yaml index 193055cb9d..ff739cb724 100644 --- a/deploy/standard/manifests/controller/helm/retina/templates/rbac.yaml +++ b/deploy/standard/manifests/controller/helm/retina/templates/rbac.yaml @@ -11,6 +11,9 @@ rules: - apiGroups: [""] # "" indicates the core API group resources: ["pods", "services", "replicationcontrollers", "nodes", "namespaces"] verbs: ["get", "watch", "list"] + - apiGroups: ["discovery.k8s.io"] + resources: ["endpointslices"] + verbs: ["get", "watch", "list"] - apiGroups: ["apps"] resources: ["deployments", "replicasets"] verbs: ["get", "watch", "list"] @@ -26,14 +29,6 @@ rules: - list - watch {{- if .Values.operator.enabled }} - - apiGroups: - - "" - resources: - - namespaces - verbs: - - get - - list - - watch - apiGroups: - retina.sh resources: diff --git a/deploy/standard/manifests/controller/helm/retina/values.yaml b/deploy/standard/manifests/controller/helm/retina/values.yaml index 2e233b6bfb..784ba49177 100644 --- a/deploy/standard/manifests/controller/helm/retina/values.yaml +++ b/deploy/standard/manifests/controller/helm/retina/values.yaml @@ -56,6 +56,22 @@ remoteContext: false enableAnnotations: true bypassLookupIPOfInterest: false dataAggregationLevel: "low" +dataSamplingRate: 1 +# Use BPF ring buffers (BPF_MAP_TYPE_RINGBUF) instead of BPF_PERF_EVENT_ARRAY. +# Pros: lower per-event overhead at high event rates, simpler variable-sized records, more consistent latency. +# Cons: fixed-size pre-allocated (locked) memory that can be wasted on low traffic; events are dropped when the buffer fills (reserve fails). +# Possible values: "enabled", "disabled". +packetParserRingBuffer: "disabled" +# Ring buffer size in bytes (only used when packetParserRingBuffer="enabled"). +# Default: 8MiB (8388608). +# Valid range: power of two between the kernel page size and 1GiB (inclusive). +# Invalid values cause startup to fail with a validation error. +# This value is not applied when ring buffers are disabled. +packetParserRingBufferSize: 8388608 +# Maximum number of entries in the eBPF filter map (retina_filter). +# This map tracks IP addresses of pods of interest for network observability. +# Default: 255. Increase for large clusters with many tracked pods. +filterMapMaxEntries: 255 imagePullSecrets: [] nameOverride: "retina" @@ -84,6 +100,8 @@ enabledPlugin_win: '["hnsstats", "ebpfwindows"]' enableTelemetry: false # Interval, in duration, to scrape/publish metrics. +# Default when unset or invalid (used by pluginmanager and all plugins). +metricsInterval: "10s" metricsIntervalDuration: "10s" azure: @@ -98,6 +116,7 @@ daemonset: args: - "--config" - "/retina/config/config.yaml" + env: [] healthProbeBindAddress: ":18081" metricsBindAddress: ":18080" ports: @@ -125,6 +144,7 @@ securityContext: - SYS_ADMIN - NET_ADMIN # for packetparser plugin - IPC_LOCK # for mmap() calls made by NewReader(), ref: https://man7.org/linux/man-pages/man2/mmap.2.html + - SYS_RESOURCE # for setting rlimit windowsOptions: runAsUserName: "NT AUTHORITY\\SYSTEM" @@ -158,6 +178,12 @@ capture: debug: true # jobNumLimit indicates the maximum number of jobs that can be created for each Capture. jobNumLimit: 0 + # hostPathBaseDir is the absolute directory on every node under which Capture + # CRs may write artifacts. The CR field outputConfiguration.hostPath is treated + # as a relative subpath name and joined under this directory; CR authors + # cannot influence the base. Leave empty to use the operator default + # (/var/log/retina/captures). + hostPathBaseDir: "" # enableManagedStorageAccount toggles the use of managed storage account for storing artifacts. # If set to true, the following fields related to Azure credentials must be set. # Ref: docs/captures/managed-storage-account.md diff --git a/deploy/standard/prometheus/deploy-retina-clusters.sh b/deploy/standard/prometheus/deploy-retina-clusters.sh index 1c74a989c5..82f922fd9c 100755 --- a/deploy/standard/prometheus/deploy-retina-clusters.sh +++ b/deploy/standard/prometheus/deploy-retina-clusters.sh @@ -65,16 +65,6 @@ az aks nodepool add \ --os-sku Windows2022 \ --node-count 1 -# Set variables for Windows 2019 node pool -myWindowsNodePool="nwin19" # Length <= 6 -az aks nodepool add \ - --resource-group $RESOURCE_GROUP \ - --cluster-name $NAME \ - --name $myWindowsNodePool \ - --os-type Windows \ - --os-sku Windows2019 \ - --node-count 1 - az aks get-credentials -g $RESOURCE_GROUP -n $NAME --overwrite-existing kubectl apply -f ama-metrics-settings-configmap.yaml diff --git a/docs/01-Introduction/01-intro.md b/docs/01-Introduction/01-intro.md index 5924a9bba8..8ffab06297 100644 --- a/docs/01-Introduction/01-intro.md +++ b/docs/01-Introduction/01-intro.md @@ -1,3 +1,7 @@ +--- +slug: /Introduction/intro +--- + # What is Retina? ## Introduction @@ -94,3 +98,18 @@ Check out our talk from KubeCon 2024 which goes into this topic even further - [ The following are known system requirements for installing Retina: - Minimum Linux Kernel Version: v5.4.0 + +## Known Limitations + +### Performance Considerations for High-Core-Count Systems + +Community users have reported performance considerations when using **Advanced metrics with the `packetparser` plugin** on nodes with high CPU core counts (32+ cores) under sustained, high-volume network load. + +If you plan to deploy Retina in Advanced mode on large node types with network-intensive workloads, consider: + +1. **Start with Basic metrics mode** (does not use `packetparser`) +2. Enable `dataSamplingRate` if you need Advanced metrics +3. Monitor CPU usage and network throughput after deployment +4. See [`packetparser` performance considerations](../03-Metrics/plugins/Linux/packetparser.md#performance-considerations) for more information + +The Retina team is evaluating options to address these reported concerns in future releases. diff --git a/docs/01-Introduction/02-architecture.md b/docs/01-Introduction/02-architecture.md index b125aeb7ca..6169d766e8 100644 --- a/docs/01-Introduction/02-architecture.md +++ b/docs/01-Introduction/02-architecture.md @@ -14,6 +14,8 @@ The plugins have a very specific scope by design, and Retina is designed to be e The plugins are responsible for installing the eBPF programs into the host kernel during startup. These eBPF programs collect metrics from events in the kernel level, which are then passed to the user space where they are parsed and converted into a `flow` data structure. Depending on the Control Plane being used, the data will either be sent to a Retina Enricher, or written to an external channel which is consumed by a Hubble observer - more on this in the [Control Plane](#control-plane) section below. It is not required for a plugin to use eBPF, it can also use syscalls or other API calls. In either case, the plugins will implement the same [interface](https://github.com/microsoft/retina/blob/main/pkg/plugin/registry/registry.go). +**Data Transfer Mechanisms:** eBPF programs transfer data from kernel to user space using specialized data structures. The `packetparser` plugin currently uses **perf arrays** (BPF_MAP_TYPE_PERF_EVENT_ARRAY), which create per-CPU buffers. Community users have reported performance considerations with this approach on high-core-count systems. See [packetparser performance considerations](../03-Metrics/plugins/Linux/packetparser.md#performance-considerations) for details. + Some examlpes of existing Retina plugins: - Drop Reason - measures the number of packets/bytes dropped and the reason and the direction of the drop. diff --git a/docs/01-Introduction/img/control-plane.excalidraw b/docs/01-Introduction/img/control-plane.excalidraw index 997b287f7d..f8e02f8c43 100644 --- a/docs/01-Introduction/img/control-plane.excalidraw +++ b/docs/01-Introduction/img/control-plane.excalidraw @@ -5,8 +5,8 @@ "elements": [ { "type": "rectangle", - "version": 230, - "versionNonce": 171869816, + "version": 370, + "versionNonce": 591263446, "index": "at", "isDeleted": false, "id": "e3X3FsYsKyc5vzIaQEkdw", @@ -17,11 +17,11 @@ "opacity": 100, "angle": 0, "x": 78.5, - "y": 841.2285322675798, + "y": 795.2285322675798, "strokeColor": "#2f9e44", "backgroundColor": "transparent", "width": 593, - "height": 320.77146773242015, + "height": 366.77146773242015, "seed": 1251338007, "groupIds": [], "frameId": null, @@ -29,14 +29,14 @@ "type": 3 }, "boundElements": [], - "updated": 1736986598046, + "updated": 1748440447638, "link": null, "locked": false }, { "type": "rectangle", - "version": 204, - "versionNonce": 1938447224, + "version": 508, + "versionNonce": 1311912598, "index": "ay", "isDeleted": false, "id": "s76sKmY7lWfW7aIIgFJbD", @@ -46,12 +46,12 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 264.68867302857984, - "y": 1013.5, + "x": 149.0713857748221, + "y": 934.9329807812408, "strokeColor": "#1e1e1e", "backgroundColor": "#b2f2bb", - "width": 216, - "height": 99, + "width": 172.61728725375775, + "height": 84.56701921875924, "seed": 747932279, "groupIds": [], "frameId": null, @@ -70,16 +70,20 @@ { "id": "DUD5pdi7NQXIRxtee70x9", "type": "arrow" + }, + { + "id": "6NVvUWMSCK2NWmpPxEvD1", + "type": "arrow" } ], - "updated": 1736986670614, + "updated": 1748440447638, "link": null, "locked": false }, { "type": "text", - "version": 405, - "versionNonce": 1946433912, + "version": 866, + "versionNonce": 1296346134, "index": "az", "isDeleted": false, "id": "xuSA2bAc_t-HYY8gtKTf6", @@ -89,18 +93,18 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 333.01870537721265, - "y": 1050.5, + "x": 196.09006663314628, + "y": 964.7164903906204, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 79.33993530273438, + "width": 78.57992553710938, "height": 25, "seed": 969847626, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1736986649279, + "updated": 1748440447638, "link": null, "locked": false, "fontSize": 20, @@ -115,8 +119,8 @@ }, { "type": "rectangle", - "version": 577, - "versionNonce": 1974394232, + "version": 682, + "versionNonce": 727629846, "index": "b00", "isDeleted": false, "id": "ffioimfd5lplFqrar_3MY", @@ -138,18 +142,27 @@ "roundness": { "type": 3 }, - "boundElements": [], - "updated": 1736984179771, + "boundElements": [ + { + "id": "QuZ5FBcpl4jamVYT2fW3w", + "type": "arrow" + }, + { + "id": "CFgmxr_lGn-ZQtxsGWPsU", + "type": "arrow" + } + ], + "updated": 1748440447638, "link": null, "locked": false }, { "id": "QuZ5FBcpl4jamVYT2fW3w", "type": "arrow", - "x": 367.48664235385485, - "y": 1206.3092702754698, - "width": 0.23669352213681805, - "height": 90.54554607871114, + "x": 231.48664235385493, + "y": 1212.931885150108, + "width": 5.684341886080802e-14, + "height": 84.15842385058886, "angle": 0, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", @@ -165,11 +178,11 @@ "type": 2 }, "seed": 2131312471, - "version": 152, - "versionNonce": 37238904, + "version": 780, + "versionNonce": 151194506, "isDeleted": false, "boundElements": [], - "updated": 1736986697521, + "updated": 1748440447640, "link": null, "locked": false, "points": [ @@ -178,17 +191,20 @@ 0 ], [ - 0.23669352213681805, - -90.54554607871114 + -5.684341886080802e-14, + -84.15842385058886 ] ], "lastCommittedPoint": null, - "startBinding": null, + "startBinding": { + "elementId": "ffioimfd5lplFqrar_3MY", + "focus": -0.4764258714124793, + "gap": 15.024103747967729 + }, "endBinding": { - "elementId": "s76sKmY7lWfW7aIIgFJbD", - "focus": 0.08569938797777649, - "gap": 3.2637241967586306, - "fixedPoint": null + "elementId": "mOQHb51yxdi4LJPn8XGhd", + "focus": 0.05509355735459114, + "gap": 9.71649039062072 }, "startArrowhead": null, "endArrowhead": "arrow", @@ -197,8 +213,8 @@ { "id": "CvGBI1ByJ4ksItctBUyD7", "type": "arrow", - "x": 367.2357749871671, - "y": 830.3497040580879, + "x": 362.2357749871671, + "y": 777.3497040580879, "width": 0.7384282555123036, "height": 85.65469253861232, "angle": 0, @@ -216,11 +232,11 @@ "type": 2 }, "seed": 2136439543, - "version": 593, - "versionNonce": 115321608, + "version": 736, + "versionNonce": 1326047574, "isDeleted": false, "boundElements": [], - "updated": 1736986697521, + "updated": 1748440453822, "link": null, "locked": false, "points": [ @@ -262,11 +278,11 @@ "type": 3 }, "seed": 1540148744, - "version": 1049, - "versionNonce": 901965576, + "version": 1136, + "versionNonce": 143466134, "isDeleted": false, "boundElements": [], - "updated": 1736984179772, + "updated": 1748440447638, "link": null, "locked": false }, @@ -292,11 +308,11 @@ "type": 3 }, "seed": 2081803528, - "version": 1051, - "versionNonce": 696839544, + "version": 1138, + "versionNonce": 1766473162, "isDeleted": false, "boundElements": [], - "updated": 1736984179772, + "updated": 1748440447638, "link": null, "locked": false }, @@ -322,8 +338,8 @@ "type": 3 }, "seed": 1223093256, - "version": 997, - "versionNonce": 1105249800, + "version": 1084, + "versionNonce": 1440275414, "isDeleted": false, "boundElements": [ { @@ -331,16 +347,16 @@ "id": "80Rqys5FW5W8TbyqTbjJL" } ], - "updated": 1736984179772, + "updated": 1748440447638, "link": null, "locked": false }, { "id": "80Rqys5FW5W8TbyqTbjJL", "type": "text", - "x": 179.63206172332542, + "x": 180.38213801727073, "y": 1301.8987447193506, - "width": 78.36387634277344, + "width": 76.86372375488281, "height": 28.786636059782506, "angle": 0, "strokeColor": "#1e1e1e", @@ -355,11 +371,11 @@ "index": "b14", "roundness": null, "seed": 1010576136, - "version": 1411, - "versionNonce": 1576828536, + "version": 1665, + "versionNonce": 898888842, "isDeleted": false, "boundElements": [], - "updated": 1736984179772, + "updated": 1748440447638, "link": null, "locked": false, "text": "Plugins", @@ -394,11 +410,11 @@ "type": 3 }, "seed": 814844424, - "version": 1343, - "versionNonce": 1827325192, + "version": 1430, + "versionNonce": 356184342, "isDeleted": false, "boundElements": [], - "updated": 1736984179772, + "updated": 1748440447638, "link": null, "locked": false }, @@ -424,11 +440,11 @@ "type": 3 }, "seed": 1357056264, - "version": 1342, - "versionNonce": 1070985080, + "version": 1429, + "versionNonce": 620286794, "isDeleted": false, "boundElements": [], - "updated": 1736984179772, + "updated": 1748440447638, "link": null, "locked": false }, @@ -454,8 +470,8 @@ "type": 3 }, "seed": 145532936, - "version": 1347, - "versionNonce": 1146938376, + "version": 1434, + "versionNonce": 1646812758, "isDeleted": false, "boundElements": [ { @@ -463,16 +479,16 @@ "id": "VmiWjaSbxHfEmmAKWJXGr" } ], - "updated": 1736984179772, + "updated": 1748440447638, "link": null, "locked": false }, { "id": "VmiWjaSbxHfEmmAKWJXGr", "type": "text", - "x": 440.54417270080165, + "x": 441.29424899474697, "y": 1297.6899726837808, - "width": 103.43266296386719, + "width": 101.93251037597656, "height": 28.786636059782506, "angle": 0, "strokeColor": "#1e1e1e", @@ -487,11 +503,11 @@ "index": "b18", "roundness": null, "seed": 1944741640, - "version": 1768, - "versionNonce": 1733902456, + "version": 2022, + "versionNonce": 2043927050, "isDeleted": false, "boundElements": [], - "updated": 1736984179772, + "updated": 1748440447638, "link": null, "locked": false, "text": "Watchers", @@ -507,8 +523,8 @@ { "id": "yQRBtn4Sn8fhshiuriGJP", "type": "image", - "x": 271.2705725972662, - "y": 649.7836515423428, + "x": 266.2705725972662, + "y": 596.7836515423428, "width": 91.30937122163323, "height": 90.83332061767578, "angle": 0, @@ -524,11 +540,11 @@ "index": "b19", "roundness": null, "seed": 162758776, - "version": 724, - "versionNonce": 225378168, + "version": 867, + "versionNonce": 1744832266, "isDeleted": false, "boundElements": [], - "updated": 1736986606306, + "updated": 1748440453822, "link": null, "locked": false, "status": "saved", @@ -549,8 +565,8 @@ { "id": "1KQ6WLF-oBYjWDBh9VwJZ", "type": "image", - "x": 369.2471191118682, - "y": 647.689869117416, + "x": 364.2471191118682, + "y": 594.689869117416, "width": 104.1665649414062, "height": 95.02088546752927, "angle": 0, @@ -566,11 +582,11 @@ "index": "b1A", "roundness": null, "seed": 1045750136, - "version": 515, - "versionNonce": 1062228088, + "version": 658, + "versionNonce": 489637526, "isDeleted": false, "boundElements": [], - "updated": 1736986606306, + "updated": 1748440453822, "link": null, "locked": false, "status": "saved", @@ -590,8 +606,8 @@ }, { "type": "rectangle", - "version": 269, - "versionNonce": 1322507640, + "version": 561, + "versionNonce": 836822218, "index": "b1F", "isDeleted": false, "id": "EK8sn12c99l9gSQpC7PqR", @@ -601,12 +617,12 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 261.61523574512586, - "y": 872.4248518016716, + "x": 145.61523574512586, + "y": 827.4248518016716, "strokeColor": "#1e1e1e", "backgroundColor": "#b2f2bb", - "width": 216, - "height": 99, + "width": 172.61728725375775, + "height": 84.56701921875924, "seed": 459565832, "groupIds": [], "frameId": null, @@ -617,20 +633,16 @@ { "type": "text", "id": "QgoUyOB0rWShIdxYAZlB5" - }, - { - "id": "DUD5pdi7NQXIRxtee70x9", - "type": "arrow" } ], - "updated": 1736986670614, + "updated": 1748440460753, "link": null, "locked": false }, { "type": "text", - "version": 484, - "versionNonce": 1598835576, + "version": 933, + "versionNonce": 947412874, "index": "b1G", "isDeleted": false, "id": "QgoUyOB0rWShIdxYAZlB5", @@ -640,18 +652,18 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 296.885285793954, - "y": 909.4248518016716, + "x": 159.32393430364536, + "y": 857.2083614110512, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 145.45989990234375, + "width": 145.19989013671875, "height": 25, "seed": 11651592, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1736986662222, + "updated": 1748440447638, "link": null, "locked": false, "fontSize": 20, @@ -665,12 +677,88 @@ "lineHeight": 1.25 }, { - "id": "DUD5pdi7NQXIRxtee70x9", + "type": "rectangle", + "version": 543, + "versionNonce": 238137354, + "index": "b1I", + "isDeleted": false, + "id": "mOQHb51yxdi4LJPn8XGhd", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 149.93304893483042, + "y": 1039.5795780498563, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 172.61728725375775, + "height": 84.56701921875924, + "seed": 485845386, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "h6vbXCI82MyW08QlpWzUS" + }, + { + "id": "QuZ5FBcpl4jamVYT2fW3w", + "type": "arrow" + } + ], + "updated": 1748440447638, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 909, + "versionNonce": 781419350, + "index": "b1J", + "isDeleted": false, + "id": "h6vbXCI82MyW08QlpWzUS", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 180.0217447467679, + "y": 1069.363087659236, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 112.43989562988281, + "height": 25, + "seed": 1784916042, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1748440447638, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 5, + "text": "Ring Buffer", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "mOQHb51yxdi4LJPn8XGhd", + "originalText": "Ring Buffer", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "_e8OSfo2Im20ekq3_CqUK", "type": "arrow", - "x": 367.6049891149232, - "y": 1006.8617545663202, - "width": 0, - "height": 31.51517001065349, + "x": 231.28312952460504, + "y": 1039.2631403180974, + "width": 0.08287392579143216, + "height": 18.800105317722455, "angle": 0, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", @@ -681,16 +769,16 @@ "opacity": 100, "groupIds": [], "frameId": null, - "index": "b1H", + "index": "b1L", "roundness": { "type": 2 }, - "seed": 1829638408, - "version": 31, - "versionNonce": 411724152, + "seed": 1162155786, + "version": 762, + "versionNonce": 1490650378, "isDeleted": false, - "boundElements": null, - "updated": 1736986697521, + "boundElements": [], + "updated": 1748440447638, "link": null, "locked": false, "points": [ @@ -698,24 +786,248 @@ 0, 0 ], + [ + -0.08287392579143216, + -18.800105317722455 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "type": "rectangle", + "version": 695, + "versionNonce": 187722186, + "index": "b1M", + "isDeleted": false, + "id": "61pa7CE40d3XVWBJLJB5v", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 426.9330489348304, + "y": 935.5795780498568, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 172.61728725375775, + "height": 84.56701921875924, + "seed": 333335050, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "zMkhTJe9b_GQQ8HbTxa1-" + }, + { + "id": "CFgmxr_lGn-ZQtxsGWPsU", + "type": "arrow" + }, + { + "id": "6NVvUWMSCK2NWmpPxEvD1", + "type": "arrow" + } + ], + "updated": 1748440447638, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 1056, + "versionNonce": 1513640906, + "index": "b1N", + "isDeleted": false, + "id": "zMkhTJe9b_GQQ8HbTxa1-", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 485.11171819647495, + "y": 965.3630876592364, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 56.25994873046875, + "height": 25, + "seed": 1113356490, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1748440447638, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 5, + "text": "Cache", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "61pa7CE40d3XVWBJLJB5v", + "originalText": "Cache", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "CFgmxr_lGn-ZQtxsGWPsU", + "type": "arrow", + "x": 519.9420862849297, + "y": 1213.5586393663832, + "width": 0.4599132255937093, + "height": 184.56031911299942, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1O", + "roundness": { + "type": 2 + }, + "seed": 181407178, + "version": 949, + "versionNonce": 141363786, + "isDeleted": false, + "boundElements": [], + "updated": 1748440447640, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.4599132255937093, + -184.56031911299942 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "ffioimfd5lplFqrar_3MY", + "focus": 0.4978491902677033, + "gap": 13.056926879035927 + }, + "endBinding": { + "elementId": "61pa7CE40d3XVWBJLJB5v", + "focus": -0.07073023493432183, + "gap": 17.716490390620038 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "6NVvUWMSCK2NWmpPxEvD1", + "type": "arrow", + "x": 329.2274001542904, + "y": 978.2756347614246, + "width": 90.0471458881147, + "height": 0.5773793669147835, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1P", + "roundness": { + "type": 2 + }, + "seed": 1528929994, + "version": 411, + "versionNonce": 1873359114, + "isDeleted": false, + "boundElements": null, + "updated": 1748440447640, + "link": null, + "locked": false, + "points": [ [ 0, - -31.51517001065349 + 0 + ], + [ + 90.0471458881147, + -0.5773793669147835 ] ], "lastCommittedPoint": null, "startBinding": { "elementId": "s76sKmY7lWfW7aIIgFJbD", - "focus": -0.00487150396282619, - "gap": 6.638245433679799, - "fixedPoint": null + "focus": 0.03894183060552685, + "gap": 8.553019533129486 }, "endBinding": { - "elementId": "EK8sn12c99l9gSQpC7PqR", - "focus": -0.023586248661747734, - "gap": 3.9217327539951157, - "fixedPoint": null + "elementId": "61pa7CE40d3XVWBJLJB5v", + "focus": 0.017911219478604776, + "gap": 8.691356373121096 + }, + "startArrowhead": "arrow", + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "EloHGnpWQI88pjLDdZIaF", + "type": "arrow", + "x": 230.28312952460504, + "y": 933.2631403180972, + "width": 0.08287392579143216, + "height": 18.800105317722455, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1Q", + "roundness": { + "type": 2 }, + "seed": 729114454, + "version": 807, + "versionNonce": 1806840650, + "isDeleted": false, + "boundElements": [], + "updated": 1748440465451, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.08287392579143216, + -18.800105317722455 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow", "elbowed": false @@ -725,7 +1037,8 @@ "gridSize": 20, "gridStep": 5, "gridModeEnabled": false, - "viewBackgroundColor": "#ffffff" + "viewBackgroundColor": "#ffffff", + "lockedMultiSelections": {} }, "files": { "12236d9445da554c5fe0bbb860b540c2168c74a1": { diff --git a/docs/01-Introduction/img/control-plane.png b/docs/01-Introduction/img/control-plane.png index 91ebbb572a..3f23aa6d4c 100644 Binary files a/docs/01-Introduction/img/control-plane.png and b/docs/01-Introduction/img/control-plane.png differ diff --git a/docs/01-Introduction/img/data-plane.excalidraw b/docs/01-Introduction/img/data-plane.excalidraw index 73be74a76d..9df4faba21 100644 --- a/docs/01-Introduction/img/data-plane.excalidraw +++ b/docs/01-Introduction/img/data-plane.excalidraw @@ -588,7 +588,8 @@ "startBinding": null, "endBinding": null, "startArrowhead": null, - "endArrowhead": null + "endArrowhead": null, + "polygon": false }, { "id": "ydmFub-8BoqVbnYfipJ-t", @@ -1252,16 +1253,11 @@ "index": "b1X", "roundness": null, "seed": 2023979896, - "version": 897, - "versionNonce": 1068160264, + "version": 898, + "versionNonce": 1900992394, "isDeleted": false, - "boundElements": [ - { - "id": "1tRtua1u0RkMdUh3gjcmi", - "type": "arrow" - } - ], - "updated": 1736981707109, + "boundElements": [], + "updated": 1748438210474, "link": null, "locked": false, "text": "Watcher Manager", @@ -1274,61 +1270,6 @@ "autoResize": true, "lineHeight": 1.25 }, - { - "id": "1tRtua1u0RkMdUh3gjcmi", - "type": "arrow", - "x": 1176.7477959794871, - "y": 338.2328976508133, - "width": 85.04785606997076, - "height": 137.79711176768973, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#96f2d7", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b1Y", - "roundness": { - "type": 2 - }, - "seed": 1184189560, - "version": 738, - "versionNonce": 1868748296, - "isDeleted": false, - "boundElements": [], - "updated": 1736981707109, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -1.5482821546930372, - -63.47956834241896 - ], - [ - -85.04785606997076, - -137.79711176768973 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "dpekxaD77Y_HDd20Rd59g", - "focus": 1.1146493866138798, - "gap": 11.602100636366941, - "fixedPoint": null - }, - "endBinding": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "elbowed": false - }, { "id": "SNQw7x8t7zQGeYtXwT3W1", "type": "rectangle", @@ -1381,8 +1322,8 @@ "type": 3 }, "seed": 1396873080, - "version": 169, - "versionNonce": 1662242168, + "version": 171, + "versionNonce": 325684630, "isDeleted": false, "boundElements": [ { @@ -1390,17 +1331,17 @@ "id": "_pl-vyPSCNw_D24KjRN3P" } ], - "updated": 1736981706947, + "updated": 1748437643530, "link": null, "locked": false }, { "id": "_pl-vyPSCNw_D24KjRN3P", "type": "text", - "x": 869.3600695168553, - "y": 129.82987544409286, - "width": 71.62249755859375, - "height": 22.613951497751323, + "x": 848.9513704812108, + "y": 128.63685119296852, + "width": 112.43989562988281, + "height": 25, "angle": 0, "strokeColor": "#1e1e1e", "backgroundColor": "#b2f2bb", @@ -1414,20 +1355,20 @@ "index": "b1bG", "roundness": null, "seed": 489024376, - "version": 236, - "versionNonce": 2083049336, + "version": 252, + "versionNonce": 1209273674, "isDeleted": false, "boundElements": [], - "updated": 1736981706947, + "updated": 1748438385414, "link": null, "locked": false, - "text": "Enricher", - "fontSize": 18.091161198201057, + "text": "Ring Buffer", + "fontSize": 20, "fontFamily": 5, "textAlign": "center", "verticalAlign": "middle", "containerId": "z9YoMOkSFkPWTsUTTSkwH", - "originalText": "Enricher", + "originalText": "Ring Buffer", "autoResize": true, "lineHeight": 1.25 }, @@ -1469,10 +1410,10 @@ { "id": "INytNNGW35A1LfRrBpWxU", "type": "text", - "x": 1073.2739879747762, - "y": 129.82987544409283, - "width": 146.76405334472656, - "height": 22.613951497751323, + "x": 1065.5260707994833, + "y": 128.6368511929685, + "width": 162.2598876953125, + "height": 25, "angle": 0, "strokeColor": "#1e1e1e", "backgroundColor": "#b2f2bb", @@ -1486,15 +1427,15 @@ "index": "b1e", "roundness": null, "seed": 1481045880, - "version": 274, - "versionNonce": 883712632, + "version": 276, + "versionNonce": 101306902, "isDeleted": false, "boundElements": [], - "updated": 1736981706948, + "updated": 1748438380516, "link": null, "locked": false, "text": "External Channel", - "fontSize": 18.091161198201057, + "fontSize": 20, "fontFamily": 5, "textAlign": "center", "verticalAlign": "middle", @@ -1539,13 +1480,88 @@ "originalText": "OR", "autoResize": true, "lineHeight": 1.25 + }, + { + "id": "EOC1C6l7kpfOlF48Jw93i", + "type": "text", + "x": 1121.2035355363544, + "y": 174.09997446362837, + "width": 47.88795471191406, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1g", + "roundness": null, + "seed": 1479607062, + "version": 85, + "versionNonce": 439501590, + "isDeleted": false, + "boundElements": null, + "updated": 1748438418192, + "link": null, + "locked": false, + "text": "Hubble", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Hubble", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "nKTJ3k3MqRAxII2NNYAMD", + "type": "text", + "x": 871.2595581803973, + "y": 175.09997446362837, + "width": 71.59994506835938, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1h", + "roundness": null, + "seed": 1286369930, + "version": 234, + "versionNonce": 767165514, + "isDeleted": false, + "boundElements": [], + "updated": 1748438501334, + "link": null, + "locked": false, + "text": "Standard", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Standard", + "autoResize": true, + "lineHeight": 1.25 } ], "appState": { "gridSize": 20, "gridStep": 5, "gridModeEnabled": false, - "viewBackgroundColor": "#ffffff" + "viewBackgroundColor": "#ffffff", + "lockedMultiSelections": {} }, "files": {} } \ No newline at end of file diff --git a/docs/01-Introduction/img/data-plane.png b/docs/01-Introduction/img/data-plane.png index dbdf731619..d391898ca0 100644 Binary files a/docs/01-Introduction/img/data-plane.png and b/docs/01-Introduction/img/data-plane.png differ diff --git a/docs/02-Installation/01-Setup.md b/docs/02-Installation/01-Setup.md index b3fd0dc2ac..13c1751dd8 100644 --- a/docs/02-Installation/01-Setup.md +++ b/docs/02-Installation/01-Setup.md @@ -2,8 +2,6 @@ This page provides the instructions on how to install Retina via Helm. -## Installation - The assumption is that a Kubernetes cluster has already been created and we have credentials to access it. >NOTE: In case you want to test out Retina quickly and you have no clusters, you can quickly create one with [kind](https://kind.sigs.k8s.io/) @@ -25,12 +23,12 @@ kubectl cluster-info --context kind-test-retina Not sure what to do next? 😅 Check out https://kind.sigs.k8s.io/docs/user/quick-start/ ``` -### Requirements +## Requirements - Helm version >= v3.8.0 - Access to a Kubernetes cluster via `kubectl` -### Control Plane and Modes +## Control Plane and Modes The installation of Retina can be configured using different control planes and modes. @@ -40,7 +38,15 @@ If the "Standard" control plane is chosen, different modes are available. The av Modes are not applicable to the Hubble control plane. For metrics related to the Hubble control plane, refer to the [Hubble metrics](../03-Metrics/02-hubble_metrics.md) documentation. -### Basic Mode +## Capture Support + +In order to support the use of the [Capture CRD](../05-Concepts/CRDs/Capture.md), the Standard Control Plane must be used, and the Retina operator pod needs to be running. + +>NOTE: Captures can still be triggered with the [CLI](../04-Captures/02-cli.md) even without the Retina operator pod running. + +Enable the operator with the `--set operator.enabled=true \` flag. + +For example, this is how you could install Retina with the Standard Control Plane and basic metric mode, with the operator. ```shell VERSION=$( curl -sL https://api.github.com/repos/microsoft/retina/releases/latest | jq -r .name) @@ -50,10 +56,19 @@ helm upgrade --install retina oci://ghcr.io/microsoft/retina/charts/retina \ --set image.tag=$VERSION \ --set operator.tag=$VERSION \ --set logLevel=info \ + --set operator.enabled=true \ --set enabledPlugin_linux="\[dropreason\,packetforward\,linuxutil\,dns\]" ``` -### Basic Mode (with Capture support) +## Installation + +### Standard Control Plane + +>NOTE: Before installing Retina via Helm, you need to authenticate to [GitHub Container Registry (GHCR)](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry) to pull the required images. + +Run the following command: `docker login ghcr.io` + +#### Basic Mode ```shell VERSION=$( curl -sL https://api.github.com/repos/microsoft/retina/releases/latest | jq -r .name) @@ -63,16 +78,10 @@ helm upgrade --install retina oci://ghcr.io/microsoft/retina/charts/retina \ --set image.tag=$VERSION \ --set operator.tag=$VERSION \ --set logLevel=info \ - --set image.pullPolicy=Always \ - --set logLevel=info \ - --set os.windows=true \ - --set operator.enabled=true \ - --set operator.enableRetinaEndpoint=true \ - --skip-crds \ - --set enabledPlugin_linux="\[dropreason\,packetforward\,linuxutil\,dns\,packetparser\]" + --set enabledPlugin_linux="\[dropreason\,packetforward\,linuxutil\,dns\]" ``` -### Advanced Mode with Remote Context (with Capture support) +#### Advanced Mode with Remote Context ```shell VERSION=$( curl -sL https://api.github.com/repos/microsoft/retina/releases/latest | jq -r .name) @@ -92,7 +101,7 @@ helm upgrade --install retina oci://ghcr.io/microsoft/retina/charts/retina \ --set remoteContext=true ``` -### Advanced Mode with Local Context (with Capture support) +#### Advanced Mode with Local Context ```shell VERSION=$( curl -sL https://api.github.com/repos/microsoft/retina/releases/latest | jq -r .name) @@ -112,7 +121,7 @@ helm upgrade --install retina oci://ghcr.io/microsoft/retina/charts/retina \ --set enableAnnotations=true ``` -### Hubble control plane +### Hubble Control Plane ```shell VERSION=$( curl -sL https://api.github.com/repos/microsoft/retina/releases/latest | jq -r .name) @@ -137,8 +146,3 @@ helm upgrade --install retina oci://ghcr.io/microsoft/retina/charts/retina-hubbl --set hubble.tls.auto.certValidityDuration=1 \ --set hubble.tls.auto.schedule="*/10 * * * *" ``` - -## Next Steps: Configuring Prometheus and Grafana - -- [Prometheus](./04-prometheus.md) -- [Grafana](./05-grafana.md) diff --git a/docs/02-Installation/03-Config.md b/docs/02-Installation/03-Config.md index b758337ae0..dc622e1227 100644 --- a/docs/02-Installation/03-Config.md +++ b/docs/02-Installation/03-Config.md @@ -50,9 +50,12 @@ Apply to both Agent and Operator. * `metricsIntervalDuration`: Interval for gathering metrics (in `time.Duration`). * `enablePodLevel`: Enables gathering of advanced pod-level metrics, attaching pods' metadata to Retina's metrics. * `enableConntrackMetrics`: Enables conntrack metrics for packets and bytes forwarded/received. -* `enableAnnotations`: Enables gathering of metrics for annotated resources. Resources can be annotated with `retina.sh=observe`. Requires the operator and `operator.enableRetinaEndpoint` to be enabled. +* `enableAnnotations`: Enables gathering of metrics for annotated resources. Resources can be annotated with `retina.sh=observe`. Requires the operator and `operator.enableRetinaEndpoint` to be enabled. By enabling annotations, the agent will not use MetricsConfiguration CRD. * `bypassLookupIPOfInterest`: If true, plugins like `packetparser` and `dropreason` will bypass IP lookup, generating an event for each packet regardless. `enableAnnotations` will not work if this is true. * `dataAggregationLevel`: Defines the level of data aggregation for Retina. See [Data Aggregation](../05-Concepts/data-aggregation.md) for more details. +* `dataSamplingRate`: Defines the data sampling rate for `packetparser`. See [Sampling](../03-Metrics/plugins/Linux/packetparser.md#sampling) for more details. +* `packetParserRingBuffer`: Selects the kernel-to-userspace transport for `packetparser`. Accepted values: `enabled` (ring buffer) or `disabled` (perf event array). `auto` is reserved for future use. +* `packetParserRingBufferSize`: Ring buffer size in bytes when `packetParserRingBuffer=enabled`. Must be a power of two between the kernel page size and 1GiB (inclusive); invalid values cause startup to fail. ## Operator Configuration @@ -60,4 +63,5 @@ Apply to both Agent and Operator. * `operator.enableRetinaEndpoint`: Allows the operator to monitor and update the cache with Pod metadata. * `capture.captureDebug`: Toggles debug mode for captures. If true, the operator uses the image from the test container registry for the capture workload. Refer to [Capture Image file](../../pkg/capture/utils/capture_image.go) for details on how the debug capture image version is selected. * `capture.captureJobNumLimit`: Sets the maximum number of jobs that can be created for each Capture. +* `capture.hostPathBaseDir`: Absolute directory on every node under which Capture CRs may write artifacts. The CR field `outputConfiguration.hostPath` is treated as a relative subpath name and joined under this directory; CR authors cannot influence the base. Defaults to `/var/log/retina/captures` when unset. * `capture.enableManagedStorageAccount`: Enables the use of a managed storage account for storing artifacts. diff --git a/docs/02-Installation/04-prometheus.md b/docs/02-Installation/04-prometheus.md index a5d7e67c20..1fd8d8cebb 100644 --- a/docs/02-Installation/04-prometheus.md +++ b/docs/02-Installation/04-prometheus.md @@ -1,4 +1,4 @@ -# Prometheus +# Configuring Prometheus Prometheus is an open-source system monitoring and alerting toolkit originally built at SoundCloud. It is now a standalone open source project and maintained independently of any company. Prometheus's main features include a multi-dimensional data model, a flexible query language, efficient time series database, and modern alerting approach. For more information, visit the [Prometheus website](https://prometheus.io). diff --git a/docs/02-Installation/05-grafana.md b/docs/02-Installation/05-grafana.md index 4725d61997..2e12c2e125 100644 --- a/docs/02-Installation/05-grafana.md +++ b/docs/02-Installation/05-grafana.md @@ -1,4 +1,4 @@ -# Grafana +# Configuring Grafana Grafana is an open-source platform for monitoring and observability. It allows you to query, visualize, alert on, and explore your metrics no matter where they are stored. For more information, visit the [Grafana open-source section](https://grafana.com/oss/). @@ -6,7 +6,7 @@ Grafana is an open-source platform for monitoring and observability. It allows y Make sure that you're still port-forwarding your server to [localhost:9090](http://localhost:9090) as part of [Prometheus](./04-prometheus.md) setup. Alternatively you can configure your server for some other HTTP endpoint, but that is not covered in the following instructions. -## Configuring Grafana +## Configuration If you followed the steps to install and configure [Prometheus](./04-prometheus.md), you will already have Grafana installed as part of the `kube-prometheus-stack` Helm chart. diff --git a/docs/03-Metrics/annotations.md b/docs/03-Metrics/annotations.md index fa171020ea..13592e7e42 100644 --- a/docs/03-Metrics/annotations.md +++ b/docs/03-Metrics/annotations.md @@ -1,11 +1,16 @@ # Annotations +**This feature is only available in Standard Control Plane.** + Annotations let you specify which Pods to observe (create metrics for). -To configure this, specify `enableAnnotations=true` in Retina's [helm installation](../02-Installation/01-Setup.md) or [ConfigMap](../02-Installation/03-Config.md). + +To enable it, specify `enableAnnotations=true` in Retina's Standard Control Plane [helm installation](../02-Installation/01-Setup.md) or [ConfigMap](../02-Installation/03-Config.md). You can then add the annotation `retina.sh: observe` to either: - individual Pods - Namespaces (to observe all the Pods in the namespace). -An exception: currently all Pods in `kube-system` are always monitored. +**Note 1**: If you enable Annotations, you cannot use the `MetricsConfiguration` CRD to specify which Pods to observe. + +**Note 2**: Currently the DNS plugin does not consider annotations when generating DNS metrics, so it generates metrics for all pods. diff --git a/docs/03-Metrics/configuration.md b/docs/03-Metrics/configuration.md index d562a339fc..3c7ee9421e 100644 --- a/docs/03-Metrics/configuration.md +++ b/docs/03-Metrics/configuration.md @@ -5,4 +5,7 @@ You can enable/disable metrics by including/omitting their Plugin from `enabledP Via [MetricsConfiguration CRD](../05-Concepts/CRDs/MetricsConfiguration.md), you can further customize the following for your enabled plugins: - Which metrics to include -- Which metadata to include for a metric. +- Which metadata to include for a metric +- Time-to-live for a metric + +**Note**: If you enable [Annotations](./annotations.md), you cannot use the `MetricsConfiguration` CRD to specify which Pods to observe. diff --git a/docs/03-Metrics/modes/modes.md b/docs/03-Metrics/modes/modes.md index ce2d3a4b00..46742db0fc 100644 --- a/docs/03-Metrics/modes/modes.md +++ b/docs/03-Metrics/modes/modes.md @@ -12,8 +12,10 @@ The larger the cardinality, the more load induced on a Prometheus server for ins | Mode | Description | Scale | Metrics | Configuration | | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------ | | *Basic* | Metrics aggregated by Node. | Metric cardinality proportional to number of nodes. | [Link to Metrics](./basic.md) | [Link to Installation](../../02-Installation/01-Setup.md#basic-mode) | -| *Advanced/Pod-Level with remote context* | Basic metrics plus extra metrics aggregated by source and destination Pod. | Has scale limitations. Metric cardinality is unbounded (proportional to number of source/destination pairs, including external IPs). | [Link to Metrics](./advanced.md) | [Link to Installation](../../02-Installation/01-Setup.md#advanced-mode-with-local-context-with-capture-support) | -| *Advanced/Pod-Level with local context* | Basic metrics plus extra metrics aggregated by "local" Pod (source for outgoing traffic, destination for incoming traffic). Also lets you specify which Pods to observe (create metrics for) with [Annotations](../annotations.md). | Designed for scale. Metric cardinality proportional to number of Pods observed. | [Link to Metrics](./advanced.md) | [Link to Installation](../../02-Installation/01-Setup.md#advanced-mode-with-local-context-with-capture-support) | +| *Advanced/Pod-Level with remote context* | Basic metrics plus extra metrics aggregated by source and destination Pod. | Has scale limitations. Metric cardinality is unbounded (proportional to number of source/destination pairs, including external IPs). | [Link to Metrics](./advanced.md) | [Link to Installation](../../02-Installation/01-Setup.md#advanced-mode-with-local-context) | +| *Advanced/Pod-Level with local context* | Basic metrics plus extra metrics aggregated by "local" Pod (source for outgoing traffic, destination for incoming traffic). | Designed for scale. Metric cardinality proportional to number of Pods observed. | [Link to Metrics](./advanced.md) | [Link to Installation](../../02-Installation/01-Setup.md#advanced-mode-with-local-context) | + +Both advanced metric modes let you specify which Pods to observe (create metrics for) by using [Annotations](../annotations.md) or [MetricsConfiguration CRD](../configuration.md). ## Where Do Metrics Come From? diff --git a/docs/03-Metrics/plugins/Linux/dns.md b/docs/03-Metrics/plugins/Linux/dns.md index eaba737e3c..1fbcdd4b1c 100644 --- a/docs/03-Metrics/plugins/Linux/dns.md +++ b/docs/03-Metrics/plugins/Linux/dns.md @@ -6,17 +6,18 @@ Tracks incoming and outgoing DNS traffic, providing various metrics and details The `dns` plugin requires the `CAP_SYS_ADMIN` capability. -- `CAP_SYS_ADMIN` is used to create a network tracer which invokes a tail call for updating the `ProgramArray` map - `NewTracer()` method at `dns_linux:50` +- `CAP_SYS_ADMIN` is used to load and attach the eBPF socket filter program ## Architecture -This plugin uses [Inspektor Gadget](https://github.com/inspektor-gadget/inspektor-gadget)'s DNS Tracer to track DNS traffic and generate basic metrics derived from the captured events. +The plugin uses a native eBPF socket filter attached to an `AF_PACKET` socket to capture DNS queries and responses. The BPF program extracts source/destination IPs, ports, and query type, then streams events to user space via a perf buffer. Go-side parsing uses `gopacket` for DNS name and response address extraction. In [Advanced mode](https://retina.sh/docs/Metrics/modes), the plugin further processes the capture results into an enriched Flow with additional Pod information. Subsequently, the Flow is transmitted to an external channel. This allows a DNS module to generate additional Pod-Level metrics. ### Code locations - Plugin and eBPF code: *pkg/plugin/dns/* +- BPF C source: *pkg/plugin/dns/_cprog/dns.c* - Module for extra Advanced metrics: *pkg/module/metrics/dns.go* ## Metrics diff --git a/docs/03-Metrics/plugins/Linux/packetparser.md b/docs/03-Metrics/plugins/Linux/packetparser.md index b772fe362a..3585ccf977 100644 --- a/docs/03-Metrics/plugins/Linux/packetparser.md +++ b/docs/03-Metrics/plugins/Linux/packetparser.md @@ -15,6 +15,49 @@ The `packetparser` plugin requires the `CAP_NET_ADMIN` and `CAP_SYS_ADMIN` capab `packetparser` does not produce Basic metrics. In Advanced mode (refer to [Metric Modes](../../modes/modes.md)), the plugin transforms an eBPF result into an enriched `Flow` by adding Pod information based on IP. It then sends the `Flow` to an external channel, enabling *several modules* to generate Pod-Level metrics. +## Performance Considerations + +### Reported Performance Impact on High-Core-Count Systems + +Community users have reported performance considerations when running the `packetparser` plugin on systems with high CPU core counts (32+ cores) under sustained network load. While these reports have not been independently verified by the Retina maintainers, we document them here for awareness. + +**User-Reported Observations:** + +A detailed analysis by a Retina user (see [this blog post](https://blog.zmalik.dev/p/who-will-observe-the-observability)) and [KubeCon 2025 talk](https://www.youtube.com/watch?v=J-Zx64mJzVk) documented performance degradation that scaled non-linearly with CPU core count on nodes running network-intensive, multi-threaded workloads. + +**Current Implementation:** + +By default, `packetparser` uses **BPF_MAP_TYPE_PERF_EVENT_ARRAY** for kernel-to-userspace data transfer. This architecture creates per-CPU buffers that must be polled by a single reader thread. On systems with many CPU cores, this can lead to: + +- Increased context switching overhead +- Memory access patterns that may not scale linearly +- Potential NUMA-related penalties on multi-socket systems + +**Alternative Approaches:** + +Alternative data transfer mechanisms like BPF ring buffers (BPF_MAP_TYPE_RINGBUF, available in Linux kernel 5.8+) use a shared buffer architecture that may perform better on high-core-count systems. Retina supports ring buffers for `packetparser` via `packetParserRingBuffer=enabled` and `packetParserRingBufferSize`. + +**Note:** Ring buffer mode requires Linux kernel 5.8 or newer. + +#### If You Experience Performance Issues + +If you observe performance degradation on high-core-count nodes: + +1. **Disable `packetparser`**: Use Basic metrics mode which doesn't require this plugin +2. **Enable Sampling**: Use the `dataSamplingRate` configuration option (see [Sampling](#sampling) section) +3. **Use High Data Aggregation**: Configure `high` [data aggregation](../../../05-Concepts/data-aggregation.md) +4. **Monitor Impact**: Watch for elevated CPU usage, context switches, or throughput changes + +**Note:** The Retina team is evaluating options for addressing reported performance concerns, including potential support for alternative data transfer mechanisms. Community feedback and contributions are welcome. + +## Sampling + +Since `packetparser` produces many enriched `Flow` objects it can be quite expensive for user space to process. Thus, when operating in `high` [data aggregation](../../../05-Concepts/data-aggregation.md) level optional sampling for reported packets is available via the `dataSamplingRate` configuration option. + +`dataSamplingRate` is expressed in 1 out of N terms, where N is the `dataSamplingRate` value. For example, if `dataSamplingRate` is 3 1/3rd of packets will be sampled for reporting. + +Keep in mind that there are cases where reporting will happen anyways as to ensure metric accuracy. + ### Code locations - Plugin and eBPF code: *pkg/plugin/packetparser/* diff --git a/docs/03-Metrics/plugins/Linux/tcpretrans.md b/docs/03-Metrics/plugins/Linux/tcpretrans.md index 9b962dea4c..81270750f3 100644 --- a/docs/03-Metrics/plugins/Linux/tcpretrans.md +++ b/docs/03-Metrics/plugins/Linux/tcpretrans.md @@ -8,13 +8,15 @@ The `tcpretrans` plugin requires the `CAP_SYS_ADMIN` capability. ## Architecture -The plugin utilizes eBPF to gather data. +The plugin uses a native eBPF tracepoint (`tracepoint/tcp/tcp_retransmit_skb`) to capture TCP retransmission events. The BPF program extracts source/destination IPs, ports, and TCP flags, then streams events to user space via a perf buffer. + The plugin does not generate Basic metrics. In Advanced mode (see [Metric Modes](../../modes/modes.md)), the plugin turns an eBPF result into an enriched `Flow` (adding Pod information based on IP), then sends the `Flow` to an external channel so that a tcpretrans module can create Pod-Level metrics. ### Code locations - Plugin and eBPF code: *pkg/plugin/tcpretrans/* +- BPF C source: *pkg/plugin/tcpretrans/_cprog/tcpretrans.c* - Module for extra Advanced metrics: *pkg/module/metrics/tcpretrans.go* ## Metrics diff --git a/docs/04-Captures/01-overview.md b/docs/04-Captures/01-overview.md index 3bc3f9104b..f0a7bd21da 100644 --- a/docs/04-Captures/01-overview.md +++ b/docs/04-Captures/01-overview.md @@ -1,78 +1,28 @@ # Overview -Retina Capture allows users to capture network traffic/metadata for specified Nodes/Pods. +Retina Capture allows users to perform distributed packet captures across the cluster, based on specified Nodes/Pods and other supported filters. -Captures are on-demand and can be output to persistent storage such as the host filesystem, a storage blob, etc. - -## Usage +Captures are on-demand and can be output to persistent storage such as the host filesystem, a storage blob or PVC. There are two methods for triggering a Capture: -- [CLI command](#option-1-retina-cli) -- [CRD/YAML configuration](#option-2-capture-crd-custom-resource-definition) +- [CLI command](./02-cli.md) +- [CRD/YAML configuration](./03-crd.md) -### Option 1: Retina CLI +It is also possible to set up a managed storage account when setting up Retina. -> Prerequisite: [Install Retina CLI](../02-Installation/02-CLI.md) +- [Managed Storage Account](../04-Captures/04-managed-storage-account.md#setup) -The command syntax is `kubectl retina capture create [--flags]`. +## Capture Jobs -Refer to the [Capture Command](../04-Captures/02-cli.md) documentation for more details. +A packet capture can cover multiple Nodes. This can be explicitly specified by using `node-selectors`. It could also be implicit - for example when using `pod-selectors` and the targetted Pods are hosted across different Nodes. -This example captures network traffic for all Linux Nodes, storing the output in the folder */mnt/capture* on each Node. +Whenever a capture is initiated, a Kubernetes Job is created on each relevant Node. -```shell -kubectl retina capture create --name capture-test --host-path /mnt/capture --node-selectors "kubernetes.io/os=linux" -``` +The Job's worker Pod runs for the specified duration, captures and wraps the network information into a tarball. It then copies the tarball to the specified output location(s). -For each Capture, a Kubernetes Job is created for each relevant Node (the Node could be selected and/or could be hosting a selected Pod). -The Job's worker Pod runs for the specified duration, captures and wraps the network information into a tarball, and copies the tarball to the specified output location(s). As a special case, a Kubernetes secret will be created containing a storage blob SAS for security concerns, then mounted to the Pod. -A random hashed name is assigned to each Retina Capture to uniquely label it. - -![Overview of Retina Capture without operator](img/capture-architecture-without-operator.png "Overview of Retina Capture without operator") - -### Option 2: Capture CRD (Custom Resource Definition) - -> Prerequisite: [Install Retina](../02-Installation/01-Setup.md) **with capture support**. - -Refer to the [Capture CRD](../05-Concepts/CRDs/Capture.md) documentation for more details. - -#### Managed Storage Account - -To simplify the user experience, a managed storage account can be configured when setting up Retina. Instructions for this are provided [here](../04-Captures/03-managed-storage-account.md#setup). - -This example creates a Capture and stores the Capture artifacts into a storage account specified by Blob SAS URL. - -Create a secret to store blob SAS URL (and store it in blob-upload-url.txt): - -```bash -kubectl create secret generic blob-sas-url --from-file=blob-upload-url=./blob-upload-url.txt -``` - -Create a Capture specifying the secret created as blobUpload, this example will also store the artifact on the node host path. - -```yaml -apiVersion: retina.sh/v1alpha1 -kind: Capture -metadata: - name: capture-test -spec: - captureConfiguration: - captureOption: - duration: 30s - captureTarget: - nodeSelector: - matchLabels: - kubernetes.io/hostname: aks-nodepool1-11396069-vmss000000 - outputConfiguration: - hostPath: "/tmp/retina" - blobUpload: blob-sas-url -``` - -More Retina Capture samples can be found [here](https://github.com/microsoft/retina/tree/main/samples/capture). - -Similarly to Option 1, a Kubernetes Job is created for each relevant Node. +A random hashed name is assigned to each Retina Capture job to uniquely label it. For example, a capture named `sample-capture` could result in a job called `sample-capture-s7n8q`. -![Overview of Retina Capture with operator](img/capture-architecture-with-operator.png "Overview of Retina Capture with operator") +Corresponding architecture diagrams are present within the [CLI command](./02-cli.md) and [CRD/YAML configuration](./03-crd.md) docs. diff --git a/docs/04-Captures/02-cli.md b/docs/04-Captures/02-cli.md index 19e3b627a6..144a2c963a 100644 --- a/docs/04-Captures/02-cli.md +++ b/docs/04-Captures/02-cli.md @@ -1,24 +1,42 @@ # Capture with Retina CLI -The capture command in Retina allows users to capture network traffic and metadata for the capture target, and send the data to the location defined by output configuration. +This page describes how the Retina CLI works in the context of performing packet captures. -> NOTE: captures can also be performed with a [Capture CRD](../05-Concepts/CRDs/Capture.md) after [installing Retina](../02-Installation/01-Setup.md) **with capture support**. +The use of the Retina CLI to perform captures does **NOT** require the Retina operator pod to be running. -## Capture Create +See the [overview](./01-overview.md#capture-jobs) for a description of how the capture jobs are created. + +![Overview of Retina Capture without operator](img/capture-architecture-without-operator.png "Overview of Retina Capture without operator") + +## Prerequisites + +- [Install Retina CLI](../02-Installation/02-CLI.md) + +## Operations + +### Capture Create `kubectl retina capture create [--flags]` creates a Capture with underlying Kubernetes jobs. -### Selecting a Target +#### Selecting a Target + +The target indicates where the packet capture will be performed. This can be set via the following flags: -The target indicates where the packet capture will be performed. This can be set via `--node-selectors`, `--node-names`, `--pod-selectors` and `--namespace-selectors` flags. Run `kubectl retina capture -h` for further details based on the Retina CLI version installed on your environment. +- `--node-selectors` +- `--node-names` +- `--pod-names` (for specific pods) +- `--pod-selectors` and `--namespace-selectors` (pairs for label-based pod selection) -Note that Node Selectors are not compatible with Pod Selectors & Namespace Selectors pairs and the capture will not go through if all are populated. +Note that the following combinations are not allowed: + +- Node Selectors are not compatible with Pod Selectors & Namespace Selectors pairs +- Pod Names are not compatible with Node Selectors, Pod Selectors, or Namespace Selectors If nothing is set, `kubectl retina capture create` will use `--node-selectors` with the default value shown below in [Flags](#flags). You can find [target selection examples](#target-selection) below. -### Configuring the Output Location +#### Configuring the Output Location The output configuration indicates the location where the capture will be stored. At least one location needs to be specified. This can either be the host path on the node, or a remote storage option. @@ -26,20 +44,22 @@ Blob-upload requires a Blob Shared Access Signature (SAS) with the write permiss You can find [output configuration examples](#output-configuration) below. -### Stopping a Capture +#### Stopping a Capture The Capture can be stopped in a number of ways: -- In a given time, by the `duration` flag, or when the file reaches the maximum allowed file size defined by the `max-size` flag. When both are specified, the capture will stop whenever **either condition is first met**. +- In a given time, by the `duration` flag, or when the file reaches the maximum allowed file size defined by the `max-size` flag. + - When both are specified, the capture will stop whenever **either condition is first met**. - On demand by [deleting the capture](#capture-delete) before the specified conditions meets. The network traffic will be uploaded to the specified output location. -### Flags +#### Flags | Flag | Type | Default | Description | Notes | |-----------------------|------------|----------|-----------------------------------------------------------------------------|-------| | `blob-upload` | string | "" | Blob SAS URL with write permission to upload capture files. | | +| `cleanup-after-upload` | bool | false | Automatically clean up capture files from the node's host path after successful upload to remote storage (blob or S3). Requires a remote storage destination. | | | `debug` | bool | false | When debug is true, a customized retina-agent image, determined by the environment variable RETINA_AGENT_IMAGE, is set. | | | `duration` | string | 1m0s | Maximum duration of the packet capture - in minutes / seconds. | | | `exclude-filter` | string | "" | A comma-separated list of IP:Port pairs that are excluded from capturing network packets. Supported formats are IP:Port, IP, Port, *:Port, IP:* | Only works on Linux. | @@ -52,10 +72,11 @@ The network traffic will be uploaded to the specified output location. | `name` | string | retina-capture | A name for the Retina Capture. | | | `namespace` | string | default | Sets the namespace which hosts the capture job and the other Kubernetes resources for a network capture. | Ensure the namespace exists. | | `namespace-selectors` | string | "" | Capture network captures on pods filtered by the provided namespace selectors. | Pair with `pod-selectors`. | -| `node-names` | string | "" | A comma-separated list of node names to select nodes on which the network capture will be performed. | | -| `node-selectors` | string | kubernetes.io/os=linux | A comma-separated list of node labels to select nodes on which the network capture will be performed. | | +| `node-names` | string | "" | A comma-separated list of node names to select nodes on which the network capture will be performed. | Overrides the default `node-selectors` value, allowing captures on nodes of any OS (including Windows). | +| `node-selectors` | string | kubernetes.io/os=linux | A comma-separated list of node labels to select nodes on which the network capture will be performed. | Cleared automatically when `node-names`, `pod-selectors`, `pod-names`, or `namespace-selectors` are specified. | | `no-wait` | bool | true | By default, Retina capture CLI will exit before the jobs are completed. If false, the CLI will wait until the jobs are completed and clean up the Kubernetes resources created. | | | `packet-size` | int | 0 | Limit the packet size in bytes. Packets longer than the defined maximum size will be truncated. The default value 0 indicates no limit. This is beneficial when the user wants to reduce the capture file size or hide customer data due to security concerns. | Only works on Linux. | +| `pod-names` | string | "" | A comma-separated list of specific pod names to select pods on which the network capture will be performed. | Mutually exclusive with `node-selectors`, `pod-selectors`, and `namespace-selectors`. | | `pod-selectors` | string | "" | A comma-separated list of pod labels to select pods on which the network capture will be performed. | Pair with `namespace-selectors`. | | `pvc` | string | "" | PersistentVolumeClaim under the specified or default namespace to store capture files. | | | `s3-access-key-id` | string | "" | S3 access key id to upload capture files. | | @@ -64,11 +85,27 @@ The network traffic will be uploaded to the specified output location. | `s3-path` | string | retina/captures | Prefix path within the S3 bucket where captures will be stored. | | | `s3-region` | string | "" | Region where the S3 compatible bucket is located. | | | `s3-secret-access-key`| string | "" | S3 access secret key to upload capture files. | | -| `tcpdump-filter` | string | "" | Raw tcpdump flags. Available tcpdump filters can be found in the [TCPDUMP MAN PAGE](https://www.tcpdump.org/manpages/tcpdump.1.html). | Only works on Linux. Includes only tcpdump flags, for boolean expressions, please use include/exclude filters. | - -### Examples - -#### Target Selection +| `interfaces` | string | "" | Comma-separated list of network interfaces to capture on (e.g., "eth0,eth1"). By default, captures are performed on all network interfaces. | | +| `pcap-filter` | string | "" | BPF filter expression for packet filtering (e.g., "host 10.0.0.1", "tcp port 443"). See [PCAP-FILTER](https://www.tcpdump.org/manpages/pcap-filter.7.html) for BPF syntax. Does NOT accept flags (arguments starting with '-'). | | +| `tcpdump-filter` | string | "" | **DEPRECATED and will be removed.** Use `--pcap-filter` instead. BPF filter expression for packet filtering. Does NOT accept flags (arguments starting with '-'). | | +| `no-promiscuous` | bool | false | Disable promiscuous mode (equivalent to tcpdump -p flag). | | +| `packet-buffered` | bool | false | Enable packet-buffered output (equivalent to tcpdump -U flag). | | +| `immediate-mode` | bool | false | Enable immediate mode for packet capture (equivalent to tcpdump --immediate-mode). | | +| `no-resolve-dns` | bool | false | Don't resolve hostnames (equivalent to tcpdump -n flag). | | +| `no-resolve-port` | bool | false | Don't resolve hostnames or port names (equivalent to tcpdump -nn flag). | | +| `verbosity` | string | "" | Verbosity level: `verbose` (tcpdump -v), `extra` (tcpdump -vv), `max` (tcpdump -vvv). Mutually exclusive. | | +| `timestamp-format` | string | "" | Timestamp format: `none` (-t), `unformatted` (-tt), `delta` (-ttt), `date` (-tttt), `delta-since-first` (-ttttt). Mutually exclusive. | | +| `print-data` | string | "" | Print packet data: `hex` (-x), `hex-with-link` (-xx), `ascii` (-A), `ascii-with-link` (-AA). Mutually exclusive. | | +| `print-link-header` | bool | false | Print link-level headers (equivalent to tcpdump -e flag). | | +| `quiet-output` | bool | false | Quick/quiet output mode (equivalent to tcpdump -q flag). | | +| `absolute-seq` | bool | false | Print absolute TCP sequence numbers (equivalent to tcpdump -S flag). | | +| `dont-verify-checksum`| bool | false | Don't verify TCP checksums (equivalent to tcpdump -K flag). | | + +> **Note on Mutually Exclusive Flags**: The `verbosity`, `timestamp-format`, and `print-data` flags are mutually exclusive within their respective groups. You can only set one value per flag. For example, you cannot use both `--verbosity=verbose` and `--verbosity=max` simultaneously. The CLI enforces this by using enum-based flags instead of individual boolean flags. + +#### Examples + +##### Target Selection Node Selectors @@ -95,7 +132,45 @@ kubectl retina capture create \ --namespace-selectors="kubernetes.io/metadata.name=kube-system" ``` -#### Output Configuration +Pod Names (Specific Pods) + +```sh +kubectl retina capture create \ + --name example-pod-names \ + --namespace myapp \ + --pod-names "my-app-pod-abc123,my-app-pod-def456" +``` + +Single Pod by Name + +```sh +kubectl retina capture create \ + --name example-single-pod \ + --namespace myapp \ + --pod-names "my-app-pod-abc123" \ + --duration 60s +``` + +##### Interface Selection + +Capture on all interfaces (default behavior) + +```sh +kubectl retina capture create \ + --name example-all-interfaces \ + --node-selectors "kubernetes.io/os=linux" +``` + +Capture on specific interfaces + +```sh +kubectl retina capture create \ + --name example-specific-interfaces \ + --node-selectors "kubernetes.io/os=linux" \ + --interfaces "eth0,eth1" +``` + +##### Output Configuration Host Path @@ -143,7 +218,7 @@ kubectl retina capture create \ --s3-secret-access-key "your-secret-access-key" ``` -#### Capture Filters +##### Capture Filters Include / Exclude Filters @@ -154,15 +229,50 @@ kubectl retina capture create \ --exclude-filter="10.224.0.26:80,10.224.0.34:8080" ``` -Tcpdump Filters +BPF Packet Filters + +```sh +kubectl retina capture create \ + --name example-pcap-filters \ + --pcap-filter="udp port 53" +``` + +Capture with Display Options ```sh kubectl retina capture create \ - --name example-tcpdump-filters \ - --tcpdump-filter="udp port 53" + --name example-with-display-options \ + --pcap-filter="tcp port 443" \ + --no-resolve-dns \ + --verbosity=verbose \ + --print-data=hex ``` -## Capture Delete +This example: + +- Captures only HTTPS traffic (tcp port 443) +- Doesn't resolve hostnames (--no-resolve-dns, equivalent to tcpdump -n) +- Shows verbose output (--verbosity=verbose, equivalent to tcpdump -v) +- Displays packet data in hex (--print-data=hex, equivalent to tcpdump -x) + +Additional display option examples: + +```sh +# Don't resolve names or ports, capture HTTP traffic +kubectl retina capture create \ + --name example-http \ + --pcap-filter="tcp port 80" \ + --no-resolve-port + +# Show timestamps with date and link-level headers for ICMP +kubectl retina capture create \ + --name example-icmp \ + --pcap-filter="icmp" \ + --timestamp-format=date \ + --print-link-header +``` + +### Capture Delete Deleting the capture job before either of the terminating conditions have been met will stop the capture. @@ -174,7 +284,7 @@ Example: kubectl retina capture delete --name retina-capture-zlx5v ``` -## Capture List +### Capture List To get a list of the captures you can run `kubectl retina capture list` to get the captures in a specific namespace or in all namespaces. @@ -190,6 +300,134 @@ List by all namespaces: kubectl retina capture list --all-namespaces ``` +### Capture Download + +The `kubectl retina capture download` command allows you to download capture files directly from the cluster or from blob storage. + +#### Download from Cluster + +Download capture files using the capture name: + +```sh +kubectl retina capture download --name +``` + +Download capture files and specify an output location: + +```sh +kubectl retina capture download --name -o +``` + +Download all available captures in the current namespace: + +```sh +kubectl retina capture download --all +``` + +Download all available captures from all namespaces: + +```sh +kubectl retina capture download --all --all-namespaces +``` + +By default, files are downloaded to the current directory. + +#### Download from Blob Storage + +Download capture files from Azure Blob Storage using a Blob URL (requires Read/List permissions): + +```sh +kubectl retina capture download --blob-url "" +``` + +#### Download Output Structure + +The command will create different output structures depending on the options used: + +##### Individual Capture Download + +For individual capture downloads using `--name`, files are organized by capture name: + +```bash +/output-directory/ +└── capture-name/ + ├── capture-name-node1-20230320013600UTC.tar.gz + └── capture-name-node2-20230320013600UTC.tar.gz +``` + +##### All Captures Download + +When using `--all`, all captures are consolidated into a single timestamped archive: + +```bash +/output-directory/ +└── all-captures-20230320134500.tar.gz +``` + +The archive contents are organized by capture name: + +```bash +# Contents of all-captures-20230320134500.tar.gz +capture-name-1/ +├── capture-name-1-node1-20230320013600UTC.tar.gz +└── capture-name-1-node2-20230320013600UTC.tar.gz +capture-name-2/ +├── capture-name-2-node1-20230320014500UTC.tar.gz +└── capture-name-2-node2-20230320014500UTC.tar.gz +``` + +##### All Captures with All Namespaces + +When using `--all --all-namespaces`, the archive contents include namespace information: + +```bash +# Contents of all-captures-20230320134500.tar.gz +namespace-1/ +└── capture-name-1/ + ├── capture-name-1-node1-20230320013600UTC.tar.gz + └── capture-name-1-node2-20230320013600UTC.tar.gz +namespace-2/ +└── capture-name-2/ + ├── capture-name-2-node1-20230320014500UTC.tar.gz + └── capture-name-2-node2-20230320014500UTC.tar.gz +``` + +#### Download Options + +| Flag | Description | Notes | +|------|-------------|-------| +| `--name` | Download capture files for a specific capture name | Creates individual files in capture-specific directory | +| `--all` | Download all available captures in the current namespace | Creates single consolidated archive | +| `--all-namespaces` | Download captures from all namespaces (requires `--all`) | Includes namespace in archive structure | +| `--blob-url` | Download from Azure Blob Storage using SAS URL | Requires Read/List permissions | +| `-o, --output` | Specify output directory | Defaults to current directory | + +#### Examples + +Download a specific capture: + +```sh +kubectl retina capture download --name my-capture +``` + +Download all captures with custom output location: + +```sh +kubectl retina capture download --all -o /tmp/retina-downloads +``` + +Download all captures from all namespaces: + +```sh +kubectl retina capture download --all --all-namespaces +``` + +Download from blob storage: + +```sh +kubectl retina capture download --blob-url "https://mystorageaccount.blob.core.windows.net/captures?sp=rl&st=..." +``` + ## Obtaining the output After downloading or copying the tarball from the location specified, extract the tarball through the `tar` command in either Linux shell or Windows Powershell, for example, @@ -200,7 +438,9 @@ tar -xvf retina-capture-aks-nodepool1-41844487-vmss000000-20230320013600UTC.tar. ### Name pattern of the tarball -the tarball take such name pattern, `$(capturename)-$(hostname)-$(date +%Y%m%d%H%M%S%Z).tar.gz`, for example, `retina-capture-aks-nodepool1-41844487-vmss000000-20230313101436UTC.tar.gz`. +The tarballs take the following name pattern, `$(capturename)-$(hostname)-$(date +%Y%m%d%H%M%S%Z).tar.gz`. + +- e.g. `retina-capture-aks-nodepool1-41844487-vmss000000-20230313101436UTC.tar.gz` ### File and directory structure inside the tarball @@ -300,7 +540,7 @@ kubectl retina capture create \ --host-path /mnt/test \ --namespace capture \ --node-selectors "kubernetes.io/os=linux" \ - -- + --debug ``` ## Cleanup @@ -308,3 +548,22 @@ kubectl retina capture create \ When creating a capture, you can specify `--no-wait` to clean up the jobs after the Capture is completed. Otherwise, after creating a Capture, a random Capture name is returned, with which you can delete the jobs by running the `kubectl retina capture delete` command. + +### Automatic Cleanup After Upload + +When using `--cleanup-after-upload` with a remote storage destination (`--blob-upload`, `--s3-bucket`, or `--pvc`), Retina will automatically delete the capture files from the node's host path after the data has been successfully uploaded to remote storage. + +This flag works with both `--no-wait=true` (default) and `--no-wait=false`: + +- **`--no-wait=true`** (default): The CLI exits immediately. Jobs are assigned a TTL (5 minutes after completion) so Kubernetes automatically garbage-collects them along with their secrets. +- **`--no-wait=false`**: The CLI waits for all jobs to complete, then deletes the jobs and secrets itself. + +In both modes, the capture agent removes the host-path files once the upload succeeds. + +```shell +kubectl retina capture create --name my-capture --node-names "node1" \ + --blob-upload "" \ + --cleanup-after-upload +``` + +If any capture job fails, the host-path files are preserved for debugging. diff --git a/docs/04-Captures/03-crd.md b/docs/04-Captures/03-crd.md new file mode 100644 index 0000000000..8807afeecb --- /dev/null +++ b/docs/04-Captures/03-crd.md @@ -0,0 +1,174 @@ +# Capture with CRD + +This page describes how the Retina Capture CRD works. + +See the [overview](./01-overview.md#capture-jobs) for a description of how the capture jobs are created. + +![Overview of Retina Capture with operator](img/capture-architecture-with-operator.png "Overview of Retina Capture with operator") + +## Prerequisites + +- [Install Retina](../02-Installation/01-Setup.md#capture-support) **with capture support**. + +## Usage + +You must create a YAML manifest file with the desired specifications and apply it to the cluster using `kubectl apply`. + +- If successful, the capture job should spin up after you apply. +- If not successful, no job will spin up. You can troubleshoot by checking the status of the CRD with `kubectl get capture -o yaml`. + +The full specification for the Capture CRD can be found in the [Capture CRD file](https://github.com/microsoft/retina/blob/main/deploy/standard/manifests/controller/helm/retina/crds/retina.sh_captures.yaml). + +Refer to the [Capture CRD](../05-Concepts/CRDs/Capture.md) page for more details. + +```shell +# Install Retina with Standard Control Plane and Operator enabled +> VERSION=$( curl -sL https://api.github.com/repos/microsoft/retina/releases/latest | jq -r .name) +helm upgrade --install retina oci://ghcr.io/microsoft/retina/charts/retina \ + --version $VERSION \ + --namespace kube-system \ + --set image.tag=$VERSION \ + --set operator.tag=$VERSION \ + --set logLevel=info \ + --set operator.enabled=true \ + --set enabledPlugin_linux="\[dropreason\,packetforward\,linuxutil\,dns\]" + +Release "retina" does not exist. Installing it now. +Pulled: ghcr.io/microsoft/retina/charts/retina:v0.0.33 +Digest: sha256:0d647b8c5090725684ad9bd0c9c988eccd4ee3cbf06a08a7270f362236057bd0 +NAME: retina +LAST DEPLOYED: Fri May 30 09:01:57 2025 +NAMESPACE: kube-system +STATUS: deployed +REVISION: 1 +NOTES: +1. Installing retina service using helm: helm install retina ./deploy/standard/manifests/controller/helm/retina/ --namespace kube-system --dependency-update +2. Cleaning up/uninstalling/deleting retina and dependencies related: + helm uninstall retina -n kube-system + +# Apply the Capture configuration +> kubectl apply -f capture.yaml +capture.retina.sh/hubble-cp-capture created + +# View the jobs which got created +> kubectl get jobs +NAME STATUS COMPLETIONS DURATION AGE +standard-cp-capture-8r8sf Complete 1/1 11s 4m31s +standard-cp-capture-sdtd7 Complete 1/1 11s 4m31s +``` + +## Examples + +Node Selectors and Host Path output target + +```yaml +apiVersion: retina.sh/v1alpha1 +kind: Capture +metadata: + name: example-node-selectors +spec: + captureConfiguration: + captureOption: + duration: "5s" + maxCaptureSize: 100 + packetSize: 1500 + captureTarget: + nodeSelector: + matchLabels: + kubernetes.io/os: linux + outputConfiguration: + hostPath: example-capture +``` + +Include / Exclude filters + +```yaml +apiVersion: retina.sh/v1alpha1 +kind: Capture +metadata: + name: example-include-exclude-filters +spec: + captureConfiguration: + captureOption: + duration: "5s" + maxCaptureSize: 100 + packetSize: 1500 + captureTarget: + nodeSelector: + matchLabels: + kubernetes.io/os: linux + filters: + include: + - 10.224.0.42:80 + - 10.224.0.33:8080 + exclude: + - 10.224.0.26:80 + - 10.224.0.34:8080 + outputConfiguration: + hostPath: example-capture +``` + +Single Pod by Name + +```yaml +apiVersion: retina.sh/v1alpha1 +kind: Capture +metadata: + name: example-pod-names + namespace: myapp +spec: + captureConfiguration: + captureOption: + duration: "60s" + maxCaptureSize: 100 + captureTarget: + podNames: + - my-app-pod-abc123 + outputConfiguration: + hostPath: example-capture +``` + +Multiple Pods by Name + +```yaml +apiVersion: retina.sh/v1alpha1 +kind: Capture +metadata: + name: example-multiple-pod-names + namespace: myapp +spec: + captureConfiguration: + captureOption: + duration: "30s" + maxCaptureSize: 100 + captureTarget: + podNames: + - my-app-pod-abc123 + - my-app-pod-def456 + outputConfiguration: + hostPath: example-capture +``` + +Additional examples can also be found in the [GitHub capture samples](https://github.com/microsoft/retina/tree/main/samples/capture). + +## Automatic Cleanup After Upload + +Set `cleanUpAfterUpload: true` in the Capture spec to have the controller automatically delete the Capture resource and all associated jobs once all capture jobs complete successfully and data has been uploaded to remote storage (Blob, S3, or PVC). + +```yaml +apiVersion: retina.sh/v1alpha1 +kind: Capture +metadata: + name: my-capture +spec: + captureConfiguration: + captureTarget: + nodeSelector: + matchLabels: + kubernetes.io/os: linux + outputConfiguration: + blobUpload: "" + cleanUpAfterUpload: true +``` + +If any job fails, the Capture resource and jobs are preserved for debugging. This option requires a remote storage output (`blobUpload`, `s3Upload`, or `persistentVolumeClaim`). diff --git a/docs/04-Captures/03-managed-storage-account.md b/docs/04-Captures/04-managed-storage-account.md similarity index 85% rename from docs/04-Captures/03-managed-storage-account.md rename to docs/04-Captures/04-managed-storage-account.md index 11c5edca14..cac86be751 100644 --- a/docs/04-Captures/03-managed-storage-account.md +++ b/docs/04-Captures/04-managed-storage-account.md @@ -1,5 +1,39 @@ # Managed Storage Account +To simplify the user experience, a managed storage account can be configured when setting up Retina. + +## Example + +This example creates a Capture and stores the Capture artifacts into a storage account specified by Blob SAS URL. + +- Create a secret to store blob SAS URL (and store it in blob-upload-url.txt): + +```bash +kubectl create secret generic blob-sas-url --from-file=blob-upload-url=./blob-upload-url.txt +``` + +- Create a Capture YAML configuration specifying the secret created as blobUpload, this example will also store the artifact on the node host path. + +```yaml +apiVersion: retina.sh/v1alpha1 +kind: Capture +metadata: + name: capture-test +spec: + captureConfiguration: + captureOption: + duration: 30s + captureTarget: + nodeSelector: + matchLabels: + kubernetes.io/hostname: aks-nodepool1-11396069-vmss000000 + outputConfiguration: + hostPath: "retina" + blobUpload: blob-sas-url +``` + +More Retina Capture samples can be found [here](https://github.com/microsoft/retina/tree/main/samples/capture). + ## Motivation Retina Capture helps customers capture network packets on Kuberentes cluster to debug network issues. Before Retina Capture can debug the packets, Retina Capture can store the network packets and the customers need to download the packets from Retina Capture supported locations with tools like Wireshark. diff --git a/docs/05-Concepts/CRDs/Capture.md b/docs/05-Concepts/CRDs/Capture.md index c5e31db1fe..41d371583e 100644 --- a/docs/05-Concepts/CRDs/Capture.md +++ b/docs/05-Concepts/CRDs/Capture.md @@ -23,15 +23,33 @@ The `Capture` CRD is defined with the following specifications: ### Fields - **spec.captureConfiguration:** Specifies the configuration for capturing network packets. It includes the following properties: - - `captureOption`: Lists options for the capture, such as duration, maximum capture size, and packet size. - - `captureTarget`: Defines the target on which the network packets will be captured. It includes namespace, node, and pod selectors. + - `captureOption`: Lists options for the capture, such as: + - `duration`: Capture duration + - `maxCaptureSize`: Maximum capture file size in MB + - `packetSize`: Maximum packet size to capture + - `interfaces`: Array of network interface names to capture from (e.g., `["eth0", "eth1"]`). If empty, captures from all interfaces. + - `pcapFilter`: BPF filter expression for packet filtering (e.g., `"host 10.0.0.1"`, `"tcp port 443"`). Does NOT accept flags. + - Boolean flags for tcpdump capture behavior and display options: + - `noPromiscuous`: Disable promiscuous mode (tcpdump -p) + - `packetBuffered`: Enable packet-buffered output (tcpdump -U) + - `immediateMode`: Enable immediate mode (tcpdump --immediate-mode) + - `noResolveDNS`: Don't resolve hostnames (tcpdump -n) + - `noResolvePort`: Don't resolve hostnames or port names (tcpdump -nn) + - `verbose`, `extraVerbose`, `maxVerbose`: Verbose output levels (tcpdump -v, -vv, -vvv). **Mutually exclusive** - set only one. + - `printDataHex`, `printDataHexLink`, `printDataASCII`, `printDataASCIILink`: Print packet data in hex (tcpdump -x, -xx) or ASCII (tcpdump -A, -AA). **Mutually exclusive** - set only one print data format. + - `printLinkHeader`: Print link-level headers (tcpdump -e) + - `quietOutput`: Quick/quiet output (tcpdump -q) + - `absoluteSeq`: Print absolute TCP sequence numbers (tcpdump -S) + - `noTimestamp`, `unformattedTimestamp`, `deltaTimestamp`, `dateTimestamp`, `deltaSinceFirst`: Timestamp options (tcpdump -t, -tt, -ttt, -tttt, -ttttt). **Mutually exclusive** - set only one. + - `dontVerifyChecksum`: Don't verify TCP checksums (tcpdump -K) + - `captureTarget`: Defines the target on which the network packets will be captured. It includes namespace, node, and pod selectors, as well as specific pod names. - `filters`: Specifies filters for including or excluding network packets based on IP or port. - `includeMetadata`: Indicates whether networking metadata should be captured. - - `tcpdumpFilter`: Allows specifying a raw tcpdump filter string. + - `tcpdumpFilter`: **DEPRECATED and will be removed.** Currently accepts BPF filter expressions only (no flags). Migrate to `captureOption.pcapFilter` for BPF expressions and `captureOption` boolean flags for display/output options (e.g., `noResolveDNS`, `packetBuffered`, `verbosity`). - **spec.outputConfiguration:** Indicates where the captured data will be stored. It includes the following properties: - `blobUpload`: Specifies a secret containing the blob SAS URL for storing the capture data. - - `hostPath`: Stores the capture files into the specified host filesystem. + - `hostPath`: A relative subpath name (e.g. `my-capture`) joined under the operator-configured host base directory (default `/var/log/retina/captures`) on every node that runs a capture pod. Capture files are written to that joined directory. Absolute paths and `..` segments are rejected. - `persistentVolumeClaim`: Mounts a PersistentVolumeClaim into the Pod to store capture files. - `s3Upload`: Specifies the configuration for uploading capture files to an S3-compatible storage service, including the bucket name, region, and optional custom endpoint. @@ -59,7 +77,7 @@ spec: matchLabels: app: target-app outputConfiguration: - hostPath: /captures + hostPath: example-capture blobUpload: blob-sas-url s3Upload: bucket: retina-bucket @@ -76,6 +94,138 @@ data: s3-secret-access-key: ``` +### Advanced Filtering + +#### Capturing on Specific Network Interfaces + +To capture packets only on specific network interfaces, use the `captureOption.interfaces` field: + +```yaml +apiVersion: retina.sh/v1alpha1 +kind: Capture +metadata: + name: capture-specific-interfaces +spec: + captureConfiguration: + captureOption: + duration: "1m" + interfaces: ["eth0", "eth1"] # Capture only on these interfaces + captureTarget: + nodeSelector: + matchLabels: + kubernetes.io/hostname: node-1 + outputConfiguration: + hostPath: /tmp/captures +``` + +#### Using BPF Filters with Display Options + +The `captureOption.pcapFilter` field accepts BPF (Berkeley Packet Filter) expressions for packet filtering. Use boolean flags in `captureOption` for display and output formatting. + +##### BPF Filters Only + +To apply packet filtering using BPF syntax: + +```yaml +apiVersion: retina.sh/v1alpha1 +kind: Capture +metadata: + name: capture-with-bpf-filter +spec: + captureConfiguration: + captureOption: + duration: "1m" + pcapFilter: "tcp and (port 443 or port 80)" # Only capture HTTP/HTTPS traffic + captureTarget: + nodeSelector: + matchLabels: + kubernetes.io/hostname: node-1 + outputConfiguration: + hostPath: /tmp/captures +``` + +**Valid BPF filter examples:** + +- `"host 10.0.0.1"` - Capture packets to/from specific host +- `"tcp port 443"` - Capture HTTPS traffic +- `"net 192.168.0.0/16 and not port 22"` - Capture subnet traffic except SSH +- `"tcp and (port 80 or port 443)"` - Capture HTTP and HTTPS traffic + +##### Using Display Options with BPF Filters + +Combine BPF filters with display option boolean flags: + +```yaml +apiVersion: retina.sh/v1alpha1 +kind: Capture +metadata: + name: capture-with-display-options +spec: + captureConfiguration: + captureOption: + duration: "1m" + pcapFilter: "tcp port 443" + noResolveDNS: true # Don't resolve hostnames (tcpdump -n) + verbose: true # Verbose output (tcpdump -v) + printDataHex: true # Show hex data (tcpdump -x) + captureTarget: + nodeSelector: + matchLabels: + kubernetes.io/hostname: node-1 + outputConfiguration: + hostPath: /tmp/captures +``` + +**Examples with different display options:** + +```yaml +# Don't resolve names or ports, very verbose, capture HTTP +captureOption: + pcapFilter: "tcp port 80" + noResolvePort: true # tcpdump -nn + extraVerbose: true # tcpdump -vv +``` + +```yaml +# Show hex data with timestamps, capture ICMP +captureOption: + pcapFilter: "icmp" + printDataHex: true # tcpdump -x + dateTimestamp: true # tcpdump -tttt +``` + +**Available display option and boolean flags:** + +- `noResolveDNS`, `noResolvePort`: Don't resolve hostnames/port names (tcpdump -n, -nn) +- **Verbosity** (mutually exclusive - choose one): + - `verbose`: Verbose output (tcpdump -v) + - `extraVerbose`: Extra verbose output (tcpdump -vv) + - `maxVerbose`: Maximum verbose output (tcpdump -vvv) +- **Print data format** (mutually exclusive - choose one): + - `printDataHex`: Show packet data in hex (tcpdump -x) + - `printDataHexLink`: Show packet data in hex with link-level headers (tcpdump -xx) + - `printDataASCII`: Show packet data in ASCII (tcpdump -A) + - `printDataASCIILink`: Show packet data in ASCII with link-level headers (tcpdump -AA) +- **Timestamp format** (mutually exclusive - choose one): + - `noTimestamp`: Don't print timestamps (tcpdump -t) + - `unformattedTimestamp`: Print timestamps as Unix epoch (tcpdump -tt) + - `deltaTimestamp`: Print time delta between packets (tcpdump -ttt) + - `dateTimestamp`: Print timestamps with date (tcpdump -tttt) + - `deltaSinceFirst`: Print time delta since first packet (tcpdump -ttttt) +- Other options: + - `printLinkHeader`: Print link-level headers (tcpdump -e) + - `quietOutput`: Quick/quiet output (tcpdump -q) + - `absoluteSeq`: Print absolute TCP sequence numbers (tcpdump -S) + - `dontVerifyChecksum`: Don't verify TCP checksums (tcpdump -K) + +> **CLI Users**: When using the `kubectl retina capture create` command, use the enum-based flags (`--verbosity`, `--timestamp-format`, `--print-data`) instead of setting these boolean fields directly. See the [CLI documentation](../../04-Captures/02-cli.md) for details. + +**Capture behavior options:** + +- `noPromiscuous`: Disable promiscuous mode (tcpdump -p) +- `packetBuffered`: Enable packet-buffered output (tcpdump -U) +- `immediateMode`: Enable immediate mode (tcpdump --immediate-mode) + ### Capture Lifecycle Once a Capture is created, the capture controller inside retina-operator is responsible for managing the lifecycle of the Capture. diff --git a/docs/05-Concepts/CRDs/MetricsConfiguration.md b/docs/05-Concepts/CRDs/MetricsConfiguration.md index 2b59205c59..91a341fafa 100644 --- a/docs/05-Concepts/CRDs/MetricsConfiguration.md +++ b/docs/05-Concepts/CRDs/MetricsConfiguration.md @@ -24,6 +24,7 @@ The `MetricsConfiguration` CRD is defined with the following specifications: - `destinationLabels`: Represents the destination context labels, such as IP, Pod, port, workload (deployment/replicaset/statefulset/daemonset). - `metricName`: Indicates the name of the metric. - `sourceLabels`: Represents the source context labels, such as IP, Pod, port. + - `ttl`: Represents the time-to-live for the metric. If there are no metric updates for a particular set of context labels for this duration the metric will be removed from export. The value of `ttl` must be a valid Golang `time.Duration` string and non-negative. A zero `ttl` (the default) means that metrics are never removed from export. - **spec.namespaces:** Specifies the namespaces to include or exclude in metric collection. It includes the following properties: - `exclude`: Specifies namespaces to be excluded from metric collection. @@ -51,6 +52,7 @@ spec: - port additionalLabels: - direction + ttl: 24h - metricName: forward_count sourceLabels: - ip diff --git a/docs/06-Troubleshooting/bpftrace.md b/docs/06-Troubleshooting/bpftrace.md new file mode 100644 index 0000000000..8e3ad0d180 --- /dev/null +++ b/docs/06-Troubleshooting/bpftrace.md @@ -0,0 +1,172 @@ +# BPF Trace (bpftrace) + +>NOTE: `retina bpftrace` is an experimental feature. The flags and behavior may change in future versions. + +The `retina bpftrace` command allows you to trace network issues on a Kubernetes node in real-time using eBPF/bpftrace. + +This is useful for debugging connectivity problems such as: + +- Packet drops (with reason codes) +- TCP RST events (connection resets) +- Socket errors (ECONNREFUSED, ETIMEDOUT, etc.) +- TCP retransmissions (packet loss indicators) + +## Getting Started + +Trace network issues on a node: + +```shell +# Basic usage - trace all network issues on a node +kubectl retina bpftrace + +# With custom duration +kubectl retina bpftrace --duration 60s + +# Filter by IP address +kubectl retina bpftrace --ip 10.224.0.5 + +# Filter by CIDR +kubectl retina bpftrace --cidr 10.224.0.0/16 + +# Output as JSON (for parsing) +kubectl retina bpftrace -o json + +# Trace only specific event types +kubectl retina bpftrace --drops --rst + +# Specify custom timeout for trace pod startup +kubectl retina bpftrace --startup-timeout 120s +``` + +Run `kubectl retina bpftrace -h` for full documentation and examples. + +## Event Types + +The bpftrace command captures several types of network events: + +### DROP - Packet Drops + +Captures packets dropped by the kernel with reason codes. Common reasons include: + +| Code | Name | Description | +|------|------|-------------| +| 0 | NOT_SPECIFIED | Unspecified reason | +| 1 | NO_SOCKET | No listening socket | +| 3 | TCP_CSUM | TCP checksum error | +| 6 | NETFILTER_DROP | Dropped by NetworkPolicy/iptables | +| 8 | IP_CSUM | IP checksum error | + +The full list of drop reasons is kernel-version specific and is printed at the start of each trace. + +### RST_SENT / RST_RECV - TCP Reset Events + +Captures TCP RST packets sent or received. These indicate: + +- Connection refused (no service listening) +- Connection reset by peer +- Firewall rejecting connections + +### SOCK_ERR - Socket Errors + +Captures socket-level errors reported to applications: + +| Code | Name | Description | +|------|------|-------------| +| 104 | ECONNRESET | Connection reset by peer | +| 110 | ETIMEDOUT | Connection timed out | +| 111 | ECONNREFUSED | Connection refused | +| 113 | EHOSTUNREACH | No route to host | + +### RETRANS - TCP Retransmissions + +Captures TCP segment retransmissions, which indicate: + +- Packet loss in the network +- Network congestion +- Slow or unresponsive peers + +The reason code shows the TCP state during retransmission. + +## Output Format + +### Table Format (default) + +```text +TIME TYPE REASON PROBE SRC -> DST +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +18:28:12 SOCK_ERR ECONNREFUSED inet_sk_error_report 127.0.0.1:35779 -> 127.0.0.1:9999 +18:28:27 DROP 6 kfree_skb 10.224.0.60:41929 -> 10.224.0.39:80 +18:28:28 RETRANS 2 tcp_retransmit_skb 10.224.0.60:41929 -> 10.224.0.39:80 +18:28:33 RST_SENT - tcp_send_reset 10.224.0.47:38470 -> 20.161.216.95:443 +``` + +### JSON Format (`-o json`) + +```json +{"time":"18:28:12","type":"SOCK_ERR","reason_code":111,"probe":"inet_sk_error_report","src_ip":"127.0.0.1","src_port":35779,"dst_ip":"127.0.0.1","dst_port":9999} +{"time":"18:28:27","type":"DROP","reason_code":6,"probe":"kfree_skb","src_ip":"10.224.0.60","src_port":41929,"dst_ip":"10.224.0.39","dst_port":80} +``` + +## Flags + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--duration` | duration | 0 | Duration to run the trace (0 = until Ctrl-C) | +| `--startup-timeout` | duration | 30s | Timeout for trace pod startup | +| `--ip` | string | "" | Filter events by IP address (matches src or dst) | +| `--cidr` | string | "" | Filter events by CIDR (matches src or dst) | +| `--drops` | bool | false | Enable only packet drop events | +| `--rst` | bool | false | Enable only TCP RST events | +| `--errors` | bool | false | Enable only socket error events | +| `--retransmits` | bool | false | Enable only retransmit events | +| `--all` | bool | false | Enable all event types (default when no event flags specified) | +| `-o, --output` | string | table | Output format: table or json | +| `--retina-shell-image-repo` | string | (default) | Override the retina-shell image repository | +| `--retina-shell-image-version` | string | (default) | Override the retina-shell image version | + +## Example: Debugging NetworkPolicy Drops + +When pods can't communicate due to NetworkPolicy: + +```shell +# Start tracing on the node where the destination pod runs +kubectl retina bpftrace aks-nodepool1-12345678-vmss000000 --drops --duration 60s + +# In another terminal, attempt the connection +kubectl exec -it client-pod -- curl http://server-service:80 +``` + +You'll see output like: + +```text +18:14:41 DROP 6 kfree_skb 10.224.0.34:33061 -> 10.224.0.49:80 +18:14:42 RETRANS 2 tcp_retransmit_skb 10.224.0.34:33061 -> 10.224.0.49:80 +18:14:42 DROP 6 kfree_skb 10.224.0.34:33061 -> 10.224.0.49:80 +``` + +The `DROP` with reason code `6` (NETFILTER_DROP) confirms NetworkPolicy is blocking traffic. + +## Example: Debugging Connection Refused + +When connecting to a service that's not listening: + +```shell +kubectl retina bpftrace node-name --rst --errors --ip 10.224.0.5 +``` + +```text +18:19:05 RST_RECV - tcp_receive_reset 10.224.0.10:35267 -> 10.224.0.5:8080 +18:19:05 SOCK_ERR ECONNREFUSED inet_sk_error_report 10.224.0.10:35267 -> 10.224.0.5:8080 +``` + +This shows the TCP RST and corresponding socket error, indicating no service is listening on port 8080. + +## Requirements + +- Linux nodes (Windows not supported) +- Kernel with BTF support (5.x+ recommended) + +## Limitations + +- IPv6 filtering not currently supported +- Cilium CNI: DROP events won't capture Cilium policy drops (Cilium uses eBPF datapath, not netfilter/kfree_skb) diff --git a/docs/06-Troubleshooting/performance.md b/docs/06-Troubleshooting/performance.md new file mode 100644 index 0000000000..74e247423d --- /dev/null +++ b/docs/06-Troubleshooting/performance.md @@ -0,0 +1,176 @@ +# Performance Troubleshooting + +This guide helps diagnose and address potential performance issues when running Retina, particularly the `packetparser` plugin on high-core-count systems. + +## Background + +Community users have reported performance considerations when running the `packetparser` plugin (used in Advanced metrics mode) on systems with high CPU core counts under sustained network load. For detailed background, see the [`packetparser` performance considerations](../03-Metrics/plugins/Linux/packetparser.md#performance-considerations). + +## Symptoms to Monitor + +Watch for these indicators after deploying Retina: + +- **Decreased network throughput** compared to baseline +- **High CPU usage** by Retina agent pods +- **Elevated context switches** on nodes running Retina +- **Increased latency** in network-intensive applications + +## Diagnostic Steps + +### Step 1: Identify Your Configuration + +Check which plugins are enabled: + +```bash +kubectl get configmap retina-config -n kube-system -o yaml | grep enabledPlugin +``` + +If `packetparser` is enabled, you're running Advanced metrics mode which is more resource-intensive. + +### Step 2: Check Node Specifications + +```bash +# Check core count on nodes +kubectl get nodes -o custom-columns=NAME:.metadata.name,CPU:.status.capacity.cpu + +# Identify nodes with high core counts (32+) +kubectl get nodes -o json | jq '.items[] | select((.status.capacity.cpu | tonumber) >= 32) | {name: .metadata.name, cpu: .status.capacity.cpu}' +``` + +### Step 3: Monitor Retina Resource Usage + +```bash +# Check CPU and memory usage of Retina pods +kubectl top pods -n kube-system -l app=retina + +# For more detailed analysis, check specific pod on a node +RETINA_POD=$(kubectl get pods -n kube-system -l app=retina -o jsonpath='{.items[0].metadata.name}') +kubectl top pod $RETINA_POD -n kube-system +``` + +### Step 4: Establish Performance Baseline + +Before and after Retina deployment, measure: + +- Network throughput (using your application's metrics or tools like iperf3) +- Application response times +- CPU utilization on nodes + +## Mitigation Options + +If you observe performance impact, consider these approaches: + +### Option 1: Use Basic Metrics Mode (Recommended) + +Basic metrics mode provides node-level observability without the `packetparser` plugin: + +```bash +# Reinstall or upgrade Retina without packetparser +helm upgrade retina oci://ghcr.io/microsoft/retina/charts/retina \ + --set enabledPlugin_linux="\[dropreason\,packetforward\,linuxutil\,dns\]" \ + --reuse-values +``` + +**Trade-off:** You'll have node-level metrics only, not pod-level metrics. + +### Option 2: Enable Data Sampling + +Reduce event volume by sampling packets: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: retina-config + namespace: kube-system +data: + config.yaml: | + dataSamplingRate: 10 # Sample 1 out of every 10 packets +``` + +**Trade-off:** Reduced data granularity, but lower overhead. + +### Option 3: Use High Data Aggregation Level + +Reduce events at the eBPF level: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: retina-config + namespace: kube-system +data: + config.yaml: | + dataAggregationLevel: "high" +``` + +**Trade-off:** Disables host interface monitoring; API server latency metrics may be less reliable. + +### Option 4: Selective Deployment + +Deploy Retina only on nodes where you need detailed observability: + +```yaml +# Use node selectors or taints/tolerations +apiVersion: apps/v1 +kind: DaemonSet +spec: + template: + spec: + nodeSelector: + retina-enabled: "true" +``` + +## Advanced Diagnostics + +### Inspecting eBPF Maps + +To see what data structures Retina is using: + +```bash +# Access the node +kubectl debug node/ -it --image=ubuntu + +# In the debug container, enter the host namespace +chroot /host + +# List BPF maps (requires bpftool) +bpftool map list | grep retina + +# Check the packetparser map type +bpftool map show name retina_packetparser_events +``` + +Currently, `packetparser` uses `BPF_MAP_TYPE_PERF_EVENT_ARRAY`. + +### Monitoring Event Rates (Advanced) + +If you have bpftrace available on nodes: + +```bash +# Monitor perf_event activity +sudo bpftrace -e ' + kprobe:perf_event_output { @events = count(); } + interval:s:5 { print(@events); clear(@events); } +' +``` + +High event rates may correlate with increased CPU usage. + +## Reporting Issues + +If you experience performance issues, please report them with: + +1. **Node specifications**: CPU count, memory, kernel version +2. **Retina configuration**: Version, enabled plugins, configuration settings +3. **Workload characteristics**: Network throughput, number of pods, traffic patterns +4. **Performance metrics**: CPU usage, network throughput before/after, specific observations + +Open an issue at: + +## Further Resources + +- [Packetparser Performance Considerations](../03-Metrics/plugins/Linux/packetparser.md#performance-considerations) +- [Data Aggregation Levels](../05-Concepts/data-aggregation.md) +- [Configuration Options](../02-Installation/03-Config.md) diff --git a/docs/06-Troubleshooting/shell.md b/docs/06-Troubleshooting/shell.md index 3bf5adbb1d..f0d12f355a 100644 --- a/docs/06-Troubleshooting/shell.md +++ b/docs/06-Troubleshooting/shell.md @@ -1,21 +1,37 @@ # Shell -**EXPERIMENTAL: `retina shell` is an experimental feature, so the flags and behavior may change in future versions.** +>NOTE: `retina shell` is an experimental feature. The flags and behavior may change in future versions. -The `retina shell` command allows you to start an interactive shell on a Kubernetes node or pod. This runs a container image with many common networking tools installed (`ping`, `curl`, etc.). +The `retina shell` command allows you to start an interactive shell on a Kubernetes node or pod for adhoc debugging. -## Testing connectivity +This runs a container image built from the Dockerfile in the `/shell` directory, with many common networking tools installed (`ping`, `curl`, etc.), as well as specialized tools such as [bpftool](#bpftool), [bpftrace](#bpftrace) [pwru](#pwru) or [Inspektor Gadget](#inspektor-gadget-ig). -Start a shell on a node or inside a pod +Currently the Retina Shell only works in Linux environments. Windows support will be added in the future. -```bash +## Getting Started + +Start a shell on a node or inside a pod: + +```shell # To start a shell in a node (root network namespace): -kubectl retina shell aks-nodepool1-15232018-vmss000001 +kubectl retina shell # To start a shell inside a pod (pod network namespace): -kubectl retina shell -n kube-system pods/coredns-d459997b4-7cpzx +kubectl retina shell -n kube-system pods/ + +# To start a shell inside of a node and mount the host file system +kubectl retina shell --mount-host-filesystem + +# To start a shell inside of a node with extra capabilities +kubectl retina shell --capabilities= ``` +For testing, you can override the image used by `retina shell` either with CLI arguments (`--retina-shell-image-repo` and `--retina-shell-image-version`) or environment variables (`RETINA_SHELL_IMAGE_REPO` and `RETINA_SHELL_IMAGE_VERSION`). + +Run `kubectl retina shell -h` for full documentation and examples. + +## Testing connectivity + Check connectivity using `ping`: ```text @@ -32,15 +48,6 @@ PING 10.224.0.4 (10.224.0.4) 56(84) bytes of data. rtt min/avg/max/mdev = 0.908/1.015/1.128/0.077 ms ``` -Check DNS resolution using `dig`: - -```text -root [ / ]# dig example.com +short -93.184.215.14 -``` - -The tools `nslookup` and `drill` are also available if you prefer those. - Check connectivity to apiserver using `nc` and `curl`: ```text @@ -61,12 +68,23 @@ root [ / ]# curl -k https://10.0.0.1 } ``` -### nftables and iptables +## DNS Resolution + +Check DNS resolution using `dig`: + +```text +root [ / ]# dig example.com +short +93.184.215.14 +``` + +The tools `nslookup` and `drill` are also available if you prefer those. + +## nftables and iptables Accessing nftables and iptables rules requires `NET_RAW` and `NET_ADMIN` capabilities. -```bash -kubectl retina shell aks-nodepool1-15232018-vmss000002 --capabilities NET_ADMIN,NET_RAW +```text +kubectl retina shell --capabilities NET_ADMIN,NET_RAW ``` Then you can run `iptables` and `nft`: @@ -74,13 +92,14 @@ Then you can run `iptables` and `nft`: ```text root [ / ]# iptables -nvL | head -n 2 Chain INPUT (policy ACCEPT 1191K packets, 346M bytes) - pkts bytes target prot opt in out source destination + pkts bytes target prot opt in out source destination + root [ / ]# nft list ruleset | head -n 2 # Warning: table ip filter is managed by iptables-nft, do not touch! table ip filter { ``` -**If you see the error "Operation not permitted (you must be root)", check that your `kubectl retina shell` command sets `--capabilities NET_RAW,NET_ADMIN`.** +>NOTE: If you see the error "Operation not permitted (you must be root)", check that your `kubectl retina shell` command sets `--capabilities NET_RAW,NET_ADMIN`. `iptables` in the shell image uses `iptables-nft`, which may or may not match the configuration on the node. For example, Azure Linux 2 maps `iptables` to `iptables-legacy`. To use the exact same `iptables` binary as installed on the node, you will need to `chroot` into the host filesystem (see below). @@ -88,8 +107,8 @@ table ip filter { On nodes, you can mount the host filesystem to `/host`: -```bash -kubectl retina shell aks-nodepool1-15232018-vmss000002 --mount-host-filesystem +```text +kubectl retina shell --mount-host-filesystem ``` This mounts the host filesystem (`/`) to `/host` in the debug pod: @@ -108,7 +127,7 @@ Symlinks between files on the host filesystem may not resolve correctly. If you `chroot` requires the `SYS_CHROOT` capability: ```bash -kubectl retina shell aks-nodepool1-15232018-vmss000002 --mount-host-filesystem --capabilities SYS_CHROOT +kubectl retina shell --mount-host-filesystem --capabilities SYS_CHROOT ``` Then you can use `chroot` to switch to start a shell inside the host filesystem: @@ -133,7 +152,7 @@ search shncgv2kgepuhm1ls1dwgholsd.cx.internal.cloudapp.net `systemctl` commands require both `chroot` to the host filesystem and host PID: ```bash -kubectl retina shell aks-nodepool1-15232018-vmss000002 --mount-host-filesystem --capabilities SYS_CHROOT --host-pid +kubectl retina shell --mount-host-filesystem --capabilities SYS_CHROOT --host-pid ``` Then `chroot` to the host filesystem and run `systemctl status`: @@ -144,7 +163,138 @@ root [ / ]# chroot /host systemctl status | head -n 2 State: running ``` -**If `systemctl` shows an error "Failed to connect to bus: No data available", check that the `retina shell` command has `--host-pid` set and that you have chroot'd to /host.** +>NOTE: If `systemctl` shows an error "Failed to connect to bus: No data available", check that the `retina shell` command has `--host-pid` set and that you have chroot'd to /host. + +## [pwru](https://github.com/cilium/pwru) + +eBPF-based tool for tracing network packets in the Linux kernel with advanced filtering capabilities. It allows fine-grained introspection of kernel state to facilitate debugging network connectivity issues. + +Requires the `NET_ADMIN` and `SYS_ADMIN` capabilities. + +Capability requirements are based on common eBPF tool practices and not directly from the pwru documentation. + +```shell +kubectl retina shell -n kube-system pod/ --capabilities=NET_ADMIN,SYS_ADMIN +``` + +You can then run, for example: + +```shell +pwru -h +pwru "tcp and (src port 8080 or dst port 8080)" +``` + +## [sysctl](https://man7.org/linux/man-pages/man8/sysctl.8.html) + +Tool for viewing and modifying kernel parameters at runtime. `sysctl` is useful for network troubleshooting as it allows you to inspect and tune various kernel networking settings such as IP forwarding, TCP congestion control, buffer sizes, and other network-related parameters. + +For viewing kernel parameters, no special capabilities are required: + +```shell +kubectl retina shell +``` + +For modifying kernel parameters, you may need the `SYS_ADMIN` capability and/or `chroot` to the host filesystem depending on the parameter: + +```shell +kubectl retina shell --capabilities=SYS_ADMIN --mount-host-filesystem +``` + +You can then run, for example: + +```shell +# View kernel parameters +sysctl net.ipv4.ip_forward +sysctl -a | grep tcp_congestion +sysctl net.core.rmem_max + +# View all networking-related parameters +sysctl -a | grep net + +# Modify parameters (may require chroot /host) +sysctl -w net.ipv4.ip_forward=1 +``` + +>NOTE: `sysctl` shows different kernel parameters depending on whether you're running in the container context or the node context. To view/modify the actual node's kernel parameters, use `chroot /host` after mounting the host filesystem. Running `sysctl` without `chroot` shows the container's view, which may have limited or different parameters. + +## [bpftool](https://github.com/libbpf/bpftool) + +Allows you to list, dump, load BPF programs, etc. Reference utility to quickly inspect and manage BPF objects on your system, to manipulate BPF object files, or to perform various other BPF-related tasks. + +Requires the `NET_ADMIN` and `SYS_ADMIN` capabilities. + +```shell +kubectl retina shell -n kube-system pod/ --capabilities=NET_ADMIN,SYS_ADMIN +``` + +You can then run for example: + +```shell +bpftool -h +bpftool prog show +bpftool map dump id +``` + +## [bpftrace](https://bpftrace.org/) + +bpftrace is a high-level tracing language for Linux and provides a quick and easy way for people to write observability-based eBPF programs. + +Requires the the flags `--mount-host-filesystem`, `--apparmor-unconfined`, `--seccomp-unconfined`, and the following capabilities: + +* `NET_ADMIN` +* `SYS_ADMIN` +* `SYS_RESOURCE` +* `BPF` +* `MKNOD` +* `SYS_CHROOT` + +```sh +# e.g. pod debugging +kubectl retina shell -n kube-system pod/ --capabilities=NET_ADMIN,SYS_ADMIN,SYS_RESOURCE,BPF,MKNOD,SYS_CHROOT --mount-host-filesystem --apparmor-unconfined --seccomp-unconfined +``` + +You can then run for example: + +```shell +bpftrace --help +bpftrace -e 'kprobe:tcp_v4_rcv { printf("tcp packet received\n"); }' +bpftrace -e 'tracepoint:syscalls:sys_enter_connect { printf("connect\n"); }' +``` + +## [Inspektor Gadget (ig)](https://inspektor-gadget.io/) + +Tools and framework for data collection and system inspection on Kubernetes clusters and Linux hosts using eBPF. + +To use `ig`, you need to add the `--mount-host-filesystem`, `--apparmor-unconfined` and `--seccomp-unconfined` flags, along with the following capabilities: + +* `NET_ADMIN` +* `SYS_ADMIN` +* `SYS_RESOURCE` +* `SYSLOG` +* `IPC_LOCK` +* `SYS_PTRACE` +* `NET_RAW` + +```shell +kubectl retina shell --capabilities=NET_ADMIN,SYS_ADMIN,SYS_RESOURCE,SYSLOG,IPC_LOCK,SYS_PTRACE,NET_RAW --mount-host-filesystem --apparmor-unconfined --seccomp-unconfined +``` + +You can then run for example: + +```shell +ig -h +ig run trace_dns:latest +``` + +## [mpstat](https://www.man7.org/linux/man-pages/man1/mpstat.1.html) + +Tool for detailed reporting of processor-related statistics. `mpstat` is useful for network troubleshooting because it shows how much CPU time is spent handling SoftIRQs, which are often triggered by network traffic, helping identify interrupt bottlenecks or imbalanced CPU usage. SoftIRQs (Software Interrupt Requests) are a type of deferred interrupt handling mechanism in the Linux kernel used to process time-consuming tasks—like network packet handling or disk I/O—outside the immediate hardware interrupt context, allowing faster and more efficient interrupt processing without blocking the system. + +This example usage of `mpstat` monitors CPU usage statistics, specifically focusing on SoftIRQ usage, across all CPU cores, sampled every 1 second, for 5 intervals. + +```shell +mpstat -P ALL 1 5 | grep -E '(CPU|%soft|Average)' +``` ## Troubleshooting @@ -158,7 +308,8 @@ If `kubectl retina shell` fails with a timeout error, then: Example: ```bash -kubectl retina shell --timeout 10m node001 # increase timeout to 10 minutes +# increase timeout to 10 minutes +kubectl retina shell --timeout 10m ``` ### Firewalls and ImagePullBackoff @@ -172,14 +323,16 @@ Example: ```bash export RETINA_SHELL_IMAGE_REPO="example.azurecr.io/retina/retina-shell" -export RETINA_SHELL_IMAGE_VERSION=v0.0.1 # optional, if not set defaults to the Retina CLI version. -kubectl retina shell node0001 # this will use the image "example.azurecr.io/retina/retina-shell:v0.0.1" +# optional, if not set defaults to the Retina CLI version. +export RETINA_SHELL_IMAGE_VERSION=v0.0.1 +# this will use the image "example.azurecr.io/retina/retina-shell:v0.0.1" +kubectl retina shell ``` ## Limitations * Windows nodes and pods are not yet supported. -* `bpftool` and `bpftrace` are not supported. +* `bpftrace` not yet supported. * The shell image links `iptables` commands to `iptables-nft`, even if the node itself links to `iptables-legacy`. * `nsenter` is not supported. * `ip netns` will not work without `chroot` to the host filesystem. diff --git a/docs/08-Contributing/02-development.md b/docs/08-Contributing/02-development.md index 051cbacfc8..67353969b5 100644 --- a/docs/08-Contributing/02-development.md +++ b/docs/08-Contributing/02-development.md @@ -1,19 +1,39 @@ # Development +This document provides steps to set up your dev environment and start contributing to the Retina project. You can find the complete documentation on [retina.sh](https://retina.sh) + ## Quick start Retina uses a forking workflow. To contribute, fork the repository and create a branch for your changes. -The easiest way to set up your Development Environment is to use the provided GitHub Codespaces configuration. +### Using a devcontainer (recommended) + +The easiest way to get started is to use the provided [devcontainer](https://github.com/microsoft/retina/blob/main/.devcontainer/devcontainer.json), which works with both [GitHub Codespaces](https://github.com/features/codespaces) and [VS Code Dev Containers](https://code.visualstudio.com/docs/devcontainers/containers). + +The devcontainer comes pre-configured with all required tools: + +- Go, clang/LLVM (for eBPF compilation), Docker, Helm, kubectl, Kind, Azure CLI, GitHub CLI, and jq +- Go modules are pre-downloaded; run `make generate` to compile eBPF programs and generate mocks before building +- A Kind cluster is created on startup for local testing +- VS Code is configured with golangci-lint and gofumpt so editor feedback matches CI + +To launch in Codespaces, click **Code > Codespaces > New codespace** on the repository page. To use locally, open the repository in VS Code and select **Reopen in Container** from the command palette. + +### Manual setup + +If you prefer to set up your environment manually, see the requirements below. ## Environment Config +Below is a list of required tools and dependencies you need to set up your local development environment for Retina. + - [Go](https://go.dev/doc/install) - [Docker](https://docs.docker.com/engine/install/) - [Helm](https://helm.sh/docs/intro/install) - jq: `sudo apt install jq` -- Fork the repository -- If you want to use [ghcr.io](https://github.com/features/packages) as container registry, login following instructions [here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-with-a-personal-access-token-classic) +- If you want to use [ghcr.io](https://github.com/features/packages) as container registry, login following instructions on [authenticating with a personal access token](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-with-a-personal-access-token-classic) + +Once you have set up your environment fork the repository and create a branch for your changes. ### LLVM/Clang Installation @@ -49,13 +69,6 @@ sudo ln -s /usr/bin/llvm-strip-16 /usr/bin/llvm-strip ## Building and Testing -### Test - -```bash -make test # run unit-test locally -make test-image # run tests in docker container -``` - ### Build Generate all mocks and BPF programs: @@ -73,7 +86,7 @@ make retina To build a `retina-agent` container image with specific tag: ```bash -make retina-image # also pushes to image registy +make retina-image # also pushes to image registry make retina-operator-image ``` @@ -95,11 +108,18 @@ debug packetforward Received PacketForward data {"Data": "IngressBytes:8 ... ``` +### Test + +```bash +make test # run unit-tests locally +make test-image # run tests in docker container +``` + ### Publishing Images and Charts To publish images to GHCR in your forked repository, simply push to `main`, or publish a tag. The `container-publish` action will run automatically and push images to your GitHub packages registry. -These registries are private by default; to pull images from your registry anonymously, [navigate to "Package Settings" for each publish image repository and set the visibility to "Public"](https://docs.github.com/en/packages/learn-github-packages/configuring-a-packages-access-control-and-visibility#configuring-access-to-packages-for-your-personal-account). +These registries are private by default; to pull images from your registry anonymously, [navigate to "Package Settings" for each published image repository and set the visibility to "Public"](https://docs.github.com/en/packages/learn-github-packages/configuring-a-packages-access-control-and-visibility#configuring-access-to-packages-for-your-personal-account). Alternatively, configure authenticated access to your registry using a [GitHub Personal Access Token](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry). @@ -180,11 +200,38 @@ Uninstall `Retina`: make helm-uninstall ``` -## Updating Documentation +## Dependency and Security Management -The documentation available on [retina.sh](https://retina.sh) can be found within the [docs](https://github.com/microsoft/retina/tree/main/docs) folder in the repository. +Retina uses automated dependency management and security scanning to maintain secure and up-to-date container images and dependencies. -The diagrams used are created with [Excalidraw](https://excalidraw.com/). The source `.excalidraw` files are stored within the repository, alongside their `.png` equivalent. +### Dependabot Configuration + +The repository uses [Dependabot](https://github.com/dependabot) to automatically track and update dependencies: + +- **Docker Base Images**: Automatically monitored for security updates and new versions +- **Go Modules**: Tracked for dependency updates +- **GitHub Actions**: Workflow dependencies are kept current + +#### Docker Base Image Tracking + +Retina has Dockerfiles in multiple directories, and each is tracked separately by Dependabot: + +- `/controller` - Main retina controller images (daily checks) +- `/shell` - Shell utility images (daily checks) +- `/cli` - CLI tool images (daily checks) +- `/operator` - Operator images (daily checks) +- `/test/image` - Test images (daily checks) +- `/hack/tools/kapinger` - Kapinger tool images (weekly checks) +- `/hack/tools/toolbox` - Toolbox utility images (weekly checks) + +When Dependabot detects a security vulnerability (CVE) in a base image, it will automatically create a pull request to update the image SHA to a patched version. + +### Adding New Dockerfiles + +When adding new Dockerfiles to the repository: + +1. Add the directory containing the Dockerfile to `.github/dependabot.yaml` +2. Choose an appropriate schedule: daily for critical components, weekly for tools ## Opening a Pull Request @@ -193,15 +240,26 @@ When you're ready to open a pull request, please ensure that your branch is up-t ### Cryptographic Signing of Commits In order to certify the provenance of commits and defend against impersonation, we require that all commits be cryptographically signed. -Documentation for setting up Git and Github to sign your commits can be found [here](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). -Additional information about Git's use of GPG can be found [here](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work) +Documentation for setting up Git and GitHub to sign your commits can be found in the [GitHub documentation on signing commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). +Additional information about Git's use of GPG can be found in the [Git documentation on signing your work](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work) > To configure your Git client to sign commits by default for a local repository, run `git config --add commit.gpgsign true`. For **GitHub Codespaces** users, please follow [this doc](https://docs.github.com/en/codespaces/managing-your-codespaces/managing-gpg-verification-for-github-codespaces) to configure GitHub to automatically use GPG to sign commits you make in your Codespaces. -### Developers Certificate of Origin (DCO) +### Developer Certificate of Origin (DCO) -Contributions to Retina must contain a Developers Certificate of Origin within their constituent commits. -This can be accomplished by providing a `-s` flag to `git commit` as documented [here](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt--s). +Contributions to Retina must contain a Developer Certificate of Origin within their constituent commits. +This can be accomplished by providing a `-s` flag to `git commit` as documented in the [Git commit documentation](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt--s). This will add a `Signed-off-by` trailer to your Git commit, affirming your acceptance of the Contributor License Agreement. + +### Updating Documentation + +The documentation available on [retina.sh](https://retina.sh) can be found within the [docs](https://github.com/microsoft/retina/tree/main/docs) folder in the repository. + +The diagrams used are created with [Excalidraw](https://excalidraw.com/). The source `.excalidraw` files are stored within the repository, alongside their `.png` equivalent. + +### GitHub issues and Good First Issue + +You can find the open issues on the repo's [GitHub issues board](https://github.com/microsoft/retina/issues) +If you are a first-time contributor, you can find the issues that are suitable for newcomers by finding the [issues labeled as "good first issue"](https://github.com/microsoft/retina/labels/good%20first%20issue) diff --git a/go.mod b/go.mod index 2424c0a2cb..f188d44f3f 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,16 @@ module github.com/microsoft/retina -go 1.24.3 +go 1.25.5 require ( - github.com/go-chi/chi/v5 v5.2.1 + github.com/go-chi/chi/v5 v5.2.5 github.com/google/uuid v1.6.0 - github.com/prometheus/client_golang v1.22.0 - github.com/spf13/cobra v1.9.1 - go.uber.org/zap v1.27.0 - k8s.io/client-go v0.32.4 - sigs.k8s.io/cloud-provider-azure/pkg/azclient v0.6.2 - sigs.k8s.io/cloud-provider-azure/pkg/azclient/configloader v0.5.3 + github.com/prometheus/client_golang v1.23.2 + github.com/spf13/cobra v1.10.2 + go.uber.org/zap v1.28.0 + k8s.io/client-go v0.35.4 + sigs.k8s.io/cloud-provider-azure/pkg/azclient v0.20.4 + sigs.k8s.io/cloud-provider-azure/pkg/azclient/configloader v0.14.0 ) retract ( @@ -19,388 +19,348 @@ retract ( ) require ( - cel.dev/expr v0.20.0 // indirect + cel.dev/expr v0.25.1 // indirect code.cloudfoundry.org/clock v1.0.0 // indirect - github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect - github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v1.2.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.5.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect - github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest v0.11.29 // indirect github.com/Azure/go-autorest/autorest/adal v0.9.24 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect - github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect + github.com/BurntSushi/toml v1.6.0 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect - github.com/aws/smithy-go v1.22.3 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 // indirect + github.com/aws/smithy-go v1.25.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect github.com/cilium/dns v1.1.51-0.20240603182237-af788769786a // indirect github.com/cilium/lumberjack/v2 v2.4.1 // indirect - github.com/cilium/stream v0.0.0-20241203114243-53c3e5d79744 // indirect - github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect - github.com/containerd/cgroups/v3 v3.0.3 // indirect - github.com/containerd/containerd v1.7.27 // indirect - github.com/containerd/continuity v0.4.4 // indirect - github.com/containerd/errdefs v0.3.0 // indirect - github.com/containerd/fifo v1.1.0 // indirect + github.com/cilium/stream v0.0.1 // indirect + github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect + github.com/containerd/cgroups/v3 v3.0.5 // indirect + github.com/containerd/containerd v1.7.32 // indirect + github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/log v0.1.0 // indirect - github.com/containerd/ttrpc v1.2.7 // indirect - github.com/containerd/typeurl/v2 v2.2.0 // indirect - github.com/containernetworking/cni v1.2.3 // indirect + github.com/containerd/typeurl/v2 v2.2.3 // indirect + github.com/containernetworking/cni v1.3.0 // indirect github.com/coreos/go-semver v0.3.1 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/cyphar/filepath-securejoin v0.3.6 // indirect + github.com/coreos/go-systemd/v22 v22.7.0 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/distribution/reference v0.6.0 // indirect - github.com/docker/cli v26.0.0+incompatible // indirect - github.com/docker/distribution v2.8.3+incompatible // indirect - github.com/docker/docker v28.0.4+incompatible // indirect - github.com/docker/docker-credential-helpers v0.8.1 // indirect - github.com/docker/go-connections v0.5.0 // indirect - github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect - github.com/docker/go-metrics v0.0.1 // indirect - github.com/docker/go-units v0.5.0 // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect - github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect github.com/fatih/camelcase v1.0.0 // indirect - github.com/fatih/color v1.18.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/fatih/color v1.19.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/analysis v0.23.0 // indirect - github.com/go-openapi/errors v0.22.1 // indirect - github.com/go-openapi/jsonpointer v0.21.1 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/loads v0.22.0 // indirect - github.com/go-openapi/runtime v0.28.0 // indirect - github.com/go-openapi/spec v0.21.0 // indirect - github.com/go-openapi/strfmt v0.23.0 // indirect - github.com/go-openapi/swag v0.23.1 // indirect - github.com/go-openapi/validate v0.24.0 // indirect + github.com/go-openapi/analysis v0.24.3 // indirect + github.com/go-openapi/errors v0.22.7 // indirect + github.com/go-openapi/jsonpointer v0.22.5 // indirect + github.com/go-openapi/jsonreference v0.21.5 // indirect + github.com/go-openapi/loads v0.23.3 // indirect + github.com/go-openapi/runtime v0.29.3 // indirect + github.com/go-openapi/spec v0.22.4 // indirect + github.com/go-openapi/strfmt v0.26.1 // indirect + github.com/go-openapi/swag v0.25.5 // indirect + github.com/go-openapi/validate v0.25.2 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gobwas/glob v0.2.3 // indirect - github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gofrs/uuid v4.2.0+incompatible // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect - github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/google/gops v0.3.28 // indirect - github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect - github.com/google/renameio/v2 v2.0.0 // indirect - github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/gorilla/mux v1.8.1 // indirect - github.com/gorilla/websocket v1.5.1 // indirect + github.com/google/renameio/v2 v2.0.2 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/gosuri/uitable v0.0.4 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmoiron/sqlx v1.4.0 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/josharian/native v1.1.0 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.4 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect - github.com/lib/pq v1.10.9 // indirect + github.com/lib/pq v1.11.2 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect - github.com/mackerelio/go-osstat v0.2.5 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/mackerelio/go-osstat v0.2.6 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/mdlayher/socket v0.5.1 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/mdlayher/socket v0.6.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/locker v1.0.1 // indirect - github.com/moby/moby v26.1.0+incompatible // indirect - github.com/moby/spdystream v0.5.0 // indirect - github.com/moby/sys/mountinfo v0.7.1 // indirect - github.com/moby/sys/sequential v0.5.0 // indirect - github.com/moby/sys/signal v0.7.0 // indirect - github.com/moby/sys/user v0.3.0 // indirect - github.com/moby/term v0.5.0 // indirect + github.com/moby/spdystream v0.5.1 // indirect + github.com/moby/term v0.5.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect - github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/opencontainers/runtime-spec v1.2.0 // indirect - github.com/opencontainers/selinux v1.11.0 // indirect - github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/peterbourgon/diskv v2.0.2-0.20180312054125-0646ccaebea1+incompatible // indirect - github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect + github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/procfs v0.16.0 // indirect - github.com/rubenv/sql-migrate v1.7.1 // indirect + github.com/prometheus/procfs v0.20.1 // indirect + github.com/rubenv/sql-migrate v1.8.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/s3rj1k/go-fanotify/fanotify v0.0.0-20210917134616-9c00a300bb7a // indirect - github.com/sagikazarmark/locafero v0.7.0 // indirect - github.com/sasha-s/go-deadlock v0.3.5 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect + github.com/sasha-s/go-deadlock v0.3.6 // indirect github.com/shopspring/decimal v1.4.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.14.0 // indirect - github.com/spf13/cast v1.7.1 // indirect - github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect - github.com/spiffe/spire-api-sdk v1.12.0 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect + github.com/spiffe/spire-api-sdk v1.14.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/vishvananda/netns v0.0.5 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect - github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect - github.com/zeebo/errs v1.4.0 // indirect - go.etcd.io/etcd/api/v3 v3.5.21 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect - go.etcd.io/etcd/client/v3 v3.5.21 // indirect - go.mongodb.org/mongo-driver v1.14.0 // indirect + go.etcd.io/etcd/api/v3 v3.6.7 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.7 // indirect + go.etcd.io/etcd/client/v3 v3.6.7 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect go.uber.org/dig v1.17.1 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect - golang.org/x/crypto v0.37.0 // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/text v0.24.0 // indirect - golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.31.0 // indirect + golang.org/x/crypto v0.51.0 // indirect + golang.org/x/mod v0.36.0 + golang.org/x/text v0.37.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.44.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect gopkg.in/inf.v0 v0.9.1 // indirect - k8s.io/apiserver v0.32.3 // indirect - k8s.io/component-base v0.32.3 // indirect - k8s.io/cri-api v0.30.1 // indirect - oras.land/oras-go v1.2.5 // indirect - sigs.k8s.io/kustomize/api v0.18.0 // indirect - sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect + k8s.io/apiserver v0.35.3 // indirect + k8s.io/component-base v0.35.3 // indirect + sigs.k8s.io/kustomize/api v0.20.1 // indirect + sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect ) require ( github.com/go-chi/chi v4.1.2+incompatible - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.7.0 - github.com/google/gofuzz v1.2.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/spf13/pflag v1.0.6 - github.com/stretchr/testify v1.10.0 + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/spf13/pflag v1.0.10 + github.com/stretchr/testify v1.11.1 go.uber.org/multierr v1.11.0 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/oauth2 v0.28.0 // indirect - golang.org/x/sync v0.14.0 - golang.org/x/sys v0.33.0 - golang.org/x/term v0.31.0 // indirect - google.golang.org/protobuf v1.36.6 + golang.org/x/net v0.54.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.20.0 + golang.org/x/sys v0.44.0 + golang.org/x/term v0.43.0 // indirect + google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.32.4 - k8s.io/apimachinery v0.32.4 - k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect - sigs.k8s.io/yaml v1.4.0 + k8s.io/api v0.35.4 + k8s.io/apimachinery v0.35.4 + k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/yaml v1.6.0 ) require ( github.com/Azure/azure-container-networking/zapai v0.0.3 - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 + github.com/Azure/azure-sdk-for-go v68.0.0+incompatible + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v4 v4.8.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dashboard/armdashboard v1.2.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/monitor/armmonitor v0.11.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v5 v5.2.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.0 - github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 - github.com/Microsoft/hcsshim v0.12.9 - github.com/Sytten/logrus-zap-hook v0.1.0 - github.com/aws/aws-sdk-go-v2 v1.36.3 - github.com/aws/aws-sdk-go-v2/config v1.29.14 - github.com/aws/aws-sdk-go-v2/credentials v1.17.67 - github.com/aws/aws-sdk-go-v2/service/s3 v1.79.3 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 + github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.7.0 + github.com/Microsoft/hcsshim v0.14.1 + github.com/aws/aws-sdk-go-v2 v1.41.7 + github.com/aws/aws-sdk-go-v2/config v1.32.17 + github.com/aws/aws-sdk-go-v2/credentials v1.19.16 + github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 - github.com/cilium/cilium v1.18.0-pre.1 - github.com/cilium/ebpf v0.18.0 - github.com/cilium/hive v0.0.0-20250328192914-7f3c86c9c25e - github.com/cilium/proxy v0.0.0-20250318065604-173988fc0adb - github.com/cilium/workerpool v1.3.0 - github.com/florianl/go-tc v0.4.5 + github.com/cilium/cilium v1.19.3 + github.com/cilium/ebpf v0.21.0 + github.com/cilium/hive v0.0.0-20260108104938-97756f6ff54c + github.com/cilium/proxy v0.0.0-20250623105955-2136f59a4ea1 + github.com/cilium/workerpool v1.4.0 + github.com/florianl/go-tc v0.4.8 github.com/go-logr/zapr v1.3.0 github.com/google/gopacket v1.1.19 + github.com/gopacket/gopacket v1.6.0 github.com/hashicorp/golang-lru/v2 v2.0.7 - github.com/inspektor-gadget/inspektor-gadget v0.27.0 - github.com/jellydator/ttlcache/v3 v3.3.0 + github.com/jellydator/ttlcache/v3 v3.4.0 github.com/jsternberg/zap-logfmt v1.3.0 - github.com/mdlayher/netlink v1.7.2 + github.com/mdlayher/netlink v1.11.2 github.com/microsoft/ApplicationInsights-Go v0.4.4 - github.com/mitchellh/mapstructure v1.5.0 - github.com/onsi/ginkgo/v2 v2.23.4 - github.com/onsi/gomega v1.37.0 + github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c + github.com/onsi/ginkgo/v2 v2.29.0 + github.com/onsi/gomega v1.41.0 github.com/pkg/errors v0.9.1 - github.com/prometheus/client_model v0.6.1 - github.com/prometheus/common v0.63.0 - github.com/safchain/ethtool v0.5.10 - github.com/sirupsen/logrus v1.9.3 - github.com/spf13/viper v1.20.1 - github.com/vishvananda/netlink v1.3.1-0.20250328051554-cb48698f2590 - go.opentelemetry.io/otel v1.35.0 - go.opentelemetry.io/otel/metric v1.35.0 - go.opentelemetry.io/otel/trace v1.35.0 - go.uber.org/mock v0.5.2 - golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 - google.golang.org/grpc v1.72.0 + github.com/prometheus/client_model v0.6.2 + github.com/prometheus/common v0.67.5 + github.com/safchain/ethtool v0.7.0 + github.com/spf13/viper v1.21.0 + github.com/vishvananda/netlink v1.3.2-0.20260109214200-c6faf428e8f8 + go.opentelemetry.io/otel v1.43.0 + go.opentelemetry.io/otel/metric v1.43.0 + go.opentelemetry.io/otel/trace v1.43.0 + go.uber.org/mock v0.6.0 + go.uber.org/zap/exp v0.3.0 + golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f + google.golang.org/grpc v1.81.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gotest.tools v2.2.0+incompatible gotest.tools/v3 v3.5.2 - helm.sh/helm/v3 v3.17.3 - k8s.io/apiextensions-apiserver v0.32.3 - k8s.io/cli-runtime v0.32.3 - k8s.io/kubectl v0.32.3 - k8s.io/metrics v0.32.3 + helm.sh/helm/v3 v3.21.0 + k8s.io/apiextensions-apiserver v0.35.3 + k8s.io/cli-runtime v0.35.3 + k8s.io/kubectl v0.35.3 + k8s.io/metrics v0.35.4 k8s.io/perf-tests/network/benchmarks/netperf v0.0.0-00010101000000-000000000000 - sigs.k8s.io/controller-runtime v0.20.4 + sigs.k8s.io/controller-runtime v0.23.3 ) require ( github.com/antlr4-go/antlr/v4 v4.13.1 // indirect - github.com/cilium/statedb v0.3.7 - github.com/containerd/containerd/api v1.8.0 // indirect + github.com/cilium/statedb v0.5.6 github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/containerd/platforms v0.2.1 // indirect - github.com/go-jose/go-jose/v4 v4.0.5 // indirect - github.com/google/cel-go v0.24.1 // indirect - github.com/moby/sys/userns v0.1.0 // indirect + github.com/containerd/platforms v1.0.0-rc.1 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/google/cel-go v0.26.1 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect ) require ( 4d63.com/gocheckcompilerdirectives v1.3.0 // indirect 4d63.com/gochecknoglobals v0.2.2 // indirect - cloud.google.com/go v0.116.0 // indirect - cloud.google.com/go/auth v0.15.0 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect - cloud.google.com/go/compute/metadata v0.6.0 // indirect - cloud.google.com/go/iam v1.2.2 // indirect - cloud.google.com/go/kms v1.20.1 // indirect - cloud.google.com/go/longrunning v0.6.2 // indirect - cloud.google.com/go/monitoring v1.21.2 // indirect - cloud.google.com/go/storage v1.49.0 // indirect + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/auth v0.18.2 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/iam v1.5.3 // indirect + cloud.google.com/go/kms v1.26.0 // indirect + cloud.google.com/go/longrunning v0.8.0 // indirect + cloud.google.com/go/monitoring v1.24.3 // indirect + cloud.google.com/go/storage v1.57.2 // indirect code.gitea.io/sdk/gitea v0.18.0 // indirect - dario.cat/mergo v1.0.1 // indirect - github.com/4meepo/tagalign v1.4.2 // indirect - github.com/Abirdcfly/dupword v0.1.3 // indirect + codeberg.org/chavacava/garif v0.2.0 // indirect + codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect + dario.cat/mergo v1.0.2 // indirect + dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect + dev.gaijin.team/go/golib v0.6.0 // indirect + github.com/4meepo/tagalign v1.4.3 // indirect + github.com/Abirdcfly/dupword v0.1.7 // indirect + github.com/AdminBenni/iota-mixing v1.0.0 // indirect github.com/AlekSi/pointer v1.2.0 // indirect - github.com/Antonboom/errname v1.0.0 // indirect - github.com/Antonboom/nilnil v1.0.1 // indirect - github.com/Antonboom/testifylint v1.5.2 // indirect - github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect + github.com/AlwxSin/noinlineerr v1.0.5 // indirect + github.com/Antonboom/errname v1.1.1 // indirect + github.com/Antonboom/nilnil v1.1.1 // indirect + github.com/Antonboom/testifylint v1.6.4 // indirect github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v6 v6.4.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v6 v6.5.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.2.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.2.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7 v7.3.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v6 v6.6.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9 v9.0.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2 v2.0.0 // indirect github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 // indirect github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect - github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect + github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect github.com/Azure/msi-dataplane v0.4.3 // indirect - github.com/Crocmagnon/fatcontext v0.7.1 // indirect - github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect - github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 // indirect + github.com/Djarvur/go-err113 v0.1.1 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect + github.com/MirrexOne/unqueryvet v1.5.4 // indirect github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect - github.com/ProtonMail/go-crypto v1.1.3 // indirect + github.com/ProtonMail/go-crypto v1.4.1 // indirect + github.com/alecthomas/chroma/v2 v2.23.1 // indirect github.com/alecthomas/go-check-sumtype v0.3.1 // indirect github.com/alessio/shellescape v1.4.1 // indirect - github.com/alexkohler/nakedret/v2 v2.0.5 // indirect - github.com/alexkohler/prealloc v1.0.0 // indirect + github.com/alexkohler/nakedret/v2 v2.0.6 // indirect + github.com/alexkohler/prealloc v1.1.0 // indirect + github.com/alfatraining/structtag v1.0.0 // indirect github.com/alingse/asasalint v0.0.11 // indirect - github.com/alingse/nilnesserr v0.1.2 // indirect - github.com/anchore/bubbly v0.0.0-20230518153401-87b6af8ccf22 // indirect - github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a // indirect - github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb // indirect - github.com/anchore/quill v0.4.1 // indirect - github.com/ashanbrown/forbidigo v1.6.0 // indirect - github.com/ashanbrown/makezero v1.2.0 // indirect + github.com/alingse/nilnesserr v0.2.0 // indirect + github.com/anchore/bubbly v0.0.0-20250717181826-8a411f9d8cbf // indirect + github.com/anchore/go-logger v0.0.0-20251106021608-a5b0513fa9a9 // indirect + github.com/anchore/go-macholibre v0.0.0-20250826193721-3cd206ca93aa // indirect + github.com/anchore/quill v0.7.1 // indirect + github.com/ashanbrown/forbidigo/v2 v2.3.0 // indirect + github.com/ashanbrown/makezero/v2 v2.1.0 // indirect github.com/atc0005/go-teams-notify/v2 v2.10.0 // indirect - github.com/aws/aws-sdk-go v1.53.0 // indirect - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.9 // indirect - github.com/aws/aws-sdk-go-v2/service/ecr v1.28.0 // indirect - github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.23.5 // indirect - github.com/aws/aws-sdk-go-v2/service/kms v1.30.0 // indirect - github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20240514230400-03fa26f5508f // indirect + github.com/aws/aws-sdk-go v1.55.8 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ecr v1.45.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.33.2 // indirect + github.com/aws/aws-sdk-go-v2/service/kms v1.50.3 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect + github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.10.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/bkielbasa/cyclop v1.2.3 // indirect - github.com/blacktop/go-dwarf v1.0.9 // indirect - github.com/blacktop/go-macho v1.1.162 // indirect + github.com/blacktop/go-dwarf v1.0.14 // indirect + github.com/blacktop/go-macho v1.1.263 // indirect github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect + github.com/blang/semver v3.5.1+incompatible // indirect github.com/blizzy78/varnamelen v0.8.0 // indirect github.com/bluesky-social/indigo v0.0.0-20240411170459-440932307e0d // indirect - github.com/bombsimon/wsl/v4 v4.5.0 // indirect - github.com/breml/bidichk v0.3.2 // indirect - github.com/breml/errchkjson v0.4.0 // indirect - github.com/buger/jsonparser v1.1.1 // indirect - github.com/butuzov/ireturn v0.3.1 // indirect + github.com/bombsimon/wsl/v4 v4.7.0 // indirect + github.com/bombsimon/wsl/v5 v5.6.0 // indirect + github.com/breml/bidichk v0.3.3 // indirect + github.com/breml/errchkjson v0.4.1 // indirect + github.com/buger/jsonparser v1.1.2 // indirect + github.com/butuzov/ireturn v0.4.0 // indirect github.com/butuzov/mirror v1.3.0 // indirect github.com/caarlos0/ctrlc v1.2.0 // indirect github.com/caarlos0/env/v11 v11.0.1 // indirect @@ -409,46 +369,80 @@ require ( github.com/caarlos0/go-version v0.1.1 // indirect github.com/caarlos0/log v0.4.4 // indirect github.com/carlmjohnson/versioninfo v0.22.5 // indirect - github.com/catenacyber/perfsprint v0.8.2 // indirect + github.com/catenacyber/perfsprint v0.10.1 // indirect github.com/cavaliergopher/cpio v1.0.1 // indirect - github.com/ccojocar/zxcvbn-go v1.0.2 // indirect + github.com/ccojocar/zxcvbn-go v1.0.4 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/charithe/durationcheck v0.0.10 // indirect - github.com/charmbracelet/bubbletea v0.22.1 // indirect - github.com/charmbracelet/lipgloss v0.10.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/charithe/durationcheck v0.0.11 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/charmbracelet/colorprofile v0.4.1 // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.11.3 // indirect + github.com/charmbracelet/x/cellbuf v0.0.14 // indirect github.com/charmbracelet/x/exp/ordered v0.0.0-20231010190216-1cb11efc897d // indirect - github.com/chavacava/garif v0.1.0 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // indirect - github.com/ckaznocha/intrange v0.3.0 // indirect - github.com/cloudflare/circl v1.3.8 // indirect - github.com/containerd/console v1.0.4 // indirect - github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect + github.com/ckaznocha/intrange v0.3.1 // indirect + github.com/clipperhouse/displaywidth v0.6.2 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.3.0 // indirect + github.com/cloudflare/cfssl v1.6.5 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect + github.com/coreos/go-oidc/v3 v3.17.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/creack/pty v1.1.24 // indirect github.com/curioswitch/go-reassign v0.3.0 // indirect - github.com/daixiang0/gci v0.13.5 // indirect + github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect + github.com/daixiang0/gci v0.13.7 // indirect + github.com/dave/dst v0.27.3 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect github.com/dghubble/go-twitter v0.0.0-20211115160449-93a8679adecb // indirect github.com/dghubble/oauth1 v0.7.3 // indirect github.com/dghubble/sling v1.4.0 // indirect + github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect + github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/docker/cli v29.2.1+incompatible // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker v28.5.2+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.3 // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elliotchance/orderedmap/v2 v2.2.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/ettle/strcase v0.2.0 // indirect github.com/fatih/structtag v1.2.0 // indirect - github.com/firefart/nonamedreturns v1.0.5 // indirect - github.com/fxamacker/cbor/v2 v2.8.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/firefart/nonamedreturns v1.0.6 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect - github.com/ghostiam/protogetter v0.3.9 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/ghostiam/protogetter v0.3.20 // indirect github.com/github/smimesign v0.2.0 // indirect - github.com/go-critic/go-critic v0.12.0 // indirect + github.com/go-critic/go-critic v0.14.3 // indirect github.com/go-fed/httpsig v1.1.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.6.0 // indirect - github.com/go-git/go-git/v5 v5.13.0 // indirect + github.com/go-git/go-billy/v5 v5.9.0 // indirect + github.com/go-git/go-git/v5 v5.19.1 // indirect + github.com/go-openapi/swag/cmdutils v0.25.5 // indirect + github.com/go-openapi/swag/conv v0.25.5 // indirect + github.com/go-openapi/swag/fileutils v0.25.5 // indirect + github.com/go-openapi/swag/jsonname v0.25.5 // indirect + github.com/go-openapi/swag/jsonutils v0.25.5 // indirect + github.com/go-openapi/swag/loading v0.25.5 // indirect + github.com/go-openapi/swag/mangling v0.25.5 // indirect + github.com/go-openapi/swag/netutils v0.25.5 // indirect + github.com/go-openapi/swag/stringutils v0.25.5 // indirect + github.com/go-openapi/swag/typeutils v0.25.5 // indirect + github.com/go-openapi/swag/yamlutils v0.25.5 // indirect github.com/go-restruct/restruct v1.2.0-alpha // indirect github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect @@ -458,30 +452,35 @@ require ( github.com/go-toolsmith/astp v1.1.0 // indirect github.com/go-toolsmith/strparse v1.1.0 // indirect github.com/go-toolsmith/typep v1.1.0 // indirect - github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect github.com/gobuffalo/flect v1.0.3 // indirect - github.com/gofrs/flock v0.12.1 // indirect + github.com/godoc-lint/godoc-lint v0.11.2 // indirect + github.com/gofrs/flock v0.13.0 // indirect + github.com/golangci/asciicheck v0.5.0 // indirect github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect - github.com/golangci/go-printf-func-name v0.1.0 // indirect + github.com/golangci/go-printf-func-name v0.1.1 // indirect github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect - github.com/golangci/golangci-lint v1.64.7 // indirect - github.com/golangci/misspell v0.6.0 // indirect - github.com/golangci/plugin-module-register v0.1.1 // indirect + github.com/golangci/golangci-lint/v2 v2.11.4 // indirect + github.com/golangci/golines v0.15.0 // indirect + github.com/golangci/misspell v0.8.0 // indirect + github.com/golangci/plugin-module-register v0.1.2 // indirect github.com/golangci/revgrep v0.8.0 // indirect - github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect - github.com/google/go-containerregistry v0.20.1 // indirect + github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect + github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect + github.com/google/certificate-transparency-go v1.3.2 // indirect + github.com/google/go-containerregistry v0.20.7 // indirect github.com/google/go-github/v62 v62.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/ko v0.15.4 // indirect - github.com/google/rpmpack v0.6.1-0.20240329070804-c2247cbb881a // indirect + github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect + github.com/google/rpmpack v0.7.1 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 // indirect - github.com/google/wire v0.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect - github.com/googleapis/gax-go/v2 v2.14.1 // indirect - github.com/gopacket/gopacket v1.3.1 // indirect - github.com/gordonklaus/ineffassign v0.1.0 // indirect + github.com/google/wire v0.7.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect + github.com/googleapis/gax-go/v2 v2.19.0 // indirect + github.com/gordonklaus/ineffassign v0.2.0 // indirect github.com/goreleaser/chglog v0.6.1 // indirect github.com/goreleaser/fileglob v1.3.0 // indirect github.com/goreleaser/goreleaser v1.26.2 // indirect @@ -489,11 +488,16 @@ require ( github.com/gostaticanalysis/analysisutil v0.7.1 // indirect github.com/gostaticanalysis/comment v1.5.0 // indirect github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect - github.com/gostaticanalysis/nilerr v0.1.1 // indirect + github.com/gostaticanalysis/nilerr v0.1.2 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect - github.com/hashicorp/go-retryablehttp v0.7.7 // indirect - github.com/hashicorp/go-version v1.7.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.8 // indirect + github.com/hashicorp/go-version v1.8.0 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect + github.com/in-toto/attestation v1.1.2 // indirect + github.com/in-toto/in-toto-golang v0.11.0 // indirect github.com/invopop/jsonschema v0.12.0 // indirect github.com/ipfs/bbloom v0.0.4 // indirect github.com/ipfs/go-block-format v0.2.0 // indirect @@ -509,47 +513,53 @@ require ( github.com/ipfs/go-metrics-interface v0.0.1 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jbenet/goprocess v0.1.4 // indirect - github.com/jgautheron/goconst v1.7.1 // indirect + github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 // indirect + github.com/jgautheron/goconst v1.8.2 // indirect github.com/jingyugao/rowserrcheck v1.1.1 // indirect - github.com/jjti/go-spancheck v0.6.4 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/jjti/go-spancheck v0.6.5 // indirect + github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect github.com/julz/importas v0.2.0 // indirect - github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect - github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/kisielk/errcheck v1.9.0 // indirect + github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect + github.com/kevinburke/ssh_config v1.4.0 // indirect + github.com/kisielk/errcheck v1.10.0 // indirect github.com/kkHAIKE/contextcheck v1.1.6 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/pgzip v1.2.6 // indirect - github.com/kulti/thelper v0.6.3 // indirect - github.com/kunwardeep/paralleltest v1.0.10 // indirect + github.com/kulti/thelper v0.7.1 // indirect + github.com/kunwardeep/paralleltest v1.0.15 // indirect github.com/lasiar/canonicalheader v1.1.2 // indirect - github.com/ldez/exptostd v0.4.2 // indirect - github.com/ldez/gomoddirectives v0.6.1 // indirect - github.com/ldez/grignotin v0.9.0 // indirect - github.com/ldez/tagliatelle v0.7.1 // indirect - github.com/ldez/usetesting v0.4.2 // indirect + github.com/ldez/exptostd v0.4.5 // indirect + github.com/ldez/gomoddirectives v0.8.0 // indirect + github.com/ldez/grignotin v0.10.1 // indirect + github.com/ldez/structtags v0.6.1 // indirect + github.com/ldez/tagliatelle v0.7.2 // indirect + github.com/ldez/usetesting v0.5.0 // indirect github.com/leonklingele/grouper v1.1.2 // indirect - github.com/letsencrypt/boulder v0.0.0-20231026200631-000cd05d5491 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/macabu/inamedparam v0.1.3 // indirect - github.com/maratori/testableexamples v1.0.0 // indirect - github.com/maratori/testpackage v1.1.1 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect + github.com/macabu/inamedparam v0.2.0 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect + github.com/manuelarte/funcorder v0.5.0 // indirect + github.com/maratori/testableexamples v1.0.1 // indirect + github.com/maratori/testpackage v1.1.2 // indirect github.com/matoous/godox v1.1.0 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect github.com/mattn/go-mastodon v0.0.8 // indirect - github.com/mgechev/revive v1.7.0 // indirect + github.com/mgechev/revive v1.15.0 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/moby/api v1.54.0 // indirect + github.com/moby/moby/client v0.3.0 // indirect github.com/moricho/tparallel v0.3.2 // indirect github.com/mr-tron/base58 v1.2.0 // indirect - github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/mango v0.1.0 // indirect github.com/muesli/mango-cobra v1.2.0 // indirect github.com/muesli/mango-pflag v0.1.0 // indirect - github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/roff v0.1.0 // indirect - github.com/muesli/termenv v0.15.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/multiformats/go-multibase v0.2.0 // indirect @@ -558,17 +568,17 @@ require ( github.com/nakabonne/nestif v0.3.1 // indirect github.com/nishanths/exhaustive v0.12.0 // indirect github.com/nishanths/predeclared v0.2.2 // indirect - github.com/nunnatsa/ginkgolinter v0.19.1 // indirect + github.com/nunnatsa/ginkgolinter v0.23.0 // indirect github.com/nxadm/tail v1.4.11 // indirect - github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/oklog/ulid/v2 v2.1.1 // indirect github.com/onsi/ginkgo v1.16.5 // indirect + github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pjbgf/sha1cd v0.6.0 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect - github.com/polyfloyd/go-errorlint v1.7.1 // indirect - github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect - github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect + github.com/quasilyte/go-ruleguard v0.4.5 // indirect + github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect github.com/quasilyte/gogrep v0.5.0 // indirect github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect @@ -576,102 +586,112 @@ require ( github.com/redis/go-redis/extra/redisotel/v9 v9.7.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/ryancurrah/gomodguard v1.3.5 // indirect - github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect + github.com/ryancurrah/gomodguard v1.4.1 // indirect + github.com/ryanrolds/sqlclosecheck v0.6.0 // indirect github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect - github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/sashamelentyev/interfacebloat v1.1.0 // indirect - github.com/sashamelentyev/usestdlibvars v1.28.0 // indirect + github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect + github.com/sassoftware/relic v7.2.1+incompatible // indirect github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e // indirect - github.com/secure-systems-lab/go-securesystemslib v0.8.0 // indirect - github.com/securego/gosec/v2 v2.22.2 // indirect + github.com/secure-systems-lab/go-securesystemslib v0.10.0 // indirect + github.com/securego/gosec/v2 v2.24.8-0.20260309165252-619ce2117e08 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect - github.com/sigstore/cosign/v2 v2.2.4 // indirect - github.com/sigstore/rekor v1.3.6 // indirect - github.com/sigstore/sigstore v1.8.3 // indirect + github.com/shibumi/go-pathspec v1.3.0 // indirect + github.com/sigstore/cosign/v2 v2.6.3 // indirect + github.com/sigstore/protobuf-specs v0.5.0 // indirect + github.com/sigstore/rekor v1.5.1 // indirect + github.com/sigstore/rekor-tiles/v2 v2.0.1 // indirect + github.com/sigstore/sigstore v1.10.5 // indirect + github.com/sigstore/sigstore-go v1.1.4 // indirect + github.com/sigstore/timestamp-authority/v2 v2.0.6 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/sivchari/containedctx v1.0.3 // indirect - github.com/sivchari/tenv v1.12.1 // indirect - github.com/skeema/knownhosts v1.3.0 // indirect - github.com/slack-go/slack v0.13.0 // indirect - github.com/sonatard/noctx v0.1.0 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect + github.com/slack-go/slack v0.23.1 // indirect + github.com/sonatard/noctx v0.5.1 // indirect github.com/sourcegraph/go-diff v0.7.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect - github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect + github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/tdakkota/asciicheck v0.4.1 // indirect - github.com/tetafro/godot v1.5.0 // indirect - github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 // indirect - github.com/timonwong/loggercheck v0.10.1 // indirect - github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect - github.com/tomarrell/wrapcheck/v2 v2.10.0 // indirect + github.com/tetafro/godot v1.5.4 // indirect + github.com/theupdateframework/go-tuf v0.7.0 // indirect + github.com/theupdateframework/go-tuf/v2 v2.4.1 // indirect + github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect + github.com/timonwong/loggercheck v0.11.0 // indirect + github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect - github.com/ulikunitz/xz v0.5.12 // indirect + github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c // indirect + github.com/transparency-dev/merkle v0.0.2 // indirect + github.com/ulikunitz/xz v0.5.15 // indirect github.com/ultraware/funlen v0.2.0 // indirect github.com/ultraware/whitespace v0.2.0 // indirect - github.com/uudashr/gocognit v1.2.0 // indirect - github.com/uudashr/iface v1.3.1 // indirect - github.com/vbatts/tar-split v0.11.5 // indirect + github.com/uudashr/gocognit v1.2.1 // indirect + github.com/uudashr/iface v1.4.1 // indirect + github.com/vbatts/tar-split v0.12.2 // indirect github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 // indirect - github.com/wagoodman/go-progress v0.0.0-20220614130704-4b1c25a33c7c // indirect + github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 // indirect github.com/whyrusleeping/cbor-gen v0.1.1-0.20240311221002-68b9f235c302 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/go-gitlab v0.105.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - github.com/xen0n/gosmopolitan v1.2.2 // indirect + github.com/xen0n/gosmopolitan v1.3.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yagipy/maintidx v1.0.0 // indirect github.com/yeya24/promlinter v0.3.0 // indirect github.com/ykadowak/zerologlint v0.1.5 // indirect gitlab.com/bosi/decorder v0.4.2 // indirect gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect - go-simpler.org/musttag v0.13.0 // indirect - go-simpler.org/sloglint v0.9.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect + go-simpler.org/musttag v0.14.0 // indirect + go-simpler.org/sloglint v0.11.1 // indirect + go.augendre.info/arangolint v0.4.0 // indirect + go.augendre.info/fatcontext v0.9.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect go.opentelemetry.io/contrib/exporters/autoexport v0.60.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 // indirect - go.opentelemetry.io/otel/sdk v1.35.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect - gocloud.dev v0.37.0 // indirect - golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect - golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect - google.golang.org/api v0.223.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + gocloud.dev v0.45.0 // indirect + golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect + google.golang.org/api v0.272.0 // indirect + google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect - gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/mail.v2 v2.3.1 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - honnef.co/go/tools v0.6.1 // indirect + honnef.co/go/tools v0.7.0 // indirect + k8s.io/code-generator v0.35.4 // indirect + k8s.io/component-helpers v0.35.3 // indirect + k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b // indirect lukechampine.com/blake3 v1.2.1 // indirect - mvdan.cc/gofumpt v0.7.0 // indirect - mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect - sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20211110210527-619e6b92dab9 // indirect - sigs.k8s.io/controller-tools v0.16.5 // indirect - sigs.k8s.io/gateway-api v1.2.1-0.20250319040149-e8b8afabf889 // indirect + mvdan.cc/gofumpt v0.9.2 // indirect + mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 // indirect + oras.land/oras-go/v2 v2.6.0 // indirect + sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20250517180713-32e5e9e948a5 // indirect + sigs.k8s.io/controller-tools v0.19.0 // indirect + sigs.k8s.io/gateway-api v1.4.0-rc.2 // indirect sigs.k8s.io/kind v0.23.0 // indirect - sigs.k8s.io/mcs-api v0.1.1-0.20250224121229-6c631f4730d0 // indirect - sigs.k8s.io/mcs-api/controllers v0.0.0-20250224121229-6c631f4730d0 // indirect + sigs.k8s.io/mcs-api v0.3.1-0.20260224125735-0f775a3eff97 // indirect + sigs.k8s.io/mcs-api/controllers v0.0.0-20260403094305-4b9911b73f14 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - software.sslmate.com/src/go-pkcs12 v0.4.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect + software.sslmate.com/src/go-pkcs12 v0.7.0 // indirect ) -replace github.com/vishvananda/netns => github.com/inspektor-gadget/netns v0.0.5-0.20230524185006-155d84c555d6 - -replace k8s.io/perf-tests/network/benchmarks/netperf => github.com/Azure/perf-tests/network/benchmarks/netperf v0.0.0-20241008140716-395a79947d2c - -replace go.universe.tf/metallb => github.com/cilium/metallb v0.1.1-0.20220829170633-5d7dfb1129f7 - tool ( github.com/cilium/ebpf/cmd/bpf2go - github.com/golangci/golangci-lint/cmd/golangci-lint + github.com/golangci/golangci-lint/v2/cmd/golangci-lint github.com/goreleaser/goreleaser github.com/onsi/ginkgo go.uber.org/mock/mockgen @@ -679,3 +699,5 @@ tool ( sigs.k8s.io/controller-runtime/tools/setup-envtest sigs.k8s.io/controller-tools/cmd/controller-gen ) + +replace k8s.io/perf-tests/network/benchmarks/netperf => github.com/Azure/perf-tests/network/benchmarks/netperf v0.0.0-20241008140716-395a79947d2c diff --git a/go.sum b/go.sum index 612ad6a949..e684ca9600 100644 --- a/go.sum +++ b/go.sum @@ -2,85 +2,97 @@ 4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY= 4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU= 4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0= -cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI= -cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= -cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= -cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= -cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= -cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= -cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= -cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= -cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= -cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA= -cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY= -cloud.google.com/go/kms v1.20.1 h1:og29Wv59uf2FVaZlesaiDAqHFzHaoUyHI3HYp9VUHVg= -cloud.google.com/go/kms v1.20.1/go.mod h1:LywpNiVCvzYNJWS9JUcGJSVTNSwPwi0vBAotzDqn2nc= -cloud.google.com/go/logging v1.12.0 h1:ex1igYcGFd4S/RZWOCU51StlIEuey5bjqwH9ZYjHibk= -cloud.google.com/go/logging v1.12.0/go.mod h1:wwYBt5HlYP1InnrtYI0wtwttpVU1rifnMT7RejksUAM= -cloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc= -cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= -cloud.google.com/go/monitoring v1.21.2 h1:FChwVtClH19E7pJ+e0xUhJPGksctZNVOk2UhMmblmdU= -cloud.google.com/go/monitoring v1.21.2/go.mod h1:hS3pXvaG8KgWTSz+dAdyzPrGUYmi2Q+WFX8g2hqVEZU= -cloud.google.com/go/storage v1.49.0 h1:zenOPBOWHCnojRd9aJZAyQXBYqkJkdQS42dxL55CIMw= -cloud.google.com/go/storage v1.49.0/go.mod h1:k1eHhhpLvrPjVGfo0mOUPEJ4Y2+a/Hv5PiwehZI9qGU= -cloud.google.com/go/trace v1.11.2 h1:4ZmaBdL8Ng/ajrgKqY5jfvzqMXbrDcBsUGXOT9aqTtI= -cloud.google.com/go/trace v1.11.2/go.mod h1:bn7OwXd4pd5rFuAnTrzBuoZ4ax2XQeG3qNgYmfCy0Io= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= +cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= +cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= +cloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU= +cloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58= +cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA= +cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak= +cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= +cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= +cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= +cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= +cloud.google.com/go/storage v1.57.2 h1:sVlym3cHGYhrp6XZKkKb+92I1V42ks2qKKpB0CF5Mb4= +cloud.google.com/go/storage v1.57.2/go.mod h1:n5ijg4yiRXXpCu0sJTD6k+eMf7GRrJmPyr9YxLXGHOk= +cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= +cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8= code.cloudfoundry.org/clock v1.0.0 h1:kFXWQM4bxYvdBw2X8BbBeXwQNgfoWv1vqAk2ZZyBN2o= code.cloudfoundry.org/clock v1.0.0/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8= code.gitea.io/sdk/gitea v0.18.0 h1:+zZrwVmujIrgobt6wVBWCqITz6bn1aBjnCUHmpZrerI= code.gitea.io/sdk/gitea v0.18.0/go.mod h1:IG9xZJoltDNeDSW0qiF2Vqx5orMWa7OhVWrjvrd5NpI= -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +codeberg.org/chavacava/garif v0.2.0 h1:F0tVjhYbuOCnvNcU3YSpO6b3Waw6Bimy4K0mM8y6MfY= +codeberg.org/chavacava/garif v0.2.0/go.mod h1:P2BPbVbT4QcvLZrORc2T29szK3xEOlnl0GiPTJmEqBQ= +codeberg.org/polyfloyd/go-errorlint v1.9.0 h1:VkdEEmA1VBpH6ecQoMR4LdphVI3fA4RrCh2an7YmodI= +codeberg.org/polyfloyd/go-errorlint v1.9.0/go.mod h1:GPRRu2LzVijNn4YkrZYJfatQIdS+TrcK8rL5Xs24qw8= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +dev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKfwODoI1Y= +dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI= +dev.gaijin.team/go/golib v0.6.0 h1:v6nnznFTs4bppib/NyU1PQxobwDHwCXXl15P7DV5Zgo= +dev.gaijin.team/go/golib v0.6.0/go.mod h1:uY1mShx8Z/aNHWDyAkZTkX+uCi5PdX7KsG1eDQa2AVE= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/4meepo/tagalign v1.4.2 h1:0hcLHPGMjDyM1gHG58cS73aQF8J4TdVR96TZViorO9E= -github.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI= -github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE= -github.com/Abirdcfly/dupword v0.1.3/go.mod h1:8VbB2t7e10KRNdwTVoxdBaxla6avbhGzb8sCTygUMhw= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/4meepo/tagalign v1.4.3 h1:Bnu7jGWwbfpAie2vyl63Zup5KuRv21olsPIha53BJr8= +github.com/4meepo/tagalign v1.4.3/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c= +github.com/Abirdcfly/dupword v0.1.7 h1:2j8sInznrje4I0CMisSL6ipEBkeJUJAmK1/lfoNGWrQ= +github.com/Abirdcfly/dupword v0.1.7/go.mod h1:K0DkBeOebJ4VyOICFdppB23Q0YMOgVafM0zYW0n9lF4= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= -github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 h1:59MxjQVfjXsBpLy+dbd2/ELV5ofnUkUZBvWSC85sheA= -github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU= +github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg= +github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM= +github.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCgpwKq7wo= +github.com/AdminBenni/iota-mixing v1.0.0/go.mod h1:i4+tpAaB+qMVIV9OK3m4/DAynOd5bQFaOu+2AhtBCNY= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= -github.com/Antonboom/errname v1.0.0 h1:oJOOWR07vS1kRusl6YRSlat7HFnb3mSfMl6sDMRoTBA= -github.com/Antonboom/errname v1.0.0/go.mod h1:gMOBFzK/vrTiXN9Oh+HFs+e6Ndl0eTFbtsRTSRdXyGI= -github.com/Antonboom/nilnil v1.0.1 h1:C3Tkm0KUxgfO4Duk3PM+ztPncTFlOf0b2qadmS0s4xs= -github.com/Antonboom/nilnil v1.0.1/go.mod h1:CH7pW2JsRNFgEh8B2UaPZTEPhCMuFowP/e8Udp9Nnb0= -github.com/Antonboom/testifylint v1.5.2 h1:4s3Xhuv5AvdIgbd8wOOEeo0uZG7PbDKQyKY5lGoQazk= -github.com/Antonboom/testifylint v1.5.2/go.mod h1:vxy8VJ0bc6NavlYqjZfmp6EfqXMtBgQ4+mhCojwC1P8= +github.com/AlwxSin/noinlineerr v1.0.5 h1:RUjt63wk1AYWTXtVXbSqemlbVTb23JOSRiNsshj7TbY= +github.com/AlwxSin/noinlineerr v1.0.5/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc= +github.com/Antonboom/errname v1.1.1 h1:bllB7mlIbTVzO9jmSWVWLjxTEbGBVQ1Ff/ClQgtPw9Q= +github.com/Antonboom/errname v1.1.1/go.mod h1:gjhe24xoxXp0ScLtHzjiXp0Exi1RFLKJb0bVBtWKCWQ= +github.com/Antonboom/nilnil v1.1.1 h1:9Mdr6BYd8WHCDngQnNVV0b554xyisFioEKi30sksufQ= +github.com/Antonboom/nilnil v1.1.1/go.mod h1:yCyAmSw3doopbOWhJlVci+HuyNRuHJKIv6V2oYQa8II= +github.com/Antonboom/testifylint v1.6.4 h1:gs9fUEy+egzxkEbq9P4cpcMB6/G0DYdMeiFS87UiqmQ= +github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9qpVy7nXGbxDD4= github.com/Azure/azure-container-networking/zapai v0.0.3 h1:73druF1cnne5Ign/ztiXP99Ss5D+UJ80EL2mzPgNRhk= github.com/Azure/azure-container-networking/zapai v0.0.3/go.mod h1:XV/aKJQAV6KqV4HQtZlDyxg2z7LaY9rsX8dqwyWFmUI= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY= github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 h1:m/sWOGCREuSBqg2htVQTBY8nOZpyajYztF0vUvSZTuM= github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0/go.mod h1:Pu5Zksi2KrU7LPbZbNINx6fuVrUp/ffvpxdDj+i8LeE= github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 h1:FbH3BbSb4bvGluTesZZ+ttN/MDsnMmQP36OSnDuSXqw= github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1/go.mod h1:9V2j0jn9jDEkCkv8w/bKTNppX/d0FVA1ud77xCIP4KA= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0 h1:Hp+EScFOu9HeCbeW8WU2yQPJd4gGwhMgKxWe+G6jNzw= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0/go.mod h1:/pz8dyNQe+Ey3yBp/XuYz7oqX8YDNWVpPB0hH3XWfbc= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v6 v6.4.0 h1:z7Mqz6l0EFH549GvHEqfjKvi+cRScxLWbaoeLm9wxVQ= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v6 v6.4.0/go.mod h1:v6gbfH+7DG7xH2kUNs+ZJ9tF6O3iNnR85wMtmr+F54o= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7 v7.3.0 h1:nyxugFxG2uhbMeJVCFFuD2j9wu+6KgeabITdINraQsE= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7 v7.3.0/go.mod h1:e4RAYykLIz73CF52KhSooo4whZGXvXrD09m0jkgnWiU= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v1.2.0 h1:DWlwvVV5r/Wy1561nZ3wrpI1/vDIBRY/Wd1HWaRBZWA= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v1.2.0/go.mod h1:E7ltexgRDmeJ0fJWv0D/HLwY2xbDdN+uv+X2uZtOx3w= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v4 v4.8.0 h1:0nGmzwBv5ougvzfGPCO2ljFRHvun57KpNrVCMrlk0ns= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v4 v4.8.0/go.mod h1:gYq8wyDgv6JLhGbAU6gg8amCPgQWRE+aCvrV2gyzdfs= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5 v5.0.0 h1:5n7dPVqsWfVKw+ZiEKSd3Kzu7gwBkbEBkeXb8rgaE9Q= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5 v5.0.0/go.mod h1:HcZY0PHPo/7d75p99lB6lK0qYOP4vLRJUBpiehYXtLQ= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v6 v6.5.0 h1:8deM0E7Il/6jxRU9Kgv8kKm3uq3O6Gh6NVNqADa4zbU= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v6 v6.5.0/go.mod h1:PhSVsfd99UdSWx7VAnbHr5i1O4WQ3YkYBFqQpSOx7oA= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v6 v6.6.0 h1:xkWEcbsnJWid3rOf/S/LOHy1I55JA+4kw/f8Tnm+Onc= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v6 v6.6.0/go.mod h1:OWKfCmX4X3Vp2w7GSx1LZn8566tOHJBA6K0IAUVNYx0= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dashboard/armdashboard v1.2.0 h1:MRPU8Bge2f9tkfG3PCr4vEnqXl8XOSjlhuK3l+8Hvkc= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dashboard/armdashboard v1.2.0/go.mod h1:xYrOYxajQvXMlp6M1E3amlaqPDXspyJxmjqTsGo6Jmw= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do= @@ -93,26 +105,30 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanage github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0/go.mod h1:mLfWfj8v3jfWKsL9G4eoBoXVcsqcIUTapmdKy7uGOp0= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/monitor/armmonitor v0.11.0 h1:Ds0KRF8ggpEGg4Vo42oX1cIt/IfOhHWJBikksZbVxeg= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/monitor/armmonitor v0.11.0/go.mod h1:jj6P8ybImR+5topJ+eH6fgcemSFBmU6/6bFF8KkwuDI= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.2.0 h1:z4YeiSXxnUI+PqB46Yj6MZA3nwb1CcJIkEMDrzUd8Cs= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.2.0/go.mod h1:rko9SzMxcMk0NJsNAxALEGaTYyy79bNRwxgJfrH0Spw= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0 h1:L7G3dExHBgUxsO3qpTGhk/P2dgnYyW48yn7AO33Tbek= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0/go.mod h1:Ms6gYEy0+A2knfKrwdatsggTXYA2+ICKug8w7STorFw= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v5 v5.2.0 h1:qBlqTo40ARdI7Pmq+enBiTnejZk2BF+PHgktgG8k3r8= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v5 v5.2.0/go.mod h1:UmyOatRyQodVpp55Jr5WJmnkmVW4wKfo85uHFmMEjfM= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.2.0 h1:HYGD75g0bQ3VO/Omedm54v4LrD3B1cGImuRF3AJ5wLo= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.2.0/go.mod h1:ulHyBFJOI0ONiRL4vcJTmS7rx18jQQlEPmAgo80cRdM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9 v9.0.0 h1:CbHDMVJhcJSmXenq+UDWyIjumzVkZIb5pVUGzsCok5M= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9 v9.0.0/go.mod h1:raqbEXrok4aycS74XoU6p9Hne1dliAFpHLizlp+qJoM= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 h1:yzrctSl9GMIQ5lHu7jc8olOsGjWDCsBpJhWqfGa/YIM= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0/go.mod h1:GE4m0rnnfwLGX0Y9A9A25Zx5N/90jneT5ABevqzhuFQ= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.0 h1:LR0kAX9ykz8G4YgLCaRDVJ3+n43R8MneB5dTy2konZo= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.0/go.mod h1:DWAciXemNf++PQJLeXUB4HHH5OpsAh12HZnu2wXE1jA= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1 h1:mrkDCdkMsD4l9wjFGhofFHFrV43Y3c53RSLKOCJ5+Ow= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1/go.mod h1:hPv41DbqMmnxcGralanA/kVlfdH5jv3T4LxGku2E1BY= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 h1:lhZdRq7TIx0GJQvSyX2Si406vrYsov2FXGp/RnSEtcs= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1/go.mod h1:8cl44BDmi+effbARHMQjgOKA2AYvcohNm7KEt42mSV8= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2 v2.0.0 h1:+vh02EiRx2UmL9NDoA36U18Bgwl9luxs6ia0GAI9Rzg= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2 v2.0.0/go.mod h1:iKOtU3WyuNvNc4L1Z4IxHaoO0dGq5tg+uhLix/KRmzE= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 h1:/g8S6wk65vfC6m3FIxJ+i5QDyN9JWwXI8Hb0Img10hU= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0/go.mod h1:gpl+q95AzZlKVI3xSoseF9QPrypk0hQqBiJYeB/cR/I= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.7.0 h1:BM85pSYlVYQHdq00nxyPoOkyLF5NArJG3bOsrmbwr4k= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.7.0/go.mod h1:QYjP2cB7ZYtS/8jAbE0VSBZde/tjExqGjp+8JY6/+ts= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= @@ -132,8 +148,8 @@ github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSY github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= -github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= -github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= +github.com/Azure/go-autorest/autorest/to v0.4.1 h1:CxNHBqdzTr7rLtdrtb5CMjJcDut+WNGCVv7OmS5+lTc= +github.com/Azure/go-autorest/autorest/to v0.4.1/go.mod h1:EtaofgU4zmtvn1zT2ARsjRFdq9vXx0YWtmElwL+GZ9M= github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= @@ -144,35 +160,31 @@ github.com/Azure/perf-tests/network/benchmarks/netperf v0.0.0-20241008140716-395 github.com/Azure/perf-tests/network/benchmarks/netperf v0.0.0-20241008140716-395a79947d2c/go.mod h1:jeV6A8q9uDVDwffTt5KBk+5g7bXfpEImYW6qLKn0E+I= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= -github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= -github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 h1:RHK7bS+HQMslb1sZpAokUt+zTVmue0hKSs2C791hhzU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= -github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/Crocmagnon/fatcontext v0.7.1 h1:SC/VIbRRZQeQWj/TcQBS6JmrXcfA+BU4OGSVUt54PjM= -github.com/Crocmagnon/fatcontext v0.7.1/go.mod h1:1wMvv3NXEBJucFGfwOJBxSVWcoIO6emV215SMkW9MFU= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM= -github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= -github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 h1:Sz1JIXEcSfhz7fUi7xHnhpIE0thVASYjvosApmHuD2k= -github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1/go.mod h1:n/LSCXNuIYqVfBlVXyHfMQkZDdp1/mmxfSjADd3z1Zg= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 h1:f2Qw/Ehhimh5uO1fayV0QIW7DShEQqhtUfhYc+cBPlw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 h1:UQ0AhxogsIRZDkElkblfnwjc3IaltCm2HUMvezQaL7s= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1/go.mod h1:jyqM3eLpJ3IbIFDTKVz2rF9T/xWGW0rIriGwnz8l9Tk= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.1 h1:oTX4vsorBZo/Zdum6OKPA4o7544hm6smoRv1QjpTwGo= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.1/go.mod h1:0wEl7vrAD8mehJyohS9HZy+WyEOaQO2mJx86Cvh93kM= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 h1:8nn+rsCvTq9axyEh382S0PFLBeaFwNsT43IrPWzctRU= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1/go.mod h1:viRWSEhtMZqz1rhwmOVKkWl6SwmVowfL9O2YR5gI2PE= +github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g= +github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= -github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= @@ -180,147 +192,150 @@ github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA4 github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Microsoft/hcsshim v0.12.9 h1:2zJy5KA+l0loz1HzEGqyNnjd3fyZA31ZBCGKacp6lLg= -github.com/Microsoft/hcsshim v0.12.9/go.mod h1:fJ0gkFAna6ukt0bLdKB8djt4XIJhF/vEPuoIWYVvZ8Y= +github.com/Microsoft/hcsshim v0.14.1 h1:CMuB3fqQVfPdhyXhUqYdUmPUIOhJkmghCx3dJet8Cqs= +github.com/Microsoft/hcsshim v0.14.1/go.mod h1:VnzvPLyWUhxiPVsJ31P6XadxCcTogTguBFDy/1GR/OM= +github.com/MirrexOne/unqueryvet v1.5.4 h1:38QOxShO7JmMWT+eCdDMbcUgGCOeJphVkzzRgyLJgsQ= +github.com/MirrexOne/unqueryvet v1.5.4/go.mod h1:fs9Zq6eh1LRIhsDIsxf9PONVUjYdFHdtkHIgZdJnyPU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4= github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo= -github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= -github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= +github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= github.com/ProtonMail/gopenpgp/v2 v2.7.1 h1:Awsg7MPc2gD3I7IFac2qE3Gdls0lZW8SzrFZ3k1oz0s= github.com/ProtonMail/gopenpgp/v2 v2.7.1/go.mod h1:/BU5gfAVwqyd8EfC3Eu7zmuhwYQpKs+cGD8M//iiaxs= -github.com/Sytten/logrus-zap-hook v0.1.0 h1:GPsDlO0b+rvfb6WohFNreI3Fe2I6MDyv1afoYPE2Kzk= -github.com/Sytten/logrus-zap-hook v0.1.0/go.mod h1:J0ktevklw/xJNpI2FzfTdJssk4P0vq3K2qzwihJ2gWU= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= +github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU= github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E= -github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= -github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= +github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= -github.com/alexkohler/nakedret/v2 v2.0.5 h1:fP5qLgtwbx9EJE8dGEERT02YwS8En4r9nnZ71RK+EVU= -github.com/alexkohler/nakedret/v2 v2.0.5/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU= -github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw= -github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= +github.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQiFSsuzQ= +github.com/alexkohler/nakedret/v2 v2.0.6/go.mod h1:l3RKju/IzOMQHmsEvXwkqMDzHHvurNQfAgE1eVmT40Q= +github.com/alexkohler/prealloc v1.1.0 h1:cKGRBqlXw5iyQGLYhrXrDlcHxugXpTq4tQ5c91wkf8M= +github.com/alexkohler/prealloc v1.1.0/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig= +github.com/alfatraining/structtag v1.0.0 h1:2qmcUqNcCoyVJ0up879K614L9PazjBSFruTB0GOFjCc= +github.com/alfatraining/structtag v1.0.0/go.mod h1:p3Xi5SwzTi+Ryj64DqjLWz7XurHxbGsq6y3ubePJPus= github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= -github.com/alingse/nilnesserr v0.1.2 h1:Yf8Iwm3z2hUUrP4muWfW83DF4nE3r1xZ26fGWUKCZlo= -github.com/alingse/nilnesserr v0.1.2/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg= -github.com/anchore/bubbly v0.0.0-20230518153401-87b6af8ccf22 h1:5NFK6VGgqBUOAX2SYyzFYvNdOiYDxzim8jga386FlZY= -github.com/anchore/bubbly v0.0.0-20230518153401-87b6af8ccf22/go.mod h1:Kv+Mm9CdtnV8iem48iEPIwy7/N4Wmk0hpxYNH5gTwKQ= -github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a h1:nJ2G8zWKASyVClGVgG7sfM5mwoZlZ2zYpIzN2OhjWkw= -github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a/go.mod h1:ubLFmlsv8/DFUQrZwY5syT5/8Er3ugSr4rDFwHsE3hg= -github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU= -github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb/go.mod h1:DmTY2Mfcv38hsHbG78xMiTDdxFtkHpgYNVDPsF2TgHk= -github.com/anchore/quill v0.4.1 h1:mffDnvnER3ZgPjN5hexc3nr/4Y1dtKdDB6td5K8uInk= -github.com/anchore/quill v0.4.1/go.mod h1:t6hOPYDohN8wn2SRWQdNkJBkhmK8s3gzuHzzgcEvzQU= +github.com/alingse/nilnesserr v0.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEWd/w= +github.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg= +github.com/anchore/bubbly v0.0.0-20250717181826-8a411f9d8cbf h1:UY7SQkfVVaeGUpPZrJxqmTc8M0ZSWc5ChiKF6I6aL3I= +github.com/anchore/bubbly v0.0.0-20250717181826-8a411f9d8cbf/go.mod h1:w8Br1ZKk1Nk82YRSh10pcD7LO7avPyFmNnaY1TRPgs0= +github.com/anchore/go-logger v0.0.0-20251106021608-a5b0513fa9a9 h1:k+D5tVys8MC+CxfzXIOGm3bZ0FEkmn8udlKACPm4PPo= +github.com/anchore/go-logger v0.0.0-20251106021608-a5b0513fa9a9/go.mod h1:oFuE8YuTCM+spgMXhePGzk3asS94yO9biUfDzVTFqNw= +github.com/anchore/go-macholibre v0.0.0-20250826193721-3cd206ca93aa h1:KPEP8f3enFJeus3Wo51I+riVuCvlf4OEYl2B4IfycbQ= +github.com/anchore/go-macholibre v0.0.0-20250826193721-3cd206ca93aa/go.mod h1:7YJA6tAfRm4SzIF93b32pR4xnbf8g2nJIeQnp+2vzzI= +github.com/anchore/quill v0.7.1 h1:/K2Yvcjs3ipmP+hjxXmy+3IL/nZ0Tnj8S+OLZvtXRYI= +github.com/anchore/quill v0.7.1/go.mod h1:1ExdNP//bdjTRROE/MJg8MxLoDzIJH5HvsbMW4chpBo= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY= -github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= -github.com/ashanbrown/makezero v1.2.0 h1:/2Lp1bypdmK9wDIq7uWBlDF1iMUpIIS4A+pF6C9IEUU= -github.com/ashanbrown/makezero v1.2.0/go.mod h1:dxlPhHbDMC6N6xICzFBSK+4njQDdK8euNO0qjQMtGY4= +github.com/ashanbrown/forbidigo/v2 v2.3.0 h1:OZZDOchCgsX5gvToVtEBoV2UWbFfI6RKQTir2UZzSxo= +github.com/ashanbrown/forbidigo/v2 v2.3.0/go.mod h1:5p6VmsG5/1xx3E785W9fouMxIOkvY2rRV9nMdWadd6c= +github.com/ashanbrown/makezero/v2 v2.1.0 h1:snuKYMbqosNokUKm+R6/+vOPs8yVAi46La7Ck6QYSaE= +github.com/ashanbrown/makezero/v2 v2.1.0/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY= github.com/atc0005/go-teams-notify/v2 v2.10.0 h1:eQvRIkyESQgBvlUdQ/iPol/lj3QcRyrdEQM3+c/nXhM= github.com/atc0005/go-teams-notify/v2 v2.10.0/go.mod h1:SIeE1UfCcVRYMqP5b+r1ZteHyA/2UAjzWF5COnZ8q0w= -github.com/aws/aws-sdk-go v1.53.0 h1:MMo1x1ggPPxDfHMXJnQudTbGXYlD4UigUAud1DJxPVo= -github.com/aws/aws-sdk-go v1.53.0/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= -github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= -github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14= -github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= -github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= -github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= -github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.9 h1:vXY/Hq1XdxHBIYgBUmug/AbMyIe1AKulPYS2/VE1X70= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.9/go.mod h1:GyJJTZoHVuENM4TeJEl5Ffs4W9m19u+4wKJcDi/GZ4A= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs= -github.com/aws/aws-sdk-go-v2/service/ecr v1.28.0 h1:rdPrcOZmqT2F+yzmKEImrx5XUs7Hpf4V9Rp6E8mhsxQ= -github.com/aws/aws-sdk-go-v2/service/ecr v1.28.0/go.mod h1:if7ybzzjOmDB8pat9FE35AHTY6ZxlYSy3YviSmFZv8c= -github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.23.5 h1:452e/nFuqPvwPg+1OD2CG/v29R9MH8egJSJKh2Qduv8= -github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.23.5/go.mod h1:8pvvNAklmq+hKmqyvFoMRg0bwg9sdGOvdwximmKiKP0= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.1 h1:4nm2G6A4pV9rdlWzGMPv4BNtQp22v1hg3yrtkYpeLl8= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.1/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA= -github.com/aws/aws-sdk-go-v2/service/kms v1.30.0 h1:yS0JkEdV6h9JOo8sy2JSpjX+i7vsKifU8SIeHrqiDhU= -github.com/aws/aws-sdk-go-v2/service/kms v1.30.0/go.mod h1:+I8VUUSVD4p5ISQtzpgSva4I8cJ4SQ4b1dcBcof7O+g= -github.com/aws/aws-sdk-go-v2/service/s3 v1.79.3 h1:BRXS0U76Z8wfF+bnkilA2QwpIch6URlm++yPUt9QPmQ= -github.com/aws/aws-sdk-go-v2/service/s3 v1.79.3/go.mod h1:bNXKFFyaiVvWuR6O16h/I1724+aXe/tAkA9/QS01t5k= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= -github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= -github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= -github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20240514230400-03fa26f5508f h1:Z0kS9pJDQgCg3u2lH6+CdYaFbyQtyukVTiUCG6re0E4= -github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20240514230400-03fa26f5508f/go.mod h1:rAE739ssmE5O5fLuQ2y8uHdmOJaelE5I0Es3SxV0y1A= +github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= +github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= +github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8= +github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD9F4akX5gnN3QZwUB20GGKH/I+Rho= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10/go.mod h1:qqY157uZoqm5OXq/amuaBJyC9hgBCBQnsaWnPe905GY= +github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU= +github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1 h1:1hWFp+52Vq8Fevy/KUhbW/1MEApMz7uitCF/PQXRJpk= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1/go.mod h1:sIec8j802/rCkCKgZV678HFR0s7lhQUYXT77tIvlaa4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE= +github.com/aws/aws-sdk-go-v2/service/ecr v1.45.1 h1:Bwzh202Aq7/MYnAjXA9VawCf6u+hjwMdoYmZ4HYsdf8= +github.com/aws/aws-sdk-go-v2/service/ecr v1.45.1/go.mod h1:xZzWl9AXYa6zsLLH41HBFW8KRKJRIzlGmvSM0mVMIX4= +github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.33.2 h1:XJ/AEFYj9VFPJdF+VFi4SUPEDfz1akHwxxm07JfZJcs= +github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.33.2/go.mod h1:JUBHdhvKbbKmhaHjLsKJAWnQL80T6nURmhB/LEprV+4= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 h1:ieLCO1JxUWuxTZ1cRd0GAaeX7O6cIxnwk7tc1LsQhC4= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15/go.mod h1:e3IzZvQ3kAWNykvE0Tr0RDZCMFInMvhku3qNpcIQXhM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 h1:03xatSQO4+AM1lTAbnRg5OK528EUg744nW7F73U8DKw= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23/go.mod h1:M8l3mwgx5ToK7wot2sBBce/ojzgnPzZXUV445gTSyE8= +github.com/aws/aws-sdk-go-v2/service/kms v1.50.3 h1:s/zDSG/a/Su9aX+v0Ld9cimUCdkr5FWPmBV8owaEbZY= +github.com/aws/aws-sdk-go-v2/service/kms v1.50.3/go.mod h1:/iSgiUor15ZuxFGQSTf3lA2FmKxFsQoc2tADOarQBSw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 h1:etqBTKY581iwLL/H/S2sVgk3C9lAsTJFeXWFDsDcWOU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0/go.mod h1:L2dcoOgS2VSgbPLvpak2NyUPsO1TBN7M45Z4H7DlRc4= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio= +github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI= +github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.10.1 h1:6lMw4/QGLFPvbKQ0eri/9Oh3YX5Nm6BPrUlZR8yuJHg= +github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.10.1/go.mod h1:EVJOSYOVeoD3VFFZ/dWCAzWJp5wZr9lTOCjW8ejAmO0= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w= github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo= -github.com/blacktop/go-dwarf v1.0.9 h1:eT/L7gt0gllvvgnRXY0MFKjNB6+jtOY5DTm2ynVX2dY= -github.com/blacktop/go-dwarf v1.0.9/go.mod h1:4W2FKgSFYcZLDwnR7k+apv5i3nrau4NGl9N6VQ9DSTo= -github.com/blacktop/go-macho v1.1.162 h1:FjM3XAsJTAOGZ1eppRSX9ZBX3Bk11JMTC1amsZAOA5I= -github.com/blacktop/go-macho v1.1.162/go.mod h1:f2X4noFBob4G5bWUrzvPBKDVcFWZgDCM7rIn7ygTID0= +github.com/blacktop/go-dwarf v1.0.14 h1:OjmzfSgg/qAKckn2tWFebcgKgJ7HOqCj7bS+CiE1lrY= +github.com/blacktop/go-dwarf v1.0.14/go.mod h1:4W2FKgSFYcZLDwnR7k+apv5i3nrau4NGl9N6VQ9DSTo= +github.com/blacktop/go-macho v1.1.263 h1:wzcLdip1PD2UwOZ5liKijF4YlYbFFF7XdqjLp3VQ1d0= +github.com/blacktop/go-macho v1.1.263/go.mod h1:Hc5E2Lvt/U1VT+jOxr1O5l/LNFJeMYK4eAmDfazTiGc= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= github.com/bluesky-social/indigo v0.0.0-20240411170459-440932307e0d h1:xxPhzCOpmOntzVe8S6tqsMdFgaB8B4NXSV54lG4B1qk= github.com/bluesky-social/indigo v0.0.0-20240411170459-440932307e0d/go.mod h1:ysMQ0a4RYWjgyvKrl5ME352oHA6QgK900g5sB9XXgPE= -github.com/bombsimon/wsl/v4 v4.5.0 h1:iZRsEvDdyhd2La0FVi5k6tYehpOR/R7qIUjmKk7N74A= -github.com/bombsimon/wsl/v4 v4.5.0/go.mod h1:NOQ3aLF4nD7N5YPXMruR6ZXDOAqLoM0GEpLwTdvmOSc= -github.com/breml/bidichk v0.3.2 h1:xV4flJ9V5xWTqxL+/PMFF6dtJPvZLPsyixAoPe8BGJs= -github.com/breml/bidichk v0.3.2/go.mod h1:VzFLBxuYtT23z5+iVkamXO386OB+/sVwZOpIj6zXGos= -github.com/breml/errchkjson v0.4.0 h1:gftf6uWZMtIa/Is3XJgibewBm2ksAQSY/kABDNFTAdk= -github.com/breml/errchkjson v0.4.0/go.mod h1:AuBOSTHyLSaaAFlWsRSuRBIroCh3eh7ZHh5YeelDIk8= +github.com/bombsimon/wsl/v4 v4.7.0 h1:1Ilm9JBPRczjyUs6hvOPKvd7VL1Q++PL8M0SXBDf+jQ= +github.com/bombsimon/wsl/v4 v4.7.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg= +github.com/bombsimon/wsl/v5 v5.6.0 h1:4z+/sBqC5vUmSp1O0mS+czxwH9+LKXtCWtHH9rZGQL8= +github.com/bombsimon/wsl/v5 v5.6.0/go.mod h1:Uqt2EfrMj2NV8UGoN1f1Y3m0NpUVCsUdrNCdet+8LvU= +github.com/breml/bidichk v0.3.3 h1:WSM67ztRusf1sMoqH6/c4OBCUlRVTKq+CbSeo0R17sE= +github.com/breml/bidichk v0.3.3/go.mod h1:ISbsut8OnjB367j5NseXEGGgO/th206dVa427kR8YTE= +github.com/breml/errchkjson v0.4.1 h1:keFSS8D7A2T0haP9kzZTi7o26r7kE3vymjZNeNDRDwg= +github.com/breml/errchkjson v0.4.1/go.mod h1:a23OvR6Qvcl7DG/Z4o0el6BRAjKnaReoPQFciAl9U3s= github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/butuzov/ireturn v0.3.1 h1:mFgbEI6m+9W8oP/oDdfA34dLisRFCj2G6o/yiI1yZrY= -github.com/butuzov/ireturn v0.3.1/go.mod h1:ZfRp+E7eJLC0NQmk1Nrm1LOrn/gQlOykv+cVPdiXH5M= +github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= +github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/butuzov/ireturn v0.4.0 h1:+s76bF/PfeKEdbG8b54aCocxXmi0wvYdOVsWxVO7n8E= +github.com/butuzov/ireturn v0.4.0/go.mod h1:ghI0FrCmap8pDWZwfPisFD1vEc56VKH4NpQUxDHta70= github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc= github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI= github.com/caarlos0/ctrlc v1.2.0 h1:AtbThhmbeYx1WW3WXdWrd94EHKi+0NPRGS4/4pzrjwk= @@ -341,119 +356,131 @@ github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 h1:BjkPE3785EwP github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5/go.mod h1:jtAfVaU/2cu1+wdSRPWE2c1N2qeAA3K4RH9pYgqwets= github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc= github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8= -github.com/catenacyber/perfsprint v0.8.2 h1:+o9zVmCSVa7M4MvabsWvESEhpsMkhfE7k0sHNGL95yw= -github.com/catenacyber/perfsprint v0.8.2/go.mod h1:q//VWC2fWbcdSLEY1R3l8n0zQCDPdE4IjZwyY1HMunM= +github.com/catenacyber/perfsprint v0.10.1 h1:u7Riei30bk46XsG8nknMhKLXG9BcXz3+3tl/WpKm0PQ= +github.com/catenacyber/perfsprint v0.10.1/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc= github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM= github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc= -github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg= -github.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60= +github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc= +github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das= github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= -github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= -github.com/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4= -github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ= -github.com/charmbracelet/bubbletea v0.22.1 h1:z66q0LWdJNOWEH9zadiAIXp2GN1AWrwNXU8obVY9X24= -github.com/charmbracelet/bubbletea v0.22.1/go.mod h1:8/7hVvbPN6ZZPkczLiB8YpLkLJ0n7DMho5Wvfd2X1C0= +github.com/charithe/durationcheck v0.0.11 h1:g1/EX1eIiKS57NTWsYtHDZ/APfeXKhye1DidBcABctk= +github.com/charithe/durationcheck v0.0.11/go.mod h1:x5iZaixRNl8ctbM+3B2RrPG5t856TxRyVQEnbIEM2X4= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= +github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= github.com/charmbracelet/keygen v0.5.0 h1:XY0fsoYiCSM9axkrU+2ziE6u6YjJulo/b9Dghnw6MZc= github.com/charmbracelet/keygen v0.5.0/go.mod h1:DfvCgLHxZ9rJxdK0DGw3C/LkV4SgdGbnliHcObV3L+8= -github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= -github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI= +github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI= +github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4= +github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA= github.com/charmbracelet/x/exp/ordered v0.0.0-20231010190216-1cb11efc897d h1:+o+e/8hf7cG0SbAzEAm/usJ8qoZPgFXhudLjop+TM0g= github.com/charmbracelet/x/exp/ordered v0.0.0-20231010190216-1cb11efc897d/go.mod h1:aoG4bThKYIOnyB55r202eHqo6TkN7ZXV+cu4Do3eoBQ= -github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc= -github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 h1:krfRl01rzPzxSxyLyrChD+U+MzsBXbm0OwYYB67uF+4= github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589/go.mod h1:OuDyvmLnMCwa2ep4Jkm6nyA0ocJuZlGyk2gGseVzERM= -github.com/cilium/cilium v1.18.0-pre.1 h1:ZE+fNsr6QAVcKsvnO1WmMQF4Jo/aztYF1IGy77kb6i0= -github.com/cilium/cilium v1.18.0-pre.1/go.mod h1:xNfreZ/3t6IxynTSSqJRr8hZd2dhX0dns+ae/WDYwWk= +github.com/cilium/cilium v1.19.3 h1:foJrHPk45HwshOd8Qf/kptf9JxPfNySkIDKsetZa9+Y= +github.com/cilium/cilium v1.19.3/go.mod h1:cd4P5LHAg4hyyZexrM4D055t5JwyudeAcZ/Jub9VxJY= github.com/cilium/dns v1.1.51-0.20240603182237-af788769786a h1:PRGN7B+72mj3OtLL2DM3F/9jp+ItgqgNS7mecgCmwsQ= github.com/cilium/dns v1.1.51-0.20240603182237-af788769786a/go.mod h1:/7LC2GOgyXJ7maupZlaVIumYQiGPIgllSf6mA9sg6RU= github.com/cilium/ebpf v0.5.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= github.com/cilium/ebpf v0.8.1/go.mod h1:f5zLIM0FSNuAkSyLAN7X+Hy6yznlF1mNiWUMfxMtrgk= -github.com/cilium/ebpf v0.18.0 h1:OsSwqS4y+gQHxaKgg2U/+Fev834kdnsQbtzRnbVC6Gs= -github.com/cilium/ebpf v0.18.0/go.mod h1:vmsAT73y4lW2b4peE+qcOqw6MxvWQdC+LiU5gd/xyo4= +github.com/cilium/ebpf v0.21.0 h1:4dpx1J/B/1apeTmWBH5BkVLayHTkFrMovVPnHEk+l3k= +github.com/cilium/ebpf v0.21.0/go.mod h1:1kHKv6Kvh5a6TePP5vvvoMa1bclRyzUXELSs272fmIQ= github.com/cilium/fake v0.7.0 h1:4EKBtTweQrJoD4q45qDGu8udulmYMo48Y0BhEbrB1jc= github.com/cilium/fake v0.7.0/go.mod h1:hA1YsEjgIs5Gdeq/DVrDWGuhLCoVok7THTvQaGDO5bc= -github.com/cilium/hive v0.0.0-20250328192914-7f3c86c9c25e h1:IbUxYFQHOMrcCjTlW2s4ir5tf4vqiYIMHCoV81DYIiQ= -github.com/cilium/hive v0.0.0-20250328192914-7f3c86c9c25e/go.mod h1:pI2GJ1n3SLKIQVFrKF7W6A6gb6BQkZ+3Hp4PAEo5SuI= +github.com/cilium/hive v0.0.0-20260108104938-97756f6ff54c h1:mP/Z+oVplgbg3oV1lwsAC86NPLWioN/TqlmZ6+BI2I0= +github.com/cilium/hive v0.0.0-20260108104938-97756f6ff54c/go.mod h1:4/8FBMcTjVdkrNNWaB7t3QqaU4kZDJLJ1leKVP9GjEI= github.com/cilium/lumberjack/v2 v2.4.1 h1:tU92KFJmLQ4Uls5vTgok5b5RbfxpawRia7L14y2qDBs= github.com/cilium/lumberjack/v2 v2.4.1/go.mod h1:yfbtPGmg4i//5oEqzaMxDqSWqgfZFmMoV70Mc2k6v0A= -github.com/cilium/proxy v0.0.0-20250318065604-173988fc0adb h1:pV58137rRui6vUJT2HM2h0HcPfaAh+6BVH42oKXytH8= -github.com/cilium/proxy v0.0.0-20250318065604-173988fc0adb/go.mod h1:ZCm83Mm0aE17bm2vnYY3kS40VIT7ymoqZhNc8YXK1us= -github.com/cilium/statedb v0.3.7 h1:htzjXktKe37FNLGDQjM899G8BK4Pcp+GM1cFQVV3HGA= -github.com/cilium/statedb v0.3.7/go.mod h1:n2lNVxi8vz5Up1Y1rRD++aQP2izQA932fUwTkedKSV0= -github.com/cilium/stream v0.0.0-20241203114243-53c3e5d79744 h1:f+CgYUy2YyZ2EX31QSqf3vwFiJJQSAMIQLn4d3QQYno= -github.com/cilium/stream v0.0.0-20241203114243-53c3e5d79744/go.mod h1:/e83AwqvNKpyg4n3C41qmnmj1x2G9DwzI+jb7GkF4lI= -github.com/cilium/workerpool v1.3.0 h1:7BhHxoqNtpqtmce6MxZdgWODze4lYHbWkEUQ+3xEu8M= -github.com/cilium/workerpool v1.3.0/go.mod h1:0evs6P39nORTphjRtTtHLXTyCPQUwelXCK4wBJmVP7g= -github.com/ckaznocha/intrange v0.3.0 h1:VqnxtK32pxgkhJgYQEeOArVidIPg+ahLP7WBOXZd5ZY= -github.com/ckaznocha/intrange v0.3.0/go.mod h1:+I/o2d2A1FBHgGELbGxzIcyd3/9l9DuwjM8FsbSS3Lo= +github.com/cilium/proxy v0.0.0-20250623105955-2136f59a4ea1 h1:SOOtIfQmW/pF1iW1I4hVUx1pvgX7Xh2E8jHv+itBXQ0= +github.com/cilium/proxy v0.0.0-20250623105955-2136f59a4ea1/go.mod h1:Kwyyx+cC2H67Aj1sDuqBLvPn6TEmEJRPvULIrJ/kBRo= +github.com/cilium/statedb v0.5.6 h1:qid1eoMlWAYi02qMHL9Ewdhtd+FCoTF6nGUrcBB36to= +github.com/cilium/statedb v0.5.6/go.mod h1:utZbqAU8l3X/2zmbBwoYC2KuRTstuSqo+c4cw4jXsCM= +github.com/cilium/stream v0.0.1 h1:82zuM/WwkLiac2Jg5FrzPxZHvIBbxXTi4VY7M+EYLs0= +github.com/cilium/stream v0.0.1/go.mod h1:/e83AwqvNKpyg4n3C41qmnmj1x2G9DwzI+jb7GkF4lI= +github.com/cilium/workerpool v1.4.0 h1:Tn0e2Ie1THjepOnj5PobkhZ3CLknostEcKRvEGvbY/Y= +github.com/cilium/workerpool v1.4.0/go.mod h1:0evs6P39nORTphjRtTtHLXTyCPQUwelXCK4wBJmVP7g= +github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs= +github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI= -github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= +github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo= +github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= +github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/cloudflare/cfssl v1.6.5 h1:46zpNkm6dlNkMZH/wMW22ejih6gIaJbzL2du6vD7ZeI= +github.com/cloudflare/cfssl v1.6.5/go.mod h1:Bk1si7sq8h2+yVEDrFJiz3d7Aw+pfjjJSZVaD+Taky4= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= -github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= -github.com/containerd/cgroups/v3 v3.0.3 h1:S5ByHZ/h9PMe5IOQoN7E+nMc2UcLEM/V48DGDJ9kip0= -github.com/containerd/cgroups/v3 v3.0.3/go.mod h1:8HBe7V3aWGLFPd/k03swSIsGjZhHI2WzJmticMgVuz0= -github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= -github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= -github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/containerd/containerd v1.7.27 h1:yFyEyojddO3MIGVER2xJLWoCIn+Up4GaHFquP7hsFII= -github.com/containerd/containerd v1.7.27/go.mod h1:xZmPnl75Vc+BLGt4MIfu6bp+fy03gdHAn9bz+FreFR0= -github.com/containerd/containerd/api v1.8.0 h1:hVTNJKR8fMc/2Tiw60ZRijntNMd1U+JVMyTRdsD2bS0= -github.com/containerd/containerd/api v1.8.0/go.mod h1:dFv4lt6S20wTu/hMcP4350RL87qPWLVa/OHOwmmdnYc= -github.com/containerd/continuity v0.4.4 h1:/fNVfTJ7wIl/YPMHjf+5H32uFhl63JucB34PlCpMKII= -github.com/containerd/continuity v0.4.4/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= -github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= -github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= +github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= +github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= +github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= +github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= +github.com/containerd/containerd v1.7.32 h1:S54xuVcPxeLaYgaRABtpJ2VyVUVsy0IGf7qHBs+sbY8= +github.com/containerd/containerd v1.7.32/go.mod h1:jdwD6s/BhV4XVJGrvtziNPVA+83n66TwptVaPKprq4E= +github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= +github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= -github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= -github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= -github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= -github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= -github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ= -github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= -github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso= -github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g= -github.com/containernetworking/cni v1.2.3 h1:hhOcjNVUQTnzdRJ6alC5XF+wd9mfGIUaj8FuJbEslXM= -github.com/containernetworking/cni v1.2.3/go.mod h1:DuLgF+aPd3DzcTQTtp/Nvl1Kim23oFKdm2okJzBQA5M= +github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E= +github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= +github.com/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8= +github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q= +github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= +github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= +github.com/containernetworking/cni v1.3.0 h1:v6EpN8RznAZj9765HhXQrtXgX+ECGebEYEmnuFjskwo= +github.com/containernetworking/cni v1.3.0/go.mod h1:Bs8glZjjFfGPHMw6hQu82RUgEPNGEaBb9KS5KtNMnJ4= +github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= +github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs= github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88= -github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= -github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= -github.com/daixiang0/gci v0.13.5 h1:kThgmH1yBmZSBCh1EJVxQ7JsHpm5Oms0AMed/0LaH4c= -github.com/daixiang0/gci v0.13.5/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/daixiang0/gci v0.13.7 h1:+0bG5eK9vlI08J+J/NWGbWPTNiXPG4WhNLJOkSxWITQ= +github.com/daixiang0/gci v0.13.7/go.mod h1:812WVN6JLFY9S6Tv76twqmNqevN0pa3SX3nih0brVzQ= +github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= +github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= +github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY= +github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= +github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= +github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -470,57 +497,61 @@ github.com/dghubble/sling v1.4.0 h1:/n8MRosVTthvMbwlNZgLx579OGVjUOy3GNEv5BIqAWY= github.com/dghubble/sling v1.4.0/go.mod h1:0r40aNsU9EdDUVBNhfCstAtFgutjgJGYbO1oNzkMoM8= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= -github.com/distribution/distribution/v3 v3.0.0-alpha.1 h1:jn7I1gvjOvmLztH1+1cLiUFud7aeJCIQcgzugtwjyJo= -github.com/distribution/distribution/v3 v3.0.0-alpha.1/go.mod h1:LCp4JZp1ZalYg0W/TN05jarCQu+h4w7xc7ZfQF4Y/cY= +github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM= +github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= -github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= - -github.com/docker/cli v26.0.0+incompatible h1:90BKrx1a1HKYpSnnBFR6AgDq/FqkHxwlUyzJVPxD30I= -github.com/docker/cli v26.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg= +github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v28.0.4+incompatible h1:JNNkBctYKurkw6FrHfKqY0nKIDf5nrbxjVBtS+cdcok= -github.com/docker/docker v28.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo= -github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= +github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= -github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/elazarl/goproxy v1.2.1 h1:njjgvO6cRG9rIqN2ebkqy6cQz2Njkx7Fsfv/zIZqgug= -github.com/elazarl/goproxy v1.2.1/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/elliotchance/orderedmap/v2 v2.2.0 h1:7/2iwO98kYT4XkOjA9mBEIwvi4KpGB4cyHeOFOnj4Vk= github.com/elliotchance/orderedmap/v2 v2.2.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= -github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= -github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= -github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= -github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= @@ -532,20 +563,20 @@ github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSY github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA= github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/firefart/nonamedreturns v1.0.5 h1:tM+Me2ZaXs8tfdDw3X6DOX++wMCOqzYUho6tUTYIdRA= -github.com/firefart/nonamedreturns v1.0.5/go.mod h1:gHJjDqhGM4WyPt639SOZs+G89Ko7QKH5R5BhnO6xJhw= -github.com/florianl/go-tc v0.4.5 h1:8lvecARs3c/vGee46j0ro8kco98ga9XjwWvXGwlzrXA= -github.com/florianl/go-tc v0.4.5/go.mod h1:uvp6pIlOw7Z8hhfnT5M4+V1hHVgZWRZwwMS8Z0JsRxc= -github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= -github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +github.com/firefart/nonamedreturns v1.0.6 h1:vmiBcKV/3EqKY3ZiPxCINmpS431OcE1S47AQUwhrg8E= +github.com/firefart/nonamedreturns v1.0.6/go.mod h1:R8NisJnSIpvPWheCq0mNRXJok6D8h7fagJTF8EMEwCo= +github.com/florianl/go-tc v0.4.8 h1:hgmakUX1Nm0Ba1I0ZkbUl9CH6HbRwqSiwipnpmYp3Es= +github.com/florianl/go-tc v0.4.8/go.mod h1:B8GeOEnmrbOnxZtaCvsYJcgIzzmM8c/AIhtfCZsDj3Q= +github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= +github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -553,92 +584,121 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= -github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/ghostiam/protogetter v0.3.9 h1:j+zlLLWzqLay22Cz/aYwTHKQ88GE2DQ6GkWSYFOI4lQ= -github.com/ghostiam/protogetter v0.3.9/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0= +github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI= github.com/github/smimesign v0.2.0 h1:Hho4YcX5N1I9XNqhq0fNx0Sts8MhLonHd+HRXVGNjvk= github.com/github/smimesign v0.2.0/go.mod h1:iZiiwNT4HbtGRVqCQu7uJPEZCuEE5sfSSttcnePkDl4= +github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= +github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= -github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= -github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= -github.com/go-critic/go-critic v0.12.0 h1:iLosHZuye812wnkEz1Xu3aBwn5ocCPfc9yqmFG9pa6w= -github.com/go-critic/go-critic v0.12.0/go.mod h1:DpE0P6OVc6JzVYzmM5gq5jMU31zLr4am5mB/VfFK64w= +github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog= +github.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-faker/faker/v4 v4.6.0 h1:6aOPzNptRiDwD14HuAnEtlTa+D1IfFuEHO8+vEFwjTs= -github.com/go-faker/faker/v4 v4.6.0/go.mod h1:ZmrHuVtTTm2Em9e0Du6CJ9CADaLEzGXW62z1YqFH0m0= +github.com/go-faker/faker/v4 v4.7.0 h1:VboC02cXHl/NuQh5lM2W8b87yp4iFXIu59x4w0RZi4E= +github.com/go-faker/faker/v4 v4.7.0/go.mod h1:u1dIRP5neLB6kTzgyVjdBOV5R1uP7BdxkcWk7tiKQXk= github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= -github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= +github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA= +github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.13.0 h1:vLn5wlGIh/X78El6r3Jr+30W16Blk0CTcxTYcYPWi5E= -github.com/go-git/go-git/v5 v5.13.0/go.mod h1:Wjo7/JyVKtQgUNdXYXIepzWfJQkUEIGvkvVkiXRR/zw= +github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= +github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= -github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-logr/zapr v1.2.0/go.mod h1:Qa4Bsj2Vb+FAVeAKsLD8RLQ+YRJB8YDmOAKxaBQf7Ro= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= -github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= -github.com/go-openapi/errors v0.22.1 h1:kslMRRnK7NCb/CvR1q1VWuEQCEIsBGn5GgKD9e+HYhU= -github.com/go-openapi/errors v0.22.1/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0= -github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= -github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= -github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= -github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ= -github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc= -github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= -github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= -github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= -github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= -github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= +github.com/go-openapi/analysis v0.24.3 h1:a1hrvMr8X0Xt69KP5uVTu5jH62DscmDifrLzNglAayk= +github.com/go-openapi/analysis v0.24.3/go.mod h1:Nc+dWJ/FxZbhSow5Yh3ozg5CLJioB+XXT6MdLvJUsUw= +github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= +github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= +github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ= +github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA= +github.com/go-openapi/runtime v0.29.3 h1:h5twGaEqxtQg40ePiYm9vFFH1q06Czd7Ot6ufdK0w/Y= +github.com/go-openapi/runtime v0.29.3/go.mod h1:8A1W0/L5eyNJvKciqZtvIVQvYO66NlB7INMSZ9bw/oI= +github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= +github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= +github.com/go-openapi/strfmt v0.26.1 h1:7zGCHji7zSYDC2tCXIusoxYQz/48jAf2q+sF6wXTG+c= +github.com/go-openapi/strfmt v0.26.1/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y= +github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU= +github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA= +github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c= +github.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= +github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= +github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk= +github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= +github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo= +github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= +github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= +github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw= +github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY= +github.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU= +github.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14= +github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= +github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= +github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= +github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= +github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= +github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.1 h1:NZOrZmIb6PTv5LTFxr5/mKV/FjbUzGE7E6gLz7vFoOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.1/go.mod h1:r7dwsujEHawapMsxA69i+XMGZrQ5tRauhLAjV/sxg3Q= +github.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn8hE6Iw= +github.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0= +github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY= github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s= github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= github.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSrdV7Nv3/gkvc= github.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk= -github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= +github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= -github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= -github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8= github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU= github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s= @@ -658,8 +718,8 @@ github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQi github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY= github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= @@ -667,15 +727,17 @@ github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4 github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= -github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= -github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8= +github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= +github.com/godoc-lint/godoc-lint v0.11.2 h1:Bp0FkJWoSdNsBikdNgIcgtaoo+xz6I/Y9s5WSBQUeeM= +github.com/godoc-lint/godoc-lint v0.11.2/go.mod h1:iVpGdL1JCikNH2gGeAn3Hh+AgN5Gx/I/cxV+91L41jo= +github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= +github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= @@ -683,17 +745,15 @@ github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= -github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -702,38 +762,43 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0= +github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ= github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 h1:WUvBfQL6EW/40l6OmeSBYQJNSif4O11+bmWEz+C7FYw= github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E= -github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUPPyAKJuzv8pEJU= -github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s= +github.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarogrvjO9AfiW3B4U= +github.com/golangci/go-printf-func-name v0.1.1/go.mod h1:Es64MpWEZbh0UBtTAICOZiB+miW53w/K9Or/4QogJss= github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE= github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY= -github.com/golangci/golangci-lint v1.64.7 h1:Xk1EyxoXqZabn5b4vnjNKSjCx1whBK53NP+mzLfX7HA= -github.com/golangci/golangci-lint v1.64.7/go.mod h1:5cEsUQBSr6zi8XI8OjmcY2Xmliqc4iYL7YoPrL+zLJ4= -github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs= -github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo= -github.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c= -github.com/golangci/plugin-module-register v0.1.1/go.mod h1:TTpqoB6KkwOJMV8u7+NyXMrkwwESJLOkfl9TxR1DGFc= +github.com/golangci/golangci-lint/v2 v2.11.4 h1:GK+UlZBN5y7rh2PBnHA93XLSX6RaF7uhzJQ3JwU1wuA= +github.com/golangci/golangci-lint/v2 v2.11.4/go.mod h1:ODQDCASMA3VqfZYIbbQLpTRTzV7O/vjmIRF6u8NyFwI= +github.com/golangci/golines v0.15.0 h1:Qnph25g8Y1c5fdo1X7GaRDGgnMHgnxh4Gk4VfPTtRx0= +github.com/golangci/golines v0.15.0/go.mod h1:AZjXd23tbHMpowhtnGlj9KCNsysj72aeZVVHnVcZx10= +github.com/golangci/misspell v0.8.0 h1:qvxQhiE2/5z+BVRo1kwYA8yGz+lOlu5Jfvtx2b04Jbg= +github.com/golangci/misspell v0.8.0/go.mod h1:WZyyI2P3hxPY2UVHs3cS8YcllAeyfquQcKfdeE9AFVg= +github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg= +github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw= github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s= github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= -github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed h1:IURFTjxeTfNFP0hTEi1YKjB/ub8zkpaOqFFMApi2EAs= -github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed/go.mod h1:XLXN8bNw4CGRPaqgl3bv/lhz7bsGPh4/xSaMTbo2vkQ= +github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2bRA5htgAG9r7s3tHsfjIhN98WshBTJ9jM= +github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s= +github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM= +github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e/go.mod h1:h+wZwLjUTJnm/P2rwlbJdRPZXOzaT36/FwnPnY2inzc= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.24.1 h1:jsBCtxG8mM5wiUJDSGUqU0K7Mtr3w7Eyv00rw4DiZxI= -github.com/google/cel-go v0.24.1/go.mod h1:Hdf9TqOaTNSFQA1ybQaRqATVoK7m/zcf7IMhGXP5zI8= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= +github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A= +github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 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.1/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.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -745,14 +810,14 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-containerregistry v0.20.1 h1:eTgx9QNYugV4DN5mz4U8hiAGTi1ybXn0TPi4Smd8du0= -github.com/google/go-containerregistry v0.20.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= +github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I= +github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM= github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4= github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/go-replayers/grpcreplay v1.1.0 h1:S5+I3zYyZ+GQz68OfbURDdt/+cSMqCK1wrvNx7WBzTE= -github.com/google/go-replayers/grpcreplay v1.1.0/go.mod h1:qzAvJ8/wi57zq7gWqaE6AwLM6miiXUQwP1S+I9icmhk= +github.com/google/go-replayers/grpcreplay v1.3.0 h1:1Keyy0m1sIpqstQmgz307zhiJ1pV4uIlFds5weTmxbo= +github.com/google/go-replayers/grpcreplay v1.3.0/go.mod h1:v6NgKtkijC0d3e3RW8il6Sy5sqRVUwoQa4mHOGEy8DI= github.com/google/go-replayers/httpreplay v1.2.0 h1:VM1wEyyjaoU53BwrOnaf9VhAyQQEEioJvFYxYcLRKzk= github.com/google/go-replayers/httpreplay v1.2.0/go.mod h1:WahEFFZZ7a1P4VM1qEeHy+tME4bwyqPcwWbNlUI1Mcg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -766,37 +831,38 @@ github.com/google/ko v0.15.4 h1:0blRbIdPmSy6v4LvedGxbI/8krdJYQgbSih3v6Y8V1c= github.com/google/ko v0.15.4/go.mod h1:ZkcmfV91Xt6ZzOBHc/cXXGYnqWdNWDVy/gHoUU9sjag= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= -github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4= -github.com/google/rpmpack v0.6.1-0.20240329070804-c2247cbb881a h1:JJBdjSfqSy3mnDT0940ASQFghwcZ4y4cb6ttjAoXqwE= -github.com/google/rpmpack v0.6.1-0.20240329070804-c2247cbb881a/go.mod h1:uqVAUVQLq8UY2hCDfmJ/+rtO3aw7qyhc90rCVEabEfI= +github.com/google/renameio/v2 v2.0.2 h1:qKZs+tfn+arruZZhQ7TKC/ergJunuJicWS6gLDt/dGw= +github.com/google/renameio/v2 v2.0.2/go.mod h1:OX+G6WHHpHq3NVj7cAOleLOwJfcQ1s3uUJQCrr78SWo= +github.com/google/rpmpack v0.7.1 h1:YdWh1IpzOjBz60Wvdw0TU0A5NWP+JTVHA5poDqwMO2o= +github.com/google/rpmpack v0.7.1/go.mod h1:h1JL16sUTWCLI/c39ox1rDaTBo3BXUQGjczVJyK4toU= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 h1:SJ+NtwL6QaZ21U+IrK7d0gGgpjGGvd2kz+FzTHVzdqI= github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2/go.mod h1:Tv1PlzqC9t8wNnpPdctvtSUOPUUg4SHeE6vR1Ir2hmg= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/trillian v1.7.2 h1:EPBxc4YWY4Ak8tcuhyFleY+zYlbCDCa4Sn24e1Ka8Js= +github.com/google/trillian v1.7.2/go.mod h1:mfQJW4qRH6/ilABtPYNBerVJAJ/upxHLX81zxNQw05s= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI= -github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= -github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= -github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= -github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= -github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4= +github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18= +github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE= +github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA= github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= -github.com/gopacket/gopacket v1.3.1 h1:ZppWyLrOJNZPe5XkdjLbtuTkfQoxQ0xyMJzQCqtqaPU= -github.com/gopacket/gopacket v1.3.1/go.mod h1:3I13qcqSpB2R9fFQg866OOgzylYkZxLTmkvcXhvf6qg= +github.com/gopacket/gopacket v1.6.0 h1:+DdqJ4EE1C4Jx2VMDUcKvsTGc4qki2LSs0ws51RgU3Y= +github.com/gopacket/gopacket v1.6.0/go.mod h1:i3NaGaqfoWKAr1+g7qxEdWsmfT+MXuWkAe9+THv8LME= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= -github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s= -github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= +github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs= +github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw= github.com/goreleaser/chglog v0.6.1 h1:NZKiX8l0FTQPRzBgKST7knvNZmZ04f7PEGkN2wInfhE= github.com/goreleaser/chglog v0.6.1/go.mod h1:Bnnfo07jMZkaAb0uRNASMZyOsX6ROW6X1qbXqN3guUo= github.com/goreleaser/fileglob v1.3.0 h1:/X6J7U8lbDpQtBvGcwwPS6OpzkNVlVEsFUVRx9+k+7I= @@ -805,23 +871,21 @@ github.com/goreleaser/goreleaser v1.26.2 h1:1iY1HaXtRiMTrwy6KE1sNjkRjsjMi+9l0k6W github.com/goreleaser/goreleaser v1.26.2/go.mod h1:mHi6zr6fuuOh5eHdWWgyo/N8BWED5WEVtb/4GETc9jQ= github.com/goreleaser/nfpm/v2 v2.37.1 h1:RUmeEt8OlEVeSzKRrO5Vl5qVWCtUwx4j9uivGuRo5fw= github.com/goreleaser/nfpm/v2 v2.37.1/go.mod h1:q8+sZXFqn106/eGw+9V+I8+izFxZ/sJjrhwmEUxXhUg= -github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= -github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= -github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado= github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= github.com/gostaticanalysis/comment v1.5.0 h1:X82FLl+TswsUMpMh17srGRuKaaXprTaytmEpgnKIDu8= github.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc= github.com/gostaticanalysis/forcetypeassert v0.2.0 h1:uSnWrrUEYDr86OCxWa4/Tp2jeYDlogZiZHzGkWFefTk= github.com/gostaticanalysis/forcetypeassert v0.2.0/go.mod h1:M5iPavzE9pPqWyeiVXSFghQjljW1+l/Uke3PXHS6ILY= -github.com/gostaticanalysis/nilerr v0.1.1 h1:ThE+hJP0fEp4zWLkWHWcRyI2Od0p7DlgYG3Uqrmrcpk= -github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A= +github.com/gostaticanalysis/nilerr v0.1.2 h1:S6nk8a9N8g062nsx63kUkF6AzbHGw7zzyHMcpu52xQU= +github.com/gostaticanalysis/nilerr v0.1.2/go.mod h1:A19UHhoY3y8ahoL7YKz6sdjDtduwTSI4CsymaC2htPA= github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8= github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs= @@ -829,12 +893,12 @@ github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -847,30 +911,44 @@ github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1T github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= -github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= -github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= +github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/arc/v2 v2.0.6 h1:4NU7uP5vSoK6TbaMj3NtY478TTAWLso/vL1gpNrInHg= github.com/hashicorp/golang-lru/arc/v2 v2.0.6/go.mod h1:cfdDIX05DWvYV6/shsxDfa/OVcRieOt+q4FnM8x+Xno= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= +github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0= +github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM= +github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/in-toto/attestation v1.1.2 h1:MBFn6lsMq6dptQZJBhalXTcWMb/aJy3V+GX3VYj/V1E= +github.com/in-toto/attestation v1.1.2/go.mod h1:gYFddHMZj3DiQ0b62ltNi1Vj5rC879bTmBbrv9CRHpM= +github.com/in-toto/in-toto-golang v0.11.0 h1:nfidMYBFx+E0lnmX5KUnN2Pdm8zdNKal1ayjJuzzRoA= +github.com/in-toto/in-toto-golang v0.11.0/go.mod h1:u3PjTnwFKjp5a1YCcw8SJg0G+tMeKfVoWsWeFMDCMtw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/inspektor-gadget/inspektor-gadget v0.27.0 h1:f28khEROUBkKVShlyXOT3sN8T68WZLn6AqRp0aZmUzM= -github.com/inspektor-gadget/inspektor-gadget v0.27.0/go.mod h1:26ArCiACp1/3lfF6RymrznxBDfuPXYgvyk0dJEhFMVI= -github.com/inspektor-gadget/netns v0.0.5-0.20230524185006-155d84c555d6 h1:fQqkJ+WkYfzy6BoUh32fr9uYrXfOGtsfw0skMQkfOic= -github.com/inspektor-gadget/netns v0.0.5-0.20230524185006-155d84c555d6/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= @@ -900,6 +978,14 @@ github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg= github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= +github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= @@ -907,28 +993,28 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= -github.com/jellydator/ttlcache/v3 v3.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc= -github.com/jellydator/ttlcache/v3 v3.3.0/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw= -github.com/jgautheron/goconst v1.7.1 h1:VpdAG7Ca7yvvJk5n8dMwQhfEZJh95kl/Hl9S1OI5Jkk= -github.com/jgautheron/goconst v1.7.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= +github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 h1:TMtDYDHKYY15rFihtRfck/bfFqNfvcabqvXAFQfAUpY= +github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267/go.mod h1:h1nSAbGFqGVzn6Jyl1R/iCcBUHN4g+gW1u9CoBTrb9E= +github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY= +github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= +github.com/jgautheron/goconst v1.8.2 h1:y0XF7X8CikZ93fSNT6WBTb/NElBu9IjaY7CCYQrCMX4= +github.com/jgautheron/goconst v1.8.2/go.mod h1:A0oxgBCHy55NQn6sYpO7UdnA9p+h7cPtoOZUmvNIako= github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= -github.com/jjti/go-spancheck v0.6.4 h1:Tl7gQpYf4/TMU7AT84MN83/6PutY21Nb9fuQjFTpRRc= -github.com/jjti/go-spancheck v0.6.4/go.mod h1:yAEYdKJ2lRkDA8g7X+oKUHXOWVAXSBJRv04OhF+QUjk= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8= +github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs= -github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= +github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= @@ -941,38 +1027,32 @@ github.com/jsimonetti/rtnetlink v0.0.0-20211022192332-93da33804786 h1:N527AHMa79 github.com/jsimonetti/rtnetlink v0.0.0-20211022192332-93da33804786/go.mod h1:v4hqbTdfQngbVSZJVWUhGE/lbTFf9jb+ygmNUDQMuOs= github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM= github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jsternberg/zap-logfmt v1.3.0 h1:z1n1AOHVVydOOVuyphbOKyR4NICDQFiJMn1IK5hVQ5Y= github.com/jsternberg/zap-logfmt v1.3.0/go.mod h1:N3DENp9WNmCZxvkBD/eReWwz1149BK6jEN9cQ4fNwZE= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ= github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY= -github.com/karamaru-alpha/copyloopvar v1.2.1 h1:wmZaZYIjnJ0b5UoKDjUHrikcV0zuPyyxI4SVplLd2CI= -github.com/karamaru-alpha/copyloopvar v1.2.1/go.mod h1:nFmMlFNlClC2BPvNaHMdkirmTJxVCY0lhxBtlfOypMM= -github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= -github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0= +github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY= +github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= +github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/errcheck v1.9.0 h1:9xt1zI9EBfcYBvdU1nVrzMzzUPUtPKs9bVSIM3TAb3M= -github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8= +github.com/kisielk/errcheck v1.10.0 h1:Lvs/YAHP24YKg08LA8oDw2z9fJVme090RAXd90S+rrw= +github.com/kisielk/errcheck v1.10.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE= github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= @@ -982,10 +1062,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 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/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs= -github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I= -github.com/kunwardeep/paralleltest v1.0.10 h1:wrodoaKYzS2mdNVnc4/w31YaXFtsc21PCTdvWJ/lDDs= -github.com/kunwardeep/paralleltest v1.0.10/go.mod h1:2C7s65hONVqY7Q5Efj5aLzRCNLjw2h4eMc9EcypGjcY= +github.com/kulti/thelper v0.7.1 h1:fI8QITAoFVLx+y+vSyuLBP+rcVIB8jKooNSCT2EiI98= +github.com/kulti/thelper v0.7.1/go.mod h1:NsMjfQEy6sd+9Kfw8kCP61W1I0nerGSYSFnGaxQkcbs= +github.com/kunwardeep/paralleltest v1.0.15 h1:ZMk4Qt306tHIgKISHWFJAO1IDQJLc6uDyJMLyncOb6w= +github.com/kunwardeep/paralleltest v1.0.15/go.mod h1:di4moFqtfz3ToSKxhNjhOZL+696QtJGCFe132CbBLGk= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= @@ -994,38 +1074,47 @@ github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhR github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4= github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI= -github.com/ldez/exptostd v0.4.2 h1:l5pOzHBz8mFOlbcifTxzfyYbgEmoUqjxLFHZkjlbHXs= -github.com/ldez/exptostd v0.4.2/go.mod h1:iZBRYaUmcW5jwCR3KROEZ1KivQQp6PHXbDPk9hqJKCQ= -github.com/ldez/gomoddirectives v0.6.1 h1:Z+PxGAY+217f/bSGjNZr/b2KTXcyYLgiWI6geMBN2Qc= -github.com/ldez/gomoddirectives v0.6.1/go.mod h1:cVBiu3AHR9V31em9u2kwfMKD43ayN5/XDgr+cdaFaKs= -github.com/ldez/grignotin v0.9.0 h1:MgOEmjZIVNn6p5wPaGp/0OKWyvq42KnzAt/DAb8O4Ow= -github.com/ldez/grignotin v0.9.0/go.mod h1:uaVTr0SoZ1KBii33c47O1M8Jp3OP3YDwhZCmzT9GHEk= -github.com/ldez/tagliatelle v0.7.1 h1:bTgKjjc2sQcsgPiT902+aadvMjCeMHrY7ly2XKFORIk= -github.com/ldez/tagliatelle v0.7.1/go.mod h1:3zjxUpsNB2aEZScWiZTHrAXOl1x25t3cRmzfK1mlo2I= -github.com/ldez/usetesting v0.4.2 h1:J2WwbrFGk3wx4cZwSMiCQQ00kjGR0+tuuyW0Lqm4lwA= -github.com/ldez/usetesting v0.4.2/go.mod h1:eEs46T3PpQ+9RgN9VjpY6qWdiw2/QmfiDeWmdZdrjIQ= +github.com/ldez/exptostd v0.4.5 h1:kv2ZGUVI6VwRfp/+bcQ6Nbx0ghFWcGIKInkG/oFn1aQ= +github.com/ldez/exptostd v0.4.5/go.mod h1:QRjHRMXJrCTIm9WxVNH6VW7oN7KrGSht69bIRwvdFsM= +github.com/ldez/gomoddirectives v0.8.0 h1:JqIuTtgvFC2RdH1s357vrE23WJF2cpDCPFgA/TWDGpk= +github.com/ldez/gomoddirectives v0.8.0/go.mod h1:jutzamvZR4XYJLr0d5Honycp4Gy6GEg2mS9+2YX3F1Q= +github.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o= +github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufpTEbwOas= +github.com/ldez/structtags v0.6.1 h1:bUooFLbXx41tW8SvkfwfFkkjPYvFFs59AAMgVg6DUBk= +github.com/ldez/structtags v0.6.1/go.mod h1:YDxVSgDy/MON6ariaxLF2X09bh19qL7MtGBN5MrvbdY= +github.com/ldez/tagliatelle v0.7.2 h1:KuOlL70/fu9paxuxbeqlicJnCspCRjH0x8FW+NfgYUk= +github.com/ldez/tagliatelle v0.7.2/go.mod h1:PtGgm163ZplJfZMZ2sf5nhUT170rSuPgBimoyYtdaSI= +github.com/ldez/usetesting v0.5.0 h1:3/QtzZObBKLy1F4F8jLuKJiKBjjVFi1IavpoWbmqLwc= +github.com/ldez/usetesting v0.5.0/go.mod h1:Spnb4Qppf8JTuRgblLrEWb7IE6rDmUpGvxY3iRrzvDQ= github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= -github.com/letsencrypt/boulder v0.0.0-20231026200631-000cd05d5491 h1:WGrKdjHtWC67RX96eTkYD2f53NDHhrq/7robWTAfk4s= -github.com/letsencrypt/boulder v0.0.0-20231026200631-000cd05d5491/go.mod h1:o158RFmdEbYyIZmXAbrvmJWesbyxlLKee6X64VPVuOc= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/letsencrypt/boulder v0.20260223.0 h1:xdS2OnJNUasR6TgVIOpqqcvdkOu47+PQQMBk9ThuWBw= +github.com/letsencrypt/boulder v0.20260223.0/go.mod h1:r3aTSA7UZ7dbDfiGK+HLHJz0bWNbHk6YSPiXgzl23sA= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= +github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/macabu/inamedparam v0.1.3 h1:2tk/phHkMlEL/1GNe/Yf6kkR/hkcUdAEY3L0hjYV1Mk= -github.com/macabu/inamedparam v0.1.3/go.mod h1:93FLICAIk/quk7eaPPQvbzihUdn/QkGDwIZEoLtpH6I= -github.com/mackerelio/go-osstat v0.2.5 h1:+MqTbZUhoIt4m8qzkVoXUJg1EuifwlAJSk4Yl2GXh+o= -github.com/mackerelio/go-osstat v0.2.5/go.mod h1:atxwWF+POUZcdtR1wnsUcQxTytoHG4uhl2AKKzrOajY= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE= +github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U= +github.com/mackerelio/go-osstat v0.2.6 h1:gs4U8BZeS1tjrL08tt5VUliVvSWP26Ai2Ob8Lr7f2i0= +github.com/mackerelio/go-osstat v0.2.6/go.mod h1:lRy8V9ZuHpuRVZh+vyTkODeDPl3/d5MgXHtLSaqG8bA= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI= -github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE= -github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04= -github.com/maratori/testpackage v1.1.1/go.mod h1:s4gRK/ym6AMrqpOa/kEbQTV4Q4jb7WeLZzVhVVVOQMc= +github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww= +github.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM= +github.com/manuelarte/funcorder v0.5.0 h1:llMuHXXbg7tD0i/LNw8vGnkDTHFpTnWqKPI85Rknc+8= +github.com/manuelarte/funcorder v0.5.0/go.mod h1:Yt3CiUQthSBMBxjShjdXMexmzpP8YGvGLjrxJNkO2hA= +github.com/maratori/testableexamples v1.0.1 h1:HfOQXs+XgfeRBJ+Wz0XfH+FHnoY9TVqL6Fcevpzy4q8= +github.com/maratori/testableexamples v1.0.1/go.mod h1:XE2F/nQs7B9N08JgyRmdGjYVGqxWwClLPCGSQhXQSrQ= +github.com/maratori/testpackage v1.1.2 h1:ffDSh+AgqluCLMXhM19f/cpvQAKygKAJXFl9aUjmbqs= +github.com/maratori/testpackage v1.1.2/go.mod h1:8F24GdVDFW5Ew43Et02jamrVMNXLUNaOynhDssITGfc= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4= github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= @@ -1036,21 +1125,17 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw= +github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-mastodon v0.0.8 h1:UgKs4SmQ5JeawxMIPP7NQ9xncmOXA+5q6jYk4erR7vk= github.com/mattn/go-mastodon v0.0.8/go.mod h1:8YkqetHoAVEktRkK15qeiv/aaIMfJ/Gc89etisPZtHU= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo= github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc= github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= @@ -1063,21 +1148,23 @@ github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnN github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys= github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8= github.com/mdlayher/netlink v1.4.1/go.mod h1:e4/KuJ+s8UhfUpO9z00/fDZZmhSrs+oxyqAS9cNgn6Q= -github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA= -github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= -github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/netlink v1.6.2/go.mod h1:O1HXX2sIWSMJ3Qn1BYZk1yZM+7iMki/uYGGiwGyq/iU= +github.com/mdlayher/netlink v1.11.2 h1:HKh2jqe+omdSWcQ88nrT7INE61B0NXfiSPFdgL4YbNI= +github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA= github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00/go.mod h1:GAFlyu4/XV68LkQKYzKhIo/WW7j3Zi0YRAz/BOoanUc= -github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs= -github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= -github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= -github.com/mgechev/revive v1.7.0 h1:JyeQ4yO5K8aZhIKf5rec56u0376h8AlKNQEmjfkjKlY= -github.com/mgechev/revive v1.7.0/go.mod h1:qZnwcNhoguE58dfi96IJeSTPeZQejNeoMQLUZGi4SW4= +github.com/mdlayher/socket v0.2.3/go.mod h1:bz12/FozYNH/VbvC3q7TRIK/Y6dH1kCKsXaUeXi/FmY= +github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU= +github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= +github.com/mgechev/revive v1.15.0 h1:vJ0HzSBzfNyPbHKolgiFjHxLek9KUijhqh42yGoqZ8Q= +github.com/mgechev/revive v1.15.0/go.mod h1:LlAKO3QQe9OJ0pVZzI2GPa8CbXGZ/9lNpCGvK4T/a8A= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/microsoft/ApplicationInsights-Go v0.4.4 h1:G4+H9WNs6ygSCe6sUyxRc2U81TI5Es90b2t/MwX5KqY= github.com/microsoft/ApplicationInsights-Go v0.4.4/go.mod h1:fKRUseBqkw6bDiXTs3ESTiU/4YTIHsQS4W3fP2ieF4U= -github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= -github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= +github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= +github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= @@ -1086,37 +1173,32 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= +github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= -github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= -github.com/moby/moby v26.1.0+incompatible h1:mjepCwMH0KpCgPvrXjqqyCeTCHgzO7p9TwZ2nQMI2qU= -github.com/moby/moby v26.1.0+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= -github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= -github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= -github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g= -github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= -github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= -github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= -github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI= -github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= -github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= -github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= -github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= -github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/moby/moby/api v1.54.0 h1:7kbUgyiKcoBhm0UrWbdrMs7RX8dnwzURKVbZGy2GnL0= +github.com/moby/moby/api v1.54.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= +github.com/moby/moby/client v0.3.0 h1:UUGL5okry+Aomj3WhGt9Aigl3ZOxZGqR7XPo+RLPlKs= +github.com/moby/moby/client v0.3.0/go.mod h1:HJgFbJRvogDQjbM8fqc1MCEm4mIAGMLjXbgwoZp6jCQ= +github.com/moby/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y= +github.com/moby/spdystream v0.5.1/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= @@ -1125,9 +1207,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= -github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= -github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA= -github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI= @@ -1136,13 +1217,10 @@ github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbY github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA= github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg= github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= -github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= -github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= -github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= -github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= -github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= @@ -1155,49 +1233,42 @@ github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/n github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= +github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= +github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg= github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= -github.com/nunnatsa/ginkgolinter v0.19.1 h1:mjwbOlDQxZi9Cal+KfbEJTCz327OLNfwNvoZ70NJ+c4= -github.com/nunnatsa/ginkgolinter v0.19.1/go.mod h1:jkQ3naZDmxaZMXPWaS9rblH+i+GWXQCaS/JFIWcOH2s= +github.com/nunnatsa/ginkgolinter v0.23.0 h1:x3o4DGYOWbBMP/VdNQKgSj+25aJKx2Pe6lHr8gBcgf8= +github.com/nunnatsa/ginkgolinter v0.23.0/go.mod h1:9qN1+0akwXEccwV1CAcCDfcoBlWXHB+ML9884pL4SZ4= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= +github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= -github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= +github.com/onsi/ginkgo/v2 v2.29.0 h1:rfh+ZFjgJhYWRoIqVf3Uwx/W20yLrcrE2h2GmYVRaag= +github.com/onsi/ginkgo/v2 v2.29.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= -github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/onsi/gomega v1.41.0 h1:OwKp4pXNgVxf6sCplzYo794OFNuoL2q2SBMU5NSWOjA= +github.com/onsi/gomega v1.41.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/opencontainers/runc v1.2.0-rc.1 h1:SMjop2pxxYRTfKdsigna/8xRoaoCfIQfD2cVuOb64/o= -github.com/opencontainers/runc v1.2.0-rc.1/go.mod h1:m9JwxfHzXz5YTTXBQr7EY9KTuazFAGPyMQx2nRR3vTw= -github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= -github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= -github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80= +github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= @@ -1210,26 +1281,25 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pborman/getopt v0.0.0-20180811024354-2b5b3bfb099b/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/peterbourgon/diskv v2.0.2-0.20180312054125-0646ccaebea1+incompatible h1:FhnA4iH8T/yYW+AolPONZjGE897wxj3MAzfEbrZkSYw= github.com/peterbourgon/diskv v2.0.2-0.20180312054125-0646ccaebea1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= -github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe h1:vHpqOnPlnkba8iSxU4j/CvDSS9J4+F4473esQsYLGoE= +github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= -github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= -github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= +github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= -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/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -1237,35 +1307,25 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= -github.com/polyfloyd/go-errorlint v1.7.1 h1:RyLVXIbosq1gBdk/pChWA8zWYLsq9UEw7a1L5TVMCnA= -github.com/polyfloyd/go-errorlint v1.7.1/go.mod h1:aXjNb1x2TNhoLsk26iv1yl7a+zTnXPhwEMtEXukiLR8= github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= -github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM= -github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg= -github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 h1:+Wl/0aFp0hpuHM3H//KMft64WQ1yX9LdJY64Qm/gFCo= -github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI= -github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE= -github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= +github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA= +github.com/quasilyte/go-ruleguard v0.4.5/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE= +github.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuMRoVWSkXC4uvY= +github.com/quasilyte/go-ruleguard/dsl v0.3.23/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo= github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng= github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU= @@ -1278,115 +1338,127 @@ github.com/redis/go-redis/extra/rediscmd/v9 v9.7.1 h1:+o7rrBoj54t8fqQSmnwRLdLzp5 github.com/redis/go-redis/extra/rediscmd/v9 v9.7.1/go.mod h1:bWIjbxmrAk9eKGg9LSko3oQefoYGyWV4xzNS55PgL60= github.com/redis/go-redis/extra/redisotel/v9 v9.7.1 h1:LJF39lvUagUpKfL2/gZIp5vHv3AwXt9zOZ/Xual/CzI= github.com/redis/go-redis/extra/redisotel/v9 v9.7.1/go.mod h1:VAY1vDpD/dLwfw/wU5SsexXNhCO9DjhRoGkmJeFONoE= -github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= -github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= +github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmiUq4= -github.com/rubenv/sql-migrate v1.7.1/go.mod h1:Ob2Psprc0/3ggbM6wCzyYVFFuc6FyZrb2AS+ezLDFb4= +github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0= +github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryancurrah/gomodguard v1.3.5 h1:cShyguSwUEeC0jS7ylOiG/idnd1TpJ1LfHGpV3oJmPU= -github.com/ryancurrah/gomodguard v1.3.5/go.mod h1:MXlEPQRxgfPQa62O8wzK3Ozbkv9Rkqr+wKjSxTdsNJE= -github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU= -github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= -github.com/s3rj1k/go-fanotify/fanotify v0.0.0-20210917134616-9c00a300bb7a h1:np2nR32/A/VcOG9Hn+IOPA8kMk1gbBzK5LpSsgq5pJI= -github.com/s3rj1k/go-fanotify/fanotify v0.0.0-20210917134616-9c00a300bb7a/go.mod h1:wiP6GQ2T378F+YIyuNw7yXtBxJZR+fqrrn1Z6UHZi0Q= -github.com/safchain/ethtool v0.5.10 h1:Im294gZtuf4pSGJRAOGKaASNi3wMeFaGaWuSaomedpc= -github.com/safchain/ethtool v0.5.10/go.mod h1:w9jh2Lx7YBR4UwzLkzCmWl85UY0W2uZdd7/DckVE5+c= -github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= -github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/ryancurrah/gomodguard v1.4.1 h1:eWC8eUMNZ/wM/PWuZBv7JxxqT5fiIKSIyTvjb7Elr+g= +github.com/ryancurrah/gomodguard v1.4.1/go.mod h1:qnMJwV1hX9m+YJseXEBhd2s90+1Xn6x9dLz11ualI1I= +github.com/ryanrolds/sqlclosecheck v0.6.0 h1:pEyL9okISdg1F1SEpJNlrEotkTGerv5BMk7U4AG0eVg= +github.com/ryanrolds/sqlclosecheck v0.6.0/go.mod h1:xyX16hsDaCMXHrMJ3JMzGf5OpDfHTOTTQrT7HOFUmeU= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/safchain/ethtool v0.7.0 h1:rlJzfDetsVvT61uz8x1YIcFn12akMfuPulHtZjtb7Is= +github.com/safchain/ethtool v0.7.0/go.mod h1:MenQKEjXdfkjD3mp2QdCk8B/hwvkrlOTm/FD4gTpFxQ= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0= github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= -github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= -github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sasha-s/go-deadlock v0.3.6 h1:TR7sfOnZ7x00tWPfD397Peodt57KzMDo+9Ae9rMiUmw= +github.com/sasha-s/go-deadlock v0.3.6/go.mod h1:CUqNyyvMxTyjFqDT7MRg9mb4Dv/btmGTqSR+rky/UXo= github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw= github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= -github.com/sashamelentyev/usestdlibvars v1.28.0 h1:jZnudE2zKCtYlGzLVreNp5pmCdOxXUzwsMDBkR21cyQ= -github.com/sashamelentyev/usestdlibvars v1.28.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8= +github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ= +github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8= github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg= github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI= +github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A= +github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk= +github.com/sassoftware/relic/v7 v7.6.2 h1:rS44Lbv9G9eXsukknS4mSjIAuuX+lMq/FnStgmZlUv4= +github.com/sassoftware/relic/v7 v7.6.2/go.mod h1:kjmP0IBVkJZ6gXeAu35/KCEfca//+PKM6vTAsyDPY+k= github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e h1:7q6NSFZDeGfvvtIRwBrU/aegEYJYmvev0cHAwo17zZQ= github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs= -github.com/secure-systems-lab/go-securesystemslib v0.8.0 h1:mr5An6X45Kb2nddcFlbmfHkLguCE9laoZCUzEEpIZXA= -github.com/secure-systems-lab/go-securesystemslib v0.8.0/go.mod h1:UH2VZVuJfCYR8WgMlCU1uFsOUU+KeyrTWcSS73NBOzU= -github.com/securego/gosec/v2 v2.22.2 h1:IXbuI7cJninj0nRpZSLCUlotsj8jGusohfONMrHoF6g= -github.com/securego/gosec/v2 v2.22.2/go.mod h1:UEBGA+dSKb+VqM6TdehR7lnQtIIMorYJ4/9CW1KVQBE= +github.com/secure-systems-lab/go-securesystemslib v0.10.0 h1:l+H5ErcW0PAehBNrBxoGv1jjNpGYdZ9RcheFkB2WI14= +github.com/secure-systems-lab/go-securesystemslib v0.10.0/go.mod h1:MRKONWmRoFzPNQ9USRF9i1mc7MvAVvF1LlW8X5VWDvk= +github.com/securego/gosec/v2 v2.24.8-0.20260309165252-619ce2117e08 h1:AoLtJX4WUtZkhhUUMFy3GgecAALp/Mb4S1iyQOA2s0U= +github.com/securego/gosec/v2 v2.24.8-0.20260309165252-619ce2117e08/go.mod h1:+XLCJiRE95ga77XInNELh2M6zQP+PdqiT9Zpm0D9Wpk= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= +github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sigstore/cosign/v2 v2.2.4 h1:iY4vtEacmu2hkNj1Fh+8EBqBwKs2DHM27/lbNWDFJro= -github.com/sigstore/cosign/v2 v2.2.4/go.mod h1:JZlRD2uaEjVAvZ1XJ3QkkZJhTqSDVtLaet+C/TMR81Y= -github.com/sigstore/rekor v1.3.6 h1:QvpMMJVWAp69a3CHzdrLelqEqpTM3ByQRt5B5Kspbi8= -github.com/sigstore/rekor v1.3.6/go.mod h1:JDTSNNMdQ/PxdsS49DJkJ+pRJCO/83nbR5p3aZQteXc= -github.com/sigstore/sigstore v1.8.3 h1:G7LVXqL+ekgYtYdksBks9B38dPoIsbscjQJX/MGWkA4= -github.com/sigstore/sigstore v1.8.3/go.mod h1:mqbTEariiGA94cn6G3xnDiV6BD8eSLdL/eA7bvJ0fVs= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sigstore/cosign/v2 v2.6.3 h1:1W+rZWz0zkTfqmTmYBOQS/Jt97NKz1OCzxTV2YAp1+s= +github.com/sigstore/cosign/v2 v2.6.3/go.mod h1:g+P/LgYyJkC85WGGDho7yySl3C6xTJzzpLm21ZV+E6s= +github.com/sigstore/protobuf-specs v0.5.0 h1:F8YTI65xOHw70NrvPwJ5PhAzsvTnuJMGLkA4FIkofAY= +github.com/sigstore/protobuf-specs v0.5.0/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= +github.com/sigstore/rekor v1.5.1 h1:Ca1egHRWRuDvXV4tZu9aXEXc3Gej9FG+HKeapV9OAMQ= +github.com/sigstore/rekor v1.5.1/go.mod h1:gTLDuZuo3SyQCuZvKqwRPA79Qo/2rw39/WtLP/rZjUQ= +github.com/sigstore/rekor-tiles/v2 v2.0.1 h1:1Wfz15oSRNGF5Dzb0lWn5W8+lfO50ork4PGIfEKjZeo= +github.com/sigstore/rekor-tiles/v2 v2.0.1/go.mod h1:Pjsbhzj5hc3MKY8FfVTYHBUHQEnP0ozC4huatu4x7OU= +github.com/sigstore/sigstore v1.10.5 h1:KqrOjDhNOVY+uOzQFat2FrGLClPPCb3uz8pK3wuI+ow= +github.com/sigstore/sigstore v1.10.5/go.mod h1:k/mcVVXw3I87dYG/iCVTSW2xTrW7vPzxxGic4KqsqXs= +github.com/sigstore/sigstore-go v1.1.4 h1:wTTsgCHOfqiEzVyBYA6mDczGtBkN7cM8mPpjJj5QvMg= +github.com/sigstore/sigstore-go v1.1.4/go.mod h1:2U/mQOT9cjjxrtIUeKDVhL+sHBKsnWddn8URlswdBsg= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.5 h1:aqHRubTITULckG9JAcq2FEhtKkT/RRE8oErfuV3smSI= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.5/go.mod h1:h9eK9QyPqpFskF/ewFkRLtwh4/Q3FLc2/DXbym4IHN8= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.5 h1:+9C6CUkv+J4iT67Lx+H1EGBfAdoAHqXumHadeIj9jA4= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.5/go.mod h1:myZsg7wRiy/vf102g5uUAitYhtXCwepmAGxgHG1VHuE= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.5 h1:BpQx6AhjwIN9LmlO4ypkcMcHiWiepgZQGSw5U69frHU= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.5/go.mod h1:ejMD/17lMJ4HykQRPdj5NNr+OQYIEZto8HjDKghVMOA= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.5 h1:OFwQZgWkB/6J6W5sy3SkXE4pJnhNRnE2cJd8ySXmHpo= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.5/go.mod h1:Ee/enmyxi/RFLVlajbnjgH2wOWQwlJ0wY8qZrk43hEw= +github.com/sigstore/timestamp-authority/v2 v2.0.6 h1:1Vh7/SdmLsVLG6Br6/bisd1SnlicfDm0MJYiA+D7Ppw= +github.com/sigstore/timestamp-authority/v2 v2.0.6/go.mod h1:Nk5ucGBDyH0tXAIMZ0prf6xn8qfTnbJhSq+CDabYcfc= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= -github.com/sivchari/tenv v1.12.1 h1:+E0QzjktdnExv/wwsnnyk4oqZBUfuh89YMQT1cyuvSY= -github.com/sivchari/tenv v1.12.1/go.mod h1:1LjSOUCc25snIr5n3DtGGrENhX3LuWefcplwVGC24mw= -github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= -github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= -github.com/slack-go/slack v0.13.0 h1:7my/pR2ubZJ9912p9FtvALYpbt0cQPAqkRy2jaSI1PQ= -github.com/slack-go/slack v0.13.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/slack-go/slack v0.23.1 h1:ZS5B96wxxYQRwvJ3/vJFtqtUZi3tXhsZCyT44Nv7M80= +github.com/slack-go/slack v0.23.1/go.mod h1:H0yR/YBuRJ39RkE+JpV/d/oEsbanzTRowR82bCN0cEs= github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= -github.com/sonatard/noctx v0.1.0 h1:JjqOc2WN16ISWAjAk8M5ej0RfExEXtkEyExl2hLW+OM= -github.com/sonatard/noctx v0.1.0/go.mod h1:0RvBxqY8D4j9cTTTWE8ylt2vqj2EPI8fHmrxHdsaZ2c= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/sonatard/noctx v0.5.1 h1:wklWg9c9ZYugOAk7qG4yP4PBrlQsmSLPTvW1K4PRQMs= +github.com/sonatard/noctx v0.5.1/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas= github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= -github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= -github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= -github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= -github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= -github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= -github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= -github.com/spiffe/spire-api-sdk v1.12.0 h1:xx6PAsoBTvMtNe7h5dGS+/SyVZccudAQg/vB9P/XPec= -github.com/spiffe/spire-api-sdk v1.12.0/go.mod h1:4uuhFlN6KBWjACRP3xXwrOTNnvaLp1zJs8Lribtr4fI= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= +github.com/spiffe/spire-api-sdk v1.14.0 h1:72Ywh9Mq/Zd2irveBEFIh0LFLG8/KYTltkVkBluPd0A= +github.com/spiffe/spire-api-sdk v1.14.0/go.mod h1:9hXJcMzatM1KwAtBDO3s6HccDCic++/5c2yOc5Iln8Y= github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= -github.com/stbenjam/no-sprintf-host-port v0.2.0 h1:i8pxvGrt1+4G0czLr/WnmyH7zbZ8Bg8etvARQ1rpyl4= -github.com/stbenjam/no-sprintf-host-port v0.2.0/go.mod h1:eL0bQ9PasS0hsyTyfTjjG+E80QIyPnBVQbYZyv20Jfk= +github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g= +github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= 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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= @@ -1403,21 +1475,21 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/tdakkota/asciicheck v0.4.1 h1:bm0tbcmi0jezRA2b5kg4ozmMuGAFotKI3RZfrhfovg8= -github.com/tdakkota/asciicheck v0.4.1/go.mod h1:0k7M3rCfRXb0Z6bwgvkEIMleKH3kXNz9UqJ9Xuqopr8= github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0= github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA= github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= -github.com/tetafro/godot v1.5.0 h1:aNwfVI4I3+gdxjMgYPus9eHmoBeJIbnajOyqZYStzuw= -github.com/tetafro/godot v1.5.0/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= +github.com/tetafro/godot v1.5.4 h1:u1ww+gqpRLiIA16yF2PV1CV1n/X3zhyezbNXC3E14Sg= +github.com/tetafro/godot v1.5.4/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU= +github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= +github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug= +github.com/theupdateframework/go-tuf/v2 v2.4.1 h1:K6ewW064rKZCPkRo1W/CTbTtm/+IB4+coG1iNURAGCw= +github.com/theupdateframework/go-tuf/v2 v2.4.1/go.mod h1:Nex2enPVYDFCklrnbTzl3OVwD7fgIAj0J5++z/rvCj8= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -1428,37 +1500,51 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 h1:y4mJRFlM6fUyPhoXuFg/Yu02fg/nIPFMOY8tOqppoFg= -github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460= -github.com/timonwong/loggercheck v0.10.1 h1:uVZYClxQFpw55eh+PIoqM7uAOHMrhVcDoWDery9R8Lg= -github.com/timonwong/loggercheck v0.10.1/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8= +github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 h1:9LPGD+jzxMlnk5r6+hJnar67cgpDIz/iyD+rfl5r2Vk= +github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460= +github.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwTnnNKn4M= +github.com/timonwong/loggercheck v0.11.0/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8= +github.com/tink-crypto/tink-go-awskms/v2 v2.1.0 h1:N9UxlsOzu5mttdjhxkDLbzwtEecuXmlxZVo/ds7JKJI= +github.com/tink-crypto/tink-go-awskms/v2 v2.1.0/go.mod h1:PxSp9GlOkKL9rlybW804uspnHuO9nbD98V/fDX4uSis= +github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0 h1:3B9i6XBXNTRspfkTC0asN5W0K6GhOSgcujNiECNRNb0= +github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0/go.mod h1:jY5YN2BqD/KSCHM9SqZPIpJNG/u3zwfLXHgws4x2IRw= +github.com/tink-crypto/tink-go-hcvault/v2 v2.4.0 h1:j+S+WKBQ5ya26A5EM/uXoVe+a2IaPQN8KgBJZ22cJ+4= +github.com/tink-crypto/tink-go-hcvault/v2 v2.4.0/go.mod h1:OCKJIujnTzDq7f+73NhVs99oA2c1TR6nsOpuasYM6Yo= +github.com/tink-crypto/tink-go/v2 v2.6.0 h1:+KHNBHhWH33Vn+igZWcsgdEPUxKwBMEe0QC60t388v4= +github.com/tink-crypto/tink-go/v2 v2.6.0/go.mod h1:2WbBA6pfNsAfBwDCggboaHeB2X29wkU8XHtGwh2YIk8= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= -github.com/tomarrell/wrapcheck/v2 v2.10.0 h1:SzRCryzy4IrAH7bVGG4cK40tNUhmVmMDuJujy4XwYDg= -github.com/tomarrell/wrapcheck/v2 v2.10.0/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo= +github.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is= +github.com/tomarrell/wrapcheck/v2 v2.12.0/go.mod h1:AQhQuZd0p7b6rfW+vUwHm5OMCGgp63moQ9Qr/0BpIWo= github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y= github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE= -github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= -github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c h1:5a2XDQ2LiAUV+/RjckMyq9sXudfrPSuCY4FuPC1NyAw= +github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c/go.mod h1:g85IafeFJZLxlzZCDRu4JLpfS7HKzR+Hw9qRh3bVzDI= +github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= +github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI= github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA= github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g= github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/uudashr/gocognit v1.2.0 h1:3BU9aMr1xbhPlvJLSydKwdLN3tEUUrzPSSM8S4hDYRA= -github.com/uudashr/gocognit v1.2.0/go.mod h1:k/DdKPI6XBZO1q7HgoV2juESI2/Ofj9AcHPZhBBdrTU= -github.com/uudashr/iface v1.3.1 h1:bA51vmVx1UIhiIsQFSNq6GZ6VPTk3WNMZgRiCe9R29U= -github.com/uudashr/iface v1.3.1/go.mod h1:4QvspiRd3JLPAEXBQ9AiZpLbJlrWWgRChOKDJEuQTdg= -github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts= -github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= -github.com/vishvananda/netlink v1.3.1-0.20250328051554-cb48698f2590 h1:vlb3S673iuHRrc359PkZ5ASE4J1vfRj0ti4g+jkNOWI= -github.com/vishvananda/netlink v1.3.1-0.20250328051554-cb48698f2590/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= +github.com/uudashr/gocognit v1.2.1 h1:CSJynt5txTnORn/DkhiB4mZjwPuifyASC8/6Q0I/QS4= +github.com/uudashr/gocognit v1.2.1/go.mod h1:acaubQc6xYlXFEMb9nWX2dYBzJ/bIjEkc1zzvyIZg5Q= +github.com/uudashr/iface v1.4.1 h1:J16Xl1wyNX9ofhpHmQ9h9gk5rnv2A6lX/2+APLTo0zU= +github.com/uudashr/iface v1.4.1/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg= +github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= +github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= +github.com/vishvananda/netlink v1.3.2-0.20260109214200-c6faf428e8f8 h1:/EaCkwYyCH9rDgccb78ZTaGwo7UGjjdh0iyCa3+miRs= +github.com/vishvananda/netlink v1.3.2-0.20260109214200-c6faf428e8f8/go.mod h1:lEui7SPMd9fgxzHVGRAvTxsBGCF6PRH81o2kLWLWHgw= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 h1:jIVmlAFIqV3d+DOxazTR9v+zgj8+VYuQBzPgBZvWBHA= github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651/go.mod h1:b26F2tHLqaoRQf8DywqzVaV1MQ9yvjb0OMcNl7Nxu20= -github.com/wagoodman/go-progress v0.0.0-20220614130704-4b1c25a33c7c h1:gFwUKtkv6QzQsFdIjvPqd0Qdw42DHUEbbUdiUTI1uco= -github.com/wagoodman/go-progress v0.0.0-20220614130704-4b1c25a33c7c/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA= +github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 h1:0KGbf+0SMg+UFy4e1A/CPVvXn21f1qtWdeJwxZFoQG8= +github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= github.com/whyrusleeping/cbor-gen v0.1.1-0.20240311221002-68b9f235c302 h1:MhInbXe4SzcImAKktUvWBCWZgcw6MYf5NfumTj1BhAw= @@ -1471,25 +1557,36 @@ github.com/xanzy/go-gitlab v0.105.0 h1:3nyLq0ESez0crcaM19o5S//SvezOQguuIHZ3wgX64 github.com/xanzy/go-gitlab v0.105.0/go.mod h1:ETg8tcj4OhrB84UEgeE8dSuV/0h4BBL1uOV/qK0vlyI= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU= -github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg= +github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM= +github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk= github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs= github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4= github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw= github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg= +github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= +github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns= +github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= +github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= +github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q= +github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= +github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= +github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= +github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU= +github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -1497,80 +1594,82 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= -github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= +github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo= gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8= gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/YjuiKhqhGlOCwlIV8SqqGh8= gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0= go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ= go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28= -go-simpler.org/musttag v0.13.0 h1:Q/YAW0AHvaoaIbsPj3bvEI5/QFP7w696IMUpnKXQfCE= -go-simpler.org/musttag v0.13.0/go.mod h1:FTzIGeK6OkKlUDVpj0iQUXZLUO1Js9+mvykDQy9C5yM= -go-simpler.org/sloglint v0.9.0 h1:/40NQtjRx9txvsB/RN022KsUJU+zaaSb/9q9BSefSrE= -go-simpler.org/sloglint v0.9.0/go.mod h1:G/OrAF6uxj48sHahCzrbarVMptL2kjWTaUeC8+fOGww= -go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8= -go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY= -go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc= -go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs= -go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY= -go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU= -go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= -go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +go-simpler.org/musttag v0.14.0 h1:XGySZATqQYSEV3/YTy+iX+aofbZZllJaqwFWs+RTtSo= +go-simpler.org/musttag v0.14.0/go.mod h1:uP8EymctQjJ4Z1kUnjX0u2l60WfUdQxCwSNKzE1JEOE= +go-simpler.org/sloglint v0.11.1 h1:xRbPepLT/MHPTCA6TS/wNfZrDzkGvCCqUv4Bdwc3H7s= +go-simpler.org/sloglint v0.11.1/go.mod h1:2PowwiCOK8mjiF+0KGifVOT8ZsCNiFzvfyJeJOIt8MQ= +go.augendre.info/arangolint v0.4.0 h1:xSCZjRoS93nXazBSg5d0OGCi9APPLNMmmLrC995tR50= +go.augendre.info/arangolint v0.4.0/go.mod h1:l+f/b4plABuFISuKnTGD4RioXiCCgghv2xqst/xOvAA= +go.augendre.info/fatcontext v0.9.0 h1:Gt5jGD4Zcj8CDMVzjOJITlSb9cEch54hjRRlN3qDojE= +go.augendre.info/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw= +go.etcd.io/etcd/api/v3 v3.6.7 h1:7BNJ2gQmc3DNM+9cRkv7KkGQDayElg8x3X+tFDYS+E0= +go.etcd.io/etcd/api/v3 v3.6.7/go.mod h1:xJ81TLj9hxrYYEDmXTeKURMeY3qEDN24hqe+q7KhbnI= +go.etcd.io/etcd/client/pkg/v3 v3.6.7 h1:vvzgyozz46q+TyeGBuFzVuI53/yd133CHceNb/AhBVs= +go.etcd.io/etcd/client/pkg/v3 v3.6.7/go.mod h1:2IVulJ3FZ/czIGl9T4lMF1uxzrhRahLqe+hSgy+Kh7Q= +go.etcd.io/etcd/client/v3 v3.6.7 h1:9WqA5RpIBtdMxAy1ukXLAdtg2pAxNqW5NUoO2wQrE6U= +go.etcd.io/etcd/client/v3 v3.6.7/go.mod h1:2XfROY56AXnUqGsvl+6k29wrwsSbEh1lAouQB1vHpeE= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/bridges/prometheus v0.60.0 h1:x7sPooQCwSg27SjtQee8GyIIRTQcF4s7eSkac6F2+VA= go.opentelemetry.io/contrib/bridges/prometheus v0.60.0/go.mod h1:4K5UXgiHxV484efGs42ejD7E2J/sIlepYgdGoPXe7hE= -go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= -go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= +go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ= +go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8= go.opentelemetry.io/contrib/exporters/autoexport v0.60.0 h1:GuQXpvSXNjpswpweIem84U9BNauqHHi2w1GtNAalvpM= go.opentelemetry.io/contrib/exporters/autoexport v0.60.0/go.mod h1:CkmxekdHco4d7thFJNPQ7Mby4jMBgZUclnrxT4e+ryk= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0 h1:HMUytBT3uGhPKYY/u/G5MR9itrlSO2SMOsSD3Tk3k7A= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0/go.mod h1:hdDXsiNLmdW/9BF2jQpnHHlhFajpWCEYfM6e5m2OAZg= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0 h1:C/Wi2F8wEmbxJ9Kuzw/nhP+Z9XaHYMkyDmXy6yR2cjw= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0/go.mod h1:0Lr9vmGKzadCTgsiBydxr6GEZ8SsZ7Ks53LzjWG5Ar4= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 h1:0NIXxOCFx+SKbhCVxwl3ETG8ClLPAa0KuKV6p3yhxP8= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0/go.mod h1:ChZSJbbfbl/DcRZNc9Gqh6DYGlfjw4PvO1pEOZH1ZsE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= -go.opentelemetry.io/otel/exporters/prometheus v0.57.0 h1:AHh/lAP1BHrY5gBwk8ncc25FXWm/gmmY3BX258z5nuk= -go.opentelemetry.io/otel/exporters/prometheus v0.57.0/go.mod h1:QpFWz1QxqevfjwzYdbMb4Y1NnlJvqSGwyuU0B4iuc9c= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.11.0 h1:k6KdfZk72tVW/QVZf60xlDziDvYAePj5QHwoQvrB2m8= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.11.0/go.mod h1:5Y3ZJLqzi/x/kYtrSrPSx7TFI/SGsL7q2kME027tH6I= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg= -go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y= -go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= -go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= -go.opentelemetry.io/otel/sdk/log v0.11.0 h1:7bAOpjpGglWhdEzP8z0VXc4jObOiDEwr3IYbhBnjk2c= -go.opentelemetry.io/otel/sdk/log v0.11.0/go.mod h1:dndLTxZbwBstZoqsJB3kGsRPkpAgaJrWfQg3lhlHFFY= -go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= -go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= -go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= +go.opentelemetry.io/otel/exporters/prometheus v0.65.0 h1:jOveH/b4lU9HT7y+Gfamf18BqlOuz2PWEvs8yM7Q6XE= +go.opentelemetry.io/otel/exporters/prometheus v0.65.0/go.mod h1:i1P8pcumauPtUI4YNopea1dhzEMuEqWP1xoUZDylLHo= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 h1:GJkybS+crDMdExT/BUNCEgfrmfboztcS6PhvSo88HKM= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0/go.mod h1:NuAyxRYIG2lKX3YQkB+83StTxM7s52PUUkRRiC0wnYI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU= +go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= +go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= +go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.step.sm/crypto v0.77.2 h1:qFjjei+RHc5kP5R7NW9OUWT7SqWIuAOvOkXqg4fNWj8= +go.step.sm/crypto v0.77.2/go.mod h1:W0YJb9onM5l78qgkXIJ2Up6grnwW8EtpCKIza/NCg0o= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= @@ -1579,33 +1678,33 @@ go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= -go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= -go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= -go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= +go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= -gocloud.dev v0.37.0 h1:XF1rN6R0qZI/9DYjN16Uy0durAmSlf58DHOcb28GPro= -gocloud.dev v0.37.0/go.mod h1:7/O4kqdInCNsc6LqgmuFnS0GRew4XNNYWpA44yQnwco= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +gocloud.dev v0.45.0 h1:WknIK8IbRdmynDvara3Q7G6wQhmEiOGwpgJufbM39sY= +gocloud.dev v0.45.0/go.mod h1:0kXKmkCLG6d31N7NyLZWzt7jDSQura9zD/mWgiB6THI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= @@ -1617,16 +1716,15 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58 golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac h1:TSSpLIG4v+p0rPv1pNOQtl1I8knsO4S9trOxNMOLVP4= -golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 h1:qWFG1Dj7TBjOjOvhEOkmyGPVoquqUKnIU0lEVLp8xyk= +golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -1640,23 +1738,18 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -1664,7 +1757,6 @@ golang.org/x/net v0.0.0-20200202094626-16171245cfb2/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-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/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-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -1673,51 +1765,40 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= -golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1735,7 +1816,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1747,60 +1827,49 @@ golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -1810,15 +1879,11 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= @@ -1827,46 +1892,45 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= +golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/api v0.223.0 h1:JUTaWEriXmEy5AhvdMgksGGPEFsYfUKaPEYXd4c3Wvc= -google.golang.org/api v0.223.0/go.mod h1:C+RS7Z+dDwds2b+zoAk5hN/eSfsiCn0UDrYof/M4d2M= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA= +google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= -google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= -google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 h1:IFnXJq3UPB3oBREOodn1v1aGQeZYQclEmvWRMN0PSsY= -google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:c8q6Z6OCqnfVIqUFJkCzKcrj8eCvUrz+K4KRzSTuANg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE= +google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= -google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1876,13 +1940,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1893,11 +1952,9 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/dnaeon/go-vcr.v3 v3.2.0 h1:Rltp0Vf+Aq0u4rQXgmXgtgoRDStTnFN83cWgSGSoRzM= gopkg.in/dnaeon/go-vcr.v3 v3.2.0/go.mod h1:2IMOnnlx9I6u9x+YBsM3tAMx6AlOxnJ0pWxQAzZ79Ag= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= -gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= @@ -1910,7 +1967,6 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1924,77 +1980,82 @@ gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -helm.sh/helm/v3 v3.17.3 h1:3n5rW3D0ArjFl0p4/oWO8IbY/HKaNNwJtOQFdH2AZHg= -helm.sh/helm/v3 v3.17.3/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8= +helm.sh/helm/v3 v3.21.0 h1:9TRbaXQH+BIKLLDYlu++JsyWodS5kBBOLF7C7HY5+cs= +helm.sh/helm/v3 v3.21.0/go.mod h1:5IvU6Ae6ruB/vasVHhnC1IU5RvqFM349vLYS1BiHqeY= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI= -honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= -k8s.io/api v0.32.4 h1:kw8Y/G8E7EpNy7gjB8gJZl3KJkNz8HM2YHrZPtAZsF4= -k8s.io/api v0.32.4/go.mod h1:5MYFvLvweRhyKylM3Es/6uh/5hGp0dg82vP34KifX4g= -k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY= -k8s.io/apiextensions-apiserver v0.32.3/go.mod h1:8YwcvVRMVzw0r1Stc7XfGAzB/SIVLunqApySV5V7Dss= -k8s.io/apimachinery v0.32.4 h1:8EEksaxA7nd7xWJkkwLDN4SvWS5ot9g6Z/VZb3ju25I= -k8s.io/apimachinery v0.32.4/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/apiserver v0.32.3 h1:kOw2KBuHOA+wetX1MkmrxgBr648ksz653j26ESuWNY8= -k8s.io/apiserver v0.32.3/go.mod h1:q1x9B8E/WzShF49wh3ADOh6muSfpmFL0I2t+TG0Zdgc= -k8s.io/cli-runtime v0.32.3 h1:khLF2ivU2T6Q77H97atx3REY9tXiA3OLOjWJxUrdvss= -k8s.io/cli-runtime v0.32.3/go.mod h1:vZT6dZq7mZAca53rwUfdFSZjdtLyfF61mkf/8q+Xjak= -k8s.io/client-go v0.32.4 h1:zaGJS7xoYOYumoWIFXlcVrsiYioRPrXGO7dBfVC5R6M= -k8s.io/client-go v0.32.4/go.mod h1:k0jftcyYnEtwlFW92xC7MTtFv5BNcZBr+zn9jPlT9Ic= -k8s.io/component-base v0.32.3 h1:98WJvvMs3QZ2LYHBzvltFSeJjEx7t5+8s71P7M74u8k= -k8s.io/component-base v0.32.3/go.mod h1:LWi9cR+yPAv7cu2X9rZanTiFKB2kHA+JjmhkKjCZRpI= -k8s.io/cri-api v0.30.1 h1:AUM78wiC56B1WJ2c795AS0IG5T57CkEdkn0IuC+miAE= -k8s.io/cri-api v0.30.1/go.mod h1://4/umPJSW1ISNSNng4OwjpkvswJOQwU8rnkvO8P+xg= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/kubectl v0.32.3 h1:VMi584rbboso+yjfv0d8uBHwwxbC438LKq+dXd5tOAI= -k8s.io/kubectl v0.32.3/go.mod h1:6Euv2aso5GKzo/UVMacV6C7miuyevpfI91SvBvV9Zdg= -k8s.io/metrics v0.32.3 h1:2vsBvw0v8rIIlczZ/lZ8Kcqk9tR6Fks9h+dtFNbc2a4= -k8s.io/metrics v0.32.3/go.mod h1:9R1Wk5cb+qJpCQon9h52mgkVCcFeYxcY+YkumfwHVCU= -k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= -k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU= +honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc= +k8s.io/api v0.35.4 h1:P7nFYKl5vo9AGUp1Z+Pmd3p2tA7bX2wbFWCvDeRv988= +k8s.io/api v0.35.4/go.mod h1:yl4lqySWOgYJJf9RERXKUwE9g2y+CkuwG+xmcOK8wXU= +k8s.io/apiextensions-apiserver v0.35.3 h1:2fQUhEO7P17sijylbdwt0nBdXP0TvHrHj0KeqHD8FiU= +k8s.io/apiextensions-apiserver v0.35.3/go.mod h1:tK4Kz58ykRpwAEkXUb634HD1ZAegEElktz/B3jgETd8= +k8s.io/apimachinery v0.35.4 h1:xtdom9RG7e+yDp71uoXoJDWEE2eOiHgeO4GdBzwWpds= +k8s.io/apimachinery v0.35.4/go.mod h1:NNi1taPOpep0jOj+oRha3mBJPqvi0hGdaV8TCqGQ+cc= +k8s.io/apiserver v0.35.3 h1:D2eIcfJ05hEAEewoSDg+05e0aSRwx8Y4Agvd/wiomUI= +k8s.io/apiserver v0.35.3/go.mod h1:JI0n9bHYzSgIxgIrfe21dbduJ9NHzKJ6RchcsmIKWKY= +k8s.io/cli-runtime v0.35.3 h1:UZq4ipNimtzBmhN7PPKbfAdqo8quK0H0UdGl6qAQnqI= +k8s.io/cli-runtime v0.35.3/go.mod h1:O7MUmCqcKSd5xI+O5X7/pRkB5l0O2NIhOdUVwbHLXu4= +k8s.io/client-go v0.35.4 h1:DN6fyaGuzK64UvnKO5fOA6ymSjvfGAnCAHAR0C66kD8= +k8s.io/client-go v0.35.4/go.mod h1:2Pg9WpsS4NeOpoYTfHHfMxBG8zFMSAUi4O/qoiJC3nY= +k8s.io/code-generator v0.35.4 h1:i0FfiXAeUMBlHarjVk5ZWf6Wjsg3YJpNYmOg0nPk6r4= +k8s.io/code-generator v0.35.4/go.mod h1:rwLDdemFgPK6dGlLFHPUieyekgAlV1x8IVafjAy/ELA= +k8s.io/component-base v0.35.3 h1:mbKbzoIMy7JDWS/wqZobYW1JDVRn/RKRaoMQHP9c4P0= +k8s.io/component-base v0.35.3/go.mod h1:IZ8LEG30kPN4Et5NeC7vjNv5aU73ku5MS15iZyvyMYk= +k8s.io/component-helpers v0.35.3 h1:Rl2p3wNMC0YU21rziLkWXavr7MwkB5Td3lNZ/+gYGm8= +k8s.io/component-helpers v0.35.3/go.mod h1:8BkyfcBA6XsCtFYxDB+mCfZqM6P39Aco12AKigNn0C8= +k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b h1:gMplByicHV/TJBizHd9aVEsTYoJBnnUAT5MHlTkbjhQ= +k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b/go.mod h1:CgujABENc3KuTrcsdpGmrrASjtQsWCT7R99mEV4U/fM= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/kubectl v0.35.3 h1:1KqSYXk/sodU7VeDvK6atX2kAGUZd2QTeR5K7Hb9r9w= +k8s.io/kubectl v0.35.3/go.mod h1:GPHxZqRe+u/i3gTBoVQHeIyq2NilfNPj9hDWeuN3x5s= +k8s.io/metrics v0.35.4 h1:KFo3xFe5rzLDarHLNjZB0J1g1c5fvl6A5Kk/8KzIwOA= +k8s.io/metrics v0.35.4/go.mod h1:5DO36o9esGC1VXylSpqJWmwhoopS/erADQcACz1tN3s= +k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 h1:kBawHLSnx/mYHmRnNUf9d4CpjREbeZuxoSGOX/J+aYM= +k8s.io/utils v0.0.0-20260319190234-28399d86e0b5/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= -mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU= -mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo= -mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f h1:lMpcwN6GxNbWtbpI1+xzFLSW8XzX0u72NttUGVFjO3U= -mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f/go.mod h1:RSLa7mKKCNeTTMHBw5Hsy2rfJmd6O2ivt9Dw9ZqCQpQ= -oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo= -oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo= -sigs.k8s.io/cloud-provider-azure/pkg/azclient v0.6.2 h1:9vsKWUUg5ZPrgx1OTvuJ+tbXU5zt2nOhEt7T1ZlmQ+U= -sigs.k8s.io/cloud-provider-azure/pkg/azclient v0.6.2/go.mod h1:QFx8YimjDv3fcvpJ1vGey5i8ZDOYmUXWAP1XV9eLVlg= -sigs.k8s.io/cloud-provider-azure/pkg/azclient/configloader v0.5.3 h1:PiQy1U20uPkBgdpbERnX3BZ4bB6tljBJKU9wXmn1GrI= -sigs.k8s.io/cloud-provider-azure/pkg/azclient/configloader v0.5.3/go.mod h1:eBK7J+xfuzLATTK5ALuERxsZv7O4kncWnCW5ILCLX0w= -sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= -sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= -sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20211110210527-619e6b92dab9 h1:ylYUI5uaq/guUFerFRVG81FHSA5/3+fERCE1RQbQUZ4= -sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20211110210527-619e6b92dab9/go.mod h1:+sJcI1F0QI0Cv+8fp5rH5B2fK1LxzrAQqYnaPx9nY8I= -sigs.k8s.io/controller-tools v0.16.5 h1:5k9FNRqziBPwqr17AMEPPV/En39ZBplLAdOwwQHruP4= -sigs.k8s.io/controller-tools v0.16.5/go.mod h1:8vztuRVzs8IuuJqKqbXCSlXcw+lkAv/M2sTpg55qjMY= -sigs.k8s.io/gateway-api v1.2.1-0.20250319040149-e8b8afabf889 h1:oCrmvARaph9Rzu+8QWnmKAjgyRqwmZ2J6llfYto8IR8= -sigs.k8s.io/gateway-api v1.2.1-0.20250319040149-e8b8afabf889/go.mod h1:jXin3F66taaYeu7CP6Bsiv+4qBgwZSQ43uF6vZ8T6yw= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +mvdan.cc/gofumpt v0.9.2 h1:zsEMWL8SVKGHNztrx6uZrXdp7AX8r421Vvp23sz7ik4= +mvdan.cc/gofumpt v0.9.2/go.mod h1:iB7Hn+ai8lPvofHd9ZFGVg2GOr8sBUw1QUWjNbmIL/s= +mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 h1:ssMzja7PDPJV8FStj7hq9IKiuiKhgz9ErWw+m68e7DI= +mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15/go.mod h1:4M5MMXl2kW6fivUT6yRGpLLPNfuGtU2Z0cPvFquGDYU= +oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= +oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/cloud-provider-azure/pkg/azclient v0.20.4 h1:UOxIkkOqrkrucdVW+N0ZyDUPL+7eZHPeJ9KYw2FXkDQ= +sigs.k8s.io/cloud-provider-azure/pkg/azclient v0.20.4/go.mod h1:qLMV2RATHSfIH8DhLRdvCxYdPDwLUgAAx4iDrMRvLRg= +sigs.k8s.io/cloud-provider-azure/pkg/azclient/configloader v0.14.0 h1:yjbZWgLUgTnLCGxM8I88l88WgvLRIOcOmAEbacFVLnU= +sigs.k8s.io/cloud-provider-azure/pkg/azclient/configloader v0.14.0/go.mod h1:6NYpRYlC7AyQaDtOQsF62plL1gDQqJwgQUwTdjp8qlU= +sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= +sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= +sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20250517180713-32e5e9e948a5 h1:eOG9vIdpeOc/NI5RStnrYrEL4Crf66SHtfpVXwvRaNc= +sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20250517180713-32e5e9e948a5/go.mod h1:Cq9jUhwSYol5tNB0O/1vLYxNV9KqnhpvEa6HvJ1w0wY= +sigs.k8s.io/controller-tools v0.19.0 h1:OU7jrPPiZusryu6YK0jYSjPqg8Vhf8cAzluP9XGI5uk= +sigs.k8s.io/controller-tools v0.19.0/go.mod h1:y5HY/iNDFkmFla2CfQoVb2AQXMsBk4ad84iR1PLANB0= +sigs.k8s.io/gateway-api v1.4.0-rc.2 h1:1JVw4/b7ug+3AgWDQDSPAnovePYBmSiZ1H1muzgQv8s= +sigs.k8s.io/gateway-api v1.4.0-rc.2/go.mod h1:AR5RSqciWP98OPckEjOjh2XJhAe2Na4LHyXD2FUY7Qk= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kind v0.23.0 h1:8fyDGWbWTeCcCTwA04v4Nfr45KKxbSPH1WO9K+jVrBg= sigs.k8s.io/kind v0.23.0/go.mod h1:ZQ1iZuJLh3T+O8fzhdi3VWcFTzsdXtNv2ppsHc8JQ7s= -sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo= -sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U= -sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E= -sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo= -sigs.k8s.io/mcs-api v0.1.1-0.20250224121229-6c631f4730d0 h1:LChl5QBr39XNzUjscGlfBJYjyclDru70cLujcC8Vn/M= -sigs.k8s.io/mcs-api v0.1.1-0.20250224121229-6c631f4730d0/go.mod h1:M1Zjh0Jn/Z5e/2JHsZyEeLMw0qGBBmkJqEOc+OceERY= -sigs.k8s.io/mcs-api/controllers v0.0.0-20250224121229-6c631f4730d0 h1:tchq4R6n8HQHTn+Gkg2wW+MSDx7hD48quYAuvFVmjt0= -sigs.k8s.io/mcs-api/controllers v0.0.0-20250224121229-6c631f4730d0/go.mod h1:+ZTKacf5PGpIh+NjwMJqloeXJspb8wxkOgWB4m0L+xI= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= +sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM= +sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78= +sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po= +sigs.k8s.io/mcs-api v0.3.1-0.20260224125735-0f775a3eff97 h1:SCB1P+SmtFETI9jkWt7bRVYXPeFPtT1zFtskvfXYetU= +sigs.k8s.io/mcs-api v0.3.1-0.20260224125735-0f775a3eff97/go.mod h1:zZ5CK8uS6HaLkxY4HqsmcBHfzHuNMrY2uJy8T7jffK4= +sigs.k8s.io/mcs-api/controllers v0.0.0-20260403094305-4b9911b73f14 h1:YSv/NHNrhf9K+AEGqqwkl6QW3Y6yLXU8YRfKZ7dfjb4= +sigs.k8s.io/mcs-api/controllers v0.0.0-20260403094305-4b9911b73f14/go.mod h1:IEVANHiCGLNsCWuPsZCJhCVzeWavUmxpJ8XgpTt9MpM= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= -software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= -software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= +software.sslmate.com/src/go-pkcs12 v0.7.0 h1:Db8W44cB54TWD7stUFFSWxdfpdn6fZVcDl0w3R4RVM0= +software.sslmate.com/src/go-pkcs12 v0.7.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/hack/tools/kapinger/Dockerfile b/hack/tools/kapinger/Dockerfile index 97394bac09..f6679f774d 100644 --- a/hack/tools/kapinger/Dockerfile +++ b/hack/tools/kapinger/Dockerfile @@ -1,30 +1,32 @@ -FROM --platform=linux/amd64 mcr.microsoft.com/oss/go/microsoft/golang:1.24.2-2 AS builder +# Linux builder - runs natively on the target platform (amd64 or arm64) +# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.26.3 --format "{{.Name}}@{{.Digest}}" +FROM mcr.microsoft.com/oss/go/microsoft/golang:1.26.3-1@sha256:bd013a5e4c7f683d44627652060269bbd4d0949143c906f6deeeda0880e72d2c AS builder WORKDIR /build ADD . . -RUN go mod download +RUN go mod download +RUN go build -o kapinger . && mkdir -p /lib64 -# Build for Linux -RUN CGO_ENABLED=0 GOOS=linux go build -o kapinger . - -# Build for Windows -RUN CGO_ENABLED=0 GOOS=windows go build -o kapinger.exe . - -# Build for ARM64 Linux -RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o kapinger-arm64 . - -FROM --platform=linux/amd64 scratch AS linux-amd64 +FROM scratch AS linux +COPY --from=builder /lib/ /lib +COPY --from=builder /lib64/ /lib64 +COPY --from=builder /usr/lib/ /usr/lib WORKDIR /app COPY --from=builder /build/kapinger . CMD ["./kapinger"] -FROM --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2022 AS windows-amd64 +# Windows builder - cross-compiles from Linux amd64 (GOOS=windows is not affected by systemcrypto) +# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.26.3 --format "{{.Name}}@{{.Digest}}" +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/oss/go/microsoft/golang:1.26.3-1@sha256:bd013a5e4c7f683d44627652060269bbd4d0949143c906f6deeeda0880e72d2c AS windows-builder + +WORKDIR /build +ADD . . +RUN go mod download +RUN GOOS=windows go build -o kapinger.exe . + +# skopeo inspect docker://mcr.microsoft.com/windows/servercore:ltsc2022 --override-os windows --format "{{.Name}}@{{.Digest}}" +FROM mcr.microsoft.com/windows/servercore:ltsc2022@sha256:86da395cfd2b35dbfc2e9d08719550c51b0570c394bff8f92622a19234766185 AS windows WORKDIR /app -COPY --from=builder /build/kapinger.exe . +COPY --from=windows-builder /build/kapinger.exe . ENTRYPOINT [ "cmd.exe" ] CMD [ "/c", "kapinger.exe" ] - -FROM --platform=linux/arm64 scratch AS linux-arm64 -WORKDIR /app -COPY --from=builder /build/kapinger-arm64 . -CMD ["./kapinger-arm64"] diff --git a/hack/tools/kapinger/go.mod b/hack/tools/kapinger/go.mod index 76613ef5c8..225994b5b1 100644 --- a/hack/tools/kapinger/go.mod +++ b/hack/tools/kapinger/go.mod @@ -28,12 +28,11 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect golang.org/x/net v0.38.0 // indirect - golang.org/x/oauth2 v0.10.0 // indirect + golang.org/x/oauth2 v0.27.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/term v0.30.0 // indirect golang.org/x/text v0.23.0 // indirect golang.org/x/time v0.3.0 // indirect - google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/hack/tools/kapinger/go.sum b/hack/tools/kapinger/go.sum index 55c50da34a..91728dde65 100644 --- a/hack/tools/kapinger/go.sum +++ b/hack/tools/kapinger/go.sum @@ -16,7 +16,6 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEe github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= @@ -80,14 +79,13 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= -golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -101,7 +99,6 @@ golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= @@ -117,8 +114,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/hack/tools/toolbox/Dockerfile b/hack/tools/toolbox/Dockerfile index ab2a9f4c13..e60385840e 100644 --- a/hack/tools/toolbox/Dockerfile +++ b/hack/tools/toolbox/Dockerfile @@ -1,9 +1,11 @@ -FROM mcr.microsoft.com/oss/go/microsoft/golang:1.24.2-2 AS build +# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.26.3 --format "{{.Name}}@{{.Digest}}" +FROM mcr.microsoft.com/oss/go/microsoft/golang:1.26.3-1@sha256:bd013a5e4c7f683d44627652060269bbd4d0949143c906f6deeeda0880e72d2c AS build ADD . . WORKDIR /go/toolbox/ -RUN CGO_ENABLED=0 GOOS=linux go build -o server . +RUN GOOS=linux go build -o server . -FROM mcr.microsoft.com/mirror/docker/library/ubuntu:24.04 +# skopeo inspect docker://mcr.microsoft.com/mirror/docker/library/ubuntu:24.04 --format "{{.Name}}@{{.Digest}}" +FROM mcr.microsoft.com/mirror/docker/library/ubuntu:24.04@sha256:c35e29c9450151419d9448b0fd75374fec4fff364a27f176fb458d472dfc9e54 RUN apt-get update RUN apt-get install -y \ axel \ diff --git a/init/retina/main_linux.go b/init/retina/main_linux.go index f07ee1b919..f284b54a5c 100644 --- a/init/retina/main_linux.go +++ b/init/retina/main_linux.go @@ -4,6 +4,7 @@ package main import ( + "context" "flag" "fmt" "os" @@ -11,6 +12,7 @@ import ( "github.com/microsoft/retina/internal/buildinfo" "github.com/microsoft/retina/pkg/bpf" "github.com/microsoft/retina/pkg/config" + "github.com/microsoft/retina/pkg/loader" "github.com/microsoft/retina/pkg/log" "github.com/microsoft/retina/pkg/telemetry" "github.com/pkg/errors" @@ -58,10 +60,21 @@ func run(args ...string) error { } // Setup BPF - err = bpf.Setup(l) + err = bpf.Setup(l, cfg.FilterMapMaxEntries) if err != nil { return errors.Wrap(err, "failed to setup Retina bpf filesystem") } + runtimeHeaderDir, headerErr := loader.PrepareVmlinuxH(context.Background()) + if headerErr != nil { + l.Warn( + "failed to prepare runtime vmlinux.h in init container, falling back to static headers", + zap.String("path", runtimeHeaderDir), + zap.Error(headerErr), + ) + } else { + l.Info("prepared runtime vmlinux.h in init container", zap.String("path", loader.VmlinuxHeaderPath())) + } + return nil } diff --git a/internal/buildinfo/buildinfo.go b/internal/buildinfo/buildinfo.go index 5e090f29e2..9315db7c77 100644 --- a/internal/buildinfo/buildinfo.go +++ b/internal/buildinfo/buildinfo.go @@ -9,4 +9,5 @@ var ( // If it is set, the application will send telemetry to the corresponding Application Insights resource. ApplicationInsightsID string Version string + RetinaAgentImageName = "ghcr.io/microsoft/retina/retina-agent" ) diff --git a/operator/Dockerfile b/operator/Dockerfile index d200a330a1..3415c1922c 100644 --- a/operator/Dockerfile +++ b/operator/Dockerfile @@ -1,5 +1,5 @@ -# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.24.4-azurelinux3.0 --format "{{.Name}}@{{.Digest}}" -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/oss/go/microsoft/golang:1.24.4-azurelinux3.0@sha256:250d01e55a37bd79d7014ae83f9f50aa6fa5570ca910e7f19faeff4bb0132ae1 AS builder +# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.26.3-azurelinux3.0 --format "{{.Name}}@{{.Digest}}" +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/oss/go/microsoft/golang:1.26.3-1-azurelinux3.0@sha256:ef480755a4126131197d7311ab1e24d55600407194b45349c4975b7ed0d176e6 AS builder ARG VERSION ARG APP_INSIGHTS_ID @@ -25,10 +25,11 @@ RUN --mount=type=cache,target="/root/.cache/go-build" \ ##################### controller ####################### # skopeo inspect docker://mcr.microsoft.com/azurelinux/distroless/minimal:3.0 --format "{{.Name}}@{{.Digest}}" -FROM mcr.microsoft.com/azurelinux/distroless/minimal:3.0@sha256:5a66f9f16ac675db2a8229dac72d83811b73b502d6ad192d8b374c7f3be498af +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/azurelinux/distroless/minimal:3.0.20260517@sha256:0c64ab9cfc44d4f100c0590bd59ead9afedda6cc54f14bb7465b5f9c35ddc037 WORKDIR / COPY --from=builder /lib /lib COPY --from=builder /usr/lib/ /usr/lib +COPY --from=builder /etc/pki/tls/ /etc/pki/tls/ COPY --from=builder /workspace/retina-operator . USER 65532:65532 diff --git a/operator/Dockerfile.windows-2019 b/operator/Dockerfile.windows-2019 deleted file mode 100644 index b9a28bb094..0000000000 --- a/operator/Dockerfile.windows-2019 +++ /dev/null @@ -1,24 +0,0 @@ -# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.24.2-2-cbl-mariner2.0 --format "{{.Name}}@{{.Digest}}" -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/oss/go/microsoft/golang@sha256:5341a0010ecff114ee2f11f5eaa4f73b721b54142954041523f3e785d5c4b978 AS builder - -# Build args -ARG VERSION -ARG APP_INSIGHTS_ID - -ENV GOOS=windows -ENV GOARCH=amd64 - -WORKDIR /usr/src/retina -# Copy the source -COPY . . - -RUN --mount=type=cache,target="/root/.cache/go-build" go build -v -ldflags "-X github.com/microsoft/retina/internal/buildinfo.Version="$VERSION" -X "github.com/microsoft/retina/internal/buildinfo.ApplicationInsightsID"="$APP_INSIGHTS_ID"" -o -o /usr/bin/retina-operator.exe retina-operator operator/main.go - -# Copy into final image -FROM mcr.microsoft.com/windows/nanoserver:ltsc2019 -COPY --from=builder /usr/src/retina/windows/kubeconfigtemplate.yaml kubeconfigtemplate.yaml -COPY --from=builder /usr/src/retina/windows/setkubeconfigpath.ps1 setkubeconfigpath.ps1 - -COPY --from=builder /usr/bin/retina-operator.exe retina-operator.exe - -CMD ["controller.exe", "start", "--kubeconfig=.\\kubeconfig"] diff --git a/operator/Dockerfile.windows-2022 b/operator/Dockerfile.windows-2022 index deccdca314..996ab5fb42 100644 --- a/operator/Dockerfile.windows-2022 +++ b/operator/Dockerfile.windows-2022 @@ -1,5 +1,5 @@ -# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.24.2-2-cbl-mariner2.0 --format "{{.Name}}@{{.Digest}}" -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/oss/go/microsoft/golang@sha256:5341a0010ecff114ee2f11f5eaa4f73b721b54142954041523f3e785d5c4b978 AS builder +# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.26.3-azurelinux3.0 --format "{{.Name}}@{{.Digest}}" +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/oss/go/microsoft/golang:1.26.3-1-azurelinux3.0@sha256:ef480755a4126131197d7311ab1e24d55600407194b45349c4975b7ed0d176e6 AS builder # Build args ARG VERSION @@ -15,7 +15,8 @@ COPY . . RUN --mount=type=cache,target="/root/.cache/go-build" go build -v -ldflags "-X github.com/microsoft/retina/internal/buildinfo.Version="$VERSION" -X "github.com/microsoft/retina/internal/buildinfo.ApplicationInsightsID"="$APP_INSIGHTS_ID"" -o -o /usr/bin/retina-operator.exe retina-operator operator/main.go # Copy into final image -FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 +# skopeo inspect docker://mcr.microsoft.com/windows/nanoserver:ltsc2022 --override-os windows --format "{{.Name}}@{{.Digest}}" +FROM mcr.microsoft.com/windows/nanoserver:ltsc2022@sha256:3ca7daac9e971bd440920e408a29d46545b717775794c71561d57ac2f9354a48 COPY --from=builder /usr/src/retina/windows/kubeconfigtemplate.yaml kubeconfigtemplate.yaml COPY --from=builder /usr/src/retina/windows/setkubeconfigpath.ps1 setkubeconfigpath.ps1 diff --git a/operator/cilium-crds/config/config_linux.go b/operator/cilium-crds/config/config_linux.go index bb73d7aff4..9774343578 100644 --- a/operator/cilium-crds/config/config_linux.go +++ b/operator/cilium-crds/config/config_linux.go @@ -8,19 +8,22 @@ import ( ) type Config struct { - EnableTelemetry bool - LeaderElection bool + EnableTelemetry bool + LeaderElection bool + LeaderElectionNamespace string } func (c Config) Flags(flags *pflag.FlagSet) { flags.Bool("enable-telemetry", c.EnableTelemetry, "enable telemetry (send logs and metrics to a remote server)") flags.Bool("leader-election", c.LeaderElection, "Enable leader election for operator. Ensures there is only one active operator Pod") + flags.String("leader-election-namespace", c.LeaderElectionNamespace, "Namespace for the leader election lease. Required when leader election is enabled.") } var ( DefaultConfig = Config{ - EnableTelemetry: false, - LeaderElection: false, + EnableTelemetry: false, + LeaderElection: false, + LeaderElectionNamespace: "", } Cell = cell.Module( diff --git a/operator/cilium-crds/k8s/apis/cell.go b/operator/cilium-crds/k8s/apis/cell.go index 4c6d86b962..37c8bfc5b7 100644 --- a/operator/cilium-crds/k8s/apis/cell.go +++ b/operator/cilium-crds/k8s/apis/cell.go @@ -8,8 +8,8 @@ package apis import ( "fmt" + "log/slog" - "github.com/sirupsen/logrus" "github.com/spf13/pflag" k8sClient "github.com/cilium/cilium/pkg/k8s/client" @@ -48,7 +48,7 @@ type RegisterCRDsFunc func(k8sClient.Clientset) error type params struct { cell.In - Logger logrus.FieldLogger + Logger *slog.Logger Lifecycle cell.Lifecycle Clientset k8sClient.Clientset diff --git a/operator/cilium-crds/k8s/apis/register.go b/operator/cilium-crds/k8s/apis/register.go index 73732bb1ce..4d6c31514a 100644 --- a/operator/cilium-crds/k8s/apis/register.go +++ b/operator/cilium-crds/k8s/apis/register.go @@ -101,8 +101,10 @@ func createCRD(crdVersionedName, crdMetaName string) func(clientset apiextension clientset, constructV1CRD(crdMetaName, ciliumCRD), crdhelpers.NewDefaultPoller(), - k8sconst.CustomResourceDefinitionSchemaVersionKey, - versioncheck.MustVersion(k8sconst.CustomResourceDefinitionSchemaVersion), + crdhelpers.NeedsUpdateV1Factory( + k8sconst.CustomResourceDefinitionSchemaVersionKey, + versioncheck.MustVersion(k8sconst.CustomResourceDefinitionSchemaVersion), + ), ) if err != nil { return fmt.Errorf("Unable to create CRD %s: %w", crdMetaName, err) diff --git a/operator/cilium-crds/k8s/fakeresource.go b/operator/cilium-crds/k8s/fakeresource_linux.go similarity index 90% rename from operator/cilium-crds/k8s/fakeresource.go rename to operator/cilium-crds/k8s/fakeresource_linux.go index c212ca61bb..c02a2286d1 100644 --- a/operator/cilium-crds/k8s/fakeresource.go +++ b/operator/cilium-crds/k8s/fakeresource_linux.go @@ -8,8 +8,7 @@ import ( "github.com/cilium/cilium/pkg/k8s/resource" ) -type fakeresource[T k8sRuntime.Object] struct { -} +type fakeresource[T k8sRuntime.Object] struct{} func (f *fakeresource[T]) Events(ctx context.Context, opts ...resource.EventsOpt) <-chan resource.Event[T] { return make(<-chan resource.Event[T]) diff --git a/operator/cilium-crds/k8s/resource_ctors.go b/operator/cilium-crds/k8s/resource_ctors.go deleted file mode 100644 index 31f61a0083..0000000000 --- a/operator/cilium-crds/k8s/resource_ctors.go +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Retina and Cilium - -// NOTE: copied and slimmed down for our use case - -package k8s - -import ( - "errors" - "fmt" - "strconv" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/tools/cache" - - cilium_api_v2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" - cilium_api_v2alpha1 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1" - "github.com/cilium/cilium/pkg/k8s/client" - "github.com/cilium/cilium/pkg/k8s/resource" - "github.com/cilium/cilium/pkg/k8s/utils" - "github.com/cilium/hive/cell" -) - -var ErrNotACiliumEndpoint = errors.New("object is not a *cilium_api_v2.CiliumEndpoint") - -func CiliumEndpointResource(lc cell.Lifecycle, cs client.Clientset, opts ...func(*metav1.ListOptions)) (resource.Resource[*cilium_api_v2.CiliumEndpoint], error) { - if !cs.IsEnabled() { - return nil, nil - } - lw := utils.ListerWatcherWithModifiers( - utils.ListerWatcherFromTyped[*cilium_api_v2.CiliumEndpointList](cs.CiliumV2().CiliumEndpoints("")), - opts..., - ) - indexers := cache.Indexers{ - cache.NamespaceIndex: cache.MetaNamespaceIndexFunc, - CiliumEndpointIndexIdentity: identityIndexFunc, - } - return resource.New[*cilium_api_v2.CiliumEndpoint]( - lc, lw, resource.WithMetric("CiliumEndpoint"), resource.WithIndexers(indexers)), nil -} - -func identityIndexFunc(obj interface{}) ([]string, error) { - if t, ok := obj.(*cilium_api_v2.CiliumEndpoint); ok { - if t.Status.Identity != nil { - id := strconv.FormatInt(t.Status.Identity.ID, 10) - return []string{id}, nil - } - return []string{"0"}, nil - } - return nil, fmt.Errorf("%w - found %T", ErrNotACiliumEndpoint, obj) -} - -func CiliumEndpointSliceResource(lc cell.Lifecycle, cs client.Clientset, opts ...func(*metav1.ListOptions)) (resource.Resource[*cilium_api_v2alpha1.CiliumEndpointSlice], error) { - if !cs.IsEnabled() { - return nil, nil - } - lw := utils.ListerWatcherWithModifiers( - utils.ListerWatcherFromTyped[*cilium_api_v2alpha1.CiliumEndpointSliceList](cs.CiliumV2alpha1().CiliumEndpointSlices()), - opts..., - ) - return resource.New[*cilium_api_v2alpha1.CiliumEndpointSlice](lc, lw, resource.WithMetric("CiliumEndpointSlice")), nil -} diff --git a/operator/cilium-crds/k8s/resource_ctors_linux.go b/operator/cilium-crds/k8s/resource_ctors_linux.go new file mode 100644 index 0000000000..26d8913273 --- /dev/null +++ b/operator/cilium-crds/k8s/resource_ctors_linux.go @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Retina and Cilium + +package k8s + +import ( + operatork8s "github.com/cilium/cilium/operator/k8s" +) + +// Re-export Cilium's operator resource constructors. +// These live in a _linux file because cilium/operator/k8s transitively +// imports Linux-only symbols (netns.GetNetNSCookie). +var ( + CiliumEndpointResource = operatork8s.CiliumEndpointResource + CiliumEndpointSliceResource = operatork8s.CiliumEndpointSliceResource + PodResource = operatork8s.PodResource + CiliumEndpointIndexIdentity = operatork8s.CiliumEndpointIndexIdentity +) diff --git a/operator/cilium-crds/k8s/resources.go b/operator/cilium-crds/k8s/resources_linux.go similarity index 96% rename from operator/cilium-crds/k8s/resources.go rename to operator/cilium-crds/k8s/resources_linux.go index da8bc18afc..91e69422e9 100644 --- a/operator/cilium-crds/k8s/resources.go +++ b/operator/cilium-crds/k8s/resources_linux.go @@ -18,10 +18,6 @@ import ( "github.com/cilium/hive/cell" ) -const ( - CiliumEndpointIndexIdentity = "identity" -) - // ResourcesCell provides a set of handles to Kubernetes resources used throughout the // operator. Each of the resources share a client-go informer and backing store so we only // have one watch API call for each resource kind and that we maintain only one copy of each object. @@ -39,7 +35,7 @@ var ResourcesCell = cell.Module( func() resource.Resource[*cilium_api_v2.CiliumNode] { return &fakeresource[*cilium_api_v2.CiliumNode]{} }, - k8s.PodResource, + PodResource, k8s.NamespaceResource, ), ) diff --git a/operator/cmd/cilium-crds/cells_linux.go b/operator/cmd/cilium-crds/cells_linux.go index 2ddcc567d0..d250ba9272 100644 --- a/operator/cmd/cilium-crds/cells_linux.go +++ b/operator/cmd/cilium-crds/cells_linux.go @@ -9,17 +9,17 @@ package ciliumcrds import ( "context" "fmt" + "log/slog" "sync/atomic" "github.com/microsoft/retina/internal/buildinfo" + "github.com/microsoft/retina/pkg/log" "github.com/microsoft/retina/pkg/shared/telemetry" - "github.com/sirupsen/logrus" k8sruntime "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" - zapf "sigs.k8s.io/controller-runtime/pkg/log/zap" "github.com/microsoft/retina/operator/cilium-crds/config" operatorK8s "github.com/microsoft/retina/operator/cilium-crds/k8s" @@ -34,20 +34,20 @@ import ( cmtypes "github.com/cilium/cilium/pkg/clustermesh/types" "github.com/cilium/cilium/pkg/controller" k8sClient "github.com/cilium/cilium/pkg/k8s/client" + "github.com/cilium/cilium/pkg/kvstore" "github.com/cilium/cilium/pkg/kvstore/store" "github.com/cilium/cilium/pkg/option" "github.com/cilium/cilium/pkg/pprof" "github.com/cilium/hive/cell" + "github.com/cilium/statedb" ) -const operatorK8sNamespace = "kube-system" - var ( Operator = cell.Module( "operator", "Retina Operator", - cell.Invoke(func(l logrus.FieldLogger) { + cell.Invoke(func(l *slog.Logger) { // to help prevent user confusion, explain why logs may include lines referencing "cilium" or "cilium operator" // e.g. level=info msg="Cilium Operator go version go1.21.4 linux/amd64" subsys=retina-operator l.Info("starting hive. Some logs will say 'cilium' since some code is derived from cilium") @@ -91,6 +91,11 @@ var ( // Provides Clientset, API for accessing Kubernetes objects. k8sClient.Cell, + // Provide in-memory kvstore client for identity GC (not using etcd in Retina) + cell.Provide(func(db *statedb.DB) kvstore.Client { + return kvstore.NewInMemoryClient(db, "default") + }), + // Provides the modular metrics registry, metric HTTP server and standard metrics cell. // NOTE: no server/metrics are created when --enable-metrics=false (default) operatorMetrics.Cell, @@ -187,8 +192,8 @@ var ( // below cluster of cells carries out Retina's custom operator logic for creating Cilium Identities and Endpoints cell.Provide(func(scheme *k8sruntime.Scheme) (ctrl.Manager, error) { - // controller-runtime requires its own logger - logf.SetLogger(zapf.New()) + // Route controller-runtime logs through Retina's zap core so they reach AI. + logf.SetLogger(log.LogrLogger()) manager, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, diff --git a/operator/cmd/cilium-crds/flags.go b/operator/cmd/cilium-crds/flags.go index c2d6e1482a..445ac65864 100644 --- a/operator/cmd/cilium-crds/flags.go +++ b/operator/cmd/cilium-crds/flags.go @@ -42,10 +42,6 @@ func InitGlobalFlags(cmd *cobra.Command, vp *viper.Viper) { // NOTE: without this the option gets overridden from the default value to the zero value via option.Config.Populate(vp) // specifically, here options.Config.AllocatorListTimeout gets overridden from the default value to 0s flags.Duration(option.AllocatorListTimeoutName, defaults.AllocatorListTimeout, "timeout to list initial allocator state") - // similar overriding happens for option.Config.KVstoreConnectivityTimeout - flags.Duration(option.KVstoreConnectivityTimeout, defaults.KVstoreConnectivityTimeout, "Time after which an incomplete kvstore operation is considered failed") - // similar overriding happens for option.Config.KVstorePeriodicSync - flags.Duration(option.KVstorePeriodicSync, defaults.KVstorePeriodicSync, "Periodic KVstore synchronization interval") flags.Duration(operatorOption.EndpointGCInterval, operatorOption.EndpointGCIntervalDefault, "GC interval for cilium endpoints") option.BindEnv(vp, operatorOption.EndpointGCInterval) @@ -56,7 +52,7 @@ func InitGlobalFlags(cmd *cobra.Command, vp *viper.Viper) { flags.StringSlice(option.LogDriver, []string{}, "Logging endpoints to use for example syslog") option.BindEnv(vp, option.LogDriver) - flags.Var(option.NewNamedMapOptions(option.LogOpt, &option.Config.LogOpt, nil), + flags.Var(option.NewMapOptions(&option.Config.LogOpt), option.LogOpt, `Log driver options for cilium-operator, `+ `configmap example for syslog driver: {"syslog.level":"info","syslog.facility":"local4"}`) option.BindEnv(vp, option.LogOpt) diff --git a/operator/cmd/cilium-crds/root_linux.go b/operator/cmd/cilium-crds/root_linux.go index 03e586dc19..ee24b5efec 100644 --- a/operator/cmd/cilium-crds/root_linux.go +++ b/operator/cmd/cilium-crds/root_linux.go @@ -11,6 +11,7 @@ import ( "context" "crypto/rand" "fmt" + "log/slog" "math/big" "os" "path/filepath" @@ -19,42 +20,65 @@ import ( operatorOption "github.com/cilium/cilium/operator/option" "github.com/cilium/cilium/pkg/hive" k8sClient "github.com/cilium/cilium/pkg/k8s/client" - k8sversion "github.com/cilium/cilium/pkg/k8s/version" "github.com/cilium/cilium/pkg/logging" "github.com/cilium/cilium/pkg/logging/logfields" "github.com/cilium/cilium/pkg/metrics" "github.com/cilium/cilium/pkg/option" "github.com/cilium/hive/cell" "github.com/microsoft/retina/internal/buildinfo" + "github.com/microsoft/retina/operator/cilium-crds/config" + "github.com/microsoft/retina/pkg/log" "github.com/pkg/errors" - "github.com/sirupsen/logrus" "github.com/spf13/viper" + "go.uber.org/zap" "k8s.io/client-go/tools/leaderelection" "k8s.io/client-go/tools/leaderelection/resourcelock" ) var ( // set logger field: subsys=retina-operator - binaryName = filepath.Base(os.Args[0]) - logger = logging.DefaultLogger.WithField(logfields.LogSubsys, binaryName) - operatorIDLength = 10 + binaryName = filepath.Base(os.Args[0]) + slogLoggerOnce sync.Once + cachedSlogLogger *slog.Logger + operatorIDLength = 10 + errLeaderElectionNamespaceRequired = errors.New("--leader-election-namespace must be set") ) +// slogLogger returns a zap-backed slog logger. Resolved lazily so that +// SetupZapLogger (run in initEnv) is the backing source — that way every +// call here reaches Application Insights. +func slogLogger() *slog.Logger { + slogLoggerOnce.Do(func() { + cachedSlogLogger = log.SlogLogger().With(logfields.LogSubsys, binaryName) + }) + return cachedSlogLogger +} + func Execute(h *hive.Hive) { initEnv(h.Viper()) - if err := h.Run(logging.DefaultSlogLogger); err != nil { - logger.Fatal(err) + // Use zap-backed slog logger for hive (routes to stdout + Application Insights) + if err := h.Run(log.SlogLogger()); err != nil { + logging.Fatal(slogLogger(), err.Error()) } } -func registerOperatorHooks(l logrus.FieldLogger, lc cell.Lifecycle, llc *LeaderLifecycle, clientset k8sClient.Clientset, shutdowner hive.Shutdowner) { +func registerOperatorHooks( + l *slog.Logger, lc cell.Lifecycle, llc *LeaderLifecycle, + clientset k8sClient.Clientset, shutdowner hive.Shutdowner, + cfg config.Config, +) error { + leaderElectionNamespace := cfg.LeaderElectionNamespace + if leaderElectionNamespace == "" { + return errLeaderElectionNamespaceRequired + } + l.Info("using namespace for leader election lease", "namespace", leaderElectionNamespace) var wg sync.WaitGroup lc.Append(cell.Hook{ OnStart: func(cell.HookContext) error { wg.Add(1) go func() { - runOperator(l, llc, clientset, shutdowner) + runOperator(l, llc, clientset, shutdowner, leaderElectionNamespace) wg.Done() }() return nil @@ -68,6 +92,7 @@ func registerOperatorHooks(l logrus.FieldLogger, lc cell.Lifecycle, llc *LeaderL return nil }, }) + return nil } func initEnv(vp *viper.Viper) { @@ -77,19 +102,34 @@ func initEnv(vp *viper.Viper) { // the default values provided in option.Config or operatorOption.Config respectively. // The values will be overridden to the "zero value". // Maybe could create a cell.Config for these instead? - option.Config.Populate(vp) - operatorOption.Config.Populate(vp) - - // add hooks after setting up metrics in the option.Confog - logging.DefaultLogger.Hooks.Add(metrics.NewLoggingHook()) + // slogloggercheck: using default logger for configuration initialization + option.Config.Populate(logging.DefaultSlogLogger, vp) + operatorOption.Config.Populate(logging.DefaultSlogLogger, vp) + + // Bring up Retina's zap logger (stdout + Application Insights). We redirect + // Go's stdlib slog default at it now; the Cilium-side tee has to wait until + // after logging.SetupLogging runs, because SetupLogging calls + // defaultMultiSlogHandler.SetHandler, which replaces all registered + // handlers. + _, _ = log.SetupZapLogger(&log.LogOpts{ + Level: option.Config.LogOpt[logging.LevelOpt], + ApplicationInsightsID: buildinfo.ApplicationInsightsID, + EnableTelemetry: buildinfo.ApplicationInsightsID != "", + }, zap.String("version", buildinfo.Version)) + log.SetDefaultSlog() // Logging should always be bootstrapped first. Do not add any code above this! if err := logging.SetupLogging(option.Config.LogDriver, logging.LogOptions(option.Config.LogOpt), binaryName, option.Config.Debug); err != nil { - logger.Fatal(err) + logging.Fatal(slogLogger(), err.Error()) } - option.LogRegisteredOptions(vp, logger) - logger.Infof("retina operator version: %s", buildinfo.Version) + // Register zap + metrics hook AFTER SetupLogging so they survive the + // MultiSlogHandler replace. From here on every logging.DefaultSlogLogger + // emission fans out to zap → Application Insights. + logging.AddHandlers(log.SlogHandler(), metrics.NewLoggingHook()) + + option.LogRegisteredSlogOptions(vp, slogLogger()) + slogLogger().Info("retina operator version", "version", buildinfo.Version) } func doCleanup() { @@ -103,37 +143,25 @@ func doCleanup() { // runOperator implements the logic of leader election for cilium-operator using // built-in leader election capability in kubernetes. // See: https://github.com/kubernetes/client-go/blob/master/examples/leader-election/main.go -func runOperator(l logrus.FieldLogger, lc *LeaderLifecycle, clientset k8sClient.Clientset, shutdowner hive.Shutdowner) { +func runOperator(l *slog.Logger, lc *LeaderLifecycle, clientset k8sClient.Clientset, shutdowner hive.Shutdowner, leaderElectionNamespace string) { isLeader.Store(false) leaderElectionCtx, leaderElectionCtxCancel = context.WithCancel(context.Background()) - // We only support Operator in HA mode for Kubernetes Versions having support for - // LeasesResourceLock. - // See docs on capabilities.LeasesResourceLock for more context. - if !k8sversion.Capabilities().LeasesResourceLock { - l.Info("Support for coordination.k8s.io/v1 not present, fallback to non HA mode") - - if err := lc.Start(logging.DefaultSlogLogger, leaderElectionCtx); err != nil { - l.WithError(err).Fatal("Failed to start leading") - } - return - } - // Get hostname for identity name of the lease lock holder. // We identify the leader of the operator cluster using hostname. operatorID, err := os.Hostname() if err != nil { - l.WithError(err).Fatal("Failed to get hostname when generating lease lock identity") + logging.Fatal(l, "Failed to get hostname when generating lease lock identity", logfields.Error, err) } operatorID, err = randomStringWithPrefix(operatorID+"-", operatorIDLength) if err != nil { - l.WithError(err).Fatal("Failed to generate random string for lease lock identity") + logging.Fatal(l, "Failed to generate random string for lease lock identity", logfields.Error, err) } leResourceLock, err := resourcelock.NewFromKubeconfig( resourcelock.LeasesResourceLock, - operatorK8sNamespace, + leaderElectionNamespace, leaderElectionResourceLockName, resourcelock.ResourceLockConfig{ // Identity name of the lock holder @@ -142,7 +170,7 @@ func runOperator(l logrus.FieldLogger, lc *LeaderLifecycle, clientset k8sClient. clientset.RestConfig(), operatorOption.Config.LeaderElectionRenewDeadline) if err != nil { - l.WithError(err).Fatal("Failed to create resource lock for leader election") + logging.Fatal(l, "Failed to create resource lock for leader election", logfields.Error, err) } // Start the leader election for running cilium-operators @@ -160,12 +188,12 @@ func runOperator(l logrus.FieldLogger, lc *LeaderLifecycle, clientset k8sClient. Callbacks: leaderelection.LeaderCallbacks{ OnStartedLeading: func(ctx context.Context) { if err := lc.Start(logging.DefaultSlogLogger, ctx); err != nil { - l.WithError(err).Error("Failed to start when elected leader, shutting down") + l.Error("Failed to start when elected leader, shutting down", logfields.Error, err) shutdowner.Shutdown(hive.ShutdownWithError(err)) } }, OnStoppedLeading: func() { - l.WithField("operator-id", operatorID).Info("Leader election lost") + l.Info("Leader election lost", "operator-id", operatorID) // Cleanup everything here, and exit. shutdowner.Shutdown(hive.ShutdownWithError(errors.New("Leader election lost"))) }, @@ -173,10 +201,7 @@ func runOperator(l logrus.FieldLogger, lc *LeaderLifecycle, clientset k8sClient. if identity == operatorID { l.Info("Leading the operator HA deployment") } else { - l.WithFields(logrus.Fields{ - "newLeader": identity, - "operatorID": operatorID, - }).Info("Leader re-election complete") + l.Info("Leader re-election complete", "newLeader", identity, "operatorID", operatorID) } }, }, diff --git a/operator/cmd/cilium-crds/zap_linux.go b/operator/cmd/cilium-crds/zap_linux.go index 8853180250..75edc9cbe1 100644 --- a/operator/cmd/cilium-crds/zap_linux.go +++ b/operator/cmd/cilium-crds/zap_linux.go @@ -4,85 +4,34 @@ package ciliumcrds import ( - "fmt" - "io" + "log/slog" - zaphook "github.com/Sytten/logrus-zap-hook" - "github.com/cilium/cilium/pkg/logging" "github.com/cilium/cilium/pkg/option" "github.com/cilium/hive/cell" "github.com/microsoft/retina/internal/buildinfo" "github.com/microsoft/retina/pkg/log" - "github.com/sirupsen/logrus" "go.uber.org/zap" "k8s.io/client-go/rest" "github.com/microsoft/retina/operator/cilium-crds/config" ) -// TODO refactor to another package? Like shared/telemetry/ - -const logFileName = "retina-operator.log" - -var ( - MaxFileSizeMB = 100 - MaxBackups = 3 - MaxAgeDays = 30 -) - type params struct { cell.In - Logger logrus.FieldLogger + Logger *slog.Logger K8sCfg *rest.Config DaemonCfg *option.DaemonConfig OperatorCfg config.Config } func setupZapHook(p params) { - // modify default logger - // properly report the caller (otherwise, will get caller=zap.go every time) - logging.DefaultLogger.ReportCaller = true - // discard default logger output in favor of zap - logging.DefaultLogger.SetOutput(io.Discard) - - level, err := logrus.ParseLevel(p.DaemonCfg.LogOpt[logging.LevelOpt]) - if err != nil { - p.Logger.WithError(err).Error("failed to parse log level") - } else { - logging.DefaultLogger.SetLevel(level) - } - - lOpts := &log.LogOpts{ - Level: p.DaemonCfg.LogOpt[logging.LevelOpt], - File: false, - FileName: logFileName, - MaxFileSizeMB: MaxFileSizeMB, - MaxBackups: MaxBackups, - MaxAgeDays: MaxAgeDays, - ApplicationInsightsID: buildinfo.ApplicationInsightsID, - EnableTelemetry: p.OperatorCfg.EnableTelemetry, - } - - persistentFields := []zap.Field{ + // Note: Zap logger is now initialized in initEnv() before hive starts. + // This hook only logs startup info with operator-specific fields from the hive DI context. + namedLogger := log.Logger().Named("retina-operator-v2") + namedLogger.Info("Traces telemetry initialized with zapai", zap.String("version", buildinfo.Version), + zap.String("appInsightsID", buildinfo.ApplicationInsightsID), zap.String("apiserver", p.K8sCfg.Host), - } - - _, err = log.SetupZapLogger(lOpts, persistentFields...) - if err != nil { - fmt.Printf("failed to setup zap logger: %v", err) - } - - namedLogger := log.Logger().Named("retina-operator-v2") - namedLogger.Info("Traces telemetry initialized with zapai", zap.String("version", buildinfo.Version), zap.String("appInsightsID", lOpts.ApplicationInsightsID)) - - var hook *zaphook.ZapHook - hook, err = zaphook.NewZapHook(namedLogger.Logger) - if err != nil { - p.Logger.WithError(err).Error("failed to create zap hook") - return - } - - logging.DefaultLogger.Hooks.Add(hook) + ) } diff --git a/operator/cmd/cilium_crds_cmd_linux.go b/operator/cmd/cilium_crds_cmd_linux.go index 701be12166..f59e189c9e 100644 --- a/operator/cmd/cilium_crds_cmd_linux.go +++ b/operator/cmd/cilium_crds_cmd_linux.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/cilium/cilium/pkg/hive" + "github.com/cilium/cilium/pkg/logging" "github.com/cilium/cilium/pkg/option" ciliumcrds "github.com/microsoft/retina/operator/cmd/cilium-crds" "github.com/spf13/cobra" @@ -40,7 +41,7 @@ func init() { } cobra.OnInitialize( - option.InitConfig(cmd, "Retina-Operator", "retina-operators", h.Viper()), + option.InitConfig(logging.DefaultSlogLogger, cmd, "Retina-Operator", "retina-operators", h.Viper()), ) rootCmd.AddCommand(cmd) diff --git a/operator/cmd/standard/deployment.go b/operator/cmd/standard/deployment.go index d47509b021..0c170371df 100644 --- a/operator/cmd/standard/deployment.go +++ b/operator/cmd/standard/deployment.go @@ -6,14 +6,11 @@ package standard import ( "context" "fmt" - "net/http" - "net/http/pprof" - - "go.uber.org/zap/zapcore" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. + "github.com/cilium/cilium/pkg/logging" "go.uber.org/zap" v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" @@ -23,7 +20,6 @@ import ( clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/healthz" - crzap "sigs.k8s.io/controller-runtime/pkg/log/zap" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" retinav1alpha1 "github.com/microsoft/retina/crd/api/v1alpha1" @@ -98,6 +94,10 @@ func (o *Operator) Start() error { } defer zl.Close() + // Tee Cilium's MultiSlogHandler into zap so any DefaultSlogLogger call + // (including package-var captures) reaches Application Insights. + logging.AddHandlers(log.SlogHandler()) + log.SetDefaultSlog() // Set Go's global slog to use zap-backed handler mainLogger := zl.Named("main").Sugar() mainLogger.Info("Operator configuration", zap.Any("configuration", oconfig)) @@ -106,11 +106,8 @@ func (o *Operator) Start() error { oconfig.CaptureConfig.CaptureImageVersion = buildinfo.Version oconfig.CaptureConfig.CaptureImageVersionSource = captureUtils.VersionSourceOperatorImageVersion - opts := &crzap.Options{ - Development: false, - } - - ctrl.SetLogger(crzap.New(crzap.UseFlagOptions(opts), crzap.Encoder(zapcore.NewConsoleEncoder(log.EncoderConfig())))) + // Route controller-runtime logs through Retina's zap core so they also reach AI. + ctrl.SetLogger(log.LogrLogger()) mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, @@ -244,20 +241,6 @@ func (o *Operator) Start() error { return nil } -func EnablePProf() { - pprofmux := http.NewServeMux() - pprofmux.HandleFunc("/debug/pprof/", pprof.Index) - pprofmux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) - pprofmux.HandleFunc("/debug/pprof/profile", pprof.Profile) - pprofmux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) - pprofmux.HandleFunc("/debug/pprof/trace", pprof.Trace) - pprofmux.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine")) - - if err := http.ListenAndServe(":8082", pprofmux); err != nil { //nolint:gosec // TODO replace with secure server that supports timeout - panic(err) - } -} - func initLogging(cfg *config.OperatorConfig, applicationInsightsID string) (*log.ZapLogger, error) { logOpts := &log.LogOpts{ Level: cfg.LogLevel, diff --git a/operator/config/config.go b/operator/config/config.go index 1c460ac878..e1d444a76f 100644 --- a/operator/config/config.go +++ b/operator/config/config.go @@ -1,10 +1,13 @@ package config import ( + "errors" "fmt" "log" + "path/filepath" "time" + "github.com/microsoft/retina/pkg/capture" "github.com/microsoft/retina/pkg/config" "github.com/spf13/viper" ) @@ -12,8 +15,9 @@ import ( const MinTelemetryInterval time.Duration = 2 * time.Minute var ( - DefaultTelemetryInterval = 5 * time.Minute - ErrorTelemetryIntervalTooSmall = fmt.Errorf("telemetryInterval smaller than %v is not allowed", MinTelemetryInterval) + DefaultTelemetryInterval = 5 * time.Minute + ErrorTelemetryIntervalTooSmall = fmt.Errorf("telemetryInterval smaller than %v is not allowed", MinTelemetryInterval) + ErrCaptureHostPathBaseDirNotAbsolute = errors.New("captureHostPathBaseDir must be an absolute path") ) type OperatorConfig struct { @@ -53,5 +57,17 @@ func GetConfig(cfgFileName string) (*OperatorConfig, error) { return nil, ErrorTelemetryIntervalTooSmall } + // If unset, default the HostPath base directory so that Capture CRs cannot + // place artifacts arbitrarily on the node filesystem. The CR's HostPath is + // always joined under this directory. + if cfg.CaptureHostPathBaseDir == "" { + log.Printf("captureHostPathBaseDir is not set, defaulting to %s", capture.DefaultHostPathBaseDir) + cfg.CaptureHostPathBaseDir = capture.DefaultHostPathBaseDir + } + cfg.CaptureHostPathBaseDir = filepath.Clean(cfg.CaptureHostPathBaseDir) + if !filepath.IsAbs(cfg.CaptureHostPathBaseDir) { + return nil, fmt.Errorf("%w: got %q", ErrCaptureHostPathBaseDirNotAbsolute, cfg.CaptureHostPathBaseDir) + } + return &cfg, nil } diff --git a/pkg/bpf/setup_linux.go b/pkg/bpf/setup_linux.go index a04cbdf172..10c8d61ee7 100644 --- a/pkg/bpf/setup_linux.go +++ b/pkg/bpf/setup_linux.go @@ -55,7 +55,7 @@ func mountBpfFs() error { return nil } -func Setup(l *zap.Logger) error { +func Setup(l *zap.Logger, filterMapMaxEntries uint32) error { err := mountBpfFs() if err != nil { return errors.Wrap(err, "failed to mount BPF filesystem") @@ -71,7 +71,7 @@ func Setup(l *zap.Logger) error { // Initialize the filter map. // This will create the filter map in kernel and pin it to /sys/fs/bpf. - _, err = filter.Init() + _, err = filter.Init(filterMapMaxEntries) if err != nil { return errors.Wrap(err, "failed to initialize filter map") } diff --git a/pkg/capture/capture_manager.go b/pkg/capture/capture_manager.go index c953a9746e..efcf509c7a 100644 --- a/pkg/capture/capture_manager.go +++ b/pkg/capture/capture_manager.go @@ -23,7 +23,7 @@ import ( captureProvider "github.com/microsoft/retina/pkg/capture/provider" "github.com/microsoft/retina/pkg/log" "github.com/microsoft/retina/pkg/telemetry" - "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // CaptureManager captures network packets and metadata into tar ball, then send the tar ball to the location(s) @@ -105,10 +105,10 @@ func (cm *CaptureManager) captureNodeHostName() string { return os.Getenv(captureConstants.NodeHostNameEnvKey) } -func (cm *CaptureManager) captureStartTimestamp() (*file.Timestamp, error) { - timestamp, err := file.StringToTimestamp((os.Getenv(captureConstants.CaptureStartTimestampEnvKey))) +func (cm *CaptureManager) captureStartTimestamp() (*metav1.Time, error) { + timestamp, err := file.StringToTime((os.Getenv(captureConstants.CaptureStartTimestampEnvKey))) if err != nil { - return nil, errors.Wrap(err, "failed to parse timestamp") + return nil, fmt.Errorf("failed to parse timestamp: %w", err) } return timestamp, nil } @@ -155,7 +155,7 @@ func (cm *CaptureManager) captureMaxSizeMB() (int, error) { } func (cm *CaptureManager) OutputCapture(ctx context.Context, srcDir string) error { - var errStr string + var errs error if _, err := os.Stat(srcDir); os.IsNotExist(err) { return fmt.Errorf("capture source directory %s does not exist", srcDir) @@ -168,12 +168,12 @@ func (cm *CaptureManager) OutputCapture(ctx context.Context, srcDir string) erro for _, location := range cm.enabledOutputLocations() { if err := location.Output(ctx, dstTarGz); err != nil { - errStr = errStr + fmt.Sprintf("location %q output error: %s\n", location.Name(), err) + errs = fmt.Errorf("%w; location %q output error: %w", errs, location.Name(), err) } } - if len(errStr) != 0 { - return fmt.Errorf(errStr) + if errs != nil { + return fmt.Errorf("failed to enable output locations: %w", errs) } // Remove tarball created inside this function. diff --git a/pkg/capture/capture_manager_test.go b/pkg/capture/capture_manager_test.go index 3fc92fa01a..9649a56665 100644 --- a/pkg/capture/capture_manager_test.go +++ b/pkg/capture/capture_manager_test.go @@ -36,7 +36,7 @@ func TestCaptureNetwork(t *testing.T) { maxSize := 100 os.Setenv(captureConstants.CaptureNameEnvKey, captureName) os.Setenv(captureConstants.NodeHostNameEnvKey, nodeHostName) - os.Setenv(captureConstants.CaptureStartTimestampEnvKey, timestamp.String()) + os.Setenv(captureConstants.CaptureStartTimestampEnvKey, file.TimeToString(timestamp)) os.Setenv(captureConstants.TcpdumpFilterEnvKey, filter) os.Setenv(captureConstants.CaptureDurationEnvKey, "10s") os.Setenv(captureConstants.CaptureMaxSizeEnvKey, strconv.Itoa(maxSize)) @@ -53,8 +53,8 @@ func TestCaptureNetwork(t *testing.T) { ctx, cancel := TestContext(t) defer cancel() - tmpFilename := file.CaptureFilename{CaptureName: captureName, NodeHostname: nodeHostName, StartTimestamp: ×tamp} - networkCaptureProvider.EXPECT().Setup(tmpFilename).Return(fmt.Sprintf("%s-%s-%s", captureName, nodeHostName, ×tamp), nil).Times(1) + tmpFilename := file.CaptureFilename{CaptureName: captureName, NodeHostname: nodeHostName, StartTimestamp: timestamp} + networkCaptureProvider.EXPECT().Setup(tmpFilename).Return(fmt.Sprintf("%s-%s-%s", captureName, nodeHostName, timestamp), nil).Times(1) networkCaptureProvider.EXPECT().CaptureNetworkPacket(ctx, filter, duration, maxSize).Return(nil).Times(1) _, err := cm.CaptureNetwork(ctx) diff --git a/pkg/capture/constants/annotations.go b/pkg/capture/constants/annotations.go new file mode 100644 index 0000000000..8f28496b88 --- /dev/null +++ b/pkg/capture/constants/annotations.go @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package constants + +// Capture job specification +const ( + CaptureFilenameAnnotationKey string = "retina-capture-filename" + CaptureTimestampAnnotationKey string = "retina-capture-timestamp" + CaptureHostPathAnnotationKey string = "retina-capture-hostpath" +) diff --git a/pkg/capture/constants/job_env.go b/pkg/capture/constants/job_env.go index 1ec9a67cd1..400e4646ca 100644 --- a/pkg/capture/constants/job_env.go +++ b/pkg/capture/constants/job_env.go @@ -20,6 +20,10 @@ const ( NodeHostNameEnvKey string = "NODE_HOST_NAME" CaptureStartTimestampEnvKey string = "CAPTURE_START_TIMESTAMP" + NamespaceEnvKey string = "NAMESPACE" + PodNameEnvKey string = "POD_NAME" + ContainerNameEnvKey string = "CONTAINER_NAME" + CaptureFilterEnvKey string = "CAPTURE_FILTER" CaptureDurationEnvKey string = "CAPTURE_DURATION" CaptureMaxSizeEnvKey string = "CAPTURE_MAX_SIZE" @@ -28,7 +32,12 @@ const ( TcpdumpFilterEnvKey string = "TCPDUMP_FILTER" TcpdumpRawFilterEnvKey string = "TCPDUMP_RAW_FILTER" + PcapFilterEnvKey string = "PCAP_FILTER" + TcpdumpFlagsEnvKey string = "TCPDUMP_FLAGS" NetshFilterEnvKey string = "NETSH_FILTER" + // Interface selection environment variables + CaptureInterfacesEnvKey string = "CAPTURE_INTERFACES" + ApiserverEnvKey = "APISERVER" ) diff --git a/pkg/capture/constants/job_specification.go b/pkg/capture/constants/job_specification.go index cd7da30ea1..569455facd 100644 --- a/pkg/capture/constants/job_specification.go +++ b/pkg/capture/constants/job_specification.go @@ -3,6 +3,10 @@ package constants +import ( + "github.com/microsoft/retina/internal/buildinfo" +) + // Capture job specification const ( CaptureHostPathVolumeName string = "hostpath" @@ -16,8 +20,10 @@ const ( CaptureContainerEntrypoint string = "./retina/captureworkload" CaptureContainerEntrypointWin string = "captureworkload.exe" - CaptureAppname string = "capture" - CaptureContainername string = "capture" + CaptureAppname string = "capture" + CaptureContainername string = "capture" + DownloadAppname string = "download" + DownloadContainerName string = "download" // CaptureOutputLocationBlobUploadSecretName is the name of the secret that stores the blob upload url. CaptureOutputLocationBlobUploadSecretName string = "capture-blob-upload-secret" @@ -35,9 +41,9 @@ const ( // CaptureOutputLocationS3UploadSecretAccessKey is the key of the secret that stores the s3 secret access key. CaptureOutputLocationS3UploadSecretAccessKey string = "s3-secret-access-key" - // CaptureWorkloadImageName defines the official capture workload image repo and image name - CaptureWorkloadImageName string = "ghcr.io/microsoft/retina/retina-agent" - // DebugCaptureWorkloadImageName defines the capture workload image for testing and debugging DebugCaptureWorkloadImageName string = "ghcr.io/microsoft/retina/retina-agent" ) + +// CaptureWorkloadImageName defines the official capture workload image repo and image name +var CaptureWorkloadImageName string = buildinfo.RetinaAgentImageName diff --git a/pkg/capture/crd_to_job.go b/pkg/capture/crd_to_job.go index 537b5563b9..86d2a8922a 100644 --- a/pkg/capture/crd_to_job.go +++ b/pkg/capture/crd_to_job.go @@ -5,6 +5,7 @@ package capture import ( "context" + "errors" "fmt" "net" "sort" @@ -32,6 +33,29 @@ import ( const anyIPOrPort = "" +var ( + errNoTargetsSelected = errors.New("no targets are selected by node selector, pod selector, or pod names") + errNoValidSelector = errors.New("neither NodeSelector, NamespaceSelector&PodSelector, nor PodNames is set") + errNodeSelectorIncompat = errors.New("NodeSelector is not compatible with NamespaceSelector&PodSelector or PodNames, please use one or the other") + errPodNamesIncompat = errors.New("PodNames is not compatible with NamespaceSelector or PodSelector, please use one or the other") +) + +// tcpdumpFlagMapping defines the mapping between CaptureOption boolean fields and their corresponding tcpdump flags. +var tcpdumpFlagMappings = []struct { + getBool func(*retinav1alpha1.CaptureOption) *bool + flag string +}{ + {func(o *retinav1alpha1.CaptureOption) *bool { return o.NoPromiscuous }, "-p"}, + {func(o *retinav1alpha1.CaptureOption) *bool { return o.PacketBuffered }, "-U"}, + {func(o *retinav1alpha1.CaptureOption) *bool { return o.ImmediateMode }, "--immediate-mode"}, + {func(o *retinav1alpha1.CaptureOption) *bool { return o.NoResolveDNS }, "-n"}, + {func(o *retinav1alpha1.CaptureOption) *bool { return o.NoResolvePort }, "-nn"}, + {func(o *retinav1alpha1.CaptureOption) *bool { return o.PrintLinkHeader }, "-e"}, + {func(o *retinav1alpha1.CaptureOption) *bool { return o.QuietOutput }, "-q"}, + {func(o *retinav1alpha1.CaptureOption) *bool { return o.AbsoluteSeq }, "-S"}, + {func(o *retinav1alpha1.CaptureOption) *bool { return o.DontVerifyChecksum }, "-K"}, +} + // CaptureTarget indicates on which the network capture will be performed on a given node. type CaptureTarget struct { // PodIpAddresses indicates the capture is performed on the Pods per their IP addresses. @@ -109,11 +133,29 @@ func NewCaptureToPodTranslator(kubeClient kubernetes.Interface, logger *log.ZapL return captureToPodTranslator } -func (translator *CaptureToPodTranslator) initJobTemplate(ctx context.Context, capture *retinav1alpha1.Capture) error { +// resolveHostPath validates the user-supplied HostPath subpath against the +// operator-configured base directory and returns the resolved on-node path. +// Returns ("", nil) when no HostPath is configured (HostPath is nil). An +// explicit empty-string HostPath is rejected via validateHostPath so user +// misconfiguration surfaces with a clear error instead of being silently +// ignored. +func (translator *CaptureToPodTranslator) resolveHostPath(oc retinav1alpha1.OutputConfiguration) (string, error) { + if oc.HostPath == nil { + return "", nil + } + resolved, err := validateHostPath(*oc.HostPath, translator.config.CaptureHostPathBaseDir) + if err != nil { + return "", fmt.Errorf("invalid OutputConfiguration.HostPath: %w", err) + } + return resolved, nil +} + +func (translator *CaptureToPodTranslator) initJobTemplate(ctx context.Context, capture *retinav1alpha1.Capture, resolvedHostPath string) error { backoffLimit := int32(0) // NOTE(mainred): We allow the capture pod to run for at most 30 minutes before being deleted to ensure the output is // uploaded, and this happens when the user want to stop a capture on demand by deleting the capture. captureTerminationGracePeriodSeconds := int64(1800) + translator.jobTemplate = &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ GenerateName: fmt.Sprintf("%s-", capture.Name), @@ -124,8 +166,9 @@ func (translator *CaptureToPodTranslator) initJobTemplate(ctx context.Context, c BackoffLimit: &backoffLimit, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Labels: captureUtils.GetContainerLabelsFromCaptureName(capture.Name), - Namespace: capture.Namespace, + Labels: captureUtils.GetContainerLabelsFromCaptureName(capture.Name), + Namespace: capture.Namespace, + Annotations: captureUtils.GetPodAnnotationsFromCapture(capture, resolvedHostPath), }, Spec: corev1.PodSpec{ HostNetwork: true, @@ -198,16 +241,15 @@ func (translator *CaptureToPodTranslator) initJobTemplate(ctx context.Context, c }, } - if capture.Spec.OutputConfiguration.HostPath != nil && *capture.Spec.OutputConfiguration.HostPath != "" { - translator.l.Info("HostPath is not empty", zap.String("HostPath", *capture.Spec.OutputConfiguration.HostPath)) + if resolvedHostPath != "" { + translator.l.Info("HostPath is not empty", zap.String("HostPath", *capture.Spec.OutputConfiguration.HostPath), zap.String("ResolvedHostPath", resolvedHostPath)) captureFolderHostPathType := corev1.HostPathDirectoryOrCreate - hostPath := *capture.Spec.OutputConfiguration.HostPath hostPathVolume := corev1.Volume{ Name: captureConstants.CaptureHostPathVolumeName, VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{ - Path: hostPath, + Path: resolvedHostPath, Type: &captureFolderHostPathType, }, }, @@ -216,7 +258,7 @@ func (translator *CaptureToPodTranslator) initJobTemplate(ctx context.Context, c hostPathVolumeMount := corev1.VolumeMount{ Name: captureConstants.CaptureHostPathVolumeName, - MountPath: hostPath, + MountPath: resolvedHostPath, } translator.jobTemplate.Spec.Template.Spec.Containers[0].VolumeMounts = append(translator.jobTemplate.Spec.Template.Spec.Containers[0].VolumeMounts, hostPathVolumeMount) } @@ -354,15 +396,23 @@ func (translator *CaptureToPodTranslator) TranslateCaptureToJobs(ctx context.Con return nil, err } - if err := translator.initJobTemplate(ctx, capture); err != nil { + // Resolve the HostPath once and reuse it for both the job template (pod + // annotation + volume mount) and the capture workload env. + resolvedHostPath, hpErr := translator.resolveHostPath(capture.Spec.OutputConfiguration) + if hpErr != nil { + translator.l.Error("Rejected HostPath in Capture", zap.Error(hpErr), zap.String("HostPath", *capture.Spec.OutputConfiguration.HostPath)) + return nil, hpErr + } + + if err := translator.initJobTemplate(ctx, capture, resolvedHostPath); err != nil { return nil, err } - captureTargetOnNode, err := translator.CalculateCaptureTargetsOnNode(ctx, capture.Spec.CaptureConfiguration.CaptureTarget) + captureTargetOnNode, err := translator.CalculateCaptureTargetsOnNode(ctx, capture.Spec.CaptureConfiguration.CaptureTarget, capture.Namespace) if err != nil { return nil, err } - jobPodEnv, err := translator.ObtainCaptureJobPodEnv(*capture) + jobPodEnv, err := translator.obtainCaptureJobPodEnv(*capture, resolvedHostPath) if err != nil { return nil, err } @@ -384,9 +434,15 @@ func (translator *CaptureToPodTranslator) renderJob(captureTargetOnNode *Capture return nil, fmt.Errorf("no nodes are selected") } - captureStartTimestamp := file.Now() + stringTimestamp := translator.jobTemplate.Spec.Template.ObjectMeta.Annotations[captureConstants.CaptureTimestampAnnotationKey] + captureTimestamp, err := file.StringToTime(stringTimestamp) + if err != nil { + return nil, fmt.Errorf("failed to parse capture start timestamp: %w", err) + } - printOutputFileNames(captureTargetOnNode, envCommon, &captureStartTimestamp) + fmt.Println("#########################") + fmt.Println("Expected Capture Files") + fmt.Println("#########################") jobs := make([]*batchv1.Job, 0, len(*captureTargetOnNode)) for nodeName, target := range *captureTargetOnNode { @@ -395,6 +451,14 @@ func (translator *CaptureToPodTranslator) renderJob(captureTargetOnNode *Capture jobEnv[k] = v } job := translator.jobTemplate.DeepCopy() + captureFilename := &file.CaptureFilename{ + CaptureName: envCommon[captureConstants.CaptureNameEnvKey], + NodeHostname: nodeName, + StartTimestamp: captureTimestamp, + } + job.Spec.Template.ObjectMeta.Annotations[captureConstants.CaptureFilenameAnnotationKey] = captureFilename.String() + + fmt.Printf("%s.tar.gz\n", captureFilename.String()) job.Spec.Template.Spec.Affinity = &corev1.Affinity{ NodeAffinity: &corev1.NodeAffinity{ @@ -454,30 +518,15 @@ func (translator *CaptureToPodTranslator) renderJob(captureTargetOnNode *Capture job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{Name: k, Value: v}) } job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{Name: captureConstants.NodeHostNameEnvKey, Value: nodeName}) - job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{Name: captureConstants.CaptureStartTimestampEnvKey, Value: captureStartTimestamp.String()}) + job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{Name: captureConstants.CaptureStartTimestampEnvKey, Value: stringTimestamp}) jobs = append(jobs, job) } - return jobs, nil -} - -func printOutputFileNames(captureTargetOnNode *CaptureTargetsOnNode, envCommon map[string]string, timestamp *file.Timestamp) { - captureFileNames := []string{} - for k := range *captureTargetOnNode { - capture := file.CaptureFilename{CaptureName: envCommon[captureConstants.CaptureNameEnvKey], NodeHostname: k, StartTimestamp: timestamp} - captureFileNames = append(captureFileNames, capture.String()) - } - fmt.Println("#########################") - fmt.Println("Expected Capture Files") - fmt.Println("#########################") - - for _, v := range captureFileNames { - fmt.Printf("%s.tar.gz\n", v) - } - fmt.Println("\nNote: The file(s) may not be created if the capture job(s) fail prematurely.") fmt.Println("#########################") + + return jobs, nil } func updateTcpdumpFilterWithPodIPAddress(podIPAddresses []string, tcpdumpFilter string) string { @@ -540,12 +589,16 @@ func getNetshFilterWithPodIPAddress(podIPAddresses []string) string { // validateTargetSelector validate target selectors defined in the capture. func (translator *CaptureToPodTranslator) validateTargetSelector(captureTarget retinav1alpha1.CaptureTarget) error { // When NamespaceSelector is nil while PodSelector is specified, the namespace will be determined by capture.Namespace. - if captureTarget.NodeSelector == nil && captureTarget.PodSelector == nil { - return fmt.Errorf("Neither NodeSelector nor NamespaceSelector&PodSelector is set.") + if captureTarget.NodeSelector == nil && captureTarget.PodSelector == nil && len(captureTarget.PodNames) == 0 { + return errNoValidSelector } - if captureTarget.NodeSelector != nil && (captureTarget.NamespaceSelector != nil || captureTarget.PodSelector != nil) { - return fmt.Errorf("NodeSelector is not compatible with NamespaceSelector&PodSelector. Please use one or the other.") + if captureTarget.NodeSelector != nil && (captureTarget.NamespaceSelector != nil || captureTarget.PodSelector != nil || len(captureTarget.PodNames) > 0) { + return errNodeSelectorIncompat + } + + if len(captureTarget.PodNames) > 0 && (captureTarget.NamespaceSelector != nil || captureTarget.PodSelector != nil) { + return errPodNamesIncompat } return nil @@ -567,10 +620,14 @@ func (translator *CaptureToPodTranslator) validateCapture(capture *retinav1alpha capture.Spec.OutputConfiguration.S3Upload == nil { return fmt.Errorf("At least one output configuration should be set") } + + if _, err := translator.resolveHostPath(capture.Spec.OutputConfiguration); err != nil { + return err + } return nil } -func (translator *CaptureToPodTranslator) getCaptureTargetsOnNode(ctx context.Context, captureTarget retinav1alpha1.CaptureTarget) (*CaptureTargetsOnNode, error) { +func (translator *CaptureToPodTranslator) getCaptureTargetsOnNode(ctx context.Context, captureTarget retinav1alpha1.CaptureTarget, namespace string) (*CaptureTargetsOnNode, error) { var err error captureTargetsOnNode := &CaptureTargetsOnNode{} if captureTarget.NodeSelector != nil { @@ -583,9 +640,14 @@ func (translator *CaptureToPodTranslator) getCaptureTargetsOnNode(ctx context.Co return nil, err } } + if len(captureTarget.PodNames) > 0 { + if captureTargetsOnNode, err = translator.calculateCaptureTargetsByPodNames(ctx, captureTarget, namespace); err != nil { + return nil, err + } + } if len(*captureTargetsOnNode) == 0 { - return nil, fmt.Errorf("no targets are selected by node selector or pod selector") + return nil, errNoTargetsSelected } return captureTargetsOnNode, nil } @@ -619,12 +681,12 @@ func (translator *CaptureToPodTranslator) updateCaptureTargetsOSOnNode(ctx conte } // CalculateCaptureTargetsOnNode returns capture target on each node. -func (translator *CaptureToPodTranslator) CalculateCaptureTargetsOnNode(ctx context.Context, captureTarget retinav1alpha1.CaptureTarget) (*CaptureTargetsOnNode, error) { +func (translator *CaptureToPodTranslator) CalculateCaptureTargetsOnNode(ctx context.Context, captureTarget retinav1alpha1.CaptureTarget, namespace string) (*CaptureTargetsOnNode, error) { if err := translator.validateTargetSelector(captureTarget); err != nil { return nil, err } - captureTargetsOnNode, err := translator.getCaptureTargetsOnNode(ctx, captureTarget) + captureTargetsOnNode, err := translator.getCaptureTargetsOnNode(ctx, captureTarget, namespace) if err != nil { return nil, err } @@ -713,6 +775,30 @@ func (translator *CaptureToPodTranslator) calculateCaptureTargetsByPodSelector(c return captureTargetOnNode, nil } +func (translator *CaptureToPodTranslator) calculateCaptureTargetsByPodNames(ctx context.Context, captureTarget retinav1alpha1.CaptureTarget, namespace string) (*CaptureTargetsOnNode, error) { + captureTargetOnNode := &CaptureTargetsOnNode{} + + // Get the pods by their names from the specified namespace + for _, podName := range captureTarget.PodNames { + pod, err := translator.kubeClient.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{}) + if err != nil { + translator.l.Error("Failed to get Pod by name", zap.String("podName", podName), zap.String("namespace", namespace), zap.Error(err)) + return nil, fmt.Errorf("failed to get pod %s in namespace %s: %w", podName, namespace, err) + } + + // We want to include all the ip addresses assigned to the Pod in case the Pod is selected. + // This may happen when a pod is allocated with IPv4 and IPv6 addresses. + // And Pod.Status.PodIPs must include pod.Status.PodIP. + podIPs := []string{} + for _, podIP := range pod.Status.PodIPs { + podIPs = append(podIPs, podIP.IP) + } + captureTargetOnNode.AddPod(pod.Spec.NodeName, podIPs) + } + + return captureTargetOnNode, nil +} + // For tcpdump, we put each filter(ip:port) into parentheses, and all include filters will be grouped in parentheses, // same case for exclude filters, finally the filters overall will be like: // ((include1) or (include2)) and not ((exclude1) or (exclude2)) @@ -903,10 +989,15 @@ func (translator *CaptureToPodTranslator) obtainTcpdumpFilters(captureConfig ret return tcpdumpFilter, nil } -func (translator *CaptureToPodTranslator) obtainCaptureOutputEnv(outputConfiguration retinav1alpha1.OutputConfiguration) (map[captureConstants.CaptureOutputLocationEnvKey]string, error) { +func (translator *CaptureToPodTranslator) obtainCaptureOutputEnv( + outputConfiguration retinav1alpha1.OutputConfiguration, + resolvedHostPath string, +) (map[captureConstants.CaptureOutputLocationEnvKey]string, error) { outputEnv := map[captureConstants.CaptureOutputLocationEnvKey]string{} - if outputConfiguration.HostPath != nil { - outputEnv[captureConstants.CaptureOutputLocationEnvKeyHostPath] = *outputConfiguration.HostPath + if resolvedHostPath != "" { + // Emit the resolved (joined, cleaned) path so the workload writes where + // the HostPath volume is actually mounted in the capture pod. + outputEnv[captureConstants.CaptureOutputLocationEnvKeyHostPath] = resolvedHostPath } if outputConfiguration.PersistentVolumeClaim != nil { outputEnv[captureConstants.CaptureOutputLocationEnvKeyPersistentVolumeClaim] = *outputConfiguration.PersistentVolumeClaim @@ -934,14 +1025,25 @@ func (translator *CaptureToPodTranslator) obtainCaptureOptionEnv(option retinav1 if option.MaxCaptureSize != nil { outputEnv[captureConstants.CaptureMaxSizeEnvKey] = strconv.Itoa(*option.MaxCaptureSize) } + if len(option.Interfaces) > 0 { + outputEnv[captureConstants.CaptureInterfacesEnvKey] = strings.Join(option.Interfaces, ",") + } return outputEnv, nil } // ObtainCaptureJobPodEnv translates Capture object to Environment variables to capture job Pod. func (translator *CaptureToPodTranslator) ObtainCaptureJobPodEnv(capture retinav1alpha1.Capture) (map[string]string, error) { + resolvedHostPath, err := translator.resolveHostPath(capture.Spec.OutputConfiguration) + if err != nil { + return nil, err + } + return translator.obtainCaptureJobPodEnv(capture, resolvedHostPath) +} + +func (translator *CaptureToPodTranslator) obtainCaptureJobPodEnv(capture retinav1alpha1.Capture, resolvedHostPath string) (map[string]string, error) { jobPodEnv := map[string]string{} - captureOutputEnv, err := translator.obtainCaptureOutputEnv(capture.Spec.OutputConfiguration) + captureOutputEnv, err := translator.obtainCaptureOutputEnv(capture.Spec.OutputConfiguration, resolvedHostPath) if err != nil { return nil, err } @@ -969,10 +1071,67 @@ func (translator *CaptureToPodTranslator) ObtainCaptureJobPodEnv(capture retinav jobPodEnv[captureConstants.PacketSizeEnvKey] = strconv.Itoa(*capture.Spec.CaptureConfiguration.CaptureOption.PacketSize) } + if capture.Spec.CaptureConfiguration.CaptureOption.PcapFilter != nil { + jobPodEnv[captureConstants.PcapFilterEnvKey] = *capture.Spec.CaptureConfiguration.CaptureOption.PcapFilter + } + if capture.Spec.CaptureConfiguration.TcpdumpFilter != nil { jobPodEnv[captureConstants.TcpdumpRawFilterEnvKey] = *capture.Spec.CaptureConfiguration.TcpdumpFilter } + // Build tcpdump flags from CaptureOption boolean fields using the mapping table + var tcpdumpFlags []string + opt := &capture.Spec.CaptureConfiguration.CaptureOption + for _, mapping := range tcpdumpFlagMappings { + if boolVal := mapping.getBool(opt); boolVal != nil && *boolVal { + tcpdumpFlags = append(tcpdumpFlags, mapping.flag) + } + } + + // Handle enum fields for verbosity, print data format, and timestamp format + if opt.Verbosity != nil && *opt.Verbosity != "" { + switch *opt.Verbosity { + case "verbose": + tcpdumpFlags = append(tcpdumpFlags, "-v") + case "extra": + tcpdumpFlags = append(tcpdumpFlags, "-vv") + case "max": + tcpdumpFlags = append(tcpdumpFlags, "-vvv") + } + } + + if opt.PrintDataFormat != nil && *opt.PrintDataFormat != "" { + switch *opt.PrintDataFormat { + case "hex": + tcpdumpFlags = append(tcpdumpFlags, "-x") + case "hex-with-link": + tcpdumpFlags = append(tcpdumpFlags, "-xx") + case "ascii": + tcpdumpFlags = append(tcpdumpFlags, "-A") + case "ascii-with-link": + tcpdumpFlags = append(tcpdumpFlags, "-AA") + } + } + + if opt.TimestampFormat != nil && *opt.TimestampFormat != "" { + switch *opt.TimestampFormat { + case "none": + tcpdumpFlags = append(tcpdumpFlags, "-t") + case "unformatted": + tcpdumpFlags = append(tcpdumpFlags, "-tt") + case "delta": + tcpdumpFlags = append(tcpdumpFlags, "-ttt") + case "date": + tcpdumpFlags = append(tcpdumpFlags, "-tttt") + case "delta-since-first": + tcpdumpFlags = append(tcpdumpFlags, "-ttttt") + } + } + + if len(tcpdumpFlags) > 0 { + jobPodEnv[captureConstants.TcpdumpFlagsEnvKey] = strings.Join(tcpdumpFlags, " ") + } + if len(capture.Name) != 0 { jobPodEnv[captureConstants.CaptureNameEnvKey] = capture.Name } diff --git a/pkg/capture/crd_to_job_test.go b/pkg/capture/crd_to_job_test.go index 4113ee0be1..9291590db8 100644 --- a/pkg/capture/crd_to_job_test.go +++ b/pkg/capture/crd_to_job_test.go @@ -4,6 +4,7 @@ package capture import ( + "context" "fmt" "strconv" "testing" @@ -44,6 +45,7 @@ func NewCaptureToPodTranslatorForTest(kubeClient kubernetes.Interface) *CaptureT CaptureImageVersion: "v0.0.1-pre", CaptureImageVersionSource: captureUtils.VersionSourceOperatorImageVersion, CaptureJobNumLimit: 10, + CaptureHostPathBaseDir: "/tmp", } captureToPodTranslator := NewCaptureToPodTranslator(kubeClient, log.Logger().Named("test"), config) @@ -396,7 +398,7 @@ func Test_CaptureToPodTranslator_GetCaptureTargetsOnNode(t *testing.T) { k8sClient := fakeclientset.NewSimpleClientset(objects...) captureToPodTranslator := NewCaptureToPodTranslatorForTest(k8sClient) - gotCaptureTargetsOnNode, err := captureToPodTranslator.getCaptureTargetsOnNode(ctx, tt.captureTarget) + gotCaptureTargetsOnNode, err := captureToPodTranslator.getCaptureTargetsOnNode(ctx, tt.captureTarget, "default") if tt.wantErr != (err != nil) { t.Errorf("getCaptureTargetsOnNode() want(%t) error, got error %s", tt.wantErr, err) } @@ -490,12 +492,23 @@ func Test_CaptureToPodTranslator_ObtainCaptureJobPodEnv(t *testing.T) { capture: retinav1alpha1.Capture{}, wantErr: true, }, + { + name: "explicit empty hostpath is rejected", + capture: retinav1alpha1.Capture{ + Spec: retinav1alpha1.CaptureSpec{ + OutputConfiguration: retinav1alpha1.OutputConfiguration{ + HostPath: pointerUtil.String(""), + }, + }, + }, + wantErr: true, + }, { name: "use hostpath", capture: retinav1alpha1.Capture{ Spec: retinav1alpha1.CaptureSpec{ OutputConfiguration: retinav1alpha1.OutputConfiguration{ - HostPath: pointerUtil.String("/tmp/capture"), + HostPath: pointerUtil.String("capture"), }, }, }, @@ -569,6 +582,113 @@ func Test_CaptureToPodTranslator_ObtainCaptureJobPodEnv(t *testing.T) { captureConstants.PacketSizeEnvKey: strconv.Itoa(packetSize), }, }, + { + name: "pcapFilter", + capture: retinav1alpha1.Capture{ + Spec: retinav1alpha1.CaptureSpec{ + OutputConfiguration: retinav1alpha1.OutputConfiguration{ + PersistentVolumeClaim: pointerUtil.String("capture-pvc"), + }, + CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ + CaptureOption: retinav1alpha1.CaptureOption{ + PcapFilter: pointerUtil.String("tcp port 80"), + }, + }, + }, + }, + wantJobEnv: map[string]string{ + string(captureConstants.CaptureOutputLocationEnvKeyPersistentVolumeClaim): "capture-pvc", + captureConstants.IncludeMetadataEnvKey: "false", + captureConstants.PcapFilterEnvKey: "tcp port 80", + }, + }, + { + name: "deprecated tcpdumpFilter", + capture: retinav1alpha1.Capture{ + Spec: retinav1alpha1.CaptureSpec{ + OutputConfiguration: retinav1alpha1.OutputConfiguration{ + PersistentVolumeClaim: pointerUtil.String("capture-pvc"), + }, + CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ + TcpdumpFilter: pointerUtil.String("udp port 53"), + }, + }, + }, + wantJobEnv: map[string]string{ + string(captureConstants.CaptureOutputLocationEnvKeyPersistentVolumeClaim): "capture-pvc", + captureConstants.IncludeMetadataEnvKey: "false", + captureConstants.TcpdumpRawFilterEnvKey: "udp port 53", + }, + }, + { + name: "tcpdump boolean flags", + capture: retinav1alpha1.Capture{ + Spec: retinav1alpha1.CaptureSpec{ + OutputConfiguration: retinav1alpha1.OutputConfiguration{ + PersistentVolumeClaim: pointerUtil.String("capture-pvc"), + }, + CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ + CaptureOption: retinav1alpha1.CaptureOption{ + NoPromiscuous: pointerUtil.Bool(true), + Verbosity: pointerUtil.String("verbose"), + }, + }, + }, + }, + wantJobEnv: map[string]string{ + string(captureConstants.CaptureOutputLocationEnvKeyPersistentVolumeClaim): "capture-pvc", + captureConstants.IncludeMetadataEnvKey: "false", + captureConstants.TcpdumpFlagsEnvKey: "-p -v", + }, + }, + { + name: "multiple tcpdump boolean flags", + capture: retinav1alpha1.Capture{ + Spec: retinav1alpha1.CaptureSpec{ + OutputConfiguration: retinav1alpha1.OutputConfiguration{ + PersistentVolumeClaim: pointerUtil.String("capture-pvc"), + }, + CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ + CaptureOption: retinav1alpha1.CaptureOption{ + NoPromiscuous: pointerUtil.Bool(true), + NoResolveDNS: pointerUtil.Bool(true), + Verbosity: pointerUtil.String("verbose"), + PrintDataFormat: pointerUtil.String("hex"), + AbsoluteSeq: pointerUtil.Bool(true), + TimestampFormat: pointerUtil.String("none"), + }, + }, + }, + }, + wantJobEnv: map[string]string{ + string(captureConstants.CaptureOutputLocationEnvKeyPersistentVolumeClaim): "capture-pvc", + captureConstants.IncludeMetadataEnvKey: "false", + captureConstants.TcpdumpFlagsEnvKey: "-p -n -S -v -x -t", + }, + }, + { + name: "pcapFilter and boolean flags combined", + capture: retinav1alpha1.Capture{ + Spec: retinav1alpha1.CaptureSpec{ + OutputConfiguration: retinav1alpha1.OutputConfiguration{ + PersistentVolumeClaim: pointerUtil.String("capture-pvc"), + }, + CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ + CaptureOption: retinav1alpha1.CaptureOption{ + PcapFilter: pointerUtil.String("tcp port 443"), + NoPromiscuous: pointerUtil.Bool(true), + Verbosity: pointerUtil.String("verbose"), + }, + }, + }, + }, + wantJobEnv: map[string]string{ + string(captureConstants.CaptureOutputLocationEnvKeyPersistentVolumeClaim): "capture-pvc", + captureConstants.IncludeMetadataEnvKey: "false", + captureConstants.PcapFilterEnvKey: "tcp port 443", + captureConstants.TcpdumpFlagsEnvKey: "-p -v", + }, + }, } for _, tt := range cases { @@ -679,12 +799,22 @@ func Test_CaptureToPodTranslator_RenderJob_NodeSelected(t *testing.T) { k8sClient := fakeclientset.NewSimpleClientset() log.SetupZapLogger(log.GetDefaultLogOpts()) captureToPodTranslator := NewCaptureToPodTranslatorForTest(k8sClient) + + startTime := time.Now() + + hostPath := "capture" // nolint:goconst // Test case needs a var + err := captureToPodTranslator.initJobTemplate(ctx, &retinav1alpha1.Capture{ Spec: retinav1alpha1.CaptureSpec{ CaptureConfiguration: retinav1alpha1.CaptureConfiguration{}, - OutputConfiguration: retinav1alpha1.OutputConfiguration{}, + OutputConfiguration: retinav1alpha1.OutputConfiguration{ + HostPath: &hostPath, + }, + }, + Status: retinav1alpha1.CaptureStatus{ + StartTime: &metav1.Time{Time: startTime}, }, - }) + }, "/tmp/"+hostPath) if err != nil { t.Errorf("initJobTemplate() want no error, got error %s", err) } @@ -715,7 +845,7 @@ func Test_CaptureToPodTranslator_RenderJob_NodeSelected(t *testing.T) { func Test_CaptureToPodTranslator_ValidateCapture(t *testing.T) { captureName := "capture-test" - hostPath := "/tmp/capture" + hostPath := "capture" nodeName := "node-name" cases := []struct { name string @@ -814,6 +944,55 @@ func Test_CaptureToPodTranslator_ValidateCapture(t *testing.T) { }, } + // Additional negative cases for HostPath validation; share the rest of the spec. + captureSpecWithHostPath := func(hp string) retinav1alpha1.Capture { + return retinav1alpha1.Capture{ + ObjectMeta: metav1.ObjectMeta{Name: captureName}, + Spec: retinav1alpha1.CaptureSpec{ + CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ + CaptureTarget: retinav1alpha1.CaptureTarget{ + NodeSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"nodename": nodeName}, + }, + }, + CaptureOption: retinav1alpha1.CaptureOption{ + Duration: &metav1.Duration{Duration: 10 * time.Second}, + }, + }, + OutputConfiguration: retinav1alpha1.OutputConfiguration{HostPath: &hp}, + }, + } + } + cases = append(cases, + struct { + name string + capture retinav1alpha1.Capture + wantErr bool + }{ + name: "raise error when HostPath is absolute", + capture: captureSpecWithHostPath("/tmp/retina"), + wantErr: true, + }, + struct { + name string + capture retinav1alpha1.Capture + wantErr bool + }{ + name: "raise error when HostPath contains traversal", + capture: captureSpecWithHostPath("foo/../bar"), + wantErr: true, + }, + struct { + name string + capture retinav1alpha1.Capture + wantErr bool + }{ + name: "raise error when HostPath uses parent segment", + capture: captureSpecWithHostPath("../etc"), + wantErr: true, + }, + ) + for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { k8sClient := fakeclientset.NewSimpleClientset() @@ -838,12 +1017,13 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { defer cancel() captureName := "capture-test" - hostPath := "/tmp/capture" + hostPath := "capture" + resolvedHostPath := "/tmp/capture" timestamp := file.Now() pvc := "capture-pvc" backoffLimit := int32(0) rootUser := int64(0) - tcpdumpFilter := "-i eth0" + tcpdumpFilter := "tcp port 443" captureFolderHostPathType := corev1.HostPathDirectoryOrCreate commonJob := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ @@ -861,6 +1041,14 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { label.CaptureNameLabel: captureName, label.AppLabel: captureConstants.CaptureContainername, }, + Annotations: map[string]string{ + captureConstants.CaptureFilenameAnnotationKey: (&file.CaptureFilename{ + CaptureName: captureName, + NodeHostname: "node1", + StartTimestamp: timestamp, + }).String(), + captureConstants.CaptureTimestampAnnotationKey: file.TimeToString(timestamp), + }, }, Spec: corev1.PodSpec{ HostNetwork: true, @@ -961,6 +1149,9 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: captureName, }, + Status: retinav1alpha1.CaptureStatus{ + StartTime: timestamp, + }, Spec: retinav1alpha1.CaptureSpec{ CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ CaptureTarget: retinav1alpha1.CaptureTarget{ @@ -987,7 +1178,7 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { volumeMounts: []corev1.VolumeMount{ { Name: captureConstants.CaptureHostPathVolumeName, - MountPath: hostPath, + MountPath: resolvedHostPath, }, }, volumes: []corev1.Volume{ @@ -995,7 +1186,7 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { Name: captureConstants.CaptureHostPathVolumeName, VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{ - Path: hostPath, + Path: resolvedHostPath, Type: &captureFolderHostPathType, }, }, @@ -1003,9 +1194,9 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { }, podEnv: []v1.EnvVar{ {Name: captureConstants.CaptureDurationEnvKey, Value: "1m0s"}, - {Name: string(captureConstants.CaptureOutputLocationEnvKeyHostPath), Value: hostPath}, + {Name: string(captureConstants.CaptureOutputLocationEnvKeyHostPath), Value: resolvedHostPath}, {Name: captureConstants.CaptureNameEnvKey, Value: captureName}, - {Name: captureConstants.CaptureStartTimestampEnvKey, Value: timestamp.String()}, + {Name: captureConstants.CaptureStartTimestampEnvKey, Value: file.TimeToString(timestamp)}, {Name: captureConstants.IncludeMetadataEnvKey, Value: "false"}, {Name: captureConstants.NodeHostNameEnvKey, Value: "node1"}, { @@ -1092,6 +1283,9 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: captureName, }, + Status: retinav1alpha1.CaptureStatus{ + StartTime: timestamp, + }, Spec: retinav1alpha1.CaptureSpec{ CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ CaptureTarget: retinav1alpha1.CaptureTarget{ @@ -1134,7 +1328,7 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { podEnv: []v1.EnvVar{ {Name: captureConstants.CaptureDurationEnvKey, Value: "1m0s"}, {Name: captureConstants.CaptureNameEnvKey, Value: captureName}, - {Name: captureConstants.CaptureStartTimestampEnvKey, Value: timestamp.String()}, + {Name: captureConstants.CaptureStartTimestampEnvKey, Value: file.TimeToString(timestamp)}, {Name: captureConstants.IncludeMetadataEnvKey, Value: "false"}, {Name: string(captureConstants.CaptureOutputLocationEnvKeyPersistentVolumeClaim), Value: pvc}, {Name: captureConstants.NodeHostNameEnvKey, Value: "node1"}, @@ -1164,6 +1358,9 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: captureName, }, + Status: retinav1alpha1.CaptureStatus{ + StartTime: timestamp, + }, Spec: retinav1alpha1.CaptureSpec{ CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ CaptureTarget: retinav1alpha1.CaptureTarget{ @@ -1206,7 +1403,7 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { podEnv: []v1.EnvVar{ {Name: captureConstants.CaptureDurationEnvKey, Value: "1m0s"}, {Name: captureConstants.CaptureNameEnvKey, Value: captureName}, - {Name: captureConstants.CaptureStartTimestampEnvKey, Value: timestamp.String()}, + {Name: captureConstants.CaptureStartTimestampEnvKey, Value: file.TimeToString(timestamp)}, {Name: captureConstants.IncludeMetadataEnvKey, Value: "false"}, {Name: string(captureConstants.CaptureOutputLocationEnvKeyPersistentVolumeClaim), Value: pvc}, {Name: captureConstants.NodeHostNameEnvKey, Value: "node1"}, @@ -1235,6 +1432,9 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: captureName, }, + Status: retinav1alpha1.CaptureStatus{ + StartTime: timestamp, + }, Spec: retinav1alpha1.CaptureSpec{ CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ CaptureTarget: retinav1alpha1.CaptureTarget{ @@ -1262,7 +1462,7 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { volumeMounts: []corev1.VolumeMount{ { Name: captureConstants.CaptureHostPathVolumeName, - MountPath: hostPath, + MountPath: resolvedHostPath, }, { Name: captureConstants.CapturePVCVolumeName, @@ -1274,7 +1474,7 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { Name: captureConstants.CaptureHostPathVolumeName, VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{ - Path: hostPath, + Path: resolvedHostPath, Type: &captureFolderHostPathType, }, }, @@ -1290,9 +1490,9 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { }, podEnv: []v1.EnvVar{ {Name: captureConstants.CaptureDurationEnvKey, Value: "1m0s"}, - {Name: string(captureConstants.CaptureOutputLocationEnvKeyHostPath), Value: hostPath}, + {Name: string(captureConstants.CaptureOutputLocationEnvKeyHostPath), Value: resolvedHostPath}, {Name: captureConstants.CaptureNameEnvKey, Value: captureName}, - {Name: captureConstants.CaptureStartTimestampEnvKey, Value: timestamp.String()}, + {Name: captureConstants.CaptureStartTimestampEnvKey, Value: file.TimeToString(timestamp)}, {Name: captureConstants.IncludeMetadataEnvKey, Value: "false"}, {Name: string(captureConstants.CaptureOutputLocationEnvKeyPersistentVolumeClaim), Value: pvc}, {Name: captureConstants.NodeHostNameEnvKey, Value: "node1"}, @@ -1316,11 +1516,14 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { }, }, { - name: "tcpdumpfilter: pod ip adddress and tcpdumpfilter coexist", + name: "tcpdumpfilter: pod ip address and tcpdumpfilter coexist", capture: retinav1alpha1.Capture{ ObjectMeta: metav1.ObjectMeta{ Name: captureName, }, + Status: retinav1alpha1.CaptureStatus{ + StartTime: timestamp, + }, Spec: retinav1alpha1.CaptureSpec{ CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ CaptureTarget: retinav1alpha1.CaptureTarget{ @@ -1356,7 +1559,7 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { volumeMounts: []corev1.VolumeMount{ { Name: captureConstants.CaptureHostPathVolumeName, - MountPath: hostPath, + MountPath: resolvedHostPath, }, }, volumes: []corev1.Volume{ @@ -1364,7 +1567,7 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { Name: captureConstants.CaptureHostPathVolumeName, VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{ - Path: hostPath, + Path: resolvedHostPath, Type: &captureFolderHostPathType, }, }, @@ -1372,12 +1575,12 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { }, podEnv: []v1.EnvVar{ {Name: captureConstants.CaptureDurationEnvKey, Value: "1m0s"}, - {Name: string(captureConstants.CaptureOutputLocationEnvKeyHostPath), Value: hostPath}, + {Name: string(captureConstants.CaptureOutputLocationEnvKeyHostPath), Value: resolvedHostPath}, {Name: captureConstants.CaptureNameEnvKey, Value: captureName}, - {Name: captureConstants.CaptureStartTimestampEnvKey, Value: timestamp.String()}, + {Name: captureConstants.CaptureStartTimestampEnvKey, Value: file.TimeToString(timestamp)}, {Name: captureConstants.IncludeMetadataEnvKey, Value: "false"}, {Name: captureConstants.NodeHostNameEnvKey, Value: "node1"}, - {Name: captureConstants.TcpdumpRawFilterEnvKey, Value: "-i eth0"}, + {Name: captureConstants.TcpdumpRawFilterEnvKey, Value: "tcp port 443"}, {Name: captureConstants.TcpdumpFilterEnvKey, Value: "(host 10.225.0.4)"}, { Name: telemetry.EnvPodName, @@ -1399,6 +1602,9 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: captureName, }, + Status: retinav1alpha1.CaptureStatus{ + StartTime: timestamp, + }, Spec: retinav1alpha1.CaptureSpec{ CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ CaptureTarget: retinav1alpha1.CaptureTarget{ @@ -1433,7 +1639,7 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { volumeMounts: []corev1.VolumeMount{ { Name: captureConstants.CaptureHostPathVolumeName, - MountPath: hostPath, + MountPath: resolvedHostPath, }, }, volumes: []corev1.Volume{ @@ -1441,7 +1647,7 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { Name: captureConstants.CaptureHostPathVolumeName, VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{ - Path: hostPath, + Path: resolvedHostPath, Type: &captureFolderHostPathType, }, }, @@ -1449,9 +1655,9 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { }, podEnv: []v1.EnvVar{ {Name: captureConstants.CaptureDurationEnvKey, Value: "1m0s"}, - {Name: string(captureConstants.CaptureOutputLocationEnvKeyHostPath), Value: hostPath}, + {Name: string(captureConstants.CaptureOutputLocationEnvKeyHostPath), Value: resolvedHostPath}, {Name: captureConstants.CaptureNameEnvKey, Value: captureName}, - {Name: captureConstants.CaptureStartTimestampEnvKey, Value: timestamp.String()}, + {Name: captureConstants.CaptureStartTimestampEnvKey, Value: file.TimeToString(timestamp)}, {Name: captureConstants.IncludeMetadataEnvKey, Value: "false"}, {Name: captureConstants.NodeHostNameEnvKey, Value: "node1"}, {Name: captureConstants.TcpdumpFilterEnvKey, Value: "(host 10.225.0.4)"}, @@ -1475,6 +1681,9 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: captureName, }, + Status: retinav1alpha1.CaptureStatus{ + StartTime: timestamp, + }, Spec: retinav1alpha1.CaptureSpec{ CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ CaptureTarget: retinav1alpha1.CaptureTarget{ @@ -1512,7 +1721,7 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { volumeMounts: []corev1.VolumeMount{ { Name: captureConstants.CaptureHostPathVolumeName, - MountPath: hostPath, + MountPath: resolvedHostPath, }, }, volumes: []corev1.Volume{ @@ -1520,7 +1729,7 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { Name: captureConstants.CaptureHostPathVolumeName, VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{ - Path: hostPath, + Path: resolvedHostPath, Type: &captureFolderHostPathType, }, }, @@ -1528,9 +1737,9 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { }, podEnv: []v1.EnvVar{ {Name: captureConstants.CaptureDurationEnvKey, Value: "1m0s"}, - {Name: string(captureConstants.CaptureOutputLocationEnvKeyHostPath), Value: hostPath}, + {Name: string(captureConstants.CaptureOutputLocationEnvKeyHostPath), Value: resolvedHostPath}, {Name: captureConstants.CaptureNameEnvKey, Value: captureName}, - {Name: captureConstants.CaptureStartTimestampEnvKey, Value: timestamp.String()}, + {Name: captureConstants.CaptureStartTimestampEnvKey, Value: file.TimeToString(timestamp)}, {Name: captureConstants.IncludeMetadataEnvKey, Value: "false"}, {Name: captureConstants.NodeHostNameEnvKey, Value: "node1"}, {Name: captureConstants.TcpdumpFilterEnvKey, Value: "(host 10.225.0.4 or host fd5c:d9f1:79c5:fd83::21e)"}, @@ -1554,6 +1763,9 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: captureName, }, + Status: retinav1alpha1.CaptureStatus{ + StartTime: timestamp, + }, Spec: retinav1alpha1.CaptureSpec{ CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ CaptureTarget: retinav1alpha1.CaptureTarget{ @@ -1591,7 +1803,7 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { volumeMounts: []corev1.VolumeMount{ { Name: captureConstants.CaptureHostPathVolumeName, - MountPath: hostPath, + MountPath: resolvedHostPath, }, }, volumes: []corev1.Volume{ @@ -1599,7 +1811,7 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { Name: captureConstants.CaptureHostPathVolumeName, VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{ - Path: hostPath, + Path: resolvedHostPath, Type: &captureFolderHostPathType, }, }, @@ -1607,9 +1819,9 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { }, podEnv: []v1.EnvVar{ {Name: captureConstants.CaptureDurationEnvKey, Value: "1m0s"}, - {Name: string(captureConstants.CaptureOutputLocationEnvKeyHostPath), Value: hostPath}, + {Name: string(captureConstants.CaptureOutputLocationEnvKeyHostPath), Value: resolvedHostPath}, {Name: captureConstants.CaptureNameEnvKey, Value: captureName}, - {Name: captureConstants.CaptureStartTimestampEnvKey, Value: timestamp.String()}, + {Name: captureConstants.CaptureStartTimestampEnvKey, Value: file.TimeToString(timestamp)}, {Name: captureConstants.IncludeMetadataEnvKey, Value: "false"}, {Name: captureConstants.NodeHostNameEnvKey, Value: "node1"}, {Name: captureConstants.NetshFilterEnvKey, Value: "IPv4.Address=(10.225.0.4) IPv6.Address=(fd5c:d9f1:79c5:fd83::21e)"}, @@ -1629,11 +1841,14 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { isWindows: true, }, { - name: "netshfilter: pod ip adddress and tcpdumpfilter coexist", + name: "netshfilter: pod ip address and tcpdumpfilter coexist", capture: retinav1alpha1.Capture{ ObjectMeta: metav1.ObjectMeta{ Name: captureName, }, + Status: retinav1alpha1.CaptureStatus{ + StartTime: timestamp, + }, Spec: retinav1alpha1.CaptureSpec{ CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ CaptureTarget: retinav1alpha1.CaptureTarget{ @@ -1669,7 +1884,7 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { volumeMounts: []corev1.VolumeMount{ { Name: captureConstants.CaptureHostPathVolumeName, - MountPath: hostPath, + MountPath: resolvedHostPath, }, }, volumes: []corev1.Volume{ @@ -1677,7 +1892,7 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { Name: captureConstants.CaptureHostPathVolumeName, VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{ - Path: hostPath, + Path: resolvedHostPath, Type: &captureFolderHostPathType, }, }, @@ -1685,12 +1900,12 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { }, podEnv: []v1.EnvVar{ {Name: captureConstants.CaptureDurationEnvKey, Value: "1m0s"}, - {Name: string(captureConstants.CaptureOutputLocationEnvKeyHostPath), Value: hostPath}, + {Name: string(captureConstants.CaptureOutputLocationEnvKeyHostPath), Value: resolvedHostPath}, {Name: captureConstants.CaptureNameEnvKey, Value: captureName}, - {Name: captureConstants.CaptureStartTimestampEnvKey, Value: timestamp.String()}, + {Name: captureConstants.CaptureStartTimestampEnvKey, Value: file.TimeToString(timestamp)}, {Name: captureConstants.IncludeMetadataEnvKey, Value: "false"}, {Name: captureConstants.NodeHostNameEnvKey, Value: "node1"}, - {Name: captureConstants.TcpdumpRawFilterEnvKey, Value: "-i eth0"}, + {Name: captureConstants.TcpdumpRawFilterEnvKey, Value: "tcp port 443"}, {Name: captureConstants.NetshFilterEnvKey, Value: "IPv4.Address=(10.225.0.4)"}, { Name: telemetry.EnvPodName, @@ -1737,7 +1952,7 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { for _, env := range tt.podEnv { if env.Name == captureConstants.CaptureStartTimestampEnvKey { - _, err := file.StringToTimestamp(env.Value) + _, err := file.StringToTime(env.Value) if err != nil { t.Errorf("TranslateCaptureToJobs() error with capture timestamp: %v", err) } @@ -1761,6 +1976,10 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs(t *testing.T) { job.Spec.Template.Spec.Containers[0].Command = []string{captureConstants.CaptureContainerEntrypointWin} } + if tt.capture.Spec.OutputConfiguration.HostPath != nil { + job.Spec.Template.Annotations[captureConstants.CaptureHostPathAnnotationKey] = resolvedHostPath + } + cmpOption := cmp.Options{ cmpopts.SortSlices(func(enVar1, enVar2 corev1.EnvVar) bool { return enVar1.Name < enVar2.Name }), cmp.Comparer(func(x, y corev1.EnvVar) bool { @@ -1782,8 +2001,10 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs_JobNumLimit(t *testing.T ctx, cancel := TestContext(t) defer cancel() + timestamp := file.Now() + captureName := "capture-test" - hostPath := "/tmp/capture" + hostPath := "capture" cases := []struct { name string capture retinav1alpha1.Capture @@ -1797,6 +2018,9 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs_JobNumLimit(t *testing.T ObjectMeta: metav1.ObjectMeta{ Name: captureName, }, + Status: retinav1alpha1.CaptureStatus{ + StartTime: timestamp, + }, Spec: retinav1alpha1.CaptureSpec{ CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ CaptureTarget: retinav1alpha1.CaptureTarget{ @@ -1831,6 +2055,9 @@ func Test_CaptureToPodTranslator_TranslateCaptureToJobs_JobNumLimit(t *testing.T ObjectMeta: metav1.ObjectMeta{ Name: captureName, }, + Status: retinav1alpha1.CaptureStatus{ + StartTime: timestamp, + }, Spec: retinav1alpha1.CaptureSpec{ CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ CaptureTarget: retinav1alpha1.CaptureTarget{ @@ -2204,3 +2431,480 @@ func TestGetNetshFilterWithPodIPAddress(t *testing.T) { }) } } + +// Pod Names Tests - Tests for capturing by specific pod names + +func TestValidateTargetSelector_PodNames(t *testing.T) { + cases := []struct { + name string + captureTarget retinav1alpha1.CaptureTarget + wantErr bool + errMsg string + }{ + { + name: "valid pod names only", + captureTarget: retinav1alpha1.CaptureTarget{ + PodNames: []string{"pod1", "pod2"}, + }, + wantErr: false, + }, + { + name: "pod names with node selector should fail", + captureTarget: retinav1alpha1.CaptureTarget{ + NodeSelector: &metav1.LabelSelector{}, + PodNames: []string{"pod1"}, + }, + wantErr: true, + errMsg: "not compatible with", + }, + { + name: "pod names with pod selector should fail", + captureTarget: retinav1alpha1.CaptureTarget{ + PodSelector: &metav1.LabelSelector{}, + PodNames: []string{"pod1"}, + }, + wantErr: true, + errMsg: "not compatible with", + }, + { + name: "pod names with namespace selector should fail", + captureTarget: retinav1alpha1.CaptureTarget{ + NamespaceSelector: &metav1.LabelSelector{}, + PodNames: []string{"pod1"}, + }, + wantErr: true, + errMsg: "not compatible with", + }, + { + name: "node selector only should pass", + captureTarget: retinav1alpha1.CaptureTarget{ + NodeSelector: &metav1.LabelSelector{}, + }, + wantErr: false, + }, + { + name: "neither selector nor pod names should fail", + captureTarget: retinav1alpha1.CaptureTarget{ + PodNames: []string{}, + }, + wantErr: true, + errMsg: "neither", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + kubeClient := fakeclientset.NewClientset() + translator := NewCaptureToPodTranslatorForTest(kubeClient) + err := translator.validateTargetSelector(tc.captureTarget) + if tc.wantErr { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errMsg) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestCalculateCaptureTargetsByPodNames(t *testing.T) { + // Combined test for pod name resolution including basic and edge cases + cases := []struct { + name string + podNames []string + namespace string + pods []*corev1.Pod + wantErr bool + wantTargets map[string][]string // node -> pod IPs + errMsg string + }{ + // Basic cases + { + name: "single pod by name", + podNames: []string{"test-pod-1"}, + namespace: "default", + pods: []*corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{Name: "test-pod-1", Namespace: "default"}, + Spec: corev1.PodSpec{NodeName: "node1"}, + Status: corev1.PodStatus{ + PodIPs: []corev1.PodIP{{IP: "10.0.0.1"}}, + }, + }, + }, + wantErr: false, + wantTargets: map[string][]string{ + "node1": {"10.0.0.1"}, + }, + }, + { + name: "multiple pods by name", + podNames: []string{"test-pod-1", "test-pod-2"}, + namespace: "default", + pods: []*corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{Name: "test-pod-1", Namespace: "default"}, + Spec: corev1.PodSpec{NodeName: "node1"}, + Status: corev1.PodStatus{ + PodIPs: []corev1.PodIP{{IP: "10.0.0.1"}}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "test-pod-2", Namespace: "default"}, + Spec: corev1.PodSpec{NodeName: "node2"}, + Status: corev1.PodStatus{ + PodIPs: []corev1.PodIP{{IP: "10.0.0.2"}}, + }, + }, + }, + wantErr: false, + wantTargets: map[string][]string{ + "node1": {"10.0.0.1"}, + "node2": {"10.0.0.2"}, + }, + }, + { + name: "pod not found", + podNames: []string{"nonexistent-pod"}, + namespace: "default", + pods: []*corev1.Pod{}, + wantErr: true, + errMsg: "failed to get pod", + }, + // Edge cases + { + name: "pod with multiple IP addresses", + podNames: []string{"test-pod-1"}, + namespace: "default", + pods: []*corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{Name: "test-pod-1", Namespace: "default"}, + Spec: corev1.PodSpec{NodeName: "node1"}, + Status: corev1.PodStatus{ + PodIPs: []corev1.PodIP{{IP: "10.0.0.1"}, {IP: "fd00::1"}}, + }, + }, + }, + wantErr: false, + wantTargets: map[string][]string{ + "node1": {"10.0.0.1", "fd00::1"}, + }, + }, + { + name: "pods on same node", + podNames: []string{"test-pod-1", "test-pod-2"}, + namespace: "default", + pods: []*corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{Name: "test-pod-1", Namespace: "default"}, + Spec: corev1.PodSpec{NodeName: "node1"}, + Status: corev1.PodStatus{ + PodIPs: []corev1.PodIP{{IP: "10.0.0.1"}}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "test-pod-2", Namespace: "default"}, + Spec: corev1.PodSpec{NodeName: "node1"}, + Status: corev1.PodStatus{ + PodIPs: []corev1.PodIP{{IP: "10.0.0.2"}}, + }, + }, + }, + wantErr: false, + wantTargets: map[string][]string{ + "node1": {"10.0.0.1", "10.0.0.2"}, + }, + }, + { + name: "pod with no IP addresses", + podNames: []string{"test-pod-1"}, + namespace: "default", + pods: []*corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{Name: "test-pod-1", Namespace: "default"}, + Spec: corev1.PodSpec{NodeName: "node1"}, + Status: corev1.PodStatus{PodIPs: []corev1.PodIP{}}, + }, + }, + wantErr: false, + wantTargets: map[string][]string{ + "node1": {}, + }, + }, + { + name: "multiple pods on different nodes", + podNames: []string{"test-pod-1", "test-pod-2", "test-pod-3"}, + namespace: "default", + pods: []*corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{Name: "test-pod-1", Namespace: "default"}, + Spec: corev1.PodSpec{NodeName: "node1"}, + Status: corev1.PodStatus{ + PodIPs: []corev1.PodIP{{IP: "10.0.0.1"}}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "test-pod-2", Namespace: "default"}, + Spec: corev1.PodSpec{NodeName: "node2"}, + Status: corev1.PodStatus{ + PodIPs: []corev1.PodIP{{IP: "10.0.0.2"}}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "test-pod-3", Namespace: "default"}, + Spec: corev1.PodSpec{NodeName: "node3"}, + Status: corev1.PodStatus{ + PodIPs: []corev1.PodIP{{IP: "10.0.0.3"}}, + }, + }, + }, + wantErr: false, + wantTargets: map[string][]string{ + "node1": {"10.0.0.1"}, + "node2": {"10.0.0.2"}, + "node3": {"10.0.0.3"}, + }, + }, + { + name: "pod with IPv4 and IPv6 addresses", + podNames: []string{"test-pod-1"}, + namespace: "default", + pods: []*corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{Name: "test-pod-1", Namespace: "default"}, + Spec: corev1.PodSpec{NodeName: "node1"}, + Status: corev1.PodStatus{ + PodIPs: []corev1.PodIP{{IP: "10.0.0.1"}, {IP: "2001:db8::1"}, {IP: "fd00::1"}}, + }, + }, + }, + wantErr: false, + wantTargets: map[string][]string{ + "node1": {"10.0.0.1", "2001:db8::1", "fd00::1"}, + }, + }, + { + name: "pod in different namespace", + podNames: []string{"test-pod-1"}, + namespace: "custom-ns", + pods: []*corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{Name: "test-pod-1", Namespace: "custom-ns"}, + Spec: corev1.PodSpec{NodeName: "node1"}, + Status: corev1.PodStatus{ + PodIPs: []corev1.PodIP{{IP: "10.0.0.1"}}, + }, + }, + }, + wantErr: false, + wantTargets: map[string][]string{ + "node1": {"10.0.0.1"}, + }, + }, + { + name: "multiple pods with same IP on different nodes", + podNames: []string{"test-pod-1", "test-pod-2"}, + namespace: "default", + pods: []*corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{Name: "test-pod-1", Namespace: "default"}, + Spec: corev1.PodSpec{NodeName: "node1"}, + Status: corev1.PodStatus{ + PodIPs: []corev1.PodIP{{IP: "10.0.0.1"}}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "test-pod-2", Namespace: "default"}, + Spec: corev1.PodSpec{NodeName: "node2"}, + Status: corev1.PodStatus{ + PodIPs: []corev1.PodIP{{IP: "10.0.0.1"}}, + }, + }, + }, + wantErr: false, + wantTargets: map[string][]string{ + "node1": {"10.0.0.1"}, + "node2": {"10.0.0.1"}, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + objects := make([]runtime.Object, 0, len(tc.pods)) + for _, pod := range tc.pods { + objects = append(objects, pod) + } + kubeClient := fakeclientset.NewClientset(objects...) + translator := NewCaptureToPodTranslatorForTest(kubeClient) + + captureTarget := retinav1alpha1.CaptureTarget{ + PodNames: tc.podNames, + } + + ctx := context.Background() + targets, err := translator.calculateCaptureTargetsByPodNames(ctx, captureTarget, tc.namespace) + + if tc.wantErr { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errMsg) + } else { + require.NoError(t, err) + require.NotNil(t, targets) + + gotTargets := make(map[string][]string) + for nodeName, target := range *targets { + gotTargets[nodeName] = target.PodIpAddresses + } + + if diff := cmp.Diff(tc.wantTargets, gotTargets); diff != "" { + t.Errorf("calculateCaptureTargetsByPodNames() mismatch (-want, +got):\n%s", diff) + } + } + }) + } +} + +func TestGetCaptureTargetsOnNode_WithPodNames(t *testing.T) { + // Combined test for getCaptureTargetsOnNode and CalculateCaptureTargetsOnNode with pod names + cases := []struct { + name string + podNames []string + namespace string + pods []*corev1.Pod + wantErr bool + wantNodeLen int + }{ + { + name: "pod names with valid pods", + podNames: []string{"test-pod-1"}, + namespace: "default", + pods: []*corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-1", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + NodeName: "node1", + }, + Status: corev1.PodStatus{ + PodIPs: []corev1.PodIP{ + {IP: "10.0.0.1"}, + }, + }, + }, + }, + wantErr: false, + wantNodeLen: 1, + }, + { + name: "empty pod names list", + podNames: []string{}, + namespace: "default", + pods: []*corev1.Pod{}, + wantErr: true, + wantNodeLen: 0, + }, + { + name: "multiple valid pod names on different nodes", + podNames: []string{"test-pod-1", "test-pod-2"}, + namespace: "default", + pods: []*corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-1", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + NodeName: "node1", + }, + Status: corev1.PodStatus{ + PodIPs: []corev1.PodIP{ + {IP: "10.0.0.1"}, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-2", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + NodeName: "node2", + }, + Status: corev1.PodStatus{ + PodIPs: []corev1.PodIP{ + {IP: "10.0.0.2"}, + }, + }, + }, + }, + wantErr: false, + wantNodeLen: 2, + }, + { + name: "pods on same node aggregated", + podNames: []string{"test-pod-1", "test-pod-2"}, + namespace: "default", + pods: []*corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-1", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + NodeName: "node1", + }, + Status: corev1.PodStatus{ + PodIPs: []corev1.PodIP{ + {IP: "10.0.0.1"}, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-2", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + NodeName: "node1", + }, + Status: corev1.PodStatus{ + PodIPs: []corev1.PodIP{ + {IP: "10.0.0.2"}, + }, + }, + }, + }, + wantErr: false, + wantNodeLen: 1, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + objects := make([]runtime.Object, 0, len(tc.pods)) + for _, pod := range tc.pods { + objects = append(objects, pod) + } + kubeClient := fakeclientset.NewClientset(objects...) + translator := NewCaptureToPodTranslatorForTest(kubeClient) + + captureTarget := retinav1alpha1.CaptureTarget{ + PodNames: tc.podNames, + } + + ctx := context.Background() + targets, err := translator.getCaptureTargetsOnNode(ctx, captureTarget, tc.namespace) + + if tc.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.NotNil(t, targets) + require.Len(t, *targets, tc.wantNodeLen) + } + }) + } +} diff --git a/pkg/capture/file/capture_filename.go b/pkg/capture/file/capture_filename.go index 70f2bf2eb8..e308eba940 100644 --- a/pkg/capture/file/capture_filename.go +++ b/pkg/capture/file/capture_filename.go @@ -2,15 +2,17 @@ package file import ( "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type CaptureFilename struct { CaptureName string NodeHostname string - StartTimestamp *Timestamp + StartTimestamp *metav1.Time } func (cf *CaptureFilename) String() string { - uniqueName := fmt.Sprintf("%s-%s-%s", cf.CaptureName, cf.NodeHostname, cf.StartTimestamp) + uniqueName := fmt.Sprintf("%s-%s-%s", cf.CaptureName, cf.NodeHostname, TimeToString(cf.StartTimestamp)) return uniqueName } diff --git a/pkg/capture/file/capture_filename_test.go b/pkg/capture/file/capture_filename_test.go new file mode 100644 index 0000000000..fc2327d4da --- /dev/null +++ b/pkg/capture/file/capture_filename_test.go @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package file + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestCaptureFilenameFormat(t *testing.T) { + tests := []struct { + name string + captureName string + nodeHostname string + timestamp *v1.Time + expected string + }{ + { + name: "valid filename", + captureName: "capture-name", + nodeHostname: "node1", + timestamp: &v1.Time{Time: time.Date(2025, 1, 1, 12, 30, 0, 0, time.UTC)}, + expected: "capture-name-node1-20250101123000UTC", + }, + { + name: "different timezone", + captureName: "capture-name", + nodeHostname: "node1", + timestamp: &v1.Time{Time: time.Date(2025, 1, 1, 8, 30, 0, 0, time.FixedZone("EDT", -4*60*60))}, // 8:30 EDT is 12:30 UTC + expected: "capture-name-node1-20250101123000UTC", + }, + { + name: "empty capture name", + captureName: "", + nodeHostname: "node1", + timestamp: &v1.Time{Time: time.Date(2025, 1, 1, 12, 30, 0, 0, time.UTC)}, + expected: "-node1-20250101123000UTC", + }, + { + name: "empty node name", + captureName: "capture-name", + nodeHostname: "", + timestamp: &v1.Time{Time: time.Date(2025, 1, 1, 12, 30, 0, 0, time.UTC)}, + expected: "capture-name--20250101123000UTC", + }, + { + name: "zero time", + captureName: "capture-name", + nodeHostname: "node1", + timestamp: &v1.Time{Time: time.Time{}}, + expected: "capture-name-node1-00010101000000UTC", + }, + { + name: "nil timestamp", + captureName: "capture-name", + nodeHostname: "node1", + timestamp: nil, + expected: "capture-name-node1-00010101000000UTC", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filename := CaptureFilename{ + CaptureName: tt.captureName, + NodeHostname: tt.nodeHostname, + StartTimestamp: tt.timestamp, + } + + // .String() here relies on TimeToString(), which should handle nil timestamps gracefully + // and return a zero time string - this should not panic + var result string + assert.NotPanics(t, func() { + result = filename.String() + }, "CaptureFilename.String() should handle nil timestamp gracefully") + + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/pkg/capture/file/timestamp.go b/pkg/capture/file/timestamp.go index d3725e71e2..946a0fc945 100644 --- a/pkg/capture/file/timestamp.go +++ b/pkg/capture/file/timestamp.go @@ -4,26 +4,30 @@ import ( "time" "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -type Timestamp struct { - time.Time -} - const captureFileNameTimestampFormat string = "20060102150405UTC" -func Now() Timestamp { - return Timestamp{Time: time.Now().UTC().Truncate(time.Second)} -} - -func (timestamp *Timestamp) String() string { - return timestamp.Time.Format(captureFileNameTimestampFormat) +func Now() *metav1.Time { + return &metav1.Time{Time: time.Now().UTC().Truncate(time.Second)} } -func StringToTimestamp(timestamp string) (*Timestamp, error) { +// Converts a string in the capture file name format to metav1.Time +func StringToTime(timestamp string) (*metav1.Time, error) { parsedTime, err := time.Parse(captureFileNameTimestampFormat, timestamp) if err != nil { return nil, errors.Wrap(err, "failed to create timestamp from string") } - return &Timestamp{parsedTime}, nil + return &metav1.Time{Time: parsedTime}, nil +} + +// Converts a metav1.Time to a string in the capture file name format +// Returns a zero time string if timestamp is nil +// Converts to UTC if other timezone is provided +func TimeToString(timestamp *metav1.Time) string { + if timestamp == nil { + return (&metav1.Time{Time: time.Time{}}).Format(captureFileNameTimestampFormat) + } + return timestamp.UTC().Format(captureFileNameTimestampFormat) } diff --git a/pkg/capture/file/timestamp_test.go b/pkg/capture/file/timestamp_test.go new file mode 100644 index 0000000000..21ad85a3bb --- /dev/null +++ b/pkg/capture/file/timestamp_test.go @@ -0,0 +1,185 @@ +package file + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestNow(t *testing.T) { + before := time.Now().UTC().Truncate(time.Second) + result := Now() + after := time.Now().UTC().Truncate(time.Second) + + require.NotNil(t, result) + assert.GreaterOrEqual(t, result.Time, before) + assert.LessOrEqual(t, result.Time, after) + assert.Equal(t, 0, result.Time.Nanosecond()) // ensure timestamp is truncated +} + +func TestStringToTime(t *testing.T) { + tests := []struct { + name string + input string + expected *metav1.Time + wantError bool + }{ + { + name: "valid timestamp", + input: "20250101120000UTC", + expected: &metav1.Time{Time: time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)}, + wantError: false, + }, + { + name: "another valid timestamp", + input: "20251231235959UTC", + expected: &metav1.Time{Time: time.Date(2025, 12, 31, 23, 59, 59, 0, time.UTC)}, + wantError: false, + }, + { + name: "midnight timestamp", + input: "20250101000000UTC", + expected: &metav1.Time{Time: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)}, + wantError: false, + }, + { + name: "invalid format", + input: "2025-01-01 12:00:00", + expected: nil, + wantError: true, + }, + { + name: "empty string", + input: "", + expected: nil, + wantError: true, + }, + { + name: "invalid month", + input: "20251301120000UTC", + expected: nil, + wantError: true, + }, + { + name: "invalid day", + input: "20250132120000UTC", + expected: nil, + wantError: true, + }, + { + name: "invalid hour", + input: "20250101250000UTC", + expected: nil, + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := StringToTime(tt.input) + if tt.wantError { + require.Error(t, err) + assert.Nil(t, result) + assert.Equal(t, tt.expected, result) + } else { + require.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestTimeToString(t *testing.T) { + tests := []struct { + name string + input *metav1.Time + expected string + }{ + { + name: "valid time", + input: &metav1.Time{Time: time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)}, + expected: "20250101120000UTC", + }, + { + name: "invalid month", + input: &metav1.Time{Time: time.Date(2025, 13, 1, 12, 0, 0, 0, time.UTC)}, + expected: "20260101120000UTC", + }, + { + name: "invalid day", + input: &metav1.Time{Time: time.Date(2025, 1, 32, 12, 0, 0, 0, time.UTC)}, + expected: "20250201120000UTC", + }, + { + name: "invalid hour", + input: &metav1.Time{Time: time.Date(2025, 1, 1, 25, 0, 0, 0, time.UTC)}, + expected: "20250102010000UTC", + }, + { + name: "invalid minutes", + input: &metav1.Time{Time: time.Date(2025, 1, 1, 12, 61, 0, 0, time.UTC)}, + expected: "20250101130100UTC", + }, + { + name: "invalid seconds", + input: &metav1.Time{Time: time.Date(2025, 1, 1, 12, 0, 61, 0, time.UTC)}, + expected: "20250101120101UTC", + }, + { + name: "midnight", + input: &metav1.Time{Time: time.Date(2025, 12, 1, 0, 0, 0, 0, time.UTC)}, + expected: "20251201000000UTC", + }, + { + name: "end of day", + input: &metav1.Time{Time: time.Date(2025, 1, 1, 23, 59, 59, 0, time.UTC)}, + expected: "20250101235959UTC", + }, + { + name: "nil time pointer", + input: nil, // Should return a zero time string + expected: "00010101000000UTC", + }, + { + name: "zero time", + input: &metav1.Time{Time: time.Time{}}, // Same as nil case + expected: "00010101000000UTC", + }, + { + name: "time with different timezone", + input: &metav1.Time{Time: time.Date(2025, 6, 15, 14, 30, 45, 0, time.FixedZone("EST", -5*60*60))}, + expected: "20250615193045UTC", // Should be converted to UTC + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := TimeToString(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestTimeToStringAndBack(t *testing.T) { + originalTime := &metav1.Time{Time: time.Date(2025, 1, 1, 12, 30, 0, 0, time.UTC)} + + timeString := TimeToString(originalTime) + parsedTime, err := StringToTime(timeString) + + require.NoError(t, err) + assert.True(t, originalTime.Time.Equal(parsedTime.Time)) +} + +func TestStringToTimeAndBack(t *testing.T) { + originalString := "20250101123000UTC" + + parsedTime, err := StringToTime(originalString) + require.NoError(t, err) + + timeString := TimeToString(parsedTime) + assert.Equal(t, originalString, timeString) +} diff --git a/pkg/capture/hostpath_validation.go b/pkg/capture/hostpath_validation.go new file mode 100644 index 0000000000..e42d6d548f --- /dev/null +++ b/pkg/capture/hostpath_validation.go @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package capture + +import ( + "errors" + "fmt" + "path/filepath" + "regexp" + "strings" +) + +// DefaultHostPathBaseDir is the safe default location under which Capture CRs may +// place captured artifacts on a node when no operator-level base directory is configured. +const DefaultHostPathBaseDir = "/var/log/retina/captures" + +var ( + // ErrHostPathEmpty is returned when the supplied HostPath is empty. + ErrHostPathEmpty = errors.New("hostPath is empty") + // ErrHostPathAbsolute is returned when the supplied HostPath is absolute. The + // CR field is a subpath name only; it must not contain a leading separator + // (POSIX or Windows) or a drive-letter prefix. + ErrHostPathAbsolute = errors.New("hostPath must be a relative subpath name, not an absolute path") + // ErrHostPathTraversal is returned when the supplied HostPath contains a parent-directory traversal. + ErrHostPathTraversal = errors.New("hostPath must not contain '..' path segments") + // ErrHostPathEscapesBase is a defense-in-depth error returned when, after + // joining and cleaning, the resulting path would lie outside the configured base directory. + ErrHostPathEscapesBase = errors.New("hostPath resolves outside the configured base directory") + // ErrHostPathBaseDir is returned when the operator-provided base directory is not usable. + ErrHostPathBaseDir = errors.New("invalid hostPath base directory") +) + +// winDriveLetter matches a Windows drive-letter prefix such as "C:\" or "c:/". +var winDriveLetter = regexp.MustCompile(`^[A-Za-z]:[\\/]`) + +// validateHostPath ensures that the user-supplied HostPath from a Capture CR is safe +// to mount into the privileged capture pod and returns the absolute, cleaned path the +// capture artifacts will live at on the node. +// +// The CR's HostPath is treated as a relative subpath name and joined under baseDir; +// CR authors cannot escape that directory. Rules: +// +// - The path must be non-empty. +// - The path must not be absolute (no leading "/" or "\\", no Windows drive letter). +// - The path must not contain any ".." segment, checked both on the raw input and +// after filepath.Clean. +// - As defense in depth, the joined path must still resolve under baseDir. +// +// If baseDir is empty, DefaultHostPathBaseDir is used. +func validateHostPath(raw, baseDir string) (string, error) { + if raw == "" { + return "", ErrHostPathEmpty + } + + if baseDir == "" { + baseDir = DefaultHostPathBaseDir + } + cleanedBase := filepath.Clean(baseDir) + if !filepath.IsAbs(cleanedBase) { + return "", fmt.Errorf("%w: %q must be absolute", ErrHostPathBaseDir, baseDir) + } + + // Reject absolute paths up front, in both POSIX and Windows styles, so existing + // CRs that supplied an absolute host path fail loudly instead of being silently + // rewritten by filepath.Join. + if filepath.IsAbs(raw) || + strings.HasPrefix(raw, "/") || + strings.HasPrefix(raw, `\`) || + winDriveLetter.MatchString(raw) { + return "", fmt.Errorf("%w: %q", ErrHostPathAbsolute, raw) + } + + // Reject literal ".." segments before cleaning so traversal attempts are + // rejected even if filepath.Clean would normalize them away. + if containsParentSegment(raw) { + return "", fmt.Errorf("%w: %q", ErrHostPathTraversal, raw) + } + + cleanedSub := filepath.Clean(raw) + if cleanedSub == "." || cleanedSub == "" { + return "", ErrHostPathEmpty + } + if filepath.IsAbs(cleanedSub) || strings.HasPrefix(cleanedSub, "/") || strings.HasPrefix(cleanedSub, `\`) { + return "", fmt.Errorf("%w: %q", ErrHostPathAbsolute, raw) + } + if containsParentSegment(cleanedSub) { + return "", fmt.Errorf("%w: %q", ErrHostPathTraversal, raw) + } + + joined := filepath.Clean(filepath.Join(cleanedBase, cleanedSub)) + rel, err := filepath.Rel(cleanedBase, joined) + if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + return "", fmt.Errorf("%w: %q resolves to %q (base %q)", ErrHostPathEscapesBase, raw, joined, cleanedBase) + } + + return joined, nil +} + +// containsParentSegment returns true if any path segment of p (split on either +// forward or back slashes) equals "..". +func containsParentSegment(p string) bool { + for _, seg := range strings.FieldsFunc(p, func(r rune) bool { return r == '/' || r == '\\' }) { + if seg == ".." { + return true + } + } + return false +} diff --git a/pkg/capture/hostpath_validation_test.go b/pkg/capture/hostpath_validation_test.go new file mode 100644 index 0000000000..e845fa28cb --- /dev/null +++ b/pkg/capture/hostpath_validation_test.go @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package capture + +import ( + "errors" + "strings" + "testing" +) + +func TestValidateHostPath(t *testing.T) { + const base = "/var/log/retina/captures" + + tests := []struct { + name string + raw string + baseDir string + want string + wantErr error + }{ + // rejection cases + {name: "empty", raw: "", baseDir: base, wantErr: ErrHostPathEmpty}, + {name: "dot only resolves to empty", raw: ".", baseDir: base, wantErr: ErrHostPathEmpty}, + {name: "absolute posix", raw: "/tmp/retina", baseDir: base, wantErr: ErrHostPathAbsolute}, + {name: "absolute posix with traversal", raw: "/var/log/../etc", baseDir: base, wantErr: ErrHostPathAbsolute}, + {name: "absolute windows backslash", raw: `\tmp\retina`, baseDir: base, wantErr: ErrHostPathAbsolute}, + {name: "absolute windows drive letter", raw: `C:\evil`, baseDir: base, wantErr: ErrHostPathAbsolute}, + {name: "absolute windows drive letter forward", raw: "c:/evil", baseDir: base, wantErr: ErrHostPathAbsolute}, + {name: "traversal raw", raw: "../etc", baseDir: base, wantErr: ErrHostPathTraversal}, + {name: "traversal mid", raw: "foo/../bar", baseDir: base, wantErr: ErrHostPathTraversal}, + {name: "traversal backslash", raw: `foo\..\bar`, baseDir: base, wantErr: ErrHostPathTraversal}, + {name: "traversal escaping base", raw: "../../etc", baseDir: base, wantErr: ErrHostPathTraversal}, + + // acceptance cases + {name: "bare name", raw: "retina", baseDir: base, want: base + "/retina"}, + {name: "nested subpath", raw: "job1/out", baseDir: base, want: base + "/job1/out"}, + {name: "trailing slash cleaned", raw: "job/", baseDir: base, want: base + "/job"}, + {name: "redundant separators cleaned", raw: "job//./out", baseDir: base, want: base + "/job/out"}, + {name: "root base dir accepts subpath", raw: "captures", baseDir: "/", want: "/captures"}, + {name: "root base dir accepts nested subpath", raw: "a/b", baseDir: "/", want: "/a/b"}, + + // defaulting + {name: "default base used when empty", raw: "x", baseDir: "", want: DefaultHostPathBaseDir + "/x"}, + + // invalid base + {name: "relative base rejected", raw: "x", baseDir: "captures", wantErr: ErrHostPathBaseDir}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := validateHostPath(tt.raw, tt.baseDir) + if tt.wantErr != nil { + if !errors.Is(err, tt.wantErr) { + t.Fatalf("expected error %v, got %v", tt.wantErr, err) + } + if got != "" { + t.Fatalf("expected empty result on error, got %q", got) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tt.want { + t.Fatalf("got %q, want %q", got, tt.want) + } + }) + } +} + +// TestValidateHostPath_ResultsAlwaysUnderBase is a property-style assertion: for +// every accepted input across a small enumerated set, the cleaned result must be +// the base or be nested under base + separator. This guards against future +// regressions where validation might let through an input whose joined form +// escapes the base directory. +func TestValidateHostPath_ResultsAlwaysUnderBase(t *testing.T) { + const base = "/var/log/retina/captures" + inputs := []string{ + "a", "a/b", "a/b/c", "x.pcap", "deep/nested/path/name", + "with-hyphen", "with_underscore", "with.dots", + } + for _, in := range inputs { + t.Run(in, func(t *testing.T) { + got, err := validateHostPath(in, base) + if err != nil { + t.Fatalf("unexpected error for %q: %v", in, err) + } + if got != base && !strings.HasPrefix(got, base+"/") { + t.Fatalf("%q resolved to %q which is not under %q", in, got, base) + } + }) + } +} diff --git a/pkg/capture/provider/network_capture_common.go b/pkg/capture/provider/network_capture_common.go index ce2cba04fc..9ec42d3a13 100644 --- a/pkg/capture/provider/network_capture_common.go +++ b/pkg/capture/provider/network_capture_common.go @@ -43,7 +43,7 @@ func (ncpc *NetworkCaptureProviderCommon) networkCaptureCommandLog(logFileName s return nil, err } - if _, err := captureCommandLogFile.WriteString(fmt.Sprintf("%s\n\n", captureCommand.String())); err != nil { + if _, err := fmt.Fprintf(captureCommandLogFile, "%s\n\n", captureCommand.String()); err != nil { ncpc.l.Error("Failed to write capture command to file", zap.String("file", captureCommandLogFile.Name()), zap.Error(err)) } diff --git a/pkg/capture/provider/network_capture_test.go b/pkg/capture/provider/network_capture_test.go index 5a715af59c..b897bd9584 100644 --- a/pkg/capture/provider/network_capture_test.go +++ b/pkg/capture/provider/network_capture_test.go @@ -1,25 +1,40 @@ +//go:build unix + // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. package provider import ( + "context" + "errors" "os" + "os/exec" + "slices" "strings" "testing" - "time" + captureConstants "github.com/microsoft/retina/pkg/capture/constants" "github.com/microsoft/retina/pkg/capture/file" "github.com/microsoft/retina/pkg/log" ) +const ( + testCaptureFilePath = "/tmp/test.pcap" + testCaptureName = "test-capture" + testNodeHostName = "test-node" + interfaceEth0 = "eth0" + interfaceEth1 = "eth1" + interfaceAny = "any" +) + func TestSetupAndCleanup(t *testing.T) { captureName := "capture-test" nodeHostName := "node1" - timestamp := file.Timestamp{Time: time.Now().UTC()} - log.SetupZapLogger(log.GetDefaultLogOpts()) - networkCaptureprovider := &NetworkCaptureProvider{l: log.Logger().Named("test")} - tmpFilename := file.CaptureFilename{CaptureName: captureName, NodeHostname: nodeHostName, StartTimestamp: ×tamp} + timestamp := file.Now() + _, _ = log.SetupZapLogger(log.GetDefaultLogOpts()) + networkCaptureprovider := NewNetworkCaptureProvider(log.Logger().Named("test")) + tmpFilename := file.CaptureFilename{CaptureName: captureName, NodeHostname: nodeHostName, StartTimestamp: timestamp} tmpCaptureLocation, err := networkCaptureprovider.Setup(tmpFilename) // remove temporary capture dir anyway in case Cleanup() fails. @@ -34,11 +49,11 @@ func TestSetupAndCleanup(t *testing.T) { if !strings.Contains(tmpCaptureLocation, nodeHostName) { t.Errorf("Temporary capture dir name %s should contains node host name %s", tmpCaptureLocation, nodeHostName) } - if !strings.Contains(tmpCaptureLocation, timestamp.String()) { + if !strings.Contains(tmpCaptureLocation, file.TimeToString(timestamp)) { t.Errorf("Temporary capture dir name %s should contain timestamp %s", tmpCaptureLocation, timestamp) } - if _, err := os.Stat(tmpCaptureLocation); os.IsNotExist(err) { + if _, statErr := os.Stat(tmpCaptureLocation); os.IsNotExist(statErr) { t.Errorf("Temporary capture dir %s should be created", tmpCaptureLocation) } @@ -51,3 +66,646 @@ func TestSetupAndCleanup(t *testing.T) { t.Errorf("Temporary capture dir %s should be deleted", tmpCaptureLocation) } } + +// Helper function to check if command args contain specific interface +func hasInterface(cmd *exec.Cmd, expectedInterface string) bool { + for i, arg := range cmd.Args { + if arg == "-i" && i+1 < len(cmd.Args) && cmd.Args[i+1] == expectedInterface { + return true + } + } + return false +} + +// Helper function to reset environment variables +func resetEnvVars() { + os.Unsetenv(captureConstants.TcpdumpRawFilterEnvKey) + os.Unsetenv(captureConstants.PcapFilterEnvKey) + os.Unsetenv(captureConstants.PacketSizeEnvKey) + os.Unsetenv(captureConstants.CaptureInterfacesEnvKey) + os.Unsetenv(captureConstants.TcpdumpFlagsEnvKey) +} + +// Helper function to check if tcpdump is available on the system +func requireTcpdump(t *testing.T) { + t.Helper() + if _, err := exec.LookPath("tcpdump"); err != nil { + t.Skipf("tcpdump not available on system: %v", err) + } +} + +// TestTcpdumpEmptyFilter verifies that empty filter falls back to default interface +// and that no unexpected or malicious arguments are injected. +func TestTcpdumpEmptyFilter(t *testing.T) { + resetEnvVars() + cmd := constructTcpdumpCommand(testCaptureFilePath, "") + + // Should fall back to "-i any" + if !hasInterface(cmd, interfaceAny) { + t.Errorf("Expected fallback to '-i any' with empty filter, but got args: %v", cmd.Args) + } + + // Verify only expected args are present and no malicious content + for _, arg := range cmd.Args { + if arg != "tcpdump" && arg != "-w" && arg != testCaptureFilePath && + arg != "--relinquish-privileges=root" && arg != "-i" && arg != interfaceAny { + t.Errorf("Unexpected argument '%s' found in empty filter command: %v", arg, cmd.Args) + } + // Check for malicious content + if strings.Contains(arg, "/etc/passwd") || strings.Contains(arg, "evil") || + strings.Contains(arg, "rm -rf") || strings.HasPrefix(arg, "-z") { + t.Errorf("Malicious content should not be present in command args: %v", cmd.Args) + } + } +} + +func TestTcpdumpWithBPFFilter(t *testing.T) { + resetEnvVars() + // Test that a valid BPF filter is properly added to the tcpdump command + // Note: Filter validation (e.g., rejecting '-' prefix) happens in CaptureNetworkPacket + + bpfFilter := "tcp port 80" + + cmd := constructTcpdumpCommand(testCaptureFilePath, bpfFilter) + + // Should have the BPF filter as an argument + found := slices.Contains(cmd.Args, bpfFilter) + if !found { + t.Errorf("Expected BPF filter '%s' in args, but got: %v", bpfFilter, cmd.Args) + } +} + +func TestTcpdumpSpecificInterfaces(t *testing.T) { + resetEnvVars() + os.Setenv(captureConstants.CaptureInterfacesEnvKey, interfaceEth0+","+interfaceEth1) + defer os.Unsetenv(captureConstants.CaptureInterfacesEnvKey) + + cmd := constructTcpdumpCommand(testCaptureFilePath, "") + + if !hasInterface(cmd, interfaceEth0) { + t.Errorf("Expected tcpdump command to include '-i %s', but got args: %v", interfaceEth0, cmd.Args) + } + if !hasInterface(cmd, interfaceEth1) { + t.Errorf("Expected tcpdump command to include '-i %s', but got args: %v", interfaceEth1, cmd.Args) + } + if hasInterface(cmd, interfaceAny) { + t.Errorf("Expected tcpdump command not to include '-i any' when specific interfaces are set, but got args: %v", cmd.Args) + } +} + +func TestTcpdumpBPFFilterWithSpecificInterfaces(t *testing.T) { + resetEnvVars() + // Verify that BPF filter and specific interface selection work together + // Both should be present in the command (they are independent features) + bpfFilter := "tcp port 443" + os.Setenv(captureConstants.CaptureInterfacesEnvKey, interfaceEth0+","+interfaceEth1) + defer os.Unsetenv(captureConstants.CaptureInterfacesEnvKey) + + cmd := constructTcpdumpCommand(testCaptureFilePath, bpfFilter) + + // The BPF filter should be present + found := slices.Contains(cmd.Args, bpfFilter) + if !found { + t.Errorf("Expected BPF filter '%s' in command, but got args: %v", bpfFilter, cmd.Args) + } + + // Interfaces should still be present (BPF filter doesn't override interface selection) + if !hasInterface(cmd, interfaceEth0) || !hasInterface(cmd, interfaceEth1) { + t.Errorf("Expected both interfaces to be present with BPF filter, but got args: %v", cmd.Args) + } +} + +func TestTcpdumpCommandConstruction(t *testing.T) { + // Default behavior tests + t.Run("EmptyFilter", TestTcpdumpEmptyFilter) + + // Interface selection tests + t.Run("SpecificInterfaceSelection", TestTcpdumpSpecificInterfaces) + t.Run("InterfaceListWithEmptyEntries", TestTcpdumpInterfaceListWithEmptyEntries) + + // BPF filter tests + t.Run("WithBPFFilter", TestTcpdumpWithBPFFilter) + t.Run("BPFFilterWithSpecificInterfaces", TestTcpdumpBPFFilterWithSpecificInterfaces) + t.Run("BPFFilterWithComplexExpression", TestTcpdumpBPFFilterComplexExpression) + t.Run("BPFFilterWithTcpFlags", TestTcpdumpBPFFilterWithTcpFlags) + + // Option tests + t.Run("PacketSizeOption", TestTcpdumpPacketSizeOption) +} + +// TestTcpdumpBPFFilterComplexExpression validates that complex BPF filter expressions +// with multiple keywords and operators are passed as a single argument, not split on spaces. +// This is critical for security - splitting would allow flag injection attacks. +func TestTcpdumpBPFFilterComplexExpression(t *testing.T) { + resetEnvVars() + // Test a complex BPF filter that should remain as one argument + bpfFilter := "tcp and (port 80 or port 443) and host 10.0.0.1" + + cmd := constructTcpdumpCommand(testCaptureFilePath, bpfFilter) + + // The entire filter must appear as a single argument + found := slices.Contains(cmd.Args, bpfFilter) + if !found { + t.Errorf("Expected entire BPF filter '%s' as single argument, but got args: %v", bpfFilter, cmd.Args) + } + + // Verify individual keywords are NOT separate arguments (which would indicate splitting) + splitIndicators := []string{"tcp", "and", "port", "80", "or", "443", "host", "10.0.0.1"} + for _, indicator := range splitIndicators { + for _, arg := range cmd.Args { + if arg == indicator { + t.Errorf("BPF filter was incorrectly split: found '%s' as separate arg in: %v", indicator, cmd.Args) + } + } + } +} + +// TestTcpdumpBPFFilterWithTcpFlags verifies that BPF filters using TCP flag syntax +// with special characters like brackets, pipes, and ampersands are passed correctly. +// Example: tcp[tcpflags] & (tcp-syn|tcp-ack) == tcp-syn +func TestTcpdumpBPFFilterWithTcpFlags(t *testing.T) { + resetEnvVars() + // Test a BPF filter with TCP flags syntax and special characters + bpfFilter := "tcp[tcpflags] & (tcp-syn|tcp-ack) == tcp-syn" + + cmd := constructTcpdumpCommand(testCaptureFilePath, bpfFilter) + + // Positive check: The entire filter must appear as a single argument + found := slices.Contains(cmd.Args, bpfFilter) + if !found { + t.Errorf("Expected entire BPF filter '%s' as single argument, but got args: %v", bpfFilter, cmd.Args) + } + + // Negative check: Verify the filter is not split on spaces (which would indicate incorrect handling) + // These are the pieces that would appear if the filter were split on spaces + splitIndicators := []string{"tcp[tcpflags]", "&", "(tcp-syn|tcp-ack)", "==", "tcp-syn"} + for _, indicator := range splitIndicators { + for _, arg := range cmd.Args { + if arg == indicator { + t.Errorf("BPF filter was incorrectly split: found '%s' as separate arg in: %v", indicator, cmd.Args) + } + } + } +} + +// TestTcpdumpInterfaceListWithEmptyEntries verifies handling of interface lists with empty values +func TestTcpdumpInterfaceListWithEmptyEntries(t *testing.T) { + resetEnvVars() + // Interface list with empty entries and extra spaces + os.Setenv(captureConstants.CaptureInterfacesEnvKey, "eth0, ,eth1,,eth2, ") + defer os.Unsetenv(captureConstants.CaptureInterfacesEnvKey) + + cmd := constructTcpdumpCommand(testCaptureFilePath, "") + + // Should only include non-empty interfaces + if !hasInterface(cmd, interfaceEth0) { + t.Errorf("Expected '-i eth0', but got args: %v", cmd.Args) + } + if !hasInterface(cmd, interfaceEth1) { + t.Errorf("Expected '-i eth1', but got args: %v", cmd.Args) + } + // eth2 should be present + if !hasInterface(cmd, "eth2") { + t.Errorf("Expected '-i eth2', but got args: %v", cmd.Args) + } +} + +// TestTcpdumpPacketSizeOption verifies that packet size option is correctly added +func TestTcpdumpPacketSizeOption(t *testing.T) { + resetEnvVars() + os.Setenv(captureConstants.PacketSizeEnvKey, "1500") + defer os.Unsetenv(captureConstants.PacketSizeEnvKey) + + cmd := constructTcpdumpCommand(testCaptureFilePath, "") + + // Should include -s 1500 + foundS := false + foundSize := false + for i, arg := range cmd.Args { + if arg == "-s" { + foundS = true + if i+1 < len(cmd.Args) && cmd.Args[i+1] == "1500" { + foundSize = true + } + } + } + if !foundS || !foundSize { + t.Errorf("Expected '-s 1500' in tcpdump args, but got: %v", cmd.Args) + } +} + +// TestTcpdumpBPFFilterOnly verifies command construction with BPF filter (no user flags) +func TestTcpdumpBPFFilterOnly(t *testing.T) { + resetEnvVars() + bpfFilter := "tcp port 80" + + cmd := constructTcpdumpCommand(testCaptureFilePath, bpfFilter) + + // BPF filter should be present as the last argument + if !slices.Contains(cmd.Args, bpfFilter) { + t.Errorf("Expected BPF filter '%s' in command args, but got: %v", bpfFilter, cmd.Args) + } + + // Should have basic structure (tcpdump, -w, path, -i, etc.) + if !slices.Contains(cmd.Args, "tcpdump") { + t.Errorf("Expected 'tcpdump' in command args, but got: %v", cmd.Args) + } + if !slices.Contains(cmd.Args, "-w") { + t.Errorf("Expected '-w' in command args, but got: %v", cmd.Args) + } + + // Verify no user-specified flags with '-' prefix (security check) + for _, arg := range cmd.Args { + // Skip our internal flags and the BPF filter + if arg == "-w" || arg == "-i" || arg == "-s" || arg == "--relinquish-privileges=root" || + arg == testCaptureFilePath || arg == "tcpdump" || arg == "any" || arg == bpfFilter { + continue + } + // Any other argument starting with '-' is suspicious + if strings.HasPrefix(arg, "-") && !strings.Contains(arg, "=") { + t.Errorf("Unexpected flag '%s' found in command (only internal flags should be present): %v", arg, cmd.Args) + } + } +} + +// TestTcpdumpFlagsEnvVar tests that TCPDUMP_FLAGS environment variable is correctly parsed +func TestTcpdumpFlagsEnvVar(t *testing.T) { + resetEnvVars() + + tests := []struct { + name string + flagsEnvValue string + expectedFlags []string + }{ + { + name: "single flag", + flagsEnvValue: "-p", + expectedFlags: []string{"-p"}, + }, + { + name: "multiple flags space-separated", + flagsEnvValue: "-p -n -v", + expectedFlags: []string{"-p", "-n", "-v"}, + }, + { + name: "multiple flags with extra spaces", + flagsEnvValue: " -p -n -v ", + expectedFlags: []string{"-p", "-n", "-v"}, + }, + { + name: "flags with arguments", + flagsEnvValue: "-s 96 -p", + expectedFlags: []string{"-s", "96", "-p"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Setenv(captureConstants.TcpdumpFlagsEnvKey, tt.flagsEnvValue) + defer os.Unsetenv(captureConstants.TcpdumpFlagsEnvKey) + + cmd := constructTcpdumpCommand(testCaptureFilePath, "") + + // Check all expected flags are present + for _, expectedFlag := range tt.expectedFlags { + if !slices.Contains(cmd.Args, expectedFlag) { + t.Errorf("Expected flag '%s' in command args, but got: %v", expectedFlag, cmd.Args) + } + } + }) + } +} + +// TestFilterValidation tests flag rejection in filter input +func TestFilterValidation(t *testing.T) { + requireTcpdump(t) + resetEnvVars() + + _, _ = log.SetupZapLogger(log.GetDefaultLogOpts()) + ncp := NewNetworkCaptureProvider(log.Logger().Named("test")).(*NetworkCaptureProvider) + + captureName := testCaptureName + nodeHostName := testNodeHostName + timestamp := file.Now() + ncp.Filename = file.CaptureFilename{CaptureName: captureName, NodeHostname: nodeHostName, StartTimestamp: timestamp} + tmpCaptureLocation, _ := ncp.Setup(ncp.Filename) + defer os.RemoveAll(tmpCaptureLocation) + + tests := []struct { + name string + setupEnv func() + shouldError bool + errorMsg string + }{ + { + name: "valid BPF filter without flags", + setupEnv: func() { + os.Setenv(captureConstants.PcapFilterEnvKey, "tcp port 80") + }, + shouldError: false, + }, + { + name: "filter with flag at start", + setupEnv: func() { + os.Setenv(captureConstants.PcapFilterEnvKey, "-n tcp port 80") + }, + shouldError: true, + errorMsg: "contains flag \"-n\"", + }, + { + name: "filter with flag at end", + setupEnv: func() { + os.Setenv(captureConstants.PcapFilterEnvKey, "tcp port 80 -n") + }, + shouldError: true, + errorMsg: "contains flag \"-n\"", + }, + { + name: "filter with flag in middle", + setupEnv: func() { + os.Setenv(captureConstants.PcapFilterEnvKey, "tcp -n port 80") + }, + shouldError: true, + errorMsg: "contains flag \"-n\"", + }, + { + name: "filter with tab-separated flag", + setupEnv: func() { + os.Setenv(captureConstants.PcapFilterEnvKey, "tcp\t-n\tport 80") + }, + shouldError: true, + errorMsg: "contains flag \"-n\"", + }, + { + name: "filter with newline-separated flag", + setupEnv: func() { + os.Setenv(captureConstants.PcapFilterEnvKey, "tcp\n-n\nport 80") + }, + shouldError: true, + errorMsg: "contains flag \"-n\"", + }, + { + name: "deprecated tcpdumpFilter with flag", + setupEnv: func() { + os.Setenv(captureConstants.TcpdumpRawFilterEnvKey, "-n tcp port 80") + }, + shouldError: true, + errorMsg: "contains flag \"-n\"", + }, + { + name: "complex BPF expression without flags (valid)", + setupEnv: func() { + os.Setenv(captureConstants.PcapFilterEnvKey, "tcp[tcpflags] & (tcp-syn|tcp-ack) == tcp-syn") + }, + shouldError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resetEnvVars() + tt.setupEnv() + defer resetEnvVars() + + err := ncp.CaptureNetworkPacket(context.Background(), "", 1, 0) + + if tt.shouldError { + if err == nil { + t.Errorf("Expected error containing '%s', but got no error", tt.errorMsg) + } else if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("Expected error containing '%s', but got: %v", tt.errorMsg, err) + } + } else { + // For valid filters, we may get tcpdump execution errors (duration too short, etc.) + // but we should NOT get validation errors + if err != nil && (strings.Contains(err.Error(), "contains flag") || strings.Contains(err.Error(), "whitespace-only")) { + t.Errorf("Expected no validation error, but got: %v", err) + } + } + }) + } +} + +// TestFilterWhitespaceValidation tests whitespace-only filter rejection +func TestFilterWhitespaceValidation(t *testing.T) { + resetEnvVars() + + _, _ = log.SetupZapLogger(log.GetDefaultLogOpts()) + ncp := NewNetworkCaptureProvider(log.Logger().Named("test")).(*NetworkCaptureProvider) + + captureName := "test-capture" + nodeHostName := "test-node" + timestamp := file.Now() + ncp.Filename = file.CaptureFilename{CaptureName: captureName, NodeHostname: nodeHostName, StartTimestamp: timestamp} + tmpCaptureLocation, _ := ncp.Setup(ncp.Filename) + defer os.RemoveAll(tmpCaptureLocation) + + tests := []struct { + name string + setupEnv func() + }{ + { + name: "pcapFilter with only spaces", + setupEnv: func() { + os.Setenv(captureConstants.PcapFilterEnvKey, " ") + }, + }, + { + name: "pcapFilter with only tabs", + setupEnv: func() { + os.Setenv(captureConstants.PcapFilterEnvKey, "\t\t") + }, + }, + { + name: "pcapFilter with only newlines", + setupEnv: func() { + os.Setenv(captureConstants.PcapFilterEnvKey, "\n\n") + }, + }, + { + name: "tcpdumpFilter with only spaces", + setupEnv: func() { + os.Setenv(captureConstants.TcpdumpRawFilterEnvKey, " ") + }, + }, + { + name: "tcpdumpFilter with mixed whitespace", + setupEnv: func() { + os.Setenv(captureConstants.TcpdumpRawFilterEnvKey, " \t\n ") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resetEnvVars() + tt.setupEnv() + defer resetEnvVars() + + err := ncp.CaptureNetworkPacket(context.Background(), "", 1, 0) + + if err == nil { + t.Errorf("Expected error for whitespace-only filter, but got no error") + } else if !errors.Is(err, errTcpdumpFilterEmptyOrWhitespace) { + t.Errorf("Expected errTcpdumpFilterEmptyOrWhitespace, but got: %v", err) + } + }) + } +} + +// TestFilterPrecedence tests that pcapFilter takes precedence over tcpdumpFilter +func TestFilterPrecedence(t *testing.T) { + requireTcpdump(t) + resetEnvVars() + + _, _ = log.SetupZapLogger(log.GetDefaultLogOpts()) + ncp := NewNetworkCaptureProvider(log.Logger().Named("test")).(*NetworkCaptureProvider) + + captureName := testCaptureName + nodeHostName := testNodeHostName + timestamp := file.Now() + ncp.Filename = file.CaptureFilename{CaptureName: captureName, NodeHostname: nodeHostName, StartTimestamp: timestamp} + tmpCaptureLocation, _ := ncp.Setup(ncp.Filename) + defer os.RemoveAll(tmpCaptureLocation) + + tests := []struct { + name string + pcapFilter string + tcpdumpRawFilter string + expectValidationErr bool + }{ + { + name: "both filters set - pcapFilter valid, tcpdumpFilter invalid", + pcapFilter: "tcp port 80", + tcpdumpRawFilter: "-n tcp", + expectValidationErr: false, // Should use pcapFilter, ignore invalid tcpdumpFilter + }, + { + name: "both filters set - pcapFilter invalid, tcpdumpFilter valid", + pcapFilter: "-n tcp", + tcpdumpRawFilter: "tcp port 80", + expectValidationErr: true, // Should validate pcapFilter and reject it + }, + { + name: "only pcapFilter set", + pcapFilter: "tcp port 443", + tcpdumpRawFilter: "", + expectValidationErr: false, + }, + { + name: "only tcpdumpFilter set", + pcapFilter: "", + tcpdumpRawFilter: "udp port 53", + expectValidationErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resetEnvVars() + if tt.pcapFilter != "" { + os.Setenv(captureConstants.PcapFilterEnvKey, tt.pcapFilter) + } + if tt.tcpdumpRawFilter != "" { + os.Setenv(captureConstants.TcpdumpRawFilterEnvKey, tt.tcpdumpRawFilter) + } + defer resetEnvVars() + + err := ncp.CaptureNetworkPacket(context.Background(), "", 1, 0) + + if tt.expectValidationErr { + if err == nil || !strings.Contains(err.Error(), "contains flag") { + t.Errorf("Expected validation error, but got: %v", err) + } + } else { + // We expect either no error or a tcpdump execution error (not validation error) + if err != nil && strings.Contains(err.Error(), "contains flag") { + t.Errorf("Unexpected validation error: %v", err) + } + // Other errors (e.g., tcpdump execution failures) are acceptable for these tests + } + }) + } +} + +// TestFilterPrecedenceValue explicitly verifies that the correct filter value is used +func TestFilterPrecedenceValue(t *testing.T) { + resetEnvVars() + + tests := []struct { + name string + pcapFilter string + tcpdumpRawFilter string + expectedFilterUsed string // The filter that should actually appear in the tcpdump command + }{ + { + name: "both set - should use pcapFilter", + pcapFilter: "tcp port 80", + tcpdumpRawFilter: "tcp port 8080", + expectedFilterUsed: "tcp port 80", + }, + { + name: "only pcapFilter set", + pcapFilter: "udp port 53", + tcpdumpRawFilter: "", + expectedFilterUsed: "udp port 53", + }, + { + name: "only tcpdumpFilter set", + pcapFilter: "", + tcpdumpRawFilter: "icmp", + expectedFilterUsed: "icmp", + }, + { + name: "both empty", + pcapFilter: "", + tcpdumpRawFilter: "", + expectedFilterUsed: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resetEnvVars() + if tt.pcapFilter != "" { + os.Setenv(captureConstants.PcapFilterEnvKey, tt.pcapFilter) + } + if tt.tcpdumpRawFilter != "" { + os.Setenv(captureConstants.TcpdumpRawFilterEnvKey, tt.tcpdumpRawFilter) + } + defer resetEnvVars() + + // Construct tcpdump command and check which filter is used + cmd := constructTcpdumpCommand(testCaptureFilePath, tt.expectedFilterUsed) + + // The filter should appear in the command args + if tt.expectedFilterUsed != "" { + found := slices.Contains(cmd.Args, tt.expectedFilterUsed) + if !found { + t.Errorf("Expected filter '%s' in command args, but got: %v", tt.expectedFilterUsed, cmd.Args) + } + } + + // Verify the environment variables are set correctly + pcapEnv := os.Getenv(captureConstants.PcapFilterEnvKey) + tcpdumpEnv := os.Getenv(captureConstants.TcpdumpRawFilterEnvKey) + + if tt.pcapFilter != "" && pcapEnv != tt.pcapFilter { + t.Errorf("Expected PCAP_FILTER='%s', but got '%s'", tt.pcapFilter, pcapEnv) + } + if tt.tcpdumpRawFilter != "" && tcpdumpEnv != tt.tcpdumpRawFilter { + t.Errorf("Expected TCPDUMP_RAW_FILTER='%s', but got '%s'", tt.tcpdumpRawFilter, tcpdumpEnv) + } + + // When both are set, verify pcapFilter env var exists + if tt.pcapFilter != "" && tt.tcpdumpRawFilter != "" { + if pcapEnv == "" { + t.Error("pcapFilter should be set when both filters provided") + } + if pcapEnv != tt.expectedFilterUsed { + t.Errorf("When both filters set, expected to use pcapFilter '%s', but would use '%s'", tt.pcapFilter, tt.expectedFilterUsed) + } + } + }) + } +} diff --git a/pkg/capture/provider/network_capture_unix.go b/pkg/capture/provider/network_capture_unix.go index 0288964539..1c38d36c2c 100644 --- a/pkg/capture/provider/network_capture_unix.go +++ b/pkg/capture/provider/network_capture_unix.go @@ -7,6 +7,7 @@ package provider import ( "context" + "errors" "fmt" "os" "os/exec" @@ -22,12 +23,91 @@ import ( "github.com/microsoft/retina/pkg/log" ) +type iptablesMode string + +const ( + legacyIptablesMode iptablesMode = "legacy" + nftIptablesMode iptablesMode = "nft" +) + +var ( + errTcpdumpCommandNotConstructed = errors.New("tcpdump command is not constructed with expected arguments") + errTcpdumpStopFailed = errors.New("tcpdump stop failed") + errIptablesUnavilable = errors.New("no iptables command is available") + errIptablesLegacySaveFailed = errors.New("failed to run iptables-legacy-save") + errIptablesNftSaveFailed = errors.New("failed to run iptables-nft-save") + errTcpdumpFilterEmptyOrWhitespace = errors.New("tcpdump filter cannot be empty or whitespace-only") + errTcpdumpFilterContainsFlag = errors.New("filter contains flags which are not allowed") +) + +// constructTcpdumpCommand creates a tcpdump command with the appropriate arguments. +// +// Arguments are added in the following order (order matters for tcpdump): +// 1. Capture control flags (-w, --relinquish-privileges, -s) +// 2. Display/output flags from CaptureOption (read from TCPDUMP_FLAGS env var) +// 3. Interface selection (-i) +// 4. BPF filter expression (MUST BE LAST) +// +// Parameters: +// - captureFilePath: path where the capture file will be written +// - bpfFilter: combined BPF filter expression (user BPF filter + system filter) +func constructTcpdumpCommand(captureFilePath, bpfFilter string) *exec.Cmd { + // NOTE(mainred): The tcpdump release of debian:bullseye image, which is for preparing clang and tools, runs as + // tcpdump user by default for savefiles for output, but when the binary and library are copied to the distroless + // base image, we lost tcpdump user, and the following error will be raised when running tcpdump in our capture pod. + // tcpdump: Couldn't find user 'tcpdump' + // To disable this behavior, we use `--relinquish-privileges=root` same as `-Z root`. + // ref: https://manpages.debian.org/bullseye/tcpdump/tcpdump.8.en.html#Z + captureStartCmd := exec.Command( + "tcpdump", + "-w", captureFilePath, + "--relinquish-privileges=root", + ) + + if packetSize := os.Getenv(captureConstants.PacketSizeEnvKey); packetSize != "" { + captureStartCmd.Args = append( + captureStartCmd.Args, + "-s", packetSize, + ) + } + + // Append tcpdump flags from CaptureOption boolean fields + // These flags are constructed from a controlled mapping table in crd_to_job.go, + // so they're already validated and don't need sanitization here. + if tcpdumpFlags := os.Getenv(captureConstants.TcpdumpFlagsEnvKey); tcpdumpFlags != "" { + flags := strings.Fields(tcpdumpFlags) + captureStartCmd.Args = append(captureStartCmd.Args, flags...) + } + + // Handle interface selection + if specificInterfaces := os.Getenv(captureConstants.CaptureInterfacesEnvKey); specificInterfaces != "" { + // Use specific interfaces if provided + interfaceList := strings.SplitSeq(specificInterfaces, ",") + for iface := range interfaceList { + iface = strings.TrimSpace(iface) + if iface != "" { + captureStartCmd.Args = append(captureStartCmd.Args, "-i", iface) + } + } + } else { + // Default to capturing on all interfaces + captureStartCmd.Args = append(captureStartCmd.Args, "-i", "any") + } + + // SECURITY: bpfFilter contains only BPF filter expressions. + // Any input containing flags (starting with '-') is rejected above. + // Passing bpfFilter as a single argument prevents shell interpretation of special characters. + if bpfFilter != "" { + captureStartCmd.Args = append(captureStartCmd.Args, bpfFilter) + } + + return captureStartCmd +} + type NetworkCaptureProvider struct { NetworkCaptureProviderCommon - TmpCaptureDir string - CaptureName string - NodeHostName string - StartTimestamp *file.Timestamp + TmpCaptureDir string + Filename file.CaptureFilename l *log.ZapLogger } @@ -41,6 +121,77 @@ func NewNetworkCaptureProvider(logger *log.ZapLogger) NetworkCaptureProviderInte } } +// obtainAndValidateUserFilter retrieves the user-provided BPF filter from environment variables, +// validates it (rejecting flags), and returns the trimmed filter and its source name. +func (ncp *NetworkCaptureProvider) obtainAndValidateUserFilter() (trimmedFilter, filterSource string, err error) { + pcapFilter := os.Getenv(captureConstants.PcapFilterEnvKey) + tcpdumpRawFilter := os.Getenv(captureConstants.TcpdumpRawFilterEnvKey) + + // PcapFilter takes precedence over TcpdumpFilter + if pcapFilter != "" { + trimmedFilter = strings.TrimSpace(pcapFilter) + filterSource = "pcapFilter" + ncp.l.Info("Using pcapFilter", zap.String("filter", trimmedFilter)) + if tcpdumpRawFilter != "" { + ncp.l.Warn("Both pcapFilter and tcpdumpFilter provided; using pcapFilter (tcpdumpFilter is deprecated and will be removed)", + zap.String("ignored_tcpdumpFilter", tcpdumpRawFilter)) + } + } else if tcpdumpRawFilter != "" { + trimmedFilter = strings.TrimSpace(tcpdumpRawFilter) + filterSource = "tcpdumpFilter (deprecated, will be removed)" + ncp.l.Info("Using deprecated tcpdumpFilter (will be removed); migrate to pcapFilter", zap.String("filter", trimmedFilter)) + } + + // Check for whitespace-only input + if trimmedFilter == "" && (pcapFilter != "" || tcpdumpRawFilter != "") { + return "", "", errTcpdumpFilterEmptyOrWhitespace + } + + // SECURITY: Reject any input containing flags (starting with '-') + // This prevents dangerous flags like -w, -i, -c from being passed to tcpdump + if trimmedFilter != "" { + tokens := strings.FieldsSeq(trimmedFilter) + for token := range tokens { + if strings.HasPrefix(token, "-") { + return "", "", fmt.Errorf("%s contains flag %q: %w", filterSource, token, errTcpdumpFilterContainsFlag) + } + } + } + + return trimmedFilter, filterSource, nil +} + +// combineFilters combines the user filter with the system filter using proper BPF syntax. +func combineFilters(userFilter, systemFilter string) string { + switch { + case userFilter != "" && systemFilter != "": + // Both filters: wrap each in parentheses to ensure correct operator precedence + return fmt.Sprintf("(%s) and (%s)", userFilter, systemFilter) + case userFilter != "": + return userFilter + case systemFilter != "": + return systemFilter + default: + return "" + } +} + +// validateBPFFilterSyntax validates the BPF filter syntax using tcpdump -d. +func (ncp *NetworkCaptureProvider) validateBPFFilterSyntax(ctx context.Context, filter string) error { + if filter == "" { + return nil + } + + //nolint:gosec // G702: tcpdump -d is used for BPF syntax validation only, filter is constructed from validated input + validateCmd := exec.CommandContext(ctx, "tcpdump", "-d", filter) + if err := validateCmd.Run(); err != nil { + ncp.l.Error("BPF filter validation failed", zap.String("filter", filter), zap.Error(err)) + return fmt.Errorf("invalid BPF filter syntax: %w (filter: %q)", err, filter) + } + ncp.l.Info("BPF filter syntax validated successfully", zap.String("filter", filter)) + return nil +} + func (ncp *NetworkCaptureProvider) Setup(filename file.CaptureFilename) (string, error) { captureFolderDir, err := ncp.NetworkCaptureProviderCommon.Setup(filename) if err != nil { @@ -49,56 +200,39 @@ func (ncp *NetworkCaptureProvider) Setup(filename file.CaptureFilename) (string, ncp.l.Info("Created temporary folder for network capture", zap.String("capture temporary folder", captureFolderDir)) ncp.TmpCaptureDir = captureFolderDir - ncp.CaptureName = filename.CaptureName - ncp.NodeHostName = filename.NodeHostname - ncp.StartTimestamp = filename.StartTimestamp + ncp.Filename = filename + return ncp.TmpCaptureDir, nil } -func (ncp *NetworkCaptureProvider) CaptureNetworkPacket(ctx context.Context, filter string, duration, maxSizeMB int) error { +func (ncp *NetworkCaptureProvider) CaptureNetworkPacket(ctx context.Context, includeExcludeFilter string, duration, maxSizeMB int) error { ctx, cancel := context.WithTimeout(ctx, time.Duration(duration)*time.Second) defer cancel() - filename := file.CaptureFilename{CaptureName: ncp.CaptureName, NodeHostname: ncp.NodeHostName, StartTimestamp: ncp.StartTimestamp} - captureFileName := fmt.Sprintf("%s.pcap", filename) + captureFileName := ncp.Filename.String() + ".pcap" captureFilePath := filepath.Join(ncp.TmpCaptureDir, captureFileName) // Remove the folder in case it already exists to mislead the file size check. - os.Remove(captureFilePath) //nolint:errcheck + os.Remove(captureFilePath) //nolint:errcheck // File may not exist, ok to ignore error - // NOTE(mainred): The tcpdump release of debian:bullseye image, which is for preparing clang and tools, runs as - // tcpdump user by default for savefiles for output, but when the binary and library are copied to the distroless - // base image, we lost tcpdump user, and the following error will be raised when running tcpdump in our capture pod. - // tcpdump: Couldn't find user 'tcpdump' - // To disable this behavior, we use `--relinquish-privileges=root` same as `-Z root`. - // ref: https://manpages.debian.org/bullseye/tcpdump/tcpdump.8.en.html#Z - captureStartCmd := exec.Command( - "tcpdump", - "-w", captureFilePath, - "--relinquish-privileges=root", - ) - - if packetSize := os.Getenv(captureConstants.PacketSizeEnvKey); len(packetSize) != 0 { - captureStartCmd.Args = append( - captureStartCmd.Args, - "-s", packetSize, - ) + // Obtain and validate user-provided BPF filter + userFilter, _, err := ncp.obtainAndValidateUserFilter() + if err != nil { + return err } - // If we set flag and value into the arg item of args, the space between flag and value will not treated as part of - // value, for example, "-i eth0" will be treated as "-i" and " eth0", thus brings a tcpdump unknown interface error. - if tcpdumpRawFilter := os.Getenv(captureConstants.TcpdumpRawFilterEnvKey); len(tcpdumpRawFilter) != 0 { - tcpdumpRawFilterSlice := strings.Split(tcpdumpRawFilter, " ") - captureStartCmd.Args = append(captureStartCmd.Args, tcpdumpRawFilterSlice...) - } + // Combine user filter with system filter (include/exclude Pod IPs) + combinedFilter := combineFilters(userFilter, includeExcludeFilter) - if len(filter) != 0 { - captureStartCmd.Args = append( - captureStartCmd.Args, - filter, - ) + // Validate BPF filter syntax before starting capture + err = ncp.validateBPFFilterSyntax(ctx, combinedFilter) + if err != nil { + return err } + // Construct tcpdump command with combined filter + captureStartCmd := constructTcpdumpCommand(captureFilePath, combinedFilter) + ncp.l.Info("Running tcpdump with args", zap.String("tcpdump command", captureStartCmd.String()), zap.Any("tcpdump args", captureStartCmd.Args)) tcpdumpLogFile, err := ncp.NetworkCaptureProviderCommon.networkCaptureCommandLog("tcpdump.log", captureStartCmd) @@ -108,10 +242,10 @@ func (ncp *NetworkCaptureProvider) CaptureNetworkPacket(ctx context.Context, fil // Store tcpdpump log as part of capture artifacts. defer func() { - if tcpdumpLog, err := os.ReadFile(tcpdumpLogFile.Name()); err != nil { - ncp.l.Warn("Failed to read tcpdump log", zap.Error(err)) + if tcpdumpLog, readErr := os.ReadFile(tcpdumpLogFile.Name()); readErr != nil { + ncp.l.Warn("Failed to read tcpdump log", zap.Error(readErr)) } else { - ncp.l.Info(fmt.Sprintf("Tcpdump command output: %s", string(tcpdumpLog))) + ncp.l.Info("Tcpdump command output: " + string(tcpdumpLog)) } tcpdumpLogFile.Close() }() @@ -144,18 +278,18 @@ func (ncp *NetworkCaptureProvider) CaptureNetworkPacket(ctx context.Context, fil go func() { // Chances are that the capture file is not created when we check the file size. time.Sleep(time.Second * time.Duration(fileSizeCheckIntervalInSecond)) - captureFile, err := os.Open(captureFilePath) - if err != nil { - ncp.l.Error("Failed to open capture file", zap.String("capture file path", captureFilePath), zap.Error(err)) + captureFile, openErr := os.Open(captureFilePath) + if openErr != nil { + ncp.l.Error("Failed to open capture file", zap.String("capture file path", captureFilePath), zap.Error(openErr)) ncp.l.Error("Please make sure tcpdump command is constructed with expected arguments", zap.String("tcpdump args", fmt.Sprintf("%+q", captureStartCmd.Args))) - errChan <- fmt.Errorf("tcpdump command is not constructed with expected arguments") + errChan <- errTcpdumpCommandNotConstructed return } for { - fileStat, err := captureFile.Stat() - if err != nil { - ncp.l.Error("Failed to get capture file info", zap.String("capture file path", captureFilePath), zap.Error(err)) + fileStat, statErr := captureFile.Stat() + if statErr != nil { + ncp.l.Error("Failed to get capture file info", zap.String("capture file path", captureFilePath), zap.Error(statErr)) continue } fileSizeBytes := fileStat.Size() @@ -173,16 +307,16 @@ func (ncp *NetworkCaptureProvider) CaptureNetworkPacket(ctx context.Context, fil case <-doneChan: case <-ctx.Done(): ncp.l.Info("Tcpdump will be stopped - got OS signal, or timeout reached", zap.Error(ctx.Err())) - case err := <-errChan: - return err + case captureErr := <-errChan: + return captureErr } ncp.l.Info("Stop tcpdump") // Kill signal will not wait until the process has actually existed, thus the captured network packets may not be // flushed to the capture file. Instead, we signal terminate and wait until the process to exit. - if err := captureStartCmd.Process.Signal(syscall.SIGTERM); err != nil { - ncp.l.Error("Failed to signal terminate to process, will kill the process", zap.Error(err)) + if signalErr := captureStartCmd.Process.Signal(syscall.SIGTERM); signalErr != nil { + ncp.l.Error("Failed to signal terminate to process, will kill the process", zap.Error(signalErr)) if killErr := captureStartCmd.Process.Kill(); killErr != nil { - return fmt.Errorf("tcpdump stop failed, error: %s", killErr) + return fmt.Errorf("%w: %w", errTcpdumpStopFailed, killErr) } return err } @@ -195,18 +329,23 @@ func (ncp *NetworkCaptureProvider) CaptureNetworkPacket(ctx context.Context, fil } type command struct { - name string - args []string - description string + name string + args []string + description string + ignoreFailure bool } func (ncp *NetworkCaptureProvider) CollectMetadata() error { ncp.l.Info("Start to collect network metadata") - iptablesMode := obtainIptablesMode() - ncp.l.Info(fmt.Sprintf("Iptables mode %s is used", iptablesMode)) - iptablesSaveCmdName := fmt.Sprintf("iptables-%s-save", iptablesMode) - iptablesCmdName := fmt.Sprintf("iptables-%s", iptablesMode) + iptablesModeName, err := obtainIptablesMode(ncp.l) + if err != nil { + return fmt.Errorf("failed to determine iptables modes. %w", err) + } + + ncp.l.Info(fmt.Sprintf("Iptables mode %s is used", iptablesModeName)) + iptablesSaveCmdName := fmt.Sprintf("iptables-%s-save", iptablesModeName) + iptablesCmdName := fmt.Sprintf("iptables-%s", iptablesModeName) metadataList := []struct { commands []command @@ -292,64 +431,23 @@ func (ncp *NetworkCaptureProvider) CollectMetadata() error { name: "cp", args: []string{"-r", "/proc/sys/net", filepath.Join(ncp.TmpCaptureDir, "proc-sys-net")}, description: "kernel networking configuration", + // Errors will occur when copying kernel networking configuration for not all files under /proc/sys/net are + // readable, like '/proc/sys/net/ipv4/route/flush', which doesn't implement the read function. + ignoreFailure: true, }, }, }, } for _, metadata := range metadataList { - if len(metadata.fileName) != 0 { - captureMetadataFilePath := filepath.Join(ncp.TmpCaptureDir, metadata.fileName) - outfile, err := os.OpenFile(captureMetadataFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) - if err != nil { - ncp.l.Error("Failed to create metadata file", zap.String("metadata file path", captureMetadataFilePath), zap.Error(err)) - continue - } - defer outfile.Close() - - if _, err := outfile.WriteString("Summary:\n\n"); err != nil { - ncp.l.Error("Failed to write summary to file", zap.String("file", outfile.Name()), zap.Error(err)) - } - - // Print headlines for all commands in output file. - cmds := []*exec.Cmd{} - for _, command := range metadata.commands { - cmd := exec.Command(command.name, command.args...) - cmds = append(cmds, cmd) - commandSummary := fmt.Sprintf("%s(%s)\n", cmd.String(), command.description) - if _, err := outfile.WriteString(commandSummary); err != nil { - ncp.l.Error("Failed to write command description to file", zap.String("file", outfile.Name()), zap.Error(err)) - } - } - - if _, err := outfile.WriteString("\nExecute:\n\n"); err != nil { - ncp.l.Error("Failed to write command output to file", zap.String("file", outfile.Name()), zap.Error(err)) - } - - // Write command stdout and stderr to output file - for _, cmd := range cmds { - if _, err := outfile.WriteString(fmt.Sprintf("%s\n\n", cmd.String())); err != nil { - ncp.l.Error("Failed to write string to file", zap.String("file", outfile.Name()), zap.Error(err)) - } - - cmd.Stdout = outfile - cmd.Stderr = outfile - if err := cmd.Run(); err != nil { - // Don't return for error to continue capturing following network metadata. - ncp.l.Error("Failed to execute command", zap.String("command", cmd.String()), zap.Error(err)) - // Log the error in output file because this error does not stop capture job pod from finishing, - // and the job can be recycled automatically leaving no info to debug. - if _, err = outfile.WriteString(fmt.Sprintf("Failed to run %q, error: %s)", cmd.String(), err.Error())); err != nil { - ncp.l.Error("Failed to write command run failure", zap.String("command", cmd.String()), zap.Error(err)) - } - } - } + if metadata.fileName != "" { + ncp.processMetadataFile(metadata) } else { for _, command := range metadata.commands { - cmd := exec.Command(command.name, command.args...) + cmd := exec.CommandContext(context.Background(), command.name, command.args...) // nolint:gosec // no sensitive data // Errors will when copying kernel networking configuration for not all files under /proc/sys/net are // readable, like '/proc/sys/net/ipv4/route/flush', which doesn't implement the read function. - if output, err := cmd.CombinedOutput(); err != nil { + if output, err := cmd.CombinedOutput(); err != nil && !command.ignoreFailure { // Don't return for error to continue capturing following network metadata. ncp.l.Error("Failed to execute command", zap.String("command", cmd.String()), zap.String("output", string(output)), zap.Error(err)) } @@ -362,22 +460,111 @@ func (ncp *NetworkCaptureProvider) CollectMetadata() error { return nil } +func (ncp *NetworkCaptureProvider) processMetadataFile(metadata struct { + commands []command + fileName string +}, +) { + captureMetadataFilePath := filepath.Join(ncp.TmpCaptureDir, metadata.fileName) + outfile, err := os.OpenFile(captureMetadataFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + ncp.l.Error("Failed to create metadata file", zap.String("metadata file path", captureMetadataFilePath), zap.Error(err)) + return + } + defer outfile.Close() + + if _, err := outfile.WriteString("Summary:\n\n"); err != nil { + ncp.l.Error("Failed to write summary to file", zap.String("file", outfile.Name()), zap.Error(err)) + } + + // Print headlines for all commands in output file. + cmds := []*exec.Cmd{} + for _, command := range metadata.commands { + cmd := exec.CommandContext(context.Background(), command.name, command.args...) // nolint:gosec // no sensitive data + cmds = append(cmds, cmd) + commandSummary := fmt.Sprintf("%s(%s)\n", cmd.String(), command.description) + if _, err := outfile.WriteString(commandSummary); err != nil { + ncp.l.Error("Failed to write command description to file", zap.String("file", outfile.Name()), zap.Error(err)) + } + } + + if _, err := outfile.WriteString("\nExecute:\n\n"); err != nil { + ncp.l.Error("Failed to write command output to file", zap.String("file", outfile.Name()), zap.Error(err)) + } + + // Write command stdout and stderr to output file + for _, cmd := range cmds { + if _, err := fmt.Fprintf(outfile, "%s\n\n", cmd.String()); err != nil { + ncp.l.Error("Failed to write string to file", zap.String("file", outfile.Name()), zap.Error(err)) + } + + cmd.Stdout = outfile + cmd.Stderr = outfile + if err := cmd.Run(); err != nil { + // Don't return for error to continue capturing following network metadata. + ncp.l.Error("Failed to execute command", zap.String("command", cmd.String()), zap.Error(err)) + // Log the error in output file because this error does not stop capture job pod from finishing, + // and the job can be recycled automatically leaving no info to debug. + if _, err = fmt.Fprintf(outfile, "Failed to run %q, error: %s)", cmd.String(), err.Error()); err != nil { + ncp.l.Error("Failed to write command run failure", zap.String("command", cmd.String()), zap.Error(err)) + } + } + } +} + func (ncp *NetworkCaptureProvider) Cleanup() error { - ncp.l.Info("Cleanup network capture", zap.String("capture name", ncp.CaptureName), zap.String("temporary dir", ncp.TmpCaptureDir)) + ncp.l.Info("Cleanup network capture", zap.String("capture name", ncp.Filename.CaptureName), zap.String("temporary dir", ncp.TmpCaptureDir)) ncp.NetworkCaptureProviderCommon.Cleanup() return nil } -func obtainIptablesMode() string { +// obtainIptablesMode return the available iptables mode, and returns empty when no iptables is available. +func obtainIptablesMode(logger *log.ZapLogger) (iptablesMode, error) { // Since iptables v1.8, nf_tables are introduced as an improvement of legacy iptables, but provides the same user // interface as legacy iptables through iptables-nft command. // based on: https://github.com/kubernetes-sigs/iptables-wrappers/blob/97b01f43a8e8db07840fc4b95e833a37c0d36b12/iptables-wrapper-installer.sh - legacySaveOut, _ := exec.Command("iptables-legacy-save").CombinedOutput() - legacySaveLineNum := len(strings.Split(string(legacySaveOut), "\n")) - nftSaveOut, _ := exec.Command("iptables-nft-save").CombinedOutput() - nftSaveLineNum := len(strings.Split(string(nftSaveOut), "\n")) - if legacySaveLineNum > nftSaveLineNum { - return "legacy" - } - return "nft" + + // When both iptables modes available, we choose the one with more rules, because the other one normally outputs empty rules. + nftIptablesModeAvaiable := true + legacyIptablesModeAvaiable := true + legacySaveLineNum := 0 + nftSaveLineNum := 0 + if _, err := exec.LookPath("iptables-legacy-save"); err != nil { + legacyIptablesModeAvaiable = false + logger.Info("iptables-legacy-save is not available", zap.Error(err)) + } else { + legacySaveOut, err := exec.CommandContext(context.Background(), "iptables-legacy-save").CombinedOutput() + if err != nil { + return "", fmt.Errorf("%w: %w", errIptablesLegacySaveFailed, err) + } + legacySaveLineNum = len(strings.Split(string(legacySaveOut), "\n")) + } + + if _, err := exec.LookPath("iptables-nft-save"); err != nil { + nftIptablesModeAvaiable = false + logger.Info("iptables-nft-save is not available", zap.Error(err)) + } else { + nftSaveOut, err := exec.CommandContext(context.Background(), "iptables-nft-save").CombinedOutput() + if err != nil { + return "", fmt.Errorf("%w: %w", errIptablesNftSaveFailed, err) + } + nftSaveLineNum = len(strings.Split(string(nftSaveOut), "\n")) + } + + if nftIptablesModeAvaiable && legacyIptablesModeAvaiable { + if legacySaveLineNum > nftSaveLineNum { + return legacyIptablesMode, nil + } + return nftIptablesMode, nil + } + + if nftIptablesModeAvaiable { + return nftIptablesMode, nil + } + + if legacyIptablesModeAvaiable { + return legacyIptablesMode, nil + } + + return "", errIptablesUnavilable } diff --git a/pkg/capture/provider/network_capture_win.go b/pkg/capture/provider/network_capture_win.go index a15da1c746..c11ef3393d 100644 --- a/pkg/capture/provider/network_capture_win.go +++ b/pkg/capture/provider/network_capture_win.go @@ -7,6 +7,7 @@ package provider import ( "context" + "errors" "fmt" "io" "net/http" @@ -24,13 +25,29 @@ import ( "github.com/microsoft/retina/pkg/log" ) +var ( + // netshFilterPattern validates that the filter contains only characters allowed in netsh capture filters. + // Allowed: alphanumeric, dots, colons (for IPv6), parentheses, commas, equals, spaces. + // Rejected: shell metacharacters like &, |, ^, <, >, %, ", ', ;, $, `, \, newlines. + netshFilterPattern = regexp.MustCompile(`^[A-Za-z0-9.=():, ]+$`) + + // ErrInvalidNetshFilter is returned when a netsh filter contains invalid characters. + ErrInvalidNetshFilter = errors.New("filter contains invalid characters; only alphanumeric, dots, colons, parentheses, commas, equals, and spaces are allowed") +) + +// validateNetshFilter validates that a filter string is safe for use with netsh trace commands. +// It rejects filters containing shell metacharacters that could enable command injection. +func validateNetshFilter(filter string) error { + if !netshFilterPattern.MatchString(filter) { + return ErrInvalidNetshFilter + } + return nil +} + type NetworkCaptureProvider struct { NetworkCaptureProviderCommon - TmpCaptureDir string - CaptureName string - NodeHostName string - StartTimestamp *file.Timestamp - Filename file.CaptureFilename + TmpCaptureDir string + Filename file.CaptureFilename l *log.ZapLogger } @@ -52,10 +69,7 @@ func (ncp *NetworkCaptureProvider) Setup(filename file.CaptureFilename) (string, ncp.l.Info("Created temporary folder for network capture", zap.String("capture temporary folder", captureFolderDir)) ncp.TmpCaptureDir = captureFolderDir - ncp.CaptureName = filename.CaptureName - ncp.NodeHostName = filename.NodeHostname - ncp.StartTimestamp = filename.StartTimestamp - ncp.Filename = file.CaptureFilename{CaptureName: ncp.CaptureName, NodeHostname: ncp.NodeHostName, StartTimestamp: ncp.StartTimestamp} + ncp.Filename = filename return ncp.TmpCaptureDir, nil } @@ -64,32 +78,48 @@ func (ncp *NetworkCaptureProvider) CaptureNetworkPacket(ctx context.Context, fil ctx, cancel := context.WithTimeout(ctx, time.Second*time.Duration(duration)) defer cancel() - stopTrace, err := ncp.needToStopTraceSession() + stopTrace, err := ncp.needToStopTraceSession(ctx) if err != nil { return err } if stopTrace { ncp.l.Info("Stopping netsh trace session before starting a new one") - _ = ncp.stopNetworkCapture() + _ = ncp.stopNetworkCapture() //nolint:contextcheck // stopNetworkCapture creates its own context } captureFileName := ncp.Filename.String() + ".etl" captureFilePath := filepath.Join(ncp.TmpCaptureDir, captureFileName) + // SECURITY: Invoke netsh.exe directly without cmd /C to prevent shell injection. + // Build the command args explicitly to avoid shell metacharacter interpretation. captureStartCmd := exec.Command( - "cmd", "/C", - fmt.Sprintf("netsh trace start capture=yes report=disabled overwrite=yes"), + "netsh", + "trace", + "start", + "capture=yes", + "report=disabled", + "overwrite=yes", fmt.Sprintf("tracefile=%s", captureFilePath), ) - // We should split arguments organized in a string delimited by spaces as - // separate ones, otherwise the whole string will be treated as one argument. - // For example, given the following filter, exec lib will treat IPv4.Address - // as the argument and the rest as the value of IPv4.Address. - // "IPv4.Address=(10.244.1.85,10.244.1.235) IPv6.Address=(fd5c:d9f1:79c5:fd83::1bc,fd5c:d9f1:79c5:fd83::11b)" + // Validate and add filter if provided. + // SECURITY: The filter is validated to contain only allowed characters for netsh capture filters. + // This prevents command injection via shell metacharacters like &, |, ^, <, >, etc. if len(filter) != 0 { - netshFilterSlice := strings.Split(filter, " ") - captureStartCmd.Args = append(captureStartCmd.Args, netshFilterSlice...) + // Validate that the filter doesn't start with a hyphen (defense in depth) + if strings.HasPrefix(strings.TrimSpace(filter), "-") { + ncp.l.Warn("Filter starts with hyphen, ignoring to prevent flag injection", zap.String("filter", filter)) + } else { + filterErr := validateNetshFilter(filter) + if filterErr != nil { + ncp.l.Error("Invalid filter for netsh, ignoring", zap.String("filter", filter), zap.Error(filterErr)) + } else { + // Split the filter on spaces and add each token as a separate argument. + // This is safe now because we've validated the filter content. + netshFilterSlice := strings.Split(filter, " ") + captureStartCmd.Args = append(captureStartCmd.Args, netshFilterSlice...) + } + } } // NOTE: netsh cannot stop when the given max size of reach reaches, but we can use maxSizeMB to limit the size of @@ -134,7 +164,8 @@ func (ncp *NetworkCaptureProvider) CaptureNetworkPacket(ctx context.Context, fil } ncp.l.Info("Stop netsh") - if err := ncp.stopNetworkCapture(); err != nil { + // stopNetworkCapture creates its own context; parent ctx is expired here + if err := ncp.stopNetworkCapture(); err != nil { //nolint:contextcheck // stopNetworkCapture creates its own context ncp.l.Error("Failed to stop netsh trace by 'netsh trace stop', will kill the process", zap.Error(err)) _ = captureStartCmd.Process.Kill() return fmt.Errorf("netsh stop failed: Output: %s", err) @@ -150,8 +181,8 @@ func (ncp *NetworkCaptureProvider) CaptureNetworkPacket(ctx context.Context, fil // needToStopTraceSession returns true when a running trace session started by Retina capture exists, otherwise returns // false. Specially, when the trace session is not started by Retina capture, determined from the capture file path, an // error will be raised. -func (ncp *NetworkCaptureProvider) needToStopTraceSession() (bool, error) { - command := exec.Command("cmd", "/C", "netsh trace show status") +func (ncp *NetworkCaptureProvider) needToStopTraceSession(ctx context.Context) (bool, error) { + command := exec.CommandContext(ctx, "netsh", "trace", "show", "status") output, err := command.CombinedOutput() // When there's no running trace session, `netsh trace show status` will exist with error code 1, in which case we @@ -177,7 +208,12 @@ func (ncp *NetworkCaptureProvider) needToStopTraceSession() (bool, error) { func (ncp *NetworkCaptureProvider) stopNetworkCapture() error { ncp.l.Info("Stopping netsh trace session") - command := exec.Command("cmd", "/C", "netsh trace stop") + // Create independent context for cleanup. + // netsh trace stop completes in ~1s; 30s timeout provides ample safety margin. + stopCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + command := exec.CommandContext(stopCtx, "netsh", "trace", "stop") output, err := command.CombinedOutput() // ignore the error when stop the trace when no live trace session exists. if strings.Contains(string(output), "There is no trace session currently in progress") { @@ -301,7 +337,7 @@ func (ncp *NetworkCaptureProvider) CollectMetadata() error { } func (ncp *NetworkCaptureProvider) Cleanup() error { - ncp.l.Info("Cleanup network capture", zap.String("capture name", ncp.CaptureName), zap.String("temporary dir", ncp.TmpCaptureDir)) + ncp.l.Info("Cleanup network capture", zap.String("capture name", ncp.Filename.CaptureName), zap.String("temporary dir", ncp.TmpCaptureDir)) ncp.NetworkCaptureProviderCommon.Cleanup() return nil } diff --git a/pkg/capture/provider/network_capture_win_test.go b/pkg/capture/provider/network_capture_win_test.go new file mode 100644 index 0000000000..5e51d45a7a --- /dev/null +++ b/pkg/capture/provider/network_capture_win_test.go @@ -0,0 +1,156 @@ +//go:build windows + +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package provider + +import ( + "context" + "testing" + "time" + + "github.com/microsoft/retina/pkg/capture/file" + "github.com/microsoft/retina/pkg/log" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestValidateNetshFilter(t *testing.T) { + tests := []struct { + name string + filter string + expectErr bool + }{ + { + name: "Valid IPv4 filter", + filter: "IPv4.Address=10.0.0.1", + expectErr: false, + }, + { + name: "Valid IPv4 filter with multiple addresses", + filter: "IPv4.Address=(10.244.1.85,10.244.1.235)", + expectErr: false, + }, + { + name: "Valid IPv6 filter", + filter: "IPv6.Address=(fd5c:d9f1:79c5:fd83::1bc,fd5c:d9f1:79c5:fd83::11b)", + expectErr: false, + }, + { + name: "Valid combined IPv4 and IPv6 filter", + filter: "IPv4.Address=(10.244.1.85,10.244.1.235) IPv6.Address=(fd5c:d9f1:79c5:fd83::1bc,fd5c:d9f1:79c5:fd83::11b)", + expectErr: false, + }, + { + name: "Shell injection with ampersand", + filter: "IPv4.Address=10.0.0.1 & powershell -enc ", + expectErr: true, + }, + { + name: "Shell injection with pipe", + filter: "IPv4.Address=10.0.0.1 | powershell -Command ", + expectErr: true, + }, + { + name: "Shell injection with caret", + filter: "IPv4.Address=10.0.0.1 ^ powershell", + expectErr: true, + }, + { + name: "Shell injection with redirect", + filter: "IPv4.Address=10.0.0.1 > c:\\temp\\output.txt", + expectErr: true, + }, + { + name: "Shell injection with semicolon", + filter: "IPv4.Address=10.0.0.1; powershell", + expectErr: true, + }, + { + name: "Shell injection with dollar sign", + filter: "IPv4.Address=$env:TEMP", + expectErr: true, + }, + { + name: "Shell injection with backtick", + filter: "IPv4.Address=`powershell`", + expectErr: true, + }, + { + name: "Shell injection with double quotes", + filter: "IPv4.Address=\"10.0.0.1\"", + expectErr: true, + }, + { + name: "Shell injection with single quote", + filter: "IPv4.Address='10.0.0.1'", + expectErr: true, + }, + { + name: "Shell injection with percent", + filter: "IPv4.Address=%TEMP%", + expectErr: true, + }, + { + name: "Shell injection with backslash", + filter: "IPv4.Address=10.0.0.1\\powershell", + expectErr: true, + }, + { + name: "Shell injection with newline", + filter: "IPv4.Address=10.0.0.1\npowershell", + expectErr: true, + }, + { + name: "Empty filter", + filter: "", + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateNetshFilter(tt.filter) + if tt.expectErr && err == nil { + t.Errorf("Expected error for filter '%s', but got none", tt.filter) + } + if !tt.expectErr && err != nil { + t.Errorf("Expected no error for filter '%s', but got: %v", tt.filter, err) + } + }) + } +} + +// TestStopNetworkCapture_ContextIndependence verifies stopNetworkCapture creates its own context +func TestStopNetworkCapture_ContextIndependence(t *testing.T) { + now := metav1.Now() + ncp := &NetworkCaptureProvider{ + NetworkCaptureProviderCommon: NetworkCaptureProviderCommon{ + TmpCaptureDir: t.TempDir(), + l: log.Logger().Named("test-capture"), + }, + Filename: file.CaptureFilename{ + CaptureName: "test-capture", + NodeHostname: "test-node", + StartTimestamp: &now, + }, + } + + // Create an expired context (simulating capture duration ending) + parentCtx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond) + time.Sleep(10 * time.Millisecond) + defer cancel() + + if parentCtx.Err() == nil { + t.Fatal("Setup error: parent context should be expired") + } + + // Call StopNetworkCapture - should NOT return "context deadline exceeded" + err := ncp.stopNetworkCapture() + + if err != nil && err.Error() == "context deadline exceeded" { + t.Fatal("StopNetworkCapture returned 'context deadline exceeded' - bug reintroduced") + } + + t.Logf("StopNetworkCapture uses independent context (netsh error expected: %v)", err) +} diff --git a/pkg/capture/utils/annotations.go b/pkg/capture/utils/annotations.go new file mode 100644 index 0000000000..b9f72cf253 --- /dev/null +++ b/pkg/capture/utils/annotations.go @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package utils + +import ( + retinav1alpha1 "github.com/microsoft/retina/crd/api/v1alpha1" + captureConstants "github.com/microsoft/retina/pkg/capture/constants" + "github.com/microsoft/retina/pkg/capture/file" +) + +// GetPodAnnotationsFromCapture builds the capture pod annotations. resolvedHostPath, +// when non-empty, is written as the CaptureHostPathAnnotationKey value (the on-node +// path actually mounted into the capture pod). When empty, the raw value from the +// Capture spec is used as a fallback (e.g. when called from contexts where the +// host path has not been resolved yet). +func GetPodAnnotationsFromCapture(capture *retinav1alpha1.Capture, resolvedHostPath string) map[string]string { + annotations := map[string]string{ + captureConstants.CaptureFilenameAnnotationKey: capture.Name, + } + if capture.Status.StartTime != nil { + annotations[captureConstants.CaptureTimestampAnnotationKey] = file.TimeToString(capture.Status.StartTime) + } + if resolvedHostPath != "" { + annotations[captureConstants.CaptureHostPathAnnotationKey] = resolvedHostPath + } else if capture.Spec.OutputConfiguration.HostPath != nil { + annotations[captureConstants.CaptureHostPathAnnotationKey] = *capture.Spec.OutputConfiguration.HostPath + } + return annotations +} diff --git a/pkg/capture/utils/annotations_test.go b/pkg/capture/utils/annotations_test.go new file mode 100644 index 0000000000..eb1161018e --- /dev/null +++ b/pkg/capture/utils/annotations_test.go @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package utils + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + retinav1alpha1 "github.com/microsoft/retina/crd/api/v1alpha1" + captureConstants "github.com/microsoft/retina/pkg/capture/constants" + "github.com/microsoft/retina/pkg/capture/file" +) + +func TestGetPodAnnotationsFromCapture(t *testing.T) { + startTime := &metav1.Time{Time: time.Date(2026, 5, 20, 10, 0, 0, 0, time.UTC)} + rawHostPath := "my-capture" + resolvedHostPath := "/var/log/retina/captures/my-capture" + + captureWithHostPath := func() *retinav1alpha1.Capture { + hp := rawHostPath + return &retinav1alpha1.Capture{ + ObjectMeta: metav1.ObjectMeta{Name: "cap1"}, + Spec: retinav1alpha1.CaptureSpec{ + OutputConfiguration: retinav1alpha1.OutputConfiguration{HostPath: &hp}, + }, + Status: retinav1alpha1.CaptureStatus{StartTime: startTime}, + } + } + + cases := []struct { + name string + capture *retinav1alpha1.Capture + resolvedHostPath string + wantHostPathAnn string // empty means annotation must be absent + }{ + { + // This is the case that caused the download bug: capture is created + // with a relative subpath, the operator resolves it to an absolute + // on-node path, and the annotation must carry the resolved value + // (because `kubectl retina capture download` mounts it verbatim). + name: "resolved path wins over raw spec value", + capture: captureWithHostPath(), + resolvedHostPath: resolvedHostPath, + wantHostPathAnn: resolvedHostPath, + }, + { + // Fallback path for callers that don't have a resolved value (e.g. + // CLI helpers that only see the CR). The raw spec value is used. + name: "raw spec value used when resolved is empty", + capture: captureWithHostPath(), + resolvedHostPath: "", + wantHostPathAnn: rawHostPath, + }, + { + // Defense-in-depth: even if the spec has no HostPath, an explicit + // resolved value should still be written (callers own the choice). + name: "resolved path is written even when spec.HostPath is nil", + capture: &retinav1alpha1.Capture{ + ObjectMeta: metav1.ObjectMeta{Name: "cap2"}, + Status: retinav1alpha1.CaptureStatus{StartTime: startTime}, + }, + resolvedHostPath: resolvedHostPath, + wantHostPathAnn: resolvedHostPath, + }, + { + name: "no HostPath annotation when neither resolved nor spec is set", + capture: &retinav1alpha1.Capture{ + ObjectMeta: metav1.ObjectMeta{Name: "cap3"}, + Status: retinav1alpha1.CaptureStatus{StartTime: startTime}, + }, + resolvedHostPath: "", + wantHostPathAnn: "", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := GetPodAnnotationsFromCapture(tc.capture, tc.resolvedHostPath) + + // Filename annotation is always set to the capture name. + assert.Equal(t, tc.capture.Name, got[captureConstants.CaptureFilenameAnnotationKey]) + + if tc.wantHostPathAnn == "" { + _, ok := got[captureConstants.CaptureHostPathAnnotationKey] + assert.False(t, ok, "HostPath annotation should be absent") + } else { + assert.Equal(t, tc.wantHostPathAnn, got[captureConstants.CaptureHostPathAnnotationKey]) + } + }) + } +} + +func TestGetPodAnnotationsFromCapture_TimestampOnlyWhenStartTimeSet(t *testing.T) { + hp := "my-capture" + base := &retinav1alpha1.Capture{ + ObjectMeta: metav1.ObjectMeta{Name: "cap"}, + Spec: retinav1alpha1.CaptureSpec{ + OutputConfiguration: retinav1alpha1.OutputConfiguration{HostPath: &hp}, + }, + } + + t.Run("no StartTime -> no timestamp annotation", func(t *testing.T) { + got := GetPodAnnotationsFromCapture(base, "") + _, ok := got[captureConstants.CaptureTimestampAnnotationKey] + assert.False(t, ok) + }) + + t.Run("StartTime set -> timestamp annotation populated", func(t *testing.T) { + ts := &metav1.Time{Time: time.Date(2026, 5, 20, 10, 0, 0, 0, time.UTC)} + c := base.DeepCopy() + c.Status.StartTime = ts + + got := GetPodAnnotationsFromCapture(c, "") + require.Equal(t, file.TimeToString(ts), got[captureConstants.CaptureTimestampAnnotationKey]) + }) +} diff --git a/pkg/capture/utils/label.go b/pkg/capture/utils/label.go index 4882f1cf24..c83d5aee52 100644 --- a/pkg/capture/utils/label.go +++ b/pkg/capture/utils/label.go @@ -28,3 +28,10 @@ func GetContainerLabelsFromCaptureName(captureName string) map[string]string { label.CaptureNameLabel: captureName, } } + +func GetDownloadLabelsFromCaptureName(captureName string) map[string]string { + return map[string]string{ + label.AppLabel: captureConstants.DownloadAppname, + label.CaptureNameLabel: captureName, + } +} diff --git a/pkg/common/endpoint.go b/pkg/common/endpoint.go index aebbcc7971..9df09aff7e 100644 --- a/pkg/common/endpoint.go +++ b/pkg/common/endpoint.go @@ -19,12 +19,17 @@ func NewRetinaEndpoint(name, namespace string, ips *IPAddresses) *RetinaEndpoint } } +func (ep *RetinaEndpoint) String() string { + return ep.NamespacedName() +} + func (ep *RetinaEndpoint) DeepCopy() interface{} { ep.RLock() defer ep.RUnlock() newEp := &RetinaEndpoint{ BaseObject: ep.BaseObject.DeepCopy(), + nodeIP: ep.nodeIP, } if ep.ownerRefs != nil { @@ -160,7 +165,7 @@ func (ep *RetinaEndpoint) PrimaryIP() (string, error) { } } - return "", errors.Wrapf(ErrNoPrimaryIPFoundEndpoint, ep.Key()) + return "", errors.Wrapf(ErrNoPrimaryIPFoundEndpoint, "endpoint: %s", ep.Key()) } func (ep *RetinaEndpoint) PrimaryNetIP() (net.IP, error) { @@ -174,7 +179,13 @@ func (ep *RetinaEndpoint) PrimaryNetIP() (net.IP, error) { } } - return nil, errors.Wrapf(ErrNoPrimaryIPFoundEndpoint, ep.Key()) + return nil, errors.Wrapf(ErrNoPrimaryIPFoundEndpoint, "endpoint: %s", ep.Key()) +} + +func (ep *RetinaEndpoint) NodeIP() string { + ep.RLock() + defer ep.RUnlock() + return ep.nodeIP } func (o *OwnerReference) DeepCopy() *OwnerReference { diff --git a/pkg/common/node.go b/pkg/common/node.go index 46735bbdf1..4e00ff9658 100644 --- a/pkg/common/node.go +++ b/pkg/common/node.go @@ -4,16 +4,18 @@ package common import "net" -func NewRetinaNode(name string, ip net.IP) *RetinaNode { +func NewRetinaNode(name string, ip net.IP, zone string) *RetinaNode { return &RetinaNode{ name: name, ip: ip, + zone: zone, } } func (n *RetinaNode) DeepCopy() interface{} { newN := &RetinaNode{ name: n.name, + zone: n.zone, } if n.ip != nil { @@ -31,3 +33,7 @@ func (n *RetinaNode) IPString() string { func (n *RetinaNode) Name() string { return n.name } + +func (n *RetinaNode) Zone() string { + return n.zone +} diff --git a/pkg/common/types.go b/pkg/common/types.go index 5905dc6ce1..359b23bd49 100644 --- a/pkg/common/types.go +++ b/pkg/common/types.go @@ -40,6 +40,7 @@ type RetinaEndpoint struct { containers []*RetinaContainer labels map[string]string annotations map[string]string + nodeIP string } func isIPV4(ipAddress string) bool { @@ -59,6 +60,7 @@ func RetinaEndpointCommonFromAPI(retinaEndpoint *retinav1alpha1.RetinaEndpoint) containers: []*RetinaContainer{}, labels: retinaEndpoint.Labels, annotations: make(map[string]string), + nodeIP: retinaEndpoint.Spec.NodeIP, } for _, ownerRef := range retinaEndpoint.Spec.OwnerReferences { @@ -203,6 +205,7 @@ type OwnerReference struct { type RetinaNode struct { name string ip net.IP + zone string } type APIServerObject struct { diff --git a/pkg/common/types_test.go b/pkg/common/types_test.go index 5eb0574dc7..d14f9dde4a 100644 --- a/pkg/common/types_test.go +++ b/pkg/common/types_test.go @@ -87,6 +87,7 @@ func TestRetinaEndpointCommonFromAPI(t *testing.T) { annotations: map[string]string{ RetinaPodAnnotation: RetinaPodAnnotationValue, }, + nodeIP: hostIP, }, }, { @@ -140,6 +141,7 @@ func TestRetinaEndpointCommonFromAPI(t *testing.T) { annotations: map[string]string{ RetinaPodAnnotation: RetinaPodAnnotationValue, }, + nodeIP: hostIP, }, }, } diff --git a/pkg/config/capture.go b/pkg/config/capture.go index c20899707b..9aea82a85d 100644 --- a/pkg/config/capture.go +++ b/pkg/config/capture.go @@ -27,4 +27,12 @@ type CaptureConfig struct { EnableManagedStorageAccount bool `yaml:"enableManagedStorageAccount"` // AzureCredentialConfig indicates the path of Azure credential configuration file. AzureCredentialConfig string `yaml:"azureCredentialConfig"` + + // CaptureHostPathBaseDir is the absolute, operator-controlled directory on every + // node under which Capture CRs may write artifacts. The user-supplied + // OutputConfiguration.HostPath is treated as a relative subpath name and joined + // under this directory. CR authors cannot influence the base, so they cannot + // place artifacts anywhere else on the node filesystem. + // If unset, the operator defaults to /var/log/retina/captures. + CaptureHostPathBaseDir string `yaml:"captureHostPathBaseDir"` } diff --git a/pkg/config/config.go b/pkg/config/config.go index 0ac6b1cb06..6f26f50cc3 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -3,6 +3,7 @@ package config import ( + "errors" "fmt" "log" "reflect" @@ -16,6 +17,16 @@ import ( // Level defines the level of monitor aggregation. type Level int +// TCXMode controls whether TCX (TC eXpress) attachment is used for packetparser. +type TCXMode string + +const ( + // TCXModeAuto detects kernel support and uses TCX if available, falling back to TC. + TCXModeAuto TCXMode = "auto" + // TCXModeOff disables TCX and always uses traditional TC. + TCXModeOff TCXMode = "off" +) + const MinTelemetryInterval time.Duration = 2 * time.Minute const ( @@ -23,9 +34,31 @@ const ( High ) +type PacketParserRingBufferMode string + +const ( + PacketParserRingBufferDisabled PacketParserRingBufferMode = "disabled" + PacketParserRingBufferEnabled PacketParserRingBufferMode = "enabled" + PacketParserRingBufferAuto PacketParserRingBufferMode = "auto" +) + +var ( + ErrEnableTCXInvalid = errors.New("enableTCX must be \"auto\" or \"off\"") + ErrPacketParserRingBufferAutoNotSupported = errors.New("packetParserRingBuffer mode auto is not supported yet") + ErrPacketParserRingBufferInvalid = errors.New("packetParserRingBuffer must be set to enabled or disabled") + ErrPacketParserRingBufferInvalidBool = errors.New( + "packetParserRingBuffer must be enabled or disabled, got boolean", + ) +) + var ( - ErrorTelemetryIntervalTooSmall = fmt.Errorf("telemetryInterval smaller than %v is not allowed", MinTelemetryInterval) - DefaultTelemetryInterval = 15 * time.Minute + ErrorTelemetryIntervalTooSmall = fmt.Errorf( + "telemetryInterval smaller than %v is not allowed", + MinTelemetryInterval, + ) + DefaultTelemetryInterval = 15 * time.Minute + DefaultSamplingRate uint32 = 1 + DefaultFilterMapMaxEntries uint32 = 255 ) func (l *Level) UnmarshalText(text []byte) error { @@ -53,6 +86,28 @@ func (l *Level) String() string { } } +func (m *PacketParserRingBufferMode) UnmarshalText(text []byte) error { + s := strings.ToLower(strings.TrimSpace(string(text))) + switch s { + case string(PacketParserRingBufferEnabled): + *m = PacketParserRingBufferEnabled + return nil + case string(PacketParserRingBufferDisabled): + *m = PacketParserRingBufferDisabled + return nil + case string(PacketParserRingBufferAuto): + return ErrPacketParserRingBufferAutoNotSupported + case "": + return ErrPacketParserRingBufferInvalid + default: + return fmt.Errorf("invalid packetParserRingBuffer %q: %w", s, ErrPacketParserRingBufferInvalid) + } +} + +func (m *PacketParserRingBufferMode) IsEnabled() bool { + return *m == PacketParserRingBufferEnabled +} + type Server struct { Host string `yaml:"host"` Port int `yaml:"port"` @@ -64,17 +119,22 @@ type Config struct { EnabledPlugin []string `yaml:"enabledPlugin"` MetricsInterval time.Duration `yaml:"metricsInterval"` // Deprecated: Use only MetricsInterval instead in the go code. - MetricsIntervalDuration time.Duration `yaml:"metricsIntervalDuration"` - EnableTelemetry bool `yaml:"enableTelemetry"` - EnableRetinaEndpoint bool `yaml:"enableRetinaEndpoint"` - EnablePodLevel bool `yaml:"enablePodLevel"` - EnableConntrackMetrics bool `yaml:"enableConntrackMetrics"` - RemoteContext bool `yaml:"remoteContext"` - EnableAnnotations bool `yaml:"enableAnnotations"` - BypassLookupIPOfInterest bool `yaml:"bypassLookupIPOfInterest"` - DataAggregationLevel Level `yaml:"dataAggregationLevel"` - MonitorSockPath string `yaml:"monitorSockPath"` - TelemetryInterval time.Duration `yaml:"telemetryInterval"` + MetricsIntervalDuration time.Duration `yaml:"metricsIntervalDuration"` + EnableTelemetry bool `yaml:"enableTelemetry"` + EnableRetinaEndpoint bool `yaml:"enableRetinaEndpoint"` + EnablePodLevel bool `yaml:"enablePodLevel"` + EnableConntrackMetrics bool `yaml:"enableConntrackMetrics"` + RemoteContext bool `yaml:"remoteContext"` + EnableAnnotations bool `yaml:"enableAnnotations"` + BypassLookupIPOfInterest bool `yaml:"bypassLookupIPOfInterest"` + DataAggregationLevel Level `yaml:"dataAggregationLevel"` + MonitorSockPath string `yaml:"monitorSockPath"` + TelemetryInterval time.Duration `yaml:"telemetryInterval"` + DataSamplingRate uint32 `yaml:"dataSamplingRate"` + PacketParserRingBuffer PacketParserRingBufferMode `yaml:"packetParserRingBuffer"` + PacketParserRingBufferSize uint32 `yaml:"packetParserRingBufferSize"` + FilterMapMaxEntries uint32 `yaml:"filterMapMaxEntries"` + EnableTCX TCXMode `yaml:"enableTCX"` } func GetConfig(cfgFilename string) (*Config, error) { @@ -100,6 +160,7 @@ func GetConfig(cfgFilename string) (*Config, error) { mapstructure.StringToTimeDurationHookFunc(), // default hook. mapstructure.StringToSliceHookFunc(","), // default hook. decodeLevelHook, + decodePacketParserRingBufferModeHook, )) err = viper.Unmarshal(&config, decoderConfigOption) @@ -122,6 +183,34 @@ func GetConfig(cfgFilename string) (*Config, error) { return nil, ErrorTelemetryIntervalTooSmall } + // If unset, default sampling rate to 1 + if config.DataSamplingRate == 0 { + log.Printf("dataSamplingRate is not set, defaulting to %v", DefaultSamplingRate) + config.DataSamplingRate = DefaultSamplingRate + } + + // Default filter map max entries to 255 if not set. + if config.FilterMapMaxEntries == 0 { + config.FilterMapMaxEntries = DefaultFilterMapMaxEntries + } + + // Default EnableTCX to "auto" if unset, reject unknown values. + switch config.EnableTCX { + case "": + config.EnableTCX = TCXModeAuto + case TCXModeAuto, TCXModeOff: + // valid + default: + return nil, fmt.Errorf("invalid enableTCX %q: %w", config.EnableTCX, ErrEnableTCXInvalid) + } + + switch config.PacketParserRingBuffer { //nolint:exhaustive // we only care about Auto and empty (default) here + case "": + config.PacketParserRingBuffer = PacketParserRingBufferDisabled + case PacketParserRingBufferAuto: + return nil, ErrPacketParserRingBufferAutoNotSupported + } + return &config, nil } @@ -141,3 +230,22 @@ func decodeLevelHook(field, target reflect.Type, data interface{}) (interface{}, } return level, nil } + +func decodePacketParserRingBufferModeHook(field, target reflect.Type, data interface{}) (interface{}, error) { + if target != reflect.TypeOf(PacketParserRingBufferMode("")) { + return data, nil + } + + switch field.Kind() { //nolint:exhaustive // we only care about String and Bool + case reflect.String: + var mode PacketParserRingBufferMode + if err := mode.UnmarshalText([]byte(data.(string))); err != nil { + return nil, err + } + return mode, nil + case reflect.Bool: + return nil, ErrPacketParserRingBufferInvalidBool + default: + return data, nil + } +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 2d6ca3b92f..3d4df8432c 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -28,7 +28,9 @@ func TestGetConfig(t *testing.T) { c.RemoteContext || c.EnableAnnotations || c.TelemetryInterval != 15*time.Minute || - c.DataAggregationLevel != Low { + c.DataAggregationLevel != Low || + c.DataSamplingRate != 1 || + c.PacketParserRingBuffer != PacketParserRingBufferDisabled { t.Errorf("Expeted config should be same as ./testwith/config.yaml; instead got %+v", c) } } @@ -65,6 +67,103 @@ func TestDecodeLevelHook(t *testing.T) { result, err := decodeLevelHook(reflect.TypeOf(test.input), reflect.TypeOf(Level(0)), test.input) require.NoError(t, err) assert.Equal(t, test.expected, result) + } +} + +func TestGetConfig_EnableTCX(t *testing.T) { + tests := []struct { + name string + configFile string + expected TCXMode + expectedError error + }{ + { + name: "auto", + configFile: "./testwith/config-tcx-auto.yaml", + expected: TCXModeAuto, + }, + { + name: "off", + configFile: "./testwith/config-tcx-off.yaml", + expected: TCXModeOff, + }, + { + name: "empty defaults to auto", + configFile: "./testwith/config-tcx-empty.yaml", + expected: TCXModeAuto, + }, + { + name: "invalid value rejected", + configFile: "./testwith/config-tcx-invalid.yaml", + expectedError: ErrEnableTCXInvalid, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg, err := GetConfig(tt.configFile) + if tt.expectedError != nil { + require.ErrorIs(t, err, tt.expectedError) + return + } + require.NoError(t, err) + assert.Equal(t, tt.expected, cfg.EnableTCX) + }) + } +} +func TestDecodePacketParserRingBufferModeHook(t *testing.T) { + tests := []struct { + name string + input interface{} + expected interface{} + expectErr bool + expectedError error + }{ + { + name: "enabled", + input: "enabled", + expected: PacketParserRingBufferEnabled, + }, + { + name: "disabled", + input: "disabled", + expected: PacketParserRingBufferDisabled, + }, + { + name: "auto not supported", + input: "auto", + expectErr: true, + expectedError: ErrPacketParserRingBufferAutoNotSupported, + }, + { + name: "boolean rejected", + input: true, + expectErr: true, + }, + { + name: "non-string passthrough", + input: 123, + expected: 123, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, err := decodePacketParserRingBufferModeHook( + reflect.TypeOf(test.input), + reflect.TypeOf(PacketParserRingBufferMode("")), + test.input, + ) + if test.expectErr { + require.Error(t, err) + if test.expectedError != nil { + require.ErrorIs(t, err, test.expectedError) + } + return + } + require.NoError(t, err) + assert.Equal(t, test.expected, result) + }) } } diff --git a/pkg/config/hubble_config_linux.go b/pkg/config/hubble_config_linux.go index 3f24742459..1f24712b71 100644 --- a/pkg/config/hubble_config_linux.go +++ b/pkg/config/hubble_config_linux.go @@ -3,12 +3,12 @@ package config import ( + "log/slog" "path/filepath" "github.com/cilium/cilium/pkg/option" "github.com/cilium/hive/cell" sharedconfig "github.com/microsoft/retina/pkg/shared/config" - "github.com/sirupsen/logrus" "github.com/spf13/pflag" ) @@ -64,14 +64,14 @@ var ( cell.Config(DefaultRetinaHubbleConfig), - cell.Provide(func(logger logrus.FieldLogger) (Config, error) { + cell.Provide(func(logger *slog.Logger) (Config, error) { retinaConfigFile := filepath.Join(option.Config.ConfigDir, configFileName) conf, err := GetConfig(retinaConfigFile) if err != nil { - logger.Error(err) + logger.Error("failed to get config", "error", err) conf = DefaultRetinaConfig } - logger.Info(conf) + logger.Info("loaded config", "config", conf) return *conf, nil }), sharedconfig.Cell, diff --git a/pkg/config/testwith/config-tcx-auto.yaml b/pkg/config/testwith/config-tcx-auto.yaml new file mode 100644 index 0000000000..f3b75199c5 --- /dev/null +++ b/pkg/config/testwith/config-tcx-auto.yaml @@ -0,0 +1,6 @@ +apiServer: + host: "0.0.0.0" + port: 10093 +metricsIntervalDuration: "10s" +telemetryInterval: "15m" +enableTCX: "auto" diff --git a/pkg/config/testwith/config-tcx-empty.yaml b/pkg/config/testwith/config-tcx-empty.yaml new file mode 100644 index 0000000000..1a299f95fe --- /dev/null +++ b/pkg/config/testwith/config-tcx-empty.yaml @@ -0,0 +1,5 @@ +apiServer: + host: "0.0.0.0" + port: 10093 +metricsIntervalDuration: "10s" +telemetryInterval: "15m" diff --git a/pkg/config/testwith/config-tcx-invalid.yaml b/pkg/config/testwith/config-tcx-invalid.yaml new file mode 100644 index 0000000000..ab48e26fd3 --- /dev/null +++ b/pkg/config/testwith/config-tcx-invalid.yaml @@ -0,0 +1,6 @@ +apiServer: + host: "0.0.0.0" + port: 10093 +metricsIntervalDuration: "10s" +telemetryInterval: "15m" +enableTCX: "always" diff --git a/pkg/config/testwith/config-tcx-off.yaml b/pkg/config/testwith/config-tcx-off.yaml new file mode 100644 index 0000000000..7ccf75cce8 --- /dev/null +++ b/pkg/config/testwith/config-tcx-off.yaml @@ -0,0 +1,6 @@ +apiServer: + host: "0.0.0.0" + port: 10093 +metricsIntervalDuration: "10s" +telemetryInterval: "15m" +enableTCX: "off" diff --git a/pkg/config/testwith/config.yaml b/pkg/config/testwith/config.yaml index edcb5d685a..ff8dc0fb4d 100644 --- a/pkg/config/testwith/config.yaml +++ b/pkg/config/testwith/config.yaml @@ -10,3 +10,5 @@ metricsIntervalDuration: "10s" telemetryEnabled: true dataAggregationLevel: "low" telemetryInterval: "15m" +packetParserRingBuffer: "disabled" +packetParserRingBufferSize: 8388608 diff --git a/pkg/controllers/cache/cache.go b/pkg/controllers/cache/cache.go index c51fa18110..431269597b 100644 --- a/pkg/controllers/cache/cache.go +++ b/pkg/controllers/cache/cache.go @@ -153,7 +153,7 @@ func (c *Cache) getObjByIPType(ip string, t objectType) interface{} { // GetObjByIP returns the retina object for the given IP. func (c *Cache) GetObjByIP(ip string) interface{} { if ep := c.GetPodByIP(ip); ep != nil { - c.l.Debug("pod found for IP", zap.String("ip", ip), zap.String("pod Name", ep.Key())) + c.l.Debug("pod found for IP", zap.String("ip", ip), zap.Stringer("pod", ep)) return ep } @@ -168,6 +168,22 @@ func (c *Cache) GetObjByIP(ip string) interface{} { return nil } +func (c *Cache) GetAllNamespaces() []string { + c.RLock() + defer c.RUnlock() + + unique := make(map[string]struct{}) + for _, ep := range c.epMap { + unique[ep.Namespace()] = struct{}{} + } + + namespaces := make([]string, 0, len(unique)) + for ns := range unique { + namespaces = append(namespaces, ns) + } + return namespaces +} + func (c *Cache) GetIPsByNamespace(ns string) []net.IP { c.RLock() defer c.RUnlock() diff --git a/pkg/controllers/cache/cache_test.go b/pkg/controllers/cache/cache_test.go index 95266d51f7..ee5e3c42de 100644 --- a/pkg/controllers/cache/cache_test.go +++ b/pkg/controllers/cache/cache_test.go @@ -4,20 +4,17 @@ package cache import ( "net" + "sync" "testing" - "time" "github.com/microsoft/retina/pkg/common" "github.com/microsoft/retina/pkg/log" "github.com/microsoft/retina/pkg/pubsub" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" gomock "go.uber.org/mock/gomock" ) -const ( - until = 1 * time.Millisecond -) - func TestNewCache(t *testing.T) { log.SetupZapLogger(log.GetDefaultLogOpts()) ctrl := gomock.NewController(t) @@ -33,7 +30,11 @@ func TestCacheEndpoints(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() p := pubsub.NewMockPubSubInterface(ctrl) - p.EXPECT().Publish(common.PubSubPods, gomock.Any()).Times(2) + var wg sync.WaitGroup + wg.Add(2) + p.EXPECT().Publish(common.PubSubPods, gomock.Any()).Times(2).Do(func(pubsub.PubSubTopic, interface{}) { + wg.Done() + }) p.EXPECT().Subscribe(common.PubSubAPIServer, gomock.Any()).Times(1) c := New(p) assert.NotNil(t, c) @@ -92,7 +93,7 @@ func TestCacheEndpoints(t *testing.T) { err = c.DeleteRetinaEndpoint(addEndpoints.Key()) assert.NoError(t, err) - time.Sleep(until) + wg.Wait() } func TestCacheServices(t *testing.T) { @@ -106,7 +107,11 @@ func TestCacheServices(t *testing.T) { addSvc := common.NewRetinaSvc("svc1", "ns1", nil, nil, nil) - p.EXPECT().Publish(gomock.Any(), gomock.Any()).Times(2) + var wg sync.WaitGroup + wg.Add(2) + p.EXPECT().Publish(gomock.Any(), gomock.Any()).Times(2).Do(func(pubsub.PubSubTopic, interface{}) { + wg.Done() + }) err := c.UpdateRetinaSvc(addSvc) assert.Error(t, err) @@ -136,7 +141,7 @@ func TestCacheServices(t *testing.T) { err = c.DeleteRetinaSvc(addSvc.Key()) assert.NoError(t, err) - time.Sleep(until) + wg.Wait() } func TestCacheNodes(t *testing.T) { @@ -148,9 +153,13 @@ func TestCacheNodes(t *testing.T) { c := New(p) assert.NotNil(t, c) - addNode := common.NewRetinaNode("node1", net.IPv4(1, 2, 3, 4)) + addNode := common.NewRetinaNode("node1", net.IPv4(1, 2, 3, 4), "zone-1") - p.EXPECT().Publish(gomock.Any(), gomock.Any()).Times(2) + var wg sync.WaitGroup + wg.Add(2) + p.EXPECT().Publish(gomock.Any(), gomock.Any()).Times(2).Do(func(pubsub.PubSubTopic, interface{}) { + wg.Done() + }) err := c.UpdateRetinaNode(addNode) assert.NoError(t, err) @@ -169,7 +178,7 @@ func TestCacheNodes(t *testing.T) { err = c.DeleteRetinaNode(addNode.Name()) assert.NoError(t, err) - time.Sleep(until) + wg.Wait() } func TestAddPodSvcNodeSameIP(t *testing.T) { @@ -177,9 +186,12 @@ func TestAddPodSvcNodeSameIP(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() p := pubsub.NewMockPubSubInterface(ctrl) - p.EXPECT().Publish(common.PubSubPods, gomock.Any()).Times(2) - p.EXPECT().Publish(common.PubSubSvc, gomock.Any()).Times(2) - p.EXPECT().Publish(common.PubSubNode, gomock.Any()).Times(1) + var wg sync.WaitGroup + wg.Add(5) // 2 pod + 2 svc + 1 node publishes + doFn := func(pubsub.PubSubTopic, interface{}) { wg.Done() } + p.EXPECT().Publish(common.PubSubPods, gomock.Any()).Times(2).Do(doFn) + p.EXPECT().Publish(common.PubSubSvc, gomock.Any()).Times(2).Do(doFn) + p.EXPECT().Publish(common.PubSubNode, gomock.Any()).Times(1).Do(doFn) p.EXPECT().Subscribe(common.PubSubAPIServer, gomock.Any()).Times(1) c := New(p) assert.NotNil(t, c) @@ -210,7 +222,7 @@ func TestAddPodSvcNodeSameIP(t *testing.T) { assert.Equal(t, addSvc.Name(), svc.Name()) assert.Equal(t, addSvc.Namespace(), svc.Namespace()) - addNode := common.NewRetinaNode("node1", net.IPv4(1, 2, 3, 4)) + addNode := common.NewRetinaNode("node1", net.IPv4(1, 2, 3, 4), "zone-1") err = c.UpdateRetinaNode(addNode) assert.NoError(t, err) @@ -221,7 +233,7 @@ func TestAddPodSvcNodeSameIP(t *testing.T) { assert.Equal(t, addNode.Name(), node.Name()) assert.Equal(t, addNode.IPString(), node.IPString()) - time.Sleep(until) + wg.Wait() } func TestAddPodSvcNodeSameIPDiffNS(t *testing.T) { @@ -229,9 +241,12 @@ func TestAddPodSvcNodeSameIPDiffNS(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() p := pubsub.NewMockPubSubInterface(ctrl) - p.EXPECT().Publish(common.PubSubPods, gomock.Any()).Times(2) - p.EXPECT().Publish(common.PubSubSvc, gomock.Any()).Times(2) - p.EXPECT().Publish(common.PubSubNode, gomock.Any()).Times(1) + var wg sync.WaitGroup + wg.Add(5) // 2 pod + 2 svc + 1 node publishes + doFn := func(pubsub.PubSubTopic, interface{}) { wg.Done() } + p.EXPECT().Publish(common.PubSubPods, gomock.Any()).Times(2).Do(doFn) + p.EXPECT().Publish(common.PubSubSvc, gomock.Any()).Times(2).Do(doFn) + p.EXPECT().Publish(common.PubSubNode, gomock.Any()).Times(1).Do(doFn) p.EXPECT().Subscribe(common.PubSubAPIServer, gomock.Any()).Times(1) c := New(p) assert.NotNil(t, c) @@ -262,7 +277,7 @@ func TestAddPodSvcNodeSameIPDiffNS(t *testing.T) { assert.Equal(t, addSvc.Name(), svc.Name()) assert.Equal(t, addSvc.Namespace(), svc.Namespace()) - addNode := common.NewRetinaNode("node1", net.IPv4(1, 2, 3, 4)) + addNode := common.NewRetinaNode("node1", net.IPv4(1, 2, 3, 4), "zone-1") err = c.UpdateRetinaNode(addNode) assert.NoError(t, err) @@ -274,7 +289,7 @@ func TestAddPodSvcNodeSameIPDiffNS(t *testing.T) { assert.Equal(t, addNode.Name(), node.Name()) assert.Equal(t, addNode.IPString(), node.IPString()) - time.Sleep(until) + wg.Wait() } func TestAddPodDiffNs(t *testing.T) { @@ -282,7 +297,11 @@ func TestAddPodDiffNs(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() p := pubsub.NewMockPubSubInterface(ctrl) - p.EXPECT().Publish(common.PubSubPods, gomock.Any()).Times(3) + var wg sync.WaitGroup + wg.Add(3) + p.EXPECT().Publish(common.PubSubPods, gomock.Any()).Times(3).Do(func(pubsub.PubSubTopic, interface{}) { + wg.Done() + }) p.EXPECT().Subscribe(common.PubSubAPIServer, gomock.Any()).Times(1) c := New(p) assert.NotNil(t, c) @@ -318,7 +337,7 @@ func TestAddPodDiffNs(t *testing.T) { assert.Equal(t, addEndpoints.Name(), ep.Name()) assert.Equal(t, addEndpoints.Namespace(), ep.Namespace()) - time.Sleep(until) + wg.Wait() } func TestFailDelete(t *testing.T) { @@ -340,12 +359,10 @@ func TestFailDelete(t *testing.T) { err = c.DeleteRetinaSvc(svc.Key()) assert.Error(t, err) - node := common.NewRetinaNode("node1", net.IPv4(1, 2, 3, 4)) + node := common.NewRetinaNode("node1", net.IPv4(1, 2, 3, 4), "zone-1") err = c.DeleteRetinaNode(node.Name()) assert.Error(t, err) - - time.Sleep(until) } func TestCachingNamespace(t *testing.T) { @@ -365,3 +382,32 @@ func TestCachingNamespace(t *testing.T) { namespaces = c.GetAnnotatedNamespaces() assert.Equal(t, 0, len(namespaces)) } + +func TestGetAllNamespaces(t *testing.T) { + _, _ = log.SetupZapLogger(log.GetDefaultLogOpts()) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + p := pubsub.NewMockPubSubInterface(ctrl) + p.EXPECT().Subscribe(common.PubSubAPIServer, gomock.Any()).Times(1) + p.EXPECT().Publish(gomock.Any(), gomock.Any()).AnyTimes() + c := New(p) + + // Empty cache should return empty list + namespaces := c.GetAllNamespaces() + assert.Empty(t, namespaces) + + // Add endpoints in different namespaces + ep1 := common.NewRetinaEndpoint("pod1", "ns1", &common.IPAddresses{IPv4: net.IPv4(10, 0, 0, 1)}) + ep2 := common.NewRetinaEndpoint("pod2", "ns2", &common.IPAddresses{IPv4: net.IPv4(10, 0, 0, 2)}) + ep3 := common.NewRetinaEndpoint("pod3", "ns1", &common.IPAddresses{IPv4: net.IPv4(10, 0, 0, 3)}) + err := c.UpdateRetinaEndpoint(ep1) + require.NoError(t, err) + err = c.UpdateRetinaEndpoint(ep2) + require.NoError(t, err) + err = c.UpdateRetinaEndpoint(ep3) + require.NoError(t, err) + + namespaces = c.GetAllNamespaces() + assert.Len(t, namespaces, 2) + assert.ElementsMatch(t, []string{"ns1", "ns2"}, namespaces) +} diff --git a/pkg/controllers/cache/mock_cacheinterface.go b/pkg/controllers/cache/mock_cacheinterface.go index dd907f7927..4376a1c376 100644 --- a/pkg/controllers/cache/mock_cacheinterface.go +++ b/pkg/controllers/cache/mock_cacheinterface.go @@ -112,6 +112,20 @@ func (mr *MockCacheInterfaceMockRecorder) DeleteRetinaSvc(arg0 any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRetinaSvc", reflect.TypeOf((*MockCacheInterface)(nil).DeleteRetinaSvc), arg0) } +// GetAllNamespaces mocks base method. +func (m *MockCacheInterface) GetAllNamespaces() []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllNamespaces") + ret0, _ := ret[0].([]string) + return ret0 +} + +// GetAllNamespaces indicates an expected call of GetAllNamespaces. +func (mr *MockCacheInterfaceMockRecorder) GetAllNamespaces() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllNamespaces", reflect.TypeOf((*MockCacheInterface)(nil).GetAllNamespaces)) +} + // GetAnnotatedNamespaces mocks base method. func (m *MockCacheInterface) GetAnnotatedNamespaces() []string { m.ctrl.T.Helper() diff --git a/pkg/controllers/cache/types.go b/pkg/controllers/cache/types.go index a37cc9f18a..22db7072ad 100644 --- a/pkg/controllers/cache/types.go +++ b/pkg/controllers/cache/types.go @@ -20,6 +20,8 @@ type CacheInterface interface { GetObjByIP(ip string) interface{} // GetIPsByNamespace returns the net.IPs for a given namespace. GetIPsByNamespace(ns string) []net.IP + // GetAllNamespaces returns all unique namespaces that have at least one endpoint in the cache. + GetAllNamespaces() []string // GetAnnotatedNamespaces returns list of namespaces that are annotated with retina to observe. GetAnnotatedNamespaces() []string diff --git a/pkg/controllers/daemon/node/controller.go b/pkg/controllers/daemon/node/controller.go index 60221106c4..f085a1dfbb 100644 --- a/pkg/controllers/daemon/node/controller.go +++ b/pkg/controllers/daemon/node/controller.go @@ -75,7 +75,12 @@ func (r *NodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. return ctrl.Result{}, nil } - retinaNodeCommon := retinaCommon.NewRetinaNode(node.Name, net.ParseIP(node.Status.Addresses[0].Address)) + if len(node.Status.Addresses) == 0 { + r.l.Warn("Node has no addresses", zap.String("Node", req.NamespacedName.String())) + return ctrl.Result{}, nil + } + + retinaNodeCommon := retinaCommon.NewRetinaNode(node.Name, net.ParseIP(node.Status.Addresses[0].Address), node.Labels[corev1.LabelTopologyZone]) if err := r.cache.UpdateRetinaNode(retinaNodeCommon); err != nil { r.l.Error("Failed to update RetinaNode in Cache", zap.Error(err), zap.String("Node", req.NamespacedName.String())) return ctrl.Result{}, err diff --git a/pkg/controllers/daemon/nodereconciler/cell_linux.go b/pkg/controllers/daemon/nodereconciler/cell_linux.go index c2f97b5dbb..222a6d9d16 100644 --- a/pkg/controllers/daemon/nodereconciler/cell_linux.go +++ b/pkg/controllers/daemon/nodereconciler/cell_linux.go @@ -1,6 +1,7 @@ package nodereconciler import ( + "log/slog" "os" datapath "github.com/cilium/cilium/pkg/datapath/types" @@ -10,7 +11,6 @@ import ( "github.com/microsoft/retina/pkg/config" "github.com/microsoft/retina/pkg/log" "github.com/pkg/errors" - "github.com/sirupsen/logrus" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -20,10 +20,10 @@ var Cell = cell.Module( "Node Controller monitors Node CRUD events", cell.Provide(newNodeController), // Setting up the node controller with the controller manager - cell.Invoke(func(l logrus.FieldLogger, nr *NodeReconciler, ctrlManager ctrl.Manager) error { + cell.Invoke(func(l *slog.Logger, nr *NodeReconciler, ctrlManager ctrl.Manager) error { l.Info("Setting up node controller with manager") if err := nr.SetupWithManager(ctrlManager); err != nil { - l.Errorf("failed to setup node controller with manager: %v", err) + l.Error("failed to setup node controller with manager", "error", err) return errors.Wrap(err, "failed to setup node controller with manager") } return nil @@ -34,7 +34,6 @@ type params struct { cell.In Config config.RetinaHubbleConfig - Logger logrus.FieldLogger Client client.Client IPCache *ipcache.IPCache } @@ -51,7 +50,7 @@ func newNodeController(params params) (*NodeReconciler, error) { n := &NodeReconciler{ Client: params.Client, clusterName: params.Config.ClusterName, - l: params.Logger.WithField("component", "node-controller"), + l: log.Logger().Named("node-controller"), nodes: make(map[string]types.Node), handlers: make(map[string]datapath.NodeHandler), c: params.IPCache, diff --git a/pkg/controllers/daemon/nodereconciler/node_controller_linux.go b/pkg/controllers/daemon/nodereconciler/node_controller_linux.go index cd67c8469d..bcbb903bfd 100644 --- a/pkg/controllers/daemon/nodereconciler/node_controller_linux.go +++ b/pkg/controllers/daemon/nodereconciler/node_controller_linux.go @@ -11,12 +11,14 @@ import ( "sync" "github.com/microsoft/retina/pkg/common/apiretry" - "github.com/sirupsen/logrus" + "github.com/microsoft/retina/pkg/log" "go.uber.org/zap" corev1 "k8s.io/api/core/v1" errors "k8s.io/apimachinery/pkg/api/errors" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" cmtypes "github.com/cilium/cilium/pkg/clustermesh/types" datapath "github.com/cilium/cilium/pkg/datapath/types" @@ -35,7 +37,7 @@ type NodeReconciler struct { clusterName string - l logrus.FieldLogger + l *log.ZapLogger handlers map[string]datapath.NodeHandler nodes map[string]types.Node c *ipc.IPCache @@ -43,20 +45,6 @@ type NodeReconciler struct { m sync.RWMutex } -// isNodeUpdated checks if the node has been updated. -// This is a simple check for labels and annotations -// being updated. Those are the only fields that are mutable. -// AKS specific for now. -func isNodeUpdated(n1, n2 types.Node) bool { - if !reflect.DeepEqual(n1.Labels, n2.Labels) { - return true - } - if !reflect.DeepEqual(n1.Annotations, n2.Annotations) { - return true - } - return false -} - func (r *NodeReconciler) addNode(node *corev1.Node) { r.m.Lock() defer r.m.Unlock() @@ -89,7 +77,7 @@ func (r *NodeReconciler) addNode(node *corev1.Node) { nd.Cluster = r.clusterName // Check if the node already exists. - if curNode, ok := r.nodes[node.Name]; ok && !isNodeUpdated(curNode, nd) { + if _, ok := r.nodes[node.Name]; ok { r.l.Debug("Node already exists", zap.String("Node", node.Name)) } @@ -117,7 +105,7 @@ func (r *NodeReconciler) addNode(node *corev1.Node) { r.l.Debug("Added IP to ipcache", zap.String("IP", address.ToString())) } - r.l.Info("Added Node", zap.String("Node", node.Name)) + r.l.Info("Added Node", zap.String("name", nd.Name)) } func (r *NodeReconciler) deleteNode(node *corev1.Node) { @@ -204,8 +192,45 @@ func (r *NodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. // SetupWithManager sets up the controller with the Manager. func (r *NodeReconciler) SetupWithManager(mgr ctrl.Manager) error { r.l.Debug("Setting up Node controller") + + // Create a predicate to filter node events + nodePredicate := predicate.Funcs{ + CreateFunc: func(event.CreateEvent) bool { + // Always reconcile on node creation + return true + }, + DeleteFunc: func(event.DeleteEvent) bool { + // Always reconcile on node deletion + return true + }, + UpdateFunc: func(e event.UpdateEvent) bool { + oldNode, ok := e.ObjectOld.(*corev1.Node) + if !ok { + r.l.Error("Failed to convert old object to Node") + return false + } + + newNode, ok := e.ObjectNew.(*corev1.Node) + if !ok { + r.l.Error("Failed to convert new object to Node") + return false + } + + // Compare node IP addresses + oldIPs := extractNodeIPs(oldNode) + newIPs := extractNodeIPs(newNode) + + // Only reconcile if IPs changed or labels/annotations changed + return !reflect.DeepEqual(oldIPs, newIPs) || !reflect.DeepEqual(oldNode.Labels, newNode.Labels) || !reflect.DeepEqual(oldNode.Annotations, newNode.Annotations) + }, + GenericFunc: func(event.GenericEvent) bool { + return false + }, + } + err := ctrl.NewControllerManagedBy(mgr). For(&corev1.Node{}). + WithEventFilter(nodePredicate). Complete(r) if err != nil { return fmt.Errorf("setting up node controller: %w", err) @@ -221,8 +246,6 @@ func (r *NodeReconciler) ClusterSizeDependantInterval(time.Duration) time.Durati return time.Second * 5 } -func (r *NodeReconciler) Enqueue(*types.Node) {} - func (r *NodeReconciler) GetNodeIdentities() []types.Identity { return []types.Identity{} } @@ -239,8 +262,15 @@ func (r *NodeReconciler) NodeSync() {} func (r *NodeReconciler) NodeUpdated(types.Node) {} -func (r *NodeReconciler) StartNeighborRefresh(datapath.NodeNeighbors) {} - -func (r *NodeReconciler) StartNodeNeighborLinkUpdater(datapath.NodeNeighbors) {} - func (r *NodeReconciler) SetPrefixClusterMutatorFn(func(*types.Node) []cmtypes.PrefixClusterOpts) {} + +// extractNodeIPs extracts IP addresses from a node +func extractNodeIPs(node *corev1.Node) map[string]string { + ips := make(map[string]string) + for _, addr := range node.Status.Addresses { + if addr.Type == corev1.NodeInternalIP || addr.Type == corev1.NodeExternalIP { + ips[string(addr.Type)] = addr.Address + } + } + return ips +} diff --git a/pkg/controllers/operator/capture/cleanup_after_upload_test.go b/pkg/controllers/operator/capture/cleanup_after_upload_test.go new file mode 100644 index 0000000000..2c3dbb0b28 --- /dev/null +++ b/pkg/controllers/operator/capture/cleanup_after_upload_test.go @@ -0,0 +1,468 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package capture + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + + retinav1alpha1 "github.com/microsoft/retina/crd/api/v1alpha1" + "github.com/microsoft/retina/pkg/log" +) + +const testSecretName = "test-secret" + +func newScheme() *runtime.Scheme { + s := runtime.NewScheme() + _ = retinav1alpha1.AddToScheme(s) + _ = batchv1.AddToScheme(s) + _ = corev1.AddToScheme(s) + return s +} + +func newTestReconciler(objects ...runtime.Object) *CaptureReconciler { + _, _ = log.SetupZapLogger(log.GetDefaultLogOpts()) + scheme := newScheme() + + clientObjects := make([]client.Object, 0, len(objects)) + for _, obj := range objects { + clientObjects = append(clientObjects, obj.(client.Object)) + } + + fakeClient := fakeclient.NewClientBuilder(). + WithScheme(scheme). + WithObjects(clientObjects...). + WithStatusSubresource(&retinav1alpha1.Capture{}, &batchv1.Job{}). + Build() + + return &CaptureReconciler{ + Client: fakeClient, + scheme: scheme, + logger: log.Logger().Named("capture-test"), + } +} + +func TestCleanUpAfterUpload_AllJobsSucceeded_WithBlobUpload(t *testing.T) { + secretName := testSecretName + capture := &retinav1alpha1.Capture{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-capture", + Namespace: "default", + Finalizers: []string{captureFinalizer}, + }, + Spec: retinav1alpha1.CaptureSpec{ + CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ + CaptureTarget: retinav1alpha1.CaptureTarget{ + NodeSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "kubernetes.io/role": "agent", + }, + }, + }, + }, + OutputConfiguration: retinav1alpha1.OutputConfiguration{ + BlobUpload: &secretName, + }, + CleanUpAfterUpload: true, + }, + } + + completionTime := metav1.Now() + jobs := []batchv1.Job{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-capture-job-1", + Namespace: "default", + }, + Status: batchv1.JobStatus{ + CompletionTime: &completionTime, + Conditions: []batchv1.JobCondition{ + { + Type: batchv1.JobComplete, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + } + + reconciler := newTestReconciler(capture) + ctx := context.Background() + + _, err := reconciler.updateCaptureStatusFromJobs(ctx, capture, jobs) + require.NoError(t, err) + + // The capture should have been deleted (triggering the finalizer cleanup) + deletedCapture := &retinav1alpha1.Capture{} + err = reconciler.Get(ctx, types.NamespacedName{Name: "test-capture", Namespace: "default"}, deletedCapture) + assert.True(t, err != nil || deletedCapture.DeletionTimestamp != nil, + "capture should be deleted or marked for deletion when CleanUpAfterUpload is true and all jobs succeeded") +} + +func TestCleanUpAfterUpload_AllJobsSucceeded_WithS3Upload(t *testing.T) { + capture := &retinav1alpha1.Capture{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-capture-s3", + Namespace: "default", + Finalizers: []string{captureFinalizer}, + }, + Spec: retinav1alpha1.CaptureSpec{ + CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ + CaptureTarget: retinav1alpha1.CaptureTarget{ + NodeSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "kubernetes.io/role": "agent", + }, + }, + }, + }, + OutputConfiguration: retinav1alpha1.OutputConfiguration{ + S3Upload: &retinav1alpha1.S3Upload{ + Bucket: "test-bucket", + SecretName: "test-s3-secret", + Region: "us-east-1", + }, + }, + CleanUpAfterUpload: true, + }, + } + + completionTime := metav1.Now() + jobs := []batchv1.Job{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-capture-s3-job-1", + Namespace: "default", + }, + Status: batchv1.JobStatus{ + CompletionTime: &completionTime, + Conditions: []batchv1.JobCondition{ + { + Type: batchv1.JobComplete, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + } + + reconciler := newTestReconciler(capture) + ctx := context.Background() + + _, err := reconciler.updateCaptureStatusFromJobs(ctx, capture, jobs) + require.NoError(t, err) + + // The capture should have been deleted + deletedCapture := &retinav1alpha1.Capture{} + err = reconciler.Get(ctx, types.NamespacedName{Name: "test-capture-s3", Namespace: "default"}, deletedCapture) + assert.True(t, err != nil || deletedCapture.DeletionTimestamp != nil, + "capture should be deleted when CleanUpAfterUpload is true with S3 upload and all jobs succeeded") +} + +func TestCleanUpAfterUpload_NotTriggeredWhenFalse(t *testing.T) { + secretName := testSecretName + capture := &retinav1alpha1.Capture{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-capture-no-cleanup", + Namespace: "default", + Finalizers: []string{captureFinalizer}, + }, + Spec: retinav1alpha1.CaptureSpec{ + CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ + CaptureTarget: retinav1alpha1.CaptureTarget{ + NodeSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "kubernetes.io/role": "agent", + }, + }, + }, + }, + OutputConfiguration: retinav1alpha1.OutputConfiguration{ + BlobUpload: &secretName, + }, + CleanUpAfterUpload: false, + }, + } + + completionTime := metav1.Now() + jobs := []batchv1.Job{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-capture-no-cleanup-job-1", + Namespace: "default", + }, + Status: batchv1.JobStatus{ + CompletionTime: &completionTime, + Conditions: []batchv1.JobCondition{ + { + Type: batchv1.JobComplete, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + } + + reconciler := newTestReconciler(capture) + ctx := context.Background() + + _, err := reconciler.updateCaptureStatusFromJobs(ctx, capture, jobs) + require.NoError(t, err) + + // Capture should still exist since CleanUpAfterUpload is false + existingCapture := &retinav1alpha1.Capture{} + err = reconciler.Get(ctx, types.NamespacedName{Name: "test-capture-no-cleanup", Namespace: "default"}, existingCapture) + require.NoError(t, err) + assert.Nil(t, existingCapture.DeletionTimestamp, "capture should NOT be deleted when CleanUpAfterUpload is false") +} + +func TestCleanUpAfterUpload_NotTriggeredOnFailedJobs(t *testing.T) { + secretName := testSecretName + capture := &retinav1alpha1.Capture{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-capture-failed", + Namespace: "default", + Finalizers: []string{captureFinalizer}, + }, + Spec: retinav1alpha1.CaptureSpec{ + CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ + CaptureTarget: retinav1alpha1.CaptureTarget{ + NodeSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "kubernetes.io/role": "agent", + }, + }, + }, + }, + OutputConfiguration: retinav1alpha1.OutputConfiguration{ + BlobUpload: &secretName, + }, + CleanUpAfterUpload: true, + }, + } + + jobs := []batchv1.Job{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-capture-failed-job-1", + Namespace: "default", + }, + Status: batchv1.JobStatus{ + Conditions: []batchv1.JobCondition{ + { + Type: batchv1.JobFailed, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + } + + reconciler := newTestReconciler(capture) + ctx := context.Background() + + _, err := reconciler.updateCaptureStatusFromJobs(ctx, capture, jobs) + require.NoError(t, err) + + // Capture should still exist since jobs failed + existingCapture := &retinav1alpha1.Capture{} + err = reconciler.Get(ctx, types.NamespacedName{Name: "test-capture-failed", Namespace: "default"}, existingCapture) + require.NoError(t, err) + assert.Nil(t, existingCapture.DeletionTimestamp, "capture should NOT be deleted when jobs failed") +} + +func TestCleanUpAfterUpload_NotTriggeredWithoutRemoteStorage(t *testing.T) { + hostPath := "/mnt/captures" + capture := &retinav1alpha1.Capture{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-capture-local", + Namespace: "default", + Finalizers: []string{captureFinalizer}, + }, + Spec: retinav1alpha1.CaptureSpec{ + CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ + CaptureTarget: retinav1alpha1.CaptureTarget{ + NodeSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "kubernetes.io/role": "agent", + }, + }, + }, + }, + OutputConfiguration: retinav1alpha1.OutputConfiguration{ + HostPath: &hostPath, + }, + CleanUpAfterUpload: true, + }, + } + + completionTime := metav1.Now() + jobs := []batchv1.Job{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-capture-local-job-1", + Namespace: "default", + }, + Status: batchv1.JobStatus{ + CompletionTime: &completionTime, + Conditions: []batchv1.JobCondition{ + { + Type: batchv1.JobComplete, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + } + + reconciler := newTestReconciler(capture) + ctx := context.Background() + + _, err := reconciler.updateCaptureStatusFromJobs(ctx, capture, jobs) + require.NoError(t, err) + + // Capture should still exist since there's no remote storage + existingCapture := &retinav1alpha1.Capture{} + err = reconciler.Get(ctx, types.NamespacedName{Name: "test-capture-local", Namespace: "default"}, existingCapture) + require.NoError(t, err) + assert.Nil(t, existingCapture.DeletionTimestamp, "capture should NOT be deleted when only local storage is used") +} + +func TestCleanUpAfterUpload_AllJobsSucceeded_WithPVC(t *testing.T) { + pvcName := "test-pvc" + capture := &retinav1alpha1.Capture{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-capture-pvc", + Namespace: "default", + Finalizers: []string{captureFinalizer}, + }, + Spec: retinav1alpha1.CaptureSpec{ + CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ + CaptureTarget: retinav1alpha1.CaptureTarget{ + NodeSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "kubernetes.io/role": "agent", + }, + }, + }, + }, + OutputConfiguration: retinav1alpha1.OutputConfiguration{ + PersistentVolumeClaim: &pvcName, + }, + CleanUpAfterUpload: true, + }, + } + + completionTime := metav1.Now() + jobs := []batchv1.Job{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-capture-pvc-job-1", + Namespace: "default", + }, + Status: batchv1.JobStatus{ + CompletionTime: &completionTime, + Conditions: []batchv1.JobCondition{ + { + Type: batchv1.JobComplete, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + } + + reconciler := newTestReconciler(capture) + ctx := context.Background() + + _, err := reconciler.updateCaptureStatusFromJobs(ctx, capture, jobs) + require.NoError(t, err) + + // The capture should have been deleted + deletedCapture := &retinav1alpha1.Capture{} + err = reconciler.Get(ctx, types.NamespacedName{Name: "test-capture-pvc", Namespace: "default"}, deletedCapture) + assert.True(t, err != nil || deletedCapture.DeletionTimestamp != nil, + "capture should be deleted when CleanUpAfterUpload is true with PVC and all jobs succeeded") +} + +func TestCleanUpAfterUpload_MixedJobResults_NotTriggered(t *testing.T) { + secretName := testSecretName + capture := &retinav1alpha1.Capture{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-capture-mixed", + Namespace: "default", + Finalizers: []string{captureFinalizer}, + }, + Spec: retinav1alpha1.CaptureSpec{ + CaptureConfiguration: retinav1alpha1.CaptureConfiguration{ + CaptureTarget: retinav1alpha1.CaptureTarget{ + NodeSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "kubernetes.io/role": "agent", + }, + }, + }, + }, + OutputConfiguration: retinav1alpha1.OutputConfiguration{ + BlobUpload: &secretName, + }, + CleanUpAfterUpload: true, + }, + } + + completionTime := metav1.Now() + jobs := []batchv1.Job{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-capture-mixed-job-1", + Namespace: "default", + }, + Status: batchv1.JobStatus{ + CompletionTime: &completionTime, + Conditions: []batchv1.JobCondition{ + { + Type: batchv1.JobComplete, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-capture-mixed-job-2", + Namespace: "default", + }, + Status: batchv1.JobStatus{ + Conditions: []batchv1.JobCondition{ + { + Type: batchv1.JobFailed, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + } + + reconciler := newTestReconciler(capture) + ctx := context.Background() + + _, err := reconciler.updateCaptureStatusFromJobs(ctx, capture, jobs) + require.NoError(t, err) + + // Capture should still exist since one job failed + existingCapture := &retinav1alpha1.Capture{} + err = reconciler.Get(ctx, types.NamespacedName{Name: "test-capture-mixed", Namespace: "default"}, existingCapture) + require.NoError(t, err) + assert.Nil(t, existingCapture.DeletionTimestamp, "capture should NOT be deleted when some jobs failed even if others succeeded") +} diff --git a/pkg/controllers/operator/capture/controller.go b/pkg/controllers/operator/capture/controller.go index 3c86f44784..8e2079d99c 100644 --- a/pkg/controllers/operator/capture/controller.go +++ b/pkg/controllers/operator/capture/controller.go @@ -215,7 +215,31 @@ func (cr *CaptureReconciler) updateCaptureStatusFromJobs(ctx context.Context, ca } } capture.Status.CompletionTime = &lastCompleteTime - return cr.updateStatus(ctx, capture) + if result, err := cr.updateStatus(ctx, capture); err != nil { + return result, err + } + + // If CleanUpAfterUpload is enabled and all jobs succeeded (uploaded to remote storage), + // automatically delete the Capture resource which triggers cleanup via the finalizer. + if !capture.Spec.CleanUpAfterUpload || len(failedJobs) != 0 { + return ctrl.Result{}, nil + } + + hasRemoteStorage := capture.Spec.OutputConfiguration.BlobUpload != nil || + capture.Spec.OutputConfiguration.S3Upload != nil || + capture.Spec.OutputConfiguration.PersistentVolumeClaim != nil + if !hasRemoteStorage { + return ctrl.Result{}, nil + } + + cr.logger.Info("All capture jobs completed successfully with remote storage upload, performing automatic cleanup", + zap.String("Capture", captureRef.String())) + if err := cr.Delete(ctx, capture); err != nil { + cr.logger.Error("Failed to auto-delete Capture after successful upload", + zap.Error(err), zap.String("Capture", captureRef.String())) + return ctrl.Result{}, fmt.Errorf("failed to auto-delete Capture after successful upload: %w", err) + } + return ctrl.Result{}, nil } func (cr *CaptureReconciler) createJobsFromCapture(ctx context.Context, capture *retinav1alpha1.Capture) (ctrl.Result, error) { @@ -226,7 +250,6 @@ func (cr *CaptureReconciler) createJobsFromCapture(ctx context.Context, capture jobs, err := cr.captureToPodTranslator.TranslateCaptureToJobs(ctx, capture) if err != nil { - cr.logger.Error("Failed to translate Capture to jobs", zap.Error(err), zap.String("Capture", captureRef.String())) var errorReason string switch err.(type) { case pkgcapture.CaptureJobNumExceedLimitError: @@ -307,6 +330,16 @@ func (cr *CaptureReconciler) handleUpdate(ctx context.Context, capture *retinav1 return cr.updateCaptureStatusFromJobs(ctx, capture, captureJobList.Items) } + // Set StartTime for CRD-created captures since the operator doesn't do it by default + // (unlike the CLI), and missing it causes a timestamp parsing error in the capture job. + if capture.Status.StartTime == nil { + now := metav1.Now() + capture.Status.StartTime = &now + if _, err := cr.updateStatus(ctx, capture); err != nil { + return ctrl.Result{}, err + } + } + // create SAS URL and then secret for the Capture if managed storage account is enabled. // Don't repeat the process if the secret already exists. if cr.managedStorageAccountEnabled() { @@ -341,10 +374,10 @@ func (cr *CaptureReconciler) handleUpdate(ctx context.Context, capture *retinav1 // TODO(mainred): update Capture with container/blob info to simply the following blob download capture.Spec.OutputConfiguration.BlobUpload = to.Ptr(secret.Name) if err = cr.Client.Update(ctx, capture); err != nil { - cr.logger.Error("Failed to update capture with managed secret", zap.Error(err), zap.String("secret", secret.Name), zap.String("Capture", captureRef.String())) + cr.logger.Error("Failed to update capture with managed secret", zap.Error(err), zap.String("Capture", captureRef.String())) return ctrl.Result{}, fmt.Errorf("failed to update capture with managed secret: %w", err) } - cr.logger.Info("Use the existing secret", zap.Error(err), zap.String("Capture", captureRef.String()), zap.String("secret", *capture.Spec.OutputConfiguration.BlobUpload)) + cr.logger.Info("Capture updated with managed secret", zap.String("Capture", captureRef.String())) } } diff --git a/pkg/controllers/operator/cilium-crds/endpoint/endpoint_controller_linux.go b/pkg/controllers/operator/cilium-crds/endpoint/endpoint_controller_linux.go index 4be4c84ddd..019079bb09 100644 --- a/pkg/controllers/operator/cilium-crds/endpoint/endpoint_controller_linux.go +++ b/pkg/controllers/operator/cilium-crds/endpoint/endpoint_controller_linux.go @@ -14,8 +14,6 @@ import ( "github.com/microsoft/retina/pkg/controllers/operator/cilium-crds/cache" "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "go.uber.org/zap" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -30,6 +28,7 @@ import ( "github.com/cilium/cilium/pkg/labels" "github.com/cilium/hive/cell" "github.com/cilium/workerpool" + retinalog "github.com/microsoft/retina/pkg/log" ) const ( @@ -47,7 +46,7 @@ var ErrClientsetDisabled = errors.New("failure due to clientset disabled") // endpointReconciler managed the lifecycle of CiliumEndpoints and CiliumIdentities from Pods. type endpointReconciler struct { *sync.Mutex - l logrus.FieldLogger + l *slog.Logger clientset versioned.Interface ciliumSlimClientSet slim_clientset.Interface // podEvents represents Pod CRUD events relayed from the Pod controller. If the Pod was deleted, the PodCacheObject.Pod field will be nil @@ -76,7 +75,6 @@ type endpointReconciler struct { type params struct { cell.In - Logger logrus.FieldLogger Lifecycle cell.Lifecycle Clientset k8sClient.Clientset CiliumEndpoints resource.Resource[*ciliumv2.CiliumEndpoint] @@ -89,7 +87,7 @@ func registerEndpointController(p params) error { return ErrClientsetDisabled } - l := p.Logger.WithField("component", "endpointcontroller") + l := retinalog.SlogLogger().With("component", "endpointcontroller") r := &endpointReconciler{ Mutex: &sync.Mutex{}, l: l, @@ -106,10 +104,10 @@ func registerEndpointController(p params) error { return nil } -func (r *endpointReconciler) Start(_ cell.HookContext) error { +func (r *endpointReconciler) Start(ctx cell.HookContext) error { // NOTE: we must create IdentityManager on leader election since its allocator auto-starts on creation. // There is a way to disable auto-start but then there is no exposed function to simply start(). - im, err := NewIdentityManager(r.l, r.clientset) + im, err := NewIdentityManager(ctx, r.l, r.clientset) if err != nil { return errors.Wrap(err, "failed to create identity manager") } @@ -152,7 +150,7 @@ func (r *endpointReconciler) run(pctx context.Context) error { } if ev.Object != nil && ev.Object.Spec.HostNetwork { - r.l.WithField("podKey", ev.Key.String()).Debug("pod is host networked, skipping") + r.l.Debug("pod is host networked, skipping", "podKey", ev.Key.String()) ev.Done(nil) continue } @@ -161,7 +159,7 @@ func (r *endpointReconciler) run(pctx context.Context) error { return r.runEventHandler(ctx, ev) }) if err != nil { - r.l.WithError(err).WithField("podKey", ev.Key.String()).Error("failed to submit pod event handler") + r.l.Error("failed to submit pod event handler", "error", err, "podKey", ev.Key.String()) } case <-pctx.Done(): @@ -180,7 +178,7 @@ func (r *endpointReconciler) runEventHandler(pctx context.Context, ev resource.E case resource.Upsert: // HANDLE UPSERT if ev.Object != nil && ev.Object.Spec.HostNetwork { - r.l.WithField("podKey", ev.Key.String()).Debug("pod is host networked, skipping") + r.l.Debug("pod is host networked, skipping", "podKey", ev.Key.String()) } else { err = r.ReconcilePod(ctx, ev.Key, ev.Object) } @@ -190,7 +188,7 @@ func (r *endpointReconciler) runEventHandler(pctx context.Context, ev resource.E cancel() if err != nil { - r.l.WithError(err).WithField("podKey", ev.Key.String()).Error("error creating cilium endpoint. requeuing pod") + r.l.Error("error creating cilium endpoint. requeuing pod", "error", err, "podKey", ev.Key.String()) } ev.Done(err) @@ -224,7 +222,7 @@ func (r *endpointReconciler) runNamespaceEvents(pctx context.Context) error { cancel() if err != nil { - r.l.WithError(err).WithField("namespaceKey", ev.Key.String()).Error("error creating cilium endpoint. requeuing namespace") + r.l.Error("error creating cilium endpoint. requeuing namespace", "error", err, "namespaceKey", ev.Key.String()) } ev.Done(err) case <-pctx.Done(): @@ -237,17 +235,17 @@ func (r *endpointReconciler) runNamespaceEvents(pctx context.Context) error { func (r *endpointReconciler) ReconcilePodsInNamespace(ctx context.Context, namespace string) error { r.Mutex.Lock() defer r.Mutex.Unlock() - r.l.Debug("reconciling pods in namespace", zap.String("namespace ", namespace)) + r.l.Debug("reconciling pods in namespace", "namespace", namespace) podList := r.store.ListPodKeysByNamespace(namespace) for _, podKey := range podList { pod, ok := r.store.GetPod(podKey) if !ok { - r.l.WithField("podKey", podKey.String()).Debug("pod not found in cache, skipping") + r.l.Debug("pod not found in cache, skipping", "podKey", podKey.String()) continue } if pod.toDelete { - r.l.WithField("podKey", podKey.String()).Debug("pod marked for deletion, skipping") + r.l.Debug("pod marked for deletion, skipping", "podKey", podKey.String()) continue } @@ -259,17 +257,16 @@ func (r *endpointReconciler) ReconcilePodsInNamespace(ctx context.Context, names newPEP.lbls = endpointsLabels r.l.Debug("upserting pod in namespace", - zap.String("namespace ", namespace), - zap.String("podKey", podKey.String()), - zap.Any("old labels ", pod.lbls), - zap.Any("new labels ", newPEP.lbls), + "namespace", namespace, + "podKey", podKey.String(), + "old labels", pod.lbls, + "new labels", newPEP.lbls, ) err = r.handlePodUpsert(ctx, newPEP) if err != nil { - r.l.Error("failed to upsert pod", zap.Error(err), zap.String("podKey", podKey.String())) + r.l.Error("failed to upsert pod", "error", err, "podKey", podKey.String()) } - if err != nil { return errors.Wrap(err, "failed to upsert pod") } @@ -282,7 +279,7 @@ func (r *endpointReconciler) ReconcilePodsInNamespace(ctx context.Context, names func (r *endpointReconciler) ReconcilePod(ctx context.Context, podKey resource.Key, pod *slim_corev1.Pod) error { r.Mutex.Lock() defer r.Mutex.Unlock() - r.l.Debug("reconciling pod with lock", zap.String("namespace", podKey.Namespace), zap.String("pod ", podKey.Name)) + r.l.Debug("reconciling pod with lock", "namespace", podKey.Namespace, "pod", podKey.Name) return r.reconcilePod(ctx, podKey, pod) } @@ -297,7 +294,7 @@ func (r *endpointReconciler) reconcilePod(ctx context.Context, podKey resource.K } if pod.Status.PodIP == "" || pod.Status.HostIP == "" { - r.l.WithField("podKey", podKey.String()).Trace("pod missing an IP, skipping") + r.l.Debug("pod missing an IP, skipping", "podKey", podKey.String()) return nil } @@ -326,7 +323,7 @@ func (r *endpointReconciler) reconcilePod(ctx context.Context, podKey resource.K func (r *endpointReconciler) HandlePodDelete(ctx context.Context, n resource.Key) error { r.Mutex.Lock() defer r.Mutex.Unlock() - r.l.Debug("handling pod delete with lock", zap.String("podKey", n.String())) + r.l.Debug("handling pod delete with lock", "podKey", n.String()) return r.handlePodDelete(ctx, n) } @@ -335,20 +332,20 @@ func (r *endpointReconciler) handlePodDelete(ctx context.Context, n resource.Key if !ok { // do not do anything if we have not processed the pod // let endpointgc delete the CiliumEndpoint as necessary - r.l.WithField("podKey", n.String()).Trace("pod not found in cache, skipping deletion") + r.l.Debug("pod not found in cache, skipping deletion", "podKey", n.String()) return nil } - r.l.WithField("podKey", n.String()).Trace("handling pod delete") + r.l.Debug("handling pod delete", "podKey", n.String()) // delete CEP even if we haven't processed the Pod (and incremented identity reference count) err := r.clientset.CiliumV2().CiliumEndpoints(n.Namespace).Delete(ctx, n.Name, metav1.DeleteOptions{}) if err != nil && !k8serrors.IsNotFound(err) { - r.l.WithError(err).WithField("podKey", n.String()).Error("failed to delete CiliumEndpoint") + r.l.Error("failed to delete CiliumEndpoint", "error", err, "podKey", n.String()) return errors.Wrap(err, "failed to delete endpoint") } - r.l.WithField("podKey", n.String()).Debug("deleted CiliumEndpoint") + r.l.Debug("deleted CiliumEndpoint", "podKey", n.String()) // Identity reference count must be modified after CiliumEndpoint is successfully deleted. // Otherwise, we could decrement reference multiple times if CiliumEndpoint deletion fails and we retry this method. @@ -359,15 +356,15 @@ func (r *endpointReconciler) handlePodDelete(ctx context.Context, n resource.Key } func (r *endpointReconciler) handlePodUpsert(ctx context.Context, newPEP *PodEndpoint) error { //nolint:gocyclo // This function is too complex and should be refactored - r.l.WithField("podKey", newPEP.key.String()).Trace("handling pod upsert") + r.l.Debug("handling pod upsert", "podKey", newPEP.key.String()) oldPEP, inCache := r.store.GetPod(newPEP.key) inStore := false if inCache { - r.l.WithFields(logrus.Fields{ - "podKey": newPEP.key.String(), - "pep": oldPEP, - }).Trace("PodEndpoint found in cache") + r.l.Debug("PodEndpoint found in cache", + "podKey", newPEP.key.String(), + "pep", oldPEP, + ) } else { // this call will block until the store is synced with API Server store, err := r.ciliumEndpoints.Store(ctx) @@ -384,21 +381,21 @@ func (r *endpointReconciler) handlePodUpsert(ctx context.Context, newPEP *PodEnd inStore = ok if inStore { - r.l.WithFields(logrus.Fields{ - "podKey": newPEP.key.String(), - "ownerReferences": oldCEP.ObjectMeta.OwnerReferences, - "endpointID": oldCEP.Status.ID, - "identity": oldCEP.Status.Identity, - "networking": oldCEP.Status.Networking, - }).Trace("CiliumEndpoint found in store") + r.l.Debug("CiliumEndpoint found in store", + "podKey", newPEP.key.String(), + "ownerReferences", oldCEP.OwnerReferences, + "endpointID", oldCEP.Status.ID, + "identity", oldCEP.Status.Identity, + "networking", oldCEP.Status.Networking, + ) if oldCEP.Status.Networking == nil || len(oldCEP.Status.Networking.Addressing) == 0 || oldCEP.Status.Networking.Addressing[0].IPV4 == "" { // FIXME handle IPv6 and dual-stack inStore = false - r.l.WithFields(logrus.Fields{ - "podKey": newPEP.key.String(), - "cep": oldCEP, - }).Warn("CiliumEndpoint has no ipv4 address, ignoring") + r.l.Warn("CiliumEndpoint has no ipv4 address, ignoring", + "podKey", newPEP.key.String(), + "cep", oldCEP, + ) } else { oldPEP = &PodEndpoint{ key: newPEP.key, @@ -424,23 +421,23 @@ func (r *endpointReconciler) handlePodUpsert(ctx context.Context, newPEP *PodEnd sameNetworking := newPEP.ipv4 == oldPEP.ipv4 && newPEP.nodeIP == oldPEP.nodeIP equalLabels := newPEP.lbls.Equals(oldPEP.lbls) - r.l.WithFields(logrus.Fields{ - "podKey": newPEP.key.String(), - "inCache": inCache, - "sameNetworking": sameNetworking, - "equalLabels": equalLabels, - "oldLbls": oldPEP.lbls, - "newLbls": newPEP.lbls, - }).Trace("patching CiliumEndpoint") + r.l.Debug("patching CiliumEndpoint", + "podKey", newPEP.key.String(), + "inCache", inCache, + "sameNetworking", sameNetworking, + "equalLabels", equalLabels, + "oldLbls", oldPEP.lbls, + "newLbls", newPEP.lbls, + ) // allocate new identity if labels have changed or if we haven't assigned an identity ID to this pod yet // TODO when implementing follower check if inCache or processedAsLeader shouldAllocateNewIdentity := !inCache || !equalLabels || newPEP.identityID == 0 if shouldAllocateNewIdentity { - r.l.WithFields(logrus.Fields{ - "podKey": newPEP.key.String(), - "pep": oldPEP, - }).Trace("creating new identity for pod") + r.l.Debug("creating new identity for pod", + "podKey", newPEP.key.String(), + "pep", oldPEP, + ) identityID, err := r.identityManager.GetIdentityAndIncrementReference(ctx, newPEP.lbls) if err != nil { @@ -452,13 +449,13 @@ func (r *endpointReconciler) handlePodUpsert(ctx context.Context, newPEP *PodEnd if sameNetworking && equalLabels { // nothing to do. pod already has a CEP with the same networking and labels - r.l.WithField("podKey", newPEP.key.String()).Trace("pod already processed") + r.l.Debug("pod already processed", "podKey", newPEP.key.String()) r.store.AddPod(newPEP) return nil } if !sameNetworking { - r.l.WithField("podKey", newPEP.key.String()).Trace("pod networking has changed") + r.l.Debug("pod networking has changed", "podKey", newPEP.key.String()) // change endpoint id since networking has changed // FIXME use endpoint allocator to get new endpoint ID newPEP.endpointID++ @@ -493,15 +490,15 @@ func (r *endpointReconciler) handlePodUpsert(ctx context.Context, newPEP *PodEnd createStatusPatch, err := json.Marshal(replaceCEPStatus) if err != nil { - r.l.WithFields(logrus.Fields{ - "podKey": newPEP.key.String(), - "pep": newPEP, - "uid": newPEP.uid, - }).Debug("marshalling status failed") + r.l.Debug("marshalling status failed", + "podKey", newPEP.key.String(), + "pep", newPEP, + "uid", newPEP.uid, + ) if shouldAllocateNewIdentity { // decrement reference for new identity - r.l.WithField("podKey", newPEP.key.String()).Trace("marshal failed, decrementing reference for new identity") + r.l.Debug("marshal failed, decrementing reference for new identity", "podKey", newPEP.key.String()) r.identityManager.DecrementReference(ctx, newPEP.lbls) } @@ -526,7 +523,7 @@ func (r *endpointReconciler) handlePodUpsert(ctx context.Context, newPEP *PodEnd // Decrement reference for new identity. // May end up incrementing reference count for this same identity again if we try to create the CEP below. // No downside to decrementing reference here and then incrementing again below (will not affect API Server). - r.l.WithField("podKey", newPEP.key.String()).Trace("patch unsuccessful, decrementing reference for new identity") + r.l.Debug("patch unsuccessful, decrementing reference for new identity", "podKey", newPEP.key.String()) r.identityManager.DecrementReference(ctx, newPEP.lbls) } @@ -535,16 +532,18 @@ func (r *endpointReconciler) handlePodUpsert(ctx context.Context, newPEP *PodEnd // No downside to this. if !k8serrors.IsNotFound(err) { - r.l.WithError(err).WithFields(logrus.Fields{ - "podKey": newPEP.key.String(), - "pep": newPEP, - "uid": newPEP.uid, - }).Error("failed to patch CiliumEndpoint") + r.l.Error("failed to patch CiliumEndpoint", + "error", err, + "podKey", newPEP.key.String(), + "pep", newPEP, + "uid", newPEP.uid, + ) return errors.Wrap(err, "failed to patch endpoint") } - r.l.WithField("podKey", newPEP.key.String()).Debug("patch unsuccessful because CiliumEndpoint is not in API Server. now creating CiliumEndpoint") + r.l.Debug("patch unsuccessful because CiliumEndpoint is not in API Server. now creating CiliumEndpoint", + "podKey", newPEP.key.String()) // Endpoint was not found, create it below. // Make sure the pod does not exist in the cache so that we don't try to patch it again (in case of a retry after a failure below). @@ -585,13 +584,13 @@ func (r *endpointReconciler) handlePodUpsert(ctx context.Context, newPEP *PodEnd _, err = r.clientset.CiliumV2().CiliumEndpoints(newPEP.key.Namespace).Create(ctx, newCEP, metav1.CreateOptions{}) if err != nil { - r.l.WithError(err).WithField("podKey", newPEP.key.String()).Error("failed to create CiliumEndpoint") + r.l.Error("failed to create CiliumEndpoint", "error", err, "podKey", newPEP.key.String()) r.identityManager.DecrementReference(ctx, newPEP.lbls) // FIXME release newly allocated endpoint ID return errors.Wrap(err, "failed to create endpoint") } - r.l.WithField("podKey", newPEP.key.String()).Debug("created CiliumEndpoint") + r.l.Debug("created CiliumEndpoint", "podKey", newPEP.key.String()) r.store.AddPod(newPEP) return nil } @@ -609,7 +608,7 @@ func (r *endpointReconciler) reconcileNamespace(ctx context.Context, namespace * // check if namespace is in cache oldNs, ok := r.store.GetNamespace(namespace.GetName()) if !ok { - r.l.Debug("Adding new namespace to cache", zap.String("namespace ", namespace.GetName())) + r.l.Debug("Adding new namespace to cache", "namespace", namespace.GetName()) // if this is the first time we see this namespace, add it to cache // there might not be any pods in this namespace yet r.store.AddNamespace(namespace) @@ -617,16 +616,16 @@ func (r *endpointReconciler) reconcileNamespace(ctx context.Context, namespace * } if oldNs.GetResourceVersion() == namespace.GetResourceVersion() { - r.l.Debug("Namespace already processed", zap.String("namespace ", namespace.GetName())) + r.l.Debug("Namespace already processed", "namespace", namespace.GetName()) return nil } if reflect.DeepEqual(oldNs.Labels, namespace.Labels) { - r.l.Debug("Namespace labels are the same", zap.String("namespace ", namespace.GetName())) + r.l.Debug("Namespace labels are the same", "namespace", namespace.GetName()) return nil } - r.l.Debug("Updating namespace in cache", zap.String("namespace ", namespace.GetName())) + r.l.Debug("Updating namespace in cache", "namespace", namespace.GetName()) r.store.AddNamespace(namespace) // now get all pods and update them as well @@ -640,11 +639,11 @@ func (r *endpointReconciler) reconcileNamespace(ctx context.Context, namespace * func (r *endpointReconciler) handleNamespaceDelete(_ context.Context, namespaceName string) error { _, ok := r.store.GetNamespace(namespaceName) if !ok { - r.l.Debug("Adding new namespace to cache", zap.String("namespace ", namespaceName)) + r.l.Debug("Adding new namespace to cache", "namespace", namespaceName) return nil } - r.l.Debug("Deleting namespace from cache", zap.String("namespace ", namespaceName)) + r.l.Debug("Deleting namespace from cache", "namespace", namespaceName) // Ignore deleting the pods for this NS, pod controller will eventually clean it up. // Once deleting all the pods in the namespace, delete the namespace r.store.DeleteNamespace(namespaceName) @@ -658,20 +657,22 @@ func (r *endpointReconciler) ciliumEndpointsLabels(ctx context.Context, pod *sli if !ok { ns, err = r.ciliumSlimClientSet.CoreV1().Namespaces().Get(ctx, pod.Namespace, metav1.GetOptions{}) if err != nil { - r.l.WithError(err).WithFields(logrus.Fields{ - "podKey": pod.Name, - "ns": pod.Namespace, - }).Error("failed to get namespace") + r.l.Error("failed to get namespace", + "error", err, + "podKey", pod.Name, + "ns", pod.Namespace, + ) return nil, errors.Wrap(err, "failed to get namespace") } r.store.AddNamespace(ns) } _, ciliumLabels := k8s.GetPodMetadata(slog.Default(), ns, pod) if err != nil { - r.l.WithError(err).WithFields(logrus.Fields{ - "podKey": pod.Name, - "ns": pod.Namespace, - }).Error("failed to get pod metadata") + r.l.Error("failed to get pod metadata", + "error", err, + "podKey", pod.Name, + "ns", pod.Namespace, + ) return nil, errors.Wrap(err, "failed to get pod metadata") } lbls := make(labels.Labels, len(ciliumLabels)) diff --git a/pkg/controllers/operator/cilium-crds/endpoint/endpoint_controller_linux_test.go b/pkg/controllers/operator/cilium-crds/endpoint/endpoint_controller_linux_test.go index 1ac913d1ea..74bad2fc6d 100644 --- a/pkg/controllers/operator/cilium-crds/endpoint/endpoint_controller_linux_test.go +++ b/pkg/controllers/operator/cilium-crds/endpoint/endpoint_controller_linux_test.go @@ -9,7 +9,7 @@ import ( "time" ciliumv2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" - ciliumclient "github.com/cilium/cilium/pkg/k8s/client" + ciliumclient "github.com/cilium/cilium/pkg/k8s/client/testutils" "github.com/cilium/cilium/pkg/k8s/resource" v1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/api/core/v1" slim_metav1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1" @@ -18,7 +18,6 @@ import ( "github.com/cilium/cilium/pkg/option" "github.com/microsoft/retina/pkg/controllers/operator/cilium-crds/cache" ciliumutil "github.com/microsoft/retina/pkg/utils/testutil/cilium" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -667,8 +666,7 @@ func TestSortedLabels(t *testing.T) { func newTestEndpointReconciler(t *testing.T) (*endpointReconciler, *ciliumutil.MockResource[*ciliumv2.CiliumEndpoint]) { t.Helper() - l := logrus.New() - l.SetLevel(logrus.DebugLevel) + l := slog.Default() ciliumEndpoints := ciliumutil.NewMockResource[*ciliumv2.CiliumEndpoint](l) fakeClientSet, _ := ciliumclient.NewFakeClientset(slog.Default()) @@ -686,7 +684,8 @@ func newTestEndpointReconciler(t *testing.T) (*endpointReconciler, *ciliumutil.M // make sure to use CRD mode (this is referenced in InitIdentityAllocator) option.Config.IdentityAllocationMode = option.IdentityAllocationModeCRD - im, err := NewIdentityManager(l, m) + // Use the Cilium fake clientset for identity manager since it has proper watch support + im, err := NewIdentityManager(context.Background(), l, fakeClientSet.CiliumFakeClientset) require.NoError(t, err) r.identityManager = im diff --git a/pkg/controllers/operator/cilium-crds/endpoint/identitymanager_linux.go b/pkg/controllers/operator/cilium-crds/endpoint/identitymanager_linux.go index 2a2b5461ab..831034be30 100644 --- a/pkg/controllers/operator/cilium-crds/endpoint/identitymanager_linux.go +++ b/pkg/controllers/operator/cilium-crds/endpoint/identitymanager_linux.go @@ -2,23 +2,26 @@ package endpointcontroller import ( "context" + "log/slog" + "time" "github.com/pkg/errors" - "github.com/sirupsen/logrus" "github.com/cilium/cilium/pkg/identity" "github.com/cilium/cilium/pkg/identity/cache" "github.com/cilium/cilium/pkg/k8s/client/clientset/versioned" "github.com/cilium/cilium/pkg/labels" - "github.com/cilium/cilium/pkg/option" ) +// Default timeout for identity allocation operations +const identityAllocatorTimeout = 2 * time.Minute + // IdentityManager is analogous to Cilium Daemon's identity allocation. // Cilium has an IPCacche holding IP to Identity mapping. // In IPCache.InjectLabels(), IPCacche is told of IPs which have been updated. // Within this function, identities are allocated/released via CachingIdentityAllocator. type IdentityManager struct { - l logrus.FieldLogger + l *slog.Logger // alloc is the CachingIdentityAllocator which helps in: // - allocating/releasing identities (maintaining reference counts and creating CRDs) // - syncing identity "keys", preventing them from being garbage collected @@ -34,8 +37,10 @@ type IdentityManager struct { type owner struct{} // UpdateIdentities is a callback when identities are updated -func (o *owner) UpdateIdentities(_, _ identity.IdentityMap) { - // no-op +func (o *owner) UpdateIdentities(_, _ identity.IdentityMap) <-chan struct{} { + ch := make(chan struct{}) + close(ch) + return ch } // GetNodeSuffix() is only used for KVStoreBackend (we use CRDBackend) @@ -43,15 +48,31 @@ func (o *owner) GetNodeSuffix() string { return "" } -func NewIdentityManager(l logrus.FieldLogger, client versioned.Interface) (*IdentityManager, error) { +func NewIdentityManager(ctx context.Context, l *slog.Logger, client versioned.Interface) (*IdentityManager, error) { + // Configure the allocator with a reasonable timeout for identity operations + allocConfig := cache.AllocatorConfig{ + Timeout: identityAllocatorTimeout, + SyncInterval: 1 * time.Hour, + } + im := &IdentityManager{ - l: l.WithField("component", "identitymanager"), - alloc: cache.NewCachingIdentityAllocator(&owner{}, cache.AllocatorConfig{}), + l: l.With("component", "identitymanager"), + alloc: cache.NewCachingIdentityAllocator(l, &owner{}, allocConfig), labelIdentities: make(map[string]identity.NumericIdentity), } im.l.Info("initializing identity allocator") - _ = im.alloc.InitIdentityAllocator(client) + initCh := im.alloc.InitIdentityAllocator(client, nil) + + // Wait for the identity allocator to be initialized (backend is created) + // Note: This doesn't wait for the full cache sync - that happens on first allocation + select { + case <-initCh: + im.l.Info("identity allocator initialized successfully") + case <-ctx.Done(): + return nil, errors.Wrap(ctx.Err(), "context cancelled while waiting for identity allocator initialization") + } + return im, nil } @@ -62,13 +83,13 @@ func (im *IdentityManager) DecrementReference(ctx context.Context, lbls labels.L sortedLabels := lbls.String() id, ok := im.labelIdentities[sortedLabels] if !ok { - im.l.WithField("labels", sortedLabels).Warn("expected identity for labels") + im.l.Warn("expected identity for labels", "labels", sortedLabels) return } idObj := im.alloc.LookupIdentityByID(ctx, id) if idObj == nil { - im.l.WithField("identity", id).Warn("expected identity for id") + im.l.Warn("expected identity for id", "identity", id) return } @@ -83,11 +104,10 @@ func (im *IdentityManager) DecrementReference(ctx context.Context, lbls labels.L // possible errors are // 1. ctx cancelled (in which case, hive is shutting down) // 2. identity not found in localKeys cache (nothing to worry about, and GC on CiliumIdentities will work as expected) - im.l.WithError(err).WithFields(logrus.Fields{ - "identity": idObj, - "identityLabels": idObj.Labels, - }).Warning( - "error while releasing previously allocated identity", + im.l.Warn("error while releasing previously allocated identity", + "error", err, + "identity", idObj, + "identityLabels", idObj.Labels, ) } @@ -95,11 +115,9 @@ func (im *IdentityManager) DecrementReference(ctx context.Context, lbls labels.L return } - im.l.WithFields(logrus.Fields{ - "identity": idObj, - "identityLabels": idObj.Labels, - }).Info( - "released identity due to no more references", + im.l.Info("released identity due to no more references", + "identity", idObj, + "identityLabels", idObj.Labels, ) delete(im.labelIdentities, sortedLabels) @@ -112,8 +130,8 @@ func (im *IdentityManager) GetIdentityAndIncrementReference(ctx context.Context, // notifyOwner=false because no need to notify owner (via UpdateIdentities callback). // oldNID=identity.InvalidIdentity would only be used for local identities (e.g. node-local CIDR identity), which we don't use. // Since this operation will create the CiliumIdentity if needed, - // pass in a context that completes either once ctx is done or kvstore timeout is reached. - allocateCtx, cancel := context.WithTimeout(ctx, option.Config.KVstoreConnectivityTimeout) + // pass in a context that completes either once ctx is done or identity allocator timeout is reached. + allocateCtx, cancel := context.WithTimeout(ctx, identityAllocatorTimeout) defer cancel() idObj, _, err := im.alloc.AllocateIdentity(allocateCtx, lbls, false, identity.InvalidIdentity) if err != nil { diff --git a/pkg/controllers/operator/cilium-crds/endpoint/identitymanager_linux_test.go b/pkg/controllers/operator/cilium-crds/endpoint/identitymanager_linux_test.go index 719f6a29c5..65e2d658b4 100644 --- a/pkg/controllers/operator/cilium-crds/endpoint/identitymanager_linux_test.go +++ b/pkg/controllers/operator/cilium-crds/endpoint/identitymanager_linux_test.go @@ -2,12 +2,12 @@ package endpointcontroller import ( "context" + "log/slog" "strconv" "testing" - ciliumutil "github.com/microsoft/retina/pkg/utils/testutil/cilium" + ciliumclient "github.com/cilium/cilium/pkg/k8s/client/testutils" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -17,12 +17,14 @@ import ( ) func TestGetIdentities(t *testing.T) { - l := logrus.New() - m := ciliumutil.NewMockVersionedClient(l, nil) + ctx := context.Background() + l := slog.Default() + // Use Cilium's fake clientset which has proper watch support for the identity allocator + fakeClientSet, _ := ciliumclient.NewFakeClientset(l) // make sure to use CRD mode (this is referenced in InitIdentityAllocator) option.Config.IdentityAllocationMode = option.IdentityAllocationModeCRD - im, err := NewIdentityManager(l, m) + im, err := NewIdentityManager(ctx, l, fakeClientSet.CiliumFakeClientset) require.NoError(t, err) lbls := labels.Labels{ @@ -45,14 +47,16 @@ func TestGetIdentities(t *testing.T) { require.Greater(t, int(id), 0) // identity should be in API Server - idObj, err := m.CiliumV2().CiliumIdentities().Get(context.TODO(), strconv.FormatInt(id, 10), metav1.GetOptions{}) + idObj, err := fakeClientSet.CiliumFakeClientset.CiliumV2().CiliumIdentities().Get( + context.TODO(), strconv.FormatInt(id, 10), metav1.GetOptions{}) require.NoError(t, err) require.Equal(t, strconv.FormatInt(id, 10), idObj.Name) + // Cilium's CRD identity backend stores labels in SecurityLabels with source:key format idLabels := map[string]string{ - "k1": "v1", - "io.kubernetes.pod.namespace": "x", + "k8s:k1": "v1", + "k8s:io.kubernetes.pod.namespace": "x", } - require.Equal(t, idLabels, idObj.Labels) + require.Equal(t, idLabels, idObj.SecurityLabels) // same labels should return the same identity id2, err := im.GetIdentityAndIncrementReference(context.TODO(), lbls) @@ -88,23 +92,26 @@ func TestGetIdentities(t *testing.T) { require.Greater(t, int(id), 0) // identity should be in API Server - idObj, err = m.CiliumV2().CiliumIdentities().Get(context.TODO(), strconv.FormatInt(id3, 10), metav1.GetOptions{}) + idObj, err = fakeClientSet.CiliumFakeClientset.CiliumV2().CiliumIdentities().Get( + context.TODO(), strconv.FormatInt(id3, 10), metav1.GetOptions{}) require.NoError(t, err) require.Equal(t, strconv.FormatInt(id3, 10), idObj.Name) idLabels = map[string]string{ - "k1": "v1", - "k2": "v2", - "io.kubernetes.pod.namespace": "x", + "k8s:k1": "v1", + "k8s:k2": "v2", + "k8s:io.kubernetes.pod.namespace": "x", } - require.Equal(t, idLabels, idObj.Labels) + require.Equal(t, idLabels, idObj.SecurityLabels) } func TestDecrementReference(t *testing.T) { - l := logrus.New() - m := ciliumutil.NewMockVersionedClient(l, nil) + ctx := context.Background() + l := slog.Default() + // Use Cilium's fake clientset which has proper watch support for the identity allocator + fakeClientSet, _ := ciliumclient.NewFakeClientset(l) // make sure to use CRD mode (this is referenced in InitIdentityAllocator) option.Config.IdentityAllocationMode = option.IdentityAllocationModeCRD - im, err := NewIdentityManager(l, m) + im, err := NewIdentityManager(ctx, l, fakeClientSet.CiliumFakeClientset) require.NoError(t, err) lbls := labels.Labels{ @@ -134,12 +141,13 @@ func TestDecrementReference(t *testing.T) { require.Empty(t, im.labelIdentities) // IdentityManager's allocator should not delete the identity (identitygc cell does garbage collection) - idObj, err := m.CiliumV2().CiliumIdentities().Get(context.TODO(), strconv.FormatInt(id, 10), metav1.GetOptions{}) + idObj, err := fakeClientSet.CiliumFakeClientset.CiliumV2().CiliumIdentities().Get( + context.TODO(), strconv.FormatInt(id, 10), metav1.GetOptions{}) require.NoError(t, err) require.Equal(t, strconv.FormatInt(id, 10), idObj.Name) idLabels := map[string]string{ - "k1": "v1", - "io.kubernetes.pod.namespace": "x", + "k8s:k1": "v1", + "k8s:io.kubernetes.pod.namespace": "x", } - require.Equal(t, idLabels, idObj.Labels) + require.Equal(t, idLabels, idObj.SecurityLabels) } diff --git a/pkg/controllers/operator/retinaendpoint/retinaendpoint_controller_test.go b/pkg/controllers/operator/retinaendpoint/retinaendpoint_controller_test.go index 60f02a7449..4a654039a9 100644 --- a/pkg/controllers/operator/retinaendpoint/retinaendpoint_controller_test.go +++ b/pkg/controllers/operator/retinaendpoint/retinaendpoint_controller_test.go @@ -224,12 +224,24 @@ func TestRetinaEndpointReconciler_ReconcilePod(t *testing.T) { return apierrors.IsNotFound(err) }, 5*time.Second, 1*time.Second, "RetinaEndpoint should not exist") } else { + // Wait for the RetinaEndpoint to be created/updated with the expected values require.Eventually(t, func() bool { err := client.Get(context.Background(), tt.fields.newlyCachedPod.Key, &got) - fmt.Println(err) - return !apierrors.IsNotFound(err) - }, 5*time.Second, 1*time.Second, "RetinaEndpoint should be created") - require.Equal(t, *tt.wantedRetinaEndpoint, got) + if apierrors.IsNotFound(err) { + return false + } + // Check that the spec matches what we expect (indicating create/update completed) + return got.Spec.PodIP == tt.wantedRetinaEndpoint.Spec.PodIP + }, 5*time.Second, 100*time.Millisecond, "RetinaEndpoint should be created/updated with expected PodIP") + + // Re-fetch to get the latest state + err := client.Get(context.Background(), tt.fields.newlyCachedPod.Key, &got) + require.NoError(t, err) + + // Compare the spec (ignore TypeMeta/ResourceVersion since fake client doesn't populate them) + require.Equal(t, tt.wantedRetinaEndpoint.Spec, got.Spec) + require.Equal(t, tt.wantedRetinaEndpoint.Name, got.Name) + require.Equal(t, tt.wantedRetinaEndpoint.Namespace, got.Namespace) } }) } diff --git a/pkg/enricher/enricher.go b/pkg/enricher/enricher.go index 98013cd2ca..408e4dfed2 100644 --- a/pkg/enricher/enricher.go +++ b/pkg/enricher/enricher.go @@ -14,6 +14,7 @@ import ( "github.com/microsoft/retina/pkg/common" "github.com/microsoft/retina/pkg/controllers/cache" "github.com/microsoft/retina/pkg/log" + "github.com/microsoft/retina/pkg/utils" "go.uber.org/zap" ) @@ -40,23 +41,28 @@ type Enricher struct { outputRing *container.Ring } -func New(ctx context.Context, cache cache.CacheInterface) *Enricher { +func New(ctx context.Context, c cache.CacheInterface) *Enricher { once.Do(func() { - ir := container.NewRing(container.Capacity1023) - e = &Enricher{ - ctx: ctx, - l: log.Logger().Named("enricher"), - cache: cache, - inputRing: ir, - Reader: container.NewRingReader(ir, ir.OldestWrite()), - outputRing: container.NewRing(container.Capacity1023), - } - initialized = true + e = newEnricher(ctx, c) }) return e } +func newEnricher(ctx context.Context, c cache.CacheInterface) *Enricher { + ir := container.NewRing(container.Capacity1023) + enricher := &Enricher{ + ctx: ctx, + l: log.Logger().Named("enricher"), + cache: c, + inputRing: ir, + Reader: container.NewRingReader(ir, ir.OldestWrite()), + outputRing: container.NewRing(container.Capacity1023), + } + initialized = true + return enricher +} + func Instance() *Enricher { return e } @@ -100,7 +106,21 @@ func (e *Enricher) Run() { // enrich takes the flow and enriches it with the information from the cache func (e *Enricher) enrich(ev *v1.Event) { + if ev == nil { + e.l.Debug("received nil event to enrich") + return + } + flow := ev.Event.(*flow.Flow) + if flow == nil { + e.l.Debug("received nil flow to enrich", zap.Any("event", ev)) + return + } + + if flow.GetIP() == nil { + e.l.Debug("flow IP is nil", zap.Any("flow", flow)) + return + } // IPversion is a enum in the flow proto // 0: IPVersion_IP_NOT_USED @@ -129,6 +149,17 @@ func (e *Enricher) enrich(ev *v1.Event) { flow.Destination = e.getEndpoint(dstObj) } + // Resolve zones lazily from the node cache using the pod's nodeIP. + srcZone := e.zoneFromObj(srcObj) + dstZone := e.zoneFromObj(dstObj) + + ext := utils.GetExtensionsStruct(flow) + if ext == nil { + ext = utils.NewExtensions() + } + utils.AddZones(ext, srcZone, dstZone) + utils.SetExtensions(flow, ext) + ev.Event = flow e.l.Debug("enriched flow", zap.Any("flow", flow)) e.export(ev) @@ -189,3 +220,22 @@ func (e *Enricher) Write(ev *v1.Event) { func (e *Enricher) ExportReader() *container.RingReader { return container.NewRingReader(e.outputRing, e.outputRing.OldestWrite()) } + +// zoneFromObj resolves the availability zone for a cached object by looking up +// the node from the cache at flow time, avoiding startup race conditions. +func (e *Enricher) zoneFromObj(obj interface{}) string { + if obj == nil { + return "unknown" + } + if o, ok := obj.(*common.RetinaEndpoint); ok { + if nodeIP := o.NodeIP(); nodeIP != "" { + node := e.cache.GetNodeByIP(nodeIP) + if node != nil { + if z := node.Zone(); z != "" { + return z + } + } + } + } + return "unknown" +} diff --git a/pkg/enricher/enricher_test.go b/pkg/enricher/enricher_test.go index cd05556d10..7f818923fd 100644 --- a/pkg/enricher/enricher_test.go +++ b/pkg/enricher/enricher_test.go @@ -12,16 +12,131 @@ import ( "github.com/cilium/cilium/api/v1/flow" v1 "github.com/cilium/cilium/pkg/hubble/api/v1" + retinav1alpha1 "github.com/microsoft/retina/crd/api/v1alpha1" "github.com/microsoft/retina/pkg/common" "github.com/microsoft/retina/pkg/controllers/cache" "github.com/microsoft/retina/pkg/log" "github.com/microsoft/retina/pkg/pubsub" + "github.com/microsoft/retina/pkg/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "google.golang.org/protobuf/types/known/timestamppb" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +var ( + // number of events + eventsGeneratedCount = 5 + + // construct the endpoints + sourcePod = common.NewRetinaEndpoint("pod1", "ns1", &common.IPAddresses{ + IPv4: net.IPv4(1, 1, 1, 1), + OtherIPv4s: []net.IP{net.IPv4(1, 1, 1, 2)}, + }) + destPod = common.NewRetinaEndpoint("pod2", "ns2", &common.IPAddresses{ + IPv4: net.IPv4(2, 2, 2, 2), + OtherIPv4s: []net.IP{net.IPv4(2, 2, 2, 3)}, + }) + // sourceIP = sourcePod.NetIPs().IPv4.String() + // destIP = destPod.NetIPs().IPv4.String() + sourceIP = sourcePod.NetIPs().OtherIPv4s[0].String() + destIP = destPod.NetIPs().OtherIPv4s[0].String() + + // construct events + normal = &v1.Event{ + Timestamp: timestamppb.Now(), + Event: &flow.Flow{ + Time: timestamppb.Now(), + IP: &flow.IP{ + IpVersion: 1, + Source: sourceIP, + Destination: destIP, + }, + }, + } + nilFlow = &v1.Event{ + Timestamp: timestamppb.Now(), + Event: nil, + } + nilIP = &v1.Event{ + Timestamp: timestamppb.Now(), + Event: &flow.Flow{ + Time: timestamppb.Now(), + IP: nil, + }, + } + events = []*v1.Event{ + normal, nilFlow, nilIP, + } +) + +func writeEventToEnricher(t *testing.T, e *Enricher, ev *v1.Event) { + t.Helper() + var wg sync.WaitGroup + wg.Add(1) + go func() { + for range eventsGeneratedCount { + l := log.Logger().Named("addev") + l.Info("Adding event", zap.Any("event", ev)) + time.Sleep(100 * time.Millisecond) + e.Write(ev) + } + wg.Done() + }() + e.Run() + wg.Wait() +} + +func TestEnricher(t *testing.T) { + opts := log.GetDefaultLogOpts() + opts.Level = "debug" + _, err := log.SetupZapLogger(opts) + require.NoError(t, err) + + c := cache.New(pubsub.New()) + + err = c.UpdateRetinaEndpoint(sourcePod) + require.NoError(t, err) + + err = c.UpdateRetinaEndpoint(destPod) + require.NoError(t, err) + + e := newEnricher(context.Background(), c) + + var wg sync.WaitGroup + defer wg.Wait() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + for _, ev := range events { + writeEventToEnricher(t, e, ev) + } + + l := log.Logger().Named("test-enricher") + + l.Info("Starting to read from enricher") + wg.Add(1) + go func() { + oreader := e.ExportReader() + for { + ev := oreader.NextFollow(ctx) + if ev == nil { + l.Info("No more events to read from enricher") + break + } + + l.Info("One Received event", zap.Any("event", ev)) + assertEqualEndpoint(t, sourcePod, ev.Event.(*flow.Flow).GetSource()) + assertEqualEndpoint(t, destPod, ev.Event.(*flow.Flow).GetDestination()) + } + wg.Done() + }() + + time.Sleep(3 * time.Second) +} + func TestEnricherSecondaryIPs(t *testing.T) { evCount := 20 // by design per ring, the last written item is not readable @@ -37,52 +152,25 @@ func TestEnricherSecondaryIPs(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) c := cache.New(pubsub.New()) - // construct the source endpoint - sourceEndpoints := common.NewRetinaEndpoint("pod1", "ns1", nil) - sourceEndpoints.SetLabels(map[string]string{ - "app": "app1", - }) - - sourceEndpoints.SetOwnerRefs([]*common.OwnerReference{ - { - Kind: "Pod", - Name: "pod1-deployment", - }, - }) - - sourceEndpoints.SetIPs(&common.IPAddresses{ - IPv4: net.IPv4(1, 1, 1, 1), - OtherIPv4s: []net.IP{net.IPv4(1, 1, 1, 2)}, - }) - err := c.UpdateRetinaEndpoint(sourceEndpoints) + err := c.UpdateRetinaEndpoint(sourcePod) require.NoError(t, err) - // construct the destination endpoint - destEndpoints := common.NewRetinaEndpoint("pod2", "ns2", nil) - destEndpoints.SetLabels(map[string]string{"app": "app2"}) - destEndpoints.SetOwnerRefs([]*common.OwnerReference{ - { - Kind: "Pod", - Name: "pod2-deployment", - }, - }) - destEndpoints.SetIPs(&common.IPAddresses{ - IPv4: net.IPv4(2, 2, 2, 2), - OtherIPv4s: []net.IP{net.IPv4(2, 2, 2, 3)}, - }) - err = c.UpdateRetinaEndpoint(destEndpoints) + err = c.UpdateRetinaEndpoint(destPod) require.NoError(t, err) - // get the enricher - e := New(ctx, c) + // create new enricher (not using singleton here) + e := newEnricher(ctx, c) var wg sync.WaitGroup wg.Add(1) go func() { for i := 0; i < evCount; i++ { // The Event Source IP is the secondary IP of the source endpoint + secondarySourceIP := sourcePod.NetIPs().OtherIPv4s[0].String() // The Event Destination IP is the secondary IP of the destination endpoint - addEvent(e, "1.1.1.2", "2.2.2.3") + secondaryDestIP := destPod.NetIPs().OtherIPv4s[0].String() + + addEvent(e, secondarySourceIP, secondaryDestIP) } wg.Done() }() @@ -101,12 +189,8 @@ func TestEnricherSecondaryIPs(t *testing.T) { l.Info("One Received event", zap.Any("event", ev)) // check whether the event is enriched correctly - sourceFlowEndPoint := ev.Event.(*flow.Flow).GetSource() - assert.Equal(t, sourceEndpoints.Namespace(), sourceFlowEndPoint.GetNamespace()) - assert.Equal(t, sourceEndpoints.Name(), sourceFlowEndPoint.GetPodName()) - destFlowEndPoint := ev.Event.(*flow.Flow).GetDestination() - assert.Equal(t, destEndpoints.Namespace(), destFlowEndPoint.GetNamespace()) - assert.Equal(t, destEndpoints.Name(), destFlowEndPoint.GetPodName()) + assertEqualEndpoint(t, sourcePod, ev.Event.(*flow.Flow).GetSource()) + assertEqualEndpoint(t, destPod, ev.Event.(*flow.Flow).GetDestination()) count++ } assert.Equal(t, expectedOutputCount, count, "one") @@ -123,12 +207,8 @@ func TestEnricherSecondaryIPs(t *testing.T) { break } // check whether the event is enriched correctly - sourceFlowEndPoint := ev.Event.(*flow.Flow).GetSource() - assert.Equal(t, sourceEndpoints.Namespace(), sourceFlowEndPoint.GetNamespace()) - assert.Equal(t, sourceEndpoints.Name(), sourceFlowEndPoint.GetPodName()) - destFlowEndPoint := ev.Event.(*flow.Flow).GetDestination() - assert.Equal(t, destEndpoints.Namespace(), destFlowEndPoint.GetNamespace()) - assert.Equal(t, destEndpoints.Name(), destFlowEndPoint.GetPodName()) + assertEqualEndpoint(t, sourcePod, ev.Event.(*flow.Flow).GetSource()) + assertEqualEndpoint(t, destPod, ev.Event.(*flow.Flow).GetDestination()) count++ } assert.Equal(t, expectedOutputCount, count, "two") @@ -157,3 +237,123 @@ func addEvent(e *Enricher, sourceIP, destIP string) { time.Sleep(100 * time.Millisecond) e.Write(ev) } + +func assertEqualEndpoint(t *testing.T, expected *common.RetinaEndpoint, actual *flow.Endpoint) { + assert.Equal(t, expected.Namespace(), actual.GetNamespace()) + assert.Equal(t, expected.Name(), actual.GetPodName()) +} + +func TestEnricherZoneResolution(t *testing.T) { + opts := log.GetDefaultLogOpts() + opts.Level = "debug" + _, err := log.SetupZapLogger(opts) + require.NoError(t, err) + + c := cache.New(pubsub.New()) + + // Add a node with a zone to the cache. + node := common.NewRetinaNode("node-1", net.IPv4(10, 0, 0, 100), "zone-1") + require.NoError(t, c.UpdateRetinaNode(node)) + + // Create endpoints with nodeIP pointing to the node above. + srcEp := common.RetinaEndpointCommonFromAPI(&retinav1alpha1.RetinaEndpoint{ + ObjectMeta: metav1.ObjectMeta{Name: "src-pod", Namespace: "ns1"}, + Spec: retinav1alpha1.RetinaEndpointSpec{ + PodIP: "1.1.1.1", + PodIPs: []string{"1.1.1.1"}, + NodeIP: "10.0.0.100", + }, + }) + dstEp := common.RetinaEndpointCommonFromAPI(&retinav1alpha1.RetinaEndpoint{ + ObjectMeta: metav1.ObjectMeta{Name: "dst-pod", Namespace: "ns2"}, + Spec: retinav1alpha1.RetinaEndpointSpec{ + PodIP: "2.2.2.2", + PodIPs: []string{"2.2.2.2"}, + NodeIP: "10.0.0.100", + }, + }) + + require.NoError(t, c.UpdateRetinaEndpoint(srcEp)) + require.NoError(t, c.UpdateRetinaEndpoint(dstEp)) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + e := newEnricher(ctx, c) + + // Get the export reader before running and writing, so we don't miss events. + oreader := e.ExportReader() + e.Run() + + // Write multiple events to ensure at least one makes it through the ring. + for range 3 { + e.Write(&v1.Event{ + Timestamp: timestamppb.Now(), + Event: &flow.Flow{ + IP: &flow.IP{ + IpVersion: 1, + Source: "1.1.1.1", + Destination: "2.2.2.2", + }, + }, + }) + time.Sleep(100 * time.Millisecond) + } + + enrichedEv := oreader.NextFollow(ctx) + require.NotNil(t, enrichedEv) + + enrichedFlow := enrichedEv.Event.(*flow.Flow) + assert.Equal(t, "zone-1", utils.SourceZone(enrichedFlow)) + assert.Equal(t, "zone-1", utils.DestinationZone(enrichedFlow)) +} + +func TestEnricherZoneResolution_NoNode(t *testing.T) { + opts := log.GetDefaultLogOpts() + opts.Level = "debug" + _, err := log.SetupZapLogger(opts) + require.NoError(t, err) + + c := cache.New(pubsub.New()) + + // Create endpoint with nodeIP but no node in cache. + ep := common.RetinaEndpointCommonFromAPI(&retinav1alpha1.RetinaEndpoint{ + ObjectMeta: metav1.ObjectMeta{Name: "pod1", Namespace: "ns1"}, + Spec: retinav1alpha1.RetinaEndpointSpec{ + PodIP: "3.3.3.3", + PodIPs: []string{"3.3.3.3"}, + NodeIP: "10.0.0.200", + }, + }) + require.NoError(t, c.UpdateRetinaEndpoint(ep)) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + e := newEnricher(ctx, c) + + oreader := e.ExportReader() + e.Run() + + for range 3 { + e.Write(&v1.Event{ + Timestamp: timestamppb.Now(), + Event: &flow.Flow{ + IP: &flow.IP{ + IpVersion: 1, + Source: "3.3.3.3", + Destination: "9.9.9.9", + }, + }, + }) + time.Sleep(100 * time.Millisecond) + } + + enrichedEv := oreader.NextFollow(ctx) + require.NotNil(t, enrichedEv) + + enrichedFlow := enrichedEv.Event.(*flow.Flow) + // Node not in cache, so zone should be "unknown". + assert.Equal(t, "unknown", utils.SourceZone(enrichedFlow)) + assert.Equal(t, "unknown", utils.DestinationZone(enrichedFlow)) +} diff --git a/pkg/hubble/common/decoder_linux.go b/pkg/hubble/common/decoder_linux.go index c247ad6a5d..d5eed5989d 100644 --- a/pkg/hubble/common/decoder_linux.go +++ b/pkg/hubble/common/decoder_linux.go @@ -7,26 +7,31 @@ import ( "github.com/cilium/cilium/api/v1/flow" "github.com/cilium/cilium/pkg/identity" ipc "github.com/cilium/cilium/pkg/ipcache" - "github.com/cilium/cilium/pkg/k8s" "github.com/cilium/cilium/pkg/labels" ) -//go:generate go run github.com/golang/mock/mockgen@v1.6.0 -source decoder.go -destination=mocks/mock_types.go -package=mocks - type EpDecoder interface { Decode(ip netip.Addr) *flow.Endpoint IsEndpointOnLocalHost(ip string) bool } +// LabelCache provides a cache for retrieving security labels associated with Cilium identities. +type LabelCache interface { + // GetLabelsFromSecurityIdentity returns the labels for a given security identity. + GetLabelsFromSecurityIdentity(identity.NumericIdentity) []string +} + type epDecoder struct { localHostIP string ipcache *ipc.IPCache + labelCache LabelCache } -func NewEpDecoder(c *ipc.IPCache) EpDecoder { +func NewEpDecoder(c *ipc.IPCache, lc LabelCache) EpDecoder { return &epDecoder{ localHostIP: os.Getenv("NODE_IP"), ipcache: c, + labelCache: lc, } } @@ -54,7 +59,7 @@ func (e *epDecoder) Decode(ip netip.Addr) *flow.Endpoint { case identity.ReservedIdentityWorld: ep.Labels = labels.LabelWorld.GetModel() default: - // TODO: We do not have an api on the ipcache to get the labels from the ip or identity. + ep.Labels = e.labelCache.GetLabelsFromSecurityIdentity(id.ID) } return ep @@ -69,20 +74,3 @@ func (e *epDecoder) IsEndpointOnLocalHost(string) bool { type SvcDecoder interface { Decode(ip netip.Addr) *flow.Service } - -type svcDecoder struct { - svccache k8s.ServiceCache -} - -func NewSvcDecoder(sc k8s.ServiceCache) SvcDecoder { - return &svcDecoder{ - svccache: sc, - } -} - -func (s *svcDecoder) Decode(netip.Addr) *flow.Service { - svc := &flow.Service{} - // TODO: serviceCache from cilium do not have a way to get the service name - // and namespace from the ip. We need to add this to the serviceCache. - return svc -} diff --git a/pkg/hubble/parser/layer34/parser_linux.go b/pkg/hubble/parser/layer34/parser_linux.go index 4b2798dfcb..9470c22650 100644 --- a/pkg/hubble/parser/layer34/parser_linux.go +++ b/pkg/hubble/parser/layer34/parser_linux.go @@ -2,29 +2,27 @@ package layer34 import ( "fmt" + "log/slog" "net/netip" "github.com/cilium/cilium/api/v1/flow" ipc "github.com/cilium/cilium/pkg/ipcache" - "github.com/cilium/cilium/pkg/k8s" "github.com/microsoft/retina/pkg/hubble/common" "github.com/microsoft/retina/pkg/utils" - "github.com/sirupsen/logrus" - "go.uber.org/zap" ) type Parser struct { - l *logrus.Entry + l *slog.Logger svd common.SvcDecoder epd common.EpDecoder } -func New(l *logrus.Entry, svc k8s.ServiceCache, c *ipc.IPCache) *Parser { +func New(l *slog.Logger, svc common.SvcDecoder, c *ipc.IPCache, labelCache common.LabelCache) *Parser { p := &Parser{ - l: l.WithField("subsys", "layer34"), - svd: common.NewSvcDecoder(svc), - epd: common.NewEpDecoder(c), + l: l.With("subsys", "layer34"), + svd: svc, + epd: common.NewEpDecoder(c, labelCache), } // Log the localHostIP for debugging purposes. return p @@ -36,17 +34,17 @@ func (p *Parser) Decode(f *flow.Flow) *flow.Flow { return nil } if f.GetIP() == nil { - p.l.Warn("Failed to get IP from flow", zap.Any("flow", f)) + p.l.Warn("Failed to get IP from flow", "flow", f) return f } sourceIP, err := netip.ParseAddr(f.GetIP().GetSource()) if err != nil { - p.l.Warn("Failed to parse source IP", zap.Error(err)) + p.l.Warn("Failed to parse source IP", "error", err) return f } destIP, err := netip.ParseAddr(f.GetIP().GetDestination()) if err != nil { - p.l.Warn("Failed to parse destination IP", zap.Error(err)) + p.l.Warn("Failed to parse destination IP", "error", err) return f } diff --git a/pkg/hubble/parser/parser_linux.go b/pkg/hubble/parser/parser_linux.go index 3d8cdd098f..6975a68b43 100644 --- a/pkg/hubble/parser/parser_linux.go +++ b/pkg/hubble/parser/parser_linux.go @@ -2,16 +2,18 @@ package parser import ( "errors" + "log/slog" "github.com/cilium/cilium/api/v1/flow" v1 "github.com/cilium/cilium/pkg/hubble/api/v1" observer "github.com/cilium/cilium/pkg/hubble/observer/types" + "github.com/cilium/cilium/pkg/hubble/parser" ipc "github.com/cilium/cilium/pkg/ipcache" - "github.com/cilium/cilium/pkg/k8s" + "github.com/cilium/hive/cell" + "github.com/microsoft/retina/pkg/hubble/common" "github.com/microsoft/retina/pkg/hubble/parser/layer34" "github.com/microsoft/retina/pkg/hubble/parser/seven" - "github.com/sirupsen/logrus" - "go.uber.org/zap" + "google.golang.org/protobuf/types/known/timestamppb" ) @@ -22,23 +24,30 @@ var ( errUnknownPayload = errors.New("unknown payload") ) +type Params struct { + cell.In + + Logger *slog.Logger + ServiceReconciler common.SvcDecoder + LabelCache common.LabelCache + + IPCache *ipc.IPCache +} + type Parser struct { - l logrus.FieldLogger - ipcache *ipc.IPCache - svc k8s.ServiceCache + l *slog.Logger l34 *layer34.Parser l7 *seven.Parser } -func New(l *logrus.Entry, svc k8s.ServiceCache, c *ipc.IPCache) *Parser { +func New(params Params) parser.Decoder { + logger := params.Logger.With("subsys", "payloadparser") return &Parser{ - l: l, - ipcache: c, - svc: svc, + l: logger, - l34: layer34.New(l, svc, c), - l7: seven.New(l, svc, c), + l34: layer34.New(logger, params.ServiceReconciler, params.IPCache, params.LabelCache), + l7: seven.New(logger, params.ServiceReconciler, params.IPCache, params.LabelCache), } } @@ -74,11 +83,11 @@ func (p *Parser) _decode(event *v1.Event) *flow.Flow { // node names. f, ok := event.Event.(*flow.Flow) if !ok { - p.l.Warn("Failed to cast event to flow", zap.Any("event", event.Event)) + p.l.Warn("Failed to cast event to flow", "event", event.Event) return nil } if f == nil { - p.l.Warn("Failed to get flow from event", zap.Any("event", event)) + p.l.Warn("Failed to get flow from event", "event", event) return nil } @@ -89,9 +98,9 @@ func (p *Parser) _decode(event *v1.Event) *flow.Flow { case flow.FlowType_L7: f = p.l7.Decode(f) default: - p.l.Warn("Unknown flow type", zap.Any("flow", f)) + p.l.Warn("Unknown flow type", "flow", f) } - p.l.Debug("Enriched flow", zap.Any("flow", f)) + p.l.Debug("Enriched flow", "flow", f) return f } diff --git a/pkg/hubble/parser/seven/parser_linux.go b/pkg/hubble/parser/seven/parser_linux.go index 03a2e4ade2..bd5d9d1ec0 100644 --- a/pkg/hubble/parser/seven/parser_linux.go +++ b/pkg/hubble/parser/seven/parser_linux.go @@ -2,29 +2,27 @@ package seven import ( "fmt" + "log/slog" "net/netip" "strings" "github.com/cilium/cilium/api/v1/flow" ipc "github.com/cilium/cilium/pkg/ipcache" - "github.com/cilium/cilium/pkg/k8s" "github.com/google/gopacket/layers" "github.com/microsoft/retina/pkg/hubble/common" - "github.com/sirupsen/logrus" - "go.uber.org/zap" ) type Parser struct { - l *logrus.Entry + l *slog.Logger svd common.SvcDecoder epd common.EpDecoder } -func New(l *logrus.Entry, svc k8s.ServiceCache, c *ipc.IPCache) *Parser { +func New(l *slog.Logger, svc common.SvcDecoder, c *ipc.IPCache, labelCache common.LabelCache) *Parser { return &Parser{ - l: l.WithField("subsys", "seven"), - svd: common.NewSvcDecoder(svc), - epd: common.NewEpDecoder(c), + l: l.With("subsys", "seven"), + svd: svc, + epd: common.NewEpDecoder(c, labelCache), } } @@ -63,17 +61,17 @@ func (p *Parser) decodeIP(f *flow.Flow) { // Decode the flow's source and destination IPs to their respective service. if f.GetIP() == nil { - p.l.Warn("Failed to get IP from flow", zap.Any("flow", f)) + p.l.Warn("Failed to get IP from flow", "flow", f) return } sourceIP, err := netip.ParseAddr(f.GetIP().GetSource()) if err != nil { - p.l.Warn("Failed to parse source IP", zap.Error(err)) + p.l.Warn("Failed to parse source IP", "error", err) return } destIP, err := netip.ParseAddr(f.GetIP().GetDestination()) if err != nil { - p.l.Warn("Failed to parse destination IP", zap.Error(err)) + p.l.Warn("Failed to parse destination IP", "error", err) return } diff --git a/pkg/hubble/resources/cell_linux.go b/pkg/hubble/resources/cell_linux.go new file mode 100644 index 0000000000..32e9435931 --- /dev/null +++ b/pkg/hubble/resources/cell_linux.go @@ -0,0 +1,16 @@ +package resources + +import ( + "github.com/cilium/cilium/pkg/k8s" + "github.com/cilium/hive/cell" +) + +var Cell = cell.Module( + "resources", + "Resources for Hubble", + cell.Provide(NewServiceHandler), + cell.Provide( + k8s.CiliumIdentityResource, + NewCiliumIdentityHandler, + ), +) diff --git a/pkg/hubble/resources/ciliumidentity_linux.go b/pkg/hubble/resources/ciliumidentity_linux.go new file mode 100644 index 0000000000..b75764452d --- /dev/null +++ b/pkg/hubble/resources/ciliumidentity_linux.go @@ -0,0 +1,169 @@ +package resources + +import ( + "context" + "sync" + + cid "github.com/cilium/cilium/pkg/identity" + cilium_api_v2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" + "github.com/cilium/cilium/pkg/k8s/resource" + "github.com/cilium/hive/cell" + "github.com/microsoft/retina/pkg/hubble/common" + "github.com/microsoft/retina/pkg/log" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +var _ common.LabelCache = (*CiliumIdentityHandler)(nil) + +const ( + CiliumIdentityHandlerName = "cilium-identity-handler" +) + +// CiliumIdentityHandler watches CiliumIdentity resources using resource.Resource +type CiliumIdentityHandler struct { + identities resource.Resource[*cilium_api_v2.CiliumIdentity] + + // Internal cache for identity management + mu sync.RWMutex + labelsByIdentityID map[cid.NumericIdentity][]string + ctxCancelFunc context.CancelFunc + + logger *log.ZapLogger +} + +type CiliumIdentityHandlerParams struct { + cell.In + + Lifecycle cell.Lifecycle + Identities resource.Resource[*cilium_api_v2.CiliumIdentity] +} + +type CiliumIdentityHandlerOut struct { + cell.Out + + common.LabelCache + *CiliumIdentityHandler +} + +// NewCiliumIdentityHandler creates a new CiliumIdentityHandler instance +func NewCiliumIdentityHandler(params CiliumIdentityHandlerParams) CiliumIdentityHandlerOut { + handler := &CiliumIdentityHandler{ + identities: params.Identities, + labelsByIdentityID: make(map[cid.NumericIdentity][]string), + logger: log.Logger().Named(CiliumIdentityHandlerName), + } + + params.Lifecycle.Append(handler) + + return CiliumIdentityHandlerOut{ + LabelCache: handler, + CiliumIdentityHandler: handler, + } +} + +func (h *CiliumIdentityHandler) Start(cell.HookContext) error { + ctx, cancel := context.WithCancel(context.Background()) + h.ctxCancelFunc = cancel + go h.run(ctx) + h.logger.Info("CiliumIdentity handler started") + return nil +} + +func (h *CiliumIdentityHandler) Stop(cell.HookContext) error { + if h.ctxCancelFunc != nil { + h.ctxCancelFunc() + } + h.logger.Info("CiliumIdentity handler stopped") + return nil +} + +func (h *CiliumIdentityHandler) run(ctx context.Context) { + h.logger.Info("Starting CiliumIdentity event handler") + + identityEvents := h.identities.Events(ctx) + + for { + select { + case ev, ok := <-identityEvents: + if !ok { + h.logger.Info("CiliumIdentity events channel closed") + return + } + + h.handleEvent(ev) + ev.Done(nil) + + case <-ctx.Done(): + h.logger.Info("CiliumIdentity event handler stopped") + return + } + } +} + +func (h *CiliumIdentityHandler) handleEvent(ev resource.Event[*cilium_api_v2.CiliumIdentity]) { + switch ev.Kind { + case resource.Sync: + // Ignore sync events + h.logger.Debug("CiliumIdentity sync event received", zap.String("key", ev.Key.String())) + case resource.Upsert: + if ev.Object != nil { + h.logger.Debug("CiliumIdentity upsert event", zap.String("key", ev.Key.String())) + if err := h.updateIdentityCache(ev.Object); err != nil { + h.logger.Error("Failed to update identity cache", zap.Error(err), zap.String("key", ev.Key.String())) + } + } + case resource.Delete: + h.logger.Debug("CiliumIdentity delete event", zap.String("key", ev.Key.String())) + h.removeIdentityFromCache(ev.Key.Name) + } +} + +func (h *CiliumIdentityHandler) GetLabelsFromSecurityIdentity(id cid.NumericIdentity) []string { + h.mu.RLock() + defer h.mu.RUnlock() + // Retrieve labels from the cache + labels, exists := h.labelsByIdentityID[id] + if !exists { + h.logger.Debug("Identity not found in cache", zap.Uint32("identity", id.Uint32())) + return nil + } + h.logger.Debug("Retrieved labels from cache", zap.Uint32("identity", id.Uint32()), zap.Strings("labels", labels)) + return labels +} + +func (h *CiliumIdentityHandler) updateIdentityCache(identity *cilium_api_v2.CiliumIdentity) error { + id, err := cid.ParseNumericIdentity(identity.Name) + if err != nil { + return errors.Wrapf(err, "invalid identity name %s", identity.Name) + } + // Parse the security labels + secLabels := make([]string, 0, len(identity.SecurityLabels)) + for k, v := range identity.SecurityLabels { + secLabels = append(secLabels, k+"="+v) + } + h.mu.Lock() + defer h.mu.Unlock() + // Update the cache with the new labels + h.labelsByIdentityID[id] = secLabels + + return nil +} + +func (h *CiliumIdentityHandler) removeIdentityFromCache(name string) { + h.mu.Lock() + defer h.mu.Unlock() + + id, err := cid.ParseNumericIdentity(name) + if err != nil { + h.logger.Error("Failed to parse identity name", zap.Error(err), zap.String("identity", name)) + return + } + + if _, exists := h.labelsByIdentityID[id]; exists { + delete(h.labelsByIdentityID, id) + h.logger.Debug("Removed identity from cache", zap.String("identity", name)) + } else { + h.logger.Debug("Identity not found in cache", zap.String("identity", name)) + } +} diff --git a/pkg/hubble/resources/ciliumidentity_linux_test.go b/pkg/hubble/resources/ciliumidentity_linux_test.go new file mode 100644 index 0000000000..1b9cd7170a --- /dev/null +++ b/pkg/hubble/resources/ciliumidentity_linux_test.go @@ -0,0 +1,549 @@ +package resources + +import ( + "context" + "testing" + "time" + + cid "github.com/cilium/cilium/pkg/identity" + cilium_api_v2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" + "github.com/cilium/cilium/pkg/k8s/resource" + "github.com/cilium/hive/cell" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// mockIdentityResource is a test implementation of resource.Resource[*cilium_api_v2.CiliumIdentity] +type mockIdentityResource struct { + eventChan chan resource.Event[*cilium_api_v2.CiliumIdentity] +} + +func newMockIdentityResource() *mockIdentityResource { + return &mockIdentityResource{ + eventChan: make(chan resource.Event[*cilium_api_v2.CiliumIdentity], 100), + } +} + +func (m *mockIdentityResource) Events(context.Context, ...resource.EventsOpt) <-chan resource.Event[*cilium_api_v2.CiliumIdentity] { + return m.eventChan +} + +func (m *mockIdentityResource) Observe(context.Context, func(resource.Event[*cilium_api_v2.CiliumIdentity]), func(error)) { +} + +func (m *mockIdentityResource) Store(context.Context) (resource.Store[*cilium_api_v2.CiliumIdentity], error) { + return nil, nil +} + +func (m *mockIdentityResource) sendEvent(kind resource.EventKind, identity *cilium_api_v2.CiliumIdentity) { + key := resource.Key{} + if identity != nil { + key.Name = identity.Name + key.Namespace = identity.Namespace + } + + done := make(chan error, 1) + m.eventChan <- resource.Event[*cilium_api_v2.CiliumIdentity]{ + Kind: kind, + Key: key, + Object: identity, + Done: func(err error) { done <- err }, + } + // Wait for event to be processed + <-done +} + +func (m *mockIdentityResource) close() { + close(m.eventChan) +} + +func TestNewCiliumIdentityHandler(t *testing.T) { + mockRes := newMockIdentityResource() + defer mockRes.close() + + lc := &cell.DefaultLifecycle{} + params := CiliumIdentityHandlerParams{ + Lifecycle: lc, + Identities: mockRes, + } + + handlerOut := NewCiliumIdentityHandler(params) + + assert.NotNil(t, handlerOut.CiliumIdentityHandler) + assert.NotNil(t, handlerOut.LabelCache) + assert.NotNil(t, handlerOut.labelsByIdentityID) + assert.NotNil(t, handlerOut.logger) + assert.Equal(t, CiliumIdentityHandlerName, handlerOut.logger.Name()) +} + +func TestCiliumIdentityHandler_EventHandling_IdentityCreated(t *testing.T) { + mockRes := newMockIdentityResource() + defer mockRes.close() + + lc := &cell.DefaultLifecycle{} + params := CiliumIdentityHandlerParams{ + Lifecycle: lc, + Identities: mockRes, + } + + handlerOut := NewCiliumIdentityHandler(params) + handler := handlerOut.CiliumIdentityHandler + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + err := handler.Start(ctx) + require.NoError(t, err) + time.Sleep(10 * time.Millisecond) + + identity := &cilium_api_v2.CiliumIdentity{ + ObjectMeta: metav1.ObjectMeta{ + Name: "123", + }, + SecurityLabels: map[string]string{ + "k8s:app": "nginx", + "k8s:version": "1.0", + "k8s:tier": "frontend", + }, + } + + mockRes.sendEvent(resource.Upsert, identity) + time.Sleep(10 * time.Millisecond) + + // Verify the identity was cached + expectedID := cid.NumericIdentity(123) + labels := handler.GetLabelsFromSecurityIdentity(expectedID) + require.NotNil(t, labels) + assert.Len(t, labels, 3) + assert.Contains(t, labels, "k8s:app=nginx") + assert.Contains(t, labels, "k8s:version=1.0") + assert.Contains(t, labels, "k8s:tier=frontend") +} + +func TestCiliumIdentityHandler_EventHandling_IdentityUpdated(t *testing.T) { + mockRes := newMockIdentityResource() + defer mockRes.close() + + lc := &cell.DefaultLifecycle{} + params := CiliumIdentityHandlerParams{ + Lifecycle: lc, + Identities: mockRes, + } + + handlerOut := NewCiliumIdentityHandler(params) + handler := handlerOut.CiliumIdentityHandler + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + err := handler.Start(ctx) + require.NoError(t, err) + time.Sleep(10 * time.Millisecond) + + // Initial identity + identity := &cilium_api_v2.CiliumIdentity{ + ObjectMeta: metav1.ObjectMeta{ + Name: "456", + }, + SecurityLabels: map[string]string{ + "k8s:app": "nginx", + }, + } + + mockRes.sendEvent(resource.Upsert, identity) + time.Sleep(10 * time.Millisecond) + + // Update the identity with new labels + updatedIdentity := &cilium_api_v2.CiliumIdentity{ + ObjectMeta: metav1.ObjectMeta{ + Name: "456", + }, + SecurityLabels: map[string]string{ + "k8s:app": "nginx", + "k8s:version": "2.0", + }, + } + + mockRes.sendEvent(resource.Upsert, updatedIdentity) + time.Sleep(10 * time.Millisecond) + + // Verify the updated labels in cache + expectedID := cid.NumericIdentity(456) + labels := handler.GetLabelsFromSecurityIdentity(expectedID) + require.NotNil(t, labels) + assert.Len(t, labels, 2) + assert.Contains(t, labels, "k8s:app=nginx") + assert.Contains(t, labels, "k8s:version=2.0") +} + +func TestCiliumIdentityHandler_EventHandling_IdentityDeleted(t *testing.T) { + mockRes := newMockIdentityResource() + defer mockRes.close() + + lc := &cell.DefaultLifecycle{} + params := CiliumIdentityHandlerParams{ + Lifecycle: lc, + Identities: mockRes, + } + + handlerOut := NewCiliumIdentityHandler(params) + handler := handlerOut.CiliumIdentityHandler + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + err := handler.Start(ctx) + require.NoError(t, err) + time.Sleep(10 * time.Millisecond) + + // Create identity first + identity := &cilium_api_v2.CiliumIdentity{ + ObjectMeta: metav1.ObjectMeta{ + Name: "789", + }, + SecurityLabels: map[string]string{ + "k8s:app": "test", + }, + } + + mockRes.sendEvent(resource.Upsert, identity) + time.Sleep(10 * time.Millisecond) + + // Delete the identity + mockRes.sendEvent(resource.Delete, identity) + time.Sleep(10 * time.Millisecond) + + // Verify the identity was removed from cache + expectedID := cid.NumericIdentity(789) + labels := handler.GetLabelsFromSecurityIdentity(expectedID) + assert.Nil(t, labels) +} + +func TestCiliumIdentityHandler_EventHandling_InvalidIdentityName(t *testing.T) { + mockRes := newMockIdentityResource() + defer mockRes.close() + + lc := &cell.DefaultLifecycle{} + params := CiliumIdentityHandlerParams{ + Lifecycle: lc, + Identities: mockRes, + } + + handlerOut := NewCiliumIdentityHandler(params) + handler := handlerOut.CiliumIdentityHandler + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + err := handler.Start(ctx) + require.NoError(t, err) + time.Sleep(10 * time.Millisecond) + + identity := &cilium_api_v2.CiliumIdentity{ + ObjectMeta: metav1.ObjectMeta{ + Name: "invalid-name", + }, + SecurityLabels: map[string]string{ + "k8s:app": "test", + }, + } + + // This should not panic, just log an error + mockRes.sendEvent(resource.Upsert, identity) + time.Sleep(10 * time.Millisecond) + + // Cache should be empty + handler.mu.RLock() + defer handler.mu.RUnlock() + assert.Empty(t, handler.labelsByIdentityID) +} + +func TestCiliumIdentityHandler_GetLabelsFromSecurityIdentity(t *testing.T) { + mockRes := newMockIdentityResource() + defer mockRes.close() + + lc := &cell.DefaultLifecycle{} + params := CiliumIdentityHandlerParams{ + Lifecycle: lc, + Identities: mockRes, + } + + handlerOut := NewCiliumIdentityHandler(params) + handler := handlerOut.CiliumIdentityHandler + + // Test case 1: Identity exists in cache + expectedID := cid.NumericIdentity(100) + expectedLabels := []string{"k8s:app=nginx", "k8s:version=1.0"} + handler.mu.Lock() + handler.labelsByIdentityID[expectedID] = expectedLabels + handler.mu.Unlock() + + labels := handler.GetLabelsFromSecurityIdentity(expectedID) + assert.Equal(t, expectedLabels, labels) + + // Test case 2: Identity does not exist in cache + nonExistentID := cid.NumericIdentity(404) + labels = handler.GetLabelsFromSecurityIdentity(nonExistentID) + assert.Nil(t, labels) +} + +func TestCiliumIdentityHandler_updateIdentityCache(t *testing.T) { + mockRes := newMockIdentityResource() + defer mockRes.close() + + lc := &cell.DefaultLifecycle{} + params := CiliumIdentityHandlerParams{ + Lifecycle: lc, + Identities: mockRes, + } + + handlerOut := NewCiliumIdentityHandler(params) + handler := handlerOut.CiliumIdentityHandler + + identity := &cilium_api_v2.CiliumIdentity{ + ObjectMeta: metav1.ObjectMeta{ + Name: "200", + }, + SecurityLabels: map[string]string{ + "k8s:app": "web", + "k8s:tier": "frontend", + "security": "restricted", + }, + } + + err := handler.updateIdentityCache(identity) + require.NoError(t, err) + + // Verify the cache was updated + expectedID := cid.NumericIdentity(200) + labels := handler.GetLabelsFromSecurityIdentity(expectedID) + require.NotNil(t, labels) + assert.Len(t, labels, 3) + assert.Contains(t, labels, "k8s:app=web") + assert.Contains(t, labels, "k8s:tier=frontend") + assert.Contains(t, labels, "security=restricted") +} + +func TestCiliumIdentityHandler_updateIdentityCache_InvalidName(t *testing.T) { + mockRes := newMockIdentityResource() + defer mockRes.close() + + lc := &cell.DefaultLifecycle{} + params := CiliumIdentityHandlerParams{ + Lifecycle: lc, + Identities: mockRes, + } + + handlerOut := NewCiliumIdentityHandler(params) + handler := handlerOut.CiliumIdentityHandler + + identity := &cilium_api_v2.CiliumIdentity{ + ObjectMeta: metav1.ObjectMeta{ + Name: "not-a-number", + }, + SecurityLabels: map[string]string{ + "k8s:app": "test", + }, + } + + err := handler.updateIdentityCache(identity) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid identity name") +} + +func TestCiliumIdentityHandler_removeIdentityFromCache(t *testing.T) { + mockRes := newMockIdentityResource() + defer mockRes.close() + + lc := &cell.DefaultLifecycle{} + params := CiliumIdentityHandlerParams{ + Lifecycle: lc, + Identities: mockRes, + } + + handlerOut := NewCiliumIdentityHandler(params) + handler := handlerOut.CiliumIdentityHandler + + // Pre-populate cache + expectedID := cid.NumericIdentity(300) + handler.mu.Lock() + handler.labelsByIdentityID[expectedID] = []string{"k8s:app=test"} + handler.mu.Unlock() + + // Test removal + handler.removeIdentityFromCache("300") + + // Verify removal + labels := handler.GetLabelsFromSecurityIdentity(expectedID) + assert.Nil(t, labels) +} + +func TestCiliumIdentityHandler_removeIdentityFromCache_InvalidName(_ *testing.T) { + mockRes := newMockIdentityResource() + defer mockRes.close() + + lc := &cell.DefaultLifecycle{} + params := CiliumIdentityHandlerParams{ + Lifecycle: lc, + Identities: mockRes, + } + + handlerOut := NewCiliumIdentityHandler(params) + handler := handlerOut.CiliumIdentityHandler + + // This should not panic even with invalid name + handler.removeIdentityFromCache("invalid-name") + + // No assertions needed, just ensure it doesn't panic +} + +func TestCiliumIdentityHandler_removeIdentityFromCache_NotInCache(_ *testing.T) { + mockRes := newMockIdentityResource() + defer mockRes.close() + + lc := &cell.DefaultLifecycle{} + params := CiliumIdentityHandlerParams{ + Lifecycle: lc, + Identities: mockRes, + } + + handlerOut := NewCiliumIdentityHandler(params) + handler := handlerOut.CiliumIdentityHandler + + // Try to remove identity that doesn't exist in cache + handler.removeIdentityFromCache("404") + + // No assertions needed, just ensure it doesn't panic +} + +func TestCiliumIdentityHandler_SyncEvent(t *testing.T) { + mockRes := newMockIdentityResource() + defer mockRes.close() + + lc := &cell.DefaultLifecycle{} + params := CiliumIdentityHandlerParams{ + Lifecycle: lc, + Identities: mockRes, + } + + handlerOut := NewCiliumIdentityHandler(params) + handler := handlerOut.CiliumIdentityHandler + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + err := handler.Start(ctx) + require.NoError(t, err) + time.Sleep(10 * time.Millisecond) + + identity := &cilium_api_v2.CiliumIdentity{ + ObjectMeta: metav1.ObjectMeta{ + Name: "123", + }, + SecurityLabels: map[string]string{ + "k8s:app": "test", + }, + } + + // Send sync event (should be ignored) + mockRes.sendEvent(resource.Sync, identity) + time.Sleep(10 * time.Millisecond) + + // Verify cache is empty + handler.mu.RLock() + defer handler.mu.RUnlock() + + assert.Empty(t, handler.labelsByIdentityID) +} + +func TestCiliumIdentityHandler_ConcurrentAccess(t *testing.T) { + mockRes := newMockIdentityResource() + defer mockRes.close() + + lc := &cell.DefaultLifecycle{} + params := CiliumIdentityHandlerParams{ + Lifecycle: lc, + Identities: mockRes, + } + + handlerOut := NewCiliumIdentityHandler(params) + handler := handlerOut.CiliumIdentityHandler + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + err := handler.Start(ctx) + require.NoError(t, err) + time.Sleep(10 * time.Millisecond) + + identity := &cilium_api_v2.CiliumIdentity{ + ObjectMeta: metav1.ObjectMeta{ + Name: "500", + }, + SecurityLabels: map[string]string{ + "k8s:app": "test", + "k8s:version": "1.0", + }, + } + + // Test concurrent read/write access + done := make(chan bool, 2) + + // Goroutine 1: Send event + go func() { + mockRes.sendEvent(resource.Upsert, identity) + done <- true + }() + + // Goroutine 2: Read from cache + go func() { + time.Sleep(5 * time.Millisecond) + identityID := cid.NumericIdentity(500) + handler.GetLabelsFromSecurityIdentity(identityID) + done <- true + }() + + // Wait for both goroutines to complete + <-done + <-done + + time.Sleep(10 * time.Millisecond) + + // Verify final state + identityID := cid.NumericIdentity(500) + labels := handler.GetLabelsFromSecurityIdentity(identityID) + require.NotNil(t, labels) + assert.Len(t, labels, 2) + assert.Contains(t, labels, "k8s:app=test") + assert.Contains(t, labels, "k8s:version=1.0") +} + +func TestCiliumIdentityHandler_StopClosesChannel(t *testing.T) { + mockRes := newMockIdentityResource() + + lc := &cell.DefaultLifecycle{} + params := CiliumIdentityHandlerParams{ + Lifecycle: lc, + Identities: mockRes, + } + + handlerOut := NewCiliumIdentityHandler(params) + handler := handlerOut.CiliumIdentityHandler + + ctx, cancel := context.WithCancel(context.Background()) + + err := handler.Start(ctx) + require.NoError(t, err) + time.Sleep(10 * time.Millisecond) + + // Close the mock resource + mockRes.close() + time.Sleep(10 * time.Millisecond) + + // Stop should complete without error + err = handler.Stop(ctx) + require.NoError(t, err) + + cancel() +} diff --git a/pkg/hubble/resources/service_linux.go b/pkg/hubble/resources/service_linux.go new file mode 100644 index 0000000000..fe26c26ce0 --- /dev/null +++ b/pkg/hubble/resources/service_linux.go @@ -0,0 +1,195 @@ +package resources + +import ( + "context" + "net/netip" + "sync" + + "github.com/cilium/cilium/api/v1/flow" + "github.com/cilium/cilium/pkg/k8s/resource" + slim_corev1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/api/core/v1" + "github.com/cilium/hive/cell" + "github.com/microsoft/retina/pkg/hubble/common" + "github.com/microsoft/retina/pkg/log" + "go.uber.org/zap" + "k8s.io/apimachinery/pkg/types" +) + +var _ common.SvcDecoder = (*ServiceHandler)(nil) + +const ( + ServiceHandlerName = "k8s-service-handler" +) + +// ServiceHandler watches Kubernetes services using resource.Resource +type ServiceHandler struct { + services resource.Resource[*slim_corev1.Service] + + // Internal cache for services and their IPs + mu sync.RWMutex + ipToService map[string]*flow.Service // Maps IP to *flow.Service + serviceToIPs map[types.NamespacedName][]string // Maps service key to its IPs + ctxCancelFunc context.CancelFunc + + logger *log.ZapLogger +} + +type ServiceHandlerParams struct { + cell.In + + Lifecycle cell.Lifecycle + Services resource.Resource[*slim_corev1.Service] +} + +type ServiceHandlerOut struct { + cell.Out + + common.SvcDecoder + *ServiceHandler +} + +// NewServiceHandler creates a new ServiceHandler instance +func NewServiceHandler(params ServiceHandlerParams) ServiceHandlerOut { + handler := &ServiceHandler{ + services: params.Services, + ipToService: make(map[string]*flow.Service), + serviceToIPs: make(map[types.NamespacedName][]string), + logger: log.Logger().Named(ServiceHandlerName), + } + + params.Lifecycle.Append(handler) + + return ServiceHandlerOut{ + SvcDecoder: handler, + ServiceHandler: handler, + } +} + +func (h *ServiceHandler) Start(cell.HookContext) error { + ctx, cancel := context.WithCancel(context.Background()) + h.ctxCancelFunc = cancel + go h.run(ctx) + h.logger.Info("Service handler started") + return nil +} + +func (h *ServiceHandler) Stop(cell.HookContext) error { + if h.ctxCancelFunc != nil { + h.ctxCancelFunc() + } + h.logger.Info("Service handler stopped") + return nil +} + +func (h *ServiceHandler) run(ctx context.Context) { + h.logger.Info("Starting service event handler") + + serviceEvents := h.services.Events(ctx) + + for { + select { + case ev, ok := <-serviceEvents: + if !ok { + h.logger.Info("Service events channel closed") + return + } + + h.handleEvent(ev) + ev.Done(nil) + + case <-ctx.Done(): + h.logger.Info("Service event handler stopped") + return + } + } +} + +func (h *ServiceHandler) handleEvent(ev resource.Event[*slim_corev1.Service]) { + switch ev.Kind { + case resource.Sync: + // Ignore sync events + h.logger.Debug("Service sync event received", zap.String("key", ev.Key.String())) + case resource.Upsert: + if ev.Object != nil { + h.logger.Debug("Service upsert event", zap.String("key", ev.Key.String())) + h.updateServiceCache(ev.Object) + } + case resource.Delete: + h.logger.Debug("Service delete event", zap.String("key", ev.Key.String())) + h.removeServiceFromCache(ev.Key.Namespace, ev.Key.Name) + } +} + +// Decode returns the service associated with the given IP address, or nil if not found. +func (h *ServiceHandler) Decode(ip netip.Addr) *flow.Service { + h.mu.RLock() + defer h.mu.RUnlock() + + return h.ipToService[ip.String()] +} + +// updateServiceCache updates the internal service cache +func (h *ServiceHandler) updateServiceCache(service *slim_corev1.Service) { + h.mu.Lock() + defer h.mu.Unlock() + + key := types.NamespacedName{ + Namespace: service.Namespace, + Name: service.Name, + } + + // Remove old IP mappings first + if oldIPs, exists := h.serviceToIPs[key]; exists { + for _, oldIP := range oldIPs { + delete(h.ipToService, oldIP) + } + } + + // Update IP mappings with new values + h.addServiceIPMappings(service) + + h.logger.Debug("Updated service cache", + zap.String("service", key.String()), + zap.Strings("clusterIPs", service.Spec.ClusterIPs)) +} + +// removeServiceFromCache removes a service from the cache +func (h *ServiceHandler) removeServiceFromCache(namespace, name string) { + h.mu.Lock() + defer h.mu.Unlock() + + serviceKey := types.NamespacedName{ + Namespace: namespace, + Name: name, + } + + if ips, exists := h.serviceToIPs[serviceKey]; exists { + for _, ip := range ips { + delete(h.ipToService, ip) + } + delete(h.serviceToIPs, serviceKey) + } +} + +// addServiceIPMappings adds IP to service mappings +func (h *ServiceHandler) addServiceIPMappings(service *slim_corev1.Service) { + key := types.NamespacedName{ + Namespace: service.Namespace, + Name: service.Name, + } + + flowService := &flow.Service{ + Name: service.Name, + Namespace: service.Namespace, + } + + h.serviceToIPs[key] = []string{} + + // Add ClusterIP mapping + for _, clusterIP := range service.Spec.ClusterIPs { + if clusterIP != "" && clusterIP != "None" { + h.ipToService[clusterIP] = flowService // Use the same cached object + h.serviceToIPs[key] = append(h.serviceToIPs[key], clusterIP) + } + } +} diff --git a/pkg/hubble/resources/service_linux_test.go b/pkg/hubble/resources/service_linux_test.go new file mode 100644 index 0000000000..602f0e95f0 --- /dev/null +++ b/pkg/hubble/resources/service_linux_test.go @@ -0,0 +1,516 @@ +package resources + +import ( + "context" + "net/netip" + "testing" + "time" + + "github.com/cilium/cilium/api/v1/flow" + "github.com/cilium/cilium/pkg/k8s/resource" + slim_corev1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/api/core/v1" + slim_metav1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1" + "github.com/cilium/hive/cell" + "github.com/microsoft/retina/pkg/log" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/types" +) + +func init() { + _, _ = log.SetupZapLogger(log.GetDefaultLogOpts()) +} + +// mockServiceResource is a test implementation of resource.Resource[*slim_corev1.Service] +type mockServiceResource struct { + eventChan chan resource.Event[*slim_corev1.Service] +} + +func newMockServiceResource() *mockServiceResource { + return &mockServiceResource{ + eventChan: make(chan resource.Event[*slim_corev1.Service], 100), + } +} + +func (m *mockServiceResource) Events(context.Context, ...resource.EventsOpt) <-chan resource.Event[*slim_corev1.Service] { + return m.eventChan +} + +func (m *mockServiceResource) Observe(context.Context, func(resource.Event[*slim_corev1.Service]), func(error)) { +} + +func (m *mockServiceResource) Store(context.Context) (resource.Store[*slim_corev1.Service], error) { + return nil, nil +} + +func (m *mockServiceResource) sendEvent(kind resource.EventKind, svc *slim_corev1.Service) { + key := resource.Key{} + if svc != nil { + key.Name = svc.Name + key.Namespace = svc.Namespace + } + + done := make(chan error, 1) + m.eventChan <- resource.Event[*slim_corev1.Service]{ + Kind: kind, + Key: key, + Object: svc, + Done: func(err error) { done <- err }, + } + // Wait for event to be processed + <-done +} + +func (m *mockServiceResource) close() { + close(m.eventChan) +} + +func TestNewServiceHandler(t *testing.T) { + mockRes := newMockServiceResource() + defer mockRes.close() + + lc := &cell.DefaultLifecycle{} + params := ServiceHandlerParams{ + Lifecycle: lc, + Services: mockRes, + } + + handlerOut := NewServiceHandler(params) + + assert.NotNil(t, handlerOut.ServiceHandler) + assert.NotNil(t, handlerOut.SvcDecoder) + assert.NotNil(t, handlerOut.ipToService) + assert.NotNil(t, handlerOut.serviceToIPs) + assert.NotNil(t, handlerOut.logger) + assert.Equal(t, ServiceHandlerName, handlerOut.logger.Name()) +} + +func TestServiceHandler_EventHandling_ServiceCreated(t *testing.T) { + mockRes := newMockServiceResource() + defer mockRes.close() + + lc := &cell.DefaultLifecycle{} + params := ServiceHandlerParams{ + Lifecycle: lc, + Services: mockRes, + } + + handlerOut := NewServiceHandler(params) + handler := handlerOut.ServiceHandler + + // Start the handler + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + err := handler.Start(ctx) + require.NoError(t, err) + + // Give the handler time to start + time.Sleep(10 * time.Millisecond) + + service := &slim_corev1.Service{ + ObjectMeta: slim_metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + Spec: slim_corev1.ServiceSpec{ + ClusterIPs: []string{"10.96.0.1", "10.96.0.2"}, + }, + } + + // Send upsert event + mockRes.sendEvent(resource.Upsert, service) + + // Give the handler time to process + time.Sleep(10 * time.Millisecond) + + // Verify the service is cached + handler.mu.RLock() + defer handler.mu.RUnlock() + + expectedKey := types.NamespacedName{Name: "test-service", Namespace: "default"} + assert.Contains(t, handler.ipToService, "10.96.0.1") + assert.Contains(t, handler.ipToService, "10.96.0.2") + + flowService1 := handler.ipToService["10.96.0.1"] + flowService2 := handler.ipToService["10.96.0.2"] + assert.Equal(t, "test-service", flowService1.GetName()) + assert.Equal(t, "default", flowService1.GetNamespace()) + assert.Same(t, flowService1, flowService2) + + assert.Contains(t, handler.serviceToIPs, expectedKey) + assert.ElementsMatch(t, []string{"10.96.0.1", "10.96.0.2"}, handler.serviceToIPs[expectedKey]) +} + +func TestServiceHandler_EventHandling_ServiceUpdated(t *testing.T) { + mockRes := newMockServiceResource() + defer mockRes.close() + + lc := &cell.DefaultLifecycle{} + params := ServiceHandlerParams{ + Lifecycle: lc, + Services: mockRes, + } + + handlerOut := NewServiceHandler(params) + handler := handlerOut.ServiceHandler + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + err := handler.Start(ctx) + require.NoError(t, err) + time.Sleep(10 * time.Millisecond) + + // Initial service + service := &slim_corev1.Service{ + ObjectMeta: slim_metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + Spec: slim_corev1.ServiceSpec{ + ClusterIPs: []string{"10.96.0.1", "10.96.0.2"}, + }, + } + + mockRes.sendEvent(resource.Upsert, service) + time.Sleep(10 * time.Millisecond) + + // Update service with new IPs + updatedService := &slim_corev1.Service{ + ObjectMeta: slim_metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + Spec: slim_corev1.ServiceSpec{ + ClusterIPs: []string{"10.96.0.3", "10.96.0.4"}, + }, + } + + mockRes.sendEvent(resource.Upsert, updatedService) + time.Sleep(10 * time.Millisecond) + + // Verify cache is updated + handler.mu.RLock() + defer handler.mu.RUnlock() + + expectedKey := types.NamespacedName{Name: "test-service", Namespace: "default"} + + // Old IPs should be gone + assert.NotContains(t, handler.ipToService, "10.96.0.1") + assert.NotContains(t, handler.ipToService, "10.96.0.2") + + // New IPs should be present + assert.Contains(t, handler.ipToService, "10.96.0.3") + assert.Contains(t, handler.ipToService, "10.96.0.4") + + flowService := handler.ipToService["10.96.0.3"] + assert.Equal(t, "test-service", flowService.GetName()) + assert.Equal(t, "default", flowService.GetNamespace()) + + assert.Contains(t, handler.serviceToIPs, expectedKey) + assert.ElementsMatch(t, []string{"10.96.0.3", "10.96.0.4"}, handler.serviceToIPs[expectedKey]) +} + +func TestServiceHandler_EventHandling_ServiceDeleted(t *testing.T) { + mockRes := newMockServiceResource() + defer mockRes.close() + + lc := &cell.DefaultLifecycle{} + params := ServiceHandlerParams{ + Lifecycle: lc, + Services: mockRes, + } + + handlerOut := NewServiceHandler(params) + handler := handlerOut.ServiceHandler + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + err := handler.Start(ctx) + require.NoError(t, err) + time.Sleep(10 * time.Millisecond) + + // Create service first + service := &slim_corev1.Service{ + ObjectMeta: slim_metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + Spec: slim_corev1.ServiceSpec{ + ClusterIPs: []string{"10.96.0.1", "10.96.0.2"}, + }, + } + + mockRes.sendEvent(resource.Upsert, service) + time.Sleep(10 * time.Millisecond) + + // Delete service + mockRes.sendEvent(resource.Delete, service) + time.Sleep(10 * time.Millisecond) + + // Verify cache is cleared + handler.mu.RLock() + defer handler.mu.RUnlock() + + serviceKey := types.NamespacedName{Name: "test-service", Namespace: "default"} + assert.NotContains(t, handler.ipToService, "10.96.0.1") + assert.NotContains(t, handler.ipToService, "10.96.0.2") + assert.NotContains(t, handler.serviceToIPs, serviceKey) +} + +func TestServiceHandler_EventHandling_HeadlessService(t *testing.T) { + mockRes := newMockServiceResource() + defer mockRes.close() + + lc := &cell.DefaultLifecycle{} + params := ServiceHandlerParams{ + Lifecycle: lc, + Services: mockRes, + } + + handlerOut := NewServiceHandler(params) + handler := handlerOut.ServiceHandler + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + err := handler.Start(ctx) + require.NoError(t, err) + time.Sleep(10 * time.Millisecond) + + service := &slim_corev1.Service{ + ObjectMeta: slim_metav1.ObjectMeta{ + Name: "headless-service", + Namespace: "default", + }, + Spec: slim_corev1.ServiceSpec{ + ClusterIPs: []string{"None"}, + }, + } + + mockRes.sendEvent(resource.Upsert, service) + time.Sleep(10 * time.Millisecond) + + // Verify no IPs are cached for headless service + handler.mu.RLock() + defer handler.mu.RUnlock() + + expectedKey := types.NamespacedName{Name: "headless-service", Namespace: "default"} + assert.NotContains(t, handler.ipToService, "None") + assert.Contains(t, handler.serviceToIPs, expectedKey) + assert.Empty(t, handler.serviceToIPs[expectedKey]) +} + +func TestServiceHandler_Decode_ExistingIP(t *testing.T) { + mockRes := newMockServiceResource() + defer mockRes.close() + + lc := &cell.DefaultLifecycle{} + params := ServiceHandlerParams{ + Lifecycle: lc, + Services: mockRes, + } + + handlerOut := NewServiceHandler(params) + handler := handlerOut.ServiceHandler + + // Pre-populate cache + serviceKey := types.NamespacedName{Name: "test-service", Namespace: "default"} + flowService := &flow.Service{Name: "test-service", Namespace: "default"} + handler.mu.Lock() + handler.ipToService["10.96.0.1"] = flowService + handler.serviceToIPs[serviceKey] = []string{"10.96.0.1"} + handler.mu.Unlock() + + ip, _ := netip.ParseAddr("10.96.0.1") + result := handler.Decode(ip) + + require.NotNil(t, result) + assert.Equal(t, "test-service", result.GetName()) + assert.Equal(t, "default", result.GetNamespace()) + assert.Same(t, flowService, result) +} + +func TestServiceHandler_Decode_NonExistingIP(t *testing.T) { + mockRes := newMockServiceResource() + defer mockRes.close() + + lc := &cell.DefaultLifecycle{} + params := ServiceHandlerParams{ + Lifecycle: lc, + Services: mockRes, + } + + handlerOut := NewServiceHandler(params) + handler := handlerOut.ServiceHandler + + ip, _ := netip.ParseAddr("10.96.0.100") + result := handler.Decode(ip) + + assert.Nil(t, result) +} + +func TestServiceHandler_Decode_IPv6(t *testing.T) { + mockRes := newMockServiceResource() + defer mockRes.close() + + lc := &cell.DefaultLifecycle{} + params := ServiceHandlerParams{ + Lifecycle: lc, + Services: mockRes, + } + + handlerOut := NewServiceHandler(params) + handler := handlerOut.ServiceHandler + + // Pre-populate cache with IPv6 + serviceKey := types.NamespacedName{Name: "test-service-v6", Namespace: "kube-system"} + flowService := &flow.Service{Name: "test-service-v6", Namespace: "kube-system"} + handler.mu.Lock() + handler.ipToService["fd00::1"] = flowService + handler.serviceToIPs[serviceKey] = []string{"fd00::1"} + handler.mu.Unlock() + + ip, _ := netip.ParseAddr("fd00::1") + result := handler.Decode(ip) + + require.NotNil(t, result) + assert.Equal(t, "test-service-v6", result.GetName()) + assert.Equal(t, "kube-system", result.GetNamespace()) + assert.Same(t, flowService, result) +} + +func TestServiceHandler_SyncEvent(t *testing.T) { + mockRes := newMockServiceResource() + defer mockRes.close() + + lc := &cell.DefaultLifecycle{} + params := ServiceHandlerParams{ + Lifecycle: lc, + Services: mockRes, + } + + handlerOut := NewServiceHandler(params) + handler := handlerOut.ServiceHandler + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + err := handler.Start(ctx) + require.NoError(t, err) + time.Sleep(10 * time.Millisecond) + + service := &slim_corev1.Service{ + ObjectMeta: slim_metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + Spec: slim_corev1.ServiceSpec{ + ClusterIPs: []string{"10.96.0.1"}, + }, + } + + // Send sync event (should be ignored) + mockRes.sendEvent(resource.Sync, service) + time.Sleep(10 * time.Millisecond) + + // Verify cache is empty + handler.mu.RLock() + defer handler.mu.RUnlock() + + assert.Empty(t, handler.ipToService) + assert.Empty(t, handler.serviceToIPs) +} + +func TestServiceHandler_ConcurrentAccess(t *testing.T) { + mockRes := newMockServiceResource() + defer mockRes.close() + + lc := &cell.DefaultLifecycle{} + params := ServiceHandlerParams{ + Lifecycle: lc, + Services: mockRes, + } + + handlerOut := NewServiceHandler(params) + handler := handlerOut.ServiceHandler + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + err := handler.Start(ctx) + require.NoError(t, err) + time.Sleep(10 * time.Millisecond) + + service := &slim_corev1.Service{ + ObjectMeta: slim_metav1.ObjectMeta{ + Name: "concurrent-test-service", + Namespace: "default", + }, + Spec: slim_corev1.ServiceSpec{ + ClusterIPs: []string{"10.100.0.1"}, + }, + } + + // Test concurrent read/write access + done := make(chan bool, 2) + + // Goroutine 1: Send event + go func() { + mockRes.sendEvent(resource.Upsert, service) + done <- true + }() + + // Goroutine 2: Read from cache + go func() { + time.Sleep(5 * time.Millisecond) + ip, _ := netip.ParseAddr("10.100.0.1") + handler.Decode(ip) + done <- true + }() + + // Wait for both goroutines to complete + <-done + <-done + + time.Sleep(10 * time.Millisecond) + + // Verify final state + ip, _ := netip.ParseAddr("10.100.0.1") + result := handler.Decode(ip) + + require.NotNil(t, result) + assert.Equal(t, "concurrent-test-service", result.GetName()) + assert.Equal(t, "default", result.GetNamespace()) +} + +func TestServiceHandler_StopClosesChannel(t *testing.T) { + mockRes := newMockServiceResource() + + lc := &cell.DefaultLifecycle{} + params := ServiceHandlerParams{ + Lifecycle: lc, + Services: mockRes, + } + + handlerOut := NewServiceHandler(params) + handler := handlerOut.ServiceHandler + + ctx, cancel := context.WithCancel(context.Background()) + + err := handler.Start(ctx) + require.NoError(t, err) + time.Sleep(10 * time.Millisecond) + + // Close the mock resource + mockRes.close() + time.Sleep(10 * time.Millisecond) + + // Stop should complete without error + err = handler.Stop(ctx) + require.NoError(t, err) + + cancel() +} diff --git a/pkg/k8s/apiserver_linux.go b/pkg/k8s/apiserver_linux.go index f766073a7a..32fd2b8fae 100644 --- a/pkg/k8s/apiserver_linux.go +++ b/pkg/k8s/apiserver_linux.go @@ -1,19 +1,20 @@ package k8s import ( + "log/slog" + "github.com/cilium/cilium/pkg/identity" "github.com/cilium/cilium/pkg/ipcache" "github.com/cilium/cilium/pkg/source" "github.com/cilium/hive/cell" "github.com/microsoft/retina/pkg/common" cc "github.com/microsoft/retina/pkg/controllers/cache" - "github.com/sirupsen/logrus" ) type params struct { cell.In - Logger logrus.FieldLogger + Logger *slog.Logger IPCache *ipcache.IPCache Lifecycle cell.Lifecycle } @@ -28,62 +29,53 @@ func newAPIServerEventHandler(p params) *APIServerEventHandler { type APIServerEventHandler struct { c *ipcache.IPCache - l logrus.FieldLogger + l *slog.Logger } func (a *APIServerEventHandler) handleAPIServerEvent(event interface{}) { cacheEvent, ok := event.(*cc.CacheEvent) if !ok { - a.l.WithField("Event", event).Warn("Received unknown event type") + a.l.Warn("Received unknown event type", "event", event) return } switch cacheEvent.Type { //nolint:exhaustive // the default case adequately handles these case cc.EventTypeAddAPIServerIPs: apiserverObj, ok := cacheEvent.Obj.(*common.APIServerObject) if !ok { - a.l.WithField("Cache Event", cacheEvent).Warn("Received unknown event type") + a.l.Warn("Received unknown event type", "cacheEvent", cacheEvent) return } ips := apiserverObj.IPs() if len(ips) == 0 { - a.l.WithField("Cache Event", cacheEvent).Warn("Received empty API server IPs") + a.l.Warn("Received empty API server IPs", "cacheEvent", cacheEvent) return } for _, ip := range ips { //nolint:staticcheck // TODO(timraymond): unclear how to migrate this _, err := a.c.Upsert(ip.String(), nil, 0, nil, ipcache.Identity{ID: identity.ReservedIdentityKubeAPIServer, Source: source.Kubernetes}) if err != nil { - a.l.WithError(err).WithFields(logrus.Fields{ - "IP": ips[0].String(), - }).Error("Failed to add API server IPs to ipcache") + a.l.Error("Failed to add API server IPs to ipcache", "error", err, "ip", ip.String()) return } } - a.l.WithFields(logrus.Fields{ - "IP": ips[0].String(), - }).Info("Added API server IPs to ipcache") + a.l.Info("Added API server IPs to ipcache", "ips", ips) case cc.EventTypeDeleteAPIServerIPs: apiserverObj, ok := cacheEvent.Obj.(*common.APIServerObject) if !ok { - a.l.WithField("Cache Event", cacheEvent).Warn("Received unknown event type") + a.l.Warn("Received unknown event type", "cacheEvent", cacheEvent) return } ips := apiserverObj.IPs() if len(ips) == 0 { - a.l.WithField("Cache Event", cacheEvent).Warn("Received empty API server IPs") + a.l.Warn("Received empty API server IPs", "cacheEvent", cacheEvent) return } for _, ip := range ips { //nolint:staticcheck // TODO(timraymond): unclear how to migrate this a.c.Delete(ip.String(), source.Kubernetes) } - a.l.WithFields(logrus.Fields{ - "IP": ips[0].String(), - }).Info("Deleted API server IPs from ipcache") + a.l.Info("Deleted API server IPs from ipcache", "ips", ips) default: - a.l.WithFields(logrus.Fields{ - "Cache Event": cacheEvent, - "Type": cacheEvent.Type, - }).Warn("Received unknown cache event") + a.l.Warn("Received unknown cache event", "cacheEvent", cacheEvent, "type", cacheEvent.Type) } } diff --git a/pkg/k8s/cell_linux.go b/pkg/k8s/cell_linux.go index 72fda13ae8..93e6a29c4f 100644 --- a/pkg/k8s/cell_linux.go +++ b/pkg/k8s/cell_linux.go @@ -2,14 +2,19 @@ package k8s import ( "context" + "fmt" "log/slog" daemonk8s "github.com/cilium/cilium/daemon/k8s" cgmngr "github.com/cilium/cilium/pkg/cgroups/manager" + cmtypes "github.com/cilium/cilium/pkg/clustermesh/types" + fakedp "github.com/cilium/cilium/pkg/datapath/fake/types" "github.com/cilium/cilium/pkg/datapath/iptables/ipset" "github.com/cilium/cilium/pkg/datapath/tables" datapath "github.com/cilium/cilium/pkg/datapath/types" "github.com/cilium/cilium/pkg/endpointmanager" + "github.com/cilium/cilium/pkg/endpointstate" + "github.com/cilium/cilium/pkg/identity" "github.com/cilium/cilium/pkg/identity/cache" identitycachecell "github.com/cilium/cilium/pkg/identity/cache/cell" "github.com/cilium/cilium/pkg/ipcache" @@ -23,47 +28,109 @@ import ( "github.com/cilium/cilium/pkg/k8s/synced" k8sTypes "github.com/cilium/cilium/pkg/k8s/types" "github.com/cilium/cilium/pkg/k8s/watchers" + "github.com/cilium/cilium/pkg/labelsfilter" + "github.com/cilium/cilium/pkg/loadbalancer" "github.com/cilium/cilium/pkg/metrics" "github.com/cilium/cilium/pkg/node" "github.com/cilium/cilium/pkg/policy" - "github.com/cilium/cilium/pkg/redirectpolicy" - "github.com/cilium/cilium/pkg/service" + policycell "github.com/cilium/cilium/pkg/policy/cell" + "github.com/cilium/cilium/pkg/promise" + testidentity "github.com/cilium/cilium/pkg/testutils/identity" + wgtypes "github.com/cilium/cilium/pkg/wireguard/types" "github.com/cilium/hive/cell" "github.com/cilium/statedb" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/workqueue" + "github.com/microsoft/retina/pkg/common" "github.com/microsoft/retina/pkg/pubsub" - "github.com/sirupsen/logrus" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +// Cell provides the Kubernetes watchers needed by Hubble flow enrichment. +// It is broken into sub-cells so that when Cilium changes its DI graph +// upstream, the failing sub-cell immediately identifies which dependency +// boundary broke. var Cell = cell.Module( "k8s-watcher", - "Kubernetes watchers needed by the agent", + "Kubernetes watchers needed by Hubble flow enrichment", + infrastructureCell, + ipcacheCell, + watcherCell, + stubsCell, +) + +// infrastructureCell provides StateDB tables, metrics, cgroups, and other +// foundational components that Cilium's internals depend on. +// Breaks when: Cilium adds new table types or changes table registration APIs. +var infrastructureCell = cell.Group( daemonk8s.PodTableCell, + daemonk8s.NamespaceTableCell, cell.Provide( tables.NewDeviceTable, statedb.RWTable[*tables.Device].ToTable, ), - cell.Invoke(func(db *statedb.DB, t statedb.Table[*tables.Device]) { - err := db.RegisterTable(t) - if err != nil { - logrus.WithError(err).Fatal("Failed to register table") - } + // Service cache tables + cell.Provide(func(db *statedb.DB) (statedb.RWTable[tables.NodeAddress], error) { + return statedb.NewTable(db, tables.NodeAddressTableName, tables.NodeAddressIndex) }), + cell.Provide(statedb.RWTable[tables.NodeAddress].ToTable), + + // Loadbalancer config (required by some Cilium cells) + loadbalancer.ConfigCell, + cell.Provide(newFrontendsTable), + + // Cluster info + cell.Provide(func() cmtypes.ClusterInfo { return cmtypes.DefaultClusterInfo }), + + metrics.Cell, + cgmngr.Cell, +) + +// ipcacheCell provides identity allocation and IPCache — the core +// components Hubble uses to enrich flows with pod/namespace metadata. +// Breaks when: Cilium changes identity allocator APIs or IPCache config. +var ipcacheCell = cell.Group( + identitycachecell.Cell, + endpointmanager.Cell, + + cell.Invoke(initLabelFilter), + cell.Provide(newIPCache), + + node.LocalNodeStoreCell, + cell.Provide(newNodeSynchronizer), +) + +// watcherCell sets up K8s resource watches for CiliumEndpoint, Services, +// and Endpoints. These feed data into IPCache for flow enrichment. +// Breaks when: Cilium changes resource constructor signatures or watcher config. +var watcherCell = cell.Group( + cell.Provide(newCiliumEndpointResource), + cell.Provide(newEndpointsResource), + cell.Provide(newServiceResource), + + cell.Provide(func() watchers.WatcherConfiguration { return &watcherconfig{} }), + cell.Provide(func() watchers.ResourceGroupFunc { return watcherResourceGroups }), + + synced.Cell, + watchers.Cell, + + cell.Provide(newAPIServerEventHandler), + cell.Invoke(subscribeAPIServerEvents), +) +// stubsCell provides no-op implementations for Cilium features that Retina +// doesn't use (policy engine, datapath, wireguard, ipsec, etc.). These +// satisfy DI requirements without any real functionality. +// Breaks when: Cilium adds new required providers or changes interfaces. +var stubsCell = cell.Group( + // Fake K8s resources for features Retina doesn't watch cell.Provide( - func() resource.Resource[*slim_corev1.Namespace] { - return &fakeresource[*slim_corev1.Namespace]{} - }, - func() daemonk8s.LocalNodeResource { - return &fakeresource[*slim_corev1.Node]{} - }, - func() daemonk8s.LocalCiliumNodeResource { - return &fakeresource[*cilium_api_v2.CiliumNode]{} - }, + func() resource.Resource[*slim_corev1.Namespace] { return &fakeresource[*slim_corev1.Namespace]{} }, + func() daemonk8s.LocalNodeResource { return &fakeresource[*slim_corev1.Node]{} }, + func() daemonk8s.LocalCiliumNodeResource { return &fakeresource[*cilium_api_v2.CiliumNode]{} }, func() resource.Resource[*slim_networkingv1.NetworkPolicy] { return &fakeresource[*slim_networkingv1.NetworkPolicy]{} }, @@ -76,156 +143,136 @@ var Cell = cell.Module( func() resource.Resource[*cilium_api_v2alpha1.CiliumCIDRGroup] { return &fakeresource[*cilium_api_v2alpha1.CiliumCIDRGroup]{} }, + func() resource.Resource[*cilium_api_v2.CiliumCIDRGroup] { + return &fakeresource[*cilium_api_v2.CiliumCIDRGroup]{} + }, func() resource.Resource[*cilium_api_v2alpha1.CiliumEndpointSlice] { return &fakeresource[*cilium_api_v2alpha1.CiliumEndpointSlice]{} }, func() resource.Resource[*cilium_api_v2.CiliumNode] { return &fakeresource[*cilium_api_v2.CiliumNode]{} }, - func() watchers.WatcherConfiguration { - return &watcherconfig{} - }, ), - svcCacheCell, - - metrics.Cell, - - endpointmanager.Cell, - - cell.Provide(func() *policy.Updater { - return &policy.Updater{} - }), - - cell.Provide(func() *redirectpolicy.Manager { - return &redirectpolicy.Manager{} - }), - - cell.Provide(func() datapath.BandwidthManager { - return &fakeBandwidthManager{} - }), - - cell.Provide(func() service.ServiceManager { - return &service.Service{} - }), - - cgmngr.Cell, - - // Provide the resources needed by the watchers. - - cell.Provide(func(lc cell.Lifecycle, cs client.Clientset) (resource.Resource[*k8sTypes.CiliumEndpoint], error) { - return ciliumk8s.CiliumSlimEndpointResource(ciliumk8s.CiliumResourceParams{ - Lifecycle: lc, - ClientSet: cs, - }, nil, func(*metav1.ListOptions) {}) - }), - - cell.Provide(func(l *slog.Logger, lc cell.Lifecycle, cs client.Clientset) (resource.Resource[*ciliumk8s.Endpoints], error) { - //nolint:wrapcheck // a wrapped error here is of dubious value - return ciliumk8s.EndpointsResource(l, lc, ciliumk8s.Config{ - EnableK8sEndpointSlice: true, - K8sServiceProxyName: "", - }, cs) - }), - - cell.Provide(func(lc cell.Lifecycle, cs client.Clientset) (resource.Resource[*slim_corev1.Service], error) { - //nolint:wrapcheck // a wrapped error here is of dubious value - return ciliumk8s.ServiceResource( - lc, - ciliumk8s.Config{ - EnableK8sEndpointSlice: false, - }, - cs, - func(*metav1.ListOptions) {}, - ) - }), - + // No-op policy/datapath (Retina doesn't use Cilium's policy engine). + // Uses Cilium's own fake types from pkg/datapath/fake/types where available. cell.Provide( - func() policy.PolicyRepository { - return &NoOpPolicyRepository{} - }, - func() datapath.Orchestrator { - return &NoOpOrchestrator{} + func() policy.PolicyRepository { return &NoOpPolicyRepository{} }, + func() datapath.Orchestrator { return &fakedp.FakeOrchestrator{} }, + func() policycell.IdentityUpdater { return &noOpIdentityUpdater{} }, + func() *policy.Updater { return &policy.Updater{} }, + func() datapath.BandwidthManager { return &fakedp.BandwidthManager{} }, + func() ipset.Manager { return &fakedp.IPSet{} }, + func() wgtypes.WireguardConfig { return fakedp.WireguardConfig{} }, + func() datapath.IPsecConfig { return fakedp.IPsecConfig{} }, + func() datapath.IptablesManager { return fakedp.NewIptablesManager() }, + func() promise.Promise[endpointstate.Restorer] { + r, p := promise.New[endpointstate.Restorer]() + r.Resolve(&fakeRestorer{}) + return p }, ), +) - identitycachecell.Cell, +// ============================================================ +// Helper functions +// ============================================================ - // Provide everything needed for the watchers. - cell.Provide( - func() *ipcache.IPCache { - alloc := cache.NewCachingIdentityAllocator( - &identityAllocatorOwner{}, - cache.AllocatorConfig{}, - ) - idAlloc := &cachingIdentityAllocator{ - alloc, - nil, - } - return ipcache.NewIPCache(&ipcache.Configuration{ - Context: context.Background(), - IdentityAllocator: idAlloc, - PolicyHandler: &policyhandler{}, - DatapathHandler: &datapathhandler{}, - }) - }, - ), +func initLabelFilter(l *slog.Logger) { + if err := labelsfilter.ParseLabelPrefixCfg(l, nil, nil, ""); err != nil { + l.Error("Failed to parse label prefix config", "error", err) + } +} - cell.Provide(func() node.LocalNodeSynchronizer { - return &nodeSynchronizer{ - l: logrus.WithField("module", "node-synchronizer"), - } - }), +func newIPCache(l *slog.Logger) *ipcache.IPCache { + alloc := cache.NewCachingIdentityAllocator( + l, + &testidentity.IdentityAllocatorOwnerMock{}, + cache.AllocatorConfig{}, + ) + return ipcache.NewIPCache(&ipcache.Configuration{ + Context: context.Background(), + Logger: l.With("module", "ipcache"), + IdentityAllocator: alloc, + IdentityUpdater: &noOpIdentityUpdater{}, + }) +} - cell.Provide(func() ipset.Manager { - return &fakeIpsetMgr{} - }), +func newNodeSynchronizer(l *slog.Logger) node.LocalNodeSynchronizer { + return &nodeSynchronizer{l: l.With("module", "node-synchronizer")} +} - node.LocalNodeStoreCell, +func newCiliumEndpointResource( + lc cell.Lifecycle, cs client.Clientset, mp workqueue.MetricsProvider, +) (resource.Resource[*k8sTypes.CiliumEndpoint], error) { + //nolint:wrapcheck // wrapped error here is of dubious value + return ciliumk8s.CiliumSlimEndpointResource(ciliumk8s.CiliumResourceParams{ + Lifecycle: lc, + ClientSet: cs, + }, nil, mp, func(*metav1.ListOptions) {}) +} - synced.Cell, - cell.Provide(newAPIServerEventHandler), +func newEndpointsResource( + l *slog.Logger, lc cell.Lifecycle, cs client.Clientset, mp workqueue.MetricsProvider, +) (resource.Resource[*ciliumk8s.Endpoints], error) { + //nolint:wrapcheck // wrapped error here is of dubious value + return ciliumk8s.EndpointsResource(l, lc, ciliumk8s.ConfigParams{ + Config: ciliumk8s.Config{K8sServiceProxyName: ""}, + WatchConfig: ciliumk8s.ServiceWatchConfig{EnableHeadlessServiceWatch: true}, + }, cs, mp) +} - cell.Provide(func() watchers.ResourceGroupFunc { return watcherResourceGroups }), +func newServiceResource( + lc cell.Lifecycle, cs client.Clientset, mp workqueue.MetricsProvider, +) (resource.Resource[*slim_corev1.Service], error) { + //nolint:wrapcheck // wrapped error here is of dubious value + return ciliumk8s.ServiceResource( + lc, + ciliumk8s.ConfigParams{ + Config: ciliumk8s.Config{K8sServiceProxyName: ""}, + WatchConfig: ciliumk8s.ServiceWatchConfig{EnableHeadlessServiceWatch: false}, + }, + cs, + mp, + func(*metav1.ListOptions) {}, + ) +} - watchers.Cell, +func newFrontendsTable(cfg loadbalancer.Config, db *statedb.DB) (statedb.Table[*loadbalancer.Frontend], error) { + tbl, err := loadbalancer.NewFrontendsTable(cfg, db) + if err != nil { + return nil, fmt.Errorf("failed to create frontends table: %w", err) + } + return tbl, nil +} - cell.Invoke(func(a *APIServerEventHandler) { - ps := pubsub.New() - fn := pubsub.CallBackFunc(a.handleAPIServerEvent) - uuid := ps.Subscribe(common.PubSubAPIServer, &fn) - a.l.WithFields(logrus.Fields{ - "uuid": uuid, - }).Info("Subscribed to PubSub APIServer") - }), -) +func subscribeAPIServerEvents(a *APIServerEventHandler) { + ps := pubsub.New() + fn := pubsub.CallBackFunc(a.handleAPIServerEvent) + uuid := ps.Subscribe(common.PubSubAPIServer, &fn) + a.l.Info("Subscribed to PubSub APIServer", "uuid", uuid) +} -var svcCacheCell = cell.Group( - cell.Provide(func() (statedb.Table[tables.NodeAddress], error) { - return statedb.NewTable(tables.NodeAddressTableName, tables.NodeAddressIndex) - }), - cell.Invoke(func(db *statedb.DB, t statedb.Table[tables.NodeAddress]) { - err := db.RegisterTable(t) - if err != nil { - logrus.WithError(err).Fatal("Failed to register table") - } - }), - cell.Provide(ciliumk8s.NewSVCMetricsNoop), - cell.Provide(ciliumk8s.NewServiceCache), - cell.Provide(func(svcCache *ciliumk8s.ServiceCacheImpl) ciliumk8s.ServiceCache { - return svcCache - }), -) +// ============================================================ +// Watcher configuration +// ============================================================ -const ( - K8sAPIGroupCiliumEndpointV2 = "cilium/v2::CiliumEndpoint" - K8sAPIGroupServiceV1Core = "core/v1::Service" -) +const K8sAPIGroupCiliumEndpointV2 = "cilium/v2::CiliumEndpoint" -var k8sResources = []string{K8sAPIGroupCiliumEndpointV2, K8sAPIGroupServiceV1Core} +var k8sResources = []string{K8sAPIGroupCiliumEndpointV2} -// resourceGroups are all of the core Kubernetes and Cilium resource groups -// which the Cilium agent watches to implement CNI functionality. func watcherResourceGroups(*slog.Logger, watchers.WatcherConfiguration) (r, w []string) { return k8sResources, w } + +// ============================================================ +// No-op identity updater +// ============================================================ + +type noOpIdentityUpdater struct{} + +func (n *noOpIdentityUpdater) UpdateIdentities(_, _ identity.IdentityMap) <-chan struct{} { + ch := make(chan struct{}) + close(ch) + return ch +} diff --git a/pkg/k8s/local_node_synchronizer_linux.go b/pkg/k8s/local_node_synchronizer_linux.go index 4ce5402d96..399bd2b4f4 100644 --- a/pkg/k8s/local_node_synchronizer_linux.go +++ b/pkg/k8s/local_node_synchronizer_linux.go @@ -2,17 +2,17 @@ package k8s import ( "context" + "log/slog" "net" "os" "github.com/cilium/cilium/pkg/node" "github.com/cilium/cilium/pkg/node/addressing" nodetypes "github.com/cilium/cilium/pkg/node/types" - "github.com/sirupsen/logrus" ) type nodeSynchronizer struct { - l *logrus.Entry + l *slog.Logger } func (n *nodeSynchronizer) InitLocalNode(_ context.Context, ln *node.LocalNode) error { diff --git a/pkg/k8s/placeholders_linux.go b/pkg/k8s/placeholders_linux.go index af145d03b0..8d97b8ceea 100644 --- a/pkg/k8s/placeholders_linux.go +++ b/pkg/k8s/placeholders_linux.go @@ -2,25 +2,14 @@ package k8s import ( "context" - "io" - "net" - "net/netip" - "sync" - "github.com/cilium/cilium/api/v1/models" "github.com/cilium/cilium/pkg/container/set" - "github.com/cilium/cilium/pkg/crypto/certificatemanager" - "github.com/cilium/cilium/pkg/datapath/iptables/ipset" - "github.com/cilium/cilium/pkg/datapath/loader/metrics" - datapathtypes "github.com/cilium/cilium/pkg/datapath/types" "github.com/cilium/cilium/pkg/identity" - "github.com/cilium/cilium/pkg/identity/cache" - "github.com/cilium/cilium/pkg/ipcache" ipcachetypes "github.com/cilium/cilium/pkg/ipcache/types" "github.com/cilium/cilium/pkg/k8s/resource" - "github.com/cilium/cilium/pkg/labels" "github.com/cilium/cilium/pkg/policy" "github.com/cilium/cilium/pkg/policy/api" + policytypes "github.com/cilium/cilium/pkg/policy/types" cilium "github.com/cilium/proxy/go/cilium/api" k8sRuntime "k8s.io/apimachinery/pkg/runtime" ) @@ -38,92 +27,10 @@ func (f *fakeresource[T]) Store(_ context.Context) (resource.Store[T], error) { func (f *fakeresource[T]) Observe(context.Context, func(resource.Event[T]), func(error)) { } -type watcherconfig struct { - internalconfigs -} - -func (w *watcherconfig) KVstoreEnabled() bool { - return false -} - -type internalconfigs struct{} - -func (w *internalconfigs) K8sNetworkPolicyEnabled() bool { - return false -} - -func (w *internalconfigs) K8sIngressControllerEnabled() bool { - return false -} - -func (w *internalconfigs) K8sGatewayAPIEnabled() bool { - return false -} - -func (w *internalconfigs) KVstoreEnabledWithoutPodNetworkSupport() bool { - return false -} - -type identityAllocatorOwner struct{} - -func (i *identityAllocatorOwner) UpdateIdentities(identity.IdentityMap, identity.IdentityMap) {} - -func (i *identityAllocatorOwner) GetNodeSuffix() string { - return "" -} - -type cachingIdentityAllocator struct { - *cache.CachingIdentityAllocator - ipcache *ipcache.IPCache -} - -func (c cachingIdentityAllocator) AllocateCIDRsForIPs([]net.IP, map[netip.Prefix]*identity.Identity) ([]*identity.Identity, error) { - return nil, nil -} - -func (c cachingIdentityAllocator) ReleaseCIDRIdentitiesByID(context.Context, []identity.NumericIdentity) { -} - -type policyhandler struct{} - -func (p *policyhandler) UpdateIdentities(identity.IdentityMap, identity.IdentityMap, *sync.WaitGroup) bool { - return false -} - -type datapathhandler struct{} - -func (d *datapathhandler) UpdatePolicyMaps(context.Context, *sync.WaitGroup) *sync.WaitGroup { - return &sync.WaitGroup{} -} - -type fakeBandwidthManager struct{} - -func (f *fakeBandwidthManager) BBREnabled() bool { - return false -} - -func (f *fakeBandwidthManager) Enabled() bool { - return false -} - -func (f *fakeBandwidthManager) UpdateBandwidthLimit(uint16, uint64, uint32) {} - -func (f *fakeBandwidthManager) DeleteBandwidthLimit(uint16) {} - -func (f *fakeBandwidthManager) UpdateIngressBandwidthLimit(uint16, uint64) { -} - -func (f *fakeBandwidthManager) DeleteIngressBandwidthLimit(uint16) {} - -type fakeIpsetMgr struct{} - -func (f *fakeIpsetMgr) NewInitializer() ipset.Initializer { - return nil -} - -func (f *fakeIpsetMgr) AddToIPSet(string, ipset.Family, ...netip.Addr) {} +type watcherconfig struct{} -func (f *fakeIpsetMgr) RemoveFromIPSet(string, ...netip.Addr) {} +func (w *watcherconfig) K8sNetworkPolicyEnabled() bool { return false } +func (w *watcherconfig) K8sClusterNetworkPolicyEnabled() bool { return false } // NoOpPolicyRepository is a no-op implementation of the PolicyRepository interface. type NoOpPolicyRepository struct{} @@ -156,43 +63,30 @@ func (n *NoOpPolicyRepository) GetSelectorCache() *policy.SelectorCache { return nil } -func (n *NoOpPolicyRepository) Iterate(func(*api.Rule)) {} - -func (n *NoOpPolicyRepository) ReplaceByResource(api.Rules, ipcachetypes.ResourceID) (affectedIDs *set.Set[identity.NumericIdentity], rev uint64, oldRevCnt int) { - return nil, 0, 0 +func (n *NoOpPolicyRepository) GetSubjectSelectorCache() *policy.SelectorCache { + return nil } -func (n *NoOpPolicyRepository) ReplaceByLabels(api.Rules, []labels.LabelArray) (affectedIDs *set.Set[identity.NumericIdentity], rev uint64, oldRevCnt int) { +func (n *NoOpPolicyRepository) Iterate(func(*policytypes.PolicyEntry)) {} + +func (n *NoOpPolicyRepository) ReplaceByResource( + _ policytypes.PolicyEntries, _ ipcachetypes.ResourceID, +) (affectedIDs *set.Set[identity.NumericIdentity], rev uint64, oldRevCnt int) { return nil, 0, 0 } -func (n *NoOpPolicyRepository) Search(labels.LabelArray) (r api.Rules, i uint64) { +func (n *NoOpPolicyRepository) Search() (entries policytypes.PolicyEntries, rev uint64) { return nil, 0 } -func (n *NoOpPolicyRepository) SetEnvoyRulesFunc(func(certificatemanager.SecretManager, *api.L7Rules, string, string) (*cilium.HttpNetworkPolicyRules, bool)) { -} - -type NoOpOrchestrator struct{} - -func (n *NoOpOrchestrator) Reinitialize(context.Context) error { +func (n *NoOpPolicyRepository) GetPolicySnapshot() map[identity.NumericIdentity]policy.SelectorPolicy { return nil } -func (n *NoOpOrchestrator) ReloadDatapath(context.Context, datapathtypes.Endpoint, *metrics.SpanStat) (string, error) { - return "", nil -} +// fakeRestorer is a no-op endpointstate.Restorer (Retina doesn't restore endpoints). +type fakeRestorer struct{} -func (n *NoOpOrchestrator) ReinitializeXDP(context.Context, []string) error { - return nil -} - -func (n *NoOpOrchestrator) EndpointHash(datapathtypes.EndpointConfiguration) (string, error) { - return "", nil -} - -func (n *NoOpOrchestrator) WriteEndpointConfig(io.Writer, datapathtypes.EndpointConfiguration) error { - return nil -} +func (fakeRestorer) WaitForEndpointRestoreWithoutRegeneration(context.Context) error { return nil } +func (fakeRestorer) WaitForEndpointRestore(context.Context) error { return nil } +func (fakeRestorer) WaitForInitialPolicy(context.Context) error { return nil } -func (n *NoOpOrchestrator) Unload(datapathtypes.Endpoint) {} diff --git a/pkg/k8s/watcher_linux.go b/pkg/k8s/watcher_linux.go index e1a119b03c..883d59f510 100644 --- a/pkg/k8s/watcher_linux.go +++ b/pkg/k8s/watcher_linux.go @@ -2,17 +2,18 @@ package k8s import ( "context" + "log/slog" "strings" + "sync" "time" "k8s.io/apimachinery/pkg/util/runtime" "github.com/cilium/cilium/pkg/k8s" "github.com/cilium/cilium/pkg/k8s/watchers" - "github.com/cilium/cilium/pkg/logging" "github.com/cilium/cilium/pkg/logging/logfields" "github.com/cilium/cilium/pkg/option" - "github.com/sirupsen/logrus" + retinalog "github.com/microsoft/retina/pkg/log" ) func init() { @@ -24,31 +25,44 @@ func init() { } var ( - logger = logging.DefaultLogger.WithField(logfields.LogSubsys, "k8s-watcher") + loggerOnce sync.Once + cachedLog *slog.Logger ) +// logger returns a zap-backed slog logger. Resolved lazily because +// SetupZapLogger runs later in program startup than this package's init(). +func logger() *slog.Logger { + loggerOnce.Do(func() { + cachedLog = retinalog.SlogLogger().With(logfields.LogSubsys, "k8s-watcher") + }) + return cachedLog +} + func Start(ctx context.Context, k *watchers.K8sWatcher) { - logger.Info("Starting Kubernetes watcher") + logger().Info("Starting Kubernetes watcher") option.Config.K8sSyncTimeout = 3 * time.Minute //nolint:gomnd // this duration is self-explanatory - syncdCache := make(chan struct{}) - go k.InitK8sSubsystem(ctx, syncdCache) - logger.WithField("k8s resources", k8sResources).Info("Kubernetes watcher started, will wait for cache sync") - - // Wait for K8s watcher to sync. If doesn't complete in 3 minutes, causes fatal error. - <-syncdCache - logger.Info("Kubernetes watcher synced") + k.InitK8sSubsystem(ctx) + logger().Info("Kubernetes watcher synced") } // retinaK8sErrorHandler is a custom error handler for the watcher // that logs the error and tags the error to easily identify func k8sWatcherErrorHandler(c context.Context, e error, s string, i ...interface{}) { + if e == nil { + // TODO: handle key/values pairs in a better way + // current example output: time="2009-11-10T23:00:00Z" level=error msg="msg: Some error message -- key/values: [int 1 str world]" + logger().ErrorContext(c, "msg: "+s, "key_values", i) + return + } + errStr := e.Error() + logError := func(er, r string) { - logger.WithFields(logrus.Fields{ - "underlyingError": er, - "resource": r, - }).Error("Error watching k8s resource") + logger().Error("Error watching k8s resource", + "underlyingError", er, + "resource", r, + ) } switch { diff --git a/pkg/loader/vmlinux.go b/pkg/loader/vmlinux.go new file mode 100644 index 0000000000..cb0bc0c194 --- /dev/null +++ b/pkg/loader/vmlinux.go @@ -0,0 +1,170 @@ +package loader + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + "unicode" + + "github.com/microsoft/retina/pkg/log" + "go.uber.org/zap" +) + +const ( + // DefaultVmlinuxHeaderDir is the default runtime include directory used by plugins. + DefaultVmlinuxHeaderDir = "/tmp/retina/include" + // VmlinuxHeaderDirEnv is an optional env var to override the runtime include directory. + VmlinuxHeaderDirEnv = "RETINA_VMLINUX_HEADER_DIR" + // VmlinuxHeaderFileName is the generated runtime BTF header filename. + VmlinuxHeaderFileName = "vmlinux.h" + // VmlinuxKernelReleaseFileName stores the kernel release used to generate vmlinux.h. + VmlinuxKernelReleaseFileName = "vmlinux.kernel.release" + kernelReleasePath = "/proc/sys/kernel/osrelease" +) + +var ( + errKernelReleaseEmpty = errors.New("kernel release is empty") + errCachedKernelReleaseEmpty = errors.New("cached kernel release is empty") +) + +// VmlinuxHeaderDir returns the runtime directory where vmlinux.h is expected. +func VmlinuxHeaderDir() string { + dir := strings.TrimSpace(os.Getenv(VmlinuxHeaderDirEnv)) + if dir == "" { + return DefaultVmlinuxHeaderDir + } + + // Security: Prevent argument injection via environment variable + // Block any path containing whitespace that could be used to inject + // additional clang arguments (e.g., "--config /etc/passwd") + // Also block paths starting with dash (would be interpreted as a flag) + for _, r := range dir { + if unicode.IsSpace(r) { + log.Logger().Named("vmlinux").Warn( + "RETINA_VMLINUX_HEADER_DIR contains whitespace, using default", + zap.String("rejected_value", dir), + ) + return DefaultVmlinuxHeaderDir + } + } + + if strings.HasPrefix(dir, "-") { + log.Logger().Named("vmlinux").Warn( + "RETINA_VMLINUX_HEADER_DIR starts with dash (potential flag injection), using default", + zap.String("rejected_value", dir), + ) + return DefaultVmlinuxHeaderDir + } + + return dir +} + +// VmlinuxHeaderPath returns the full runtime path to vmlinux.h. +func VmlinuxHeaderPath() string { + return filepath.Join(VmlinuxHeaderDir(), VmlinuxHeaderFileName) +} + +// PrepareVmlinuxH ensures runtime vmlinux.h exists and returns the include dir. +func PrepareVmlinuxH(ctx context.Context) (string, error) { + headerDir := VmlinuxHeaderDir() + headerPath := filepath.Join(headerDir, VmlinuxHeaderFileName) + kernelRelease, err := currentKernelRelease() + if err != nil { + return headerDir, err + } + + if info, err := os.Stat(headerPath); err == nil { + if info.Size() > 0 { + cachedKernelRelease, readErr := readCachedKernelRelease(headerDir) + if readErr == nil && cachedKernelRelease == kernelRelease { + return headerDir, nil + } + } + } else if !errors.Is(err, os.ErrNotExist) { + return headerDir, fmt.Errorf("failed to stat %s: %w", headerPath, err) + } + + if err := GenerateVmlinuxH(ctx, headerDir); err != nil { + return headerDir, err + } + + if err := writeCachedKernelRelease(headerDir, kernelRelease); err != nil { + return headerDir, err + } + + return headerDir, nil +} + +func GenerateVmlinuxH(ctx context.Context, outputDir string) error { + l := log.Logger().Named("vmlinux-generator") + vmlinuxPath := filepath.Join(outputDir, VmlinuxHeaderFileName) + start := time.Now() + + if err := os.MkdirAll(outputDir, 0o755); err != nil { + return fmt.Errorf("failed to create output directory: %w", err) + } + + cmd := exec.CommandContext(ctx, "bpftool", "btf", "dump", "file", "/sys/kernel/btf/vmlinux", "format", "c") + + outfile, err := os.Create(vmlinuxPath) + if err != nil { + return fmt.Errorf("failed to create vmlinux.h: %w", err) + } + defer outfile.Close() + + cmd.Stdout = outfile + // Capture stderr for debugging + cmd.Stderr = os.Stderr + + l.Info("Generating vmlinux.h", zap.String("path", vmlinuxPath)) + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to generate vmlinux.h: %w", err) + } + + l.Info("Generated vmlinux.h", zap.String("path", vmlinuxPath), zap.Duration("duration", time.Since(start))) + + return nil +} + +func currentKernelRelease() (string, error) { + b, err := os.ReadFile(kernelReleasePath) + if err != nil { + return "", fmt.Errorf("failed to read %s: %w", kernelReleasePath, err) + } + + release := strings.TrimSpace(string(b)) + if release == "" { + return "", fmt.Errorf("%w: %s", errKernelReleaseEmpty, kernelReleasePath) + } + + return release, nil +} + +func readCachedKernelRelease(outputDir string) (string, error) { + metaPath := filepath.Join(outputDir, VmlinuxKernelReleaseFileName) + b, err := os.ReadFile(metaPath) + if err != nil { + return "", fmt.Errorf("failed to read cached kernel release %s: %w", metaPath, err) + } + + release := strings.TrimSpace(string(b)) + if release == "" { + return "", errCachedKernelReleaseEmpty + } + + return release, nil +} + +func writeCachedKernelRelease(outputDir, kernelRelease string) error { + metaPath := filepath.Join(outputDir, VmlinuxKernelReleaseFileName) + if err := os.WriteFile(metaPath, []byte(kernelRelease+"\n"), 0o600); err != nil { + return fmt.Errorf("failed to write cached kernel release %s: %w", metaPath, err) + } + + return nil +} diff --git a/pkg/loader/vmlinux_test.go b/pkg/loader/vmlinux_test.go new file mode 100644 index 0000000000..f939e39f0c --- /dev/null +++ b/pkg/loader/vmlinux_test.go @@ -0,0 +1,155 @@ +package loader + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/microsoft/retina/pkg/log" + "github.com/stretchr/testify/require" +) + +func TestGenerateVmlinuxH(t *testing.T) { + // Initialize logger for test + _, err := log.SetupZapLogger(log.GetDefaultLogOpts()) + require.NoError(t, err) + + // Create a temporary directory for output + tmpDir, err := os.MkdirTemp("", "vmlinux-test") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + ctx := context.Background() + err = GenerateVmlinuxH(ctx, tmpDir) + require.NoError(t, err) + + // Check if vmlinux.h exists + vmlinuxPath := filepath.Join(tmpDir, "vmlinux.h") + info, err := os.Stat(vmlinuxPath) + require.NoError(t, err) + require.False(t, info.IsDir()) + require.Positive(t, info.Size()) + + // Optional: Check content starts with typedef or similar C code + content, err := os.ReadFile(vmlinuxPath) + require.NoError(t, err) + require.Contains(t, string(content), "typedef") +} + +func TestVmlinuxHeaderDir_Security(t *testing.T) { + // Initialize logger for test + _, err := log.SetupZapLogger(log.GetDefaultLogOpts()) + require.NoError(t, err) + + tests := []struct { + name string + envValue string + expected string + desc string + }{ + { + name: "default when empty", + envValue: "", + expected: DefaultVmlinuxHeaderDir, + desc: "Should use default when env var is empty", + }, + { + name: "valid path", + envValue: "/custom/path/headers", + expected: "/custom/path/headers", + desc: "Should accept valid path without special characters", + }, + { + name: "valid path with dashes", + envValue: "/tmp/fake-config", + expected: "/tmp/fake-config", + desc: "Should accept valid path with dashes in the middle", + }, + { + name: "valid path with multiple dashes", + envValue: "/opt/my-app/retina-headers", + expected: "/opt/my-app/retina-headers", + desc: "Should accept valid path with multiple dashes", + }, + { + name: "block space injection", + envValue: "/tmp/fake --config /etc/passwd", + expected: DefaultVmlinuxHeaderDir, + desc: "Should reject path with spaces (argument injection attempt)", + }, + { + name: "block leading dash (flag injection)", + envValue: "--config=/etc/passwd", + expected: DefaultVmlinuxHeaderDir, + desc: "Should reject path starting with dash (would be interpreted as flag)", + }, + { + name: "block config flag", + envValue: "/tmp/dir --config /etc/shadow", + expected: DefaultVmlinuxHeaderDir, + desc: "Should block clang --config file leak attack", + }, + { + name: "block tab injection", + envValue: "/tmp/fake\t--config\t/etc/shadow", + expected: DefaultVmlinuxHeaderDir, + desc: "Should reject path with tabs", + }, + { + name: "block newline injection", + envValue: "/tmp/fake\n--config /etc/passwd", + expected: DefaultVmlinuxHeaderDir, + desc: "Should reject path with newlines", + }, + { + name: "trimmed whitespace valid", + envValue: " /custom/path ", + expected: "/custom/path", + desc: "Should trim leading/trailing whitespace from valid paths", + }, + { + name: "block multiple arguments", + envValue: "/tmp -I/etc -w /tmp/out.txt", + expected: DefaultVmlinuxHeaderDir, + desc: "Should block attempts to inject multiple clang arguments", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set environment variable + if tt.envValue != "" { + err := os.Setenv(VmlinuxHeaderDirEnv, tt.envValue) + require.NoError(t, err) + defer os.Unsetenv(VmlinuxHeaderDirEnv) + } else { + os.Unsetenv(VmlinuxHeaderDirEnv) + } + + // Call function and verify result + result := VmlinuxHeaderDir() + require.Equal(t, tt.expected, result, tt.desc) + }) + } +} + +func TestVmlinuxHeaderDir_PreventArgumentInjection(t *testing.T) { + // Initialize logger for test + _, err := log.SetupZapLogger(log.GetDefaultLogOpts()) + require.NoError(t, err) + + // Simulate attack: inject --config flag to leak /etc/passwd + maliciousInput := "/tmp/headers --config /etc/passwd" + err = os.Setenv(VmlinuxHeaderDirEnv, maliciousInput) + require.NoError(t, err) + defer os.Unsetenv(VmlinuxHeaderDirEnv) + + // Get the directory (should be sanitized to default) + dir := VmlinuxHeaderDir() + + // Verify attack was blocked + require.Equal(t, DefaultVmlinuxHeaderDir, dir, "Malicious input should be rejected") + require.NotContains(t, dir, "--config", "Result should not contain injected flag") + require.NotContains(t, dir, " ", "Result should not contain spaces") +} diff --git a/pkg/log/export_test.go b/pkg/log/export_test.go new file mode 100644 index 0000000000..2fa5f380be --- /dev/null +++ b/pkg/log/export_test.go @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package log + +import "sync" + +// resetGlobalForTest clears the zap global and the self-init once so each test +// can call SetupZapLogger with its own options. Tests in this package do not +// run in parallel, so no locking is required beyond the atomic store. +func resetGlobalForTest() { + global.Store(nil) + initOnce = sync.Once{} +} diff --git a/pkg/log/zap.go b/pkg/log/zap.go index 9d3e218d3b..dda5ebd461 100644 --- a/pkg/log/zap.go +++ b/pkg/log/zap.go @@ -3,22 +3,37 @@ package log import ( + "context" + "log/slog" "net/http" "os" "runtime" + "sync" + "sync/atomic" "time" "github.com/Azure/azure-container-networking/zapai" "github.com/go-chi/chi/middleware" + "github.com/go-logr/logr" + "github.com/go-logr/zapr" logfmt "github.com/jsternberg/zap-logfmt" "github.com/microsoft/ApplicationInsights-Go/appinsights" "github.com/pkg/errors" "go.uber.org/zap" + "go.uber.org/zap/exp/zapslog" "go.uber.org/zap/zapcore" "gopkg.in/natefinch/lumberjack.v2" ) -var global *ZapLogger +// global holds the configured ZapLogger. Written once by SetupZapLogger and +// read by every helper in this package; atomic for race-free access from +// goroutines started after (or concurrently with) SetupZapLogger. +var ( + global atomic.Pointer[ZapLogger] + initOnce sync.Once +) + +func loadGlobal() *ZapLogger { return global.Load() } const ( defaultFileName = "retina.log" @@ -46,7 +61,12 @@ func GetDefaultLogOpts() *LogOpts { } func Logger() *ZapLogger { - return global + initOnce.Do(func() { + if loadGlobal() == nil { + _, _ = SetupZapLogger(GetDefaultLogOpts()) + } + }) + return loadGlobal() } type ZapLogger struct { @@ -62,8 +82,8 @@ func EncoderConfig() zapcore.EncoderConfig { } func SetupZapLogger(lOpts *LogOpts, persistentFields ...zap.Field) (*ZapLogger, error) { - if global != nil { - return global, nil + if g := loadGlobal(); g != nil { + return g, nil } logger := &ZapLogger{} @@ -130,8 +150,8 @@ func SetupZapLogger(lOpts *LogOpts, persistentFields ...zap.Field) (*ZapLogger, core = zapcore.NewTee(core, filecore) } logger.Logger = zap.New(core, zap.AddCaller()) - global = logger - return global, nil + global.Store(logger) + return logger, nil } func (l *ZapLogger) Close() { @@ -202,3 +222,158 @@ func (lOpts *LogOpts) validate() { } } } + +// SlogHandler returns an slog.Handler backed by the global zap core. +// All slog messages will flow through zap's pipeline (stdout + Application Insights). +// The returned handler resolves the global zap core on every call, so it keeps +// working even if registered (e.g. via logging.AddHandlers) before SetupZapLogger +// runs — once the global is initialized, records start flowing to AI. +func SlogHandler() slog.Handler { + return &lazyZapHandler{} +} + +// lazyZapHandler resolves the Retina zap core at call time. This lets callers +// register a single slog.Handler into Cilium's MultiSlogHandler (or capture +// it via slog.Default/.With) before SetupZapLogger runs — once `global` is +// set, records flow through zap → stdout + Application Insights. +// +// `ops` records WithAttrs / WithGroup calls in their original order so the +// inner zap-backed handler is built with attribute-group nesting identical to +// what the caller requested (per the slog.Handler contract). +type lazyZapHandler struct { + ops []lazyOp +} + +type lazyOp struct { + attrs []slog.Attr // when non-nil, a WithAttrs op + group string // when attrs is nil and group != "", a WithGroup op +} + +func (h *lazyZapHandler) inner() slog.Handler { + g := loadGlobal() + if g == nil { + return nil + } + var sh slog.Handler = zapslog.NewHandler(g.Core(), zapslog.WithCaller(true)) + for _, op := range h.ops { + if op.attrs != nil { + sh = sh.WithAttrs(op.attrs) + } else { + sh = sh.WithGroup(op.group) + } + } + return sh +} + +func (h *lazyZapHandler) Enabled(ctx context.Context, lvl slog.Level) bool { + sh := h.inner() + if sh == nil { + return true // accept records; they'll be dropped below if still uninitialized + } + return sh.Enabled(ctx, lvl) +} + +func (h *lazyZapHandler) Handle(ctx context.Context, r slog.Record) error { + sh := h.inner() + if sh == nil { + return nil // zap not ready yet; Cilium's own text handler still logs to stderr + } + //nolint:wrapcheck // passthrough to the inner slog.Handler; wrapping would mangle zap diagnostics + return sh.Handle(ctx, r) +} + +func (h *lazyZapHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + if len(attrs) == 0 { + return h + } + ops := make([]lazyOp, len(h.ops), len(h.ops)+1) + copy(ops, h.ops) + ops = append(ops, lazyOp{attrs: attrs}) + return &lazyZapHandler{ops: ops} +} + +func (h *lazyZapHandler) WithGroup(name string) slog.Handler { + if name == "" { + return h // per slog.Handler contract + } + ops := make([]lazyOp, len(h.ops), len(h.ops)+1) + copy(ops, h.ops) + ops = append(ops, lazyOp{group: name}) + return &lazyZapHandler{ops: ops} +} + +// SetDefaultSlog sets Go's global slog default to use the zap-backed handler. +// After calling this, slog.Default() returns a logger that routes through zap. +func SetDefaultSlog() { + slog.SetDefault(slog.New(SlogHandler())) +} + +// SlogLogger returns a new *slog.Logger backed by the global zap core. +func SlogLogger() *slog.Logger { + return slog.New(SlogHandler()) +} + +// LogrLogger returns a logr.Logger backed by the global zap logger. +// This is useful for integrating with controller-runtime and other libraries +// that use logr.Logger, ensuring consistent log format across the application. +// +// The returned logr.Logger is backed by a logr.LogSink that resolves the zap +// core on every call, so controller-runtime loggers set up before +// SetupZapLogger runs automatically start flowing to Application Insights +// once the zap global is initialized. +func LogrLogger() logr.Logger { + return logr.New(&lazyLogrSink{}) +} + +// lazyLogrSink resolves the Retina zap.Logger on every call and delegates to +// a fresh zapr sink. A production-zap fallback keeps the sink safe to use +// before SetupZapLogger runs. +type lazyLogrSink struct { + name string + values []any +} + +func (s *lazyLogrSink) delegate() logr.LogSink { + g := loadGlobal() + var zl *zap.Logger + if g != nil { + zl = g.Logger + } else { + zl = zap.Must(zap.NewProduction()) + } + lr := zapr.NewLogger(zl) + if s.name != "" { + lr = lr.WithName(s.name) + } + if len(s.values) > 0 { + lr = lr.WithValues(s.values...) + } + return lr.GetSink() +} + +func (s *lazyLogrSink) Init(ri logr.RuntimeInfo) { s.delegate().Init(ri) } +func (s *lazyLogrSink) Enabled(level int) bool { return s.delegate().Enabled(level) } +func (s *lazyLogrSink) Info(level int, msg string, kv ...any) { + s.delegate().Info(level, msg, kv...) +} + +func (s *lazyLogrSink) Error(err error, msg string, kv ...any) { + s.delegate().Error(err, msg, kv...) +} + +func (s *lazyLogrSink) WithValues(kv ...any) logr.LogSink { + nv := make([]any, 0, len(s.values)+len(kv)) + nv = append(nv, s.values...) + nv = append(nv, kv...) + return &lazyLogrSink{name: s.name, values: nv} +} + +func (s *lazyLogrSink) WithName(name string) logr.LogSink { + out := s.name + if out == "" { + out = name + } else { + out = out + "." + name + } + return &lazyLogrSink{name: out, values: s.values} +} diff --git a/pkg/log/zap_test.go b/pkg/log/zap_test.go index 3da2668f2a..4f4179fe8d 100644 --- a/pkg/log/zap_test.go +++ b/pkg/log/zap_test.go @@ -3,12 +3,14 @@ package log import ( + "log/slog" "os" "path/filepath" "strings" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.uber.org/zap" ) @@ -26,9 +28,9 @@ func TestLogFileRotation(t *testing.T) { logsToPrint := 10 for i := 0; i < logsToPrint; i++ { - global.Info("test", zap.Int("i", i)) + loadGlobal().Info("test", zap.Int("i", i)) } - global.Close() + loadGlobal().Close() _, err := os.Stat(lOpts.FileName) assert.NoError(t, err, "Test log file is not found") @@ -40,7 +42,7 @@ func TestLogFileRotation(t *testing.T) { assert.NoError(t, err, "Getwd failed with err") err = filepath.Walk(p, func(path string, info os.FileInfo, err error) error { - global.Info("Filename: ", zap.String("path", path), zap.String("name", info.Name())) + loadGlobal().Info("Filename: ", zap.String("path", path), zap.String("name", info.Name())) if !info.IsDir() { if strings.HasPrefix(info.Name(), "test") && strings.HasSuffix(info.Name(), ".log") { curReplicas++ @@ -51,3 +53,67 @@ func TestLogFileRotation(t *testing.T) { assert.NoError(t, err, "Test log file walk through failed with err") assert.Equal(t, expectedReplicas, curReplicas, "Test log file replicas are not as expected on 2nd try") } + +func TestSlogHandler(t *testing.T) { + // Reset the global logger for this test + resetGlobalForTest() + + _, err := SetupZapLogger(GetDefaultLogOpts()) + require.NoError(t, err) + + handler := SlogHandler() + require.NotNil(t, handler) + + logger := slog.New(handler) + // Should not panic + logger.Info("test message from slog", "key", "value") + + // SetDefaultSlog should make slog.Default() return zap-backed logger + SetDefaultSlog() + slog.Default().Info("default slog test", "source", "TestSlogHandler") + + // SlogLogger should return a new logger + slogLogger := SlogLogger() + require.NotNil(t, slogLogger) + slogLogger.Info("from SlogLogger", "test", true) +} + +func TestSlogHandlerFallback(t *testing.T) { + // Reset global to test fallback behavior + resetGlobalForTest() + + // Without zap setup, SlogHandler should return a fallback text handler + handler := SlogHandler() + require.NotNil(t, handler) + + // Should not panic even without zap being setup + logger := slog.New(handler) + logger.Info("fallback test message", "key", "value") +} + +func TestLogrLogger(t *testing.T) { + // Reset the global logger for this test + resetGlobalForTest() + + _, err := SetupZapLogger(GetDefaultLogOpts()) + require.NoError(t, err) + + logrLogger := LogrLogger() + require.NotNil(t, logrLogger) + + // Should not panic and should log using the same format as Retina's zap logger + logrLogger.Info("test message from logr", "key", "value") + logrLogger.V(1).Info("debug message from logr", "level", 1) +} + +func TestLogrLoggerFallback(t *testing.T) { + // Reset global to test fallback behavior + resetGlobalForTest() + + // Without zap setup, LogrLogger should return a fallback logger + logrLogger := LogrLogger() + require.NotNil(t, logrLogger) + + // Should not panic even without zap being setup + logrLogger.Info("fallback logr test message", "key", "value") +} diff --git a/pkg/managers/controllermanager/controllermanager.go b/pkg/managers/controllermanager/controllermanager.go index 0ee6b46479..b5fcd86380 100644 --- a/pkg/managers/controllermanager/controllermanager.go +++ b/pkg/managers/controllermanager/controllermanager.go @@ -4,17 +4,16 @@ package controllermanager import ( "context" + "log/slog" "time" kcfg "github.com/microsoft/retina/pkg/config" "github.com/microsoft/retina/pkg/controllers/cache" "github.com/microsoft/retina/pkg/enricher" - "github.com/microsoft/retina/pkg/log" pm "github.com/microsoft/retina/pkg/managers/pluginmanager" sm "github.com/microsoft/retina/pkg/managers/servermanager" "github.com/microsoft/retina/pkg/pubsub" "github.com/microsoft/retina/pkg/telemetry" - "go.uber.org/zap" "golang.org/x/sync/errgroup" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/informers" @@ -26,7 +25,7 @@ const ( ) type Controller struct { - l *log.ZapLogger + l *slog.Logger httpServer *sm.HTTPServer pluginManager *pm.PluginManager tel telemetry.Telemetry @@ -36,8 +35,10 @@ type Controller struct { enricher *enricher.Enricher } -func NewControllerManager(conf *kcfg.Config, kubeclient kubernetes.Interface, tel telemetry.Telemetry) (*Controller, error) { - cmLogger := log.Logger().Named("controller-manager") +func NewControllerManager( + conf *kcfg.Config, kubeclient kubernetes.Interface, tel telemetry.Telemetry, logger *slog.Logger, +) (*Controller, error) { + cmLogger := logger.With("module", "controller-manager") if conf.EnablePodLevel { // informer factory for pods/services @@ -48,6 +49,7 @@ func NewControllerManager(conf *kcfg.Config, kubeclient kubernetes.Interface, te pMgr, err := pm.NewPluginManager( conf, tel, + logger, ) if err != nil { return nil, err @@ -109,7 +111,12 @@ func (m *Controller) Start(ctx context.Context) { // }) if err := g.Wait(); err != nil { - m.l.Panic("Error running controller manager", zap.Error(err)) + m.l.Error("Error running controller manager", "error", err) + // Flush the slog handler before panicking so the error message is not lost + if h, ok := m.l.Handler().(interface{ Flush() error }); ok { + _ = h.Flush() + } + panic(err) } } diff --git a/pkg/managers/controllermanager/controllermanager_test.go b/pkg/managers/controllermanager/controllermanager_test.go index 841dda9dd6..7a92174967 100644 --- a/pkg/managers/controllermanager/controllermanager_test.go +++ b/pkg/managers/controllermanager/controllermanager_test.go @@ -5,6 +5,7 @@ package controllermanager import ( "context" "errors" + "log/slog" "testing" "time" @@ -33,7 +34,7 @@ func TestNewControllerManager(t *testing.T) { log.SetupZapLogger(log.GetDefaultLogOpts()) kubeclient := k8sfake.NewSimpleClientset() - cm, err := NewControllerManager(c, kubeclient, telemetry.NewNoopTelemetry()) + cm, err := NewControllerManager(c, kubeclient, telemetry.NewNoopTelemetry(), slog.Default()) assert.NoError(t, err, "Expected no error, instead got %+v", err) assert.NotNil(t, cm) } @@ -45,7 +46,7 @@ func TestNewControllerManagerWin(t *testing.T) { log.SetupZapLogger(log.GetDefaultLogOpts()) kubeclient := k8sfake.NewSimpleClientset() - cm, err := NewControllerManager(c, kubeclient, telemetry.NewNoopTelemetry()) + cm, err := NewControllerManager(c, kubeclient, telemetry.NewNoopTelemetry(), slog.Default()) assert.Error(t, err, "Expected error of not recognising windows plugins in linux, instead got no error") assert.Nil(t, cm) } @@ -57,7 +58,7 @@ func TestNewControllerManagerInit(t *testing.T) { log.SetupZapLogger(log.GetDefaultLogOpts()) kubeclient := k8sfake.NewSimpleClientset() - cm, err := NewControllerManager(c, kubeclient, telemetry.NewNoopTelemetry()) + cm, err := NewControllerManager(c, kubeclient, telemetry.NewNoopTelemetry(), slog.Default()) assert.NoError(t, err, "Expected no error, instead got %+v", err) assert.NotNil(t, cm) @@ -72,7 +73,7 @@ func TestControllerPluginManagerStartFail(t *testing.T) { log.SetupZapLogger(log.GetDefaultLogOpts()) kubeclient := k8sfake.NewSimpleClientset() - cm, err := NewControllerManager(c, kubeclient, telemetry.NewNoopTelemetry()) + cm, err := NewControllerManager(c, kubeclient, telemetry.NewNoopTelemetry(), slog.Default()) assert.NoError(t, err, "Expected no error, instead got %+v", err) assert.NotNil(t, cm) @@ -86,7 +87,7 @@ func TestControllerPluginManagerStartFail(t *testing.T) { EnablePodLevel: true, EnabledPlugin: []string{pluginName}, } - mgr, err := pm.NewPluginManager(cfg, telemetry.NewNoopTelemetry()) + mgr, err := pm.NewPluginManager(cfg, telemetry.NewNoopTelemetry(), slog.Default()) require.NoError(t, err, "Expected no error, instead got %+v", err) mockPlugin := plugin.NewMockPlugin(ctl) diff --git a/pkg/managers/filtermanager/manager_linux.go b/pkg/managers/filtermanager/manager_linux.go index a275fc2fed..ff123d67c6 100644 --- a/pkg/managers/filtermanager/manager_linux.go +++ b/pkg/managers/filtermanager/manager_linux.go @@ -38,7 +38,7 @@ type FilterManager struct { // Total time: 7 seconds // The manager locks the cache during retry. // Suggest to keep retry to a small number (not more than 3). -func Init(retry int) (*FilterManager, error) { +func Init(retry int, filterMapMaxEntries uint32) (*FilterManager, error) { var err error if retry < 1 { return nil, errors.New("retry should be greater than 0") @@ -55,7 +55,7 @@ func Init(retry int) (*FilterManager, error) { if f.c == nil { f.c = getCache() } - f.fm, err = filter.Init() + f.fm, err = filter.Init(filterMapMaxEntries) return f, errors.Wrapf(err, "failed to initialize filter map") } diff --git a/pkg/managers/filtermanager/manager_windows.go b/pkg/managers/filtermanager/manager_windows.go index 4f285ca75f..c52621dcaf 100644 --- a/pkg/managers/filtermanager/manager_windows.go +++ b/pkg/managers/filtermanager/manager_windows.go @@ -11,7 +11,7 @@ import ( type FilterManager struct{} -func Init(_ int) (*FilterManager, error) { +func Init(_ int, _ uint32) (*FilterManager, error) { return nil, nil } diff --git a/pkg/managers/pluginmanager/cells_linux.go b/pkg/managers/pluginmanager/cells_linux.go index 5a77ef1c0b..a8e4a7ffa4 100644 --- a/pkg/managers/pluginmanager/cells_linux.go +++ b/pkg/managers/pluginmanager/cells_linux.go @@ -2,6 +2,8 @@ package pluginmanager import ( "context" + "log/slog" + "os" "sync" v1 "github.com/cilium/cilium/pkg/hubble/api/v1" @@ -9,7 +11,6 @@ import ( "github.com/microsoft/retina/pkg/config" "github.com/microsoft/retina/pkg/metrics" "github.com/microsoft/retina/pkg/telemetry" - "github.com/sirupsen/logrus" ) const ( @@ -30,7 +31,7 @@ var Cell = cell.Module( type pluginManagerParams struct { cell.In - Log logrus.FieldLogger + Log *slog.Logger Lifecycle cell.Lifecycle Config config.Config Telemetry telemetry.Telemetry @@ -38,12 +39,12 @@ type pluginManagerParams struct { } func newPluginManager(params pluginManagerParams) (*PluginManager, error) { - logger := params.Log.WithField("module", "pluginmanager") + logger := params.Log.With("module", "plugin-manager") // Enable Metrics in retina - metrics.InitializeMetrics() + metrics.InitializeMetrics(params.Log) - pluginMgr, err := NewPluginManager(¶ms.Config, params.Telemetry) + pluginMgr, err := NewPluginManager(¶ms.Config, params.Telemetry, params.Log) if err != nil { return &PluginManager{}, err } @@ -61,7 +62,8 @@ func newPluginManager(params pluginManagerParams) (*PluginManager, error) { defer wg.Done() err = pluginMgr.Start(pmCtx) if err != nil { - logger.WithError(err).Fatal("failed to start plugin manager") + logger.Error("failed to start plugin manager", "error", err) + os.Exit(1) } }() diff --git a/pkg/managers/pluginmanager/pluginmanager.go b/pkg/managers/pluginmanager/pluginmanager.go index 2209f5cf1a..f3f66a28bc 100644 --- a/pkg/managers/pluginmanager/pluginmanager.go +++ b/pkg/managers/pluginmanager/pluginmanager.go @@ -5,55 +5,59 @@ package pluginmanager import ( "context" "fmt" + "log/slog" "sync" "time" v1 "github.com/cilium/cilium/pkg/hubble/api/v1" kcfg "github.com/microsoft/retina/pkg/config" - "github.com/microsoft/retina/pkg/log" "github.com/microsoft/retina/pkg/managers/watchermanager" "github.com/microsoft/retina/pkg/metrics" "github.com/microsoft/retina/pkg/plugin" "github.com/microsoft/retina/pkg/plugin/conntrack" "github.com/microsoft/retina/pkg/telemetry" "github.com/pkg/errors" - "go.uber.org/zap" "golang.org/x/sync/errgroup" ) const ( + // DefaultMetricsInterval is used when MetricsInterval is zero or negative. + DefaultMetricsInterval = 10 * time.Second // In any run I haven't seen reconcile take longer than 5 seconds, // and 10 seconds seems like a reasonable SLA for reconciliation to be completed MAX_RECONCILE_TIME = 10 * time.Second + + // plugin name used for conntrack GC check + pluginNamePacketparser = "packetparser" ) var ( - ErrNilCfg = errors.New("pluginmanager requires a non-nil config") - ErrZeroInterval = errors.New("pluginmanager requires a positive MetricsInterval in its config") + ErrNilCfg = errors.New("pluginmanager requires a non-nil config") + ErrZeroInterval = errors.New("pluginmanager requires a positive MetricsInterval in its config") + ErrPluginNotFound = errors.New("plugin not found in registry") ) type PluginManager struct { cfg *kcfg.Config - l *log.ZapLogger + l *slog.Logger plugins map[string]plugin.Plugin tel telemetry.Telemetry watcherManager watchermanager.IWatcherManager } -func NewPluginManager(cfg *kcfg.Config, tel telemetry.Telemetry) (*PluginManager, error) { - logger := log.Logger().Named("plugin-manager") +func NewPluginManager(cfg *kcfg.Config, tel telemetry.Telemetry, logger *slog.Logger) (*PluginManager, error) { mgr := &PluginManager{ cfg: cfg, - l: logger, + l: logger.With("module", "plugin-manager"), tel: tel, plugins: map[string]plugin.Plugin{}, } if mgr.cfg.EnablePodLevel { mgr.l.Info("plugin manager has pod level enabled") - mgr.watcherManager = watchermanager.NewWatcherManager() + mgr.watcherManager = watchermanager.NewWatcherManager(mgr.cfg.FilterMapMaxEntries) } else { mgr.l.Info("plugin manager has pod level disabled") } @@ -61,7 +65,7 @@ func NewPluginManager(cfg *kcfg.Config, tel telemetry.Telemetry) (*PluginManager for _, name := range cfg.EnabledPlugin { newPluginFn, ok := plugin.Get(name) if !ok { - return nil, fmt.Errorf("plugin %s not found in registry", name) + return nil, errors.Wrapf(ErrPluginNotFound, "%s", name) } mgr.plugins[name] = newPluginFn(mgr.cfg) } @@ -76,12 +80,12 @@ func (p *PluginManager) Stop() { go func(plugin plugin.Plugin) { defer wg.Done() if err := plugin.Stop(); err != nil { - p.l.Error("failed to stop plugin", zap.Error(err)) + p.l.Error("failed to stop plugin", "error", err) // Continue stopping other plugins. // This allows us to stop as many plugins as possible, // even if some plugins fail to stop. } - p.l.Info("Cleaned up resource for plugin", zap.String("name", plugin.Name())) + p.l.Info("Cleaned up resource for plugin", "name", plugin.Name()) }(pl) } wg.Wait() @@ -107,7 +111,7 @@ func (p *PluginManager) Reconcile(ctx context.Context, pl plugin.Plugin) error { return errors.Wrap(err, "failed to init plugin") } - p.l.Info("Reconciled plugin", zap.String("name", pl.Name())) + p.l.Info("Reconciled plugin", "name", pl.Name()) return nil } @@ -122,8 +126,9 @@ func (p *PluginManager) Start(ctx context.Context) error { return ErrNilCfg } - if p.cfg.MetricsInterval == 0 { - return ErrZeroInterval + if p.cfg.MetricsInterval <= 0 { + p.l.Warn("MetricsInterval is invalid or unset; defaulting to 10s", slog.Duration("interval", p.cfg.MetricsInterval)) + p.cfg.MetricsInterval = DefaultMetricsInterval } if p.cfg.EnablePodLevel { @@ -136,7 +141,7 @@ func (p *PluginManager) Start(ctx context.Context) error { } g, ctx := errgroup.WithContext(ctx) - _, isPacketParserEnabled := p.plugins["packetparser"] + _, isPacketParserEnabled := p.plugins[pluginNamePacketparser] // run conntrack GC only if packetparser is enabled if isPacketParserEnabled { ct, connErr := conntrack.New() @@ -174,7 +179,7 @@ func (p *PluginManager) Start(ctx context.Context) error { // on cancel context wait for all plugins to exit err = g.Wait() if err != nil { - p.l.Error("plugin manager exited with error", zap.Error(err)) + p.l.Error("plugin manager exited with error", "error", err) return errors.Wrapf(err, "failed to start plugin manager, plugin exited") } @@ -203,10 +208,13 @@ func (p *PluginManager) SetPlugin(name string, pl plugin.Plugin) { } func (p *PluginManager) SetupChannel(c chan *v1.Event) { + p.l.Info("Setting up external event channel for plugins", "channelCap", cap(c), "pluginCount", len(p.plugins)) for name, plugin := range p.plugins { err := plugin.SetupChannel(c) if err != nil { - p.l.Error("failed to setup channel for plugin", zap.String("plugin name", name), zap.Error(err)) + p.l.Error("failed to setup channel for plugin", "plugin name", name, "error", err) + } else { + p.l.Info("Setup channel for plugin", "plugin", name) } } } diff --git a/pkg/managers/pluginmanager/pluginmanager_test.go b/pkg/managers/pluginmanager/pluginmanager_test.go index 2203468b5b..d5f50ee9bf 100644 --- a/pkg/managers/pluginmanager/pluginmanager_test.go +++ b/pkg/managers/pluginmanager/pluginmanager_test.go @@ -5,6 +5,7 @@ package pluginmanager import ( "context" "errors" + "log/slog" "strings" "testing" "time" @@ -24,7 +25,8 @@ import ( ) const ( - timeInter = time.Second * 10 + timeInter = time.Second * 10 + mockPluginName = "mockplugin" ) var ( @@ -82,7 +84,7 @@ func TestNewManager(t *testing.T) { for _, tt := range tests { tt.cfg.EnabledPlugin = append(tt.cfg.EnabledPlugin, tt.pluginName) - mgr, err := NewPluginManager(&tt.cfg, tel) + mgr, err := NewPluginManager(&tt.cfg, tel, slog.Default()) if tt.wantErr { require.NotNil(t, err, "Expected error but got nil") require.Nil(t, mgr, "Expected mgr to be nil but it isn't") @@ -122,7 +124,7 @@ func TestNewManagerStart(t *testing.T) { for _, tt := range tests { tt.cfg.EnabledPlugin = append(tt.cfg.EnabledPlugin, tt.pluginName) - mgr, err := NewPluginManager(&tt.cfg, tel) + mgr, err := NewPluginManager(&tt.cfg, tel, slog.Default()) require.NoError(t, err) mgr.watcherManager = setupWatcherManagerMock(gomock.NewController(t)) require.Nil(t, err, "Expected nil but got error:%w", err) @@ -151,6 +153,65 @@ func TestNewManagerStart(t *testing.T) { } } +func TestStart_InvalidMetricsIntervalDefaultsTo10s(t *testing.T) { + _, err := log.SetupZapLogger(log.GetDefaultLogOpts()) + require.NoError(t, err) + + tests := []struct { + name string + interval time.Duration + }{ + {"zero interval", 0}, + {"negative interval", -1 * time.Second}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctl := gomock.NewController(t) + defer ctl.Finish() + + cfg := cfgPodLevelDisabled + cfg.MetricsInterval = tt.interval + cfg.EnabledPlugin = append(cfg.EnabledPlugin, mockPluginName) + + mgr, err := NewPluginManager(&cfg, telemetry.NewNoopTelemetry(), slog.Default()) + require.NoError(t, err) + require.NotNil(t, mgr) + mgr.watcherManager = setupWatcherManagerMock(ctl) + + mockPlugin := pluginmock.NewMockPlugin(ctl) + mockPlugin.EXPECT().Generate(gomock.Any()).Return(nil).AnyTimes() + mockPlugin.EXPECT().Compile(gomock.Any()).Return(nil).AnyTimes() + mockPlugin.EXPECT().Stop().Return(nil).AnyTimes() + mockPlugin.EXPECT().Init().Return(nil).AnyTimes() + mockPlugin.EXPECT().Start(gomock.Any()).Return(nil).AnyTimes() + mockPlugin.EXPECT().Name().Return(mockPluginName).AnyTimes() + mgr.plugins[mockPluginName] = mockPlugin + + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan error, 1) + go func() { + done <- mgr.Start(ctx) + }() + + // Give Start() time to apply the default + time.Sleep(100 * time.Millisecond) + require.Equal(t, DefaultMetricsInterval, mgr.cfg.MetricsInterval, + "MetricsInterval should be defaulted to DefaultMetricsInterval") + + cancel() + // Start returns when context is cancelled (plugins exit) + err = <-done + if err != nil { + // Ignore conntrack-related errors in this test + if !strings.Contains(err.Error(), "failed to get conntrack instance") && + !strings.Contains(err.Error(), "failed to run conntrack GC") { + require.NoError(t, err) + } + } + }) + } +} + func TestNewManagerWithPluginStartFailure(t *testing.T) { ctl := gomock.NewController(t) defer ctl.Finish() @@ -161,7 +222,7 @@ func TestNewManagerWithPluginStartFailure(t *testing.T) { cfg := cfgPodLevelEnabled mgr := &PluginManager{ cfg: &cfg, - l: log.Logger().Named("plugin-manager"), + l: slog.Default().With("module", "plugin-manager"), plugins: make(map[string]plugin.Plugin), tel: telemetry.NewNoopTelemetry(), watcherManager: setupWatcherManagerMock(ctl), @@ -193,14 +254,14 @@ func TestNewManagerWithPluginReconcileFailure(t *testing.T) { ctl := gomock.NewController(t) defer ctl.Finish() log.SetupZapLogger(log.GetDefaultLogOpts()) - metrics.InitializeMetrics() + metrics.InitializeMetrics(slog.Default()) pluginName := "mockplugin" cfg := cfgPodLevelEnabled mgr := &PluginManager{ cfg: &cfg, - l: log.Logger().Named("plugin-manager"), + l: slog.Default().With("module", "plugin-manager"), plugins: make(map[string]plugin.Plugin), tel: telemetry.NewNoopTelemetry(), watcherManager: setupWatcherManagerMock(ctl), @@ -257,7 +318,7 @@ func TestPluginInit(t *testing.T) { } for _, tt := range tests { tt.cfg.EnabledPlugin = append(tt.cfg.EnabledPlugin, tt.pluginName) - mgr, err := NewPluginManager(&tt.cfg, tel) + mgr, err := NewPluginManager(&tt.cfg, tel, slog.Default()) require.Nil(t, err, "Expected nil but got error:%w", err) for _, plugin := range mgr.plugins { if tt.wantErr { @@ -299,7 +360,7 @@ func TestPluginStartWithoutInit(t *testing.T) { } for _, tt := range tests { tt.cfg.EnabledPlugin = append(tt.cfg.EnabledPlugin, tt.pluginName) - mgr, err := NewPluginManager(&tt.cfg, tel) + mgr, err := NewPluginManager(&tt.cfg, tel, slog.Default()) require.Nil(t, err, "Expected nil but got error:%w", err) for _, plugin := range mgr.plugins { if tt.initPlugin { @@ -387,7 +448,7 @@ func TestPluginStop(t *testing.T) { } for _, tt := range tests { tt.cfg.EnabledPlugin = append(tt.cfg.EnabledPlugin, tt.pluginName) - mgr, err := NewPluginManager(&tt.cfg, tel) + mgr, err := NewPluginManager(&tt.cfg, tel, slog.Default()) require.Nil(t, err, "Expected nil but got error:%w", err) for _, plugin := range mgr.plugins { if tt.initPlugin { @@ -422,7 +483,7 @@ func TestStopPluginManagerGracefully(t *testing.T) { cfg := cfgPodLevelEnabled mgr := &PluginManager{ cfg: &cfg, - l: log.Logger().Named("plugin-manager"), + l: slog.Default().With("module", "plugin-manager"), plugins: make(map[string]plugin.Plugin), tel: telemetry.NewNoopTelemetry(), watcherManager: setupWatcherManagerMock(ctl), @@ -462,7 +523,7 @@ func TestWatcherManagerFailure(t *testing.T) { cfg := cfgPodLevelEnabled mgr := &PluginManager{ cfg: &cfg, - l: log.Logger().Named("plugin-manager"), + l: slog.Default().With("module", "plugin-manager"), plugins: make(map[string]plugin.Plugin), tel: telemetry.NewNoopTelemetry(), watcherManager: m, diff --git a/pkg/managers/watchermanager/watchermanager.go b/pkg/managers/watchermanager/watchermanager.go index 55617db286..be945e0796 100644 --- a/pkg/managers/watchermanager/watchermanager.go +++ b/pkg/managers/watchermanager/watchermanager.go @@ -19,11 +19,11 @@ const ( DefaultRefreshRate = 30 * time.Second ) -func NewWatcherManager() *WatcherManager { +func NewWatcherManager(filterMapMaxEntries uint32) *WatcherManager { return &WatcherManager{ Watchers: []IWatcher{ endpoint.Watcher(), - apiserver.Watcher(), + apiserver.Watcher(filterMapMaxEntries), }, l: log.Logger().Named("watcher-manager"), refreshRate: DefaultRefreshRate, diff --git a/pkg/managers/watchermanager/watchermanager_test.go b/pkg/managers/watchermanager/watchermanager_test.go index 37dcf4809f..bdc8d3af04 100644 --- a/pkg/managers/watchermanager/watchermanager_test.go +++ b/pkg/managers/watchermanager/watchermanager_test.go @@ -7,6 +7,7 @@ import ( "errors" "testing" + kcfg "github.com/microsoft/retina/pkg/config" "github.com/microsoft/retina/pkg/log" mock "github.com/microsoft/retina/pkg/managers/watchermanager/mocks" "github.com/stretchr/testify/require" @@ -20,7 +21,7 @@ func TestStopWatcherManagerGracefully(t *testing.T) { ctl := gomock.NewController(t) defer ctl.Finish() log.SetupZapLogger(log.GetDefaultLogOpts()) - mgr := NewWatcherManager() + mgr := NewWatcherManager(kcfg.DefaultFilterMapMaxEntries) mockAPIServerWatcher := mock.NewMockIWatcher(ctl) mockEndpointWatcher := mock.NewMockIWatcher(ctl) @@ -56,7 +57,7 @@ func TestWatcherInitFailsGracefully(t *testing.T) { mockAPIServerWatcher := mock.NewMockIWatcher(ctl) mockEndpointWatcher := mock.NewMockIWatcher(ctl) - mgr := NewWatcherManager() + mgr := NewWatcherManager(kcfg.DefaultFilterMapMaxEntries) mgr.Watchers = []IWatcher{ mockAPIServerWatcher, mockEndpointWatcher, @@ -74,7 +75,7 @@ func TestWatcherStopWithoutStart(t *testing.T) { defer ctl.Finish() log.SetupZapLogger(log.GetDefaultLogOpts()) - mgr := NewWatcherManager() + mgr := NewWatcherManager(kcfg.DefaultFilterMapMaxEntries) err := mgr.Stop(context.Background()) require.Nil(t, err, "Expected no error when stopping watcher manager without starting it") diff --git a/pkg/metrics/interfaces.go b/pkg/metrics/interfaces.go index ddb9a07a5b..cb556e82ac 100644 --- a/pkg/metrics/interfaces.go +++ b/pkg/metrics/interfaces.go @@ -9,12 +9,18 @@ import ( //go:generate go run go.uber.org/mock/mockgen@v0.4.0 -source=interfaces.go -destination=mock_types.go -package=metrics +type MetricVec interface { + DeleteLabelValues(lvs ...string) bool +} + type CounterVec interface { + MetricVec WithLabelValues(lvs ...string) prometheus.Counter GetMetricWithLabelValues(lvs ...string) (prometheus.Counter, error) } type GaugeVec interface { + MetricVec WithLabelValues(lvs ...string) prometheus.Gauge GetMetricWithLabelValues(lvs ...string) (prometheus.Gauge, error) } diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 190ae2360f..d0f62df14b 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -3,16 +3,20 @@ package metrics import ( + "log/slog" + "github.com/microsoft/retina/pkg/exporter" - "github.com/microsoft/retina/pkg/log" "github.com/microsoft/retina/pkg/utils" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" ) // Initiates and creates the common metrics -func InitializeMetrics() { - metricsLogger = log.Logger().Named("metrics") +func InitializeMetrics(logger *slog.Logger) { + if logger == nil { + logger = slog.Default() + } + metricsLogger = logger.With("module", "metrics") if isInitialized { metricsLogger.Warn("Metrics already initialized. Exiting.") @@ -158,6 +162,49 @@ func InitializeMetrics() { utils.InterfaceName, ) + ConntrackPacketsTx = exporter.CreatePrometheusGaugeVecForMetric( + exporter.DefaultRegistry, + utils.ConntrackPacketsTxGaugeName, + ConntrackPacketTxDescription, + ) + + ConntrackPacketsRx = exporter.CreatePrometheusGaugeVecForMetric( + exporter.DefaultRegistry, + utils.ConntrackPacketsRxGaugeName, + ConntrackPacketRxDescription, + ) + + ConntrackBytesTx = exporter.CreatePrometheusGaugeVecForMetric( + exporter.DefaultRegistry, + utils.ConntrackBytesTxGaugeName, + ConntrackBytesTxDescription, + ) + + ConntrackBytesRx = exporter.CreatePrometheusGaugeVecForMetric( + exporter.DefaultRegistry, + utils.ConntrackBytesRxGaugeName, + ConntrackBytesRxDescription, + ) + + ConntrackTotalConnections = exporter.CreatePrometheusGaugeVecForMetric( + exporter.DefaultRegistry, + utils.ConntrackTotalConnectionsName, + ConntrackTotalConnectionsDescription, + ) + + ParsedPacketsCounter = exporter.CreatePrometheusCounterVecForControlPlaneMetric( + exporter.DefaultRegistry, + parsedPacketsCounterName, + parsedPacketsCounterDescription, + ) + + MetricsExpiredCounter = exporter.CreatePrometheusCounterVecForControlPlaneMetric( + exporter.DefaultRegistry, + expiredMetricsCounterName, + expiredMetricsCounterDescription, + utils.Metric, + ) + isInitialized = true metricsLogger.Info("Metrics initialized") } diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go index 10a8d8012d..26514e7283 100644 --- a/pkg/metrics/metrics_test.go +++ b/pkg/metrics/metrics_test.go @@ -4,6 +4,7 @@ package metrics import ( + "log/slog" "testing" "github.com/microsoft/retina/pkg/log" @@ -12,7 +13,7 @@ import ( func TestInitialization_FirstInit(t *testing.T) { log.SetupZapLogger(log.GetDefaultLogOpts()) - InitializeMetrics() + InitializeMetrics(slog.Default()) // All metrics should be initialized. objs := []interface{}{DropPacketsGauge, DropBytesGauge, ForwardBytesGauge, ForwardPacketsGauge, NodeConnectivityStatusGauge, NodeConnectivityLatencyGauge, PluginManagerFailedToReconcileCounter} @@ -31,7 +32,7 @@ func TestInitialization_MultipleInit(t *testing.T) { }() log.SetupZapLogger(log.GetDefaultLogOpts()) - InitializeMetrics() + InitializeMetrics(slog.Default()) // Should not panic when reinitializing. - InitializeMetrics() + InitializeMetrics(slog.Default()) } diff --git a/pkg/metrics/mock_types.go b/pkg/metrics/mock_types.go index 5d80bb22da..4dbc97c1ab 100644 --- a/pkg/metrics/mock_types.go +++ b/pkg/metrics/mock_types.go @@ -17,6 +17,47 @@ import ( gomock "go.uber.org/mock/gomock" ) +// MockMetricVec is a mock of MetricVec interface. +type MockMetricVec struct { + ctrl *gomock.Controller + recorder *MockMetricVecMockRecorder +} + +// MockMetricVecMockRecorder is the mock recorder for MockMetricVec. +type MockMetricVecMockRecorder struct { + mock *MockMetricVec +} + +// NewMockMetricVec creates a new mock instance. +func NewMockMetricVec(ctrl *gomock.Controller) *MockMetricVec { + mock := &MockMetricVec{ctrl: ctrl} + mock.recorder = &MockMetricVecMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockMetricVec) EXPECT() *MockMetricVecMockRecorder { + return m.recorder +} + +// DeleteLabelValues mocks base method. +func (m *MockMetricVec) DeleteLabelValues(lvs ...string) bool { + m.ctrl.T.Helper() + varargs := []any{} + for _, a := range lvs { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DeleteLabelValues", varargs...) + ret0, _ := ret[0].(bool) + return ret0 +} + +// DeleteLabelValues indicates an expected call of DeleteLabelValues. +func (mr *MockMetricVecMockRecorder) DeleteLabelValues(lvs ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLabelValues", reflect.TypeOf((*MockMetricVec)(nil).DeleteLabelValues), lvs...) +} + // MockCounterVec is a mock of CounterVec interface. type MockCounterVec struct { ctrl *gomock.Controller @@ -40,6 +81,24 @@ func (m *MockCounterVec) EXPECT() *MockCounterVecMockRecorder { return m.recorder } +// DeleteLabelValues mocks base method. +func (m *MockCounterVec) DeleteLabelValues(lvs ...string) bool { + m.ctrl.T.Helper() + varargs := []any{} + for _, a := range lvs { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DeleteLabelValues", varargs...) + ret0, _ := ret[0].(bool) + return ret0 +} + +// DeleteLabelValues indicates an expected call of DeleteLabelValues. +func (mr *MockCounterVecMockRecorder) DeleteLabelValues(lvs ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLabelValues", reflect.TypeOf((*MockCounterVec)(nil).DeleteLabelValues), lvs...) +} + // GetMetricWithLabelValues mocks base method. func (m *MockCounterVec) GetMetricWithLabelValues(lvs ...string) (prometheus.Counter, error) { m.ctrl.T.Helper() @@ -100,6 +159,24 @@ func (m *MockGaugeVec) EXPECT() *MockGaugeVecMockRecorder { return m.recorder } +// DeleteLabelValues mocks base method. +func (m *MockGaugeVec) DeleteLabelValues(lvs ...string) bool { + m.ctrl.T.Helper() + varargs := []any{} + for _, a := range lvs { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DeleteLabelValues", varargs...) + ret0, _ := ret[0].(bool) + return ret0 +} + +// DeleteLabelValues indicates an expected call of DeleteLabelValues. +func (mr *MockGaugeVecMockRecorder) DeleteLabelValues(lvs ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLabelValues", reflect.TypeOf((*MockGaugeVec)(nil).DeleteLabelValues), lvs...) +} + // GetMetricWithLabelValues mocks base method. func (m *MockGaugeVec) GetMetricWithLabelValues(lvs ...string) (prometheus.Gauge, error) { m.ctrl.T.Helper() diff --git a/pkg/metrics/types.go b/pkg/metrics/types.go index 562d8612e7..54f8446bb5 100644 --- a/pkg/metrics/types.go +++ b/pkg/metrics/types.go @@ -3,15 +3,17 @@ package metrics import ( - "github.com/microsoft/retina/pkg/log" + "log/slog" + "github.com/prometheus/client_golang/prometheus" - "go.uber.org/zap" ) const ( // Control plane metrics pluginManagerFailedToReconcileCounterName = "plugin_manager_failed_to_reconcile" lostEventsCounterName = "lost_events_counter" + parsedPacketsCounterName = "parsed_packets_counter" + expiredMetricsCounterName = "expired_metrics_counter" // Windows hnsStats = "windows_hns_stats" @@ -43,6 +45,15 @@ const ( // Control plane metrics pluginManagerFailedToReconcileCounterDescription = "Number of times the plugin manager failed to reconcile the plugins" lostEventsCounterDescription = "Number of events lost in control plane" + parsedPacketsCounterDescription = "Number of packets parsed by the packetparser plugin" + expiredMetricsCounterDescription = "Number of metrics expired due to lack of updates and no longer exported" + + // Conntrack metrics + ConntrackPacketTxDescription = "Number of tx packets" + ConntrackPacketRxDescription = "Number of rx packets" + ConntrackBytesTxDescription = "Number of tx bytes" + ConntrackBytesRxDescription = "Number of rx bytes" + ConntrackTotalConnectionsDescription = "Total number of connections" ) // Metric Counters @@ -78,11 +89,13 @@ var ( // Interface Stats InterfaceStatsGauge GaugeVec - metricsLogger *log.ZapLogger + metricsLogger *slog.Logger // Control Plane Metrics PluginManagerFailedToReconcileCounter CounterVec LostEventsCounter CounterVec + ParsedPacketsCounter CounterVec + MetricsExpiredCounter CounterVec // DNS Metrics. DNSRequestCounter CounterVec @@ -90,6 +103,13 @@ var ( InfinibandStatsGauge GaugeVec InfinibandStatusParamsGauge GaugeVec + + // Conntrack + ConntrackPacketsTx GaugeVec + ConntrackPacketsRx GaugeVec + ConntrackBytesTx GaugeVec + ConntrackBytesRx GaugeVec + ConntrackTotalConnections GaugeVec ) func ToPrometheusType(metric interface{}) prometheus.Collector { @@ -102,7 +122,9 @@ func ToPrometheusType(metric interface{}) prometheus.Collector { case CounterVec: return m.(*prometheus.CounterVec) default: - metricsLogger.Error("error converting unknown metric type", zap.Any("metric", m)) + if metricsLogger != nil { + metricsLogger.Error("error converting unknown metric type", slog.Any("metric", m)) + } return nil } } diff --git a/pkg/module/metrics/basemetricsobject.go b/pkg/module/metrics/basemetricsobject.go index 19f0b9e675..0b4bb7b286 100644 --- a/pkg/module/metrics/basemetricsobject.go +++ b/pkg/module/metrics/basemetricsobject.go @@ -3,29 +3,186 @@ package metrics import ( + "context" + "strings" + "sync" + "time" + api "github.com/microsoft/retina/crd/api/v1alpha1" "github.com/microsoft/retina/pkg/log" + "go.uber.org/zap" ) +type expireFn func(lbs []string) bool + +type updated struct { + t time.Time + lbs []string +} + +//go:generate go run go.uber.org/mock/mockgen@v0.4.0 -source=basemetricsobject.go -destination=mock_basemetricsobject.go -package=metrics +type baseMetricInterface interface { + // This func is used to clean up any resources used by the base metric object + clean() + isAdvanced() bool + sourceCtx() ContextOptionsInterface + destinationCtx() ContextOptionsInterface + additionalLabels() []string + isLocalContext() bool + // This func is used to track updates to the metric labels. It is called by the child metric object whenever the metric is updated + updated(lbs []string) + getLogger() *log.ZapLogger + // Returns the full set of tracked metric labels, this is expensive so should only be used for testing and debugging purposes + trackedMetricLabels() [][]string +} + type baseMetricObject struct { + *sync.RWMutex advEnable bool contextMode enrichmentContext ctxOptions *api.MetricsContextOptions srcCtx ContextOptionsInterface dstCtx ContextOptionsInterface l *log.ZapLogger + lastUpdated map[string]updated + expireFn expireFn + cancelFn context.CancelFunc + ctx context.Context +} + +func (b *baseMetricObject) additionalLabels() []string { + if b.ctxOptions == nil { + return nil + } + + return b.ctxOptions.AdditionalLabels +} + +func (b *baseMetricObject) trackedMetricLabels() [][]string { + if b.expireFn == nil { + return nil + } + + b.RLock() + defer b.RUnlock() + + labels := make([][]string, 0, len(b.lastUpdated)) + for _, u := range b.lastUpdated { + labels = append(labels, u.lbs) + } + + return labels +} + +func (b *baseMetricObject) isAdvanced() bool { + return b.advEnable +} + +func (b *baseMetricObject) sourceCtx() ContextOptionsInterface { + return b.srcCtx +} + +func (b *baseMetricObject) destinationCtx() ContextOptionsInterface { + return b.dstCtx +} + +func (b *baseMetricObject) getLogger() *log.ZapLogger { + return b.l } -func newBaseMetricsObject(ctxOptions *api.MetricsContextOptions, fl *log.ZapLogger, isLocalContext enrichmentContext) baseMetricObject { +func (b *baseMetricObject) expire(ttl time.Duration) int { + if b.expireFn == nil { + return 0 + } + + b.Lock() + defer b.Unlock() + + var expired int + n := make(map[string]updated) + + for k, u := range b.lastUpdated { + if time.Since(u.t) >= ttl { + d := b.expireFn(u.lbs) + if d { + expired++ + } + } else { + n[k] = u + } + } + + b.lastUpdated = n + + return expired +} + +func (b *baseMetricObject) updated(lbs []string) { + // no expiration function is defined, so we don't need to track updates + if b.expireFn == nil { + return + } + + k := strings.Join(lbs, "") + + b.Lock() + defer b.Unlock() + + b.lastUpdated[k] = updated{ + t: time.Now(), + lbs: lbs, + } +} + +func newBaseMetricsObject(ctxOptions *api.MetricsContextOptions, fl *log.ZapLogger, isLocalContext enrichmentContext, expire expireFn, ttl time.Duration) *baseMetricObject { + expireOrInfiniteTTL := expire + if ttl <= 0 { + // infinite TTL, so make sure the expiration function is unset + expireOrInfiniteTTL = nil + } + b := baseMetricObject{ advEnable: ctxOptions.IsAdvanced(), ctxOptions: ctxOptions, l: fl, contextMode: isLocalContext, + expireFn: expireOrInfiniteTTL, + } + + if expireOrInfiniteTTL != nil { + // only initialize these if we have a valid expiration function to save some memory + b.RWMutex = &sync.RWMutex{} + b.lastUpdated = make(map[string]updated) + ctx, cancel := context.WithCancel(context.Background()) + b.ctx = ctx + b.cancelFn = cancel + b.l.Info( + "Starting metric expiration routine: "+ctxOptions.MetricName, + zap.Duration("ttl", ttl), + ) + go func() { + ticker := time.NewTicker(ttl) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + b.l.Info("Stopping metric expiration routine: " + b.ctxOptions.MetricName) + return + case t := <-ticker.C: + b.l.Debug("Expiring metrics: " + b.ctxOptions.MetricName) + n := b.expire(ttl) + b.l.Debug( + "Metric expiration finished: "+b.ctxOptions.MetricName, + zap.Time("next_expiration", t.Add(ttl)), + zap.Int("expired", n), + ) + } + } + }() } b.populateCtxOptions(ctxOptions) - return b + return &b } func (b *baseMetricObject) populateCtxOptions(ctxOptions *api.MetricsContextOptions) { @@ -51,3 +208,9 @@ func (b *baseMetricObject) populateCtxOptions(ctxOptions *api.MetricsContextOpti func (b *baseMetricObject) isLocalContext() bool { return b.contextMode == localContext } + +func (b *baseMetricObject) clean() { + if b.cancelFn != nil { + b.cancelFn() + } +} diff --git a/pkg/module/metrics/basemetricsobject_test.go b/pkg/module/metrics/basemetricsobject_test.go new file mode 100644 index 0000000000..6a33cd39b1 --- /dev/null +++ b/pkg/module/metrics/basemetricsobject_test.go @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package metrics + +import ( + "runtime" + "slices" + "testing" + "time" + + api "github.com/microsoft/retina/crd/api/v1alpha1" + "github.com/microsoft/retina/pkg/log" +) + +func TestBaseMetricObject(t *testing.T) { + l, err := log.SetupZapLogger(log.GetDefaultLogOpts()) + if err != nil { + t.Fatalf("failed to set up logger: %v", err) + } + + tests := []struct { + name string + ttl time.Duration + trackMetrics bool + }{ + { + name: "test base metric object zero ttl", + ttl: 0, + trackMetrics: false, + }, + { + name: "test base metric object negative ttl", + ttl: -time.Millisecond, + trackMetrics: false, + }, + { + name: "test base metric object positive ttl", + ttl: time.Millisecond, + trackMetrics: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + before := runtime.NumGoroutine() + expireCalled := new([]string) + b := newBaseMetricsObject( + &api.MetricsContextOptions{ + MetricName: "test_metric", + }, + l, + localContext, + func(lbs []string) bool { + *expireCalled = lbs + return true + }, + tt.ttl, + ) + + testLabels := []string{"test"} + b.updated(testLabels) + + metrics := len(b.trackedMetricLabels()) + if tt.trackMetrics { + if metrics != 1 { + t.Errorf("expected 1 tracked metric label, got %d", metrics) + } + } else { + if metrics != 0 { + t.Errorf("expected 0 tracked metric labels, got %d", metrics) + } + } + + // If we have a positive TTL, we should see the expire function get called after the TTL has passed + if tt.ttl > 0 { + time.Sleep(tt.ttl + time.Millisecond*100) + if !slices.Equal(*expireCalled, testLabels) { + t.Errorf("expected expire to be called with %v, got %v", testLabels, *expireCalled) + } + metrics = len(b.trackedMetricLabels()) + if metrics != 0 { + t.Errorf("expected 0 tracked metric labels after expiration, got %d", metrics) + } + } else if len(*expireCalled) != 0 { + t.Errorf("expected expire to not be called, but got %v", *expireCalled) + } + + b.clean() + if b.expireFn != nil { + <-b.ctx.Done() + } + + // Wait for any goroutines to exit after clean is called + if tt.trackMetrics { + time.Sleep(tt.ttl + time.Millisecond*100) + } + + after := runtime.NumGoroutine() + if after != before { + t.Errorf("expected number of goroutines to be the same as before, expected %d, got %d", before, after) + } + }) + } +} diff --git a/pkg/module/metrics/dns.go b/pkg/module/metrics/dns.go index 003d14cb3d..96f4886951 100644 --- a/pkg/module/metrics/dns.go +++ b/pkg/module/metrics/dns.go @@ -7,6 +7,7 @@ import ( "fmt" "strconv" "strings" + "time" v1 "github.com/cilium/cilium/api/v1/flow" api "github.com/microsoft/retina/crd/api/v1alpha1" @@ -30,21 +31,21 @@ var ( ) type DNSMetrics struct { - baseMetricObject + baseMetricInterface dnsMetrics metricsinit.CounterVec metricName string } -func NewDNSMetrics(ctxOptions *api.MetricsContextOptions, fl *log.ZapLogger, isLocalContext enrichmentContext) *DNSMetrics { +func NewDNSMetrics(ctxOptions *api.MetricsContextOptions, fl *log.ZapLogger, isLocalContext enrichmentContext, ttl time.Duration) *DNSMetrics { if ctxOptions == nil || !strings.Contains(strings.ToLower(ctxOptions.MetricName), "dns") { return nil } fl = fl.Named("dns-metricsmodule") fl.Info("Creating DNS count metrics", zap.Any("options", ctxOptions)) - return &DNSMetrics{ - baseMetricObject: newBaseMetricsObject(ctxOptions, fl, isLocalContext), - } + d := &DNSMetrics{} + d.baseMetricInterface = newBaseMetricsObject(ctxOptions, fl, isLocalContext, d.expire, ttl) + return d } func (d *DNSMetrics) Init(metricName string) { @@ -71,14 +72,14 @@ func (d *DNSMetrics) Init(metricName string) { func (d *DNSMetrics) getRequestLabels() []string { labels := utils.DNSRequestLabels - if d.srcCtx != nil { - labels = append(labels, d.srcCtx.getLabels()...) - d.l.Info("src labels", zap.Any("labels", labels)) + if d.sourceCtx() != nil { + labels = append(labels, d.sourceCtx().getLabels()...) + d.getLogger().Info("src labels", zap.Any("labels", labels)) } - if d.dstCtx != nil { - labels = append(labels, d.dstCtx.getLabels()...) - d.l.Info("dst labels", zap.Any("labels", labels)) + if d.destinationCtx() != nil { + labels = append(labels, d.destinationCtx().getLabels()...) + d.getLogger().Info("dst labels", zap.Any("labels", labels)) } return labels @@ -86,14 +87,14 @@ func (d *DNSMetrics) getRequestLabels() []string { func (d *DNSMetrics) getResponseLabels() []string { labels := utils.DNSResponseLabels - if d.srcCtx != nil { - labels = append(labels, d.srcCtx.getLabels()...) - d.l.Info("src labels", zap.Any("labels", labels)) + if d.sourceCtx() != nil { + labels = append(labels, d.sourceCtx().getLabels()...) + d.getLogger().Info("src labels", zap.Any("labels", labels)) } - if d.dstCtx != nil { - labels = append(labels, d.dstCtx.getLabels()...) - d.l.Info("dst labels", zap.Any("labels", labels)) + if d.destinationCtx() != nil { + labels = append(labels, d.destinationCtx().getLabels()...) + d.getLogger().Info("dst labels", zap.Any("labels", labels)) } return labels @@ -141,18 +142,15 @@ func (d *DNSMetrics) responseValues(flow *v1.Flow) []string { func (d *DNSMetrics) getLabelsForProcessFlow(flow *v1.Flow) ([]string, error) { var labels []string // Get the DNS query type - meta := utils.RetinaMetadata{} - if err := flow.GetExtensions().UnmarshalTo(&meta); err != nil { - return labels, errors.Wrapf(err, "failed to unmarshal flow extensions") - } - switch meta.GetDnsType() { + _, dnsType, _ := utils.GetDNS(flow) + switch dnsType { case utils.DNSType_QUERY: labels = d.requestValues(flow) case utils.DNSType_RESPONSE: labels = d.responseValues(flow) case utils.DNSType_UNKNOWN: default: - return labels, errors.Errorf("invalid DNS type %d", int32(meta.GetDnsType())) + return labels, errors.Errorf("invalid DNS type %d", int32(dnsType)) } return labels, nil } @@ -175,7 +173,7 @@ func (d *DNSMetrics) ProcessFlow(flow *v1.Flow) { labels, err := d.getLabelsForProcessFlow(flow) if err != nil { - d.l.Error("Failed to get labels for process flow", zap.Error(err)) + d.getLogger().Error("Failed to get labels for process flow", zap.Error(err)) return } @@ -183,33 +181,33 @@ func (d *DNSMetrics) ProcessFlow(flow *v1.Flow) { return } - if d.srcCtx != nil { - srcLabels := d.srcCtx.getValues(flow) + if d.sourceCtx() != nil { + srcLabels := d.sourceCtx().getValues(flow) if len(srcLabels) > 0 { labels = append(labels, srcLabels...) } } - if d.dstCtx != nil { - dstLabels := d.dstCtx.getValues(flow) + if d.destinationCtx() != nil { + dstLabels := d.destinationCtx().getValues(flow) if len(dstLabels) > 0 { labels = append(labels, dstLabels...) } } - d.dnsMetrics.WithLabelValues(labels...).Inc() - d.l.Debug("Update dns metric in remote ctx", zap.Any("metric", d.dnsMetrics), zap.Any("labels", labels)) + d.update(labels) + d.getLogger().Debug("Update dns metric in remote ctx", zap.Any("metric", d.dnsMetrics), zap.Any("labels", labels)) } func (d *DNSMetrics) processLocalCtxFlow(flow *v1.Flow) { - labelValuesMap := d.srcCtx.getLocalCtxValues(flow) + labelValuesMap := d.sourceCtx().getLocalCtxValues(flow) if labelValuesMap == nil { return } labels, err := d.getLabelsForProcessFlow(flow) if err != nil { - d.l.Error("Failed to get labels for process flow", zap.Error(err)) + d.getLogger().Error("Failed to get labels for process flow", zap.Error(err)) return } @@ -233,10 +231,27 @@ func (d *DNSMetrics) processLocalCtxFlow(flow *v1.Flow) { } else { return } + d.update(labels) + d.getLogger().Debug("Update dns metric in local ctx", zap.Any("metric", d.dnsMetrics), zap.Any("labels", labels)) +} + +func (d *DNSMetrics) expire(labels []string) bool { + var del bool + if d.dnsMetrics != nil { + del = d.dnsMetrics.DeleteLabelValues(labels...) + if del { + metricsinit.MetricsExpiredCounter.WithLabelValues(d.metricName).Inc() + } + } + return del +} + +func (d *DNSMetrics) update(labels []string) { d.dnsMetrics.WithLabelValues(labels...).Inc() - d.l.Debug("Update dns metric in local ctx", zap.Any("metric", d.dnsMetrics), zap.Any("labels", labels)) + d.updated(labels) } func (d *DNSMetrics) Clean() { exporter.UnregisterMetric(exporter.AdvancedRegistry, metricsinit.ToPrometheusType(d.dnsMetrics)) + d.clean() } diff --git a/pkg/module/metrics/dns_test.go b/pkg/module/metrics/dns_test.go index 7e15fedf4a..7bf582139c 100644 --- a/pkg/module/metrics/dns_test.go +++ b/pkg/module/metrics/dns_test.go @@ -4,12 +4,15 @@ package metrics import ( + "log/slog" "reflect" "testing" + "time" "github.com/cilium/cilium/api/v1/flow" + "github.com/microsoft/retina/crd/api/v1alpha1" "github.com/microsoft/retina/pkg/log" - "github.com/microsoft/retina/pkg/metrics" + metricsinit "github.com/microsoft/retina/pkg/metrics" "github.com/microsoft/retina/pkg/utils" "github.com/prometheus/client_golang/prometheus" "go.uber.org/mock/gomock" @@ -35,7 +38,7 @@ func TestGetLabels(t *testing.T) { name: "basic context request labels", want: utils.DNSRequestLabels, d: &DNSMetrics{ - baseMetricObject: baseMetricObject{ + baseMetricInterface: &baseMetricObject{ srcCtx: nil, dstCtx: nil, }, @@ -46,7 +49,7 @@ func TestGetLabels(t *testing.T) { name: "basic context response labels", want: utils.DNSResponseLabels, d: &DNSMetrics{ - baseMetricObject: baseMetricObject{ + baseMetricInterface: &baseMetricObject{ srcCtx: nil, dstCtx: nil, }, @@ -57,7 +60,7 @@ func TestGetLabels(t *testing.T) { name: "local context request labels", want: append(utils.DNSRequestLabels, "ip", "namespace", "podname", "workload_kind", "workload_name", "service", "port"), d: &DNSMetrics{ - baseMetricObject: baseMetricObject{ + baseMetricInterface: &baseMetricObject{ srcCtx: &ContextOptions{ option: localCtx, IP: true, @@ -94,19 +97,19 @@ func TestGetLabels(t *testing.T) { func TestValues(t *testing.T) { testR := &flow.Flow{} - metaR := &utils.RetinaMetadata{} - utils.AddDNSInfo(testR, metaR, "R", 0, "bing.com", []string{"A"}, 1, []string{"1.1.1.1"}) - utils.AddRetinaMetadata(testR, metaR) + extR := utils.NewExtensions() + utils.AddDNSInfo(testR, extR, "R", 0, "bing.com", []string{"A"}, 1, []string{"1.1.1.1"}) + utils.SetExtensions(testR, extR) testQ := &flow.Flow{} - metaQ := &utils.RetinaMetadata{} - utils.AddDNSInfo(testQ, metaQ, "Q", 0, "bing.com", []string{"A"}, 0, []string{}) - utils.AddRetinaMetadata(testQ, metaQ) + extQ := utils.NewExtensions() + utils.AddDNSInfo(testQ, extQ, "Q", 0, "bing.com", []string{"A"}, 0, []string{}) + utils.SetExtensions(testQ, extQ) testU := &flow.Flow{} - metaU := &utils.RetinaMetadata{} - utils.AddDNSInfo(testU, metaU, "U", 0, "bing.com", []string{"A"}, 0, []string{}) - utils.AddRetinaMetadata(testU, metaU) + extU := utils.NewExtensions() + utils.AddDNSInfo(testU, extU, "U", 0, "bing.com", []string{"A"}, 0, []string{}) + utils.SetExtensions(testU, extU) tests := []struct { name string @@ -202,114 +205,134 @@ func TestProcessLocalCtx(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - testR := &flow.Flow{} - metaR := &utils.RetinaMetadata{} - utils.AddDNSInfo(testR, metaR, "R", 0, "bing.com", []string{"A"}, 1, []string{"1.1.1.1"}) - utils.AddRetinaMetadata(testR, metaR) - - testIngress := &flow.Flow{TrafficDirection: flow.TrafficDirection_INGRESS} - metaIngress := &utils.RetinaMetadata{} - utils.AddDNSInfo(testIngress, metaIngress, "R", 0, "bing.com", []string{"A"}, 1, []string{"1.1.1.1"}) - utils.AddRetinaMetadata(testIngress, metaIngress) - - testEgress := &flow.Flow{TrafficDirection: flow.TrafficDirection_EGRESS} - metaEgress := &utils.RetinaMetadata{} - utils.AddDNSInfo(testEgress, metaEgress, "R", 0, "bing.com", []string{"A"}, 1, []string{"1.1.1.1"}) - utils.AddRetinaMetadata(testEgress, metaEgress) - tests := []struct { name string - d *DNSMetrics input *flow.Flow - output map[string][]string expectedLabels []string metricsUpdate bool }{ { - name: "No context labels", - input: nil, - output: nil, - d: &DNSMetrics{}, + name: "No context labels", + input: &flow.Flow{ + Verdict: utils.Verdict_DNS, + }, metricsUpdate: false, }, { - name: "Only ingress labels", - input: testR, - output: map[string][]string{ - ingress: {"PodA", "NamespaceA"}, - egress: nil, - }, - d: &DNSMetrics{ - metricName: utils.DNSResponseCounterName, - baseMetricObject: baseMetricObject{ - l: l, + name: "Only ingress labels", + input: &flow.Flow{ + Verdict: utils.Verdict_DNS, + TrafficDirection: flow.TrafficDirection_INGRESS, + Destination: &flow.Endpoint{ + PodName: "PodB", + Namespace: "NamespaceB", }, }, - expectedLabels: []string{"NOERROR", "A", "bing.com", "1.1.1.1", "1", "PodA", "NamespaceA"}, + expectedLabels: []string{"NOERROR", "A", "bing.com", "1.1.1.1", "1", "NamespaceB", "PodB"}, metricsUpdate: true, }, { - name: "Only egress labels", - input: testR, - output: map[string][]string{ - ingress: nil, - egress: {"PodA", "NamespaceA"}, - }, - d: &DNSMetrics{ - metricName: utils.DNSResponseCounterName, - baseMetricObject: baseMetricObject{ - l: l, + name: "Only egress labels", + input: &flow.Flow{ + Verdict: utils.Verdict_DNS, + TrafficDirection: flow.TrafficDirection_EGRESS, + Source: &flow.Endpoint{ + PodName: "PodA", + Namespace: "NamespaceA", }, }, - expectedLabels: []string{"NOERROR", "A", "bing.com", "1.1.1.1", "1", "PodA", "NamespaceA"}, + expectedLabels: []string{"NOERROR", "A", "bing.com", "1.1.1.1", "1", "NamespaceA", "PodA"}, metricsUpdate: true, }, { - name: "Both ingress and egress labels with ingress flow", - input: testIngress, - output: map[string][]string{ - ingress: {"PodA", "NamespaceA"}, - egress: {"PodB", "NamespaceB"}, - }, - d: &DNSMetrics{ - metricName: utils.DNSResponseCounterName, - baseMetricObject: baseMetricObject{ - l: l, + name: "Both ingress and egress labels with ingress flow", + input: &flow.Flow{ + Verdict: utils.Verdict_DNS, + TrafficDirection: flow.TrafficDirection_INGRESS, + Destination: &flow.Endpoint{ + PodName: "PodA", + Namespace: "NamespaceA", + }, + Source: &flow.Endpoint{ + PodName: "PodB", + Namespace: "NamespaceB", }, }, - expectedLabels: []string{"NOERROR", "A", "bing.com", "1.1.1.1", "1", "PodA", "NamespaceA"}, + expectedLabels: []string{"NOERROR", "A", "bing.com", "1.1.1.1", "1", "NamespaceA", "PodA"}, metricsUpdate: true, }, { - name: "Both ingress and egress labels with egress flow", - input: testEgress, - output: map[string][]string{ - ingress: {"PodA", "NamespaceA"}, - egress: {"PodB", "NamespaceB"}, - }, - d: &DNSMetrics{ - metricName: utils.DNSResponseCounterName, - baseMetricObject: baseMetricObject{ - l: l, + name: "Both source and destination labels with egress flow", + input: &flow.Flow{ + Verdict: utils.Verdict_DNS, + TrafficDirection: flow.TrafficDirection_EGRESS, + Source: &flow.Endpoint{ + PodName: "PodB", + Namespace: "NamespaceB", + }, + Destination: &flow.Endpoint{ + PodName: "PodA", + Namespace: "NamespaceA", }, }, - expectedLabels: []string{"NOERROR", "A", "bing.com", "1.1.1.1", "1", "PodB", "NamespaceB"}, + expectedLabels: []string{"NOERROR", "A", "bing.com", "1.1.1.1", "1", "NamespaceB", "PodB"}, metricsUpdate: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - m := NewMockContextOptionsInterface(ctrl) //nolint:typecheck - m.EXPECT().getLocalCtxValues(tt.input).Return(tt.output).Times(1) + ext := utils.NewExtensions() + utils.AddDNSInfo(tt.input, ext, "R", 0, "bing.com", []string{"A"}, 1, []string{"1.1.1.1"}) + utils.SetExtensions(tt.input, ext) + + mockCV := metricsinit.NewMockCounterVec(ctrl) + if tt.metricsUpdate { + mockCV.EXPECT().WithLabelValues(tt.expectedLabels).Return(c).Times(1) + } + + ctxOptions := &v1alpha1.MetricsContextOptions{ + MetricName: utils.DNSResponseCounterName, + SourceLabels: []string{ + podCtxOption, + namespaceCtxOption, + }, + } + d := NewDNSMetrics(ctxOptions, l, localContext, 0) + d.dnsMetrics = mockCV + + d.ProcessFlow(tt.input) + + // There should be no tracked metrics when TTL is infinite + assert.Equal(t, 0, len(d.trackedMetricLabels()), "there should be no tracked metrics when TTL is infinite") + + // Test TTL based expiration + metricsinit.InitializeMetrics(slog.Default()) + + // Set the TTL to something high to ensure that our call to expire is the only one that expires the metrics + d = NewDNSMetrics(ctxOptions, l, localContext, time.Minute) + d.dnsMetrics = mockCV - mockCV := metrics.NewMockCounterVec(ctrl) if tt.metricsUpdate { mockCV.EXPECT().WithLabelValues(tt.expectedLabels).Return(c).Times(1) } - tt.d.dnsMetrics = mockCV - tt.d.srcCtx = m - tt.d.processLocalCtxFlow(tt.input) + d.ProcessFlow(tt.input) + + if tt.metricsUpdate { + mockCV.EXPECT().DeleteLabelValues(tt.expectedLabels).Return(true).Times(1) + } + + for _, ls := range d.trackedMetricLabels() { + assert.Check(t, d.expire(ls), "metrics should expire successfully") + } + + // Test that clean calls the base object + baseMetricObjectMock := NewMockbaseMetricInterface(ctrl) + d.baseMetricInterface = baseMetricObjectMock + + baseMetricObjectMock.EXPECT().clean().Times(1) + + d.Clean() }) } } diff --git a/pkg/module/metrics/drops.go b/pkg/module/metrics/drops.go index cf5db7d7a1..f1ed9bc345 100644 --- a/pkg/module/metrics/drops.go +++ b/pkg/module/metrics/drops.go @@ -5,6 +5,7 @@ package metrics import ( "strings" + "time" v1 "github.com/cilium/cilium/api/v1/flow" api "github.com/microsoft/retina/crd/api/v1alpha1" @@ -24,21 +25,21 @@ const ( ) type DropCountMetrics struct { - baseMetricObject + baseMetricInterface dropMetric metrics.GaugeVec metricName string } -func NewDropCountMetrics(ctxOptions *api.MetricsContextOptions, fl *log.ZapLogger, isLocalContext enrichmentContext) *DropCountMetrics { +func NewDropCountMetrics(ctxOptions *api.MetricsContextOptions, fl *log.ZapLogger, isLocalContext enrichmentContext, ttl time.Duration) *DropCountMetrics { if ctxOptions == nil || !strings.Contains(strings.ToLower(ctxOptions.MetricName), "drop") { return nil } fl = fl.Named("dropreason-metricsmodule") fl.Info("Creating drop count metrics", zap.Any("options", ctxOptions)) - return &DropCountMetrics{ - baseMetricObject: newBaseMetricsObject(ctxOptions, fl, isLocalContext), - } + d := &DropCountMetrics{} + d.baseMetricInterface = newBaseMetricsObject(ctxOptions, fl, isLocalContext, d.expire, ttl) + return d } func (d *DropCountMetrics) Init(metricName string) { @@ -56,7 +57,7 @@ func (d *DropCountMetrics) Init(metricName string) { TotalDropBytesDesc, d.getLabels()...) default: - d.l.Error("unknown metric name", zap.String("metricName", metricName)) + d.getLogger().Error("unknown metric name", zap.String("metricName", metricName)) } d.metricName = metricName } @@ -67,14 +68,14 @@ func (d *DropCountMetrics) getLabels() []string { utils.Direction, } - if d.srcCtx != nil { - labels = append(labels, d.srcCtx.getLabels()...) - d.l.Info("src labels", zap.Any("labels", labels)) + if d.sourceCtx() != nil { + labels = append(labels, d.sourceCtx().getLabels()...) + d.getLogger().Info("src labels", zap.Any("labels", labels)) } - if d.dstCtx != nil { - labels = append(labels, d.dstCtx.getLabels()...) - d.l.Info("dst labels", zap.Any("labels", labels)) + if d.destinationCtx() != nil { + labels = append(labels, d.destinationCtx().getLabels()...) + d.getLogger().Info("dst labels", zap.Any("labels", labels)) } // No additional context options @@ -84,6 +85,7 @@ func (d *DropCountMetrics) getLabels() []string { func (d *DropCountMetrics) Clean() { exporter.UnregisterMetric(exporter.AdvancedRegistry, metrics.ToPrometheusType(d.dropMetric)) + d.clean() } // TODO: update ProcessFlow with bytes metrics. We are only accounting for count. @@ -111,20 +113,20 @@ func (d *DropCountMetrics) ProcessFlow(flow *v1.Flow) { flow.TrafficDirection.String(), } - if !d.advEnable { + if !d.isAdvanced() { d.update(flow, labels) return } - if d.srcCtx != nil { - srcLabels := d.srcCtx.getValues(flow) + if d.sourceCtx() != nil { + srcLabels := d.sourceCtx().getValues(flow) if len(srcLabels) > 0 { labels = append(labels, srcLabels...) } } - if d.dstCtx != nil { - dstLabel := d.dstCtx.getValues(flow) + if d.destinationCtx() != nil { + dstLabel := d.destinationCtx().getValues(flow) if len(dstLabel) > 0 { labels = append(labels, dstLabel...) } @@ -133,11 +135,11 @@ func (d *DropCountMetrics) ProcessFlow(flow *v1.Flow) { // No additional context options d.update(flow, labels) - d.l.Debug("drop count metric is added", zap.Any("labels", labels)) + d.getLogger().Debug("drop count metric is added", zap.Any("labels", labels)) } func (d *DropCountMetrics) processLocalCtxFlow(flow *v1.Flow) { - labelValuesMap := d.srcCtx.getLocalCtxValues(flow) + labelValuesMap := d.sourceCtx().getLocalCtxValues(flow) if labelValuesMap == nil { return } @@ -149,7 +151,7 @@ func (d *DropCountMetrics) processLocalCtxFlow(flow *v1.Flow) { labels = append(labels, dropReason, ingress) labels = append(labels, labelValuesMap[ingress]...) d.update(flow, labels) - d.l.Debug("drop count metric is added in INGRESS in local ctx", zap.Any("labels", labels)) + d.getLogger().Debug("drop count metric is added in INGRESS in local ctx", zap.Any("labels", labels)) } if l := len(labelValuesMap[egress]); l > 0 { @@ -157,15 +159,32 @@ func (d *DropCountMetrics) processLocalCtxFlow(flow *v1.Flow) { labels = append(labels, dropReason, egress) labels = append(labels, labelValuesMap[egress]...) d.update(flow, labels) - d.l.Debug("drop count metric is added in EGRESS in local ctx", zap.Any("labels", labels)) + d.getLogger().Debug("drop count metric is added in EGRESS in local ctx", zap.Any("labels", labels)) + } +} + +func (d *DropCountMetrics) expire(labels []string) bool { + var del bool + if d.dropMetric != nil { + del = d.dropMetric.DeleteLabelValues(labels...) + if del { + metrics.MetricsExpiredCounter.WithLabelValues(d.metricName).Inc() + } } + return del } func (d *DropCountMetrics) update(fl *v1.Flow, labels []string) { + var updated bool switch d.metricName { case utils.DroppedPacketsGaugeName: + updated = true d.dropMetric.WithLabelValues(labels...).Inc() case utils.DropBytesGaugeName: + updated = true d.dropMetric.WithLabelValues(labels...).Add(float64(utils.PacketSize(fl))) } + if updated { + d.updated(labels) + } } diff --git a/pkg/module/metrics/drops_test.go b/pkg/module/metrics/drops_test.go index bc2e9f707f..b6774fae04 100644 --- a/pkg/module/metrics/drops_test.go +++ b/pkg/module/metrics/drops_test.go @@ -6,6 +6,7 @@ package metrics import ( "testing" + "time" "github.com/cilium/cilium/api/v1/flow" "github.com/microsoft/retina/crd/api/v1alpha1" @@ -15,6 +16,7 @@ import ( "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "go.uber.org/zap" + "log/slog" ) func TestNewDrop(t *testing.T) { @@ -64,6 +66,7 @@ func TestNewDrop(t *testing.T) { }, exepectedLabels: []string{"reason", "direction"}, metricCall: 1, + trackedMetrics: 1, }, { name: "plain opts dropped verdict nil flow", @@ -87,8 +90,9 @@ func TestNewDrop(t *testing.T) { "reason", "direction", }, - metricCall: 1, - nilObj: true, + metricCall: 1, + trackedMetrics: 1, + nilObj: true, }, { name: "source opts 1", @@ -111,7 +115,8 @@ func TestNewDrop(t *testing.T) { "source_service", "source_port", }, - metricCall: 1, + metricCall: 1, + trackedMetrics: 1, }, { name: "dest opts 1", @@ -134,7 +139,8 @@ func TestNewDrop(t *testing.T) { "destination_service", "destination_port", }, - metricCall: 1, + metricCall: 1, + trackedMetrics: 1, }, { name: "source opts with flow", @@ -158,7 +164,8 @@ func TestNewDrop(t *testing.T) { "source_service", "source_port", }, - metricCall: 1, + metricCall: 1, + trackedMetrics: 1, }, { name: "forward source opts with flow", @@ -182,8 +189,9 @@ func TestNewDrop(t *testing.T) { "source_service", "source_port", }, - metricCall: 1, - nilObj: true, + metricCall: 1, + trackedMetrics: 1, + nilObj: true, }, { name: "drop source opts with flow in localcontext", @@ -207,9 +215,10 @@ func TestNewDrop(t *testing.T) { "service", "port", }, - metricCall: 1, - nilObj: false, - localContext: localContext, + metricCall: 1, + trackedMetrics: 1, + nilObj: false, + localContext: localContext, }, { name: "drop source opts with destination flow in localcontext", @@ -233,9 +242,10 @@ func TestNewDrop(t *testing.T) { "service", "port", }, - metricCall: 1, - nilObj: false, - localContext: localContext, + metricCall: 1, + trackedMetrics: 1, + nilObj: false, + localContext: localContext, }, { name: "drop source opts with source and destination flow in localcontext", @@ -260,9 +270,10 @@ func TestNewDrop(t *testing.T) { "service", "port", }, - metricCall: 2, - nilObj: false, - localContext: localContext, + metricCall: 2, + trackedMetrics: 2, + nilObj: false, + localContext: localContext, }, } @@ -270,7 +281,7 @@ func TestNewDrop(t *testing.T) { for _, metricName := range []string{"drop_count", "drop_bytes"} { log.Logger().Info("Running test name", zap.String("name", tc.name), zap.String("metricName", metricName)) ctrl := gomock.NewController(t) - f := NewDropCountMetrics(tc.opts, log.Logger(), tc.localContext) + f := NewDropCountMetrics(tc.opts, log.Logger(), tc.localContext, time.Duration(0)) if tc.nilObj { assert.Nil(t, f, "drop metrics should be nil Test Name: %s", tc.name) continue @@ -288,11 +299,40 @@ func TestNewDrop(t *testing.T) { dropMock.EXPECT().WithLabelValues(gomock.Any()).Return(testmetric).Times(tc.metricCall) - assert.Equal(t, f.advEnable, tc.checkIsAdvance, "advance metrics options should be equal Test Name: %s", tc.name) + assert.Equal(t, f.isAdvanced(), tc.checkIsAdvance, "advance metrics options should be equal Test Name: %s", tc.name) assert.Equal(t, tc.exepectedLabels, f.getLabels(), "labels should be equal Test Name: %s", tc.name) f.metricName = metricName f.ProcessFlow(tc.f) + + // There should be no tracked metrics when TTL is infinite + assert.Equal(t, 0, len(f.trackedMetricLabels()), "there should be no tracked metrics when TTL is infinite Test Name: %s", tc.name) + + // Test TTL based expiration + metricsinit.InitializeMetrics(slog.Default()) + + // Set the TTL to something high to ensure that our call to expire is the only one that expires the metrics + f = NewDropCountMetrics(tc.opts, log.Logger(), tc.localContext, time.Minute) + f.dropMetric = dropMock + + dropMock.EXPECT().WithLabelValues(gomock.Any()).Return(testmetric).Times(tc.metricCall) + + f.metricName = metricName + f.ProcessFlow(tc.f) + + dropMock.EXPECT().DeleteLabelValues(gomock.Any()).Return(true).Times(tc.trackedMetrics) + + for _, ls := range f.trackedMetricLabels() { + assert.True(t, f.expire(ls), "metric should expire successfully Test Name: %s", tc.name) + } + + // Test that clean calls the base object + baseMetricObjectMock := NewMockbaseMetricInterface(ctrl) + f.baseMetricInterface = baseMetricObjectMock + + baseMetricObjectMock.EXPECT().clean().Times(1) + + f.Clean() ctrl.Finish() } } diff --git a/pkg/module/metrics/forward.go b/pkg/module/metrics/forward.go index 7263ed0b7c..e52e621779 100644 --- a/pkg/module/metrics/forward.go +++ b/pkg/module/metrics/forward.go @@ -4,7 +4,10 @@ package metrics import ( + "slices" + "strconv" "strings" + "time" v1 "github.com/cilium/cilium/api/v1/flow" api "github.com/microsoft/retina/crd/api/v1alpha1" @@ -26,22 +29,22 @@ const ( ) type ForwardMetrics struct { - baseMetricObject + baseMetricInterface forwardMetric metricsinit.GaugeVec // bytesMetric metricsinit.IGaugeVec metricName string } -func NewForwardCountMetrics(ctxOptions *api.MetricsContextOptions, fl *log.ZapLogger, isLocalContext enrichmentContext) *ForwardMetrics { +func NewForwardCountMetrics(ctxOptions *api.MetricsContextOptions, fl *log.ZapLogger, isLocalContext enrichmentContext, ttl time.Duration) *ForwardMetrics { if ctxOptions == nil || !strings.Contains(strings.ToLower(ctxOptions.MetricName), "forward") { return nil } l := fl.Named("forward-metricsmodule") l.Info("Creating forward count metrics", zap.Any("options", ctxOptions)) - return &ForwardMetrics{ - baseMetricObject: newBaseMetricsObject(ctxOptions, fl, isLocalContext), - } + fm := ForwardMetrics{} + fm.baseMetricInterface = newBaseMetricsObject(ctxOptions, fl, isLocalContext, fm.expire, ttl) + return &fm } func (f *ForwardMetrics) Init(metricName string) { @@ -59,7 +62,7 @@ func (f *ForwardMetrics) Init(metricName string) { TotalBytesDesc, f.getLabels()...) default: - f.l.Error("unknown metric name", zap.String("name", metricName)) + f.getLogger().Error("unknown metric name", zap.String("name", metricName)) } f.metricName = metricName } @@ -69,27 +72,30 @@ func (f *ForwardMetrics) getLabels() []string { utils.Direction, } - if !f.advEnable { + if !f.isAdvanced() { return labels } - if f.srcCtx != nil { - labels = append(labels, f.srcCtx.getLabels()...) - f.l.Info("src labels", zap.Any("labels", labels)) + if f.sourceCtx() != nil { + labels = append(labels, f.sourceCtx().getLabels()...) + f.getLogger().Info("src labels", zap.Any("labels", labels)) } - if f.dstCtx != nil { - labels = append(labels, f.dstCtx.getLabels()...) - f.l.Info("dst labels", zap.Any("labels", labels)) + if f.destinationCtx() != nil { + labels = append(labels, f.destinationCtx().getLabels()...) + f.getLogger().Info("dst labels", zap.Any("labels", labels)) } - // No additional context options + if slices.Contains(f.additionalLabels(), utils.IsReply) { + labels = append(labels, utils.IsReply) + } return labels } func (f *ForwardMetrics) Clean() { exporter.UnregisterMetric(exporter.AdvancedRegistry, metricsinit.ToPrometheusType(f.forwardMetric)) + f.clean() } // TODO: update ProcessFlow with bytes metrics. We are only accounting for count. @@ -116,33 +122,35 @@ func (f *ForwardMetrics) ProcessFlow(flow *v1.Flow) { flow.TrafficDirection.String(), } - if !f.advEnable { + if !f.isAdvanced() { f.update(flow, labels) return } - if f.srcCtx != nil { - srcLabels := f.srcCtx.getValues(flow) + if f.sourceCtx() != nil { + srcLabels := f.sourceCtx().getValues(flow) if len(srcLabels) > 0 { labels = append(labels, srcLabels...) } } - if f.dstCtx != nil { - dstLabel := f.dstCtx.getValues(flow) + if f.destinationCtx() != nil { + dstLabel := f.destinationCtx().getValues(flow) if len(dstLabel) > 0 { labels = append(labels, dstLabel...) } } - // No additional context options + if slices.Contains(f.additionalLabels(), utils.IsReply) { + labels = append(labels, strconv.FormatBool(flow.GetIsReply().GetValue())) + } f.update(flow, labels) - f.l.Debug("forward count metric is added", zap.Any("labels", labels)) + f.getLogger().Debug("forward count metric is added", zap.Any("labels", labels)) } func (f *ForwardMetrics) processLocalCtxFlow(flow *v1.Flow) { - labelValuesMap := f.srcCtx.getLocalCtxValues(flow) + labelValuesMap := f.sourceCtx().getLocalCtxValues(flow) if labelValuesMap == nil { return } @@ -150,22 +158,39 @@ func (f *ForwardMetrics) processLocalCtxFlow(flow *v1.Flow) { if len(labelValuesMap[ingress]) > 0 { labels := append([]string{ingress}, labelValuesMap[ingress]...) f.update(flow, labels) - f.l.Debug("forward count metric in INGRESS in local ctx", zap.Any("labels", labels)) + f.getLogger().Debug("forward count metric in INGRESS in local ctx", zap.Any("labels", labels)) } // Egress values. if len(labelValuesMap[egress]) > 0 { labels := append([]string{egress}, labelValuesMap[egress]...) f.update(flow, labels) - f.l.Debug("forward count metric in EGRESS in local ctx", zap.Any("labels", labels)) + f.getLogger().Debug("forward count metric in EGRESS in local ctx", zap.Any("labels", labels)) + } +} + +func (f *ForwardMetrics) expire(labels []string) bool { + var d bool + if f.forwardMetric != nil { + d = f.forwardMetric.DeleteLabelValues(labels...) + if d { + metricsinit.MetricsExpiredCounter.WithLabelValues(f.metricName).Inc() + } } + return d } func (f *ForwardMetrics) update(fl *v1.Flow, labels []string) { + var updated bool switch f.metricName { case utils.ForwardPacketsGaugeName: - f.forwardMetric.WithLabelValues(labels...).Inc() + updated = true + f.forwardMetric.WithLabelValues(labels...).Add(float64(utils.PreviouslyObservedPackets(fl) + 1)) case utils.ForwardBytesGaugeName: - f.forwardMetric.WithLabelValues(labels...).Add(float64(utils.PacketSize(fl))) + updated = true + f.forwardMetric.WithLabelValues(labels...).Add(float64(utils.PacketSize(fl) + utils.PreviouslyObservedBytes(fl))) + } + if updated { + f.updated(labels) } } diff --git a/pkg/module/metrics/forward_test.go b/pkg/module/metrics/forward_test.go index 9d48f30577..e1a94c0314 100644 --- a/pkg/module/metrics/forward_test.go +++ b/pkg/module/metrics/forward_test.go @@ -6,6 +6,7 @@ package metrics import ( "testing" + "time" "github.com/cilium/cilium/api/v1/flow" "github.com/microsoft/retina/crd/api/v1alpha1" @@ -15,6 +16,7 @@ import ( "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "go.uber.org/zap" + "log/slog" ) type TestMetrics struct { @@ -26,6 +28,7 @@ type TestMetrics struct { metricCall int nilObj bool localContext enrichmentContext + trackedMetrics int } func TestNewForward(t *testing.T) { @@ -53,6 +56,7 @@ func TestNewForward(t *testing.T) { }, exepectedLabels: []string{"direction"}, metricCall: 1, + trackedMetrics: 1, }, { name: "plain opts with nil flow", @@ -108,7 +112,8 @@ func TestNewForward(t *testing.T) { "source_service", "source_port", }, - metricCall: 1, + metricCall: 1, + trackedMetrics: 1, }, { name: "dest opts 1", @@ -130,7 +135,8 @@ func TestNewForward(t *testing.T) { "destination_service", "destination_port", }, - metricCall: 1, + metricCall: 1, + trackedMetrics: 1, }, { name: "source opts with flow", @@ -153,7 +159,8 @@ func TestNewForward(t *testing.T) { "source_service", "source_port", }, - metricCall: 1, + metricCall: 1, + trackedMetrics: 1, }, { name: "drop source opts expect nil", @@ -223,8 +230,9 @@ func TestNewForward(t *testing.T) { "service", "port", }, - metricCall: 1, - localContext: localContext, + metricCall: 1, + trackedMetrics: 1, + localContext: localContext, }, { name: "dest opts 1 with flow in local context", @@ -247,8 +255,9 @@ func TestNewForward(t *testing.T) { "service", "port", }, - metricCall: 1, - localContext: localContext, + metricCall: 1, + trackedMetrics: 1, + localContext: localContext, }, { name: "src and dest opts 1 with flow in local context", @@ -272,8 +281,37 @@ func TestNewForward(t *testing.T) { "service", "port", }, - metricCall: 2, - localContext: localContext, + metricCall: 2, + trackedMetrics: 2, + localContext: localContext, + }, + { + name: "src and dest opts 1 with flow in local context and is_reply", + opts: &v1alpha1.MetricsContextOptions{ + MetricName: "FORWARD", + SourceLabels: []string{"ip", "namespace", "podName", "Workload", "PORT", "serVICE"}, + AdditionalLabels: []string{"is_reply"}, + }, + f: &flow.Flow{ + Verdict: flow.Verdict_FORWARDED, + Destination: &flow.Endpoint{}, + Source: &flow.Endpoint{}, + }, + checkIsAdvance: true, + exepectedLabels: []string{ + "direction", + "ip", + "namespace", + "podname", + "workload_kind", + "workload_name", + "service", + "port", + "is_reply", + }, + metricCall: 2, + trackedMetrics: 2, + localContext: localContext, }, } @@ -282,7 +320,7 @@ func TestNewForward(t *testing.T) { l.Info("Running test", zap.String("name", tc.name), zap.String("metricName", metricName)) ctrl := gomock.NewController(t) - f := NewForwardCountMetrics(tc.opts, log.Logger(), tc.localContext) + f := NewForwardCountMetrics(tc.opts, log.Logger(), tc.localContext, time.Duration(0)) if tc.nilObj { assert.Nil(t, f, "forward metrics should be nil Test Name: %s", tc.name) continue @@ -299,11 +337,40 @@ func TestNewForward(t *testing.T) { Help: "testmetric", }) forwardMock.EXPECT().WithLabelValues(gomock.Any()).Return(testmetric).Times(tc.metricCall) - assert.Equal(t, f.advEnable, tc.checkIsAdvance, "advance metrics options should be equal Test Name: %s", tc.name) + assert.Equal(t, f.isAdvanced(), tc.checkIsAdvance, "advance metrics options should be equal Test Name: %s", tc.name) assert.Equal(t, tc.exepectedLabels, f.getLabels(), "labels should be equal Test Name: %s", tc.name) f.metricName = metricName f.ProcessFlow(tc.f) + + // There should be no tracked metrics when TTL is infinite + assert.Equal(t, 0, len(f.trackedMetricLabels()), "there should be no tracked metrics when TTL is infinite Test Name: %s", tc.name) + + // Test TTL based expiration + metricsinit.InitializeMetrics(slog.Default()) + + // Set the TTL to something high to ensure that our call to expire is the only one that expires the metrics + f = NewForwardCountMetrics(tc.opts, log.Logger(), tc.localContext, time.Minute) + f.forwardMetric = forwardMock + + forwardMock.EXPECT().WithLabelValues(gomock.Any()).Return(testmetric).Times(tc.metricCall) + + f.metricName = metricName + f.ProcessFlow(tc.f) + + forwardMock.EXPECT().DeleteLabelValues(gomock.Any()).Return(true).Times(tc.trackedMetrics) + + for _, ls := range f.trackedMetricLabels() { + assert.True(t, f.expire(ls), "metric should expire successfully Test Name: %s", tc.name) + } + + // Test that clean calls the base object + baseMetricObjectMock := NewMockbaseMetricInterface(ctrl) + f.baseMetricInterface = baseMetricObjectMock + + baseMetricObjectMock.EXPECT().clean().Times(1) + + f.Clean() ctrl.Finish() } } diff --git a/pkg/module/metrics/latency.go b/pkg/module/metrics/latency.go index 3810054d04..0fb88ec7b9 100644 --- a/pkg/module/metrics/latency.go +++ b/pkg/module/metrics/latency.go @@ -33,10 +33,11 @@ const ( apiServerHandshakeLatencyDesc = "Latency of node apiserver tcp handshake in ms" TTL time.Duration = 500 * time.Millisecond LIMIT uint64 = 100000 - // Bucket size. - start = 0 - width = 0.5 - count = 10 + // Histogram bucket parameters (units: milliseconds). + // Produces buckets: 0.5, 1.5, 2.5, ..., 63.5, +Inf. + start = 0.5 + width = 1 + count = 64 ) type key struct { @@ -123,10 +124,18 @@ func (lm *LatencyMetrics) Init(metricName string) { lm.cache.OnEviction(func(ctx context.Context, reason ttlcache.EvictionReason, item *ttlcache.Item[key, *val]) { if reason == ttlcache.EvictionReasonExpired { // Didn't get the corresponding packet. - lm.l.Debug("Evicted item", zap.Any("item", item)) + k := item.Key() + v := item.Value() + lm.l.Debug("Evicted item", + zap.String("srcIP", k.srcIP), + zap.String("dstIP", k.dstIP), + zap.Uint32("srcPort", k.srcP), + zap.Uint32("dstPort", k.dstP), + zap.Uint64("id", k.id), + zap.Int32("timestamp", v.t)) if lm.noResponseMetric != nil { lm.noResponseMetric.WithLabelValues("no_response").Inc() - lm.l.Debug("Incremented no response metric", zap.Any("metric", lm.noResponseMetric)) + lm.l.Debug("Incremented no response metric", zap.String("metric", noResponseFromNodeAPIServerName)) } } }) @@ -323,25 +332,33 @@ func (lm *LatencyMetrics) apiserverWatcherCallbackFn(obj interface{}) { switch event.Type { case cc.EventTypeAddAPIServerIPs: - lm.l.Debug("Add apiserver ips", zap.Any("ips", apiServerIPs)) - lm.addIps(apiServerIPs) + ipStrings := lm.addIps(apiServerIPs) + lm.l.Debug("Add apiserver ips", zap.Strings("ips", ipStrings)) case cc.EventTypeDeleteAPIServerIPs: - lm.l.Debug("Delete apiserver ips", zap.Any("ips", apiServerIPs)) - lm.removeIps(apiServerIPs) + ipStrings := lm.removeIps(apiServerIPs) + lm.l.Debug("Delete apiserver ips", zap.Strings("ips", ipStrings)) default: - lm.l.Debug("Unknown event type", zap.Any("event", event)) + lm.l.Debug("Unknown event type", zap.String("eventType", event.Type.String())) } } -func (lm *LatencyMetrics) addIps(ips []net.IP) { - for _, ip := range ips { - lm.apiServerIps[ip.String()] = struct{}{} +func (lm *LatencyMetrics) addIps(ips []net.IP) []string { + ipStrings := make([]string, len(ips)) + for i, ip := range ips { + ipString := ip.String() + ipStrings[i] = ipString + lm.apiServerIps[ipString] = struct{}{} } + return ipStrings } -func (lm *LatencyMetrics) removeIps(ips []net.IP) { - for _, ip := range ips { - delete(lm.apiServerIps, ip.String()) +func (lm *LatencyMetrics) removeIps(ips []net.IP) []string { + ipStrings := make([]string, len(ips)) + for i, ip := range ips { + ipString := ip.String() + ipStrings[i] = ipString + delete(lm.apiServerIps, ipString) } + return ipStrings } diff --git a/pkg/module/metrics/latency_test.go b/pkg/module/metrics/latency_test.go index fcdbfee87c..1a78fcb37a 100644 --- a/pkg/module/metrics/latency_test.go +++ b/pkg/module/metrics/latency_test.go @@ -122,20 +122,20 @@ func TestProcessFlow(t *testing.T) { */ // Node -> Api server. f1 := utils.ToFlow(l, t1, apiSeverIp, nodeIp, 80, 443, 6, 3, 0) - metaf1 := &utils.RetinaMetadata{} - utils.AddTCPID(metaf1, 1234) - utils.AddTCPFlags(f1, 1, 0, 0, 0, 0, 0) - utils.AddRetinaMetadata(f1, metaf1) + ext1 := utils.NewExtensions() + utils.AddTCPID(ext1, 1234) + utils.AddTCPFlags(f1, 1, 0, 0, 0, 0, 0, 0, 0, 0) + utils.SetExtensions(f1, ext1) f1.Destination = &flow.Endpoint{ PodName: "kubernetes-apiserver", } // Api server -> Node. f2 := utils.ToFlow(l, t2, nodeIp, apiSeverIp, 443, 80, 6, 2, 0) - metaf2 := &utils.RetinaMetadata{} - utils.AddTCPID(metaf2, 1234) - utils.AddTCPFlags(f2, 1, 1, 0, 0, 0, 0) - utils.AddRetinaMetadata(f2, metaf2) + ext2 := utils.NewExtensions() + utils.AddTCPID(ext2, 1234) + utils.AddTCPFlags(f2, 1, 1, 0, 0, 0, 0, 0, 0, 0) + utils.SetExtensions(f2, ext2) f2.Source = &flow.Endpoint{ PodName: "kubernetes-apiserver", } @@ -147,9 +147,9 @@ func TestProcessFlow(t *testing.T) { * Test case 2: Existing TCP connection. */ // Node -> Api server. - utils.AddTCPFlags(f1, 1, 0, 0, 0, 0, 0) + utils.AddTCPFlags(f1, 1, 0, 0, 0, 0, 0, 0, 0, 0) // Api server -> Node. - utils.AddTCPFlags(f2, 0, 1, 0, 0, 0, 0) + utils.AddTCPFlags(f2, 0, 1, 0, 0, 0, 0, 0, 0, 0) // Process flow. lm.ProcessFlow(f1) lm.ProcessFlow(f2) diff --git a/pkg/module/metrics/metrics_module.go b/pkg/module/metrics/metrics_module.go index 0d38c06522..6d473f1d49 100644 --- a/pkg/module/metrics/metrics_module.go +++ b/pkg/module/metrics/metrics_module.go @@ -4,6 +4,7 @@ package metrics import ( "context" + "fmt" "net" "strings" "sync" @@ -177,28 +178,20 @@ func (m *Module) Reconcile(spec *api.MetricsSpec) error { func (m *Module) updateNamespaceLists(spec *api.MetricsSpec) { if len(spec.Namespaces.Include) > 0 && len(spec.Namespaces.Exclude) > 0 { m.l.Error("Both included and excluded namespaces are specified. Cannot reconcile.") - } - - if len(spec.Namespaces.Include) == 0 { - m.appendIncludeList([]string{}) - m.appendExcludeList([]string{}) - } - - if len(spec.Namespaces.Exclude) == 0 { - m.appendIncludeList([]string{}) - m.appendExcludeList([]string{}) + return } if len(spec.Namespaces.Include) > 0 { m.l.Info("Including namespaces", zap.Strings("namespaces", spec.Namespaces.Include)) - m.appendIncludeList(spec.Namespaces.Include) m.appendExcludeList([]string{}) - } - - if len(spec.Namespaces.Exclude) > 0 { + m.appendIncludeList(spec.Namespaces.Include) + } else if len(spec.Namespaces.Exclude) > 0 { m.l.Info("Excluding namespaces", zap.Strings("namespaces", spec.Namespaces.Exclude)) + m.appendIncludeList([]string{}) m.appendExcludeList(spec.Namespaces.Exclude) + } else { m.appendIncludeList([]string{}) + m.appendExcludeList([]string{}) } } @@ -221,23 +214,33 @@ func (m *Module) updateMetricsContexts(spec *api.MetricsSpec) { } for _, ctxOption := range spec.ContextOptions { + var ttl time.Duration + var err error + if ctxOption.TTL != "" { + ttl, err = time.ParseDuration(ctxOption.TTL) + // this shouldn't happen since we've already validated the CRD, but put some safety here just in case + if err != nil { + m.l.Error("Invalid TTL format", zap.String("metricName", ctxOption.MetricName), zap.Error(err)) + continue + } + } switch { case strings.Contains(ctxOption.MetricName, forward): - fm := NewForwardCountMetrics(&ctxOption, m.l, ctxType) + fm := NewForwardCountMetrics(&ctxOption, m.l, ctxType, ttl) if fm != nil { m.registry[ctxOption.MetricName] = fm } case strings.Contains(ctxOption.MetricName, drop): - dm := NewDropCountMetrics(&ctxOption, m.l, ctxType) + dm := NewDropCountMetrics(&ctxOption, m.l, ctxType, ttl) if dm != nil { m.registry[ctxOption.MetricName] = dm } case strings.Contains(ctxOption.MetricName, tcp): - tm := NewTCPMetrics(&ctxOption, m.l, ctxType) + tm := NewTCPMetrics(&ctxOption, m.l, ctxType, ttl) if tm != nil { m.registry[ctxOption.MetricName] = tm } - tr := NewTCPRetransMetrics(&ctxOption, m.l, ctxType) + tr := NewTCPRetransMetrics(&ctxOption, m.l, ctxType, ttl) if tr != nil { m.registry[ctxOption.MetricName] = tr } @@ -249,7 +252,7 @@ func (m *Module) updateMetricsContexts(spec *api.MetricsSpec) { m.registry[nodeApiserver] = lm } case strings.Contains(ctxOption.MetricName, dns) || strings.Contains(ctxOption.MetricName, pktmon): - dm := NewDNSMetrics(&ctxOption, m.l, ctxType) + dm := NewDNSMetrics(&ctxOption, m.l, ctxType, ttl) if dm != nil { m.registry[ctxOption.MetricName] = dm } @@ -383,7 +386,7 @@ func (m *Module) appendIncludeList(namespaces []string) { // toAdd namespace IPs to filter manager for _, ns := range toAdd { ips := m.daemonCache.GetIPsByNamespace(ns) - m.l.Info("Adding IPs to filter manager", zap.String("namespace", ns), zap.Any("ips", ips)) + m.l.Info("Adding IPs to filter manager", zap.String("namespace", ns), zap.String("ips", fmt.Sprint(ips))) err := m.filterManager.AddIPs(ips, metricModuleReq, moduleReqMetadata) if err != nil { @@ -394,7 +397,7 @@ func (m *Module) appendIncludeList(namespaces []string) { // toRemove namespace IPs from filter manager for _, ns := range toRemove { ips := m.daemonCache.GetIPsByNamespace(ns) - m.l.Info("Removing IPs from filter manager", zap.String("namespace", ns), zap.Any("ips", ips)) + m.l.Info("Removing IPs from filter manager", zap.String("namespace", ns), zap.String("ips", fmt.Sprint(ips))) err := m.filterManager.DeleteIPs(ips, metricModuleReq, moduleReqMetadata) if err != nil { @@ -408,8 +411,72 @@ func (m *Module) appendExcludeList(ns []string) { m.excludedNamespaces = make(map[string]struct{}) } - // TODO here we will need to check for IP which - // needs to be added to filter manager and which needs to be removed + m.l.Info("Appending namespaces to exclude list", zap.Strings("namespaces", ns)) + + tempNewNs := make(map[string]struct{}) + for _, n := range ns { + tempNewNs[n] = struct{}{} + } + + m.l.Info("Current excluded namespaces", zap.Any("namespaces", m.excludedNamespaces)) + newlyExcluded, newlyUnexcluded := make([]string, 0), make([]string, 0) + + // Namespaces that are in the new list but not in the old list + for _, n := range ns { + if _, ok := m.excludedNamespaces[n]; !ok { + newlyExcluded = append(newlyExcluded, n) + } + } + + // Namespaces that were in the old list but not in the new list + for n := range m.excludedNamespaces { + if _, ok := tempNewNs[n]; !ok { + newlyUnexcluded = append(newlyUnexcluded, n) + } + } + + m.excludedNamespaces = tempNewNs + + m.l.Info("Namespaces newly excluded", zap.Strings("namespaces", newlyExcluded)) + m.l.Info("Namespaces newly un-excluded", zap.Strings("namespaces", newlyUnexcluded)) + + // For newly excluded namespaces: remove their IPs from filtermanager + for _, n := range newlyExcluded { + ips := m.daemonCache.GetIPsByNamespace(n) + m.l.Info("Removing IPs from filter manager (excluded)", zap.String("namespace", n), zap.Any("ips", ips)) + + err := m.filterManager.DeleteIPs(ips, metricModuleReq, moduleReqMetadata) + if err != nil { + m.l.Error("Error removing IPs from filter manager", zap.Error(err)) + } + } + + // For newly un-excluded namespaces: add their IPs to filtermanager + for _, n := range newlyUnexcluded { + ips := m.daemonCache.GetIPsByNamespace(n) + m.l.Info("Adding IPs to filter manager (un-excluded)", zap.String("namespace", n), zap.Any("ips", ips)) + + err := m.filterManager.AddIPs(ips, metricModuleReq, moduleReqMetadata) + if err != nil { + m.l.Error("Error adding IPs to filter manager", zap.Error(err)) + } + } + + // When transitioning to exclude mode, add IPs for all non-excluded namespaces + if len(newlyExcluded) > 0 && len(newlyUnexcluded) == 0 && len(newlyExcluded) == len(ns) { + allNs := m.daemonCache.GetAllNamespaces() + for _, n := range allNs { + if _, excluded := m.excludedNamespaces[n]; !excluded { + ips := m.daemonCache.GetIPsByNamespace(n) + m.l.Info("Adding IPs to filter manager (non-excluded)", zap.String("namespace", n), zap.Any("ips", ips)) + + err := m.filterManager.AddIPs(ips, metricModuleReq, moduleReqMetadata) + if err != nil { + m.l.Error("Error adding IPs to filter manager", zap.Error(err)) + } + } + } + } } func (m *Module) PodCallBackFn(obj interface{}) { @@ -429,25 +496,32 @@ func (m *Module) PodCallBackFn(obj interface{}) { return } - m.Lock() - if !m.nsOfInterest(pod.Namespace()) && !m.podOfInterest(ip, pod.Annotations()) { - m.Unlock() + // Compute interest flags under RLock so we don't race with Reconcile/appendIncludeList + // which mutate the namespace maps under Lock. + m.RLock() + annotated := m.podAnnotated(pod.Annotations()) + namespaced := m.nsOfInterest(pod.Namespace()) + m.RUnlock() + + if event.Type != cache.EventTypePodDeleted && !namespaced && !m.filterManager.HasIP(ip) && !annotated { return } - m.Unlock() - handlePodEvent(event, m, pod, ip) + handlePodEvent(event, m, pod, ip, annotated, namespaced) } -func handlePodEvent(event *cache.CacheEvent, m *Module, pod *common.RetinaEndpoint, ip net.IP) { +func handlePodEvent( + event *cache.CacheEvent, m *Module, pod *common.RetinaEndpoint, + ip net.IP, annotated, namespaced bool, +) { if pod.Name() == common.APIServerEndpointName && pod.Namespace() == common.APIServerEndpointName { m.l.Debug("Ignoring apiserver endpoint") return } podCacheEntry := DirtyCachePod{ IP: ip, - Annotated: m.podAnnotated(pod.Annotations()), - Namespaced: m.nsOfInterest(pod.Namespace()), + Annotated: annotated, + Namespaced: namespaced, } switch event.Type { case cache.EventTypePodAdded: @@ -456,13 +530,24 @@ func handlePodEvent(event *cache.CacheEvent, m *Module, pod *common.RetinaEndpoi // This case should only occur when the pod annotation is removed since this is an EventTypePodAdded (also accounts for pod update) if !podCacheEntry.Annotated && !podCacheEntry.Namespaced { m.l.Info("Adding pod IP to DELETE dirty pods cache. Pod not annotated or in namespace of interest.", zap.String("pod name", pod.NamespacedName())) - podCacheEntry.Annotated = true m.dirtyPods.ToDelete(ip.String(), podCacheEntry) return } m.l.Info("Adding pod IP to ADD dirty pods cache", zap.String("pod name", pod.NamespacedName())) m.dirtyPods.ToAdd(podCacheEntry.IP.String(), podCacheEntry) case cache.EventTypePodDeleted: + // Guard against spurious DELETE events during pod churn / IP reuse. + // The cache (pkg/controllers/cache/cache.go) updates its maps synchronously + // (epMap/ipToEpKey) and then publishes events asynchronously via a goroutine. + // So when we process this DELETE, if a new pod already reused the IP, the + // cache will still contain a valid entry. Deleting would incorrectly remove it. + if endpoint := m.daemonCache.GetPodByIP(ip.String()); endpoint != nil { + m.l.Debug("Ignoring DELETE for reused IP — pod still exists in cache", + zap.String("deleted pod", pod.NamespacedName()), + zap.String("ip", ip.String()), + zap.String("cached pod", endpoint.NamespacedName())) + return + } m.l.Info("Adding pod IP to DELETE dirty pods cache", zap.String("pod name", pod.NamespacedName())) m.dirtyPods.ToDelete(ip.String(), podCacheEntry) default: @@ -500,14 +585,14 @@ func (m *Module) applyDirtyPodsAdd() { } } if len(podsToAdd) > 0 { - m.l.Debug("Adding annotated pod IPs to filtermap", zap.Any("IPs", podsToAdd)) + m.l.Debug("Adding annotated pod IPs to filtermap", zap.String("IPs", fmt.Sprint(podsToAdd))) err := m.filterManager.AddIPs(podsToAdd, metricModuleReq, modulePodReqMetadata) if err != nil { m.l.Error("Error adding pod IP to filter manager", zap.Error(err)) } } if len(podsToAddNamespaced) > 0 { - m.l.Debug("Adding namespaced pod IPs to filtermap", zap.Any("IPs", podsToAddNamespaced)) + m.l.Debug("Adding namespaced pod IPs to filtermap", zap.String("IPs", fmt.Sprint(podsToAddNamespaced))) err := m.filterManager.AddIPs(podsToAddNamespaced, metricModuleReq, moduleReqMetadata) if err != nil { m.l.Error("Error adding pod IP to filter manager", zap.Error(err)) @@ -517,43 +602,36 @@ func (m *Module) applyDirtyPodsAdd() { m.dirtyPods.ClearAdd() } -// applyDirtyPodsDelete deletes pod ips from filtermanager +// applyDirtyPodsDelete deletes pod ips from filtermanager. +// Always attempts deletion with both metadata types (pod and namespace). +// The filtermanager cache makes extra deletes a safe no-op when the metadata doesn't exist. func (m *Module) applyDirtyPodsDelete() { deletes := m.dirtyPods.GetDeleteList() if len(deletes) > 0 { - podOfInterestDeleteList := make([]net.IP, 0) - namespaceOfInterestDeleteList := make([]net.IP, 0) + ipsToDelete := make([]net.IP, 0, len(deletes)) for _, entry := range deletes { podEntry := entry.(DirtyCachePod) - if podEntry.Annotated { - podOfInterestDeleteList = append(podOfInterestDeleteList, podEntry.IP) - } - if podEntry.Namespaced { - namespaceOfInterestDeleteList = append(namespaceOfInterestDeleteList, podEntry.IP) - } + ipsToDelete = append(ipsToDelete, podEntry.IP) } - if len(podOfInterestDeleteList) > 0 { - m.l.Debug("Deleting Ips in dirty pods from filtermap", zap.Any("IPs", podOfInterestDeleteList)) - err := m.filterManager.DeleteIPs(podOfInterestDeleteList, metricModuleReq, modulePodReqMetadata) - if err != nil { - m.l.Error("Error deleting pod IP from filter manager", zap.Error(err)) - } + m.l.Debug("Deleting Ips in dirty pods from filtermap", zap.String("IPs", fmt.Sprint(ipsToDelete))) + err := m.filterManager.DeleteIPs(ipsToDelete, metricModuleReq, modulePodReqMetadata) + if err != nil { + m.l.Error("Error deleting pod IP from filter manager", zap.Error(err)) } - if len(namespaceOfInterestDeleteList) > 0 { - m.l.Debug("Deleting Ips in dirty pods from filtermap", zap.Any("IPs", namespaceOfInterestDeleteList)) - err := m.filterManager.DeleteIPs(namespaceOfInterestDeleteList, metricModuleReq, moduleReqMetadata) - if err != nil { - m.l.Error("Error deleting pod IP from filter manager", zap.Error(err)) - } + err = m.filterManager.DeleteIPs(ipsToDelete, metricModuleReq, moduleReqMetadata) + if err != nil { + m.l.Error("Error deleting pod IP from filter manager", zap.Error(err)) } } m.dirtyPods.ClearDelete() } -// nsOfInterest checks if the namespace is in the included or excluded list -// included namespaces can be defined by crd or automatically applied by annotated namespaces. +// nsOfInterest checks if the namespace is in the included or excluded list. +// Included namespaces can be defined by CRD or automatically applied by annotated namespaces. +// When no namespace filters are configured (both lists empty), returns false — pods must be +// individually annotated or already present in the filtermap to be tracked. func (m *Module) nsOfInterest(ns string) bool { if len(m.includedNamespaces) > 0 { if _, ok := m.includedNamespaces[ns]; ok { diff --git a/pkg/module/metrics/metrics_module_linux_test.go b/pkg/module/metrics/metrics_module_linux_test.go index 1d53ac4b54..faca72d199 100644 --- a/pkg/module/metrics/metrics_module_linux_test.go +++ b/pkg/module/metrics/metrics_module_linux_test.go @@ -259,6 +259,7 @@ func TestPodCallBack(t *testing.T) { fm := filtermanager.NewMockIFilterManager(ctrl) //nolint:typecheck c := cache.NewMockCacheInterface(ctrl) //nolint:typecheck c.EXPECT().GetIPsByNamespace(gomock.Any()).Return([]net.IP{}).AnyTimes() + c.EXPECT().GetPodByIP(gomock.Any()).Return(nil).AnyTimes() fm.EXPECT().AddIPs([]net.IP{}, gomock.Any(), gomock.Any()).Return(nil).Times(1) fm.EXPECT().HasIP(gomock.Any()).Return(tt.fmHasIP).AnyTimes() if len(tt.addExpected) > 0 { @@ -373,7 +374,7 @@ func TestModule_Reconcile(t *testing.T) { l := log.Logger().Named("test") testDropMetric := &DropCountMetrics{ - baseMetricObject: baseMetricObject{ + baseMetricInterface: &baseMetricObject{ advEnable: true, ctxOptions: &api.MetricsContextOptions{ MetricName: "drop_count", @@ -385,7 +386,7 @@ func TestModule_Reconcile(t *testing.T) { } testDropMetric.Init("drop_count") testDropMetricBytes := &DropCountMetrics{ - baseMetricObject: baseMetricObject{ + baseMetricInterface: &baseMetricObject{ advEnable: true, ctxOptions: &api.MetricsContextOptions{ MetricName: "drop_bytes", @@ -395,7 +396,7 @@ func TestModule_Reconcile(t *testing.T) { } testDropMetricBytes.Init("drop_bytes") testForwardMetric := &ForwardMetrics{ - baseMetricObject: baseMetricObject{ + baseMetricInterface: &baseMetricObject{ advEnable: true, ctxOptions: &api.MetricsContextOptions{ MetricName: "forward_count", @@ -407,7 +408,7 @@ func TestModule_Reconcile(t *testing.T) { } testForwardMetric.Init("forward_count") testForwardMetricBytes := &ForwardMetrics{ - baseMetricObject: baseMetricObject{ + baseMetricInterface: &baseMetricObject{ advEnable: true, ctxOptions: &api.MetricsContextOptions{ MetricName: "forward_bytes", @@ -658,3 +659,691 @@ func TestPodAnnotated(t *testing.T) { assert.Equal(t, tt.expected, tt.m.podAnnotated(tt.annotations)) } } + +func TestNsOfInterest(t *testing.T) { + _, _ = log.SetupZapLogger(log.GetDefaultLogOpts()) + + tests := []struct { + name string + includedNamespaces map[string]struct{} + excludedNamespaces map[string]struct{} + ns string + expected bool + }{ + { + name: "no filters configured (empty maps) - no namespace is of interest", + includedNamespaces: map[string]struct{}{}, + excludedNamespaces: map[string]struct{}{}, + ns: "default", + expected: false, + }, + { + name: "no filters configured (nil maps) - no namespace is of interest", + includedNamespaces: nil, + excludedNamespaces: nil, + ns: "default", + expected: false, + }, + { + name: "included namespace matches", + includedNamespaces: map[string]struct{}{"ns1": {}}, + ns: "ns1", + expected: true, + }, + { + name: "included namespace does not match", + includedNamespaces: map[string]struct{}{"ns1": {}}, + ns: "ns2", + expected: false, + }, + { + name: "excluded namespace matches - should not be of interest", + excludedNamespaces: map[string]struct{}{"ns1": {}}, + ns: "ns1", + expected: false, + }, + { + name: "excluded namespace does not match - should be of interest", + excludedNamespaces: map[string]struct{}{"ns1": {}}, + ns: "ns2", + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &Module{ + includedNamespaces: tt.includedNamespaces, + excludedNamespaces: tt.excludedNamespaces, + } + assert.Equal(t, tt.expected, m.nsOfInterest(tt.ns)) + }) + } +} + +func TestAppendExcludeList(t *testing.T) { + _, _ = log.SetupZapLogger(log.GetDefaultLogOpts()) + cfg, err := kcfg.GetConfig(testCfgFile) + require.NoError(t, err) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + p := pubsub.NewMockPubSubInterface(ctrl) + e := enricher.NewMockEnricherInterface(ctrl) + fm := filtermanager.NewMockIFilterManager(ctrl) + c := cache.NewMockCacheInterface(ctrl) + + c.EXPECT().GetIPsByNamespace(gomock.Any()).Return([]net.IP{}).AnyTimes() + c.EXPECT().GetAllNamespaces().Return([]string{}).AnyTimes() + fm.EXPECT().AddIPs(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + fm.EXPECT().DeleteIPs(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + + me := &Module{ + RWMutex: &sync.RWMutex{}, + l: log.Logger().Named("MetricModule"), + pubsub: p, + configs: make([]*api.MetricsConfiguration, 0), + enricher: e, + wg: sync.WaitGroup{}, + registry: make(map[string]AdvMetricsInterface), + moduleCtx: context.Background(), + filterManager: fm, + daemonCache: c, + dirtyPods: common.NewDirtyCache(), + pubsubPodSub: "", + daemonConfig: cfg, + } + + testcases := []struct { + description string + namespaces []string + wantExcludedNamespaces map[string]struct{} + }{ + { + "input 0 namespaces", + []string{}, + map[string]struct{}{}, + }, + { + "input 1 namespace (add)", + []string{"ns1"}, + map[string]struct{}{"ns1": {}}, + }, + { + "input 1 namespace different than previous (add 1 & remove 1)", + []string{"ns2"}, + map[string]struct{}{"ns2": {}}, + }, + { + "input 2 namespaces (add 1)", + []string{"ns1", "ns2"}, + map[string]struct{}{"ns1": {}, "ns2": {}}, + }, + { + "input 0 namespaces (remove all)", + []string{}, + map[string]struct{}{}, + }, + } + + for _, test := range testcases { + t.Run(test.description, func(t *testing.T) { + me.appendExcludeList(test.namespaces) + assert.Equal(t, test.wantExcludedNamespaces, me.excludedNamespaces) + }) + } +} + +func TestUpdateNamespaceListsExclude(t *testing.T) { + _, _ = log.SetupZapLogger(log.GetDefaultLogOpts()) + cfg, err := kcfg.GetConfig(testCfgFile) + require.NoError(t, err) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + p := pubsub.NewMockPubSubInterface(ctrl) + e := enricher.NewMockEnricherInterface(ctrl) + fm := filtermanager.NewMockIFilterManager(ctrl) + c := cache.NewMockCacheInterface(ctrl) + + c.EXPECT().GetIPsByNamespace(gomock.Any()).Return([]net.IP{}).AnyTimes() + c.EXPECT().GetAllNamespaces().Return([]string{}).AnyTimes() + fm.EXPECT().AddIPs(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + fm.EXPECT().DeleteIPs(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + + me := &Module{ + RWMutex: &sync.RWMutex{}, + l: log.Logger().Named("MetricModule"), + pubsub: p, + configs: make([]*api.MetricsConfiguration, 0), + enricher: e, + wg: sync.WaitGroup{}, + registry: make(map[string]AdvMetricsInterface), + moduleCtx: context.Background(), + filterManager: fm, + daemonCache: c, + dirtyPods: common.NewDirtyCache(), + pubsubPodSub: "", + daemonConfig: cfg, + } + + testcases := []struct { + description string + spec *api.MetricsSpec + wantIncludedNamespaces map[string]struct{} + wantExcludedNamespaces map[string]struct{} + }{ + { + "exclude only - should populate excludedNamespaces", + &api.MetricsSpec{ + Namespaces: api.MetricsNamespaces{ + Exclude: []string{"ns1"}, + }, + }, + map[string]struct{}{}, + map[string]struct{}{"ns1": {}}, + }, + { + "neither set - both empty", + &api.MetricsSpec{}, + map[string]struct{}{}, + map[string]struct{}{}, + }, + } + + for _, test := range testcases { + t.Run(test.description, func(t *testing.T) { + me.updateNamespaceLists(test.spec) + assert.Equal(t, test.wantIncludedNamespaces, me.includedNamespaces) + assert.Equal(t, test.wantExcludedNamespaces, me.excludedNamespaces) + }) + } +} + +func TestPodCallBackExclude(t *testing.T) { + _, _ = log.SetupZapLogger(log.GetDefaultLogOpts()) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + cfg, _ := kcfg.GetConfig(testCfgFile) + + p := pubsub.NewMockPubSubInterface(ctrl) + e := enricher.NewMockEnricherInterface(ctrl) + fm := filtermanager.NewMockIFilterManager(ctrl) + c := cache.NewMockCacheInterface(ctrl) + + c.EXPECT().GetIPsByNamespace(gomock.Any()).Return([]net.IP{}).AnyTimes() + c.EXPECT().GetAllNamespaces().Return([]string{}).AnyTimes() + fm.EXPECT().AddIPs(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + fm.EXPECT().DeleteIPs(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + fm.EXPECT().HasIP(gomock.Any()).Return(false).AnyTimes() + + me := &Module{ + RWMutex: &sync.RWMutex{}, + l: log.Logger().Named("MetricModule"), + pubsub: p, + configs: make([]*api.MetricsConfiguration, 0), + enricher: e, + wg: sync.WaitGroup{}, + registry: make(map[string]AdvMetricsInterface), + moduleCtx: context.Background(), + filterManager: fm, + daemonCache: c, + dirtyPods: common.NewDirtyCache(), + pubsubPodSub: "", + daemonConfig: cfg, + } + + // Setup exclude list via updateNamespaceLists + spec := &api.MetricsSpec{ + Namespaces: api.MetricsNamespaces{ + Exclude: []string{"ns1"}, + }, + } + me.updateNamespaceLists(spec) + + // Pod in non-excluded namespace should be tracked + pod1 := common.NewRetinaEndpoint("pod1", "default", &common.IPAddresses{IPv4: net.IPv4(10, 0, 0, 1)}) + me.PodCallBackFn(cache.NewCacheEvent(cache.EventTypePodAdded, pod1)) + adds := me.dirtyPods.GetAddList() + assert.Len(t, adds, 1, "pod in non-excluded namespace should be added to dirty pods") + + // Pod in excluded namespace should NOT be tracked + pod2 := common.NewRetinaEndpoint("pod2", "ns1", &common.IPAddresses{IPv4: net.IPv4(10, 0, 0, 2)}) + me.PodCallBackFn(cache.NewCacheEvent(cache.EventTypePodAdded, pod2)) + adds = me.dirtyPods.GetAddList() + assert.Len(t, adds, 1, "pod in excluded namespace should not be added to dirty pods") +} + +func TestDeletePodAfterNamespaceRemoved(t *testing.T) { + _, err := log.SetupZapLogger(log.GetDefaultLogOpts()) + require.NoError(t, err) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + cfg, _ := kcfg.GetConfig(testCfgFile) + cfg.EnableAnnotations = false + + p := pubsub.NewMockPubSubInterface(ctrl) + e := enricher.NewMockEnricherInterface(ctrl) + fm := filtermanager.NewMockIFilterManager(ctrl) + c := cache.NewMockCacheInterface(ctrl) + + ip1 := net.IPv4(10, 0, 0, 1) + + // appendIncludeList(["ns1"]) will add namespace IPs + c.EXPECT().GetIPsByNamespace("ns1").Return([]net.IP{}).Times(1) + fm.EXPECT().AddIPs([]net.IP{}, gomock.Any(), gomock.Any()).Return(nil).Times(1) + // PodAdded: add with namespace metadata + fm.EXPECT().AddIPs([]net.IP{ip1}, gomock.Any(), moduleReqMetadata).Return(nil).Times(1) + // appendIncludeList([]) removes ns1 + c.EXPECT().GetIPsByNamespace("ns1").Return([]net.IP{ip1}).Times(1) + fm.EXPECT().DeleteIPs([]net.IP{ip1}, gomock.Any(), moduleReqMetadata).Return(nil).Times(1) + // PodDeleted: applyDirtyPodsDelete should attempt both metadata types + fm.EXPECT().HasIP(gomock.Any()).Return(false).AnyTimes() + fm.EXPECT().DeleteIPs([]net.IP{ip1}, gomock.Any(), modulePodReqMetadata).Return(nil).Times(1) + fm.EXPECT().DeleteIPs([]net.IP{ip1}, gomock.Any(), moduleReqMetadata).Return(nil).Times(1) + // Pod is genuinely deleted — GetPodByIP returns nil + c.EXPECT().GetPodByIP(gomock.Any()).Return(nil).AnyTimes() + + me := &Module{ + RWMutex: &sync.RWMutex{}, + l: log.Logger().Named("MetricModule"), + pubsub: p, + configs: make([]*api.MetricsConfiguration, 0), + enricher: e, + wg: sync.WaitGroup{}, + registry: make(map[string]AdvMetricsInterface), + moduleCtx: context.Background(), + filterManager: fm, + daemonCache: c, + dirtyPods: common.NewDirtyCache(), + pubsubPodSub: "", + daemonConfig: cfg, + } + + // Add pod in tracked namespace + me.appendIncludeList([]string{"ns1"}) + pod1 := common.NewRetinaEndpoint("pod1", "ns1", &common.IPAddresses{IPv4: ip1}) + me.PodCallBackFn(cache.NewCacheEvent(cache.EventTypePodAdded, pod1)) + me.applyDirtyPods() + + // Remove namespace from include list + me.appendIncludeList([]string{}) + + // Delete pod — ns no longer of interest + me.PodCallBackFn(cache.NewCacheEvent(cache.EventTypePodDeleted, pod1)) + me.applyDirtyPods() +} + +func TestDeletePodAfterAnnotationRemovedDirectDelete(t *testing.T) { + _, err := log.SetupZapLogger(log.GetDefaultLogOpts()) + require.NoError(t, err) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + cfg, _ := kcfg.GetConfig(testCfgFile) + cfg.EnableAnnotations = true + + p := pubsub.NewMockPubSubInterface(ctrl) + e := enricher.NewMockEnricherInterface(ctrl) + fm := filtermanager.NewMockIFilterManager(ctrl) + c := cache.NewMockCacheInterface(ctrl) + + ip1 := net.IPv4(10, 0, 0, 1) + + // PodAdded: add with pod metadata (annotated, ns not of interest) + fm.EXPECT().HasIP(gomock.Any()).Return(false).AnyTimes() + fm.EXPECT().AddIPs([]net.IP{ip1}, gomock.Any(), modulePodReqMetadata).Return(nil).Times(1) + // PodDeleted: applyDirtyPodsDelete should attempt both metadata types + fm.EXPECT().DeleteIPs([]net.IP{ip1}, gomock.Any(), modulePodReqMetadata).Return(nil).Times(1) + fm.EXPECT().DeleteIPs([]net.IP{ip1}, gomock.Any(), moduleReqMetadata).Return(nil).Times(1) + // Pod is genuinely deleted — GetPodByIP returns nil + c.EXPECT().GetPodByIP(gomock.Any()).Return(nil).AnyTimes() + + me := &Module{ + RWMutex: &sync.RWMutex{}, + l: log.Logger().Named("MetricModule"), + pubsub: p, + configs: make([]*api.MetricsConfiguration, 0), + enricher: e, + wg: sync.WaitGroup{}, + registry: make(map[string]AdvMetricsInterface), + moduleCtx: context.Background(), + filterManager: fm, + daemonCache: c, + dirtyPods: common.NewDirtyCache(), + pubsubPodSub: "", + daemonConfig: cfg, + } + + // Add annotated pod + pod1 := common.NewRetinaEndpoint("pod1", "ns1", &common.IPAddresses{IPv4: ip1}) + pod1.SetAnnotations(map[string]string{common.RetinaPodAnnotation: common.RetinaPodAnnotationValue}) + me.PodCallBackFn(cache.NewCacheEvent(cache.EventTypePodAdded, pod1)) + me.applyDirtyPods() + + // Delete pod with annotation already removed (no intermediate update event) + pod1NoAnnotation := common.NewRetinaEndpoint("pod1", "ns1", &common.IPAddresses{IPv4: ip1}) + me.PodCallBackFn(cache.NewCacheEvent(cache.EventTypePodDeleted, pod1NoAnnotation)) + me.applyDirtyPods() +} + +func TestDeletePodTrackedByBothAfterNamespaceRemoved(t *testing.T) { + _, err := log.SetupZapLogger(log.GetDefaultLogOpts()) + require.NoError(t, err) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + cfg, _ := kcfg.GetConfig(testCfgFile) + cfg.EnableAnnotations = true + + p := pubsub.NewMockPubSubInterface(ctrl) + e := enricher.NewMockEnricherInterface(ctrl) + fm := filtermanager.NewMockIFilterManager(ctrl) + c := cache.NewMockCacheInterface(ctrl) + + ip1 := net.IPv4(10, 0, 0, 1) + + // appendIncludeList(["ns1"]) initial setup + c.EXPECT().GetIPsByNamespace("ns1").Return([]net.IP{}).Times(1) + fm.EXPECT().AddIPs([]net.IP{}, gomock.Any(), gomock.Any()).Return(nil).Times(1) + // PodAdded: add with both metadata types (annotated + namespaced) + fm.EXPECT().HasIP(gomock.Any()).Return(true).AnyTimes() + fm.EXPECT().AddIPs([]net.IP{ip1}, gomock.Any(), modulePodReqMetadata).Return(nil).Times(1) + fm.EXPECT().AddIPs([]net.IP{ip1}, gomock.Any(), moduleReqMetadata).Return(nil).Times(1) + // appendIncludeList([]) removes ns1 + c.EXPECT().GetIPsByNamespace("ns1").Return([]net.IP{ip1}).Times(1) + fm.EXPECT().DeleteIPs([]net.IP{ip1}, gomock.Any(), moduleReqMetadata).Return(nil).Times(1) + // PodDeleted: applyDirtyPodsDelete should attempt both metadata types + fm.EXPECT().DeleteIPs([]net.IP{ip1}, gomock.Any(), modulePodReqMetadata).Return(nil).Times(1) + fm.EXPECT().DeleteIPs([]net.IP{ip1}, gomock.Any(), moduleReqMetadata).Return(nil).Times(1) + // Pod is genuinely deleted — GetPodByIP returns nil + c.EXPECT().GetPodByIP(gomock.Any()).Return(nil).AnyTimes() + + me := &Module{ + RWMutex: &sync.RWMutex{}, + l: log.Logger().Named("MetricModule"), + pubsub: p, + configs: make([]*api.MetricsConfiguration, 0), + enricher: e, + wg: sync.WaitGroup{}, + registry: make(map[string]AdvMetricsInterface), + moduleCtx: context.Background(), + filterManager: fm, + daemonCache: c, + dirtyPods: common.NewDirtyCache(), + pubsubPodSub: "", + daemonConfig: cfg, + } + + // Add annotated pod in tracked namespace + me.appendIncludeList([]string{"ns1"}) + pod1 := common.NewRetinaEndpoint("pod1", "ns1", &common.IPAddresses{IPv4: ip1}) + pod1.SetAnnotations(map[string]string{common.RetinaPodAnnotation: common.RetinaPodAnnotationValue}) + me.PodCallBackFn(cache.NewCacheEvent(cache.EventTypePodAdded, pod1)) + me.applyDirtyPods() + + // Remove namespace from include list + me.appendIncludeList([]string{}) + + // Delete pod — annotation still present but ns no longer of interest + me.PodCallBackFn(cache.NewCacheEvent(cache.EventTypePodDeleted, pod1)) + me.applyDirtyPods() +} + +func TestDeletePodNeverTracked(t *testing.T) { + _, err := log.SetupZapLogger(log.GetDefaultLogOpts()) + require.NoError(t, err) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + cfg, _ := kcfg.GetConfig(testCfgFile) + cfg.EnableAnnotations = false + + p := pubsub.NewMockPubSubInterface(ctrl) + e := enricher.NewMockEnricherInterface(ctrl) + fm := filtermanager.NewMockIFilterManager(ctrl) + c := cache.NewMockCacheInterface(ctrl) + + fm.EXPECT().HasIP(gomock.Any()).Return(false).AnyTimes() + fm.EXPECT().DeleteIPs(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + // Pod is genuinely deleted — GetPodByIP returns nil + c.EXPECT().GetPodByIP(gomock.Any()).Return(nil).AnyTimes() + + me := &Module{ + RWMutex: &sync.RWMutex{}, + l: log.Logger().Named("MetricModule"), + pubsub: p, + configs: make([]*api.MetricsConfiguration, 0), + enricher: e, + wg: sync.WaitGroup{}, + registry: make(map[string]AdvMetricsInterface), + moduleCtx: context.Background(), + filterManager: fm, + daemonCache: c, + dirtyPods: common.NewDirtyCache(), + pubsubPodSub: "", + daemonConfig: cfg, + includedNamespaces: map[string]struct{}{"ns1": {}}, + } + + // Delete a pod in ns2 (not tracked, never added) + pod1 := common.NewRetinaEndpoint("pod1", "ns2", &common.IPAddresses{IPv4: net.IPv4(10, 0, 0, 1)}) + me.PodCallBackFn(cache.NewCacheEvent(cache.EventTypePodDeleted, pod1)) + me.applyDirtyPods() +} + +func TestNormalPodLifecycleInTrackedNamespace(t *testing.T) { + _, err := log.SetupZapLogger(log.GetDefaultLogOpts()) + require.NoError(t, err) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + cfg, _ := kcfg.GetConfig(testCfgFile) + cfg.EnableAnnotations = false + + p := pubsub.NewMockPubSubInterface(ctrl) + e := enricher.NewMockEnricherInterface(ctrl) + fm := filtermanager.NewMockIFilterManager(ctrl) + c := cache.NewMockCacheInterface(ctrl) + + ip1 := net.IPv4(10, 0, 0, 1) + + c.EXPECT().GetIPsByNamespace("ns1").Return([]net.IP{}).Times(1) + fm.EXPECT().AddIPs([]net.IP{}, gomock.Any(), gomock.Any()).Return(nil).Times(1) + fm.EXPECT().HasIP(gomock.Any()).Return(false).AnyTimes() + fm.EXPECT().AddIPs([]net.IP{ip1}, gomock.Any(), moduleReqMetadata).Return(nil).Times(1) + fm.EXPECT().DeleteIPs([]net.IP{ip1}, gomock.Any(), moduleReqMetadata).Return(nil).Times(1) + // Allow the extra modulePodReqMetadata delete (no-op) after fix + fm.EXPECT().DeleteIPs([]net.IP{ip1}, gomock.Any(), modulePodReqMetadata).Return(nil).AnyTimes() + // Pod is genuinely deleted — GetPodByIP returns nil + c.EXPECT().GetPodByIP(gomock.Any()).Return(nil).AnyTimes() + + me := &Module{ + RWMutex: &sync.RWMutex{}, + l: log.Logger().Named("MetricModule"), + pubsub: p, + configs: make([]*api.MetricsConfiguration, 0), + enricher: e, + wg: sync.WaitGroup{}, + registry: make(map[string]AdvMetricsInterface), + moduleCtx: context.Background(), + filterManager: fm, + daemonCache: c, + dirtyPods: common.NewDirtyCache(), + pubsubPodSub: "", + daemonConfig: cfg, + } + + me.appendIncludeList([]string{"ns1"}) + pod1 := common.NewRetinaEndpoint("pod1", "ns1", &common.IPAddresses{IPv4: ip1}) + me.PodCallBackFn(cache.NewCacheEvent(cache.EventTypePodAdded, pod1)) + me.applyDirtyPods() + + me.PodCallBackFn(cache.NewCacheEvent(cache.EventTypePodDeleted, pod1)) + me.applyDirtyPods() +} + +func TestAnnotatedPodNormalLifecycle(t *testing.T) { + _, err := log.SetupZapLogger(log.GetDefaultLogOpts()) + require.NoError(t, err) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + cfg, _ := kcfg.GetConfig(testCfgFile) + cfg.EnableAnnotations = true + + p := pubsub.NewMockPubSubInterface(ctrl) + e := enricher.NewMockEnricherInterface(ctrl) + fm := filtermanager.NewMockIFilterManager(ctrl) + c := cache.NewMockCacheInterface(ctrl) + + ip1 := net.IPv4(10, 0, 0, 1) + + fm.EXPECT().HasIP(gomock.Any()).Return(false).AnyTimes() + fm.EXPECT().AddIPs([]net.IP{ip1}, gomock.Any(), modulePodReqMetadata).Return(nil).Times(1) + fm.EXPECT().DeleteIPs([]net.IP{ip1}, gomock.Any(), modulePodReqMetadata).Return(nil).Times(1) + // Allow the extra moduleReqMetadata delete (no-op) after fix + fm.EXPECT().DeleteIPs([]net.IP{ip1}, gomock.Any(), moduleReqMetadata).Return(nil).AnyTimes() + // Pod is genuinely deleted — GetPodByIP returns nil + c.EXPECT().GetPodByIP(gomock.Any()).Return(nil).AnyTimes() + + me := &Module{ + RWMutex: &sync.RWMutex{}, + l: log.Logger().Named("MetricModule"), + pubsub: p, + configs: make([]*api.MetricsConfiguration, 0), + enricher: e, + wg: sync.WaitGroup{}, + registry: make(map[string]AdvMetricsInterface), + moduleCtx: context.Background(), + filterManager: fm, + daemonCache: c, + dirtyPods: common.NewDirtyCache(), + pubsubPodSub: "", + daemonConfig: cfg, + } + + pod1 := common.NewRetinaEndpoint("pod1", "ns1", &common.IPAddresses{IPv4: ip1}) + pod1.SetAnnotations(map[string]string{common.RetinaPodAnnotation: common.RetinaPodAnnotationValue}) + me.PodCallBackFn(cache.NewCacheEvent(cache.EventTypePodAdded, pod1)) + me.applyDirtyPods() + + me.PodCallBackFn(cache.NewCacheEvent(cache.EventTypePodDeleted, pod1)) + me.applyDirtyPods() +} + +// TestSpuriousDeleteIPReuse verifies that a DELETE event is ignored when the +// daemon cache still contains a pod at the same IP (IP reuse by a new pod). +func TestSpuriousDeleteIPReuse(t *testing.T) { + _, err := log.SetupZapLogger(log.GetDefaultLogOpts()) + require.NoError(t, err) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + cfg, _ := kcfg.GetConfig(testCfgFile) + cfg.EnableAnnotations = false + + p := pubsub.NewMockPubSubInterface(ctrl) + e := enricher.NewMockEnricherInterface(ctrl) + fm := filtermanager.NewMockIFilterManager(ctrl) + c := cache.NewMockCacheInterface(ctrl) + + ip1 := net.IPv4(10, 0, 0, 1) + + // Setup tracked namespace + c.EXPECT().GetIPsByNamespace("ns1").Return([]net.IP{}).Times(1) + fm.EXPECT().AddIPs([]net.IP{}, gomock.Any(), gomock.Any()).Return(nil).Times(1) + fm.EXPECT().HasIP(gomock.Any()).Return(false).AnyTimes() + fm.EXPECT().AddIPs([]net.IP{ip1}, gomock.Any(), moduleReqMetadata).Return(nil).Times(1) + + // Pod still exists in daemon cache (IP reused) — GetPodByIP returns a valid endpoint. + // No DeleteIPs calls should occur because the DELETE event is skipped. + newPod := common.NewRetinaEndpoint("pod2", "ns1", &common.IPAddresses{IPv4: ip1}) + c.EXPECT().GetPodByIP(ip1.String()).Return(newPod).Times(1) + + me := &Module{ + RWMutex: &sync.RWMutex{}, + l: log.Logger().Named("MetricModule"), + pubsub: p, + configs: make([]*api.MetricsConfiguration, 0), + enricher: e, + wg: sync.WaitGroup{}, + registry: make(map[string]AdvMetricsInterface), + moduleCtx: context.Background(), + filterManager: fm, + daemonCache: c, + dirtyPods: common.NewDirtyCache(), + pubsubPodSub: "", + daemonConfig: cfg, + } + + me.appendIncludeList([]string{"ns1"}) + + // Add original pod + pod1 := common.NewRetinaEndpoint("pod1", "ns1", &common.IPAddresses{IPv4: ip1}) + me.PodCallBackFn(cache.NewCacheEvent(cache.EventTypePodAdded, pod1)) + me.applyDirtyPods() + + // Simulate spurious DELETE — daemon cache already has pod2 at the same IP. + // The DELETE should be ignored; dirty pods delete list should remain empty. + me.PodCallBackFn(cache.NewCacheEvent(cache.EventTypePodDeleted, pod1)) + me.applyDirtyPods() // No DeleteIPs expected +} + +// TestPodCallBackConcurrentWithReconcile verifies that PodCallBackFn (which reads +// namespace maps under RLock) does not race with appendIncludeList (which mutates +// them under Lock). Run with -race to detect data races. +func TestPodCallBackConcurrentWithReconcile(t *testing.T) { + _, err := log.SetupZapLogger(log.GetDefaultLogOpts()) + require.NoError(t, err) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + cfg, _ := kcfg.GetConfig(testCfgFile) + cfg.EnableAnnotations = true + + p := pubsub.NewMockPubSubInterface(ctrl) + e := enricher.NewMockEnricherInterface(ctrl) + fm := filtermanager.NewMockIFilterManager(ctrl) + c := cache.NewMockCacheInterface(ctrl) + + c.EXPECT().GetIPsByNamespace(gomock.Any()).Return([]net.IP{}).AnyTimes() + fm.EXPECT().AddIPs(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + fm.EXPECT().DeleteIPs(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + fm.EXPECT().HasIP(gomock.Any()).Return(false).AnyTimes() + c.EXPECT().GetPodByIP(gomock.Any()).Return(nil).AnyTimes() + + me := &Module{ + RWMutex: &sync.RWMutex{}, + l: log.Logger().Named("MetricModule"), + pubsub: p, + configs: make([]*api.MetricsConfiguration, 0), + enricher: e, + wg: sync.WaitGroup{}, + registry: make(map[string]AdvMetricsInterface), + moduleCtx: context.Background(), + filterManager: fm, + daemonCache: c, + dirtyPods: common.NewDirtyCache(), + pubsubPodSub: "", + daemonConfig: cfg, + } + + me.appendIncludeList([]string{"ns1"}) + + pod := common.NewRetinaEndpoint("pod1", "ns1", &common.IPAddresses{IPv4: net.IPv4(10, 0, 0, 1)}) + pod.SetAnnotations(map[string]string{common.RetinaPodAnnotation: common.RetinaPodAnnotationValue}) + + // Run PodCallBackFn and appendIncludeList concurrently. + // With -race this will catch any unprotected map access. + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + for i := 0; i < 100; i++ { + me.PodCallBackFn(cache.NewCacheEvent(cache.EventTypePodAdded, pod)) + } + }() + go func() { + defer wg.Done() + for i := 0; i < 100; i++ { + me.Lock() + me.appendIncludeList([]string{"ns1", "ns2"}) + me.Unlock() + } + }() + wg.Wait() +} diff --git a/pkg/module/metrics/mock_basemetricsobject.go b/pkg/module/metrics/mock_basemetricsobject.go new file mode 100644 index 0000000000..f00d0b510f --- /dev/null +++ b/pkg/module/metrics/mock_basemetricsobject.go @@ -0,0 +1,162 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: basemetricsobject.go +// +// Generated by this command: +// +// mockgen -source=basemetricsobject.go -destination=mock_basemetricsobject.go -package=metrics +// + +// Package metrics is a generated GoMock package. +package metrics + +import ( + reflect "reflect" + + log "github.com/microsoft/retina/pkg/log" + gomock "go.uber.org/mock/gomock" +) + +// MockbaseMetricInterface is a mock of baseMetricInterface interface. +type MockbaseMetricInterface struct { + ctrl *gomock.Controller + recorder *MockbaseMetricInterfaceMockRecorder +} + +// MockbaseMetricInterfaceMockRecorder is the mock recorder for MockbaseMetricInterface. +type MockbaseMetricInterfaceMockRecorder struct { + mock *MockbaseMetricInterface +} + +// NewMockbaseMetricInterface creates a new mock instance. +func NewMockbaseMetricInterface(ctrl *gomock.Controller) *MockbaseMetricInterface { + mock := &MockbaseMetricInterface{ctrl: ctrl} + mock.recorder = &MockbaseMetricInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockbaseMetricInterface) EXPECT() *MockbaseMetricInterfaceMockRecorder { + return m.recorder +} + +// additionalLabels mocks base method. +func (m *MockbaseMetricInterface) additionalLabels() []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "additionalLabels") + ret0, _ := ret[0].([]string) + return ret0 +} + +// additionalLabels indicates an expected call of additionalLabels. +func (mr *MockbaseMetricInterfaceMockRecorder) additionalLabels() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "additionalLabels", reflect.TypeOf((*MockbaseMetricInterface)(nil).additionalLabels)) +} + +// clean mocks base method. +func (m *MockbaseMetricInterface) clean() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "clean") +} + +// clean indicates an expected call of clean. +func (mr *MockbaseMetricInterfaceMockRecorder) clean() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "clean", reflect.TypeOf((*MockbaseMetricInterface)(nil).clean)) +} + +// destinationCtx mocks base method. +func (m *MockbaseMetricInterface) destinationCtx() ContextOptionsInterface { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "destinationCtx") + ret0, _ := ret[0].(ContextOptionsInterface) + return ret0 +} + +// destinationCtx indicates an expected call of destinationCtx. +func (mr *MockbaseMetricInterfaceMockRecorder) destinationCtx() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "destinationCtx", reflect.TypeOf((*MockbaseMetricInterface)(nil).destinationCtx)) +} + +// getLogger mocks base method. +func (m *MockbaseMetricInterface) getLogger() *log.ZapLogger { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "getLogger") + ret0, _ := ret[0].(*log.ZapLogger) + return ret0 +} + +// getLogger indicates an expected call of getLogger. +func (mr *MockbaseMetricInterfaceMockRecorder) getLogger() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "getLogger", reflect.TypeOf((*MockbaseMetricInterface)(nil).getLogger)) +} + +// isAdvanced mocks base method. +func (m *MockbaseMetricInterface) isAdvanced() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "isAdvanced") + ret0, _ := ret[0].(bool) + return ret0 +} + +// isAdvanced indicates an expected call of isAdvanced. +func (mr *MockbaseMetricInterfaceMockRecorder) isAdvanced() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "isAdvanced", reflect.TypeOf((*MockbaseMetricInterface)(nil).isAdvanced)) +} + +// isLocalContext mocks base method. +func (m *MockbaseMetricInterface) isLocalContext() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "isLocalContext") + ret0, _ := ret[0].(bool) + return ret0 +} + +// isLocalContext indicates an expected call of isLocalContext. +func (mr *MockbaseMetricInterfaceMockRecorder) isLocalContext() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "isLocalContext", reflect.TypeOf((*MockbaseMetricInterface)(nil).isLocalContext)) +} + +// sourceCtx mocks base method. +func (m *MockbaseMetricInterface) sourceCtx() ContextOptionsInterface { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "sourceCtx") + ret0, _ := ret[0].(ContextOptionsInterface) + return ret0 +} + +// sourceCtx indicates an expected call of sourceCtx. +func (mr *MockbaseMetricInterfaceMockRecorder) sourceCtx() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "sourceCtx", reflect.TypeOf((*MockbaseMetricInterface)(nil).sourceCtx)) +} + +// trackedMetricLabels mocks base method. +func (m *MockbaseMetricInterface) trackedMetricLabels() [][]string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "trackedMetricLabels") + ret0, _ := ret[0].([][]string) + return ret0 +} + +// trackedMetricLabels indicates an expected call of trackedMetricLabels. +func (mr *MockbaseMetricInterfaceMockRecorder) trackedMetricLabels() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "trackedMetricLabels", reflect.TypeOf((*MockbaseMetricInterface)(nil).trackedMetricLabels)) +} + +// updated mocks base method. +func (m *MockbaseMetricInterface) updated(lbs []string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "updated", lbs) +} + +// updated indicates an expected call of updated. +func (mr *MockbaseMetricInterfaceMockRecorder) updated(lbs any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "updated", reflect.TypeOf((*MockbaseMetricInterface)(nil).updated), lbs) +} diff --git a/pkg/module/metrics/tcpflags.go b/pkg/module/metrics/tcpflags.go index 02c8eb539a..9114ca4d13 100644 --- a/pkg/module/metrics/tcpflags.go +++ b/pkg/module/metrics/tcpflags.go @@ -5,6 +5,7 @@ package metrics import ( "strings" + "time" v1 "github.com/cilium/cilium/api/v1/flow" api "github.com/microsoft/retina/crd/api/v1alpha1" @@ -24,20 +25,20 @@ const ( ) type TCPMetrics struct { - baseMetricObject + baseMetricInterface tcpFlagsMetrics metricsinit.GaugeVec } -func NewTCPMetrics(ctxOptions *api.MetricsContextOptions, fl *log.ZapLogger, isLocalContext enrichmentContext) *TCPMetrics { +func NewTCPMetrics(ctxOptions *api.MetricsContextOptions, fl *log.ZapLogger, isLocalContext enrichmentContext, ttl time.Duration) *TCPMetrics { if ctxOptions == nil || !strings.Contains(strings.ToLower(ctxOptions.MetricName), "flag") { return nil } fl = fl.Named("tcpflags-metricsmodule") fl.Info("Creating TCP Flags count metrics", zap.Any("options", ctxOptions)) - return &TCPMetrics{ - baseMetricObject: newBaseMetricsObject(ctxOptions, fl, isLocalContext), - } + t := &TCPMetrics{} + t.baseMetricInterface = newBaseMetricsObject(ctxOptions, fl, isLocalContext, t.expire, ttl) + return t } func (t *TCPMetrics) Init(metricName string) { @@ -54,17 +55,38 @@ func (t *TCPMetrics) getLabels() []string { labels := []string{ utils.Flag, } - if t.srcCtx != nil { - labels = append(labels, t.srcCtx.getLabels()...) + if t.sourceCtx() != nil { + labels = append(labels, t.sourceCtx().getLabels()...) } - if t.dstCtx != nil { - labels = append(labels, t.dstCtx.getLabels()...) + if t.destinationCtx() != nil { + labels = append(labels, t.destinationCtx().getLabels()...) } return labels } +func combineFlagsWithPrevious(flags []string, flow *v1.Flow) map[string]uint32 { + var combinedFlags map[string]uint32 + + previous := utils.PreviouslyObservedTCPFlags(flow) + if previous != nil { + combinedFlags = previous + } else { + combinedFlags = map[string]uint32{} + } + + for _, flag := range flags { + if _, ok := combinedFlags[flag]; !ok { + combinedFlags[flag] = 1 + } else { + combinedFlags[flag]++ + } + } + + return combinedFlags +} + func (t *TCPMetrics) ProcessFlow(flow *v1.Flow) { if flow == nil { return @@ -92,45 +114,64 @@ func (t *TCPMetrics) ProcessFlow(flow *v1.Flow) { } var srcLabels, dstLabels []string - if t.srcCtx != nil { - srcLabels = t.srcCtx.getValues(flow) + if t.sourceCtx() != nil { + srcLabels = t.sourceCtx().getValues(flow) } - if t.dstCtx != nil { - dstLabels = t.dstCtx.getValues(flow) + if t.destinationCtx() != nil { + dstLabels = t.destinationCtx().getValues(flow) } - for _, flag := range flags { + for flag, count := range combineFlagsWithPrevious(flags, flow) { labels := append([]string{flag}, srcLabels...) labels = append(labels, dstLabels...) - t.tcpFlagsMetrics.WithLabelValues(labels...).Inc() - t.l.Debug("TCP flag metric", zap.String("flag", flag), zap.Strings("labels", labels)) + t.update(labels, count) + t.getLogger().Debug("TCP flag metric", zap.String("flag", flag), zap.Strings("labels", labels), zap.Uint32("count", count)) } } func (t *TCPMetrics) processLocalCtxFlow(flow *v1.Flow, flags []string) { - labelValuesMap := t.srcCtx.getLocalCtxValues(flow) + labelValuesMap := t.sourceCtx().getLocalCtxValues(flow) if labelValuesMap == nil { return } + + combinedFlags := combineFlagsWithPrevious(flags, flow) + // Ingress values if l := len(labelValuesMap[ingress]); l > 0 { - for _, flag := range flags { + for flag, count := range combinedFlags { labels := append([]string{flag}, labelValuesMap[ingress]...) - t.tcpFlagsMetrics.WithLabelValues(labels...).Inc() - t.l.Debug("TCP flag metric", zap.String("flag", flag), zap.Strings("labels", labels)) + t.update(labels, count) + t.getLogger().Debug("TCP flag metric", zap.String("flag", flag), zap.Strings("labels", labels), zap.Uint32("count", count)) } } if l := len(labelValuesMap[egress]); l > 0 { - for _, flag := range flags { + for flag, count := range combinedFlags { labels := append([]string{flag}, labelValuesMap[egress]...) - t.tcpFlagsMetrics.WithLabelValues(labels...).Inc() - t.l.Debug("TCP flag metric", zap.String("flag", flag), zap.Strings("labels", labels)) + t.update(labels, count) + t.getLogger().Debug("TCP flag metric", zap.String("flag", flag), zap.Strings("labels", labels), zap.Uint32("count", count)) } } } +func (t *TCPMetrics) expire(labels []string) bool { + var d bool + if t.tcpFlagsMetrics != nil { + d = t.tcpFlagsMetrics.DeleteLabelValues(labels...) + if d { + metricsinit.MetricsExpiredCounter.WithLabelValues(TCPFlagsCountName).Inc() + } + } + return d +} + +func (t *TCPMetrics) update(labels []string, count uint32) { + t.tcpFlagsMetrics.WithLabelValues(labels...).Add(float64(count)) + t.updated(labels) +} + func (t *TCPMetrics) getFlagValues(flags *v1.TCPFlags) []string { f := make([]string, 0) if flags == nil { @@ -171,9 +212,14 @@ func (t *TCPMetrics) getFlagValues(flags *v1.TCPFlags) []string { f = append(f, utils.CWR) } + if flags.GetNS() { + f = append(f, utils.NS) + } + return f } func (t *TCPMetrics) Clean() { exporter.UnregisterMetric(exporter.AdvancedRegistry, metricsinit.ToPrometheusType(t.tcpFlagsMetrics)) + t.clean() } diff --git a/pkg/module/metrics/tcpflags_test.go b/pkg/module/metrics/tcpflags_test.go index 8b845df6bb..9be4e15b00 100644 --- a/pkg/module/metrics/tcpflags_test.go +++ b/pkg/module/metrics/tcpflags_test.go @@ -6,6 +6,7 @@ package metrics import ( "testing" + "time" "github.com/cilium/cilium/api/v1/flow" "github.com/microsoft/retina/crd/api/v1alpha1" @@ -15,6 +16,7 @@ import ( "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "go.uber.org/zap" + "log/slog" ) func TestNewTCPMetrics(t *testing.T) { @@ -159,7 +161,8 @@ func TestNewTCPMetrics(t *testing.T) { "source_service", "source_port", }, - metricCall: 1, + metricCall: 1, + trackedMetrics: 1, }, { name: "source opts with nil flow", @@ -227,7 +230,8 @@ func TestNewTCPMetrics(t *testing.T) { "source_service", "source_port", }, - metricCall: 7, + metricCall: 7, + trackedMetrics: 7, }, { name: "dest opts with flow with all flags ", @@ -266,7 +270,8 @@ func TestNewTCPMetrics(t *testing.T) { "destination_service", "destination_port", }, - metricCall: 7, + metricCall: 7, + trackedMetrics: 7, }, { name: "dest opts with flow with all but syn flags ", @@ -304,7 +309,8 @@ func TestNewTCPMetrics(t *testing.T) { "destination_service", "destination_port", }, - metricCall: 7, + metricCall: 7, + trackedMetrics: 7, }, { name: "dest opts with flow with all flags dropped verdict", @@ -382,8 +388,9 @@ func TestNewTCPMetrics(t *testing.T) { "service", "port", }, - localContext: localContext, - metricCall: 7, + localContext: localContext, + metricCall: 7, + trackedMetrics: 7, }, { name: "local ctx dest opts with flow with all flags ", @@ -462,8 +469,9 @@ func TestNewTCPMetrics(t *testing.T) { "service", "port", }, - localContext: localContext, - metricCall: 14, + localContext: localContext, + metricCall: 14, + trackedMetrics: 7, }, } @@ -471,7 +479,7 @@ func TestNewTCPMetrics(t *testing.T) { log.Logger().Info("Running test name", zap.String("name", tc.name)) ctrl := gomock.NewController(t) - tcp := NewTCPMetrics(tc.opts, log.Logger(), tc.localContext) + tcp := NewTCPMetrics(tc.opts, log.Logger(), tc.localContext, time.Duration(0)) if tc.nilObj { assert.Nil(t, tcp, "forward metrics should be nil Test Name: %s", tc.name) continue @@ -488,10 +496,37 @@ func TestNewTCPMetrics(t *testing.T) { }) tcpFlagMockMetrics.EXPECT().WithLabelValues(gomock.Any()).Return(testmetric).Times(tc.metricCall) - assert.Equal(t, tc.checkIsAdvance, tcp.advEnable, "IsAdvance should be %v Test Name: %s", tc.checkIsAdvance, tc.name) + assert.Equal(t, tc.checkIsAdvance, tcp.isAdvanced(), "IsAdvance should be %v Test Name: %s", tc.checkIsAdvance, tc.name) assert.Equal(t, tc.exepectedLabels, tcp.getLabels(), "labels should be %v Test Name: %s", tc.exepectedLabels, tc.name) tcp.ProcessFlow(tc.f) + + assert.Equal(t, 0, len(tcp.trackedMetricLabels()), "there should be no tracked metrics when TTL is infinite Test Name: %s", tc.name) + + // Test TTL based expiration + metricsinit.InitializeMetrics(slog.Default()) + + // Set the TTL to something high to ensure that our call to expire is the only one that expires the metrics + tcp = NewTCPMetrics(tc.opts, log.Logger(), tc.localContext, time.Minute) + tcp.tcpFlagsMetrics = tcpFlagMockMetrics + + tcpFlagMockMetrics.EXPECT().WithLabelValues(gomock.Any()).Return(testmetric).Times(tc.metricCall) + + tcp.ProcessFlow(tc.f) + + tcpFlagMockMetrics.EXPECT().DeleteLabelValues(gomock.Any()).Return(true).Times(tc.trackedMetrics) + + for _, ls := range tcp.trackedMetricLabels() { + assert.True(t, tcp.expire(ls), "metric should expire successfully Test Name: %s", tc.name) + } + + // Test that clean calls the base object + baseMetricObjectMock := NewMockbaseMetricInterface(ctrl) + tcp.baseMetricInterface = baseMetricObjectMock + + baseMetricObjectMock.EXPECT().clean().Times(1) + + tcp.Clean() ctrl.Finish() } } diff --git a/pkg/module/metrics/tcpretrans.go b/pkg/module/metrics/tcpretrans.go index 3e307eabf4..623fbcf933 100644 --- a/pkg/module/metrics/tcpretrans.go +++ b/pkg/module/metrics/tcpretrans.go @@ -5,6 +5,7 @@ package metrics import ( "strings" + "time" v1 "github.com/cilium/cilium/api/v1/flow" api "github.com/microsoft/retina/crd/api/v1alpha1" @@ -24,20 +25,20 @@ const ( ) type TCPRetransMetrics struct { - baseMetricObject + baseMetricInterface tcpRetransMetrics metricsinit.GaugeVec } -func NewTCPRetransMetrics(ctxOptions *api.MetricsContextOptions, fl *log.ZapLogger, isLocalContext enrichmentContext) *TCPRetransMetrics { +func NewTCPRetransMetrics(ctxOptions *api.MetricsContextOptions, fl *log.ZapLogger, isLocalContext enrichmentContext, ttl time.Duration) *TCPRetransMetrics { if ctxOptions == nil || !strings.Contains(strings.ToLower(ctxOptions.MetricName), "retrans") { return nil } fl = fl.Named("tcpretrans-metricsmodule") fl.Info("Creating TCP retransmit count metrics", zap.Any("options", ctxOptions)) - return &TCPRetransMetrics{ - baseMetricObject: newBaseMetricsObject(ctxOptions, fl, isLocalContext), - } + t := &TCPRetransMetrics{} + t.baseMetricInterface = newBaseMetricsObject(ctxOptions, fl, isLocalContext, t.expire, ttl) + return t } func (t *TCPRetransMetrics) Init(metricName string) { @@ -52,14 +53,14 @@ func (t *TCPRetransMetrics) Init(metricName string) { func (t *TCPRetransMetrics) getLabels() []string { labels := []string{utils.Direction} - if t.srcCtx != nil { - labels = append(labels, t.srcCtx.getLabels()...) - t.l.Info("src labels", zap.Any("labels", labels)) + if t.sourceCtx() != nil { + labels = append(labels, t.sourceCtx().getLabels()...) + t.getLogger().Info("src labels", zap.Any("labels", labels)) } - if t.dstCtx != nil { - labels = append(labels, t.dstCtx.getLabels()...) - t.l.Info("dst labels", zap.Any("labels", labels)) + if t.destinationCtx() != nil { + labels = append(labels, t.destinationCtx().getLabels()...) + t.getLogger().Info("dst labels", zap.Any("labels", labels)) } return labels @@ -82,42 +83,59 @@ func (t *TCPRetransMetrics) ProcessFlow(flow *v1.Flow) { } labels := []string{flow.TrafficDirection.String()} - if t.srcCtx != nil { - srcLabels := t.srcCtx.getValues(flow) + if t.sourceCtx() != nil { + srcLabels := t.sourceCtx().getValues(flow) if len(srcLabels) > 0 { labels = append(labels, srcLabels...) } } - if t.dstCtx != nil { - dstLabels := t.dstCtx.getValues(flow) + if t.destinationCtx() != nil { + dstLabels := t.destinationCtx().getValues(flow) if len(dstLabels) > 0 { labels = append(labels, dstLabels...) } } - t.tcpRetransMetrics.WithLabelValues(labels...).Inc() + t.update(labels) } func (t *TCPRetransMetrics) processLocalCtxFlow(flow *v1.Flow) { - labelValuesMap := t.srcCtx.getLocalCtxValues(flow) + labelValuesMap := t.sourceCtx().getLocalCtxValues(flow) if labelValuesMap == nil { return } if len(labelValuesMap[ingress]) > 0 { labels := append([]string{ingress}, labelValuesMap[ingress]...) - t.tcpRetransMetrics.WithLabelValues(labels...).Inc() - t.l.Debug("tcp retransmission count metric in INGRESS in local ctx", zap.Any("labels", labels)) + t.update(labels) + t.getLogger().Debug("tcp retransmission count metric in INGRESS in local ctx", zap.Any("labels", labels)) } if len(labelValuesMap[egress]) > 0 { labels := append([]string{egress}, labelValuesMap[egress]...) - t.tcpRetransMetrics.WithLabelValues(labels...).Inc() - t.l.Debug("tcp retransmission count metric in EGRESS in local ctx", zap.Any("labels", labels)) + t.update(labels) + t.getLogger().Debug("tcp retransmission count metric in EGRESS in local ctx", zap.Any("labels", labels)) } } +func (t *TCPRetransMetrics) expire(labels []string) bool { + var d bool + if t.tcpRetransMetrics != nil { + d = t.tcpRetransMetrics.DeleteLabelValues(labels...) + if d { + metricsinit.MetricsExpiredCounter.WithLabelValues(TCPRetransCountName).Inc() + } + } + return d +} + +func (t *TCPRetransMetrics) update(labels []string) { + t.tcpRetransMetrics.WithLabelValues(labels...).Inc() + t.updated(labels) +} + func (t *TCPRetransMetrics) Clean() { exporter.UnregisterMetric(exporter.AdvancedRegistry, metricsinit.ToPrometheusType(t.tcpRetransMetrics)) + t.clean() } diff --git a/pkg/module/metrics/types.go b/pkg/module/metrics/types.go index 41d923a661..5887c1ab5d 100644 --- a/pkg/module/metrics/types.go +++ b/pkg/module/metrics/types.go @@ -44,6 +44,9 @@ const ( // workload context option workloadCtxOption = "workload" + // zone context option + zoneCtxOption = "zone" + // localContext means only the pods on this node will be watched // and only these events will be enriched localContext enrichmentContext = "local" @@ -95,6 +98,7 @@ type ContextOptions struct { Workload bool Service bool Port bool + Zone bool } type DirtyCachePod struct { @@ -129,6 +133,8 @@ func NewCtxOption(opts []string, option ctxOptionType) *ContextOptions { c.Service = true case portCtxOption: c.Port = true + case zoneCtxOption: + c.Zone = true } } @@ -170,6 +176,10 @@ func (c *ContextOptions) getLabels() []string { labels = append(labels, prefix+portCtxOption) } + if c.Zone { + labels = append(labels, prefix+zoneCtxOption) + } + return labels } @@ -310,6 +320,14 @@ func (c *ContextOptions) getByDirectionValues(f *flow.Flow, dest bool) []string } } + if c.Zone { + if !dest { + values = append(values, utils.SourceZone(f)) + } else { + values = append(values, utils.DestinationZone(f)) + } + } + return values } diff --git a/pkg/module/metrics/types_test.go b/pkg/module/metrics/types_test.go index d488bbd358..1d901e3d40 100644 --- a/pkg/module/metrics/types_test.go +++ b/pkg/module/metrics/types_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/cilium/cilium/api/v1/flow" + "github.com/microsoft/retina/pkg/utils" "github.com/stretchr/testify/assert" ) @@ -130,6 +131,34 @@ func TestNewCtxOps(t *testing.T) { }, expectedVals: []string{"", "unknown", "unknown"}, }, + { + name: "source zone option", + opts: []string{"namespace", "zone"}, + ctxType: source, + expected: []string{ + "source_namespace", "source_zone", + }, + f: flowWithZones("zone-1", "zone-2"), + expectedVals: []string{"unknown", "zone-1"}, + }, + { + name: "destination zone option", + opts: []string{"namespace", "zone"}, + ctxType: destination, + expected: []string{ + "destination_namespace", "destination_zone", + }, + f: flowWithZones("zone-1", "zone-2"), + expectedVals: []string{"unknown", "zone-2"}, + }, + { + name: "zone with no extensions", + opts: []string{"zone"}, + ctxType: source, + expected: []string{"source_zone"}, + f: &flow.Flow{}, + expectedVals: []string{"unknown"}, + }, } for _, tc := range tt { @@ -139,3 +168,12 @@ func TestNewCtxOps(t *testing.T) { assert.Equal(t, tc.expectedVals, values, "values should match %s", tc.name) } } + +// flowWithZones creates a flow with zone extensions populated. +func flowWithZones(srcZone, dstZone string) *flow.Flow { + f := &flow.Flow{} + ext := utils.NewExtensions() + utils.AddZones(ext, srcZone, dstZone) + utils.SetExtensions(f, ext) + return f +} diff --git a/pkg/monitoragent/cell_linux.go b/pkg/monitoragent/cell_linux.go index 9a9055d09a..5a0f9dc720 100644 --- a/pkg/monitoragent/cell_linux.go +++ b/pkg/monitoragent/cell_linux.go @@ -2,16 +2,17 @@ package monitoragent import ( "context" + "log/slog" + "sync" "github.com/cilium/cilium/pkg/defaults" - "github.com/cilium/cilium/pkg/logging" "github.com/cilium/cilium/pkg/logging/logfields" ciliumagent "github.com/cilium/cilium/pkg/monitor/agent" "github.com/cilium/cilium/pkg/monitor/agent/consumer" "github.com/cilium/cilium/pkg/monitor/agent/listener" "github.com/cilium/ebpf" "github.com/cilium/hive/cell" - "github.com/sirupsen/logrus" + retinalog "github.com/microsoft/retina/pkg/log" "github.com/spf13/pflag" ) @@ -24,9 +25,19 @@ var ( cell.Config(defaultConfig), ) - log = logging.DefaultLogger.WithField(logfields.LogSubsys, "monitor-agent") + logOnce sync.Once + cachedLog *slog.Logger ) +// log returns a zap-backed slog logger. Resolved lazily so that SetupZapLogger +// (run at program startup, after package init) is the source. +func log() *slog.Logger { + logOnce.Do(func() { + cachedLog = retinalog.SlogLogger().With(logfields.LogSubsys, "monitor-agent") + }) + return cachedLog +} + type AgentConfig struct { // EnableMonitor enables the monitor unix domain socket server EnableMonitor bool @@ -48,7 +59,6 @@ type agentParams struct { cell.In Lifecycle cell.Lifecycle - Log logrus.FieldLogger Config AgentConfig } diff --git a/pkg/monitoragent/monitoragent_linux.go b/pkg/monitoragent/monitoragent_linux.go index 757e4a9ac1..c171ee82e3 100644 --- a/pkg/monitoragent/monitoragent_linux.go +++ b/pkg/monitoragent/monitoragent_linux.go @@ -13,7 +13,6 @@ import ( "github.com/cilium/cilium/pkg/monitor/agent/listener" "github.com/cilium/cilium/pkg/monitor/api" "github.com/cilium/cilium/pkg/monitor/payload" - "github.com/sirupsen/logrus" ) var ( @@ -107,7 +106,7 @@ func (a *monitorAgent) RegisterNewListener(newListener listener.MonitorListener) defer a.Unlock() if isCtxDone(a.ctx) { - log.Debug("RegisterNewListener called on stopped monitor") + log().Debug("RegisterNewListener called on stopped monitor") newListener.Close() return } @@ -118,13 +117,13 @@ func (a *monitorAgent) RegisterNewListener(newListener listener.MonitorListener) a.listeners[newListener] = struct{}{} default: newListener.Close() - log.WithField("version", version).Error("Closing listener from unsupported monitor client version") + log().Error("Closing listener from unsupported monitor client version", "version", version) } - log.WithFields(logrus.Fields{ - "count.listener": len(a.listeners), - "version": version, - }).Debug("New listener connected") + log().Debug("New listener connected", + "count.listener", len(a.listeners), + "version", version, + ) } func (a *monitorAgent) RemoveListener(ml listener.MonitorListener) { @@ -137,20 +136,23 @@ func (a *monitorAgent) RemoveListener(ml listener.MonitorListener) { // Remove the listener and close it. delete(a.listeners, ml) - log.WithFields(logrus.Fields{ - "count.listener": len(a.listeners), - "version": ml.Version(), - }).Debug("Removed listener") + log().Debug("Removed listener", + "count.listener", len(a.listeners), + "version", ml.Version(), + ) ml.Close() } func (a *monitorAgent) RegisterNewConsumer(newConsumer consumer.MonitorConsumer) { if a == nil || newConsumer == nil { + log().Info("RegisterNewConsumer called with nil agent or consumer", + "agentNil", a == nil, + "consumerNil", newConsumer == nil) return } if isCtxDone(a.ctx) { - log.Debug("RegisterNewConsumer called on stopped monitor") + log().Debug("RegisterNewConsumer called on stopped monitor") return } @@ -158,6 +160,9 @@ func (a *monitorAgent) RegisterNewConsumer(newConsumer consumer.MonitorConsumer) defer a.Unlock() a.consumers[newConsumer] = struct{}{} + log().Info("Registered new consumer with monitor agent", + "consumerCount", len(a.consumers), + "consumerType", fmt.Sprintf("%T", newConsumer)) } func (a *monitorAgent) RemoveConsumer(mc consumer.MonitorConsumer) { @@ -207,8 +212,7 @@ func (a *monitorAgent) sendToListenersLocked(pl *payload.Payload) { } } -// notifyAgentEvent notifies all consumers about an agent event. -func (a *monitorAgent) notifyAgentEvent(typ int, message interface{}) { +func (a *monitorAgent) notifyAgentEvent(typ int, message any) { a.Lock() defer a.Unlock() for mc := range a.consumers { diff --git a/pkg/plugin/ciliumeventobserver/ciliumeventobserver_linux.go b/pkg/plugin/ciliumeventobserver/ciliumeventobserver_linux.go index 5373e5c4fd..324c697e08 100644 --- a/pkg/plugin/ciliumeventobserver/ciliumeventobserver_linux.go +++ b/pkg/plugin/ciliumeventobserver/ciliumeventobserver_linux.go @@ -156,24 +156,23 @@ func (c *ciliumeventobserver) monitorLoop(ctx context.Context) error { decoder := gob.NewDecoder(c.connection) for { var pl payload.Payload - select { - case <-ctx.Done(): // cancelled or done - c.l.Info("Context done, exiting monitor loop") - return nil - default: - if err := pl.DecodeBinary(decoder); err != nil { - if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { - return err //nolint:wrapcheck // Error is handled by the caller - } - c.l.Warn("Failed to decode payload from cilium", zap.Error(err)) - metrics.LostEventsCounter.WithLabelValues(parserMetric, name).Inc() - continue + if err := pl.DecodeBinary(decoder); err != nil { + // Check if context was cancelled (e.g. connection closed during shutdown). + if ctx.Err() != nil { + c.l.Info("Context done, exiting monitor loop") + return ctx.Err() //nolint:wrapcheck // no additional context needed } - select { - case c.payloadEvents <- &pl: - default: - metrics.LostEventsCounter.WithLabelValues(utils.BufferedChannel, name).Inc() + if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { + return err //nolint:wrapcheck // Error is handled by the caller } + c.l.Warn("Failed to decode payload from cilium", zap.Error(err)) + metrics.LostEventsCounter.WithLabelValues(parserMetric, name).Inc() + continue + } + select { + case c.payloadEvents <- &pl: + default: + metrics.LostEventsCounter.WithLabelValues(utils.BufferedChannel, name).Inc() } } } diff --git a/pkg/plugin/ciliumeventobserver/ciliumeventobserver_linux_test.go b/pkg/plugin/ciliumeventobserver/ciliumeventobserver_linux_test.go index 92eec5ce1f..b0b4f53053 100644 --- a/pkg/plugin/ciliumeventobserver/ciliumeventobserver_linux_test.go +++ b/pkg/plugin/ciliumeventobserver/ciliumeventobserver_linux_test.go @@ -4,6 +4,7 @@ package ciliumeventobserver import ( "context" "errors" + "log/slog" "net" "testing" "time" @@ -13,10 +14,12 @@ import ( "github.com/cilium/cilium/pkg/monitor" monitorAPI "github.com/cilium/cilium/pkg/monitor/api" "github.com/cilium/cilium/pkg/monitor/payload" + "github.com/gopacket/gopacket/layers" "github.com/microsoft/retina/pkg/config" "github.com/microsoft/retina/pkg/controllers/cache" "github.com/microsoft/retina/pkg/enricher" "github.com/microsoft/retina/pkg/log" + "github.com/microsoft/retina/pkg/metrics" "github.com/microsoft/retina/pkg/pubsub" "gotest.tools/assert" ) @@ -46,19 +49,20 @@ func TestStartError(t *testing.T) { func TestStart(t *testing.T) { ctxWithCancel, cancel := context.WithCancel(context.Background()) - defer cancel() _, _ = log.SetupZapLogger(log.GetDefaultLogOpts()) cfg := &config.Config{ EnablePodLevel: true, } cil := New(cfg) - exChan := make(chan *v1.Event) + exChan := make(chan *v1.Event, 1) _ = cil.SetupChannel(exChan) _ = cil.Init() md := NewMockDialer(false) cil.(*ciliumeventobserver).d = md cil.(*ciliumeventobserver).connection = md.reader + cil.(*ciliumeventobserver).retryDelay = 1 * time.Millisecond + cil.(*ciliumeventobserver).maxAttempts = 1 go cil.Start(ctxWithCancel) //nolint:errcheck // do not need for test pl := getPayload() @@ -66,6 +70,11 @@ func TestStart(t *testing.T) { _, _ = md.writer.Write(msg) event := <-exChan assert.Assert(t, event != nil) + + // Clean up: cancel context then close pipe to unblock monitorLoop. + cancel() + md.reader.Close() + md.writer.Close() } func TestMonitorLoop(t *testing.T) { @@ -92,19 +101,23 @@ func TestMonitorLoop(t *testing.T) { time.Sleep(2 * time.Second) plEvent := <-cil.(*ciliumeventobserver).payloadEvents assert.Assert(t, plEvent != nil) + + // Clean up: cancel context then close pipe to unblock monitorLoop. cancel() + md.reader.Close() + md.writer.Close() } func TestParse(t *testing.T) { ctxWithCancel, cancel := context.WithCancel(context.Background()) - defer cancel() _, _ = log.SetupZapLogger(log.GetDefaultLogOpts()) + metrics.InitializeMetrics(slog.Default()) cfg := &config.Config{ EnablePodLevel: true, } cil := New(cfg) _ = cil.Init() - exChannel := make(chan *v1.Event) + exChannel := make(chan *v1.Event, 1) _ = cil.SetupChannel(exChannel) cil.(*ciliumeventobserver).retryDelay = 1 * time.Millisecond cil.(*ciliumeventobserver).maxAttempts = 1 @@ -123,6 +136,11 @@ func TestParse(t *testing.T) { assert.Assert(t, len(cil.(*ciliumeventobserver).payloadEvents) == 0) event := <-exChannel assert.Assert(t, event != nil) + + // Clean up: cancel context then close pipe to unblock monitorLoop. + cancel() + md.reader.Close() + md.writer.Close() } func getPayload() payload.Payload { @@ -131,12 +149,30 @@ func getPayload() payload.Payload { SubType: uint8(130), } - data, _ := testutils.CreateL3L4Payload(dn) + eth := &layers.Ethernet{ + SrcMAC: net.HardwareAddr{1, 2, 3, 4, 5, 6}, + DstMAC: net.HardwareAddr{6, 5, 4, 3, 2, 1}, + EthernetType: layers.EthernetTypeIPv4, + } + ip := &layers.IPv4{ + SrcIP: net.IP{1, 2, 3, 4}, + DstIP: net.IP{5, 6, 7, 8}, + Version: 4, + Protocol: layers.IPProtocolTCP, + TTL: 64, + } + tcp := &layers.TCP{ + SrcPort: 12345, + DstPort: 80, + } + _ = tcp.SetNetworkLayerForChecksum(ip) + + data, _ := testutils.CreateL3L4Payload(dn, eth, ip, tcp) pl := payload.Payload{ Data: data, CPU: 0, Lost: 0, - Type: 9, + Type: payload.EventSample, } return pl } diff --git a/pkg/plugin/ciliumeventobserver/parser_linux.go b/pkg/plugin/ciliumeventobserver/parser_linux.go index 78e6a772b9..cbf3b2d7e8 100644 --- a/pkg/plugin/ciliumeventobserver/parser_linux.go +++ b/pkg/plugin/ciliumeventobserver/parser_linux.go @@ -17,8 +17,7 @@ import ( ) var ( - ErrNotImplemented = errors.New("Error, not implemented") - ErrEmptyData = errors.New("empty data") + ErrEmptyData = errors.New("empty data") ) func (p *parser) Init() error { @@ -31,7 +30,6 @@ func (p *parser) Init() error { &hptestutils.NoopServiceGetter, &hptestutils.NoopLinkGetter, &hptestutils.NoopPodMetadataGetter, - true, ) if err != nil { p.l.Fatal("Failed to create parser", zap.Error(err)) @@ -75,7 +73,7 @@ func (p *parser) Decode(pl *payload.Payload) (*v1.Event, error) { // Log Records can be DNS traces for CNP related pods // AccessLogs can also reflect kafka related metrics monEvent.Payload = &observerTypes.AgentEvent{} - return nil, ErrNotImplemented //nolint:goerr113 //no specific handling expected + return nil, nil //nolint:goerr113 //no specific handling expected // MessageTypeTraceSock and MessageTypeDebug are also perf events but have their own dedicated decoders in cilium. case monitorAPI.MessageTypeDrop, monitorAPI.MessageTypeTrace, monitorAPI.MessageTypePolicyVerdict, monitorAPI.MessageTypeCapture: perfEvent := &observerTypes.PerfEvent{} diff --git a/pkg/plugin/common/common_linux.go b/pkg/plugin/common/common_linux.go index 16f7695722..2c7e196c5e 100644 --- a/pkg/plugin/common/common_linux.go +++ b/pkg/plugin/common/common_linux.go @@ -22,18 +22,7 @@ import ( "golang.org/x/sys/unix" ) -//go:generate go run go.uber.org/mock/mockgen@v0.4.0 -destination=mocks/mock_types.go -package=mocks . ITracer - -// Interface for IG tracers. -// Ref: https://pkg.go.dev/github.com/inspektor-gadget/inspektor-gadget@v0.18.1/pkg/gadgets/trace/dns/tracer#Tracer -type ITracer interface { - SetEventHandler(interface{}) - Attach(pid uint32) error - Detach(pid uint32) error - Close() -} - -// Interface for IG event handlers. Maps to cilum Flow. +// ProtocolToFlow converts a protocol string to the corresponding Unix protocol number. func ProtocolToFlow(protocol string) int { switch protocol { case "tcp": @@ -138,3 +127,13 @@ func readIDField(path string) string { } return "" } + +// IsFtraceEnabled checks if ftrace is enabled in the kernel. +// This is required for fexit/fentry programs to work. +func IsFtraceEnabled() bool { + data, err := os.ReadFile("/proc/sys/kernel/ftrace_enabled") + if err != nil { + return false + } + return strings.TrimSpace(string(data)) == "1" +} diff --git a/pkg/plugin/common/mocks/mock_types.go b/pkg/plugin/common/mocks/mock_types.go deleted file mode 100644 index 700efb176e..0000000000 --- a/pkg/plugin/common/mocks/mock_types.go +++ /dev/null @@ -1,91 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/microsoft/retina/pkg/plugin/common (interfaces: ITracer) -// -// Generated by this command: -// -// mockgen -destination=mocks/mock_types.go -package=mocks . ITracer -// - -// Package mocks is a generated GoMock package. -package mocks - -import ( - reflect "reflect" - - gomock "go.uber.org/mock/gomock" -) - -// MockITracer is a mock of ITracer interface. -type MockITracer struct { - ctrl *gomock.Controller - recorder *MockITracerMockRecorder -} - -// MockITracerMockRecorder is the mock recorder for MockITracer. -type MockITracerMockRecorder struct { - mock *MockITracer -} - -// NewMockITracer creates a new mock instance. -func NewMockITracer(ctrl *gomock.Controller) *MockITracer { - mock := &MockITracer{ctrl: ctrl} - mock.recorder = &MockITracerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockITracer) EXPECT() *MockITracerMockRecorder { - return m.recorder -} - -// Attach mocks base method. -func (m *MockITracer) Attach(arg0 uint32) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Attach", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// Attach indicates an expected call of Attach. -func (mr *MockITracerMockRecorder) Attach(arg0 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Attach", reflect.TypeOf((*MockITracer)(nil).Attach), arg0) -} - -// Close mocks base method. -func (m *MockITracer) Close() { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Close") -} - -// Close indicates an expected call of Close. -func (mr *MockITracerMockRecorder) Close() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockITracer)(nil).Close)) -} - -// Detach mocks base method. -func (m *MockITracer) Detach(arg0 uint32) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Detach", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// Detach indicates an expected call of Detach. -func (mr *MockITracerMockRecorder) Detach(arg0 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Detach", reflect.TypeOf((*MockITracer)(nil).Detach), arg0) -} - -// SetEventHandler mocks base method. -func (m *MockITracer) SetEventHandler(arg0 any) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetEventHandler", arg0) -} - -// SetEventHandler indicates an expected call of SetEventHandler. -func (mr *MockITracerMockRecorder) SetEventHandler(arg0 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetEventHandler", reflect.TypeOf((*MockITracer)(nil).SetEventHandler), arg0) -} diff --git a/pkg/plugin/conntrack/_cprog/conntrack.c b/pkg/plugin/conntrack/_cprog/conntrack.c index ffc7fd34f6..f5fb2b3729 100644 --- a/pkg/plugin/conntrack/_cprog/conntrack.c +++ b/pkg/plugin/conntrack/_cprog/conntrack.c @@ -21,33 +21,63 @@ struct conntrackmetadata { bytes_*_count indicates the number of bytes sent and received in the forward and reply direction. These values will be based on the conntrack entry. */ - __u64 bytes_forward_count; - __u64 bytes_reply_count; + __u64 bytes_tx_count; + __u64 bytes_rx_count; /* packets_*_count indicates the number of packets sent and received in the forward and reply direction. These values will be based on the conntrack entry. */ - __u32 packets_forward_count; - __u32 packets_reply_count; + __u32 packets_tx_count; + __u32 packets_rx_count; +}; + +/** + * The structure representing the count of observed TCP flags. + * To observe new flags, they should be added to this structure and _ct_record_tcp_flags updated. + */ +struct tcpflagscount +{ + __u32 syn; + __u32 ack; + __u32 fin; + __u32 rst; + __u32 psh; + __u32 urg; + __u32 ece; + __u32 cwr; + __u32 ns; }; struct packet { - __u64 t_nsec; // timestamp in nanoseconds - __u32 bytes; // packet size in bytes - __u32 src_ip; - __u32 dst_ip; - __u16 src_port; - __u16 dst_port; - struct tcpmetadata tcp_metadata; // TCP metadata - __u8 observation_point; - __u8 traffic_direction; - __u8 proto; - __u8 flags; // For TCP packets, this is the TCP flags. For UDP packets, this is will always be 1 for conntrack purposes. - bool is_reply; + __u64 t_nsec; // timestamp in nanoseconds + __u32 bytes; // packet size in bytes + __u32 src_ip; + __u32 dst_ip; + __u16 src_port; + __u16 dst_port; + struct tcpmetadata tcp_metadata; // TCP metadata + __u8 observation_point; + __u8 traffic_direction; + __u8 proto; + __u16 flags; // For TCP packets, this is the TCP flags. For UDP packets, this is will always be 1 for conntrack purposes. + bool is_reply; + __u32 previously_observed_packets; // When sampling, this is the number of observed packets since the last report. + __u32 previously_observed_bytes; // When sampling, this is the number of observed bytes since the last report. + struct tcpflagscount previously_observed_flags; // When sampling, this is the previously observed TCP flags since the last report. struct conntrackmetadata conntrack_metadata; }; +/** + * The structure representing whether or not to report a packet and associated metadata. + */ +struct packetreport +{ + __u32 previously_observed_packets; + __u32 previously_observed_bytes; + struct tcpflagscount previously_observed_flags; + bool report; +}; /** * The structure representing an ipv4 5-tuple key in the connection tracking map. @@ -59,6 +89,7 @@ struct ct_v4_key { __u16 dst_port; __u8 proto; }; + /** * The structure representing a connection in the connection tracking map. */ @@ -69,6 +100,21 @@ struct ct_entry { */ __u32 last_report_tx_dir; __u32 last_report_rx_dir; + /** + * bytes_seen_since_last_report_*_dir stores the number of bytes observed since the last packet event was reported. + */ + __u32 bytes_seen_since_last_report_tx_dir; + __u32 bytes_seen_since_last_report_rx_dir; + /** + * packets_seen_since_last_report_*_dir stores the number of packets observed since the last packet event was reported. + */ + __u32 packets_seen_since_last_report_tx_dir; + __u32 packets_seen_since_last_report_rx_dir; + /** + * flags_seen_since_last_report_*_dir stores the number of TCP flags observed since the last packet event was reported. + */ + struct tcpflagscount flags_seen_since_last_report_tx_dir; + struct tcpflagscount flags_seen_since_last_report_rx_dir; /** * traffic_direction indicates the direction of the connection in relation to the host. * If the connection is initiated from within the host, the traffic_direction is egress. Otherwise, the traffic_direction is ingress. @@ -95,6 +141,62 @@ struct { __uint(pinning, LIBBPF_PIN_BY_NAME); // needs pinning so this can be access from other processes .i.e debug cli } retina_conntrack SEC(".maps"); +/** + * Helper function to update the count of observed TCP flags. + * @arg flags The observed flags. + * @arg count The TCP flag count to update. + */ +static inline void _ct_record_tcp_flags(__u8 flags, struct tcpflagscount *count) { + if (!count) { + return; + } + if (flags & TCP_SYN) { + if (count->syn < UINT32_MAX) { + count->syn += 1; + } + } + if (flags & TCP_ACK) { + if (count->ack < UINT32_MAX) { + count->ack += 1; + } + } + if (flags & TCP_FIN) { + if (count->fin < UINT32_MAX) { + count->fin += 1; + } + } + if (flags & TCP_RST) { + if (count->rst < UINT32_MAX) { + count->rst += 1; + } + } + if (flags & TCP_PSH) { + if (count->psh < UINT32_MAX) { + count->psh += 1; + } + } + if (flags & TCP_URG) { + if (count->urg < UINT32_MAX) { + count->urg += 1; + } + } + if (flags & TCP_ECE) { + if (count->ece < UINT32_MAX) { + count->ece += 1; + } + } + if (flags & TCP_CWR) { + if (count->cwr < UINT32_MAX) { + count->cwr += 1; + } + } + if (flags & TCP_NS) { + if (count->ns < UINT32_MAX) { + count->ns += 1; + } + } +} + /** * Helper function to reverse a key. * @arg reverse_key The key to store the reversed key. @@ -130,8 +232,10 @@ static __always_inline __u8 _ct_get_traffic_direction(__u8 observation_point) { * @arg *p pointer to the packet to be processed. * @arg key The key to be used to create the new connection. * @arg observation_point The point in the network stack where the packet is observed. + * @arg is_reply true if the packet is a SYN-ACK packet. False if it is a SYN packet. + * @arg sampled Whether or not the packet was sampled for reporting. */ -static __always_inline bool _ct_create_new_tcp_connection(struct packet *p, struct ct_v4_key key, __u8 observation_point) { +static __always_inline bool _ct_create_new_tcp_connection(struct packet *p, struct ct_v4_key *key, __u8 observation_point, bool is_reply, bool sampled) { struct ct_entry new_value; __builtin_memset(&new_value, 0, sizeof(struct ct_entry)); __u64 now = bpf_mono_now(); @@ -140,22 +244,43 @@ static __always_inline bool _ct_create_new_tcp_connection(struct packet *p, stru return false; } new_value.eviction_time = now + CT_SYN_TIMEOUT; - new_value.flags_seen_tx_dir = p->flags; + if(is_reply) { + new_value.flags_seen_rx_dir = p->flags; + new_value.last_report_rx_dir = sampled ? now : 0; + new_value.bytes_seen_since_last_report_rx_dir = !sampled ? p->bytes : 0; + new_value.packets_seen_since_last_report_rx_dir = !sampled; + if (!sampled) { + _ct_record_tcp_flags(p->flags, &new_value.flags_seen_since_last_report_rx_dir); + } + } else { + new_value.flags_seen_tx_dir = p->flags; + new_value.last_report_tx_dir = sampled ? now : 0; + new_value.bytes_seen_since_last_report_tx_dir = !sampled ? p->bytes : 0; + new_value.packets_seen_since_last_report_tx_dir = !sampled; + if (!sampled) { + _ct_record_tcp_flags(p->flags, &new_value.flags_seen_since_last_report_tx_dir); + } + } new_value.is_direction_unknown = false; new_value.traffic_direction = _ct_get_traffic_direction(observation_point); #ifdef ENABLE_CONNTRACK_METRICS - new_value.conntrack_metadata.packets_forward_count = 1; - new_value.conntrack_metadata.bytes_forward_count = p->bytes; + if(is_reply){ + new_value.conntrack_metadata.packets_rx_count = 1; + new_value.conntrack_metadata.bytes_rx_count = p->bytes; + } else { + new_value.conntrack_metadata.packets_tx_count = 1; + new_value.conntrack_metadata.bytes_tx_count = p->bytes; + } // Update initial conntrack metadata for the connection. __builtin_memcpy(&p->conntrack_metadata, &new_value.conntrack_metadata, sizeof(struct conntrackmetadata)); #endif // ENABLE_CONNTRACK_METRICS // Update packet - p->is_reply = false; + p->is_reply = is_reply; p->traffic_direction = new_value.traffic_direction; - bpf_map_update_elem(&retina_conntrack, &key, &new_value, BPF_ANY); - return true; + bpf_map_update_elem(&retina_conntrack, key, &new_value, BPF_ANY); + return sampled; } /** @@ -163,8 +288,12 @@ static __always_inline bool _ct_create_new_tcp_connection(struct packet *p, stru * @arg *p pointer to the packet to be processed. * @arg key The key to be used to create the new connection. * @arg observation_point The point in the network stack where the packet is observed. + * @arg sampled Whether or not the packet was sampled for reporting. */ -static __always_inline bool _ct_handle_udp_connection(struct packet *p, struct ct_v4_key key, __u8 observation_point) { +static __always_inline bool _ct_handle_udp_connection(struct packet *p, struct ct_v4_key *key, __u8 observation_point, bool sampled) { + if (!p || !key) { + return false; + } struct ct_entry new_value; __builtin_memset(&new_value, 0, sizeof(struct ct_entry)); __u64 now = bpf_mono_now(); @@ -174,11 +303,13 @@ static __always_inline bool _ct_handle_udp_connection(struct packet *p, struct c } new_value.eviction_time = now + CT_CONNECTION_LIFETIME_NONTCP; new_value.flags_seen_tx_dir = p->flags; - new_value.last_report_tx_dir = now; + new_value.last_report_tx_dir = sampled ? now : 0; + new_value.bytes_seen_since_last_report_tx_dir = !sampled ? p->bytes : 0; + new_value.packets_seen_since_last_report_tx_dir = !sampled; new_value.traffic_direction = _ct_get_traffic_direction(observation_point); #ifdef ENABLE_CONNTRACK_METRICS - new_value.conntrack_metadata.packets_forward_count = 1; - new_value.conntrack_metadata.bytes_forward_count = p->bytes; + new_value.conntrack_metadata.packets_tx_count = 1; + new_value.conntrack_metadata.bytes_tx_count = p->bytes; // Update packet's conntrack metadata. __builtin_memcpy(&p->conntrack_metadata, &new_value.conntrack_metadata, sizeof(struct conntrackmetadata));; #endif // ENABLE_CONNTRACK_METRICS @@ -186,8 +317,8 @@ static __always_inline bool _ct_handle_udp_connection(struct packet *p, struct c // Update packet p->is_reply = false; p->traffic_direction = new_value.traffic_direction; - bpf_map_update_elem(&retina_conntrack, &key, &new_value, BPF_ANY); - return true; + bpf_map_update_elem(&retina_conntrack, key, &new_value, BPF_ANY); + return sampled; } /** @@ -196,12 +327,19 @@ static __always_inline bool _ct_handle_udp_connection(struct packet *p, struct c * @arg key The key to be used to handle the connection. * @arg reverse_key The reverse key to be used to handle the connection. * @arg observation_point The point in the network stack where the packet is observed. + * @arg sampled Whether or not the packet was sampled for reporting. */ -static __always_inline bool _ct_handle_tcp_connection(struct packet *p, struct ct_v4_key key, struct ct_v4_key reverse_key, __u8 observation_point) { - // Check if the packet is a SYN packet. - if (p->flags & TCP_SYN) { - // Create a new connection with a timeout of CT_SYN_TIMEOUT. - return _ct_create_new_tcp_connection(p, key, observation_point); +static __always_inline bool _ct_handle_tcp_connection(struct packet *p, struct ct_v4_key *key, struct ct_v4_key *reverse_key, __u8 observation_point, bool sampled) { + if (!p || !key || !reverse_key) { + return false; + } + u8 tcp_handshake = p->flags & (TCP_SYN|TCP_ACK); + if (tcp_handshake == TCP_SYN) { + // We have a SYN, we set `is_reply` to false and we provide `key` + return _ct_create_new_tcp_connection(p, key, observation_point, false, sampled); + } else if(tcp_handshake == (TCP_SYN|TCP_ACK)) { + // We have a SYN-ACK, we set `is_reply` to true and we provide `reverse_key` + return _ct_create_new_tcp_connection(p, reverse_key, observation_point, true, sampled); } // The packet is not a SYN packet and the connection corresponding to this packet is not found. @@ -224,27 +362,37 @@ static __always_inline bool _ct_handle_tcp_connection(struct packet *p, struct c if (p->flags & TCP_ACK) { p->is_reply = true; new_value.flags_seen_rx_dir = p->flags; - new_value.last_report_rx_dir = now; + new_value.last_report_rx_dir = sampled ? now : 0; + new_value.bytes_seen_since_last_report_rx_dir = !sampled ? p->bytes : 0; + new_value.packets_seen_since_last_report_rx_dir = !sampled; + if (!sampled) { + _ct_record_tcp_flags(p->flags, &new_value.flags_seen_since_last_report_rx_dir); + } #ifdef ENABLE_CONNTRACK_METRICS - new_value.conntrack_metadata.bytes_reply_count = p->bytes; - new_value.conntrack_metadata.packets_reply_count = 1; + new_value.conntrack_metadata.bytes_rx_count = p->bytes; + new_value.conntrack_metadata.packets_rx_count = 1; #endif // ENABLE_CONNTRACK_METRICS - bpf_map_update_elem(&retina_conntrack, &reverse_key, &new_value, BPF_ANY); + bpf_map_update_elem(&retina_conntrack, reverse_key, &new_value, BPF_ANY); } else { // Otherwise, the packet is considered as a packet in the send direction. p->is_reply = false; new_value.flags_seen_tx_dir = p->flags; - new_value.last_report_tx_dir = now; + new_value.last_report_tx_dir = sampled ? now : 0; + new_value.bytes_seen_since_last_report_tx_dir = !sampled ? p->bytes : 0; + new_value.packets_seen_since_last_report_tx_dir = !sampled; + if (!sampled) { + _ct_record_tcp_flags(p->flags, &new_value.flags_seen_since_last_report_tx_dir); + } #ifdef ENABLE_CONNTRACK_METRICS - new_value.conntrack_metadata.bytes_forward_count = p->bytes; - new_value.conntrack_metadata.packets_forward_count = 1; + new_value.conntrack_metadata.bytes_tx_count = p->bytes; + new_value.conntrack_metadata.packets_tx_count = 1; #endif // ENABLE_CONNTRACK_METRICS - bpf_map_update_elem(&retina_conntrack, &key, &new_value, BPF_ANY); + bpf_map_update_elem(&retina_conntrack, key, &new_value, BPF_ANY); } #ifdef ENABLE_CONNTRACK_METRICS // Update packet's conntrack metadata. __builtin_memcpy(&p->conntrack_metadata, &new_value.conntrack_metadata, sizeof(struct conntrackmetadata)); #endif // ENABLE_CONNTRACK_METRICS - return true; + return sampled; } /** @@ -253,15 +401,22 @@ static __always_inline bool _ct_handle_tcp_connection(struct packet *p, struct c * @arg key The key to be used to handle the connection. * @arg reverse_key The reverse key to be used to handle the connection. * @arg observation_point The point in the network stack where the packet is observed. + * @arg sampled Whether or not the packet was sampled for reporting. */ -static __always_inline bool _ct_handle_new_connection(struct packet *p, struct ct_v4_key key, struct ct_v4_key reverse_key, __u8 observation_point) { - if (key.proto & IPPROTO_TCP) { - return _ct_handle_tcp_connection(p, key, reverse_key, observation_point); - } else if (key.proto & IPPROTO_UDP) { - return _ct_handle_udp_connection(p, key, observation_point); +static __always_inline struct packetreport _ct_handle_new_connection(struct packet *p, struct ct_v4_key *key, struct ct_v4_key *reverse_key, __u8 observation_point, bool sampled) { + struct packetreport report; + __builtin_memset(&report, 0, sizeof(struct packetreport)); + if (!p || !key || !reverse_key) { + return report; + } + if (key->proto & IPPROTO_TCP) { + report.report = _ct_handle_tcp_connection(p, key, reverse_key, observation_point, sampled); + } else if (key->proto & IPPROTO_UDP) { + report.report = _ct_handle_udp_connection(p, key, observation_point, sampled); } else { - return false; // We are not interested in other protocols. + report.report = false; // We are not interested in other protocols. } + return report; } /** @@ -270,40 +425,70 @@ static __always_inline bool _ct_handle_new_connection(struct packet *p, struct c * @arg entry The entry of the connection in Retina's conntrack map. * @arg flags The flags of the packet. * @arg direction The direction of the packet in relation to the connection. - * Returns true if the packet should be reported to userspace. False otherwise. + * @arg bytes The size of the packet in bytes. + * @arg sampled Whether or not the packet was sampled for reporting. + * Returns a packetreport struct representing if the packet should be reported to userspace. */ -static __always_inline bool _ct_should_report_packet(struct ct_v4_key *key, struct ct_entry *entry, __u8 flags, __u8 direction) { +static __always_inline struct packetreport _ct_should_report_packet(struct ct_v4_key *key, struct ct_entry *entry, __u8 flags, __u8 direction, __u32 bytes, bool sampled) { + struct packetreport report; + __builtin_memset(&report, 0, sizeof(struct packetreport)); + report.report = false; + // Check for null parameters. if (!entry || !key) { - return false; - } - - __u64 now = bpf_mono_now(); - __u32 eviction_time = READ_ONCE(entry->eviction_time); - // Check if the connection timed out - if (now >= eviction_time) { - bpf_map_delete_elem(&retina_conntrack, key); - return true; // Report the last packet received before deletion + return report; } // Get direction-specific data __u8 seen_flags; __u32 last_report; + __u32 packets_seen; + __u32 bytes_seen; if (direction == CT_PACKET_DIR_TX) { seen_flags = READ_ONCE(entry->flags_seen_tx_dir); last_report = READ_ONCE(entry->last_report_tx_dir); + bytes_seen = READ_ONCE(entry->bytes_seen_since_last_report_tx_dir); + packets_seen = READ_ONCE(entry->packets_seen_since_last_report_tx_dir); + __builtin_memcpy(&report.previously_observed_flags, &entry->flags_seen_since_last_report_tx_dir, sizeof(struct tcpflagscount)); } else { seen_flags = READ_ONCE(entry->flags_seen_rx_dir); last_report = READ_ONCE(entry->last_report_rx_dir); + bytes_seen = READ_ONCE(entry->bytes_seen_since_last_report_rx_dir); + packets_seen = READ_ONCE(entry->packets_seen_since_last_report_rx_dir); + __builtin_memcpy(&report.previously_observed_flags, &entry->flags_seen_since_last_report_rx_dir, sizeof(struct tcpflagscount)); } + report.previously_observed_bytes = bytes_seen; + report.previously_observed_packets = packets_seen; + + // Check for overflow + if (bytes_seen <= UINT32_MAX-bytes) { + bytes_seen += bytes; + } + + if (packets_seen <= UINT32_MAX-1) { + packets_seen += 1; + } + + __u64 now = bpf_mono_now(); + __u32 eviction_time = READ_ONCE(entry->eviction_time); + + // Check if the connection timed out + if (now >= eviction_time) { + bpf_map_delete_elem(&retina_conntrack, key); + report.report = true; + return report; // Report the last packet received before deletion + } + + __u8 packet_flags = flags; + // OR the seen flags with the new flags flags |= seen_flags; __u8 protocol = key->proto; - + // Handle connection state updates and reporting conditions bool should_report = false; - + // TCP-specific state management if (protocol == IPPROTO_TCP) { // Handle TCP connection termination states @@ -315,11 +500,19 @@ static __always_inline bool _ct_should_report_packet(struct ct_v4_key *key, stru (entry->flags_seen_tx_dir & TCP_FIN) && (entry->flags_seen_rx_dir & TCP_FIN)) { bpf_map_delete_elem(&retina_conntrack, key); - return true; // Report final ACK before connection removal + report.report = true; + return report; // Report final ACK before connection removal + } + + // If RST is seen, delete connection immediately + if (flags & TCP_RST) { + bpf_map_delete_elem(&retina_conntrack, key); + report.report = true; + return report; // Report RST before connection removal } // Update FIN flag status in the appropriate direction - if (flags & TCP_FIN) { + if (packet_flags & TCP_FIN) { if (direction == CT_PACKET_DIR_TX) { entry->flags_seen_tx_dir |= TCP_FIN; } else { @@ -327,61 +520,87 @@ static __always_inline bool _ct_should_report_packet(struct ct_v4_key *key, stru } should_report = true; // Always report FIN packets } - - // If FIN seen in both directions, transition to TIME_WAIT state - if ((entry->flags_seen_tx_dir & TCP_FIN) && (entry->flags_seen_rx_dir & TCP_FIN)) { - WRITE_ONCE(entry->eviction_time, now + CT_TIME_WAIT_TIMEOUT_TCP); - return true; // Report transition to TIME_WAIT - } - // If RST is seen, delete connection immediately - if (flags & TCP_RST) { - bpf_map_delete_elem(&retina_conntrack, key); - return true; // Report RST before connection removal - } - // Always report important TCP control flags - if (flags & (TCP_SYN | TCP_URG | TCP_ECE | TCP_CWR)) { + if (packet_flags & (TCP_SYN | TCP_URG | TCP_ECE | TCP_CWR)) { should_report = true; } - - // Extend TCP connection lifetime - WRITE_ONCE(entry->eviction_time, now + CT_CONNECTION_LIFETIME_TCP); + + // If FIN seen in both directions, transition to TIME_WAIT state + if ((entry->flags_seen_tx_dir & TCP_FIN) && (entry->flags_seen_rx_dir & TCP_FIN)) { + WRITE_ONCE(entry->eviction_time, now + CT_TIME_WAIT_TIMEOUT_TCP); + should_report = true; // Report transition to TIME_WAIT + } else { + // Extend TCP connection lifetime + WRITE_ONCE(entry->eviction_time, now + CT_CONNECTION_LIFETIME_TCP); + } } else if (protocol == IPPROTO_UDP) { // Extend UDP/other connection lifetime WRITE_ONCE(entry->eviction_time, now + CT_CONNECTION_LIFETIME_NONTCP); } + if (flags != seen_flags) { + if (direction == CT_PACKET_DIR_TX) { + WRITE_ONCE(entry->flags_seen_tx_dir, flags); + } else { + WRITE_ONCE(entry->flags_seen_rx_dir, flags); + } + } + // Report if: // 1. We already decided to report based on protocol-specific rules, or - // 2. New flags have appeared, or + // 2. New flags have appeared and the packet has been sampled, or // 3. Reporting interval has elapsed - if (should_report || flags != seen_flags || now - last_report >= CT_REPORT_INTERVAL) { + if (should_report || (sampled && flags != seen_flags) || now - last_report >= CT_REPORT_INTERVAL) { + report.report = true; // Update the connection's state if (direction == CT_PACKET_DIR_TX) { - WRITE_ONCE(entry->flags_seen_tx_dir, flags); WRITE_ONCE(entry->last_report_tx_dir, now); + WRITE_ONCE(entry->bytes_seen_since_last_report_tx_dir, 0); + WRITE_ONCE(entry->packets_seen_since_last_report_tx_dir, 0); + __builtin_memset(&entry->flags_seen_since_last_report_tx_dir, 0, sizeof(struct tcpflagscount)); } else { - WRITE_ONCE(entry->flags_seen_rx_dir, flags); WRITE_ONCE(entry->last_report_rx_dir, now); + WRITE_ONCE(entry->bytes_seen_since_last_report_rx_dir, 0); + WRITE_ONCE(entry->packets_seen_since_last_report_rx_dir, 0); + __builtin_memset(&entry->flags_seen_since_last_report_rx_dir, 0, sizeof(struct tcpflagscount)); + } + return report; + } else { + struct tcpflagscount newcount; + __builtin_memcpy(&newcount, &report.previously_observed_flags, sizeof(struct tcpflagscount)); + _ct_record_tcp_flags(packet_flags, &newcount); + if (direction == CT_PACKET_DIR_TX) { + WRITE_ONCE(entry->bytes_seen_since_last_report_tx_dir, bytes_seen); + WRITE_ONCE(entry->packets_seen_since_last_report_tx_dir, packets_seen); + __builtin_memcpy(&entry->flags_seen_since_last_report_tx_dir, &newcount, sizeof(struct tcpflagscount)); + } else { + WRITE_ONCE(entry->bytes_seen_since_last_report_rx_dir, bytes_seen); + WRITE_ONCE(entry->packets_seen_since_last_report_rx_dir, packets_seen); + __builtin_memcpy(&entry->flags_seen_since_last_report_rx_dir, &newcount, sizeof(struct tcpflagscount)); } - return true; } - return false; + return report; } /** * Process a packet and update the connection tracking map. * @arg *p pointer to the packet to be processed. * @arg observation_point The point in the network stack where the packet is observed. - * Returns true if the packet should be report to userspace. False otherwise. + * @arg sampled Whether or not the packet has been sampled for reporting. + * Returns a packetreport struct representing if the packet should be reported to userspace. */ -static __always_inline __attribute__((unused)) bool ct_process_packet(struct packet *p, __u8 observation_point) { - +static __always_inline __attribute__((unused)) struct packetreport ct_process_packet(struct packet *p, __u8 observation_point, bool sampled) { if (!p) { - return false; + struct packetreport report; + __builtin_memset(&report, 0, sizeof(struct packetreport)); + report.report = false; + report.previously_observed_packets = 0; + report.previously_observed_bytes = 0; + return report; } + // Create a new key for the send direction. struct ct_v4_key key; __builtin_memset(&key, 0, sizeof(struct ct_v4_key)); @@ -390,6 +609,7 @@ static __always_inline __attribute__((unused)) bool ct_process_packet(struct pac key.src_port = p->src_port; key.dst_port = p->dst_port; key.proto = p->proto; + // Lookup the connection in the map. struct ct_entry *entry = bpf_map_lookup_elem(&retina_conntrack, &key); @@ -400,12 +620,12 @@ static __always_inline __attribute__((unused)) bool ct_process_packet(struct pac p->traffic_direction = entry->traffic_direction; #ifdef ENABLE_CONNTRACK_METRICS // Update packet count and bytes count on conntrack entry. - WRITE_ONCE(entry->conntrack_metadata.packets_forward_count, READ_ONCE(entry->conntrack_metadata.packets_forward_count) + 1); - WRITE_ONCE(entry->conntrack_metadata.bytes_forward_count, READ_ONCE(entry->conntrack_metadata.bytes_forward_count) + p->bytes); + WRITE_ONCE(entry->conntrack_metadata.packets_tx_count, READ_ONCE(entry->conntrack_metadata.packets_tx_count) + 1); + WRITE_ONCE(entry->conntrack_metadata.bytes_tx_count, READ_ONCE(entry->conntrack_metadata.bytes_tx_count) + p->bytes); // Update packet's conntract metadata. __builtin_memcpy(&p->conntrack_metadata, &entry->conntrack_metadata, sizeof(struct conntrackmetadata)); #endif // ENABLE_CONNTRACK_METRICS - return _ct_should_report_packet(&key, entry, p->flags, CT_PACKET_DIR_TX); + return _ct_should_report_packet(&key, entry, p->flags, CT_PACKET_DIR_TX, p->bytes, sampled); } // The connection is not found in the send direction. Check the reply direction by reversing the key. @@ -422,14 +642,14 @@ static __always_inline __attribute__((unused)) bool ct_process_packet(struct pac p->traffic_direction = entry->traffic_direction; #ifdef ENABLE_CONNTRACK_METRICS // Update packet count and bytes count on conntrack entry. - WRITE_ONCE(entry->conntrack_metadata.packets_reply_count, READ_ONCE(entry->conntrack_metadata.packets_reply_count) + 1); - WRITE_ONCE(entry->conntrack_metadata.bytes_reply_count, READ_ONCE(entry->conntrack_metadata.bytes_reply_count) + p->bytes); + WRITE_ONCE(entry->conntrack_metadata.packets_rx_count, READ_ONCE(entry->conntrack_metadata.packets_rx_count) + 1); + WRITE_ONCE(entry->conntrack_metadata.bytes_rx_count, READ_ONCE(entry->conntrack_metadata.bytes_rx_count) + p->bytes); // Update packet's conntract metadata. __builtin_memcpy(&p->conntrack_metadata, &entry->conntrack_metadata, sizeof(struct conntrackmetadata)); #endif // ENABLE_CONNTRACK_METRICS - return _ct_should_report_packet(&reverse_key, entry, p->flags, CT_PACKET_DIR_RX); + return _ct_should_report_packet(&reverse_key, entry, p->flags, CT_PACKET_DIR_RX, p->bytes, sampled); } // If the connection is still not found, the connection is new. - return _ct_handle_new_connection(p, key, reverse_key, observation_point); + return _ct_handle_new_connection(p, &key, &reverse_key, observation_point, sampled); } diff --git a/pkg/plugin/conntrack/_cprog/conntrack.h b/pkg/plugin/conntrack/_cprog/conntrack.h index 490736648c..0de6018180 100644 --- a/pkg/plugin/conntrack/_cprog/conntrack.h +++ b/pkg/plugin/conntrack/_cprog/conntrack.h @@ -38,6 +38,7 @@ #define TCP_URG 0x20 #define TCP_ECE 0x40 #define TCP_CWR 0x80 +#define TCP_NS 0x100 #define CT_PACKET_DIR_TX 0x00 #define CT_PACKET_DIR_RX 0x01 diff --git a/pkg/plugin/conntrack/_cprog/dynamic.h b/pkg/plugin/conntrack/_cprog/dynamic.h index 80abbd931f..76e9c73f6b 100644 --- a/pkg/plugin/conntrack/_cprog/dynamic.h +++ b/pkg/plugin/conntrack/_cprog/dynamic.h @@ -1,2 +1 @@ -// Place holder header file that will be replaced by the actual header file during runtime -// DO NOT DELETE +#define ENABLE_CONNTRACK_METRICS 1 diff --git a/pkg/plugin/conntrack/conntrack_bpfel_arm64.go b/pkg/plugin/conntrack/conntrack_bpfel_arm64.go index efb93738a6..3a542cef6e 100644 --- a/pkg/plugin/conntrack/conntrack_bpfel_arm64.go +++ b/pkg/plugin/conntrack/conntrack_bpfel_arm64.go @@ -13,18 +13,44 @@ import ( ) type conntrackCtEntry struct { - EvictionTime uint32 - LastReportTxDir uint32 - LastReportRxDir uint32 + EvictionTime uint32 + LastReportTxDir uint32 + LastReportRxDir uint32 + BytesSeenSinceLastReportTxDir uint32 + BytesSeenSinceLastReportRxDir uint32 + PacketsSeenSinceLastReportTxDir uint32 + PacketsSeenSinceLastReportRxDir uint32 + FlagsSeenSinceLastReportTxDir struct { + Syn uint32 + Ack uint32 + Fin uint32 + Rst uint32 + Psh uint32 + Urg uint32 + Ece uint32 + Cwr uint32 + Ns uint32 + } + FlagsSeenSinceLastReportRxDir struct { + Syn uint32 + Ack uint32 + Fin uint32 + Rst uint32 + Psh uint32 + Urg uint32 + Ece uint32 + Cwr uint32 + Ns uint32 + } TrafficDirection uint8 FlagsSeenTxDir uint8 FlagsSeenRxDir uint8 IsDirectionUnknown bool ConntrackMetadata struct { - BytesForwardCount uint64 - BytesReplyCount uint64 - PacketsForwardCount uint32 - PacketsReplyCount uint32 + BytesTxCount uint64 + BytesRxCount uint64 + PacketsTxCount uint32 + PacketsRxCount uint32 } } diff --git a/pkg/plugin/conntrack/conntrack_bpfel_x86.go b/pkg/plugin/conntrack/conntrack_bpfel_x86.go index baa46bcbfa..14a95f49f4 100644 --- a/pkg/plugin/conntrack/conntrack_bpfel_x86.go +++ b/pkg/plugin/conntrack/conntrack_bpfel_x86.go @@ -13,18 +13,44 @@ import ( ) type conntrackCtEntry struct { - EvictionTime uint32 - LastReportTxDir uint32 - LastReportRxDir uint32 + EvictionTime uint32 + LastReportTxDir uint32 + LastReportRxDir uint32 + BytesSeenSinceLastReportTxDir uint32 + BytesSeenSinceLastReportRxDir uint32 + PacketsSeenSinceLastReportTxDir uint32 + PacketsSeenSinceLastReportRxDir uint32 + FlagsSeenSinceLastReportTxDir struct { + Syn uint32 + Ack uint32 + Fin uint32 + Rst uint32 + Psh uint32 + Urg uint32 + Ece uint32 + Cwr uint32 + Ns uint32 + } + FlagsSeenSinceLastReportRxDir struct { + Syn uint32 + Ack uint32 + Fin uint32 + Rst uint32 + Psh uint32 + Urg uint32 + Ece uint32 + Cwr uint32 + Ns uint32 + } TrafficDirection uint8 FlagsSeenTxDir uint8 FlagsSeenRxDir uint8 IsDirectionUnknown bool ConntrackMetadata struct { - BytesForwardCount uint64 - BytesReplyCount uint64 - PacketsForwardCount uint32 - PacketsReplyCount uint32 + BytesTxCount uint64 + BytesRxCount uint64 + PacketsTxCount uint32 + PacketsRxCount uint32 } } diff --git a/pkg/plugin/conntrack/conntrack_linux.go b/pkg/plugin/conntrack/conntrack_linux.go index dc2fb0c449..4e1eb41cf3 100644 --- a/pkg/plugin/conntrack/conntrack_linux.go +++ b/pkg/plugin/conntrack/conntrack_linux.go @@ -16,6 +16,7 @@ import ( "github.com/microsoft/retina/internal/ktime" "github.com/microsoft/retina/pkg/loader" "github.com/microsoft/retina/pkg/log" + "github.com/microsoft/retina/pkg/metrics" plugincommon "github.com/microsoft/retina/pkg/plugin/common" _ "github.com/microsoft/retina/pkg/plugin/conntrack/_cprog" // nolint // This is needed so cprog is included when vendoring "github.com/microsoft/retina/pkg/utils" @@ -23,6 +24,8 @@ import ( "go.uber.org/zap" ) +var conntrackMetricsEnabled = false // conntrack metrics global variable + //go:generate go run github.com/cilium/ebpf/cmd/bpf2go@master -cflags "-g -O2 -Wall -D__TARGET_ARCH_${GOARCH} -Wall" -target ${GOARCH} -type ct_v4_key conntrack ./_cprog/conntrack.c -- -I../lib/_${GOARCH} -I../lib/common/libbpf/_src -I../lib/common/libbpf/_include/linux -I../lib/common/libbpf/_include/uapi/linux -I../lib/common/libbpf/_include/asm // Init initializes the conntrack eBPF map in the kernel for the first time. @@ -88,6 +91,10 @@ func GenerateDynamic(ctx context.Context, dynamicHeaderPath string, conntrackMet if err != nil { return errors.Wrap(err, "failed to write conntrack dynamic header") } + // set a global variable + if conntrackMetrics == 1 { + conntrackMetricsEnabled = true + } return nil } @@ -118,6 +125,10 @@ func (ct *Conntrack) Run(ctx context.Context) error { // List of keys to be deleted var keysToDelete []conntrackCtV4Key + // metrics counters + var packetsCountTx, packetsCountRx, totConnections uint32 + var bytesCountTx, bytesCountRx uint64 + iter := ct.ctMap.Iterate() for iter.Next(&key, &value) { noOfCtEntries++ @@ -133,6 +144,18 @@ func (ct *Conntrack) Run(ctx context.Context) error { dstIP := utils.Int2ip(key.DstIp).To4() sourcePortShort := uint32(utils.HostToNetShort(key.SrcPort)) destinationPortShort := uint32(utils.HostToNetShort(key.DstPort)) + + // Add conntrack metrics. + if conntrackMetricsEnabled { + // Basic metrics, node-level + ctMeta := value.ConntrackMetadata + totConnections++ + bytesCountTx += ctMeta.BytesTxCount + bytesCountRx += ctMeta.BytesRxCount + packetsCountTx += ctMeta.PacketsTxCount + packetsCountRx += ctMeta.PacketsRxCount + } + ct.l.Debug("conntrack entry", zap.String("src_ip", srcIP.String()), zap.Uint32("src_port", sourcePortShort), @@ -151,6 +174,16 @@ func (ct *Conntrack) Run(ctx context.Context) error { if err := iter.Err(); err != nil { ct.l.Error("Iterate failed", zap.Error(err)) } + + // create metrics + if conntrackMetricsEnabled { + metrics.ConntrackPacketsTx.WithLabelValues().Set(float64(packetsCountTx)) + metrics.ConntrackBytesTx.WithLabelValues().Set(float64(bytesCountTx)) + metrics.ConntrackPacketsRx.WithLabelValues().Set(float64(packetsCountRx)) + metrics.ConntrackBytesRx.WithLabelValues().Set(float64(bytesCountRx)) + metrics.ConntrackTotalConnections.WithLabelValues().Set(float64(totConnections)) + } + // Delete the conntrack entries for _, key := range keysToDelete { if err := ct.ctMap.Delete(key); err != nil { diff --git a/pkg/plugin/dns/_cprog/dns.c b/pkg/plugin/dns/_cprog/dns.c new file mode 100644 index 0000000000..2e7c229da3 --- /dev/null +++ b/pkg/plugin/dns/_cprog/dns.c @@ -0,0 +1,394 @@ +// go:build ignore + +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// DNS tracer eBPF program - captures DNS queries and responses +// +// Adapted from Inspektor Gadget's trace_dns gadget (Apache 2.0 License) +// https://github.com/inspektor-gadget/inspektor-gadget +// Copyright (c) The Inspektor Gadget authors + +#include "vmlinux.h" +#include "bpf_helpers.h" +#include "bpf_endian.h" + +char __license[] SEC("license") = "Dual MIT/GPL"; + +// Ethernet and IP constants +#define ETH_P_IP 0x0800 +#define ETH_P_IPV6 0x86DD +#define ETH_HLEN 14 + +// IP protocol constants +#define IPPROTO_TCP 6 +#define IPPROTO_UDP 17 + +// IPv6 next header values +#define NEXTHDR_HOP 0 +#define NEXTHDR_TCP 6 +#define NEXTHDR_UDP 17 +#define NEXTHDR_ROUTING 43 +#define NEXTHDR_FRAGMENT 44 +#define NEXTHDR_AUTH 51 +#define NEXTHDR_NONE 59 +#define NEXTHDR_DEST 60 + +// Packet types from linux/if_packet.h +#define PACKET_HOST 0 // Incoming packets +#define PACKET_OUTGOING 4 // Outgoing packets + +// DNS constants +#define DNS_PORT 53 +#define DNS_MDNS_PORT 5353 +#define DNS_QR_QUERY 0 +#define DNS_QR_RESP 1 + +// ── Packet read helpers ── +// +// All packet reads use bpf_skb_load_bytes, which copies raw bytes from +// the sk_buff into a local variable. Unlike the legacy BPF_LD_ABS +// intrinsics (load_byte/load_half), this works with BPF_PROG_TEST_RUN +// and is the recommended approach for modern BPF programs. +// +// bpf_skb_load_bytes does NOT convert byte order — multi-byte values +// are in network byte order (big-endian) and must be converted with +// bpf_ntohs/bpf_ntohl when used as host integers. + +// Read a 1-byte value from the packet at the given offset. +static __always_inline int skb_load_byte(const struct __sk_buff *skb, + __u32 off, __u8 *out) { + return bpf_skb_load_bytes(skb, off, out, 1); +} + +// Read a 2-byte value from the packet at the given offset. +// Returns the value in host byte order. +static __always_inline int skb_load_half(const struct __sk_buff *skb, + __u32 off, __u16 *out) { + __u16 val; + int ret = bpf_skb_load_bytes(skb, off, &val, 2); + if (ret == 0) + *out = bpf_ntohs(val); + return ret; +} + +// DNS header structure (RFC 1035 §4.1.1). +// Used for sizeof and offsetof only — flag fields (QR, RCODE) are +// extracted manually to avoid bitfield portability issues. +struct dnshdr { + __u16 id; + __u16 flags; + __u16 qdcount; // Question count + __u16 ancount; // Answer count + __u16 nscount; // Authority records + __u16 arcount; // Additional records +}; + +// DNS event structure - sent to userspace. +// Fields are ordered by descending alignment (8 → 4 → 2 → 1) to avoid +// internal padding. The compiler adds 5 bytes of trailing padding to +// reach 8-byte struct alignment (required by the __u64 field). +struct dns_event { + __u64 timestamp; // Boot time in nanoseconds + __u32 src_ip; // Source IPv4 address + __u32 dst_ip; // Destination IPv4 address + __u8 src_ip6[16]; // Source IPv6 address + __u8 dst_ip6[16]; // Destination IPv6 address + __u16 src_port; // Source port + __u16 dst_port; // Destination port + __u16 id; // DNS query ID + __u16 qtype; // Query type (from first question) + __u16 ancount; // Answer count + __u16 dns_off; // DNS offset in packet + __u16 data_len; // Total packet length + __u8 af; // Address family (4 or 6) + __u8 proto; // Protocol (TCP=6, UDP=17) + __u8 pkt_type; // Packet type (HOST=0, OUTGOING=4) + __u8 qr; // Query(0) or Response(1) + __u8 rcode; // Response code +}; + +// Force bpf2go to generate a Go type for dns_event. Without this, +// bpf2go only generates types that appear in map definitions or +// program parameters. The -type flag references this symbol. +const struct dns_event *unused_dns_event __attribute__((unused)); + +// Perf event array for streaming events to userspace +struct { + __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); + __uint(key_size, sizeof(__u32)); + __uint(value_size, sizeof(__u32)); +} retina_dns_events SEC(".maps"); + +// BPF programs are limited to 512 bytes of stack (MAX_BPF_STACK in +// include/linux/filter.h: https://github.com/torvalds/linux/blob/master/include/linux/filter.h#L99), +// which is too small to hold a dns_event struct plus local variables. +// Instead we use a per-CPU array map with a single entry as heap-like +// scratch space — each CPU gets its own copy so there are no data races. +// Ref: +// https://github.com/inspektor-gadget/inspektor-gadget/blob/c414fc1/gadgets/trace_dns/program.bpf.c#L103-L109 +struct { + __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); + __uint(max_entries, 1); + __type(key, __u32); + __type(value, struct dns_event); +} tmp_dns_events SEC(".maps"); + +// Check if port is standard DNS (53) or mDNS (5353). +static __always_inline bool is_dns_port(__u16 port) { + return port == DNS_PORT || port == DNS_MDNS_PORT; +} + +// Socket filter attached to a raw AF_PACKET socket (bound to all interfaces). +// Filters for DNS traffic (port 53/5353), extracts header metadata into a +// dns_event struct, and sends it to userspace via perf buffer with the raw +// packet appended for Go-side DNS payload parsing. +SEC("socket1") +int retina_dns_filter(struct __sk_buff *skb) { + struct dns_event *event; + __u16 h_proto, sport, dport, l4_off, dns_off; + __u8 proto; + int zero = 0; + + // Dedupe: drop TX-side observations. Every packet has at most one + // RX-side observation in the host netns, so filtering out PACKET_OUTGOING + // gives exactly-once counting across multi-interface hosts (pod veths, + // bonded VFs, etc.). pkt_type reference: + // https://github.com/torvalds/linux/blob/master/include/linux/etherdevice.h#L615 + if (skb->pkt_type == PACKET_OUTGOING) + return 0; + + // First pass: Quick filter to check if this is a DNS packet + if (skb_load_half(skb, offsetof(struct ethhdr, h_proto), &h_proto)) + return 0; + + switch (h_proto) { + case ETH_P_IP: { + // Get IP protocol + if (skb_load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol), + &proto)) + return 0; + + // Calculate L4 offset - account for variable IP header length + __u8 ihl_byte; + if (skb_load_byte(skb, ETH_HLEN, &ihl_byte)) + return 0; + __u8 ip_header_len = (ihl_byte & 0x0F) * 4; + l4_off = ETH_HLEN + ip_header_len; + break; + } + case ETH_P_IPV6: { + // Get next header (protocol) + if (skb_load_byte(skb, ETH_HLEN + offsetof(struct ipv6hdr, nexthdr), + &proto)) + return 0; + l4_off = ETH_HLEN + sizeof(struct ipv6hdr); + +// Parse IPv6 extension headers (up to 6) +#pragma unroll + for (int i = 0; i < 6; i++) { + __u8 nextproto, ext_len; + + // Stop if we found TCP or UDP + if (proto == NEXTHDR_TCP || proto == NEXTHDR_UDP) + break; + + if (skb_load_byte(skb, l4_off, &nextproto)) + return 0; + + switch (proto) { + case NEXTHDR_FRAGMENT: + l4_off += 8; + break; + case NEXTHDR_AUTH: + if (skb_load_byte(skb, l4_off + 1, &ext_len)) + return 0; + l4_off += 4 * (ext_len + 2); + break; + case NEXTHDR_HOP: + case NEXTHDR_ROUTING: + case NEXTHDR_DEST: + if (skb_load_byte(skb, l4_off + 1, &ext_len)) + return 0; + l4_off += 8 * (ext_len + 1); + break; + case NEXTHDR_NONE: + return 0; + default: + return 0; + } + proto = nextproto; + } + break; + } + default: + return 0; + } + + // Check protocol is TCP or UDP + if (proto != IPPROTO_UDP && proto != IPPROTO_TCP) + return 0; + + // Extract ports (same offsets for UDP and TCP) + if (skb_load_half(skb, l4_off + offsetof(struct udphdr, source), &sport)) + return 0; + if (skb_load_half(skb, l4_off + offsetof(struct udphdr, dest), &dport)) + return 0; + + // Early exit if not DNS port + if (!is_dns_port(sport) && !is_dns_port(dport)) + return 0; + + // Calculate DNS offset + switch (proto) { + case IPPROTO_UDP: + dns_off = l4_off + sizeof(struct udphdr); + break; + case IPPROTO_TCP: { + // Get TCP header length (data offset field) + __u8 doff_byte; + if (skb_load_byte(skb, l4_off + 12, &doff_byte)) + return 0; + __u8 tcp_header_len = ((doff_byte >> 4) & 0x0F) * 4; + + // Skip if no data (control segment) + dns_off = l4_off + tcp_header_len; + if (skb->len <= dns_off) + return 0; + + // DNS over TCP has 2-byte length prefix + dns_off += 2; + break; + } + default: + return 0; + } + + // Look up index 0 of the per-CPU array to get a pointer to this CPU's + // scratch buffer. The map only has one entry (max_entries=1), so index 0 + // is the only valid key. This returns a pointer the verifier trusts for + // bounded writes, unlike a raw stack allocation. + event = bpf_map_lookup_elem(&tmp_dns_events, &zero); + if (!event) + return 0; + + // Initialize event with zeros for fields that might be skipped + __builtin_memset(event, 0, sizeof(*event)); + + // Fill in event data + event->timestamp = bpf_ktime_get_boot_ns(); + event->data_len = skb->len; + event->dns_off = dns_off; + event->pkt_type = skb->pkt_type; + event->proto = proto; + event->src_port = sport; + event->dst_port = dport; + + // Extract IP addresses using bpf_skb_load_bytes for raw byte copy — + // no byte-order conversion, so the Go side gets network-order bytes + // that map directly to net.IP. + switch (h_proto) { + case ETH_P_IP: + event->af = 4; + bpf_skb_load_bytes(skb, ETH_HLEN + offsetof(struct iphdr, saddr), + &event->src_ip, 4); + bpf_skb_load_bytes(skb, ETH_HLEN + offsetof(struct iphdr, daddr), + &event->dst_ip, 4); + break; + case ETH_P_IPV6: + event->af = 6; + bpf_skb_load_bytes(skb, ETH_HLEN + offsetof(struct ipv6hdr, saddr), + event->src_ip6, 16); + bpf_skb_load_bytes(skb, ETH_HLEN + offsetof(struct ipv6hdr, daddr), + event->dst_ip6, 16); + break; + } + + // Bounds check: ensure DNS header (12 bytes) fits in packet + if (skb->len < dns_off + sizeof(struct dnshdr)) + return 0; + + // Extract fields from the 12-byte DNS header (RFC 1035 §4.1.1): + // + // Offset Field + // 0-1 ID (transaction identifier) + // 2 flags[0] QR(1) | Opcode(4) | AA(1) | TC(1) | RD(1) + // 3 flags[1] RA(1) | Z(3) | RCODE(4) + // 4-5 QDCOUNT + // 6-7 ANCOUNT + // 8-9 NSCOUNT + // 10-11 ARCOUNT + // + // We read the flag bytes individually rather than using a C bitfield + // struct because bitfield layout is compiler-dependent (bit ordering + // varies between GCC and Clang, and between big/little-endian targets). + // Manual shifts give us portable, predictable extraction. + __u8 flags0, flags1; + if (skb_load_byte(skb, dns_off + 2, &flags0)) + goto send; + if (skb_load_byte(skb, dns_off + 3, &flags1)) + goto send; + event->qr = (flags0 >> 7) & 1; // QR is the high bit of flags[0] + event->rcode = flags1 & 0x0F; // RCODE is the low 4 bits of flags[1] + skb_load_half(skb, dns_off + offsetof(struct dnshdr, id), &event->id); + skb_load_half(skb, dns_off + offsetof(struct dnshdr, ancount), + &event->ancount); + + // ── Extract QTYPE (query type) from the first DNS question ── + // + // DNS question format (RFC 1035 §4.1.2): + // + // [12-byte header] ← already parsed above + // [QNAME] ← variable-length, encoded as labels + // [QTYPE] ← 2 bytes (e.g. 1=A, 28=AAAA) + // [QCLASS] ← 2 bytes (usually 1=IN) + // + // QNAME is a sequence of length-prefixed labels ending with a zero byte: + // "kubernetes.default.svc." → [10]kubernetes [7]default [3]svc [0] + // + // We read each label's length byte, skip that many bytes, and repeat + // until we hit the zero terminator. QTYPE sits right after it. + // + // The loop is bounded to 64 iterations for the BPF verifier (DNS names + // can be at most 253 bytes / ~127 labels). If the packet is truncated + // mid-name, we skip QTYPE and let the Go side extract it via gopacket. + { + __u16 qoff = dns_off + sizeof(struct dnshdr); +#pragma unroll + for (int i = 0; i < 64; i++) { + if (qoff >= skb->len) + goto send; + __u8 label_len; + if (skb_load_byte(skb, qoff, &label_len)) + goto send; + if (label_len == 0) { + qoff += 1; // skip zero terminator + break; + } + qoff += 1 + label_len; // skip length byte + label + } + if (qoff + 2 <= skb->len) + skb_load_half(skb, qoff, &event->qtype); + } + +send: + // Send the structured event + raw packet bytes to userspace in one call. + // + // bpf_perf_event_output writes `event` (sizeof(*event) bytes) into the + // perf ring buffer, then optionally appends raw packet data from `skb`. + // + // The flags parameter encodes two things: + // - Lower 32 bits: BPF_F_CURRENT_CPU — target this CPU's ring buffer + // - Upper 32 bits: skb->len — number of packet bytes to append + // + // The Go side receives a single record.RawSample containing both: + // [ dns_event struct ][ raw packet (skb->len bytes) ] + // + // It splits at sizeof(dns_event) to get the structured metadata and the + // raw DNS payload, which gopacket parses for the query name and answers. + bpf_perf_event_output(skb, &retina_dns_events, + (__u64)skb->len << 32 | BPF_F_CURRENT_CPU, event, + sizeof(*event)); + + return 0; +} diff --git a/pkg/plugin/dns/dns_arm64_bpfel.go b/pkg/plugin/dns/dns_arm64_bpfel.go new file mode 100644 index 0000000000..755a50b2c2 --- /dev/null +++ b/pkg/plugin/dns/dns_arm64_bpfel.go @@ -0,0 +1,159 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build arm64 + +package dns + +import ( + "bytes" + _ "embed" + "fmt" + "io" + + "github.com/cilium/ebpf" +) + +type dnsDnsEvent struct { + Timestamp uint64 + SrcIp uint32 + DstIp uint32 + SrcIp6 [16]uint8 + DstIp6 [16]uint8 + SrcPort uint16 + DstPort uint16 + Id uint16 + Qtype uint16 + Ancount uint16 + DnsOff uint16 + DataLen uint16 + Af uint8 + Proto uint8 + PktType uint8 + Qr uint8 + Rcode uint8 + _ [5]byte +} + +// loadDns returns the embedded CollectionSpec for dns. +func loadDns() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_DnsBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load dns: %w", err) + } + + return spec, err +} + +// loadDnsObjects loads dns and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *dnsObjects +// *dnsPrograms +// *dnsMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadDnsObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadDns() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// dnsSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type dnsSpecs struct { + dnsProgramSpecs + dnsMapSpecs + dnsVariableSpecs +} + +// dnsProgramSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type dnsProgramSpecs struct { + RetinaDnsFilter *ebpf.ProgramSpec `ebpf:"retina_dns_filter"` +} + +// dnsMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type dnsMapSpecs struct { + RetinaDnsEvents *ebpf.MapSpec `ebpf:"retina_dns_events"` + TmpDnsEvents *ebpf.MapSpec `ebpf:"tmp_dns_events"` +} + +// dnsVariableSpecs contains global variables before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type dnsVariableSpecs struct { + UnusedDnsEvent *ebpf.VariableSpec `ebpf:"unused_dns_event"` +} + +// dnsObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadDnsObjects or ebpf.CollectionSpec.LoadAndAssign. +type dnsObjects struct { + dnsPrograms + dnsMaps + dnsVariables +} + +func (o *dnsObjects) Close() error { + return _DnsClose( + &o.dnsPrograms, + &o.dnsMaps, + ) +} + +// dnsMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadDnsObjects or ebpf.CollectionSpec.LoadAndAssign. +type dnsMaps struct { + RetinaDnsEvents *ebpf.Map `ebpf:"retina_dns_events"` + TmpDnsEvents *ebpf.Map `ebpf:"tmp_dns_events"` +} + +func (m *dnsMaps) Close() error { + return _DnsClose( + m.RetinaDnsEvents, + m.TmpDnsEvents, + ) +} + +// dnsVariables contains all global variables after they have been loaded into the kernel. +// +// It can be passed to loadDnsObjects or ebpf.CollectionSpec.LoadAndAssign. +type dnsVariables struct { + UnusedDnsEvent *ebpf.Variable `ebpf:"unused_dns_event"` +} + +// dnsPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadDnsObjects or ebpf.CollectionSpec.LoadAndAssign. +type dnsPrograms struct { + RetinaDnsFilter *ebpf.Program `ebpf:"retina_dns_filter"` +} + +func (p *dnsPrograms) Close() error { + return _DnsClose( + p.RetinaDnsFilter, + ) +} + +func _DnsClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed dns_arm64_bpfel.o +var _DnsBytes []byte diff --git a/pkg/plugin/dns/dns_arm64_bpfel.o b/pkg/plugin/dns/dns_arm64_bpfel.o new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pkg/plugin/dns/dns_ebpf_test.go b/pkg/plugin/dns/dns_ebpf_test.go new file mode 100644 index 0000000000..6189bdc428 --- /dev/null +++ b/pkg/plugin/dns/dns_ebpf_test.go @@ -0,0 +1,411 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//go:build ebpf && linux + +// Tests for the DNS BPF socket filter program. +// +// These load the compiled BPF program and run it against crafted packets +// using BPF_PROG_TEST_RUN, then read the per-CPU scratch map to verify +// the kernel-side parsing logic. +// +// Requires: root (or CAP_BPF+CAP_NET_ADMIN), Linux kernel 5.10+. +// Run: sudo go test -tags=ebpf -v -count=1 ./pkg/plugin/dns/... + +package dns + +import ( + "net" + "testing" + + "github.com/gopacket/gopacket/layers" + "github.com/microsoft/retina/pkg/plugin/ebpftest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// loadTestObjects loads the DNS BPF program and maps for testing. +func loadTestObjects(t *testing.T) *dnsObjects { + t.Helper() + ebpftest.RequirePrivileged(t) + + spec, err := loadDns() + require.NoError(t, err) + ebpftest.RemoveMapPinning(spec) + + var objs dnsObjects + err = spec.LoadAndAssign(&objs, nil) + require.NoError(t, err) + t.Cleanup(func() { objs.Close() }) + return &objs +} + +// --- Tests --- + +// TestDNSQueryFields sends a standard A record query and verifies all event +// fields: address family, protocol, ports, DNS ID, QR, QTYPE, IPs, and dns_off. +func TestDNSQueryFields(t *testing.T) { + objs := loadTestObjects(t) + + srcIP := net.ParseIP("10.0.0.1") + dstIP := net.ParseIP("10.0.0.2") + pkt := ebpftest.BuildDNSQueryPacket(ebpftest.DNSQueryOpts{ + SrcIP: srcIP, DstIP: dstIP, + SrcPort: 12345, DstPort: 53, + ID: 0x1234, Name: "kubernetes.default.svc", QType: layers.DNSTypeA, + }) + + ret := ebpftest.RunSocketFilter(t, objs.RetinaDnsFilter, pkt) + assert.Equal(t, uint32(0), ret) + + event, ok := ebpftest.ReadPerCPUMap[dnsDnsEvent](t, objs.TmpDnsEvents, 0) + require.True(t, ok, "expected DNS event") + + assert.Equal(t, uint8(4), event.Af, "address family") + assert.Equal(t, uint8(17), event.Proto, "protocol = UDP") + assert.Equal(t, uint16(12345), event.SrcPort) + assert.Equal(t, uint16(53), event.DstPort) + assert.Equal(t, uint16(0x1234), event.Id, "DNS ID") + assert.Equal(t, uint8(0), event.Qr, "should be query (QR=0)") + assert.Equal(t, uint16(1), event.Qtype, "QTYPE = A") + assert.Equal(t, uint16(0), event.Ancount, "query has no answers") + assert.NotZero(t, event.Timestamp) + assert.Equal(t, ebpftest.IPToNative("10.0.0.1"), event.SrcIp) + assert.Equal(t, ebpftest.IPToNative("10.0.0.2"), event.DstIp) + // dns_off = ETH(14) + IP(20) + UDP(8) = 42 + assert.Equal(t, uint16(42), event.DnsOff) +} + +// TestDNSResponseFields sends a NOERROR response with one A record answer and +// verifies QR=1, RCODE, answer count, and QTYPE extraction. +func TestDNSResponseFields(t *testing.T) { + objs := loadTestObjects(t) + + pkt := ebpftest.BuildDNSResponsePacket(ebpftest.DNSResponseOpts{ + SrcIP: net.ParseIP("8.8.8.8"), DstIP: net.ParseIP("10.0.0.1"), + SrcPort: 53, DstPort: 40000, + ID: 0xABCD, Name: "example.com", QType: layers.DNSTypeA, + RCode: layers.DNSResponseCodeNoErr, + Answers: []net.IP{net.ParseIP("93.184.216.34")}, + }) + + ebpftest.RunSocketFilter(t, objs.RetinaDnsFilter, pkt) + + event, ok := ebpftest.ReadPerCPUMap[dnsDnsEvent](t, objs.TmpDnsEvents, 0) + require.True(t, ok) + + assert.Equal(t, uint8(1), event.Qr, "should be response (QR=1)") + assert.Equal(t, uint8(0), event.Rcode, "RCODE = NOERROR") + assert.Equal(t, uint16(0xABCD), event.Id) + assert.Equal(t, uint16(1), event.Ancount) + assert.Equal(t, uint16(1), event.Qtype, "QTYPE = A") +} + +// TestDNSResponseNXDOMAIN verifies RCODE=3 (NXDOMAIN) is correctly extracted +// from a negative response with zero answers. +func TestDNSResponseNXDOMAIN(t *testing.T) { + objs := loadTestObjects(t) + + pkt := ebpftest.BuildDNSResponsePacket(ebpftest.DNSResponseOpts{ + SrcIP: net.ParseIP("8.8.8.8"), DstIP: net.ParseIP("10.0.0.1"), + SrcPort: 53, DstPort: 40000, + ID: 0x5678, Name: "nxdomain.test", QType: layers.DNSTypeA, + RCode: layers.DNSResponseCodeNXDomain, + }) + + ebpftest.RunSocketFilter(t, objs.RetinaDnsFilter, pkt) + + event, ok := ebpftest.ReadPerCPUMap[dnsDnsEvent](t, objs.TmpDnsEvents, 0) + require.True(t, ok) + + assert.Equal(t, uint8(1), event.Qr) + assert.Equal(t, uint8(3), event.Rcode, "RCODE = NXDOMAIN") + assert.Equal(t, uint16(0), event.Ancount) +} + +// TestAAAAQuery verifies QTYPE extraction for AAAA (28) queries. +func TestAAAAQuery(t *testing.T) { + objs := loadTestObjects(t) + + pkt := ebpftest.BuildDNSQueryPacket(ebpftest.DNSQueryOpts{ + SrcIP: net.ParseIP("10.0.0.5"), DstIP: net.ParseIP("10.0.0.10"), + SrcPort: 55555, DstPort: 53, + ID: 0x9999, Name: "google.com", QType: layers.DNSTypeAAAA, + }) + + ebpftest.RunSocketFilter(t, objs.RetinaDnsFilter, pkt) + + event, ok := ebpftest.ReadPerCPUMap[dnsDnsEvent](t, objs.TmpDnsEvents, 0) + require.True(t, ok) + + assert.Equal(t, uint16(28), event.Qtype, "QTYPE = AAAA") + assert.Equal(t, uint8(0), event.Qr) +} + +// TestIPv6DNSQuery verifies parsing of a DNS query over IPv6, including +// address family, IPv6 address extraction, and QTYPE. +func TestIPv6DNSQuery(t *testing.T) { + objs := loadTestObjects(t) + + srcIP := net.ParseIP("fd00::1") + dstIP := net.ParseIP("fd00::2") + pkt := ebpftest.BuildDNSQueryPacket(ebpftest.DNSQueryOpts{ + SrcIP: srcIP, DstIP: dstIP, + SrcPort: 44444, DstPort: 53, + ID: 0xBEEF, Name: "v6.example.com", QType: layers.DNSTypeAAAA, + }) + + ebpftest.RunSocketFilter(t, objs.RetinaDnsFilter, pkt) + + event, ok := ebpftest.ReadPerCPUMap[dnsDnsEvent](t, objs.TmpDnsEvents, 0) + require.True(t, ok, "expected DNS event for IPv6") + + assert.Equal(t, uint8(6), event.Af, "address family = IPv6") + assert.Equal(t, uint8(17), event.Proto, "protocol = UDP") + assert.Equal(t, uint16(44444), event.SrcPort) + assert.Equal(t, uint16(53), event.DstPort) + assert.Equal(t, uint16(0xBEEF), event.Id) + assert.Equal(t, uint8(0), event.Qr, "should be query") + assert.Equal(t, uint16(28), event.Qtype, "QTYPE = AAAA") + // Verify IPv6 addresses (stored as raw 16-byte network order). + assert.Equal(t, srcIP.To16(), net.IP(event.SrcIp6[:])) + assert.Equal(t, dstIP.To16(), net.IP(event.DstIp6[:])) + // IPv4 fields should be zero for IPv6 packets. + assert.Equal(t, uint32(0), event.SrcIp) + assert.Equal(t, uint32(0), event.DstIp) + // dns_off = ETH(14) + IPv6(40) + UDP(8) = 62 + assert.Equal(t, uint16(62), event.DnsOff) +} + +// TestIPv6DNSResponse verifies parsing of a DNS response over IPv6. +func TestIPv6DNSResponse(t *testing.T) { + objs := loadTestObjects(t) + + pkt := ebpftest.BuildDNSResponsePacket(ebpftest.DNSResponseOpts{ + SrcIP: net.ParseIP("2001:4860:4860::8888"), DstIP: net.ParseIP("fd00::1"), + SrcPort: 53, DstPort: 50000, + ID: 0xCAFE, Name: "ipv6.example.com", QType: layers.DNSTypeA, + RCode: layers.DNSResponseCodeNoErr, + Answers: []net.IP{net.ParseIP("93.184.216.34")}, + }) + + ebpftest.RunSocketFilter(t, objs.RetinaDnsFilter, pkt) + + event, ok := ebpftest.ReadPerCPUMap[dnsDnsEvent](t, objs.TmpDnsEvents, 0) + require.True(t, ok) + + assert.Equal(t, uint8(6), event.Af, "address family = IPv6") + assert.Equal(t, uint8(1), event.Qr, "should be response") + assert.Equal(t, uint8(0), event.Rcode, "RCODE = NOERROR") + assert.Equal(t, uint16(0xCAFE), event.Id) + assert.Equal(t, uint16(1), event.Ancount) + assert.Equal(t, uint16(1), event.Qtype, "QTYPE = A") +} + +// TestMDNSPort verifies that mDNS traffic on port 5353 is recognized as DNS. +func TestMDNSPort(t *testing.T) { + objs := loadTestObjects(t) + + pkt := ebpftest.BuildDNSQueryPacket(ebpftest.DNSQueryOpts{ + SrcIP: net.ParseIP("10.0.0.1"), DstIP: net.ParseIP("224.0.0.251"), + SrcPort: 5353, DstPort: 5353, + ID: 0x1111, Name: "_http._tcp.local", QType: layers.DNSTypePTR, + }) + + ebpftest.RunSocketFilter(t, objs.RetinaDnsFilter, pkt) + + event, ok := ebpftest.ReadPerCPUMap[dnsDnsEvent](t, objs.TmpDnsEvents, 0) + require.True(t, ok) + + assert.Equal(t, uint16(5353), event.SrcPort) + assert.Equal(t, uint16(5353), event.DstPort) + assert.Equal(t, uint16(12), event.Qtype, "QTYPE = PTR") +} + +// TestMultiLabelDomain sends a query with a 15-label domain name to exercise +// the BPF QNAME walking loop and verify QTYPE is correctly extracted. +func TestMultiLabelDomain(t *testing.T) { + objs := loadTestObjects(t) + + name := "a.b.c.d.e.f.g.h.i.j.kubernetes.default.svc.cluster.local" + pkt := ebpftest.BuildDNSQueryPacket(ebpftest.DNSQueryOpts{ + SrcIP: net.ParseIP("10.0.0.1"), DstIP: net.ParseIP("10.0.0.2"), + SrcPort: 12345, DstPort: 53, + ID: 0x4242, Name: name, QType: layers.DNSTypeSRV, + }) + + ebpftest.RunSocketFilter(t, objs.RetinaDnsFilter, pkt) + + event, ok := ebpftest.ReadPerCPUMap[dnsDnsEvent](t, objs.TmpDnsEvents, 0) + require.True(t, ok) + + assert.Equal(t, uint16(33), event.Qtype, "QTYPE = SRV") +} + +// TestTCPDNS verifies DNS-over-TCP parsing: the BPF program must skip the TCP +// header (using the data offset field) and the 2-byte DNS length prefix. +func TestTCPDNS(t *testing.T) { + objs := loadTestObjects(t) + + pkt := ebpftest.BuildDNSTCPQueryPacket(ebpftest.DNSQueryOpts{ + SrcIP: net.ParseIP("10.0.0.1"), DstIP: net.ParseIP("10.0.0.2"), + SrcPort: 12345, DstPort: 53, + ID: 0x7777, Name: "tcp.example.com", QType: layers.DNSTypeA, + }) + + ret := ebpftest.RunSocketFilter(t, objs.RetinaDnsFilter, pkt) + assert.Equal(t, uint32(0), ret) + + event, ok := ebpftest.ReadPerCPUMap[dnsDnsEvent](t, objs.TmpDnsEvents, 0) + require.True(t, ok, "expected DNS event for TCP") + + assert.Equal(t, uint8(6), event.Proto, "protocol = TCP") + assert.Equal(t, uint16(12345), event.SrcPort) + assert.Equal(t, uint16(53), event.DstPort) + assert.Equal(t, uint16(0x7777), event.Id) + assert.Equal(t, uint16(1), event.Qtype, "QTYPE = A") + // dns_off = ETH(14) + IP(20) + TCP(20) + len_prefix(2) = 56 + assert.Equal(t, uint16(56), event.DnsOff) +} + +// TestTCPDNSEdgeCases verifies the BPF program handles TCP edge cases without +// crashing: a SYN to port 53 with no payload, and a segment with only the +// 2-byte length prefix but no DNS data. In both cases the program may write +// a partial event to the scratch map but does NOT call bpf_perf_event_output, +// so userspace never sees it. +func TestTCPDNSEdgeCases(t *testing.T) { + t.Run("SYN no payload", func(t *testing.T) { + objs := loadTestObjects(t) + pkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: net.ParseIP("10.0.0.1"), DstIP: net.ParseIP("10.0.0.2"), + SrcPort: 12345, DstPort: 53, + SYN: true, + }) + ebpftest.RunSocketFilter(t, objs.RetinaDnsFilter, pkt) + }) + + t.Run("length prefix only", func(t *testing.T) { + objs := loadTestObjects(t) + pkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: net.ParseIP("10.0.0.1"), DstIP: net.ParseIP("10.0.0.2"), + SrcPort: 12345, DstPort: 53, + PSH: true, ACK: true, + Payload: []byte{0x00, 0x20}, // claims 32 bytes but has none + }) + ebpftest.RunSocketFilter(t, objs.RetinaDnsFilter, pkt) + }) +} + +// TestNonDNSPort_NoEvent verifies that UDP packets to non-DNS ports (neither +// 53 nor 5353) are silently dropped without writing an event. +func TestNonDNSPort_NoEvent(t *testing.T) { + objs := loadTestObjects(t) + + pkt := ebpftest.BuildUDPPacket(ebpftest.UDPPacketOpts{ + SrcIP: net.ParseIP("10.0.0.1"), DstIP: net.ParseIP("10.0.0.2"), + SrcPort: 12345, DstPort: 8080, + PayloadSize: 20, + }) + ebpftest.RunSocketFilter(t, objs.RetinaDnsFilter, pkt) + + _, ok := ebpftest.ReadPerCPUMap[dnsDnsEvent](t, objs.TmpDnsEvents, 0) + assert.False(t, ok, "non-DNS port should not produce an event") +} + +// TestICMP_NoEvent verifies that non-TCP/UDP protocols are filtered out. +func TestICMP_NoEvent(t *testing.T) { + objs := loadTestObjects(t) + + pkt := ebpftest.BuildICMPPacket(net.ParseIP("10.0.0.1"), net.ParseIP("10.0.0.2")) + ebpftest.RunSocketFilter(t, objs.RetinaDnsFilter, pkt) + + _, ok := ebpftest.ReadPerCPUMap[dnsDnsEvent](t, objs.TmpDnsEvents, 0) + assert.False(t, ok, "ICMP should not produce a DNS event") +} + +// TestNonIPv4_NoEvent verifies that non-IP EtherTypes (ARP) are filtered out. +func TestNonIPv4_NoEvent(t *testing.T) { + objs := loadTestObjects(t) + + pkt := ebpftest.BuildNonIPPacket(layers.EthernetTypeARP) + ebpftest.RunSocketFilter(t, objs.RetinaDnsFilter, pkt) + + _, ok := ebpftest.ReadPerCPUMap[dnsDnsEvent](t, objs.TmpDnsEvents, 0) + assert.False(t, ok, "ARP should not produce a DNS event") +} + +// TestMalformedPackets verifies the BPF program handles edge cases without +// crashing: runt frames, truncated IP headers, and payloads too short for +// a DNS header. +func TestMalformedPackets(t *testing.T) { + t.Run("runt packet", func(t *testing.T) { + objs := loadTestObjects(t) + pkt := ebpftest.BuildRuntPacket() + padded := append(make([]byte, 14), pkt...) //nolint:gocritic // intentional prepend + // Runt packets may be rejected by the kernel (EINVAL). Verify no panic. + _, _, err := objs.RetinaDnsFilter.Test(padded) + _ = err + }) + + t.Run("truncated IP", func(t *testing.T) { + objs := loadTestObjects(t) + pkt := ebpftest.BuildTruncatedIPPacket() + ebpftest.RunSocketFilter(t, objs.RetinaDnsFilter, pkt) + + _, ok := ebpftest.ReadPerCPUMap[dnsDnsEvent](t, objs.TmpDnsEvents, 0) + assert.False(t, ok, "truncated IP should not produce an event") + }) + + t.Run("UDP too short for DNS header", func(t *testing.T) { + objs := loadTestObjects(t) + // DNS port but payload < 12 bytes (DNS header size). + // BPF program writes partial event (timestamp) to scratch map but + // does NOT call bpf_perf_event_output, so userspace never sees it. + // Just verify no crash. + pkt := ebpftest.BuildUDPPacket(ebpftest.UDPPacketOpts{ + SrcIP: net.ParseIP("10.0.0.1"), DstIP: net.ParseIP("10.0.0.2"), + SrcPort: 12345, DstPort: 53, + Payload: []byte{0x00, 0x01}, + }) + ebpftest.RunSocketFilter(t, objs.RetinaDnsFilter, pkt) + }) +} + +// TestReturnValue verifies the socket filter always returns 0 regardless of +// packet type. The DNS plugin captures events via perf buffer, not the +// socket filter return value. +func TestReturnValue(t *testing.T) { + objs := loadTestObjects(t) + + tests := []struct { + name string + pkt []byte + }{ + { + "DNS query", + ebpftest.BuildDNSQueryPacket(ebpftest.DNSQueryOpts{ + SrcIP: net.ParseIP("10.0.0.1"), DstIP: net.ParseIP("10.0.0.2"), + SrcPort: 12345, DstPort: 53, + ID: 1, Name: "test.com", QType: layers.DNSTypeA, + }), + }, + { + "non-DNS UDP", + ebpftest.BuildUDPPacket(ebpftest.UDPPacketOpts{ + SrcIP: net.ParseIP("10.0.0.1"), DstIP: net.ParseIP("10.0.0.2"), + SrcPort: 1000, DstPort: 8080, PayloadSize: 20, + }), + }, + { + "ICMP", + ebpftest.BuildICMPPacket(net.ParseIP("10.0.0.1"), net.ParseIP("10.0.0.2")), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ret := ebpftest.RunSocketFilter(t, objs.RetinaDnsFilter, tt.pkt) + assert.Equal(t, uint32(0), ret, "socket filter should always return 0") + }) + } +} diff --git a/pkg/plugin/dns/dns_linux.go b/pkg/plugin/dns/dns_linux.go index 0e41bb7ca9..164f2b64cf 100644 --- a/pkg/plugin/dns/dns_linux.go +++ b/pkg/plugin/dns/dns_linux.go @@ -1,26 +1,44 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -// Package dns contains the Retina DNS plugin. It uses the Inspektor Gadget DNS tracer to capture DNS events. +// Package dns contains the Retina DNS plugin. It uses eBPF socket filters to capture DNS events. package dns import ( "context" "net" - "os" + "syscall" + "unsafe" v1 "github.com/cilium/cilium/pkg/hubble/api/v1" - "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/dns/tracer" - "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/dns/types" - "github.com/inspektor-gadget/inspektor-gadget/pkg/utils/host" + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/perf" + "github.com/gopacket/gopacket" + "github.com/gopacket/gopacket/layers" + "github.com/microsoft/retina/internal/ktime" kcfg "github.com/microsoft/retina/pkg/config" "github.com/microsoft/retina/pkg/enricher" "github.com/microsoft/retina/pkg/log" "github.com/microsoft/retina/pkg/metrics" - "github.com/microsoft/retina/pkg/plugin/common" + plugincommon "github.com/microsoft/retina/pkg/plugin/common" "github.com/microsoft/retina/pkg/plugin/registry" "github.com/microsoft/retina/pkg/utils" + "github.com/pkg/errors" "go.uber.org/zap" + "golang.org/x/sys/unix" +) + +// Per-arch target needed because vmlinux.h differs between amd64/arm64. +// Cross-generate: GOARCH=arm64 go generate ./pkg/plugin/dns/... +// +//go:generate go run github.com/cilium/ebpf/cmd/bpf2go@v0.18.0 -cflags "-Wall" -target ${GOARCH} -type dns_event dns ./_cprog/dns.c -- -I../lib/_${GOARCH} -I../lib/common/libbpf/_src + +const ( + // perCPUBuffer is the max number of pages passed to NewPerfReader. + // The reader tries this first, then halves until allocation succeeds. + perCPUBuffer = 8192 + recordsBuffer = 1000 // Channel buffer for records + workers = 2 // Number of worker goroutines ) func init() { @@ -38,30 +56,58 @@ func (d *dns) Name() string { return name } -func (d *dns) Generate(ctx context.Context) error { - return nil -} - -func (d *dns) Compile(ctx context.Context) error { - return nil -} +// Generate and Compile are no-ops. The plugin manager lifecycle requires them, +// but DNS uses bpf2go which pre-compiles the BPF program at build time and +// embeds it in the binary — no runtime code generation or compilation needed. +func (d *dns) Generate(_ context.Context) error { return nil } +func (d *dns) Compile(_ context.Context) error { return nil } func (d *dns) Init() error { - // Create tracer. In this case no parameters are passed. - err := host.Init(host.Config{}) - tracer, err := tracer.NewTracer() + objs := &dnsObjects{} + if err := loadDnsObjects(objs, &ebpf.CollectionOptions{ + Maps: ebpf.MapOptions{ + PinPath: plugincommon.MapPath, + }, + }); err != nil { + return errors.Wrap(err, "failed to load eBPF objects") + } + + // Bind to all interfaces (ifindex=0). The BPF program dedupes by + // capturing only PACKET_HOST — see dns.c for the filter logic. + sock, err := utils.OpenRawSocket(0) if err != nil { - d.l.Error("Failed to create tracer", zap.Error(err)) - return err + objs.Close() + return errors.Wrap(err, "failed to open raw socket") } - d.tracer = tracer - d.tracer.SetEventHandler(d.eventHandler) - d.pid = uint32(os.Getpid()) - d.l.Info("Initialized dns plugin") + + fd := objs.RetinaDnsFilter.FD() + if err = syscall.SetsockoptInt(sock, syscall.SOL_SOCKET, unix.SO_ATTACH_BPF, fd); err != nil { + syscall.Close(sock) //nolint:errcheck // best-effort cleanup + objs.Close() + return errors.Wrap(err, "failed to attach BPF to socket") + } + + reader, err := plugincommon.NewPerfReader(d.l, objs.RetinaDnsEvents, perCPUBuffer, 1) + if err != nil { + syscall.Close(sock) //nolint:errcheck // best-effort cleanup + objs.Close() + return errors.Wrap(err, "failed to create perf reader") + } + + // Only assign to struct fields after all setup succeeds, + // so partial Init failures don't leave dangling resources. + d.objs = objs + d.sock = sock + d.reader = reader + + d.l.Info("DNS plugin initialized") return nil } func (d *dns) Start(ctx context.Context) error { + d.isRunning = true + d.recordsChannel = make(chan perf.Record, recordsBuffer) + if d.cfg.EnablePodLevel { if enricher.IsInitialized() { d.enricher = enricher.Instance() @@ -69,89 +115,165 @@ func (d *dns) Start(ctx context.Context) error { d.l.Warn("retina enricher is not initialized") } } - if err := d.tracer.Attach(d.pid); err != nil { - d.l.Error("Failed to attach tracer", zap.Error(err)) - return err + + return d.run(ctx) +} + +func (d *dns) run(ctx context.Context) error { + for i := range workers { + d.wg.Add(1) + go d.processRecord(ctx, i) } + // readEvents is not tracked by wg — it blocks inside reader.Read() which + // is only unblocked by reader.Close() in Stop(). The lifecycle is: + // ctx cancel → run() returns → Stop() → reader.Close() → readEvents exits. + go d.readEvents(ctx) <-ctx.Done() + d.wg.Wait() return nil } -func (d *dns) Stop() error { - if d.tracer != nil { - d.tracer.Detach(d.pid) - d.tracer.Close() +func (d *dns) readEvents(ctx context.Context) { + for { + // Note: ctx.Done is only checked between Read() calls. Once blocked + // inside Read(), only reader.Close() (called from Stop) unblocks it. + select { + case <-ctx.Done(): + return + default: + record, err := d.reader.Read() + if err != nil { + if errors.Is(err, perf.ErrClosed) { + return + } + d.l.Error("Error reading perf event", zap.Error(err)) + continue + } + + if record.LostSamples > 0 { + metrics.LostEventsCounter.WithLabelValues(utils.Kernel, name).Add(float64(record.LostSamples)) + continue + } + + select { + case d.recordsChannel <- record: + default: + metrics.LostEventsCounter.WithLabelValues(utils.BufferedChannel, name).Inc() + } + } } - d.l.Info("Stopped dns plugin") - return nil } -func (d *dns) SetupChannel(c chan *v1.Event) error { - d.externalChannel = c - return nil +func (d *dns) processRecord(ctx context.Context, _ int) { + defer d.wg.Done() + + for { + select { + case <-ctx.Done(): + return + case record := <-d.recordsChannel: + d.handleDNSEvent(record) + } + } } -func (d *dns) eventHandler(event *types.Event) { - if event == nil { +func (d *dns) handleDNSEvent(record perf.Record) { + eventSize := int(unsafe.Sizeof(dnsDnsEvent{})) + if len(record.RawSample) < eventSize { return } - d.l.Debug("Event received", zap.Any("event", event)) - // Update basic metrics - if event.Qr == types.DNSPktTypeQuery { - m = metrics.DNSRequestCounter - } else if event.Qr == types.DNSPktTypeResponse { - m = metrics.DNSResponseCounter + event := (*dnsDnsEvent)(unsafe.Pointer(&record.RawSample[0])) //nolint:gosec // perf record is aligned + + // Increment basic counter (always, regardless of pod-level) + if event.Qr == 0 { + metrics.DNSRequestCounter.WithLabelValues().Inc() } else { - return + metrics.DNSResponseCounter.WithLabelValues().Inc() } - m.WithLabelValues().Inc() if !d.cfg.EnablePodLevel { return } + // Unlike the old IG tracer which observed both ingress and egress per + // endpoint, this plugin only sees each packet once — the BPF filter + // drops PACKET_OUTGOING so only RX-side observations remain (see dns.c). + // We derive direction from QR: queries are egress, responses are ingress. + const ( + dirIngress uint8 = 2 + dirEgress uint8 = 3 + ) var dir uint8 - if event.PktType == "HOST" { - // Ingress. - dir = 2 - } else if event.PktType == "OUTGOING" { - // Egress. - dir = 3 + if event.Qr == 0 { + dir = dirEgress } else { + dir = dirIngress + } + + // IP addresses — copy raw bytes, no endian conversion needed + var srcIP, dstIP net.IP + switch event.Af { + case 4: + var srcBuf, dstBuf [net.IPv4len]byte + *(*uint32)(unsafe.Pointer(&srcBuf[0])) = event.SrcIp //nolint:gosec // same size + *(*uint32)(unsafe.Pointer(&dstBuf[0])) = event.DstIp //nolint:gosec // same size + srcIP = srcBuf[:] + dstIP = dstBuf[:] + case 6: + srcIP = event.SrcIp6[:] + dstIP = event.DstIp6[:] + default: return } - // Update advanced metrics. + // Parse DNS name and response addresses from the packet payload. + var dnsName string + var addresses []string + var qtype layers.DNSType + if packetData := record.RawSample[eventSize:]; len(packetData) > 0 && int(event.DnsOff) < len(packetData) { + dnsName, addresses, qtype = d.parseDNSPayload(packetData[event.DnsOff:], event.Qr == 1) + } + // Prefer BPF-extracted qtype; fall back to gopacket if BPF couldn't + // parse it (e.g. truncated name, packet too short). + if event.Qtype != 0 { + qtype = layers.DNSType(event.Qtype) + } + qTypes := []string{qtype.String()} + + var qrStr string + if event.Qr == 0 { + qrStr = "Q" + } else { + qrStr = "R" + } + fl := utils.ToFlow( d.l, - int64(event.Timestamp), - net.ParseIP(event.SrcIP), - net.ParseIP(event.DstIP), - uint32(event.SrcPort), - uint32(event.DstPort), - uint8(common.ProtocolToFlow(event.Protocol)), - dir, + ktime.MonotonicOffset.Nanoseconds()+int64(event.Timestamp), //nolint:gosec // timestamp fits in int64 + srcIP, dstIP, + uint32(event.SrcPort), uint32(event.DstPort), + event.Proto, dir, utils.Verdict_DNS, ) + if fl == nil { + return + } - meta := &utils.RetinaMetadata{} - - utils.AddDNSInfo(fl, meta, string(event.Qr), common.RCodeToFlow(event.Rcode), event.DNSName, []string{event.QType}, event.NumAnswers, event.Addresses) - - // Add metadata to the flow. - utils.AddRetinaMetadata(fl, meta) + ext := utils.NewExtensions() + utils.AddDNSInfo(fl, ext, qrStr, uint32(event.Rcode), dnsName, qTypes, int(event.Ancount), addresses) + utils.SetExtensions(fl, ext) - ev := (&v1.Event{ + ev := &v1.Event{ Event: fl, Timestamp: fl.GetTime(), - }) + } + if d.enricher != nil { d.enricher.Write(ev) } - // Send event to external channel. if d.externalChannel != nil { select { case d.externalChannel <- ev: @@ -160,3 +282,67 @@ func (d *dns) eventHandler(event *types.Event) { } } } + +// parseDNSPayload extracts the query name, response addresses, and query type +// from the raw DNS payload using gopacket. +//nolint:nonamedreturns // named returns used by defer recovery +func (d *dns) parseDNSPayload(payload []byte, isResponse bool) ( + dnsName string, addresses []string, qtype layers.DNSType, +) { + if len(payload) < 12 { + return "", nil, 0 + } + + // gopacket's DNS decoder can panic on malformed input. + defer func() { + if r := recover(); r != nil { + d.l.Debug("DNS decode panic (malformed packet)", zap.Any("recover", r)) + dnsName, addresses, qtype = "", nil, 0 + } + }() + + var parser layers.DNS + if err := parser.DecodeFromBytes(payload, gopacket.NilDecodeFeedback); err != nil { + return "", nil, 0 + } + + if len(parser.Questions) > 0 { + dnsName = string(parser.Questions[0].Name) + "." + qtype = parser.Questions[0].Type + } + + if isResponse { + for i := range parser.Answers { + if parser.Answers[i].IP != nil { + addresses = append(addresses, parser.Answers[i].IP.String()) + } + } + } + + return dnsName, addresses, qtype +} + +func (d *dns) Stop() error { + if !d.isRunning { + return nil + } + if d.reader != nil { + d.reader.Close() + } + if d.recordsChannel != nil { + close(d.recordsChannel) + } + if d.sock != 0 { + syscall.Close(d.sock) //nolint:errcheck // best-effort cleanup + } + if d.objs != nil { + d.objs.Close() + } + d.isRunning = false + return nil +} + +func (d *dns) SetupChannel(c chan *v1.Event) error { + d.externalChannel = c + return nil +} diff --git a/pkg/plugin/dns/dns_linux_test.go b/pkg/plugin/dns/dns_linux_test.go index 397c10dc55..92a3574c80 100644 --- a/pkg/plugin/dns/dns_linux_test.go +++ b/pkg/plugin/dns/dns_linux_test.go @@ -1,253 +1,285 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//nolint:typecheck package dns import ( "context" - "errors" - "reflect" + "log/slog" + "net" + "os" "testing" - "time" + "unsafe" - "github.com/cilium/cilium/api/v1/flow" v1 "github.com/cilium/cilium/pkg/hubble/api/v1" - "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/dns/types" + "github.com/cilium/ebpf/perf" + "github.com/gopacket/gopacket" + "github.com/gopacket/gopacket/layers" "github.com/microsoft/retina/pkg/config" - "github.com/microsoft/retina/pkg/controllers/cache" "github.com/microsoft/retina/pkg/enricher" "github.com/microsoft/retina/pkg/log" "github.com/microsoft/retina/pkg/metrics" - "github.com/microsoft/retina/pkg/plugin/common/mocks" - "github.com/microsoft/retina/pkg/pubsub" - "github.com/microsoft/retina/pkg/utils" - "github.com/prometheus/client_golang/prometheus" - dto "github.com/prometheus/client_model/go" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" - "gotest.tools/v3/assert" ) -func TestStop(t *testing.T) { +func TestMain(m *testing.M) { log.SetupZapLogger(log.GetDefaultLogOpts()) - d := &dns{ - l: log.Logger().Named(name), - pid: 1234, - } - // Check nil tracer. - d.Stop() - - // Check with tracer. - ctrl := gomock.NewController(t) - defer ctrl.Finish() - m := mocks.NewMockITracer(ctrl) - m.EXPECT().Detach(d.pid).Return(nil).Times(1) - m.EXPECT().Close().Times(1) - d.tracer = m - d.Stop() + metrics.InitializeMetrics(slog.Default()) + os.Exit(m.Run()) } -func TestStart(t *testing.T) { - ctxTimeout, cancel := context.WithTimeout(context.Background(), 3*time.Second) - defer cancel() - - log.SetupZapLogger(log.GetDefaultLogOpts()) - - c := cache.New(pubsub.New()) - e := enricher.New(ctxTimeout, c) - e.Run() - defer e.Reader.Close() +func TestNew(t *testing.T) { + cfg := &config.Config{EnablePodLevel: true} + d := New(cfg) + require.NotNil(t, d) + assert.Equal(t, name, d.Name()) +} +func TestStop(t *testing.T) { d := &dns{ + cfg: &config.Config{EnablePodLevel: true}, l: log.Logger().Named(name), - pid: 1234, - cfg: &config.Config{ - EnablePodLevel: true, - }, } + // Should not panic when not running. + require.NoError(t, d.Stop()) +} - ctrl := gomock.NewController(t) - defer ctrl.Finish() - m := mocks.NewMockITracer(ctrl) - m.EXPECT().Attach(d.pid).Return(nil).Times(1) - d.tracer = m - err := d.Start(ctxTimeout) - assert.Equal(t, err, nil) - if d.enricher == nil { - t.Fatal("enricher is nil") +func TestSetupChannel(t *testing.T) { + d := &dns{ + cfg: &config.Config{EnablePodLevel: true}, + l: log.Logger().Named(name), } + ch := make(chan *v1.Event, 10) + require.NoError(t, d.SetupChannel(ch)) + assert.Equal(t, ch, d.externalChannel) +} - // Test error case. - expected := errors.New("Error") - m = mocks.NewMockITracer(ctrl) - m.EXPECT().Attach(d.pid).Return(expected).Times(1) - d.tracer = m +func TestGenerate(t *testing.T) { + d := &dns{cfg: &config.Config{}, l: log.Logger().Named(name)} + require.NoError(t, d.Generate(context.Background())) +} - err = d.Start(ctxTimeout) - assert.Error(t, err, expected.Error()) +// buildTestRecord creates a synthetic perf.Record containing a dns_event struct +// followed by raw packet bytes, mimicking what the BPF program produces. +// The struct is reinterpreted as raw bytes via unsafe — the same layout the +// BPF perf ring uses, so handleDNSEvent's unsafe cast works correctly. +func buildTestRecord(event dnsDnsEvent, packetData []byte) perf.Record { + eventSize := int(unsafe.Sizeof(event)) + eventBytes := unsafe.Slice((*byte)(unsafe.Pointer(&event)), eventSize) //nolint:gosec // test-only + buf := make([]byte, eventSize+len(packetData)) + copy(buf, eventBytes) + copy(buf[eventSize:], packetData) + return perf.Record{RawSample: buf} } -func TestMalformedEventHandler(t *testing.T) { - log.SetupZapLogger(log.GetDefaultLogOpts()) +// TestHandleDNSEvent_RequestCounter verifies the request counter increments +// for QR=0 events when pod-level is disabled (only counters, no flow). +func TestHandleDNSEvent_RequestCounter(t *testing.T) { d := &dns{ - l: log.Logger().Named(name), + cfg: &config.Config{EnablePodLevel: false}, + l: log.Logger().Named(name), } + before := testutil.ToFloat64(metrics.DNSRequestCounter.WithLabelValues()) + record := buildTestRecord(dnsDnsEvent{Timestamp: 1000, Qr: 0}, nil) + d.handleDNSEvent(record) + after := testutil.ToFloat64(metrics.DNSRequestCounter.WithLabelValues()) + assert.InDelta(t, before+1, after, 0, "request counter should increment by 1") +} - // Test nil event. - m = nil - d.eventHandler(nil) - assert.Equal(t, m, nil) - - // Test event with no Query type. - m = nil - event := &types.Event{ - Qr: "Z", +// TestHandleDNSEvent_ResponseCounter verifies the response counter increments +// for QR=1 events. +func TestHandleDNSEvent_ResponseCounter(t *testing.T) { + d := &dns{ + cfg: &config.Config{EnablePodLevel: false}, + l: log.Logger().Named(name), } - d.eventHandler(event) - assert.Equal(t, m, nil) + before := testutil.ToFloat64(metrics.DNSResponseCounter.WithLabelValues()) + record := buildTestRecord(dnsDnsEvent{Timestamp: 2000, Qr: 1, Rcode: 3}, nil) + d.handleDNSEvent(record) + after := testutil.ToFloat64(metrics.DNSResponseCounter.WithLabelValues()) + assert.InDelta(t, before+1, after, 0, "response counter should increment by 1") } -func TestRequestEventHandler(t *testing.T) { - log.SetupZapLogger(log.GetDefaultLogOpts()) - metrics.InitializeMetrics() +// TestHandleDNSEvent_TooShortRecord verifies a record shorter than the event +// struct is silently skipped (no counter change). +func TestHandleDNSEvent_TooShortRecord(t *testing.T) { + d := &dns{ + cfg: &config.Config{EnablePodLevel: true}, + l: log.Logger().Named(name), + } + reqBefore := testutil.ToFloat64(metrics.DNSRequestCounter.WithLabelValues()) + respBefore := testutil.ToFloat64(metrics.DNSResponseCounter.WithLabelValues()) + record := perf.Record{RawSample: []byte{0x01, 0x02, 0x03}} + d.handleDNSEvent(record) + assert.InDelta(t, reqBefore, testutil.ToFloat64(metrics.DNSRequestCounter.WithLabelValues()), 0) + assert.InDelta(t, respBefore, testutil.ToFloat64(metrics.DNSResponseCounter.WithLabelValues()), 0) +} +// TestHandleDNSEvent_PktTypeIgnored verifies that pkt_type does not affect +// flow emission — direction is derived from QR, not pkt_type. Even +// PACKET_OUTGOING (normally filtered by BPF) produces a flow if it somehow +// reaches userspace. +func TestHandleDNSEvent_PktTypeIgnored(t *testing.T) { + ch := make(chan *v1.Event, 10) d := &dns{ - l: log.Logger().Named(name), - cfg: &config.Config{ - EnablePodLevel: true, - }, + cfg: &config.Config{EnablePodLevel: true}, + l: log.Logger().Named(name), + externalChannel: ch, } + record := buildTestRecord(dnsDnsEvent{ + Timestamp: 1000, Af: 4, PktType: 4, // PACKET_OUTGOING (filtered by BPF, not by Go) + SrcIp: 0x0100000A, DstIp: 0x0200000A, + }, nil) + d.handleDNSEvent(record) + assert.Len(t, ch, 1, "pkt_type should not affect flow emission; direction comes from QR") +} - // Test event with Query type. - m = nil - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - event := &types.Event{ - Qr: "Q", - Rcode: "No Error", - QType: "A", - DNSName: "test.com", - Addresses: []string{}, - NumAnswers: 0, - PktType: "OUTGOING", - SrcIP: "1.1.1.1", - DstIP: "2.2.2.2", - SrcPort: 58, - DstPort: 8080, - Protocol: "TCP", +// TestHandleDNSEvent_UnknownAF verifies packets with an unknown address family +// are dropped when pod-level is enabled. +func TestHandleDNSEvent_UnknownAF(t *testing.T) { + ch := make(chan *v1.Event, 10) + d := &dns{ + cfg: &config.Config{EnablePodLevel: true}, + l: log.Logger().Named(name), + externalChannel: ch, } - c := prometheus.NewCounter(prometheus.CounterOpts{}) + record := buildTestRecord(dnsDnsEvent{ + Timestamp: 1000, Af: 99, PktType: 0, + }, nil) + d.handleDNSEvent(record) + assert.Empty(t, ch, "unknown AF should not emit a flow") +} - // Basic metrics. - mockCV := metrics.NewMockCounterVec(ctrl) - mockCV.EXPECT().WithLabelValues().Return(c).Times(1) - before := value(c) - metrics.DNSRequestCounter = mockCV +// TestParseDNSPayload_Query verifies query name and type extraction. +func TestParseDNSPayload_Query(t *testing.T) { + d := &dns{l: log.Logger().Named(name)} - // Advanced metrics. - mockEnricher := enricher.NewMockEnricherInterface(ctrl) - mockEnricher.EXPECT().Write(EventMatched( - utils.DNSType_QUERY, 0, event.DNSName, []string{event.QType}, 0, []string{}, - )).Times(1) - d.enricher = mockEnricher + payload := buildMinimalDNSQuery("example.com", 1) + dnsName, addresses, qtype := d.parseDNSPayload(payload, false) - d.eventHandler(event) - after := value(c) - assert.Equal(t, after-before, float64(1)) + assert.Equal(t, "example.com.", dnsName) + assert.Empty(t, addresses) + assert.Equal(t, uint16(1), uint16(qtype)) } -func TestResponseEventHandler(t *testing.T) { - log.SetupZapLogger(log.GetDefaultLogOpts()) - metrics.InitializeMetrics() +// TestParseDNSPayload_Response verifies response parsing with an A record. +func TestParseDNSPayload_Response(t *testing.T) { + d := &dns{l: log.Logger().Named(name)} - d := &dns{ - l: log.Logger().Named(name), - cfg: &config.Config{ - EnablePodLevel: true, - }, - } + payload := buildMinimalDNSResponse("test.local", 1, net.ParseIP("1.2.3.4")) + dnsName, addresses, qtype := d.parseDNSPayload(payload, true) - // Test event with Query type. - m = nil - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - event := &types.Event{ - Qr: "R", - Rcode: "No Error", - QType: "A", - DNSName: "test.com", - Addresses: []string{"1.1.1.1", "2.2.2.2"}, - NumAnswers: 2, - PktType: "HOST", - SrcIP: "1.1.1.1", - DstIP: "2.2.2.2", - SrcPort: 58, - DstPort: 8080, - Protocol: "TCP", - } + assert.Equal(t, "test.local.", dnsName) + assert.Equal(t, uint16(1), uint16(qtype)) + require.Len(t, addresses, 1) + assert.Equal(t, "1.2.3.4", addresses[0]) +} - // Basic metrics. - c := prometheus.NewCounter(prometheus.CounterOpts{}) - mockCV := metrics.NewMockCounterVec(ctrl) - mockCV.EXPECT().WithLabelValues().Return(c).Times(1) - before := value(c) - metrics.DNSResponseCounter = mockCV +// TestParseDNSPayload_TooShort verifies payloads under 12 bytes return empty. +func TestParseDNSPayload_TooShort(t *testing.T) { + d := &dns{l: log.Logger().Named(name)} - // Advanced metrics. - mockEnricher := enricher.NewMockEnricherInterface(ctrl) - mockEnricher.EXPECT().Write(EventMatched( - utils.DNSType_RESPONSE, 0, event.DNSName, []string{event.QType}, 2, []string{"1.1.1.1", "2.2.2.2"}, - )).Times(1) - d.enricher = mockEnricher + dnsName, addresses, qtype := d.parseDNSPayload([]byte{0x00, 0x01}, false) - d.eventHandler(event) - after := value(c) - assert.Equal(t, after-before, float64(1)) + assert.Empty(t, dnsName) + assert.Nil(t, addresses) + assert.Equal(t, uint16(0), uint16(qtype)) } -func value(c prometheus.Counter) float64 { - m := &dto.Metric{} - c.Write(m) - - return m.Counter.GetValue() +// TestParseDNSPayload_Malformed verifies malformed DNS payloads don't panic. +func TestParseDNSPayload_Malformed(t *testing.T) { + d := &dns{l: log.Logger().Named(name)} + + // 12-byte header with QDCOUNT=1 but no question data. + payload := make([]byte, 20) + payload[4] = 0x00 + payload[5] = 0x01 + assert.NotPanics(t, func() { + d.parseDNSPayload(payload, false) + }) } -// Helpers. +// TestHandleDNSEvent_WithPacketData verifies handleDNSEvent processes a +// complete event with attached packet data, writes to the enricher, and +// emits a flow to the external channel with correct DNS info. +func TestHandleDNSEvent_WithPacketData(t *testing.T) { + ctrl := gomock.NewController(t) -type EventMatcher struct { - qType utils.DNSType - rCode uint32 - query string - qTypes []string - numAnswers uint32 - ips []string -} + menricher := enricher.NewMockEnricherInterface(ctrl) //nolint:typecheck + menricher.EXPECT().Write(gomock.Any()).MinTimes(1) + + ch := make(chan *v1.Event, 10) + d := &dns{ + cfg: &config.Config{EnablePodLevel: true}, + l: log.Logger().Named(name), + enricher: menricher, + externalChannel: ch, + } -func (m *EventMatcher) Matches(x interface{}) bool { - inputFlow := x.(*v1.Event).Event.(*flow.Flow) - expectedDNS, expectedDNSType, expectedNumResponses := utils.GetDNS(inputFlow) - return expectedDNS != nil && - expectedDNS.GetRcode() == m.rCode && - expectedDNS.GetQuery() == m.query && - reflect.DeepEqual(expectedDNS.GetIps(), m.ips) && - reflect.DeepEqual(expectedDNS.GetQtypes(), m.qTypes) && - expectedDNSType == m.qType && - expectedNumResponses == m.numAnswers + dnsPayload := buildMinimalDNSQuery("k8s.io", 1) + // Build a minimal packet: Ethernet(14) + IP(20) + UDP(8) + DNS. + // dns_off = 42 points to the DNS payload within the packet. + pktData := make([]byte, 42+len(dnsPayload)) + copy(pktData[42:], dnsPayload) + + record := buildTestRecord(dnsDnsEvent{ + Timestamp: 5000, + Af: 4, + Proto: 17, + PktType: 0, // PACKET_HOST + Qr: 0, + Qtype: 1, + SrcPort: 12345, + DstPort: 53, + DnsOff: 42, + SrcIp: 0x0100000A, // 10.0.0.1 + DstIp: 0x0200000A, // 10.0.0.2 + }, pktData) + + d.handleDNSEvent(record) + + require.Len(t, ch, 1, "expected one flow on the external channel") + ev := <-ch + fl := ev.GetFlow() + require.NotNil(t, fl) + assert.NotNil(t, fl.GetL4().GetUDP()) + assert.Equal(t, uint32(53), fl.GetL4().GetUDP().GetDestinationPort()) + assert.NotNil(t, fl.GetExtensions()) } -func (m *EventMatcher) String() string { - return "is anything" +// --- DNS payload helpers --- + +func buildMinimalDNSQuery(name string, qtype uint16) []byte { + buf := gopacket.NewSerializeBuffer() + dns := &layers.DNS{ + ID: 1, RD: true, QDCount: 1, + Questions: []layers.DNSQuestion{{ + Name: []byte(name), Type: layers.DNSType(qtype), Class: layers.DNSClassIN, + }}, + } + if err := dns.SerializeTo(buf, gopacket.SerializeOptions{FixLengths: true}); err != nil { + panic("failed to serialize DNS query: " + err.Error()) + } + return buf.Bytes() } -func EventMatched(qType utils.DNSType, rCode uint32, query string, qTypes []string, numAnswers uint32, ips []string) gomock.Matcher { - return &EventMatcher{ - qType: qType, - rCode: rCode, - query: query, - qTypes: qTypes, - numAnswers: numAnswers, - ips: ips, +func buildMinimalDNSResponse(name string, qtype uint16, answerIP net.IP) []byte { + buf := gopacket.NewSerializeBuffer() + dns := &layers.DNS{ + ID: 1, QR: true, RD: true, QDCount: 1, ANCount: 1, + Questions: []layers.DNSQuestion{{ + Name: []byte(name), Type: layers.DNSType(qtype), Class: layers.DNSClassIN, + }}, + Answers: []layers.DNSResourceRecord{{ + Name: []byte(name), Type: layers.DNSType(qtype), Class: layers.DNSClassIN, + TTL: 60, IP: answerIP, + }}, + } + if err := dns.SerializeTo(buf, gopacket.SerializeOptions{FixLengths: true}); err != nil { + panic("failed to serialize DNS response: " + err.Error()) } + return buf.Bytes() } diff --git a/pkg/plugin/dns/dns_x86_bpfel.go b/pkg/plugin/dns/dns_x86_bpfel.go new file mode 100644 index 0000000000..61b3098ea8 --- /dev/null +++ b/pkg/plugin/dns/dns_x86_bpfel.go @@ -0,0 +1,159 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build 386 || amd64 + +package dns + +import ( + "bytes" + _ "embed" + "fmt" + "io" + + "github.com/cilium/ebpf" +) + +type dnsDnsEvent struct { + Timestamp uint64 + SrcIp uint32 + DstIp uint32 + SrcIp6 [16]uint8 + DstIp6 [16]uint8 + SrcPort uint16 + DstPort uint16 + Id uint16 + Qtype uint16 + Ancount uint16 + DnsOff uint16 + DataLen uint16 + Af uint8 + Proto uint8 + PktType uint8 + Qr uint8 + Rcode uint8 + _ [5]byte +} + +// loadDns returns the embedded CollectionSpec for dns. +func loadDns() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_DnsBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load dns: %w", err) + } + + return spec, err +} + +// loadDnsObjects loads dns and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *dnsObjects +// *dnsPrograms +// *dnsMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadDnsObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadDns() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// dnsSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type dnsSpecs struct { + dnsProgramSpecs + dnsMapSpecs + dnsVariableSpecs +} + +// dnsProgramSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type dnsProgramSpecs struct { + RetinaDnsFilter *ebpf.ProgramSpec `ebpf:"retina_dns_filter"` +} + +// dnsMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type dnsMapSpecs struct { + RetinaDnsEvents *ebpf.MapSpec `ebpf:"retina_dns_events"` + TmpDnsEvents *ebpf.MapSpec `ebpf:"tmp_dns_events"` +} + +// dnsVariableSpecs contains global variables before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type dnsVariableSpecs struct { + UnusedDnsEvent *ebpf.VariableSpec `ebpf:"unused_dns_event"` +} + +// dnsObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadDnsObjects or ebpf.CollectionSpec.LoadAndAssign. +type dnsObjects struct { + dnsPrograms + dnsMaps + dnsVariables +} + +func (o *dnsObjects) Close() error { + return _DnsClose( + &o.dnsPrograms, + &o.dnsMaps, + ) +} + +// dnsMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadDnsObjects or ebpf.CollectionSpec.LoadAndAssign. +type dnsMaps struct { + RetinaDnsEvents *ebpf.Map `ebpf:"retina_dns_events"` + TmpDnsEvents *ebpf.Map `ebpf:"tmp_dns_events"` +} + +func (m *dnsMaps) Close() error { + return _DnsClose( + m.RetinaDnsEvents, + m.TmpDnsEvents, + ) +} + +// dnsVariables contains all global variables after they have been loaded into the kernel. +// +// It can be passed to loadDnsObjects or ebpf.CollectionSpec.LoadAndAssign. +type dnsVariables struct { + UnusedDnsEvent *ebpf.Variable `ebpf:"unused_dns_event"` +} + +// dnsPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadDnsObjects or ebpf.CollectionSpec.LoadAndAssign. +type dnsPrograms struct { + RetinaDnsFilter *ebpf.Program `ebpf:"retina_dns_filter"` +} + +func (p *dnsPrograms) Close() error { + return _DnsClose( + p.RetinaDnsFilter, + ) +} + +func _DnsClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed dns_x86_bpfel.o +var _DnsBytes []byte diff --git a/pkg/plugin/dns/dns_x86_bpfel.o b/pkg/plugin/dns/dns_x86_bpfel.o new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pkg/plugin/dns/types_linux.go b/pkg/plugin/dns/types_linux.go index 9517cc9b2f..a91475708c 100644 --- a/pkg/plugin/dns/types_linux.go +++ b/pkg/plugin/dns/types_linux.go @@ -3,23 +3,26 @@ package dns import ( + "sync" + v1 "github.com/cilium/cilium/pkg/hubble/api/v1" + "github.com/cilium/ebpf/perf" kcfg "github.com/microsoft/retina/pkg/config" "github.com/microsoft/retina/pkg/enricher" "github.com/microsoft/retina/pkg/log" - "github.com/microsoft/retina/pkg/metrics" - "github.com/microsoft/retina/pkg/plugin/common" ) const name = "dns" -var m metrics.CounterVec - type dns struct { cfg *kcfg.Config l *log.ZapLogger - tracer common.ITracer - pid uint32 enricher enricher.EnricherInterface externalChannel chan *v1.Event + objs *dnsObjects + reader *perf.Reader + sock int + isRunning bool + recordsChannel chan perf.Record + wg sync.WaitGroup } diff --git a/pkg/plugin/dropreason/_cprog/drop_reason.c b/pkg/plugin/dropreason/_cprog/drop_reason.c index c716157857..ee2cbc770b 100644 --- a/pkg/plugin/dropreason/_cprog/drop_reason.c +++ b/pkg/plugin/dropreason/_cprog/drop_reason.c @@ -12,6 +12,19 @@ #include "dynamic.h" #include "retina_filter.c" +// CO-RE flavor struct for Linux 6.10+ where inet_csk_accept changed signature from +// (struct sock *sk, int flags, int *err, bool kern) to +// (struct sock *sk, struct proto_accept_arg *arg). +// The ___new suffix is stripped during BTF type lookup, so bpf_core_type_exists() +// checks for 'proto_accept_arg' in the running kernel's BTF. +// https://github.com/torvalds/linux/commit/92ef0fd55ac80dfc2e4654edfe5d1ddfa6e070fe +struct proto_accept_arg___new { + int flags; + int err; + int is_empty; + bool kern; +} __attribute__((preserve_access_index)); + char __license[] SEC("license") = "Dual MIT/GPL"; #define ETH_P_IP 0x0800 @@ -358,7 +371,7 @@ int BPF_PROG(tcp_v4_connect_fexit, struct sock *sk, struct sockaddr *uaddr, int } SEC("kprobe/inet_csk_accept") -int BPF_KPROBE(inet_csk_accept, struct sock *sk, int flags, int *err, bool kern) +int BPF_KPROBE(inet_csk_accept) { /* This function will save the reference value to error. @@ -366,7 +379,20 @@ int BPF_KPROBE(inet_csk_accept, struct sock *sk, int flags, int *err, bool kern) */ __u64 pid_tgid = bpf_get_current_pid_tgid(); __u32 pid = pid_tgid >> 32; - __u64 err_ptr = (__u64)err; + + __u64 err_ptr = 0; + + // Linux 6.10+: inet_csk_accept(struct sock *sk, struct proto_accept_arg *arg) + // err is an inline int field inside proto_accept_arg. + if (bpf_core_type_exists(struct proto_accept_arg___new)) { + struct proto_accept_arg___new *arg = (struct proto_accept_arg___new *)PT_REGS_PARM2(ctx); + err_ptr = (__u64)&arg->err; + } else { + // Pre-6.10: inet_csk_accept(struct sock *sk, int flags, int *err, bool kern) + int *err = (int *)PT_REGS_PARM3(ctx); + err_ptr = (__u64)err; + } + bpf_map_update_elem(&retina_dropreason_accept_pids, &pid, &err_ptr, BPF_ANY); return 0; } @@ -415,17 +441,23 @@ int BPF_KRETPROBE(inet_csk_accept_ret, struct sock *sk) } SEC("fexit/inet_csk_accept") -int BPF_PROG(inet_csk_accept_fexit, struct sock *sk, int flags, int *err, struct sock *retsk) +int BPF_PROG(inet_csk_accept_fexit) { - if (retsk != NULL) { - return 0; + // Each branch must be fully self-contained so the compiler generates a real + // branch instruction. If merged into a select, the verifier rejects the + // variable-offset ctx access ("dereference of modified ctx ptr"). + // + // fexit ctx layout: [param1, param2, ..., paramN, return_value] + // Linux 6.10+: 2 params (sk, arg) -> retsk at ctx[2] + // Pre-6.10: 4 params (sk, flags, err, kern) -> retsk at ctx[4] + if (bpf_core_type_exists(struct proto_accept_arg___new)) { + if ((struct sock *)ctx[2] == NULL) + update_metrics_map_basic(TCP_ACCEPT_BASIC, 0, 0); + } else { + if ((struct sock *)ctx[4] == NULL) + update_metrics_map_basic(TCP_ACCEPT_BASIC, 0, 0); } - // TODO - // Pass 0 packet length - get_packet_from_sock above doesn't obtain this value, either. - // Pass 0 return value; verifier failure, same as buggy kprobe above. - update_metrics_map_basic(TCP_ACCEPT_BASIC, 0, 0); - return 0; } diff --git a/pkg/plugin/dropreason/dropreason_linux.go b/pkg/plugin/dropreason/dropreason_linux.go index cfbee0886b..aadf48ae2f 100644 --- a/pkg/plugin/dropreason/dropreason_linux.go +++ b/pkg/plugin/dropreason/dropreason_linux.go @@ -13,13 +13,9 @@ import ( "runtime" "time" - "github.com/blang/semver/v4" "github.com/cilium/cilium/api/v1/flow" hubblev1 "github.com/cilium/cilium/pkg/hubble/api/v1" - "github.com/cilium/cilium/pkg/version" - "github.com/cilium/cilium/pkg/versioncheck" "github.com/cilium/ebpf" - "github.com/cilium/ebpf/link" "github.com/cilium/ebpf/perf" "github.com/microsoft/retina/internal/ktime" kcfg "github.com/microsoft/retina/pkg/config" @@ -121,8 +117,15 @@ func (dr *dropReason) Compile(ctx context.Context) error { if arch == "arm64" { targetArch = "-D__TARGET_ARCH_arm64" } + + runtimeIncludeDir := "-I" + loader.VmlinuxHeaderDir() + // Keep target as bpf, otherwise clang compilation yields bpf object that elf reader cannot load. - err = loader.CompileEbpf(ctx, "-target", "bpf", "-Wall", targetArch, "-g", "-O2", "-c", bpfSourceFile, "-o", bpfOutputFile, includeDir, libbpfDir, filterDir) + err = loader.CompileEbpf( + ctx, + "-target", "bpf", "-Wall", targetArch, "-g", "-O2", "-c", bpfSourceFile, + "-o", bpfOutputFile, runtimeIncludeDir, includeDir, libbpfDir, filterDir, + ) if err != nil { return errors.Wrap(err, "unable to compile eBPF code") } @@ -131,7 +134,6 @@ func (dr *dropReason) Compile(ctx context.Context) error { } func (dr *dropReason) Init() error { - var err error // Get the absolute path to this file during runtime. dir, err := absPath() if err != nil { @@ -139,25 +141,21 @@ func (dr *dropReason) Init() error { } bpfOutputFile := fmt.Sprintf("%s/%s", dir, bpfObjectFileName) - - var objs interface{} - maps := &kprobeMaps{} - isMariner := plugincommon.IsAzureLinux() - - if !isMariner { - objs = &kprobeObjects{} //nolint:typecheck // this is a generated struct - maps = &objs.(*kprobeObjects).kprobeMaps - } else { - objs = &kprobeObjectsMariner{} //nolint:typecheck // needs to match a generated struct until we fix Mariner - maps = &objs.(*kprobeObjectsMariner).kprobeMaps - dr.l.Info("Detected Mariner distro") + spec, err := ebpf.LoadCollectionSpec(bpfOutputFile) + if err != nil { + return err //nolint:wrapcheck // no additional context needed } - spec, err := ebpf.LoadCollectionSpec(bpfOutputFile) + objs, maps, supportsFexit, err := dr.getEbpfPayload() if err != nil { return err } + // Override filter map max entries to match the configured size from init container. + if mapSpec, ok := spec.Maps[plugincommon.FilterMapName]; ok && dr.cfg.FilterMapMaxEntries > 0 { + mapSpec.MaxEntries = dr.cfg.FilterMapMaxEntries + } + // TODO remove the opts if err := spec.LoadAndAssign(objs, &ebpf.CollectionOptions{ Programs: ebpf.ProgramOptions{ @@ -181,28 +179,10 @@ func (dr *dropReason) Init() error { progsKprobe, progsKprobeRet := buildKprobePrograms(objs) progsFexit := buildFexitPrograms(objs) - if dr.cfg.EnablePodLevel { - err = dr.attachKprobes(progsKprobe, progsKprobeRet) + if supportsFexit { + err = dr.attachFexitPrograms(progsFexit) } else { - var kv semver.Version - kv, err = version.GetKernelVersion() - if err != nil { - dr.l.Warn("Failed to get kernel version", zap.Error(err)) - - kv, err = plugincommon.GetKernelVersionMajMin() - if err != nil { - return fmt.Errorf("Failed to get kernel version: %w", err) //nolint:goerr113 //wrapping error from external module - } - } - dr.l.Info("Detected kernel >= ", zap.String("version", kv.String())) - - minVersionAmd64, _ := versioncheck.Version("5.5") - minVersionArm64, _ := versioncheck.Version("6.0") - if (runtime.GOARCH == "amd64" && kv.GTE(minVersionAmd64)) || runtime.GOARCH == "arm64" && kv.GTE(minVersionArm64) { - err = dr.attachFexitPrograms(progsFexit) - } else { - err = dr.attachKprobes(progsKprobe, progsKprobeRet) - } + err = dr.attachKprobes(progsKprobe, progsKprobeRet) } dr.metricsMapData = maps.RetinaDropreasonMetrics @@ -350,16 +330,16 @@ func (dr *dropReason) processRecord(ctx context.Context, id int) { // IsReply is not applicable for DROPPED verdicts. fl.IsReply = nil - meta := &utils.RetinaMetadata{} + ext := utils.NewExtensions() - // Add drop reason to the flow's metadata. - utils.AddDropReason(fl, meta, bpfEvent.DropType) + // Add drop reason to the flow's extensions. + utils.AddDropReason(fl, ext, bpfEvent.DropType) - // Add packet size to the flow's metadata. - utils.AddPacketSize(meta, bpfEvent.SkbLen) + // Add packet size to the flow's extensions. + utils.AddPacketSize(ext, bpfEvent.SkbLen) - // Add metadata to the flow. - utils.AddRetinaMetadata(fl, meta) + // Set extensions on the flow. + utils.SetExtensions(fl, ext) // This is only for development purposes. // Removing this makes logs way too chatter-y. @@ -427,44 +407,6 @@ func (dr *dropReason) processMapValue(dataKey dropMetricKey, dataValue dropMetri dr.dropMetricAdd(dataKey.getType(), dataKey.getDirection(), pktCount, pktBytes) } -func (dr *dropReason) attachKprobes(kprobes, kprobesRet map[string]*ebpf.Program) error { - for name := range kprobes { - progLink, err := link.Kprobe(name, kprobes[name], nil) - if err != nil { - dr.l.Error("Failed to attach kprobe", zap.String("program", name), zap.Error(err)) - return fmt.Errorf("Failed to attach program: %w", err) //nolint:goerr113 //wrapping error from external module - } - dr.hooks = append(dr.hooks, progLink) - dr.l.Info("Attached kprobe", zap.String("program", name)) - } - - for name := range kprobesRet { - progLink, err := link.Kretprobe(name, kprobesRet[name], nil) - if err != nil { - dr.l.Error("Failed to attach kretprobe", zap.String("program", name), zap.Error(err)) - return fmt.Errorf("Failed to attach program: %w", err) //nolint:goerr113 //wrapping error from external module - } - dr.hooks = append(dr.hooks, progLink) - dr.l.Info("Attached kretprobe", zap.String("program", name)) - } - - return nil -} - -func (dr *dropReason) attachFexitPrograms(objs map[string]*ebpf.Program) error { - for name, prog := range objs { - progLink, err := link.AttachTracing(link.TracingOptions{Program: prog, AttachType: ebpf.AttachTraceFExit}) - if err != nil { - dr.l.Error("Failed to attach", zap.String("program", name), zap.Error(err)) - return fmt.Errorf("Failed to attach program: %w", err) //nolint:goerr113 //wrapping error from external module - } - dr.hooks = append(dr.hooks, progLink) - dr.l.Info("Attached program", zap.String("program", name)) - } - - return nil -} - func (dr *dropReason) Stop() error { if !dr.isRunning { return nil @@ -516,52 +458,3 @@ func absPath() (string, error) { dir := path.Dir(filename) return dir, nil } - -func buildKprobePrograms(objs any) (progsKprobe, progsKprobeRet map[string]*ebpf.Program) { - progsKprobe = make(map[string]*ebpf.Program) - progsKprobeRet = make(map[string]*ebpf.Program) - - switch o := objs.(type) { - case *kprobeObjects: - progsKprobe[inetCskAcceptFn] = o.InetCskAccept - progsKprobe[nfHookSlowFn] = o.NfHookSlow - progsKprobe[nfNatInetFn] = o.NfNatInetFn - progsKprobe[nfConntrackConfirmFn] = o.NfConntrackConfirm - - progsKprobeRet[nfHookSlowFn] = o.NfHookSlowRet - progsKprobeRet[inetCskAcceptFn] = o.InetCskAcceptRet - progsKprobeRet[tcpConnectFn] = o.TcpV4ConnectRet - progsKprobeRet[nfNatInetFn] = o.NfNatInetFnRet - progsKprobeRet[nfConntrackConfirmFn] = o.NfConntrackConfirmRet - - case *kprobeObjectsMariner: - progsKprobe[inetCskAcceptFn] = o.InetCskAccept - progsKprobe[nfHookSlowFn] = o.NfHookSlow - - progsKprobeRet[nfHookSlowFn] = o.NfHookSlowRet - progsKprobeRet[inetCskAcceptFn] = o.InetCskAcceptRet - progsKprobeRet[tcpConnectFn] = o.TcpV4ConnectRet - - } - return progsKprobe, progsKprobeRet -} - -func buildFexitPrograms(objs any) map[string]*ebpf.Program { - progsFexit := make(map[string]*ebpf.Program) - - switch o := objs.(type) { - case *kprobeObjects: - progsFexit[inetCskAcceptFnFexit] = o.InetCskAcceptFexit - progsFexit[nfHookSlowFnFexit] = o.NfHookSlowFexit - progsFexit[tcpV4ConnectFexit] = o.TcpV4ConnectFexit - progsFexit[nfNatInetFnFexit] = o.NfNatInetFnFexit - progsFexit[nfConntrackConfirmFnFexit] = o.NfConntrackConfirmFexit - - case *kprobeObjectsMariner: - progsFexit[inetCskAcceptFnFexit] = o.InetCskAcceptFexit - progsFexit[nfHookSlowFnFexit] = o.NfHookSlowFexit - progsFexit[tcpV4ConnectFexit] = o.TcpV4ConnectFexit - - } - return progsFexit -} diff --git a/pkg/plugin/dropreason/dropreason_linux_test.go b/pkg/plugin/dropreason/dropreason_linux_test.go index e494a03f02..0b39bfdfb7 100644 --- a/pkg/plugin/dropreason/dropreason_linux_test.go +++ b/pkg/plugin/dropreason/dropreason_linux_test.go @@ -7,13 +7,16 @@ import ( "context" "errors" "fmt" + "log/slog" "os" "path" + "reflect" "runtime" "testing" "time" "unsafe" + "github.com/blang/semver/v4" "github.com/cilium/ebpf/perf" kcfg "github.com/microsoft/retina/pkg/config" "github.com/microsoft/retina/pkg/enricher" @@ -119,7 +122,7 @@ func TestCompile(t *testing.T) { func TestProcessMapValue(t *testing.T) { log.SetupZapLogger(log.GetDefaultLogOpts()) - metrics.InitializeMetrics() + metrics.InitializeMetrics(slog.Default()) dr := &dropReason{ cfg: cfgPodLevelEnabled, l: log.Logger().Named(name), @@ -177,11 +180,10 @@ func TestDropReasonRun_Error(t *testing.T) { // Create a context with a short timeout for testing purposes ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + errCh := make(chan error, 1) // Start the drop reason routine in a goroutine go func() { - if err := dr.run(ctx); err != nil { - t.Fatalf("unexpected error: %v", err) - } + errCh <- dr.run(ctx) }() // Wait for a short period of time for the routine to start @@ -189,6 +191,9 @@ func TestDropReasonRun_Error(t *testing.T) { cancel() ticker.Stop() + if err := <-errCh; err != nil { + t.Fatalf("unexpected error: %v", err) + } } func TestDropReasonRun(t *testing.T) { @@ -239,11 +244,10 @@ func TestDropReasonRun(t *testing.T) { // create a ticker with a short interval for testing purposes ticker := time.NewTicker(2 * time.Second) + errCh := make(chan error, 1) // Start the drop reason routine in a goroutine go func() { - if err := dr.run(ctx); err != nil { - t.Fatalf("unexpected error: %v", err) - } + errCh <- dr.run(ctx) }() // Wait for a short period of time for the routine to start @@ -251,6 +255,9 @@ func TestDropReasonRun(t *testing.T) { cancel() ticker.Stop() + if err := <-errCh; err != nil { + t.Fatalf("unexpected error: %v", err) + } } func TestDropReasonReadDataPodLevelEnabled(t *testing.T) { @@ -296,8 +303,8 @@ func TestDropReasonReadDataPodLevelEnabled(t *testing.T) { dr.readEventArrayData() }() + dr.wg.Add(1) go func() { - dr.wg.Add(1) dr.processRecord(ctx, 0) }() @@ -467,6 +474,122 @@ func TestDropReasonGenerate(t *testing.T) { } } +func mustVersion(v string) semver.Version { + ver, err := semver.Parse(v) + if err != nil { + panic(err) + } + return ver +} + +func TestResolveEbpfPayload(t *testing.T) { + tests := []struct { + name string + arch string + kv semver.Version + isMariner bool + isPodLevel bool + ftraceEnabled bool + wantType string + wantSupportsFexit bool + }{ + { + name: "old kernel - fallback to allKprobeObjects", + arch: "amd64", + kv: mustVersion("5.4.0"), + isMariner: false, + isPodLevel: false, + ftraceEnabled: true, + wantType: "*dropreason.allKprobeObjects", + wantSupportsFexit: false, + }, + { + name: "new kernel - fexitObjects for Ubuntu", + arch: "amd64", + kv: mustVersion("5.10.0"), + isMariner: false, + isPodLevel: false, + ftraceEnabled: true, + wantType: "*dropreason.allFexitObjects", + wantSupportsFexit: true, + }, + { + name: "new kernel - marinerObjects for Mariner", + arch: "amd64", + kv: mustVersion("5.10.0"), + isMariner: true, + isPodLevel: false, + ftraceEnabled: true, + wantType: "*dropreason.marinerObjects", + wantSupportsFexit: true, + }, + { + name: "arm64 old kernel - fallback to allKprobeObjects", + arch: "arm64", + kv: mustVersion("5.8.0"), + isMariner: true, + isPodLevel: false, + ftraceEnabled: true, + wantType: "*dropreason.allKprobeObjects", + wantSupportsFexit: false, + }, + { + name: "arm64 new kernel - marinerObjects", + arch: "arm64", + kv: mustVersion("6.1.0"), + isMariner: true, + isPodLevel: false, + ftraceEnabled: true, + wantType: "*dropreason.marinerObjects", + wantSupportsFexit: true, + }, + { + name: "pod level - use allKprobeObjects", + arch: "amd64", + kv: mustVersion("5.15.0"), + isMariner: false, + isPodLevel: true, + ftraceEnabled: true, + wantType: "*dropreason.allKprobeObjects", + wantSupportsFexit: false, + }, + { + name: "mariner with ftrace disabled - fallback to kprobes", + arch: "amd64", + kv: mustVersion("5.15.0"), + isMariner: true, + isPodLevel: false, + ftraceEnabled: false, + wantType: "*dropreason.allKprobeObjects", + wantSupportsFexit: false, + }, + { + name: "ubuntu with ftrace disabled - fallback to kprobes", + arch: "amd64", + kv: mustVersion("6.6.0"), + isMariner: false, + isPodLevel: false, + ftraceEnabled: false, + wantType: "*dropreason.allKprobeObjects", + wantSupportsFexit: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + objs, _, isFexit := resolvePayload(tt.arch, tt.kv, tt.isMariner, tt.isPodLevel, tt.ftraceEnabled) + + if isFexit != tt.wantSupportsFexit { + t.Errorf("isFexit = %v, want %v", isFexit, tt.wantSupportsFexit) + } + + if gotType := reflect.TypeOf(objs).String(); gotType != tt.wantType { + t.Errorf("object type = %v, want %v", gotType, tt.wantType) + } + }) + } +} + // Helpers. func takeBackup() { // Get the directory of the current test file. diff --git a/pkg/plugin/dropreason/ebpfsetup_linux.go b/pkg/plugin/dropreason/ebpfsetup_linux.go new file mode 100644 index 0000000000..c67efabe0a --- /dev/null +++ b/pkg/plugin/dropreason/ebpfsetup_linux.go @@ -0,0 +1,205 @@ +package dropreason + +import ( + "fmt" + "runtime" + + "github.com/blang/semver/v4" + "github.com/cilium/cilium/pkg/version" + "github.com/cilium/cilium/pkg/versioncheck" + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/link" + plugincommon "github.com/microsoft/retina/pkg/plugin/common" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +const ( + MinAmdVersionNum = "5.5" + MinArmVersionNum = "6.0" +) + +/* +getEbpfPayload() returns the ebpf program and map objects to load, based on the kernel version, architecture, and distro. +We use fexit programs for better performance, and fall back to kprobes. + +eBPF Program Support Matrix +=========================== + +Program Type Selection: +- If: + - Ftrace is enabled AND + - Arch == amd64 and kernel >= 5.5 + - Arch == arm64 and kernel >= 6.0 + → Use `fexit` + - Else: + → Use `kprobe` + +Scope Selection: +- If: + - Distro == Mariner + → Scope = core (only core kernel funcs) + - Else: + → Scope = all (core + module funcs) + ++-----------+------------------------+--------------+--------+ +| Distro | Arch + Kernel | Prog | Scope | ++-----------+------------------------+--------------+--------+ +| Mariner | amd64, kernel >= 5.5 | fexit* | core | +| Mariner | arm64, kernel >= 6.0 | fexit* | core | +| Non-Marin | amd64, kernel >= 5.5 | fexit* | all | +| Non-Marin | arm64, kernel >= 6.0 | fexit* | all | +| * | (otherwise) | kprobe | per OS | ++-----------+------------------------+--------------+--------+ + +* fexit requires ftrace to be enabled in the kernel + +core kernel funcs: +- tcp_v4_connect +- inet_csk_accept +- nf_hook_slow + +module funcs: +- nf_conntrack_confirm +- nf_nat_inet_fn +*/ + +func (dr *dropReason) getEbpfPayload() (objs interface{}, maps *kprobeMaps, supportsFexit bool, err error) { + isMariner := plugincommon.IsAzureLinux() + dr.l.Info("Distro check:", zap.Bool("isMariner", isMariner)) + + kv, err := version.GetKernelVersion() + if err != nil { + kv, err = plugincommon.GetKernelVersionMajMin() + if err != nil { + return nil, nil, false, fmt.Errorf("failed to get kernel version: %w", err) //nolint:goerr113 //wrapping error from external module + } + } + dr.l.Info("Detected kernel", zap.String("version", kv.String())) + + // Check if ftrace is enabled (required for fexit programs) + ftraceEnabled := plugincommon.IsFtraceEnabled() + dr.l.Info("Ftrace status", zap.Bool("enabled", ftraceEnabled)) + + objs, maps, supportsFexit = resolvePayload(runtime.GOARCH, kv, isMariner, dr.cfg.EnablePodLevel, ftraceEnabled) + return objs, maps, supportsFexit, nil +} + +func resolvePayload(arch string, kv semver.Version, isMariner, isPodLevel, ftraceEnabled bool) (interface{}, *kprobeMaps, bool) { + minVersionAmd64, _ := versioncheck.Version(MinAmdVersionNum) + minVersionArm64, _ := versioncheck.Version(MinArmVersionNum) + + supportsFexit := ftraceEnabled && + ((arch == "amd64" && kv.GTE(minVersionAmd64)) || + (arch == "arm64" && kv.GTE(minVersionArm64))) + + var objs interface{} + var maps *kprobeMaps + + switch { + case isPodLevel: // TODO: fexit support is being rolled out in two stages, remove this when we have it for advanced metrics. + objs = &allKprobeObjects{} //nolint:typecheck // this is a generated struct + maps = &objs.(*allKprobeObjects).kprobeMaps + case isMariner && supportsFexit: // Mariner supports a subset of the fexit programs, need to check for it first. + objs = &marinerObjects{} //nolint:typecheck // needs to match a generated struct until we fix Mariner + maps = &objs.(*marinerObjects).kprobeMaps + case supportsFexit: + objs = &allFexitObjects{} //nolint:typecheck // this is a generated struct + maps = &objs.(*allFexitObjects).kprobeMaps + default: + objs = &allKprobeObjects{} //nolint:typecheck // this is a generated struct + maps = &objs.(*allKprobeObjects).kprobeMaps + } + + return objs, maps, (supportsFexit && !isPodLevel) +} + +func (dr *dropReason) attachKprobes(kprobes, kprobesRet map[string]*ebpf.Program) error { + for name := range kprobes { + progLink, err := link.Kprobe(name, kprobes[name], nil) + if err != nil { + dr.l.Error("Failed to attach kprobe", zap.String("program", name), zap.Error(err)) + } else { + dr.hooks = append(dr.hooks, progLink) + dr.l.Info("Attached kprobe", zap.String("program", name)) + } + } + + // The kretprobes set metric values. If none were attached, report an error. + retprobeCount := 0 + for name := range kprobesRet { + progLink, err := link.Kretprobe(name, kprobesRet[name], nil) + if err != nil { + dr.l.Error("Failed to attach kretprobe", zap.String("program", name), zap.Error(err)) + } else { + dr.hooks = append(dr.hooks, progLink) + retprobeCount++ + dr.l.Info("Attached kretprobe", zap.String("program", name)) + } + } + if retprobeCount == 0 { + dr.l.Error("No kretprobes attached, cannot collect drop metrics") + return errors.New("No kretprobes attached, cannot collect drop metrics") //nolint:goerr113 // no sentinel type used + } + + return nil +} + +func (dr *dropReason) attachFexitPrograms(objs map[string]*ebpf.Program) error { + progCount := 0 + for name, prog := range objs { + progLink, err := link.AttachTracing(link.TracingOptions{Program: prog, AttachType: ebpf.AttachTraceFExit}) + if err != nil { + dr.l.Error("Failed to attach", zap.String("program", name), zap.Error(err)) + } else { + dr.hooks = append(dr.hooks, progLink) + progCount++ + dr.l.Info("Attached program", zap.String("program", name)) + } + } + + if progCount == 0 { + dr.l.Error("No programs attached, cannot collect drop metrics") + return errors.New("No programs attached, cannot collect drop metrics") //nolint:goerr113 // no sentinel type used + } + + return nil +} + +func buildKprobePrograms(objs any) (progsKprobe, progsKprobeRet map[string]*ebpf.Program) { + progsKprobe = make(map[string]*ebpf.Program) + progsKprobeRet = make(map[string]*ebpf.Program) + + if o, ok := objs.(*allKprobeObjects); ok { + progsKprobe[inetCskAcceptFn] = o.InetCskAccept + progsKprobe[nfHookSlowFn] = o.NfHookSlow + progsKprobe[nfNatInetFn] = o.NfNatInetFn + progsKprobe[nfConntrackConfirmFn] = o.NfConntrackConfirm + + progsKprobeRet[nfHookSlowFn] = o.NfHookSlowRet + progsKprobeRet[inetCskAcceptFn] = o.InetCskAcceptRet + progsKprobeRet[tcpConnectFn] = o.TcpV4ConnectRet + progsKprobeRet[nfNatInetFn] = o.NfNatInetFnRet + progsKprobeRet[nfConntrackConfirmFn] = o.NfConntrackConfirmRet + } + return progsKprobe, progsKprobeRet +} + +func buildFexitPrograms(objs any) map[string]*ebpf.Program { + progsFexit := make(map[string]*ebpf.Program) + + switch o := objs.(type) { + case *allFexitObjects: + progsFexit[inetCskAcceptFnFexit] = o.InetCskAcceptFexit + progsFexit[nfHookSlowFnFexit] = o.NfHookSlowFexit + progsFexit[tcpV4ConnectFexit] = o.TcpV4ConnectFexit + progsFexit[nfNatInetFnFexit] = o.NfNatInetFnFexit + progsFexit[nfConntrackConfirmFnFexit] = o.NfConntrackConfirmFexit + + case *marinerObjects: + progsFexit[inetCskAcceptFnFexit] = o.InetCskAcceptFexit + progsFexit[nfHookSlowFnFexit] = o.NfHookSlowFexit + progsFexit[tcpV4ConnectFexit] = o.TcpV4ConnectFexit + } + return progsFexit +} diff --git a/pkg/plugin/dropreason/mocks/mock_types_linux.go b/pkg/plugin/dropreason/mocks/mock_types_linux.go index 21f3fda4e6..7f49d93498 100644 --- a/pkg/plugin/dropreason/mocks/mock_types_linux.go +++ b/pkg/plugin/dropreason/mocks/mock_types_linux.go @@ -21,7 +21,6 @@ import ( type MockIMapIterator struct { ctrl *gomock.Controller recorder *MockIMapIteratorMockRecorder - isgomock struct{} } // MockIMapIteratorMockRecorder is the mock recorder for MockIMapIterator. @@ -73,7 +72,6 @@ func (mr *MockIMapIteratorMockRecorder) Next(keyOut, valueOut any) *gomock.Call type MockIMap struct { ctrl *gomock.Controller recorder *MockIMapMockRecorder - isgomock struct{} } // MockIMapMockRecorder is the mock recorder for MockIMap. @@ -125,7 +123,6 @@ func (mr *MockIMapMockRecorder) Iterate() *gomock.Call { type MockIPerfReader struct { ctrl *gomock.Controller recorder *MockIPerfReaderMockRecorder - isgomock struct{} } // MockIPerfReaderMockRecorder is the mock recorder for MockIPerfReader. diff --git a/pkg/plugin/dropreason/types_linux.go b/pkg/plugin/dropreason/types_linux.go index a78fea8f85..5e2129e886 100644 --- a/pkg/plugin/dropreason/types_linux.go +++ b/pkg/plugin/dropreason/types_linux.go @@ -42,22 +42,47 @@ type dropReason struct { externalChannel chan *hubblev1.Event } -type kprobeObjectsMariner struct { - kprobeProgramsMariner +type allFexitObjects struct { + allFexitPrograms kprobeMaps } -type kprobeProgramsMariner struct { - InetCskAccept *ebpf.Program `ebpf:"inet_csk_accept"` - InetCskAcceptRet *ebpf.Program `ebpf:"inet_csk_accept_ret"` +type allFexitPrograms struct { + InetCskAcceptFexit *ebpf.Program `ebpf:"inet_csk_accept_fexit"` + NfConntrackConfirmFexit *ebpf.Program `ebpf:"nf_conntrack_confirm_fexit"` + NfHookSlowFexit *ebpf.Program `ebpf:"nf_hook_slow_fexit"` + NfNatInetFnFexit *ebpf.Program `ebpf:"nf_nat_inet_fn_fexit"` + TcpV4ConnectFexit *ebpf.Program `ebpf:"tcp_v4_connect_fexit"` // nolint:revive // needs to match generated code +} + +type marinerObjects struct { + marinerPrograms + kprobeMaps +} + +type marinerPrograms struct { InetCskAcceptFexit *ebpf.Program `ebpf:"inet_csk_accept_fexit"` - NfHookSlow *ebpf.Program `ebpf:"nf_hook_slow"` - NfHookSlowRet *ebpf.Program `ebpf:"nf_hook_slow_ret"` NfHookSlowFexit *ebpf.Program `ebpf:"nf_hook_slow_fexit"` - TcpV4ConnectRet *ebpf.Program `ebpf:"tcp_v4_connect_ret"` // nolint:revive // needs to match generated code TcpV4ConnectFexit *ebpf.Program `ebpf:"tcp_v4_connect_fexit"` // nolint:revive // needs to match generated code } +type allKprobeObjects struct { + allKprobePrograms + kprobeMaps +} + +type allKprobePrograms struct { + InetCskAccept *ebpf.Program `ebpf:"inet_csk_accept"` + InetCskAcceptRet *ebpf.Program `ebpf:"inet_csk_accept_ret"` + NfConntrackConfirm *ebpf.Program `ebpf:"nf_conntrack_confirm"` + NfConntrackConfirmRet *ebpf.Program `ebpf:"nf_conntrack_confirm_ret"` + NfHookSlow *ebpf.Program `ebpf:"nf_hook_slow"` + NfHookSlowRet *ebpf.Program `ebpf:"nf_hook_slow_ret"` + NfNatInetFn *ebpf.Program `ebpf:"nf_nat_inet_fn"` + NfNatInetFnRet *ebpf.Program `ebpf:"nf_nat_inet_fn_ret"` + TcpV4ConnectRet *ebpf.Program `ebpf:"tcp_v4_connect_ret"` // nolint:revive // needs to match generated code +} + type ( returnValue uint32 ) @@ -65,7 +90,7 @@ type ( // Interface to https://pkg.go.dev/github.com/cilium/ebpf#Map. // Added for unit tests. // -//go:generate go run go.uber.org/mock/mockgen@v0.4.0 -source=types_linux.go -destination=mocks/mock_types.go -package=dropreason . IMap IMapIterator IPerfReader +//go:generate go run go.uber.org/mock/mockgen@v0.4.0 -source=types_linux.go -destination=mocks/mock_types_linux.go -package=dropreason . IMap IMapIterator IPerfReader type IMapIterator interface { Next(keyOut interface{}, valueOut interface{}) bool Err() error diff --git a/pkg/plugin/ebpftest/helpers.go b/pkg/plugin/ebpftest/helpers.go new file mode 100644 index 0000000000..ac316b0d8a --- /dev/null +++ b/pkg/plugin/ebpftest/helpers.go @@ -0,0 +1,228 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//go:build ebpf && linux + +package ebpftest + +import ( + "bytes" + "encoding/binary" + "errors" + "net" + "os" + "testing" + "time" + "unsafe" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/perf" + "github.com/cilium/ebpf/ringbuf" + "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" +) + +// RequirePrivileged skips the test if the current process lacks BPF privileges. +// It checks for CAP_BPF or CAP_SYS_ADMIN in the effective capability set. +func RequirePrivileged(t *testing.T) { + t.Helper() + + if os.Geteuid() == 0 { + return // root has all capabilities + } + + var hdr unix.CapUserHeader + var data [2]unix.CapUserData + hdr.Version = unix.LINUX_CAPABILITY_VERSION_3 + + if err := unix.Capget(&hdr, &data[0]); err != nil { + t.Skipf("skipping eBPF test: cannot query capabilities: %v", err) + return + } + + if !hasEffectiveCap(data, unix.CAP_BPF) && !hasEffectiveCap(data, unix.CAP_SYS_ADMIN) { + t.Skip("skipping eBPF test: need CAP_BPF or CAP_SYS_ADMIN") + } +} + +// hasEffectiveCap checks whether the given capability is set in the effective set. +func hasEffectiveCap(data [2]unix.CapUserData, cap uintptr) bool { + i := cap / 32 + bit := uint32(1) << (cap % 32) + return data[i].Effective&bit != 0 +} + +// RemoveMapPinning sets Pinning to PinNone on all maps in the collection spec. +// This is required because embedded objects may have LIBBPF_PIN_BY_NAME which +// would fail in test environments without /sys/fs/bpf access. +func RemoveMapPinning(spec *ebpf.CollectionSpec) { + for _, m := range spec.Maps { + m.Pinning = ebpf.PinNone + } +} + +// ReadPerfEvent reads one perf record from the reader within the given timeout, +// decodes it into T using binary.Read (little-endian), and returns it. +// Returns (zero, false) if the deadline expires without an event. +func ReadPerfEvent[T any](t *testing.T, reader *perf.Reader, timeout time.Duration) (T, bool) { + t.Helper() + + reader.SetDeadline(time.Now().Add(timeout)) + + record, err := reader.Read() + var zero T + if errors.Is(err, os.ErrDeadlineExceeded) { + return zero, false + } + require.NoError(t, err) + require.Zero(t, record.LostSamples, "perf reader lost samples") + + var event T + err = binary.Read(bytes.NewReader(record.RawSample), binary.LittleEndian, &event) + require.NoError(t, err, "failed to decode perf event") + + return event, true +} + +// AssertNoPerfEvent asserts that no perf event is emitted within the given timeout. +func AssertNoPerfEvent(t *testing.T, reader *perf.Reader, timeout time.Duration) { + t.Helper() + + reader.SetDeadline(time.Now().Add(timeout)) + + _, err := reader.Read() + if errors.Is(err, os.ErrDeadlineExceeded) { + return // expected: no event + } + if err != nil { + return // reader error is acceptable for "no event" assertion + } + t.Fatal("expected no perf event but got one") +} + +// IPToNative converts an IP string to its uint32 representation as stored by +// eBPF programs. Programs store ip->saddr directly from the packet (network +// byte order), which on a LE machine is the raw 4 bytes interpreted as a LE uint32. +func IPToNative(ipStr string) uint32 { + ip := net.ParseIP(ipStr).To4() + return *(*uint32)(unsafe.Pointer(&ip[0])) +} + +// PortToNetwork converts a host port number to the byte order used by eBPF +// programs. Programs store tcp->source / tcp->dest directly from the packet +// header (big-endian), then perf events are decoded as LE by binary.Read. +func PortToNetwork(port uint16) uint16 { + var buf [2]byte + binary.BigEndian.PutUint16(buf[:], port) + return binary.LittleEndian.Uint16(buf[:]) +} + +// LPMTrieKey represents the key for the retina_filter LPM trie map. +// This struct matches the layout generated by bpf2go for all plugins +// that use the shared retina_filter map. +type LPMTrieKey struct { + Prefixlen uint32 + Data uint32 +} + +// PopulateFilterMap inserts IPs into a retina_filter LPM trie map so that +// the eBPF program's lookup() function matches them. +func PopulateFilterMap(t *testing.T, filterMap *ebpf.Map, ips ...net.IP) { + t.Helper() + for _, ip := range ips { + ipBytes := ip.To4() + require.NotNil(t, ipBytes, "expected IPv4 address") + + key := LPMTrieKey{ + Prefixlen: 32, + Data: *(*uint32)(unsafe.Pointer(&ipBytes[0])), + } + val := uint8(1) + err := filterMap.Put(key, val) + require.NoError(t, err) + } +} + +// RunProgram executes an eBPF program via prog.Run() and returns the retval. +// Use this for TC classifier programs where perf events fire during Run(). +func RunProgram(t *testing.T, prog *ebpf.Program, pkt []byte) uint32 { + t.Helper() + ret, err := prog.Run(&ebpf.RunOptions{ + Data: pkt, + }) + require.NoError(t, err) + return ret +} + +// RunSocketFilter executes a socket filter program via prog.Test(). +// +// BPF_PROG_TEST_RUN for socket filters calls eth_type_trans(), which +// strips the first 14 bytes (Ethernet header) from the packet data. +// This function prepends a 14-byte dummy so the kernel strips that +// instead of the real Ethernet header, keeping bpf_skb_load_bytes +// offsets consistent with production. +// +// Perf events do NOT fire during Test() — use ReadPerCPUMap to read +// results from a per-CPU scratch map instead. +func RunSocketFilter(t *testing.T, prog *ebpf.Program, pkt []byte) uint32 { + t.Helper() + padded := append(make([]byte, 14), pkt...) + ret, _, err := prog.Test(padded) + require.NoError(t, err) + return ret +} + +// ReadPerCPUMap reads a per-CPU array map at the given key and returns the +// first entry with a non-zero first 8 bytes (typically a timestamp field). +// This is the standard way to read results from BPF programs that use a +// per-CPU scratch map during BPF_PROG_TEST_RUN (where perf events don't fire). +func ReadPerCPUMap[T any](t *testing.T, m *ebpf.Map, key uint32) (T, bool) { + t.Helper() + var values []T + err := m.Lookup(key, &values) + require.NoError(t, err, "failed to read per-CPU map") + + // Find the CPU that ran the program by looking for a non-zero timestamp + // (the first 8 bytes of the struct). + for _, v := range values { + raw := (*[8]byte)(unsafe.Pointer(&v)) + ts := binary.LittleEndian.Uint64(raw[:]) + if ts != 0 { + return v, true + } + } + var zero T + return zero, false +} + +// ReadRingBufEvent reads one ringbuf record from the reader within the given timeout, +// decodes it into T using binary.Read (little-endian), and returns it. +// Returns (zero, false) if the deadline expires without an event. +func ReadRingBufEvent[T any](t *testing.T, rb *ringbuf.Reader, timeout time.Duration) (T, bool) { + t.Helper() + + type result struct { + rec ringbuf.Record + err error + } + + // ringbuf.Read is blocking, so we run it in a goroutine to support timeout. + // When the test ends or rb is closed, this should unblock. + ch := make(chan result, 1) + go func() { + rec, err := rb.Read() + ch <- result{rec, err} + }() + + select { + case res := <-ch: + require.NoError(t, res.err) + var event T + err := binary.Read(bytes.NewReader(res.rec.RawSample), binary.LittleEndian, &event) + require.NoError(t, err) + return event, true + case <-time.After(timeout): + // Caller should cleanup reader to stop the goroutine + return *new(T), false + } +} diff --git a/pkg/plugin/ebpftest/packet.go b/pkg/plugin/ebpftest/packet.go new file mode 100644 index 0000000000..ea1a94dda9 --- /dev/null +++ b/pkg/plugin/ebpftest/packet.go @@ -0,0 +1,376 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//go:build ebpf && linux + +package ebpftest + +import ( + "net" + + "github.com/gopacket/gopacket" + "github.com/gopacket/gopacket/layers" +) + +var ( + // Default MAC addresses for test packets (arbitrary, eBPF programs don't inspect MACs). + DefaultSrcMAC = net.HardwareAddr{0x00, 0x00, 0x5e, 0x00, 0x53, 0x01} + DefaultDstMAC = net.HardwareAddr{0x00, 0x00, 0x5e, 0x00, 0x53, 0x02} +) + +// TCPPacketOpts configures a test TCP packet. +type TCPPacketOpts struct { + SrcIP, DstIP net.IP + SrcPort, DstPort uint16 + SYN, ACK, FIN bool + RST, PSH, URG bool + ECE, CWR bool + SeqNum, AckNum uint32 + TSval, TSecr uint32 // Zero means omit TCP timestamp option. + Payload []byte // Raw payload bytes. If nil, PayloadSize zero bytes are used. + PayloadSize int // Ignored when Payload is set. +} + +// BuildTCPPacket constructs a valid Ethernet + IPv4 + TCP packet. +func BuildTCPPacket(opts TCPPacketOpts) []byte { + eth := &layers.Ethernet{ + SrcMAC: DefaultSrcMAC, + DstMAC: DefaultDstMAC, + EthernetType: layers.EthernetTypeIPv4, + } + + ip := &layers.IPv4{ + Version: 4, + IHL: 5, + TTL: 64, + Protocol: layers.IPProtocolTCP, + SrcIP: opts.SrcIP.To4(), + DstIP: opts.DstIP.To4(), + } + + tcp := &layers.TCP{ + SrcPort: layers.TCPPort(opts.SrcPort), + DstPort: layers.TCPPort(opts.DstPort), + SYN: opts.SYN, + ACK: opts.ACK, + FIN: opts.FIN, + RST: opts.RST, + PSH: opts.PSH, + URG: opts.URG, + ECE: opts.ECE, + CWR: opts.CWR, + Seq: opts.SeqNum, + Ack: opts.AckNum, + Window: 65535, + } + + if opts.TSval != 0 || opts.TSecr != 0 { + tcp.Options = append(tcp.Options, layers.TCPOption{ + OptionType: layers.TCPOptionKindTimestamps, + OptionLength: 10, + OptionData: []byte{ + byte(opts.TSval >> 24), byte(opts.TSval >> 16), byte(opts.TSval >> 8), byte(opts.TSval), + byte(opts.TSecr >> 24), byte(opts.TSecr >> 16), byte(opts.TSecr >> 8), byte(opts.TSecr), + }, + }) + } + + tcp.SetNetworkLayerForChecksum(ip) + + payload := opts.Payload + if payload == nil { + payload = make([]byte, opts.PayloadSize) + } + + buf := gopacket.NewSerializeBuffer() + serializeOpts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + + err := gopacket.SerializeLayers(buf, serializeOpts, + eth, ip, tcp, gopacket.Payload(payload)) + if err != nil { + panic("failed to serialize TCP packet: " + err.Error()) + } + + return buf.Bytes() +} + +// UDPPacketOpts configures a test UDP packet. +type UDPPacketOpts struct { + SrcIP, DstIP net.IP + SrcPort, DstPort uint16 + Payload []byte // Raw payload bytes. If nil, PayloadSize zero bytes are used. + PayloadSize int // Ignored when Payload is set. +} + +// BuildUDPPacket constructs a valid Ethernet + IP + UDP packet. +// Automatically selects IPv4 or IPv6 based on the source IP address. +func BuildUDPPacket(opts UDPPacketOpts) []byte { + udp := &layers.UDP{ + SrcPort: layers.UDPPort(opts.SrcPort), + DstPort: layers.UDPPort(opts.DstPort), + } + + payload := opts.Payload + if payload == nil { + payload = make([]byte, opts.PayloadSize) + } + + serOpts := gopacket.SerializeOptions{FixLengths: true, ComputeChecksums: true} + + if isIPv6(opts.SrcIP) { + return serializeUDPv6(opts.SrcIP, opts.DstIP, udp, payload, serOpts) + } + return serializeUDPv4(opts.SrcIP, opts.DstIP, udp, payload, serOpts) +} + +func serializeUDPv4(srcIP, dstIP net.IP, udp *layers.UDP, payload []byte, serOpts gopacket.SerializeOptions) []byte { + eth := &layers.Ethernet{SrcMAC: DefaultSrcMAC, DstMAC: DefaultDstMAC, EthernetType: layers.EthernetTypeIPv4} + ip := &layers.IPv4{Version: 4, IHL: 5, TTL: 64, Protocol: layers.IPProtocolUDP, SrcIP: srcIP.To4(), DstIP: dstIP.To4()} + udp.SetNetworkLayerForChecksum(ip) + buf := gopacket.NewSerializeBuffer() + if err := gopacket.SerializeLayers(buf, serOpts, eth, ip, udp, gopacket.Payload(payload)); err != nil { + panic("failed to serialize UDPv4 packet: " + err.Error()) + } + return buf.Bytes() +} + +func serializeUDPv6(srcIP, dstIP net.IP, udp *layers.UDP, payload []byte, serOpts gopacket.SerializeOptions) []byte { + eth := &layers.Ethernet{SrcMAC: DefaultSrcMAC, DstMAC: DefaultDstMAC, EthernetType: layers.EthernetTypeIPv6} + ip := &layers.IPv6{Version: 6, HopLimit: 64, NextHeader: layers.IPProtocolUDP, SrcIP: srcIP.To16(), DstIP: dstIP.To16()} + udp.SetNetworkLayerForChecksum(ip) + buf := gopacket.NewSerializeBuffer() + if err := gopacket.SerializeLayers(buf, serOpts, eth, ip, udp, gopacket.Payload(payload)); err != nil { + panic("failed to serialize UDPv6 packet: " + err.Error()) + } + return buf.Bytes() +} + +// isIPv6 returns true if ip is an IPv6 address (not IPv4-mapped). +func isIPv6(ip net.IP) bool { + return ip.To4() == nil +} + +// DNSQueryOpts configures a test DNS query packet (Ethernet + IP + UDP + DNS). +type DNSQueryOpts struct { + SrcIP, DstIP net.IP + SrcPort, DstPort uint16 + ID uint16 + Name string + QType layers.DNSType +} + +// BuildDNSQueryPacket constructs a complete Ethernet + IP + UDP + DNS query packet. +// Automatically selects IPv4 or IPv6 based on the source IP address. +func BuildDNSQueryPacket(opts DNSQueryOpts) []byte { + dns := &layers.DNS{ + ID: opts.ID, + RD: true, + QDCount: 1, + Questions: []layers.DNSQuestion{{ + Name: []byte(opts.Name), + Type: opts.QType, + Class: layers.DNSClassIN, + }}, + } + return buildDNSPacket(opts.SrcIP, opts.DstIP, opts.SrcPort, opts.DstPort, dns) +} + +// DNSResponseOpts configures a test DNS response packet (Ethernet + IP + UDP + DNS). +type DNSResponseOpts struct { + SrcIP, DstIP net.IP + SrcPort, DstPort uint16 + ID uint16 + Name string + QType layers.DNSType + RCode layers.DNSResponseCode + Answers []net.IP // A/AAAA record answers. +} + +// BuildDNSResponsePacket constructs a complete Ethernet + IP + UDP + DNS response packet. +// Automatically selects IPv4 or IPv6 based on the source IP address. +func BuildDNSResponsePacket(opts DNSResponseOpts) []byte { + dns := &layers.DNS{ + ID: opts.ID, + QR: true, + RD: true, + ResponseCode: opts.RCode, + QDCount: 1, + Questions: []layers.DNSQuestion{{ + Name: []byte(opts.Name), + Type: opts.QType, + Class: layers.DNSClassIN, + }}, + } + for _, ip := range opts.Answers { + rr := layers.DNSResourceRecord{ + Name: []byte(opts.Name), + Type: opts.QType, + Class: layers.DNSClassIN, + TTL: 60, + IP: ip, + } + dns.Answers = append(dns.Answers, rr) + } + dns.ANCount = uint16(len(dns.Answers)) + return buildDNSPacket(opts.SrcIP, opts.DstIP, opts.SrcPort, opts.DstPort, dns) +} + +// serializeDNS returns the raw bytes of a DNS layer. +func serializeDNS(dns *layers.DNS) []byte { + buf := gopacket.NewSerializeBuffer() + if err := dns.SerializeTo(buf, gopacket.SerializeOptions{FixLengths: true}); err != nil { + panic("failed to serialize DNS: " + err.Error()) + } + return buf.Bytes() +} + +// buildDNSPacket serializes a DNS layer inside an Ethernet + IP + UDP frame. +func buildDNSPacket(srcIP, dstIP net.IP, srcPort, dstPort uint16, dns *layers.DNS) []byte { + return BuildUDPPacket(UDPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: srcPort, DstPort: dstPort, + Payload: serializeDNS(dns), + }) +} + +// BuildDNSTCPQueryPacket constructs an Ethernet + IPv4 + TCP packet carrying a +// DNS query. DNS over TCP prepends a 2-byte length field before the message. +func BuildDNSTCPQueryPacket(opts DNSQueryOpts) []byte { + dnsBytes := serializeDNS(&layers.DNS{ + ID: opts.ID, RD: true, QDCount: 1, + Questions: []layers.DNSQuestion{{ + Name: []byte(opts.Name), Type: opts.QType, Class: layers.DNSClassIN, + }}, + }) + tcpPayload := make([]byte, 2+len(dnsBytes)) + tcpPayload[0] = byte(len(dnsBytes) >> 8) + tcpPayload[1] = byte(len(dnsBytes)) + copy(tcpPayload[2:], dnsBytes) + + return BuildTCPPacket(TCPPacketOpts{ + SrcIP: opts.SrcIP, DstIP: opts.DstIP, + SrcPort: opts.SrcPort, DstPort: opts.DstPort, + PSH: true, ACK: true, + Payload: tcpPayload, + }) +} + +// BuildNonIPPacket constructs an Ethernet frame with the given EtherType and a small payload. +// Useful for testing that eBPF programs correctly skip non-IPv4 traffic. +func BuildNonIPPacket(etherType layers.EthernetType) []byte { + eth := &layers.Ethernet{ + SrcMAC: DefaultSrcMAC, + DstMAC: DefaultDstMAC, + EthernetType: etherType, + } + + buf := gopacket.NewSerializeBuffer() + serializeOpts := gopacket.SerializeOptions{FixLengths: true} + + // Add enough payload to be a valid-looking frame. + err := gopacket.SerializeLayers(buf, serializeOpts, + eth, gopacket.Payload(make([]byte, 46))) + if err != nil { + panic("failed to serialize non-IP packet: " + err.Error()) + } + + return buf.Bytes() +} + +// BuildRuntPacket returns a byte slice shorter than 14 bytes (Ethernet header size). +func BuildRuntPacket() []byte { + return []byte{0x00, 0x00, 0x5e, 0x00, 0x53, 0x01, 0x00, 0x00, 0x5e, 0x00} +} + +// BuildTruncatedIPPacket returns a valid Ethernet header followed by a truncated IPv4 header. +func BuildTruncatedIPPacket() []byte { + eth := &layers.Ethernet{ + SrcMAC: DefaultSrcMAC, + DstMAC: DefaultDstMAC, + EthernetType: layers.EthernetTypeIPv4, + } + + buf := gopacket.NewSerializeBuffer() + serializeOpts := gopacket.SerializeOptions{FixLengths: true} + + err := gopacket.SerializeLayers(buf, serializeOpts, eth) + if err != nil { + panic("failed to serialize ethernet header: " + err.Error()) + } + + // Append a truncated IPv4 header (only 10 bytes instead of minimum 20). + result := buf.Bytes() + result = append(result, []byte{0x45, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x40, 0x06}...) + return result +} + +// BuildICMPPacket constructs a valid Ethernet + IPv4 + ICMP echo request packet. +func BuildICMPPacket(srcIP, dstIP net.IP) []byte { + eth := &layers.Ethernet{ + SrcMAC: DefaultSrcMAC, + DstMAC: DefaultDstMAC, + EthernetType: layers.EthernetTypeIPv4, + } + + ip := &layers.IPv4{ + Version: 4, + IHL: 5, + TTL: 64, + Protocol: layers.IPProtocolICMPv4, + SrcIP: srcIP.To4(), + DstIP: dstIP.To4(), + } + + icmp := &layers.ICMPv4{ + TypeCode: layers.CreateICMPv4TypeCode(layers.ICMPv4TypeEchoRequest, 0), + Id: 1, + Seq: 1, + } + + buf := gopacket.NewSerializeBuffer() + serializeOpts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + + err := gopacket.SerializeLayers(buf, serializeOpts, + eth, ip, icmp, gopacket.Payload([]byte("ping"))) + if err != nil { + panic("failed to serialize ICMP packet: " + err.Error()) + } + + return buf.Bytes() +} + +// BuildTruncatedTCPPacket constructs an Ethernet + IPv4 header with a truncated TCP header. +func BuildTruncatedTCPPacket(srcIP, dstIP net.IP) []byte { + eth := &layers.Ethernet{ + SrcMAC: DefaultSrcMAC, + DstMAC: DefaultDstMAC, + EthernetType: layers.EthernetTypeIPv4, + } + + ip := &layers.IPv4{ + Version: 4, + IHL: 5, + TTL: 64, + Protocol: layers.IPProtocolTCP, + SrcIP: srcIP.To4(), + DstIP: dstIP.To4(), + } + + buf := gopacket.NewSerializeBuffer() + serializeOpts := gopacket.SerializeOptions{FixLengths: true, ComputeChecksums: true} + + err := gopacket.SerializeLayers(buf, serializeOpts, eth, ip) + if err != nil { + panic("failed to serialize IP header: " + err.Error()) + } + + // Append only 10 bytes of TCP header (minimum is 20). + result := buf.Bytes() + result = append(result, []byte{0x30, 0x39, 0x00, 0x50, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00}...) + return result +} diff --git a/pkg/plugin/filter/filter_map_linux.go b/pkg/plugin/filter/filter_map_linux.go index 061ae7313d..e2b626f8ae 100644 --- a/pkg/plugin/filter/filter_map_linux.go +++ b/pkg/plugin/filter/filter_map_linux.go @@ -34,7 +34,7 @@ type FilterMap struct { batchApiNotSupported bool } -func Init() (*FilterMap, error) { +func Init(maxEntries uint32) (*FilterMap, error) { once.Do(func() { f = &FilterMap{} }) @@ -51,8 +51,20 @@ func Init() (*FilterMap, error) { return f, err } - obj := &filterObjects{} //nolint:typecheck - err := loadFilterObjects(obj, &ebpf.CollectionOptions{ //nolint:typecheck + spec, err := loadFilter() //nolint:typecheck // generated by bpf2go + if err != nil { + f.l.Error("loadFilter failed", zap.Error(err)) + return f, err + } + + // Override the filter map max entries from config. + if mapSpec, ok := spec.Maps["retina_filter"]; ok && maxEntries > 0 { + mapSpec.MaxEntries = maxEntries + f.l.Info("Filter map max entries configured", zap.Uint32("maxEntries", maxEntries)) + } + + obj := &filterObjects{} //nolint:typecheck // generated by bpf2go + err = spec.LoadAndAssign(obj, &ebpf.CollectionOptions{ Maps: ebpf.MapOptions{ PinPath: plugincommon.MapPath, }, diff --git a/pkg/plugin/filter/filter_map_windows.go b/pkg/plugin/filter/filter_map_windows.go index d7968bacca..c176994afa 100644 --- a/pkg/plugin/filter/filter_map_windows.go +++ b/pkg/plugin/filter/filter_map_windows.go @@ -7,7 +7,7 @@ import "net" type FilterMap struct{} -func Init() (*FilterMap, error) { +func Init(_ uint32) (*FilterMap, error) { return &FilterMap{}, nil } diff --git a/pkg/plugin/linuxutil/ethtool_handle_linux.go b/pkg/plugin/linuxutil/ethtool_handle_linux.go index ee71a8611f..42e7378a48 100644 --- a/pkg/plugin/linuxutil/ethtool_handle_linux.go +++ b/pkg/plugin/linuxutil/ethtool_handle_linux.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/pkg/errors" + "github.com/safchain/ethtool" lru "github.com/hashicorp/golang-lru/v2" @@ -26,13 +27,13 @@ func NewCachedEthtool(ethHandle EthtoolInterface, unsupportedInterfacesCache *lr var errskip = errors.New("skip interface") -func (ce *CachedEthtool) Stats(intf string) (map[string]uint64, error) { +func (ce *CachedEthtool) StatsWithBuffer(intf string, gstring *ethtool.EthtoolGStrings, stats *ethtool.EthtoolStats) (map[string]uint64, error) { // Skip unsupported interfaces if _, ok := ce.unsupported.Get(intf); ok { return nil, errskip } - ifaceStats, err := ce.EthtoolInterface.Stats(intf) + ifaceStats, err := ce.EthtoolInterface.StatsWithBuffer(intf, gstring, stats) if err != nil { if strings.Contains(err.Error(), "operation not supported") { ce.unsupported.Add(intf, struct{}{}) diff --git a/pkg/plugin/linuxutil/ethtool_stats_linux.go b/pkg/plugin/linuxutil/ethtool_stats_linux.go index 0d2088d846..2609bee969 100644 --- a/pkg/plugin/linuxutil/ethtool_stats_linux.go +++ b/pkg/plugin/linuxutil/ethtool_stats_linux.go @@ -20,9 +20,19 @@ type EthtoolReader struct { opts *EthtoolOpts data *EthtoolStats ethHandle EthtoolInterface + gstrings *ethtool.EthtoolGStrings + stats *ethtool.EthtoolStats } -func NewEthtoolReader(opts *EthtoolOpts, ethHandle EthtoolInterface, unsupportedInterfacesCache *lru.Cache[string, struct{}]) *EthtoolReader { +// if gstrings and stats are nil, a new buffer is initialized +// to avoid null pointer +func NewEthtoolReader( + opts *EthtoolOpts, + ethHandle EthtoolInterface, + unsupportedInterfacesCache *lru.Cache[string, struct{}], + gstrings *ethtool.EthtoolGStrings, + stats *ethtool.EthtoolStats, +) *EthtoolReader { if ethHandle == nil { var err error ethHandle, err = ethtool.NewEthtool() @@ -33,11 +43,23 @@ func NewEthtoolReader(opts *EthtoolOpts, ethHandle EthtoolInterface, unsupported } // Construct a cached ethtool handle CachedEthHandle := NewCachedEthtool(ethHandle, unsupportedInterfacesCache) + + // if gstrings is nil, initialize it + if gstrings == nil { + gstrings = ðtool.EthtoolGStrings{} + } + // if stats is nil, initialize it + if stats == nil { + stats = ðtool.EthtoolStats{} + } + return &EthtoolReader{ l: log.Logger().Named(string("EthtoolReader")), opts: opts, data: &EthtoolStats{}, ethHandle: CachedEthHandle, + gstrings: gstrings, + stats: stats, } } @@ -73,7 +95,7 @@ func (er *EthtoolReader) readInterfaceStats() error { } // Retrieve tx from eth0 - ifaceStats, err := er.ethHandle.Stats(i.Name) + ifaceStats, err := er.ethHandle.StatsWithBuffer(i.Name, er.gstrings, er.stats) if err != nil { if errors.Is(err, errskip) { er.l.Debug("Skipping unsupported interface", zap.String("ifacename", i.Name)) diff --git a/pkg/plugin/linuxutil/ethtool_stats_linux_test.go b/pkg/plugin/linuxutil/ethtool_stats_linux_test.go index d77044be32..563ebb0e82 100644 --- a/pkg/plugin/linuxutil/ethtool_stats_linux_test.go +++ b/pkg/plugin/linuxutil/ethtool_stats_linux_test.go @@ -5,6 +5,7 @@ import ( "testing" lru "github.com/hashicorp/golang-lru/v2" + "github.com/safchain/ethtool" "github.com/microsoft/retina/pkg/log" "github.com/microsoft/retina/pkg/metrics" @@ -38,8 +39,11 @@ func TestNewEthtool(t *testing.T) { t.Fatal("failed to create cache:", err) } + stats := new(ethtool.EthtoolStats) + gstrings := new(ethtool.EthtoolGStrings) + ethHandle := NewMockEthtoolInterface(ctrl) - ethReader := NewEthtoolReader(opts, ethHandle, unsupportedInterfacesCache) + ethReader := NewEthtoolReader(opts, ethHandle, unsupportedInterfacesCache, gstrings, stats) assert.NotNil(t, ethReader) } @@ -56,7 +60,7 @@ func TestNewEthtoolWithNil(t *testing.T) { t.Fatal("failed to create cache:", err) } - ethReader := NewEthtoolReader(opts, nil, unsupportedInterfacesCache) + ethReader := NewEthtoolReader(opts, nil, unsupportedInterfacesCache, nil, nil) assert.NotNil(t, ethReader) } @@ -127,6 +131,11 @@ func TestReadInterfaceStats(t *testing.T) { }, } + gstrings := new(ethtool.EthtoolGStrings) + stats := new(ethtool.EthtoolStats) + + // Create a mock EthtoolInterface + for _, tt := range tests { l.Infof("Running TestReadInterfaceStats %s", tt.name) @@ -135,11 +144,11 @@ func TestReadInterfaceStats(t *testing.T) { ethHandle := NewMockEthtoolInterface(ctrl) - ethReader := NewEthtoolReader(tt.opts, ethHandle, unsupportedInterfacesCache) + ethReader := NewEthtoolReader(tt.opts, ethHandle, unsupportedInterfacesCache, gstrings, stats) assert.NotNil(t, ethReader) - ethHandle.EXPECT().Stats(gomock.Any()).Return(tt.statsReturn, tt.statErr).AnyTimes() + ethHandle.EXPECT().StatsWithBuffer(gomock.Any(), gstrings, stats).Return(tt.statsReturn, tt.statErr).AnyTimes() InitalizeMetricsForTesting(ctrl) if tt.statErr == nil { diff --git a/pkg/plugin/linuxutil/linuxutil_linux.go b/pkg/plugin/linuxutil/linuxutil_linux.go index b96f6c698b..c1b460eabe 100644 --- a/pkg/plugin/linuxutil/linuxutil_linux.go +++ b/pkg/plugin/linuxutil/linuxutil_linux.go @@ -69,6 +69,9 @@ func (lu *linuxUtil) run(ctx context.Context) error { return err } + gstrings := new(ethtool.EthtoolGStrings) + stats := new(ethtool.EthtoolStats) + ticker := time.NewTicker(lu.cfg.MetricsInterval) defer ticker.Stop() @@ -79,10 +82,9 @@ func (lu *linuxUtil) run(ctx context.Context) error { return nil case <-ticker.C: opts := &NetstatOpts{ - CuratedKeys: true, - AddZeroVal: false, - ListenSock: false, - PrevTCPSockStats: lu.prevTCPSockStats, + CuratedKeys: true, + AddZeroVal: false, + ListenSock: false, } var wg sync.WaitGroup @@ -91,11 +93,10 @@ func (lu *linuxUtil) run(ctx context.Context) error { wg.Add(1) go func() { defer wg.Done() - tcpSocketStats, err := nsReader.readAndUpdate() + _, err := nsReader.readAndUpdate() if err != nil { lu.l.Error("Reading netstat failed", zap.Error(err)) } - lu.prevTCPSockStats = tcpSocketStats }() ethtoolOpts := &EthtoolOpts{ @@ -109,7 +110,7 @@ func (lu *linuxUtil) run(ctx context.Context) error { return fmt.Errorf("failed to create ethHandle: %w", err) } - ethReader := NewEthtoolReader(ethtoolOpts, ethHandle, unsupportedInterfacesCache) + ethReader := NewEthtoolReader(ethtoolOpts, ethHandle, unsupportedInterfacesCache, gstrings, stats) if ethReader == nil { lu.l.Error("Error while creating ethReader") return errors.New("error while creating ethReader") diff --git a/pkg/plugin/linuxutil/linuxutil_mock_generated_linux.go b/pkg/plugin/linuxutil/linuxutil_mock_generated_linux.go index c635ee37e9..dd35032ecb 100644 --- a/pkg/plugin/linuxutil/linuxutil_mock_generated_linux.go +++ b/pkg/plugin/linuxutil/linuxutil_mock_generated_linux.go @@ -13,6 +13,7 @@ import ( reflect "reflect" netstat "github.com/cakturk/go-netstat/netstat" + ethtool "github.com/safchain/ethtool" gomock "go.uber.org/mock/gomock" ) @@ -51,19 +52,19 @@ func (mr *MockEthtoolInterfaceMockRecorder) Close() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockEthtoolInterface)(nil).Close)) } -// Stats mocks base method. -func (m *MockEthtoolInterface) Stats(intf string) (map[string]uint64, error) { +// StatsWithBuffer mocks base method. +func (m *MockEthtoolInterface) StatsWithBuffer(intf string, gstrings *ethtool.EthtoolGStrings, stats *ethtool.EthtoolStats) (map[string]uint64, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Stats", intf) + ret := m.ctrl.Call(m, "StatsWithBuffer", intf, gstrings, stats) ret0, _ := ret[0].(map[string]uint64) ret1, _ := ret[1].(error) return ret0, ret1 } -// Stats indicates an expected call of Stats. -func (mr *MockEthtoolInterfaceMockRecorder) Stats(intf any) *gomock.Call { +// StatsWithBuffer indicates an expected call of StatsWithBuffer. +func (mr *MockEthtoolInterfaceMockRecorder) StatsWithBuffer(intf, gstrings, stats any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stats", reflect.TypeOf((*MockEthtoolInterface)(nil).Stats), intf) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StatsWithBuffer", reflect.TypeOf((*MockEthtoolInterface)(nil).StatsWithBuffer), intf, gstrings, stats) } // MockNetstatInterface is a mock of NetstatInterface interface. diff --git a/pkg/plugin/linuxutil/types_linux.go b/pkg/plugin/linuxutil/types_linux.go index e7c20764fc..83d1e0a956 100644 --- a/pkg/plugin/linuxutil/types_linux.go +++ b/pkg/plugin/linuxutil/types_linux.go @@ -6,6 +6,7 @@ import ( "github.com/cakturk/go-netstat/netstat" kcfg "github.com/microsoft/retina/pkg/config" "github.com/microsoft/retina/pkg/log" + "github.com/safchain/ethtool" ) const name = "linuxutil" @@ -87,9 +88,6 @@ type NetstatOpts struct { // get only listening sockets ListenSock bool - - // previous TCP socket stats - PrevTCPSockStats *SocketStats } type EthtoolStats struct { @@ -106,7 +104,8 @@ type EthtoolOpts struct { } type EthtoolInterface interface { - Stats(intf string) (map[string]uint64, error) + // the buffer is used internally by the ethtool package to avoid memory allocation churn + StatsWithBuffer(intf string, gstrings *ethtool.EthtoolGStrings, stats *ethtool.EthtoolStats) (map[string]uint64, error) Close() } diff --git a/pkg/plugin/packetforward/packetforward_linux.go b/pkg/plugin/packetforward/packetforward_linux.go index 3e14aee541..b94592fda0 100644 --- a/pkg/plugin/packetforward/packetforward_linux.go +++ b/pkg/plugin/packetforward/packetforward_linux.go @@ -124,8 +124,15 @@ func (p *packetForward) Compile(ctx context.Context) error { if arch == "arm64" { targetArch = "-D__TARGET_ARCH_arm64" } + + runtimeIncludeDir := "-I" + loader.VmlinuxHeaderDir() + // Keep target as bpf, otherwise clang compilation yields bpf object that elf reader cannot load. - err = loader.CompileEbpf(ctx, "-target", "bpf", "-Wall", targetArch, "-g", "-O2", "-c", bpfSourceFile, "-o", bpfOutputFile, includeDir, libbpfDir) + err = loader.CompileEbpf( + ctx, + "-target", "bpf", "-Wall", targetArch, "-g", "-O2", "-c", bpfSourceFile, + "-o", bpfOutputFile, runtimeIncludeDir, includeDir, libbpfDir, + ) if err != nil { return errors.Wrap(err, "error compiling ebpf code") } diff --git a/pkg/plugin/packetforward/packetforward_linux_test.go b/pkg/plugin/packetforward/packetforward_linux_test.go index 2393889ee4..de727cf82d 100644 --- a/pkg/plugin/packetforward/packetforward_linux_test.go +++ b/pkg/plugin/packetforward/packetforward_linux_test.go @@ -10,6 +10,7 @@ import ( "context" "errors" "fmt" + "log/slog" "os" "testing" "time" @@ -181,7 +182,7 @@ func TestShutdown(t *testing.T) { func TestRun(t *testing.T) { log.SetupZapLogger(log.GetDefaultLogOpts()) - metrics.InitializeMetrics() + metrics.InitializeMetrics(slog.Default()) ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -214,7 +215,7 @@ func TestRun(t *testing.T) { func TestRun_ReturnError_Ingress(t *testing.T) { log.SetupZapLogger(log.GetDefaultLogOpts()) - metrics.InitializeMetrics() + metrics.InitializeMetrics(slog.Default()) ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -247,7 +248,7 @@ func TestRun_ReturnError_Ingress(t *testing.T) { func TestRun_ReturnError_Egress(t *testing.T) { log.SetupZapLogger(log.GetDefaultLogOpts()) - metrics.InitializeMetrics() + metrics.InitializeMetrics(slog.Default()) ctrl := gomock.NewController(t) defer ctrl.Finish() diff --git a/pkg/plugin/packetparser/_cprog/dynamic.h b/pkg/plugin/packetparser/_cprog/dynamic.h index ecadc42211..43f0976f19 100644 --- a/pkg/plugin/packetparser/_cprog/dynamic.h +++ b/pkg/plugin/packetparser/_cprog/dynamic.h @@ -1,2 +1,3 @@ #define BYPASS_LOOKUP_IP_OF_INTEREST 0 #define DATA_AGGREGATION_LEVEL 0 +#define DATA_SAMPLING_RATE 1 \ No newline at end of file diff --git a/pkg/plugin/packetparser/_cprog/packetparser.c b/pkg/plugin/packetparser/_cprog/packetparser.c index 40ec8fb03e..5d63edde0a 100644 --- a/pkg/plugin/packetparser/_cprog/packetparser.c +++ b/pkg/plugin/packetparser/_cprog/packetparser.c @@ -16,8 +16,16 @@ char __license[] SEC("license") = "Dual MIT/GPL"; struct { +#ifdef USE_RING_BUFFER + __uint(type, BPF_MAP_TYPE_RINGBUF); +#ifndef RING_BUFFER_SIZE +#define RING_BUFFER_SIZE (8 * 1024 * 1024) +#endif + __uint(max_entries, RING_BUFFER_SIZE); +#else __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); __uint(max_entries, 16384); +#endif } retina_packetparser_events SEC(".maps"); // Define const variables to avoid warnings. @@ -208,18 +216,46 @@ static void parse(struct __sk_buff *skb, __u8 obs) p.conntrack_metadata = conntrack_metadata; #endif // ENABLE_CONNTRACK_METRICS + #ifdef DATA_AGGREGATION_LEVEL + + // Calculate sampling + bool sampled __attribute__((unused)); + sampled = true; + + #ifdef DATA_SAMPLING_RATE + u32 rand __attribute__((unused)); + rand = bpf_get_prandom_u32(); + if (rand >= UINT32_MAX / DATA_SAMPLING_RATE) { + sampled = false; + } + #endif + // Process the packet in ct - bool report __attribute__((unused)); - report = ct_process_packet(&p, obs); - #ifdef DATA_AGGREGATION_LEVEL + struct packetreport report __attribute__((unused)); + report = ct_process_packet(&p, obs, sampled); + // If the data aggregation level is low, always send the packet to the perf buffer. #if DATA_AGGREGATION_LEVEL == DATA_AGGREGATION_LEVEL_LOW + p.previously_observed_packets = 0; + p.previously_observed_bytes = 0; + __builtin_memset(&p.previously_observed_flags, 0, sizeof(struct tcpflagscount)); +#ifdef USE_RING_BUFFER + bpf_ringbuf_output(&retina_packetparser_events, &p, sizeof(p), 0); +#else bpf_perf_event_output(skb, &retina_packetparser_events, BPF_F_CURRENT_CPU, &p, sizeof(p)); +#endif return; // If the data aggregation level is high, only send the packet to the perf buffer if it needs to be reported. #elif DATA_AGGREGATION_LEVEL == DATA_AGGREGATION_LEVEL_HIGH - if (report) { + if (report.report) { + p.previously_observed_packets = report.previously_observed_packets; + p.previously_observed_bytes = report.previously_observed_bytes; + p.previously_observed_flags = report.previously_observed_flags; +#ifdef USE_RING_BUFFER + bpf_ringbuf_output(&retina_packetparser_events, &p, sizeof(p), 0); +#else bpf_perf_event_output(skb, &retina_packetparser_events, BPF_F_CURRENT_CPU, &p, sizeof(p)); +#endif } #endif #endif diff --git a/pkg/plugin/packetparser/mocks/mock_types_linux.go b/pkg/plugin/packetparser/mocks/mock_types_linux.go index 5b6ce7599b..65478d8b5f 100644 --- a/pkg/plugin/packetparser/mocks/mock_types_linux.go +++ b/pkg/plugin/packetparser/mocks/mock_types_linux.go @@ -3,7 +3,7 @@ // // Generated by this command: // -// mockgen -source=types_linux.go -destination=mocks/mock_types_linux.go -package=mocks +// mockgen -source=types_linux.go -destination=mocks/mock_types_linux.go -package=mocks -exclude_interfaces=perfReader // // Package mocks is a generated GoMock package. @@ -12,7 +12,6 @@ package mocks import ( reflect "reflect" - perf "github.com/cilium/ebpf/perf" tc "github.com/florianl/go-tc" netlink "github.com/mdlayher/netlink" gomock "go.uber.org/mock/gomock" @@ -22,7 +21,6 @@ import ( type Mockqdisc struct { ctrl *gomock.Controller recorder *MockqdiscMockRecorder - isgomock struct{} } // MockqdiscMockRecorder is the mock recorder for Mockqdisc. @@ -74,7 +72,6 @@ func (mr *MockqdiscMockRecorder) Delete(info any) *gomock.Call { type Mockfilter struct { ctrl *gomock.Controller recorder *MockfilterMockRecorder - isgomock struct{} } // MockfilterMockRecorder is the mock recorder for Mockfilter. @@ -112,7 +109,6 @@ func (mr *MockfilterMockRecorder) Add(info any) *gomock.Call { type Mocknltc struct { ctrl *gomock.Controller recorder *MocknltcMockRecorder - isgomock struct{} } // MocknltcMockRecorder is the mock recorder for Mocknltc. @@ -187,56 +183,3 @@ func (mr *MocknltcMockRecorder) SetOption(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetOption", reflect.TypeOf((*Mocknltc)(nil).SetOption), arg0, arg1) } - -// MockperfReader is a mock of perfReader interface. -type MockperfReader struct { - ctrl *gomock.Controller - recorder *MockperfReaderMockRecorder - isgomock struct{} -} - -// MockperfReaderMockRecorder is the mock recorder for MockperfReader. -type MockperfReaderMockRecorder struct { - mock *MockperfReader -} - -// NewMockperfReader creates a new mock instance. -func NewMockperfReader(ctrl *gomock.Controller) *MockperfReader { - mock := &MockperfReader{ctrl: ctrl} - mock.recorder = &MockperfReaderMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockperfReader) EXPECT() *MockperfReaderMockRecorder { - return m.recorder -} - -// Close mocks base method. -func (m *MockperfReader) Close() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Close") - ret0, _ := ret[0].(error) - return ret0 -} - -// Close indicates an expected call of Close. -func (mr *MockperfReaderMockRecorder) Close() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockperfReader)(nil).Close)) -} - -// Read mocks base method. -func (m *MockperfReader) Read() (perf.Record, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Read") - ret0, _ := ret[0].(perf.Record) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Read indicates an expected call of Read. -func (mr *MockperfReaderMockRecorder) Read() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockperfReader)(nil).Read)) -} diff --git a/pkg/plugin/packetparser/packetparser_bpfel_arm64.go b/pkg/plugin/packetparser/packetparser_bpfel_arm64.go index ebfa05e37f..3ce8c9a4c8 100644 --- a/pkg/plugin/packetparser/packetparser_bpfel_arm64.go +++ b/pkg/plugin/packetparser/packetparser_bpfel_arm64.go @@ -13,18 +13,44 @@ import ( ) type packetparserCtEntry struct { - EvictionTime uint32 - LastReportTxDir uint32 - LastReportRxDir uint32 + EvictionTime uint32 + LastReportTxDir uint32 + LastReportRxDir uint32 + BytesSeenSinceLastReportTxDir uint32 + BytesSeenSinceLastReportRxDir uint32 + PacketsSeenSinceLastReportTxDir uint32 + PacketsSeenSinceLastReportRxDir uint32 + FlagsSeenSinceLastReportTxDir struct { + Syn uint32 + Ack uint32 + Fin uint32 + Rst uint32 + Psh uint32 + Urg uint32 + Ece uint32 + Cwr uint32 + Ns uint32 + } + FlagsSeenSinceLastReportRxDir struct { + Syn uint32 + Ack uint32 + Fin uint32 + Rst uint32 + Psh uint32 + Urg uint32 + Ece uint32 + Cwr uint32 + Ns uint32 + } TrafficDirection uint8 FlagsSeenTxDir uint8 FlagsSeenRxDir uint8 IsDirectionUnknown bool ConntrackMetadata struct { - BytesForwardCount uint64 - BytesReplyCount uint64 - PacketsForwardCount uint32 - PacketsReplyCount uint32 + BytesTxCount uint64 + BytesRxCount uint64 + PacketsTxCount uint32 + PacketsRxCount uint32 } } @@ -55,17 +81,32 @@ type packetparserPacket struct { Tsval uint32 Tsecr uint32 } - ObservationPoint uint8 - TrafficDirection uint8 - Proto uint8 - Flags uint8 - IsReply bool - _ [3]byte + ObservationPoint uint8 + TrafficDirection uint8 + Proto uint8 + _ [1]byte + Flags uint16 + IsReply bool + _ [1]byte + PreviouslyObservedPackets uint32 + PreviouslyObservedBytes uint32 + PreviouslyObservedFlags struct { + Syn uint32 + Ack uint32 + Fin uint32 + Rst uint32 + Psh uint32 + Urg uint32 + Ece uint32 + Cwr uint32 + Ns uint32 + } + _ [4]byte ConntrackMetadata struct { - BytesForwardCount uint64 - BytesReplyCount uint64 - PacketsForwardCount uint32 - PacketsReplyCount uint32 + BytesTxCount uint64 + BytesRxCount uint64 + PacketsTxCount uint32 + PacketsRxCount uint32 } } diff --git a/pkg/plugin/packetparser/packetparser_bpfel_x86.go b/pkg/plugin/packetparser/packetparser_bpfel_x86.go index 36de41247b..ce86707cca 100644 --- a/pkg/plugin/packetparser/packetparser_bpfel_x86.go +++ b/pkg/plugin/packetparser/packetparser_bpfel_x86.go @@ -13,18 +13,44 @@ import ( ) type packetparserCtEntry struct { - EvictionTime uint32 - LastReportTxDir uint32 - LastReportRxDir uint32 + EvictionTime uint32 + LastReportTxDir uint32 + LastReportRxDir uint32 + BytesSeenSinceLastReportTxDir uint32 + BytesSeenSinceLastReportRxDir uint32 + PacketsSeenSinceLastReportTxDir uint32 + PacketsSeenSinceLastReportRxDir uint32 + FlagsSeenSinceLastReportTxDir struct { + Syn uint32 + Ack uint32 + Fin uint32 + Rst uint32 + Psh uint32 + Urg uint32 + Ece uint32 + Cwr uint32 + Ns uint32 + } + FlagsSeenSinceLastReportRxDir struct { + Syn uint32 + Ack uint32 + Fin uint32 + Rst uint32 + Psh uint32 + Urg uint32 + Ece uint32 + Cwr uint32 + Ns uint32 + } TrafficDirection uint8 FlagsSeenTxDir uint8 FlagsSeenRxDir uint8 IsDirectionUnknown bool ConntrackMetadata struct { - BytesForwardCount uint64 - BytesReplyCount uint64 - PacketsForwardCount uint32 - PacketsReplyCount uint32 + BytesTxCount uint64 + BytesRxCount uint64 + PacketsTxCount uint32 + PacketsRxCount uint32 } } @@ -55,17 +81,32 @@ type packetparserPacket struct { Tsval uint32 Tsecr uint32 } - ObservationPoint uint8 - TrafficDirection uint8 - Proto uint8 - Flags uint8 - IsReply bool - _ [3]byte + ObservationPoint uint8 + TrafficDirection uint8 + Proto uint8 + _ [1]byte + Flags uint16 + IsReply bool + _ [1]byte + PreviouslyObservedPackets uint32 + PreviouslyObservedBytes uint32 + PreviouslyObservedFlags struct { + Syn uint32 + Ack uint32 + Fin uint32 + Rst uint32 + Psh uint32 + Urg uint32 + Ece uint32 + Cwr uint32 + Ns uint32 + } + _ [4]byte ConntrackMetadata struct { - BytesForwardCount uint64 - BytesReplyCount uint64 - PacketsForwardCount uint32 - PacketsReplyCount uint32 + BytesTxCount uint64 + BytesRxCount uint64 + PacketsTxCount uint32 + PacketsRxCount uint32 } } diff --git a/pkg/plugin/packetparser/packetparser_config_test.go b/pkg/plugin/packetparser/packetparser_config_test.go new file mode 100644 index 0000000000..0f8ea27587 --- /dev/null +++ b/pkg/plugin/packetparser/packetparser_config_test.go @@ -0,0 +1,89 @@ +//go:build linux +// +build linux + +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package packetparser + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidateRingBufferSize(t *testing.T) { + const maxSize = 1 * 1024 * 1024 * 1024 // 1GB + intPageSize := os.Getpagesize() + if intPageSize <= 0 { + intPageSize = 4096 + } + if intPageSize > int(^uint32(0)) { + intPageSize = int(^uint32(0)) + } + //nolint:gosec // bounded to uint32 + pageSize := uint32(intPageSize) + + tests := []struct { + name string + inputSize uint32 + expectedErr bool + expectedMsg string + }{ + { + name: "Zero input returns error", + inputSize: 0, + expectedErr: true, + expectedMsg: "must be set", + }, + { + name: "Below page size returns error", + inputSize: pageSize - 1, + expectedErr: true, + expectedMsg: "page size", + }, + { + name: "Above max size returns error", + inputSize: maxSize + 1, + expectedErr: true, + expectedMsg: "maximum", + }, + { + name: "Not power of 2 returns error", + inputSize: (8 * 1024 * 1024) + 1, + expectedErr: true, + expectedMsg: "power of 2", + }, + { + name: "Valid size returns no error", + inputSize: 16 * 1024 * 1024, + expectedErr: false, + }, + { + name: "Valid max size returns no error", + inputSize: maxSize, + expectedErr: false, + }, + { + name: "Valid page size returns no error (assuming page size is power of 2)", + inputSize: pageSize, + expectedErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateRingBufferSize(tt.inputSize) + if tt.expectedErr { + require.Error(t, err) + if tt.expectedMsg != "" { + assert.Contains(t, err.Error(), tt.expectedMsg) + } + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/plugin/packetparser/packetparser_ebpf_test.go b/pkg/plugin/packetparser/packetparser_ebpf_test.go new file mode 100644 index 0000000000..d7a6f056e1 --- /dev/null +++ b/pkg/plugin/packetparser/packetparser_ebpf_test.go @@ -0,0 +1,1301 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//go:build ebpf && linux + +package packetparser + +import ( + "context" + "fmt" + "net" + "os" + "os/exec" + "path" + "runtime" + "testing" + "time" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/perf" + "github.com/cilium/ebpf/ringbuf" + "github.com/gopacket/gopacket/layers" + "github.com/microsoft/retina/pkg/loader" + "github.com/microsoft/retina/pkg/log" + "github.com/microsoft/retina/pkg/plugin/ebpftest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + // Observation points from conntrack.h + observationPointFromEndpoint = 0x00 + observationPointToEndpoint = 0x01 + observationPointFromNetwork = 0x02 + observationPointToNetwork = 0x03 + + // TC_ACT_UNSPEC is -1 in C, which becomes 0xFFFFFFFF as uint32. + tcActUnspec = 0xFFFFFFFF + + // Protocol numbers + protoTCP = 6 + protoUDP = 17 + + // Traffic directions from conntrack.h + trafficDirectionUnknown = 0x00 + trafficDirectionIngress = 0x01 + trafficDirectionEgress = 0x02 + + // perfReaderTimeout is how long to wait for a perf event. + perfReaderTimeout = 500 * time.Millisecond +) + +// loadTestObjects loads the packetparser eBPF programs and maps for testing. +func loadTestObjects(t *testing.T) (*packetparserObjects, *perf.Reader) { + t.Helper() + ebpftest.RequirePrivileged(t) + + spec, err := loadPacketparser() + require.NoError(t, err) + + ebpftest.RemoveMapPinning(spec) + + var objs packetparserObjects + err = spec.LoadAndAssign(&objs, nil) + require.NoError(t, err) + t.Cleanup(func() { objs.Close() }) + + reader, err := perf.NewReader(objs.RetinaPacketparserEvents, os.Getpagesize()*4) + require.NoError(t, err) + t.Cleanup(func() { reader.Close() }) + + return &objs, reader +} + +func TestEndpointIngressFilter_TCP(t *testing.T) { + objs, reader := loadTestObjects(t) + + srcIP := net.ParseIP("10.0.0.1") + dstIP := net.ParseIP("10.0.0.2") + ebpftest.PopulateFilterMap(t, objs.RetinaFilter, srcIP, dstIP) + + pkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, + DstIP: dstIP, + SrcPort: 12345, + DstPort: 80, + SYN: true, + SeqNum: 1000, + }) + + ret := ebpftest.RunProgram(t, objs.EndpointIngressFilter, pkt) + assert.Equal(t, uint32(tcActUnspec), ret, "expected TC_ACT_UNSPEC return value") + + event, ok := ebpftest.ReadPerfEvent[packetparserPacket](t, reader, perfReaderTimeout) + require.True(t, ok, "expected a perf event") + + assert.Equal(t, ebpftest.IPToNative("10.0.0.1"), event.SrcIp) + assert.Equal(t, ebpftest.IPToNative("10.0.0.2"), event.DstIp) + assert.Equal(t, ebpftest.PortToNetwork(12345), event.SrcPort) + assert.Equal(t, ebpftest.PortToNetwork(80), event.DstPort) + assert.Equal(t, uint8(protoTCP), event.Proto) + assert.Equal(t, uint8(observationPointFromEndpoint), event.ObservationPoint) + assert.NotZero(t, event.T_nsec, "timestamp should be set") + assert.Equal(t, uint32(len(pkt)), event.Bytes) + + // SYN flag should be set (bit 1) + assert.NotZero(t, event.Flags&0x02, "SYN flag should be set") +} + +func TestAllObservationPoints(t *testing.T) { + objs, reader := loadTestObjects(t) + + srcIP := net.ParseIP("10.0.1.1") + dstIP := net.ParseIP("10.0.1.2") + ebpftest.PopulateFilterMap(t, objs.RetinaFilter, srcIP, dstIP) + + tests := []struct { + name string + prog *ebpf.Program + expected uint8 + }{ + {"endpoint_ingress", objs.EndpointIngressFilter, observationPointFromEndpoint}, + {"endpoint_egress", objs.EndpointEgressFilter, observationPointToEndpoint}, + {"host_ingress", objs.HostIngressFilter, observationPointFromNetwork}, + {"host_egress", objs.HostEgressFilter, observationPointToNetwork}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + pkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, + DstIP: dstIP, + SrcPort: 1111, + DstPort: 2222, + SYN: true, + }) + + ebpftest.RunProgram(t, tc.prog, pkt) + + event, ok := ebpftest.ReadPerfEvent[packetparserPacket](t, reader, perfReaderTimeout) + require.True(t, ok, "expected a perf event") + assert.Equal(t, tc.expected, event.ObservationPoint) + }) + } +} + +func TestTCPFlags(t *testing.T) { + objs, reader := loadTestObjects(t) + + srcIP := net.ParseIP("10.0.2.1") + dstIP := net.ParseIP("10.0.2.2") + ebpftest.PopulateFilterMap(t, objs.RetinaFilter, srcIP, dstIP) + + // TCP flags bitmask as computed by the eBPF program: + // fin=bit0, syn=bit1, rst=bit2, psh=bit3, ack=bit4, urg=bit5, ece=bit6, cwr=bit7 + tests := []struct { + name string + opts ebpftest.TCPPacketOpts + expected uint16 + }{ + { + name: "SYN", + opts: ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 1000, DstPort: 80, + SYN: true, + }, + expected: 0x02, // syn=bit1 + }, + { + name: "ACK", + opts: ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 1001, DstPort: 80, + ACK: true, + }, + expected: 0x10, // ack=bit4 + }, + { + name: "SYN+ACK", + opts: ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 1002, DstPort: 80, + SYN: true, ACK: true, + }, + expected: 0x12, // syn=bit1 | ack=bit4 + }, + { + name: "FIN", + opts: ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 1003, DstPort: 80, + FIN: true, ACK: true, + }, + expected: 0x11, // fin=bit0 | ack=bit4 + }, + { + name: "RST", + opts: ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 1004, DstPort: 80, + RST: true, + }, + expected: 0x04, // rst=bit2 + }, + { + name: "PSH+ACK", + opts: ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 1005, DstPort: 80, + PSH: true, ACK: true, + }, + expected: 0x18, // psh=bit3 | ack=bit4 + }, + { + name: "URG", + opts: ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 1006, DstPort: 80, + URG: true, + }, + expected: 0x20, // urg=bit5 + }, + { + name: "ECE", + opts: ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 1007, DstPort: 80, + ECE: true, + }, + expected: 0x40, // ece=bit6 + }, + { + name: "CWR", + opts: ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 1008, DstPort: 80, + CWR: true, + }, + expected: 0x80, // cwr=bit7 + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + pkt := ebpftest.BuildTCPPacket(tc.opts) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, pkt) + + event, ok := ebpftest.ReadPerfEvent[packetparserPacket](t, reader, perfReaderTimeout) + require.True(t, ok, "expected a perf event") + assert.Equal(t, tc.expected, event.Flags, "TCP flags mismatch") + }) + } +} + +func TestTCPTimestamps(t *testing.T) { + objs, reader := loadTestObjects(t) + + srcIP := net.ParseIP("10.0.3.1") + dstIP := net.ParseIP("10.0.3.2") + ebpftest.PopulateFilterMap(t, objs.RetinaFilter, srcIP, dstIP) + + t.Run("with_timestamps", func(t *testing.T) { + pkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, + DstIP: dstIP, + SrcPort: 5000, + DstPort: 443, + SYN: true, + TSval: 12345678, + TSecr: 87654321, + }) + + ebpftest.RunProgram(t, objs.EndpointIngressFilter, pkt) + + event, ok := ebpftest.ReadPerfEvent[packetparserPacket](t, reader, perfReaderTimeout) + require.True(t, ok, "expected a perf event") + assert.Equal(t, uint32(12345678), event.TcpMetadata.Tsval) + assert.Equal(t, uint32(87654321), event.TcpMetadata.Tsecr) + }) + + t.Run("without_timestamps", func(t *testing.T) { + pkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, + DstIP: dstIP, + SrcPort: 5001, + DstPort: 443, + SYN: true, + }) + + ebpftest.RunProgram(t, objs.EndpointIngressFilter, pkt) + + event, ok := ebpftest.ReadPerfEvent[packetparserPacket](t, reader, perfReaderTimeout) + require.True(t, ok, "expected a perf event") + assert.Zero(t, event.TcpMetadata.Tsval, "TSval should be zero when no timestamp option") + assert.Zero(t, event.TcpMetadata.Tsecr, "TSecr should be zero when no timestamp option") + }) +} + +func TestUDPPacket(t *testing.T) { + objs, reader := loadTestObjects(t) + + srcIP := net.ParseIP("10.0.4.1") + dstIP := net.ParseIP("10.0.4.2") + ebpftest.PopulateFilterMap(t, objs.RetinaFilter, srcIP, dstIP) + + pkt := ebpftest.BuildUDPPacket(ebpftest.UDPPacketOpts{ + SrcIP: srcIP, + DstIP: dstIP, + SrcPort: 53000, + DstPort: 53, + }) + + ret := ebpftest.RunProgram(t, objs.EndpointIngressFilter, pkt) + assert.Equal(t, uint32(tcActUnspec), ret) + + event, ok := ebpftest.ReadPerfEvent[packetparserPacket](t, reader, perfReaderTimeout) + require.True(t, ok, "expected a perf event") + + assert.Equal(t, ebpftest.IPToNative("10.0.4.1"), event.SrcIp) + assert.Equal(t, ebpftest.IPToNative("10.0.4.2"), event.DstIp) + assert.Equal(t, ebpftest.PortToNetwork(53000), event.SrcPort) + assert.Equal(t, ebpftest.PortToNetwork(53), event.DstPort) + assert.Equal(t, uint8(protoUDP), event.Proto) + // UDP packets have flags=1 in the eBPF program. + assert.Equal(t, uint16(1), event.Flags) +} + +func TestFilterMapFiltering(t *testing.T) { + objs, reader := loadTestObjects(t) + + srcIP := net.ParseIP("10.0.5.1") + dstIP := net.ParseIP("10.0.5.2") + + // Don't populate the filter map — neither IP should match. + pkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, + DstIP: dstIP, + SrcPort: 6000, + DstPort: 80, + SYN: true, + }) + + t.Run("no_match_no_event", func(t *testing.T) { + ebpftest.RunProgram(t, objs.EndpointIngressFilter, pkt) + ebpftest.AssertNoPerfEvent(t, reader, perfReaderTimeout) + }) + + t.Run("match_after_adding_ip", func(t *testing.T) { + ebpftest.PopulateFilterMap(t, objs.RetinaFilter, srcIP) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, pkt) + + event, ok := ebpftest.ReadPerfEvent[packetparserPacket](t, reader, perfReaderTimeout) + require.True(t, ok, "expected a perf event after adding IP to filter") + assert.Equal(t, ebpftest.IPToNative("10.0.5.1"), event.SrcIp) + }) +} + +func TestNonTCPUDP_NoEvent(t *testing.T) { + objs, reader := loadTestObjects(t) + + srcIP := net.ParseIP("10.0.6.1") + dstIP := net.ParseIP("10.0.6.2") + ebpftest.PopulateFilterMap(t, objs.RetinaFilter, srcIP, dstIP) + + // ICMP is protocol 1 — the eBPF program only handles TCP and UDP. + pkt := ebpftest.BuildICMPPacket(srcIP, dstIP) + + ret := ebpftest.RunProgram(t, objs.EndpointIngressFilter, pkt) + assert.Equal(t, uint32(tcActUnspec), ret, "should still return TC_ACT_UNSPEC") + + ebpftest.AssertNoPerfEvent(t, reader, perfReaderTimeout) +} + +func TestNonIPv4_NoEvent(t *testing.T) { + objs, reader := loadTestObjects(t) + + tests := []struct { + name string + etherType layers.EthernetType + }{ + {"ARP", layers.EthernetTypeARP}, + {"IPv6", layers.EthernetTypeIPv6}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + pkt := ebpftest.BuildNonIPPacket(tc.etherType) + + ret := ebpftest.RunProgram(t, objs.EndpointIngressFilter, pkt) + assert.Equal(t, uint32(tcActUnspec), ret) + + ebpftest.AssertNoPerfEvent(t, reader, perfReaderTimeout) + }) + } +} + +func TestMalformedPackets(t *testing.T) { + objs, reader := loadTestObjects(t) + + srcIP := net.ParseIP("10.0.7.1") + dstIP := net.ParseIP("10.0.7.2") + ebpftest.PopulateFilterMap(t, objs.RetinaFilter, srcIP, dstIP) + + t.Run("runt_packet", func(t *testing.T) { + // BPF_PROG_TEST_RUN requires at least ETH_HLEN (14) bytes for TC programs. + // The kernel rejects shorter inputs with EINVAL. + pkt := ebpftest.BuildRuntPacket() + _, err := objs.EndpointIngressFilter.Run(&ebpf.RunOptions{Data: pkt}) + assert.Error(t, err, "kernel should reject packets shorter than ETH_HLEN") + }) + + t.Run("truncated_ip", func(t *testing.T) { + pkt := ebpftest.BuildTruncatedIPPacket() + ret := ebpftest.RunProgram(t, objs.EndpointIngressFilter, pkt) + assert.Equal(t, uint32(tcActUnspec), ret) + ebpftest.AssertNoPerfEvent(t, reader, perfReaderTimeout) + }) + + t.Run("truncated_tcp", func(t *testing.T) { + // Note: BPF_PROG_TEST_RUN may pad sk_buff to minimum frame size, + // which means the eBPF bounds check can pass even for truncated packets. + // This test verifies the program doesn't crash on short TCP headers. + pkt := ebpftest.BuildTruncatedTCPPacket(srcIP, dstIP) + ret := ebpftest.RunProgram(t, objs.EndpointIngressFilter, pkt) + assert.Equal(t, uint32(tcActUnspec), ret) + + // Drain any event that may have been emitted due to kernel padding. + reader.SetDeadline(time.Now().Add(100 * time.Millisecond)) + reader.Read() //nolint:errcheck + }) +} + +func TestReturnValue(t *testing.T) { + objs, reader := loadTestObjects(t) + + srcIP := net.ParseIP("10.0.8.1") + dstIP := net.ParseIP("10.0.8.2") + ebpftest.PopulateFilterMap(t, objs.RetinaFilter, srcIP, dstIP) + + programs := []*ebpf.Program{ + objs.EndpointIngressFilter, + objs.EndpointEgressFilter, + objs.HostIngressFilter, + objs.HostEgressFilter, + } + + // Test with various packet types (all >= ETH_HLEN to satisfy kernel). + packets := [][]byte{ + ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 8000, DstPort: 80, SYN: true, + }), + ebpftest.BuildUDPPacket(ebpftest.UDPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 8001, DstPort: 53, + }), + ebpftest.BuildNonIPPacket(layers.EthernetTypeARP), + ebpftest.BuildTruncatedIPPacket(), + } + + for _, prog := range programs { + for _, pkt := range packets { + ret := ebpftest.RunProgram(t, prog, pkt) + assert.Equal(t, uint32(tcActUnspec), ret) + + // Drain any perf event. + reader.SetDeadline(time.Now().Add(50 * time.Millisecond)) + reader.Read() //nolint:errcheck + } + } +} + +func TestPacketBytesField(t *testing.T) { + objs, reader := loadTestObjects(t) + + srcIP := net.ParseIP("10.0.9.1") + dstIP := net.ParseIP("10.0.9.2") + ebpftest.PopulateFilterMap(t, objs.RetinaFilter, srcIP, dstIP) + + payloadSizes := []int{0, 100, 1000} + + for _, payloadSize := range payloadSizes { + pkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, + DstIP: dstIP, + SrcPort: 9000, + DstPort: 80, + SYN: true, + PayloadSize: payloadSize, + }) + + ebpftest.RunProgram(t, objs.EndpointIngressFilter, pkt) + + event, ok := ebpftest.ReadPerfEvent[packetparserPacket](t, reader, perfReaderTimeout) + require.True(t, ok, "expected a perf event") + assert.Equal(t, uint32(len(pkt)), event.Bytes, "Bytes field should equal packet length (payload size: %d)", payloadSize) + } +} + +func TestConntrackMapUpdated(t *testing.T) { + objs, reader := loadTestObjects(t) + + srcIP := net.ParseIP("10.0.10.1") + dstIP := net.ParseIP("10.0.10.2") + ebpftest.PopulateFilterMap(t, objs.RetinaFilter, srcIP, dstIP) + + pkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, + DstIP: dstIP, + SrcPort: 10000, + DstPort: 80, + SYN: true, + }) + + // First run — should create a new conntrack entry. + ebpftest.RunProgram(t, objs.EndpointIngressFilter, pkt) + _, ok := ebpftest.ReadPerfEvent[packetparserPacket](t, reader, perfReaderTimeout) + require.True(t, ok, "expected a perf event on first run") + + // Check that the conntrack map now has an entry for this flow. + lookupConntrackEntry(t, objs.RetinaConntrack, + "10.0.10.1", "10.0.10.2", 10000, 80, protoTCP) +} + +func TestConntrackIsReply(t *testing.T) { + objs, reader := loadTestObjects(t) + + srcIP := net.ParseIP("10.0.11.1") + dstIP := net.ParseIP("10.0.11.2") + ebpftest.PopulateFilterMap(t, objs.RetinaFilter, srcIP, dstIP) + + // Send SYN: 10.0.11.1:20000 → 10.0.11.2:80 (new connection, forward direction). + synPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 20000, DstPort: 80, SYN: true, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, synPkt) + event, ok := ebpftest.ReadPerfEvent[packetparserPacket](t, reader, perfReaderTimeout) + require.True(t, ok, "expected perf event for SYN") + assert.False(t, event.IsReply, "SYN packet should not be a reply") + + // Send SYN-ACK: 10.0.11.2:80 → 10.0.11.1:20000 (reply direction — reversed 5-tuple). + synAckPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: dstIP, DstIP: srcIP, SrcPort: 80, DstPort: 20000, SYN: true, ACK: true, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, synAckPkt) + event, ok = ebpftest.ReadPerfEvent[packetparserPacket](t, reader, perfReaderTimeout) + require.True(t, ok, "expected perf event for SYN-ACK") + assert.True(t, event.IsReply, "SYN-ACK with reversed 5-tuple should be a reply") + + // Send another packet in forward direction: 10.0.11.1:20000 → 10.0.11.2:80. + ackPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 20000, DstPort: 80, ACK: true, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, ackPkt) + event, ok = ebpftest.ReadPerfEvent[packetparserPacket](t, reader, perfReaderTimeout) + require.True(t, ok, "expected perf event for ACK") + assert.False(t, event.IsReply, "forward direction ACK should not be a reply") +} + +func TestConntrackTrafficDirection(t *testing.T) { + objs, reader := loadTestObjects(t) + + srcIP := net.ParseIP("10.0.12.1") + dstIP := net.ParseIP("10.0.12.2") + ebpftest.PopulateFilterMap(t, objs.RetinaFilter, srcIP, dstIP) + + // For a NEW connection, traffic_direction is derived from the observation point: + // FROM_ENDPOINT (0x00) or TO_NETWORK (0x03) → EGRESS (0x02) + // TO_ENDPOINT (0x01) or FROM_NETWORK (0x02) → INGRESS (0x01) + tests := []struct { + name string + prog *ebpf.Program + srcPort uint16 + expectedDirection uint8 + }{ + {"endpoint_ingress→egress", objs.EndpointIngressFilter, 30000, trafficDirectionEgress}, + {"endpoint_egress→ingress", objs.EndpointEgressFilter, 30001, trafficDirectionIngress}, + {"host_ingress→ingress", objs.HostIngressFilter, 30002, trafficDirectionIngress}, + {"host_egress→egress", objs.HostEgressFilter, 30003, trafficDirectionEgress}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + pkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: tc.srcPort, DstPort: 80, SYN: true, + }) + ebpftest.RunProgram(t, tc.prog, pkt) + + event, ok := ebpftest.ReadPerfEvent[packetparserPacket](t, reader, perfReaderTimeout) + require.True(t, ok, "expected a perf event") + assert.Equal(t, tc.expectedDirection, event.TrafficDirection, + "unexpected traffic direction for observation point") + }) + } +} + +// lookupConntrackEntry looks up a conntrack entry by 5-tuple and returns it. +func lookupConntrackEntry(t *testing.T, ctMap *ebpf.Map, srcIP, dstIP string, srcPort, dstPort uint16, proto uint8) packetparserCtEntry { + t.Helper() + ctKey := packetparserCtV4Key{ + SrcIp: ebpftest.IPToNative(srcIP), + DstIp: ebpftest.IPToNative(dstIP), + SrcPort: ebpftest.PortToNetwork(srcPort), + DstPort: ebpftest.PortToNetwork(dstPort), + Proto: proto, + } + var entry packetparserCtEntry + err := ctMap.Lookup(ctKey, &entry) + require.NoError(t, err, "conntrack entry not found for %s:%d → %s:%d proto=%d", + srcIP, srcPort, dstIP, dstPort, proto) + return entry +} + +// drainPerfEvent reads and discards one perf event if available. +func drainPerfEvent(reader *perf.Reader, timeout time.Duration) { + reader.SetDeadline(time.Now().Add(timeout)) + reader.Read() //nolint:errcheck +} + +// compileOpts controls the dynamic.h flags for a custom-compiled eBPF variant. +type compileOpts struct { + bypassFilter int + enableConntrack bool + aggregationLevel int + samplingRate int + enableRingBuf bool +} + +// compileAndLoadVariantBase compiles the packetparser eBPF program with custom +// dynamic.h settings and returns loaded objects + perf/ringbuf reader. +// Requires clang to be installed. +func compileAndLoadVariantBase(t *testing.T, opts compileOpts) (*packetparserObjects, *perf.Reader, *ringbuf.Reader) { + t.Helper() + ebpftest.RequirePrivileged(t) + + if _, err := exec.LookPath("clang"); err != nil { + t.Skip("skipping: clang not available for eBPF compilation") + } + + // Get source directory (uses runtime.Caller to find the file path). + _, filename, _, ok := runtime.Caller(0) + require.True(t, ok, "failed to get current file path") + dir := path.Dir(filename) + + // Write custom packetparser dynamic.h, restore on cleanup. + ppDynamic := fmt.Sprintf("%s/%s/%s", dir, bpfSourceDir, dynamicHeaderFileName) + origPP, err := os.ReadFile(ppDynamic) + require.NoError(t, err) + t.Cleanup(func() { os.WriteFile(ppDynamic, origPP, 0o644) }) //nolint:errcheck + + var st string + st += fmt.Sprintf("#define BYPASS_LOOKUP_IP_OF_INTEREST %d\n", opts.bypassFilter) + if opts.enableConntrack { + st += "#define ENABLE_CONNTRACK_METRICS 1\n" + } + st += fmt.Sprintf("#define DATA_AGGREGATION_LEVEL %d\n", opts.aggregationLevel) + st += fmt.Sprintf("#define DATA_SAMPLING_RATE %d\n", opts.samplingRate) + require.NoError(t, os.WriteFile(ppDynamic, []byte(st), 0o644)) + + // Write conntrack dynamic.h if conntrack metrics enabled. + ctDynamic := fmt.Sprintf("%s/../conntrack/%s/%s", dir, bpfSourceDir, dynamicHeaderFileName) + origCT, err := os.ReadFile(ctDynamic) + require.NoError(t, err) + t.Cleanup(func() { os.WriteFile(ctDynamic, origCT, 0o644) }) //nolint:errcheck + + if opts.enableConntrack { + require.NoError(t, os.WriteFile(ctDynamic, []byte("#define ENABLE_CONNTRACK_METRICS 1\n"), 0o644)) + } + + // Compile the eBPF program. + bpfSourceFile := fmt.Sprintf("%s/%s/%s", dir, bpfSourceDir, bpfSourceFileName) + outputFile := fmt.Sprintf("%s/packetparser_test.o", t.TempDir()) + + arch := runtime.GOARCH + targetArch := "-D__TARGET_ARCH_x86" + if arch == "arm64" { + targetArch = "-D__TARGET_ARCH_arm64" + } + + log.SetupZapLogger(log.GetDefaultLogOpts()) //nolint:errcheck + + cflags := []string{ + "-target", "bpf", "-Wall", targetArch, "-g", "-O2", + "-c", bpfSourceFile, "-o", outputFile, + fmt.Sprintf("-I%s/../lib/_%s", dir, arch), + fmt.Sprintf("-I%s/../lib/common/libbpf/_src", dir), + fmt.Sprintf("-I%s/../lib/common/libbpf/_include/linux", dir), + fmt.Sprintf("-I%s/../lib/common/libbpf/_include/uapi/linux", dir), + fmt.Sprintf("-I%s/../lib/common/libbpf/_include/asm", dir), + fmt.Sprintf("-I%s/../filter/_cprog/", dir), + fmt.Sprintf("-I%s/../conntrack/_cprog/", dir), + } + if opts.enableRingBuf { + cflags = append(cflags, "-DUSE_RING_BUFFER", "-DRING_BUFFER_SIZE=4096") + } + + err = loader.CompileEbpf(context.Background(), cflags...) + require.NoError(t, err, "failed to compile eBPF program") + + // Load the compiled object. + spec, err := ebpf.LoadCollectionSpec(outputFile) + require.NoError(t, err) + ebpftest.RemoveMapPinning(spec) + + var objs packetparserObjects + err = spec.LoadAndAssign(&objs, nil) + require.NoError(t, err) + t.Cleanup(func() { objs.Close() }) + + var pReader *perf.Reader + var rReader *ringbuf.Reader + if opts.enableRingBuf { + rReader, err = ringbuf.NewReader(objs.RetinaPacketparserEvents) + require.NoError(t, err) + t.Cleanup(func() { rReader.Close() }) + } else { + pReader, err = perf.NewReader(objs.RetinaPacketparserEvents, os.Getpagesize()*4) + require.NoError(t, err) + t.Cleanup(func() { pReader.Close() }) + } + + return &objs, pReader, rReader +} + +func compileAndLoadVariant(t *testing.T, opts compileOpts) (*packetparserObjects, *perf.Reader) { + objs, pReader, _ := compileAndLoadVariantBase(t, opts) + return objs, pReader +} + +func compileAndLoadRingBufVariant(t *testing.T, opts compileOpts) (*packetparserObjects, *ringbuf.Reader) { + opts.enableRingBuf = true + objs, _, rReader := compileAndLoadVariantBase(t, opts) + return objs, rReader +} + +func TestConntrackMetricsEnabled(t *testing.T) { + objs, reader := compileAndLoadVariant(t, compileOpts{ + bypassFilter: 1, // skip filter for simplicity + enableConntrack: true, + aggregationLevel: 0, // LOW — always emit + samplingRate: 1, + }) + + srcIP := net.ParseIP("10.0.13.1") + dstIP := net.ParseIP("10.0.13.2") + + // Send SYN (creates conntrack entry). + synPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 40000, DstPort: 80, SYN: true, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, synPkt) + _, ok := ebpftest.ReadPerfEvent[packetparserPacket](t, reader, perfReaderTimeout) + require.True(t, ok) + + // Send a second packet in the same direction (existing forward connection). + ackPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 40000, DstPort: 80, ACK: true, + PayloadSize: 100, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, ackPkt) + event, ok := ebpftest.ReadPerfEvent[packetparserPacket](t, reader, perfReaderTimeout) + require.True(t, ok) + + // With ENABLE_CONNTRACK_METRICS, the conntrack_metadata should be populated. + // After 2 packets in TX direction, packets_tx_count should be >= 1. + assert.True(t, event.ConntrackMetadata.PacketsTxCount >= 1, + "expected packets_tx_count >= 1, got %d", event.ConntrackMetadata.PacketsTxCount) + assert.True(t, event.ConntrackMetadata.BytesTxCount > 0, + "expected bytes_tx_count > 0, got %d", event.ConntrackMetadata.BytesTxCount) + + // Send a reply packet (reverse direction). + replyPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: dstIP, DstIP: srcIP, SrcPort: 80, DstPort: 40000, ACK: true, + PayloadSize: 200, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, replyPkt) + event, ok = ebpftest.ReadPerfEvent[packetparserPacket](t, reader, perfReaderTimeout) + require.True(t, ok) + + assert.True(t, event.IsReply, "reverse packet should be is_reply") + assert.True(t, event.ConntrackMetadata.PacketsRxCount >= 1, + "expected packets_rx_count >= 1, got %d", event.ConntrackMetadata.PacketsRxCount) + assert.True(t, event.ConntrackMetadata.BytesRxCount > 0, + "expected bytes_rx_count > 0, got %d", event.ConntrackMetadata.BytesRxCount) +} + +func TestHighAggregationLevel(t *testing.T) { + objs, reader := compileAndLoadVariant(t, compileOpts{ + bypassFilter: 1, + enableConntrack: false, + aggregationLevel: 1, // HIGH — only emit when report.report is true + samplingRate: 1, // sample everything + }) + + srcIP := net.ParseIP("10.0.14.1") + dstIP := net.ParseIP("10.0.14.2") + + // First SYN packet should be reported (new connection). + synPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 50000, DstPort: 80, SYN: true, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, synPkt) + event, ok := ebpftest.ReadPerfEvent[packetparserPacket](t, reader, perfReaderTimeout) + require.True(t, ok, "first SYN should be reported at HIGH aggregation") + assert.Equal(t, uint8(protoTCP), event.Proto) + + // Send several identical ACK packets. At HIGH aggregation, the conntrack + // logic suppresses repeated reports until new flags appear or a timeout. + ackPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 50000, DstPort: 80, ACK: true, + }) + emittedCount := 0 + for i := 0; i < 5; i++ { + ebpftest.RunProgram(t, objs.EndpointIngressFilter, ackPkt) + _, ok := ebpftest.ReadPerfEvent[packetparserPacket](t, reader, 100*time.Millisecond) + if ok { + emittedCount++ + } + } + // At HIGH aggregation, not all 5 identical ACK packets should be reported. + // The first ACK introduces a new flag (ACK vs SYN), so it should be reported. + // Subsequent identical ACKs may be suppressed. + t.Logf("HIGH aggregation: %d/5 ACK packets emitted events", emittedCount) + assert.True(t, emittedCount < 5, + "HIGH aggregation should suppress some repeated packets, but all %d were emitted", emittedCount) +} + +func TestHighAggregationPreviouslyObserved(t *testing.T) { + objs, reader := compileAndLoadVariant(t, compileOpts{ + bypassFilter: 1, + enableConntrack: false, + aggregationLevel: 1, // HIGH + samplingRate: 1, + }) + + srcIP := net.ParseIP("10.0.15.1") + dstIP := net.ParseIP("10.0.15.2") + + // Send SYN to create the connection. + synPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 60000, DstPort: 80, SYN: true, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, synPkt) + _, ok := ebpftest.ReadPerfEvent[packetparserPacket](t, reader, perfReaderTimeout) + require.True(t, ok) + + // Send multiple packets to accumulate stats, then send a packet with a new + // flag to trigger a report that includes previously_observed_* fields. + ackPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 60000, DstPort: 80, ACK: true, + PayloadSize: 50, + }) + for i := 0; i < 3; i++ { + ebpftest.RunProgram(t, objs.EndpointIngressFilter, ackPkt) + // Drain any events. + reader.SetDeadline(time.Now().Add(50 * time.Millisecond)) + reader.Read() //nolint:errcheck + } + + // Send FIN to introduce a new flag — this should trigger a report. + finPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 60000, DstPort: 80, FIN: true, ACK: true, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, finPkt) + event, ok := ebpftest.ReadPerfEvent[packetparserPacket](t, reader, perfReaderTimeout) + require.True(t, ok, "FIN should trigger a report due to new flag") + + // At HIGH aggregation, the report should include accumulated stats. + // previously_observed_packets counts packets seen since last report. + t.Logf("previously_observed_packets=%d, previously_observed_bytes=%d", + event.PreviouslyObservedPackets, event.PreviouslyObservedBytes) + assert.True(t, event.PreviouslyObservedPackets > 0, + "expected previously_observed_packets > 0 at HIGH aggregation") +} + +// ============================================================================= +// Conntrack map-state verification tests +// ============================================================================= + +func TestConntrackEntryCreation(t *testing.T) { + objs, reader := loadTestObjects(t) + + srcIP := net.ParseIP("10.0.20.1") + dstIP := net.ParseIP("10.0.20.2") + ebpftest.PopulateFilterMap(t, objs.RetinaFilter, srcIP, dstIP) + + pkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 20000, DstPort: 80, SYN: true, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, pkt) + _, ok := ebpftest.ReadPerfEvent[packetparserPacket](t, reader, perfReaderTimeout) + require.True(t, ok) + + entry := lookupConntrackEntry(t, objs.RetinaConntrack, + "10.0.20.1", "10.0.20.2", 20000, 80, protoTCP) + + // FlagsSeenTxDir: SYN = 0x02. + assert.Equal(t, uint8(0x02), entry.FlagsSeenTxDir, "should have SYN in TX flags") + // No reply yet. + assert.Equal(t, uint8(0x00), entry.FlagsSeenRxDir, "RX flags should be empty") + // Direction is known because we saw SYN. + assert.False(t, entry.IsDirectionUnknown, "direction should be known for SYN") + // EndpointIngressFilter = FROM_ENDPOINT → EGRESS. + assert.Equal(t, uint8(trafficDirectionEgress), entry.TrafficDirection) + // Eviction time must be set. + assert.NotZero(t, entry.EvictionTime, "eviction time should be non-zero") + // Sampled + reported → "since last report" counters are 0. + assert.Equal(t, uint32(0), entry.PacketsSeenSinceLastReportTxDir) + assert.Equal(t, uint32(0), entry.BytesSeenSinceLastReportTxDir) +} + +func TestConntrackFlagAccumulation(t *testing.T) { + objs, reader := loadTestObjects(t) + + srcIP := net.ParseIP("10.0.21.1") + dstIP := net.ParseIP("10.0.21.2") + ebpftest.PopulateFilterMap(t, objs.RetinaFilter, srcIP, dstIP) + + // Step 1: SYN → flags_seen_tx_dir = SYN (0x02). + synPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 21000, DstPort: 80, SYN: true, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, synPkt) + drainPerfEvent(reader, perfReaderTimeout) + + entry := lookupConntrackEntry(t, objs.RetinaConntrack, + "10.0.21.1", "10.0.21.2", 21000, 80, protoTCP) + assert.Equal(t, uint8(0x02), entry.FlagsSeenTxDir, "after SYN") + + // Step 2: ACK → flags_seen_tx_dir = SYN|ACK (0x12). + ackPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 21000, DstPort: 80, ACK: true, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, ackPkt) + drainPerfEvent(reader, perfReaderTimeout) + + entry = lookupConntrackEntry(t, objs.RetinaConntrack, + "10.0.21.1", "10.0.21.2", 21000, 80, protoTCP) + assert.Equal(t, uint8(0x12), entry.FlagsSeenTxDir, "after ACK: should be SYN|ACK") + + // Step 3: PSH+ACK → flags_seen_tx_dir = SYN|PSH|ACK (0x1A). + pshPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 21000, DstPort: 80, PSH: true, ACK: true, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, pshPkt) + drainPerfEvent(reader, perfReaderTimeout) + + entry = lookupConntrackEntry(t, objs.RetinaConntrack, + "10.0.21.1", "10.0.21.2", 21000, 80, protoTCP) + assert.Equal(t, uint8(0x1A), entry.FlagsSeenTxDir, "after PSH+ACK: should be SYN|PSH|ACK") +} + +func TestConntrackReplyUpdatesEntry(t *testing.T) { + objs, reader := loadTestObjects(t) + + srcIP := net.ParseIP("10.0.22.1") + dstIP := net.ParseIP("10.0.22.2") + ebpftest.PopulateFilterMap(t, objs.RetinaFilter, srcIP, dstIP) + + // Forward SYN: 10.0.22.1:22000 → 10.0.22.2:80. + synPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 22000, DstPort: 80, SYN: true, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, synPkt) + drainPerfEvent(reader, perfReaderTimeout) + + entry := lookupConntrackEntry(t, objs.RetinaConntrack, + "10.0.22.1", "10.0.22.2", 22000, 80, protoTCP) + assert.Equal(t, uint8(0x02), entry.FlagsSeenTxDir, "initial TX: SYN") + assert.Equal(t, uint8(0x00), entry.FlagsSeenRxDir, "initial RX: empty") + + // Reply SYN-ACK: 10.0.22.2:80 → 10.0.22.1:22000 (reverse 5-tuple). + // Conntrack finds the entry via reverse key lookup and updates RX fields. + synAckPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: dstIP, DstIP: srcIP, SrcPort: 80, DstPort: 22000, SYN: true, ACK: true, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, synAckPkt) + drainPerfEvent(reader, perfReaderTimeout) + + // Entry is still under the forward (initiator's) key. + entry = lookupConntrackEntry(t, objs.RetinaConntrack, + "10.0.22.1", "10.0.22.2", 22000, 80, protoTCP) + // TX unchanged. + assert.Equal(t, uint8(0x02), entry.FlagsSeenTxDir, "TX should still be SYN") + // RX updated with SYN|ACK. + assert.Equal(t, uint8(0x12), entry.FlagsSeenRxDir, "RX should be SYN|ACK after reply") + + // Forward ACK: 10.0.22.1:22000 → 10.0.22.2:80. + ackPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 22000, DstPort: 80, ACK: true, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, ackPkt) + drainPerfEvent(reader, perfReaderTimeout) + + entry = lookupConntrackEntry(t, objs.RetinaConntrack, + "10.0.22.1", "10.0.22.2", 22000, 80, protoTCP) + // TX accumulates ACK. + assert.Equal(t, uint8(0x12), entry.FlagsSeenTxDir, "TX should be SYN|ACK after handshake") + // RX unchanged. + assert.Equal(t, uint8(0x12), entry.FlagsSeenRxDir, "RX still SYN|ACK") +} + +func TestConntrackDirectionUnknown(t *testing.T) { + objs, reader := loadTestObjects(t) + + srcIP := net.ParseIP("10.0.23.1") + dstIP := net.ParseIP("10.0.23.2") + ebpftest.PopulateFilterMap(t, objs.RetinaFilter, srcIP, dstIP) + + // Send PSH+ACK as the first packet (simulates missed SYN — ongoing connection). + pkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 23000, DstPort: 80, PSH: true, ACK: true, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, pkt) + drainPerfEvent(reader, perfReaderTimeout) + + // ACK flag is set → _ct_handle_tcp_connection treats as reply → stored under reverse key. + entry := lookupConntrackEntry(t, objs.RetinaConntrack, + "10.0.23.2", "10.0.23.1", 80, 23000, protoTCP) + + assert.True(t, entry.IsDirectionUnknown, + "IsDirectionUnknown should be true for non-SYN first packet") + // Flags stored in RX direction (treated as reply due to ACK). + assert.Equal(t, uint8(0x18), entry.FlagsSeenRxDir, "RX should have PSH|ACK (0x18)") + assert.Equal(t, uint8(0x00), entry.FlagsSeenTxDir, "TX should be empty") +} + +func TestConntrackSinceLastReportAccumulation(t *testing.T) { + // At HIGH aggregation, packets that don't introduce new flags are suppressed. + // Their bytes and packet counts accumulate in "since last report" counters. + objs, reader := compileAndLoadVariant(t, compileOpts{ + bypassFilter: 1, + enableConntrack: false, + aggregationLevel: 1, // HIGH + samplingRate: 1, + }) + + srcIP := net.ParseIP("10.0.24.1") + dstIP := net.ParseIP("10.0.24.2") + + // SYN creates the entry (reported — SYN is a should_report flag). + synPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 24000, DstPort: 80, SYN: true, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, synPkt) + drainPerfEvent(reader, perfReaderTimeout) + + // First ACK introduces new flag → reported → counters reset. + ackPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 24000, DstPort: 80, ACK: true, + PayloadSize: 100, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, ackPkt) + drainPerfEvent(reader, perfReaderTimeout) + + // Subsequent identical ACKs: no new flags, within 30s → NOT reported → counters accumulate. + for i := 0; i < 3; i++ { + ebpftest.RunProgram(t, objs.EndpointIngressFilter, ackPkt) + drainPerfEvent(reader, 100*time.Millisecond) + } + + entry := lookupConntrackEntry(t, objs.RetinaConntrack, + "10.0.24.1", "10.0.24.2", 24000, 80, protoTCP) + + // 3 non-reported ACK packets should have accumulated. + assert.True(t, entry.PacketsSeenSinceLastReportTxDir >= 3, + "expected PacketsSeenSinceLastReportTxDir >= 3, got %d", + entry.PacketsSeenSinceLastReportTxDir) + assert.True(t, entry.BytesSeenSinceLastReportTxDir > 0, + "expected BytesSeenSinceLastReportTxDir > 0, got %d", + entry.BytesSeenSinceLastReportTxDir) + + // FlagsSeenSinceLastReportTxDir should have ACK counts. + assert.True(t, entry.FlagsSeenSinceLastReportTxDir.Ack >= 3, + "expected ACK flag count >= 3 since last report, got %d", + entry.FlagsSeenSinceLastReportTxDir.Ack) +} + +func TestConntrackCounterResetOnReport(t *testing.T) { + // Verify that "since last report" counters reset to 0 when a report is emitted. + objs, reader := compileAndLoadVariant(t, compileOpts{ + bypassFilter: 1, + enableConntrack: false, + aggregationLevel: 1, // HIGH + samplingRate: 1, + }) + + srcIP := net.ParseIP("10.0.25.1") + dstIP := net.ParseIP("10.0.25.2") + + // SYN → reported. + synPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 25000, DstPort: 80, SYN: true, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, synPkt) + drainPerfEvent(reader, perfReaderTimeout) + + // First ACK → reported (new flag), counters reset. + ackPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 25000, DstPort: 80, ACK: true, + PayloadSize: 100, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, ackPkt) + drainPerfEvent(reader, perfReaderTimeout) + + // Send 3 more ACKs to accumulate counters. + for i := 0; i < 3; i++ { + ebpftest.RunProgram(t, objs.EndpointIngressFilter, ackPkt) + drainPerfEvent(reader, 100*time.Millisecond) + } + + // Verify counters accumulated. + entry := lookupConntrackEntry(t, objs.RetinaConntrack, + "10.0.25.1", "10.0.25.2", 25000, 80, protoTCP) + require.True(t, entry.PacketsSeenSinceLastReportTxDir >= 3, + "precondition: counters should be accumulated before FIN") + + // Send FIN+ACK → FIN is a should_report flag → report triggered → counters reset. + finPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 25000, DstPort: 80, FIN: true, ACK: true, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, finPkt) + drainPerfEvent(reader, perfReaderTimeout) + + entry = lookupConntrackEntry(t, objs.RetinaConntrack, + "10.0.25.1", "10.0.25.2", 25000, 80, protoTCP) + assert.Equal(t, uint32(0), entry.PacketsSeenSinceLastReportTxDir, + "counters should be 0 after FIN report") + assert.Equal(t, uint32(0), entry.BytesSeenSinceLastReportTxDir, + "byte counters should be 0 after FIN report") + assert.Equal(t, uint32(0), entry.FlagsSeenSinceLastReportTxDir.Ack, + "flag counts should be 0 after FIN report") +} + +func TestConntrackMetadataCountersInMap(t *testing.T) { + // With ENABLE_CONNTRACK_METRICS, the ConntrackMetadata lifetime counters + // are updated on every packet and never reset. + objs, reader := compileAndLoadVariant(t, compileOpts{ + bypassFilter: 1, + enableConntrack: true, + aggregationLevel: 0, // LOW — always emit + samplingRate: 1, + }) + + srcIP := net.ParseIP("10.0.26.1") + dstIP := net.ParseIP("10.0.26.2") + + // Forward SYN. + synPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 26000, DstPort: 80, SYN: true, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, synPkt) + drainPerfEvent(reader, perfReaderTimeout) + + entry := lookupConntrackEntry(t, objs.RetinaConntrack, + "10.0.26.1", "10.0.26.2", 26000, 80, protoTCP) + // _ct_create_new_tcp_connection sets packets_tx_count=1. + assert.Equal(t, uint32(1), entry.ConntrackMetadata.PacketsTxCount, "initial TX packets") + assert.True(t, entry.ConntrackMetadata.BytesTxCount > 0, "initial TX bytes > 0") + assert.Equal(t, uint32(0), entry.ConntrackMetadata.PacketsRxCount, "initial RX packets") + assert.Equal(t, uint64(0), entry.ConntrackMetadata.BytesRxCount, "initial RX bytes") + + // Send 2 more forward packets. + ackPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 26000, DstPort: 80, ACK: true, + PayloadSize: 200, + }) + for i := 0; i < 2; i++ { + ebpftest.RunProgram(t, objs.EndpointIngressFilter, ackPkt) + drainPerfEvent(reader, perfReaderTimeout) + } + + entry = lookupConntrackEntry(t, objs.RetinaConntrack, + "10.0.26.1", "10.0.26.2", 26000, 80, protoTCP) + // 1 SYN + 2 ACK = 3 TX packets. + assert.Equal(t, uint32(3), entry.ConntrackMetadata.PacketsTxCount, "3 TX packets total") + + // Send a reply packet. + replyPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: dstIP, DstIP: srcIP, SrcPort: 80, DstPort: 26000, ACK: true, + PayloadSize: 300, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, replyPkt) + drainPerfEvent(reader, perfReaderTimeout) + + entry = lookupConntrackEntry(t, objs.RetinaConntrack, + "10.0.26.1", "10.0.26.2", 26000, 80, protoTCP) + // TX unchanged. + assert.Equal(t, uint32(3), entry.ConntrackMetadata.PacketsTxCount, "TX still 3") + // RX incremented. + assert.Equal(t, uint32(1), entry.ConntrackMetadata.PacketsRxCount, "1 RX packet") + assert.True(t, entry.ConntrackMetadata.BytesRxCount > 0, "RX bytes > 0") +} + +func TestConntrackEvictionTimeExtended(t *testing.T) { + // Verify that EvictionTime is extended on subsequent packets. + objs, reader := loadTestObjects(t) + + srcIP := net.ParseIP("10.0.27.1") + dstIP := net.ParseIP("10.0.27.2") + ebpftest.PopulateFilterMap(t, objs.RetinaFilter, srcIP, dstIP) + + // SYN: eviction_time = now + CT_SYN_TIMEOUT (60s). + synPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 27000, DstPort: 80, SYN: true, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, synPkt) + drainPerfEvent(reader, perfReaderTimeout) + + entry := lookupConntrackEntry(t, objs.RetinaConntrack, + "10.0.27.1", "10.0.27.2", 27000, 80, protoTCP) + synEviction := entry.EvictionTime + assert.NotZero(t, synEviction) + + // ACK: eviction_time = now + CT_CONNECTION_LIFETIME_TCP (360s) — should be larger. + ackPkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, DstIP: dstIP, SrcPort: 27000, DstPort: 80, ACK: true, + }) + ebpftest.RunProgram(t, objs.EndpointIngressFilter, ackPkt) + drainPerfEvent(reader, perfReaderTimeout) + + entry = lookupConntrackEntry(t, objs.RetinaConntrack, + "10.0.27.1", "10.0.27.2", 27000, 80, protoTCP) + assert.True(t, entry.EvictionTime > synEviction, + "eviction time should increase after ACK (60s SYN timeout → 360s established), got %d → %d", + synEviction, entry.EvictionTime) +} + +func TestEndpointIngressFilter_TCP_RingBuf(t *testing.T) { + if err := ensureRingBufKernelSupported(); err != nil { + t.Skipf("ring buffer not supported: %v", err) + } + + objs, reader := compileAndLoadRingBufVariant(t, compileOpts{ + bypassFilter: 1, + enableConntrack: false, + aggregationLevel: 0, + samplingRate: 1, + }) + + srcIP := net.ParseIP("10.0.0.1") + dstIP := net.ParseIP("10.0.0.2") + ebpftest.PopulateFilterMap(t, objs.RetinaFilter, srcIP, dstIP) + + pkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, + DstIP: dstIP, + SrcPort: 12345, + DstPort: 80, + SYN: true, + SeqNum: 1000, + }) + + // Run program + ret := ebpftest.RunProgram(t, objs.EndpointIngressFilter, pkt) + assert.Equal(t, uint32(tcActUnspec), ret, "expected TC_ACT_UNSPEC return value") + + // Read event from Ring Buffer + event, ok := ebpftest.ReadRingBufEvent[packetparserPacket](t, reader, perfReaderTimeout) + require.True(t, ok, "expected a ringbuf event") + + assert.Equal(t, ebpftest.IPToNative("10.0.0.1"), event.SrcIp) + assert.Equal(t, ebpftest.IPToNative("10.0.0.2"), event.DstIp) + assert.Equal(t, ebpftest.PortToNetwork(12345), event.SrcPort) + assert.Equal(t, ebpftest.PortToNetwork(80), event.DstPort) + assert.Equal(t, uint8(protoTCP), event.Proto) +} + +func TestRingBufReaderWrapper(t *testing.T) { + if err := ensureRingBufKernelSupported(); err != nil { + t.Skipf("ring buffer not supported: %v", err) + } + + objs, reader := compileAndLoadRingBufVariant(t, compileOpts{ + bypassFilter: 1, + enableConntrack: false, + aggregationLevel: 0, + samplingRate: 1, + }) + + wrapper := &ringBufReaderWrapper{reader: reader} + + srcIP := net.ParseIP("10.0.0.1") + dstIP := net.ParseIP("10.0.0.2") + ebpftest.PopulateFilterMap(t, objs.RetinaFilter, srcIP, dstIP) + + pkt := ebpftest.BuildTCPPacket(ebpftest.TCPPacketOpts{ + SrcIP: srcIP, + DstIP: dstIP, + SrcPort: 54321, + DstPort: 80, + SYN: true, + SeqNum: 2000, + }) + + // Run program to generate an event + ebpftest.RunProgram(t, objs.EndpointIngressFilter, pkt) + + // Test Read() behavior + rec, err := wrapper.Read() + require.NoError(t, err) + require.NotNil(t, rec.RawSample) + + // Test Close() behavior + err = wrapper.Close() + require.NoError(t, err) + + // After closing, Read() should return ringbuf.ErrClosed + _, err = wrapper.Read() + assert.ErrorIs(t, err, ringbuf.ErrClosed) +} diff --git a/pkg/plugin/packetparser/packetparser_linux.go b/pkg/plugin/packetparser/packetparser_linux.go index c11ef9cef9..005f708542 100644 --- a/pkg/plugin/packetparser/packetparser_linux.go +++ b/pkg/plugin/packetparser/packetparser_linux.go @@ -20,7 +20,10 @@ import ( "github.com/cilium/cilium/api/v1/flow" v1 "github.com/cilium/cilium/pkg/hubble/api/v1" "github.com/cilium/ebpf" + "github.com/cilium/ebpf/asm" + "github.com/cilium/ebpf/link" "github.com/cilium/ebpf/perf" + "github.com/cilium/ebpf/ringbuf" tc "github.com/florianl/go-tc" helper "github.com/florianl/go-tc/core" nl "github.com/mdlayher/netlink" @@ -51,7 +54,14 @@ import ( ) //go:generate go run github.com/cilium/ebpf/cmd/bpf2go@master -cflags "-g -O2 -Wall -D__TARGET_ARCH_${GOARCH} -Wall" -target ${GOARCH} -type packet packetparser ./_cprog/packetparser.c -- -I../lib/_${GOARCH} -I../lib/common/libbpf/_src -I../lib/common/libbpf/_include/linux -I../lib/common/libbpf/_include/uapi/linux -I../lib/common/libbpf/_include/asm -I../filter/_cprog/ -I../conntrack/_cprog/ -var errNoOutgoingLinks = errors.New("could not determine any outgoing links") +var ( + errNoOutgoingLinks = errors.New("could not determine any outgoing links") + errRingBufKernelTooOld = errors.New("ring buffer requires newer kernel") + errRingBufSizeUnset = errors.New("ring buffer size must be set when ring buffers are enabled") + errRingBufSizeTooSmall = errors.New("ring buffer size is smaller than the kernel page size") + errRingBufSizeTooLarge = errors.New("ring buffer size is larger than the allowed maximum") + errRingBufSizeNotPowerOfTwo = errors.New("ring buffer size is not a power of 2") +) func init() { registry.Add(name, New) @@ -93,8 +103,8 @@ func (p *packetParser) Generate(ctx context.Context) error { if p.cfg.BypassLookupIPOfInterest { p.l.Info("bypassing lookup IP of interest") bypassLookupIPOfInterest = 1 - st = fmt.Sprintf("#define BYPASS_LOOKUP_IP_OF_INTEREST %d\n", bypassLookupIPOfInterest) } + st = fmt.Sprintf("#define BYPASS_LOOKUP_IP_OF_INTEREST %d\n", bypassLookupIPOfInterest) conntrackMetrics := 0 // Check if packetparser has Conntrack metrics enabled. @@ -117,6 +127,10 @@ func (p *packetParser) Generate(ctx context.Context) error { p.l.Info("data aggregation level", zap.String("level", p.cfg.DataAggregationLevel.String())) st += fmt.Sprintf("#define DATA_AGGREGATION_LEVEL %d\n", p.cfg.DataAggregationLevel) + // Process packetparser sampling rate. + p.l.Info("sampling rate", zap.Uint32("rate", p.cfg.DataSamplingRate)) + st += fmt.Sprintf("#define DATA_SAMPLING_RATE %d\n", p.cfg.DataSamplingRate) + // Generate dynamic header for packetparser. err = loader.WriteFile(ctx, dynamicHeaderPath, st) if err != nil { @@ -126,6 +140,36 @@ func (p *packetParser) Generate(ctx context.Context) error { return nil } +// validateRingBufferSize validates the ring buffer size and returns an error for invalid values. +func validateRingBufferSize(size uint32) error { + const maxSize = 1 * 1024 * 1024 * 1024 // 1GB + intPageSize := os.Getpagesize() + if intPageSize <= 0 { + intPageSize = 4096 + } + if intPageSize > int(^uint32(0)) { + intPageSize = int(^uint32(0)) + } + //nolint:gosec // bounded to uint32 + pageSize := uint32(intPageSize) + + if size == 0 { + return errRingBufSizeUnset + } + if size < pageSize { + return fmt.Errorf("%w: size=%d page_size=%d", errRingBufSizeTooSmall, size, pageSize) + } + if size > maxSize { + return fmt.Errorf("%w: size=%d max_size=%d", errRingBufSizeTooLarge, size, maxSize) + } + // Check if size is a power of 2. + if (size & (size - 1)) != 0 { + return fmt.Errorf("%w: size=%d", errRingBufSizeNotPowerOfTwo, size) + } + + return nil +} + func (p *packetParser) Compile(ctx context.Context) error { // Get the absolute path to this file during runtime. dir, err := absPath() @@ -147,8 +191,13 @@ func (p *packetParser) Compile(ctx context.Context) error { if arch == "arm64" { targetArch = "-D__TARGET_ARCH_arm64" } + + runtimeIncludeDir := "-I" + loader.VmlinuxHeaderDir() + // Keep target as bpf, otherwise clang compilation yields bpf object that elf reader cannot load. - err = loader.CompileEbpf(ctx, "-target", "bpf", "-Wall", targetArch, "-g", "-O2", "-c", bpfSourceFile, "-o", bpfOutputFile, + cflags := []string{ + "-target", "bpf", "-Wall", targetArch, "-g", "-O2", "-c", bpfSourceFile, "-o", bpfOutputFile, + runtimeIncludeDir, archLibDir, libbpfSrcDir, libbpfIncludeAsmDir, @@ -156,7 +205,21 @@ func (p *packetParser) Compile(ctx context.Context) error { libbpfIncludeUapiLinuxDir, filterDir, conntrackDir, - ) + } + + if p.cfg.PacketParserRingBuffer.IsEnabled() { + if validateErr := validateRingBufferSize(p.cfg.PacketParserRingBufferSize); validateErr != nil { + return validateErr + } + + p.l.Info("Compiling with Ring Buffer enabled", zap.Uint32("size", p.cfg.PacketParserRingBufferSize)) + cflags = append(cflags, + "-DUSE_RING_BUFFER", + fmt.Sprintf("-DRING_BUFFER_SIZE=%d", p.cfg.PacketParserRingBufferSize), + ) + } + + err = loader.CompileEbpf(ctx, cflags...) if err != nil { return err } @@ -170,6 +233,11 @@ func (p *packetParser) Init() error { p.l.Warn("packet parser and latency plugin will not init because pod level is disabled") return nil } + if p.cfg.PacketParserRingBuffer.IsEnabled() { + if ringBufErr := ensureRingBufKernelSupported(); ringBufErr != nil { + return ringBufErr + } + } // Get the absolute path to this file during runtime. dir, err := absPath() if err != nil { @@ -183,6 +251,12 @@ func (p *packetParser) Init() error { if err != nil { return err } + + // Override filter map max entries to match the configured size from init container. + if mapSpec, ok := spec.Maps[plugincommon.FilterMapName]; ok && p.cfg.FilterMapMaxEntries > 0 { + mapSpec.MaxEntries = p.cfg.FilterMapMaxEntries + } + //nolint:typecheck if err := spec.LoadAndAssign(objs, &ebpf.CollectionOptions{ //nolint:typecheck Maps: ebpf.MapOptions{ @@ -218,15 +292,45 @@ func (p *packetParser) Init() error { return err } - p.reader, err = plugincommon.NewPerfReader(p.l, objs.RetinaPacketparserEvents, perCPUBuffer, 1) - if err != nil { - p.l.Error("Error NewReader", zap.Error(err)) - return err + if p.cfg.PacketParserRingBuffer.IsEnabled() { + p.l.Info("Initializing Ring Buffer reader") + var rb *ringbuf.Reader + rb, err = ringbuf.NewReader(objs.RetinaPacketparserEvents) + if err != nil { + p.l.Error("Error NewReader ringbuf", zap.Error(err)) + return fmt.Errorf("failed to create ringbuf reader: %w", err) + } + p.reader = &ringBufReaderWrapper{reader: rb} + } else { + p.l.Info("Initializing Perf Reader") + var pr *perf.Reader + pr, err = plugincommon.NewPerfReader(p.l, objs.RetinaPacketparserEvents, perCPUBuffer, 1) + if err != nil { + p.l.Error("Error NewReader", zap.Error(err)) + return fmt.Errorf("failed to create perf reader: %w", err) + } + p.reader = &perfReaderWrapper{reader: pr} } - p.tcMap = &sync.Map{} + p.attachmentMap = &sync.Map{} p.interfaceLockMap = &sync.Map{} + // Resolve TCX support based on config. + switch p.cfg.EnableTCX { + case kcfg.TCXModeOff: + p.tcxSupported = false + p.l.Info("EnableTCX=off: will use traditional TC attachment") + case kcfg.TCXModeAuto: + p.tcxSupported = isTCXSupported() + if p.tcxSupported { + p.l.Info("EnableTCX=auto: TCX supported, will use TCX attachment") + } else { + p.l.Info("EnableTCX=auto: TCX not supported, will use traditional TC attachment") + } + default: + p.l.Warn("Unknown EnableTCX value, defaulting to traditional TC attachment", zap.String("enableTCX", string(p.cfg.EnableTCX))) + } + return nil } @@ -280,7 +384,7 @@ func (p *packetParser) Start(ctx context.Context) error { } // Create the channel. - p.recordsChannel = make(chan perf.Record, buffer) + p.recordsChannel = make(chan perfRecord, buffer) p.l.Debug("Created records channel") return p.run(ctx) @@ -341,14 +445,17 @@ func (p *packetParser) SetupChannel(ch chan *v1.Event) error { // cleanAll is NOT thread safe. // Not required for now. func (p *packetParser) cleanAll() error { - // Delete tunnel and qdiscs. - if p.tcMap == nil { + if p.attachmentMap == nil { return nil } - p.tcMap.Range(func(key, value interface{}) bool { - v := value.(*tcValue) - p.clean(v.tc, v.qdisc) + p.attachmentMap.Range(func(_, value interface{}) bool { + v := value.(*attachmentValue) + if v.attachmentType == attachmentTypeTCX { + p.cleanTCX(v.tcxIngressLink, v.tcxEgressLink) + } else { + p.clean(v.tc, v.qdisc) + } return true }) @@ -356,7 +463,7 @@ func (p *packetParser) cleanAll() error { // It is OK to do this without a lock because // cleanAll is only invoked from Stop(), and Stop can // only be called from PluginManager (which is single threaded). - p.tcMap = &sync.Map{} + p.attachmentMap = &sync.Map{} return nil } @@ -372,6 +479,55 @@ func (p *packetParser) clean(rtnl nltc, qdisc *tc.Object) { } } +// cleanTCX closes TCX links. This is best effort. +func (p *packetParser) cleanTCX(ingressLink, egressLink link.Link) { + if ingressLink != nil { + if err := ingressLink.Close(); err != nil { + p.l.Debug("could not close ingress TCX link", zap.Error(err)) + } + } + if egressLink != nil { + if err := egressLink.Close(); err != nil { + p.l.Debug("could not close egress TCX link", zap.Error(err)) + } + } +} + +// isTCXSupported probes whether the running kernel supports TCX attachment (kernel 6.6+) +// by creating a minimal BPF program and attempting to attach it to the loopback interface. +func isTCXSupported() bool { + loopback, err := netlink.LinkByName("lo") + if err != nil { + return false + } + + progSpec := &ebpf.ProgramSpec{ + Type: ebpf.SchedCLS, + AttachType: ebpf.AttachTCXIngress, + License: "Dual MIT/GPL", + Instructions: asm.Instructions{ + asm.Mov.Imm(asm.R0, -1), + asm.Return(), + }, + } + prog, err := ebpf.NewProgram(progSpec) + if err != nil { + return false + } + defer prog.Close() + + testLink, err := link.AttachTCX(link.TCXOptions{ + Program: prog, + Attach: ebpf.AttachTCXIngress, + Interface: loopback.Attrs().Index, + }) + if err != nil { + return false + } + testLink.Close() //nolint:errcheck // probe cleanup + return true +} + func (p *packetParser) endpointWatcherCallbackFn(obj interface{}) { // Contract is that we will receive an endpoint event pointer. event := obj.(*endpoint.EndpointEvent) @@ -394,11 +550,15 @@ func (p *packetParser) endpointWatcherCallbackFn(obj interface{}) { case endpoint.EndpointDeleted: p.l.Debug("Endpoint deleted", zap.String("name", iface.Name)) // Clean. - if value, ok := p.tcMap.Load(ifaceKey); ok { - v := value.(*tcValue) - p.clean(v.tc, v.qdisc) + if value, ok := p.attachmentMap.Load(ifaceKey); ok { + v := value.(*attachmentValue) + if v.attachmentType == attachmentTypeTCX { + p.cleanTCX(v.tcxIngressLink, v.tcxEgressLink) + } else { + p.clean(v.tc, v.qdisc) + } // Delete from map. - p.tcMap.Delete(ifaceKey) + p.attachmentMap.Delete(ifaceKey) } // Delete from lock map. p.interfaceLockMap.Delete(ifaceKey) @@ -408,9 +568,75 @@ func (p *packetParser) endpointWatcherCallbackFn(obj interface{}) { } } -// createQdiscAndAttach creates a qdisc of type clsact on the interface and attaches the ingress and egress bpf filter programs to it. +// createQdiscAndAttach attaches BPF ingress/egress programs to the interface using TCX or TC +// depending on kernel support and configuration. // Only support interfaces of type veth and device. func (p *packetParser) createQdiscAndAttach(iface netlink.LinkAttrs, ifaceType interfaceType) { + p.l.Debug("Starting attachment", zap.String("interface", iface.Name)) + + if p.tcxSupported { + p.attachViaTCX(iface, ifaceType) + return + } + + p.attachViaTC(iface, ifaceType) +} + +// attachViaTCX attaches BPF programs using TCX (TC eXpress, kernel 6.6+). +func (p *packetParser) attachViaTCX(iface netlink.LinkAttrs, ifaceType interfaceType) { + var ingressProgram, egressProgram *ebpf.Program + + switch ifaceType { + case Device: + ingressProgram = p.objs.HostIngressFilter + egressProgram = p.objs.HostEgressFilter + case Veth: + ingressProgram = p.objs.EndpointIngressFilter + egressProgram = p.objs.EndpointEgressFilter + default: + p.l.Error("Unknown interface type for TCX", zap.String("interface type", string(ifaceType))) + return + } + + // Attach at the head of the TCX chain so Retina sees every packet before + // any policy-enforcing program (e.g. Cilium) can drop it. This is safe + // because Retina's programs always return TC_ACT_UNSPEC, which passes + // control to the next program in the chain without making any forwarding + // decision. + ingressLink, err := link.AttachTCX(link.TCXOptions{ + Program: ingressProgram, + Attach: ebpf.AttachTCXIngress, + Interface: iface.Index, + Anchor: link.Head(), + }) + if err != nil { + p.l.Error("could not attach TCX ingress program", zap.String("interface", iface.Name), zap.Error(err)) + return + } + + egressLink, err := link.AttachTCX(link.TCXOptions{ + Program: egressProgram, + Attach: ebpf.AttachTCXEgress, + Interface: iface.Index, + Anchor: link.Head(), + }) + if err != nil { + p.l.Error("could not attach TCX egress program", zap.String("interface", iface.Name), zap.Error(err)) + ingressLink.Close() //nolint:errcheck // best effort + return + } + + ifaceKey := ifaceToKey(iface) + p.attachmentMap.Store(ifaceKey, &attachmentValue{ + attachmentType: attachmentTypeTCX, + tcxIngressLink: ingressLink, + tcxEgressLink: egressLink, + }) + p.l.Debug("Successfully attached BPF programs using TCX", zap.String("interface", iface.Name)) +} + +// attachViaTC attaches BPF programs using traditional TC with a clsact qdisc. +func (p *packetParser) attachViaTC(iface netlink.LinkAttrs, ifaceType interfaceType) { p.l.Debug("Starting qdisc attachment", zap.String("interface", iface.Name)) var ( @@ -431,7 +657,7 @@ func (p *packetParser) createQdiscAndAttach(iface netlink.LinkAttrs, ifaceType i ingressInfo = p.endpointIngressInfo egressInfo = p.endpointEgressInfo default: - p.l.Error("Unknown interface type", zap.String("interface type", string(ifaceType))) + p.l.Error("Unknown interface type for TC", zap.String("interface type", string(ifaceType))) return } @@ -519,13 +745,13 @@ func (p *packetParser) createQdiscAndAttach(iface netlink.LinkAttrs, ifaceType i // Cache. ifaceKey := ifaceToKey(iface) - tcValue := &tcValue{ - tc: rtnl, - qdisc: clsactQdisc, - } - p.tcMap.Store(ifaceKey, tcValue) + p.attachmentMap.Store(ifaceKey, &attachmentValue{ + attachmentType: attachmentTypeTC, + tc: rtnl, + qdisc: clsactQdisc, + }) - p.l.Debug("Successfully added bpf", zap.String("interface", iface.Name)) + p.l.Debug("Successfully attached BPF programs using traditional TC", zap.String("interface", iface.Name)) } func (p *packetParser) run(ctx context.Context) error { @@ -568,6 +794,8 @@ func (p *packetParser) processRecord(ctx context.Context, id int) { zap.Int("worker_id", id), ) + metrics.ParsedPacketsCounter.WithLabelValues().Inc() + var bpfEvent packetparserPacket err := binary.Read(bytes.NewReader(record.RawSample), binary.LittleEndian, &bpfEvent) if err != nil { @@ -602,10 +830,14 @@ func (p *packetParser) processRecord(ctx context.Context, id int) { // Add the traffic direction to the flow. fl.TrafficDirection = flow.TrafficDirection(bpfEvent.TrafficDirection) - meta := &utils.RetinaMetadata{} + ext := utils.NewExtensions() - // Add packet size to the flow's metadata. - utils.AddPacketSize(meta, bpfEvent.Bytes) + // Add packet size to the flow's extensions. + utils.AddPacketSize(ext, bpfEvent.Bytes) + + // Add previously observed byte and packet counts to the flow's extensions + utils.AddPreviouslyObservedBytes(ext, bpfEvent.PreviouslyObservedBytes) + utils.AddPreviouslyObservedPackets(ext, bpfEvent.PreviouslyObservedPackets) // Add the TCP metadata to the flow. tcpMetadata := bpfEvent.TcpMetadata @@ -617,18 +849,33 @@ func (p *packetParser) processRecord(ctx context.Context, id int) { uint16((bpfEvent.Flags&TCPFlagRST)>>2), // nolint:gomnd // 2 is the offset for RST. uint16((bpfEvent.Flags&TCPFlagPSH)>>3), // nolint:gomnd // 3 is the offset for PSH. uint16((bpfEvent.Flags&TCPFlagURG)>>5), // nolint:gomnd // 5 is the offset for URG. + uint16((bpfEvent.Flags&TCPFlagECE)>>6), // nolint:gomnd // 6 is the offset for ECE. + uint16((bpfEvent.Flags&TCPFlagCWR)>>7), // nolint:gomnd // 7 is the offset for CWR. + uint16((bpfEvent.Flags&TCPFlagNS)>>8), // nolint:gomnd // 8 is the offset for NS. + ) + utils.AddPreviouslyObservedTCPFlags( + ext, + bpfEvent.PreviouslyObservedFlags.Syn, + bpfEvent.PreviouslyObservedFlags.Ack, + bpfEvent.PreviouslyObservedFlags.Fin, + bpfEvent.PreviouslyObservedFlags.Rst, + bpfEvent.PreviouslyObservedFlags.Psh, + bpfEvent.PreviouslyObservedFlags.Urg, + bpfEvent.PreviouslyObservedFlags.Ece, + bpfEvent.PreviouslyObservedFlags.Cwr, + bpfEvent.PreviouslyObservedFlags.Ns, ) // For packets originating from node, we use tsval as the tcpID. // Packets coming back has the tsval echoed in tsecr. if fl.GetTraceObservationPoint() == flow.TraceObservationPoint_TO_NETWORK { - utils.AddTCPID(meta, uint64(tcpMetadata.Tsval)) + utils.AddTCPID(ext, uint64(tcpMetadata.Tsval)) } else if fl.GetTraceObservationPoint() == flow.TraceObservationPoint_FROM_NETWORK { - utils.AddTCPID(meta, uint64(tcpMetadata.Tsecr)) + utils.AddTCPID(ext, uint64(tcpMetadata.Tsecr)) } - // Add metadata to the flow. - utils.AddRetinaMetadata(fl, meta) + // Set extensions on the flow. + utils.SetExtensions(fl, ext) // Write the event to the enricher. ev := &v1.Event{ @@ -671,7 +918,7 @@ func (p *packetParser) readData() { // This is unblocked by the close call. record, err := p.reader.Read() if err != nil { - if errors.Is(err, perf.ErrClosed) { + if errors.Is(err, perf.ErrClosed) || errors.Is(err, ringbuf.ErrClosed) { p.l.Error("Perf array is empty") // nothing to do, we're done } else { @@ -706,3 +953,67 @@ func absPath() (string, error) { dir := path.Dir(filename) return dir, nil } + +type ringBufReaderWrapper struct { + reader *ringbuf.Reader +} + +func (r *ringBufReaderWrapper) Read() (perfRecord, error) { + rec, err := r.reader.Read() + if err != nil { + return perfRecord{}, fmt.Errorf("failed to read from ringbuf: %w", err) + } + return perfRecord{ + RawSample: rec.RawSample, + Remaining: rec.Remaining, + }, nil +} + +func (r *ringBufReaderWrapper) Close() error { + if err := r.reader.Close(); err != nil { + return fmt.Errorf("failed to close ringbuf reader: %w", err) + } + return nil +} + +type perfReaderWrapper struct { + reader *perf.Reader +} + +func (r *perfReaderWrapper) Read() (perfRecord, error) { + rec, err := r.reader.Read() + if err != nil { + return perfRecord{}, fmt.Errorf("failed to read perf record: %w", err) + } + return perfRecord{ + CPU: rec.CPU, + LostSamples: rec.LostSamples, + RawSample: rec.RawSample, + }, nil +} + +func (r *perfReaderWrapper) Close() error { + if err := r.reader.Close(); err != nil { + return fmt.Errorf("failed to close perf reader: %w", err) + } + return nil +} + +var getKernelVersion = utils.LinuxKernelVersion + +func ensureRingBufKernelSupported() error { + kv, err := getKernelVersion() + if err != nil { + return fmt.Errorf("failed to detect kernel version for ring buffer support: %w", err) + } + + if !kv.AtLeast(ringBufMinKernelMajor, ringBufMinKernelMinor, ringBufMinKernelPatch) { + return fmt.Errorf( + "%w: requires >= %d.%d.%d, current: %s", + errRingBufKernelTooOld, + ringBufMinKernelMajor, ringBufMinKernelMinor, ringBufMinKernelPatch, kv.Release, + ) + } + + return nil +} diff --git a/pkg/plugin/packetparser/packetparser_linux_test.go b/pkg/plugin/packetparser/packetparser_linux_test.go index 50903852a1..4fa4a5394c 100644 --- a/pkg/plugin/packetparser/packetparser_linux_test.go +++ b/pkg/plugin/packetparser/packetparser_linux_test.go @@ -10,6 +10,7 @@ import ( "fmt" "os" "path" + "reflect" "runtime" "sync" "testing" @@ -17,7 +18,7 @@ import ( v1 "github.com/cilium/cilium/pkg/hubble/api/v1" "github.com/cilium/ebpf" - "github.com/cilium/ebpf/perf" + "github.com/cilium/ebpf/ringbuf" tc "github.com/florianl/go-tc" nl "github.com/mdlayher/netlink" kcfg "github.com/microsoft/retina/pkg/config" @@ -25,6 +26,7 @@ import ( "github.com/microsoft/retina/pkg/log" "github.com/microsoft/retina/pkg/metrics" "github.com/microsoft/retina/pkg/plugin/packetparser/mocks" + "github.com/microsoft/retina/pkg/utils" "github.com/microsoft/retina/pkg/watchers/endpoint" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" @@ -33,6 +35,50 @@ import ( "go.uber.org/mock/gomock" ) +// mockPerfReader is a gomock-based mock for the perfReader interface. +// Defined here (not in mocks/) because perfReader uses the unexported perfRecord type. +type mockPerfReader struct { + ctrl *gomock.Controller + recorder *mockPerfReaderRecorder +} + +type mockPerfReaderRecorder struct { + mock *mockPerfReader +} + +func newMockPerfReader(ctrl *gomock.Controller) *mockPerfReader { + m := &mockPerfReader{ctrl: ctrl} + m.recorder = &mockPerfReaderRecorder{m} + return m +} + +func (m *mockPerfReader) EXPECT() *mockPerfReaderRecorder { return m.recorder } + +func (m *mockPerfReader) Read() (perfRecord, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Read") + rec, _ := ret[0].(perfRecord) + err, _ := ret[1].(error) + return rec, err +} + +func (mr *mockPerfReaderRecorder) Read() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*mockPerfReader)(nil).Read)) +} + +func (m *mockPerfReader) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + err, _ := ret[0].(error) + return err +} + +func (mr *mockPerfReaderRecorder) Close() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*mockPerfReader)(nil).Close)) +} + +var errTestRead = errors.New("error") + var ( cfgPodLevelEnabled = &kcfg.Config{ EnablePodLevel: true, @@ -56,6 +102,11 @@ var ( BypassLookupIPOfInterest: true, EnableConntrackMetrics: true, } + cfgRingBufferEnabled = &kcfg.Config{ + EnablePodLevel: true, + PacketParserRingBuffer: kcfg.PacketParserRingBufferEnabled, + PacketParserRingBufferSize: 4096, + } ) func TestCleanAll(t *testing.T) { @@ -68,7 +119,7 @@ func TestCleanAll(t *testing.T) { } assert.Nil(t, p.cleanAll()) - p.tcMap = &sync.Map{} + p.attachmentMap = &sync.Map{} assert.Nil(t, p.cleanAll()) ctrl := gomock.NewController(t) @@ -85,13 +136,13 @@ func TestCleanAll(t *testing.T) { return mq } - p.tcMap.Store(tcKey{"test", "test", 1}, &tcValue{mrtnl, &tc.Object{}}) - p.tcMap.Store(tcKey{"test2", "test2", 2}, &tcValue{mrtnl, &tc.Object{}}) + p.attachmentMap.Store(attachmentKey{"test", "test", 1}, &attachmentValue{tc: mrtnl, qdisc: &tc.Object{}}) + p.attachmentMap.Store(attachmentKey{"test2", "test2", 2}, &attachmentValue{tc: mrtnl, qdisc: &tc.Object{}}) assert.Nil(t, p.cleanAll()) keyCount := 0 - p.tcMap.Range(func(k, v interface{}) bool { + p.attachmentMap.Range(func(_ interface{}, _ interface{}) bool { keyCount++ return true }) @@ -167,7 +218,7 @@ func TestEndpointWatcherCallbackFn_EndpointDeleted(t *testing.T) { cfg: cfgPodLevelEnabled, l: log.Logger().Named("test"), interfaceLockMap: &sync.Map{}, - tcMap: &sync.Map{}, + attachmentMap: &sync.Map{}, } // Create test interface attributes. @@ -180,7 +231,7 @@ func TestEndpointWatcherCallbackFn_EndpointDeleted(t *testing.T) { // Pre-populate both maps to simulate existing interface p.interfaceLockMap.Store(key, &sync.Mutex{}) - p.tcMap.Store(key, &tcValue{nil, &tc.Object{}}) + p.attachmentMap.Store(key, &attachmentValue{tc: nil, qdisc: &tc.Object{}}) // Create EndpointDeleted event. e := &endpoint.EndpointEvent{ @@ -192,11 +243,10 @@ func TestEndpointWatcherCallbackFn_EndpointDeleted(t *testing.T) { p.endpointWatcherCallbackFn(e) // Verify both maps are cleaned up. - _, tcMapExists := p.tcMap.Load(key) + _, attachmentMapExists := p.attachmentMap.Load(key) _, lockMapExists := p.interfaceLockMap.Load(key) - // Assert both maps are cleaned up - assert.False(t, tcMapExists, "tcMap entry should be deleted") + assert.False(t, attachmentMapExists, "attachmentMap entry should be deleted") assert.False(t, lockMapExists, "interfaceLockMap entry should be deleted") } @@ -227,7 +277,7 @@ func TestCreateQdiscAndAttach(t *testing.T) { return mrtnl, nil } - getFD = func(e *ebpf.Program) int { + getFD = func(_ *ebpf.Program) int { return 1 } @@ -252,7 +302,7 @@ func TestCreateQdiscAndAttach(t *testing.T) { hostEgressInfo: &ebpf.ProgramInfo{ Name: "egress", }, - tcMap: &sync.Map{}, + attachmentMap: &sync.Map{}, } linkAttr := netlink.LinkAttrs{ Name: "test", @@ -263,7 +313,7 @@ func TestCreateQdiscAndAttach(t *testing.T) { p.createQdiscAndAttach(linkAttr, Veth) key := ifaceToKey(linkAttr) - _, ok := p.tcMap.Load(key) + _, ok := p.attachmentMap.Load(key) assert.True(t, ok) pObj.HostIngressFilter = &ebpf.Program{} @@ -277,7 +327,7 @@ func TestCreateQdiscAndAttach(t *testing.T) { p.createQdiscAndAttach(linkAttr2, Device) key = ifaceToKey(linkAttr2) - _, ok = p.tcMap.Load(key) + _, ok = p.attachmentMap.Load(key) assert.True(t, ok) } @@ -286,8 +336,8 @@ func TestReadData_Error(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mperf := mocks.NewMockperfReader(ctrl) - mperf.EXPECT().Read().Return(perf.Record{}, errors.New("error")).AnyTimes() + mperf := newMockPerfReader(ctrl) + mperf.EXPECT().Read().Return(perfRecord{}, errTestRead).AnyTimes() menricher := enricher.NewMockEnricherInterface(ctrl) //nolint:typecheck menricher.EXPECT().Write(gomock.Any()).Times(0) @@ -300,12 +350,31 @@ func TestReadData_Error(t *testing.T) { p.readData() // Lost samples. - mperf.EXPECT().Read().Return(perf.Record{ + mperf.EXPECT().Read().Return(perfRecord{ LostSamples: 1, }, nil).AnyTimes() p.readData() } +func TestReadData_RingBufClosed(t *testing.T) { + log.SetupZapLogger(log.GetDefaultLogOpts()) //nolint:errcheck // ignore + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mperf := newMockPerfReader(ctrl) + mperf.EXPECT().Read().Return(perfRecord{}, ringbuf.ErrClosed).AnyTimes() + + menricher := enricher.NewMockEnricherInterface(ctrl) //nolint:typecheck + menricher.EXPECT().Write(gomock.Any()).Times(0) + + p := &packetParser{ + cfg: cfgRingBufferEnabled, + l: log.Logger().Named("test"), + reader: mperf, + } + p.readData() +} + func TestReadDataPodLevelEnabled(t *testing.T) { log.SetupZapLogger(log.GetDefaultLogOpts()) ctrl := gomock.NewController(t) @@ -320,12 +389,12 @@ func TestReadDataPodLevelEnabled(t *testing.T) { DstPort: uint16(443), } bytes, _ := json.Marshal(bpfEvent) - record := perf.Record{ + record := perfRecord{ LostSamples: 0, RawSample: bytes, } - mperf := mocks.NewMockperfReader(ctrl) + mperf := newMockPerfReader(ctrl) mperf.EXPECT().Read().Return(record, nil).MinTimes(1) menricher := enricher.NewMockEnricherInterface(ctrl) //nolint:typecheck @@ -336,7 +405,7 @@ func TestReadDataPodLevelEnabled(t *testing.T) { l: log.Logger().Named("test"), reader: mperf, enricher: menricher, - recordsChannel: make(chan perf.Record, buffer), + recordsChannel: make(chan perfRecord, buffer), } mICounterVec := metrics.NewMockCounterVec(ctrl) @@ -344,6 +413,11 @@ func TestReadDataPodLevelEnabled(t *testing.T) { metrics.LostEventsCounter = mICounterVec + mParsedPacketsCounter := metrics.NewMockCounterVec(ctrl) + mParsedPacketsCounter.EXPECT().WithLabelValues(gomock.Any()). + Return(prometheus.NewCounter(prometheus.CounterOpts{})).AnyTimes() + metrics.ParsedPacketsCounter = mParsedPacketsCounter + exCh := make(chan *v1.Event, 10) p.SetupChannel(exCh) @@ -395,12 +469,12 @@ func TestStartWithDataAggregationLevelLow(t *testing.T) { } bytes, err := json.Marshal(bpfEvent) // nolint:musttag // ignore require.NoError(t, err) - record := perf.Record{ + record := perfRecord{ LostSamples: 0, RawSample: bytes, } - mockReader := mocks.NewMockperfReader(ctrl) + mockReader := newMockPerfReader(ctrl) mockReader.EXPECT().Read().Return(record, nil).MinTimes(1) getQdisc = func(_ nltc) qdisc { @@ -428,7 +502,7 @@ func TestStartWithDataAggregationLevelLow(t *testing.T) { l: log.Logger().Named("test"), objs: pObj, reader: mockReader, - recordsChannel: make(chan perf.Record, buffer), + recordsChannel: make(chan perfRecord, buffer), interfaceLockMap: &sync.Map{}, endpointIngressInfo: &ebpf.ProgramInfo{ Name: "ingress", @@ -442,7 +516,7 @@ func TestStartWithDataAggregationLevelLow(t *testing.T) { hostEgressInfo: &ebpf.ProgramInfo{ Name: "egress", }, - tcMap: &sync.Map{}, + attachmentMap: &sync.Map{}, } ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() @@ -474,12 +548,12 @@ func TestStartWithDataAggregationLevelHigh(t *testing.T) { } bytes, err := json.Marshal(bpfEvent) // nolint:musttag // ignore require.NoError(t, err) - record := perf.Record{ + record := perfRecord{ LostSamples: 0, RawSample: bytes, } - mockReader := mocks.NewMockperfReader(ctrl) + mockReader := newMockPerfReader(ctrl) mockReader.EXPECT().Read().Return(record, nil).MinTimes(1) getQdisc = func(_ nltc) qdisc { @@ -507,7 +581,7 @@ func TestStartWithDataAggregationLevelHigh(t *testing.T) { l: log.Logger().Named("test"), objs: pObj, reader: mockReader, - recordsChannel: make(chan perf.Record, buffer), + recordsChannel: make(chan perfRecord, buffer), interfaceLockMap: &sync.Map{}, endpointIngressInfo: &ebpf.ProgramInfo{ Name: "ingress", @@ -521,7 +595,7 @@ func TestStartWithDataAggregationLevelHigh(t *testing.T) { hostEgressInfo: &ebpf.ProgramInfo{ Name: "egress", }, - tcMap: &sync.Map{}, + attachmentMap: &sync.Map{}, } ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() @@ -558,24 +632,33 @@ func TestPacketParseGenerate(t *testing.T) { expectedContents string }{ { - name: "PodLevelEnabled", - cfg: cfgPodLevelEnabled, - expectedContents: "#define BYPASS_LOOKUP_IP_OF_INTEREST 1\n#define DATA_AGGREGATION_LEVEL 0\n", + name: "PodLevelEnabled", + cfg: cfgPodLevelEnabled, + expectedContents: "#define BYPASS_LOOKUP_IP_OF_INTEREST 1\n" + + "#define DATA_AGGREGATION_LEVEL 0\n" + + "#define DATA_SAMPLING_RATE 0\n", }, { - name: "ConntrackMetricsEnabled", - cfg: cfgConntrackMetricsEnabled, - expectedContents: "#define BYPASS_LOOKUP_IP_OF_INTEREST 1\n#define ENABLE_CONNTRACK_METRICS 1\n#define DATA_AGGREGATION_LEVEL 1\n", + name: "ConntrackMetricsEnabled", + cfg: cfgConntrackMetricsEnabled, + expectedContents: "#define BYPASS_LOOKUP_IP_OF_INTEREST 1\n" + + "#define ENABLE_CONNTRACK_METRICS 1\n" + + "#define DATA_AGGREGATION_LEVEL 1\n" + + "#define DATA_SAMPLING_RATE 0\n", }, { - name: "DataAggregationLevelLow", - cfg: cfgDataAggregationLevelLow, - expectedContents: "#define DATA_AGGREGATION_LEVEL 0\n", + name: "DataAggregationLevelLow", + cfg: cfgDataAggregationLevelLow, + expectedContents: "#define BYPASS_LOOKUP_IP_OF_INTEREST 0\n" + + "#define DATA_AGGREGATION_LEVEL 0\n" + + "#define DATA_SAMPLING_RATE 0\n", }, { - name: "DataAggregationLevelHigh", - cfg: cfgDataAggregationLevelHigh, - expectedContents: "#define DATA_AGGREGATION_LEVEL 1\n", + name: "DataAggregationLevelHigh", + cfg: cfgDataAggregationLevelHigh, + expectedContents: "#define BYPASS_LOOKUP_IP_OF_INTEREST 0\n" + + "#define DATA_AGGREGATION_LEVEL 1\n" + + "#define DATA_SAMPLING_RATE 0\n", }, } @@ -640,6 +723,81 @@ func TestCompile(t *testing.T) { } } +func TestCompileRingBuffer(t *testing.T) { + takeBackup() + defer restoreBackup() + + log.SetupZapLogger(log.GetDefaultLogOpts()) //nolint:errcheck // ignore + p := &packetParser{ + cfg: cfgRingBufferEnabled, + l: log.Logger().Named(name), + } + dir, _ := absPath() + expectedOutputFile := fmt.Sprintf("%s/%s", dir, bpfObjectFileName) + + err := os.Remove(expectedOutputFile) + if err != nil && !errors.Is(err, os.ErrNotExist) { + t.Fatalf("Expected no error. Error: %+v", err) + } + + err = p.Generate(context.Background()) + if err != nil { + t.Fatalf("Expected no error. Error: %+v", err) + } + + err = p.Compile(context.Background()) + if err != nil { + t.Fatalf("Expected no error. Error: %+v", err) + } + if _, err := os.Stat(expectedOutputFile); errors.Is(err, os.ErrNotExist) { + t.Fatalf("File %+v doesn't exist", expectedOutputFile) + } +} + +func TestEnsureRingBufKernelSupported(t *testing.T) { + orig := getKernelVersion + defer func() { getKernelVersion = orig }() + + tests := []struct { + name string + major int + minor int + patch int + errExists bool + }{ + {"Supported", 5, 8, 0, false}, + {"Supported newer", 6, 1, 0, false}, + {"Not supported old major", 4, 15, 0, true}, + {"Not supported old minor", 5, 7, 0, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + getKernelVersion = func() (utils.KernelVersion, error) { + return utils.KernelVersion{ + Major: tt.major, + Minor: tt.minor, + Patch: tt.patch, + }, nil + } + err := ensureRingBufKernelSupported() + if tt.errExists { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } + + t.Run("Kernel version error", func(t *testing.T) { + getKernelVersion = func() (utils.KernelVersion, error) { + return utils.KernelVersion{}, errors.New("failed to get kernel version") //nolint:err113 // ignore + } + err := ensureRingBufKernelSupported() + assert.Error(t, err) + }) +} + // Helpers. func takeBackup() { // Get the directory of the current test file. diff --git a/pkg/plugin/packetparser/types_linux.go b/pkg/plugin/packetparser/types_linux.go index 4fc06f35c3..631e62b34b 100644 --- a/pkg/plugin/packetparser/types_linux.go +++ b/pkg/plugin/packetparser/types_linux.go @@ -10,7 +10,7 @@ import ( v1 "github.com/cilium/cilium/pkg/hubble/api/v1" "github.com/cilium/ebpf" - "github.com/cilium/ebpf/perf" + "github.com/cilium/ebpf/link" tc "github.com/florianl/go-tc" nl "github.com/mdlayher/netlink" "github.com/vishvananda/netlink" @@ -28,6 +28,7 @@ const ( TCPFlagURG TCPFlagECE TCPFlagCWR + TCPFlagNS ) const ( @@ -50,6 +51,12 @@ const ( Device interfaceType = "device" ) +const ( + ringBufMinKernelMajor = 5 + ringBufMinKernelMinor = 8 + ringBufMinKernelPatch = 0 +) + var ( getQdisc = func(tcnl nltc) qdisc { return tcnl.Qdisc() @@ -68,18 +75,33 @@ var ( perCPUBuffer = 32 ) -type tcKey struct { +// attachmentKey uniquely identifies a network interface for BPF program attachment. +type attachmentKey struct { name string hardwareAddr string netNs int } -type tcValue struct { +// attachmentType represents the method used to attach BPF programs. +type attachmentType int + +const ( + attachmentTypeTC attachmentType = iota // Traditional TC with clsact qdisc + attachmentTypeTCX // TCX (TC eXpress) - kernel 6.6+ +) + +// attachmentValue stores the attachment details for a network interface. +type attachmentValue struct { + // TC-specific fields tc nltc qdisc *tc.Object + // TCX-specific fields + attachmentType attachmentType + tcxIngressLink link.Link + tcxEgressLink link.Link } -//go:generate go run go.uber.org/mock/mockgen@v0.4.0 -source=types_linux.go -destination=mocks/mock_types.go -package=mocks +//go:generate go run go.uber.org/mock/mockgen@v0.4.0 -source=types_linux.go -destination=mocks/mock_types_linux.go -package=mocks -exclude_interfaces=perfReader // tc qdisc interface type qdisc interface { @@ -101,19 +123,26 @@ type nltc interface { } type perfReader interface { - Read() (perf.Record, error) + Read() (perfRecord, error) Close() error } +type perfRecord struct { + CPU int + LostSamples uint64 + RawSample []byte + Remaining int +} + type packetParser struct { cfg *kcfg.Config l *log.ZapLogger callbackID string objs *packetparserObjects //nolint:typecheck - // tcMap is a map of key to *val. - tcMap *sync.Map - reader perfReader - enricher enricher.EnricherInterface + // attachmentMap is a map of interface key to attachment details (TC or TCX). + attachmentMap *sync.Map + reader perfReader + enricher enricher.EnricherInterface // interfaceLockMap is a map of key to *sync.Mutex. interfaceLockMap *sync.Map endpointIngressInfo *ebpf.ProgramInfo @@ -121,12 +150,13 @@ type packetParser struct { hostIngressInfo *ebpf.ProgramInfo hostEgressInfo *ebpf.ProgramInfo wg sync.WaitGroup - recordsChannel chan perf.Record + recordsChannel chan perfRecord externalChannel chan *v1.Event + tcxSupported bool // Whether TCX is supported on this system } -func ifaceToKey(iface netlink.LinkAttrs) tcKey { - return tcKey{ +func ifaceToKey(iface netlink.LinkAttrs) attachmentKey { + return attachmentKey{ name: iface.Name, hardwareAddr: iface.HardwareAddr.String(), netNs: iface.NetNsID, diff --git a/pkg/plugin/pktmon/pktmon_windows.go b/pkg/plugin/pktmon/pktmon_windows.go index 4b0759f292..3edce9c552 100644 --- a/pkg/plugin/pktmon/pktmon_windows.go +++ b/pkg/plugin/pktmon/pktmon_windows.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "os/exec" + "time" "github.com/pkg/errors" @@ -26,17 +27,21 @@ import ( ) var ( - ErrNilEnricher = errors.New("enricher is nil") - ErrUnexpectedExit = errors.New("unexpected exit") - ErrNilGrpcClient = errors.New("grpc client is nil") + ErrNilEnricher = errors.New("enricher is nil") + ErrUnexpectedExit = errors.New("unexpected exit") + ErrNilGrpcClient = errors.New("grpc client is nil") + ErrStreamHealthCheckFailed = errors.New("pktmon stream health check failed - likely another ETW consumer is active or stream is broken") + ErrTooManyNilFlows = errors.New("received too many nil flows during health check - likely proto mismatch") socket = "/temp/retina-pktmon.sock" ) const ( - name = "pktmon" - connectionRetryAttempts = 5 - eventChannelSize = 1000 + name = "pktmon" + connectionRetryAttempts = 5 + eventChannelSize = 1000 + pktmonHealthCheckTimeout = 60 * time.Second + maxNilFlowsAllowed = 5 ) type Plugin struct { @@ -157,6 +162,19 @@ func (p *Plugin) Start(ctx context.Context) error { return errors.Wrapf(err, "failed to setup initial pktmon stream") } + // Start the stream before verifying + err = p.StartStream(ctx) + if err != nil { + return errors.Wrapf(err, "failed to start initial pktmon stream") + } + + // Verify that the event stream is producing events + // This detects silent ETW registration failures where another consumer is already active + err = p.verifyEventStream() + if err != nil { + return errors.Wrapf(err, "pktmon event stream health check failed") + } + // run the getflows loop g.Go(func() error { for { @@ -198,7 +216,10 @@ func (p *Plugin) StartStream(ctx context.Context) error { var err error fn := func() error { - p.stream, err = p.grpcClient.GetFlows(ctx, &observerv1.GetFlowsRequest{}) + // Use a long timeout for stream setup, independent of parent context cancellation + streamCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + p.stream, err = p.grpcClient.GetFlows(streamCtx, &observerv1.GetFlowsRequest{}) if err != nil { return errors.Wrapf(err, "failed to open pktmon stream") } @@ -212,6 +233,66 @@ func (p *Plugin) StartStream(ctx context.Context) error { return nil } +// verifyEventStream checks that the gRPC stream is responding and that events can flow. +// This detects scenarios where ETW registration silently fails because another +// consumer is already active on the EVENTS_MAP (indicated by gRPC errors). +// If the stream is healthy but no traffic is present, it logs a warning but continues. +func (p *Plugin) verifyEventStream() error { + // Create an independent background context for the health check + // This is NOT a child of the parent ctx to avoid cancellation when the parent shuts down + // while the health check is still running + healthCtx, cancel := context.WithTimeout(context.Background(), pktmonHealthCheckTimeout) + defer cancel() + + // Create a channel to receive the result + resultCh := make(chan error, 1) + + go func() { + nilFlowCount := 0 + + for { + event, err := p.stream.Recv() + if err != nil { + resultCh <- errors.Wrapf(err, "failed to receive first event during health check") + return + } + + if event.GetFlow() == nil { + nilFlowCount++ + if nilFlowCount > maxNilFlowsAllowed { + resultCh <- fmt.Errorf("exceeded max nil flows allowed: %d: %w", maxNilFlowsAllowed, ErrTooManyNilFlows) + return + } + // Skip nil flows and wait for next event + continue + } + + // Got a valid flow + resultCh <- nil + return + } + }() + + // Wait for either a successful event or timeout + select { + case <-healthCtx.Done(): + // Timeout occurred - no events received, but stream connection is working. + // This could be due to low/no traffic rather than ETW registration failure. + p.l.Warn("no events received during health check", + zap.Duration("timeout", pktmonHealthCheckTimeout), + zap.String("cause", "likely no network traffic or node is idle")) + return nil + case err := <-resultCh: + if err != nil { + // Stream error indicates ETW registration failure or connection issue + // Use %w to properly wrap for ErrorIs traversal + return fmt.Errorf("%w: %w", ErrStreamHealthCheckFailed, err) + } + p.l.Info("pktmon event stream verified - events flowing normally") + return nil + } +} + func (p *Plugin) GetFlow(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) defer cancel() diff --git a/pkg/plugin/pktmon/pktmon_windows_test.go b/pkg/plugin/pktmon/pktmon_windows_test.go new file mode 100644 index 0000000000..1c71b14670 --- /dev/null +++ b/pkg/plugin/pktmon/pktmon_windows_test.go @@ -0,0 +1,304 @@ +//go:build windows +// +build windows + +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// Package pktmon tests the pktmon plugin behavior on Windows, including the event stream +// health check which detects silent ETW registration failures when another consumer +// is already active on the EVENTS_MAP. +// +// The verifyEventStream() method is critical for detecting scenarios where: +// 1. Another ETW consumer prevents the pktmon server from registering +// 2. The registration appears successful but no events are being produced +// 3. Network traffic exists but isn't being captured +// +// These tests ensure that such failures are caught early rather than resulting in +// silent data loss and misleading metrics. +package pktmon + +import ( + "context" + "errors" + "testing" + "time" + + observerv1 "github.com/cilium/cilium/api/v1/observer" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/metadata" + + "github.com/microsoft/retina/pkg/log" +) + +var ( + errEOF = errors.New("EOF") + errConnectionLost = errors.New("connection lost") +) + +// MockGetFlowsClient implements observerv1.Observer_GetFlowsClient for testing +type MockGetFlowsClient struct { + // Sequence of responses to return + // Can be: nil (nil flow), "valid" (valid flow), error, or *observerv1.Flow (explicit flow) + responses []interface{} + index int + // Control test behavior + recvDelay time.Duration + blockCh chan struct{} // If set, block recv() forever + blockFor time.Duration // If > 0, block recv() for this duration +} + +// Recv returns the next mocked response +func (m *MockGetFlowsClient) Recv() (*observerv1.GetFlowsResponse, error) { + if m.recvDelay > 0 { + time.Sleep(m.recvDelay) + } + + if m.blockCh != nil { + <-m.blockCh + } + + if m.blockFor > 0 { + time.Sleep(m.blockFor) + } + + if m.index >= len(m.responses) { + return nil, errEOF + } + + resp := m.responses[m.index] + m.index++ + + // Return error if configured + if err, ok := resp.(error); ok { + return nil, err + } + + // Return response with non-nil flow if configured + if flow, ok := resp.(*observerv1.Flow); ok { + return &observerv1.GetFlowsResponse{ResponseTypes: &observerv1.GetFlowsResponse_Flow{Flow: flow}}, nil + } + + // Handle "valid" string marker - return a valid flow for testing + if marker, ok := resp.(string); ok && marker == "valid" { + validFlow := &observerv1.Flow{ + Source: &observerv1.Endpoint{PodName: "test-src"}, + Destination: &observerv1.Endpoint{PodName: "test-dst"}, + } + return &observerv1.GetFlowsResponse{ResponseTypes: &observerv1.GetFlowsResponse_Flow{Flow: validFlow}}, nil + } + + // For test purposes, return empty GetFlowsResponse with nil Flow + return &observerv1.GetFlowsResponse{}, nil +} + +// Header implements grpc.ClientStream +func (m *MockGetFlowsClient) Header() (metadata.MD, error) { + return nil, nil +} + +// Trailer implements grpc.ClientStream +func (m *MockGetFlowsClient) Trailer() metadata.MD { + return nil +} + +// CloseSend implements grpc.ClientStream +func (m *MockGetFlowsClient) CloseSend() error { + return nil +} + +// Context implements grpc.ClientStream +func (m *MockGetFlowsClient) Context() context.Context { + return context.Background() +} + +// SendMsg implements grpc.ClientStream +func (m *MockGetFlowsClient) SendMsg(_ interface{}) error { + return nil +} + +// RecvMsg implements grpc.ClientStream +func (m *MockGetFlowsClient) RecvMsg(_ interface{}) error { + return nil +} + +// Test_verifyEventStream_NoEventsTimeout tests timeout when no events are received +// With the new behavior, no events (due to lack of traffic) should return nil, +// not an error, since the stream connection itself is healthy. +// Note: verifyEventStream() uses an independent 60-second internal timeout, +// so the test context parameter is not used for timeout control. +func Test_verifyEventStream_NoEventsTimeout(t *testing.T) { + plugin := &Plugin{ + l: NewTestLogger(), + stream: &MockGetFlowsClient{ + blockCh: make(chan struct{}), // Block forever - will timeout internally + }, + } + + start := time.Now() + err := plugin.verifyEventStream() + elapsed := time.Since(start) + + require.NoError(t, err, "verifyEventStream should return nil when no events are received (timeout due to no traffic)") + // Should timeout after approximately pktmonHealthCheckTimeout (60s) + assert.GreaterOrEqual(t, elapsed, 59*time.Second, "should wait close to the 60s health check timeout") + assert.Less(t, elapsed, 62*time.Second, "should not wait significantly longer than the 60s timeout") +} + +// Test_verifyEventStream_StreamRecvError tests error handling when Recv() fails +// Actual connection errors (not timeouts) should still fail +func Test_verifyEventStream_StreamRecvError(t *testing.T) { + plugin := &Plugin{ + l: NewTestLogger(), + stream: &MockGetFlowsClient{ + responses: []interface{}{errConnectionLost}, + }, + } + + err := plugin.verifyEventStream() + + require.Error(t, err, "verifyEventStream should fail when Recv() returns an error") +} + +// Test_verifyEventStream_NilFlow tests that occasional nil flows are skipped +// The health check should continue waiting for valid flows rather than failing immediately +func Test_verifyEventStream_NilFlow(t *testing.T) { + plugin := &Plugin{ + l: NewTestLogger(), + stream: &MockGetFlowsClient{ + // Start with nil flow, then valid flow + responses: []interface{}{nil, "valid"}, + }, + } + + err := plugin.verifyEventStream() + + require.NoError(t, err, "verifyEventStream should skip occasional nil flows and succeed when valid flow arrives") + assert.Equal(t, 2, plugin.stream.(*MockGetFlowsClient).index, + "should consume both nil and valid flow") +} + +// Test_verifyEventStream_TooManyNilFlows tests that persistent nil flows are detected as proto mismatch +func Test_verifyEventStream_TooManyNilFlows(t *testing.T) { + plugin := &Plugin{ + l: NewTestLogger(), + stream: &MockGetFlowsClient{ + // More nil flows than allowed + responses: make([]interface{}, maxNilFlowsAllowed+2), + }, + } + + err := plugin.verifyEventStream() + + require.Error(t, err, "verifyEventStream should fail when too many nil flows are received") + // The error is wrapped as ErrStreamHealthCheckFailed which contains ErrTooManyNilFlows + assert.ErrorIs(t, err, ErrStreamHealthCheckFailed, "error should be a stream health check failure") +} + +// Test_verifyEventStream_EventWithDelay tests successful reception of delayed non-nil flow +// This verifies that the health check properly waits for a valid flow even with delay +func Test_verifyEventStream_EventWithDelay(t *testing.T) { + // Create a valid flow with basic structure + validFlow := &observerv1.Flow{ + Source: &observerv1.Endpoint{PodName: "pod-1", Namespace: "default"}, + Destination: &observerv1.Endpoint{PodName: "pod-2", Namespace: "default"}, + } + + plugin := &Plugin{ + l: NewTestLogger(), + stream: &MockGetFlowsClient{ + blockFor: 2 * time.Second, // Delay before first message + responses: []interface{}{validFlow}, // Then send valid non-nil flow + }, + } + + start := time.Now() + err := plugin.verifyEventStream() + elapsed := time.Since(start) + + require.NoError(t, err, "should succeed when valid non-nil flow eventually arrives") + assert.GreaterOrEqual(t, elapsed, 1900*time.Millisecond, "should wait at least 1.9s for blockFor delay") + assert.Less(t, elapsed, 3*time.Second, "should not wait much longer than the 2s blockFor") +} + +// Test_verifyEventStream_TimeoutExactlyAtLimit tests timeout enforcement +// When the health check timeout occurs (no traffic), it should return nil (success) +// Note: verifyEventStream() uses an independent 60-second internal context +func Test_verifyEventStream_TimeoutExactlyAtLimit(t *testing.T) { + plugin := &Plugin{ + l: NewTestLogger(), + stream: &MockGetFlowsClient{ + blockFor: pktmonHealthCheckTimeout + 100*time.Millisecond, + }, + } + + start := time.Now() + err := plugin.verifyEventStream() + elapsed := time.Since(start) + + require.NoError(t, err, "verifyEventStream should return nil when health check timeout occurs (no traffic)") + // Should timeout after approximately pktmonHealthCheckTimeout (60s) + expectedMin := pktmonHealthCheckTimeout - 500*time.Millisecond + expectedMax := pktmonHealthCheckTimeout + 2*time.Second + assert.GreaterOrEqual(t, elapsed, expectedMin, "should wait at least close to the health check timeout") + assert.Less(t, elapsed, expectedMax, "should not wait significantly longer than the health check timeout") +} + +// Test_verifyEventStream_MultipleEvents tests that verification succeeds with valid flows +func Test_verifyEventStream_MultipleEvents(t *testing.T) { + // Test that verifyEventStream succeeds when valid flows are available + validFlow := &observerv1.Flow{ + Source: &observerv1.Endpoint{PodName: "source", Namespace: "default"}, + Destination: &observerv1.Endpoint{PodName: "destination", Namespace: "default"}, + } + + plugin := &Plugin{ + l: NewTestLogger(), + stream: &MockGetFlowsClient{ + // Multiple valid flows + responses: []interface{}{validFlow, validFlow, validFlow}, + }, + } + + err := plugin.verifyEventStream() + + require.NoError(t, err, "should succeed when valid flows are available") +} + +// Test_verifyEventStream_NoTrafficLogsWarning tests that no-traffic timeout returns success +// Note: verifyEventStream() uses an independent 60-second internal context +func Test_verifyEventStream_NoTrafficLogsWarning(t *testing.T) { + plugin := &Plugin{ + l: NewTestLogger(), + stream: &MockGetFlowsClient{ + blockCh: make(chan struct{}), // Block forever - will timeout internally + }, + } + + err := plugin.verifyEventStream() + + require.NoError(t, err, "verifyEventStream should succeed when timeout occurs (no traffic scenario)") +} + +// Test_StartStream_WithNilGrpcClient tests error handling when grpcClient is nil +func Test_StartStream_WithNilGrpcClient(t *testing.T) { + plugin := &Plugin{ + l: NewTestLogger(), + grpcClient: nil, + } + + err := plugin.StartStream(context.Background()) + + require.Error(t, err, "StartStream should fail when grpcClient is nil") + assert.ErrorIs(t, err, ErrNilGrpcClient, "error should be ErrNilGrpcClient") +} + +// Helper function to create a test logger +func NewTestLogger() *log.ZapLogger { + // Create a simple logger for testing + testLogger, _ := log.SetupZapLogger(&log.LogOpts{ + Level: "info", + File: false, + }) + return testLogger +} diff --git a/pkg/plugin/tcpretrans/_cprog/tcpretrans.c b/pkg/plugin/tcpretrans/_cprog/tcpretrans.c new file mode 100644 index 0000000000..e6f1ee7c02 --- /dev/null +++ b/pkg/plugin/tcpretrans/_cprog/tcpretrans.c @@ -0,0 +1,192 @@ +//go:build ignore + +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// tcpretrans — eBPF tracepoint program for TCP retransmission tracking. +// +// Attaches to the kernel tracepoint "tcp/tcp_retransmit_skb" which fires +// every time the kernel retransmits a TCP segment. For each event we +// extract the 5-tuple (src/dst IP + port, protocol), TCP state, and the +// TCP flags byte, then push the result to userspace via a perf buffer. +// +// Key design decisions: +// +// Kernel version portability (CO-RE) +// This program is compiled once and must load on any kernel from 5.8+. +// (5.8 is the minimum because we use bpf_ktime_get_boot_ns, added in +// commit 71d19214776e; the tracepoint itself exists since 4.15.) +// The kernel can change struct layouts between versions, so we never +// hard-code field offsets. Instead every field read goes through +// BPF_CORE_READ / bpf_core_field_offset, which the loader (cilium/ebpf) +// patches at load time to match the running kernel's actual layout. +// +// Tracepoint struct rename (kernel 6.17) +// Older kernels expose the tracepoint context as +// "struct trace_event_raw_tcp_event_sk_skb" (shared with tcp_send_reset). +// Kernel 6.17 (commit ad892e912b84) converted tcp_retransmit_skb to its +// own TRACE_EVENT with a dedicated struct and an added "err" field. +// We define both names using libbpf's ___flavor suffix convention — the +// loader strips the suffix and matches whichever name exists in the +// running kernel's BTF. +// See: https://nakryiko.com/posts/bpf-core-reference-guide/#handling-incompatible-field-and-type-changes +// bpf_core_type_exists() picks the right branch; the verifier removes +// the dead one. +// +// Reading IPs from the sock, not the tracepoint context +// The tracepoint context has pre-copied IP fields (saddr/daddr), but +// reading them would require type-branching for both struct names. +// Instead we read IPs from the sock struct (sk->__sk_common.*), which +// has the same data and a stable layout across all kernel versions. +// +// Ref: https://github.com/inspektor-gadget/inspektor-gadget/blob/c414fc1/gadgets/trace_tcpretrans/program.bpf.c + +#include "vmlinux.h" +#include "bpf_helpers.h" +#include "bpf_core_read.h" + +char __license[] SEC("license") = "Dual MIT/GPL"; + +// Tracepoint context structs — two flavors for kernel version portability. +// +// The ___old / ___new suffixes are "CO-RE flavors": the loader strips +// everything after ___ when searching the kernel's BTF for a matching +// type. This avoids collisions with vmlinux.h (which may define either +// name depending on the kernel it was generated from). + +// Kernel < 6.17: shared struct for tcp_retransmit_skb and tcp_send_reset. +struct trace_event_raw_tcp_event_sk_skb___old { + struct trace_entry ent; + const void *skbaddr; // pointer to the retransmitted sk_buff + const void *skaddr; // pointer to the sock (TCP connection) + int state; // TCP state (ESTABLISHED, SYN_SENT, …) + __u16 sport; // source port + __u16 dport; // destination port + __u16 family; // address family (AF_INET / AF_INET6) + __u8 saddr[4]; // source IPv4 (pre-copied from sock) + __u8 daddr[4]; // dest IPv4 + __u8 saddr_v6[16]; // source IPv6 + __u8 daddr_v6[16]; // dest IPv6 + char __data[0]; +}; + +// Kernel >= 6.17: per-tracepoint struct, adds an 'err' field. +struct trace_event_raw_tcp_retransmit_skb___new { + struct trace_entry ent; + const void *skbaddr; + const void *skaddr; + int state; + __u16 sport; + __u16 dport; + __u16 family; + __u8 saddr[4]; + __u8 daddr[4]; + __u8 saddr_v6[16]; + __u8 daddr_v6[16]; + int err; // retransmission error code (new in 6.17) + char __data[0]; +}; + +struct tcpretrans_event { + __u64 timestamp; // boot time in nanoseconds (includes suspend) + __u32 src_ip; // source IPv4 (network byte order) + __u32 dst_ip; // destination IPv4 (network byte order) + __u32 state; // TCP state enum + __u16 src_port; // source port (host byte order) + __u16 dst_port; // destination port (host byte order) + __u8 src_ip6[16]; // source IPv6 address + __u8 dst_ip6[16]; // destination IPv6 address + __u8 tcpflags; // TCP flags byte (FIN/SYN/RST/PSH/ACK/URG/ECE/CWR) + __u8 af; // address family shorthand (4 = IPv4, 6 = IPv6) +}; + +// Needed by bpf2go's -type flag to generate the Go struct. +const struct tcpretrans_event *unused_tcpretrans_event __attribute__((unused)); + +// Perf buffer map — one ring per CPU, sized by the Go loader. +struct { + __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); + __uint(key_size, sizeof(__u32)); + __uint(value_size, sizeof(__u32)); +} retina_tcpretrans_events SEC(".maps"); + +// Helper macro — reads the five tracepoint fields we need regardless of +// which struct flavor is active. The unused branch is eliminated by the +// verifier at load time. +#define READ_TP_FIELDS(tp_type, ctx, out_state, out_sport, out_dport, \ + out_sk, out_skb) \ +do { \ + tp_type *tp = (tp_type *)(ctx); \ + (out_state) = BPF_CORE_READ(tp, state); \ + (out_sport) = BPF_CORE_READ(tp, sport); \ + (out_dport) = BPF_CORE_READ(tp, dport); \ + (out_sk) = (const struct sock *)BPF_CORE_READ(tp, skaddr); \ + (out_skb) = (const void *)BPF_CORE_READ(tp, skbaddr); \ +} while (0) + +SEC("tracepoint/tcp/tcp_retransmit_skb") +int retina_tcp_retransmit_skb(void *ctx) { + struct tcpretrans_event event = {}; + + event.timestamp = bpf_ktime_get_boot_ns(); + + const struct sock *sk; + const void *skbaddr; + + // Detect which tracepoint struct the running kernel uses. + if (bpf_core_type_exists(struct trace_event_raw_tcp_retransmit_skb___new)) { + READ_TP_FIELDS(struct trace_event_raw_tcp_retransmit_skb___new, + ctx, event.state, event.src_port, + event.dst_port, sk, skbaddr); + } else { + READ_TP_FIELDS(struct trace_event_raw_tcp_event_sk_skb___old, + ctx, event.state, event.src_port, + event.dst_port, sk, skbaddr); + } + + // Read address family and IPs from the sock. These fields live in + // sock_common (embedded in every sock) and are stable across versions. + __u16 family = 0; + BPF_CORE_READ_INTO(&family, sk, __sk_common.skc_family); + + if (family == 2) { // AF_INET + event.af = 4; + BPF_CORE_READ_INTO(&event.src_ip, sk, + __sk_common.skc_rcv_saddr); + BPF_CORE_READ_INTO(&event.dst_ip, sk, __sk_common.skc_daddr); + } else if (family == 10) { // AF_INET6 + event.af = 6; + BPF_CORE_READ_INTO(event.src_ip6, sk, + __sk_common.skc_v6_rcv_saddr.in6_u.u6_addr8); + BPF_CORE_READ_INTO(event.dst_ip6, sk, + __sk_common.skc_v6_daddr.in6_u.u6_addr8); + } else { + return 0; + } + + // Read TCP flags from the skb's control buffer. + // + // Every sk_buff has a 48-byte scratch area called "cb" (char[48]). + // For TCP, the kernel casts this area to "struct tcp_skb_cb" via the + // TCP_SKB_CB() macro: + // #define TCP_SKB_CB(__skb) ((struct tcp_skb_cb *)&((__skb)->cb[0])) + // See: https://github.com/torvalds/linux/blob/v6.12/include/net/tcp.h#L989 + // + // We mirror that cast here. Neither &skb->cb[0] nor &tcb->tcp_flags + // dereferences memory — they only compute addresses, with the compiler + // recording CO-RE relocations for both field offsets. The single + // bpf_probe_read_kernel() does the actual read. This is the same + // pattern used by inspektor-gadget's trace_tcpretrans: + // https://github.com/inspektor-gadget/inspektor-gadget/blob/c414fc1/gadgets/trace_tcpretrans/program.bpf.c#L158-L163 + if (skbaddr) { + struct sk_buff *skb = (struct sk_buff *)skbaddr; + struct tcp_skb_cb *tcb = (struct tcp_skb_cb *)&(skb->cb[0]); + bpf_probe_read_kernel(&event.tcpflags, sizeof(event.tcpflags), + &tcb->tcp_flags); + } + + bpf_perf_event_output(ctx, &retina_tcpretrans_events, BPF_F_CURRENT_CPU, + &event, sizeof(event)); + + return 0; +} diff --git a/pkg/plugin/tcpretrans/tcpretrans_arm64_bpfel.go b/pkg/plugin/tcpretrans/tcpretrans_arm64_bpfel.go new file mode 100644 index 0000000000..5b2826563c --- /dev/null +++ b/pkg/plugin/tcpretrans/tcpretrans_arm64_bpfel.go @@ -0,0 +1,149 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build arm64 + +package tcpretrans + +import ( + "bytes" + _ "embed" + "fmt" + "io" + + "github.com/cilium/ebpf" +) + +type tcpretransTcpretransEvent struct { + Timestamp uint64 + SrcIp uint32 + DstIp uint32 + State uint32 + SrcPort uint16 + DstPort uint16 + SrcIp6 [16]uint8 + DstIp6 [16]uint8 + Tcpflags uint8 + Af uint8 + _ [6]byte +} + +// loadTcpretrans returns the embedded CollectionSpec for tcpretrans. +func loadTcpretrans() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_TcpretransBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load tcpretrans: %w", err) + } + + return spec, err +} + +// loadTcpretransObjects loads tcpretrans and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *tcpretransObjects +// *tcpretransPrograms +// *tcpretransMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadTcpretransObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadTcpretrans() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// tcpretransSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type tcpretransSpecs struct { + tcpretransProgramSpecs + tcpretransMapSpecs + tcpretransVariableSpecs +} + +// tcpretransProgramSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type tcpretransProgramSpecs struct { + RetinaTcpRetransmitSkb *ebpf.ProgramSpec `ebpf:"retina_tcp_retransmit_skb"` +} + +// tcpretransMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type tcpretransMapSpecs struct { + RetinaTcpretransEvents *ebpf.MapSpec `ebpf:"retina_tcpretrans_events"` +} + +// tcpretransVariableSpecs contains global variables before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type tcpretransVariableSpecs struct { + UnusedTcpretransEvent *ebpf.VariableSpec `ebpf:"unused_tcpretrans_event"` +} + +// tcpretransObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadTcpretransObjects or ebpf.CollectionSpec.LoadAndAssign. +type tcpretransObjects struct { + tcpretransPrograms + tcpretransMaps + tcpretransVariables +} + +func (o *tcpretransObjects) Close() error { + return _TcpretransClose( + &o.tcpretransPrograms, + &o.tcpretransMaps, + ) +} + +// tcpretransMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadTcpretransObjects or ebpf.CollectionSpec.LoadAndAssign. +type tcpretransMaps struct { + RetinaTcpretransEvents *ebpf.Map `ebpf:"retina_tcpretrans_events"` +} + +func (m *tcpretransMaps) Close() error { + return _TcpretransClose( + m.RetinaTcpretransEvents, + ) +} + +// tcpretransVariables contains all global variables after they have been loaded into the kernel. +// +// It can be passed to loadTcpretransObjects or ebpf.CollectionSpec.LoadAndAssign. +type tcpretransVariables struct { + UnusedTcpretransEvent *ebpf.Variable `ebpf:"unused_tcpretrans_event"` +} + +// tcpretransPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadTcpretransObjects or ebpf.CollectionSpec.LoadAndAssign. +type tcpretransPrograms struct { + RetinaTcpRetransmitSkb *ebpf.Program `ebpf:"retina_tcp_retransmit_skb"` +} + +func (p *tcpretransPrograms) Close() error { + return _TcpretransClose( + p.RetinaTcpRetransmitSkb, + ) +} + +func _TcpretransClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed tcpretrans_arm64_bpfel.o +var _TcpretransBytes []byte diff --git a/pkg/plugin/tcpretrans/tcpretrans_arm64_bpfel.o b/pkg/plugin/tcpretrans/tcpretrans_arm64_bpfel.o new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pkg/plugin/tcpretrans/tcpretrans_ebpf_test.go b/pkg/plugin/tcpretrans/tcpretrans_ebpf_test.go new file mode 100644 index 0000000000..0f8a43c587 --- /dev/null +++ b/pkg/plugin/tcpretrans/tcpretrans_ebpf_test.go @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//go:build ebpf && linux + +// Tests for the tcpretrans BPF tracepoint program. +// +// These load the compiled BPF program, verify it passes the kernel verifier, +// and confirm it can attach to the tcp/tcp_retransmit_skb tracepoint. +// +// Unlike socket-filter or TC programs, tracepoint programs cannot be exercised +// with synthetic packets via BPF_PROG_TEST_RUN because the handler dereferences +// kernel pointers (skaddr, skbaddr) that cannot be mocked from userspace. +// The load+attach tests still provide significant value: they catch verifier +// regressions, CO-RE relocation failures, and missing tracepoints. +// +// Requires: root (or CAP_BPF+CAP_SYS_ADMIN), Linux kernel 5.8+ +// (bpf_ktime_get_boot_ns helper, commit 71d19214776e). +// Run: sudo go test -tags=ebpf -v -count=1 ./pkg/plugin/tcpretrans/... + +package tcpretrans + +import ( + "os" + "testing" + + "github.com/cilium/ebpf/link" + "github.com/cilium/ebpf/perf" + kcfg "github.com/microsoft/retina/pkg/config" + "github.com/microsoft/retina/pkg/log" + "github.com/microsoft/retina/pkg/plugin/ebpftest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func loadTestObjects(t *testing.T) *tcpretransObjects { + t.Helper() + ebpftest.RequirePrivileged(t) + + spec, err := loadTcpretrans() + require.NoError(t, err) + ebpftest.RemoveMapPinning(spec) + + var objs tcpretransObjects + err = spec.LoadAndAssign(&objs, nil) + require.NoError(t, err) + t.Cleanup(func() { objs.Close() }) + return &objs +} + +// TestBPFLoadAndVerify verifies the compiled BPF program passes the kernel +// verifier and all expected objects (program + perf event array) are created. +func TestBPFLoadAndVerify(t *testing.T) { + objs := loadTestObjects(t) + + assert.NotNil(t, objs.RetinaTcpRetransmitSkb, "tracepoint program should be loaded") + assert.NotNil(t, objs.RetinaTcpretransEvents, "perf event array map should be created") +} + +// TestBPFTracepointAttach verifies the program can attach to the +// tcp/tcp_retransmit_skb tracepoint (stable since kernel 4.15, commit e086101b150a). +func TestBPFTracepointAttach(t *testing.T) { + objs := loadTestObjects(t) + + tp, err := link.Tracepoint("tcp", "tcp_retransmit_skb", objs.RetinaTcpRetransmitSkb, nil) + require.NoError(t, err, "should attach to tcp/tcp_retransmit_skb tracepoint") + t.Cleanup(func() { tp.Close() }) +} + +// TestBPFPerfReaderCreate verifies a perf reader can be opened on the events map. +func TestBPFPerfReaderCreate(t *testing.T) { + objs := loadTestObjects(t) + + reader, err := perf.NewReader(objs.RetinaTcpretransEvents, os.Getpagesize()*4) + require.NoError(t, err, "should create perf reader") + t.Cleanup(func() { reader.Close() }) +} + +// TestBPFStopAfterInitWithoutStart exercises the fixed resource-leak path: +// Init() loads BPF objects and attaches the tracepoint, but Start() is never +// called. Stop() must still release all kernel resources. +func TestBPFStopAfterInitWithoutStart(t *testing.T) { + ebpftest.RequirePrivileged(t) + + log.SetupZapLogger(log.GetDefaultLogOpts()) + + p := New(&kcfg.Config{EnablePodLevel: true}) + require.NoError(t, p.Init()) + // Start() deliberately not called. + require.NoError(t, p.Stop(), "Stop() must clean up even without Start()") +} diff --git a/pkg/plugin/tcpretrans/tcpretrans_linux.go b/pkg/plugin/tcpretrans/tcpretrans_linux.go index 9895dc78a9..21d1681518 100644 --- a/pkg/plugin/tcpretrans/tcpretrans_linux.go +++ b/pkg/plugin/tcpretrans/tcpretrans_linux.go @@ -1,30 +1,46 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -// Package tcpretrans contains the Retina tcpretrans plugin. It utilizes inspektor-gadget to trace TCP retransmissions. +// Package tcpretrans contains the Retina tcpretrans plugin. It utilizes eBPF to trace TCP retransmissions. package tcpretrans import ( "context" + "errors" "fmt" "net" - "strings" + "unsafe" v1 "github.com/cilium/cilium/pkg/hubble/api/v1" - gadgetcontext "github.com/inspektor-gadget/inspektor-gadget/pkg/gadget-context" - "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/tcpretrans/tracer" - "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/tcpretrans/types" - "github.com/inspektor-gadget/inspektor-gadget/pkg/socketenricher" - "github.com/inspektor-gadget/inspektor-gadget/pkg/utils/host" + "github.com/cilium/ebpf/link" + "github.com/cilium/ebpf/perf" + "github.com/microsoft/retina/internal/ktime" kcfg "github.com/microsoft/retina/pkg/config" "github.com/microsoft/retina/pkg/enricher" "github.com/microsoft/retina/pkg/log" + "github.com/microsoft/retina/pkg/metrics" + plugincommon "github.com/microsoft/retina/pkg/plugin/common" "github.com/microsoft/retina/pkg/plugin/registry" "github.com/microsoft/retina/pkg/utils" "go.uber.org/zap" "golang.org/x/sys/unix" ) +// Per-arch target needed because vmlinux.h differs between amd64/arm64. +// Cross-generate: GOARCH=arm64 go generate ./pkg/plugin/tcpretrans/... +// +//go:generate go run github.com/cilium/ebpf/cmd/bpf2go@v0.18.0 -cflags "-Wall" -target ${GOARCH} -type tcpretrans_event tcpretrans ./_cprog/tcpretrans.c -- -I../lib/_${GOARCH} -I../lib/common/libbpf/_src + +const ( + // perCPUBuffer is the starting perf buffer size in pages per CPU. + // NewPerfReader halves this on ENOMEM down to 1 page. Retransmits are + // comparatively rare (much less frequent than packet drops), so 16 pages + // (64 KiB per CPU) is plenty — same starting point as dropreason. + perCPUBuffer = 16 + recordsBuffer = 500 // Channel buffer for records + workers = 2 // Number of worker goroutines +) + func init() { registry.Add(name, New) } @@ -40,33 +56,55 @@ func (t *tcpretrans) Name() string { return name } -func (t *tcpretrans) Generate(ctx context.Context) error { - return nil -} - -func (t *tcpretrans) Compile(ctx context.Context) error { - return nil -} +// Generate and Compile are no-ops. The plugin manager lifecycle requires them, +// but tcpretrans uses bpf2go which pre-compiles the BPF program at build time +// and embeds it in the binary — no runtime code generation or compilation needed. +func (t *tcpretrans) Generate(_ context.Context) error { return nil } +func (t *tcpretrans) Compile(_ context.Context) error { return nil } func (t *tcpretrans) Init() error { if !t.cfg.EnablePodLevel { t.l.Warn("tcpretrans will not init because pod level is disabled") return nil } - // Create tracer. In this case no parameters are passed. - if err := host.Init(host.Config{}); err != nil { - t.l.Error("failed to init host", zap.Error(err)) - return fmt.Errorf("failed to init host: %w", err) + + // tcpretrans has a single per-CPU perf event array and no shared maps, + // so there's nothing to pin — leaving CollectionOptions nil avoids + // dropping a dangling entry under /sys/fs/bpf. + objs := &tcpretransObjects{} + if err := loadTcpretransObjects(objs, nil); err != nil { + return fmt.Errorf("failed to load eBPF objects: %w", err) + } + // Clean up loaded objects if a later step fails. + ok := false + defer func() { + if !ok { + objs.Close() + } + }() + + // Attach to the tcp/tcp_retransmit_skb tracepoint (stable API, kernel 4.15+) + tp, err := link.Tracepoint("tcp", "tcp_retransmit_skb", objs.RetinaTcpRetransmitSkb, nil) + if err != nil { + return fmt.Errorf("failed to attach tracepoint tcp/tcp_retransmit_skb: %w", err) } - t.tracer = &tracer.Tracer{} - t.tracer.SetEventHandler(t.eventHandler) - socketEnricher, err := socketenricher.NewSocketEnricher() + defer func() { + if !ok { + tp.Close() + } + }() + + reader, err := plugincommon.NewPerfReader(t.l, objs.RetinaTcpretransEvents, perCPUBuffer, 1) if err != nil { - t.l.Error("failed to new socketEnricher", zap.Error(err)) - return fmt.Errorf("failed to new socketEnricher: %w", err) + return fmt.Errorf("failed to create perf reader: %w", err) } - t.tracer.SetSocketEnricherMap(socketEnricher.SocketsMap()) - t.l.Info("Initialized tcpretrans plugin") + + t.objs = objs + t.hooks = append(t.hooks, tp) + t.reader = reader + ok = true + + t.l.Info("tcpretrans plugin initialized") return nil } @@ -75,101 +113,188 @@ func (t *tcpretrans) Start(ctx context.Context) error { t.l.Warn("tcpretrans will not start because pod level is disabled") return nil } - // Set up enricher + if enricher.IsInitialized() { t.enricher = enricher.Instance() } else { - t.l.Error(errEnricherNotInitialized.Error()) - return errEnricherNotInitialized + t.l.Warn("retina enricher is not initialized") } - t.gadgetCtx = gadgetcontext.New(ctx, "tcpretrans", nil, nil, nil, nil, nil, nil, nil, nil, 0, nil) - err := t.tracer.Run(t.gadgetCtx) - if err != nil { - t.l.Error("Failed to run tracer", zap.Error(err)) - return err - } - t.l.Info("Started tcpretrans plugin") - return nil + t.recordsChannel = make(chan perf.Record, recordsBuffer) + + return t.run(ctx) } -func (t *tcpretrans) Stop() error { - if !t.cfg.EnablePodLevel { - return nil +func (t *tcpretrans) run(ctx context.Context) error { + for range workers { + t.wg.Add(1) + go t.processRecord(ctx) } - if t.gadgetCtx == nil { - t.l.Warn("tcpretrans plugin does not have a gadget context") - return nil + // readEvents is deliberately not tracked in wg: its reader.Read() call + // blocks until the perf reader is closed, which we do immediately below + // on ctx cancellation to unblock it. + go t.readEvents(ctx) + + <-ctx.Done() + // Close the reader before waiting on workers so readEvents unblocks + // from its pending Read() instead of racing to send records onto a + // channel that no worker is draining anymore. Safe to call Close twice + // — Stop() will no-op if the reader has already been closed. + if t.reader != nil { + if err := t.reader.Close(); err != nil { + t.l.Warn("failed to close perf reader", zap.Error(err)) + } } - t.gadgetCtx.Cancel() - t.l.Info("Stopped tcpretrans plugin") + t.wg.Wait() return nil } -func (t *tcpretrans) SetupChannel(ch chan *v1.Event) error { - t.l.Warn("SetupChannel is not supported by plugin", zap.String("plugin", name)) - return nil +func (t *tcpretrans) readEvents(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + default: + record, err := t.reader.Read() + if err != nil { + if errors.Is(err, perf.ErrClosed) { + return + } + t.l.Error("Error reading perf event", zap.Error(err)) + continue + } + + if record.LostSamples > 0 { + metrics.LostEventsCounter.WithLabelValues(utils.Kernel, name).Add(float64(record.LostSamples)) + continue + } + + select { + case t.recordsChannel <- record: + default: + metrics.LostEventsCounter.WithLabelValues(utils.BufferedChannel, name).Inc() + } + } + } +} + +func (t *tcpretrans) processRecord(ctx context.Context) { + defer t.wg.Done() + + for { + select { + case <-ctx.Done(): + return + case record := <-t.recordsChannel: + t.handleTCPRetransEvent(record) + } + } } -func (t *tcpretrans) eventHandler(event *types.Event) { - if event == nil { +func (t *tcpretrans) handleTCPRetransEvent(record perf.Record) { + eventSize := int(unsafe.Sizeof(tcpretransTcpretransEvent{})) + if len(record.RawSample) < eventSize { return } - if event.IPVersion != 4 { + event := (*tcpretransTcpretransEvent)(unsafe.Pointer(&record.RawSample[0])) //nolint:gosec // perf record is aligned + + var srcIP, dstIP net.IP + switch event.Af { + case 4: // IPv4 + var srcBuf, dstBuf [net.IPv4len]byte + *(*uint32)(unsafe.Pointer(&srcBuf[0])) = event.SrcIp //nolint:gosec // same size + *(*uint32)(unsafe.Pointer(&dstBuf[0])) = event.DstIp //nolint:gosec // same size + srcIP = srcBuf[:] + dstIP = dstBuf[:] + case 6: // IPv6 + srcIP = event.SrcIp6[:] + dstIP = event.DstIp6[:] + default: return } - // TODO add metric here or add a enriched value fl := utils.ToFlow( t.l, - int64(event.Timestamp), - net.ParseIP(event.SrcEndpoint.L3Endpoint.Addr).To4(), // Precautionary To4() call. - net.ParseIP(event.DstEndpoint.L3Endpoint.Addr).To4(), // Precautionary To4() call. - uint32(event.SrcEndpoint.Port), - uint32(event.DstEndpoint.Port), - unix.IPPROTO_TCP, // only TCP can have retransmissions - 0, // drop reason packet doesn't have a direction yet, so we set it to 0 + ktime.MonotonicOffset.Nanoseconds()+int64(event.Timestamp), //nolint:gosec // timestamp fits in int64 + srcIP, dstIP, + uint32(event.SrcPort), uint32(event.DstPort), + unix.IPPROTO_TCP, 0, utils.Verdict_RETRANSMISSION, ) - if fl == nil { - t.l.Warn("Could not convert tracer Event to flow", zap.Any("tracer event", event)) return } - syn, ack, fin, rst, psh, urg := getTcpFlags(event.Tcpflags) - utils.AddTCPFlags(fl, syn, ack, fin, rst, psh, urg) - // This is only for development purposes. - // Removing this makes logs way too chatter-y. - // dr.l.Debug("DropReason Packet Received", zap.Any("flow", fl), zap.Any("Raw Bpf Event", bpfEvent), zap.Uint32("drop type", bpfEvent.Key.DropType)) + syn := flagBit(event.Tcpflags, 0x02) + ack := flagBit(event.Tcpflags, 0x10) + fin := flagBit(event.Tcpflags, 0x01) + rst := flagBit(event.Tcpflags, 0x04) + psh := flagBit(event.Tcpflags, 0x08) + urg := flagBit(event.Tcpflags, 0x20) + ece := flagBit(event.Tcpflags, 0x40) + cwr := flagBit(event.Tcpflags, 0x80) + // NS is always 0: tcp_skb_cb->tcp_flags is a single byte that only holds + // byte 13 of the TCP header (FIN/SYN/RST/PSH/ACK/URG/ECE/CWR). NS lives + // in byte 12 and isn't carried in the control block. It's also effectively + // deprecated — RFC 8311 reclassified ECN nonce as historic — so preserving + // it would require parsing the full TCP header from the skb clone for no + // observable benefit. + utils.AddTCPFlags(fl, syn, ack, fin, rst, psh, urg, ece, cwr, 0) - // Write the event to the enricher. - t.enricher.Write(&v1.Event{ + ev := &v1.Event{ Event: fl, Timestamp: fl.Time, - }) + } + + if t.enricher != nil { + t.enricher.Write(ev) + } + + if t.externalChannel != nil { + select { + case t.externalChannel <- ev: + default: + metrics.LostEventsCounter.WithLabelValues(utils.ExternalChannel, name).Inc() + } + } } -func getTcpFlags(flags string) (syn, ack, fin, rst, psh, urg uint16) { - // this limiter is used in IG to put all the flags together - syn, ack, fin, rst, psh, urg = 0, 0, 0, 0, 0, 0 - result := strings.Split(flags, "|") - for _, flag := range result { - switch flag { - case "SYN": - syn = 1 - case "ACK": - ack = 1 - case "FIN": - fin = 1 - case "RST": - rst = 1 - case "PSH": - psh = 1 - case "URG": - urg = 1 +func (t *tcpretrans) Stop() error { + if !t.cfg.EnablePodLevel { + return nil + } + // Always clean up kernel resources regardless of whether Start() was + // called. Init() loads BPF objects, attaches the tracepoint, and + // creates a perf reader — all of which must be released. + if t.reader != nil { + // Idempotent: run() already closes the reader on normal shutdown, + // in which case cilium/ebpf returns nil here. + if err := t.reader.Close(); err != nil { + t.l.Warn("failed to close perf reader", zap.Error(err)) + } + } + for _, h := range t.hooks { + if err := h.Close(); err != nil { + t.l.Warn("failed to close hook", zap.Error(err)) + } + } + if t.objs != nil { + if err := t.objs.Close(); err != nil { + t.l.Warn("failed to close eBPF objects", zap.Error(err)) } } - return + return nil +} + +func (t *tcpretrans) SetupChannel(ch chan *v1.Event) error { + t.externalChannel = ch + return nil +} + +func flagBit(flags, bit uint8) uint16 { + if flags&bit != 0 { + return 1 + } + return 0 } diff --git a/pkg/plugin/tcpretrans/tcpretrans_linux_test.go b/pkg/plugin/tcpretrans/tcpretrans_linux_test.go new file mode 100644 index 0000000000..75c965fee0 --- /dev/null +++ b/pkg/plugin/tcpretrans/tcpretrans_linux_test.go @@ -0,0 +1,329 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package tcpretrans + +import ( + "log/slog" + "net" + "os" + "testing" + "unsafe" + + v1 "github.com/cilium/cilium/pkg/hubble/api/v1" + "github.com/cilium/ebpf/perf" + kcfg "github.com/microsoft/retina/pkg/config" + "github.com/microsoft/retina/pkg/enricher" + "github.com/microsoft/retina/pkg/log" + "github.com/microsoft/retina/pkg/metrics" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func TestMain(m *testing.M) { + _, _ = log.SetupZapLogger(log.GetDefaultLogOpts()) + metrics.InitializeMetrics(slog.Default()) + os.Exit(m.Run()) +} + +// --- helpers --- + +// buildTestRecord serialises a tcpretransTcpretransEvent into a perf.Record +// using the same memory layout the BPF perf ring produces. +func buildTestRecord(event tcpretransTcpretransEvent) perf.Record { + eventSize := int(unsafe.Sizeof(event)) + eventBytes := unsafe.Slice((*byte)(unsafe.Pointer(&event)), eventSize) //nolint:gosec // test-only + buf := make([]byte, eventSize) + copy(buf, eventBytes) + return perf.Record{RawSample: buf} +} + +// ipv4Native converts a dotted-quad string to the uint32 representation +// stored by the BPF program (network byte order bytes read as LE uint32). +func ipv4Native(s string) uint32 { + ip := net.ParseIP(s).To4() + return *(*uint32)(unsafe.Pointer(&ip[0])) //nolint:gosec // test-only +} + +// ipv6Bytes converts an IPv6 string to the [16]uint8 stored by the BPF program. +func ipv6Bytes(s string) [16]uint8 { + var b [16]uint8 + copy(b[:], net.ParseIP(s).To16()) + return b +} + +// newTestPlugin returns a minimal tcpretrans for unit testing with an +// external channel to receive emitted events. +func newTestPlugin(ch chan *v1.Event) *tcpretrans { + return &tcpretrans{ + cfg: &kcfg.Config{EnablePodLevel: true}, + l: log.Logger().Named(name), + externalChannel: ch, + } +} + +// --- constructor / lifecycle --- + +func TestNew(t *testing.T) { + cfg := &kcfg.Config{EnablePodLevel: true} + p := New(cfg) + require.NotNil(t, p) + assert.Equal(t, name, p.Name()) +} + +func TestSetupChannel(t *testing.T) { + p := &tcpretrans{ + cfg: &kcfg.Config{EnablePodLevel: true}, + l: log.Logger().Named(name), + } + ch := make(chan *v1.Event, 10) + require.NoError(t, p.SetupChannel(ch)) + assert.Equal(t, ch, p.externalChannel) +} + +func TestStop_PodLevelDisabled(t *testing.T) { + p := &tcpretrans{ + cfg: &kcfg.Config{EnablePodLevel: false}, + l: log.Logger().Named(name), + } + require.NoError(t, p.Stop()) +} + +func TestStop_NilResources(t *testing.T) { + // Stop() must not panic when Init() was never called (all resources nil). + p := &tcpretrans{ + cfg: &kcfg.Config{EnablePodLevel: true}, + l: log.Logger().Named(name), + } + require.NoError(t, p.Stop()) +} + +// --- flagBit --- + +func TestFlagBit(t *testing.T) { + tests := []struct { + name string + flags uint8 + bit uint8 + expected uint16 + }{ + {"SYN set", 0x02, 0x02, 1}, + {"SYN clear", 0x00, 0x02, 0}, + {"ACK set", 0x10, 0x10, 1}, + {"ACK clear", 0x02, 0x10, 0}, + {"FIN set", 0x01, 0x01, 1}, + {"RST set", 0x04, 0x04, 1}, + {"PSH set", 0x08, 0x08, 1}, + {"URG set", 0x20, 0x20, 1}, + {"ECE set", 0x40, 0x40, 1}, + {"CWR set", 0x80, 0x80, 1}, + {"all set", 0xFF, 0x02, 1}, + {"all set check FIN", 0xFF, 0x01, 1}, + {"none set", 0x00, 0xFF, 0}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, flagBit(tt.flags, tt.bit)) + }) + } +} + +// --- handleTCPRetransEvent --- + +func TestHandleTCPRetransEvent_IPv4(t *testing.T) { + ch := make(chan *v1.Event, 10) + p := newTestPlugin(ch) + + record := buildTestRecord(tcpretransTcpretransEvent{ + Timestamp: 1000, + SrcIp: ipv4Native("10.0.0.1"), + DstIp: ipv4Native("10.0.0.2"), + SrcPort: 12345, + DstPort: 80, + Tcpflags: 0x12, // SYN+ACK + Af: 4, + }) + + p.handleTCPRetransEvent(record) + + require.Len(t, ch, 1) + ev := <-ch + fl := ev.GetFlow() + require.NotNil(t, fl) + + assert.Equal(t, "10.0.0.1", fl.GetIP().GetSource()) + assert.Equal(t, "10.0.0.2", fl.GetIP().GetDestination()) + assert.Equal(t, uint32(12345), fl.GetL4().GetTCP().GetSourcePort()) + assert.Equal(t, uint32(80), fl.GetL4().GetTCP().GetDestinationPort()) + + flags := fl.GetL4().GetTCP().GetFlags() + require.NotNil(t, flags) + assert.True(t, flags.GetSYN(), "SYN should be set") + assert.True(t, flags.GetACK(), "ACK should be set") + assert.False(t, flags.GetFIN(), "FIN should not be set") + assert.False(t, flags.GetRST(), "RST should not be set") +} + +func TestHandleTCPRetransEvent_IPv6(t *testing.T) { + ch := make(chan *v1.Event, 10) + p := newTestPlugin(ch) + + srcIP := "fd00::1" + dstIP := "fd00::2" + + record := buildTestRecord(tcpretransTcpretransEvent{ + Timestamp: 2000, + SrcIp6: ipv6Bytes(srcIP), + DstIp6: ipv6Bytes(dstIP), + SrcPort: 44444, + DstPort: 443, + Tcpflags: 0x10, // ACK + Af: 6, + }) + + p.handleTCPRetransEvent(record) + + require.Len(t, ch, 1) + ev := <-ch + fl := ev.GetFlow() + require.NotNil(t, fl) + + assert.Equal(t, srcIP, fl.GetIP().GetSource()) + assert.Equal(t, dstIP, fl.GetIP().GetDestination()) + assert.Equal(t, uint32(44444), fl.GetL4().GetTCP().GetSourcePort()) + assert.Equal(t, uint32(443), fl.GetL4().GetTCP().GetDestinationPort()) +} + +func TestHandleTCPRetransEvent_AllFlags(t *testing.T) { + ch := make(chan *v1.Event, 10) + p := newTestPlugin(ch) + + record := buildTestRecord(tcpretransTcpretransEvent{ + Timestamp: 3000, + SrcIp: ipv4Native("1.2.3.4"), + DstIp: ipv4Native("5.6.7.8"), + SrcPort: 1000, + DstPort: 2000, + Tcpflags: 0xFF, + Af: 4, + }) + + p.handleTCPRetransEvent(record) + + require.Len(t, ch, 1) + flags := (<-ch).GetFlow().GetL4().GetTCP().GetFlags() + require.NotNil(t, flags) + + assert.True(t, flags.GetFIN(), "FIN") + assert.True(t, flags.GetSYN(), "SYN") + assert.True(t, flags.GetRST(), "RST") + assert.True(t, flags.GetPSH(), "PSH") + assert.True(t, flags.GetACK(), "ACK") + assert.True(t, flags.GetURG(), "URG") + assert.True(t, flags.GetECE(), "ECE") + assert.True(t, flags.GetCWR(), "CWR") + assert.False(t, flags.GetNS(), "NS is never set from tcp_skb_cb") +} + +func TestHandleTCPRetransEvent_NoFlags(t *testing.T) { + ch := make(chan *v1.Event, 10) + p := newTestPlugin(ch) + + record := buildTestRecord(tcpretransTcpretransEvent{ + Timestamp: 4000, + SrcIp: ipv4Native("1.2.3.4"), + DstIp: ipv4Native("5.6.7.8"), + SrcPort: 1000, + DstPort: 2000, + Tcpflags: 0x00, + Af: 4, + }) + + p.handleTCPRetransEvent(record) + + require.Len(t, ch, 1) + flags := (<-ch).GetFlow().GetL4().GetTCP().GetFlags() + require.NotNil(t, flags) + + assert.False(t, flags.GetFIN()) + assert.False(t, flags.GetSYN()) + assert.False(t, flags.GetRST()) + assert.False(t, flags.GetPSH()) + assert.False(t, flags.GetACK()) + assert.False(t, flags.GetURG()) + assert.False(t, flags.GetECE()) + assert.False(t, flags.GetCWR()) +} + +func TestHandleTCPRetransEvent_TruncatedRecord(t *testing.T) { + ch := make(chan *v1.Event, 10) + p := newTestPlugin(ch) + + // Record shorter than the event struct — must be silently dropped. + record := perf.Record{RawSample: []byte{0x01, 0x02, 0x03}} + p.handleTCPRetransEvent(record) + + assert.Empty(t, ch, "truncated record should not emit an event") +} + +func TestHandleTCPRetransEvent_UnknownAF(t *testing.T) { + ch := make(chan *v1.Event, 10) + p := newTestPlugin(ch) + + record := buildTestRecord(tcpretransTcpretransEvent{ + Timestamp: 5000, + Af: 99, // unsupported address family + }) + + p.handleTCPRetransEvent(record) + + assert.Empty(t, ch, "unknown AF should not emit an event") +} + +func TestHandleTCPRetransEvent_WithEnricher(t *testing.T) { + ctrl := gomock.NewController(t) + + menricher := enricher.NewMockEnricherInterface(ctrl) //nolint:typecheck // generated mock + menricher.EXPECT().Write(gomock.Any()).Times(1) + + ch := make(chan *v1.Event, 10) + p := newTestPlugin(ch) + p.enricher = menricher + + record := buildTestRecord(tcpretransTcpretransEvent{ + Timestamp: 6000, + SrcIp: ipv4Native("10.0.0.1"), + DstIp: ipv4Native("10.0.0.2"), + SrcPort: 5555, + DstPort: 80, + Tcpflags: 0x10, + Af: 4, + }) + + p.handleTCPRetransEvent(record) + + require.Len(t, ch, 1) +} + +func TestHandleTCPRetransEvent_ChannelFull(t *testing.T) { + // When the external channel is full, the event is dropped (not blocking). + ch := make(chan *v1.Event) // unbuffered — will always be full + p := newTestPlugin(ch) + + record := buildTestRecord(tcpretransTcpretransEvent{ + Timestamp: 7000, + SrcIp: ipv4Native("10.0.0.1"), + DstIp: ipv4Native("10.0.0.2"), + SrcPort: 1111, + DstPort: 2222, + Tcpflags: 0x02, + Af: 4, + }) + + // Must not block. + p.handleTCPRetransEvent(record) + + // Channel still empty (nobody reading), event was dropped. + assert.Empty(t, ch) +} diff --git a/pkg/plugin/tcpretrans/tcpretrans_x86_bpfel.go b/pkg/plugin/tcpretrans/tcpretrans_x86_bpfel.go new file mode 100644 index 0000000000..e4e9f03744 --- /dev/null +++ b/pkg/plugin/tcpretrans/tcpretrans_x86_bpfel.go @@ -0,0 +1,149 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build 386 || amd64 + +package tcpretrans + +import ( + "bytes" + _ "embed" + "fmt" + "io" + + "github.com/cilium/ebpf" +) + +type tcpretransTcpretransEvent struct { + Timestamp uint64 + SrcIp uint32 + DstIp uint32 + State uint32 + SrcPort uint16 + DstPort uint16 + SrcIp6 [16]uint8 + DstIp6 [16]uint8 + Tcpflags uint8 + Af uint8 + _ [6]byte +} + +// loadTcpretrans returns the embedded CollectionSpec for tcpretrans. +func loadTcpretrans() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_TcpretransBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load tcpretrans: %w", err) + } + + return spec, err +} + +// loadTcpretransObjects loads tcpretrans and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *tcpretransObjects +// *tcpretransPrograms +// *tcpretransMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadTcpretransObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadTcpretrans() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// tcpretransSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type tcpretransSpecs struct { + tcpretransProgramSpecs + tcpretransMapSpecs + tcpretransVariableSpecs +} + +// tcpretransProgramSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type tcpretransProgramSpecs struct { + RetinaTcpRetransmitSkb *ebpf.ProgramSpec `ebpf:"retina_tcp_retransmit_skb"` +} + +// tcpretransMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type tcpretransMapSpecs struct { + RetinaTcpretransEvents *ebpf.MapSpec `ebpf:"retina_tcpretrans_events"` +} + +// tcpretransVariableSpecs contains global variables before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type tcpretransVariableSpecs struct { + UnusedTcpretransEvent *ebpf.VariableSpec `ebpf:"unused_tcpretrans_event"` +} + +// tcpretransObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadTcpretransObjects or ebpf.CollectionSpec.LoadAndAssign. +type tcpretransObjects struct { + tcpretransPrograms + tcpretransMaps + tcpretransVariables +} + +func (o *tcpretransObjects) Close() error { + return _TcpretransClose( + &o.tcpretransPrograms, + &o.tcpretransMaps, + ) +} + +// tcpretransMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadTcpretransObjects or ebpf.CollectionSpec.LoadAndAssign. +type tcpretransMaps struct { + RetinaTcpretransEvents *ebpf.Map `ebpf:"retina_tcpretrans_events"` +} + +func (m *tcpretransMaps) Close() error { + return _TcpretransClose( + m.RetinaTcpretransEvents, + ) +} + +// tcpretransVariables contains all global variables after they have been loaded into the kernel. +// +// It can be passed to loadTcpretransObjects or ebpf.CollectionSpec.LoadAndAssign. +type tcpretransVariables struct { + UnusedTcpretransEvent *ebpf.Variable `ebpf:"unused_tcpretrans_event"` +} + +// tcpretransPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadTcpretransObjects or ebpf.CollectionSpec.LoadAndAssign. +type tcpretransPrograms struct { + RetinaTcpRetransmitSkb *ebpf.Program `ebpf:"retina_tcp_retransmit_skb"` +} + +func (p *tcpretransPrograms) Close() error { + return _TcpretransClose( + p.RetinaTcpRetransmitSkb, + ) +} + +func _TcpretransClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed tcpretrans_x86_bpfel.o +var _TcpretransBytes []byte diff --git a/pkg/plugin/tcpretrans/tcpretrans_x86_bpfel.o b/pkg/plugin/tcpretrans/tcpretrans_x86_bpfel.o new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pkg/plugin/tcpretrans/types_linux.go b/pkg/plugin/tcpretrans/types_linux.go index b3856bbe3e..e48e5812ab 100644 --- a/pkg/plugin/tcpretrans/types_linux.go +++ b/pkg/plugin/tcpretrans/types_linux.go @@ -3,10 +3,10 @@ package tcpretrans import ( - "errors" + "sync" - gadgetcontext "github.com/inspektor-gadget/inspektor-gadget/pkg/gadget-context" - "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/tcpretrans/tracer" + v1 "github.com/cilium/cilium/pkg/hubble/api/v1" + "github.com/cilium/ebpf/perf" kcfg "github.com/microsoft/retina/pkg/config" "github.com/microsoft/retina/pkg/enricher" "github.com/microsoft/retina/pkg/log" @@ -15,11 +15,13 @@ import ( const name = "tcpretrans" type tcpretrans struct { - cfg *kcfg.Config - l *log.ZapLogger - tracer *tracer.Tracer - gadgetCtx *gadgetcontext.GadgetContext - enricher enricher.EnricherInterface + cfg *kcfg.Config + l *log.ZapLogger + enricher enricher.EnricherInterface + externalChannel chan *v1.Event + objs *tcpretransObjects + reader *perf.Reader + hooks []interface{ Close() error } + recordsChannel chan perf.Record + wg sync.WaitGroup } - -var errEnricherNotInitialized = errors.New("enricher not initialized") diff --git a/pkg/servermanager/cell_linux.go b/pkg/servermanager/cell_linux.go index 5ab934284f..237df90479 100644 --- a/pkg/servermanager/cell_linux.go +++ b/pkg/servermanager/cell_linux.go @@ -3,12 +3,12 @@ package servermanager import ( "context" "fmt" + "log/slog" "sync" "github.com/cilium/hive/cell" "github.com/microsoft/retina/pkg/config" sm "github.com/microsoft/retina/pkg/managers/servermanager" - "github.com/sirupsen/logrus" ) var Cell = cell.Module( @@ -20,18 +20,18 @@ var Cell = cell.Module( type serverParams struct { cell.In - Log logrus.FieldLogger + Log *slog.Logger Lifecycle cell.Lifecycle Config config.Config } func newServerManager(params serverParams) (*sm.HTTPServer, error) { - logger := params.Log.WithField("module", "servermanager") + logger := params.Log.With("module", "servermanager") serverCtx, cancelCtx := context.WithCancel(context.Background()) serverManager := sm.NewHTTPServer(params.Config.APIServer.Host, params.Config.APIServer.Port) if err := serverManager.Init(); err != nil { - logger.WithError(err).Error("Unable to initialize Http server") + logger.Error("Unable to initialize Http server", "error", err) cancelCtx() return nil, fmt.Errorf("unable to initialize Http server: %w", err) } @@ -43,7 +43,7 @@ func newServerManager(params serverParams) (*sm.HTTPServer, error) { go func() { defer wg.Done() if err := serverManager.Start(serverCtx); err != nil { - logger.WithError(err).Error("Unable to start server") + logger.Error("Unable to start server", "error", err) } }() diff --git a/pkg/shared/telemetry/cell_linux.go b/pkg/shared/telemetry/cell_linux.go index a43b4df8d1..f4561116e6 100644 --- a/pkg/shared/telemetry/cell_linux.go +++ b/pkg/shared/telemetry/cell_linux.go @@ -3,12 +3,12 @@ package telemetry import ( "context" "fmt" + "log/slog" "strings" "time" "github.com/cilium/hive/cell" "github.com/microsoft/retina/pkg/telemetry" - "github.com/sirupsen/logrus" "k8s.io/client-go/rest" ) @@ -34,11 +34,11 @@ var ( Constructor = cell.Module( "telemetry", "provides telemetry", - cell.Provide(func(p params, l logrus.FieldLogger) (telemetry.Telemetry, error) { - l.WithFields(logrus.Fields{ - "app-insights-id": p.Config.ApplicationInsightsID, - "retina-version": p.Config.RetinaVersion, - }).Info("configuring telemetry") + cell.Provide(func(p params, l *slog.Logger) (telemetry.Telemetry, error) { + l.Info("configuring telemetry", + "app-insights-id", p.Config.ApplicationInsightsID, + "retina-version", p.Config.RetinaVersion, + ) if p.Config.EnableTelemetry { if p.Config.ApplicationInsightsID == "" { @@ -75,7 +75,7 @@ var ( "heartbeat", "sends periodic telemetry heartbeat", cell.Invoke( - func(tel telemetry.Telemetry, lifecycle cell.Lifecycle, l logrus.FieldLogger) { + func(tel telemetry.Telemetry, lifecycle cell.Lifecycle, l *slog.Logger) { ctx, cancelCtx := context.WithCancel(context.Background()) lifecycle.Append(cell.Hook{ OnStart: func(cell.HookContext) error { diff --git a/pkg/telemetry/heartbeat_unix.go b/pkg/telemetry/heartbeat_unix.go index dc20736099..ae725cabb1 100644 --- a/pkg/telemetry/heartbeat_unix.go +++ b/pkg/telemetry/heartbeat_unix.go @@ -7,17 +7,15 @@ package telemetry import ( "context" - "os/exec" - "strings" + "github.com/microsoft/retina/pkg/utils" "github.com/pkg/errors" ) -func KernelVersion(ctx context.Context) (string, error) { - cmd := exec.CommandContext(ctx, "uname", "-r") - output, err := cmd.CombinedOutput() +func KernelVersion(context.Context) (string, error) { + release, err := utils.KernelRelease() if err != nil { - return "", errors.Wrapf(err, "failed to get linux kernel version: %s", string(output)) + return "", errors.Wrap(err, "failed to get linux kernel version") } - return strings.TrimSuffix(string(output), "\n"), nil + return release, nil } diff --git a/pkg/telemetry/noop_telemetry.go b/pkg/telemetry/noop_telemetry.go index 8391af105c..12fee02381 100644 --- a/pkg/telemetry/noop_telemetry.go +++ b/pkg/telemetry/noop_telemetry.go @@ -23,7 +23,7 @@ func (m NoopTelemetry) StartPerf(functionName string) *PerformanceCounter { func (m NoopTelemetry) StopPerf(counter *PerformanceCounter) { } -func (m NoopTelemetry) Heartbeat(ctx context.Context, heartbeatInterval time.Duration) { +func (m NoopTelemetry) Heartbeat(_ context.Context, _ time.Duration, _ ...func() map[string]string) { } func (m NoopTelemetry) TrackEvent(name string, properties map[string]string) { diff --git a/pkg/telemetry/telemetry.go b/pkg/telemetry/telemetry.go index 0c91f32e67..abbd3a22f4 100644 --- a/pkg/telemetry/telemetry.go +++ b/pkg/telemetry/telemetry.go @@ -48,7 +48,10 @@ var ( type Telemetry interface { StartPerf(name string) *PerformanceCounter StopPerf(counter *PerformanceCounter) - Heartbeat(ctx context.Context, heartbeatInterval time.Duration) + // Heartbeat sends a heartbeat event with system metrics and custom properties. + // funcs are optional functions that return additional properties to be included in the heartbeat event. + // Add custom data with caution as it will increase the size of Heartbeat obejct and may infer storage costs + Heartbeat(ctx context.Context, heartbeatInterval time.Duration, funcs ...func() map[string]string) TrackEvent(name string, properties map[string]string) TrackMetric(name string, value float64, properties map[string]string) TrackTrace(name string, severity contracts.SeverityLevel, properties map[string]string) @@ -102,6 +105,13 @@ func NewAppInsightsTelemetryClient(processName string, additionalproperties map[ properties := GetEnvironmentProperties() + kernelVer, err := KernelVersion(context.Background()) + if err != nil { + fmt.Printf("failed to get kernel version for telemetry properties: %v\n", err) + } else { + properties[kernelversion] = kernelVer + } + for k, v := range additionalproperties { properties[k] = v } @@ -167,7 +177,7 @@ func (t *TelemetryClient) trackWarning(err error, msg string) { t.TrackTrace(msg+": "+err.Error(), contracts.Warning, GetEnvironmentProperties()) } -func (t *TelemetryClient) heartbeat(ctx context.Context) { +func (t *TelemetryClient) heartbeat(ctx context.Context, funcs ...func() map[string]string) { kernelVersion, err := KernelVersion(ctx) if err != nil { t.trackWarning(err, "failed to get kernel version") @@ -189,6 +199,10 @@ func (t *TelemetryClient) heartbeat(ctx context.Context) { props["metricscardinality"] = strconv.Itoa(metricscardinality) + for _, f := range funcs { + maps.Copy(props, f()) + } + maps.Copy(props, cpuProps) maps.Copy(props, t.profile.GetMemoryUsage()) t.TrackEvent("heartbeat", props) @@ -352,7 +366,7 @@ func (t *TelemetryClient) StopPerf(counter *PerformanceCounter) { t.TrackMetric(counter.functionName, ms, nil) } -func (t *TelemetryClient) Heartbeat(ctx context.Context, heartbeatInterval time.Duration) { +func (t *TelemetryClient) Heartbeat(ctx context.Context, heartbeatInterval time.Duration, funcs ...func() map[string]string) { ticker := time.NewTicker(heartbeatInterval) // TODOL: make configurable defer ticker.Stop() @@ -361,7 +375,7 @@ func (t *TelemetryClient) Heartbeat(ctx context.Context, heartbeatInterval time. case <-ctx.Done(): return case <-ticker.C: - t.heartbeat(ctx) + t.heartbeat(ctx, funcs...) } } } diff --git a/pkg/telemetry/telemetry_test.go b/pkg/telemetry/telemetry_test.go index 6dfae581db..7eccb09459 100644 --- a/pkg/telemetry/telemetry_test.go +++ b/pkg/telemetry/telemetry_test.go @@ -68,7 +68,8 @@ func TestHeartbeat(t *testing.T) { properties map[string]string } type args struct { - ctx context.Context + ctx context.Context + funcs []func() map[string]string } tests := []struct { name string @@ -84,6 +85,26 @@ func TestHeartbeat(t *testing.T) { }, args: args{ ctx: context.Background(), + funcs: []func() map[string]string{ + func() map[string]string { + return map[string]string{ + "customLabel1": "value1", + "customLabel2": "value2", + } + }, + }, + }, + }, + { + name: "test heartbeat with labels", + fields: fields{ + properties: map[string]string{ + "test": "test", + }, + }, + args: args{ + ctx: context.Background(), + funcs: []func() map[string]string{}, }, }, } @@ -94,7 +115,7 @@ func TestHeartbeat(t *testing.T) { properties: tt.fields.properties, profile: NewNoopPerfProfile(), } - tr.heartbeat(tt.args.ctx) + tr.heartbeat(tt.args.ctx, tt.args.funcs...) }) } } diff --git a/pkg/utils/attr_utils.go b/pkg/utils/attr_utils.go index e5266e9b02..5223b34772 100644 --- a/pkg/utils/attr_utils.go +++ b/pkg/utils/attr_utils.go @@ -33,6 +33,7 @@ var ( Type = "type" Reason = "reason" Direction = "direction" + IsReply = "is_reply" SourceNodeName = "source_node_name" TargetNodeName = "target_node_name" State = "state" @@ -46,6 +47,7 @@ var ( AclRule = "aclrule" Active = "ACTIVE" Device = "device" + Metric = "metric" // TCP Connection Statistic Names ResetCount = "ResetCount" diff --git a/pkg/utils/flow_utils.go b/pkg/utils/flow_utils.go index cffbeed2f9..6d3da9c243 100644 --- a/pkg/utils/flow_utils.go +++ b/pkg/utils/flow_utils.go @@ -11,20 +11,38 @@ import ( "github.com/microsoft/retina/pkg/log" "go.uber.org/zap" "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/wrapperspb" ) +// Extension field keys for structpb.Struct +const ( + ExtKeyBytes = "bytes" + ExtKeyDNSType = "dns_type" + ExtKeyNumResponses = "num_responses" + ExtKeyTCPID = "tcp_id" + ExtKeyDropReason = "drop_reason" + ExtKeyPrevObservedPackets = "previously_observed_packets" + ExtKeyPrevObservedBytes = "previously_observed_bytes" + ExtKeyPrevObservedTCPFlags = "previously_observed_tcp_flags" + ExtKeySourceZone = "source_zone" + ExtKeyDestinationZone = "destination_zone" + + zoneUnknown = "unknown" +) + // Additional Verdicts to be used for flow objects const ( - Verdict_RETRANSMISSION flow.Verdict = 15 - Verdict_DNS flow.Verdict = 16 - TypeUrl string = "retina.sh" + Verdict_RETRANSMISSION flow.Verdict = 15 //nolint:revive,stylecheck // existing API, renaming would break callers + Verdict_DNS flow.Verdict = 16 //nolint:revive,stylecheck // existing API, renaming would break callers + TypeUrl string = "retina.sh" //nolint:revive,stylecheck // existing API, renaming would break callers ) // ToFlow returns a flow.Flow object. // This sets up a L3/L4 flow object. -// sourceIP, destIP are IPv4 addresses. +// sourceIP, destIP are IPv4 or IPv6 addresses; IpVersion is derived from the +// address family via net.IP.To4(). // sourcePort, destPort are TCP/UDP ports. // proto is the protocol number. Ref: https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml . // observationPoint is the observation point+direction of the flow. 0 is from n/w stack to container, 1 is from container to stack, @@ -95,7 +113,10 @@ func ToFlow( verdict = flow.Verdict_FORWARDED } - ext, _ := anypb.New(&RetinaMetadata{}) //nolint:typecheck + ipVersion := flow.IPVersion_IPv4 + if sourceIP.To4() == nil { + ipVersion = flow.IPVersion_IPv6 + } f := &flow.Flow{ Type: flow.FlowType_L3_L4, @@ -106,15 +127,13 @@ func ToFlow( IP: &flow.IP{ Source: sourceIP.String(), Destination: destIP.String(), - // We only support IPv4 for now. - IpVersion: flow.IPVersion_IPv4, + IpVersion: ipVersion, }, L4: l4, TraceObservationPoint: checkpoint, // Packetparser running with conntrack can determine the traffic direction correctly and will override this value. TrafficDirection: direction, Verdict: verdict, - Extensions: ext, // Setting IsReply to false by default. // Packetparser running with conntrack can determine the direction of the flow, and will override this value. IsReply: &wrapperspb.BoolValue{Value: false}, @@ -127,13 +146,35 @@ func ToFlow( return f } -// AddRetinaMetadata adds the RetinaMetadata to the flow's extensions field. -func AddRetinaMetadata(f *flow.Flow, meta *RetinaMetadata) { - ext, _ := anypb.New(meta) +// NewExtensions creates a new structpb.Struct for use as flow extensions. +func NewExtensions() *structpb.Struct { + return &structpb.Struct{Fields: make(map[string]*structpb.Value)} +} + +// SetExtensions wraps the struct in Any and sets it on the flow. +// Only call this after populating all extension fields. +func SetExtensions(f *flow.Flow, s *structpb.Struct) { + if f == nil || s == nil || len(s.GetFields()) == 0 { + return + } + ext, _ := anypb.New(s) f.Extensions = ext } -func AddTCPFlags(f *flow.Flow, syn, ack, fin, rst, psh, urg uint16) { +// GetExtensionsStruct extracts the structpb.Struct from flow.Extensions. +// Returns nil if Extensions is nil or not a Struct. +func GetExtensionsStruct(f *flow.Flow) *structpb.Struct { + if f == nil || f.GetExtensions() == nil { + return nil + } + s := &structpb.Struct{} + if err := f.GetExtensions().UnmarshalTo(s); err != nil { + return nil + } + return s +} + +func AddTCPFlags(f *flow.Flow, syn, ack, fin, rst, psh, urg, ece, cwr, ns uint16) { if f.GetL4().GetTCP() == nil { return } @@ -145,7 +186,89 @@ func AddTCPFlags(f *flow.Flow, syn, ack, fin, rst, psh, urg uint16) { RST: rst == uint16(1), PSH: psh == uint16(1), URG: urg == uint16(1), + ECE: ece == uint16(1), + CWR: cwr == uint16(1), + NS: ns == uint16(1), + } +} + +// AddPreviouslyObservedTCPFlags adds the previously observed TCP flags to the flow's extensions. +func AddPreviouslyObservedTCPFlags(s *structpb.Struct, syn, ack, fin, rst, psh, urg, ece, cwr, ns uint32) { + if s == nil { + return + } + // Only add if at least one flag is non-zero + if syn == 0 && ack == 0 && fin == 0 && rst == 0 && psh == 0 && urg == 0 && ece == 0 && cwr == 0 && ns == 0 { + return + } + tcpFlags := &structpb.Struct{Fields: map[string]*structpb.Value{ + SYN: structpb.NewNumberValue(float64(syn)), + ACK: structpb.NewNumberValue(float64(ack)), + FIN: structpb.NewNumberValue(float64(fin)), + RST: structpb.NewNumberValue(float64(rst)), + PSH: structpb.NewNumberValue(float64(psh)), + URG: structpb.NewNumberValue(float64(urg)), + ECE: structpb.NewNumberValue(float64(ece)), + CWR: structpb.NewNumberValue(float64(cwr)), + NS: structpb.NewNumberValue(float64(ns)), + }} + s.GetFields()[ExtKeyPrevObservedTCPFlags] = structpb.NewStructValue(tcpFlags) +} + +func PreviouslyObservedTCPFlags(f *flow.Flow) map[string]uint32 { + s := GetExtensionsStruct(f) + if s == nil { + return nil + } + v, ok := s.GetFields()[ExtKeyPrevObservedTCPFlags] + if !ok || v.GetStructValue() == nil { + return nil + } + result := make(map[string]uint32) + for k, val := range v.GetStructValue().GetFields() { + result[k] = uint32(val.GetNumberValue()) + } + return result +} + +// AddPreviouslyObservedBytes adds the previously observed bytes to the flow's extensions. +func AddPreviouslyObservedBytes(s *structpb.Struct, bytes uint32) { + if s == nil || bytes == 0 { + return + } + s.GetFields()[ExtKeyPrevObservedBytes] = structpb.NewNumberValue(float64(bytes)) +} + +func PreviouslyObservedBytes(f *flow.Flow) uint32 { + s := GetExtensionsStruct(f) + if s == nil { + return 0 + } + v, ok := s.GetFields()[ExtKeyPrevObservedBytes] + if !ok { + return 0 + } + return uint32(v.GetNumberValue()) +} + +// AddPreviouslyObservedPackets adds the previously observed packets to the flow's extensions. +func AddPreviouslyObservedPackets(s *structpb.Struct, packets uint32) { + if s == nil || packets == 0 { + return } + s.GetFields()[ExtKeyPrevObservedPackets] = structpb.NewNumberValue(float64(packets)) +} + +func PreviouslyObservedPackets(f *flow.Flow) uint32 { + s := GetExtensionsStruct(f) + if s == nil { + return 0 + } + v, ok := s.GetFields()[ExtKeyPrevObservedPackets] + if !ok { + return 0 + } + return uint32(v.GetNumberValue()) } func AddTCPFlagsBool(f *flow.Flow, syn, ack, fin, rst, psh, urg bool) { @@ -163,28 +286,37 @@ func AddTCPFlagsBool(f *flow.Flow, syn, ack, fin, rst, psh, urg bool) { } } -// Add TSval/TSecr to the flow's metadata as TCP ID. +// AddTCPID adds TSval/TSecr to the flow's extensions as TCP ID. // The TSval/TSecr works as ID for the flow. // We will use this ID to calculate latency. -func AddTCPID(meta *RetinaMetadata, id uint64) { - if meta == nil { +func AddTCPID(s *structpb.Struct, id uint64) { + if s == nil || id == 0 { return } - meta.TcpId = id + s.GetFields()[ExtKeyTCPID] = structpb.NewNumberValue(float64(id)) } func GetTCPID(f *flow.Flow) uint64 { if f.GetL4() == nil || f.GetL4().GetTCP() == nil { return 0 } - k := &RetinaMetadata{} //nolint:typecheck - f.Extensions.UnmarshalTo(k) //nolint:errcheck - return k.TcpId + s := GetExtensionsStruct(f) + if s == nil { + return 0 + } + v, ok := s.GetFields()[ExtKeyTCPID] + if !ok { + return 0 + } + return uint64(v.GetNumberValue()) } -// AddDNSInfo adds DNS information to the flow's metadata. -func AddDNSInfo(f *flow.Flow, meta *RetinaMetadata, qType string, rCode uint32, query string, qTypes []string, numAnswers int, ips []string) { - if f == nil || meta == nil { +// AddDNSInfo adds DNS information to the flow and its extensions. +func AddDNSInfo( + f *flow.Flow, s *structpb.Struct, qType string, rCode uint32, + query string, qTypes []string, numAnswers int, ips []string, +) { + if f == nil || s == nil { return } // Set type to L7. @@ -206,17 +338,18 @@ func AddDNSInfo(f *flow.Flow, meta *RetinaMetadata, qType string, rCode uint32, } switch qType { case "Q": - meta.DnsType = DNSType_QUERY + s.GetFields()[ExtKeyDNSType] = structpb.NewStringValue(DNSType_QUERY.String()) f.L7.Type = flow.L7FlowType_REQUEST case "R": - meta.DnsType = DNSType_RESPONSE + s.GetFields()[ExtKeyDNSType] = structpb.NewStringValue(DNSType_RESPONSE.String()) f.L7.Type = flow.L7FlowType_RESPONSE f.IsReply = &wrapperspb.BoolValue{Value: true} // we can definitely say that this is a reply default: - meta.DnsType = DNSType_UNKNOWN f.L7.Type = flow.L7FlowType_UNKNOWN_L7_TYPE } - meta.NumResponses = uint32(numAnswers) + if numAnswers > 0 { + s.GetFields()[ExtKeyNumResponses] = structpb.NewNumberValue(float64(numAnswers)) + } } func GetDNS(f *flow.Flow) (*flow.DNS, DNSType, uint32) { @@ -224,13 +357,27 @@ func GetDNS(f *flow.Flow) (*flow.DNS, DNSType, uint32) { return nil, DNSType_UNKNOWN, 0 } dns := f.L7.GetDns() - if f.Extensions == nil { + s := GetExtensionsStruct(f) + if s == nil { return dns, DNSType_UNKNOWN, 0 } - k := &RetinaMetadata{} //nolint:typecheck - f.Extensions.UnmarshalTo(k) //nolint:errcheck - return dns, k.DnsType, k.NumResponses + dnsType := DNSType_UNKNOWN + if v, ok := s.GetFields()[ExtKeyDNSType]; ok { + switch v.GetStringValue() { + case DNSType_QUERY.String(): + dnsType = DNSType_QUERY + case DNSType_RESPONSE.String(): + dnsType = DNSType_RESPONSE + } + } + + var numResponses uint32 + if v, ok := s.GetFields()[ExtKeyNumResponses]; ok { + numResponses = uint32(v.GetNumberValue()) + } + + return dns, dnsType, numResponses } // DNS Return code to string. @@ -256,38 +403,42 @@ func DNSRcodeToString(f *flow.Flow) string { } } -// AddPacketSize adds the packet size to the flow's metadata. -func AddPacketSize(meta *RetinaMetadata, packetSize uint32) { - if meta == nil { +// AddPacketSize adds the packet size to the flow's extensions. +func AddPacketSize(s *structpb.Struct, packetSize uint32) { + if s == nil || packetSize == 0 { return } - meta.Bytes = packetSize + s.GetFields()[ExtKeyBytes] = structpb.NewNumberValue(float64(packetSize)) } func PacketSize(f *flow.Flow) uint32 { - if f.Extensions == nil { + s := GetExtensionsStruct(f) + if s == nil { return 0 } - k := &RetinaMetadata{} //nolint:typecheck - f.Extensions.UnmarshalTo(k) //nolint:errcheck - return k.Bytes + v, ok := s.GetFields()[ExtKeyBytes] + if !ok { + return 0 + } + return uint32(v.GetNumberValue()) } -// AddDropReason adds the drop reason to the flow's metadata. -func AddDropReason(f *flow.Flow, meta *RetinaMetadata, dropReason uint16) { - if f == nil || meta == nil { +// AddDropReason adds the drop reason to the flow and its extensions. +func AddDropReason(f *flow.Flow, s *structpb.Struct, dropReason uint16) { + if f == nil || s == nil { return } - meta.DropReason = DropReason(dropReason) + dr := DropReason(dropReason) + s.GetFields()[ExtKeyDropReason] = structpb.NewStringValue(dr.String()) f.Verdict = flow.Verdict_DROPPED // Set the drop reason. // Retina drop reasons are different from the drop reasons available in flow library. // We map the ones available in flow library to the ones available in Retina. - // Rest are set to UNKNOWN. The details are added in the metadata. - f.DropReasonDesc = GetDropReasonDesc(meta.GetDropReason()) + // Rest are set to UNKNOWN. The details are added in the extensions. + f.DropReasonDesc = GetDropReasonDesc(dr) f.EventType = &flow.CiliumEventType{ Type: int32(api.MessageTypeDrop), @@ -299,9 +450,15 @@ func DropReasonDescription(f *flow.Flow) string { if f == nil { return "" } - k := &RetinaMetadata{} //nolint:typecheck // Not required to check type as we are setting it. - f.GetExtensions().UnmarshalTo(k) //nolint:errcheck // Not required to check error as we are setting it. - return k.GetDropReason().String() + s := GetExtensionsStruct(f) + if s == nil { + return "" + } + v, ok := s.GetFields()[ExtKeyDropReason] + if !ok { + return "" + } + return v.GetStringValue() } func decodeTime(nanoseconds int64) (pbTime *timestamppb.Timestamp, err error) { @@ -315,3 +472,38 @@ func decodeTime(nanoseconds int64) (pbTime *timestamppb.Timestamp, err error) { } return pbTime, nil } + +// AddZones adds source and destination availability zones to the flow's extensions. +func AddZones(s *structpb.Struct, srcZone, dstZone string) { + if s == nil { + return + } + s.GetFields()[ExtKeySourceZone] = structpb.NewStringValue(srcZone) + s.GetFields()[ExtKeyDestinationZone] = structpb.NewStringValue(dstZone) +} + +// SourceZone returns the source availability zone from the flow's extensions. +func SourceZone(f *flow.Flow) string { + s := GetExtensionsStruct(f) + if s == nil { + return zoneUnknown + } + v, ok := s.GetFields()[ExtKeySourceZone] + if !ok { + return zoneUnknown + } + return v.GetStringValue() +} + +// DestinationZone returns the destination availability zone from the flow's extensions. +func DestinationZone(f *flow.Flow) string { + s := GetExtensionsStruct(f) + if s == nil { + return zoneUnknown + } + v, ok := s.GetFields()[ExtKeyDestinationZone] + if !ok { + return zoneUnknown + } + return v.GetStringValue() +} diff --git a/pkg/utils/kernel_release_unix.go b/pkg/utils/kernel_release_unix.go new file mode 100644 index 0000000000..c3d09bc70b --- /dev/null +++ b/pkg/utils/kernel_release_unix.go @@ -0,0 +1,38 @@ +//go:build unix + +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//revive:disable:var-naming +package utils + +//revive:enable:var-naming + +import ( + "fmt" + + "golang.org/x/sys/unix" +) + +// KernelRelease returns the kernel release string (e.g. "5.15.0-101-generic"). +func KernelRelease() (string, error) { + var uts unix.Utsname + if err := unix.Uname(&uts); err != nil { + return "", fmt.Errorf("uname failed: %w", err) + } + return charsToString(uts.Release[:]), nil +} + +func charsToString(ca []byte) string { + n := 0 + for ; n < len(ca); n++ { + if ca[n] == 0 { + break + } + } + b := make([]byte, n) + for i := 0; i < n; i++ { + b[i] = ca[i] + } + return string(b) +} diff --git a/pkg/utils/kernel_version_linux.go b/pkg/utils/kernel_version_linux.go new file mode 100644 index 0000000000..2f415e315e --- /dev/null +++ b/pkg/utils/kernel_version_linux.go @@ -0,0 +1,95 @@ +//go:build linux + +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//revive:disable:var-naming +package utils + +//revive:enable:var-naming + +import ( + "errors" + "fmt" + "strconv" + "strings" + + "golang.org/x/mod/semver" +) + +// KernelVersion represents a parsed Linux kernel version. +type KernelVersion struct { + Major int + Minor int + Patch int + Release string +} + +var ( + errUnexpectedKernelReleaseFormat = errors.New("unexpected kernel release format") + errInvalidKernelSemver = errors.New("invalid kernel semver") +) + +// LinuxKernelVersion returns the parsed Linux kernel version and release string. +func LinuxKernelVersion() (KernelVersion, error) { + release, err := KernelRelease() + if err != nil { + return KernelVersion{}, err + } + major, minor, patch, err := ParseLinuxKernelRelease(release) + if err != nil { + return KernelVersion{Release: release}, err + } + return KernelVersion{Major: major, Minor: minor, Patch: patch, Release: release}, nil +} + +// AtLeast reports whether the kernel version is >= the required version. +func (v KernelVersion) AtLeast(major, minor, patch int) bool { + current := kernelSemverFromParts(v.Major, v.Minor, v.Patch) + required := kernelSemverFromParts(major, minor, patch) + return semver.Compare(current, required) >= 0 +} + +// ParseLinuxKernelRelease parses the uname release string into semantic version parts. +func ParseLinuxKernelRelease(release string) (major, minor, patch int, err error) { + _, major, minor, patch, err = normalizeKernelReleaseToSemver(release) + if err != nil { + return 0, 0, 0, err + } + return major, minor, patch, nil +} + +func kernelSemverFromParts(major, minor, patch int) string { + return fmt.Sprintf("v%d.%d.%d", major, minor, patch) +} + +func normalizeKernelReleaseToSemver(release string) (version string, major, minor, patch int, err error) { + base := strings.SplitN(release, "-", 2)[0] + base = strings.SplitN(base, "+", 2)[0] + parts := strings.Split(base, ".") + if len(parts) < 2 { + return "", 0, 0, 0, fmt.Errorf("%w: %q", errUnexpectedKernelReleaseFormat, release) + } + + major, err = strconv.Atoi(parts[0]) + if err != nil { + return "", 0, 0, 0, fmt.Errorf("invalid kernel major in %q: %w", release, err) + } + minor, err = strconv.Atoi(parts[1]) + if err != nil { + return "", 0, 0, 0, fmt.Errorf("invalid kernel minor in %q: %w", release, err) + } + patch = 0 + if len(parts) >= 3 { + if patch, err = strconv.Atoi(parts[2]); err != nil { + return "", 0, 0, 0, fmt.Errorf("invalid kernel patch in %q: %w", release, err) + } + } + + version = kernelSemverFromParts(major, minor, patch) + if !semver.IsValid(version) { + return "", 0, 0, 0, fmt.Errorf("%w for %q: %s", errInvalidKernelSemver, release, version) + } + + return version, major, minor, patch, nil +} diff --git a/pkg/utils/kernel_version_linux_test.go b/pkg/utils/kernel_version_linux_test.go new file mode 100644 index 0000000000..f1619734e8 --- /dev/null +++ b/pkg/utils/kernel_version_linux_test.go @@ -0,0 +1,112 @@ +//go:build linux + +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//revive:disable-next-line:var-naming +package utils + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseLinuxKernelRelease(t *testing.T) { + tests := []struct { + name string + release string + expectMajor int + expectMinor int + expectPatch int + expectErr bool + }{ + { + name: "full version with suffix", + release: "5.15.0-101-generic", + expectMajor: 5, + expectMinor: 15, + expectPatch: 0, + expectErr: false, + }, + { + name: "no patch version", + release: "6.1-foo", + expectMajor: 6, + expectMinor: 1, + expectPatch: 0, + expectErr: false, + }, + { + name: "full numeric version", + release: "4.19.260", + expectMajor: 4, + expectMinor: 19, + expectPatch: 260, + expectErr: false, + }, + { + name: "extra numeric segment", + release: "5.15.153.1-microsoft-standard-WSL2", + expectMajor: 5, + expectMinor: 15, + expectPatch: 153, + expectErr: false, + }, + { + name: "plus suffix (GKE Container-Optimized OS)", + release: "6.6.113+", + expectMajor: 6, + expectMinor: 6, + expectPatch: 113, + expectErr: false, + }, + { + name: "plus suffix with build metadata", + release: "6.6.113+gke-12345", + expectMajor: 6, + expectMinor: 6, + expectPatch: 113, + expectErr: false, + }, + { + name: "invalid format", + release: "foo", + expectErr: true, + }, + { + name: "invalid minor", + release: "5.x.1", + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + major, minor, patch, err := ParseLinuxKernelRelease(tt.release) + if tt.expectErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.expectMajor, major) + assert.Equal(t, tt.expectMinor, minor) + assert.Equal(t, tt.expectPatch, patch) + }) + } +} + +func TestKernelVersionAtLeast(t *testing.T) { + v := KernelVersion{Major: 5, Minor: 8, Patch: 0} + + assert.True(t, v.AtLeast(5, 8, 0)) + assert.True(t, v.AtLeast(5, 7, 99)) + assert.False(t, v.AtLeast(5, 8, 1)) + assert.False(t, v.AtLeast(6, 0, 0)) + + v2 := KernelVersion{Major: 6, Minor: 1, Patch: 3} + assert.True(t, v2.AtLeast(5, 8, 0)) + assert.True(t, v2.AtLeast(6, 1, 3)) + assert.False(t, v2.AtLeast(6, 2, 0)) +} diff --git a/pkg/utils/metadata_darwin.pb.go b/pkg/utils/metadata_darwin.pb.go new file mode 100644 index 0000000000..9775e5881f --- /dev/null +++ b/pkg/utils/metadata_darwin.pb.go @@ -0,0 +1,367 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.2 +// protoc v4.24.2 +// source: pkg/utils/metadata_darwin.proto + +package utils + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type DNSType int32 + +const ( + DNSType_UNKNOWN DNSType = 0 + DNSType_QUERY DNSType = 1 + DNSType_RESPONSE DNSType = 2 +) + +// Enum value maps for DNSType. +var ( + DNSType_name = map[int32]string{ + 0: "UNKNOWN", + 1: "QUERY", + 2: "RESPONSE", + } + DNSType_value = map[string]int32{ + "UNKNOWN": 0, + "QUERY": 1, + "RESPONSE": 2, + } +) + +func (x DNSType) Enum() *DNSType { + p := new(DNSType) + *p = x + return p +} + +func (x DNSType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (DNSType) Descriptor() protoreflect.EnumDescriptor { + return file_pkg_utils_metadata_darwin_proto_enumTypes[0].Descriptor() +} + +func (DNSType) Type() protoreflect.EnumType { + return &file_pkg_utils_metadata_darwin_proto_enumTypes[0] +} + +func (x DNSType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use DNSType.Descriptor instead. +func (DNSType) EnumDescriptor() ([]byte, []int) { + return file_pkg_utils_metadata_darwin_proto_rawDescGZIP(), []int{0} +} + +// Ref: pkg/plugin/dropreason/_cprog/drop_reason.h. +type DropReason int32 + +const ( + DropReason_IPTABLE_RULE_DROP DropReason = 0 + DropReason_IPTABLE_NAT_DROP DropReason = 1 + DropReason_TCP_CONNECT_BASIC DropReason = 2 + DropReason_TCP_ACCEPT_BASIC DropReason = 3 + DropReason_TCP_CLOSE_BASIC DropReason = 4 + DropReason_CONNTRACK_ADD_DROP DropReason = 5 + DropReason_UNKNOWN_DROP DropReason = 6 +) + +// Enum value maps for DropReason. +var ( + DropReason_name = map[int32]string{ + 0: "IPTABLE_RULE_DROP", + 1: "IPTABLE_NAT_DROP", + 2: "TCP_CONNECT_BASIC", + 3: "TCP_ACCEPT_BASIC", + 4: "TCP_CLOSE_BASIC", + 5: "CONNTRACK_ADD_DROP", + 6: "UNKNOWN_DROP", + } + DropReason_value = map[string]int32{ + "IPTABLE_RULE_DROP": 0, + "IPTABLE_NAT_DROP": 1, + "TCP_CONNECT_BASIC": 2, + "TCP_ACCEPT_BASIC": 3, + "TCP_CLOSE_BASIC": 4, + "CONNTRACK_ADD_DROP": 5, + "UNKNOWN_DROP": 6, + } +) + +func (x DropReason) Enum() *DropReason { + p := new(DropReason) + *p = x + return p +} + +func (x DropReason) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (DropReason) Descriptor() protoreflect.EnumDescriptor { + return file_pkg_utils_metadata_darwin_proto_enumTypes[1].Descriptor() +} + +func (DropReason) Type() protoreflect.EnumType { + return &file_pkg_utils_metadata_darwin_proto_enumTypes[1] +} + +func (x DropReason) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use DropReason.Descriptor instead. +func (DropReason) EnumDescriptor() ([]byte, []int) { + return file_pkg_utils_metadata_darwin_proto_rawDescGZIP(), []int{1} +} + +type RetinaMetadata struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Bytes uint32 `protobuf:"varint,1,opt,name=bytes,proto3" json:"bytes,omitempty"` + // DNS metadata. + DnsType DNSType `protobuf:"varint,2,opt,name=dns_type,json=dnsType,proto3,enum=utils.DNSType" json:"dns_type,omitempty"` + NumResponses uint32 `protobuf:"varint,3,opt,name=num_responses,json=numResponses,proto3" json:"num_responses,omitempty"` + // TCP ID. Either Tsval or Tsecr will be set. + TcpId uint64 `protobuf:"varint,4,opt,name=tcp_id,json=tcpId,proto3" json:"tcp_id,omitempty"` + // Drop reason in Retina. + DropReason DropReason `protobuf:"varint,5,opt,name=drop_reason,json=dropReason,proto3,enum=utils.DropReason" json:"drop_reason,omitempty"` + // Sampling metadata, for packetparser. + PreviouslyObservedPackets uint32 `protobuf:"varint,6,opt,name=previously_observed_packets,json=previouslyObservedPackets,proto3" json:"previously_observed_packets,omitempty"` + PreviouslyObservedBytes uint32 `protobuf:"varint,7,opt,name=previously_observed_bytes,json=previouslyObservedBytes,proto3" json:"previously_observed_bytes,omitempty"` + PreviouslyObservedTcpFlags map[string]uint32 `protobuf:"bytes,8,rep,name=previously_observed_tcp_flags,json=previouslyObservedTcpFlags,proto3" json:"previously_observed_tcp_flags,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` +} + +func (x *RetinaMetadata) Reset() { + *x = RetinaMetadata{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_utils_metadata_darwin_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RetinaMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RetinaMetadata) ProtoMessage() {} + +func (x *RetinaMetadata) ProtoReflect() protoreflect.Message { + mi := &file_pkg_utils_metadata_darwin_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RetinaMetadata.ProtoReflect.Descriptor instead. +func (*RetinaMetadata) Descriptor() ([]byte, []int) { + return file_pkg_utils_metadata_darwin_proto_rawDescGZIP(), []int{0} +} + +func (x *RetinaMetadata) GetBytes() uint32 { + if x != nil { + return x.Bytes + } + return 0 +} + +func (x *RetinaMetadata) GetDnsType() DNSType { + if x != nil { + return x.DnsType + } + return DNSType_UNKNOWN +} + +func (x *RetinaMetadata) GetNumResponses() uint32 { + if x != nil { + return x.NumResponses + } + return 0 +} + +func (x *RetinaMetadata) GetTcpId() uint64 { + if x != nil { + return x.TcpId + } + return 0 +} + +func (x *RetinaMetadata) GetDropReason() DropReason { + if x != nil { + return x.DropReason + } + return DropReason_IPTABLE_RULE_DROP +} + +func (x *RetinaMetadata) GetPreviouslyObservedPackets() uint32 { + if x != nil { + return x.PreviouslyObservedPackets + } + return 0 +} + +func (x *RetinaMetadata) GetPreviouslyObservedBytes() uint32 { + if x != nil { + return x.PreviouslyObservedBytes + } + return 0 +} + +func (x *RetinaMetadata) GetPreviouslyObservedTcpFlags() map[string]uint32 { + if x != nil { + return x.PreviouslyObservedTcpFlags + } + return nil +} + +var File_pkg_utils_metadata_darwin_proto protoreflect.FileDescriptor + +var file_pkg_utils_metadata_darwin_proto_rawDesc = []byte{ + 0x0a, 0x1f, 0x70, 0x6b, 0x67, 0x2f, 0x75, 0x74, 0x69, 0x6c, 0x73, 0x2f, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x5f, 0x64, 0x61, 0x72, 0x77, 0x69, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x05, 0x75, 0x74, 0x69, 0x6c, 0x73, 0x22, 0x86, 0x04, 0x0a, 0x0e, 0x52, 0x65, 0x74, + 0x69, 0x6e, 0x61, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x62, + 0x79, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x62, 0x79, 0x74, 0x65, + 0x73, 0x12, 0x29, 0x0a, 0x08, 0x64, 0x6e, 0x73, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x75, 0x74, 0x69, 0x6c, 0x73, 0x2e, 0x44, 0x4e, 0x53, 0x54, + 0x79, 0x70, 0x65, 0x52, 0x07, 0x64, 0x6e, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, 0x23, 0x0a, 0x0d, + 0x6e, 0x75, 0x6d, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x6e, 0x75, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x73, 0x12, 0x15, 0x0a, 0x06, 0x74, 0x63, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x05, 0x74, 0x63, 0x70, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x0b, 0x64, 0x72, 0x6f, 0x70, + 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, + 0x75, 0x74, 0x69, 0x6c, 0x73, 0x2e, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, + 0x52, 0x0a, 0x64, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x3e, 0x0a, 0x1b, + 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x6c, 0x79, 0x5f, 0x6f, 0x62, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x64, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x19, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x6c, 0x79, 0x4f, 0x62, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x64, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x12, 0x3a, 0x0a, 0x19, + 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x6c, 0x79, 0x5f, 0x6f, 0x62, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x17, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x6c, 0x79, 0x4f, 0x62, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x78, 0x0a, 0x1d, 0x70, 0x72, 0x65, 0x76, + 0x69, 0x6f, 0x75, 0x73, 0x6c, 0x79, 0x5f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x5f, + 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x35, 0x2e, 0x75, 0x74, 0x69, 0x6c, 0x73, 0x2e, 0x52, 0x65, 0x74, 0x69, 0x6e, 0x61, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x50, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x6c, + 0x79, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x54, 0x63, 0x70, 0x46, 0x6c, 0x61, 0x67, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x1a, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, + 0x6c, 0x79, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x54, 0x63, 0x70, 0x46, 0x6c, 0x61, + 0x67, 0x73, 0x1a, 0x4d, 0x0a, 0x1f, 0x50, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x6c, 0x79, + 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x54, 0x63, 0x70, 0x46, 0x6c, 0x61, 0x67, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, + 0x01, 0x2a, 0x2f, 0x0a, 0x07, 0x44, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, + 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x51, 0x55, 0x45, + 0x52, 0x59, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x52, 0x45, 0x53, 0x50, 0x4f, 0x4e, 0x53, 0x45, + 0x10, 0x02, 0x2a, 0xa5, 0x01, 0x0a, 0x0a, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x12, 0x15, 0x0a, 0x11, 0x49, 0x50, 0x54, 0x41, 0x42, 0x4c, 0x45, 0x5f, 0x52, 0x55, 0x4c, + 0x45, 0x5f, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x49, 0x50, 0x54, 0x41, + 0x42, 0x4c, 0x45, 0x5f, 0x4e, 0x41, 0x54, 0x5f, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x12, 0x15, + 0x0a, 0x11, 0x54, 0x43, 0x50, 0x5f, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x5f, 0x42, 0x41, + 0x53, 0x49, 0x43, 0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x54, 0x43, 0x50, 0x5f, 0x41, 0x43, 0x43, + 0x45, 0x50, 0x54, 0x5f, 0x42, 0x41, 0x53, 0x49, 0x43, 0x10, 0x03, 0x12, 0x13, 0x0a, 0x0f, 0x54, + 0x43, 0x50, 0x5f, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x5f, 0x42, 0x41, 0x53, 0x49, 0x43, 0x10, 0x04, + 0x12, 0x16, 0x0a, 0x12, 0x43, 0x4f, 0x4e, 0x4e, 0x54, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x41, 0x44, + 0x44, 0x5f, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x05, 0x12, 0x10, 0x0a, 0x0c, 0x55, 0x4e, 0x4b, 0x4e, + 0x4f, 0x57, 0x4e, 0x5f, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x06, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, + 0x66, 0x74, 0x2f, 0x72, 0x65, 0x74, 0x69, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x75, 0x74, + 0x69, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_pkg_utils_metadata_darwin_proto_rawDescOnce sync.Once + file_pkg_utils_metadata_darwin_proto_rawDescData = file_pkg_utils_metadata_darwin_proto_rawDesc +) + +func file_pkg_utils_metadata_darwin_proto_rawDescGZIP() []byte { + file_pkg_utils_metadata_darwin_proto_rawDescOnce.Do(func() { + file_pkg_utils_metadata_darwin_proto_rawDescData = protoimpl.X.CompressGZIP(file_pkg_utils_metadata_darwin_proto_rawDescData) + }) + return file_pkg_utils_metadata_darwin_proto_rawDescData +} + +var file_pkg_utils_metadata_darwin_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_pkg_utils_metadata_darwin_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_pkg_utils_metadata_darwin_proto_goTypes = []any{ + (DNSType)(0), // 0: utils.DNSType + (DropReason)(0), // 1: utils.DropReason + (*RetinaMetadata)(nil), // 2: utils.RetinaMetadata + nil, // 3: utils.RetinaMetadata.PreviouslyObservedTcpFlagsEntry +} +var file_pkg_utils_metadata_darwin_proto_depIdxs = []int32{ + 0, // 0: utils.RetinaMetadata.dns_type:type_name -> utils.DNSType + 1, // 1: utils.RetinaMetadata.drop_reason:type_name -> utils.DropReason + 3, // 2: utils.RetinaMetadata.previously_observed_tcp_flags:type_name -> utils.RetinaMetadata.PreviouslyObservedTcpFlagsEntry + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_pkg_utils_metadata_darwin_proto_init() } +func file_pkg_utils_metadata_darwin_proto_init() { + if File_pkg_utils_metadata_darwin_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_pkg_utils_metadata_darwin_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*RetinaMetadata); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_pkg_utils_metadata_darwin_proto_rawDesc, + NumEnums: 2, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_pkg_utils_metadata_darwin_proto_goTypes, + DependencyIndexes: file_pkg_utils_metadata_darwin_proto_depIdxs, + EnumInfos: file_pkg_utils_metadata_darwin_proto_enumTypes, + MessageInfos: file_pkg_utils_metadata_darwin_proto_msgTypes, + }.Build() + File_pkg_utils_metadata_darwin_proto = out.File + file_pkg_utils_metadata_darwin_proto_rawDesc = nil + file_pkg_utils_metadata_darwin_proto_goTypes = nil + file_pkg_utils_metadata_darwin_proto_depIdxs = nil +} diff --git a/pkg/utils/metadata_darwin.proto b/pkg/utils/metadata_darwin.proto new file mode 100644 index 0000000000..9e939bccd1 --- /dev/null +++ b/pkg/utils/metadata_darwin.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; +package utils; + +option go_package = "github.com/microsoft/retina/pkg/utils"; + +message RetinaMetadata { + uint32 bytes = 1; + + // DNS metadata. + DNSType dns_type = 2; + uint32 num_responses = 3; + + // TCP ID. Either Tsval or Tsecr will be set. + uint64 tcp_id = 4; + + // Drop reason in Retina. + DropReason drop_reason = 5; + + // Sampling metadata, for packetparser. + uint32 previously_observed_packets = 6; + uint32 previously_observed_bytes = 7; + map previously_observed_tcp_flags = 8; +} + +enum DNSType { + UNKNOWN = 0; + QUERY = 1; + RESPONSE = 2; +} + +// Ref: pkg/plugin/dropreason/_cprog/drop_reason.h. +enum DropReason { + IPTABLE_RULE_DROP = 0; + IPTABLE_NAT_DROP = 1; + TCP_CONNECT_BASIC = 2; + TCP_ACCEPT_BASIC = 3; + TCP_CLOSE_BASIC = 4; + CONNTRACK_ADD_DROP = 5; + UNKNOWN_DROP = 6; +} diff --git a/pkg/utils/metadata_linux.pb.go b/pkg/utils/metadata_linux.pb.go index 7455a1191a..6914c0b3e3 100644 --- a/pkg/utils/metadata_linux.pb.go +++ b/pkg/utils/metadata_linux.pb.go @@ -144,6 +144,10 @@ type RetinaMetadata struct { TcpId uint64 `protobuf:"varint,4,opt,name=tcp_id,json=tcpId,proto3" json:"tcp_id,omitempty"` // Drop reason in Retina. DropReason DropReason `protobuf:"varint,5,opt,name=drop_reason,json=dropReason,proto3,enum=utils.DropReason" json:"drop_reason,omitempty"` + // Sampling metadata, for packetparser. + PreviouslyObservedPackets uint32 `protobuf:"varint,6,opt,name=previously_observed_packets,json=previouslyObservedPackets,proto3" json:"previously_observed_packets,omitempty"` + PreviouslyObservedBytes uint32 `protobuf:"varint,7,opt,name=previously_observed_bytes,json=previouslyObservedBytes,proto3" json:"previously_observed_bytes,omitempty"` + PreviouslyObservedTcpFlags map[string]uint32 `protobuf:"bytes,8,rep,name=previously_observed_tcp_flags,json=previouslyObservedTcpFlags,proto3" json:"previously_observed_tcp_flags,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` } func (x *RetinaMetadata) Reset() { @@ -213,12 +217,33 @@ func (x *RetinaMetadata) GetDropReason() DropReason { return DropReason_IPTABLE_RULE_DROP } +func (x *RetinaMetadata) GetPreviouslyObservedPackets() uint32 { + if x != nil { + return x.PreviouslyObservedPackets + } + return 0 +} + +func (x *RetinaMetadata) GetPreviouslyObservedBytes() uint32 { + if x != nil { + return x.PreviouslyObservedBytes + } + return 0 +} + +func (x *RetinaMetadata) GetPreviouslyObservedTcpFlags() map[string]uint32 { + if x != nil { + return x.PreviouslyObservedTcpFlags + } + return nil +} + var File_pkg_utils_metadata_linux_proto protoreflect.FileDescriptor var file_pkg_utils_metadata_linux_proto_rawDesc = []byte{ 0x0a, 0x1e, 0x70, 0x6b, 0x67, 0x2f, 0x75, 0x74, 0x69, 0x6c, 0x73, 0x2f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x6c, 0x69, 0x6e, 0x75, 0x78, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x12, 0x05, 0x75, 0x74, 0x69, 0x6c, 0x73, 0x22, 0xc1, 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x74, 0x69, + 0x12, 0x05, 0x75, 0x74, 0x69, 0x6c, 0x73, 0x22, 0x86, 0x04, 0x0a, 0x0e, 0x52, 0x65, 0x74, 0x69, 0x6e, 0x61, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x62, 0x79, 0x74, 0x65, 0x73, 0x12, 0x29, 0x0a, 0x08, 0x64, 0x6e, 0x73, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, @@ -230,24 +255,44 @@ var file_pkg_utils_metadata_linux_proto_rawDesc = []byte{ 0x52, 0x05, 0x74, 0x63, 0x70, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x0b, 0x64, 0x72, 0x6f, 0x70, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x75, 0x74, 0x69, 0x6c, 0x73, 0x2e, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, - 0x0a, 0x64, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x2a, 0x2f, 0x0a, 0x07, 0x44, - 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, - 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x51, 0x55, 0x45, 0x52, 0x59, 0x10, 0x01, 0x12, 0x0c, - 0x0a, 0x08, 0x52, 0x45, 0x53, 0x50, 0x4f, 0x4e, 0x53, 0x45, 0x10, 0x02, 0x2a, 0xa5, 0x01, 0x0a, - 0x0a, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x15, 0x0a, 0x11, 0x49, - 0x50, 0x54, 0x41, 0x42, 0x4c, 0x45, 0x5f, 0x52, 0x55, 0x4c, 0x45, 0x5f, 0x44, 0x52, 0x4f, 0x50, - 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x49, 0x50, 0x54, 0x41, 0x42, 0x4c, 0x45, 0x5f, 0x4e, 0x41, - 0x54, 0x5f, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x12, 0x15, 0x0a, 0x11, 0x54, 0x43, 0x50, 0x5f, - 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x5f, 0x42, 0x41, 0x53, 0x49, 0x43, 0x10, 0x02, 0x12, - 0x14, 0x0a, 0x10, 0x54, 0x43, 0x50, 0x5f, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x5f, 0x42, 0x41, - 0x53, 0x49, 0x43, 0x10, 0x03, 0x12, 0x13, 0x0a, 0x0f, 0x54, 0x43, 0x50, 0x5f, 0x43, 0x4c, 0x4f, - 0x53, 0x45, 0x5f, 0x42, 0x41, 0x53, 0x49, 0x43, 0x10, 0x04, 0x12, 0x16, 0x0a, 0x12, 0x43, 0x4f, - 0x4e, 0x4e, 0x54, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x41, 0x44, 0x44, 0x5f, 0x44, 0x52, 0x4f, 0x50, - 0x10, 0x05, 0x12, 0x10, 0x0a, 0x0c, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x44, 0x52, - 0x4f, 0x50, 0x10, 0x06, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0x2f, 0x72, 0x65, 0x74, - 0x69, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x75, 0x74, 0x69, 0x6c, 0x73, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x0a, 0x64, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x3e, 0x0a, 0x1b, 0x70, + 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x6c, 0x79, 0x5f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x64, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x19, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x6c, 0x79, 0x4f, 0x62, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x64, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x12, 0x3a, 0x0a, 0x19, 0x70, + 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x6c, 0x79, 0x5f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x17, + 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x6c, 0x79, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x78, 0x0a, 0x1d, 0x70, 0x72, 0x65, 0x76, 0x69, + 0x6f, 0x75, 0x73, 0x6c, 0x79, 0x5f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x5f, 0x74, + 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x35, + 0x2e, 0x75, 0x74, 0x69, 0x6c, 0x73, 0x2e, 0x52, 0x65, 0x74, 0x69, 0x6e, 0x61, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x50, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x6c, 0x79, + 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x54, 0x63, 0x70, 0x46, 0x6c, 0x61, 0x67, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x1a, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x6c, + 0x79, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x54, 0x63, 0x70, 0x46, 0x6c, 0x61, 0x67, + 0x73, 0x1a, 0x4d, 0x0a, 0x1f, 0x50, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x6c, 0x79, 0x4f, + 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x54, 0x63, 0x70, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, + 0x2a, 0x2f, 0x0a, 0x07, 0x44, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, + 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x51, 0x55, 0x45, 0x52, + 0x59, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x52, 0x45, 0x53, 0x50, 0x4f, 0x4e, 0x53, 0x45, 0x10, + 0x02, 0x2a, 0xa5, 0x01, 0x0a, 0x0a, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, + 0x12, 0x15, 0x0a, 0x11, 0x49, 0x50, 0x54, 0x41, 0x42, 0x4c, 0x45, 0x5f, 0x52, 0x55, 0x4c, 0x45, + 0x5f, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x49, 0x50, 0x54, 0x41, 0x42, + 0x4c, 0x45, 0x5f, 0x4e, 0x41, 0x54, 0x5f, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x12, 0x15, 0x0a, + 0x11, 0x54, 0x43, 0x50, 0x5f, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x5f, 0x42, 0x41, 0x53, + 0x49, 0x43, 0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x54, 0x43, 0x50, 0x5f, 0x41, 0x43, 0x43, 0x45, + 0x50, 0x54, 0x5f, 0x42, 0x41, 0x53, 0x49, 0x43, 0x10, 0x03, 0x12, 0x13, 0x0a, 0x0f, 0x54, 0x43, + 0x50, 0x5f, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x5f, 0x42, 0x41, 0x53, 0x49, 0x43, 0x10, 0x04, 0x12, + 0x16, 0x0a, 0x12, 0x43, 0x4f, 0x4e, 0x4e, 0x54, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x41, 0x44, 0x44, + 0x5f, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x05, 0x12, 0x10, 0x0a, 0x0c, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, + 0x57, 0x4e, 0x5f, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x06, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, + 0x74, 0x2f, 0x72, 0x65, 0x74, 0x69, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x75, 0x74, 0x69, + 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -263,20 +308,22 @@ func file_pkg_utils_metadata_linux_proto_rawDescGZIP() []byte { } var file_pkg_utils_metadata_linux_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_pkg_utils_metadata_linux_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_pkg_utils_metadata_linux_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_pkg_utils_metadata_linux_proto_goTypes = []any{ (DNSType)(0), // 0: utils.DNSType (DropReason)(0), // 1: utils.DropReason (*RetinaMetadata)(nil), // 2: utils.RetinaMetadata + nil, // 3: utils.RetinaMetadata.PreviouslyObservedTcpFlagsEntry } var file_pkg_utils_metadata_linux_proto_depIdxs = []int32{ 0, // 0: utils.RetinaMetadata.dns_type:type_name -> utils.DNSType 1, // 1: utils.RetinaMetadata.drop_reason:type_name -> utils.DropReason - 2, // [2:2] is the sub-list for method output_type - 2, // [2:2] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 3, // 2: utils.RetinaMetadata.previously_observed_tcp_flags:type_name -> utils.RetinaMetadata.PreviouslyObservedTcpFlagsEntry + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } func init() { file_pkg_utils_metadata_linux_proto_init() } @@ -304,7 +351,7 @@ func file_pkg_utils_metadata_linux_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_pkg_utils_metadata_linux_proto_rawDesc, NumEnums: 2, - NumMessages: 1, + NumMessages: 2, NumExtensions: 0, NumServices: 0, }, diff --git a/pkg/utils/metadata_linux.proto b/pkg/utils/metadata_linux.proto index c0e13aa444..9e939bccd1 100644 --- a/pkg/utils/metadata_linux.proto +++ b/pkg/utils/metadata_linux.proto @@ -15,6 +15,11 @@ message RetinaMetadata { // Drop reason in Retina. DropReason drop_reason = 5; + + // Sampling metadata, for packetparser. + uint32 previously_observed_packets = 6; + uint32 previously_observed_bytes = 7; + map previously_observed_tcp_flags = 8; } enum DNSType { diff --git a/pkg/utils/metadata_windows.pb.go b/pkg/utils/metadata_windows.pb.go index 45871b3080..a28aeeec6d 100644 --- a/pkg/utils/metadata_windows.pb.go +++ b/pkg/utils/metadata_windows.pb.go @@ -1,8 +1,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.9 -// protoc v6.32.0 -// source: metadata_windows.proto +// protoc-gen-go v1.34.2 +// protoc v4.24.2 +// source: pkg/utils/metadata_windows.proto package utils @@ -11,7 +11,6 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" - unsafe "unsafe" ) const ( @@ -54,11 +53,11 @@ func (x DNSType) String() string { } func (DNSType) Descriptor() protoreflect.EnumDescriptor { - return file_metadata_windows_proto_enumTypes[0].Descriptor() + return file_pkg_utils_metadata_windows_proto_enumTypes[0].Descriptor() } func (DNSType) Type() protoreflect.EnumType { - return &file_metadata_windows_proto_enumTypes[0] + return &file_pkg_utils_metadata_windows_proto_enumTypes[0] } func (x DNSType) Number() protoreflect.EnumNumber { @@ -67,13 +66,15 @@ func (x DNSType) Number() protoreflect.EnumNumber { // Deprecated: Use DNSType.Descriptor instead. func (DNSType) EnumDescriptor() ([]byte, []int) { - return file_metadata_windows_proto_rawDescGZIP(), []int{0} + return file_pkg_utils_metadata_windows_proto_rawDescGZIP(), []int{0} } type DropReason int32 const ( + // // Cilium drop reasons + // DropReason_Reason_Success DropReason = 0 DropReason_Reason_InvalidPacket DropReason = 2 DropReason_Reason_PlainText DropReason = 3 @@ -158,7 +159,9 @@ const ( DropReason_DropReason_HostNotReady DropReason = 202 DropReason_DropReason_EpNotReady DropReason = 203 DropReason_DropReason_PacketMonitor DropReason = 220 + // // Matching VMS_PACKET_DROP_REASON + // DropReason_Drop_InvalidData DropReason = 1073741825 DropReason_Drop_InvalidPacket DropReason = 1073741826 DropReason_Drop_Resources DropReason = 1073741827 @@ -197,7 +200,9 @@ const ( DropReason_Drop_FilteredIsolationUntagged DropReason = 1073741860 DropReason_Drop_InvalidPDQueue DropReason = 1073741861 DropReason_Drop_LowPower DropReason = 1073741862 + // // General errors + // DropReason_Drop_Pause DropReason = 1073742025 DropReason_Drop_Reset DropReason = 1073742026 DropReason_Drop_SendAborted DropReason = 1073742027 @@ -225,12 +230,16 @@ const ( DropReason_Drop_UnallowedEtherType DropReason = 1073742050 DropReason_Drop_VportDown DropReason = 1073742051 DropReason_Drop_SteeringMismatch DropReason = 1073742052 + // // NetVsc errors + // DropReason_Drop_MicroportError DropReason = 1073742225 DropReason_Drop_VfNotReady DropReason = 1073742226 DropReason_Drop_MicroportNotReady DropReason = 1073742227 DropReason_Drop_VMBusError DropReason = 1073742228 + // // Tcpip FL errors + // DropReason_Drop_FL_LoopbackPacket DropReason = 1073742425 DropReason_Drop_FL_InvalidSnapHeader DropReason = 1073742426 DropReason_Drop_FL_InvalidEthernetType DropReason = 1073742427 @@ -246,7 +255,9 @@ const ( DropReason_Drop_FL_NoClientInterface DropReason = 1073742437 DropReason_Drop_FL_TooManyNetBuffers DropReason = 1073742438 DropReason_Drop_FL_FlsNpiClientDrop DropReason = 1073742439 + // // VFP errors + // DropReason_Drop_ArpGuard DropReason = 1073742525 DropReason_Drop_ArpLimiter DropReason = 1073742526 DropReason_Drop_DhcpLimiter DropReason = 1073742527 @@ -266,7 +277,9 @@ const ( DropReason_Drop_NDPGuard DropReason = 1073742541 DropReason_Drop_PortBlocked DropReason = 1073742542 DropReason_Drop_NicSuspended DropReason = 1073742543 + // // Tcpip NL errors + // DropReason_Drop_NL_BadSourceAddress DropReason = 1073742725 DropReason_Drop_NL_NotLocallyDestined DropReason = 1073742726 DropReason_Drop_NL_ProtocolUnreachable DropReason = 1073742727 @@ -369,7 +382,9 @@ const ( DropReason_Drop_NL_SourceViolation DropReason = 1073742824 DropReason_Drop_NL_IcmpJumbogram DropReason = 1073742825 DropReason_Drop_NL_SwUsoFailure DropReason = 1073742826 + // // INET discard reasons + // DropReason_Drop_INET_SourceUnspecified DropReason = 1073743024 DropReason_Drop_INET_DestinationMulticast DropReason = 1073743025 DropReason_Drop_INET_HeaderInvalid DropReason = 1073743026 @@ -403,7 +418,9 @@ const ( DropReason_Drop_INET_SynAttack DropReason = 1073743054 DropReason_Drop_INET_AcceptInspection DropReason = 1073743055 DropReason_Drop_INET_AcceptRedirection DropReason = 1073743056 + // // Slbmux Error + // DropReason_Drop_SlbMux_ParsingFailure DropReason = 1073743125 DropReason_Drop_SlbMux_FirstFragmentMiss DropReason = 1073743126 DropReason_Drop_SlbMux_ICMPErrorPayloadValidationFailure DropReason = 1073743127 @@ -431,7 +448,9 @@ const ( DropReason_Drop_SlbMux_InvalidDiagPacketEncapType DropReason = 1073743149 DropReason_Drop_SlbMux_DiagPacketIsRedirect DropReason = 1073743150 DropReason_Drop_SlbMux_UnableToHandleRedirect DropReason = 1073743151 + // // Ipsec Errors + // DropReason_Drop_Ipsec_BadSpi DropReason = 1073743225 DropReason_Drop_Ipsec_SALifetimeExpired DropReason = 1073743226 DropReason_Drop_Ipsec_WrongSA DropReason = 1073743227 @@ -450,7 +469,9 @@ const ( DropReason_Drop_Ipsec_Dosp_MaxPerIpRateLimitQueues DropReason = 1073743240 DropReason_Drop_Ipsec_NoMemory DropReason = 1073743241 DropReason_Drop_Ipsec_Unsuccessful DropReason = 1073743242 + // // NetCx Drop Reasons + // DropReason_Drop_NetCx_NetPacketLayoutParseFailure DropReason = 1073743325 DropReason_Drop_NetCx_SoftwareChecksumFailure DropReason = 1073743326 DropReason_Drop_NetCx_NicQueueStop DropReason = 1073743327 @@ -458,10 +479,14 @@ const ( DropReason_Drop_NetCx_LSOFailure DropReason = 1073743329 DropReason_Drop_NetCx_USOFailure DropReason = 1073743330 DropReason_Drop_NetCx_BufferBounceFailureAndPacketIgnore DropReason = 1073743331 + // // Http errors 3000 - 4000. // These must be in sync with cmd\resource.h + // DropReason_Drop_Http_Begin DropReason = 1073744824 + // // UlErrors + // DropReason_Drop_Http_UlError_Begin DropReason = 1073744825 DropReason_Drop_Http_UlError DropReason = 1073744826 DropReason_Drop_Http_UlErrorVerb DropReason = 1073744827 @@ -556,9 +581,13 @@ const ( DropReason_Drop_Http_UxDuoFaultContentLengthDisallowed DropReason = 1073745285 DropReason_Drop_Http_UxDuoFaultTrailerDisallowed DropReason = 1073745286 DropReason_Drop_Http_UxDuoFaultEnd DropReason = 1073745287 - // WSK layer drops + // + // WSK layer drops + // DropReason_Drop_Http_ReceiveSuppressed DropReason = 1073745424 - // Http/SSL layer drops + // + // Http/SSL layer drops + // DropReason_Drop_Http_Generic DropReason = 1073745624 DropReason_Drop_Http_InvalidParameter DropReason = 1073745625 DropReason_Drop_Http_InsufficientResources DropReason = 1073745626 @@ -1568,11 +1597,11 @@ func (x DropReason) String() string { } func (DropReason) Descriptor() protoreflect.EnumDescriptor { - return file_metadata_windows_proto_enumTypes[1].Descriptor() + return file_pkg_utils_metadata_windows_proto_enumTypes[1].Descriptor() } func (DropReason) Type() protoreflect.EnumType { - return &file_metadata_windows_proto_enumTypes[1] + return &file_pkg_utils_metadata_windows_proto_enumTypes[1] } func (x DropReason) Number() protoreflect.EnumNumber { @@ -1581,28 +1610,35 @@ func (x DropReason) Number() protoreflect.EnumNumber { // Deprecated: Use DropReason.Descriptor instead. func (DropReason) EnumDescriptor() ([]byte, []int) { - return file_metadata_windows_proto_rawDescGZIP(), []int{1} + return file_pkg_utils_metadata_windows_proto_rawDescGZIP(), []int{1} } type RetinaMetadata struct { - state protoimpl.MessageState `protogen:"open.v1"` - Bytes uint32 `protobuf:"varint,1,opt,name=bytes,proto3" json:"bytes,omitempty"` + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Bytes uint32 `protobuf:"varint,1,opt,name=bytes,proto3" json:"bytes,omitempty"` // DNS metadata. DnsType DNSType `protobuf:"varint,2,opt,name=dns_type,json=dnsType,proto3,enum=utils.DNSType" json:"dns_type,omitempty"` NumResponses uint32 `protobuf:"varint,3,opt,name=num_responses,json=numResponses,proto3" json:"num_responses,omitempty"` // TCP ID. Either Tsval or Tsecr will be set. TcpId uint64 `protobuf:"varint,4,opt,name=tcp_id,json=tcpId,proto3" json:"tcp_id,omitempty"` // Drop reason in Retina. - DropReason DropReason `protobuf:"varint,5,opt,name=drop_reason,json=dropReason,proto3,enum=utils.DropReason" json:"drop_reason,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + DropReason DropReason `protobuf:"varint,5,opt,name=drop_reason,json=dropReason,proto3,enum=utils.DropReason" json:"drop_reason,omitempty"` + // Sampling metadata, for packetparser. + PreviouslyObservedPackets uint32 `protobuf:"varint,6,opt,name=previously_observed_packets,json=previouslyObservedPackets,proto3" json:"previously_observed_packets,omitempty"` + PreviouslyObservedBytes uint32 `protobuf:"varint,7,opt,name=previously_observed_bytes,json=previouslyObservedBytes,proto3" json:"previously_observed_bytes,omitempty"` + PreviouslyObservedTcpFlags map[string]uint32 `protobuf:"bytes,8,rep,name=previously_observed_tcp_flags,json=previouslyObservedTcpFlags,proto3" json:"previously_observed_tcp_flags,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` } func (x *RetinaMetadata) Reset() { *x = RetinaMetadata{} - mi := &file_metadata_windows_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_pkg_utils_metadata_windows_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *RetinaMetadata) String() string { @@ -1612,8 +1648,8 @@ func (x *RetinaMetadata) String() string { func (*RetinaMetadata) ProtoMessage() {} func (x *RetinaMetadata) ProtoReflect() protoreflect.Message { - mi := &file_metadata_windows_proto_msgTypes[0] - if x != nil { + mi := &file_pkg_utils_metadata_windows_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1625,7 +1661,7 @@ func (x *RetinaMetadata) ProtoReflect() protoreflect.Message { // Deprecated: Use RetinaMetadata.ProtoReflect.Descriptor instead. func (*RetinaMetadata) Descriptor() ([]byte, []int) { - return file_metadata_windows_proto_rawDescGZIP(), []int{0} + return file_pkg_utils_metadata_windows_proto_rawDescGZIP(), []int{0} } func (x *RetinaMetadata) GetBytes() uint32 { @@ -1663,565 +1699,1221 @@ func (x *RetinaMetadata) GetDropReason() DropReason { return DropReason_Reason_Success } -var File_metadata_windows_proto protoreflect.FileDescriptor +func (x *RetinaMetadata) GetPreviouslyObservedPackets() uint32 { + if x != nil { + return x.PreviouslyObservedPackets + } + return 0 +} -const file_metadata_windows_proto_rawDesc = "" + - "\n" + - "\x16metadata_windows.proto\x12\x05utils\"\xc1\x01\n" + - "\x0eRetinaMetadata\x12\x14\n" + - "\x05bytes\x18\x01 \x01(\rR\x05bytes\x12)\n" + - "\bdns_type\x18\x02 \x01(\x0e2\x0e.utils.DNSTypeR\adnsType\x12#\n" + - "\rnum_responses\x18\x03 \x01(\rR\fnumResponses\x12\x15\n" + - "\x06tcp_id\x18\x04 \x01(\x04R\x05tcpId\x122\n" + - "\vdrop_reason\x18\x05 \x01(\x0e2\x11.utils.DropReasonR\n" + - "dropReason*/\n" + - "\aDNSType\x12\v\n" + - "\aUNKNOWN\x10\x00\x12\t\n" + - "\x05QUERY\x10\x01\x12\f\n" + - "\bRESPONSE\x10\x02*\xfb\x86\x01\n" + - "\n" + - "DropReason\x12\x12\n" + - "\x0eReason_Success\x10\x00\x12\x18\n" + - "\x14Reason_InvalidPacket\x10\x02\x12\x14\n" + - "\x10Reason_PlainText\x10\x03\x12\x1d\n" + - "\x19Reason_InterfaceDecrypted\x10\x04\x12\x1a\n" + - "\x16Reason_LbNoBackendSlot\x10\x05\x12\x16\n" + - "\x12Reason_LbNoBackend\x10\x06\x12\x1d\n" + - "\x19Reason_LbReverseNatUpdate\x10\a\x12\x1c\n" + - "\x18Resaon_LbReverseNatStale\x10\b\x12\x1b\n" + - "\x17Reason_FragmentedPacket\x10\t\x12\"\n" + - "\x1eReason_FragmentedPacketUpdated\x10\n" + - "\x12\x1b\n" + - "\x17Reason_MissedCustomCall\x10\v\x12\x1a\n" + - "\x15DropReason_InvalidSIP\x10\x84\x01\x12\x16\n" + - "\x11DropReason_Policy\x10\x85\x01\x12\x17\n" + - "\x12DropReason_Invalid\x10\x86\x01\x12\x1c\n" + - "\x17DropReason_CTInvalidHdr\x10\x87\x01\x12\x1a\n" + - "\x15DropReason_FragNeeded\x10\x88\x01\x12\x1e\n" + - "\x19DropReason_CTUnknownProto\x10\x89\x01\x12\x19\n" + - "\x14DropReason_UnknownL3\x10\x8a\x01\x12\x1e\n" + - "\x19DropReason_MissedTailCall\x10\x8b\x01\x12\x1a\n" + - "\x15DropReason_WriteError\x10\x8c\x01\x12\x19\n" + - "\x14DropReason_UnknownL4\x10\x8d\x01\x12\x1f\n" + - "\x1aDropReason_UnknownICMPCode\x10\x8e\x01\x12\x1f\n" + - "\x1aDropReason_UnknownICMPType\x10\x8f\x01\x12 \n" + - "\x1bDropReason_UnknownICMP6Code\x10\x90\x01\x12 \n" + - "\x1bDropReason_UnknownICMP6Type\x10\x91\x01\x12\"\n" + - "\x1dDropReason_UnknownICMP6Type_2\x10\x92\x01\x12\x1b\n" + - "\x16DropReason_NoTunnelKey\x10\x93\x01\x12\x19\n" + - "\x14DropReason_Unknown_1\x10\x94\x01\x12\x19\n" + - "\x14DropReason_Unknown_2\x10\x95\x01\x12\x1d\n" + - "\x18DropReason_UnknownTarget\x10\x96\x01\x12\x1a\n" + - "\x15DropReason_Unroutable\x10\x97\x01\x12\x19\n" + - "\x14DropReason_Unknown_3\x10\x98\x01\x12\x17\n" + - "\x12DropReason_CSUM_L3\x10\x99\x01\x12\x17\n" + - "\x12DropReason_CSUM_L4\x10\x9a\x01\x12\x1e\n" + - "\x19DropReason_CTCreateFailed\x10\x9b\x01\x12\x1d\n" + - "\x18DropReason_InvalidExthdr\x10\x9c\x01\x12\x1d\n" + - "\x18DropReason_FragNoSupport\x10\x9d\x01\x12\x19\n" + - "\x14DropReason_NoService\x10\x9e\x01\x12\"\n" + - "\x1dDropReason_UnsuppServiceProto\x10\x9f\x01\x12 \n" + - "\x1bDropReason_NoTunnelEndpoint\x10\xa0\x01\x12 \n" + - "\x1bDropReason_NAT46X64Disabled\x10\xa1\x01\x12\x1a\n" + - "\x15DropReason_EDTHorizon\x10\xa2\x01\x12\x19\n" + - "\x14DropReason_UnknownCT\x10\xa3\x01\x12\x1f\n" + - "\x1aDropReason_HostUnreachable\x10\xa4\x01\x12\x18\n" + - "\x13DropReason_NoConfig\x10\xa5\x01\x12\x1d\n" + - "\x18DropReason_UnsupportedL2\x10\xa6\x01\x12\x1c\n" + - "\x17DropReason_NatNoMapping\x10\xa7\x01\x12\x1e\n" + - "\x19DropReason_NatUnsuppProto\x10\xa8\x01\x12\x15\n" + - "\x10DropReason_NoFIB\x10\xa9\x01\x12\x1f\n" + - "\x1aDropReason_EncapProhibited\x10\xaa\x01\x12\x1f\n" + - "\x1aDropReason_InvalidIdentity\x10\xab\x01\x12\x1d\n" + - "\x18DropReason_UnknownSender\x10\xac\x01\x12\x1c\n" + - "\x17DropReason_NatNotNeeded\x10\xad\x01\x12\x1b\n" + - "\x16DropReason_IsClusterIP\x10\xae\x01\x12\x1c\n" + - "\x17DropReason_FragNotFound\x10\xaf\x01\x12\x1e\n" + - "\x19DropReason_ForbiddenICMP6\x10\xb0\x01\x12\x1d\n" + - "\x18DropReason_NotInSrcRange\x10\xb1\x01\x12!\n" + - "\x1cDropReason_ProxyLookupFailed\x10\xb2\x01\x12\x1e\n" + - "\x19DropReason_ProxySetFailed\x10\xb3\x01\x12!\n" + - "\x1cDropReason_ProxyUnknownProto\x10\xb4\x01\x12\x1a\n" + - "\x15DropReason_PolicyDeny\x10\xb5\x01\x12\x1c\n" + - "\x17DropReason_VlanFiltered\x10\xb6\x01\x12\x1a\n" + - "\x15DropReason_InvalidVNI\x10\xb7\x01\x12\x1f\n" + - "\x1aDropReason_InvalidTCBuffer\x10\xb8\x01\x12\x15\n" + - "\x10DropReason_NoSID\x10\xb9\x01\x12 \n" + - "\x1bDropReason_MissingSRv6State\x10\xba\x01\x12\x15\n" + - "\x10DropReason_NAT46\x10\xbb\x01\x12\x15\n" + - "\x10DropReason_NAT64\x10\xbc\x01\x12\"\n" + - "\x1dDropReason_PolicyAuthRequired\x10\xbd\x01\x12\x1c\n" + - "\x17DropReason_CTNoMapFound\x10\xbe\x01\x12\x1e\n" + - "\x19DropReason_SNATNoMapFound\x10\xbf\x01\x12 \n" + - "\x1bDropReason_InvalidClusterID\x10\xc0\x01\x12&\n" + - "!DropReason_DSR_ENCAP_UNSUPP_PROTO\x10\xc1\x01\x12\x1f\n" + - "\x1aDropReason_NoEgressGateway\x10\xc2\x01\x12\"\n" + - "\x1dDropReason_UnencryptedTraffic\x10\xc3\x01\x12\x1b\n" + - "\x16DropReason_TTLExceeded\x10\xc4\x01\x12\x18\n" + - "\x13DropReason_NoNodeID\x10\xc5\x01\x12\x1b\n" + - "\x16DropReason_RateLimited\x10\xc6\x01\x12\x1b\n" + - "\x16DropReason_IGMPHandled\x10\xc7\x01\x12\x1e\n" + - "\x19DropReason_IGMPSubscribed\x10\xc8\x01\x12 \n" + - "\x1bDropReason_MulticastHandled\x10\xc9\x01\x12\x1c\n" + - "\x17DropReason_HostNotReady\x10\xca\x01\x12\x1a\n" + - "\x15DropReason_EpNotReady\x10\xcb\x01\x12\x1d\n" + - "\x18DropReason_PacketMonitor\x10\xdc\x01\x12\x18\n" + - "\x10Drop_InvalidData\x10\x81\x80\x80\x80\x04\x12\x1a\n" + - "\x12Drop_InvalidPacket\x10\x82\x80\x80\x80\x04\x12\x16\n" + - "\x0eDrop_Resources\x10\x83\x80\x80\x80\x04\x12\x15\n" + - "\rDrop_NotReady\x10\x84\x80\x80\x80\x04\x12\x19\n" + - "\x11Drop_Disconnected\x10\x85\x80\x80\x80\x04\x12\x18\n" + - "\x10Drop_NotAccepted\x10\x86\x80\x80\x80\x04\x12\x11\n" + - "\tDrop_Busy\x10\x87\x80\x80\x80\x04\x12\x15\n" + - "\rDrop_Filtered\x10\x88\x80\x80\x80\x04\x12\x19\n" + - "\x11Drop_FilteredVLAN\x10\x89\x80\x80\x80\x04\x12\x1d\n" + - "\x15Drop_UnauthorizedVLAN\x10\x8a\x80\x80\x80\x04\x12\x1c\n" + - "\x14Drop_UnauthorizedMAC\x10\x8b\x80\x80\x80\x04\x12!\n" + - "\x19Drop_FailedSecurityPolicy\x10\x8c\x80\x80\x80\x04\x12\x1f\n" + - "\x17Drop_FailedPvlanSetting\x10\x8d\x80\x80\x80\x04\x12\x10\n" + - "\bDrop_Qos\x10\x8e\x80\x80\x80\x04\x12\x12\n" + - "\n" + - "Drop_Ipsec\x10\x8f\x80\x80\x80\x04\x12\x18\n" + - "\x10Drop_MacSpoofing\x10\x90\x80\x80\x80\x04\x12\x16\n" + - "\x0eDrop_DhcpGuard\x10\x91\x80\x80\x80\x04\x12\x18\n" + - "\x10Drop_RouterGuard\x10\x92\x80\x80\x80\x04\x12\x1b\n" + - "\x13Drop_BridgeReserved\x10\x93\x80\x80\x80\x04\x12\x1c\n" + - "\x14Drop_VirtualSubnetId\x10\x94\x80\x80\x80\x04\x12%\n" + - "\x1dDrop_RequiredExtensionMissing\x10\x95\x80\x80\x80\x04\x12\x1a\n" + - "\x12Drop_InvalidConfig\x10\x96\x80\x80\x80\x04\x12\x18\n" + - "\x10Drop_MTUMismatch\x10\x97\x80\x80\x80\x04\x12\x1c\n" + - "\x14Drop_NativeFwdingReq\x10\x98\x80\x80\x80\x04\x12\x1e\n" + - "\x16Drop_InvalidVlanFormat\x10\x99\x80\x80\x80\x04\x12\x1b\n" + - "\x13Drop_InvalidDestMac\x10\x9a\x80\x80\x80\x04\x12\x1d\n" + - "\x15Drop_InvalidSourceMac\x10\x9b\x80\x80\x80\x04\x12#\n" + - "\x1bDrop_InvalidFirstNBTooSmall\x10\x9c\x80\x80\x80\x04\x12\x10\n" + - "\bDrop_Wnv\x10\x9d\x80\x80\x80\x04\x12\x17\n" + - "\x0fDrop_StormLimit\x10\x9e\x80\x80\x80\x04\x12\x19\n" + - "\x11Drop_InjectedIcmp\x10\x9f\x80\x80\x80\x04\x12(\n" + - " Drop_FailedDestinationListUpdate\x10\xa0\x80\x80\x80\x04\x12\x18\n" + - "\x10Drop_NicDisabled\x10\xa1\x80\x80\x80\x04\x12\x1f\n" + - "\x17Drop_FailedPacketFilter\x10\xa2\x80\x80\x80\x04\x12#\n" + - "\x1bDrop_SwitchDataFlowDisabled\x10\xa3\x80\x80\x80\x04\x12&\n" + - "\x1eDrop_FilteredIsolationUntagged\x10\xa4\x80\x80\x80\x04\x12\x1b\n" + - "\x13Drop_InvalidPDQueue\x10\xa5\x80\x80\x80\x04\x12\x15\n" + - "\rDrop_LowPower\x10\xa6\x80\x80\x80\x04\x12\x12\n" + - "\n" + - "Drop_Pause\x10Ɂ\x80\x80\x04\x12\x12\n" + - "\n" + - "Drop_Reset\x10ʁ\x80\x80\x04\x12\x18\n" + - "\x10Drop_SendAborted\x10ˁ\x80\x80\x04\x12\x1d\n" + - "\x15Drop_ProtocolNotBound\x10́\x80\x80\x04\x12\x14\n" + - "\fDrop_Failure\x10́\x80\x80\x04\x12\x1a\n" + - "\x12Drop_InvalidLength\x10\u0381\x80\x80\x04\x12\x1c\n" + - "\x14Drop_HostOutOfMemory\x10ρ\x80\x80\x04\x12\x19\n" + - "\x11Drop_FrameTooLong\x10Ё\x80\x80\x04\x12\x1a\n" + - "\x12Drop_FrameTooShort\x10с\x80\x80\x04\x12\x1d\n" + - "\x15Drop_FrameLengthError\x10ҁ\x80\x80\x04\x12\x15\n" + - "\rDrop_CrcError\x10Ӂ\x80\x80\x04\x12\x1d\n" + - "\x15Drop_BadFrameChecksum\x10ԁ\x80\x80\x04\x12\x15\n" + - "\rDrop_FcsError\x10Ձ\x80\x80\x04\x12\x18\n" + - "\x10Drop_SymbolError\x10ց\x80\x80\x04\x12\x19\n" + - "\x11Drop_HeadQTimeout\x10ׁ\x80\x80\x04\x12\x1b\n" + - "\x13Drop_StalledDiscard\x10\u0601\x80\x80\x04\x12\x14\n" + - "\fDrop_RxQFull\x10ف\x80\x80\x04\x12\x1b\n" + - "\x13Drop_PhysLayerError\x10ځ\x80\x80\x04\x12\x15\n" + - "\rDrop_DmaError\x10ہ\x80\x80\x04\x12\x1a\n" + - "\x12Drop_FirmwareError\x10܁\x80\x80\x04\x12\x1d\n" + - "\x15Drop_DecryptionFailed\x10݁\x80\x80\x04\x12\x19\n" + - "\x11Drop_BadSignature\x10ށ\x80\x80\x04\x12\x1c\n" + - "\x14Drop_CoalescingError\x10߁\x80\x80\x04\x12\x19\n" + - "\x11Drop_VlanSpoofing\x10၀\x80\x04\x12\x1f\n" + - "\x17Drop_UnallowedEtherType\x10⁀\x80\x04\x12\x16\n" + - "\x0eDrop_VportDown\x10\u3040\x80\x04\x12\x1d\n" + - "\x15Drop_SteeringMismatch\x10䁀\x80\x04\x12\x1b\n" + - "\x13Drop_MicroportError\x10\x91\x83\x80\x80\x04\x12\x17\n" + - "\x0fDrop_VfNotReady\x10\x92\x83\x80\x80\x04\x12\x1e\n" + - "\x16Drop_MicroportNotReady\x10\x93\x83\x80\x80\x04\x12\x17\n" + - "\x0fDrop_VMBusError\x10\x94\x83\x80\x80\x04\x12\x1e\n" + - "\x16Drop_FL_LoopbackPacket\x10ل\x80\x80\x04\x12!\n" + - "\x19Drop_FL_InvalidSnapHeader\x10ڄ\x80\x80\x04\x12#\n" + - "\x1bDrop_FL_InvalidEthernetType\x10ۄ\x80\x80\x04\x12#\n" + - "\x1bDrop_FL_InvalidPacketLength\x10܄\x80\x80\x04\x12#\n" + - "\x1bDrop_FL_HeaderNotContiguous\x10݄\x80\x80\x04\x12&\n" + - "\x1eDrop_FL_InvalidDestinationType\x10ބ\x80\x80\x04\x12!\n" + - "\x19Drop_FL_InterfaceNotReady\x10߄\x80\x80\x04\x12 \n" + - "\x18Drop_FL_ProviderNotReady\x10\xe0\x84\x80\x80\x04\x12\x1e\n" + - "\x16Drop_FL_InvalidLsoInfo\x10ᄀ\x80\x04\x12\x1e\n" + - "\x16Drop_FL_InvalidUsoInfo\x10℀\x80\x04\x12\x1d\n" + - "\x15Drop_FL_InvalidMedium\x10\u3100\x80\x04\x12 \n" + - "\x18Drop_FL_InvalidArpHeader\x10䄀\x80\x04\x12!\n" + - "\x19Drop_FL_NoClientInterface\x10儀\x80\x04\x12!\n" + - "\x19Drop_FL_TooManyNetBuffers\x10愀\x80\x04\x12 \n" + - "\x18Drop_FL_FlsNpiClientDrop\x10焀\x80\x04\x12\x15\n" + - "\rDrop_ArpGuard\x10\xbd\x85\x80\x80\x04\x12\x17\n" + - "\x0fDrop_ArpLimiter\x10\xbe\x85\x80\x80\x04\x12\x18\n" + - "\x10Drop_DhcpLimiter\x10\xbf\x85\x80\x80\x04\x12\x1b\n" + - "\x13Drop_BlockBroadcast\x10\xc0\x85\x80\x80\x04\x12\x17\n" + - "\x0fDrop_BlockNonIp\x10\xc1\x85\x80\x80\x04\x12\x16\n" + - "\x0eDrop_ArpFilter\x10\u0085\x80\x80\x04\x12\x16\n" + - "\x0eDrop_Ipv4Guard\x10Å\x80\x80\x04\x12\x16\n" + - "\x0eDrop_Ipv6Guard\x10ą\x80\x80\x04\x12\x15\n" + - "\rDrop_MacGuard\x10Ņ\x80\x80\x04\x12$\n" + - "\x1cDrop_BroadcastNoDestinations\x10ƅ\x80\x80\x04\x12!\n" + - "\x19Drop_UnicastNoDestination\x10Dž\x80\x80\x04\x12 \n" + - "\x18Drop_UnicastPortNotReady\x10ȅ\x80\x80\x04\x12!\n" + - "\x19Drop_SwitchCallbackFailed\x10Ʌ\x80\x80\x04\x12\x1a\n" + - "\x12Drop_Icmpv6Limiter\x10ʅ\x80\x80\x04\x12\x16\n" + - "\x0eDrop_Intercept\x10˅\x80\x80\x04\x12\x1b\n" + - "\x13Drop_InterceptBlock\x10̅\x80\x80\x04\x12\x15\n" + - "\rDrop_NDPGuard\x10ͅ\x80\x80\x04\x12\x18\n" + - "\x10Drop_PortBlocked\x10΅\x80\x80\x04\x12\x19\n" + - "\x11Drop_NicSuspended\x10υ\x80\x80\x04\x12 \n" + - "\x18Drop_NL_BadSourceAddress\x10\x85\x87\x80\x80\x04\x12\"\n" + - "\x1aDrop_NL_NotLocallyDestined\x10\x86\x87\x80\x80\x04\x12#\n" + - "\x1bDrop_NL_ProtocolUnreachable\x10\x87\x87\x80\x80\x04\x12\x1f\n" + - "\x17Drop_NL_PortUnreachable\x10\x88\x87\x80\x80\x04\x12\x19\n" + - "\x11Drop_NL_BadLength\x10\x89\x87\x80\x80\x04\x12\x1f\n" + - "\x17Drop_NL_MalformedHeader\x10\x8a\x87\x80\x80\x04\x12\x17\n" + - "\x0fDrop_NL_NoRoute\x10\x8b\x87\x80\x80\x04\x12\x1b\n" + - "\x13Drop_NL_BeyondScope\x10\x8c\x87\x80\x80\x04\x12\x1e\n" + - "\x16Drop_NL_InspectionDrop\x10\x8d\x87\x80\x80\x04\x12%\n" + - "\x1dDrop_NL_TooManyDecapsulations\x10\x8e\x87\x80\x80\x04\x12*\n" + - "\"Drop_NL_AdministrativelyProhibited\x10\x8f\x87\x80\x80\x04\x12\x1b\n" + - "\x13Drop_NL_BadChecksum\x10\x90\x87\x80\x80\x04\x12\x1e\n" + - "\x16Drop_NL_ReceivePathMax\x10\x91\x87\x80\x80\x04\x12 \n" + - "\x18Drop_NL_HopLimitExceeded\x10\x92\x87\x80\x80\x04\x12\"\n" + - "\x1aDrop_NL_AddressUnreachable\x10\x93\x87\x80\x80\x04\x12\x19\n" + - "\x11Drop_NL_RscPacket\x10\x94\x87\x80\x80\x04\x12\x1e\n" + - "\x16Drop_NL_ForwardPathMax\x10\x95\x87\x80\x80\x04\x12$\n" + - "\x1cDrop_NL_ArbitrationUnhandled\x10\x96\x87\x80\x80\x04\x12 \n" + - "\x18Drop_NL_InspectionAbsorb\x10\x97\x87\x80\x80\x04\x12'\n" + - "\x1fDrop_NL_DontFragmentMtuExceeded\x10\x98\x87\x80\x80\x04\x12$\n" + - "\x1cDrop_NL_BufferLengthExceeded\x10\x99\x87\x80\x80\x04\x12(\n" + - " Drop_NL_AddressResolutionTimeout\x10\x9a\x87\x80\x80\x04\x12(\n" + - " Drop_NL_AddressResolutionFailure\x10\x9b\x87\x80\x80\x04\x12\x1c\n" + - "\x14Drop_NL_IpsecFailure\x10\x9c\x87\x80\x80\x04\x12'\n" + - "\x1fDrop_NL_ExtensionHeadersFailure\x10\x9d\x87\x80\x80\x04\x12 \n" + - "\x18Drop_NL_IpsnpiClientDrop\x10\x9e\x87\x80\x80\x04\x12\"\n" + - "\x1aDrop_NL_UnsupportedOffload\x10\x9f\x87\x80\x80\x04\x12\x1e\n" + - "\x16Drop_NL_RoutingFailure\x10\xa0\x87\x80\x80\x04\x12$\n" + - "\x1cDrop_NL_AncillaryDataFailure\x10\xa1\x87\x80\x80\x04\x12\x1e\n" + - "\x16Drop_NL_RawDataFailure\x10\xa2\x87\x80\x80\x04\x12#\n" + - "\x1bDrop_NL_SessionStateFailure\x10\xa3\x87\x80\x80\x04\x12-\n" + - "%Drop_NL_IpsnpiModifiedButNotForwarded\x10\xa4\x87\x80\x80\x04\x12\x1f\n" + - "\x17Drop_NL_IpsnpiNoNextHop\x10\xa5\x87\x80\x80\x04\x12#\n" + - "\x1bDrop_NL_IpsnpiNoCompartment\x10\xa6\x87\x80\x80\x04\x12!\n" + - "\x19Drop_NL_IpsnpiNoInterface\x10\xa7\x87\x80\x80\x04\x12$\n" + - "\x1cDrop_NL_IpsnpiNoSubInterface\x10\xa8\x87\x80\x80\x04\x12'\n" + - "\x1fDrop_NL_IpsnpiInterfaceDisabled\x10\xa9\x87\x80\x80\x04\x12(\n" + - " Drop_NL_IpsnpiSegmentationFailed\x10\xaa\x87\x80\x80\x04\x12&\n" + - "\x1eDrop_NL_IpsnpiNoEthernetHeader\x10\xab\x87\x80\x80\x04\x12(\n" + - " Drop_NL_IpsnpiUnexpectedFragment\x10\xac\x87\x80\x80\x04\x12.\n" + - "&Drop_NL_IpsnpiUnsupportedInterfaceType\x10\xad\x87\x80\x80\x04\x12$\n" + - "\x1cDrop_NL_IpsnpiInvalidLsoInfo\x10\xae\x87\x80\x80\x04\x12$\n" + - "\x1cDrop_NL_IpsnpiInvalidUsoInfo\x10\xaf\x87\x80\x80\x04\x12\x1d\n" + - "\x15Drop_NL_InternalError\x10\xb0\x87\x80\x80\x04\x12*\n" + - "\"Drop_NL_AdministrativelyConfigured\x10\xb1\x87\x80\x80\x04\x12\x19\n" + - "\x11Drop_NL_BadOption\x10\xb2\x87\x80\x80\x04\x12\"\n" + - "\x1aDrop_NL_LoopbackDisallowed\x10\xb3\x87\x80\x80\x04\x12\x1c\n" + - "\x14Drop_NL_SmallerScope\x10\xb4\x87\x80\x80\x04\x12\x19\n" + - "\x11Drop_NL_QueueFull\x10\xb5\x87\x80\x80\x04\x12!\n" + - "\x19Drop_NL_InterfaceDisabled\x10\xb6\x87\x80\x80\x04\x12\x1b\n" + - "\x13Drop_NL_IcmpGeneric\x10\xb7\x87\x80\x80\x04\x12#\n" + - "\x1bDrop_NL_IcmpTruncatedHeader\x10\xb8\x87\x80\x80\x04\x12#\n" + - "\x1bDrop_NL_IcmpInvalidChecksum\x10\xb9\x87\x80\x80\x04\x12\x1e\n" + - "\x16Drop_NL_IcmpInspection\x10\xba\x87\x80\x80\x04\x12-\n" + - "%Drop_NL_IcmpNeighborDiscoveryLoopback\x10\xbb\x87\x80\x80\x04\x12\x1f\n" + - "\x17Drop_NL_IcmpUnknownType\x10\xbc\x87\x80\x80\x04\x12%\n" + - "\x1dDrop_NL_IcmpTruncatedIpHeader\x10\xbd\x87\x80\x80\x04\x12%\n" + - "\x1dDrop_NL_IcmpOversizedIpHeader\x10\xbe\x87\x80\x80\x04\x12\x1d\n" + - "\x15Drop_NL_IcmpNoHandler\x10\xbf\x87\x80\x80\x04\x12%\n" + - "\x1dDrop_NL_IcmpRespondingToError\x10\xc0\x87\x80\x80\x04\x12!\n" + - "\x19Drop_NL_IcmpInvalidSource\x10\xc1\x87\x80\x80\x04\x12&\n" + - "\x1eDrop_NL_IcmpInterfaceRateLimit\x10\u0087\x80\x80\x04\x12!\n" + - "\x19Drop_NL_IcmpPathRateLimit\x10Ç\x80\x80\x04\x12\x1b\n" + - "\x13Drop_NL_IcmpNoRoute\x10ć\x80\x80\x04\x12+\n" + - "#Drop_NL_IcmpMatchingRequestNotFound\x10Ň\x80\x80\x04\x12\"\n" + - "\x1aDrop_NL_IcmpBufferTooSmall\x10Ƈ\x80\x80\x04\x12&\n" + - "\x1eDrop_NL_IcmpAncillaryDataQuery\x10LJ\x80\x80\x04\x12%\n" + - "\x1dDrop_NL_IcmpIncorrectHopLimit\x10ȇ\x80\x80\x04\x12\x1f\n" + - "\x17Drop_NL_IcmpUnknownCode\x10ɇ\x80\x80\x04\x12&\n" + - "\x1eDrop_NL_IcmpSourceNotLinkLocal\x10ʇ\x80\x80\x04\x12%\n" + - "\x1dDrop_NL_IcmpTruncatedNdHeader\x10ˇ\x80\x80\x04\x12.\n" + - "&Drop_NL_IcmpInvalidNdOptSourceLinkAddr\x10̇\x80\x80\x04\x12#\n" + - "\x1bDrop_NL_IcmpInvalidNdOptMtu\x10͇\x80\x80\x04\x121\n" + - ")Drop_NL_IcmpInvalidNdOptPrefixInformation\x10·\x80\x80\x04\x120\n" + - "(Drop_NL_IcmpInvalidNdOptRouteInformation\x10χ\x80\x80\x04\x12%\n" + - "\x1dDrop_NL_IcmpInvalidNdOptRdnss\x10Ї\x80\x80\x04\x12%\n" + - "\x1dDrop_NL_IcmpInvalidNdOptDnssl\x10ч\x80\x80\x04\x12(\n" + - " Drop_NL_IcmpPacketParsingFailure\x10҇\x80\x80\x04\x12\x1e\n" + - "\x16Drop_NL_IcmpDisallowed\x10Ӈ\x80\x80\x04\x12.\n" + - "&Drop_NL_IcmpInvalidRouterAdvertisement\x10ԇ\x80\x80\x04\x12+\n" + - "#Drop_NL_IcmpSourceFromDifferentLink\x10Շ\x80\x80\x04\x126\n" + - ".Drop_NL_IcmpInvalidRedirectDestinationOrTarget\x10և\x80\x80\x04\x12#\n" + - "\x1bDrop_NL_IcmpInvalidNdTarget\x10ׇ\x80\x80\x04\x12+\n" + - "#Drop_NL_IcmpNaMulticastAndSolicited\x10؇\x80\x80\x04\x12-\n" + - "%Drop_NL_IcmpNdLinkLayerAddressIsLocal\x10ه\x80\x80\x04\x12(\n" + - " Drop_NL_IcmpDuplicateEchoRequest\x10ڇ\x80\x80\x04\x12'\n" + - "\x1fDrop_NL_IcmpNotAPotentialRouter\x10ۇ\x80\x80\x04\x12#\n" + - "\x1bDrop_NL_IcmpInvalidMldQuery\x10܇\x80\x80\x04\x12$\n" + - "\x1cDrop_NL_IcmpInvalidMldReport\x10݇\x80\x80\x04\x12+\n" + - "#Drop_NL_IcmpLocallySourcedMldReport\x10އ\x80\x80\x04\x12&\n" + - "\x1eDrop_NL_IcmpNotLocallyDestined\x10߇\x80\x80\x04\x12 \n" + - "\x18Drop_NL_ArpInvalidSource\x10\xe0\x87\x80\x80\x04\x12 \n" + - "\x18Drop_NL_ArpInvalidTarget\x10ᇀ\x80\x04\x12\"\n" + - "\x1aDrop_NL_ArpDlSourceIsLocal\x10⇀\x80\x04\x12%\n" + - "\x1dDrop_NL_ArpNotLocallyDestined\x10㇀\x80\x04\x12\x1f\n" + - "\x17Drop_NL_NlClientDiscard\x10䇀\x80\x04\x12.\n" + - "&Drop_NL_IpsnpiUroSegmentSizeExceedsMtu\x10净\x80\x04\x12$\n" + - "\x1cDrop_NL_IcmpFragmentedPacket\x10懀\x80\x04\x12'\n" + - "\x1fDrop_NL_FirstFragmentIncomplete\x10燀\x80\x04\x12\x1f\n" + - "\x17Drop_NL_SourceViolation\x10臀\x80\x04\x12\x1d\n" + - "\x15Drop_NL_IcmpJumbogram\x10釀\x80\x04\x12\x1c\n" + - "\x14Drop_NL_SwUsoFailure\x10ꇀ\x80\x04\x12#\n" + - "\x1bDrop_INET_SourceUnspecified\x10\xb0\x89\x80\x80\x04\x12&\n" + - "\x1eDrop_INET_DestinationMulticast\x10\xb1\x89\x80\x80\x04\x12\x1f\n" + - "\x17Drop_INET_HeaderInvalid\x10\xb2\x89\x80\x80\x04\x12!\n" + - "\x19Drop_INET_ChecksumInvalid\x10\xb3\x89\x80\x80\x04\x12\"\n" + - "\x1aDrop_INET_EndpointNotFound\x10\xb4\x89\x80\x80\x04\x12\x1f\n" + - "\x17Drop_INET_ConnectedPath\x10\xb5\x89\x80\x80\x04\x12\x1e\n" + - "\x16Drop_INET_SessionState\x10\xb6\x89\x80\x80\x04\x12#\n" + - "\x1bDrop_INET_ReceiveInspection\x10\xb7\x89\x80\x80\x04\x12\x1c\n" + - "\x14Drop_INET_AckInvalid\x10\xb8\x89\x80\x80\x04\x12\x1d\n" + - "\x15Drop_INET_ExpectedSyn\x10\xb9\x89\x80\x80\x04\x12\x15\n" + - "\rDrop_INET_Rst\x10\xba\x89\x80\x80\x04\x12\x1c\n" + - "\x14Drop_INET_SynRcvdSyn\x10\xbb\x89\x80\x80\x04\x12%\n" + - "\x1dDrop_INET_SimultaneousConnect\x10\xbc\x89\x80\x80\x04\x12\x1c\n" + - "\x14Drop_INET_PawsFailed\x10\xbd\x89\x80\x80\x04\x12\x1c\n" + - "\x14Drop_INET_LandAttack\x10\xbe\x89\x80\x80\x04\x12\x1d\n" + - "\x15Drop_INET_MissedReset\x10\xbf\x89\x80\x80\x04\x12\x1f\n" + - "\x17Drop_INET_OutsideWindow\x10\xc0\x89\x80\x80\x04\x12\"\n" + - "\x1aDrop_INET_DuplicateSegment\x10\xc1\x89\x80\x80\x04\x12\x1e\n" + - "\x16Drop_INET_ClosedWindow\x10\u0089\x80\x80\x04\x12\x1c\n" + - "\x14Drop_INET_TcbRemoved\x10É\x80\x80\x04\x12\x1a\n" + - "\x12Drop_INET_FinWait2\x10ĉ\x80\x80\x04\x12$\n" + - "\x1cDrop_INET_ReassemblyConflict\x10ʼn\x80\x80\x04\x12\x1d\n" + - "\x15Drop_INET_FinReceived\x10Ɖ\x80\x80\x04\x12&\n" + - "\x1eDrop_INET_ListenerInvalidFlags\x10lj\x80\x80\x04\x12\"\n" + - "\x1aDrop_INET_TcbNotInTcbTable\x10ȉ\x80\x80\x04\x125\n" + - "-Drop_INET_TimeWaitTcbReceivedRstOutsideWindow\x10ɉ\x80\x80\x04\x12-\n" + - "%Drop_INET_TimeWaitTcbSynAndOtherFlags\x10ʉ\x80\x80\x04\x12\x1d\n" + - "\x15Drop_INET_TimeWaitTcb\x10ˉ\x80\x80\x04\x121\n" + - ")Drop_INET_SynAckWithFastopenCookieRequest\x10̉\x80\x80\x04\x12\x1d\n" + - "\x15Drop_INET_PauseAccept\x10͉\x80\x80\x04\x12\x1b\n" + - "\x13Drop_INET_SynAttack\x10Ή\x80\x80\x04\x12\"\n" + - "\x1aDrop_INET_AcceptInspection\x10ω\x80\x80\x04\x12#\n" + - "\x1bDrop_INET_AcceptRedirection\x10Љ\x80\x80\x04\x12\"\n" + - "\x1aDrop_SlbMux_ParsingFailure\x10\x95\x8a\x80\x80\x04\x12%\n" + - "\x1dDrop_SlbMux_FirstFragmentMiss\x10\x96\x8a\x80\x80\x04\x125\n" + - "-Drop_SlbMux_ICMPErrorPayloadValidationFailure\x10\x97\x8a\x80\x80\x04\x121\n" + - ")Drop_SlbMux_ICMPErrorPacketMatchNoSession\x10\x98\x8a\x80\x80\x04\x127\n" + - "/Drop_SlbMux_ExternalHairpinNexthopLookupFailure\x10\x99\x8a\x80\x80\x04\x12+\n" + - "#Drop_SlbMux_NoMatchingStaticMapping\x10\x9a\x8a\x80\x80\x04\x12+\n" + - "#Drop_SlbMux_NexthopReferenceFailure\x10\x9b\x8a\x80\x80\x04\x12\"\n" + - "\x1aDrop_SlbMux_CloningFailure\x10\x9c\x8a\x80\x80\x04\x12&\n" + - "\x1eDrop_SlbMux_TranslationFailure\x10\x9d\x8a\x80\x80\x04\x12$\n" + - "\x1cDrop_SlbMux_HopLimitExceeded\x10\x9e\x8a\x80\x80\x04\x12'\n" + - "\x1fDrop_SlbMux_PacketBiggerThanMTU\x10\x9f\x8a\x80\x80\x04\x120\n" + - "(Drop_SlbMux_UnexpectedRouteLookupFailure\x10\xa0\x8a\x80\x80\x04\x12\x1b\n" + - "\x13Drop_SlbMux_NoRoute\x10\xa1\x8a\x80\x80\x04\x12*\n" + - "\"Drop_SlbMux_SessionCreationFailure\x10\xa2\x8a\x80\x80\x04\x123\n" + - "+Drop_SlbMux_NexthopNotOverExternalInterface\x10\xa3\x8a\x80\x80\x04\x12;\n" + - "3Drop_SlbMux_NexthopExternalInterfaceMissNATInstance\x10\xa4\x8a\x80\x80\x04\x122\n" + - "*Drop_SlbMux_NATItselfCantBeInternalNexthop\x10\xa5\x8a\x80\x80\x04\x129\n" + - "1Drop_SlbMux_PacketRoutableInItsArrivalCompartment\x10\xa6\x8a\x80\x80\x04\x127\n" + - "/Drop_SlbMux_PacketTransportProtocolNotSupported\x10\xa7\x8a\x80\x80\x04\x12+\n" + - "#Drop_SlbMux_PacketIsDestinedLocally\x10\xa8\x8a\x80\x80\x04\x12=\n" + - "5Drop_SlbMux_PacketDestinationIPandPortNotSubjectToNAT\x10\xa9\x8a\x80\x80\x04\x12\x1d\n" + - "\x15Drop_SlbMux_MuxReject\x10\xaa\x8a\x80\x80\x04\x12$\n" + - "\x1cDrop_SlbMux_DipLookupFailure\x10\xab\x8a\x80\x80\x04\x12+\n" + - "#Drop_SlbMux_MuxEncapsulationFailure\x10\xac\x8a\x80\x80\x04\x12.\n" + - "&Drop_SlbMux_InvalidDiagPacketEncapType\x10\xad\x8a\x80\x80\x04\x12(\n" + - " Drop_SlbMux_DiagPacketIsRedirect\x10\xae\x8a\x80\x80\x04\x12*\n" + - "\"Drop_SlbMux_UnableToHandleRedirect\x10\xaf\x8a\x80\x80\x04\x12\x19\n" + - "\x11Drop_Ipsec_BadSpi\x10\xf9\x8a\x80\x80\x04\x12$\n" + - "\x1cDrop_Ipsec_SALifetimeExpired\x10\xfa\x8a\x80\x80\x04\x12\x1a\n" + - "\x12Drop_Ipsec_WrongSA\x10\xfb\x8a\x80\x80\x04\x12$\n" + - "\x1cDrop_Ipsec_ReplayCheckFailed\x10\xfc\x8a\x80\x80\x04\x12 \n" + - "\x18Drop_Ipsec_InvalidPacket\x10\xfd\x8a\x80\x80\x04\x12'\n" + - "\x1fDrop_Ipsec_IntegrityCheckFailed\x10\xfe\x8a\x80\x80\x04\x12 \n" + - "\x18Drop_Ipsec_ClearTextDrop\x10\xff\x8a\x80\x80\x04\x12#\n" + - "\x1bDrop_Ipsec_AuthFirewallDrop\x10\x80\x8b\x80\x80\x04\x12\x1f\n" + - "\x17Drop_Ipsec_ThrottleDrop\x10\x81\x8b\x80\x80\x04\x12\x1d\n" + - "\x15Drop_Ipsec_Dosp_Block\x10\x82\x8b\x80\x80\x04\x12)\n" + - "!Drop_Ipsec_Dosp_ReceivedMulticast\x10\x83\x8b\x80\x80\x04\x12%\n" + - "\x1dDrop_Ipsec_Dosp_InvalidPacket\x10\x84\x8b\x80\x80\x04\x12)\n" + - "!Drop_Ipsec_Dosp_StateLookupFailed\x10\x85\x8b\x80\x80\x04\x12\"\n" + - "\x1aDrop_Ipsec_Dosp_MaxEntries\x10\x86\x8b\x80\x80\x04\x12(\n" + - " Drop_Ipsec_Dosp_KeymodNotAllowed\x10\x87\x8b\x80\x80\x04\x12/\n" + - "'Drop_Ipsec_Dosp_MaxPerIpRateLimitQueues\x10\x88\x8b\x80\x80\x04\x12\x1b\n" + - "\x13Drop_Ipsec_NoMemory\x10\x89\x8b\x80\x80\x04\x12\x1f\n" + - "\x17Drop_Ipsec_Unsuccessful\x10\x8a\x8b\x80\x80\x04\x12.\n" + - "&Drop_NetCx_NetPacketLayoutParseFailure\x10\u074b\x80\x80\x04\x12*\n" + - "\"Drop_NetCx_SoftwareChecksumFailure\x10ދ\x80\x80\x04\x12\x1f\n" + - "\x17Drop_NetCx_NicQueueStop\x10ߋ\x80\x80\x04\x12)\n" + - "!Drop_NetCx_InvalidNetBufferLength\x10\xe0\x8b\x80\x80\x04\x12\x1d\n" + - "\x15Drop_NetCx_LSOFailure\x10ዀ\x80\x04\x12\x1d\n" + - "\x15Drop_NetCx_USOFailure\x10⋀\x80\x04\x125\n" + - "-Drop_NetCx_BufferBounceFailureAndPacketIgnore\x10㋀\x80\x04\x12\x17\n" + - "\x0fDrop_Http_Begin\x10\xb8\x97\x80\x80\x04\x12\x1f\n" + - "\x17Drop_Http_UlError_Begin\x10\xb9\x97\x80\x80\x04\x12\x19\n" + - "\x11Drop_Http_UlError\x10\xba\x97\x80\x80\x04\x12\x1d\n" + - "\x15Drop_Http_UlErrorVerb\x10\xbb\x97\x80\x80\x04\x12\x1c\n" + - "\x14Drop_Http_UlErrorUrl\x10\xbc\x97\x80\x80\x04\x12\x1f\n" + - "\x17Drop_Http_UlErrorHeader\x10\xbd\x97\x80\x80\x04\x12\x1d\n" + - "\x15Drop_Http_UlErrorHost\x10\xbe\x97\x80\x80\x04\x12\x1c\n" + - "\x14Drop_Http_UlErrorNum\x10\xbf\x97\x80\x80\x04\x12$\n" + - "\x1cDrop_Http_UlErrorFieldLength\x10\xc0\x97\x80\x80\x04\x12&\n" + - "\x1eDrop_Http_UlErrorRequestLength\x10\xc1\x97\x80\x80\x04\x12%\n" + - "\x1dDrop_Http_UlErrorUnauthorized\x10\u0097\x80\x80\x04\x12%\n" + - "\x1dDrop_Http_UlErrorForbiddenUrl\x10×\x80\x80\x04\x12!\n" + - "\x19Drop_Http_UlErrorNotFound\x10ė\x80\x80\x04\x12&\n" + - "\x1eDrop_Http_UlErrorContentLength\x10ŗ\x80\x80\x04\x12+\n" + - "#Drop_Http_UlErrorPreconditionFailed\x10Ɨ\x80\x80\x04\x12'\n" + - "\x1fDrop_Http_UlErrorEntityTooLarge\x10Ǘ\x80\x80\x04\x12\"\n" + - "\x1aDrop_Http_UlErrorUrlLength\x10ȗ\x80\x80\x04\x12,\n" + - "$Drop_Http_UlErrorRangeNotSatisfiable\x10ɗ\x80\x80\x04\x12+\n" + - "#Drop_Http_UlErrorMisdirectedRequest\x10ʗ\x80\x80\x04\x12'\n" + - "\x1fDrop_Http_UlErrorInternalServer\x10˗\x80\x80\x04\x12'\n" + - "\x1fDrop_Http_UlErrorNotImplemented\x10̗\x80\x80\x04\x12$\n" + - "\x1cDrop_Http_UlErrorUnavailable\x10͗\x80\x80\x04\x12(\n" + - " Drop_Http_UlErrorConnectionLimit\x10Η\x80\x80\x04\x12,\n" + - "$Drop_Http_UlErrorRapidFailProtection\x10ϗ\x80\x80\x04\x12)\n" + - "!Drop_Http_UlErrorRequestQueueFull\x10З\x80\x80\x04\x12(\n" + - " Drop_Http_UlErrorDisabledByAdmin\x10ї\x80\x80\x04\x12&\n" + - "\x1eDrop_Http_UlErrorDisabledByApp\x10җ\x80\x80\x04\x12'\n" + - "\x1fDrop_Http_UlErrorJobObjectFired\x10ӗ\x80\x80\x04\x12$\n" + - "\x1cDrop_Http_UlErrorAppPoolBusy\x10ԗ\x80\x80\x04\x12 \n" + - "\x18Drop_Http_UlErrorVersion\x10\u0557\x80\x80\x04\x12\x1d\n" + - "\x15Drop_Http_UlError_End\x10֗\x80\x80\x04\x12!\n" + - "\x19Drop_Http_UxDuoFaultBegin\x10Ț\x80\x80\x04\x12%\n" + - "\x1dDrop_Http_UxDuoFaultUserAbort\x10ɚ\x80\x80\x04\x12&\n" + - "\x1eDrop_Http_UxDuoFaultCollection\x10ʚ\x80\x80\x04\x12-\n" + - "%Drop_Http_UxDuoFaultClientResetStream\x10˚\x80\x80\x04\x12*\n" + - "\"Drop_Http_UxDuoFaultMethodNotFound\x10̚\x80\x80\x04\x12*\n" + - "\"Drop_Http_UxDuoFaultSchemeMismatch\x10͚\x80\x80\x04\x12*\n" + - "\"Drop_Http_UxDuoFaultSchemeNotFound\x10Κ\x80\x80\x04\x12(\n" + - " Drop_Http_UxDuoFaultDataAfterEnd\x10Ϛ\x80\x80\x04\x12(\n" + - " Drop_Http_UxDuoFaultPathNotFound\x10К\x80\x80\x04\x12+\n" + - "#Drop_Http_UxDuoFaultHalfClosedLocal\x10њ\x80\x80\x04\x12,\n" + - "$Drop_Http_UxDuoFaultIncompatibleAuth\x10Қ\x80\x80\x04\x12'\n" + - "\x1fDrop_Http_UxDuoFaultDeprecated3\x10Ӛ\x80\x80\x04\x12-\n" + - "%Drop_Http_UxDuoFaultClientCertBlocked\x10Ԛ\x80\x80\x04\x12+\n" + - "#Drop_Http_UxDuoFaultHeaderNameEmpty\x10՚\x80\x80\x04\x12'\n" + - "\x1fDrop_Http_UxDuoFaultIllegalSend\x10֚\x80\x80\x04\x12+\n" + - "#Drop_Http_UxDuoFaultPushUpperAttach\x10ך\x80\x80\x04\x12-\n" + - "%Drop_Http_UxDuoFaultStreamUpperAttach\x10ؚ\x80\x80\x04\x12-\n" + - "%Drop_Http_UxDuoFaultActiveStreamLimit\x10ٚ\x80\x80\x04\x12-\n" + - "%Drop_Http_UxDuoFaultAuthorityNotFound\x10ښ\x80\x80\x04\x12*\n" + - "\"Drop_Http_UxDuoFaultUnexpectedTail\x10ۚ\x80\x80\x04\x12%\n" + - "\x1dDrop_Http_UxDuoFaultTruncated\x10ܚ\x80\x80\x04\x12(\n" + - " Drop_Http_UxDuoFaultResponseHold\x10ݚ\x80\x80\x04\x12*\n" + - "\"Drop_Http_UxDuoFaultRequestChunked\x10ޚ\x80\x80\x04\x120\n" + - "(Drop_Http_UxDuoFaultRequestContentLength\x10ߚ\x80\x80\x04\x12+\n" + - "#Drop_Http_UxDuoFaultResponseChunked\x10\xe0\x9a\x80\x80\x04\x121\n" + - ")Drop_Http_UxDuoFaultResponseContentLength\x10\u1680\x80\x04\x124\n" + - ",Drop_Http_UxDuoFaultResponseTransferEncoding\x10⚀\x80\x04\x12(\n" + - " Drop_Http_UxDuoFaultResponseLine\x10㚀\x80\x04\x12*\n" + - "\"Drop_Http_UxDuoFaultResponseHeader\x10䚀\x80\x04\x12#\n" + - "\x1bDrop_Http_UxDuoFaultConnect\x10嚀\x80\x04\x12&\n" + - "\x1eDrop_Http_UxDuoFaultChunkStart\x10暀\x80\x04\x12'\n" + - "\x1fDrop_Http_UxDuoFaultChunkLength\x10皀\x80\x04\x12%\n" + - "\x1dDrop_Http_UxDuoFaultChunkStop\x10蚀\x80\x04\x120\n" + - "(Drop_Http_UxDuoFaultHeadersAfterTrailers\x10隀\x80\x04\x12+\n" + - "#Drop_Http_UxDuoFaultHeadersAfterEnd\x10Ꚁ\x80\x04\x12*\n" + - "\"Drop_Http_UxDuoFaultEndlessTrailer\x10뚀\x80\x04\x12,\n" + - "$Drop_Http_UxDuoFaultTransferEncoding\x10욀\x80\x04\x123\n" + - "+Drop_Http_UxDuoFaultMultipleTransferCodings\x10횀\x80\x04\x12$\n" + - "\x1cDrop_Http_UxDuoFaultPushBody\x10\ue680\x80\x04\x12+\n" + - "#Drop_Http_UxDuoFaultStreamAbandoned\x10\uf680\x80\x04\x12)\n" + - "!Drop_Http_UxDuoFaultMalformedHost\x10\U0001a000\x04\x121\n" + - ")Drop_Http_UxDuoFaultDecompressionOverflow\x10\U0005a000\x04\x12-\n" + - "%Drop_Http_UxDuoFaultIllegalHeaderName\x10\U0009a000\x04\x12.\n" + - "&Drop_Http_UxDuoFaultIllegalHeaderValue\x10\U000da000\x04\x120\n" + - "(Drop_Http_UxDuoFaultConnHeaderDisallowed\x10\xf4\x9a\x80\x80\x04\x12/\n" + - "'Drop_Http_UxDuoFaultConnHeaderMalformed\x10\xf5\x9a\x80\x80\x04\x12,\n" + - "$Drop_Http_UxDuoFaultCookieReassembly\x10\xf6\x9a\x80\x80\x04\x12(\n" + - " Drop_Http_UxDuoFaultStatusHeader\x10\xf7\x9a\x80\x80\x04\x12,\n" + - "$Drop_Http_UxDuoFaultSchemeDisallowed\x10\xf8\x9a\x80\x80\x04\x12*\n" + - "\"Drop_Http_UxDuoFaultPathDisallowed\x10\xf9\x9a\x80\x80\x04\x12$\n" + - "\x1cDrop_Http_UxDuoFaultPushHost\x10\xfa\x9a\x80\x80\x04\x12*\n" + - "\"Drop_Http_UxDuoFaultGoawayReceived\x10\xfb\x9a\x80\x80\x04\x12*\n" + - "\"Drop_Http_UxDuoFaultAbortLegacyApp\x10\xfc\x9a\x80\x80\x04\x123\n" + - "+Drop_Http_UxDuoFaultUpgradeHeaderDisallowed\x10\xfd\x9a\x80\x80\x04\x121\n" + - ")Drop_Http_UxDuoFaultResponseUpgradeHeader\x10\xfe\x9a\x80\x80\x04\x125\n" + - "-Drop_Http_UxDuoFaultKeepAliveHeaderDisallowed\x10\xff\x9a\x80\x80\x04\x123\n" + - "+Drop_Http_UxDuoFaultResponseKeepAliveHeader\x10\x80\x9b\x80\x80\x04\x125\n" + - "-Drop_Http_UxDuoFaultProxyConnHeaderDisallowed\x10\x81\x9b\x80\x80\x04\x123\n" + - "+Drop_Http_UxDuoFaultResponseProxyConnHeader\x10\x82\x9b\x80\x80\x04\x12/\n" + - "'Drop_Http_UxDuoFaultConnectionGoingAway\x10\x83\x9b\x80\x80\x04\x126\n" + - ".Drop_Http_UxDuoFaultTransferEncodingDisallowed\x10\x84\x9b\x80\x80\x04\x123\n" + - "+Drop_Http_UxDuoFaultContentLengthDisallowed\x10\x85\x9b\x80\x80\x04\x12-\n" + - "%Drop_Http_UxDuoFaultTrailerDisallowed\x10\x86\x9b\x80\x80\x04\x12\x1f\n" + - "\x17Drop_Http_UxDuoFaultEnd\x10\x87\x9b\x80\x80\x04\x12#\n" + - "\x1bDrop_Http_ReceiveSuppressed\x10\x90\x9c\x80\x80\x04\x12\x19\n" + - "\x11Drop_Http_Generic\x10؝\x80\x80\x04\x12\"\n" + - "\x1aDrop_Http_InvalidParameter\x10ٝ\x80\x80\x04\x12'\n" + - "\x1fDrop_Http_InsufficientResources\x10ڝ\x80\x80\x04\x12\x1f\n" + - "\x17Drop_Http_InvalidHandle\x10\u06dd\x80\x80\x04\x12\x1e\n" + - "\x16Drop_Http_NotSupported\x10ܝ\x80\x80\x04\x12 \n" + - "\x18Drop_Http_BadNetworkPath\x10ݝ\x80\x80\x04\x12\x1f\n" + - "\x17Drop_Http_InternalError\x10ޝ\x80\x80\x04\x12\x1f\n" + - "\x17Drop_Http_NoSuchPackage\x10ߝ\x80\x80\x04\x12\"\n" + - "\x1aDrop_Http_PrivilegeNotHeld\x10\xe0\x9d\x80\x80\x04\x12#\n" + - "\x1bDrop_Http_CannotImpersonate\x10ᝀ\x80\x04\x12\x1e\n" + - "\x16Drop_Http_LogonFailure\x10❀\x80\x04\x12$\n" + - "\x1cDrop_Http_NoSuchLogonSession\x10㝀\x80\x04\x12\x1e\n" + - "\x16Drop_Http_AccessDenied\x10䝀\x80\x04\x12 \n" + - "\x18Drop_Http_NoLogonServers\x10址\x80\x04\x12$\n" + - "\x1cDrop_Http_TimeDifferenceAtDc\x10杀\x80\x04\x12\x15\n" + - "\rDrop_Http_End\x10\xa0\x9f\x80\x80\x04B'Z%github.com/microsoft/retina/pkg/utilsb\x06proto3" +func (x *RetinaMetadata) GetPreviouslyObservedBytes() uint32 { + if x != nil { + return x.PreviouslyObservedBytes + } + return 0 +} + +func (x *RetinaMetadata) GetPreviouslyObservedTcpFlags() map[string]uint32 { + if x != nil { + return x.PreviouslyObservedTcpFlags + } + return nil +} + +var File_pkg_utils_metadata_windows_proto protoreflect.FileDescriptor + +var file_pkg_utils_metadata_windows_proto_rawDesc = []byte{ + 0x0a, 0x20, 0x70, 0x6b, 0x67, 0x2f, 0x75, 0x74, 0x69, 0x6c, 0x73, 0x2f, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x5f, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x05, 0x75, 0x74, 0x69, 0x6c, 0x73, 0x22, 0x86, 0x04, 0x0a, 0x0e, 0x52, 0x65, + 0x74, 0x69, 0x6e, 0x61, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, + 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x62, 0x79, 0x74, + 0x65, 0x73, 0x12, 0x29, 0x0a, 0x08, 0x64, 0x6e, 0x73, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x75, 0x74, 0x69, 0x6c, 0x73, 0x2e, 0x44, 0x4e, 0x53, + 0x54, 0x79, 0x70, 0x65, 0x52, 0x07, 0x64, 0x6e, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, 0x23, 0x0a, + 0x0d, 0x6e, 0x75, 0x6d, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x6e, 0x75, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x73, 0x12, 0x15, 0x0a, 0x06, 0x74, 0x63, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x05, 0x74, 0x63, 0x70, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x0b, 0x64, 0x72, 0x6f, + 0x70, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, + 0x2e, 0x75, 0x74, 0x69, 0x6c, 0x73, 0x2e, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x52, 0x0a, 0x64, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x3e, 0x0a, + 0x1b, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x6c, 0x79, 0x5f, 0x6f, 0x62, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x64, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x19, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x6c, 0x79, 0x4f, 0x62, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x12, 0x3a, 0x0a, + 0x19, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x6c, 0x79, 0x5f, 0x6f, 0x62, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x17, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x6c, 0x79, 0x4f, 0x62, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x78, 0x0a, 0x1d, 0x70, 0x72, 0x65, + 0x76, 0x69, 0x6f, 0x75, 0x73, 0x6c, 0x79, 0x5f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, + 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x35, 0x2e, 0x75, 0x74, 0x69, 0x6c, 0x73, 0x2e, 0x52, 0x65, 0x74, 0x69, 0x6e, 0x61, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x50, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, + 0x6c, 0x79, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x54, 0x63, 0x70, 0x46, 0x6c, 0x61, + 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x1a, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, + 0x73, 0x6c, 0x79, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x54, 0x63, 0x70, 0x46, 0x6c, + 0x61, 0x67, 0x73, 0x1a, 0x4d, 0x0a, 0x1f, 0x50, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x6c, + 0x79, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x54, 0x63, 0x70, 0x46, 0x6c, 0x61, 0x67, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x2a, 0x2f, 0x0a, 0x07, 0x44, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, + 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x51, 0x55, + 0x45, 0x52, 0x59, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x52, 0x45, 0x53, 0x50, 0x4f, 0x4e, 0x53, + 0x45, 0x10, 0x02, 0x2a, 0xfb, 0x86, 0x01, 0x0a, 0x0a, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x0e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x53, 0x75, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x10, 0x00, 0x12, 0x18, 0x0a, 0x14, 0x52, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x5f, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x10, + 0x02, 0x12, 0x14, 0x0a, 0x10, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x50, 0x6c, 0x61, 0x69, + 0x6e, 0x54, 0x65, 0x78, 0x74, 0x10, 0x03, 0x12, 0x1d, 0x0a, 0x19, 0x52, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x5f, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x44, 0x65, 0x63, 0x72, 0x79, + 0x70, 0x74, 0x65, 0x64, 0x10, 0x04, 0x12, 0x1a, 0x0a, 0x16, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, + 0x5f, 0x4c, 0x62, 0x4e, 0x6f, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x53, 0x6c, 0x6f, 0x74, + 0x10, 0x05, 0x12, 0x16, 0x0a, 0x12, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x4c, 0x62, 0x4e, + 0x6f, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x10, 0x06, 0x12, 0x1d, 0x0a, 0x19, 0x52, 0x65, + 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x4c, 0x62, 0x52, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x4e, 0x61, + 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x10, 0x07, 0x12, 0x1c, 0x0a, 0x18, 0x52, 0x65, 0x73, + 0x61, 0x6f, 0x6e, 0x5f, 0x4c, 0x62, 0x52, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x4e, 0x61, 0x74, + 0x53, 0x74, 0x61, 0x6c, 0x65, 0x10, 0x08, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x5f, 0x46, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x65, 0x64, 0x50, 0x61, 0x63, 0x6b, + 0x65, 0x74, 0x10, 0x09, 0x12, 0x22, 0x0a, 0x1e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x46, + 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x65, 0x64, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x10, 0x0a, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x65, 0x61, 0x73, + 0x6f, 0x6e, 0x5f, 0x4d, 0x69, 0x73, 0x73, 0x65, 0x64, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, + 0x61, 0x6c, 0x6c, 0x10, 0x0b, 0x12, 0x1a, 0x0a, 0x15, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x5f, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x53, 0x49, 0x50, 0x10, 0x84, + 0x01, 0x12, 0x16, 0x0a, 0x11, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, + 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x10, 0x85, 0x01, 0x12, 0x17, 0x0a, 0x12, 0x44, 0x72, 0x6f, + 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x10, + 0x86, 0x01, 0x12, 0x1c, 0x0a, 0x17, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, + 0x5f, 0x43, 0x54, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x48, 0x64, 0x72, 0x10, 0x87, 0x01, + 0x12, 0x1a, 0x0a, 0x15, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x46, + 0x72, 0x61, 0x67, 0x4e, 0x65, 0x65, 0x64, 0x65, 0x64, 0x10, 0x88, 0x01, 0x12, 0x1e, 0x0a, 0x19, + 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x43, 0x54, 0x55, 0x6e, 0x6b, + 0x6e, 0x6f, 0x77, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x10, 0x89, 0x01, 0x12, 0x19, 0x0a, 0x14, + 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, + 0x77, 0x6e, 0x4c, 0x33, 0x10, 0x8a, 0x01, 0x12, 0x1e, 0x0a, 0x19, 0x44, 0x72, 0x6f, 0x70, 0x52, + 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x4d, 0x69, 0x73, 0x73, 0x65, 0x64, 0x54, 0x61, 0x69, 0x6c, + 0x43, 0x61, 0x6c, 0x6c, 0x10, 0x8b, 0x01, 0x12, 0x1a, 0x0a, 0x15, 0x44, 0x72, 0x6f, 0x70, 0x52, + 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x57, 0x72, 0x69, 0x74, 0x65, 0x45, 0x72, 0x72, 0x6f, 0x72, + 0x10, 0x8c, 0x01, 0x12, 0x19, 0x0a, 0x14, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x5f, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x4c, 0x34, 0x10, 0x8d, 0x01, 0x12, 0x1f, + 0x0a, 0x1a, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x55, 0x6e, 0x6b, + 0x6e, 0x6f, 0x77, 0x6e, 0x49, 0x43, 0x4d, 0x50, 0x43, 0x6f, 0x64, 0x65, 0x10, 0x8e, 0x01, 0x12, + 0x1f, 0x0a, 0x1a, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x55, 0x6e, + 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x49, 0x43, 0x4d, 0x50, 0x54, 0x79, 0x70, 0x65, 0x10, 0x8f, 0x01, + 0x12, 0x20, 0x0a, 0x1b, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x55, + 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x49, 0x43, 0x4d, 0x50, 0x36, 0x43, 0x6f, 0x64, 0x65, 0x10, + 0x90, 0x01, 0x12, 0x20, 0x0a, 0x1b, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, + 0x5f, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x49, 0x43, 0x4d, 0x50, 0x36, 0x54, 0x79, 0x70, + 0x65, 0x10, 0x91, 0x01, 0x12, 0x22, 0x0a, 0x1d, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, + 0x6f, 0x6e, 0x5f, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x49, 0x43, 0x4d, 0x50, 0x36, 0x54, + 0x79, 0x70, 0x65, 0x5f, 0x32, 0x10, 0x92, 0x01, 0x12, 0x1b, 0x0a, 0x16, 0x44, 0x72, 0x6f, 0x70, + 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x4e, 0x6f, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x4b, + 0x65, 0x79, 0x10, 0x93, 0x01, 0x12, 0x19, 0x0a, 0x14, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x5f, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x5f, 0x31, 0x10, 0x94, 0x01, + 0x12, 0x19, 0x0a, 0x14, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x55, + 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x5f, 0x32, 0x10, 0x95, 0x01, 0x12, 0x1d, 0x0a, 0x18, 0x44, + 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, + 0x6e, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x10, 0x96, 0x01, 0x12, 0x1a, 0x0a, 0x15, 0x44, 0x72, + 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x55, 0x6e, 0x72, 0x6f, 0x75, 0x74, 0x61, + 0x62, 0x6c, 0x65, 0x10, 0x97, 0x01, 0x12, 0x19, 0x0a, 0x14, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, + 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x5f, 0x33, 0x10, 0x98, + 0x01, 0x12, 0x17, 0x0a, 0x12, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, + 0x43, 0x53, 0x55, 0x4d, 0x5f, 0x4c, 0x33, 0x10, 0x99, 0x01, 0x12, 0x17, 0x0a, 0x12, 0x44, 0x72, + 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x43, 0x53, 0x55, 0x4d, 0x5f, 0x4c, 0x34, + 0x10, 0x9a, 0x01, 0x12, 0x1e, 0x0a, 0x19, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x5f, 0x43, 0x54, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, + 0x10, 0x9b, 0x01, 0x12, 0x1d, 0x0a, 0x18, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x5f, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x45, 0x78, 0x74, 0x68, 0x64, 0x72, 0x10, + 0x9c, 0x01, 0x12, 0x1d, 0x0a, 0x18, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, + 0x5f, 0x46, 0x72, 0x61, 0x67, 0x4e, 0x6f, 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x10, 0x9d, + 0x01, 0x12, 0x19, 0x0a, 0x14, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, + 0x4e, 0x6f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x10, 0x9e, 0x01, 0x12, 0x22, 0x0a, 0x1d, + 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x55, 0x6e, 0x73, 0x75, 0x70, + 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x10, 0x9f, 0x01, + 0x12, 0x20, 0x0a, 0x1b, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x4e, + 0x6f, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x10, + 0xa0, 0x01, 0x12, 0x20, 0x0a, 0x1b, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, + 0x5f, 0x4e, 0x41, 0x54, 0x34, 0x36, 0x58, 0x36, 0x34, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, + 0x64, 0x10, 0xa1, 0x01, 0x12, 0x1a, 0x0a, 0x15, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, + 0x6f, 0x6e, 0x5f, 0x45, 0x44, 0x54, 0x48, 0x6f, 0x72, 0x69, 0x7a, 0x6f, 0x6e, 0x10, 0xa2, 0x01, + 0x12, 0x19, 0x0a, 0x14, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x55, + 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x43, 0x54, 0x10, 0xa3, 0x01, 0x12, 0x1f, 0x0a, 0x1a, 0x44, + 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x48, 0x6f, 0x73, 0x74, 0x55, 0x6e, + 0x72, 0x65, 0x61, 0x63, 0x68, 0x61, 0x62, 0x6c, 0x65, 0x10, 0xa4, 0x01, 0x12, 0x18, 0x0a, 0x13, + 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x4e, 0x6f, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x10, 0xa5, 0x01, 0x12, 0x1d, 0x0a, 0x18, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, + 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x55, 0x6e, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, + 0x4c, 0x32, 0x10, 0xa6, 0x01, 0x12, 0x1c, 0x0a, 0x17, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x5f, 0x4e, 0x61, 0x74, 0x4e, 0x6f, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, + 0x10, 0xa7, 0x01, 0x12, 0x1e, 0x0a, 0x19, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x5f, 0x4e, 0x61, 0x74, 0x55, 0x6e, 0x73, 0x75, 0x70, 0x70, 0x50, 0x72, 0x6f, 0x74, 0x6f, + 0x10, 0xa8, 0x01, 0x12, 0x15, 0x0a, 0x10, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x5f, 0x4e, 0x6f, 0x46, 0x49, 0x42, 0x10, 0xa9, 0x01, 0x12, 0x1f, 0x0a, 0x1a, 0x44, 0x72, + 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x45, 0x6e, 0x63, 0x61, 0x70, 0x50, 0x72, + 0x6f, 0x68, 0x69, 0x62, 0x69, 0x74, 0x65, 0x64, 0x10, 0xaa, 0x01, 0x12, 0x1f, 0x0a, 0x1a, 0x44, + 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x10, 0xab, 0x01, 0x12, 0x1d, 0x0a, 0x18, + 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, + 0x77, 0x6e, 0x53, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x10, 0xac, 0x01, 0x12, 0x1c, 0x0a, 0x17, 0x44, + 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x4e, 0x61, 0x74, 0x4e, 0x6f, 0x74, + 0x4e, 0x65, 0x65, 0x64, 0x65, 0x64, 0x10, 0xad, 0x01, 0x12, 0x1b, 0x0a, 0x16, 0x44, 0x72, 0x6f, + 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x49, 0x73, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, + 0x72, 0x49, 0x50, 0x10, 0xae, 0x01, 0x12, 0x1c, 0x0a, 0x17, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, + 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x46, 0x72, 0x61, 0x67, 0x4e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, + 0x64, 0x10, 0xaf, 0x01, 0x12, 0x1e, 0x0a, 0x19, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, + 0x6f, 0x6e, 0x5f, 0x46, 0x6f, 0x72, 0x62, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x49, 0x43, 0x4d, 0x50, + 0x36, 0x10, 0xb0, 0x01, 0x12, 0x1d, 0x0a, 0x18, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, + 0x6f, 0x6e, 0x5f, 0x4e, 0x6f, 0x74, 0x49, 0x6e, 0x53, 0x72, 0x63, 0x52, 0x61, 0x6e, 0x67, 0x65, + 0x10, 0xb1, 0x01, 0x12, 0x21, 0x0a, 0x1c, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x5f, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x46, 0x61, 0x69, + 0x6c, 0x65, 0x64, 0x10, 0xb2, 0x01, 0x12, 0x1e, 0x0a, 0x19, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, + 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x53, 0x65, 0x74, 0x46, 0x61, 0x69, + 0x6c, 0x65, 0x64, 0x10, 0xb3, 0x01, 0x12, 0x21, 0x0a, 0x1c, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, + 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, + 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x10, 0xb4, 0x01, 0x12, 0x1a, 0x0a, 0x15, 0x44, 0x72, 0x6f, + 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x44, 0x65, + 0x6e, 0x79, 0x10, 0xb5, 0x01, 0x12, 0x1c, 0x0a, 0x17, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x5f, 0x56, 0x6c, 0x61, 0x6e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x65, 0x64, + 0x10, 0xb6, 0x01, 0x12, 0x1a, 0x0a, 0x15, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x5f, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x56, 0x4e, 0x49, 0x10, 0xb7, 0x01, 0x12, + 0x1f, 0x0a, 0x1a, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x49, 0x6e, + 0x76, 0x61, 0x6c, 0x69, 0x64, 0x54, 0x43, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x10, 0xb8, 0x01, + 0x12, 0x15, 0x0a, 0x10, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x4e, + 0x6f, 0x53, 0x49, 0x44, 0x10, 0xb9, 0x01, 0x12, 0x20, 0x0a, 0x1b, 0x44, 0x72, 0x6f, 0x70, 0x52, + 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x53, 0x52, 0x76, + 0x36, 0x53, 0x74, 0x61, 0x74, 0x65, 0x10, 0xba, 0x01, 0x12, 0x15, 0x0a, 0x10, 0x44, 0x72, 0x6f, + 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x4e, 0x41, 0x54, 0x34, 0x36, 0x10, 0xbb, 0x01, + 0x12, 0x15, 0x0a, 0x10, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x4e, + 0x41, 0x54, 0x36, 0x34, 0x10, 0xbc, 0x01, 0x12, 0x22, 0x0a, 0x1d, 0x44, 0x72, 0x6f, 0x70, 0x52, + 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x41, 0x75, 0x74, 0x68, + 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x10, 0xbd, 0x01, 0x12, 0x1c, 0x0a, 0x17, 0x44, + 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x43, 0x54, 0x4e, 0x6f, 0x4d, 0x61, + 0x70, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x10, 0xbe, 0x01, 0x12, 0x1e, 0x0a, 0x19, 0x44, 0x72, 0x6f, + 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x53, 0x4e, 0x41, 0x54, 0x4e, 0x6f, 0x4d, 0x61, + 0x70, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x10, 0xbf, 0x01, 0x12, 0x20, 0x0a, 0x1b, 0x44, 0x72, 0x6f, + 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x43, + 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x44, 0x10, 0xc0, 0x01, 0x12, 0x26, 0x0a, 0x21, 0x44, + 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x44, 0x53, 0x52, 0x5f, 0x45, 0x4e, + 0x43, 0x41, 0x50, 0x5f, 0x55, 0x4e, 0x53, 0x55, 0x50, 0x50, 0x5f, 0x50, 0x52, 0x4f, 0x54, 0x4f, + 0x10, 0xc1, 0x01, 0x12, 0x1f, 0x0a, 0x1a, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x5f, 0x4e, 0x6f, 0x45, 0x67, 0x72, 0x65, 0x73, 0x73, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, + 0x79, 0x10, 0xc2, 0x01, 0x12, 0x22, 0x0a, 0x1d, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, + 0x6f, 0x6e, 0x5f, 0x55, 0x6e, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x54, 0x72, + 0x61, 0x66, 0x66, 0x69, 0x63, 0x10, 0xc3, 0x01, 0x12, 0x1b, 0x0a, 0x16, 0x44, 0x72, 0x6f, 0x70, + 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x54, 0x54, 0x4c, 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, + 0x65, 0x64, 0x10, 0xc4, 0x01, 0x12, 0x18, 0x0a, 0x13, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x5f, 0x4e, 0x6f, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x44, 0x10, 0xc5, 0x01, 0x12, + 0x1b, 0x0a, 0x16, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x52, 0x61, + 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x65, 0x64, 0x10, 0xc6, 0x01, 0x12, 0x1b, 0x0a, 0x16, + 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x49, 0x47, 0x4d, 0x50, 0x48, + 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x64, 0x10, 0xc7, 0x01, 0x12, 0x1e, 0x0a, 0x19, 0x44, 0x72, 0x6f, + 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x49, 0x47, 0x4d, 0x50, 0x53, 0x75, 0x62, 0x73, + 0x63, 0x72, 0x69, 0x62, 0x65, 0x64, 0x10, 0xc8, 0x01, 0x12, 0x20, 0x0a, 0x1b, 0x44, 0x72, 0x6f, + 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x63, 0x61, 0x73, + 0x74, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x64, 0x10, 0xc9, 0x01, 0x12, 0x1c, 0x0a, 0x17, 0x44, + 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x48, 0x6f, 0x73, 0x74, 0x4e, 0x6f, + 0x74, 0x52, 0x65, 0x61, 0x64, 0x79, 0x10, 0xca, 0x01, 0x12, 0x1a, 0x0a, 0x15, 0x44, 0x72, 0x6f, + 0x70, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x45, 0x70, 0x4e, 0x6f, 0x74, 0x52, 0x65, 0x61, + 0x64, 0x79, 0x10, 0xcb, 0x01, 0x12, 0x1d, 0x0a, 0x18, 0x44, 0x72, 0x6f, 0x70, 0x52, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x5f, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, + 0x72, 0x10, 0xdc, 0x01, 0x12, 0x18, 0x0a, 0x10, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x6e, 0x76, + 0x61, 0x6c, 0x69, 0x64, 0x44, 0x61, 0x74, 0x61, 0x10, 0x81, 0x80, 0x80, 0x80, 0x04, 0x12, 0x1a, + 0x0a, 0x12, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x50, 0x61, + 0x63, 0x6b, 0x65, 0x74, 0x10, 0x82, 0x80, 0x80, 0x80, 0x04, 0x12, 0x16, 0x0a, 0x0e, 0x44, 0x72, + 0x6f, 0x70, 0x5f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x10, 0x83, 0x80, 0x80, + 0x80, 0x04, 0x12, 0x15, 0x0a, 0x0d, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x6f, 0x74, 0x52, 0x65, + 0x61, 0x64, 0x79, 0x10, 0x84, 0x80, 0x80, 0x80, 0x04, 0x12, 0x19, 0x0a, 0x11, 0x44, 0x72, 0x6f, + 0x70, 0x5f, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x10, 0x85, + 0x80, 0x80, 0x80, 0x04, 0x12, 0x18, 0x0a, 0x10, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x6f, 0x74, + 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x10, 0x86, 0x80, 0x80, 0x80, 0x04, 0x12, 0x11, + 0x0a, 0x09, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x42, 0x75, 0x73, 0x79, 0x10, 0x87, 0x80, 0x80, 0x80, + 0x04, 0x12, 0x15, 0x0a, 0x0d, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x65, 0x64, 0x10, 0x88, 0x80, 0x80, 0x80, 0x04, 0x12, 0x19, 0x0a, 0x11, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x65, 0x64, 0x56, 0x4c, 0x41, 0x4e, 0x10, 0x89, 0x80, + 0x80, 0x80, 0x04, 0x12, 0x1d, 0x0a, 0x15, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x55, 0x6e, 0x61, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x56, 0x4c, 0x41, 0x4e, 0x10, 0x8a, 0x80, 0x80, + 0x80, 0x04, 0x12, 0x1c, 0x0a, 0x14, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x55, 0x6e, 0x61, 0x75, 0x74, + 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x4d, 0x41, 0x43, 0x10, 0x8b, 0x80, 0x80, 0x80, 0x04, + 0x12, 0x21, 0x0a, 0x19, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x53, + 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x10, 0x8c, 0x80, + 0x80, 0x80, 0x04, 0x12, 0x1f, 0x0a, 0x17, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x46, 0x61, 0x69, 0x6c, + 0x65, 0x64, 0x50, 0x76, 0x6c, 0x61, 0x6e, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x10, 0x8d, + 0x80, 0x80, 0x80, 0x04, 0x12, 0x10, 0x0a, 0x08, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x51, 0x6f, 0x73, + 0x10, 0x8e, 0x80, 0x80, 0x80, 0x04, 0x12, 0x12, 0x0a, 0x0a, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, + 0x70, 0x73, 0x65, 0x63, 0x10, 0x8f, 0x80, 0x80, 0x80, 0x04, 0x12, 0x18, 0x0a, 0x10, 0x44, 0x72, + 0x6f, 0x70, 0x5f, 0x4d, 0x61, 0x63, 0x53, 0x70, 0x6f, 0x6f, 0x66, 0x69, 0x6e, 0x67, 0x10, 0x90, + 0x80, 0x80, 0x80, 0x04, 0x12, 0x16, 0x0a, 0x0e, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x44, 0x68, 0x63, + 0x70, 0x47, 0x75, 0x61, 0x72, 0x64, 0x10, 0x91, 0x80, 0x80, 0x80, 0x04, 0x12, 0x18, 0x0a, 0x10, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x47, 0x75, 0x61, 0x72, 0x64, + 0x10, 0x92, 0x80, 0x80, 0x80, 0x04, 0x12, 0x1b, 0x0a, 0x13, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x42, + 0x72, 0x69, 0x64, 0x67, 0x65, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x10, 0x93, 0x80, + 0x80, 0x80, 0x04, 0x12, 0x1c, 0x0a, 0x14, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x56, 0x69, 0x72, 0x74, + 0x75, 0x61, 0x6c, 0x53, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x49, 0x64, 0x10, 0x94, 0x80, 0x80, 0x80, + 0x04, 0x12, 0x25, 0x0a, 0x1d, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, + 0x65, 0x64, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x69, 0x73, 0x73, 0x69, + 0x6e, 0x67, 0x10, 0x95, 0x80, 0x80, 0x80, 0x04, 0x12, 0x1a, 0x0a, 0x12, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x10, 0x96, + 0x80, 0x80, 0x80, 0x04, 0x12, 0x18, 0x0a, 0x10, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4d, 0x54, 0x55, + 0x4d, 0x69, 0x73, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x10, 0x97, 0x80, 0x80, 0x80, 0x04, 0x12, 0x1c, + 0x0a, 0x14, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x46, 0x77, 0x64, + 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x10, 0x98, 0x80, 0x80, 0x80, 0x04, 0x12, 0x1e, 0x0a, 0x16, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x56, 0x6c, 0x61, 0x6e, + 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x10, 0x99, 0x80, 0x80, 0x80, 0x04, 0x12, 0x1b, 0x0a, 0x13, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x44, 0x65, 0x73, 0x74, + 0x4d, 0x61, 0x63, 0x10, 0x9a, 0x80, 0x80, 0x80, 0x04, 0x12, 0x1d, 0x0a, 0x15, 0x44, 0x72, 0x6f, + 0x70, 0x5f, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, + 0x61, 0x63, 0x10, 0x9b, 0x80, 0x80, 0x80, 0x04, 0x12, 0x23, 0x0a, 0x1b, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x46, 0x69, 0x72, 0x73, 0x74, 0x4e, 0x42, 0x54, + 0x6f, 0x6f, 0x53, 0x6d, 0x61, 0x6c, 0x6c, 0x10, 0x9c, 0x80, 0x80, 0x80, 0x04, 0x12, 0x10, 0x0a, + 0x08, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x57, 0x6e, 0x76, 0x10, 0x9d, 0x80, 0x80, 0x80, 0x04, 0x12, + 0x17, 0x0a, 0x0f, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, 0x74, 0x6f, 0x72, 0x6d, 0x4c, 0x69, 0x6d, + 0x69, 0x74, 0x10, 0x9e, 0x80, 0x80, 0x80, 0x04, 0x12, 0x19, 0x0a, 0x11, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x49, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x49, 0x63, 0x6d, 0x70, 0x10, 0x9f, 0x80, + 0x80, 0x80, 0x04, 0x12, 0x28, 0x0a, 0x20, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x46, 0x61, 0x69, 0x6c, + 0x65, 0x64, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, + 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x10, 0xa0, 0x80, 0x80, 0x80, 0x04, 0x12, 0x18, 0x0a, + 0x10, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x69, 0x63, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, + 0x64, 0x10, 0xa1, 0x80, 0x80, 0x80, 0x04, 0x12, 0x1f, 0x0a, 0x17, 0x44, 0x72, 0x6f, 0x70, 0x5f, + 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x46, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x10, 0xa2, 0x80, 0x80, 0x80, 0x04, 0x12, 0x23, 0x0a, 0x1b, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x53, 0x77, 0x69, 0x74, 0x63, 0x68, 0x44, 0x61, 0x74, 0x61, 0x46, 0x6c, 0x6f, 0x77, 0x44, + 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x10, 0xa3, 0x80, 0x80, 0x80, 0x04, 0x12, 0x26, 0x0a, + 0x1e, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x65, 0x64, 0x49, 0x73, + 0x6f, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x6e, 0x74, 0x61, 0x67, 0x67, 0x65, 0x64, 0x10, + 0xa4, 0x80, 0x80, 0x80, 0x04, 0x12, 0x1b, 0x0a, 0x13, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x6e, + 0x76, 0x61, 0x6c, 0x69, 0x64, 0x50, 0x44, 0x51, 0x75, 0x65, 0x75, 0x65, 0x10, 0xa5, 0x80, 0x80, + 0x80, 0x04, 0x12, 0x15, 0x0a, 0x0d, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4c, 0x6f, 0x77, 0x50, 0x6f, + 0x77, 0x65, 0x72, 0x10, 0xa6, 0x80, 0x80, 0x80, 0x04, 0x12, 0x12, 0x0a, 0x0a, 0x44, 0x72, 0x6f, + 0x70, 0x5f, 0x50, 0x61, 0x75, 0x73, 0x65, 0x10, 0xc9, 0x81, 0x80, 0x80, 0x04, 0x12, 0x12, 0x0a, + 0x0a, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x52, 0x65, 0x73, 0x65, 0x74, 0x10, 0xca, 0x81, 0x80, 0x80, + 0x04, 0x12, 0x18, 0x0a, 0x10, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, 0x65, 0x6e, 0x64, 0x41, 0x62, + 0x6f, 0x72, 0x74, 0x65, 0x64, 0x10, 0xcb, 0x81, 0x80, 0x80, 0x04, 0x12, 0x1d, 0x0a, 0x15, 0x44, + 0x72, 0x6f, 0x70, 0x5f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x4e, 0x6f, 0x74, 0x42, + 0x6f, 0x75, 0x6e, 0x64, 0x10, 0xcc, 0x81, 0x80, 0x80, 0x04, 0x12, 0x14, 0x0a, 0x0c, 0x44, 0x72, + 0x6f, 0x70, 0x5f, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x10, 0xcd, 0x81, 0x80, 0x80, 0x04, + 0x12, 0x1a, 0x0a, 0x12, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, + 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x10, 0xce, 0x81, 0x80, 0x80, 0x04, 0x12, 0x1c, 0x0a, 0x14, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x6f, 0x73, 0x74, 0x4f, 0x75, 0x74, 0x4f, 0x66, 0x4d, 0x65, + 0x6d, 0x6f, 0x72, 0x79, 0x10, 0xcf, 0x81, 0x80, 0x80, 0x04, 0x12, 0x19, 0x0a, 0x11, 0x44, 0x72, + 0x6f, 0x70, 0x5f, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x54, 0x6f, 0x6f, 0x4c, 0x6f, 0x6e, 0x67, 0x10, + 0xd0, 0x81, 0x80, 0x80, 0x04, 0x12, 0x1a, 0x0a, 0x12, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x46, 0x72, + 0x61, 0x6d, 0x65, 0x54, 0x6f, 0x6f, 0x53, 0x68, 0x6f, 0x72, 0x74, 0x10, 0xd1, 0x81, 0x80, 0x80, + 0x04, 0x12, 0x1d, 0x0a, 0x15, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x4c, + 0x65, 0x6e, 0x67, 0x74, 0x68, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x10, 0xd2, 0x81, 0x80, 0x80, 0x04, + 0x12, 0x15, 0x0a, 0x0d, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x43, 0x72, 0x63, 0x45, 0x72, 0x72, 0x6f, + 0x72, 0x10, 0xd3, 0x81, 0x80, 0x80, 0x04, 0x12, 0x1d, 0x0a, 0x15, 0x44, 0x72, 0x6f, 0x70, 0x5f, + 0x42, 0x61, 0x64, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, + 0x10, 0xd4, 0x81, 0x80, 0x80, 0x04, 0x12, 0x15, 0x0a, 0x0d, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x46, + 0x63, 0x73, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x10, 0xd5, 0x81, 0x80, 0x80, 0x04, 0x12, 0x18, 0x0a, + 0x10, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x45, 0x72, 0x72, 0x6f, + 0x72, 0x10, 0xd6, 0x81, 0x80, 0x80, 0x04, 0x12, 0x19, 0x0a, 0x11, 0x44, 0x72, 0x6f, 0x70, 0x5f, + 0x48, 0x65, 0x61, 0x64, 0x51, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x10, 0xd7, 0x81, 0x80, + 0x80, 0x04, 0x12, 0x1b, 0x0a, 0x13, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, 0x74, 0x61, 0x6c, 0x6c, + 0x65, 0x64, 0x44, 0x69, 0x73, 0x63, 0x61, 0x72, 0x64, 0x10, 0xd8, 0x81, 0x80, 0x80, 0x04, 0x12, + 0x14, 0x0a, 0x0c, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x52, 0x78, 0x51, 0x46, 0x75, 0x6c, 0x6c, 0x10, + 0xd9, 0x81, 0x80, 0x80, 0x04, 0x12, 0x1b, 0x0a, 0x13, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x50, 0x68, + 0x79, 0x73, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x10, 0xda, 0x81, 0x80, + 0x80, 0x04, 0x12, 0x15, 0x0a, 0x0d, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x44, 0x6d, 0x61, 0x45, 0x72, + 0x72, 0x6f, 0x72, 0x10, 0xdb, 0x81, 0x80, 0x80, 0x04, 0x12, 0x1a, 0x0a, 0x12, 0x44, 0x72, 0x6f, + 0x70, 0x5f, 0x46, 0x69, 0x72, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x10, + 0xdc, 0x81, 0x80, 0x80, 0x04, 0x12, 0x1d, 0x0a, 0x15, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x44, 0x65, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0xdd, + 0x81, 0x80, 0x80, 0x04, 0x12, 0x19, 0x0a, 0x11, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x42, 0x61, 0x64, + 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x10, 0xde, 0x81, 0x80, 0x80, 0x04, 0x12, + 0x1c, 0x0a, 0x14, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x43, 0x6f, 0x61, 0x6c, 0x65, 0x73, 0x63, 0x69, + 0x6e, 0x67, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x10, 0xdf, 0x81, 0x80, 0x80, 0x04, 0x12, 0x19, 0x0a, + 0x11, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x56, 0x6c, 0x61, 0x6e, 0x53, 0x70, 0x6f, 0x6f, 0x66, 0x69, + 0x6e, 0x67, 0x10, 0xe1, 0x81, 0x80, 0x80, 0x04, 0x12, 0x1f, 0x0a, 0x17, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x55, 0x6e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x45, 0x74, 0x68, 0x65, 0x72, 0x54, + 0x79, 0x70, 0x65, 0x10, 0xe2, 0x81, 0x80, 0x80, 0x04, 0x12, 0x16, 0x0a, 0x0e, 0x44, 0x72, 0x6f, + 0x70, 0x5f, 0x56, 0x70, 0x6f, 0x72, 0x74, 0x44, 0x6f, 0x77, 0x6e, 0x10, 0xe3, 0x81, 0x80, 0x80, + 0x04, 0x12, 0x1d, 0x0a, 0x15, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, 0x74, 0x65, 0x65, 0x72, 0x69, + 0x6e, 0x67, 0x4d, 0x69, 0x73, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x10, 0xe4, 0x81, 0x80, 0x80, 0x04, + 0x12, 0x1b, 0x0a, 0x13, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x70, 0x6f, + 0x72, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x10, 0x91, 0x83, 0x80, 0x80, 0x04, 0x12, 0x17, 0x0a, + 0x0f, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x56, 0x66, 0x4e, 0x6f, 0x74, 0x52, 0x65, 0x61, 0x64, 0x79, + 0x10, 0x92, 0x83, 0x80, 0x80, 0x04, 0x12, 0x1e, 0x0a, 0x16, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4d, + 0x69, 0x63, 0x72, 0x6f, 0x70, 0x6f, 0x72, 0x74, 0x4e, 0x6f, 0x74, 0x52, 0x65, 0x61, 0x64, 0x79, + 0x10, 0x93, 0x83, 0x80, 0x80, 0x04, 0x12, 0x17, 0x0a, 0x0f, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x56, + 0x4d, 0x42, 0x75, 0x73, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x10, 0x94, 0x83, 0x80, 0x80, 0x04, 0x12, + 0x1e, 0x0a, 0x16, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x46, 0x4c, 0x5f, 0x4c, 0x6f, 0x6f, 0x70, 0x62, + 0x61, 0x63, 0x6b, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x10, 0xd9, 0x84, 0x80, 0x80, 0x04, 0x12, + 0x21, 0x0a, 0x19, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x46, 0x4c, 0x5f, 0x49, 0x6e, 0x76, 0x61, 0x6c, + 0x69, 0x64, 0x53, 0x6e, 0x61, 0x70, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x10, 0xda, 0x84, 0x80, + 0x80, 0x04, 0x12, 0x23, 0x0a, 0x1b, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x46, 0x4c, 0x5f, 0x49, 0x6e, + 0x76, 0x61, 0x6c, 0x69, 0x64, 0x45, 0x74, 0x68, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x54, 0x79, 0x70, + 0x65, 0x10, 0xdb, 0x84, 0x80, 0x80, 0x04, 0x12, 0x23, 0x0a, 0x1b, 0x44, 0x72, 0x6f, 0x70, 0x5f, + 0x46, 0x4c, 0x5f, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, + 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x10, 0xdc, 0x84, 0x80, 0x80, 0x04, 0x12, 0x23, 0x0a, 0x1b, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x46, 0x4c, 0x5f, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4e, 0x6f, + 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x69, 0x67, 0x75, 0x6f, 0x75, 0x73, 0x10, 0xdd, 0x84, 0x80, 0x80, + 0x04, 0x12, 0x26, 0x0a, 0x1e, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x46, 0x4c, 0x5f, 0x49, 0x6e, 0x76, + 0x61, 0x6c, 0x69, 0x64, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, + 0x79, 0x70, 0x65, 0x10, 0xde, 0x84, 0x80, 0x80, 0x04, 0x12, 0x21, 0x0a, 0x19, 0x44, 0x72, 0x6f, + 0x70, 0x5f, 0x46, 0x4c, 0x5f, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x6f, + 0x74, 0x52, 0x65, 0x61, 0x64, 0x79, 0x10, 0xdf, 0x84, 0x80, 0x80, 0x04, 0x12, 0x20, 0x0a, 0x18, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x46, 0x4c, 0x5f, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x4e, 0x6f, 0x74, 0x52, 0x65, 0x61, 0x64, 0x79, 0x10, 0xe0, 0x84, 0x80, 0x80, 0x04, 0x12, 0x1e, + 0x0a, 0x16, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x46, 0x4c, 0x5f, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x4c, 0x73, 0x6f, 0x49, 0x6e, 0x66, 0x6f, 0x10, 0xe1, 0x84, 0x80, 0x80, 0x04, 0x12, 0x1e, + 0x0a, 0x16, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x46, 0x4c, 0x5f, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x55, 0x73, 0x6f, 0x49, 0x6e, 0x66, 0x6f, 0x10, 0xe2, 0x84, 0x80, 0x80, 0x04, 0x12, 0x1d, + 0x0a, 0x15, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x46, 0x4c, 0x5f, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x4d, 0x65, 0x64, 0x69, 0x75, 0x6d, 0x10, 0xe3, 0x84, 0x80, 0x80, 0x04, 0x12, 0x20, 0x0a, + 0x18, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x46, 0x4c, 0x5f, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, + 0x41, 0x72, 0x70, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x10, 0xe4, 0x84, 0x80, 0x80, 0x04, 0x12, + 0x21, 0x0a, 0x19, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x46, 0x4c, 0x5f, 0x4e, 0x6f, 0x43, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x10, 0xe5, 0x84, 0x80, + 0x80, 0x04, 0x12, 0x21, 0x0a, 0x19, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x46, 0x4c, 0x5f, 0x54, 0x6f, + 0x6f, 0x4d, 0x61, 0x6e, 0x79, 0x4e, 0x65, 0x74, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x73, 0x10, + 0xe6, 0x84, 0x80, 0x80, 0x04, 0x12, 0x20, 0x0a, 0x18, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x46, 0x4c, + 0x5f, 0x46, 0x6c, 0x73, 0x4e, 0x70, 0x69, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x44, 0x72, 0x6f, + 0x70, 0x10, 0xe7, 0x84, 0x80, 0x80, 0x04, 0x12, 0x15, 0x0a, 0x0d, 0x44, 0x72, 0x6f, 0x70, 0x5f, + 0x41, 0x72, 0x70, 0x47, 0x75, 0x61, 0x72, 0x64, 0x10, 0xbd, 0x85, 0x80, 0x80, 0x04, 0x12, 0x17, + 0x0a, 0x0f, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x41, 0x72, 0x70, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x65, + 0x72, 0x10, 0xbe, 0x85, 0x80, 0x80, 0x04, 0x12, 0x18, 0x0a, 0x10, 0x44, 0x72, 0x6f, 0x70, 0x5f, + 0x44, 0x68, 0x63, 0x70, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x65, 0x72, 0x10, 0xbf, 0x85, 0x80, 0x80, + 0x04, 0x12, 0x1b, 0x0a, 0x13, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x42, + 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x10, 0xc0, 0x85, 0x80, 0x80, 0x04, 0x12, 0x17, + 0x0a, 0x0f, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x4e, 0x6f, 0x6e, 0x49, + 0x70, 0x10, 0xc1, 0x85, 0x80, 0x80, 0x04, 0x12, 0x16, 0x0a, 0x0e, 0x44, 0x72, 0x6f, 0x70, 0x5f, + 0x41, 0x72, 0x70, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x10, 0xc2, 0x85, 0x80, 0x80, 0x04, 0x12, + 0x16, 0x0a, 0x0e, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x70, 0x76, 0x34, 0x47, 0x75, 0x61, 0x72, + 0x64, 0x10, 0xc3, 0x85, 0x80, 0x80, 0x04, 0x12, 0x16, 0x0a, 0x0e, 0x44, 0x72, 0x6f, 0x70, 0x5f, + 0x49, 0x70, 0x76, 0x36, 0x47, 0x75, 0x61, 0x72, 0x64, 0x10, 0xc4, 0x85, 0x80, 0x80, 0x04, 0x12, + 0x15, 0x0a, 0x0d, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4d, 0x61, 0x63, 0x47, 0x75, 0x61, 0x72, 0x64, + 0x10, 0xc5, 0x85, 0x80, 0x80, 0x04, 0x12, 0x24, 0x0a, 0x1c, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x42, + 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x4e, 0x6f, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x10, 0xc6, 0x85, 0x80, 0x80, 0x04, 0x12, 0x21, 0x0a, 0x19, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x55, 0x6e, 0x69, 0x63, 0x61, 0x73, 0x74, 0x4e, 0x6f, 0x44, 0x65, + 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x10, 0xc7, 0x85, 0x80, 0x80, 0x04, 0x12, + 0x20, 0x0a, 0x18, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x55, 0x6e, 0x69, 0x63, 0x61, 0x73, 0x74, 0x50, + 0x6f, 0x72, 0x74, 0x4e, 0x6f, 0x74, 0x52, 0x65, 0x61, 0x64, 0x79, 0x10, 0xc8, 0x85, 0x80, 0x80, + 0x04, 0x12, 0x21, 0x0a, 0x19, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, 0x77, 0x69, 0x74, 0x63, 0x68, + 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0xc9, + 0x85, 0x80, 0x80, 0x04, 0x12, 0x1a, 0x0a, 0x12, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x63, 0x6d, + 0x70, 0x76, 0x36, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x65, 0x72, 0x10, 0xca, 0x85, 0x80, 0x80, 0x04, + 0x12, 0x16, 0x0a, 0x0e, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, + 0x70, 0x74, 0x10, 0xcb, 0x85, 0x80, 0x80, 0x04, 0x12, 0x1b, 0x0a, 0x13, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x10, + 0xcc, 0x85, 0x80, 0x80, 0x04, 0x12, 0x15, 0x0a, 0x0d, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x44, + 0x50, 0x47, 0x75, 0x61, 0x72, 0x64, 0x10, 0xcd, 0x85, 0x80, 0x80, 0x04, 0x12, 0x18, 0x0a, 0x10, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x50, 0x6f, 0x72, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, + 0x10, 0xce, 0x85, 0x80, 0x80, 0x04, 0x12, 0x19, 0x0a, 0x11, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, + 0x69, 0x63, 0x53, 0x75, 0x73, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x10, 0xcf, 0x85, 0x80, 0x80, + 0x04, 0x12, 0x20, 0x0a, 0x18, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x42, 0x61, 0x64, + 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x10, 0x85, 0x87, + 0x80, 0x80, 0x04, 0x12, 0x22, 0x0a, 0x1a, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x4e, + 0x6f, 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x6c, 0x79, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x65, + 0x64, 0x10, 0x86, 0x87, 0x80, 0x80, 0x04, 0x12, 0x23, 0x0a, 0x1b, 0x44, 0x72, 0x6f, 0x70, 0x5f, + 0x4e, 0x4c, 0x5f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x55, 0x6e, 0x72, 0x65, 0x61, + 0x63, 0x68, 0x61, 0x62, 0x6c, 0x65, 0x10, 0x87, 0x87, 0x80, 0x80, 0x04, 0x12, 0x1f, 0x0a, 0x17, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x50, 0x6f, 0x72, 0x74, 0x55, 0x6e, 0x72, 0x65, + 0x61, 0x63, 0x68, 0x61, 0x62, 0x6c, 0x65, 0x10, 0x88, 0x87, 0x80, 0x80, 0x04, 0x12, 0x19, 0x0a, + 0x11, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x42, 0x61, 0x64, 0x4c, 0x65, 0x6e, 0x67, + 0x74, 0x68, 0x10, 0x89, 0x87, 0x80, 0x80, 0x04, 0x12, 0x1f, 0x0a, 0x17, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x4e, 0x4c, 0x5f, 0x4d, 0x61, 0x6c, 0x66, 0x6f, 0x72, 0x6d, 0x65, 0x64, 0x48, 0x65, 0x61, + 0x64, 0x65, 0x72, 0x10, 0x8a, 0x87, 0x80, 0x80, 0x04, 0x12, 0x17, 0x0a, 0x0f, 0x44, 0x72, 0x6f, + 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x4e, 0x6f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x10, 0x8b, 0x87, 0x80, + 0x80, 0x04, 0x12, 0x1b, 0x0a, 0x13, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x42, 0x65, + 0x79, 0x6f, 0x6e, 0x64, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x10, 0x8c, 0x87, 0x80, 0x80, 0x04, 0x12, + 0x1e, 0x0a, 0x16, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x6e, 0x73, 0x70, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x72, 0x6f, 0x70, 0x10, 0x8d, 0x87, 0x80, 0x80, 0x04, 0x12, + 0x25, 0x0a, 0x1d, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x54, 0x6f, 0x6f, 0x4d, 0x61, + 0x6e, 0x79, 0x44, 0x65, 0x63, 0x61, 0x70, 0x73, 0x75, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x10, 0x8e, 0x87, 0x80, 0x80, 0x04, 0x12, 0x2a, 0x0a, 0x22, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, + 0x4c, 0x5f, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x76, 0x65, + 0x6c, 0x79, 0x50, 0x72, 0x6f, 0x68, 0x69, 0x62, 0x69, 0x74, 0x65, 0x64, 0x10, 0x8f, 0x87, 0x80, + 0x80, 0x04, 0x12, 0x1b, 0x0a, 0x13, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x42, 0x61, + 0x64, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x10, 0x90, 0x87, 0x80, 0x80, 0x04, 0x12, + 0x1e, 0x0a, 0x16, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x52, 0x65, 0x63, 0x65, 0x69, + 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x78, 0x10, 0x91, 0x87, 0x80, 0x80, 0x04, 0x12, + 0x20, 0x0a, 0x18, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x48, 0x6f, 0x70, 0x4c, 0x69, + 0x6d, 0x69, 0x74, 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x10, 0x92, 0x87, 0x80, 0x80, + 0x04, 0x12, 0x22, 0x0a, 0x1a, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x41, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x55, 0x6e, 0x72, 0x65, 0x61, 0x63, 0x68, 0x61, 0x62, 0x6c, 0x65, 0x10, + 0x93, 0x87, 0x80, 0x80, 0x04, 0x12, 0x19, 0x0a, 0x11, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, + 0x5f, 0x52, 0x73, 0x63, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x10, 0x94, 0x87, 0x80, 0x80, 0x04, + 0x12, 0x1e, 0x0a, 0x16, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x46, 0x6f, 0x72, 0x77, + 0x61, 0x72, 0x64, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x78, 0x10, 0x95, 0x87, 0x80, 0x80, 0x04, + 0x12, 0x24, 0x0a, 0x1c, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x41, 0x72, 0x62, 0x69, + 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x6e, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x64, + 0x10, 0x96, 0x87, 0x80, 0x80, 0x04, 0x12, 0x20, 0x0a, 0x18, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, + 0x4c, 0x5f, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x62, 0x73, 0x6f, + 0x72, 0x62, 0x10, 0x97, 0x87, 0x80, 0x80, 0x04, 0x12, 0x27, 0x0a, 0x1f, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x4e, 0x4c, 0x5f, 0x44, 0x6f, 0x6e, 0x74, 0x46, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, + 0x4d, 0x74, 0x75, 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x10, 0x98, 0x87, 0x80, 0x80, + 0x04, 0x12, 0x24, 0x0a, 0x1c, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x42, 0x75, 0x66, + 0x66, 0x65, 0x72, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, + 0x64, 0x10, 0x99, 0x87, 0x80, 0x80, 0x04, 0x12, 0x28, 0x0a, 0x20, 0x44, 0x72, 0x6f, 0x70, 0x5f, + 0x4e, 0x4c, 0x5f, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, + 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x10, 0x9a, 0x87, 0x80, 0x80, + 0x04, 0x12, 0x28, 0x0a, 0x20, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x41, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x61, + 0x69, 0x6c, 0x75, 0x72, 0x65, 0x10, 0x9b, 0x87, 0x80, 0x80, 0x04, 0x12, 0x1c, 0x0a, 0x14, 0x44, + 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x70, 0x73, 0x65, 0x63, 0x46, 0x61, 0x69, 0x6c, + 0x75, 0x72, 0x65, 0x10, 0x9c, 0x87, 0x80, 0x80, 0x04, 0x12, 0x27, 0x0a, 0x1f, 0x44, 0x72, 0x6f, + 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x48, 0x65, + 0x61, 0x64, 0x65, 0x72, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x10, 0x9d, 0x87, 0x80, + 0x80, 0x04, 0x12, 0x20, 0x0a, 0x18, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x70, + 0x73, 0x6e, 0x70, 0x69, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x44, 0x72, 0x6f, 0x70, 0x10, 0x9e, + 0x87, 0x80, 0x80, 0x04, 0x12, 0x22, 0x0a, 0x1a, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, + 0x55, 0x6e, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x4f, 0x66, 0x66, 0x6c, 0x6f, + 0x61, 0x64, 0x10, 0x9f, 0x87, 0x80, 0x80, 0x04, 0x12, 0x1e, 0x0a, 0x16, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x4e, 0x4c, 0x5f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x46, 0x61, 0x69, 0x6c, 0x75, + 0x72, 0x65, 0x10, 0xa0, 0x87, 0x80, 0x80, 0x04, 0x12, 0x24, 0x0a, 0x1c, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x4e, 0x4c, 0x5f, 0x41, 0x6e, 0x63, 0x69, 0x6c, 0x6c, 0x61, 0x72, 0x79, 0x44, 0x61, 0x74, + 0x61, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x10, 0xa1, 0x87, 0x80, 0x80, 0x04, 0x12, 0x1e, + 0x0a, 0x16, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x52, 0x61, 0x77, 0x44, 0x61, 0x74, + 0x61, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x10, 0xa2, 0x87, 0x80, 0x80, 0x04, 0x12, 0x23, + 0x0a, 0x1b, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x10, 0xa3, 0x87, + 0x80, 0x80, 0x04, 0x12, 0x2d, 0x0a, 0x25, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, + 0x70, 0x73, 0x6e, 0x70, 0x69, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x42, 0x75, 0x74, + 0x4e, 0x6f, 0x74, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x65, 0x64, 0x10, 0xa4, 0x87, 0x80, + 0x80, 0x04, 0x12, 0x1f, 0x0a, 0x17, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x70, + 0x73, 0x6e, 0x70, 0x69, 0x4e, 0x6f, 0x4e, 0x65, 0x78, 0x74, 0x48, 0x6f, 0x70, 0x10, 0xa5, 0x87, + 0x80, 0x80, 0x04, 0x12, 0x23, 0x0a, 0x1b, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, + 0x70, 0x73, 0x6e, 0x70, 0x69, 0x4e, 0x6f, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x74, 0x6d, 0x65, + 0x6e, 0x74, 0x10, 0xa6, 0x87, 0x80, 0x80, 0x04, 0x12, 0x21, 0x0a, 0x19, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x70, 0x73, 0x6e, 0x70, 0x69, 0x4e, 0x6f, 0x49, 0x6e, 0x74, 0x65, + 0x72, 0x66, 0x61, 0x63, 0x65, 0x10, 0xa7, 0x87, 0x80, 0x80, 0x04, 0x12, 0x24, 0x0a, 0x1c, 0x44, + 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x70, 0x73, 0x6e, 0x70, 0x69, 0x4e, 0x6f, 0x53, + 0x75, 0x62, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x10, 0xa8, 0x87, 0x80, 0x80, + 0x04, 0x12, 0x27, 0x0a, 0x1f, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x70, 0x73, + 0x6e, 0x70, 0x69, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x44, 0x69, 0x73, 0x61, + 0x62, 0x6c, 0x65, 0x64, 0x10, 0xa9, 0x87, 0x80, 0x80, 0x04, 0x12, 0x28, 0x0a, 0x20, 0x44, 0x72, + 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x70, 0x73, 0x6e, 0x70, 0x69, 0x53, 0x65, 0x67, 0x6d, + 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0xaa, + 0x87, 0x80, 0x80, 0x04, 0x12, 0x26, 0x0a, 0x1e, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, + 0x49, 0x70, 0x73, 0x6e, 0x70, 0x69, 0x4e, 0x6f, 0x45, 0x74, 0x68, 0x65, 0x72, 0x6e, 0x65, 0x74, + 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x10, 0xab, 0x87, 0x80, 0x80, 0x04, 0x12, 0x28, 0x0a, 0x20, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x70, 0x73, 0x6e, 0x70, 0x69, 0x55, 0x6e, + 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x46, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, + 0x10, 0xac, 0x87, 0x80, 0x80, 0x04, 0x12, 0x2e, 0x0a, 0x26, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, + 0x4c, 0x5f, 0x49, 0x70, 0x73, 0x6e, 0x70, 0x69, 0x55, 0x6e, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, + 0x74, 0x65, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, + 0x10, 0xad, 0x87, 0x80, 0x80, 0x04, 0x12, 0x24, 0x0a, 0x1c, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, + 0x4c, 0x5f, 0x49, 0x70, 0x73, 0x6e, 0x70, 0x69, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x4c, + 0x73, 0x6f, 0x49, 0x6e, 0x66, 0x6f, 0x10, 0xae, 0x87, 0x80, 0x80, 0x04, 0x12, 0x24, 0x0a, 0x1c, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x70, 0x73, 0x6e, 0x70, 0x69, 0x49, 0x6e, + 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, 0x73, 0x6f, 0x49, 0x6e, 0x66, 0x6f, 0x10, 0xaf, 0x87, 0x80, + 0x80, 0x04, 0x12, 0x1d, 0x0a, 0x15, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x10, 0xb0, 0x87, 0x80, 0x80, + 0x04, 0x12, 0x2a, 0x0a, 0x22, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x41, 0x64, 0x6d, + 0x69, 0x6e, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x76, 0x65, 0x6c, 0x79, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x64, 0x10, 0xb1, 0x87, 0x80, 0x80, 0x04, 0x12, 0x19, 0x0a, + 0x11, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x42, 0x61, 0x64, 0x4f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x10, 0xb2, 0x87, 0x80, 0x80, 0x04, 0x12, 0x22, 0x0a, 0x1a, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x4e, 0x4c, 0x5f, 0x4c, 0x6f, 0x6f, 0x70, 0x62, 0x61, 0x63, 0x6b, 0x44, 0x69, 0x73, 0x61, + 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x10, 0xb3, 0x87, 0x80, 0x80, 0x04, 0x12, 0x1c, 0x0a, 0x14, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x53, 0x6d, 0x61, 0x6c, 0x6c, 0x65, 0x72, 0x53, + 0x63, 0x6f, 0x70, 0x65, 0x10, 0xb4, 0x87, 0x80, 0x80, 0x04, 0x12, 0x19, 0x0a, 0x11, 0x44, 0x72, + 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x51, 0x75, 0x65, 0x75, 0x65, 0x46, 0x75, 0x6c, 0x6c, 0x10, + 0xb5, 0x87, 0x80, 0x80, 0x04, 0x12, 0x21, 0x0a, 0x19, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, + 0x5f, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x10, 0xb6, 0x87, 0x80, 0x80, 0x04, 0x12, 0x1b, 0x0a, 0x13, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x69, 0x63, 0x10, + 0xb7, 0x87, 0x80, 0x80, 0x04, 0x12, 0x23, 0x0a, 0x1b, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, + 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x54, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64, 0x48, 0x65, + 0x61, 0x64, 0x65, 0x72, 0x10, 0xb8, 0x87, 0x80, 0x80, 0x04, 0x12, 0x23, 0x0a, 0x1b, 0x44, 0x72, + 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x10, 0xb9, 0x87, 0x80, 0x80, 0x04, 0x12, + 0x1e, 0x0a, 0x16, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x49, + 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x10, 0xba, 0x87, 0x80, 0x80, 0x04, 0x12, + 0x2d, 0x0a, 0x25, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x4e, + 0x65, 0x69, 0x67, 0x68, 0x62, 0x6f, 0x72, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, + 0x4c, 0x6f, 0x6f, 0x70, 0x62, 0x61, 0x63, 0x6b, 0x10, 0xbb, 0x87, 0x80, 0x80, 0x04, 0x12, 0x1f, + 0x0a, 0x17, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x55, 0x6e, + 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x10, 0xbc, 0x87, 0x80, 0x80, 0x04, 0x12, + 0x25, 0x0a, 0x1d, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x54, + 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64, 0x49, 0x70, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, + 0x10, 0xbd, 0x87, 0x80, 0x80, 0x04, 0x12, 0x25, 0x0a, 0x1d, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, + 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x4f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x7a, 0x65, 0x64, 0x49, + 0x70, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x10, 0xbe, 0x87, 0x80, 0x80, 0x04, 0x12, 0x1d, 0x0a, + 0x15, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x4e, 0x6f, 0x48, + 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x10, 0xbf, 0x87, 0x80, 0x80, 0x04, 0x12, 0x25, 0x0a, 0x1d, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x54, 0x6f, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x10, 0xc0, 0x87, + 0x80, 0x80, 0x04, 0x12, 0x21, 0x0a, 0x19, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, + 0x63, 0x6d, 0x70, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x10, 0xc1, 0x87, 0x80, 0x80, 0x04, 0x12, 0x26, 0x0a, 0x1e, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, + 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x52, + 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x10, 0xc2, 0x87, 0x80, 0x80, 0x04, 0x12, 0x21, + 0x0a, 0x19, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x50, 0x61, + 0x74, 0x68, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x10, 0xc3, 0x87, 0x80, 0x80, + 0x04, 0x12, 0x1b, 0x0a, 0x13, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, + 0x70, 0x4e, 0x6f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x10, 0xc4, 0x87, 0x80, 0x80, 0x04, 0x12, 0x2b, + 0x0a, 0x23, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x4d, 0x61, + 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4e, 0x6f, 0x74, + 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x10, 0xc5, 0x87, 0x80, 0x80, 0x04, 0x12, 0x22, 0x0a, 0x1a, 0x44, + 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x42, 0x75, 0x66, 0x66, 0x65, + 0x72, 0x54, 0x6f, 0x6f, 0x53, 0x6d, 0x61, 0x6c, 0x6c, 0x10, 0xc6, 0x87, 0x80, 0x80, 0x04, 0x12, + 0x26, 0x0a, 0x1e, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x41, + 0x6e, 0x63, 0x69, 0x6c, 0x6c, 0x61, 0x72, 0x79, 0x44, 0x61, 0x74, 0x61, 0x51, 0x75, 0x65, 0x72, + 0x79, 0x10, 0xc7, 0x87, 0x80, 0x80, 0x04, 0x12, 0x25, 0x0a, 0x1d, 0x44, 0x72, 0x6f, 0x70, 0x5f, + 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x49, 0x6e, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x63, 0x74, + 0x48, 0x6f, 0x70, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x10, 0xc8, 0x87, 0x80, 0x80, 0x04, 0x12, 0x1f, + 0x0a, 0x17, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x55, 0x6e, + 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x43, 0x6f, 0x64, 0x65, 0x10, 0xc9, 0x87, 0x80, 0x80, 0x04, 0x12, + 0x26, 0x0a, 0x1e, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x53, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4e, 0x6f, 0x74, 0x4c, 0x69, 0x6e, 0x6b, 0x4c, 0x6f, 0x63, 0x61, + 0x6c, 0x10, 0xca, 0x87, 0x80, 0x80, 0x04, 0x12, 0x25, 0x0a, 0x1d, 0x44, 0x72, 0x6f, 0x70, 0x5f, + 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x54, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64, + 0x4e, 0x64, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x10, 0xcb, 0x87, 0x80, 0x80, 0x04, 0x12, 0x2e, + 0x0a, 0x26, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x49, 0x6e, + 0x76, 0x61, 0x6c, 0x69, 0x64, 0x4e, 0x64, 0x4f, 0x70, 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x4c, 0x69, 0x6e, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x10, 0xcc, 0x87, 0x80, 0x80, 0x04, 0x12, 0x23, + 0x0a, 0x1b, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x49, 0x6e, + 0x76, 0x61, 0x6c, 0x69, 0x64, 0x4e, 0x64, 0x4f, 0x70, 0x74, 0x4d, 0x74, 0x75, 0x10, 0xcd, 0x87, + 0x80, 0x80, 0x04, 0x12, 0x31, 0x0a, 0x29, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, + 0x63, 0x6d, 0x70, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x4e, 0x64, 0x4f, 0x70, 0x74, 0x50, + 0x72, 0x65, 0x66, 0x69, 0x78, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x10, 0xce, 0x87, 0x80, 0x80, 0x04, 0x12, 0x30, 0x0a, 0x28, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, + 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x4e, 0x64, 0x4f, + 0x70, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x10, 0xcf, 0x87, 0x80, 0x80, 0x04, 0x12, 0x25, 0x0a, 0x1d, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x4e, + 0x64, 0x4f, 0x70, 0x74, 0x52, 0x64, 0x6e, 0x73, 0x73, 0x10, 0xd0, 0x87, 0x80, 0x80, 0x04, 0x12, + 0x25, 0x0a, 0x1d, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x49, + 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x4e, 0x64, 0x4f, 0x70, 0x74, 0x44, 0x6e, 0x73, 0x73, 0x6c, + 0x10, 0xd1, 0x87, 0x80, 0x80, 0x04, 0x12, 0x28, 0x0a, 0x20, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, + 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x50, 0x61, 0x72, 0x73, + 0x69, 0x6e, 0x67, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x10, 0xd2, 0x87, 0x80, 0x80, 0x04, + 0x12, 0x1e, 0x0a, 0x16, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, + 0x44, 0x69, 0x73, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x10, 0xd3, 0x87, 0x80, 0x80, 0x04, + 0x12, 0x2e, 0x0a, 0x26, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, + 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x41, 0x64, 0x76, + 0x65, 0x72, 0x74, 0x69, 0x73, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x10, 0xd4, 0x87, 0x80, 0x80, 0x04, + 0x12, 0x2b, 0x0a, 0x23, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, + 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x46, 0x72, 0x6f, 0x6d, 0x44, 0x69, 0x66, 0x66, 0x65, 0x72, + 0x65, 0x6e, 0x74, 0x4c, 0x69, 0x6e, 0x6b, 0x10, 0xd5, 0x87, 0x80, 0x80, 0x04, 0x12, 0x36, 0x0a, + 0x2e, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x49, 0x6e, 0x76, + 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x44, 0x65, 0x73, 0x74, + 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x72, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x10, + 0xd6, 0x87, 0x80, 0x80, 0x04, 0x12, 0x23, 0x0a, 0x1b, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, + 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x4e, 0x64, 0x54, 0x61, + 0x72, 0x67, 0x65, 0x74, 0x10, 0xd7, 0x87, 0x80, 0x80, 0x04, 0x12, 0x2b, 0x0a, 0x23, 0x44, 0x72, + 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x4e, 0x61, 0x4d, 0x75, 0x6c, 0x74, + 0x69, 0x63, 0x61, 0x73, 0x74, 0x41, 0x6e, 0x64, 0x53, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x74, 0x65, + 0x64, 0x10, 0xd8, 0x87, 0x80, 0x80, 0x04, 0x12, 0x2d, 0x0a, 0x25, 0x44, 0x72, 0x6f, 0x70, 0x5f, + 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x4e, 0x64, 0x4c, 0x69, 0x6e, 0x6b, 0x4c, 0x61, 0x79, + 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x49, 0x73, 0x4c, 0x6f, 0x63, 0x61, 0x6c, + 0x10, 0xd9, 0x87, 0x80, 0x80, 0x04, 0x12, 0x28, 0x0a, 0x20, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, + 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x44, 0x75, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x45, + 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x10, 0xda, 0x87, 0x80, 0x80, 0x04, + 0x12, 0x27, 0x0a, 0x1f, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, + 0x4e, 0x6f, 0x74, 0x41, 0x50, 0x6f, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x52, 0x6f, 0x75, + 0x74, 0x65, 0x72, 0x10, 0xdb, 0x87, 0x80, 0x80, 0x04, 0x12, 0x23, 0x0a, 0x1b, 0x44, 0x72, 0x6f, + 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, + 0x4d, 0x6c, 0x64, 0x51, 0x75, 0x65, 0x72, 0x79, 0x10, 0xdc, 0x87, 0x80, 0x80, 0x04, 0x12, 0x24, + 0x0a, 0x1c, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x49, 0x6e, + 0x76, 0x61, 0x6c, 0x69, 0x64, 0x4d, 0x6c, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x10, 0xdd, + 0x87, 0x80, 0x80, 0x04, 0x12, 0x2b, 0x0a, 0x23, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, + 0x49, 0x63, 0x6d, 0x70, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x6c, 0x79, 0x53, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x64, 0x4d, 0x6c, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x10, 0xde, 0x87, 0x80, 0x80, + 0x04, 0x12, 0x26, 0x0a, 0x1e, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, + 0x70, 0x4e, 0x6f, 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x6c, 0x79, 0x44, 0x65, 0x73, 0x74, 0x69, + 0x6e, 0x65, 0x64, 0x10, 0xdf, 0x87, 0x80, 0x80, 0x04, 0x12, 0x20, 0x0a, 0x18, 0x44, 0x72, 0x6f, + 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x41, 0x72, 0x70, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x53, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x10, 0xe0, 0x87, 0x80, 0x80, 0x04, 0x12, 0x20, 0x0a, 0x18, 0x44, + 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x41, 0x72, 0x70, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x10, 0xe1, 0x87, 0x80, 0x80, 0x04, 0x12, 0x22, 0x0a, + 0x1a, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x41, 0x72, 0x70, 0x44, 0x6c, 0x53, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x49, 0x73, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x10, 0xe2, 0x87, 0x80, 0x80, + 0x04, 0x12, 0x25, 0x0a, 0x1d, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x41, 0x72, 0x70, + 0x4e, 0x6f, 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x6c, 0x79, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, + 0x65, 0x64, 0x10, 0xe3, 0x87, 0x80, 0x80, 0x04, 0x12, 0x1f, 0x0a, 0x17, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x4e, 0x4c, 0x5f, 0x4e, 0x6c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x44, 0x69, 0x73, 0x63, + 0x61, 0x72, 0x64, 0x10, 0xe4, 0x87, 0x80, 0x80, 0x04, 0x12, 0x2e, 0x0a, 0x26, 0x44, 0x72, 0x6f, + 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x70, 0x73, 0x6e, 0x70, 0x69, 0x55, 0x72, 0x6f, 0x53, 0x65, + 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x69, 0x7a, 0x65, 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, 0x73, + 0x4d, 0x74, 0x75, 0x10, 0xe5, 0x87, 0x80, 0x80, 0x04, 0x12, 0x24, 0x0a, 0x1c, 0x44, 0x72, 0x6f, + 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x46, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, + 0x74, 0x65, 0x64, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x10, 0xe6, 0x87, 0x80, 0x80, 0x04, 0x12, + 0x27, 0x0a, 0x1f, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x46, 0x69, 0x72, 0x73, 0x74, + 0x46, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x10, 0xe7, 0x87, 0x80, 0x80, 0x04, 0x12, 0x1f, 0x0a, 0x17, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x4e, 0x4c, 0x5f, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x69, 0x6f, 0x6c, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x10, 0xe8, 0x87, 0x80, 0x80, 0x04, 0x12, 0x1d, 0x0a, 0x15, 0x44, 0x72, 0x6f, + 0x70, 0x5f, 0x4e, 0x4c, 0x5f, 0x49, 0x63, 0x6d, 0x70, 0x4a, 0x75, 0x6d, 0x62, 0x6f, 0x67, 0x72, + 0x61, 0x6d, 0x10, 0xe9, 0x87, 0x80, 0x80, 0x04, 0x12, 0x1c, 0x0a, 0x14, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x4e, 0x4c, 0x5f, 0x53, 0x77, 0x55, 0x73, 0x6f, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, + 0x10, 0xea, 0x87, 0x80, 0x80, 0x04, 0x12, 0x23, 0x0a, 0x1b, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, + 0x4e, 0x45, 0x54, 0x5f, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x55, 0x6e, 0x73, 0x70, 0x65, 0x63, + 0x69, 0x66, 0x69, 0x65, 0x64, 0x10, 0xb0, 0x89, 0x80, 0x80, 0x04, 0x12, 0x26, 0x0a, 0x1e, 0x44, + 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x4e, 0x45, 0x54, 0x5f, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x63, 0x61, 0x73, 0x74, 0x10, 0xb1, 0x89, + 0x80, 0x80, 0x04, 0x12, 0x1f, 0x0a, 0x17, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x4e, 0x45, 0x54, + 0x5f, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x10, 0xb2, + 0x89, 0x80, 0x80, 0x04, 0x12, 0x21, 0x0a, 0x19, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x4e, 0x45, + 0x54, 0x5f, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x10, 0xb3, 0x89, 0x80, 0x80, 0x04, 0x12, 0x22, 0x0a, 0x1a, 0x44, 0x72, 0x6f, 0x70, 0x5f, + 0x49, 0x4e, 0x45, 0x54, 0x5f, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x4e, 0x6f, 0x74, + 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x10, 0xb4, 0x89, 0x80, 0x80, 0x04, 0x12, 0x1f, 0x0a, 0x17, 0x44, + 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x4e, 0x45, 0x54, 0x5f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x65, 0x64, 0x50, 0x61, 0x74, 0x68, 0x10, 0xb5, 0x89, 0x80, 0x80, 0x04, 0x12, 0x1e, 0x0a, 0x16, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x4e, 0x45, 0x54, 0x5f, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x10, 0xb6, 0x89, 0x80, 0x80, 0x04, 0x12, 0x23, 0x0a, 0x1b, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x4e, 0x45, 0x54, 0x5f, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, + 0x65, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x10, 0xb7, 0x89, 0x80, 0x80, + 0x04, 0x12, 0x1c, 0x0a, 0x14, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x4e, 0x45, 0x54, 0x5f, 0x41, + 0x63, 0x6b, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x10, 0xb8, 0x89, 0x80, 0x80, 0x04, 0x12, + 0x1d, 0x0a, 0x15, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x4e, 0x45, 0x54, 0x5f, 0x45, 0x78, 0x70, + 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x79, 0x6e, 0x10, 0xb9, 0x89, 0x80, 0x80, 0x04, 0x12, 0x15, + 0x0a, 0x0d, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x4e, 0x45, 0x54, 0x5f, 0x52, 0x73, 0x74, 0x10, + 0xba, 0x89, 0x80, 0x80, 0x04, 0x12, 0x1c, 0x0a, 0x14, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x4e, + 0x45, 0x54, 0x5f, 0x53, 0x79, 0x6e, 0x52, 0x63, 0x76, 0x64, 0x53, 0x79, 0x6e, 0x10, 0xbb, 0x89, + 0x80, 0x80, 0x04, 0x12, 0x25, 0x0a, 0x1d, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x4e, 0x45, 0x54, + 0x5f, 0x53, 0x69, 0x6d, 0x75, 0x6c, 0x74, 0x61, 0x6e, 0x65, 0x6f, 0x75, 0x73, 0x43, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x10, 0xbc, 0x89, 0x80, 0x80, 0x04, 0x12, 0x1c, 0x0a, 0x14, 0x44, 0x72, + 0x6f, 0x70, 0x5f, 0x49, 0x4e, 0x45, 0x54, 0x5f, 0x50, 0x61, 0x77, 0x73, 0x46, 0x61, 0x69, 0x6c, + 0x65, 0x64, 0x10, 0xbd, 0x89, 0x80, 0x80, 0x04, 0x12, 0x1c, 0x0a, 0x14, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x49, 0x4e, 0x45, 0x54, 0x5f, 0x4c, 0x61, 0x6e, 0x64, 0x41, 0x74, 0x74, 0x61, 0x63, 0x6b, + 0x10, 0xbe, 0x89, 0x80, 0x80, 0x04, 0x12, 0x1d, 0x0a, 0x15, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, + 0x4e, 0x45, 0x54, 0x5f, 0x4d, 0x69, 0x73, 0x73, 0x65, 0x64, 0x52, 0x65, 0x73, 0x65, 0x74, 0x10, + 0xbf, 0x89, 0x80, 0x80, 0x04, 0x12, 0x1f, 0x0a, 0x17, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x4e, + 0x45, 0x54, 0x5f, 0x4f, 0x75, 0x74, 0x73, 0x69, 0x64, 0x65, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, + 0x10, 0xc0, 0x89, 0x80, 0x80, 0x04, 0x12, 0x22, 0x0a, 0x1a, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, + 0x4e, 0x45, 0x54, 0x5f, 0x44, 0x75, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x65, 0x67, + 0x6d, 0x65, 0x6e, 0x74, 0x10, 0xc1, 0x89, 0x80, 0x80, 0x04, 0x12, 0x1e, 0x0a, 0x16, 0x44, 0x72, + 0x6f, 0x70, 0x5f, 0x49, 0x4e, 0x45, 0x54, 0x5f, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x64, 0x57, 0x69, + 0x6e, 0x64, 0x6f, 0x77, 0x10, 0xc2, 0x89, 0x80, 0x80, 0x04, 0x12, 0x1c, 0x0a, 0x14, 0x44, 0x72, + 0x6f, 0x70, 0x5f, 0x49, 0x4e, 0x45, 0x54, 0x5f, 0x54, 0x63, 0x62, 0x52, 0x65, 0x6d, 0x6f, 0x76, + 0x65, 0x64, 0x10, 0xc3, 0x89, 0x80, 0x80, 0x04, 0x12, 0x1a, 0x0a, 0x12, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x49, 0x4e, 0x45, 0x54, 0x5f, 0x46, 0x69, 0x6e, 0x57, 0x61, 0x69, 0x74, 0x32, 0x10, 0xc4, + 0x89, 0x80, 0x80, 0x04, 0x12, 0x24, 0x0a, 0x1c, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x4e, 0x45, + 0x54, 0x5f, 0x52, 0x65, 0x61, 0x73, 0x73, 0x65, 0x6d, 0x62, 0x6c, 0x79, 0x43, 0x6f, 0x6e, 0x66, + 0x6c, 0x69, 0x63, 0x74, 0x10, 0xc5, 0x89, 0x80, 0x80, 0x04, 0x12, 0x1d, 0x0a, 0x15, 0x44, 0x72, + 0x6f, 0x70, 0x5f, 0x49, 0x4e, 0x45, 0x54, 0x5f, 0x46, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x65, 0x69, + 0x76, 0x65, 0x64, 0x10, 0xc6, 0x89, 0x80, 0x80, 0x04, 0x12, 0x26, 0x0a, 0x1e, 0x44, 0x72, 0x6f, + 0x70, 0x5f, 0x49, 0x4e, 0x45, 0x54, 0x5f, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x49, + 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x10, 0xc7, 0x89, 0x80, 0x80, + 0x04, 0x12, 0x22, 0x0a, 0x1a, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x4e, 0x45, 0x54, 0x5f, 0x54, + 0x63, 0x62, 0x4e, 0x6f, 0x74, 0x49, 0x6e, 0x54, 0x63, 0x62, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x10, + 0xc8, 0x89, 0x80, 0x80, 0x04, 0x12, 0x35, 0x0a, 0x2d, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x4e, + 0x45, 0x54, 0x5f, 0x54, 0x69, 0x6d, 0x65, 0x57, 0x61, 0x69, 0x74, 0x54, 0x63, 0x62, 0x52, 0x65, + 0x63, 0x65, 0x69, 0x76, 0x65, 0x64, 0x52, 0x73, 0x74, 0x4f, 0x75, 0x74, 0x73, 0x69, 0x64, 0x65, + 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x10, 0xc9, 0x89, 0x80, 0x80, 0x04, 0x12, 0x2d, 0x0a, 0x25, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x4e, 0x45, 0x54, 0x5f, 0x54, 0x69, 0x6d, 0x65, 0x57, 0x61, + 0x69, 0x74, 0x54, 0x63, 0x62, 0x53, 0x79, 0x6e, 0x41, 0x6e, 0x64, 0x4f, 0x74, 0x68, 0x65, 0x72, + 0x46, 0x6c, 0x61, 0x67, 0x73, 0x10, 0xca, 0x89, 0x80, 0x80, 0x04, 0x12, 0x1d, 0x0a, 0x15, 0x44, + 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x4e, 0x45, 0x54, 0x5f, 0x54, 0x69, 0x6d, 0x65, 0x57, 0x61, 0x69, + 0x74, 0x54, 0x63, 0x62, 0x10, 0xcb, 0x89, 0x80, 0x80, 0x04, 0x12, 0x31, 0x0a, 0x29, 0x44, 0x72, + 0x6f, 0x70, 0x5f, 0x49, 0x4e, 0x45, 0x54, 0x5f, 0x53, 0x79, 0x6e, 0x41, 0x63, 0x6b, 0x57, 0x69, + 0x74, 0x68, 0x46, 0x61, 0x73, 0x74, 0x6f, 0x70, 0x65, 0x6e, 0x43, 0x6f, 0x6f, 0x6b, 0x69, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x10, 0xcc, 0x89, 0x80, 0x80, 0x04, 0x12, 0x1d, 0x0a, + 0x15, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x4e, 0x45, 0x54, 0x5f, 0x50, 0x61, 0x75, 0x73, 0x65, + 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x10, 0xcd, 0x89, 0x80, 0x80, 0x04, 0x12, 0x1b, 0x0a, 0x13, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x4e, 0x45, 0x54, 0x5f, 0x53, 0x79, 0x6e, 0x41, 0x74, 0x74, + 0x61, 0x63, 0x6b, 0x10, 0xce, 0x89, 0x80, 0x80, 0x04, 0x12, 0x22, 0x0a, 0x1a, 0x44, 0x72, 0x6f, + 0x70, 0x5f, 0x49, 0x4e, 0x45, 0x54, 0x5f, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x49, 0x6e, 0x73, + 0x70, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x10, 0xcf, 0x89, 0x80, 0x80, 0x04, 0x12, 0x23, 0x0a, + 0x1b, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x4e, 0x45, 0x54, 0x5f, 0x41, 0x63, 0x63, 0x65, 0x70, + 0x74, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x10, 0xd0, 0x89, 0x80, + 0x80, 0x04, 0x12, 0x22, 0x0a, 0x1a, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, 0x6c, 0x62, 0x4d, 0x75, + 0x78, 0x5f, 0x50, 0x61, 0x72, 0x73, 0x69, 0x6e, 0x67, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, + 0x10, 0x95, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x25, 0x0a, 0x1d, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, + 0x6c, 0x62, 0x4d, 0x75, 0x78, 0x5f, 0x46, 0x69, 0x72, 0x73, 0x74, 0x46, 0x72, 0x61, 0x67, 0x6d, + 0x65, 0x6e, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x10, 0x96, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x35, 0x0a, + 0x2d, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, 0x6c, 0x62, 0x4d, 0x75, 0x78, 0x5f, 0x49, 0x43, 0x4d, + 0x50, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x56, 0x61, 0x6c, + 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x10, 0x97, + 0x8a, 0x80, 0x80, 0x04, 0x12, 0x31, 0x0a, 0x29, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, 0x6c, 0x62, + 0x4d, 0x75, 0x78, 0x5f, 0x49, 0x43, 0x4d, 0x50, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x50, 0x61, 0x63, + 0x6b, 0x65, 0x74, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x4e, 0x6f, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x10, 0x98, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x37, 0x0a, 0x2f, 0x44, 0x72, 0x6f, 0x70, 0x5f, + 0x53, 0x6c, 0x62, 0x4d, 0x75, 0x78, 0x5f, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x48, + 0x61, 0x69, 0x72, 0x70, 0x69, 0x6e, 0x4e, 0x65, 0x78, 0x74, 0x68, 0x6f, 0x70, 0x4c, 0x6f, 0x6f, + 0x6b, 0x75, 0x70, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x10, 0x99, 0x8a, 0x80, 0x80, 0x04, + 0x12, 0x2b, 0x0a, 0x23, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, 0x6c, 0x62, 0x4d, 0x75, 0x78, 0x5f, + 0x4e, 0x6f, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, + 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x10, 0x9a, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x2b, 0x0a, + 0x23, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, 0x6c, 0x62, 0x4d, 0x75, 0x78, 0x5f, 0x4e, 0x65, 0x78, + 0x74, 0x68, 0x6f, 0x70, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x46, 0x61, 0x69, + 0x6c, 0x75, 0x72, 0x65, 0x10, 0x9b, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x22, 0x0a, 0x1a, 0x44, 0x72, + 0x6f, 0x70, 0x5f, 0x53, 0x6c, 0x62, 0x4d, 0x75, 0x78, 0x5f, 0x43, 0x6c, 0x6f, 0x6e, 0x69, 0x6e, + 0x67, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x10, 0x9c, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x26, + 0x0a, 0x1e, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, 0x6c, 0x62, 0x4d, 0x75, 0x78, 0x5f, 0x54, 0x72, + 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, + 0x10, 0x9d, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x24, 0x0a, 0x1c, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, + 0x6c, 0x62, 0x4d, 0x75, 0x78, 0x5f, 0x48, 0x6f, 0x70, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x45, 0x78, + 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x10, 0x9e, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x27, 0x0a, 0x1f, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, 0x6c, 0x62, 0x4d, 0x75, 0x78, 0x5f, 0x50, 0x61, 0x63, 0x6b, + 0x65, 0x74, 0x42, 0x69, 0x67, 0x67, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, 0x4d, 0x54, 0x55, 0x10, + 0x9f, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x30, 0x0a, 0x28, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, 0x6c, + 0x62, 0x4d, 0x75, 0x78, 0x5f, 0x55, 0x6e, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x52, + 0x6f, 0x75, 0x74, 0x65, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, + 0x65, 0x10, 0xa0, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x1b, 0x0a, 0x13, 0x44, 0x72, 0x6f, 0x70, 0x5f, + 0x53, 0x6c, 0x62, 0x4d, 0x75, 0x78, 0x5f, 0x4e, 0x6f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x10, 0xa1, + 0x8a, 0x80, 0x80, 0x04, 0x12, 0x2a, 0x0a, 0x22, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, 0x6c, 0x62, + 0x4d, 0x75, 0x78, 0x5f, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x10, 0xa2, 0x8a, 0x80, 0x80, 0x04, + 0x12, 0x33, 0x0a, 0x2b, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, 0x6c, 0x62, 0x4d, 0x75, 0x78, 0x5f, + 0x4e, 0x65, 0x78, 0x74, 0x68, 0x6f, 0x70, 0x4e, 0x6f, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x45, 0x78, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x10, + 0xa3, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x3b, 0x0a, 0x33, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, 0x6c, + 0x62, 0x4d, 0x75, 0x78, 0x5f, 0x4e, 0x65, 0x78, 0x74, 0x68, 0x6f, 0x70, 0x45, 0x78, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4d, 0x69, 0x73, + 0x73, 0x4e, 0x41, 0x54, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x10, 0xa4, 0x8a, 0x80, + 0x80, 0x04, 0x12, 0x32, 0x0a, 0x2a, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, 0x6c, 0x62, 0x4d, 0x75, + 0x78, 0x5f, 0x4e, 0x41, 0x54, 0x49, 0x74, 0x73, 0x65, 0x6c, 0x66, 0x43, 0x61, 0x6e, 0x74, 0x42, + 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x4e, 0x65, 0x78, 0x74, 0x68, 0x6f, 0x70, + 0x10, 0xa5, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x39, 0x0a, 0x31, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, + 0x6c, 0x62, 0x4d, 0x75, 0x78, 0x5f, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x74, + 0x61, 0x62, 0x6c, 0x65, 0x49, 0x6e, 0x49, 0x74, 0x73, 0x41, 0x72, 0x72, 0x69, 0x76, 0x61, 0x6c, + 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x10, 0xa6, 0x8a, 0x80, 0x80, + 0x04, 0x12, 0x37, 0x0a, 0x2f, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, 0x6c, 0x62, 0x4d, 0x75, 0x78, + 0x5f, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x4e, 0x6f, 0x74, 0x53, 0x75, 0x70, 0x70, 0x6f, + 0x72, 0x74, 0x65, 0x64, 0x10, 0xa7, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x2b, 0x0a, 0x23, 0x44, 0x72, + 0x6f, 0x70, 0x5f, 0x53, 0x6c, 0x62, 0x4d, 0x75, 0x78, 0x5f, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, + 0x49, 0x73, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x65, 0x64, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x6c, + 0x79, 0x10, 0xa8, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x3d, 0x0a, 0x35, 0x44, 0x72, 0x6f, 0x70, 0x5f, + 0x53, 0x6c, 0x62, 0x4d, 0x75, 0x78, 0x5f, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x44, 0x65, 0x73, + 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x50, 0x61, 0x6e, 0x64, 0x50, 0x6f, 0x72, + 0x74, 0x4e, 0x6f, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x54, 0x6f, 0x4e, 0x41, 0x54, + 0x10, 0xa9, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x1d, 0x0a, 0x15, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, + 0x6c, 0x62, 0x4d, 0x75, 0x78, 0x5f, 0x4d, 0x75, 0x78, 0x52, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x10, + 0xaa, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x24, 0x0a, 0x1c, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, 0x6c, + 0x62, 0x4d, 0x75, 0x78, 0x5f, 0x44, 0x69, 0x70, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x46, 0x61, + 0x69, 0x6c, 0x75, 0x72, 0x65, 0x10, 0xab, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x2b, 0x0a, 0x23, 0x44, + 0x72, 0x6f, 0x70, 0x5f, 0x53, 0x6c, 0x62, 0x4d, 0x75, 0x78, 0x5f, 0x4d, 0x75, 0x78, 0x45, 0x6e, + 0x63, 0x61, 0x70, 0x73, 0x75, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x61, 0x69, 0x6c, 0x75, + 0x72, 0x65, 0x10, 0xac, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x2e, 0x0a, 0x26, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x53, 0x6c, 0x62, 0x4d, 0x75, 0x78, 0x5f, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x44, + 0x69, 0x61, 0x67, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x45, 0x6e, 0x63, 0x61, 0x70, 0x54, 0x79, + 0x70, 0x65, 0x10, 0xad, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x28, 0x0a, 0x20, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x53, 0x6c, 0x62, 0x4d, 0x75, 0x78, 0x5f, 0x44, 0x69, 0x61, 0x67, 0x50, 0x61, 0x63, 0x6b, + 0x65, 0x74, 0x49, 0x73, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x10, 0xae, 0x8a, 0x80, + 0x80, 0x04, 0x12, 0x2a, 0x0a, 0x22, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x53, 0x6c, 0x62, 0x4d, 0x75, + 0x78, 0x5f, 0x55, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x6f, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, + 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x10, 0xaf, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x19, + 0x0a, 0x11, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x70, 0x73, 0x65, 0x63, 0x5f, 0x42, 0x61, 0x64, + 0x53, 0x70, 0x69, 0x10, 0xf9, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x24, 0x0a, 0x1c, 0x44, 0x72, 0x6f, + 0x70, 0x5f, 0x49, 0x70, 0x73, 0x65, 0x63, 0x5f, 0x53, 0x41, 0x4c, 0x69, 0x66, 0x65, 0x74, 0x69, + 0x6d, 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x10, 0xfa, 0x8a, 0x80, 0x80, 0x04, 0x12, + 0x1a, 0x0a, 0x12, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x70, 0x73, 0x65, 0x63, 0x5f, 0x57, 0x72, + 0x6f, 0x6e, 0x67, 0x53, 0x41, 0x10, 0xfb, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x24, 0x0a, 0x1c, 0x44, + 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x70, 0x73, 0x65, 0x63, 0x5f, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x79, + 0x43, 0x68, 0x65, 0x63, 0x6b, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0xfc, 0x8a, 0x80, 0x80, + 0x04, 0x12, 0x20, 0x0a, 0x18, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x70, 0x73, 0x65, 0x63, 0x5f, + 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x10, 0xfd, 0x8a, + 0x80, 0x80, 0x04, 0x12, 0x27, 0x0a, 0x1f, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x70, 0x73, 0x65, + 0x63, 0x5f, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x69, 0x74, 0x79, 0x43, 0x68, 0x65, 0x63, 0x6b, + 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0xfe, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x20, 0x0a, 0x18, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x70, 0x73, 0x65, 0x63, 0x5f, 0x43, 0x6c, 0x65, 0x61, 0x72, + 0x54, 0x65, 0x78, 0x74, 0x44, 0x72, 0x6f, 0x70, 0x10, 0xff, 0x8a, 0x80, 0x80, 0x04, 0x12, 0x23, + 0x0a, 0x1b, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x70, 0x73, 0x65, 0x63, 0x5f, 0x41, 0x75, 0x74, + 0x68, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x44, 0x72, 0x6f, 0x70, 0x10, 0x80, 0x8b, + 0x80, 0x80, 0x04, 0x12, 0x1f, 0x0a, 0x17, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x70, 0x73, 0x65, + 0x63, 0x5f, 0x54, 0x68, 0x72, 0x6f, 0x74, 0x74, 0x6c, 0x65, 0x44, 0x72, 0x6f, 0x70, 0x10, 0x81, + 0x8b, 0x80, 0x80, 0x04, 0x12, 0x1d, 0x0a, 0x15, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x70, 0x73, + 0x65, 0x63, 0x5f, 0x44, 0x6f, 0x73, 0x70, 0x5f, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x10, 0x82, 0x8b, + 0x80, 0x80, 0x04, 0x12, 0x29, 0x0a, 0x21, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x70, 0x73, 0x65, + 0x63, 0x5f, 0x44, 0x6f, 0x73, 0x70, 0x5f, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x64, 0x4d, + 0x75, 0x6c, 0x74, 0x69, 0x63, 0x61, 0x73, 0x74, 0x10, 0x83, 0x8b, 0x80, 0x80, 0x04, 0x12, 0x25, + 0x0a, 0x1d, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x70, 0x73, 0x65, 0x63, 0x5f, 0x44, 0x6f, 0x73, + 0x70, 0x5f, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x10, + 0x84, 0x8b, 0x80, 0x80, 0x04, 0x12, 0x29, 0x0a, 0x21, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x70, + 0x73, 0x65, 0x63, 0x5f, 0x44, 0x6f, 0x73, 0x70, 0x5f, 0x53, 0x74, 0x61, 0x74, 0x65, 0x4c, 0x6f, + 0x6f, 0x6b, 0x75, 0x70, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0x85, 0x8b, 0x80, 0x80, 0x04, + 0x12, 0x22, 0x0a, 0x1a, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x70, 0x73, 0x65, 0x63, 0x5f, 0x44, + 0x6f, 0x73, 0x70, 0x5f, 0x4d, 0x61, 0x78, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x10, 0x86, + 0x8b, 0x80, 0x80, 0x04, 0x12, 0x28, 0x0a, 0x20, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x70, 0x73, + 0x65, 0x63, 0x5f, 0x44, 0x6f, 0x73, 0x70, 0x5f, 0x4b, 0x65, 0x79, 0x6d, 0x6f, 0x64, 0x4e, 0x6f, + 0x74, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x10, 0x87, 0x8b, 0x80, 0x80, 0x04, 0x12, 0x2f, + 0x0a, 0x27, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x70, 0x73, 0x65, 0x63, 0x5f, 0x44, 0x6f, 0x73, + 0x70, 0x5f, 0x4d, 0x61, 0x78, 0x50, 0x65, 0x72, 0x49, 0x70, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, + 0x6d, 0x69, 0x74, 0x51, 0x75, 0x65, 0x75, 0x65, 0x73, 0x10, 0x88, 0x8b, 0x80, 0x80, 0x04, 0x12, + 0x1b, 0x0a, 0x13, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x70, 0x73, 0x65, 0x63, 0x5f, 0x4e, 0x6f, + 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x10, 0x89, 0x8b, 0x80, 0x80, 0x04, 0x12, 0x1f, 0x0a, 0x17, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x49, 0x70, 0x73, 0x65, 0x63, 0x5f, 0x55, 0x6e, 0x73, 0x75, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x10, 0x8a, 0x8b, 0x80, 0x80, 0x04, 0x12, 0x2e, 0x0a, + 0x26, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x65, 0x74, 0x43, 0x78, 0x5f, 0x4e, 0x65, 0x74, 0x50, + 0x61, 0x63, 0x6b, 0x65, 0x74, 0x4c, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x50, 0x61, 0x72, 0x73, 0x65, + 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x10, 0xdd, 0x8b, 0x80, 0x80, 0x04, 0x12, 0x2a, 0x0a, + 0x22, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x65, 0x74, 0x43, 0x78, 0x5f, 0x53, 0x6f, 0x66, 0x74, + 0x77, 0x61, 0x72, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x46, 0x61, 0x69, 0x6c, + 0x75, 0x72, 0x65, 0x10, 0xde, 0x8b, 0x80, 0x80, 0x04, 0x12, 0x1f, 0x0a, 0x17, 0x44, 0x72, 0x6f, + 0x70, 0x5f, 0x4e, 0x65, 0x74, 0x43, 0x78, 0x5f, 0x4e, 0x69, 0x63, 0x51, 0x75, 0x65, 0x75, 0x65, + 0x53, 0x74, 0x6f, 0x70, 0x10, 0xdf, 0x8b, 0x80, 0x80, 0x04, 0x12, 0x29, 0x0a, 0x21, 0x44, 0x72, + 0x6f, 0x70, 0x5f, 0x4e, 0x65, 0x74, 0x43, 0x78, 0x5f, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, + 0x4e, 0x65, 0x74, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x10, + 0xe0, 0x8b, 0x80, 0x80, 0x04, 0x12, 0x1d, 0x0a, 0x15, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x65, + 0x74, 0x43, 0x78, 0x5f, 0x4c, 0x53, 0x4f, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x10, 0xe1, + 0x8b, 0x80, 0x80, 0x04, 0x12, 0x1d, 0x0a, 0x15, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x65, 0x74, + 0x43, 0x78, 0x5f, 0x55, 0x53, 0x4f, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x10, 0xe2, 0x8b, + 0x80, 0x80, 0x04, 0x12, 0x35, 0x0a, 0x2d, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x4e, 0x65, 0x74, 0x43, + 0x78, 0x5f, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x42, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x46, 0x61, + 0x69, 0x6c, 0x75, 0x72, 0x65, 0x41, 0x6e, 0x64, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x49, 0x67, + 0x6e, 0x6f, 0x72, 0x65, 0x10, 0xe3, 0x8b, 0x80, 0x80, 0x04, 0x12, 0x17, 0x0a, 0x0f, 0x44, 0x72, + 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x10, 0xb8, 0x97, + 0x80, 0x80, 0x04, 0x12, 0x1f, 0x0a, 0x17, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, + 0x5f, 0x55, 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x10, 0xb9, + 0x97, 0x80, 0x80, 0x04, 0x12, 0x19, 0x0a, 0x11, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, + 0x70, 0x5f, 0x55, 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x10, 0xba, 0x97, 0x80, 0x80, 0x04, 0x12, + 0x1d, 0x0a, 0x15, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x6c, 0x45, + 0x72, 0x72, 0x6f, 0x72, 0x56, 0x65, 0x72, 0x62, 0x10, 0xbb, 0x97, 0x80, 0x80, 0x04, 0x12, 0x1c, + 0x0a, 0x14, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x6c, 0x45, 0x72, + 0x72, 0x6f, 0x72, 0x55, 0x72, 0x6c, 0x10, 0xbc, 0x97, 0x80, 0x80, 0x04, 0x12, 0x1f, 0x0a, 0x17, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x6c, 0x45, 0x72, 0x72, 0x6f, + 0x72, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x10, 0xbd, 0x97, 0x80, 0x80, 0x04, 0x12, 0x1d, 0x0a, + 0x15, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x6c, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x48, 0x6f, 0x73, 0x74, 0x10, 0xbe, 0x97, 0x80, 0x80, 0x04, 0x12, 0x1c, 0x0a, 0x14, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x6c, 0x45, 0x72, 0x72, 0x6f, + 0x72, 0x4e, 0x75, 0x6d, 0x10, 0xbf, 0x97, 0x80, 0x80, 0x04, 0x12, 0x24, 0x0a, 0x1c, 0x44, 0x72, + 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x46, + 0x69, 0x65, 0x6c, 0x64, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x10, 0xc0, 0x97, 0x80, 0x80, 0x04, + 0x12, 0x26, 0x0a, 0x1e, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x6c, + 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x65, 0x6e, 0x67, + 0x74, 0x68, 0x10, 0xc1, 0x97, 0x80, 0x80, 0x04, 0x12, 0x25, 0x0a, 0x1d, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x55, 0x6e, 0x61, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x10, 0xc2, 0x97, 0x80, 0x80, 0x04, 0x12, + 0x25, 0x0a, 0x1d, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x6c, 0x45, + 0x72, 0x72, 0x6f, 0x72, 0x46, 0x6f, 0x72, 0x62, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x55, 0x72, 0x6c, + 0x10, 0xc3, 0x97, 0x80, 0x80, 0x04, 0x12, 0x21, 0x0a, 0x19, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, + 0x74, 0x74, 0x70, 0x5f, 0x55, 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x4e, 0x6f, 0x74, 0x46, 0x6f, + 0x75, 0x6e, 0x64, 0x10, 0xc4, 0x97, 0x80, 0x80, 0x04, 0x12, 0x26, 0x0a, 0x1e, 0x44, 0x72, 0x6f, + 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, + 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x10, 0xc5, 0x97, 0x80, 0x80, + 0x04, 0x12, 0x2b, 0x0a, 0x23, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, + 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0xc6, 0x97, 0x80, 0x80, 0x04, 0x12, 0x27, + 0x0a, 0x1f, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x6c, 0x45, 0x72, + 0x72, 0x6f, 0x72, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x54, 0x6f, 0x6f, 0x4c, 0x61, 0x72, 0x67, + 0x65, 0x10, 0xc7, 0x97, 0x80, 0x80, 0x04, 0x12, 0x22, 0x0a, 0x1a, 0x44, 0x72, 0x6f, 0x70, 0x5f, + 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x55, 0x72, 0x6c, 0x4c, + 0x65, 0x6e, 0x67, 0x74, 0x68, 0x10, 0xc8, 0x97, 0x80, 0x80, 0x04, 0x12, 0x2c, 0x0a, 0x24, 0x44, + 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, + 0x52, 0x61, 0x6e, 0x67, 0x65, 0x4e, 0x6f, 0x74, 0x53, 0x61, 0x74, 0x69, 0x73, 0x66, 0x69, 0x61, + 0x62, 0x6c, 0x65, 0x10, 0xc9, 0x97, 0x80, 0x80, 0x04, 0x12, 0x2b, 0x0a, 0x23, 0x44, 0x72, 0x6f, + 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x69, + 0x73, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x10, 0xca, 0x97, 0x80, 0x80, 0x04, 0x12, 0x27, 0x0a, 0x1f, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, + 0x74, 0x74, 0x70, 0x5f, 0x55, 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x49, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x10, 0xcb, 0x97, 0x80, 0x80, 0x04, 0x12, + 0x27, 0x0a, 0x1f, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x6c, 0x45, + 0x72, 0x72, 0x6f, 0x72, 0x4e, 0x6f, 0x74, 0x49, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x65, 0x64, 0x10, 0xcc, 0x97, 0x80, 0x80, 0x04, 0x12, 0x24, 0x0a, 0x1c, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x55, 0x6e, 0x61, + 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x10, 0xcd, 0x97, 0x80, 0x80, 0x04, 0x12, 0x28, + 0x0a, 0x20, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x6c, 0x45, 0x72, + 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x6d, + 0x69, 0x74, 0x10, 0xce, 0x97, 0x80, 0x80, 0x04, 0x12, 0x2c, 0x0a, 0x24, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x61, 0x70, + 0x69, 0x64, 0x46, 0x61, 0x69, 0x6c, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x10, 0xcf, 0x97, 0x80, 0x80, 0x04, 0x12, 0x29, 0x0a, 0x21, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, + 0x74, 0x74, 0x70, 0x5f, 0x55, 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x51, 0x75, 0x65, 0x75, 0x65, 0x46, 0x75, 0x6c, 0x6c, 0x10, 0xd0, 0x97, 0x80, 0x80, + 0x04, 0x12, 0x28, 0x0a, 0x20, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, + 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x79, + 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x10, 0xd1, 0x97, 0x80, 0x80, 0x04, 0x12, 0x26, 0x0a, 0x1e, 0x44, + 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, + 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x79, 0x41, 0x70, 0x70, 0x10, 0xd2, 0x97, + 0x80, 0x80, 0x04, 0x12, 0x27, 0x0a, 0x1f, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, + 0x5f, 0x55, 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x4a, 0x6f, 0x62, 0x4f, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x46, 0x69, 0x72, 0x65, 0x64, 0x10, 0xd3, 0x97, 0x80, 0x80, 0x04, 0x12, 0x24, 0x0a, 0x1c, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x6c, 0x45, 0x72, 0x72, 0x6f, + 0x72, 0x41, 0x70, 0x70, 0x50, 0x6f, 0x6f, 0x6c, 0x42, 0x75, 0x73, 0x79, 0x10, 0xd4, 0x97, 0x80, + 0x80, 0x04, 0x12, 0x20, 0x0a, 0x18, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, + 0x55, 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x10, 0xd5, + 0x97, 0x80, 0x80, 0x04, 0x12, 0x1d, 0x0a, 0x15, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, + 0x70, 0x5f, 0x55, 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x45, 0x6e, 0x64, 0x10, 0xd6, 0x97, + 0x80, 0x80, 0x04, 0x12, 0x21, 0x0a, 0x19, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, + 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x42, 0x65, 0x67, 0x69, 0x6e, + 0x10, 0xc8, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x25, 0x0a, 0x1d, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, + 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x55, 0x73, + 0x65, 0x72, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x10, 0xc9, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x26, 0x0a, + 0x1e, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, + 0x46, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x10, + 0xca, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x2d, 0x0a, 0x25, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, + 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x65, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x10, 0xcb, + 0x9a, 0x80, 0x80, 0x04, 0x12, 0x2a, 0x0a, 0x22, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, + 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x4d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x4e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x10, 0xcc, 0x9a, 0x80, 0x80, 0x04, + 0x12, 0x2a, 0x0a, 0x22, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, + 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x4d, 0x69, + 0x73, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x10, 0xcd, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x2a, 0x0a, 0x22, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, + 0x61, 0x75, 0x6c, 0x74, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x4e, 0x6f, 0x74, 0x46, 0x6f, 0x75, + 0x6e, 0x64, 0x10, 0xce, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x28, 0x0a, 0x20, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, + 0x44, 0x61, 0x74, 0x61, 0x41, 0x66, 0x74, 0x65, 0x72, 0x45, 0x6e, 0x64, 0x10, 0xcf, 0x9a, 0x80, + 0x80, 0x04, 0x12, 0x28, 0x0a, 0x20, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, + 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x61, 0x74, 0x68, 0x4e, 0x6f, + 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x10, 0xd0, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x2b, 0x0a, 0x23, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, + 0x61, 0x75, 0x6c, 0x74, 0x48, 0x61, 0x6c, 0x66, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x64, 0x4c, 0x6f, + 0x63, 0x61, 0x6c, 0x10, 0xd1, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x2c, 0x0a, 0x24, 0x44, 0x72, 0x6f, + 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, + 0x74, 0x49, 0x6e, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x74, 0x69, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, + 0x68, 0x10, 0xd2, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x27, 0x0a, 0x1f, 0x44, 0x72, 0x6f, 0x70, 0x5f, + 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x44, + 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, 0x33, 0x10, 0xd3, 0x9a, 0x80, 0x80, 0x04, + 0x12, 0x2d, 0x0a, 0x25, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, + 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x65, + 0x72, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x10, 0xd4, 0x9a, 0x80, 0x80, 0x04, 0x12, + 0x2b, 0x0a, 0x23, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, + 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4e, 0x61, 0x6d, + 0x65, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x10, 0xd5, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x27, 0x0a, 0x1f, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, + 0x61, 0x75, 0x6c, 0x74, 0x49, 0x6c, 0x6c, 0x65, 0x67, 0x61, 0x6c, 0x53, 0x65, 0x6e, 0x64, 0x10, + 0xd6, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x2b, 0x0a, 0x23, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, + 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x75, 0x73, + 0x68, 0x55, 0x70, 0x70, 0x65, 0x72, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x10, 0xd7, 0x9a, 0x80, + 0x80, 0x04, 0x12, 0x2d, 0x0a, 0x25, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, + 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, + 0x55, 0x70, 0x70, 0x65, 0x72, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x10, 0xd8, 0x9a, 0x80, 0x80, + 0x04, 0x12, 0x2d, 0x0a, 0x25, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, + 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x53, + 0x74, 0x72, 0x65, 0x61, 0x6d, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x10, 0xd9, 0x9a, 0x80, 0x80, 0x04, + 0x12, 0x2d, 0x0a, 0x25, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, + 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, + 0x79, 0x4e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x10, 0xda, 0x9a, 0x80, 0x80, 0x04, 0x12, + 0x2a, 0x0a, 0x22, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, + 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x55, 0x6e, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, + 0x64, 0x54, 0x61, 0x69, 0x6c, 0x10, 0xdb, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x25, 0x0a, 0x1d, 0x44, + 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, + 0x75, 0x6c, 0x74, 0x54, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64, 0x10, 0xdc, 0x9a, 0x80, + 0x80, 0x04, 0x12, 0x28, 0x0a, 0x20, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, + 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x48, 0x6f, 0x6c, 0x64, 0x10, 0xdd, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x2a, 0x0a, 0x22, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, + 0x61, 0x75, 0x6c, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x68, 0x75, 0x6e, 0x6b, + 0x65, 0x64, 0x10, 0xde, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x30, 0x0a, 0x28, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x4c, 0x65, + 0x6e, 0x67, 0x74, 0x68, 0x10, 0xdf, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x2b, 0x0a, 0x23, 0x44, 0x72, + 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, + 0x6c, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x65, + 0x64, 0x10, 0xe0, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x31, 0x0a, 0x29, 0x44, 0x72, 0x6f, 0x70, 0x5f, + 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x4c, 0x65, + 0x6e, 0x67, 0x74, 0x68, 0x10, 0xe1, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x34, 0x0a, 0x2c, 0x44, 0x72, + 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, + 0x6c, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, + 0x65, 0x72, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x10, 0xe2, 0x9a, 0x80, 0x80, 0x04, + 0x12, 0x28, 0x0a, 0x20, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, + 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x4c, 0x69, 0x6e, 0x65, 0x10, 0xe3, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x2a, 0x0a, 0x22, 0x44, 0x72, + 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, + 0x6c, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, + 0x10, 0xe4, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x23, 0x0a, 0x1b, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, + 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x10, 0xe5, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x26, 0x0a, 0x1e, 0x44, + 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, + 0x75, 0x6c, 0x74, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x53, 0x74, 0x61, 0x72, 0x74, 0x10, 0xe6, 0x9a, + 0x80, 0x80, 0x04, 0x12, 0x27, 0x0a, 0x1f, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, + 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x68, 0x75, 0x6e, 0x6b, + 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x10, 0xe7, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x25, 0x0a, 0x1d, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, + 0x61, 0x75, 0x6c, 0x74, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x53, 0x74, 0x6f, 0x70, 0x10, 0xe8, 0x9a, + 0x80, 0x80, 0x04, 0x12, 0x30, 0x0a, 0x28, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, + 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x48, 0x65, 0x61, 0x64, 0x65, + 0x72, 0x73, 0x41, 0x66, 0x74, 0x65, 0x72, 0x54, 0x72, 0x61, 0x69, 0x6c, 0x65, 0x72, 0x73, 0x10, + 0xe9, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x2b, 0x0a, 0x23, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, + 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x48, 0x65, 0x61, + 0x64, 0x65, 0x72, 0x73, 0x41, 0x66, 0x74, 0x65, 0x72, 0x45, 0x6e, 0x64, 0x10, 0xea, 0x9a, 0x80, + 0x80, 0x04, 0x12, 0x2a, 0x0a, 0x22, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, + 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x45, 0x6e, 0x64, 0x6c, 0x65, 0x73, + 0x73, 0x54, 0x72, 0x61, 0x69, 0x6c, 0x65, 0x72, 0x10, 0xeb, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x2c, + 0x0a, 0x24, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, + 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x45, 0x6e, + 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x10, 0xec, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x33, 0x0a, 0x2b, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, + 0x61, 0x75, 0x6c, 0x74, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x54, 0x72, 0x61, 0x6e, + 0x73, 0x66, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x10, 0xed, 0x9a, 0x80, 0x80, + 0x04, 0x12, 0x24, 0x0a, 0x1c, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, + 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x75, 0x73, 0x68, 0x42, 0x6f, 0x64, + 0x79, 0x10, 0xee, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x2b, 0x0a, 0x23, 0x44, 0x72, 0x6f, 0x70, 0x5f, + 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x53, + 0x74, 0x72, 0x65, 0x61, 0x6d, 0x41, 0x62, 0x61, 0x6e, 0x64, 0x6f, 0x6e, 0x65, 0x64, 0x10, 0xef, + 0x9a, 0x80, 0x80, 0x04, 0x12, 0x29, 0x0a, 0x21, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, + 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x4d, 0x61, 0x6c, 0x66, + 0x6f, 0x72, 0x6d, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x10, 0xf0, 0x9a, 0x80, 0x80, 0x04, 0x12, + 0x31, 0x0a, 0x29, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, + 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x44, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x4f, 0x76, 0x65, 0x72, 0x66, 0x6c, 0x6f, 0x77, 0x10, 0xf1, 0x9a, 0x80, + 0x80, 0x04, 0x12, 0x2d, 0x0a, 0x25, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, + 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x49, 0x6c, 0x6c, 0x65, 0x67, 0x61, + 0x6c, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x10, 0xf2, 0x9a, 0x80, 0x80, + 0x04, 0x12, 0x2e, 0x0a, 0x26, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, + 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x49, 0x6c, 0x6c, 0x65, 0x67, 0x61, 0x6c, + 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x10, 0xf3, 0x9a, 0x80, 0x80, + 0x04, 0x12, 0x30, 0x0a, 0x28, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, + 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x48, 0x65, 0x61, + 0x64, 0x65, 0x72, 0x44, 0x69, 0x73, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x10, 0xf4, 0x9a, + 0x80, 0x80, 0x04, 0x12, 0x2f, 0x0a, 0x27, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, + 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x48, + 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, 0x61, 0x6c, 0x66, 0x6f, 0x72, 0x6d, 0x65, 0x64, 0x10, 0xf5, + 0x9a, 0x80, 0x80, 0x04, 0x12, 0x2c, 0x0a, 0x24, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, + 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x6f, 0x6b, + 0x69, 0x65, 0x52, 0x65, 0x61, 0x73, 0x73, 0x65, 0x6d, 0x62, 0x6c, 0x79, 0x10, 0xf6, 0x9a, 0x80, + 0x80, 0x04, 0x12, 0x28, 0x0a, 0x20, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, + 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x10, 0xf7, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x2c, 0x0a, 0x24, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, + 0x61, 0x75, 0x6c, 0x74, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x44, 0x69, 0x73, 0x61, 0x6c, 0x6c, + 0x6f, 0x77, 0x65, 0x64, 0x10, 0xf8, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x2a, 0x0a, 0x22, 0x44, 0x72, + 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, + 0x6c, 0x74, 0x50, 0x61, 0x74, 0x68, 0x44, 0x69, 0x73, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, + 0x10, 0xf9, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x24, 0x0a, 0x1c, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, + 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x75, + 0x73, 0x68, 0x48, 0x6f, 0x73, 0x74, 0x10, 0xfa, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x2a, 0x0a, 0x22, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, + 0x61, 0x75, 0x6c, 0x74, 0x47, 0x6f, 0x61, 0x77, 0x61, 0x79, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, + 0x65, 0x64, 0x10, 0xfb, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x2a, 0x0a, 0x22, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, + 0x41, 0x62, 0x6f, 0x72, 0x74, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x41, 0x70, 0x70, 0x10, 0xfc, + 0x9a, 0x80, 0x80, 0x04, 0x12, 0x33, 0x0a, 0x2b, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, + 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x55, 0x70, 0x67, 0x72, + 0x61, 0x64, 0x65, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x44, 0x69, 0x73, 0x61, 0x6c, 0x6c, 0x6f, + 0x77, 0x65, 0x64, 0x10, 0xfd, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x31, 0x0a, 0x29, 0x44, 0x72, 0x6f, + 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, + 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x10, 0xfe, 0x9a, 0x80, 0x80, 0x04, 0x12, 0x35, 0x0a, 0x2d, + 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, + 0x61, 0x75, 0x6c, 0x74, 0x4b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, 0x48, 0x65, 0x61, + 0x64, 0x65, 0x72, 0x44, 0x69, 0x73, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x10, 0xff, 0x9a, + 0x80, 0x80, 0x04, 0x12, 0x33, 0x0a, 0x2b, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, + 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x4b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, 0x48, 0x65, 0x61, 0x64, + 0x65, 0x72, 0x10, 0x80, 0x9b, 0x80, 0x80, 0x04, 0x12, 0x35, 0x0a, 0x2d, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, + 0x50, 0x72, 0x6f, 0x78, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x44, + 0x69, 0x73, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x10, 0x81, 0x9b, 0x80, 0x80, 0x04, 0x12, + 0x33, 0x0a, 0x2b, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, + 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x50, + 0x72, 0x6f, 0x78, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x10, 0x82, + 0x9b, 0x80, 0x80, 0x04, 0x12, 0x2f, 0x0a, 0x27, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, + 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x6f, 0x69, 0x6e, 0x67, 0x41, 0x77, 0x61, 0x79, 0x10, + 0x83, 0x9b, 0x80, 0x80, 0x04, 0x12, 0x36, 0x0a, 0x2e, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, + 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x54, 0x72, 0x61, + 0x6e, 0x73, 0x66, 0x65, 0x72, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x44, 0x69, 0x73, + 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x10, 0x84, 0x9b, 0x80, 0x80, 0x04, 0x12, 0x33, 0x0a, + 0x2b, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, 0x78, 0x44, 0x75, 0x6f, + 0x46, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x4c, 0x65, 0x6e, 0x67, + 0x74, 0x68, 0x44, 0x69, 0x73, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x10, 0x85, 0x9b, 0x80, + 0x80, 0x04, 0x12, 0x2d, 0x0a, 0x25, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, + 0x55, 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x54, 0x72, 0x61, 0x69, 0x6c, 0x65, + 0x72, 0x44, 0x69, 0x73, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x10, 0x86, 0x9b, 0x80, 0x80, + 0x04, 0x12, 0x1f, 0x0a, 0x17, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x55, + 0x78, 0x44, 0x75, 0x6f, 0x46, 0x61, 0x75, 0x6c, 0x74, 0x45, 0x6e, 0x64, 0x10, 0x87, 0x9b, 0x80, + 0x80, 0x04, 0x12, 0x23, 0x0a, 0x1b, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, + 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x53, 0x75, 0x70, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, + 0x64, 0x10, 0x90, 0x9c, 0x80, 0x80, 0x04, 0x12, 0x19, 0x0a, 0x11, 0x44, 0x72, 0x6f, 0x70, 0x5f, + 0x48, 0x74, 0x74, 0x70, 0x5f, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x69, 0x63, 0x10, 0xd8, 0x9d, 0x80, + 0x80, 0x04, 0x12, 0x22, 0x0a, 0x1a, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, + 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, + 0x10, 0xd9, 0x9d, 0x80, 0x80, 0x04, 0x12, 0x27, 0x0a, 0x1f, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, + 0x74, 0x74, 0x70, 0x5f, 0x49, 0x6e, 0x73, 0x75, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x74, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x10, 0xda, 0x9d, 0x80, 0x80, 0x04, 0x12, + 0x1f, 0x0a, 0x17, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x49, 0x6e, 0x76, + 0x61, 0x6c, 0x69, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x10, 0xdb, 0x9d, 0x80, 0x80, 0x04, + 0x12, 0x1e, 0x0a, 0x16, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x4e, 0x6f, + 0x74, 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x10, 0xdc, 0x9d, 0x80, 0x80, 0x04, + 0x12, 0x20, 0x0a, 0x18, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x42, 0x61, + 0x64, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x50, 0x61, 0x74, 0x68, 0x10, 0xdd, 0x9d, 0x80, + 0x80, 0x04, 0x12, 0x1f, 0x0a, 0x17, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, + 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x10, 0xde, 0x9d, + 0x80, 0x80, 0x04, 0x12, 0x1f, 0x0a, 0x17, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, + 0x5f, 0x4e, 0x6f, 0x53, 0x75, 0x63, 0x68, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x10, 0xdf, + 0x9d, 0x80, 0x80, 0x04, 0x12, 0x22, 0x0a, 0x1a, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, + 0x70, 0x5f, 0x50, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x4e, 0x6f, 0x74, 0x48, 0x65, + 0x6c, 0x64, 0x10, 0xe0, 0x9d, 0x80, 0x80, 0x04, 0x12, 0x23, 0x0a, 0x1b, 0x44, 0x72, 0x6f, 0x70, + 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x43, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x49, 0x6d, 0x70, 0x65, + 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x74, 0x65, 0x10, 0xe1, 0x9d, 0x80, 0x80, 0x04, 0x12, 0x1e, 0x0a, + 0x16, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x4c, 0x6f, 0x67, 0x6f, 0x6e, + 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x10, 0xe2, 0x9d, 0x80, 0x80, 0x04, 0x12, 0x24, 0x0a, + 0x1c, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x4e, 0x6f, 0x53, 0x75, 0x63, + 0x68, 0x4c, 0x6f, 0x67, 0x6f, 0x6e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x10, 0xe3, 0x9d, + 0x80, 0x80, 0x04, 0x12, 0x1e, 0x0a, 0x16, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, + 0x5f, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x44, 0x65, 0x6e, 0x69, 0x65, 0x64, 0x10, 0xe4, 0x9d, + 0x80, 0x80, 0x04, 0x12, 0x20, 0x0a, 0x18, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, + 0x5f, 0x4e, 0x6f, 0x4c, 0x6f, 0x67, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x10, + 0xe5, 0x9d, 0x80, 0x80, 0x04, 0x12, 0x24, 0x0a, 0x1c, 0x44, 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, + 0x74, 0x70, 0x5f, 0x54, 0x69, 0x6d, 0x65, 0x44, 0x69, 0x66, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, + 0x65, 0x41, 0x74, 0x44, 0x63, 0x10, 0xe6, 0x9d, 0x80, 0x80, 0x04, 0x12, 0x15, 0x0a, 0x0d, 0x44, + 0x72, 0x6f, 0x70, 0x5f, 0x48, 0x74, 0x74, 0x70, 0x5f, 0x45, 0x6e, 0x64, 0x10, 0xa0, 0x9f, 0x80, + 0x80, 0x04, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0x2f, 0x72, 0x65, 0x74, 0x69, 0x6e, + 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x75, 0x74, 0x69, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, +} var ( - file_metadata_windows_proto_rawDescOnce sync.Once - file_metadata_windows_proto_rawDescData []byte + file_pkg_utils_metadata_windows_proto_rawDescOnce sync.Once + file_pkg_utils_metadata_windows_proto_rawDescData = file_pkg_utils_metadata_windows_proto_rawDesc ) -func file_metadata_windows_proto_rawDescGZIP() []byte { - file_metadata_windows_proto_rawDescOnce.Do(func() { - file_metadata_windows_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_metadata_windows_proto_rawDesc), len(file_metadata_windows_proto_rawDesc))) +func file_pkg_utils_metadata_windows_proto_rawDescGZIP() []byte { + file_pkg_utils_metadata_windows_proto_rawDescOnce.Do(func() { + file_pkg_utils_metadata_windows_proto_rawDescData = protoimpl.X.CompressGZIP(file_pkg_utils_metadata_windows_proto_rawDescData) }) - return file_metadata_windows_proto_rawDescData + return file_pkg_utils_metadata_windows_proto_rawDescData } -var file_metadata_windows_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_metadata_windows_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_metadata_windows_proto_goTypes = []any{ +var file_pkg_utils_metadata_windows_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_pkg_utils_metadata_windows_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_pkg_utils_metadata_windows_proto_goTypes = []any{ (DNSType)(0), // 0: utils.DNSType (DropReason)(0), // 1: utils.DropReason (*RetinaMetadata)(nil), // 2: utils.RetinaMetadata + nil, // 3: utils.RetinaMetadata.PreviouslyObservedTcpFlagsEntry } -var file_metadata_windows_proto_depIdxs = []int32{ +var file_pkg_utils_metadata_windows_proto_depIdxs = []int32{ 0, // 0: utils.RetinaMetadata.dns_type:type_name -> utils.DNSType 1, // 1: utils.RetinaMetadata.drop_reason:type_name -> utils.DropReason - 2, // [2:2] is the sub-list for method output_type - 2, // [2:2] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 3, // 2: utils.RetinaMetadata.previously_observed_tcp_flags:type_name -> utils.RetinaMetadata.PreviouslyObservedTcpFlagsEntry + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } -func init() { file_metadata_windows_proto_init() } -func file_metadata_windows_proto_init() { - if File_metadata_windows_proto != nil { +func init() { file_pkg_utils_metadata_windows_proto_init() } +func file_pkg_utils_metadata_windows_proto_init() { + if File_pkg_utils_metadata_windows_proto != nil { return } + if !protoimpl.UnsafeEnabled { + file_pkg_utils_metadata_windows_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*RetinaMetadata); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_metadata_windows_proto_rawDesc), len(file_metadata_windows_proto_rawDesc)), + RawDescriptor: file_pkg_utils_metadata_windows_proto_rawDesc, NumEnums: 2, - NumMessages: 1, + NumMessages: 2, NumExtensions: 0, NumServices: 0, }, - GoTypes: file_metadata_windows_proto_goTypes, - DependencyIndexes: file_metadata_windows_proto_depIdxs, - EnumInfos: file_metadata_windows_proto_enumTypes, - MessageInfos: file_metadata_windows_proto_msgTypes, + GoTypes: file_pkg_utils_metadata_windows_proto_goTypes, + DependencyIndexes: file_pkg_utils_metadata_windows_proto_depIdxs, + EnumInfos: file_pkg_utils_metadata_windows_proto_enumTypes, + MessageInfos: file_pkg_utils_metadata_windows_proto_msgTypes, }.Build() - File_metadata_windows_proto = out.File - file_metadata_windows_proto_goTypes = nil - file_metadata_windows_proto_depIdxs = nil + File_pkg_utils_metadata_windows_proto = out.File + file_pkg_utils_metadata_windows_proto_rawDesc = nil + file_pkg_utils_metadata_windows_proto_goTypes = nil + file_pkg_utils_metadata_windows_proto_depIdxs = nil } diff --git a/pkg/utils/metadata_windows.proto b/pkg/utils/metadata_windows.proto index 10aa93ae26..9c7a730827 100644 --- a/pkg/utils/metadata_windows.proto +++ b/pkg/utils/metadata_windows.proto @@ -15,6 +15,11 @@ message RetinaMetadata { // Drop reason in Retina. DropReason drop_reason = 5; + + // Sampling metadata, for packetparser. + uint32 previously_observed_packets = 6; + uint32 previously_observed_bytes = 7; + map previously_observed_tcp_flags = 8; } enum DNSType { diff --git a/pkg/utils/metric_names.go b/pkg/utils/metric_names.go index 8060b05243..5bb7f280d3 100644 --- a/pkg/utils/metric_names.go +++ b/pkg/utils/metric_names.go @@ -34,6 +34,13 @@ const ( // Common Gauges across os distributions NodeConnectivityStatusName = "node_connectivity_status" NodeConnectivityLatencySecondsName = "node_connectivity_latency_seconds" + + // Conntrack + ConntrackPacketsTxGaugeName = "conntrack_packets_tx" + ConntrackPacketsRxGaugeName = "conntrack_packets_rx" + ConntrackBytesTxGaugeName = "conntrack_bytes_tx" + ConntrackBytesRxGaugeName = "conntrack_bytes_rx" + ConntrackTotalConnectionsName = "conntrack_total_connections" ) // IsAdvancedMetric is a helper function to determine if a name is an advanced metric diff --git a/pkg/utils/testutil/cilium/endpoint_client.go b/pkg/utils/testutil/cilium/endpoint_client.go index dc57a5e240..df5d961620 100644 --- a/pkg/utils/testutil/cilium/endpoint_client.go +++ b/pkg/utils/testutil/cilium/endpoint_client.go @@ -6,8 +6,7 @@ import ( "context" "encoding/json" "fmt" - - "github.com/sirupsen/logrus" + "log/slog" v2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" ciliumv2 "github.com/cilium/cilium/pkg/k8s/client/clientset/versioned/typed/cilium.io/v2" @@ -21,13 +20,15 @@ import ( var _ ciliumv2.CiliumEndpointInterface = &MockEndpointClient{} type MockEndpointClient struct { - l logrus.FieldLogger + l *slog.Logger namespace string ciliumEndpoints *MockResource[*v2.CiliumEndpoint] watchers []watch.Interface } -func NewMockEndpointClient(l logrus.FieldLogger, namespace string, ciliumEndpoints *MockResource[*v2.CiliumEndpoint]) *MockEndpointClient { +func NewMockEndpointClient( + l *slog.Logger, namespace string, ciliumEndpoints *MockResource[*v2.CiliumEndpoint], +) *MockEndpointClient { return &MockEndpointClient{ l: l, namespace: namespace, diff --git a/pkg/utils/testutil/cilium/identity_client.go b/pkg/utils/testutil/cilium/identity_client.go index 625c990c81..b0aa3bc45d 100644 --- a/pkg/utils/testutil/cilium/identity_client.go +++ b/pkg/utils/testutil/cilium/identity_client.go @@ -4,13 +4,12 @@ package ciliumutil import ( "context" + "log/slog" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/watch" - "github.com/sirupsen/logrus" - v2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" ciliumv2 "github.com/cilium/cilium/pkg/k8s/client/clientset/versioned/typed/cilium.io/v2" ) @@ -23,14 +22,14 @@ var _ ciliumv2.CiliumIdentityInterface = &MockIdentityClient{} // - CRDBackend within the Allocator within the IdentityManager // - identitygc cell type MockIdentityClient struct { - l logrus.FieldLogger + l *slog.Logger // identities maps identity name to identity // namespace is irrelevant since identity names must be globally unique numbers identities map[string]*v2.CiliumIdentity watchers []watch.Interface } -func NewMockIdentityClient(l logrus.FieldLogger) *MockIdentityClient { +func NewMockIdentityClient(l *slog.Logger) *MockIdentityClient { return &MockIdentityClient{ l: l, identities: make(map[string]*v2.CiliumIdentity), diff --git a/pkg/utils/testutil/cilium/resource.go b/pkg/utils/testutil/cilium/resource.go index fa2e4d3a08..f56d8bc815 100644 --- a/pkg/utils/testutil/cilium/resource.go +++ b/pkg/utils/testutil/cilium/resource.go @@ -4,9 +4,9 @@ package ciliumutil import ( "context" + "log/slog" "github.com/pkg/errors" - "github.com/sirupsen/logrus" k8sRuntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/cache" @@ -28,12 +28,12 @@ var ( // i.e. Store() and GetByKey() // plus some helpers to add/remove items from the cache and error on the next call to Store() type MockResource[T k8sRuntime.Object] struct { - l logrus.FieldLogger + l *slog.Logger cache map[resource.Key]T shouldFailNextStoreCall bool } -func NewMockResource[T k8sRuntime.Object](l logrus.FieldLogger) *MockResource[T] { +func NewMockResource[T k8sRuntime.Object](l *slog.Logger) *MockResource[T] { return &MockResource[T]{ l: l, cache: make(map[resource.Key]T), diff --git a/pkg/utils/testutil/cilium/versioned_client.go b/pkg/utils/testutil/cilium/versioned_client.go index 7e29c0e92b..40bf3a49c1 100644 --- a/pkg/utils/testutil/cilium/versioned_client.go +++ b/pkg/utils/testutil/cilium/versioned_client.go @@ -3,7 +3,7 @@ package ciliumutil import ( - "github.com/sirupsen/logrus" + "log/slog" "k8s.io/client-go/rest" @@ -23,11 +23,11 @@ var ( // MockVersionedClient is a mock implementation of versioned.Interface // Currently it only returns a real value for CiliumV2() type MockVersionedClient struct { - l logrus.FieldLogger + l *slog.Logger c *MockCiliumV2Client } -func NewMockVersionedClient(l logrus.FieldLogger, ciliumEndpoints *MockResource[*v2.CiliumEndpoint]) *MockVersionedClient { +func NewMockVersionedClient(l *slog.Logger, ciliumEndpoints *MockResource[*v2.CiliumEndpoint]) *MockVersionedClient { return &MockVersionedClient{ l: l, c: NewMockCiliumV2Client(l, ciliumEndpoints), @@ -52,12 +52,12 @@ func (m *MockVersionedClient) CiliumV2alpha1() ciliumv2alpha1.CiliumV2alpha1Inte // MockCiliumV2Client is a mock implementation of ciliumv2.CiliumV2Interface. // Currently it only returns a real value for CiliumIdentities() type MockCiliumV2Client struct { - l logrus.FieldLogger + l *slog.Logger identitiyClient *MockIdentityClient ciliumEndpoints *MockResource[*v2.CiliumEndpoint] } -func NewMockCiliumV2Client(l logrus.FieldLogger, ciliumEndpoints *MockResource[*v2.CiliumEndpoint]) *MockCiliumV2Client { +func NewMockCiliumV2Client(l *slog.Logger, ciliumEndpoints *MockResource[*v2.CiliumEndpoint]) *MockCiliumV2Client { return &MockCiliumV2Client{ l: l, identitiyClient: NewMockIdentityClient(l), @@ -144,3 +144,13 @@ func (m *MockCiliumV2Client) CiliumBGPPeerConfigs() ciliumv2.CiliumBGPPeerConfig m.l.Warn("MockCiliumV2Client.CiliumNetworkPoliciesForCRD() called but this returns nil because it's not implemented") return nil } + +func (m *MockCiliumV2Client) CiliumCIDRGroups() ciliumv2.CiliumCIDRGroupInterface { + m.l.Warn("MockCiliumV2Client.CiliumCIDRGroups() called but this returns nil because it's not implemented") + return nil +} + +func (m *MockCiliumV2Client) CiliumLoadBalancerIPPools() ciliumv2.CiliumLoadBalancerIPPoolInterface { + m.l.Warn("MockCiliumV2Client.CiliumLoadBalancerIPPools() called but this returns nil because it's not implemented") + return nil +} diff --git a/pkg/utils/utils_darwin.go b/pkg/utils/utils_darwin.go new file mode 100644 index 0000000000..f4dd60ae27 --- /dev/null +++ b/pkg/utils/utils_darwin.go @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package utils + +import ( + "github.com/cilium/cilium/api/v1/flow" +) + +func GetDropReasonDesc(dr DropReason) flow.DropReason { + // Keep mapping aligned with Linux where drop reasons overlap. + switch dr { //nolint:exhaustive // We are handling all the cases. + case DropReason_IPTABLE_RULE_DROP: + return flow.DropReason_POLICY_DENIED + case DropReason_IPTABLE_NAT_DROP: + return flow.DropReason_SNAT_NO_MAP_FOUND + case DropReason_CONNTRACK_ADD_DROP: + return flow.DropReason_UNKNOWN_CONNECTION_TRACKING_STATE + default: + return flow.DropReason_DROP_REASON_UNKNOWN + } +} diff --git a/pkg/utils/utils_linux.go b/pkg/utils/utils_linux.go index 6aff0d5d2b..3bdd288fbe 100644 --- a/pkg/utils/utils_linux.go +++ b/pkg/utils/utils_linux.go @@ -10,8 +10,9 @@ import ( "syscall" "unsafe" + "errors" + "github.com/cilium/cilium/api/v1/flow" - "github.com/pkg/errors" "github.com/vishvananda/netlink" "golang.org/x/exp/maps" ) @@ -115,6 +116,21 @@ func isDefaultRoute(route netlink.Route) bool { return false } +// GetDefaultIfaceIndex returns the ifindex of the default route interface. +// Returns 0 (all interfaces) if the default route cannot be determined. +var ErrNoDefaultLink = errors.New("no default outgoing link found") + +func GetDefaultIfaceIndex() (int, error) { + links, err := GetDefaultOutgoingLinks() + if err != nil { + return 0, fmt.Errorf("failed to get default outgoing links: %w", err) + } + if len(links) == 0 { + return 0, ErrNoDefaultLink + } + return links[0].Attrs().Index, nil +} + func GetDropReasonDesc(dr DropReason) flow.DropReason { // Set the drop reason. // Retina drop reasons are different from the drop reasons available in flow library. diff --git a/pkg/utils/utils_linux_test.go b/pkg/utils/utils_linux_test.go index b672033496..1fecf2f7a4 100644 --- a/pkg/utils/utils_linux_test.go +++ b/pkg/utils/utils_linux_test.go @@ -47,7 +47,6 @@ func TestToFlow(t *testing.T) { assert.EqualValues(t, f.GetL4().Protocol.(*flow.Layer4_TCP).TCP.SourcePort, uint32(443)) assert.EqualValues(t, f.GetL4().Protocol.(*flow.Layer4_TCP).TCP.DestinationPort, uint32(80)) assert.NotNil(t, f.Time) - assert.NotNil(t, f.Extensions) assert.Equal(t, f.Type, flow.FlowType_L3_L4) if !f.GetTime().IsValid() { @@ -87,9 +86,9 @@ func TestAddPacketSize(t *testing.T) { uint8(1), flow.Verdict_FORWARDED, ) - meta := &RetinaMetadata{} - AddPacketSize(meta, uint32(100)) - AddRetinaMetadata(fl, meta) + ext := NewExtensions() + AddPacketSize(ext, uint32(100)) + SetExtensions(fl, ext) res := PacketSize(fl) assert.EqualValues(t, res, uint32(100)) @@ -111,9 +110,9 @@ func TestTcpID(t *testing.T) { flow.Verdict_FORWARDED, ) - meta := &RetinaMetadata{} - AddTCPID(meta, uint64(1234)) - AddRetinaMetadata(fl, meta) + ext := NewExtensions() + AddTCPID(ext, uint64(1234)) + SetExtensions(fl, ext) assert.EqualValues(t, GetTCPID(fl), uint64(1234)) } @@ -154,9 +153,9 @@ func TestAddDropReason(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { f := &flow.Flow{} - meta := &RetinaMetadata{} - AddDropReason(f, meta, tc.dropReason) - AddRetinaMetadata(f, meta) + ext := NewExtensions() + AddDropReason(f, ext, tc.dropReason) + SetExtensions(f, ext) assert.Equal(t, f.DropReasonDesc, tc.expectedDesc) assert.Equal(t, f.Verdict, flow.Verdict_DROPPED) assert.NotNil(t, f.EventType.Type, 1) @@ -166,6 +165,54 @@ func TestAddDropReason(t *testing.T) { } } +func TestZoneHelpers(t *testing.T) { + tests := []struct { + name string + srcZone string + dstZone string + expectedSrcZone string + expectedDstZone string + }{ + { + name: "both zones set", + srcZone: "zone-1", + dstZone: "zone-2", + expectedSrcZone: "zone-1", + expectedDstZone: "zone-2", + }, + { + name: "empty zones stored as empty strings", + srcZone: "", + dstZone: "", + expectedSrcZone: "", + expectedDstZone: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + f := &flow.Flow{} + ext := NewExtensions() + AddZones(ext, tc.srcZone, tc.dstZone) + SetExtensions(f, ext) + + assert.Equal(t, tc.expectedSrcZone, SourceZone(f)) + assert.Equal(t, tc.expectedDstZone, DestinationZone(f)) + }) + } +} + +func TestZoneHelpers_NilExtensions(t *testing.T) { + f := &flow.Flow{} + assert.Equal(t, "unknown", SourceZone(f)) + assert.Equal(t, "unknown", DestinationZone(f)) +} + +func TestAddZones_NilStruct(t *testing.T) { + // Should not panic. + AddZones(nil, "zone-1", "zone-2") +} + func TestIsDefaultRoute(t *testing.T) { tests := []struct { Route netlink.Route diff --git a/pkg/watchers/apiserver/apiserver.go b/pkg/watchers/apiserver/apiserver.go index 606c2cde74..d21a3f5c41 100644 --- a/pkg/watchers/apiserver/apiserver.go +++ b/pkg/watchers/apiserver/apiserver.go @@ -17,7 +17,11 @@ import ( "github.com/microsoft/retina/pkg/pubsub" "github.com/microsoft/retina/pkg/utils" "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" + discoveryv1 "k8s.io/api/discovery/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/rest" + kclient "sigs.k8s.io/controller-runtime/pkg/client" kcfg "sigs.k8s.io/controller-runtime/pkg/client/config" ) @@ -27,26 +31,29 @@ const ( ) type ApiServerWatcher struct { - isRunning bool - l *log.ZapLogger - current cache - new cache - apiServerHostName string - hostResolver IHostResolver - filterManager fm.IFilterManager - restConfig *rest.Config + isRunning bool + l *log.ZapLogger + current cache + new cache + apiServerHostName string + hostResolver IHostResolver + filterManager fm.IFilterManager + restConfig *rest.Config + client kclient.Client + filterMapMaxEntries uint32 } var a *ApiServerWatcher // Watcher creates a new ApiServerWatcher instance. -func Watcher() *ApiServerWatcher { +func Watcher(filterMapMaxEntries uint32) *ApiServerWatcher { if a == nil { a = &ApiServerWatcher{ - isRunning: false, - l: log.Logger().Named("apiserver-watcher"), - current: make(cache), - hostResolver: net.DefaultResolver, + isRunning: false, + l: log.Logger().Named("apiserver-watcher"), + current: make(cache), + hostResolver: net.DefaultResolver, + filterMapMaxEntries: filterMapMaxEntries, } } @@ -62,7 +69,7 @@ func (a *ApiServerWatcher) Init(ctx context.Context) error { // Get filter manager. if a.filterManager == nil { var err error - a.filterManager, err = fm.Init(filterManagerRetries) + a.filterManager, err = fm.Init(filterManagerRetries, a.filterMapMaxEntries) if err != nil { a.l.Error("failed to init filter manager", zap.Error(err)) return fmt.Errorf("failed to init filter manager: %w", err) @@ -79,6 +86,15 @@ func (a *ApiServerWatcher) Init(ctx context.Context) error { a.restConfig = config } + if a.client == nil { + c, err := kclient.New(a.restConfig, kclient.Options{}) + if err != nil { + a.l.Error("failed to create kubernetes client", zap.Error(err)) + return fmt.Errorf("failed to create kubernetes client: %w", err) + } + a.client = c + } + hostName, err := a.getHostName() if err != nil { a.l.Error("failed to get host name", zap.Error(err)) @@ -149,11 +165,24 @@ func (a *ApiServerWatcher) Refresh(ctx context.Context) error { } func (a *ApiServerWatcher) initNewCache(ctx context.Context) error { + svcIPs, err := a.ipsFromService(ctx) + if err != nil { + return fmt.Errorf("failed to retrieve ips from kubernetes service: %w", err) + } + + endpointIPs, err := a.ipsFromEndpointSlice(ctx) + if err != nil { + return fmt.Errorf("failed to retrieve ips from kubernetes endpointslices: %w", err) + } + ips, err := a.resolveIPs(ctx, a.apiServerHostName) if err != nil { return fmt.Errorf("failed to resolve IPs: %w", err) } + ips = append(ips, endpointIPs...) + ips = append(ips, svcIPs...) + // Reset new cache. a.new = make(cache) for _, ip := range ips { @@ -210,6 +239,41 @@ func (a *ApiServerWatcher) resolveIPs(ctx context.Context, host string) ([]strin return hostIPs, nil } +// ipsFromService retrieves IP addresses from the master service "kubernetes" in the default namespace. +// These IPs are used as a virtual-ip to the kube-apiserver. +func (a *ApiServerWatcher) ipsFromService(ctx context.Context) ([]string, error) { + svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kubernetes", + Namespace: "default", + }, + } + if err := a.client.Get(ctx, kclient.ObjectKeyFromObject(svc), svc); err != nil { + return nil, fmt.Errorf("retrieving kubernetes service: %w", err) + } + return svc.Spec.ClusterIPs, nil +} + +// ipsFromEndpointSlice retrieves IP addresses from the EndpointSlices that +// back the "kubernetes" service in the default namespace. These IPs are the +// addresses for the kube-apiserver. +func (a *ApiServerWatcher) ipsFromEndpointSlice(ctx context.Context) ([]string, error) { + var sliceList discoveryv1.EndpointSliceList + if err := a.client.List(ctx, &sliceList, + kclient.InNamespace("default"), + kclient.MatchingLabels{discoveryv1.LabelServiceName: "kubernetes"}, + ); err != nil { + return nil, fmt.Errorf("retrieving kubernetes endpointslices: %w", err) + } + ips := []string{} + for i := range sliceList.Items { + for _, ep := range sliceList.Items[i].Endpoints { + ips = append(ips, ep.Addresses...) + } + } + return ips, nil +} + func (a *ApiServerWatcher) publish(netIPs []net.IP, eventType cc.EventType) { if len(netIPs) == 0 { return diff --git a/pkg/watchers/apiserver/apiserver_test.go b/pkg/watchers/apiserver/apiserver_test.go index 04105e85c1..988e0641b4 100644 --- a/pkg/watchers/apiserver/apiserver_test.go +++ b/pkg/watchers/apiserver/apiserver_test.go @@ -12,13 +12,20 @@ import ( "testing" "time" + kcfg "github.com/microsoft/retina/pkg/config" "github.com/microsoft/retina/pkg/log" filtermanagermocks "github.com/microsoft/retina/pkg/managers/filtermanager" "github.com/microsoft/retina/pkg/watchers/apiserver/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + + corev1 "k8s.io/api/core/v1" + discoveryv1 "k8s.io/api/discovery/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" ) var errDNS = errors.New("DNS error") @@ -26,11 +33,11 @@ var errDNS = errors.New("DNS error") func TestGetWatcher(t *testing.T) { log.SetupZapLogger(log.GetDefaultLogOpts()) - a := Watcher() + a := Watcher(kcfg.DefaultFilterMapMaxEntries) assert.NotNil(t, a) - a_again := Watcher() - assert.Equal(t, a, a_again, "Expected the same veth watcher instance") + aAgain := Watcher(kcfg.DefaultFilterMapMaxEntries) + assert.Equal(t, a, aAgain, "Expected the same veth watcher instance") } func TestAPIServerWatcherStop(t *testing.T) { @@ -78,6 +85,7 @@ func TestRefresh(t *testing.T) { l: log.Logger().Named("apiserver-watcher"), hostResolver: mockedResolver, filterManager: mockedFilterManager, + client: getMockKubeClient(), } // Return 2 random IPs for the host everytime LookupHost is called. @@ -132,6 +140,7 @@ func TestRefreshLookUpAlwaysFail(t *testing.T) { a := &ApiServerWatcher{ l: log.Logger().Named("apiserver-watcher"), hostResolver: mockedResolver, + client: getMockKubeClient(), } mockedResolver.EXPECT().LookupHost(gomock.Any(), gomock.Any()).Return(nil, errors.New("Error")).AnyTimes() @@ -155,6 +164,7 @@ func TestInitWithIncorrectURL(t *testing.T) { l: log.Logger().Named("apiserver-watcher"), hostResolver: mockedResolver, restConfig: getMockConfig(false), + client: getMockKubeClient(), filterManager: mockedFilterManager, } @@ -178,6 +188,34 @@ func getMockConfig(isCorrect bool) *rest.Config { } } +func getMockKubeClient() client.Client { + kubernetesSvc := corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kubernetes", + Namespace: "default", + }, + Spec: corev1.ServiceSpec{ + ClusterIPs: []string{"172.0.16.1"}, + }, + } + + slice := discoveryv1.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kubernetes", + Namespace: "default", + Labels: map[string]string{ + discoveryv1.LabelServiceName: "kubernetes", + }, + }, + AddressType: discoveryv1.AddressTypeIPv4, + Endpoints: []discoveryv1.Endpoint{ + {Addresses: []string{"100.64.83.200"}}, + {Addresses: []string{"100.64.83.201"}}, + }, + } + return fake.NewFakeClient(&slice, &kubernetesSvc) +} + func TestRefreshFailsFirstFourAttemptsSucceedsOnFifth(t *testing.T) { _, err := log.SetupZapLogger(log.GetDefaultLogOpts()) require.NoError(t, err) @@ -194,6 +232,7 @@ func TestRefreshFailsFirstFourAttemptsSucceedsOnFifth(t *testing.T) { l: log.Logger().Named("apiserver-watcher"), hostResolver: mockedResolver, filterManager: mockedFilterManager, + client: getMockKubeClient(), } // Simulate LookupHost failing the first four times and succeeding on the fifth. diff --git a/samples/capture/capture-specific-pod-on-a-node.yaml b/samples/capture/capture-specific-pod-on-a-node.yaml index d404e0ebfe..bcb697adcb 100644 --- a/samples/capture/capture-specific-pod-on-a-node.yaml +++ b/samples/capture/capture-specific-pod-on-a-node.yaml @@ -25,5 +25,5 @@ spec: outputConfiguration: # the artifact will be copied to hostpath # and uploaded to azure storage account - hostPath: "/tmp/retina" + hostPath: "retina" blobUpload: blobsassecret diff --git a/samples/capture/node-s3upload-aws.yaml b/samples/capture/node-s3upload-aws.yaml index 9714249200..1a5bdd91b3 100644 --- a/samples/capture/node-s3upload-aws.yaml +++ b/samples/capture/node-s3upload-aws.yaml @@ -19,7 +19,7 @@ spec: ], } outputConfiguration: - hostPath: "/tmp/retina" + hostPath: "retina" s3Upload: bucket: retina-bucket region: ap-northeast-2 diff --git a/samples/capture/node-s3upload-minio.yaml b/samples/capture/node-s3upload-minio.yaml index a6199960f3..f533220951 100644 --- a/samples/capture/node-s3upload-minio.yaml +++ b/samples/capture/node-s3upload-minio.yaml @@ -19,7 +19,7 @@ spec: ], } outputConfiguration: - hostPath: "/tmp/retina" + hostPath: "retina" s3Upload: bucket: retina-bucket endpoint: https://play.min.io:9000 diff --git a/samples/capture/nodeblobupload.yaml b/samples/capture/nodeblobupload.yaml index 80cae35c01..1b8e26d615 100644 --- a/samples/capture/nodeblobupload.yaml +++ b/samples/capture/nodeblobupload.yaml @@ -19,5 +19,5 @@ spec: ], } outputConfiguration: - hostPath: "/tmp/retina" + hostPath: "retina" blobUpload: blobsassecret diff --git a/samples/capture/podblobupload.yaml b/samples/capture/podblobupload.yaml index 88260f86c4..97e3a6141e 100644 --- a/samples/capture/podblobupload.yaml +++ b/samples/capture/podblobupload.yaml @@ -16,5 +16,5 @@ spec: outputConfiguration: # the artifact will be copied to hostpath # and uploaded to azure storage account - hostPath: "/tmp/retina" + hostPath: "retina" blobUpload: blobsassecret diff --git a/scripts/bump-images.sh b/scripts/bump-images.sh new file mode 100755 index 0000000000..7e79753b31 --- /dev/null +++ b/scripts/bump-images.sh @@ -0,0 +1,191 @@ +#!/usr/bin/env bash +# bump-images.sh — update all Dockerfile base image digests. +# +# Parses "# skopeo inspect docker://IMAGE:TAG ..." comments in Dockerfiles +# and replaces the sha256 digest on the following FROM line with the latest +# digest from the registry. Supports --override-os windows for Windows images. +# +# For Go builder images (golang), also checks for newer minor/patch versions +# and prompts the user to select a version before updating digests. +# +# Usage: make bump-images +# Requires: skopeo, curl, python3 + +set -euo pipefail + +if ! command -v skopeo &>/dev/null; then + echo "error: skopeo is required but not installed" >&2 + exit 1 +fi + +REPO_ROOT="$(git rev-parse --show-toplevel)" +DOCKERFILES=$(find "$REPO_ROOT" -name "Dockerfile*" -not -path "*/.git/*" -not -path "*/vendor/*") + +# --- Go version bump detection --- +# Scan Dockerfiles for Go builder images and check for newer versions. + +go_image_repo="" +current_go_version="" + +for dockerfile in $DOCKERFILES; do + if match=$(grep -oP 'docker://\S+/golang:\K[0-9]+\.[0-9]+\.[0-9]+' "$dockerfile" | head -1); then + if [[ -n "$match" ]]; then + current_go_version="$match" + go_image_repo=$(grep -oP 'docker://\K\S+/golang(?=:)' "$dockerfile" | head -1) + break + fi + fi +done + +if [[ -n "$current_go_version" && -n "$go_image_repo" ]]; then + echo "Current Go builder version: $current_go_version ($go_image_repo)" + + # Parse current version components + IFS='.' read -r cur_major cur_minor cur_patch <<< "$current_go_version" + + # Fetch available tags from MCR API + # Convert image repo to registry API path (mcr.microsoft.com/foo/bar → /v2/foo/bar/tags/list) + registry_host=$(echo "$go_image_repo" | cut -d/ -f1) + repo_path=$(echo "$go_image_repo" | cut -d/ -f2-) + tags_json=$(curl -s "https://${registry_host}/v2/${repo_path}/tags/list" 2>/dev/null || true) + + if [[ -n "$tags_json" ]]; then + # Extract unique Go versions (MAJOR.MINOR.PATCH only, no suffix) that are newer + newer_versions=$(echo "$tags_json" | python3 -c " +import sys, json + +data = json.load(sys.stdin) +tags = data.get('tags', []) + +cur_major, cur_minor, cur_patch = ${cur_major}, ${cur_minor}, ${cur_patch} +seen = set() +results = [] + +for tag in tags: + # Match tags that are exactly MAJOR.MINOR.PATCH (no suffix) + parts = tag.split('.') + if len(parts) != 3: + continue + try: + major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2]) + except ValueError: + continue + + version = (major, minor, patch) + if version in seen: + continue + seen.add(version) + + # Only include versions newer than current, same major + if major != cur_major: + continue + if (minor, patch) <= (cur_minor, cur_patch): + continue + results.append(version) + +for v in sorted(results): + kind = 'minor' if v[1] > cur_minor else 'patch' + print(f'{v[0]}.{v[1]}.{v[2]} ({kind})') +" 2>/dev/null || true) + + if [[ -n "$newer_versions" ]]; then + echo "" + echo "Newer Go versions available:" + i=1 + versions_array=() + while IFS= read -r line; do + echo " $i) $line" + # Extract just the version number (before the space) + versions_array+=("${line%% *}") + ((i++)) + done <<< "$newer_versions" + echo " 0) Keep current ($current_go_version)" + echo "" + + if [[ -t 0 ]]; then + read -r -p "Select version to bump to [0]: " choice + else + echo "Non-interactive mode, keeping current version." + choice=0 + fi + choice="${choice:-0}" + + if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#versions_array[@]} )); then + new_go_version="${versions_array[$((choice - 1))]}" + echo "" + echo "Bumping Go version: $current_go_version → $new_go_version" + + # Update all Dockerfiles: replace old Go version with new in golang image tags + for dockerfile in $DOCKERFILES; do + # Update skopeo comment lines and FROM lines referencing the golang image + if grep -q "golang:${current_go_version}" "$dockerfile"; then + sed -i "s|golang:${current_go_version}|golang:${new_go_version}|g" "$dockerfile" + rel_path="${dockerfile#$REPO_ROOT/}" + echo " GO $rel_path: ${current_go_version} → ${new_go_version}" + fi + done + echo "" + else + echo "Keeping Go version at $current_go_version." + echo "" + fi + else + echo "No newer Go versions found." + echo "" + fi + fi +fi + +# --- Digest bump loop --- + +updated=0 +skipped=0 + +for dockerfile in $DOCKERFILES; do + # Find all skopeo comment lines + while IFS= read -r line_num; do + # Extract the skopeo command from the comment + comment=$(sed -n "${line_num}p" "$dockerfile") + + # Parse image reference from: # skopeo inspect docker://IMAGE:TAG [--override-os windows] ... + image_ref=$(echo "$comment" | grep -oP 'docker://\S+' | sed 's/docker:\/\///') + override_os=$(echo "$comment" | grep -oP '\-\-override-os \S+' || true) + + if [[ -z "$image_ref" ]]; then + continue + fi + + # Get current digest from the FROM line (next non-empty line after comment) + from_line=$((line_num + 1)) + current=$(sed -n "${from_line}p" "$dockerfile" | grep -oP 'sha256:[a-f0-9]+' || true) + + if [[ -z "$current" ]]; then + continue + fi + + # Fetch latest digest + latest=$(skopeo inspect "docker://${image_ref}" $override_os --format "{{.Digest}}" 2>/dev/null || true) + + if [[ -z "$latest" ]]; then + echo "SKIP $image_ref (inspect failed)" >&2 + ((skipped++)) || true + continue + fi + + if [[ "$current" == "$latest" ]]; then + ((skipped++)) || true + continue + fi + + # Replace old digest with new in the entire file (handles multi-use of same digest) + sed -i "s|${current}|${latest}|g" "$dockerfile" + rel_path="${dockerfile#$REPO_ROOT/}" + echo "BUMP $rel_path: $image_ref" + echo " ${current:0:19}... → ${latest:0:19}..." + ((updated++)) || true + + done < <(grep -n "skopeo inspect" "$dockerfile" | cut -d: -f1) +done + +echo "" +echo "Done: $updated updated, $skipped unchanged" diff --git a/scripts/coverage/compare_cov.py b/scripts/coverage/compare_cov.py index af48966518..47032a7c34 100644 --- a/scripts/coverage/compare_cov.py +++ b/scripts/coverage/compare_cov.py @@ -9,7 +9,7 @@ pr_num = os.environ.get("PULL_REQUEST_NUMBER") print("PR number is", pr_num) # Set repository information -owner = "azure" +owner = "microsoft" repo = "retina" current_branch_file = "coverageexpanded.out" @@ -42,7 +42,7 @@ def getAvgPerFile(given_dict): # read the current branch coverage file with open(current_branch_file, "r") as f: current_branch_lines = f.readlines() - if current_branch_lines is None: + if not current_branch_lines: print("No coverage data found for current branch") exit(1) for line in current_branch_lines: @@ -74,7 +74,7 @@ def getAvgPerFile(given_dict): # read the main branch coverage file with open(main_branch_file, "r") as f: main_branch_lines = f.readlines() - if main_branch_lines is None: + if not main_branch_lines: print("No coverage data found for main branch") exit(1) for line in main_branch_lines: @@ -266,13 +266,14 @@ def compare_dicts(main_dict, cur_dict): # Make the API call to update the comment update_response = requests.patch(update_url, headers=headers, json=payload) - if update_response.status_code != 200: + if update_response.status_code == 403: + print("Insufficient permissions to update PR comment (expected for fork PRs)") + elif update_response.status_code != 200: print( f"Failed to update the comment with url {update_url}", update_response.content) exit(1) - - # Print the response to confirm that the comment was updated successfully - print(update_response.content) + else: + print("Successfully updated coverage comment on PR") # If there is no existing comment, add a new comment else: @@ -283,10 +284,17 @@ def compare_dicts(main_dict, cur_dict): # Make the API call to add the new comment add_response = requests.post(comments_url, headers=headers, json=payload) - if add_response.status_code != 201: + if add_response.status_code == 403: + print("Insufficient permissions to post PR comment (expected for fork PRs)") + elif add_response.status_code != 201: print( f"Failed to add the comment with url {comments_url}", add_response.content) exit(1) - - # Print the response to confirm that the comment was added successfully - print(add_response.content) + else: + print("Successfully posted coverage comment on PR") + +# Write coverage report to step summary file if available (always visible) +step_summary = os.environ.get("GITHUB_STEP_SUMMARY") +if step_summary: + with open(step_summary, "a") as f: + f.write("\n" + body_of_comment + "\n") diff --git a/scripts/coverage/get_coverage.py b/scripts/coverage/get_coverage.py index d82b304de8..b42b203c0e 100644 --- a/scripts/coverage/get_coverage.py +++ b/scripts/coverage/get_coverage.py @@ -18,10 +18,10 @@ exit(0) # Set repository information -owner = "azure" +owner = "microsoft" repo = "retina" -ut_workflow_yaml = "retina-test.yaml" +ut_workflow_yaml = "test.yaml" # Get the id of UT workflow wf_url = f"https://api.github.com/repos/{owner}/{repo}/actions/workflows/{ut_workflow_yaml}" @@ -34,7 +34,10 @@ params = {"branch": "main", "status": "completed", "per_page": 10} response = requests.get(runs_url, headers=headers, params=params) response.raise_for_status() -artifacts_url = response.json()["workflow_runs"][0]["artifacts_url"] +workflow_runs = response.json()["workflow_runs"] +if not workflow_runs: + print("No completed workflow runs found on main branch") + exit(0) # Create the main branch folder for coverage folder_name = "mainbranchcoverage" @@ -42,7 +45,7 @@ if not os.path.exists(folder_name): os.makedirs(folder_name) -for wf in response.json()["workflow_runs"]: +for wf in workflow_runs: artifacts_url = wf["artifacts_url"] # Get any artifacts named "coverage" for the specified workflow run # artifacts_url = f"https://api.github.com/repos/{owner}/{repo}/actions/runs/{run_id}/artifacts" diff --git a/scripts/watch_drops.sh b/scripts/watch_drops.sh new file mode 100755 index 0000000000..8512ba51c4 --- /dev/null +++ b/scripts/watch_drops.sh @@ -0,0 +1,752 @@ +#!/usr/bin/env bash +set -o pipefail + +############################################################################### +# HELP & ARGUMENT PARSING +############################################################################### +usage() { + cat <&2 +} + +# Set initial DEBUG flag for early usage +DEBUG=0 +for arg in "$@"; do + [[ "$arg" == "--debug" ]] && DEBUG=1 && break +done + +# Function to detect agent type automatically +detect_agent_type() { + debug "Detecting agent type in cluster..." + + # Check for Retina pods first - try k8s-app=retina (more common) + local retina_pods=$(kubectl get pods -n kube-system -l k8s-app=retina --no-headers 2>/dev/null | wc -l) + if [[ $retina_pods -gt 0 ]]; then + debug "Found $retina_pods Retina pods in cluster (k8s-app=retina)" + echo "retina" + return 0 + fi + + # Check for Retina pods with alternative label + local retina_alt_pods=$(kubectl get pods -n kube-system -l app=retina --no-headers 2>/dev/null | wc -l) + if [[ $retina_alt_pods -gt 0 ]]; then + debug "Found $retina_alt_pods Retina pods in cluster (app=retina)" + echo "retina" + return 0 + fi + + # Check for Cilium pods + local cilium_pods=$(kubectl get pods -n kube-system -l k8s-app=cilium --no-headers 2>/dev/null | wc -l) + if [[ $cilium_pods -gt 0 ]]; then + debug "Found $cilium_pods Cilium pods in cluster" + echo "cilium" + return 0 + fi + + # Check for alternative Cilium selectors + local cilium_alt_pods=$(kubectl get pods -n kube-system -l app=cilium --no-headers 2>/dev/null | wc -l) + if [[ $cilium_alt_pods -gt 0 ]]; then + debug "Found $cilium_alt_pods Cilium pods (with app=cilium selector) in cluster" + echo "cilium" + return 0 + fi + + # No supported agent found + echo "" + return 1 +} + +# Detect agent type automatically +echo "Detecting network monitoring agent in cluster..." +AGENT_TYPE=$(detect_agent_type) + +if [[ -z "$AGENT_TYPE" ]]; then + echo "Error: No supported network monitoring agent found in cluster" + echo "This script supports:" + echo " • Retina (pods with label app=retina)" + echo " • Cilium (pods with label k8s-app=cilium or app=cilium)" + echo "" + echo "Available pods in kube-system namespace:" + kubectl get pods -n kube-system --no-headers | head -20 + exit 1 +fi + +echo "Detected agent type: $AGENT_TYPE" + +# Configure agent-specific settings +if [[ "$AGENT_TYPE" == "retina" ]]; then + AGENT_PORT=10093 + AGENT_NS="kube-system" + AGENT_SELECTOR="k8s-app=retina" +elif [[ "$AGENT_TYPE" == "cilium" ]]; then + AGENT_PORT=9962 + AGENT_NS="kube-system" + # Use the more common selector first, fallback handled in get_agent_pod function + AGENT_SELECTOR="k8s-app=cilium" +else + echo "Error: Unsupported agent type detected: $AGENT_TYPE" + exit 1 +fi + +# Parse node name (now the first argument) +if [[ $# -eq 0 ]]; then + echo "Error: NODE name is required" + usage +fi +NODE="$1" +shift + +# Parse interval (optional) +INTERVAL=30 +if [[ $# -gt 0 && "$1" != "--debug" ]]; then + if [[ "$1" =~ ^[0-9]+$ ]]; then + INTERVAL="$1" + shift + else + echo "Error: INTERVAL must be a number" + usage + fi +fi + +# Parse debug flag (optional) +DEBUG=0 +if [[ $# -gt 0 && "$1" == "--debug" ]]; then + DEBUG=1 +fi + +############################################################################### +# FUNCTIONS +############################################################################### + +# Colorize text if supported +colorize() { + local text=$1 + local type=$2 # "good", "bad", or "neutral" + + # Check if we're in a terminal that supports colors + if [[ -t 1 && -n "$TERM" && "$TERM" != "dumb" ]]; then + if [[ "$type" == "good" ]]; then + echo -e "\033[32m$text\033[0m" # Green + elif [[ "$type" == "bad" ]]; then + echo -e "\033[31m$text\033[0m" # Red + elif [[ "$type" == "neutral" ]]; then + echo -e "\033[33m$text\033[0m" # Yellow + else + echo "$text" + fi + else + echo "$text" + fi +} + +# Get the pod name for the agent running on the specified node +get_agent_pod() { + local node=$1 + + debug "kubectl get pods -n $AGENT_NS -l $AGENT_SELECTOR --field-selector spec.nodeName=$node -o jsonpath='{.items[0].metadata.name}'" + + # First check if any pods match our criteria and output more detail if we have issues + local pod_count=$(kubectl get pods -n $AGENT_NS -l $AGENT_SELECTOR --field-selector spec.nodeName=$node -o name 2>/dev/null | wc -l) + + # If no pods found with primary selector, try alternative selectors + if [[ $pod_count -eq 0 && "$AGENT_TYPE" == "cilium" && "$AGENT_SELECTOR" == "k8s-app=cilium" ]]; then + debug "No pods found with k8s-app=cilium selector, trying app=cilium..." + AGENT_SELECTOR="app=cilium" + pod_count=$(kubectl get pods -n $AGENT_NS -l $AGENT_SELECTOR --field-selector spec.nodeName=$node -o name 2>/dev/null | wc -l) + elif [[ $pod_count -eq 0 && "$AGENT_TYPE" == "retina" && "$AGENT_SELECTOR" == "k8s-app=retina" ]]; then + debug "No pods found with k8s-app=retina selector, trying app=retina..." + AGENT_SELECTOR="app=retina" + pod_count=$(kubectl get pods -n $AGENT_NS -l $AGENT_SELECTOR --field-selector spec.nodeName=$node -o name 2>/dev/null | wc -l) + fi + + if [[ $pod_count -eq 0 ]]; then + echo "No pods found with selector '$AGENT_SELECTOR' on node '$node'" >&2 + echo "Available pods in namespace $AGENT_NS:" >&2 + kubectl get pods -n $AGENT_NS -o wide | grep $node >&2 || true + echo "Trying to find any retina or cilium pods on this node:" >&2 + kubectl get pods -n $AGENT_NS -o wide | grep -E "(retina|cilium)" | grep $node >&2 || true + return 1 + fi + + kubectl get pods -n $AGENT_NS -l $AGENT_SELECTOR --field-selector spec.nodeName=$node -o jsonpath='{.items[0].metadata.name}' +} + +# Setup port forwarding +setup_port_forward() { + local pod=$1 + local local_port=$2 + local remote_port=$3 + local pid_var=$4 + + debug "kubectl port-forward -n $AGENT_NS $pod $local_port:$remote_port &" + + # Check if the port is already in use + if lsof -i:$local_port &>/dev/null; then + echo "Port $local_port is already in use. Killing process..." + lsof -i:$local_port -t | xargs kill -9 2>/dev/null || true + sleep 1 + fi + + # Start port forwarding + kubectl port-forward -n $AGENT_NS "$pod" "$local_port:$remote_port" & + eval "$pid_var=$!" + + # Give port-forward a moment to establish + sleep 2 + + # Check if port-forward is still running + if ! kill -0 "${!pid_var}" 2>/dev/null; then + echo "Port-forward process ${!pid_var} failed to start or died immediately" + return 1 + fi + + # Verify the port is actually listening + if ! lsof -i:$local_port &>/dev/null; then + echo "Port $local_port is not listening after port-forward setup" + return 1 + fi + + return 0 +} + +# Get metrics from endpoint +get_metrics() { + local endpoint=$1 + + debug "curl -s -m 5 $endpoint" + local metrics + metrics=$(curl -s -m 5 "$endpoint" || echo "") + + if [[ $DEBUG -eq 1 && -n "$metrics" ]]; then + local filename="$(date +%Y%m%d-%H%M%S)-${endpoint##*/}-metrics.txt" + filename="${filename//\//_}" + echo "$metrics" > "$filename" + debug "Metrics saved to $filename" + fi + + echo "$metrics" +} + +# Extract drop counts from metrics +parse_drops() { + local metrics=$1 + local prefix=$2 + local alternative_patterns=${3:-""} # Optional additional patterns to search with default empty string + + if [[ -z "$metrics" ]]; then + return 0 + fi + + local drop_metrics="" + + # First try specific patterns based on the prefix + if [[ "$prefix" == "cilium" ]]; then + # For cilium, check multiple drop-related patterns + drop_metrics=$(echo "$metrics" | grep -E "(drop_count_total|drop_bytes_total|_dropped|_errors_total)" | sort) + elif [[ "$prefix" == "retina" ]]; then + # For retina, use its specific patterns - Retina uses different metric names + drop_metrics=$(echo "$metrics" | grep -E "(drop_count|dropped_packets|packet_drop|networkobservability.*drop)" | sort) + elif [[ "$prefix" == "hubble" ]]; then + # For hubble, use its specific patterns + drop_metrics=$(echo "$metrics" | grep -E "(hubble_drop_total|hubble.*drop|flow.*drop)" | sort) + else + # Generic fallback + drop_metrics=$(echo "$metrics" | grep -E "(drop|dropped)" | sort) + fi + + # If no metrics found with specific patterns, try generic drop patterns + if [[ -z "$drop_metrics" ]]; then + debug "No metrics found with $prefix patterns, trying generic drop patterns..." + drop_metrics=$(echo "$metrics" | grep -iE "(drop|dropped)" | grep -v " 0$" | sort) + fi + + # If alternative patterns were provided and no metrics found, try those + if [[ -z "$drop_metrics" && -n "$alternative_patterns" ]]; then + drop_metrics=$(echo "$metrics" | grep -E "$alternative_patterns" | sort) + fi + + if [[ -z "$drop_metrics" && $DEBUG -eq 1 ]]; then + echo "DEBUG: No drop metrics found with standard patterns for prefix '$prefix'" >&2 + echo "DEBUG: Trying generic 'drop' pattern..." >&2 + local generic_drops=$(echo "$metrics" | grep -i "drop" | head -5) + if [[ -n "$generic_drops" ]]; then + echo "DEBUG: Found some metrics containing 'drop':" >&2 + echo "$generic_drops" >&2 + else + echo "DEBUG: No metrics containing 'drop' found" >&2 + echo "DEBUG: Looking for error or packet metrics..." >&2 + echo "$metrics" | grep -i -E "(error|packet|reject)" | head -5 >&2 + fi + + # Create a debug file with all metrics for analysis + local debug_file="${prefix}_all_metrics.txt" + echo "$metrics" > "$debug_file" + echo "DEBUG: All metrics saved to $debug_file for analysis" >&2 + fi + + echo "$drop_metrics" +} + +# Cleanup function +cleanup() { + debug "Cleaning up port forwards" + [[ -n "${AGENT_PF_PID:-}" ]] && kill -9 $AGENT_PF_PID 2>/dev/null || true + [[ -n "${HUBBLE_PF_PID:-}" ]] && kill -9 $HUBBLE_PF_PID 2>/dev/null || true + exit 0 +} + +# Extract drop counts into a structured format +extract_structured_drops() { + local metrics=$1 + local agent_type=$2 + local output_file=$3 + + debug "Extracting structured drops for $agent_type to $output_file" + debug "Input metrics has $(echo "$metrics" | wc -l) lines" + + local line_count=0 + # Process each line of the metrics to a simpler format + while IFS= read -r line; do + # Skip comment and empty lines + [[ "$line" =~ ^#.*$ || -z "$line" ]] && continue + + # Skip zero values if not in debug mode + [[ "$line" =~ [[:space:]]0$ && $DEBUG -eq 0 ]] && continue + + # Extract the key information and append to the output file + echo "$agent_type: $line" >> "$output_file" + line_count=$((line_count + 1)) + [[ $DEBUG -eq 1 ]] && debug "Added line: $agent_type: $line" + done <<< "$metrics" + + # Debug check file contents + debug "Wrote $line_count lines to $output_file" + if [[ $DEBUG -eq 1 && -f "$output_file" ]]; then + debug "File contents (first 10 lines):" + head -10 "$output_file" >&2 + fi +} + +# Create a comparison table from old and new metrics +generate_diff_table() { + local old_metrics_file=$1 + local new_metrics_file=$2 + local output_file=$3 + + debug "Generating diff table: comparing $old_metrics_file and $new_metrics_file" + + # Ensure output file is empty + > "$output_file" + + # Check if files exist and have content + if [[ ! -s "$old_metrics_file" ]]; then + debug "Warning: Old metrics file is empty or doesn't exist" + echo "No previous metrics data available for comparison" > "$output_file" + return 0 + fi + + if [[ ! -s "$new_metrics_file" ]]; then + debug "Warning: New metrics file is empty or doesn't exist" + echo "No current metrics data available for comparison" > "$output_file" + return 0 + fi + + # Create table header + echo "=== Drop Counter Changes ===" > "$output_file" + echo "" >> "$output_file" + printf "%-80s %15s %15s %15s\n" "Metric Name" "Previous" "Current" "Change" >> "$output_file" + printf "%-80s %15s %15s %15s\n" "$(printf '%*s' 80 '' | tr ' ' '-')" "$(printf '%*s' 15 '' | tr ' ' '-')" "$(printf '%*s' 15 '' | tr ' ' '-')" "$(printf '%*s' 15 '' | tr ' ' '-')" >> "$output_file" + + # Create associative arrays to store old and new values + declare -A old_values new_values + local changes_found=0 + + # Parse old metrics + while IFS= read -r line; do + if [[ -n "$line" && ! "$line" =~ ^# ]]; then + # Extract metric name and value + local metric_name=$(echo "$line" | sed 's/^[^:]*: //' | awk '{print $1}') + local metric_value=$(echo "$line" | awk '{print $NF}') + old_values["$metric_name"]="$metric_value" + fi + done < "$old_metrics_file" + + # Parse new metrics + while IFS= read -r line; do + if [[ -n "$line" && ! "$line" =~ ^# ]]; then + # Extract metric name and value + local metric_name=$(echo "$line" | sed 's/^[^:]*: //' | awk '{print $1}') + local metric_value=$(echo "$line" | awk '{print $NF}') + new_values["$metric_name"]="$metric_value" + fi + done < "$new_metrics_file" + + # Compare metrics and generate table rows + local all_metrics=() + + # Collect all unique metric names + for metric in "${!old_values[@]}"; do + all_metrics+=("$metric") + done + for metric in "${!new_values[@]}"; do + if [[ ! " ${all_metrics[*]} " =~ " ${metric} " ]]; then + all_metrics+=("$metric") + fi + done + + # Sort metrics for consistent output + IFS=$'\n' all_metrics=($(sort <<<"${all_metrics[*]}")) + unset IFS + + # Generate table rows + for metric in "${all_metrics[@]}"; do + local old_val="${old_values[$metric]:-0}" + local new_val="${new_values[$metric]:-0}" + + # Only show metrics that have changed or are non-zero + if [[ "$old_val" != "$new_val" ]] || [[ "$old_val" != "0" ]] || [[ "$new_val" != "0" ]]; then + local change="" + local change_indicator="" + + # Calculate numeric change if both values are numbers (including scientific notation) + if [[ "$old_val" =~ ^[0-9]+\.?[0-9]*([eE][+-]?[0-9]+)?$ ]] && [[ "$new_val" =~ ^[0-9]+\.?[0-9]*([eE][+-]?[0-9]+)?$ ]]; then + # Use awk for floating point arithmetic to handle scientific notation + local diff=$(awk "BEGIN {printf \"%.0f\", $new_val - $old_val}") + if [[ $diff -gt 0 ]]; then + change="+$diff" + change_indicator="↑" + elif [[ $diff -lt 0 ]]; then + change="$diff" + change_indicator="↓" + else + change="0" + change_indicator="=" + fi + else + # Non-numeric comparison + if [[ "$old_val" != "$new_val" ]]; then + change="changed" + change_indicator="~" + else + change="same" + change_indicator="=" + fi + fi + + # Don't truncate metric names - show full names + local display_metric="$metric" + + # Format change with arrow indicators (no colors) + local formatted_change="$change $change_indicator" + + printf "%-80s %15s %15s %15s\n" "$display_metric" "$old_val" "$new_val" "$formatted_change" >> "$output_file" + changes_found=1 + fi + done + + if [[ $changes_found -eq 0 ]]; then + echo "" >> "$output_file" + echo "No changes detected in drop counters" >> "$output_file" + else + echo "" >> "$output_file" + echo "Legend: ↑ = increase (potential issue), ↓ = decrease (improvement), = = no change" >> "$output_file" + fi + + # Add note about detailed metrics file + echo "" >> "$output_file" + echo "Note: For full metric details and complete names, see: $new_metrics_file" >> "$output_file" + + return $changes_found +} + +############################################################################### +# MAIN SCRIPT +############################################################################### + +# Check for required commands +for cmd in kubectl curl grep sort; do + if ! command -v $cmd &>/dev/null; then + echo "Error: Required command '$cmd' not found" + exit 1 + fi +done + +# Optional command check +for cmd in lsof; do + if ! command -v $cmd &>/dev/null; then + echo "Warning: Optional command '$cmd' not found, some features may be limited" + fi +done + +# Verify kubectl can connect to the cluster +if ! kubectl get nodes -o name --request-timeout=5s &>/dev/null; then + echo "Error: Cannot connect to Kubernetes cluster. Please check your kubeconfig." + exit 1 +fi + +# Verify the node exists in the cluster +if ! kubectl get nodes -o name --request-timeout=5s | grep -q "${NODE}"; then + echo "Error: Node '${NODE}' not found in the cluster." + echo "Available nodes:" + kubectl get nodes + exit 1 +fi + +# Setup trap for cleanup +trap cleanup EXIT INT TERM + +echo "Watching drop counters on node $NODE (${AGENT_TYPE}) every ${INTERVAL}s..." + +# Local ports for port forwarding +LOCAL_AGENT_PORT=8093 +LOCAL_HUBBLE_PORT=8965 +HUBBLE_PORT=9965 +HUBBLE_ENABLED=0 +HUBBLE_WARNING_SHOWN=0 + +# Files to store metrics for comparison +METRICS_DIR="${TMPDIR:-/tmp}/watchmetrics" +mkdir -p "$METRICS_DIR" +CURRENT_METRICS_FILE="$METRICS_DIR/current_metrics.txt" +PREVIOUS_METRICS_FILE="$METRICS_DIR/previous_metrics.txt" +DIFF_TABLE_FILE="$METRICS_DIR/diff_table.txt" + +# Initialize files if they don't exist +> "$CURRENT_METRICS_FILE" +> "$PREVIOUS_METRICS_FILE" +> "$DIFF_TABLE_FILE" + +debug "Metrics files:" +debug "- Current: $CURRENT_METRICS_FILE" +debug "- Previous: $PREVIOUS_METRICS_FILE" +debug "- Diff Table: $DIFF_TABLE_FILE" + +while true; do + echo -e "\n$(date): Checking ${AGENT_TYPE} drop counters on $NODE" + + # Clear current metrics for this iteration + > "$CURRENT_METRICS_FILE" + + # Get the agent pod on the specified node + echo "Looking for ${AGENT_TYPE} pod on node $NODE..." + AGENT_POD=$(get_agent_pod "$NODE") + + if [[ -z "$AGENT_POD" ]]; then + echo "Error: No ${AGENT_TYPE} pod found on node $NODE" + echo "Checking node existence..." + kubectl get nodes | grep "$NODE" || echo "Node $NODE doesn't exist in the cluster" + echo "Waiting ${INTERVAL}s before next check..." + sleep $INTERVAL + continue + fi + + echo "Found pod: $AGENT_POD" + + # Kill any existing port-forwards + [[ -n "${AGENT_PF_PID:-}" ]] && kill -9 $AGENT_PF_PID 2>/dev/null || true + [[ -n "${HUBBLE_PF_PID:-}" ]] && kill -9 $HUBBLE_PF_PID 2>/dev/null || true + + # Counter for number of iterations with no metrics found + if [[ -z "${NO_METRICS_COUNT:-}" ]]; then + NO_METRICS_COUNT=0 + fi + + # Flag to track if any metrics were found in this iteration + FOUND_ANY_METRICS=0 + + # Setup port forwarding for agent metrics + echo "Setting up port forwarding for agent metrics to port $AGENT_PORT..." + if setup_port_forward "$AGENT_POD" "$LOCAL_AGENT_PORT" "$AGENT_PORT" "AGENT_PF_PID"; then + echo "Port forwarding established for agent metrics" + + # Get and display agent drop metrics + echo "Fetching metrics from http://localhost:$LOCAL_AGENT_PORT/metrics..." + AGENT_METRICS=$(get_metrics "http://localhost:$LOCAL_AGENT_PORT/metrics") + + if [[ -z "$AGENT_METRICS" ]]; then + echo "Warning: No metrics data received from agent endpoint" + fi + + # Try with the standard pattern first + AGENT_DROPS=$(parse_drops "$AGENT_METRICS" "$AGENT_TYPE") + + if [[ -n "$AGENT_DROPS" ]]; then + echo -e "\n=== Agent Drop Counters ===" + echo "$AGENT_DROPS" + FOUND_ANY_METRICS=1 + + # Extract structured metrics for comparison + extract_structured_drops "$AGENT_DROPS" "$AGENT_TYPE" "$CURRENT_METRICS_FILE" + else + echo "No drop counters found in agent metrics" + + # Try again with a fallback pattern focused on errors + FALLBACK_DROPS=$(echo "$AGENT_METRICS" | grep -E "(^${AGENT_TYPE}.*error|^${AGENT_TYPE}.*fail)" | grep -v "0$" | sort) + if [[ -n "$FALLBACK_DROPS" ]]; then + echo -e "\n=== Agent Error Counters (potential drops) ===" + echo "$FALLBACK_DROPS" + FOUND_ANY_METRICS=1 + + # Extract structured metrics for comparison + extract_structured_drops "$FALLBACK_DROPS" "$AGENT_TYPE" "$CURRENT_METRICS_FILE" + elif [[ $DEBUG -eq 1 ]]; then + echo "Sample of received metrics (first 10 lines):" + echo "$AGENT_METRICS" | head -10 + fi + fi + else + echo "Failed to establish port forwarding for agent metrics" + fi + + # Setup port forwarding for Hubble metrics if not disabled + if [[ $HUBBLE_ENABLED -eq 0 || $HUBBLE_WARNING_SHOWN -eq 0 ]]; then + echo "Setting up port forwarding for Hubble metrics to port $HUBBLE_PORT..." + if setup_port_forward "$AGENT_POD" "$LOCAL_HUBBLE_PORT" "$HUBBLE_PORT" "HUBBLE_PF_PID"; then + HUBBLE_ENABLED=1 + + # Get and display Hubble drop metrics + echo "Fetching metrics from http://localhost:$LOCAL_HUBBLE_PORT/metrics..." + HUBBLE_METRICS=$(get_metrics "http://localhost:$LOCAL_HUBBLE_PORT/metrics") + + if [[ -z "$HUBBLE_METRICS" ]]; then + echo "Warning: No metrics data received from Hubble endpoint" + else + # Try with hubble prefix first, then fall back to flow metrics if needed + HUBBLE_DROPS=$(parse_drops "$HUBBLE_METRICS" "hubble" "flow.*drop|flow.*reject") + + if [[ -n "$HUBBLE_DROPS" ]]; then + echo -e "\n=== Hubble Drop Counters ===" + echo "$HUBBLE_DROPS" + FOUND_ANY_METRICS=1 + + # Extract structured metrics for comparison + extract_structured_drops "$HUBBLE_DROPS" "hubble" "$CURRENT_METRICS_FILE" + else + echo "No drop counters found in Hubble metrics" + + if [[ $DEBUG -eq 1 ]]; then + echo "Sample of received Hubble metrics (first 10 lines):" + echo "$HUBBLE_METRICS" | head -10 + + # Try to find flow-related metrics as they might be useful + echo "Looking for flow metrics (might contain useful information):" + FLOW_METRICS=$(echo "$HUBBLE_METRICS" | grep -i "flow" | head -10) + if [[ -n "$FLOW_METRICS" ]]; then + echo "$FLOW_METRICS" + else + echo "No flow metrics found" + fi + fi + fi + fi + else + HUBBLE_ENABLED=0 + if [[ $HUBBLE_WARNING_SHOWN -eq 0 ]]; then + echo "Notice: Hubble port-forwarding failed - Hubble metrics will be ignored" + HUBBLE_WARNING_SHOWN=1 + + if [[ $DEBUG -eq 1 ]]; then + echo "DEBUG: Checking if Hubble is enabled in Cilium..." + kubectl -n $AGENT_NS exec $AGENT_POD -- cilium status | grep -i hubble || echo "Hubble not found in Cilium status" + fi + fi + fi + fi + + # Update metrics counter + if [[ $FOUND_ANY_METRICS -eq 0 ]]; then + NO_METRICS_COUNT=$((NO_METRICS_COUNT + 1)) + + # After 3 attempts with no metrics, provide some help + if [[ $NO_METRICS_COUNT -eq 3 ]]; then + echo -e "\n==================================================================================================" + echo "NOTE: No drop metrics found after multiple attempts. This could be due to:" + echo " 1. There are genuinely no packet drops occurring on this node" + echo " 2. The metrics format used by ${AGENT_TYPE} in your cluster is different than expected" + echo " 3. ${AGENT_TYPE} is not configured to expose drop metrics" + echo "" + if [[ "$AGENT_TYPE" == "cilium" ]]; then + echo "For Cilium, you might want to check:" + echo " - Cilium version: kubectl -n kube-system exec $AGENT_POD -- cilium version" + echo " - Hubble status: kubectl -n kube-system exec $AGENT_POD -- cilium status | grep Hubble" + echo " - Metrics directly: kubectl -n kube-system port-forward $AGENT_POD 9962:9962 & curl localhost:9962/metrics | grep -i drop" + elif [[ "$AGENT_TYPE" == "retina" ]]; then + echo "For Retina, you might want to check:" + echo " - Retina version: kubectl -n kube-system describe pod $AGENT_POD | grep Image" + echo " - Metrics directly: kubectl -n kube-system port-forward $AGENT_POD 10093:10093 & curl localhost:10093/metrics | grep -i drop" + fi + echo "==================================================================================================" + + # Reset counter so we don't show this message too often + NO_METRICS_COUNT=0 + fi + else + # Reset the counter when metrics are found + NO_METRICS_COUNT=0 + + # Generate and display the diff table if we have previous metrics + if [[ -s "$PREVIOUS_METRICS_FILE" && -s "$CURRENT_METRICS_FILE" ]]; then + echo -e "\n=== Metrics Changes Since Last Check ===" + debug "Comparing metrics in $PREVIOUS_METRICS_FILE and $CURRENT_METRICS_FILE" + + # Generate the diff output + generate_diff_table "$PREVIOUS_METRICS_FILE" "$CURRENT_METRICS_FILE" "$DIFF_TABLE_FILE" + + # Display the diff output + if [[ -s "$DIFF_TABLE_FILE" ]]; then + cat "$DIFF_TABLE_FILE" + else + echo " No changes in drop counters since last check" + fi + else + debug "Not generating diff - missing or empty metrics files" + fi + + # Save current metrics as previous for next comparison + if [[ -s "$CURRENT_METRICS_FILE" ]]; then + debug "Saving current metrics for next comparison" + cp "$CURRENT_METRICS_FILE" "$PREVIOUS_METRICS_FILE" + else + debug "Current metrics file is empty, not saving for comparison" + fi + + # Clear current metrics file for next iteration + > "$CURRENT_METRICS_FILE" + fi + + echo -e "\nWaiting ${INTERVAL}s before next check..." + sleep $INTERVAL +done diff --git a/shell/Dockerfile b/shell/Dockerfile index a5c31b3b5e..370bbba478 100644 --- a/shell/Dockerfile +++ b/shell/Dockerfile @@ -1,5 +1,5 @@ # skopeo inspect docker://mcr.microsoft.com/azurelinux/base/core:3.0 --format "{{.Name}}@{{.Digest}}" -FROM mcr.microsoft.com/azurelinux/base/core:3.0@sha256:35149ae8dd179684f969944f54a337c665a64e702486154eb44253fb39c2505b +FROM mcr.microsoft.com/azurelinux/base/core:3.0.20260517@sha256:f5e224c47997aa4a5d3d8addfcc3866e175e7026368a71ce1be2c0eed1876f04 RUN tdnf install -y \ bind-utils \ @@ -28,16 +28,20 @@ RUN tdnf install -y \ tar \ file \ bpftool \ + bpftrace \ ig \ && tdnf clean all -# Create the entrypoint script to mount debugfs +# Create the entrypoint script to mount debugfs and tracefs SHELL ["/bin/bash", "-c"] RUN echo $'#!/bin/bash\n\ if ! mountpoint -q /sys/kernel/debug; then\n\ mount -t debugfs none /sys/kernel/debug\n\ fi\n\ +if ! mountpoint -q /sys/kernel/tracing; then\n\ +mount -t tracefs tracefs /sys/kernel/tracing\n\ +fi\n\ exec "$@"' > /usr/local/bin/entrypoint.sh; # Set the host root directory for IG @@ -53,7 +57,7 @@ ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] ARG GOARCH=amd64 ENV ARCH=${GOARCH} # https://github.com/cilium/pwru/releases -ARG PWRU_TAG="v1.0.9" +ARG PWRU_TAG="v1.0.11" ENV PWRU_TAG=${PWRU_TAG} # Download and extract latest pwru release for the correct architecture (amd64 or arm64) diff --git a/shell/README.md b/shell/README.md deleted file mode 100644 index 1f3033814c..0000000000 --- a/shell/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# retina-shell - -Retina CLI provides a command to launch an interactive shell in a node or pod for adhoc debugging. - -* The CLI command `kubectl retina shell` creates a pod with `HostNetwork=true` (for node debugging) or an ephemeral container in an existing pod (for pod debugging). -* The container runs an image built from the Dockerfile in this directory. The image is based on Azure Linux and includes commonly-used networking tools. - -For testing, you can override the image used by `retina shell` either with CLI arguments -(`--retina-shell-image-repo` and `--retina-shell-image-version`) or environment variables -(`RETINA_SHELL_IMAGE_REPO` and `RETINA_SHELL_IMAGE_VERSION`). - -Run `kubectl retina shell -h` for full documentation and examples. - -Currently only Linux is supported; Windows support will be added in the future. diff --git a/shell/trace.go b/shell/trace.go new file mode 100644 index 0000000000..dea6015c74 --- /dev/null +++ b/shell/trace.go @@ -0,0 +1,267 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package shell + +import ( + "context" + "fmt" + "io" + "net" + "os" + "time" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilrand "k8s.io/apimachinery/pkg/util/rand" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/remotecommand" +) + +// TraceConfig holds the validated configuration for network tracing. +// All fields are typed values - no raw user strings for security. +type TraceConfig struct { + // Kubernetes configuration + RestConfig *rest.Config + RetinaShellImage string + + // Filter configuration (validated, typed values only) + FilterIPs []net.IP // Validated IP addresses to filter + FilterCIDRs []*net.IPNet // Validated CIDRs to filter + + // Output configuration + OutputJSON bool // true for JSON output, false for table + + // Event selection - which probes to enable + EnableDrops bool // Enable packet drop tracing (kfree_skb) + EnableRST bool // Enable TCP RST tracing (tcp_send_reset/tcp_receive_reset) + EnableErrors bool // Enable socket error tracing (inet_sk_error_report) + EnableRetransmits bool // Enable TCP retransmit tracing (tcp_retransmit_skb) + EnableNfqueueDrops bool // Enable NFQUEUE drop tracing (fexit:vmlinux:__nf_queue) + + // Timing configuration + TraceDuration time.Duration // How long to trace (0 = until Ctrl-C) + Timeout time.Duration // Pod startup timeout +} + +// TraceCapabilities returns the required Linux capabilities for bpftrace. +// These are set automatically and not user-configurable. +func TraceCapabilities() []string { + return []string{ + "SYS_ADMIN", // Required for bpftrace + "BPF", // Load BPF programs + "PERFMON", // Perf events access + "NET_ADMIN", // Network tracing + "SYS_PTRACE", // Process tracing (for stack traces) + "SYS_RESOURCE", // Increase rlimits for BPF maps + "MKNOD", // Required for bpftrace debugfs access + "SYS_CHROOT", // Required for bpftrace + } +} + +// RunTrace starts a network trace on a node. +// It creates a privileged pod on the target node, runs bpftrace, and streams output. +func RunTrace(ctx context.Context, config TraceConfig, nodeName, debugPodNamespace string) error { + clientset, err := kubernetes.NewForConfig(config.RestConfig) + if err != nil { + return fmt.Errorf("error constructing kube clientset: %w", err) + } + + // Validate node OS + err = validateOperatingSystemSupportedForNode(ctx, clientset, nodeName) + if err != nil { + return fmt.Errorf("error validating operating system for node %s: %w", nodeName, err) + } + + // Create the trace pod + pod := hostNetworkPodForTrace(config, debugPodNamespace, nodeName) + + fmt.Printf("Creating trace pod %s/%s on node %s\n", debugPodNamespace, pod.Name, nodeName) + createdPod, err := clientset.CoreV1(). + Pods(debugPodNamespace). + Create(ctx, pod, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("error creating trace pod %s in namespace %s: %w", pod.Name, debugPodNamespace, err) + } + + // Ensure cleanup on exit (Ctrl-C, error, or normal termination) + // Note: intentionally using context.Background() for cleanup so it runs even if ctx is canceled + defer func() { //nolint:contextcheck // cleanup must run regardless of parent context state + fmt.Printf("Cleaning up trace pod %s/%s\n", debugPodNamespace, createdPod.Name) + deleteCtx := context.Background() // Use fresh context for cleanup + deleteErr := clientset.CoreV1(). + Pods(debugPodNamespace). + Delete(deleteCtx, createdPod.Name, metav1.DeleteOptions{}) + if deleteErr != nil { + fmt.Fprintf(os.Stderr, "warning: failed to delete trace pod %s: %v\n", createdPod.Name, deleteErr) + } + }() + + // Wait for pod to be running + err = waitForContainerRunning(ctx, config.Timeout, clientset, debugPodNamespace, createdPod.Name, createdPod.Spec.Containers[0].Name) + if err != nil { + return fmt.Errorf("error waiting for trace pod to start: %w", err) + } + + fmt.Printf("Trace pod ready, starting trace...\n") + + // First, fetch and display reason/state codes from kernel + // These are kernel-version specific so we read them at runtime + fmt.Printf("\n") + + // Display SKB drop reason codes (for DROP events) + dropReasonsCommand := DropReasonsCommand() + err = execInPod(ctx, config.RestConfig, clientset, debugPodNamespace, createdPod.Name, createdPod.Spec.Containers[0].Name, dropReasonsCommand, os.Stdout, os.Stderr) + if err != nil { + // Non-fatal: continue even if we can't get reason codes + fmt.Fprintf(os.Stderr, "warning: could not fetch drop reason codes: %v\n", err) + } + fmt.Printf("\n") + + // Generate and run the bpftrace script + gen := NewScriptGenerator(config) + script := gen.Generate() + + // Run bpftrace with the generated script + // SECURITY: The script is passed via -e flag, not interpolated into a shell command + bpftraceCommand := []string{"bpftrace", "-e", script} + + err = execInPod(ctx, config.RestConfig, clientset, debugPodNamespace, createdPod.Name, createdPod.Spec.Containers[0].Name, bpftraceCommand, os.Stdout, os.Stderr) + if err != nil { + // If duration was specified and context was cancelled, it's expected behavior + if config.TraceDuration > 0 && ctx.Err() != nil { + fmt.Printf("\nTrace completed after %s\n", config.TraceDuration) + return nil + } + return fmt.Errorf("error executing trace command: %w", err) + } + + return nil +} + +// execInPod executes a command inside a pod container without using a shell. +// SECURITY: The command is passed as an array directly to the container runtime, +// preventing shell injection attacks. No shell interpolation occurs. +// +// Parameters: +// - ctx: Context for cancellation (e.g., Ctrl-C) +// - restConfig: Kubernetes REST client config +// - clientset: Kubernetes clientset +// - namespace: Pod namespace +// - podName: Pod name +// - containerName: Container name +// - command: Command and arguments as string array (NO SHELL - passed directly) +// - stdout: Writer for stdout (typically os.Stdout) +// - stderr: Writer for stderr (typically os.Stderr) +func execInPod( + ctx context.Context, + restConfig *rest.Config, + clientset *kubernetes.Clientset, + namespace, podName, containerName string, + command []string, + stdout, stderr io.Writer, +) error { + // Build the exec request using the REST API directly + // SECURITY: Command is passed as array in PodExecOptions, NOT through a shell + req := clientset.CoreV1().RESTClient(). + Post(). + Resource("pods"). + Name(podName). + Namespace(namespace). + SubResource("exec"). + VersionedParams(&v1.PodExecOptions{ + Container: containerName, + Command: command, // Direct command array - no shell! + Stdin: false, + Stdout: true, + Stderr: true, + TTY: false, + }, scheme.ParameterCodec) + + // Create the SPDY executor + exec, err := remotecommand.NewSPDYExecutor(restConfig, "POST", req.URL()) + if err != nil { + return fmt.Errorf("error creating executor: %w", err) + } + + // Stream the output + // The Stream function blocks until the command completes or context is cancelled + err = exec.StreamWithContext(ctx, remotecommand.StreamOptions{ + Stdout: stdout, + Stderr: stderr, + }) + if err != nil { + // Check if it was a context cancellation (user pressed Ctrl-C) + if ctx.Err() != nil { + return fmt.Errorf("context error: %w", ctx.Err()) + } + return fmt.Errorf("error streaming command output: %w", err) + } + + return nil +} + +// hostNetworkPodForTrace creates a pod manifest for network tracing. +// The pod runs with host network and required capabilities for bpftrace. +func hostNetworkPodForTrace(config TraceConfig, debugPodNamespace, nodeName string) *v1.Pod { + // Use Args (not Command) to preserve the image entrypoint. + // The entrypoint.sh in retina-shell image mounts debugfs/tracefs which bpftrace needs. + args := []string{"sleep", "infinity"} + + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: randomTraceContainerName(), + Namespace: debugPodNamespace, + Labels: map[string]string{ + "app": "retina-trace", + "retina.sh/component": "trace", + "retina.sh/trace-target-node": nodeName, + }, + }, + Spec: v1.PodSpec{ + NodeName: nodeName, + RestartPolicy: v1.RestartPolicyNever, + Tolerations: []v1.Toleration{{Operator: v1.TolerationOpExists}}, + HostNetwork: true, + HostPID: true, // Required for full process visibility + Containers: []v1.Container{ + { + Name: "retina-trace", + Image: config.RetinaShellImage, + Args: args, // Use Args to preserve entrypoint.sh + Stdin: false, // Not interactive + TTY: false, // Not interactive + SecurityContext: &v1.SecurityContext{ + Privileged: boolPtr(false), // Use capabilities instead + Capabilities: &v1.Capabilities{ + Drop: []v1.Capability{"ALL"}, + Add: stringSliceToCapabilities(TraceCapabilities()), + }, + // Required for bpftrace (per shell.md documentation) + SeccompProfile: &v1.SeccompProfile{ + Type: v1.SeccompProfileTypeUnconfined, + }, + AppArmorProfile: &v1.AppArmorProfile{ + Type: v1.AppArmorProfileTypeUnconfined, + }, + }, + }, + }, + }, + } + + return pod +} + +// randomTraceContainerName generates a unique name for the trace pod. +func randomTraceContainerName() string { + const randLen = 5 + return "retina-trace-" + utilrand.String(randLen) +} + +// boolPtr returns a pointer to a bool value. +func boolPtr(b bool) *bool { + return &b +} diff --git a/shell/trace_test.go b/shell/trace_test.go new file mode 100644 index 0000000000..59f8d444dc --- /dev/null +++ b/shell/trace_test.go @@ -0,0 +1,383 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package shell + +import ( + "bytes" + "context" + "errors" + "net" + "strings" + "testing" + "time" + + v1 "k8s.io/api/core/v1" +) + +func TestTraceCapabilities(t *testing.T) { + caps := TraceCapabilities() + + // Must have at least these required capabilities for bpftrace + requiredCaps := []string{ + "SYS_ADMIN", + "BPF", + "PERFMON", + "NET_ADMIN", + } + + capSet := make(map[string]bool) + for _, c := range caps { + capSet[c] = true + } + + for _, required := range requiredCaps { + if !capSet[required] { + t.Errorf("TraceCapabilities() missing required capability: %s", required) + } + } +} + +func TestTraceConfigTypedFields(t *testing.T) { + // Verify TraceConfig only accepts typed values, not raw strings + config := TraceConfig{ + FilterIPs: []net.IP{net.ParseIP("10.0.0.1")}, + FilterCIDRs: []*net.IPNet{}, + OutputJSON: true, + } + + // FilterIPs should be net.IP, not string + if len(config.FilterIPs) != 1 { + t.Errorf("Expected 1 FilterIP, got %d", len(config.FilterIPs)) + } + if !config.FilterIPs[0].Equal(net.ParseIP("10.0.0.1")) { + t.Errorf("FilterIP not set correctly") + } + + // Verify OutputJSON is bool, not string + if !config.OutputJSON { + t.Errorf("OutputJSON should be true") + } +} + +func TestHostNetworkPodForTrace(t *testing.T) { + config := TraceConfig{ + RetinaShellImage: "mcr.microsoft.com/containernetworking/retina-shell:v1.0.0", + } + + pod := hostNetworkPodForTrace(config, "kube-system", "node-001") + + // Test basic pod properties + t.Run("namespace", func(t *testing.T) { + if pod.Namespace != "kube-system" { + t.Errorf("Expected namespace kube-system, got %s", pod.Namespace) + } + }) + + t.Run("node selector", func(t *testing.T) { + if pod.Spec.NodeName != "node-001" { + t.Errorf("Expected nodeName node-001, got %s", pod.Spec.NodeName) + } + }) + + t.Run("host network enabled", func(t *testing.T) { + if !pod.Spec.HostNetwork { + t.Error("Expected HostNetwork to be true") + } + }) + + t.Run("host PID enabled", func(t *testing.T) { + if !pod.Spec.HostPID { + t.Error("Expected HostPID to be true for bpftrace") + } + }) + + t.Run("tolerates all taints", func(t *testing.T) { + found := false + for _, tol := range pod.Spec.Tolerations { + if tol.Operator == v1.TolerationOpExists { + found = true + break + } + } + if !found { + t.Error("Expected toleration with Operator=Exists to run on any node") + } + }) + + t.Run("restart policy never", func(t *testing.T) { + if pod.Spec.RestartPolicy != v1.RestartPolicyNever { + t.Errorf("Expected RestartPolicy Never, got %s", pod.Spec.RestartPolicy) + } + }) + + t.Run("image set correctly", func(t *testing.T) { + if len(pod.Spec.Containers) != 1 { + t.Fatalf("Expected 1 container, got %d", len(pod.Spec.Containers)) + } + if pod.Spec.Containers[0].Image != config.RetinaShellImage { + t.Errorf("Expected image %s, got %s", config.RetinaShellImage, pod.Spec.Containers[0].Image) + } + }) + + t.Run("not interactive", func(t *testing.T) { + container := pod.Spec.Containers[0] + if container.Stdin { + t.Error("Expected Stdin to be false for non-interactive trace") + } + if container.TTY { + t.Error("Expected TTY to be false for non-interactive trace") + } + }) + + t.Run("has trace labels", func(t *testing.T) { + if pod.Labels["app"] != "retina-trace" { + t.Errorf("Expected label app=retina-trace, got %s", pod.Labels["app"]) + } + if pod.Labels["retina.sh/trace-target-node"] != "node-001" { + t.Errorf("Expected node label, got %s", pod.Labels["retina.sh/trace-target-node"]) + } + }) +} + +func TestHostNetworkPodForTraceSecurityContext(t *testing.T) { + config := TraceConfig{ + RetinaShellImage: "test-image:v1", + } + + pod := hostNetworkPodForTrace(config, "default", "test-node") + container := pod.Spec.Containers[0] + secCtx := container.SecurityContext + + t.Run("not privileged", func(t *testing.T) { + if secCtx.Privileged != nil && *secCtx.Privileged { + t.Error("Pod should use capabilities, not privileged mode") + } + }) + + t.Run("drops all capabilities", func(t *testing.T) { + if secCtx.Capabilities == nil { + t.Fatal("Expected Capabilities to be set") + } + + foundDropAll := false + for _, drop := range secCtx.Capabilities.Drop { + if string(drop) == "ALL" { + foundDropAll = true + break + } + } + if !foundDropAll { + t.Error("Expected to drop ALL capabilities first") + } + }) + + t.Run("adds required capabilities", func(t *testing.T) { + if secCtx.Capabilities == nil { + t.Fatal("Expected Capabilities to be set") + } + + addedCaps := make(map[string]bool) + for _, cap := range secCtx.Capabilities.Add { + addedCaps[string(cap)] = true + } + + requiredCaps := TraceCapabilities() + for _, required := range requiredCaps { + if !addedCaps[required] { + t.Errorf("Missing required capability: %s", required) + } + } + }) + + t.Run("seccomp unconfined", func(t *testing.T) { + if secCtx.SeccompProfile == nil { + t.Fatal("Expected SeccompProfile to be set") + } + if secCtx.SeccompProfile.Type != v1.SeccompProfileTypeUnconfined { + t.Errorf("Expected Seccomp Unconfined, got %s", secCtx.SeccompProfile.Type) + } + }) +} + +func TestRandomTraceContainerName(t *testing.T) { + name1 := randomTraceContainerName() + name2 := randomTraceContainerName() + + t.Run("has prefix", func(t *testing.T) { + if len(name1) < 14 || name1[:13] != "retina-trace-" { + t.Errorf("Expected prefix 'retina-trace-', got %s", name1) + } + }) + + t.Run("unique names", func(t *testing.T) { + if name1 == name2 { + t.Errorf("Expected unique names, got %s and %s", name1, name2) + } + }) +} + +func TestBoolPtr(t *testing.T) { + truePtr := boolPtr(true) + falsePtr := boolPtr(false) + + if truePtr == nil || *truePtr != true { + t.Error("boolPtr(true) should return pointer to true") + } + if falsePtr == nil || *falsePtr != false { + t.Error("boolPtr(false) should return pointer to false") + } +} + +// TestExecInPodCommandIsArray verifies that execInPod takes command as array +// This is a security test - commands must NOT be passed through a shell +func TestExecInPodCommandIsArray(t *testing.T) { + // This test verifies the function signature and documentation + // The actual exec requires a running cluster, but we can verify + // that the function is designed correctly + + // Verify the function signature accepts []string for command + // If this compiles, the function correctly uses array, not string + commandArray := []string{"bpftrace", "-e", "tracepoint:skb:kfree_skb { }"} + + // Ensure the command array contains separate elements + if len(commandArray) != 3 { + t.Errorf("Command should be array of 3 elements, got %d", len(commandArray)) + } + + // Verify no shell metacharacters would be interpreted + // (they're just strings in an array, not executed by shell) + dangerousInput := "test; rm -rf /" + testCommand := []string{"echo", dangerousInput} + + // In a shell: echo "test; rm -rf /" would be safe + // But echo test; rm -rf / would be dangerous + // With array exec: ["echo", "test; rm -rf /"] is always safe + // because "test; rm -rf /" is passed as a single argument to echo + + if testCommand[0] != "echo" { + t.Error("First element should be 'echo'") + } + if testCommand[1] != dangerousInput { + t.Error("Second element should be the dangerous input as-is (not interpreted)") + } + + // The key security property: the dangerous input stays as ONE argument + // It's not split by shell + if len(testCommand) != 2 { + t.Errorf("Command should have exactly 2 elements (not shell-split), got %d", len(testCommand)) + } +} + +// TestExecInPodContextCancellation verifies context cancellation behavior +func TestExecInPodContextCancellation(t *testing.T) { + // Create a cancelled context + ctx, cancel := context.WithCancel(context.Background()) + cancel() // Cancel immediately + + // Verify context is done + select { + case <-ctx.Done(): + // Expected - context should be cancelled + if !errors.Is(ctx.Err(), context.Canceled) { + t.Errorf("Expected context.Canceled, got %v", ctx.Err()) + } + default: + t.Error("Context should be done after cancel()") + } +} + +// TestExecInPodTimeoutContext verifies timeout context behavior +func TestExecInPodTimeoutContext(t *testing.T) { + // Create a context with very short timeout + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) + defer cancel() + + // Wait for timeout + time.Sleep(5 * time.Millisecond) + + // Verify context is done due to deadline + select { + case <-ctx.Done(): + if !errors.Is(ctx.Err(), context.DeadlineExceeded) { + t.Errorf("Expected context.DeadlineExceeded, got %v", ctx.Err()) + } + default: + t.Error("Context should be done after timeout") + } +} + +// TestExecOutputWriters verifies stdout/stderr writers work correctly +func TestExecOutputWriters(t *testing.T) { + // This tests that our output handling pattern works correctly + var stdout, stderr bytes.Buffer + + // Simulate writing to both + stdout.WriteString("stdout output\n") + stderr.WriteString("stderr output\n") + + if !strings.Contains(stdout.String(), "stdout") { + t.Error("stdout buffer should contain stdout output") + } + if !strings.Contains(stderr.String(), "stderr") { + t.Error("stderr buffer should contain stderr output") + } +} + +// TestExecCommandNoShellInterpolation verifies that special characters +// are NOT interpreted when passed in command array +func TestExecCommandNoShellInterpolation(t *testing.T) { + // These strings would be dangerous if passed to a shell + // But in array form, they're just literal strings + testCases := []struct { + name string + command []string + }{ + { + name: "semicolon injection", + command: []string{"echo", "safe; rm -rf /"}, + }, + { + name: "backtick injection", + command: []string{"echo", "`whoami`"}, + }, + { + name: "dollar injection", + command: []string{"echo", "$(id)"}, + }, + { + name: "pipe injection", + command: []string{"echo", "data | cat /etc/passwd"}, + }, + { + name: "redirect injection", + command: []string{"echo", "> /tmp/evil"}, + }, + { + name: "newline injection", + command: []string{"echo", "line1\nrm -rf /"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // In proper array exec, the "dangerous" part is always + // the second element - it's never parsed or split + if len(tc.command) != 2 { + t.Errorf("Expected 2-element array, got %d", len(tc.command)) + } + if tc.command[0] != "echo" { + t.Error("First element should be 'echo'") + } + // The second element contains the "dangerous" string + // but it's just a literal string, not executed + if tc.command[1] == "" { + t.Error("Second element should not be empty") + } + + // Key assertion: the command array length proves no shell parsing + // Shell would split "echo safe; rm -rf /" into multiple commands + // Array exec keeps it as ["echo", "safe; rm -rf /"] = 2 elements + }) + } +} diff --git a/shell/tracescript.go b/shell/tracescript.go new file mode 100644 index 0000000000..37332077a5 --- /dev/null +++ b/shell/tracescript.go @@ -0,0 +1,689 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package shell + +import ( + "encoding/binary" + "fmt" + "net" + "strings" +) + +// ScriptGenerator generates bpftrace scripts for network tracing. +// SECURITY: All IP addresses are converted to hex representation to prevent injection. +type ScriptGenerator struct { + config TraceConfig +} + +// NewScriptGenerator creates a new script generator with the given config. +func NewScriptGenerator(config TraceConfig) *ScriptGenerator { + return &ScriptGenerator{config: config} +} + +// Generate creates the complete bpftrace script. +func (g *ScriptGenerator) Generate() string { + var sb strings.Builder + + // Write script header + sb.WriteString(g.generateHeader()) + + // Write BEGIN block + sb.WriteString(g.generateBeginBlock()) + + // Conditionally write probes based on config + if g.config.EnableDrops { + sb.WriteString(g.generateDropTracepoint()) + } + + if g.config.EnableRST { + sb.WriteString(g.generateRSTTracepoints()) + } + + if g.config.EnableErrors { + sb.WriteString(g.generateSocketErrorTracepoint()) + } + + if g.config.EnableRetransmits { + sb.WriteString(g.generateRetransmitTracepoint()) + } + + if g.config.EnableNfqueueDrops { + sb.WriteString(g.generateNfqueueDropProbe()) + } + + // Write END block + sb.WriteString(g.generateEndBlock()) + + return sb.String() +} + +// generateHeader creates the script header with comments. +// Note: No #include directives - bpftrace uses BTF for struct access. +func (g *ScriptGenerator) generateHeader() string { + return `#!/usr/bin/env bpftrace +/* + * Network Issue Tracer - Generated by retina bpftrace + * Traces: + * - Packet drops with reason codes + * - TCP RST sent/received (connection failures) + * + * Note: Uses BTF for struct access (no kernel headers needed) + */ + +` +} + +// generateBeginBlock creates the BEGIN block with initialization. +func (g *ScriptGenerator) generateBeginBlock() string { + var sb strings.Builder + + sb.WriteString("BEGIN {\n") + + if g.config.OutputJSON { + sb.WriteString(` printf("{\"event\":\"start\",\"message\":\"Tracing network issues...\"}\n");`) + } else { + sb.WriteString(` printf("Tracing network issues... Press Ctrl-C to stop.\n\n");`) + sb.WriteString("\n") + sb.WriteString(` printf("%-12s %-10s %-18s %-18s %-18s %s\n",`) + sb.WriteString("\n") + sb.WriteString(` "TIME", "TYPE", "REASON", "STATE", "PROBE", "SRC -> DST");`) + sb.WriteString("\n") + sb.WriteString(` printf("────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\n");`) + } + + sb.WriteString("\n}\n\n") + return sb.String() +} + +// generateDropTracepoint creates the kfree_skb tracepoint for packet drops. +func (g *ScriptGenerator) generateDropTracepoint() string { + var sb strings.Builder + + // Tracepoint for packet drops + // Note: No pre-filter here because we need to parse skb to get IPs. + // IP filtering is done in the body after extracting addresses. + sb.WriteString("tracepoint:skb:kfree_skb\n") + sb.WriteString("{\n") + + // Extract packet info + sb.WriteString(` $skb = (struct sk_buff *)args->skbaddr; + $reason = args->reason; + + // Skip non-drop events (reason 0-2 are not real drops) + if ($reason <= 2) { return; } + + $protocol = $skb->protocol; + + // Only process IPv4 for now + if (bswap($protocol) != 0x0800) { return; } + + $iph = (struct iphdr *)($skb->head + $skb->network_header); + $saddr_raw = $iph->saddr; + $daddr_raw = $iph->daddr; + $saddr = ntop(2, $saddr_raw); + $daddr = ntop(2, $daddr_raw); + $ipproto = $iph->protocol; + +`) + + // Add IP/CIDR filter inside the body if specified + ipFilter := g.buildSkbIPFilterCondition() + if ipFilter != "" { + sb.WriteString(ipFilter) + } + + sb.WriteString(` $sport = (uint16)0; + $dport = (uint16)0; + + // Extract ports for TCP/UDP + if ($ipproto == 6 || $ipproto == 17) { + $transport_off = $skb->transport_header; + if ($transport_off > 0 && $transport_off < 65535) { + $th = $skb->head + $transport_off; + $sport = bswap(*(uint16*)($th)); + $dport = bswap(*(uint16*)($th + 2)); + } + } + +`) + + // Generate output based on format + if g.config.OutputJSON { + sb.WriteString(g.generateJSONOutput()) + } else { + sb.WriteString(g.generateTableOutput()) + } + + sb.WriteString("}\n\n") + return sb.String() +} + +// generateRSTTracepoints creates tracepoints for TCP RST events. +func (g *ScriptGenerator) generateRSTTracepoints() string { + var sb strings.Builder + + // TCP RST sent - when this host sends a RST + sb.WriteString(g.generateRSTSentTracepoint()) + + // TCP RST received - when this host receives a RST + sb.WriteString(g.generateRSTReceivedTracepoint()) + + return sb.String() +} + +// generateRSTSentTracepoint creates the tcp_send_reset tracepoint. +func (g *ScriptGenerator) generateRSTSentTracepoint() string { + var sb strings.Builder + + sb.WriteString("tracepoint:tcp:tcp_send_reset\n") + sb.WriteString("{\n") + + // Only process IPv4 (family == 2) + sb.WriteString(` if (args->family != 2) { return; } + +`) + + // Extract bytes into local vars first (required for BPF verifier) + if g.hasIPFilter() { + sb.WriteString(` // Read IP bytes into local vars for filtering + $s0 = args->saddr[0]; $s1 = args->saddr[1]; $s2 = args->saddr[2]; $s3 = args->saddr[3]; + $d0 = args->daddr[0]; $d1 = args->daddr[1]; $d2 = args->daddr[2]; $d3 = args->daddr[3]; + +`) + // Add IP filter using local vars + sb.WriteString(g.buildTCPIPFilterCheckFromLocalVars()) + } + + sb.WriteString(` $saddr = ntop(2, args->saddr); + $daddr = ntop(2, args->daddr); + $sport = args->sport; + $dport = args->dport; + +`) + + if g.config.OutputJSON { + sb.WriteString(` printf("{\"time\":\"%s\",\"type\":\"RST_SENT\",\"probe\":\"tcp_send_reset\",\"src_ip\":\"%s\",\"src_port\":%d,\"dst_ip\":\"%s\",\"dst_port\":%d}\n", + strftime("%H:%M:%S", nsecs), + $saddr, $sport, + $daddr, $dport); +`) + } else { + sb.WriteString(` printf("%-12s %-10s %-18s %-18s %-18s %s:%-5d -> %s:%-5d\n", + strftime("%H:%M:%S", nsecs), + "RST_SENT", + "-", + "-", + "tcp_send_reset", + $saddr, $sport, + $daddr, $dport); +`) + } + + sb.WriteString("}\n\n") + return sb.String() +} + +// generateRSTReceivedTracepoint creates the tcp_receive_reset tracepoint. +func (g *ScriptGenerator) generateRSTReceivedTracepoint() string { + var sb strings.Builder + + sb.WriteString("tracepoint:tcp:tcp_receive_reset\n") + sb.WriteString("{\n") + + // Only process IPv4 (family == 2) + sb.WriteString(` if (args->family != 2) { return; } + +`) + + // Extract bytes into local vars first (required for BPF verifier) + if g.hasIPFilter() { + sb.WriteString(` // Read IP bytes into local vars for filtering + $s0 = args->saddr[0]; $s1 = args->saddr[1]; $s2 = args->saddr[2]; $s3 = args->saddr[3]; + $d0 = args->daddr[0]; $d1 = args->daddr[1]; $d2 = args->daddr[2]; $d3 = args->daddr[3]; + +`) + // Add IP filter using local vars + sb.WriteString(g.buildTCPIPFilterCheckFromLocalVars()) + } + + sb.WriteString(` $saddr = ntop(2, args->saddr); + $daddr = ntop(2, args->daddr); + $sport = args->sport; + $dport = args->dport; + +`) + + if g.config.OutputJSON { + sb.WriteString(` printf("{\"time\":\"%s\",\"type\":\"RST_RECV\",\"probe\":\"tcp_receive_reset\",\"src_ip\":\"%s\",\"src_port\":%d,\"dst_ip\":\"%s\",\"dst_port\":%d}\n", + strftime("%H:%M:%S", nsecs), + $saddr, $sport, + $daddr, $dport); +`) + } else { + sb.WriteString(` printf("%-12s %-10s %-18s %-18s %-18s %s:%-5d -> %s:%-5d\n", + strftime("%H:%M:%S", nsecs), + "RST_RECV", + "-", + "-", + "tcp_receive_reset", + $saddr, $sport, + $daddr, $dport); +`) + } + + sb.WriteString("}\n\n") + return sb.String() +} + +// generateSocketErrorTracepoint creates the inet_sk_error_report tracepoint. +// This captures socket errors like ECONNREFUSED, ETIMEDOUT, etc. +func (g *ScriptGenerator) generateSocketErrorTracepoint() string { + var sb strings.Builder + + sb.WriteString("tracepoint:sock:inet_sk_error_report\n") + sb.WriteString("{\n") + + // Only process IPv4 (family == 2) and skip error=0 (not a real error) + sb.WriteString(` if (args->family != 2) { return; } + if (args->error == 0) { return; } // Skip non-error events (socket cleanup) + +`) + + // Extract bytes into local vars first (required for BPF verifier) + if g.hasIPFilter() { + sb.WriteString(` // Read IP bytes into local vars for filtering + $s0 = args->saddr[0]; $s1 = args->saddr[1]; $s2 = args->saddr[2]; $s3 = args->saddr[3]; + $d0 = args->daddr[0]; $d1 = args->daddr[1]; $d2 = args->daddr[2]; $d3 = args->daddr[3]; + +`) + // Add IP filter using local vars + sb.WriteString(g.buildTCPIPFilterCheckFromLocalVars()) + } + + sb.WriteString(` $saddr = ntop(2, args->saddr); + $daddr = ntop(2, args->daddr); + $sport = args->sport; + $dport = args->dport; + $error = args->error; + +`) + + if g.config.OutputJSON { + // JSON output: use error code only (parsers can decode POSIX errno) + sb.WriteString(` printf("{\"time\":\"%s\",\"type\":\"SOCK_ERR\",\"errno\":%d,\"probe\":\"inet_sk_error_report\",\"src_ip\":\"%s\",\"src_port\":%d,\"dst_ip\":\"%s\",\"dst_port\":%d}\n", + strftime("%H:%M:%S", nsecs), + $error, + $saddr, $sport, + $daddr, $dport); +`) + } else { + // Table output: decode errno to human-readable name + sb.WriteString(` // Decode common socket errno values (POSIX standard) + $errno_name = $error == 104 ? "ECONNRESET" : + $error == 110 ? "ETIMEDOUT" : + $error == 111 ? "ECONNREFUSED" : + $error == 113 ? "EHOSTUNREACH" : + $error == 101 ? "ENETUNREACH" : + $error == 99 ? "EADDRNOTAVAIL" : + $error == 112 ? "EHOSTDOWN" : + $error == 103 ? "ECONNABORTED" : + "UNKNOWN"; + + printf("%-12s %-10s %-18s %-18s %-18s %s:%-5d -> %s:%-5d\n", + strftime("%H:%M:%S", nsecs), + "SOCK_ERR", + $errno_name, + "-", + "inet_sk_error_report", + $saddr, $sport, + $daddr, $dport); +`) + } + + sb.WriteString("}\n\n") + return sb.String() +} + +// generateRetransmitTracepoint creates the tcp_retransmit_skb tracepoint. +// This captures TCP retransmissions which indicate packet loss or congestion. +func (g *ScriptGenerator) generateRetransmitTracepoint() string { + var sb strings.Builder + + sb.WriteString("tracepoint:tcp:tcp_retransmit_skb\n") + sb.WriteString("{\n") + + // Only process IPv4 (family == 2) + sb.WriteString(` if (args->family != 2) { return; } + +`) + + // Extract bytes into local vars first (required for BPF verifier) + if g.hasIPFilter() { + sb.WriteString(` // Read IP bytes into local vars for filtering + $s0 = args->saddr[0]; $s1 = args->saddr[1]; $s2 = args->saddr[2]; $s3 = args->saddr[3]; + $d0 = args->daddr[0]; $d1 = args->daddr[1]; $d2 = args->daddr[2]; $d3 = args->daddr[3]; + +`) + // Add IP filter using local vars + sb.WriteString(g.buildTCPIPFilterCheckFromLocalVars()) + } + + sb.WriteString(` $saddr = ntop(2, args->saddr); + $daddr = ntop(2, args->daddr); + $sport = args->sport; + $dport = args->dport; + $state = args->state; + +`) + + if g.config.OutputJSON { + // JSON: use numeric tcp_state to avoid BPF verifier complexity with string variables + // State values: 1=ESTABLISHED, 2=SYN_SENT, 3=SYN_RECV, 4=FIN_WAIT1, etc. + sb.WriteString(` printf("{\"time\":\"%s\",\"type\":\"RETRANS\",\"tcp_state\":%d,\"probe\":\"tcp_retransmit_skb\",\"src_ip\":\"%s\",\"src_port\":%d,\"dst_ip\":\"%s\",\"dst_port\":%d}\n", + strftime("%H:%M:%S", nsecs), + $state, + $saddr, $sport, + $daddr, $dport); +`) + } else { + // Table: decode TCP state to human-readable name (fixed values from include/net/tcp_states.h) + sb.WriteString(` $state_name = $state == 1 ? "ESTABLISHED" : + $state == 2 ? "SYN_SENT" : + $state == 3 ? "SYN_RECV" : + $state == 4 ? "FIN_WAIT1" : + $state == 5 ? "FIN_WAIT2" : + $state == 6 ? "TIME_WAIT" : + $state == 7 ? "CLOSE" : + $state == 8 ? "CLOSE_WAIT" : + $state == 9 ? "LAST_ACK" : + $state == 10 ? "LISTEN" : + $state == 11 ? "CLOSING" : + $state == 12 ? "NEW_SYN_RECV" : + "UNKNOWN"; + + printf("%-12s %-10s %-18s %-18s %-18s %s:%-5d -> %s:%-5d\n", + strftime("%H:%M:%S", nsecs), + "RETRANS", + "-", + $state_name, + "tcp_retransmit_skb", + $saddr, $sport, + $daddr, $dport); +`) + } + + sb.WriteString("}\n\n") + return sb.String() +} + +// generateNfqueueDropProbe creates the fexit probe for NFQUEUE drops. +// Detects packets sent to NFQUEUE when no userspace consumer is bound to the queue. +// Uses fexit:vmlinux:__nf_queue which provides access to both function arguments +// and return value in a single probe. Requires kernel >= 5.5 with BTF support. +// +// __nf_queue returns negative errno on failure: +// +// -ESRCH (-3): no queue handler registered (no consumer on the queue) +// -ENOMEM (-12): failed to allocate queue entry +// -ENETDOWN (-100): network interface down during queue +func (g *ScriptGenerator) generateNfqueueDropProbe() string { + var sb strings.Builder + + sb.WriteString("fexit:vmlinux:__nf_queue\n") + sb.WriteString("{\n") + + // Only report failures (retval < 0 means queue delivery failed) + sb.WriteString(` if (retval >= 0) { return; } + + $skb = args->skb; + + // Only process IPv4 + $protocol = $skb->protocol; + if (bswap($protocol) != 0x0800) { return; } + + $iph = (struct iphdr *)($skb->head + $skb->network_header); + $saddr_raw = $iph->saddr; + $daddr_raw = $iph->daddr; + +`) + + // Add IP/CIDR filter if specified (reuses the same bswap-based filter as kfree_skb) + ipFilter := g.buildSkbIPFilterCondition() + if ipFilter != "" { + sb.WriteString(ipFilter) + } + + sb.WriteString(` $saddr = ntop(2, $saddr_raw); + $daddr = ntop(2, $daddr_raw); + $queuenum = args->queuenum; + $ipproto = $iph->protocol; + + $sport = (uint16)0; + $dport = (uint16)0; + + // Extract ports for TCP/UDP + if ($ipproto == 6 || $ipproto == 17) { + $transport_off = $skb->transport_header; + if ($transport_off > 0 && $transport_off < 65535) { + $th = $skb->head + $transport_off; + $sport = bswap(*(uint16*)($th)); + $dport = bswap(*(uint16*)($th + 2)); + } + } + +`) + + if g.config.OutputJSON { + sb.WriteString(` printf("{\"time\":\"%s\",\"type\":\"NFQ_DROP\",\"queue\":%d,\"errno\":%d,\"probe\":\"__nf_queue\",\"src_ip\":\"%s\",\"src_port\":%d,\"dst_ip\":\"%s\",\"dst_port\":%d}\n", + strftime("%H:%M:%S", nsecs), + $queuenum, retval, + $saddr, $sport, + $daddr, $dport); +`) + } else { + sb.WriteString(` // Decode errno to human-readable name + $errno_name = retval == -3 ? "ESRCH" : + retval == -12 ? "ENOMEM" : + retval == -100 ? "ENETDOWN" : + "UNKNOWN"; + + printf("%-12s %-10s %-18s %-18s %-18s %s:%-5d -> %s:%-5d\n", + strftime("%H:%M:%S", nsecs), + "NFQ_DROP", + $errno_name, + "-", + "__nf_queue", + $saddr, $sport, + $daddr, $dport); +`) + } + + sb.WriteString("}\n\n") + return sb.String() +} + +// hasIPFilter returns true if any IP or CIDR filter is configured. +func (g *ScriptGenerator) hasIPFilter() bool { + return len(g.config.FilterIPs) > 0 || len(g.config.FilterCIDRs) > 0 +} + +// buildTCPIPFilterCheckFromLocalVars creates a filter using local vars $s0-$s3 and $d0-$d3. +// This is required because the BPF verifier doesn't allow reading from different +// offsets of args after a probe_read. We read bytes into local vars first, then filter. +// SECURITY: IPs are converted to integer byte values - no string interpolation. +func (g *ScriptGenerator) buildTCPIPFilterCheckFromLocalVars() string { + if len(g.config.FilterIPs) == 0 && len(g.config.FilterCIDRs) == 0 { + return "" // No filter + } + + var conditions []string + + // Add IP filters - compare using local vars + for _, ip := range g.config.FilterIPs { + ipv4 := ip.To4() + if ipv4 == nil { + continue + } + // Match either source or destination using local vars + conditions = append(conditions, fmt.Sprintf( + "(($s0 == %d && $s1 == %d && $s2 == %d && $s3 == %d) || ($d0 == %d && $d1 == %d && $d2 == %d && $d3 == %d))", + ipv4[0], ipv4[1], ipv4[2], ipv4[3], + ipv4[0], ipv4[1], ipv4[2], ipv4[3])) + } + + // Add CIDR filters - mask and compare using local vars + for _, cidr := range g.config.FilterCIDRs { + if cidr == nil { + continue + } + ipv4 := cidr.IP.To4() + mask := cidr.Mask + if ipv4 == nil || len(mask) != 4 { + continue + } + // (byte & mask) == network_byte for each byte + conditions = append(conditions, fmt.Sprintf( + "((($s0 & %d) == %d && ($s1 & %d) == %d && ($s2 & %d) == %d && ($s3 & %d) == %d) || (($d0 & %d) == %d && ($d1 & %d) == %d && ($d2 & %d) == %d && ($d3 & %d) == %d))", + mask[0], ipv4[0], mask[1], ipv4[1], mask[2], ipv4[2], mask[3], ipv4[3], + mask[0], ipv4[0], mask[1], ipv4[1], mask[2], ipv4[2], mask[3], ipv4[3])) + } + + if len(conditions) == 0 { + return "" + } + + // Generate an if-statement that returns early if no condition matches + combined := strings.Join(conditions, " || ") + return fmt.Sprintf(" // IP/CIDR filter: skip if not matching\n if (!(%s)) { return; }\n\n", combined) +} + +// generateTableOutput generates printf for table format. +func (g *ScriptGenerator) generateTableOutput() string { + return ` // Format source and destination with numeric reason code + if ($sport > 0) { + printf("%-12s %-10s %-18d %-18s %-18s %s:%-5d -> %s:%-5d\n", + strftime("%H:%M:%S", nsecs), + "DROP", + $reason, + "-", + "kfree_skb", + $saddr, $sport, + $daddr, $dport); + } else { + printf("%-12s %-10s %-18d %-18s %-18s %s -> %s\n", + strftime("%H:%M:%S", nsecs), + "DROP", + $reason, + "-", + "kfree_skb", + $saddr, + $daddr); + } +` +} + +// generateJSONOutput generates printf for JSON format. +func (g *ScriptGenerator) generateJSONOutput() string { + return ` // Output JSON with numeric reason code + printf("{\"time\":\"%s\",\"type\":\"DROP\",\"reason_code\":%d,\"probe\":\"kfree_skb\",\"src_ip\":\"%s\",\"src_port\":%d,\"dst_ip\":\"%s\",\"dst_port\":%d}\n", + strftime("%H:%M:%S", nsecs), + $reason, + $saddr, $sport, + $daddr, $dport); +` +} + +// generateEndBlock creates the END block. +func (g *ScriptGenerator) generateEndBlock() string { + if g.config.OutputJSON { + return `END { + printf("{\"event\":\"end\",\"message\":\"Trace complete\"}\n"); +} +` + } + return `END { + printf("\n────────────────────────────────────────────────────────────────────────────────────────────────────────────\n"); + printf("Trace complete.\n"); +} +` +} + +// buildSkbIPFilterCondition creates an if-statement to filter by IP inside kfree_skb body. +// This is used instead of a pre-filter because we need to parse the skb to get the IPs. +// SECURITY: IPs are converted to hex integers - no string interpolation of user input. +// +// IMPORTANT: $saddr_raw/$daddr_raw come from $iph->saddr which is __be32 (big-endian). +// bpftrace reads struct fields as native integers, so on little-endian (x86) the value +// is byte-swapped relative to network order. We use bswap() to convert back to network +// byte order before comparing with the big-endian hex constants from ipToHex(). +// This mirrors how $skb->protocol is handled: bswap($protocol) != 0x0800. +func (g *ScriptGenerator) buildSkbIPFilterCondition() string { + if len(g.config.FilterIPs) == 0 && len(g.config.FilterCIDRs) == 0 { + return "" // No filter + } + + var conditions []string + + // Add IP filters - compare against the raw uint32 IP values + // bswap() converts $saddr_raw from host byte order to network byte order + for _, ip := range g.config.FilterIPs { + ipv4 := ip.To4() + if ipv4 == nil { + continue + } + ipHex := ipToHex(ipv4) + // Match either source or destination (bswap to network byte order first) + conditions = append(conditions, fmt.Sprintf("(bswap($saddr_raw) == 0x%08x || bswap($daddr_raw) == 0x%08x)", ipHex, ipHex)) + } + + // Add CIDR filters - mask and compare + // bswap() converts to network byte order before masking + for _, cidr := range g.config.FilterCIDRs { + if cidr == nil { + continue + } + ipv4 := cidr.IP.To4() + if ipv4 == nil { + continue + } + networkHex := ipToHex(ipv4) + maskHex := ipToHex(net.IP(cidr.Mask).To4()) + conditions = append(conditions, fmt.Sprintf("((bswap($saddr_raw) & 0x%08x) == 0x%08x || (bswap($daddr_raw) & 0x%08x) == 0x%08x)", + maskHex, networkHex, maskHex, networkHex)) + } + + if len(conditions) == 0 { + return "" + } + + // Generate an if-statement that returns early if no condition matches + combined := strings.Join(conditions, " || ") + return fmt.Sprintf(" // IP/CIDR filter: skip if not matching\n if (!(%s)) { return; }\n\n", combined) +} + +// ipToHex converts an IPv4 address to a uint32 in network byte order. +// SECURITY: This function only outputs hex digits - no user input passes through. +func ipToHex(ip net.IP) uint32 { + ipv4 := ip.To4() + if ipv4 == nil { + return 0 + } + // Network byte order (big endian) - this matches how IPs are stored in IP headers + return binary.BigEndian.Uint32(ipv4) +} + +// DropReasonsCommand returns the command to fetch SKB drop reason enum from kernel. +// The enum values are kernel-version specific and must be read at runtime. +// This reads from the tracepoint format file which contains the enum definition. +func DropReasonsCommand() []string { + return []string{ + "sh", "-c", + `echo "=== SKB Drop Reason Codes (kernel-specific) ===" && ` + + `cat /sys/kernel/debug/tracing/events/skb/kfree_skb/format 2>/dev/null | ` + + `grep -oE '\{ [0-9]+, "[^"]+" \}' | ` + + `sed 's/{ \([0-9]*\), "\([^"]*\)" }/\1 = \2/' | ` + + `head -30 || ` + + `echo "(Could not read drop reasons - requires debugfs mounted)"`, + } +} diff --git a/shell/tracescript_test.go b/shell/tracescript_test.go new file mode 100644 index 0000000000..dd928859ab --- /dev/null +++ b/shell/tracescript_test.go @@ -0,0 +1,635 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package shell + +import ( + "net" + "strings" + "testing" +) + +func TestIPToHex(t *testing.T) { + tests := []struct { + name string + ip string + expected uint32 + }{ + { + name: "10.0.0.1", + ip: "10.0.0.1", + expected: 0x0a000001, + }, + { + name: "192.168.1.100", + ip: "192.168.1.100", + expected: 0xc0a80164, + }, + { + name: "0.0.0.0", + ip: "0.0.0.0", + expected: 0x00000000, + }, + { + name: "255.255.255.255", + ip: "255.255.255.255", + expected: 0xffffffff, + }, + { + name: "127.0.0.1", + ip: "127.0.0.1", + expected: 0x7f000001, + }, + { + name: "172.16.0.1", + ip: "172.16.0.1", + expected: 0xac100001, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ip := net.ParseIP(tt.ip) + if ip == nil { + t.Fatalf("failed to parse IP: %s", tt.ip) + } + result := ipToHex(ip) + if result != tt.expected { + t.Errorf("ipToHex(%s) = 0x%08x, want 0x%08x", tt.ip, result, tt.expected) + } + }) + } +} + +func TestIPToHexOnlyHexOutput(t *testing.T) { + // SECURITY TEST: Verify that output only contains hex digits + // This ensures no injection is possible through IP addresses + ips := []string{ + "10.0.0.1", + "192.168.1.100", + "172.16.255.255", + "0.0.0.0", + "255.255.255.255", + } + + for _, ipStr := range ips { + ip := net.ParseIP(ipStr) + if ip == nil { + t.Fatalf("failed to parse IP: %s", ipStr) + } + + hex := ipToHex(ip) + // Convert to string representation + formatted := strings.ToLower(string([]byte{ + '0', 'x', + hexChar((hex >> 28) & 0xf), + hexChar((hex >> 24) & 0xf), + hexChar((hex >> 20) & 0xf), + hexChar((hex >> 16) & 0xf), + hexChar((hex >> 12) & 0xf), + hexChar((hex >> 8) & 0xf), + hexChar((hex >> 4) & 0xf), + hexChar(hex & 0xf), + })) + + // Verify only contains 0-9, a-f, x + for _, c := range formatted { + if (c < '0' || c > '9') && (c < 'a' || c > 'f') && c != 'x' { + t.Errorf("ipToHex(%s) produced non-hex character: %c in %s", ipStr, c, formatted) + } + } + } +} + +func hexChar(b uint32) byte { + // Mask to 4 bits to guarantee 0-15 range and prevent G115 overflow. + n := byte(b & 0xf) + if n < 10 { + return '0' + n + } + return 'a' + n - 10 +} + +func TestGenerateDropScript(t *testing.T) { + config := TraceConfig{ + EnableDrops: true, + EnableRST: true, + EnableErrors: true, + EnableRetransmits: true, + } + + gen := NewScriptGenerator(config) + script := gen.Generate() + + // Verify script structure + if !strings.Contains(script, "#!/usr/bin/env bpftrace") { + t.Error("script missing shebang") + } + if !strings.Contains(script, "tracepoint:skb:kfree_skb") { + t.Error("script missing kfree_skb tracepoint") + } + if !strings.Contains(script, "BEGIN {") { + t.Error("script missing BEGIN block") + } + if !strings.Contains(script, "END {") { + t.Error("script missing END block") + } + if !strings.Contains(script, "$reason") { + t.Error("script missing reason variable") + } + if !strings.Contains(script, "DROP") { + t.Error("script missing DROP output") + } +} + +func TestGenerateDropScriptNoFilter(t *testing.T) { + config := TraceConfig{ + EnableDrops: true, + } + + gen := NewScriptGenerator(config) + filter := gen.buildSkbIPFilterCondition() + + // No filter should be empty + if filter != "" { + t.Errorf("expected empty filter, got: %s", filter) + } +} + +func TestGenerateDropScriptWithIPFilter(t *testing.T) { + ip := net.ParseIP("10.0.0.1") + config := TraceConfig{ + FilterIPs: []net.IP{ip}, + OutputJSON: false, + } + + gen := NewScriptGenerator(config) + filter := gen.buildSkbIPFilterCondition() + + // Should contain hex representation + if !strings.Contains(filter, "0x0a000001") { + t.Errorf("expected filter to contain hex IP 0x0a000001, got: %s", filter) + } + + // Should use bswap() to handle endianness (kfree_skb reads __be32 as native int) + if !strings.Contains(filter, "bswap($saddr_raw)") || !strings.Contains(filter, "bswap($daddr_raw)") { + t.Errorf("expected filter to use bswap() for endianness conversion, got: %s", filter) + } + + // Should NOT contain the original IP string (security check) + if strings.Contains(filter, "10.0.0.1") { + t.Error("filter should not contain original IP string - security risk") + } +} + +func TestGenerateDropScriptWithCIDRFilter(t *testing.T) { + _, cidr, err := net.ParseCIDR("10.0.0.0/24") + if err != nil { + t.Fatalf("failed to parse CIDR: %v", err) + } + + config := TraceConfig{ + FilterCIDRs: []*net.IPNet{cidr}, + OutputJSON: false, + } + + gen := NewScriptGenerator(config) + filter := gen.buildSkbIPFilterCondition() + + // Should contain hex network + if !strings.Contains(filter, "0x0a000000") { + t.Errorf("expected filter to contain hex network 0x0a000000, got: %s", filter) + } + + // Should contain hex mask for /24 + if !strings.Contains(filter, "0xffffff00") { + t.Errorf("expected filter to contain hex mask 0xffffff00, got: %s", filter) + } + + // Should use bswap() for endianness + if !strings.Contains(filter, "bswap($saddr_raw)") { + t.Errorf("expected filter to use bswap() for endianness conversion, got: %s", filter) + } +} + +func TestGenerateDropScriptJSONOutput(t *testing.T) { + config := TraceConfig{ + OutputJSON: true, + EnableDrops: true, + EnableRST: true, + EnableErrors: true, + EnableRetransmits: true, + } + + gen := NewScriptGenerator(config) + script := gen.Generate() + + // JSON output should have JSON-formatted printf statements + // Note: In the bpftrace script, quotes are escaped with backslash + if !strings.Contains(script, `\"type\":\"DROP\"`) { + t.Error("JSON script missing DROP type field") + } + if !strings.Contains(script, `\"reason_code\"`) { + t.Error("JSON script missing reason_code field") + } + if !strings.Contains(script, `\"src_ip\"`) { + t.Error("JSON script missing src_ip field") + } +} + +func TestGenerateDropScriptTableOutput(t *testing.T) { + config := TraceConfig{ + EnableDrops: true, + EnableRST: true, + EnableErrors: true, + EnableRetransmits: true, + } + + gen := NewScriptGenerator(config) + script := gen.Generate() + + // Table output should have header + if !strings.Contains(script, "TIME") { + t.Error("table script missing TIME header") + } + if !strings.Contains(script, "TYPE") { + t.Error("table script missing TYPE header") + } + if !strings.Contains(script, "REASON") { + t.Error("table script missing REASON header") + } + if !strings.Contains(script, "STATE") { + t.Error("table script missing STATE header") + } + if !strings.Contains(script, "PROBE") { + t.Error("table script missing PROBE header") + } +} + +func TestScriptGeneratorNoUserStringInterpolation(t *testing.T) { + // SECURITY TEST: Verify that user-provided values are never + // directly interpolated as strings into the script + + // Try various concerning inputs - all should be rejected by validation + // but even if they made it here, they should be safe + + // Create a generator with an IP that could be injection attempt + // Note: This IP is valid but we verify it's converted to hex + ip := net.ParseIP("127.0.0.1") + config := TraceConfig{ + FilterIPs: []net.IP{ip}, + OutputJSON: false, + } + + gen := NewScriptGenerator(config) + filter := gen.buildSkbIPFilterCondition() + + // The node name should NOT appear in the filter + if strings.Contains(filter, "evil-node") { + t.Error("node name should not appear in filter condition") + } + + // The filter should only contain safe characters + // Allow: hex digits, whitespace, operators, struct names, etc. + // Disallow: semicolons, backticks, $() syntax + dangerousPatterns := []string{ + "`;", + "$()", + "`", + "system(", + "exec(", + } + + for _, pattern := range dangerousPatterns { + if strings.Contains(filter, pattern) { + t.Errorf("filter contains dangerous pattern: %s", pattern) + } + } +} + +func TestBuildIPFilter(t *testing.T) { + tests := []struct { + name string + ips []string + expectHexIPs []string + noStringIPs bool // Verify original strings don't appear + }{ + { + name: "single IP", + ips: []string{"10.0.0.1"}, + expectHexIPs: []string{"0x0a000001"}, + noStringIPs: true, + }, + { + name: "multiple IPs", + ips: []string{"10.0.0.1", "192.168.1.1"}, + expectHexIPs: []string{"0x0a000001", "0xc0a80101"}, + noStringIPs: true, + }, + { + name: "Class B network", + ips: []string{"172.16.0.1"}, + expectHexIPs: []string{"0xac100001"}, + noStringIPs: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var parsedIPs []net.IP + for _, ipStr := range tt.ips { + ip := net.ParseIP(ipStr) + if ip == nil { + t.Fatalf("failed to parse IP: %s", ipStr) + } + parsedIPs = append(parsedIPs, ip) + } + + config := TraceConfig{ + FilterIPs: parsedIPs, + OutputJSON: false, + } + + gen := NewScriptGenerator(config) + filter := gen.buildSkbIPFilterCondition() + + // Verify hex IPs are present + for _, hexIP := range tt.expectHexIPs { + if !strings.Contains(filter, hexIP) { + t.Errorf("expected filter to contain %s, got: %s", hexIP, filter) + } + } + + // Verify original string IPs are NOT present (security) + if tt.noStringIPs { + for _, ipStr := range tt.ips { + if strings.Contains(filter, ipStr) { + t.Errorf("filter should not contain original IP string %s - security risk", ipStr) + } + } + } + }) + } +} + +func TestBuildCIDRFilter(t *testing.T) { + tests := []struct { + name string + cidr string + expectNet string // hex network address + expectMask string // hex mask + }{ + { + name: "/24 network", + cidr: "10.0.0.0/24", + expectNet: "0x0a000000", + expectMask: "0xffffff00", + }, + { + name: "/16 network", + cidr: "172.16.0.0/16", + expectNet: "0xac100000", + expectMask: "0xffff0000", + }, + { + name: "/8 network", + cidr: "10.0.0.0/8", + expectNet: "0x0a000000", + expectMask: "0xff000000", + }, + { + name: "/32 single host", + cidr: "192.168.1.100/32", + expectNet: "0xc0a80164", + expectMask: "0xffffffff", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, cidr, err := net.ParseCIDR(tt.cidr) + if err != nil { + t.Fatalf("failed to parse CIDR: %v", err) + } + + config := TraceConfig{ + FilterCIDRs: []*net.IPNet{cidr}, + OutputJSON: false, + } + + gen := NewScriptGenerator(config) + filter := gen.buildSkbIPFilterCondition() + + if !strings.Contains(filter, tt.expectNet) { + t.Errorf("expected filter to contain network %s, got: %s", tt.expectNet, filter) + } + if !strings.Contains(filter, tt.expectMask) { + t.Errorf("expected filter to contain mask %s, got: %s", tt.expectMask, filter) + } + }) + } +} + +func TestMixedIPAndCIDRFilter(t *testing.T) { + ip := net.ParseIP("10.0.0.1") + _, cidr, _ := net.ParseCIDR("192.168.0.0/16") + + config := TraceConfig{ + FilterIPs: []net.IP{ip}, + FilterCIDRs: []*net.IPNet{cidr}, + OutputJSON: false, + } + + gen := NewScriptGenerator(config) + filter := gen.buildSkbIPFilterCondition() + + // Should contain both IP and CIDR hex values + if !strings.Contains(filter, "0x0a000001") { + t.Error("filter missing IP hex value") + } + if !strings.Contains(filter, "0xc0a80000") { + t.Error("filter missing CIDR network hex value") + } + + // Conditions should be combined with OR + if !strings.Contains(filter, "||") { + t.Error("filter should combine conditions with OR") + } +} + +func TestDropReasonsCommand(t *testing.T) { + cmd := DropReasonsCommand() + + // Should be a shell command + if len(cmd) != 3 { + t.Errorf("expected 3 elements (sh -c cmd), got %d", len(cmd)) + } + if cmd[0] != "sh" || cmd[1] != "-c" { + t.Error("expected sh -c prefix") + } + + // Should read from the tracepoint format file + if !strings.Contains(cmd[2], "kfree_skb/format") { + t.Error("command should read from kfree_skb/format") + } + + // Should have fallback message + if !strings.Contains(cmd[2], "Could not read") { + t.Error("command should have fallback error message") + } +} + +func TestScriptUsesNumericReasonCode(t *testing.T) { + config := TraceConfig{ + EnableDrops: true, + } + + gen := NewScriptGenerator(config) + script := gen.Generate() + + // Script should NOT contain hardcoded reason names + hardcodedReasons := []string{ + "NO_SOCKET", + "NETFILTER_DROP", + "OTHERHOST", + } + + for _, reason := range hardcodedReasons { + if strings.Contains(script, reason) { + t.Errorf("script should not hardcode reason name: %s", reason) + } + } + + // Script should use $reason (numeric) in output + if !strings.Contains(script, "$reason") { + t.Error("script should use $reason variable for numeric code") + } +} + +func TestGenerateNfqueueDropProbe(t *testing.T) { + config := TraceConfig{ + EnableNfqueueDrops: true, + } + + gen := NewScriptGenerator(config) + script := gen.Generate() + + // Verify fexit probe is present + if !strings.Contains(script, "fexit:vmlinux:__nf_queue") { + t.Error("script missing fexit:vmlinux:__nf_queue probe") + } + + // Verify it checks return value + if !strings.Contains(script, "retval") { + t.Error("script missing retval check") + } + + // Verify it accesses args->skb + if !strings.Contains(script, "args->skb") { + t.Error("script missing args->skb access") + } + + // Verify it reads queue number + if !strings.Contains(script, "args->queuenum") { + t.Error("script missing args->queuenum access") + } + + // Verify NFQ_DROP event type in table output + if !strings.Contains(script, "NFQ_DROP") { + t.Error("script missing NFQ_DROP event type") + } + + // Verify __nf_queue probe name in output + if !strings.Contains(script, "__nf_queue") { + t.Error("script missing __nf_queue probe name in output") + } + + // Verify errno decoding + if !strings.Contains(script, "ESRCH") { + t.Error("script missing ESRCH errno name") + } + if !strings.Contains(script, "ENOMEM") { + t.Error("script missing ENOMEM errno name") + } +} + +func TestGenerateNfqueueDropProbeJSON(t *testing.T) { + config := TraceConfig{ + EnableNfqueueDrops: true, + OutputJSON: true, + } + + gen := NewScriptGenerator(config) + script := gen.Generate() + + // Verify JSON output fields + if !strings.Contains(script, `\"type\":\"NFQ_DROP\"`) { + t.Error("JSON script missing NFQ_DROP type field") + } + if !strings.Contains(script, `\"queue\"`) { + t.Error("JSON script missing queue field") + } + if !strings.Contains(script, `\"errno\"`) { + t.Error("JSON script missing errno field") + } + if !strings.Contains(script, `\"probe\":\"__nf_queue\"`) { + t.Error("JSON script missing __nf_queue probe field") + } +} + +func TestGenerateNfqueueDropProbeWithIPFilter(t *testing.T) { + ip := net.ParseIP("10.0.0.1") + config := TraceConfig{ + EnableNfqueueDrops: true, + FilterIPs: []net.IP{ip}, + } + + gen := NewScriptGenerator(config) + script := gen.Generate() + + // Should contain fexit probe + if !strings.Contains(script, "fexit:vmlinux:__nf_queue") { + t.Error("script missing fexit probe") + } + + // Should contain IP filter hex + if !strings.Contains(script, "0x0a000001") { + t.Error("script missing hex IP in NFQUEUE probe filter") + } + + // Should use bswap for filter + if !strings.Contains(script, "bswap($saddr_raw)") { + t.Error("script missing bswap in NFQUEUE probe filter") + } +} + +func TestGenerateAllProbesIncludingNfqueue(t *testing.T) { + // Verify all probes can be generated together + config := TraceConfig{ + EnableDrops: true, + EnableRST: true, + EnableErrors: true, + EnableRetransmits: true, + EnableNfqueueDrops: true, + } + + gen := NewScriptGenerator(config) + script := gen.Generate() + + // All probes should be present + if !strings.Contains(script, "tracepoint:skb:kfree_skb") { + t.Error("script missing kfree_skb probe") + } + if !strings.Contains(script, "tcp_send_reset") { + t.Error("script missing RST probe") + } + if !strings.Contains(script, "inet_sk_error_report") { + t.Error("script missing socket error probe") + } + if !strings.Contains(script, "tcp_retransmit_skb") { + t.Error("script missing retransmit probe") + } + if !strings.Contains(script, "fexit:vmlinux:__nf_queue") { + t.Error("script missing NFQUEUE probe") + } +} diff --git a/site/docusaurus.config.ts b/site/docusaurus.config.ts index 33729a1c61..3fdbc12587 100644 --- a/site/docusaurus.config.ts +++ b/site/docusaurus.config.ts @@ -12,7 +12,6 @@ const config = { organizationName: 'Azure', projectName: 'Retina', onBrokenLinks: 'throw', - onBrokenMarkdownLinks: 'warn', i18n: { defaultLocale: 'en', @@ -22,10 +21,24 @@ const config = { markdown: { format: "detect", mermaid: true, + hooks: { + onBrokenMarkdownLinks: 'warn', + }, }, plugins: [ "docusaurus-lunr-search", + [ + "@docusaurus/plugin-client-redirects", + { + redirects: [ + { + from: ["/docs/intro", "/intro"], + to: "/docs/Introduction/intro", + }, + ], + }, + ], [ "@docusaurus/plugin-ideal-image", { @@ -55,12 +68,12 @@ const config = { 'classic', { docs: { - sidebarPath: require.resolve('./sidebars.js'), + sidebarPath: './sidebars.ts', path: '../docs', editUrl: 'https://github.com/microsoft/retina/blob/main/docs', }, theme: { - customCss: require.resolve('./src/css/custom.css'), + customCss: './src/css/custom.css', }, }, ], diff --git a/site/package-lock.json b/site/package-lock.json index 1c3345cdaf..6c0c7e5b2e 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -8,250 +8,173 @@ "name": "retina", "version": "0.0.0", "dependencies": { - "@docusaurus/core": "^3.6.1", - "@docusaurus/plugin-ideal-image": "^3.6.1", - "@docusaurus/preset-classic": "^3.6.1", - "@mdx-js/react": "^3.0.0", - "@types/react": "^18.3.7", - "clsx": "^2.0.0", - "docusaurus-lunr-search": "^3.5.0", + "@docusaurus/core": "^3.10.1", + "@docusaurus/plugin-client-redirects": "^3.10.1", + "@docusaurus/plugin-ideal-image": "^3.10.1", + "@docusaurus/preset-classic": "^3.10.1", + "@mdx-js/react": "^3.1.1", + "@types/react": "^19.2.15", + "clsx": "^2.1.1", + "docusaurus-lunr-search": "^3.6.0", "micromatch": "^4.0.8", - "prism-react-renderer": "^2.3.1", - "react": "^18.3.1", - "react-dom": "^18.2.0", - "sharp": "^0.33.5" + "prism-react-renderer": "^2.4.1", + "react": "^19.2.5", + "react-dom": "^19.2.6", + "sharp": "^0.34.5" }, "devDependencies": { - "@docusaurus/module-type-aliases": "^3.5.2", - "@docusaurus/tsconfig": "^3.5.2", - "@docusaurus/types": "^3.5.2", - "css-loader": "^7.1.2", + "@docusaurus/module-type-aliases": "^3.9.2", + "@docusaurus/tsconfig": "^3.10.1", + "@docusaurus/types": "^3.10.1", + "css-loader": "^7.1.4", "style-loader": "^4.0.0", - "typescript": "^5.6.2" + "typescript": "^6.0.3" }, "engines": { - "node": ">=16.14" + "node": ">=18.0.0" } }, - "node_modules/@algolia/autocomplete-core": { - "version": "1.17.6", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.6.tgz", - "integrity": "sha512-lkDoW4I7h2kKlIgf3pUt1LqvxyYKkVyiypoGLlUnhPSnCpmeOwudM6rNq6YYsCmdQtnDQoW5lUNNuj6ASg3qeg==", + "node_modules/@algolia/abtesting": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.18.1.tgz", + "integrity": "sha512-aehCadlWOGvrT91KUIZpC0MbB8KBW9yUuvTJFd2xesR7le/IsT4nJUnjCCZ4ZqZCeTcPHPV5mo//fZ5oxcSVYw==", "license": "MIT", "dependencies": { - "@algolia/autocomplete-plugin-algolia-insights": "1.17.6", - "@algolia/autocomplete-shared": "1.17.6" + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@algolia/autocomplete-plugin-algolia-insights": { - "version": "1.17.6", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.6.tgz", - "integrity": "sha512-17NnaacuFzSWVuZu4NKzVeaFIe9Abpw8w+/gjc7xhZFtqj+GadufzodIdchwiB2eM2cDdiR3icW7gbNTB3K2YA==", + "node_modules/@algolia/autocomplete-core": { + "version": "1.19.8", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.19.8.tgz", + "integrity": "sha512-3YEorYg44niXcm7gkft3nXYItHd44e8tmh4D33CTszPgP0QWkaLEaFywiNyJBo7UL/mqObA/G9RYuU7R8tN1IA==", "license": "MIT", "dependencies": { - "@algolia/autocomplete-shared": "1.17.6" - }, - "peerDependencies": { - "search-insights": ">= 1 < 3" + "@algolia/autocomplete-plugin-algolia-insights": "1.19.8", + "@algolia/autocomplete-shared": "1.19.8" } }, - "node_modules/@algolia/autocomplete-preset-algolia": { - "version": "1.17.6", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.6.tgz", - "integrity": "sha512-Cvg5JENdSCMuClwhJ1ON1/jSuojaYMiUW2KePm18IkdCzPJj/NXojaOxw58RFtQFpJgfVW8h2E8mEoDtLlMdeA==", + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.19.8", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.8.tgz", + "integrity": "sha512-ZvJWO8ZZJDpc1LNM2TTBdmQsZBLMR4rU5iNR2OYvEeFBiaf/0ESnRSSLQbryarJY4SVxtoz6A2ZtDMNM+iQEAA==", "license": "MIT", "dependencies": { - "@algolia/autocomplete-shared": "1.17.6" + "@algolia/autocomplete-shared": "1.19.8" }, "peerDependencies": { - "@algolia/client-search": ">= 4.9.1 < 6", - "algoliasearch": ">= 4.9.1 < 6" + "search-insights": ">= 1 < 3" } }, "node_modules/@algolia/autocomplete-shared": { - "version": "1.17.6", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.6.tgz", - "integrity": "sha512-aq/3V9E00Tw2GC/PqgyPGXtqJUlVc17v4cn1EUhSc+O/4zd04Uwb3UmPm8KDaYQQOrkt1lwvCj2vG2wRE5IKhw==", + "version": "1.19.8", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.8.tgz", + "integrity": "sha512-h5hf2t8ejF6vlOgvLaZzQbWs5SyH2z4PAWygNAvvD/2RI29hdQ54ldUGwqVuj9Srs+n8XUKTPUqb7fvhBhQrnQ==", "license": "MIT", "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", "algoliasearch": ">= 4.9.1 < 6" } }, - "node_modules/@algolia/cache-browser-local-storage": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.24.0.tgz", - "integrity": "sha512-t63W9BnoXVrGy9iYHBgObNXqYXM3tYXCjDSHeNwnsc324r4o5UiVKUiAB4THQ5z9U5hTj6qUvwg/Ez43ZD85ww==", - "license": "MIT", - "dependencies": { - "@algolia/cache-common": "4.24.0" - } - }, - "node_modules/@algolia/cache-common": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.24.0.tgz", - "integrity": "sha512-emi+v+DmVLpMGhp0V9q9h5CdkURsNmFC+cOS6uK9ndeJm9J4TiqSvPYVu+THUP8P/S08rxf5x2P+p3CfID0Y4g==", - "license": "MIT" - }, - "node_modules/@algolia/cache-in-memory": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.24.0.tgz", - "integrity": "sha512-gDrt2so19jW26jY3/MkFg5mEypFIPbPoXsQGQWAi6TrCPsNOSEYepBMPlucqWigsmEy/prp5ug2jy/N3PVG/8w==", - "license": "MIT", - "dependencies": { - "@algolia/cache-common": "4.24.0" - } - }, "node_modules/@algolia/client-abtesting": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.13.0.tgz", - "integrity": "sha512-6CoQjlMi1pmQYMQO8tXfuGxSPf6iKX5FP9MuMe6IWmvC81wwTvOehnwchyBl2wuPVhcw2Ar53K53mQ60DAC64g==", + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.52.1.tgz", + "integrity": "sha512-HmXOGBOAOJPounpBzBpuY0zDYeiCpxgHnQmuA7JO6ScukcBdGp3/XM9zJk5pJx/xNGD68mbPGXWpDxGtl6BwDQ==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.13.0", - "@algolia/requester-browser-xhr": "5.13.0", - "@algolia/requester-fetch": "5.13.0", - "@algolia/requester-node-http": "5.13.0" + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" }, "engines": { "node": ">= 14.0.0" } }, - "node_modules/@algolia/client-account": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.24.0.tgz", - "integrity": "sha512-adcvyJ3KjPZFDybxlqnf+5KgxJtBjwTPTeyG2aOyoJvx0Y8dUQAEOEVOJ/GBxX0WWNbmaSrhDURMhc+QeevDsA==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "4.24.0", - "@algolia/client-search": "4.24.0", - "@algolia/transporter": "4.24.0" - } - }, - "node_modules/@algolia/client-account/node_modules/@algolia/client-common": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.24.0.tgz", - "integrity": "sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==", - "license": "MIT", - "dependencies": { - "@algolia/requester-common": "4.24.0", - "@algolia/transporter": "4.24.0" - } - }, - "node_modules/@algolia/client-account/node_modules/@algolia/client-search": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.24.0.tgz", - "integrity": "sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "4.24.0", - "@algolia/requester-common": "4.24.0", - "@algolia/transporter": "4.24.0" - } - }, "node_modules/@algolia/client-analytics": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.24.0.tgz", - "integrity": "sha512-y8jOZt1OjwWU4N2qr8G4AxXAzaa8DBvyHTWlHzX/7Me1LX8OayfgHexqrsL4vSBcoMmVw2XnVW9MhL+Y2ZDJXg==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "4.24.0", - "@algolia/client-search": "4.24.0", - "@algolia/requester-common": "4.24.0", - "@algolia/transporter": "4.24.0" - } - }, - "node_modules/@algolia/client-analytics/node_modules/@algolia/client-common": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.24.0.tgz", - "integrity": "sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==", - "license": "MIT", - "dependencies": { - "@algolia/requester-common": "4.24.0", - "@algolia/transporter": "4.24.0" - } - }, - "node_modules/@algolia/client-analytics/node_modules/@algolia/client-search": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.24.0.tgz", - "integrity": "sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA==", + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.52.1.tgz", + "integrity": "sha512-5oo4+I8iixie9vXhCyNFCzeIr8pqA3FQ//VsLHTDvZAV4ttYOPGvYHGQq5NSalrLx5Jc3dRro/5uDOlnUMcBJg==", "license": "MIT", "dependencies": { - "@algolia/client-common": "4.24.0", - "@algolia/requester-common": "4.24.0", - "@algolia/transporter": "4.24.0" + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" } }, "node_modules/@algolia/client-common": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.13.0.tgz", - "integrity": "sha512-2SP6bGGWOTN920MLZv8s7yIR3OqY03vEe4U+vb2MGdL8a/8EQznF3L/nTC/rGf/hvEfZlX2tGFxPJaF2waravg==", + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.52.1.tgz", + "integrity": "sha512-qCDoZfx5MpX7XQzvQ3bC4tSEMkQWQMaF/ABtLuoze03Y/flR563CCSws02qIJ23oX7lxl92LsilZjINVyTdtLw==", "license": "MIT", "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-insights": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.13.0.tgz", - "integrity": "sha512-ldHTe+LVgC6L4Wr6doAQQ7Ku0jAdhaaPg1T+IHzmmiRZb2Uq5OsjW2yC65JifOmzPCiMkIZE2mGRpWgkn5ktlw==", + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.52.1.tgz", + "integrity": "sha512-hnGs0/lsFJ2PWDxNBz7pxreXo/Xz7gxYRcfePBUjsH26ad0kU/sgnVZd9LwWBpsQv65z2jlb5dkyaB9WE9M9FQ==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.13.0", - "@algolia/requester-browser-xhr": "5.13.0", - "@algolia/requester-fetch": "5.13.0", - "@algolia/requester-node-http": "5.13.0" + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-personalization": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.24.0.tgz", - "integrity": "sha512-l5FRFm/yngztweU0HdUzz1rC4yoWCFo3IF+dVIVTfEPg906eZg5BOd1k0K6rZx5JzyyoP4LdmOikfkfGsKVE9w==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "4.24.0", - "@algolia/requester-common": "4.24.0", - "@algolia/transporter": "4.24.0" - } - }, - "node_modules/@algolia/client-personalization/node_modules/@algolia/client-common": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.24.0.tgz", - "integrity": "sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==", + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.52.1.tgz", + "integrity": "sha512-2VxxNc/uBysyKvGeBdSM5n9eIDKH8kWD7wd9/yqbJAiVwU4Yv6tU1LSJusHKrXV/aCu1KW7t9Gug9QyeEmtn/Q==", "license": "MIT", "dependencies": { - "@algolia/requester-common": "4.24.0", - "@algolia/transporter": "4.24.0" + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" } }, "node_modules/@algolia/client-query-suggestions": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.13.0.tgz", - "integrity": "sha512-pYo0jbLUtPDN1r341UHTaF2fgN5rbaZfDZqjPRKPM+FRlRmxFxqFQm1UUfpkSUWYGn7lECwDpbKYiKUf81MTwA==", + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.52.1.tgz", + "integrity": "sha512-O6mPtsw3xEfNOe6gWFpYLeAZAIljNa4Hgna3bq15PwyN7nbjTY0wXJFRbzs/0YVf75Br+SbOQUmjKxXYjDiSiQ==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.13.0", - "@algolia/requester-browser-xhr": "5.13.0", - "@algolia/requester-fetch": "5.13.0", - "@algolia/requester-node-http": "5.13.0" + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-search": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.13.0.tgz", - "integrity": "sha512-s2ge3uZ6Zg2sPSFibqijgEYsuorxcc8KVHg3I95nOPHvFHdnBtSHymhZvq4sp/fu8ijt/Y8jLwkuqm5myn+2Sg==", + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.52.1.tgz", + "integrity": "sha512-gA8oJOV1LnQQkDf91iebNnFInHuW0gRPEgLSOQ7EfipCEjYTHm5swm1DlH9H5RaRw4RrHuzHBegnlzc0MAstcg==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.13.0", - "@algolia/requester-browser-xhr": "5.13.0", - "@algolia/requester-fetch": "5.13.0", - "@algolia/requester-node-http": "5.13.0" + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" }, "engines": { "node": ">= 14.0.0" @@ -264,212 +187,125 @@ "license": "MIT" }, "node_modules/@algolia/ingestion": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.13.0.tgz", - "integrity": "sha512-fm5LEOe4FPDOc1D+M9stEs8hfcdmbdD+pt9og5shql6ueTZJANDbFoQhDOpiPJizR/ps1GwmjkWfUEywx3sV+Q==", + "version": "1.52.1", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.52.1.tgz", + "integrity": "sha512-U9zZfc5xIu9wRxZkt+HceJUAD4VKHKbAyLSloJdEyMRmphXeibfrY9cxqIXBcmPeZzGhn3Imb35Dq8l19PkJhw==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.13.0", - "@algolia/requester-browser-xhr": "5.13.0", - "@algolia/requester-fetch": "5.13.0", - "@algolia/requester-node-http": "5.13.0" + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" }, "engines": { "node": ">= 14.0.0" } }, - "node_modules/@algolia/logger-common": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.24.0.tgz", - "integrity": "sha512-LLUNjkahj9KtKYrQhFKCzMx0BY3RnNP4FEtO+sBybCjJ73E8jNdaKJ/Dd8A/VA4imVHP5tADZ8pn5B8Ga/wTMA==", - "license": "MIT" - }, - "node_modules/@algolia/logger-console": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.24.0.tgz", - "integrity": "sha512-X4C8IoHgHfiUROfoRCV+lzSy+LHMgkoEEU1BbKcsfnV0i0S20zyy0NLww9dwVHUWNfPPxdMU+/wKmLGYf96yTg==", - "license": "MIT", - "dependencies": { - "@algolia/logger-common": "4.24.0" - } - }, "node_modules/@algolia/monitoring": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.13.0.tgz", - "integrity": "sha512-e8Hshlnm2G5fapyUgWTBwhJ22yXcnLtPC4LWZKx7KOvv35GcdoHtlUBX94I/sWCJLraUr65JvR8qOo3LXC43dg==", + "version": "1.52.1", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.52.1.tgz", + "integrity": "sha512-a3SGNceHmkQfq77iG8Ka+w1pvwfZa/0lzEIgse30fL0kD+yKnd/dg0dQvSfFPAEt2f21DMcGkDSSeJlO3KdQjQ==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.13.0", - "@algolia/requester-browser-xhr": "5.13.0", - "@algolia/requester-fetch": "5.13.0", - "@algolia/requester-node-http": "5.13.0" + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/recommend": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-4.24.0.tgz", - "integrity": "sha512-P9kcgerfVBpfYHDfVZDvvdJv0lEoCvzNlOy2nykyt5bK8TyieYyiD0lguIJdRZZYGre03WIAFf14pgE+V+IBlw==", - "license": "MIT", - "dependencies": { - "@algolia/cache-browser-local-storage": "4.24.0", - "@algolia/cache-common": "4.24.0", - "@algolia/cache-in-memory": "4.24.0", - "@algolia/client-common": "4.24.0", - "@algolia/client-search": "4.24.0", - "@algolia/logger-common": "4.24.0", - "@algolia/logger-console": "4.24.0", - "@algolia/requester-browser-xhr": "4.24.0", - "@algolia/requester-common": "4.24.0", - "@algolia/requester-node-http": "4.24.0", - "@algolia/transporter": "4.24.0" - } - }, - "node_modules/@algolia/recommend/node_modules/@algolia/client-common": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.24.0.tgz", - "integrity": "sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==", - "license": "MIT", - "dependencies": { - "@algolia/requester-common": "4.24.0", - "@algolia/transporter": "4.24.0" - } - }, - "node_modules/@algolia/recommend/node_modules/@algolia/client-search": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.24.0.tgz", - "integrity": "sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "4.24.0", - "@algolia/requester-common": "4.24.0", - "@algolia/transporter": "4.24.0" - } - }, - "node_modules/@algolia/recommend/node_modules/@algolia/requester-browser-xhr": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.24.0.tgz", - "integrity": "sha512-Z2NxZMb6+nVXSjF13YpjYTdvV3032YTBSGm2vnYvYPA6mMxzM3v5rsCiSspndn9rzIW4Qp1lPHBvuoKJV6jnAA==", - "license": "MIT", - "dependencies": { - "@algolia/requester-common": "4.24.0" - } - }, - "node_modules/@algolia/recommend/node_modules/@algolia/requester-node-http": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.24.0.tgz", - "integrity": "sha512-JF18yTjNOVYvU/L3UosRcvbPMGT9B+/GQWNWnenIImglzNVGpyzChkXLnrSf6uxwVNO6ESGu6oN8MqcGQcjQJw==", + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.52.1.tgz", + "integrity": "sha512-z98QEguCFDpxb4S/PyrUK1igqF8tPsdbqOUUO6ON91vJ58w+Gwa6ncrI0oNXSFcrkxA5EqPKPQ2A1PBCn08TYQ==", "license": "MIT", "dependencies": { - "@algolia/requester-common": "4.24.0" + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-browser-xhr": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.13.0.tgz", - "integrity": "sha512-NV6oSCt5lFuzfsVQoSBpewEWf/h4ySr7pv2bfwu9yF/jc/g39pig8+YpuqsxlRWBm/lTGVA2V0Ai9ySwrNumIA==", + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.52.1.tgz", + "integrity": "sha512-CI7+/0I11QeZM59Uc8whd2or0kqzFVjpaPn9Qpwll/krHcBAxk24WkAQ6WX+IwDVMfpont4YGbKwAmCre3vE8Q==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.13.0" + "@algolia/client-common": "5.52.1" }, "engines": { "node": ">= 14.0.0" } }, - "node_modules/@algolia/requester-common": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.24.0.tgz", - "integrity": "sha512-k3CXJ2OVnvgE3HMwcojpvY6d9kgKMPRxs/kVohrwF5WMr2fnqojnycZkxPoEg+bXm8fi5BBfFmOqgYztRtHsQA==", - "license": "MIT" - }, "node_modules/@algolia/requester-fetch": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.13.0.tgz", - "integrity": "sha512-094bK4rumf+rXJazxv3mq6eKRM0ep5AxIo8T0YmOdldswQt79apeufFiPLN19nHEWH22xR2FelimD+T/wRSP+Q==", + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.52.1.tgz", + "integrity": "sha512-S6bDuw9byfOvm3T71cgdoZgrgnZq6hpdMLkx52Louh57nUAmvGQESz2aojOynQHjbTiV55smvAFbgn0qT4tJrg==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.13.0" + "@algolia/client-common": "5.52.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-node-http": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.13.0.tgz", - "integrity": "sha512-JY5xhEYMgki53Wm+A6R2jUpOUdD0zZnBq+PC5R1TGMNOYL1s6JjDrJeMsvaI2YWxYMUSoCnRoltN/yf9RI8n3A==", + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.52.1.tgz", + "integrity": "sha512-tqZXM+54rWo4mk5jL5Z/flE11nPmNEdXwFBM5py9DkOmbjeCNemfVd45FyM97XdzfZ0dl9uOJC6PYn1FpkeyQg==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.13.0" + "@algolia/client-common": "5.52.1" }, "engines": { "node": ">= 14.0.0" } }, - "node_modules/@algolia/transporter": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.24.0.tgz", - "integrity": "sha512-86nI7w6NzWxd1Zp9q3413dRshDqAzSbsQjhcDhPIatEFiZrL1/TjnHL8S7jVKFePlIMzDsZWXAXwXzcok9c5oA==", - "license": "MIT", - "dependencies": { - "@algolia/cache-common": "4.24.0", - "@algolia/logger-common": "4.24.0", - "@algolia/requester-common": "4.24.0" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz", - "integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", - "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.0", - "@babel/generator": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.0", - "@babel/parser": "^7.26.0", - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.26.0", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -488,20 +324,21 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", - "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.2", - "@babel/types": "^7.26.0", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -509,38 +346,25 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.9.tgz", - "integrity": "sha512-C47lC7LIDCnz0h4vai/tpNOI95tCd5ZT3iBt/DBH5lXKHZsyNQv18yf1wIIg2ntiQNgmAvA+DgZ82iW8Qdym8g==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", - "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -559,17 +383,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", - "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/traverse": "^7.25.9", + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "engines": { @@ -589,13 +413,13 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.9.tgz", - "integrity": "sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "regexpu-core": "^6.1.1", + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "engines": { @@ -609,61 +433,71 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", - "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.6.tgz", + "integrity": "sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" + "resolve": "^1.22.11" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", - "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -673,35 +507,35 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", - "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", - "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-wrap-function": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -711,14 +545,14 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", - "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -727,93 +561,80 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.9.tgz", - "integrity": "sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", - "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", - "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", + "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", "license": "MIT", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", - "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "license": "MIT", "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.10" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", - "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "license": "MIT", "dependencies": { - "@babel/types": "^7.26.10" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -823,13 +644,13 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", - "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -839,12 +660,12 @@ } }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", - "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -854,12 +675,12 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", - "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -869,14 +690,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -886,13 +707,13 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", - "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", + "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -905,6 +726,7 @@ "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "license": "MIT", "engines": { "node": ">=6.9.0" }, @@ -916,6 +738,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -924,12 +747,12 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", - "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", + "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -939,12 +762,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", - "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -954,12 +777,12 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", - "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -969,12 +792,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", - "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -987,6 +810,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -999,12 +823,12 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", - "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1014,14 +838,14 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz", - "integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", + "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-remap-async-to-generator": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.29.0" }, "engines": { "node": ">=6.9.0" @@ -1031,14 +855,14 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", - "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", + "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-remap-async-to-generator": "^7.25.9" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1048,12 +872,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz", - "integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1063,12 +887,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", - "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", + "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1078,13 +902,13 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", - "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", + "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1094,13 +918,13 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", - "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", + "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1110,17 +934,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", - "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", + "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/traverse": "^7.25.9", - "globals": "^11.1.0" + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1130,13 +954,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", - "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", + "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/template": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/template": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1146,12 +970,13 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", - "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1161,13 +986,13 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", - "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", + "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1177,12 +1002,12 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", - "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1192,13 +1017,13 @@ } }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", - "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1208,12 +1033,28 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", - "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", + "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1223,13 +1064,12 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.9.tgz", - "integrity": "sha512-KRhdhlVk2nObA5AYa7QMgTMTVJdfHprfpAk4DjZVtllqRg9qarilstTKEhpVjyt+Npi8ThRyiV8176Am3CodPA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", + "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", "license": "MIT", "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1239,12 +1079,12 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", - "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1254,13 +1094,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", - "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1270,14 +1110,14 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", - "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1287,12 +1127,12 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", - "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", + "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1302,12 +1142,12 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", - "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1317,12 +1157,12 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", - "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", + "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1332,12 +1172,12 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", - "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1347,13 +1187,13 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", - "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1363,14 +1203,13 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.9.tgz", - "integrity": "sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-simple-access": "^7.25.9" + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1380,15 +1219,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", - "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "version": "7.29.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz", + "integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.29.0" }, "engines": { "node": ">=6.9.0" @@ -1398,13 +1237,13 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", - "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1414,13 +1253,13 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", - "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1430,12 +1269,12 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", - "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1445,12 +1284,12 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.9.tgz", - "integrity": "sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", + "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1460,12 +1299,12 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", - "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", + "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1475,14 +1314,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", - "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", + "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9" + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1492,13 +1333,13 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", - "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1508,12 +1349,12 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", - "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", + "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1523,13 +1364,13 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", + "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1539,12 +1380,12 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", - "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1554,13 +1395,13 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", - "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1570,14 +1411,14 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", - "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", + "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1587,12 +1428,12 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", - "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1602,12 +1443,12 @@ } }, "node_modules/@babel/plugin-transform-react-constant-elements": { - "version": "7.25.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.25.1.tgz", - "integrity": "sha512-SLV/giH/V4SmloZ6Dt40HjTGTAIkxn33TVIHxNGNvo8ezMhrxBkzisj4op1KZYPIOHFLqhv60OHvX+YRu4xbmQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.27.1.tgz", + "integrity": "sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1617,12 +1458,12 @@ } }, "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.25.9.tgz", - "integrity": "sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", + "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1632,16 +1473,16 @@ } }, "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.9.tgz", - "integrity": "sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", + "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-syntax-jsx": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-jsx": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1651,12 +1492,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.25.9.tgz", - "integrity": "sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", "license": "MIT", "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.25.9" + "@babel/plugin-transform-react-jsx": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1666,13 +1507,13 @@ } }, "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.25.9.tgz", - "integrity": "sha512-KQ/Takk3T8Qzj5TppkS1be588lkbTp5uj7w6a0LeQaTMSckU/wK0oJ/pih+T690tkgI5jfmg2TqDJvd41Sj1Cg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", + "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1682,13 +1523,12 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", - "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", + "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "regenerator-transform": "^0.15.2" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1698,13 +1538,13 @@ } }, "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", - "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", + "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1714,12 +1554,12 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", - "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1729,16 +1569,16 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.25.9.tgz", - "integrity": "sha512-nZp7GlEl+yULJrClz0SwHPqir3lc0zsPrDHQUcxGspSL7AKrexNSEfTbfqnDNJUO13bgKyfuOLMF8Xqtu8j3YQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.0.tgz", + "integrity": "sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.6", - "babel-plugin-polyfill-regenerator": "^0.6.1", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", "semver": "^6.3.1" }, "engines": { @@ -1758,12 +1598,12 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", - "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1773,13 +1613,13 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", - "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", + "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1789,12 +1629,12 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", - "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1804,12 +1644,12 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", - "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1819,12 +1659,12 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz", - "integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1834,16 +1674,16 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.9.tgz", - "integrity": "sha512-7PbZQZP50tzv2KGGnhh82GSyMB01yKY9scIjf1a+GfZCtInOWqUH5+1EBU4t9fyR5Oykkkc9vFTs4OHrhHXljQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-syntax-typescript": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1853,12 +1693,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", - "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1868,13 +1708,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", - "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", + "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1884,13 +1724,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", - "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1900,13 +1740,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", - "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", + "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1916,79 +1756,80 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.0.tgz", - "integrity": "sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.0.tgz", + "integrity": "sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.26.0", - "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-syntax-import-assertions": "^7.28.6", + "@babel/plugin-syntax-import-attributes": "^7.28.6", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.25.9", - "@babel/plugin-transform-async-generator-functions": "^7.25.9", - "@babel/plugin-transform-async-to-generator": "^7.25.9", - "@babel/plugin-transform-block-scoped-functions": "^7.25.9", - "@babel/plugin-transform-block-scoping": "^7.25.9", - "@babel/plugin-transform-class-properties": "^7.25.9", - "@babel/plugin-transform-class-static-block": "^7.26.0", - "@babel/plugin-transform-classes": "^7.25.9", - "@babel/plugin-transform-computed-properties": "^7.25.9", - "@babel/plugin-transform-destructuring": "^7.25.9", - "@babel/plugin-transform-dotall-regex": "^7.25.9", - "@babel/plugin-transform-duplicate-keys": "^7.25.9", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-dynamic-import": "^7.25.9", - "@babel/plugin-transform-exponentiation-operator": "^7.25.9", - "@babel/plugin-transform-export-namespace-from": "^7.25.9", - "@babel/plugin-transform-for-of": "^7.25.9", - "@babel/plugin-transform-function-name": "^7.25.9", - "@babel/plugin-transform-json-strings": "^7.25.9", - "@babel/plugin-transform-literals": "^7.25.9", - "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", - "@babel/plugin-transform-member-expression-literals": "^7.25.9", - "@babel/plugin-transform-modules-amd": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.25.9", - "@babel/plugin-transform-modules-systemjs": "^7.25.9", - "@babel/plugin-transform-modules-umd": "^7.25.9", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-new-target": "^7.25.9", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.9", - "@babel/plugin-transform-numeric-separator": "^7.25.9", - "@babel/plugin-transform-object-rest-spread": "^7.25.9", - "@babel/plugin-transform-object-super": "^7.25.9", - "@babel/plugin-transform-optional-catch-binding": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9", - "@babel/plugin-transform-private-methods": "^7.25.9", - "@babel/plugin-transform-private-property-in-object": "^7.25.9", - "@babel/plugin-transform-property-literals": "^7.25.9", - "@babel/plugin-transform-regenerator": "^7.25.9", - "@babel/plugin-transform-regexp-modifiers": "^7.26.0", - "@babel/plugin-transform-reserved-words": "^7.25.9", - "@babel/plugin-transform-shorthand-properties": "^7.25.9", - "@babel/plugin-transform-spread": "^7.25.9", - "@babel/plugin-transform-sticky-regex": "^7.25.9", - "@babel/plugin-transform-template-literals": "^7.25.9", - "@babel/plugin-transform-typeof-symbol": "^7.25.9", - "@babel/plugin-transform-unicode-escapes": "^7.25.9", - "@babel/plugin-transform-unicode-property-regex": "^7.25.9", - "@babel/plugin-transform-unicode-regex": "^7.25.9", - "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.29.0", + "@babel/plugin-transform-async-to-generator": "^7.28.6", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.6", + "@babel/plugin-transform-class-properties": "^7.28.6", + "@babel/plugin-transform-class-static-block": "^7.28.6", + "@babel/plugin-transform-classes": "^7.28.6", + "@babel/plugin-transform-computed-properties": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-dotall-regex": "^7.28.6", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.6", + "@babel/plugin-transform-exponentiation-operator": "^7.28.6", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.28.6", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.28.6", + "@babel/plugin-transform-modules-systemjs": "^7.29.0", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", + "@babel/plugin-transform-numeric-separator": "^7.28.6", + "@babel/plugin-transform-object-rest-spread": "^7.28.6", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.28.6", + "@babel/plugin-transform-optional-chaining": "^7.28.6", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.28.6", + "@babel/plugin-transform-private-property-in-object": "^7.28.6", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.29.0", + "@babel/plugin-transform-regexp-modifiers": "^7.28.6", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.28.6", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.28.6", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.6", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.38.1", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", "semver": "^6.3.1" }, "engines": { @@ -1998,10 +1839,24 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.0.tgz", + "integrity": "sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.6", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/@babel/preset-env/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -2010,6 +1865,7 @@ "version": "0.1.6-no-external-plugins", "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", @@ -2020,17 +1876,17 @@ } }, "node_modules/@babel/preset-react": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.25.9.tgz", - "integrity": "sha512-D3to0uSPiWE7rBrdIICCd0tJSIGpLaaGptna2+w7Pft5xMqLpA1sz99DK5TZ1TjGbdQ/VI1eCSZ06dv3lT4JOw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", + "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-transform-react-display-name": "^7.25.9", - "@babel/plugin-transform-react-jsx": "^7.25.9", - "@babel/plugin-transform-react-jsx-development": "^7.25.9", - "@babel/plugin-transform-react-pure-annotations": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-react-display-name": "^7.28.0", + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@babel/plugin-transform-react-pure-annotations": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -2040,16 +1896,16 @@ } }, "node_modules/@babel/preset-typescript": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.26.0.tgz", - "integrity": "sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-syntax-jsx": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.25.9", - "@babel/plugin-transform-typescript": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -2070,59 +1926,46 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/runtime-corejs3": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.26.10.tgz", - "integrity": "sha512-uITFQYO68pMEYR46AHgQoyBg7KPPJDAbGn4jUTIRgCFJIp88MIBUianVOplhZDEec07bp9zIyr4Kp0FCyQzmWg==", - "license": "MIT", - "dependencies": { - "core-js-pure": "^3.30.2", - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", - "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", - "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/generator": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/template": "^7.25.9", - "@babel/types": "^7.25.9", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", - "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -2132,131 +1975,1432 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "license": "MIT", "optional": true, "engines": { "node": ">=0.1.90" } }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "node_modules/@csstools/cascade-layer-name-parser": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.5.tgz", + "integrity": "sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docsearch/css": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.7.0.tgz", - "integrity": "sha512-1OorbTwi1eeDmr0v5t+ckSRlt1zM5GHjm92iIl3kUu7im3GHuP+csf6E0WBg8pdXQczTWP9J9+o9n+Vg6DH5cQ==", - "license": "MIT" - }, - "node_modules/@docsearch/react": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.7.0.tgz", - "integrity": "sha512-8e6tdDfkYoxafEEPuX5eE1h9cTkLvhe4KgoFkO5JCddXSQONnN1FHcDZRI4r8894eMpbYq6rdJF0dVYh8ikwNQ==", - "license": "MIT", - "dependencies": { - "@algolia/autocomplete-core": "1.17.6", - "@algolia/autocomplete-preset-algolia": "1.17.6", - "@docsearch/css": "3.7.0", - "algoliasearch": "^5.12.0" + "node": ">=18" }, "peerDependencies": { - "@types/react": ">= 16.8.0 < 19.0.0", - "react": ">= 16.8.0 < 19.0.0", - "react-dom": ">= 16.8.0 < 19.0.0", - "search-insights": ">= 1 < 3" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" }, - "search-insights": { - "optional": true + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" } }, - "node_modules/@docsearch/react/node_modules/@algolia/client-analytics": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.13.0.tgz", - "integrity": "sha512-pS3qyXiWTwKnrt/jE79fqkNqZp7kjsFNlJDcBGkSWid74DNc6DmArlkvPqyLxnoaYGjUGACT6g56n7E3mVV2TA==", + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.13.0", - "@algolia/requester-browser-xhr": "5.13.0", - "@algolia/requester-fetch": "5.13.0", - "@algolia/requester-node-http": "5.13.0" - }, "engines": { - "node": ">= 14.0.0" + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, - "node_modules/@docsearch/react/node_modules/@algolia/client-personalization": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.13.0.tgz", - "integrity": "sha512-RnCfOSN4OUJDuMNHFca2M8lY64Tmw0kQOZikge4TknTqHmlbKJb8IbJE7Rol79Z80W2Y+B1ydcjV7DPje4GMRA==", + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", "dependencies": { - "@algolia/client-common": "5.13.0", - "@algolia/requester-browser-xhr": "5.13.0", - "@algolia/requester-fetch": "5.13.0", - "@algolia/requester-node-http": "5.13.0" + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" }, "engines": { - "node": ">= 14.0.0" + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, - "node_modules/@docsearch/react/node_modules/@algolia/recommend": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.13.0.tgz", - "integrity": "sha512-53/wW96oaj1FKMzGdFcZ/epygfTppLDUvgI1thLkd475EtVZCH3ZZVUNCEvf1AtnNyH1RnItkFzX8ayWCpx2PQ==", + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.13.0", - "@algolia/requester-browser-xhr": "5.13.0", - "@algolia/requester-fetch": "5.13.0", - "@algolia/requester-node-http": "5.13.0" - }, "engines": { - "node": ">= 14.0.0" + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" } }, - "node_modules/@docsearch/react/node_modules/algoliasearch": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.13.0.tgz", - "integrity": "sha512-04lyQX3Ev/oLYQx+aagamQDXvkUUfX1mwrLrus15+9fNaYj28GDxxEzbwaRfvmHFcZyoxvup7mMtDTTw8SrTEQ==", - "license": "MIT", - "dependencies": { - "@algolia/client-abtesting": "5.13.0", - "@algolia/client-analytics": "5.13.0", - "@algolia/client-common": "5.13.0", - "@algolia/client-insights": "5.13.0", - "@algolia/client-personalization": "5.13.0", - "@algolia/client-query-suggestions": "5.13.0", - "@algolia/client-search": "5.13.0", - "@algolia/ingestion": "1.13.0", - "@algolia/monitoring": "1.13.0", - "@algolia/recommend": "5.13.0", - "@algolia/requester-browser-xhr": "5.13.0", - "@algolia/requester-fetch": "5.13.0", - "@algolia/requester-node-http": "5.13.0" - }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", + "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/postcss-alpha-function": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-alpha-function/-/postcss-alpha-function-1.0.1.tgz", + "integrity": "sha512-isfLLwksH3yHkFXfCI2Gcaqg7wGGHZZwunoJzEZk0yKYIokgre6hYVFibKL3SYAoR1kBXova8LB+JoO5vZzi9w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-cascade-layers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.2.tgz", + "integrity": "sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-cascade-layers/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@csstools/postcss-cascade-layers/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-color-function": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.12.tgz", + "integrity": "sha512-yx3cljQKRaSBc2hfh8rMZFZzChaFgwmO2JfFgFr1vMcF3C/uyy5I4RFIBOIWGq1D+XbKCG789CGkG6zzkLpagA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-function-display-p3-linear": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function-display-p3-linear/-/postcss-color-function-display-p3-linear-1.0.1.tgz", + "integrity": "sha512-E5qusdzhlmO1TztYzDIi8XPdPoYOjoTY6HBYBCYSj+Gn4gQRBlvjgPQXzfzuPQqt8EhkC/SzPKObg4Mbn8/xMg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-mix-function": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.12.tgz", + "integrity": "sha512-4STERZfCP5Jcs13P1U5pTvI9SkgLgfMUMhdXW8IlJWkzOOOqhZIjcNhWtNJZes2nkBDsIKJ0CJtFtuaZ00moag==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-mix-variadic-function-arguments": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-variadic-function-arguments/-/postcss-color-mix-variadic-function-arguments-1.0.2.tgz", + "integrity": "sha512-rM67Gp9lRAkTo+X31DUqMEq+iK+EFqsidfecmhrteErxJZb6tUoJBVQca1Vn1GpDql1s1rD1pKcuYzMsg7Z1KQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-content-alt-text": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.8.tgz", + "integrity": "sha512-9SfEW9QCxEpTlNMnpSqFaHyzsiRpZ5J5+KqCu1u5/eEJAWsMhzT40qf0FIbeeglEvrGRMdDzAxMIz3wqoGSb+Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-contrast-color-function": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-contrast-color-function/-/postcss-contrast-color-function-2.0.12.tgz", + "integrity": "sha512-YbwWckjK3qwKjeYz/CijgcS7WDUCtKTd8ShLztm3/i5dhh4NaqzsbYnhm4bjrpFpnLZ31jVcbK8YL77z3GBPzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-exponential-functions": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.9.tgz", + "integrity": "sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-font-format-keywords": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-4.0.0.tgz", + "integrity": "sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gamut-mapping": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.11.tgz", + "integrity": "sha512-fCpCUgZNE2piVJKC76zFsgVW1apF6dpYsqGyH8SIeCcM4pTEsRTWTLCaJIMKFEundsCKwY1rwfhtrio04RJ4Dw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gradients-interpolation-method": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.12.tgz", + "integrity": "sha512-jugzjwkUY0wtNrZlFeyXzimUL3hN4xMvoPnIXxoZqxDvjZRiSh+itgHcVUWzJ2VwD/VAMEgCLvtaJHX+4Vj3Ow==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-hwb-function": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.12.tgz", + "integrity": "sha512-mL/+88Z53KrE4JdePYFJAQWFrcADEqsLprExCM04GDNgHIztwFzj0Mbhd/yxMBngq0NIlz58VVxjt5abNs1VhA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-ic-unit": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.4.tgz", + "integrity": "sha512-yQ4VmossuOAql65sCPppVO1yfb7hDscf4GseF0VCA/DTDaBc0Wtf8MTqVPfjGYlT5+2buokG0Gp7y0atYZpwjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-initial": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-initial/-/postcss-initial-2.0.1.tgz", + "integrity": "sha512-L1wLVMSAZ4wovznquK0xmC7QSctzO4D0Is590bxpGqhqjboLXYA16dWZpfwImkdOgACdQ9PqXsuRroW6qPlEsg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.3.tgz", + "integrity": "sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-light-dark-function": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.11.tgz", + "integrity": "sha512-fNJcKXJdPM3Lyrbmgw2OBbaioU7yuKZtiXClf4sGdQttitijYlZMD5K7HrC/eF83VRWRrYq6OZ0Lx92leV2LFA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-float-and-clear": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-3.0.0.tgz", + "integrity": "sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overflow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-2.0.0.tgz", + "integrity": "sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overscroll-behavior": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-2.0.0.tgz", + "integrity": "sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-resize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-3.0.0.tgz", + "integrity": "sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-viewport-units": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.4.tgz", + "integrity": "sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-minmax": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.9.tgz", + "integrity": "sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.5.tgz", + "integrity": "sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-nested-calc": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-4.0.0.tgz", + "integrity": "sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-normalize-display-values": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.1.tgz", + "integrity": "sha512-TQUGBuRvxdc7TgNSTevYqrL8oItxiwPDixk20qCB5me/W8uF7BPbhRrAvFuhEoywQp/woRsUZ6SJ+sU5idZAIA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-oklab-function": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.12.tgz", + "integrity": "sha512-HhlSmnE1NKBhXsTnNGjxvhryKtO7tJd1w42DKOGFD6jSHtYOrsJTQDKPMwvOfrzUAk8t7GcpIfRyM7ssqHpFjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-position-area-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-position-area-property/-/postcss-position-area-property-1.0.0.tgz", + "integrity": "sha512-fUP6KR8qV2NuUZV3Cw8itx0Ep90aRjAZxAEzC3vrl6yjFv+pFsQbR18UuQctEKmA72K9O27CoYiKEgXxkqjg8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-progressive-custom-properties": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.2.1.tgz", + "integrity": "sha512-uPiiXf7IEKtUQXsxu6uWtOlRMXd2QWWy5fhxHDnPdXKCQckPP3E34ZgDoZ62r2iT+UOgWsSbM4NvHE5m3mAEdw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-property-rule-prelude-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-property-rule-prelude-list/-/postcss-property-rule-prelude-list-1.0.0.tgz", + "integrity": "sha512-IxuQjUXq19fobgmSSvUDO7fVwijDJaZMvWQugxfEUxmjBeDCVaDuMpsZ31MsTm5xbnhA+ElDi0+rQ7sQQGisFA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-random-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-random-function/-/postcss-random-function-2.0.1.tgz", + "integrity": "sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-relative-color-syntax": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.12.tgz", + "integrity": "sha512-0RLIeONxu/mtxRtf3o41Lq2ghLimw0w9ByLWnnEVuy89exmEEq8bynveBxNW3nyHqLAFEeNtVEmC1QK9MZ8Huw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-scope-pseudo-class": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-4.0.1.tgz", + "integrity": "sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-scope-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-sign-functions": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.4.tgz", + "integrity": "sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-stepped-value-functions": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.9.tgz", + "integrity": "sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-syntax-descriptor-syntax-production": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-syntax-descriptor-syntax-production/-/postcss-syntax-descriptor-syntax-production-1.0.1.tgz", + "integrity": "sha512-GneqQWefjM//f4hJ/Kbox0C6f2T7+pi4/fqTqOFGTL3EjnvOReTqO1qUQ30CaUjkwjYq9qZ41hzarrAxCc4gow==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-system-ui-font-family": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-system-ui-font-family/-/postcss-system-ui-font-family-1.0.0.tgz", + "integrity": "sha512-s3xdBvfWYfoPSBsikDXbuorcMG1nN1M6GdU0qBsGfcmNR0A/qhloQZpTxjA3Xsyrk1VJvwb2pOfiOT3at/DuIQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-text-decoration-shorthand": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.3.tgz", + "integrity": "sha512-KSkGgZfx0kQjRIYnpsD7X2Om9BUXX/Kii77VBifQW9Ih929hK0KNjVngHDH0bFB9GmfWcR9vJYJJRvw/NQjkrA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-trigonometric-functions": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.9.tgz", + "integrity": "sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-unset-value": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-4.0.0.tgz", + "integrity": "sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "engines": { - "node": ">= 14.0.0" + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/utilities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/utilities/-/utilities-2.0.0.tgz", + "integrity": "sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@docsearch/core": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/@docsearch/core/-/core-4.6.3.tgz", + "integrity": "sha512-rUOujwIpxJRgD7+kicVsI3D5sqBvdiRTquzWBpTEXZs8ZXfGbfzpus5HqumaNYTppN2HvH8E2yNuRwYdHJeOlA==", + "license": "MIT", + "peerDependencies": { + "@types/react": ">= 16.8.0 < 20.0.0", + "react": ">= 16.8.0 < 20.0.0", + "react-dom": ">= 16.8.0 < 20.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@docsearch/css": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.6.3.tgz", + "integrity": "sha512-nlOwcXcsNAptQl4vlL4MA78qNJKO0Qlds5GuBjCoePgkebTXLSf8Qt1oyZ3YBshYupKXG9VRGEsk1zr23d+bzQ==", + "license": "MIT" + }, + "node_modules/@docsearch/react": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-4.6.3.tgz", + "integrity": "sha512-Bg2wdDsoQVlNCcEKuEJAU04tvHCqgx8rIu+uIoM4pRtcx3TBKJuXutJik3LTA8LRc9YEyHkrYUrmcC0D7BYf+g==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-core": "1.19.2", + "@docsearch/core": "4.6.3", + "@docsearch/css": "4.6.3" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 20.0.0", + "react": ">= 16.8.0 < 20.0.0", + "react-dom": ">= 16.8.0 < 20.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@docsearch/react/node_modules/@algolia/autocomplete-core": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.19.2.tgz", + "integrity": "sha512-mKv7RyuAzXvwmq+0XRK8HqZXt9iZ5Kkm2huLjgn5JoCPtDy+oh9yxUMfDDaVCw0oyzZ1isdJBc7l9nuCyyR7Nw==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.19.2", + "@algolia/autocomplete-shared": "1.19.2" + } + }, + "node_modules/@docsearch/react/node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.2.tgz", + "integrity": "sha512-TjxbcC/r4vwmnZaPwrHtkXNeqvlpdyR+oR9Wi2XyfORkiGkLTVhX2j+O9SaCCINbKoDfc+c2PB8NjfOnz7+oKg==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.19.2" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@docsearch/react/node_modules/@algolia/autocomplete-shared": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.2.tgz", + "integrity": "sha512-jEazxZTVD2nLrC+wYlVHQgpBoBB5KPStrJxLzsIFl6Kqd1AlG9sIAGl39V5tECLpIQzB3Qa2T6ZPJ1ChkwMK/w==", + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" } }, "node_modules/@docusaurus/babel": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.6.1.tgz", - "integrity": "sha512-JcKaunW8Ml2nTnfnvFc55T00Y+aCpNWnf1KY/gG+wWxHYDH0IdXOOz+k6NAlEAerW8+VYLfUqRIqHZ7N/DVXvQ==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.10.1.tgz", + "integrity": "sha512-DZzFO1K3v/GoEt1fx1DiYHF4en+PuhtQf1AkQJa5zu3CoeKSpr5cpQRUlz3jr0m44wyzmSXu9bVpfir+N4+8bg==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.9", @@ -2267,52 +3411,50 @@ "@babel/preset-react": "^7.25.9", "@babel/preset-typescript": "^7.25.9", "@babel/runtime": "^7.25.9", - "@babel/runtime-corejs3": "^7.25.9", "@babel/traverse": "^7.25.9", - "@docusaurus/logger": "3.6.1", - "@docusaurus/utils": "3.6.1", + "@docusaurus/logger": "3.10.1", + "@docusaurus/utils": "3.10.1", "babel-plugin-dynamic-import-node": "^2.3.3", "fs-extra": "^11.1.1", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@docusaurus/bundler": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.6.1.tgz", - "integrity": "sha512-vHSEx8Ku9x/gfIC6k4xb8J2nTxagLia0KvZkPZhxfkD1+n8i+Dj4BZPWTmv+kCA17RbgAvECG0XRZ0/ZEspQBQ==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.10.1.tgz", + "integrity": "sha512-HIqQPvbqnnQRe4NsBd1774KRarjXqS6wHsWELtyuSs1gCfvixJO2jUGH/OEBtr1Gvzpw+ze5CjGMvSJ8UE1KUw==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.9", - "@docusaurus/babel": "3.6.1", - "@docusaurus/cssnano-preset": "3.6.1", - "@docusaurus/logger": "3.6.1", - "@docusaurus/types": "3.6.1", - "@docusaurus/utils": "3.6.1", - "autoprefixer": "^10.4.14", + "@docusaurus/babel": "3.10.1", + "@docusaurus/cssnano-preset": "3.10.1", + "@docusaurus/logger": "3.10.1", + "@docusaurus/types": "3.10.1", + "@docusaurus/utils": "3.10.1", "babel-loader": "^9.2.1", - "clean-css": "^5.3.2", + "clean-css": "^5.3.3", "copy-webpack-plugin": "^11.0.0", - "css-loader": "^6.8.1", + "css-loader": "^6.11.0", "css-minimizer-webpack-plugin": "^5.0.1", "cssnano": "^6.1.2", "file-loader": "^6.2.0", "html-minifier-terser": "^7.2.0", - "mini-css-extract-plugin": "^2.9.1", + "mini-css-extract-plugin": "^2.9.2", "null-loader": "^4.0.1", - "postcss": "^8.4.26", - "postcss-loader": "^7.3.3", - "react-dev-utils": "^12.0.1", + "postcss": "^8.5.4", + "postcss-loader": "^7.3.4", + "postcss-preset-env": "^10.2.1", "terser-webpack-plugin": "^5.3.9", "tslib": "^2.6.0", "url-loader": "^4.1.1", "webpack": "^5.95.0", - "webpackbar": "^6.0.1" + "webpackbar": "^7.0.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "@docusaurus/faster": "*" @@ -2359,18 +3501,18 @@ } }, "node_modules/@docusaurus/core": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.6.1.tgz", - "integrity": "sha512-cDKxPihiM2z7G+4QtpTczS7uxNfNG6naSqM65OmAJET0CFRHbc9mDlLFtQF0lsVES91SHqfcGaaLZmi2FjdwWA==", - "license": "MIT", - "dependencies": { - "@docusaurus/babel": "3.6.1", - "@docusaurus/bundler": "3.6.1", - "@docusaurus/logger": "3.6.1", - "@docusaurus/mdx-loader": "3.6.1", - "@docusaurus/utils": "3.6.1", - "@docusaurus/utils-common": "3.6.1", - "@docusaurus/utils-validation": "3.6.1", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.10.1.tgz", + "integrity": "sha512-3pf2fXXw0eVk8WnC3T4LIigRDupcpvngpKo9Vy7mYyBhuddc0klDUuZAIfzMoK6z05pdlk6EFC/vBSX43+1O5w==", + "license": "MIT", + "dependencies": { + "@docusaurus/babel": "3.10.1", + "@docusaurus/bundler": "3.10.1", + "@docusaurus/logger": "3.10.1", + "@docusaurus/mdx-loader": "3.10.1", + "@docusaurus/utils": "3.10.1", + "@docusaurus/utils-common": "3.10.1", + "@docusaurus/utils-validation": "3.10.1", "boxen": "^6.2.1", "chalk": "^4.1.2", "chokidar": "^3.5.3", @@ -2378,104 +3520,95 @@ "combine-promises": "^1.1.0", "commander": "^5.1.0", "core-js": "^3.31.1", - "del": "^6.1.1", "detect-port": "^1.5.1", "escape-html": "^1.0.3", "eta": "^2.2.0", "eval": "^0.1.8", + "execa": "^5.1.1", "fs-extra": "^11.1.1", "html-tags": "^3.3.1", "html-webpack-plugin": "^5.6.0", "leven": "^3.1.0", "lodash": "^4.17.21", + "open": "^8.4.0", "p-map": "^4.0.0", "prompts": "^2.4.2", - "react-dev-utils": "^12.0.1", - "react-helmet-async": "^1.3.0", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", - "react-loadable-ssr-addon-v5-slorber": "^1.0.1", + "react-loadable-ssr-addon-v5-slorber": "^1.0.3", "react-router": "^5.3.4", "react-router-config": "^5.1.1", "react-router-dom": "^5.3.4", - "rtl-detect": "^1.0.4", "semver": "^7.5.4", - "serve-handler": "^6.1.6", - "shelljs": "^0.8.5", + "serve-handler": "^6.1.7", + "tinypool": "^1.0.2", "tslib": "^2.6.0", "update-notifier": "^6.0.2", "webpack": "^5.95.0", "webpack-bundle-analyzer": "^4.10.2", - "webpack-dev-server": "^4.15.2", + "webpack-dev-server": "^5.2.2", "webpack-merge": "^6.0.1" }, "bin": { "docusaurus": "bin/docusaurus.mjs" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { + "@docusaurus/faster": "*", "@mdx-js/react": "^3.0.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/core/node_modules/webpack-merge": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", - "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.1" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" }, - "engines": { - "node": ">=18.0.0" + "peerDependenciesMeta": { + "@docusaurus/faster": { + "optional": true + } } }, "node_modules/@docusaurus/cssnano-preset": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.6.1.tgz", - "integrity": "sha512-ZxYUmNeyQHW2w4/PJ7d07jQDuxzmKr9uPAQ6IVe5dTkeIeV0mDBB3jOLeJkNoI42Ru9JKEqQ9aVDtM9ct6QHnw==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.10.1.tgz", + "integrity": "sha512-eNfHGcTKCSq6xmcavAkX3RRclHaE2xRCMParlDXLdXVP01/a2e/jKXMj/0ULnLFQSNwwuI62L0Ge8J+nZsR7UQ==", "license": "MIT", "dependencies": { "cssnano-preset-advanced": "^6.1.2", - "postcss": "^8.4.38", + "postcss": "^8.5.4", "postcss-sort-media-queries": "^5.2.0", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@docusaurus/logger": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.6.1.tgz", - "integrity": "sha512-OvetI/nnOMBSqCkUzKAQhnIjhxduECK4qTu3tq/8/h/qqvLsvKURojm04WPE54L+Uy+UXMas0hnbBJd8zDlEOw==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.10.1.tgz", + "integrity": "sha512-oPjNFnfJsRCkePVjkGrxWGq4MvJKRQT0r9jOP0eRBTZ7Wr9FAbzdP/Gjs0I2Ss6YRkPoEgygKG112OkE6skvJw==", "license": "MIT", "dependencies": { "chalk": "^4.1.2", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@docusaurus/lqip-loader": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/lqip-loader/-/lqip-loader-3.6.1.tgz", - "integrity": "sha512-H/VVvnvFupFhQ81FuTyA/XHxEZPKh99T6Wg6KgN+/yvcn7869RdgrlDhKDnXZ7j2u80eFsVNjAcPfW1cSAtK6A==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/lqip-loader/-/lqip-loader-3.10.1.tgz", + "integrity": "sha512-ushByv88FWxsh3BS9QccWcEbKsW0QnNvWnl0+NCLe7weL5AkHS4HnSDszGMSzn2v5jidT4QjOVHacNVsU5I9Lw==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.6.1", + "@docusaurus/logger": "3.10.1", "file-loader": "^6.2.0", "lodash": "^4.17.21", "sharp": "^0.32.3", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@docusaurus/lqip-loader/node_modules/sharp": { @@ -2502,21 +3635,21 @@ } }, "node_modules/@docusaurus/mdx-loader": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.6.1.tgz", - "integrity": "sha512-KPIsYi0S3X3/rNrW3V1fgOu5t6ahYWc31zTHHod8pacFxdmk9Uf6uuw+Jd6Cly1ilgal+41Ku+s0gmMuqKqiqg==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.10.1.tgz", + "integrity": "sha512-GRmeb/wQ+iXRrFwcHBfgQhrJxGElgCsoTWZYDhccjsZVne1p8MK/EpQVIloXttz76TCe78kKD5AEG9n1xc1oxQ==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.6.1", - "@docusaurus/utils": "3.6.1", - "@docusaurus/utils-validation": "3.6.1", + "@docusaurus/logger": "3.10.1", + "@docusaurus/utils": "3.10.1", + "@docusaurus/utils-validation": "3.10.1", "@mdx-js/mdx": "^3.0.0", "@slorber/remark-comment": "^1.0.0", "escape-html": "^1.0.3", "estree-util-value-to-estree": "^3.0.1", "file-loader": "^6.2.0", "fs-extra": "^11.1.1", - "image-size": "^1.0.2", + "image-size": "^2.0.2", "mdast-util-mdx": "^3.0.0", "mdast-util-to-string": "^4.0.0", "rehype-raw": "^7.0.0", @@ -2533,25 +3666,25 @@ "webpack": "^5.88.1" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@docusaurus/module-type-aliases": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.6.1.tgz", - "integrity": "sha512-J+q1jgm7TnEfVIUZImSFeLA1rghb6nwtoB9siHdcgKpDqFJ9/S7xhQL2aEKE7iZMZYzpu+2F390E9A7GkdEJNA==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.10.1.tgz", + "integrity": "sha512-YoOZKUdGlp8xSYhuAkGdSo5Ydkbq4V4eK3sD8v0a2hloxCWdQbNBhkc+Ko9QyjpESc0BYcIGM5iHVAy5hdFV6w==", "license": "MIT", "dependencies": { - "@docusaurus/types": "3.6.1", + "@docusaurus/types": "3.10.1", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", "@types/react-router-dom": "*", - "react-helmet-async": "*", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" }, "peerDependencies": { @@ -2559,25 +3692,50 @@ "react-dom": "*" } }, + "node_modules/@docusaurus/plugin-client-redirects": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-client-redirects/-/plugin-client-redirects-3.10.1.tgz", + "integrity": "sha512-LHgd+YDvkhfOHMAE6XtUng3DQNzVM765RqVRrMJgHtzAvfopQhY6ieprqjxDVBdv21cLma6I0jHr+YCZH8fL9A==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.1", + "@docusaurus/logger": "3.10.1", + "@docusaurus/utils": "3.10.1", + "@docusaurus/utils-common": "3.10.1", + "@docusaurus/utils-validation": "3.10.1", + "eta": "^2.2.0", + "fs-extra": "^11.1.1", + "lodash": "^4.17.21", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/@docusaurus/plugin-content-blog": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.6.1.tgz", - "integrity": "sha512-FUmsn3xg/XD/K/4FQd8XHrs92aQdZO5LUtpHnRvO1/6DY87SMz6B6ERAN9IGQQld//M2/LVTHkZy8oVhQZQHIQ==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.6.1", - "@docusaurus/logger": "3.6.1", - "@docusaurus/mdx-loader": "3.6.1", - "@docusaurus/theme-common": "3.6.1", - "@docusaurus/types": "3.6.1", - "@docusaurus/utils": "3.6.1", - "@docusaurus/utils-common": "3.6.1", - "@docusaurus/utils-validation": "3.6.1", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.10.1.tgz", + "integrity": "sha512-mmkgE6Q2+K74tnkou7tXlpDLvoCU/qkSa2GSQ3XUiHWvcebCoDQzS670RR3tO8PmaWlIyWWISYWzZLuMfxunRA==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.1", + "@docusaurus/logger": "3.10.1", + "@docusaurus/mdx-loader": "3.10.1", + "@docusaurus/theme-common": "3.10.1", + "@docusaurus/types": "3.10.1", + "@docusaurus/utils": "3.10.1", + "@docusaurus/utils-common": "3.10.1", + "@docusaurus/utils-validation": "3.10.1", "cheerio": "1.0.0-rc.12", + "combine-promises": "^1.1.0", "feed": "^4.2.2", "fs-extra": "^11.1.1", "lodash": "^4.17.21", - "reading-time": "^1.5.0", + "schema-dts": "^1.1.2", "srcset": "^4.0.0", "tslib": "^2.6.0", "unist-util-visit": "^5.0.0", @@ -2585,173 +3743,188 @@ "webpack": "^5.88.1" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "@docusaurus/plugin-content-docs": "*", - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@docusaurus/plugin-content-docs": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.6.1.tgz", - "integrity": "sha512-Uq8kyn5DYCDmkUlB9sWChhWghS4lUFNiQU+RXcAXJ3qCVXsBpPsh6RF+npQG1N+j4wAbjydM1iLLJJzp+x3eMQ==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.6.1", - "@docusaurus/logger": "3.6.1", - "@docusaurus/mdx-loader": "3.6.1", - "@docusaurus/module-type-aliases": "3.6.1", - "@docusaurus/theme-common": "3.6.1", - "@docusaurus/types": "3.6.1", - "@docusaurus/utils": "3.6.1", - "@docusaurus/utils-common": "3.6.1", - "@docusaurus/utils-validation": "3.6.1", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.10.1.tgz", + "integrity": "sha512-2jRVrtzjf8LClGTHQlwlwuD3wQXRx3WEoF7XUarJ8Ou+0onV+SLtejsyfY9JLpfUh9hPhXM4pbBGkyAY4Bi3HQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.1", + "@docusaurus/logger": "3.10.1", + "@docusaurus/mdx-loader": "3.10.1", + "@docusaurus/module-type-aliases": "3.10.1", + "@docusaurus/theme-common": "3.10.1", + "@docusaurus/types": "3.10.1", + "@docusaurus/utils": "3.10.1", + "@docusaurus/utils-common": "3.10.1", + "@docusaurus/utils-validation": "3.10.1", "@types/react-router-config": "^5.0.7", "combine-promises": "^1.1.0", "fs-extra": "^11.1.1", "js-yaml": "^4.1.0", "lodash": "^4.17.21", + "schema-dts": "^1.1.2", "tslib": "^2.6.0", "utility-types": "^3.10.0", "webpack": "^5.88.1" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@docusaurus/plugin-content-pages": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.6.1.tgz", - "integrity": "sha512-TZtL+2zq20gqGalzoIT2rEF1T4YCZ26jTvlCJXs78+incIajfdHtmdOq7rQW0oV7oqTjpGllbp788nY/vY9jgw==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.10.1.tgz", + "integrity": "sha512-huJpaRPMl42nsFwuCXvV8bVDj2MazuwRJIUylI/RSlmZeJssVoZXeCjVf1y+1Drtpa9SKcdGn8yoJ76IRJijtw==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.6.1", - "@docusaurus/mdx-loader": "3.6.1", - "@docusaurus/types": "3.6.1", - "@docusaurus/utils": "3.6.1", - "@docusaurus/utils-validation": "3.6.1", + "@docusaurus/core": "3.10.1", + "@docusaurus/mdx-loader": "3.10.1", + "@docusaurus/types": "3.10.1", + "@docusaurus/utils": "3.10.1", + "@docusaurus/utils-validation": "3.10.1", "fs-extra": "^11.1.1", "tslib": "^2.6.0", "webpack": "^5.88.1" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-css-cascade-layers": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.10.1.tgz", + "integrity": "sha512-r//fn+MNHkE1wCof8T29VAQezt1enGCpsFxoziBbvLgBM4JfXN2P3rxrBaavHmvLvm7lYkpJeitcDthwnmWCTw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.1", + "@docusaurus/types": "3.10.1", + "@docusaurus/utils": "3.10.1", + "@docusaurus/utils-validation": "3.10.1", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" } }, "node_modules/@docusaurus/plugin-debug": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.6.1.tgz", - "integrity": "sha512-DeKPZtoVExDSYCbzoz7y5Dhc6+YPqRWfVGwEEUyKopSyQYefp0OV8hvASmbJCn2WyThRgspOUhog3FSEhz+agw==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.10.1.tgz", + "integrity": "sha512-9KqOpKNfAyqGZykRb9LhIT/vyRF6sm/ykhjj/39JvaJahDS+jZJE0Z1Wfz9q3DUNDTMNN0Q7u/kk4rKKU+IJuA==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.6.1", - "@docusaurus/types": "3.6.1", - "@docusaurus/utils": "3.6.1", + "@docusaurus/core": "3.10.1", + "@docusaurus/types": "3.10.1", + "@docusaurus/utils": "3.10.1", "fs-extra": "^11.1.1", - "react-json-view-lite": "^1.2.0", + "react-json-view-lite": "^2.3.0", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@docusaurus/plugin-google-analytics": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.6.1.tgz", - "integrity": "sha512-ZEoERiDHxSfhaEeT35ukQ892NzGHWiUvfxUsnPiRuGEhMoQlxMSp60shBuSZ1sUKuZlndoEl5qAXJg09Wls/Sg==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.10.1.tgz", + "integrity": "sha512-8o0P1KtmgdYQHH+oInitPpRWI0Of5XednAX4+DMhQNSmGSRNrsEEHg1ebv35m9AgRClfAytCJ5jA9KvcASTyuA==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.6.1", - "@docusaurus/types": "3.6.1", - "@docusaurus/utils-validation": "3.6.1", + "@docusaurus/core": "3.10.1", + "@docusaurus/types": "3.10.1", + "@docusaurus/utils-validation": "3.10.1", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@docusaurus/plugin-google-gtag": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.6.1.tgz", - "integrity": "sha512-u/E9vXUsZxYaV6Brvfee8NiH/iR0cMml9P/ifz4EpH/Jfxdbw8rbCT0Nm/h7EFgEY48Uqkl5huSbIvFB9n8aTQ==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.10.1.tgz", + "integrity": "sha512-pu3xIUo5o/zCMLfUY9BO5KOwSH0zIsAGyFRPvXHayFSA5XIhCU/SFuB0g0ZNjFn9niZLCaNvoeAuOGFJZq0fdw==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.6.1", - "@docusaurus/types": "3.6.1", - "@docusaurus/utils-validation": "3.6.1", - "@types/gtag.js": "^0.0.12", + "@docusaurus/core": "3.10.1", + "@docusaurus/types": "3.10.1", + "@docusaurus/utils-validation": "3.10.1", + "@types/gtag.js": "^0.0.20", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@docusaurus/plugin-google-tag-manager": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.6.1.tgz", - "integrity": "sha512-By+NKkGYV8tSo8/RyS1OXikOtqsko5jJZ/uioJfBjsBGgSbiMJ+Y/HogFBke0mgSvf7NPGKZTbYm5+FJ8YUtPQ==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.10.1.tgz", + "integrity": "sha512-f6fyGHiCm7kJHBtAisGQS5oNBnpnMTYQZxDXeVrnw/3zWU+LMA22pr6UHGYkBKDbN+qPC5QHG3NuOfzQLq3+Lw==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.6.1", - "@docusaurus/types": "3.6.1", - "@docusaurus/utils-validation": "3.6.1", + "@docusaurus/core": "3.10.1", + "@docusaurus/types": "3.10.1", + "@docusaurus/utils-validation": "3.10.1", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@docusaurus/plugin-ideal-image": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-ideal-image/-/plugin-ideal-image-3.6.1.tgz", - "integrity": "sha512-hiGRPPlsM02aEOPlQc9rVnrckbVR6HswG7yDpZOtBEhw+ysXFsl/8gzAxFBL4ogKjN28WrlMCn/6IIkxY/EyOQ==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-ideal-image/-/plugin-ideal-image-3.10.1.tgz", + "integrity": "sha512-zIjQ/BtFS6YwEgnk9ypZxuSnA/Z011Z9cuaawKVfgyT7T+vuGx6T6ZgKur0IFnOkpI7EfI1DhbfdABCtfEzWFA==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.6.1", - "@docusaurus/lqip-loader": "3.6.1", + "@docusaurus/core": "3.10.1", + "@docusaurus/lqip-loader": "3.10.1", "@docusaurus/responsive-loader": "^1.7.0", - "@docusaurus/theme-translations": "3.6.1", - "@docusaurus/types": "3.6.1", - "@docusaurus/utils-validation": "3.6.1", - "@slorber/react-ideal-image": "^0.0.12", - "react-waypoint": "^10.3.0", + "@docusaurus/theme-translations": "3.10.1", + "@docusaurus/types": "3.10.1", + "@docusaurus/utils-validation": "3.10.1", "sharp": "^0.32.3", "tslib": "^2.6.0", "webpack": "^5.88.1" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "jimp": "*", - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "jimp": { @@ -2782,55 +3955,80 @@ } }, "node_modules/@docusaurus/plugin-sitemap": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.6.1.tgz", - "integrity": "sha512-i8R/GTKew4Cufb+7YQTwfPcNOhKTJzZ1VZ5OqQwI9c3pZK2TltQyhqKDVN94KCTbSSKvOYYytYfRAB2uPnH1/A==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.10.1.tgz", + "integrity": "sha512-C26MbmmqgdjkDq1htaZ3aD7LzEDKFWXfpyQpt0EOUThuq5nV77zDaedV20yHcVo9p+3ey9aZ4pbHA0D3QcZTzg==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.6.1", - "@docusaurus/logger": "3.6.1", - "@docusaurus/types": "3.6.1", - "@docusaurus/utils": "3.6.1", - "@docusaurus/utils-common": "3.6.1", - "@docusaurus/utils-validation": "3.6.1", + "@docusaurus/core": "3.10.1", + "@docusaurus/logger": "3.10.1", + "@docusaurus/types": "3.10.1", + "@docusaurus/utils": "3.10.1", + "@docusaurus/utils-common": "3.10.1", + "@docusaurus/utils-validation": "3.10.1", "fs-extra": "^11.1.1", "sitemap": "^7.1.1", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, - "node_modules/@docusaurus/preset-classic": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.6.1.tgz", - "integrity": "sha512-b90Y1XRH9e+oa/E3NmiFEFOwgYUd+knFcZUy81nM3FJs038WbEA0T55NQsuPW0s7nOsCShQ7dVFyKxV+Wp31Nw==", + "node_modules/@docusaurus/plugin-svgr": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-svgr/-/plugin-svgr-3.10.1.tgz", + "integrity": "sha512-6SFxsmjWFkVLDmBUvFK6i72QjUwqyQFe4Ovz+SUJophJjOyVG3ZZG5IQpBC/kX/Gfv1yWeU9nWauH6F6Q7QX/Q==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.6.1", - "@docusaurus/plugin-content-blog": "3.6.1", - "@docusaurus/plugin-content-docs": "3.6.1", - "@docusaurus/plugin-content-pages": "3.6.1", - "@docusaurus/plugin-debug": "3.6.1", - "@docusaurus/plugin-google-analytics": "3.6.1", - "@docusaurus/plugin-google-gtag": "3.6.1", - "@docusaurus/plugin-google-tag-manager": "3.6.1", - "@docusaurus/plugin-sitemap": "3.6.1", - "@docusaurus/theme-classic": "3.6.1", - "@docusaurus/theme-common": "3.6.1", - "@docusaurus/theme-search-algolia": "3.6.1", - "@docusaurus/types": "3.6.1" + "@docusaurus/core": "3.10.1", + "@docusaurus/types": "3.10.1", + "@docusaurus/utils": "3.10.1", + "@docusaurus/utils-validation": "3.10.1", + "@svgr/core": "8.1.0", + "@svgr/webpack": "^8.1.0", + "tslib": "^2.6.0", + "webpack": "^5.88.1" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/preset-classic": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.10.1.tgz", + "integrity": "sha512-YO/FL8v1zmbxoTso6mjMz/RDjhaTJxb1UpFFTDdY5847LLDCeyYiYlrhyTbgN1RIN3xnkLKZ9Lj1x8hUzI4JOg==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.1", + "@docusaurus/plugin-content-blog": "3.10.1", + "@docusaurus/plugin-content-docs": "3.10.1", + "@docusaurus/plugin-content-pages": "3.10.1", + "@docusaurus/plugin-css-cascade-layers": "3.10.1", + "@docusaurus/plugin-debug": "3.10.1", + "@docusaurus/plugin-google-analytics": "3.10.1", + "@docusaurus/plugin-google-gtag": "3.10.1", + "@docusaurus/plugin-google-tag-manager": "3.10.1", + "@docusaurus/plugin-sitemap": "3.10.1", + "@docusaurus/plugin-svgr": "3.10.1", + "@docusaurus/theme-classic": "3.10.1", + "@docusaurus/theme-common": "3.10.1", + "@docusaurus/theme-search-algolia": "3.10.1", + "@docusaurus/types": "3.10.1" + }, + "engines": { + "node": ">=20.0" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@docusaurus/responsive-loader": { @@ -2857,31 +4055,31 @@ } }, "node_modules/@docusaurus/theme-classic": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.6.1.tgz", - "integrity": "sha512-5lVUmIXk7zp+n9Ki2lYWrmhbd6mssOlKCnnDJvY4QDi3EgjRisIu5g4yKXoWTIbiqE7m7q/dS9cbeShEtfkKng==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.6.1", - "@docusaurus/logger": "3.6.1", - "@docusaurus/mdx-loader": "3.6.1", - "@docusaurus/module-type-aliases": "3.6.1", - "@docusaurus/plugin-content-blog": "3.6.1", - "@docusaurus/plugin-content-docs": "3.6.1", - "@docusaurus/plugin-content-pages": "3.6.1", - "@docusaurus/theme-common": "3.6.1", - "@docusaurus/theme-translations": "3.6.1", - "@docusaurus/types": "3.6.1", - "@docusaurus/utils": "3.6.1", - "@docusaurus/utils-common": "3.6.1", - "@docusaurus/utils-validation": "3.6.1", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.10.1.tgz", + "integrity": "sha512-VU1RK0qb2pab0si4r7HFK37cYco8VzqLj3u1PspVipSr/z/GPVKHO4/HXbnePqHoWDk8urjyGSeatH0NIMBM1A==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.1", + "@docusaurus/logger": "3.10.1", + "@docusaurus/mdx-loader": "3.10.1", + "@docusaurus/module-type-aliases": "3.10.1", + "@docusaurus/plugin-content-blog": "3.10.1", + "@docusaurus/plugin-content-docs": "3.10.1", + "@docusaurus/plugin-content-pages": "3.10.1", + "@docusaurus/theme-common": "3.10.1", + "@docusaurus/theme-translations": "3.10.1", + "@docusaurus/types": "3.10.1", + "@docusaurus/utils": "3.10.1", + "@docusaurus/utils-common": "3.10.1", + "@docusaurus/utils-validation": "3.10.1", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "copy-text-to-clipboard": "^3.2.0", "infima": "0.2.0-alpha.45", "lodash": "^4.17.21", "nprogress": "^0.2.0", - "postcss": "^8.4.26", + "postcss": "^8.5.4", "prism-react-renderer": "^2.3.0", "prismjs": "^1.29.0", "react-router-dom": "^5.3.4", @@ -2890,23 +4088,23 @@ "utility-types": "^3.10.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@docusaurus/theme-common": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.6.1.tgz", - "integrity": "sha512-18iEYNpMvarGfq9gVRpGowSZD24vZ39Iz4acqaj64180i54V9el8tVnhNr/wRvrUm1FY30A1NHLqnMnDz4rYEQ==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.10.1.tgz", + "integrity": "sha512-0YtmIeoNo1fIw65LO8+/1dPgmDV86UmhMkow37gzjytuiCSQm9xob6PJy0L4kuQEMTLfUOGvkXvZr7GPrHquMA==", "license": "MIT", "dependencies": { - "@docusaurus/mdx-loader": "3.6.1", - "@docusaurus/module-type-aliases": "3.6.1", - "@docusaurus/utils": "3.6.1", - "@docusaurus/utils-common": "3.6.1", + "@docusaurus/mdx-loader": "3.10.1", + "@docusaurus/module-type-aliases": "3.10.1", + "@docusaurus/utils": "3.10.1", + "@docusaurus/utils-common": "3.10.1", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -2917,30 +4115,31 @@ "utility-types": "^3.10.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "@docusaurus/plugin-content-docs": "*", - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@docusaurus/theme-search-algolia": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.6.1.tgz", - "integrity": "sha512-BjmuiFRpQP1WEm8Mzu1Bb0Wdas6G65VHXDDNr7XTKgbstxalE6vuxt0ioXTDFS2YVep5748aVhKvnxR9gm2Liw==", - "license": "MIT", - "dependencies": { - "@docsearch/react": "^3.5.2", - "@docusaurus/core": "3.6.1", - "@docusaurus/logger": "3.6.1", - "@docusaurus/plugin-content-docs": "3.6.1", - "@docusaurus/theme-common": "3.6.1", - "@docusaurus/theme-translations": "3.6.1", - "@docusaurus/utils": "3.6.1", - "@docusaurus/utils-validation": "3.6.1", - "algoliasearch": "^4.18.0", - "algoliasearch-helper": "^3.13.3", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.10.1.tgz", + "integrity": "sha512-OTaARARVZj2GvkJQjB+1jOIxntRaXea+G+fMsNqrZBAU1O1vJKDW22R7kECOHW27oJCLFN9HKaZeRrfAUyviug==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-core": "^1.19.2", + "@docsearch/react": "^3.9.0 || ^4.3.2", + "@docusaurus/core": "3.10.1", + "@docusaurus/logger": "3.10.1", + "@docusaurus/plugin-content-docs": "3.10.1", + "@docusaurus/theme-common": "3.10.1", + "@docusaurus/theme-translations": "3.10.1", + "@docusaurus/utils": "3.10.1", + "@docusaurus/utils-validation": "3.10.1", + "algoliasearch": "^5.37.0", + "algoliasearch-helper": "^3.26.0", "clsx": "^2.0.0", "eta": "^2.2.0", "fs-extra": "^11.1.1", @@ -2949,64 +4148,80 @@ "utility-types": "^3.10.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@docusaurus/theme-translations": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.6.1.tgz", - "integrity": "sha512-bNm5G6sueUezvyhsBegA1wwM38yW0BnqpZTE9KHO2yKnkERNMaV5x/yPJ/DNCOHjJtCcJ5Uz55g2AS75Go31xA==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.10.1.tgz", + "integrity": "sha512-cLMyaKivjBVWKMJuWqyFVVgtqe8DPJNPkog0bn8W1MDVAKcPdxRFycBfC1We1RaNp7Rdk513bmtW78RR6OBxBw==", "license": "MIT", "dependencies": { "fs-extra": "^11.1.1", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@docusaurus/tsconfig": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/tsconfig/-/tsconfig-3.5.2.tgz", - "integrity": "sha512-rQ7toURCFnWAIn8ubcquDs0ewhPwviMzxh6WpRjBW7sJVCXb6yzwUaY3HMNa0VXCFw+qkIbFywrMTf+Pb4uHWQ==", - "dev": true + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/tsconfig/-/tsconfig-3.10.1.tgz", + "integrity": "sha512-rYvB7yqkdqWIpAbDzQljGfM4cDBkLTbhmagZBEcsyj6oPUsz47lmW2pYdN1j+7sGFgltbAmQH62xfbrij4Eh6Q==", + "dev": true, + "license": "MIT" }, "node_modules/@docusaurus/types": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.6.1.tgz", - "integrity": "sha512-hCB1hj9DYutVYBisnPNobz9SzEmCcf1EetJv09O49Cov3BqOkm+vnnjB3d957YJMtpLGQoKBeN/FF1DZ830JwQ==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.10.1.tgz", + "integrity": "sha512-XYMK8k1szDCFMw2V+Xyen0g7Kee1sP3dtFnl7vkGkZOkeAJ/oPDQPL8iz4HBKOo/cwU8QeV6onVjMqtP+tFzsw==", "license": "MIT", "dependencies": { "@mdx-js/mdx": "^3.0.0", "@types/history": "^4.7.11", + "@types/mdast": "^4.0.2", "@types/react": "*", "commander": "^5.1.0", "joi": "^17.9.2", - "react-helmet-async": "^1.3.0", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", "utility-types": "^3.10.0", "webpack": "^5.95.0", "webpack-merge": "^5.9.0" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/types/node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" } }, "node_modules/@docusaurus/utils": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.6.1.tgz", - "integrity": "sha512-nS3WCvepwrnBEgSG5vQu40XG95lC9Jeh/odV5u5IhU1eQFEGDst9xBi6IK5yZdsGvbuaXBZLZtOqWYtuuFa/rQ==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.10.1.tgz", + "integrity": "sha512-3ojeJry9xBYdJO6qoyyzqeJFSJBVx2mXhyDzSdjwL2+URFQMf+h25gG38iswGImicK0ELjTd1EL2xzk8hf3QPw==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.6.1", - "@docusaurus/types": "3.6.1", - "@docusaurus/utils-common": "3.6.1", - "@svgr/webpack": "^8.1.0", + "@docusaurus/logger": "3.10.1", + "@docusaurus/types": "3.10.1", + "@docusaurus/utils-common": "3.10.1", "escape-string-regexp": "^4.0.0", + "execa": "^5.1.1", "file-loader": "^6.2.0", "fs-extra": "^11.1.1", "github-slugger": "^1.5.0", @@ -3016,40 +4231,40 @@ "js-yaml": "^4.1.0", "lodash": "^4.17.21", "micromatch": "^4.0.5", + "p-queue": "^6.6.2", "prompts": "^2.4.2", "resolve-pathname": "^3.0.0", - "shelljs": "^0.8.5", "tslib": "^2.6.0", "url-loader": "^4.1.1", "utility-types": "^3.10.0", "webpack": "^5.88.1" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@docusaurus/utils-common": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.6.1.tgz", - "integrity": "sha512-LX1qiTiC0aS8c92uZ+Wj2iNCNJyYZJIKY8/nZDKNMBfo759VYVS3RX3fKP3DznB+16sYp7++MyCz/T6fOGaRfw==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.10.1.tgz", + "integrity": "sha512-5mFSgEADtnFxFH7RLw02QA5MpU5JVUCj0MPeIvi/aF4Fi45tQRIuTwXoXDqJ+1VfQJuYJGz3SI63wmGz4HvXzA==", "license": "MIT", "dependencies": { - "@docusaurus/types": "3.6.1", + "@docusaurus/types": "3.10.1", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@docusaurus/utils-validation": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.6.1.tgz", - "integrity": "sha512-+iMd6zRl5cJQm7nUP+7pSO/oAXsN79eHO34ME7l2YJt4GEAr70l5kkD58u2jEPpp+wSXT70c7x2A2lzJI1E8jw==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.10.1.tgz", + "integrity": "sha512-cRv1X69jwaWv47waglllgZVWzeBFLhl53XT/XED/83BerVBTC5FTP8WTcVl8Z6sZOegDSwitu/wpCSPCDOT6lg==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.6.1", - "@docusaurus/utils": "3.6.1", - "@docusaurus/utils-common": "3.6.1", + "@docusaurus/logger": "3.10.1", + "@docusaurus/utils": "3.10.1", + "@docusaurus/utils-common": "3.10.1", "fs-extra": "^11.2.0", "joi": "^17.9.2", "js-yaml": "^4.1.0", @@ -3057,13 +4272,14 @@ "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", - "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" @@ -3084,13 +4300,23 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ "arm64" ], + "license": "Apache-2.0", "optional": true, "os": [ "darwin" @@ -3102,16 +4328,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", "cpu": [ "x64" ], + "license": "Apache-2.0", "optional": true, "os": [ "darwin" @@ -3123,16 +4350,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "cpu": [ "arm64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "darwin" @@ -3142,12 +4370,13 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", "cpu": [ "x64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "darwin" @@ -3157,12 +4386,13 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", "cpu": [ "arm" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" @@ -3172,12 +4402,45 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", "cpu": [ "arm64" ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" @@ -3187,12 +4450,13 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", "cpu": [ "s390x" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" @@ -3202,12 +4466,13 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", "cpu": [ "x64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" @@ -3217,42 +4482,89 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", "cpu": [ "arm64" ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, "funding": { "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" } }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", "cpu": [ - "x64" + "arm64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, "funding": { "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" } }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", "cpu": [ - "arm" + "ppc64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -3264,16 +4576,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" + "@img/sharp-libvips-linux-ppc64": "1.2.4" } }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", "cpu": [ - "arm64" + "riscv64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -3285,16 +4598,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" + "@img/sharp-libvips-linux-riscv64": "1.2.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", "cpu": [ "s390x" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -3306,16 +4620,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" + "@img/sharp-libvips-linux-s390x": "1.2.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", "cpu": [ "x64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -3327,16 +4642,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" + "@img/sharp-libvips-linux-x64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", "cpu": [ "arm64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -3348,16 +4664,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", "cpu": [ "x64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -3369,19 +4686,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", "cpu": [ "wasm32" ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.2.0" + "@emnapi/runtime": "^1.7.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -3390,13 +4708,33 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", "cpu": [ "ia32" ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" @@ -3409,12 +4747,13 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", "cpu": [ "x64" ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" @@ -3456,17 +4795,23 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -3477,37 +4822,438 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "license": "MIT", + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", + "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-core": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.2.tgz", + "integrity": "sha512-SVjwklkpIV5wrynpYtuYnfYH1QF4/nDuLBX7VXdb+3miglcAgBVZb/5y0cOsehRV/9Vb+3UqhkMq3/NR3ztdkQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-fsa": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.2.tgz", + "integrity": "sha512-fhO8+iR2I+OCw668ISDJdn1aArc9zx033sWejIyzQ8RBeXa9bDSaUeA3ix0poYOfrj1KdOzytmYNv2/uLDfV6g==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.2.tgz", + "integrity": "sha512-nX2AdL6cOFwLdju9G4/nbRnYevmCJbh7N7hvR3gGm97Cs60uEjyd0rpR+YBS7cTg175zzl22pGKXR5USaQMvKg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "@jsonjoy.com/fs-print": "4.57.2", + "@jsonjoy.com/fs-snapshot": "4.57.2", + "glob-to-regex.js": "^1.0.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-builtins": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.2.tgz", + "integrity": "sha512-xhiegylRmhw43Ki2HO1ZBL7DQ5ja/qpRsL29VtQ2xuUHiuDGbgf2uD4p9Qd8hJI5P6RCtGYD50IXHXVq/Ocjcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-to-fsa": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.2.tgz", + "integrity": "sha512-18LmWTSONhoAPW+IWRuf8w/+zRolPFGPeGwMxlAhhfY11EKzX+5XHDBPAw67dBF5dxDErHJbl40U+3IXSDRXSQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-fsa": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-utils": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.57.2.tgz", + "integrity": "sha512-rsPSJgekz43IlNbLyAM/Ab+ouYLWGp5DDBfYBNNEqDaSpsbXfthBn29Q4muFA9L0F+Z3mKo+CWlgSCXrf+mOyQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.57.2" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-print": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.57.2.tgz", + "integrity": "sha512-wK9NSow48i4DbDl9F1CQE5TqnyZOJ04elU3WFG5aJ76p+YxO/ulyBBQvKsessPxdo381Bc2pcEoyPujMOhcRqQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-utils": "4.57.2", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.57.2.tgz", + "integrity": "sha512-GdduDZuoP5V/QCgJkx9+BZ6SC0EZ/smXAdTS7PfMqgMTGXLlt/bH/FqMYaqB9JmLf05sJPtO0XRbAwwkEEPbVw==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^17.65.0", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "@jsonjoy.com/json-pack": "^17.65.0", + "@jsonjoy.com/util": "^17.65.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", + "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", + "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", + "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "17.67.0", + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0", + "@jsonjoy.com/json-pointer": "17.67.0", + "@jsonjoy.com/util": "17.67.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", + "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/util": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", + "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, "engines": { - "node": ">=6.0.0" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", - "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "license": "Apache-2.0", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, "node_modules/@leichtgewicht/ip-codec": { @@ -3517,25 +5263,27 @@ "license": "MIT" }, "node_modules/@mdx-js/mdx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.0.1.tgz", - "integrity": "sha512-eIQ4QTrOWyL3LWEe/bu6Taqzq2HQvHcyTMaOrI95P2/LmJE7AsfPfgJGuFLPVqBUE1BC1rik3VIhU+s9u72arA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", - "estree-util-build-jsx": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", - "estree-util-to-js": "^2.0.0", + "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", - "hast-util-to-estree": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", - "periscopic": "^3.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", @@ -3552,9 +5300,10 @@ } }, "node_modules/@mdx-js/react": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.0.0.tgz", - "integrity": "sha512-nDctevR9KyYFyV+m+/+S4cpzCWHqj+iHDHq3QrsWezcC+B17uZdIWgCguESUkwFhM3n/56KxWVE3V6EokrmONQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", + "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", + "license": "MIT", "dependencies": { "@types/mdx": "^2.0.0" }, @@ -3567,10 +5316,23 @@ "react": ">=16" } }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -3583,6 +5345,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", "engines": { "node": ">= 8" } @@ -3591,6 +5354,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -3599,10 +5363,168 @@ "node": ">= 8" } }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.7.0.tgz", + "integrity": "sha512-hew63shtzzvBcSHbhm+cyAmKe6AIfinT9hzEqSPjDC6opTTMKmTkQ0gHuN2KsWlvqiKw1S/fS94fhag/FJkioQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/asn1-x509": "^2.7.0", + "@peculiar/asn1-x509-attr": "^2.7.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.7.0.tgz", + "integrity": "sha512-VVsAyGqErT9D1SY4aEqozThXMVI+ssVRiv2DDeYuvpBKLIgZ3hYs3Ay3u/VSoKq6ESFi9cf6rf3IOOzfwh7oMA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/asn1-x509": "^2.7.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.7.0.tgz", + "integrity": "sha512-n7KEs/Q/wrB415cxy4fHOBhegp4NdJ15fkJPwcB/3/8iNBQC2L/N7SChJPKDJPZGYH0jD4Tg4/0vnHmwghnbKw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/asn1-x509": "^2.7.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.7.0.tgz", + "integrity": "sha512-V/nrlQVmhg7lYAsM7E13UDL5erAwFv6kCIVFqNaMIHSVi7dngcT839JkRTkQBqznMG98l2XjxYk74ZztAohZzA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.7.0", + "@peculiar/asn1-pkcs8": "^2.7.0", + "@peculiar/asn1-rsa": "^2.7.0", + "@peculiar/asn1-schema": "^2.7.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.7.0.tgz", + "integrity": "sha512-9GTl1nE8Mx1kTZ+7QyYatDyKsm34QcWRBFkY1iPvWC3X4Dona5s/tlLiQsx5WzVdZqiMBZNYT0buyw4/vbhnjw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/asn1-x509": "^2.7.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.7.0.tgz", + "integrity": "sha512-Bh7m+OuIaSEllPQcSd9OSp93F4ROWH7sbITWV8MI+8dwsjE5111/87VxiWVvYFKyww3vp39geLv9ENqhwWHcew==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.7.0", + "@peculiar/asn1-pfx": "^2.7.0", + "@peculiar/asn1-pkcs8": "^2.7.0", + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/asn1-x509": "^2.7.0", + "@peculiar/asn1-x509-attr": "^2.7.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.7.0.tgz", + "integrity": "sha512-/qvENQrXyTZURjMqSeofHul0JJt2sNSzSwk36pl2olkHbaioMQgrASDZAlHXl0xUlnVbHj0uGgOrBMTb5x2aJQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/asn1-x509": "^2.7.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.7.0.tgz", + "integrity": "sha512-W8ZfWzLmQnrcky+eh3tni4IozMdqBDiHWU0N+vve/UGjMaUs8c0L7A2oEdkBXS8rTpWDpK/aoI3DG/L/hxmxPg==", + "license": "MIT", + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.7.0.tgz", + "integrity": "sha512-mUn9RRrkGDnG4ALfunDmzyRW5dg+sWCj/pfnCCqEHYbkGxEpvUt6iVJv8Yw1cyp6SWZ26ZE5oSmI5SqEaen15g==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.7.0.tgz", + "integrity": "sha512-NS8e7SOgXipkzUPLF/sce7ukpMpWjhxYsH0n6Y+bHYo4TTxOb95Zv7hqwSuL212mj5YxovjdOKQOgH1As3E94w==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/asn1-x509": "^2.7.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "license": "MIT", "engines": { "node": ">=12.22.0" } @@ -3611,6 +5533,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "license": "MIT", "dependencies": { "graceful-fs": "4.2.10" }, @@ -3621,12 +5544,14 @@ "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "license": "ISC" }, "node_modules/@pnpm/npm-conf": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.2.2.tgz", - "integrity": "sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-3.0.2.tgz", + "integrity": "sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA==", + "license": "MIT", "dependencies": { "@pnpm/config.env-replace": "^1.1.0", "@pnpm/network.ca-file": "^1.0.1", @@ -3637,9 +5562,9 @@ } }, "node_modules/@polka/url": { - "version": "1.0.0-next.28", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", - "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", "license": "MIT" }, "node_modules/@sideway/address": { @@ -3664,9 +5589,9 @@ "license": "BSD-3-Clause" }, "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", "license": "MIT" }, "node_modules/@sindresorhus/is": { @@ -3681,20 +5606,6 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/@slorber/react-ideal-image": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@slorber/react-ideal-image/-/react-ideal-image-0.0.12.tgz", - "integrity": "sha512-u8KiDTEkMA7/KAeA5ywg/P7YG4zuKhWtswfVZDH8R8HXgQsFcHIYU2WaQnGuK/Du7Wdj90I+SdFmajSGFRvoKA==", - "engines": { - "node": ">= 8.9.0", - "npm": "> 3" - }, - "peerDependencies": { - "prop-types": ">=15", - "react": ">=0.14.x", - "react-waypoint": ">=9.0.2" - } - }, "node_modules/@slorber/remark-comment": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@slorber/remark-comment/-/remark-comment-1.0.0.tgz", @@ -3967,6 +5878,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "license": "MIT", "dependencies": { "defer-to-connect": "^2.0.1" }, @@ -3974,28 +5886,10 @@ "node": ">=14.16" } }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@types/acorn": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz", - "integrity": "sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==", - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", "license": "MIT", "dependencies": { "@types/connect": "*", @@ -4060,9 +5954,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, "node_modules/@types/estree-jsx": { @@ -4075,33 +5969,21 @@ } }, "node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", - "@types/serve-static": "*" + "@types/serve-static": "^1" } }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz", - "integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/express/node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", "license": "MIT", "dependencies": { "@types/node": "*", @@ -4111,9 +5993,9 @@ } }, "node_modules/@types/gtag.js": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.12.tgz", - "integrity": "sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg==", + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.20.tgz", + "integrity": "sha512-wwAbk3SA2QeU67unN7zPxjEHmPmlXwZXZvQEpbEUQuMCRGgKyE1m6XDuTUA9b6pCGb/GqJmdfMOY5LuDjJSbbg==", "license": "MIT" }, "node_modules/@types/hast": { @@ -4133,23 +6015,25 @@ "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==" + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "license": "MIT" }, "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "license": "MIT" }, "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", "license": "MIT" }, "node_modules/@types/http-proxy": { - "version": "1.17.15", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", - "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -4205,9 +6089,9 @@ "license": "MIT" }, "node_modules/@types/ms": { - "version": "0.7.34", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, "node_modules/@types/node": { @@ -4218,39 +6102,21 @@ "undici-types": "~5.26.4" } }, - "node_modules/@types/node-forge": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", - "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" - }, "node_modules/@types/parse5": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.3.tgz", "integrity": "sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==" }, "node_modules/@types/prismjs": { - "version": "1.26.3", - "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.3.tgz", - "integrity": "sha512-A0D0aTXvjlqJ5ZILMz3rNfDBOx9hHxLZYv2by47Sm/pqW35zzjusrZTryatjN/Rf8Us2gZrJD+KeHbUSTux1Cw==" - }, - "node_modules/@types/prop-types": { - "version": "15.7.11", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", - "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", + "license": "MIT" }, "node_modules/@types/qs": { - "version": "6.9.17", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", - "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", "license": "MIT" }, "node_modules/@types/range-parser": { @@ -4260,13 +6126,12 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.7.tgz", - "integrity": "sha512-KUnDCJF5+AiZd8owLIeVHqmW9yM4sqmDVf2JRJiBMFkGvkoZ4/WyV2lL4zVsoinmRS/W3FeEdZLEWFRofnT2FQ==", + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", + "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", "license": "MIT", "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-router": { @@ -4292,6 +6157,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "license": "MIT", "dependencies": { "@types/history": "^4.7.11", "@types/react": "*", @@ -4299,9 +6165,9 @@ } }, "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", "license": "MIT" }, "node_modules/@types/sax": { @@ -4314,12 +6180,11 @@ } }, "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", "license": "MIT", "dependencies": { - "@types/mime": "^1", "@types/node": "*" } }, @@ -4333,14 +6198,24 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", "license": "MIT", "dependencies": { "@types/http-errors": "*", "@types/node": "*", - "@types/send": "*" + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" } }, "node_modules/@types/sockjs": { @@ -4359,18 +6234,18 @@ "license": "MIT" }, "node_modules/@types/ws": { - "version": "8.5.13", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", - "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", "license": "MIT", "dependencies": { "@types/yargs-parser": "*" @@ -4383,151 +6258,168 @@ "license": "MIT" }, "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==" + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" }, "node_modules/abbrev": { "version": "1.1.1", @@ -4578,9 +6470,9 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -4589,6 +6481,18 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -4599,9 +6503,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", "license": "MIT", "dependencies": { "acorn": "^8.11.0" @@ -4614,6 +6518,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", + "license": "MIT", "engines": { "node": ">= 10.0.0" } @@ -4622,6 +6527,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -4631,9 +6537,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -4676,32 +6582,34 @@ } }, "node_modules/algoliasearch": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.24.0.tgz", - "integrity": "sha512-bf0QV/9jVejssFBmz2HQLxUadxk574t4iwjCKp5E7NBzwKkrDEhKPISIIjAU/p6K5qDx3qoeh4+26zWN1jmw3g==", - "license": "MIT", - "dependencies": { - "@algolia/cache-browser-local-storage": "4.24.0", - "@algolia/cache-common": "4.24.0", - "@algolia/cache-in-memory": "4.24.0", - "@algolia/client-account": "4.24.0", - "@algolia/client-analytics": "4.24.0", - "@algolia/client-common": "4.24.0", - "@algolia/client-personalization": "4.24.0", - "@algolia/client-search": "4.24.0", - "@algolia/logger-common": "4.24.0", - "@algolia/logger-console": "4.24.0", - "@algolia/recommend": "4.24.0", - "@algolia/requester-browser-xhr": "4.24.0", - "@algolia/requester-common": "4.24.0", - "@algolia/requester-node-http": "4.24.0", - "@algolia/transporter": "4.24.0" + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.52.1.tgz", + "integrity": "sha512-fHA8+kXTbjagw3jkLiaS7KKrH8qe2DyOsiUhGlN4cdT77PEsfqXZl7ewDk1hsg+pJnPlnE50XtLxjR91iJOpmg==", + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.18.1", + "@algolia/client-abtesting": "5.52.1", + "@algolia/client-analytics": "5.52.1", + "@algolia/client-common": "5.52.1", + "@algolia/client-insights": "5.52.1", + "@algolia/client-personalization": "5.52.1", + "@algolia/client-query-suggestions": "5.52.1", + "@algolia/client-search": "5.52.1", + "@algolia/ingestion": "1.52.1", + "@algolia/monitoring": "1.52.1", + "@algolia/recommend": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" } }, "node_modules/algoliasearch-helper": { - "version": "3.22.5", - "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.22.5.tgz", - "integrity": "sha512-lWvhdnc+aKOKx8jyA3bsdEgHzm/sglC4cYdMG4xSQyRiPLJVJtH/IVYZG3Hp6PkTEhQqhyVYkeP9z2IlcHJsWw==", + "version": "3.29.1", + "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.29.1.tgz", + "integrity": "sha512-6ck2YFudF2Pje7szQoPBiRFTGfd+1I+0I/WfLPGn0bj1kvrFoOQmNyedNiDxTk3/r4IfSLDYk+RA4G7u8H6+yA==", "license": "MIT", "dependencies": { "@algolia/events": "^4.0.1" @@ -4710,49 +6618,11 @@ "algoliasearch": ">= 3.1 < 6" } }, - "node_modules/algoliasearch/node_modules/@algolia/client-common": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.24.0.tgz", - "integrity": "sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==", - "license": "MIT", - "dependencies": { - "@algolia/requester-common": "4.24.0", - "@algolia/transporter": "4.24.0" - } - }, - "node_modules/algoliasearch/node_modules/@algolia/client-search": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.24.0.tgz", - "integrity": "sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "4.24.0", - "@algolia/requester-common": "4.24.0", - "@algolia/transporter": "4.24.0" - } - }, - "node_modules/algoliasearch/node_modules/@algolia/requester-browser-xhr": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.24.0.tgz", - "integrity": "sha512-Z2NxZMb6+nVXSjF13YpjYTdvV3032YTBSGm2vnYvYPA6mMxzM3v5rsCiSspndn9rzIW4Qp1lPHBvuoKJV6jnAA==", - "license": "MIT", - "dependencies": { - "@algolia/requester-common": "4.24.0" - } - }, - "node_modules/algoliasearch/node_modules/@algolia/requester-node-http": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.24.0.tgz", - "integrity": "sha512-JF18yTjNOVYvU/L3UosRcvbPMGT9B+/GQWNWnenIImglzNVGpyzChkXLnrSf6uxwVNO6ESGu6oN8MqcGQcjQJw==", - "license": "MIT", - "dependencies": { - "@algolia/requester-common": "4.24.0" - } - }, "node_modules/ansi-align": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "license": "ISC", "dependencies": { "string-width": "^4.1.0" } @@ -4760,46 +6630,21 @@ "node_modules/ansi-align/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" }, "node_modules/ansi-align/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + }, + "engines": { + "node": ">=8" } }, "node_modules/ansi-html-community": { @@ -4836,10 +6681,20 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -4862,7 +6717,8 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" }, "node_modules/array-flatten": { "version": "1.1.1", @@ -4874,27 +6730,34 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/astring": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/astring/-/astring-1.8.6.tgz", - "integrity": "sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", "license": "MIT", "bin": { "astring": "bin/astring" } }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/autocomplete.js": { "version": "0.37.1", "resolved": "https://registry.npmjs.org/autocomplete.js/-/autocomplete.js-0.37.1.tgz", @@ -4904,9 +6767,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.20", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", - "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", "funding": [ { "type": "opencollective", @@ -4923,11 +6786,10 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.23.3", - "caniuse-lite": "^1.0.30001646", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.1", + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -4972,13 +6834,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", - "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.15.tgz", + "integrity": "sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.2", + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.6", "semver": "^6.3.1" }, "peerDependencies": { @@ -4995,25 +6857,25 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.10.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", - "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2", - "core-js-compat": "^3.38.0" + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", - "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.6.tgz", + "integrity": "sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==", "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2" + "@babel/helper-define-polyfill-provider": "^0.6.6" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -5125,6 +6987,18 @@ } ] }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -5149,11 +7023,15 @@ } }, "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/bl": { @@ -5167,23 +7045,23 @@ } }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", @@ -5215,9 +7093,9 @@ "license": "MIT" }, "node_modules/bonjour-service": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", - "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -5233,6 +7111,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/boxen/-/boxen-6.2.1.tgz", "integrity": "sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==", + "license": "MIT", "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^6.2.0", @@ -5251,9 +7130,10 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5271,9 +7151,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", - "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "funding": [ { "type": "opencollective", @@ -5290,10 +7170,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.1" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -5330,6 +7211,21 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -5339,10 +7235,20 @@ "node": ">= 0.8" } }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/cacheable-lookup": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "license": "MIT", "engines": { "node": ">=14.16" } @@ -5351,6 +7257,7 @@ "version": "10.2.14", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "license": "MIT", "dependencies": { "@types/http-cache-semantics": "^4.0.2", "get-stream": "^6.0.1", @@ -5364,39 +7271,45 @@ "node": ">=14.16" } }, - "node_modules/cacheable-request/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/cacheable-request/node_modules/normalize-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", - "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==", - "engines": { - "node": ">=14.16" + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 0.4" } }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -5409,6 +7322,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", "engines": { "node": ">=6" } @@ -5417,6 +7331,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "license": "MIT", "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" @@ -5446,9 +7361,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001680", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz", - "integrity": "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==", + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", "funding": [ { "type": "opencollective", @@ -5578,15 +7493,10 @@ } }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -5599,6 +7509,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -5626,6 +7539,7 @@ "url": "https://github.com/sponsors/sibiraj-s" } ], + "license": "MIT", "engines": { "node": ">=8" } @@ -5634,6 +7548,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "license": "MIT", "dependencies": { "source-map": "~0.6.0" }, @@ -5645,6 +7560,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -5653,6 +7569,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", "engines": { "node": ">=6" } @@ -5661,6 +7578,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -5669,9 +7587,10 @@ } }, "node_modules/cli-table3": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", - "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "license": "MIT", "dependencies": { "string-width": "^4.2.0" }, @@ -5685,12 +7604,14 @@ "node_modules/cli-table3/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" }, "node_modules/cli-table3/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -5725,9 +7646,10 @@ } }, "node_modules/clsx": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", - "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -5844,25 +7766,25 @@ } }, "node_modules/compressible/node_modules/mime-db": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", - "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/compression": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz", - "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "license": "MIT", "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", - "on-headers": "~1.0.2", + "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" }, @@ -5903,6 +7825,7 @@ "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "license": "MIT", "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" @@ -5912,6 +7835,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/configstore/-/configstore-6.0.0.tgz", "integrity": "sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==", + "license": "BSD-2-Clause", "dependencies": { "dot-prop": "^6.0.1", "graceful-fs": "^4.2.6", @@ -5936,9 +7860,9 @@ } }, "node_modules/consola": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", - "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", "license": "MIT", "engines": { "node": "^14.18.0 || >=16.10.0" @@ -5949,11 +7873,6 @@ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" }, - "node_modules/consolidated-events": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/consolidated-events/-/consolidated-events-2.0.2.tgz", - "integrity": "sha512-2/uRVMdRypf5z/TW/ncD/66l75P5hH2vM/GR8Jf8HLc2xnfJtmina6F6du8+v4Z2vTrMo7jC+W1tmEEuuELgkQ==" - }, "node_modules/content-disposition": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", @@ -5975,27 +7894,28 @@ "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" }, "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "license": "MIT" }, "node_modules/copy-text-to-clipboard": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.0.tgz", - "integrity": "sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.2.tgz", + "integrity": "sha512-T6SqyLd1iLuqPA90J5N4cTalrtovCySh58iiZDGJ6FGznbclKh4UI+FGacQSgFzwKG77W7XT5gwbVEbd9cIH1A==", "license": "MIT", "engines": { "node": ">=12" @@ -6072,39 +7992,29 @@ } }, "node_modules/core-js": { - "version": "3.34.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.34.0.tgz", - "integrity": "sha512-aDdvlDder8QmY91H88GzNi9EtQi2TjvQhpCX6B1v/dAZHU1AuLgHvRh54RiOerpEhEW46Tkf+vgAViB/CWC0ag==", + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", "hasInstallScript": true, + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" } }, "node_modules/core-js-compat": { - "version": "3.39.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz", - "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==", + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz", + "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==", "license": "MIT", "dependencies": { - "browserslist": "^4.24.2" + "browserslist": "^4.28.1" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" } }, - "node_modules/core-js-pure": { - "version": "3.39.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.39.0.tgz", - "integrity": "sha512-7fEcWwKI4rJinnK+wLTezeg2smbFFdSBP6E2kQZNbnzM2s1rpKQ6aaRteZSSg7FLU3P0HGGVo/gbpfanU36urg==", - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -6154,6 +8064,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "license": "MIT", "dependencies": { "type-fest": "^1.0.1" }, @@ -6168,6 +8079,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -6175,10 +8087,48 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/css-blank-pseudo": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-7.0.1.tgz", + "integrity": "sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-blank-pseudo/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/css-declaration-sorter": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz", - "integrity": "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.4.0.tgz", + "integrity": "sha512-LTuzjPoyA2vMGKKcaOqKSp7Ub2eGrNfKiZH4LpezxpNrsICGCSFvsQOI29psISxNZtaXibkC2CXzrQ5enMeGGw==", "license": "ISC", "engines": { "node": "^14 || ^16 || >=18" @@ -6187,20 +8137,83 @@ "postcss": "^8.0.9" } }, + "node_modules/css-has-pseudo": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-7.0.3.tgz", + "integrity": "sha512-oG+vKuGyqe/xvEMoxAQrhi7uY16deJR3i7wwhBerVrGQKSqUC5GiOVxTpM9F9B9hw0J+eKeOWLH7E9gZ1Dr5rA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-has-pseudo/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/css-loader": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", - "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz", + "integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==", "dev": true, + "license": "MIT", "dependencies": { "icss-utils": "^5.1.0", - "postcss": "^8.4.33", + "postcss": "^8.4.40", "postcss-modules-extract-imports": "^3.1.0", "postcss-modules-local-by-default": "^4.0.5", "postcss-modules-scope": "^3.2.0", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" + "semver": "^7.6.3" }, "engines": { "node": ">= 18.12.0" @@ -6210,7 +8223,7 @@ "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "@rspack/core": "0.x || 1.x", + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", "webpack": "^5.27.0" }, "peerDependenciesMeta": { @@ -6266,10 +8279,32 @@ } } }, + "node_modules/css-prefers-color-scheme": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-10.0.0.tgz", + "integrity": "sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", @@ -6311,6 +8346,22 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/cssdb": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.9.0.tgz", + "integrity": "sha512-J8jOU/hLjaXcO1LldOLraJSQpfLXRKof0I7mtbRyOy2AAXgqst0x9rlgi2qXeD6d0ou3ZLqcPAMqYVbpCbrxEw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + } + ], + "license": "MIT-0" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -6453,9 +8504,10 @@ "license": "CC0-1.0" }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" }, "node_modules/debounce": { "version": "1.2.1", @@ -6464,11 +8516,12 @@ "license": "MIT" }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -6480,9 +8533,9 @@ } }, "node_modules/decode-named-character-reference": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", - "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", "license": "MIT", "dependencies": { "character-entities": "^2.0.0" @@ -6529,26 +8582,44 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/default-gateway": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", - "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", - "license": "BSD-2-Clause", + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", "dependencies": { - "execa": "^5.0.0" + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", "engines": { - "node": ">= 10" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "license": "MIT", "engines": { "node": ">=10" } @@ -6574,6 +8645,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", "engines": { "node": ">=8" } @@ -6595,27 +8667,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/del": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", - "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", - "dependencies": { - "globby": "^11.0.1", - "graceful-fs": "^4.2.4", - "is-glob": "^4.0.1", - "is-path-cwd": "^2.2.0", - "is-path-inside": "^3.0.2", - "p-map": "^4.0.0", - "rimraf": "^3.0.2", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -6645,9 +8696,10 @@ } }, "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", "engines": { "node": ">=8" } @@ -6659,9 +8711,10 @@ "license": "MIT" }, "node_modules/detect-port": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.5.1.tgz", - "integrity": "sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.6.1.tgz", + "integrity": "sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==", + "license": "MIT", "dependencies": { "address": "^1.0.1", "debug": "4" @@ -6669,37 +8722,11 @@ "bin": { "detect": "bin/detect-port.js", "detect-port": "bin/detect-port.js" - } - }, - "node_modules/detect-port-alt": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", - "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", - "dependencies": { - "address": "^1.0.1", - "debug": "^2.6.0" - }, - "bin": { - "detect": "bin/detect-port", - "detect-port": "bin/detect-port" }, "engines": { - "node": ">= 4.2.1" - } - }, - "node_modules/detect-port-alt/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" + "node": ">= 4.0.0" } }, - "node_modules/detect-port-alt/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -6717,6 +8744,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "license": "MIT", "dependencies": { "path-type": "^4.0.0" }, @@ -6749,32 +8777,33 @@ } }, "node_modules/docusaurus-lunr-search": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/docusaurus-lunr-search/-/docusaurus-lunr-search-3.5.0.tgz", - "integrity": "sha512-k3zN4jYMi/prWInJILGKOxE+BVcgYinwj9+gcECsYm52tS+4ZKzXQzbPnVJAEXmvKOfFMcDFvS3MSmm6cEaxIQ==", - "dependencies": { - "autocomplete.js": "^0.37.0", - "clsx": "^1.2.1", - "gauge": "^3.0.0", - "hast-util-select": "^4.0.0", - "hast-util-to-text": "^2.0.0", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/docusaurus-lunr-search/-/docusaurus-lunr-search-3.6.0.tgz", + "integrity": "sha512-CCEAnj5e67sUZmIb2hOl4xb4nDN07fb0fvRDDmdWlYpUvyS1CSKbw4lsGInLyUFEEEBzxQmT6zaVQdF/8Zretg==", + "license": "MIT", + "dependencies": { + "autocomplete.js": "^0.37.1", + "clsx": "^2.1.1", + "gauge": "^3.0.2", + "hast-util-select": "^4.0.2", + "hast-util-to-text": "^2.0.1", "hogan.js": "^3.0.2", - "lunr": "^2.3.8", + "lunr": "^2.3.9", "lunr-languages": "^1.4.0", "mark.js": "^8.11.1", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "rehype-parse": "^7.0.1", "to-vfile": "^6.1.0", - "unified": "^9.0.0", - "unist-util-is": "^4.0.2" + "unified": "^9.2.2", + "unist-util-is": "^4.1.0" }, "engines": { "node": ">= 8.10.0" }, "peerDependencies": { "@docusaurus/core": "^2.0.0-alpha.60 || ^2.0.0 || ^3.0.0", - "react": "^16.8.4 || ^17 || ^18", - "react-dom": "^16.8.4 || ^17 || ^18" + "react": "^16.8.4 || ^17 || ^18 || ^19", + "react-dom": "^16.8.4 || ^17 || ^18 || ^19" } }, "node_modules/docusaurus-lunr-search/node_modules/@types/unist": { @@ -6791,14 +8820,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/docusaurus-lunr-search/node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "engines": { - "node": ">=6" - } - }, "node_modules/docusaurus-lunr-search/node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -6886,6 +8907,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "license": "MIT", "dependencies": { "utila": "~0.4" } @@ -6931,9 +8953,9 @@ } }, "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", @@ -6957,6 +8979,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "license": "MIT", "dependencies": { "is-obj": "^2.0.0" }, @@ -6971,19 +8994,36 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "license": "MIT" }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" }, "node_modules/ee-first": { "version": "1.1.1", @@ -6992,15 +9032,16 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.55", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.55.tgz", - "integrity": "sha512-6maZ2ASDOTBtjt9FhqYPRnbvKU5tjG0IN9SztUOWYw2AzNDNpKJYLJmlK0/En4Hs/aiWnB+JZ+gW19PIGszgKg==", + "version": "1.5.354", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.354.tgz", + "integrity": "sha512-JaBHwWcfIdmSAfWM5l3uwjGd431j8YEMikZ+K/2nXVuBqJKyZ0f+2h4n4JY5AyNiZmnY9qQr2RU3v9DxDmHMNg==", "license": "ISC" }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" }, "node_modules/emojilib": { "version": "2.4.0", @@ -7044,12 +9085,13 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -7059,6 +9101,7 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -7067,21 +9110,19 @@ } }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, "engines": { "node": ">= 0.4" } @@ -7096,9 +9137,54 @@ } }, "node_modules/es-module-lexer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", - "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, "node_modules/escalade": { "version": "3.2.0", @@ -7113,6 +9199,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -7227,6 +9314,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/estree-util-to-js": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", @@ -7243,9 +9344,9 @@ } }, "node_modules/estree-util-value-to-estree": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.3.3.tgz", - "integrity": "sha512-Db+m1WSD4+mUO7UgMeKkAwdbfNWwIxLt48XF2oFU9emPfXkIu+k5/nlOj313v7wqtAPo0f9REhUvznFrPkG8CQ==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.5.0.tgz", + "integrity": "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" @@ -7281,6 +9382,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } @@ -7354,18 +9456,6 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/execa/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -7375,39 +9465,39 @@ } }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -7448,9 +9538,9 @@ "license": "MIT" }, "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, "node_modules/express/node_modules/range-parser": { @@ -7491,15 +9581,16 @@ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -7508,18 +9599,30 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "BSD-3-Clause" }, "node_modules/fastq": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", - "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -7561,34 +9664,11 @@ "node": ">=0.4.0" } }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/file-loader": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "license": "MIT", "dependencies": { "loader-utils": "^2.0.0", "schema-utils": "^3.0.0" @@ -7605,9 +9685,10 @@ } }, "node_modules/file-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -7623,6 +9704,7 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", "peerDependencies": { "ajv": "^6.9.1" } @@ -7630,12 +9712,14 @@ "node_modules/file-loader/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" }, "node_modules/file-loader/node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -7649,14 +9733,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/filesize": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", - "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -7669,17 +9745,17 @@ } }, "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", "license": "MIT", "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "statuses": "2.0.1", + "statuses": "~2.0.2", "unpipe": "~1.0.0" }, "engines": { @@ -7742,9 +9818,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -7761,132 +9837,13 @@ } } }, - "node_modules/fork-ts-checker-webpack-plugin": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", - "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==", - "dependencies": { - "@babel/code-frame": "^7.8.3", - "@types/json-schema": "^7.0.5", - "chalk": "^4.1.0", - "chokidar": "^3.4.2", - "cosmiconfig": "^6.0.0", - "deepmerge": "^4.2.2", - "fs-extra": "^9.0.0", - "glob": "^7.1.6", - "memfs": "^3.1.2", - "minimatch": "^3.0.4", - "schema-utils": "2.7.0", - "semver": "^7.3.2", - "tapable": "^1.0.0" - }, - "engines": { - "node": ">=10", - "yarn": ">=1.0.0" - }, - "peerDependencies": { - "eslint": ">= 6", - "typescript": ">= 2.7", - "vue-template-compiler": "*", - "webpack": ">= 4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - }, - "vue-template-compiler": { - "optional": true - } - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "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/fork-ts-checker-webpack-plugin/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==", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", - "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", - "dependencies": { - "@types/json-schema": "^7.0.4", - "ajv": "^6.12.2", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "engines": { - "node": ">=6" - } - }, "node_modules/form-data-encoder": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.0.2.tgz", - "integrity": "sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "license": "MIT", "engines": { - "node": ">= 18" + "node": ">= 14.17" } }, "node_modules/format": { @@ -7907,15 +9864,15 @@ } }, "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", "license": "MIT", "engines": { "node": "*" }, "funding": { - "type": "patreon", + "type": "github", "url": "https://github.com/sponsors/rawify" } }, @@ -7946,21 +9903,12 @@ "node": ">=14.14" } }, - "node_modules/fs-monkey": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz", - "integrity": "sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -7973,6 +9921,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8019,21 +9968,27 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -8048,12 +10003,26 @@ "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", "license": "ISC" }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", "engines": { - "node": ">=16" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -8070,29 +10039,11 @@ "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", "license": "ISC" }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -8100,15 +10051,33 @@ "node": ">= 6" } }, + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" }, "node_modules/global-dirs": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "license": "MIT", "dependencies": { "ini": "2.0.0" }, @@ -8123,58 +10092,16 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "license": "ISC", "engines": { "node": ">=10" } }, - "node_modules/global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "dependencies": { - "global-prefix": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "dependencies": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "license": "MIT", "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -8191,47 +10118,49 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/got": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/got/-/got-14.0.0.tgz", - "integrity": "sha512-X01vTgaX9SwaMq5DfImvS+3GMQFFs5HtrrlS9CuzUSzkxAf/tWGEyynuI+Qy7BjciMczZGjyVSmawYbP4eYhYA==", + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", + "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", + "license": "MIT", "dependencies": { - "@sindresorhus/is": "^6.1.0", + "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.14", + "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", - "form-data-encoder": "^4.0.2", - "get-stream": "^8.0.1", - "http2-wrapper": "^2.2.1", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", - "p-cancelable": "^4.0.1", + "p-cancelable": "^3.0.0", "responselike": "^3.0.0" }, "engines": { - "node": ">=20" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sindresorhus/got?sponsor=1" } }, "node_modules/got/node_modules/@sindresorhus/is": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-6.1.0.tgz", - "integrity": "sha512-BuvU07zq3tQ/2SIgBsEuxKYDyDjC0n7Zir52bpHy2xnBbW81+po43aLFPLbeV3HRAheFbGud1qgcqSYfhtHMAg==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "license": "MIT", "engines": { - "node": ">=16" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sindresorhus/is?sponsor=1" @@ -8267,9 +10196,9 @@ } }, "node_modules/gray-matter/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -8283,6 +10212,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "license": "MIT", "dependencies": { "duplexer": "^0.1.2" }, @@ -8319,22 +10249,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -8352,6 +10270,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-3.0.0.tgz", "integrity": "sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==", + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -8360,9 +10279,10 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -8371,16 +10291,16 @@ } }, "node_modules/hast-util-from-parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz", - "integrity": "sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", - "hastscript": "^8.0.0", - "property-information": "^6.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" @@ -8422,9 +10342,9 @@ } }, "node_modules/hast-util-raw": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.0.4.tgz", - "integrity": "sha512-LHE65TD2YiNsHD3YuXcKPHXPLuYh/gjp12mOfU8jxSrm1f/yJpsb0F/KKljS6U9LJoP0Ux+tCe8iJ2AsPzTdgA==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -8561,9 +10481,9 @@ } }, "node_modules/hast-util-to-estree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.0.tgz", - "integrity": "sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -8577,9 +10497,9 @@ "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^6.0.0", + "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", - "style-to-object": "^0.4.0", + "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" }, @@ -8589,9 +10509,9 @@ } }, "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz", - "integrity": "sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -8604,9 +10524,9 @@ "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^6.0.0", + "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", - "style-to-object": "^1.0.0", + "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" }, @@ -8615,31 +10535,16 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/hast-util-to-jsx-runtime/node_modules/inline-style-parser": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.3.tgz", - "integrity": "sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==", - "license": "MIT" - }, - "node_modules/hast-util-to-jsx-runtime/node_modules/style-to-object": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.6.tgz", - "integrity": "sha512-khxq+Qm3xEyZfKd/y9L3oIWQimxuc4STrQKtQn8aSDRHb8mFgpukgX1hdzfrMEW6JCjyJ8p89x+IUMVnCBI1PA==", - "license": "MIT", - "dependencies": { - "inline-style-parser": "0.2.3" - } - }, "node_modules/hast-util-to-parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", - "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", - "property-information": "^6.0.0", + "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" @@ -8686,15 +10591,15 @@ } }, "node_modules/hastscript": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz", - "integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", - "property-information": "^6.0.0", + "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" }, "funding": { @@ -8706,6 +10611,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", "bin": { "he": "bin/he" } @@ -8791,22 +10697,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/html-entities": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", - "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ], - "license": "MIT" - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -8847,6 +10737,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "license": "MIT", "engines": { "node": ">=8" }, @@ -8865,9 +10756,10 @@ } }, "node_modules/html-webpack-plugin": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz", - "integrity": "sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw==", + "version": "5.6.7", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.7.tgz", + "integrity": "sha512-md+vXtdCAe60s1k6AU3dUyMJnDxUyQAwfwPKoLisvgUF1IXjtlLsk2se54+qfL9Mdm26bbwvjJybpNx48NKRLw==", + "license": "MIT", "dependencies": { "@types/html-minifier-terser": "^6.0.0", "html-minifier-terser": "^6.0.2", @@ -8899,6 +10791,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", "engines": { "node": ">= 12" } @@ -8907,6 +10800,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "license": "MIT", "dependencies": { "camel-case": "^4.1.2", "clean-css": "^5.2.2", @@ -8943,9 +10837,10 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" }, "node_modules/http-deceiver": { "version": "1.2.7", @@ -8954,25 +10849,29 @@ "license": "MIT" }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/http-parser-js": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", - "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", "license": "MIT" }, "node_modules/http-proxy": { @@ -9029,6 +10928,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "license": "MIT", "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" @@ -9046,6 +10946,15 @@ "node": ">=10.17.0" } }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -9089,21 +10998,19 @@ ] }, "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", + "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, "bin": { "image-size": "bin/image-size.js" }, @@ -9116,19 +11023,11 @@ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz", "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==" }, - "node_modules/immer": { - "version": "9.0.21", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", - "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -9144,6 +11043,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "license": "MIT", "engines": { "node": ">=8" } @@ -9152,6 +11052,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", "engines": { "node": ">=0.8.19" } @@ -9160,6 +11061,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", "engines": { "node": ">=8" } @@ -9173,15 +11075,6 @@ "node": ">=12" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -9193,31 +11086,24 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, "node_modules/inline-style-parser": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", - "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", "license": "MIT" }, - "node_modules/interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", "dependencies": { "loose-envify": "^1.0.0" } }, "node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", "license": "MIT", "engines": { "node": ">= 10" @@ -9250,12 +11136,14 @@ "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -9289,6 +11177,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "license": "MIT", "dependencies": { "ci-info": "^3.2.0" }, @@ -9297,11 +11186,15 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9321,6 +11214,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", "bin": { "is-docker": "cli.js" }, @@ -9344,6 +11238,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -9360,6 +11255,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -9377,10 +11273,44 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-installed-globally": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "license": "MIT", "dependencies": { "global-dirs": "^3.0.0", "is-path-inside": "^3.0.2" @@ -9392,10 +11322,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-network-error": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", + "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-npm": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.0.0.tgz", - "integrity": "sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.1.0.tgz", + "integrity": "sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==", + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -9420,18 +11363,11 @@ "node": ">=0.10.0" } }, - "node_modules/is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "engines": { - "node": ">=6" - } - }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "license": "MIT", "engines": { "node": ">=8" } @@ -9448,15 +11384,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-reference": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", - "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, "node_modules/is-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", @@ -9466,14 +11393,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-root": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", - "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", - "engines": { - "node": ">=6" - } - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -9489,12 +11408,14 @@ "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", "dependencies": { "is-docker": "^2.0.0" }, @@ -9506,6 +11427,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.4.1.tgz", "integrity": "sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==", + "license": "MIT", "engines": { "node": ">=12" } @@ -9576,9 +11498,10 @@ } }, "node_modules/jiti": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", "bin": { "jiti": "bin/jiti.js" } @@ -9602,9 +11525,10 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -9613,9 +11537,9 @@ } }, "node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -9627,7 +11551,8 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -9666,6 +11591,7 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -9690,6 +11616,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==", + "license": "MIT", "dependencies": { "package-json": "^8.1.0" }, @@ -9701,27 +11628,28 @@ } }, "node_modules/launch-editor": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", - "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.2.tgz", + "integrity": "sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==", "license": "MIT", "dependencies": { - "picocolors": "^1.0.0", - "shell-quote": "^1.8.1" + "picocolors": "^1.1.1", + "shell-quote": "^1.8.3" } }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/lilconfig": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", - "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "license": "MIT", "engines": { "node": ">=14" @@ -9733,14 +11661,20 @@ "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" }, "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "license": "MIT", "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/loader-utils": { @@ -9772,9 +11706,10 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" }, "node_modules/lodash.debounce": { "version": "4.0.8", @@ -9827,6 +11762,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -9880,14 +11816,24 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdast-util-directive": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.0.0.tgz", - "integrity": "sha512-JUpYOqKI4mM3sZcNxmF/ox04XYFFkNwr0CFlrQIkCwbvH0xzMCqkMqAde9wRd80VAhaUrwFwKm2nxretdT1h7Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", + "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", + "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", @@ -9901,9 +11847,9 @@ } }, "node_modules/mdast-util-find-and-replace": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", - "integrity": "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -9929,9 +11875,9 @@ } }, "node_modules/mdast-util-from-markdown": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz", - "integrity": "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -9953,9 +11899,9 @@ } }, "node_modules/mdast-util-from-markdown/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -9999,9 +11945,9 @@ } }, "node_modules/mdast-util-gfm": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz", - "integrity": "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", "license": "MIT", "dependencies": { "mdast-util-from-markdown": "^2.0.0", @@ -10035,9 +11981,9 @@ } }, "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -10055,9 +12001,9 @@ } }, "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -10071,9 +12017,9 @@ "license": "MIT" }, "node_modules/mdast-util-gfm-footnote": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz", - "integrity": "sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -10153,9 +12099,9 @@ } }, "node_modules/mdast-util-mdx-expression": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz", - "integrity": "sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", @@ -10171,9 +12117,9 @@ } }, "node_modules/mdast-util-mdx-jsx": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.2.tgz", - "integrity": "sha512-eKMQDeywY2wlHc97k5eD8VC+9ASMjN8ItEZQNGwJ6E0XWKiW/Z0V5/H8pvoXUf+y+Mj0VIgeRRbujBmFn4FTyA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", @@ -10186,7 +12132,6 @@ "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", - "unist-util-remove-position": "^5.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" }, @@ -10228,9 +12173,9 @@ } }, "node_modules/mdast-util-to-hast": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", - "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -10249,9 +12194,9 @@ } }, "node_modules/mdast-util-to-markdown": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", - "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -10259,6 +12204,7 @@ "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" @@ -10297,14 +12243,32 @@ } }, "node_modules/memfs": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", - "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.57.2.tgz", + "integrity": "sha512-2nWzSsJzrukurSDna4Z0WywuScK4Id3tSKejgu74u8KCdW4uNrseKRSIDg75C6Yw5ZRqBe0F0EtMNlTbUq8bAQ==", + "license": "Apache-2.0", "dependencies": { - "fs-monkey": "^1.0.4" + "@jsonjoy.com/fs-core": "4.57.2", + "@jsonjoy.com/fs-fsa": "4.57.2", + "@jsonjoy.com/fs-node": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-to-fsa": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "@jsonjoy.com/fs-print": "4.57.2", + "@jsonjoy.com/fs-snapshot": "4.57.2", + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" }, - "engines": { - "node": ">= 4.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, "node_modules/merge-descriptors": { @@ -10325,6 +12289,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", "engines": { "node": ">= 8" } @@ -10339,9 +12304,9 @@ } }, "node_modules/micromark": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz", - "integrity": "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", "funding": [ { "type": "GitHub Sponsors", @@ -10374,9 +12339,9 @@ } }, "node_modules/micromark-core-commonmark": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.1.tgz", - "integrity": "sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", "funding": [ { "type": "GitHub Sponsors", @@ -10408,9 +12373,9 @@ } }, "node_modules/micromark-core-commonmark/node_modules/micromark-factory-space": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", - "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", "funding": [ { "type": "GitHub Sponsors", @@ -10428,9 +12393,9 @@ } }, "node_modules/micromark-core-commonmark/node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -10448,9 +12413,9 @@ } }, "node_modules/micromark-core-commonmark/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -10483,9 +12448,9 @@ } }, "node_modules/micromark-extension-directive/node_modules/micromark-factory-space": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", - "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", "funding": [ { "type": "GitHub Sponsors", @@ -10503,9 +12468,9 @@ } }, "node_modules/micromark-extension-directive/node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -10523,9 +12488,9 @@ } }, "node_modules/micromark-extension-directive/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -10555,9 +12520,9 @@ } }, "node_modules/micromark-extension-frontmatter/node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -10575,9 +12540,9 @@ } }, "node_modules/micromark-extension-frontmatter/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -10627,9 +12592,9 @@ } }, "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -10647,9 +12612,9 @@ } }, "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -10683,9 +12648,9 @@ } }, "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-factory-space": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", - "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", "funding": [ { "type": "GitHub Sponsors", @@ -10703,9 +12668,9 @@ } }, "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -10723,9 +12688,9 @@ } }, "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -10757,9 +12722,9 @@ } }, "node_modules/micromark-extension-gfm-strikethrough/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -10773,9 +12738,9 @@ "license": "MIT" }, "node_modules/micromark-extension-gfm-table": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.0.tgz", - "integrity": "sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -10790,9 +12755,9 @@ } }, "node_modules/micromark-extension-gfm-table/node_modules/micromark-factory-space": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", - "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", "funding": [ { "type": "GitHub Sponsors", @@ -10810,9 +12775,9 @@ } }, "node_modules/micromark-extension-gfm-table/node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -10830,9 +12795,9 @@ } }, "node_modules/micromark-extension-gfm-table/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -10876,9 +12841,9 @@ } }, "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-factory-space": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", - "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", "funding": [ { "type": "GitHub Sponsors", @@ -10896,9 +12861,9 @@ } }, "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -10916,9 +12881,9 @@ } }, "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -10932,9 +12897,9 @@ "license": "MIT" }, "node_modules/micromark-extension-mdx-expression": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.0.tgz", - "integrity": "sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", "funding": [ { "type": "GitHub Sponsors", @@ -10958,9 +12923,9 @@ } }, "node_modules/micromark-extension-mdx-expression/node_modules/micromark-factory-space": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", - "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", "funding": [ { "type": "GitHub Sponsors", @@ -10978,9 +12943,9 @@ } }, "node_modules/micromark-extension-mdx-expression/node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -10998,9 +12963,9 @@ } }, "node_modules/micromark-extension-mdx-expression/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11014,18 +12979,18 @@ "license": "MIT" }, "node_modules/micromark-extension-mdx-jsx": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.0.tgz", - "integrity": "sha512-uvhhss8OGuzR4/N17L1JwvmJIpPhAd8oByMawEKx6NVdBCbesjH4t+vjEp3ZXft9DwvlKSD07fCeI44/N0Vf2w==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", "license": "MIT", "dependencies": { - "@types/acorn": "^4.0.0", "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" @@ -11036,9 +13001,9 @@ } }, "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-factory-space": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", - "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", "funding": [ { "type": "GitHub Sponsors", @@ -11056,9 +13021,9 @@ } }, "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11076,9 +13041,9 @@ } }, "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11146,9 +13111,9 @@ } }, "node_modules/micromark-extension-mdxjs-esm/node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11166,9 +13131,9 @@ } }, "node_modules/micromark-extension-mdxjs-esm/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11182,9 +13147,9 @@ "license": "MIT" }, "node_modules/micromark-factory-destination": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", - "integrity": "sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", "funding": [ { "type": "GitHub Sponsors", @@ -11203,9 +13168,9 @@ } }, "node_modules/micromark-factory-destination/node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11223,9 +13188,9 @@ } }, "node_modules/micromark-factory-destination/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11239,9 +13204,9 @@ "license": "MIT" }, "node_modules/micromark-factory-label": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz", - "integrity": "sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", "funding": [ { "type": "GitHub Sponsors", @@ -11261,9 +13226,9 @@ } }, "node_modules/micromark-factory-label/node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11281,9 +13246,9 @@ } }, "node_modules/micromark-factory-label/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11297,9 +13262,9 @@ "license": "MIT" }, "node_modules/micromark-factory-mdx-expression": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.1.tgz", - "integrity": "sha512-F0ccWIUHRLRrYp5TC9ZYXmZo+p2AM13ggbsW4T0b5CRKP8KHVRB8t4pwtBgTxtjRmwrK0Irwm7vs2JOZabHZfg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", "funding": [ { "type": "GitHub Sponsors", @@ -11314,6 +13279,7 @@ "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", @@ -11322,10 +13288,30 @@ "vfile-message": "^4.0.0" } }, + "node_modules/micromark-factory-mdx-expression/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, "node_modules/micromark-factory-mdx-expression/node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11343,9 +13329,9 @@ } }, "node_modules/micromark-factory-mdx-expression/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11395,9 +13381,9 @@ "license": "MIT" }, "node_modules/micromark-factory-title": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz", - "integrity": "sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", "funding": [ { "type": "GitHub Sponsors", @@ -11417,9 +13403,9 @@ } }, "node_modules/micromark-factory-title/node_modules/micromark-factory-space": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", - "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", "funding": [ { "type": "GitHub Sponsors", @@ -11437,9 +13423,9 @@ } }, "node_modules/micromark-factory-title/node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11457,9 +13443,9 @@ } }, "node_modules/micromark-factory-title/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11473,9 +13459,9 @@ "license": "MIT" }, "node_modules/micromark-factory-whitespace": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz", - "integrity": "sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", "funding": [ { "type": "GitHub Sponsors", @@ -11495,9 +13481,9 @@ } }, "node_modules/micromark-factory-whitespace/node_modules/micromark-factory-space": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", - "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", "funding": [ { "type": "GitHub Sponsors", @@ -11515,9 +13501,9 @@ } }, "node_modules/micromark-factory-whitespace/node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11535,9 +13521,9 @@ } }, "node_modules/micromark-factory-whitespace/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11587,9 +13573,9 @@ "license": "MIT" }, "node_modules/micromark-util-chunked": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", - "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", "funding": [ { "type": "GitHub Sponsors", @@ -11606,9 +13592,9 @@ } }, "node_modules/micromark-util-chunked/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11622,9 +13608,9 @@ "license": "MIT" }, "node_modules/micromark-util-classify-character": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz", - "integrity": "sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11643,9 +13629,9 @@ } }, "node_modules/micromark-util-classify-character/node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11663,9 +13649,9 @@ } }, "node_modules/micromark-util-classify-character/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11679,9 +13665,9 @@ "license": "MIT" }, "node_modules/micromark-util-combine-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz", - "integrity": "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", "funding": [ { "type": "GitHub Sponsors", @@ -11699,9 +13685,9 @@ } }, "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz", - "integrity": "sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", "funding": [ { "type": "GitHub Sponsors", @@ -11718,9 +13704,9 @@ } }, "node_modules/micromark-util-decode-numeric-character-reference/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11734,9 +13720,9 @@ "license": "MIT" }, "node_modules/micromark-util-decode-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz", - "integrity": "sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", "funding": [ { "type": "GitHub Sponsors", @@ -11756,9 +13742,9 @@ } }, "node_modules/micromark-util-decode-string/node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11776,9 +13762,9 @@ } }, "node_modules/micromark-util-decode-string/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11792,9 +13778,9 @@ "license": "MIT" }, "node_modules/micromark-util-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", - "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", "funding": [ { "type": "GitHub Sponsors", @@ -11808,9 +13794,9 @@ "license": "MIT" }, "node_modules/micromark-util-events-to-acorn": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.2.tgz", - "integrity": "sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", "funding": [ { "type": "GitHub Sponsors", @@ -11823,7 +13809,6 @@ ], "license": "MIT", "dependencies": { - "@types/acorn": "^4.0.0", "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", @@ -11834,9 +13819,9 @@ } }, "node_modules/micromark-util-events-to-acorn/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11850,9 +13835,9 @@ "license": "MIT" }, "node_modules/micromark-util-html-tag-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz", - "integrity": "sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", "funding": [ { "type": "GitHub Sponsors", @@ -11866,9 +13851,9 @@ "license": "MIT" }, "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz", - "integrity": "sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11885,9 +13870,9 @@ } }, "node_modules/micromark-util-normalize-identifier/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11901,9 +13886,9 @@ "license": "MIT" }, "node_modules/micromark-util-resolve-all": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz", - "integrity": "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", "funding": [ { "type": "GitHub Sponsors", @@ -11920,9 +13905,9 @@ } }, "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", - "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", "funding": [ { "type": "GitHub Sponsors", @@ -11941,9 +13926,9 @@ } }, "node_modules/micromark-util-sanitize-uri/node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11961,9 +13946,9 @@ } }, "node_modules/micromark-util-sanitize-uri/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11977,9 +13962,9 @@ "license": "MIT" }, "node_modules/micromark-util-subtokenize": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.1.tgz", - "integrity": "sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", "funding": [ { "type": "GitHub Sponsors", @@ -11999,9 +13984,9 @@ } }, "node_modules/micromark-util-subtokenize/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -12031,9 +14016,9 @@ "license": "MIT" }, "node_modules/micromark-util-types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", - "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", "funding": [ { "type": "GitHub Sponsors", @@ -12047,9 +14032,9 @@ "license": "MIT" }, "node_modules/micromark/node_modules/micromark-factory-space": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", - "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", "funding": [ { "type": "GitHub Sponsors", @@ -12067,9 +14052,9 @@ } }, "node_modules/micromark/node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -12087,9 +14072,9 @@ } }, "node_modules/micromark/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -12161,6 +14146,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -12169,9 +14155,9 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", - "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.2.tgz", + "integrity": "sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg==", "license": "MIT", "dependencies": { "schema-utils": "^4.0.0", @@ -12195,9 +14181,10 @@ "license": "ISC" }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -12228,18 +14215,19 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, "node_modules/mrmime": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", - "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/multicast-dns": { "version": "7.2.5", @@ -12255,15 +14243,16 @@ } }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -12316,9 +14305,9 @@ "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" }, "node_modules/node-emoji": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.1.3.tgz", - "integrity": "sha512-E2WEOVsgs7O16zsURJ/eH8BqhF029wGpEOnv7Urwdo2wmQanOACwJQh0devF9D9RhoZru0+9JXIS0dBXIAz+lA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", "license": "MIT", "dependencies": { "@sindresorhus/is": "^4.6.0", @@ -12330,19 +14319,10 @@ "node": ">=18" } }, - "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "license": "(BSD-3-Clause OR GPL-2.0)", - "engines": { - "node": ">= 6.13.0" - } - }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", "license": "MIT" }, "node_modules/nopt": { @@ -12363,17 +14343,21 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "node_modules/normalize-url": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/not": { @@ -12431,9 +14415,9 @@ } }, "node_modules/null-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -12488,9 +14472,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", - "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -12509,14 +14493,16 @@ } }, "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", "object-keys": "^1.1.1" }, "engines": { @@ -12545,9 +14531,9 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -12580,6 +14566,7 @@ "version": "8.4.2", "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", @@ -12602,11 +14589,21 @@ } }, "node_modules/p-cancelable": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", - "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "license": "MIT", "engines": { - "node": ">=14.16" + "node": ">=12.20" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" } }, "node_modules/p-limit": { @@ -12643,6 +14640,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", "dependencies": { "aggregate-error": "^3.0.0" }, @@ -12653,31 +14651,56 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", "license": "MIT", "dependencies": { - "@types/retry": "0.12.0", + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", "retry": "^0.13.1" }, "engines": { - "node": ">=8" + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, "engines": { - "node": ">=6" + "node": ">=8" } }, "node_modules/package-json": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz", "integrity": "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==", + "license": "MIT", "dependencies": { "got": "^12.1.0", "registry-auth-token": "^5.0.1", @@ -12695,6 +14718,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "license": "MIT", "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -12704,6 +14728,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -12712,13 +14737,12 @@ } }, "node_modules/parse-entities": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", - "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", - "character-entities": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", @@ -12741,6 +14765,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -12761,12 +14786,12 @@ "license": "ISC" }, "node_modules/parse5": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", - "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "license": "MIT", "dependencies": { - "entities": "^4.5.0" + "entities": "^6.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" @@ -12785,6 +14810,18 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -12798,6 +14835,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "license": "MIT", "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -12812,14 +14850,6 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-is-inside": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", @@ -12831,279 +14861,680 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "engines": { - "node": ">=8" + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "license": "MIT", + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkijs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", + "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", + "license": "BSD-3-Clause", + "dependencies": { + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-attribute-case-insensitive": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-7.0.1.tgz", + "integrity": "sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-attribute-case-insensitive/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-calc": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", + "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=7.6.0" + }, + "peerDependencies": { + "postcss": "^8.4.6" + } + }, + "node_modules/postcss-color-functional-notation": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.12.tgz", + "integrity": "sha512-TLCW9fN5kvO/u38/uesdpbx3e8AkTYhMvDZYa9JpmImWuTE99bDQ7GU7hdOADIZsiI9/zuxfAJxny/khknp1Zw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-hex-alpha": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-10.0.0.tgz", + "integrity": "sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-rebeccapurple": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-10.0.0.tgz", + "integrity": "sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-colormin": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz", + "integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "colord": "^2.9.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-to-regexp": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", - "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "node_modules/postcss-convert-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz", + "integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==", + "license": "MIT", "dependencies": { - "isarray": "0.0.1" - } - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, "engines": { - "node": ">=8" + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/periscopic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", - "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", + "node_modules/postcss-custom-media": { + "version": "11.0.6", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-11.0.6.tgz", + "integrity": "sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^3.0.0", - "is-reference": "^3.0.0" + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/postcss-custom-properties": { + "version": "14.0.6", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-14.0.6.tgz", + "integrity": "sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, "engines": { - "node": ">=8.6" + "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "peerDependencies": { + "postcss": "^8.4" } }, - "node_modules/pkg-dir": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", - "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "node_modules/postcss-custom-selectors": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-8.0.5.tgz", + "integrity": "sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", "dependencies": { - "find-up": "^6.3.0" + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "postcss-selector-parser": "^7.0.0" }, "engines": { - "node": ">=14.16" + "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "postcss": "^8.4" } }, - "node_modules/pkg-up": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", - "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", "dependencies": { - "find-up": "^3.0.0" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/pkg-up/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "node_modules/postcss-dir-pseudo-class": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-9.0.1.tgz", + "integrity": "sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { - "locate-path": "^3.0.0" + "postcss-selector-parser": "^7.0.0" }, "engines": { - "node": ">=6" + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" } }, - "node_modules/pkg-up/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": ">=6" + "node": ">=4" } }, - "node_modules/pkg-up/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" + "node_modules/postcss-discard-comments": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz", + "integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz", + "integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==", + "license": "MIT", "engines": { - "node": ">=6" + "node": "^14 || ^16 || >=18.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/pkg-up/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dependencies": { - "p-limit": "^2.0.0" + "node_modules/postcss-discard-empty": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz", + "integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz", + "integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==", + "license": "MIT", "engines": { - "node": ">=6" + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/pkg-up/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "node_modules/postcss-discard-unused": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-6.0.5.tgz", + "integrity": "sha512-wHalBlRHkaNnNwfC8z+ppX57VhvS+HWgjW508esjdaEYr3Mx7Gnn2xA4R/CKf5+Z9S5qsqC+Uzh4ueENWwCVUA==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, "engines": { - "node": ">=4" + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/postcss": { - "version": "8.4.41", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", - "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "node_modules/postcss-double-position-gradients": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.4.tgz", + "integrity": "sha512-m6IKmxo7FxSP5nF2l63QbCC3r+bWpFUWmZXZf096WxG0m7Vl1Q1+ruFOhpdDRmKrRS+S3Jtk+TVk/7z0+BVK6g==", "funding": [ { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" + "type": "github", + "url": "https://github.com/sponsors/csstools" }, { - "type": "github", - "url": "https://github.com/sponsors/ai" + "type": "opencollective", + "url": "https://opencollective.com/csstools" } ], - "license": "MIT", + "license": "MIT-0", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" } }, - "node_modules/postcss-calc": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", - "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", - "license": "MIT", + "node_modules/postcss-focus-visible": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-10.0.1.tgz", + "integrity": "sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { - "postcss-selector-parser": "^6.0.11", - "postcss-value-parser": "^4.2.0" + "postcss-selector-parser": "^7.0.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": ">=18" }, "peerDependencies": { - "postcss": "^8.2.2" + "postcss": "^8.4" } }, - "node_modules/postcss-colormin": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz", - "integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==", + "node_modules/postcss-focus-visible/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { - "browserslist": "^4.23.0", - "caniuse-api": "^3.0.0", - "colord": "^2.9.3", - "postcss-value-parser": "^4.2.0" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=4" } }, - "node_modules/postcss-convert-values": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz", - "integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==", - "license": "MIT", + "node_modules/postcss-focus-within": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-9.0.1.tgz", + "integrity": "sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { - "browserslist": "^4.23.0", - "postcss-value-parser": "^4.2.0" + "postcss-selector-parser": "^7.0.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": ">=18" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4" } }, - "node_modules/postcss-discard-comments": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz", - "integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==", + "node_modules/postcss-focus-within/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, - "peerDependencies": { - "postcss": "^8.4.31" + "engines": { + "node": ">=4" } }, - "node_modules/postcss-discard-duplicates": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz", - "integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==", + "node_modules/postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.1.0" } }, - "node_modules/postcss-discard-empty": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz", - "integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==", - "license": "MIT", + "node_modules/postcss-gap-properties": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-6.0.0.tgz", + "integrity": "sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "engines": { - "node": "^14 || ^16 || >=18.0" + "node": ">=18" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4" } }, - "node_modules/postcss-discard-overridden": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz", - "integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==", - "license": "MIT", + "node_modules/postcss-image-set-function": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-7.0.0.tgz", + "integrity": "sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": ">=18" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4" } }, - "node_modules/postcss-discard-unused": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-6.0.5.tgz", - "integrity": "sha512-wHalBlRHkaNnNwfC8z+ppX57VhvS+HWgjW508esjdaEYr3Mx7Gnn2xA4R/CKf5+Z9S5qsqC+Uzh4ueENWwCVUA==", - "license": "MIT", + "node_modules/postcss-lab-function": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.12.tgz", + "integrity": "sha512-tUcyRk1ZTPec3OuKFsqtRzW2Go5lehW29XA21lZ65XmzQkz43VY2tyWEC202F7W3mILOjw0voOiuxRGTsN+J9w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { - "postcss-selector-parser": "^6.0.16" + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": ">=18" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4" } }, "node_modules/postcss-loader": { @@ -13128,6 +15559,31 @@ "webpack": "^5.0.0" } }, + "node_modules/postcss-logical": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-8.1.0.tgz", + "integrity": "sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, "node_modules/postcss-merge-idents": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-6.0.3.tgz", @@ -13297,6 +15753,90 @@ "postcss": "^8.1.0" } }, + "node_modules/postcss-nesting": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.2.tgz", + "integrity": "sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-resolve-nested": "^3.1.0", + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-nesting/node_modules/@csstools/selector-resolve-nested": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz", + "integrity": "sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/postcss-nesting/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/postcss-nesting/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-normalize-charset": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz", @@ -13421,29 +15961,243 @@ "integrity": "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==", "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-opacity-percentage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz", + "integrity": "sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ==", + "funding": [ + { + "type": "kofi", + "url": "https://ko-fi.com/mrcgrtz" + }, + { + "type": "liberapay", + "url": "https://liberapay.com/mrcgrtz" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-ordered-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz", + "integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==", + "license": "MIT", + "dependencies": { + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-overflow-shorthand": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-6.0.0.tgz", + "integrity": "sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8" + } + }, + "node_modules/postcss-place": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-10.0.0.tgz", + "integrity": "sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-preset-env": { + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.6.1.tgz", + "integrity": "sha512-yrk74d9EvY+W7+lO9Aj1QmjWY9q5NsKjK2V9drkOPZB/X6KZ0B3igKsHUYakb7oYVhnioWypQX3xGuePf89f3g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-alpha-function": "^1.0.1", + "@csstools/postcss-cascade-layers": "^5.0.2", + "@csstools/postcss-color-function": "^4.0.12", + "@csstools/postcss-color-function-display-p3-linear": "^1.0.1", + "@csstools/postcss-color-mix-function": "^3.0.12", + "@csstools/postcss-color-mix-variadic-function-arguments": "^1.0.2", + "@csstools/postcss-content-alt-text": "^2.0.8", + "@csstools/postcss-contrast-color-function": "^2.0.12", + "@csstools/postcss-exponential-functions": "^2.0.9", + "@csstools/postcss-font-format-keywords": "^4.0.0", + "@csstools/postcss-gamut-mapping": "^2.0.11", + "@csstools/postcss-gradients-interpolation-method": "^5.0.12", + "@csstools/postcss-hwb-function": "^4.0.12", + "@csstools/postcss-ic-unit": "^4.0.4", + "@csstools/postcss-initial": "^2.0.1", + "@csstools/postcss-is-pseudo-class": "^5.0.3", + "@csstools/postcss-light-dark-function": "^2.0.11", + "@csstools/postcss-logical-float-and-clear": "^3.0.0", + "@csstools/postcss-logical-overflow": "^2.0.0", + "@csstools/postcss-logical-overscroll-behavior": "^2.0.0", + "@csstools/postcss-logical-resize": "^3.0.0", + "@csstools/postcss-logical-viewport-units": "^3.0.4", + "@csstools/postcss-media-minmax": "^2.0.9", + "@csstools/postcss-media-queries-aspect-ratio-number-values": "^3.0.5", + "@csstools/postcss-nested-calc": "^4.0.0", + "@csstools/postcss-normalize-display-values": "^4.0.1", + "@csstools/postcss-oklab-function": "^4.0.12", + "@csstools/postcss-position-area-property": "^1.0.0", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/postcss-property-rule-prelude-list": "^1.0.0", + "@csstools/postcss-random-function": "^2.0.1", + "@csstools/postcss-relative-color-syntax": "^3.0.12", + "@csstools/postcss-scope-pseudo-class": "^4.0.1", + "@csstools/postcss-sign-functions": "^1.1.4", + "@csstools/postcss-stepped-value-functions": "^4.0.9", + "@csstools/postcss-syntax-descriptor-syntax-production": "^1.0.1", + "@csstools/postcss-system-ui-font-family": "^1.0.0", + "@csstools/postcss-text-decoration-shorthand": "^4.0.3", + "@csstools/postcss-trigonometric-functions": "^4.0.9", + "@csstools/postcss-unset-value": "^4.0.0", + "autoprefixer": "^10.4.23", + "browserslist": "^4.28.1", + "css-blank-pseudo": "^7.0.1", + "css-has-pseudo": "^7.0.3", + "css-prefers-color-scheme": "^10.0.0", + "cssdb": "^8.6.0", + "postcss-attribute-case-insensitive": "^7.0.1", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^7.0.12", + "postcss-color-hex-alpha": "^10.0.0", + "postcss-color-rebeccapurple": "^10.0.0", + "postcss-custom-media": "^11.0.6", + "postcss-custom-properties": "^14.0.6", + "postcss-custom-selectors": "^8.0.5", + "postcss-dir-pseudo-class": "^9.0.1", + "postcss-double-position-gradients": "^6.0.4", + "postcss-focus-visible": "^10.0.1", + "postcss-focus-within": "^9.0.1", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^6.0.0", + "postcss-image-set-function": "^7.0.0", + "postcss-lab-function": "^7.0.12", + "postcss-logical": "^8.1.0", + "postcss-nesting": "^13.0.2", + "postcss-opacity-percentage": "^3.0.0", + "postcss-overflow-shorthand": "^6.0.0", + "postcss-page-break": "^3.0.4", + "postcss-place": "^10.0.0", + "postcss-pseudo-class-any-link": "^10.0.1", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^8.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-pseudo-class-any-link": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-10.0.1.tgz", + "integrity": "sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": ">=18" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4" } }, - "node_modules/postcss-ordered-values": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz", - "integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==", + "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { - "cssnano-utils": "^4.0.2", - "postcss-value-parser": "^4.2.0" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=4" } }, "node_modules/postcss-reduce-idents": { @@ -13492,6 +16246,53 @@ "postcss": "^8.4.31" } }, + "node_modules/postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8.0.3" + } + }, + "node_modules/postcss-selector-not": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-8.0.1.tgz", + "integrity": "sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-selector-not/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-selector-parser": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", @@ -13594,9 +16395,9 @@ } }, "node_modules/prebuild-install/node_modules/tar-fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", - "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "license": "MIT", "dependencies": { "chownr": "^1.1.1", @@ -13624,6 +16425,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "license": "MIT", "dependencies": { "lodash": "^4.17.20", "renderkid": "^3.0.0" @@ -13639,9 +16441,10 @@ } }, "node_modules/prism-react-renderer": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.3.1.tgz", - "integrity": "sha512-Rdf+HzBLR7KYjzpJ1rSoxT9ioO85nZngQEoFIhL07XhtJHlCU3SOz0GJ6+qvMyQe0Se+BV3qpe6Yd/NmQF5Juw==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", + "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", + "license": "MIT", "dependencies": { "@types/prismjs": "^1.26.0", "clsx": "^2.0.0" @@ -13688,9 +16491,9 @@ } }, "node_modules/property-information": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", - "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", "license": "MIT", "funding": { "type": "github", @@ -13700,7 +16503,8 @@ "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==" + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "license": "ISC" }, "node_modules/proxy-addr": { "version": "2.0.7", @@ -13733,10 +16537,20 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pupa": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.1.0.tgz", - "integrity": "sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.3.0.tgz", + "integrity": "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==", + "license": "MIT", "dependencies": { "escape-goat": "^4.0.0" }, @@ -13747,13 +16561,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -13762,15 +16594,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "license": "MIT", - "dependencies": { - "inherits": "~2.0.3" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -13788,12 +16611,14 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -13801,14 +16626,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/range-parser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", @@ -13819,15 +16636,15 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" @@ -13865,161 +16682,38 @@ } }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, "engines": { "node": ">=0.10.0" } }, - "node_modules/react-dev-utils": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", - "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", - "dependencies": { - "@babel/code-frame": "^7.16.0", - "address": "^1.1.2", - "browserslist": "^4.18.1", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.3", - "detect-port-alt": "^1.1.6", - "escape-string-regexp": "^4.0.0", - "filesize": "^8.0.6", - "find-up": "^5.0.0", - "fork-ts-checker-webpack-plugin": "^6.5.0", - "global-modules": "^2.0.0", - "globby": "^11.0.4", - "gzip-size": "^6.0.0", - "immer": "^9.0.7", - "is-root": "^2.1.0", - "loader-utils": "^3.2.0", - "open": "^8.4.0", - "pkg-up": "^3.1.0", - "prompts": "^2.4.2", - "react-error-overlay": "^6.0.11", - "recursive-readdir": "^2.2.2", - "shell-quote": "^1.7.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/react-dev-utils/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/loader-utils": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", - "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/react-dev-utils/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/react-dev-utils/node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^19.2.6" } }, - "node_modules/react-error-overlay": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", - "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" - }, "node_modules/react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", - "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" }, "node_modules/react-helmet-async": { + "name": "@slorber/react-helmet-async", "version": "1.3.0", - "resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-1.3.0.tgz", - "integrity": "sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg==", + "resolved": "https://registry.npmjs.org/@slorber/react-helmet-async/-/react-helmet-async-1.3.0.tgz", + "integrity": "sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A==", + "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.12.5", "invariant": "^2.2.4", @@ -14028,8 +16722,8 @@ "shallowequal": "^1.1.0" }, "peerDependencies": { - "react": "^16.6.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0" + "react": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/react-is": { @@ -14038,15 +16732,15 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-json-view-lite": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-1.5.0.tgz", - "integrity": "sha512-nWqA1E4jKPklL2jvHWs6s+7Na0qNgw9HCP6xehdQJeg6nPBTFZgGwyko9Q0oj+jQWKTTVRS30u0toM5wiuL3iw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-2.5.0.tgz", + "integrity": "sha512-tk7o7QG9oYyELWHL8xiMQ8x4WzjCzbWNyig3uexmkLb54r8jO0yH3WCWx8UZS0c49eSA4QUmG5caiRJ8fAn58g==", "license": "MIT", "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "react": "^16.13.1 || ^17.0.0 || ^18.0.0" + "react": "^18.0.0 || ^19.0.0" } }, "node_modules/react-loadable": { @@ -14063,9 +16757,10 @@ } }, "node_modules/react-loadable-ssr-addon-v5-slorber": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz", - "integrity": "sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.3.tgz", + "integrity": "sha512-GXfh9VLwB5ERaCsU6RULh7tkemeX15aNh6wuMEBtfdyMa7fFG8TXrhXlx1SoEK2Ty/l6XIkzzYIQmyaWW3JgdQ==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.3" }, @@ -14100,6 +16795,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/react-router-config/-/react-router-config-5.1.1.tgz", "integrity": "sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.1.2" }, @@ -14125,25 +16821,6 @@ "react": ">=15" } }, - "node_modules/react-waypoint": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/react-waypoint/-/react-waypoint-10.3.0.tgz", - "integrity": "sha512-iF1y2c1BsoXuEGz08NoahaLFIGI9gTUAAOKip96HUmylRT6DUtpgoBPjk/Y8dfcFVmfVDvUzWjNXpZyKTOV0SQ==", - "dependencies": { - "@babel/runtime": "^7.12.5", - "consolidated-events": "^1.1.0 || ^2.0.0", - "prop-types": "^15.0.0", - "react-is": "^17.0.1 || ^18.0.0" - }, - "peerDependencies": { - "react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/react-waypoint/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -14161,6 +16838,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -14168,34 +16846,79 @@ "node": ">=8.10.0" } }, - "node_modules/reading-time": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/reading-time/-/reading-time-1.5.0.tgz", - "integrity": "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==", - "license": "MIT" + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, - "node_modules/rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "license": "MIT", "dependencies": { - "resolve": "^1.1.6" + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" }, - "engines": { - "node": ">= 0.10" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/recursive-readdir": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", - "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", "dependencies": { - "minimatch": "^3.0.5" + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" }, - "engines": { - "node": ">=6.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -14203,9 +16926,9 @@ "license": "MIT" }, "node_modules/regenerate-unicode-properties": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", - "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", "license": "MIT", "dependencies": { "regenerate": "^1.4.2" @@ -14219,38 +16942,30 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, - "node_modules/regenerator-transform": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", - "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, "node_modules/regexpu-core": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.1.1.tgz", - "integrity": "sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", "license": "MIT", "dependencies": { "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.0", + "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", - "regjsparser": "^0.11.0", + "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" + "unicode-match-property-value-ecmascript": "^2.2.1" }, "engines": { "node": ">=4" } }, "node_modules/registry-auth-token": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.2.tgz", - "integrity": "sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.1.tgz", + "integrity": "sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==", + "license": "MIT", "dependencies": { - "@pnpm/npm-conf": "^2.1.0" + "@pnpm/npm-conf": "^3.0.2" }, "engines": { "node": ">=14" @@ -14260,6 +16975,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", + "license": "MIT", "dependencies": { "rc": "1.2.8" }, @@ -14277,12 +16993,12 @@ "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.11.2.tgz", - "integrity": "sha512-3OGZZ4HoLJkkAZx/48mTXJNlmqTGOzc0o9OWQPuWpkOlXXPbyN6OafCcoXUnBqE2D3f/T5L+pWc1kdEmnfnRsA==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", "license": "BSD-2-Clause", "dependencies": { - "jsesc": "~3.0.2" + "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" @@ -14456,8 +17172,23 @@ "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", - "hast-util-raw": "^9.0.0", - "vfile": "^6.0.0" + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" }, "funding": { "type": "opencollective", @@ -14468,14 +17199,15 @@ "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "license": "MIT", "engines": { "node": ">= 0.10" } }, "node_modules/remark-directive": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.0.tgz", - "integrity": "sha512-l1UyWJ6Eg1VPU7Hm/9tt0zKtReJQNOA4+iDMAxTyZNWnJnFlbS/7zhiel/rogTLQ2vMYwDzSJa4BiVNqGlqIMA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.1.tgz", + "integrity": "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -14521,9 +17253,9 @@ } }, "node_modules/remark-gfm": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz", - "integrity": "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -14539,9 +17271,9 @@ } }, "node_modules/remark-mdx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.0.1.tgz", - "integrity": "sha512-3Pz3yPQ5Rht2pM5R+0J2MrGoBSrzf+tJG94N+t/ilfdh8YLyyKYtidAYwTveB20BoHAcwIopOUqhcmh2F7hGYA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", "license": "MIT", "dependencies": { "mdast-util-mdx": "^3.0.0", @@ -14569,9 +17301,9 @@ } }, "node_modules/remark-rehype": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.0.tgz", - "integrity": "sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -14604,6 +17336,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "license": "MIT", "dependencies": { "css-select": "^4.1.3", "dom-converter": "^0.2.0", @@ -14616,6 +17349,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.0.1", @@ -14631,6 +17365,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", @@ -14644,6 +17379,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.2.0" }, @@ -14658,6 +17394,7 @@ "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", @@ -14671,6 +17408,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } @@ -14686,6 +17424,7 @@ "url": "https://github.com/sponsors/fb55" } ], + "license": "MIT", "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.0.0", @@ -14725,17 +17464,21 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -14743,12 +17486,14 @@ "node_modules/resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", "engines": { "node": ">=4" } @@ -14762,6 +17507,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "license": "MIT", "dependencies": { "lowercase-keys": "^3.0.0" }, @@ -14782,33 +17528,15 @@ } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rtl-detect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/rtl-detect/-/rtl-detect-1.1.2.tgz", - "integrity": "sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ==" - }, "node_modules/rtlcss": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", @@ -14827,6 +17555,18 @@ "node": ">=12.0.0" } }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -14845,6 +17585,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -14875,23 +17616,30 @@ "license": "MIT" }, "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "license": "ISC" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", + "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } }, "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/schema-dts": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-1.1.5.tgz", + "integrity": "sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg==", + "license": "Apache-2.0" }, "node_modules/schema-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", - "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", @@ -14900,7 +17648,7 @@ "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", @@ -14908,9 +17656,9 @@ } }, "node_modules/search-insights": { - "version": "2.17.2", - "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.2.tgz", - "integrity": "sha512-zFNpOpUO+tY2D85KrxJ+aqwnIfdEGi06UH2+xEb+Bp9Mwznmauqc9djbnBibJO5mpfUPPa8st6Sx65+vbeO45g==", + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", "license": "MIT", "peer": true }, @@ -14934,22 +17682,23 @@ "license": "MIT" }, "node_modules/selfsigned": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", - "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-5.5.0.tgz", + "integrity": "sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew==", "license": "MIT", "dependencies": { - "@types/node-forge": "^1.3.0", - "node-forge": "^1" + "@peculiar/x509": "^1.14.2", + "pkijs": "^3.3.3" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -14961,6 +17710,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-4.0.0.tgz", "integrity": "sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==", + "license": "MIT", "dependencies": { "semver": "^7.3.5" }, @@ -14972,24 +17722,24 @@ } }, "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "range-parser": "~1.2.1", - "statuses": "2.0.1" + "statuses": "~2.0.2" }, "engines": { "node": ">= 0.8.0" @@ -15010,21 +17760,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/send/node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -15035,24 +17770,24 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz", + "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==", "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" + "engines": { + "node": ">=20.0.0" } }, "node_modules/serve-handler": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz", - "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.7.tgz", + "integrity": "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==", "license": "MIT", "dependencies": { "bytes": "3.0.0", "content-disposition": "0.5.2", "mime-types": "2.1.18", - "minimatch": "3.1.2", + "minimatch": "3.1.5", "path-is-inside": "1.0.2", "path-to-regexp": "3.3.0", "range-parser": "1.2.0" @@ -15065,21 +17800,25 @@ "license": "MIT" }, "node_modules/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz", + "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", "license": "MIT", "dependencies": { - "accepts": "~1.3.4", + "accepts": "~1.3.8", "batch": "0.6.1", "debug": "2.6.9", "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" + "http-errors": "~1.8.0", + "mime-types": "~2.1.35", + "parseurl": "~1.3.3" }, "engines": { "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/serve-index/node_modules/debug": { @@ -15101,25 +17840,41 @@ } }, "node_modules/serve-index/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "license": "MIT", "dependencies": { "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" }, "engines": { "node": ">= 0.6" } }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "license": "ISC" + "node_modules/serve-index/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } }, "node_modules/serve-index/node_modules/ms": { "version": "2.0.0", @@ -15127,12 +17882,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "license": "ISC" - }, "node_modules/serve-index/node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -15143,15 +17892,15 @@ } }, "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.19.0" + "send": "~0.19.1" }, "engines": { "node": ">= 0.8.0" @@ -15194,17 +17943,19 @@ "node_modules/shallowequal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" }, "node_modules/sharp": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -15213,25 +17964,30 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, "node_modules/shebang-command": { @@ -15254,39 +18010,81 @@ } }, "node_modules/shell-quote": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", - "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/shelljs": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", - "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", "dependencies": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" }, - "bin": { - "shjs": "bin/shjs" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" }, "engines": { - "node": ">=4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -15376,9 +18174,9 @@ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" }, "node_modules/sitemap": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-7.1.2.tgz", - "integrity": "sha512-ARCqzHJ0p4gWt+j7NlU5eDlIO9+Rkr/JhPFZKKQ1l5GCus7rJH4UdrlVAh0xC/gDS/Qir2UMxqYNHtsKr2rpCw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-7.1.3.tgz", + "integrity": "sha512-tAjEd+wt/YwnEbfNB2ht51ybBJxbEWwe5ki/Z//Wh0rpBFTCUSj46GnxUKEWzhfuJTsee8x3lybHxFgUMig2hw==", "license": "MIT", "dependencies": { "@types/node": "^17.0.5", @@ -15416,6 +18214,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", "engines": { "node": ">=8" } @@ -15451,18 +18250,18 @@ } }, "node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", "license": "BSD-3-Clause", "engines": { - "node": ">= 8" + "node": ">= 12" } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -15544,18 +18343,18 @@ } }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/std-env": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", - "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "license": "MIT" }, "node_modules/streamx": { @@ -15583,6 +18382,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -15596,9 +18396,10 @@ } }, "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -15607,11 +18408,12 @@ } }, "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -15705,13 +18507,22 @@ "webpack": "^5.27.0" } }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, "node_modules/style-to-object": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz", - "integrity": "sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==", + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", "license": "MIT", "dependencies": { - "inline-style-parser": "0.1.1" + "inline-style-parser": "0.2.7" } }, "node_modules/stylehacks": { @@ -15745,6 +18556,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -15759,18 +18571,18 @@ "license": "MIT" }, "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz", + "integrity": "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==", "license": "MIT", "dependencies": { - "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", - "picocolors": "^1.0.0" + "picocolors": "^1.0.0", + "sax": "^1.5.0" }, "bin": { "svgo": "bin/svgo" @@ -15793,17 +18605,22 @@ } }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/tar-fs": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz", - "integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "license": "MIT", "dependencies": { "pump": "^3.0.0", @@ -15825,12 +18642,13 @@ } }, "node_modules/terser": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz", - "integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -15842,15 +18660,16 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" }, "engines": { "node": ">= 10.13.0" @@ -15874,29 +18693,6 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "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/terser-webpack-plugin/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==", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/terser-webpack-plugin/node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -15910,28 +18706,6 @@ "node": ">= 10.13.0" } }, - "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/terser-webpack-plugin/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -15959,10 +18733,21 @@ "b4a": "^1.6.4" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + "node_modules/thingies": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.0.tgz", + "integrity": "sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg==", + "license": "MIT", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } }, "node_modules/thunky": { "version": "1.1.0", @@ -15980,6 +18765,15 @@ "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -16067,6 +18861,22 @@ "node": ">=6" } }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -16088,9 +18898,28 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" }, "node_modules/tunnel-agent": { "version": "0.6.0", @@ -16107,6 +18936,7 @@ "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=12.20" }, @@ -16152,14 +18982,17 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "license": "MIT", "dependencies": { "is-typedarray": "^1.0.0" } }, "node_modules/typescript": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", - "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "devOptional": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16205,18 +19038,18 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", - "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", "license": "MIT", "engines": { "node": ">=4" @@ -16245,6 +19078,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "license": "MIT", "dependencies": { "crypto-random-string": "^4.0.0" }, @@ -16277,9 +19111,9 @@ } }, "node_modules/unist-util-is": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", - "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -16315,20 +19149,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-remove-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", - "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-visit": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/unist-util-stringify-position": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", @@ -16343,9 +19163,9 @@ } }, "node_modules/unist-util-visit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", - "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -16358,9 +19178,9 @@ } }, "node_modules/unist-util-visit-parents": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", - "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -16389,9 +19209,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "funding": [ { "type": "opencollective", @@ -16409,7 +19229,7 @@ "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -16422,6 +19242,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-6.0.2.tgz", "integrity": "sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==", + "license": "BSD-2-Clause", "dependencies": { "boxen": "^7.0.0", "chalk": "^5.0.1", @@ -16449,6 +19270,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz", "integrity": "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==", + "license": "MIT", "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^7.0.1", @@ -16470,6 +19292,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "license": "MIT", "engines": { "node": ">=14.16" }, @@ -16478,9 +19301,10 @@ } }, "node_modules/update-notifier/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -16492,22 +19316,16 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, - "node_modules/uri-js/node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "engines": { - "node": ">=6" - } - }, "node_modules/url-loader": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", + "license": "MIT", "dependencies": { "loader-utils": "^2.0.0", "mime-types": "^2.1.27", @@ -16531,9 +19349,10 @@ } }, "node_modules/url-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -16549,6 +19368,7 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", "peerDependencies": { "ajv": "^6.9.1" } @@ -16556,12 +19376,14 @@ "node_modules/url-loader/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" }, "node_modules/url-loader/node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -16570,6 +19392,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -16581,6 +19404,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -16602,7 +19426,8 @@ "node_modules/utila": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==" + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "license": "MIT" }, "node_modules/utility-types": { "version": "3.11.0", @@ -16626,6 +19451,7 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", "license": "MIT", "bin": { "uuid": "dist/bin/uuid" @@ -16646,13 +19472,12 @@ } }, "node_modules/vfile": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.2.tgz", - "integrity": "sha512-zND7NlS8rJYb/sPqkb13ZvbbUoExdbi4w3SfRrMq6R3FvnLQmmfpajJNITuuYm6AZ5uao9vy4BAos3EXBPf2rg==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" }, "funding": { @@ -16675,9 +19500,9 @@ } }, "node_modules/vfile-message": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", - "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -16689,9 +19514,10 @@ } }, "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -16720,34 +19546,36 @@ } }, "node_modules/webpack": { - "version": "5.96.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", - "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.14.0", - "browserslist": "^4.24.0", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" @@ -16801,47 +19629,57 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", - "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz", + "integrity": "sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==", "license": "MIT", "dependencies": { "colorette": "^2.0.10", - "memfs": "^3.4.3", - "mime-types": "^2.1.31", + "memfs": "^4.43.1", + "mime-types": "^3.0.1", + "on-finished": "^2.4.1", "range-parser": "^1.2.1", "schema-utils": "^4.0.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } } }, "node_modules/webpack-dev-middleware/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/webpack-dev-middleware/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/webpack-dev-middleware/node_modules/range-parser": { @@ -16854,54 +19692,52 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.15.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", - "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", - "license": "MIT", - "dependencies": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/serve-static": "^1.13.10", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.5", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.4.tgz", + "integrity": "sha512-GqDPGZN9bRqKBTkp4aWkobDDHMsrXKoGSdOH56smIri8qR0JG8gfL8/v/f/OZR3/OKXjG8uwJbFVhKm/FNU/UA==", + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.25", + "@types/express-serve-static-core": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.0.11", - "chokidar": "^3.5.3", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", "colorette": "^2.0.10", - "compression": "^1.7.4", + "compression": "^1.8.1", "connect-history-api-fallback": "^2.0.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", + "express": "^4.22.1", "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", - "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "launch-editor": "^2.6.0", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.1.1", + "http-proxy-middleware": "^2.0.9", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^5.5.0", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.4", - "ws": "^8.13.0" + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" + "webpack": "^5.0.0" }, "peerDependenciesMeta": { "webpack": { @@ -16912,10 +19748,40 @@ } } }, + "node_modules/webpack-dev-server/node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -16934,54 +19800,28 @@ } }, "node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "license": "MIT", "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", - "wildcard": "^2.0.0" + "wildcard": "^2.0.1" }, "engines": { - "node": ">=10.0.0" + "node": ">=18.0.0" } }, "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "license": "MIT", "engines": { "node": ">=10.13.0" } }, - "node_modules/webpack/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "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/webpack/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==", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, "node_modules/webpack/node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -17001,93 +19841,31 @@ "node": ">= 0.6" } }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/webpackbar": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-6.0.1.tgz", - "integrity": "sha512-TnErZpmuKdwWBdMoexjio3KKX6ZtoKHRVvLIU0A47R0VVBDtx3ZyOJDktgYixhoJokZTYTt1Z37OkO9pnGJa9Q==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-7.0.0.tgz", + "integrity": "sha512-aS9soqSO2iCHgqHoCrj4LbfGQUboDCYJPSFOAchEK+9psIjNrfSWW4Y0YEz67MKURNvMmfo0ycOg9d/+OOf9/Q==", "license": "MIT", "dependencies": { - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", + "ansis": "^3.2.0", "consola": "^3.2.3", - "figures": "^3.2.0", - "markdown-table": "^2.0.0", "pretty-time": "^1.1.0", - "std-env": "^3.7.0", - "wrap-ansi": "^7.0.0" + "std-env": "^3.7.0" }, "engines": { "node": ">=14.21.3" }, "peerDependencies": { + "@rspack/core": "*", "webpack": "3 || 4 || 5" - } - }, - "node_modules/webpackbar/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/webpackbar/node_modules/markdown-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", - "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", - "license": "MIT", - "dependencies": { - "repeat-string": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/webpackbar/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/webpackbar/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/websocket-driver": { @@ -17157,6 +19935,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "license": "MIT", "dependencies": { "string-width": "^5.0.1" }, @@ -17176,6 +19955,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -17189,9 +19969,10 @@ } }, "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -17200,9 +19981,10 @@ } }, "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -17211,11 +19993,12 @@ } }, "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -17233,6 +20016,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", "is-typedarray": "^1.0.0", @@ -17261,10 +20045,41 @@ } } }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wsl-utils/node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/xdg-basedir": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -17298,18 +20113,10 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "license": "ISC" }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "engines": { - "node": ">= 6" - } - }, "node_modules/yocto-queue": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", - "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", "license": "MIT", "engines": { "node": ">=12.20" diff --git a/site/package.json b/site/package.json index f590603572..8e9a97094f 100644 --- a/site/package.json +++ b/site/package.json @@ -2,10 +2,6 @@ "name": "retina", "version": "0.0.0", "private": true, - "overrides": { - "trim": ">0.0.3", - "got": ">11.8.5" - }, "scripts": { "docusaurus": "docusaurus", "start": "docusaurus start", @@ -18,26 +14,27 @@ "write-heading-ids": "docusaurus write-heading-ids" }, "dependencies": { - "@docusaurus/core": "^3.6.1", - "@docusaurus/plugin-ideal-image": "^3.6.1", - "@docusaurus/preset-classic": "^3.6.1", - "@mdx-js/react": "^3.0.0", - "@types/react": "^18.3.7", - "clsx": "^2.0.0", - "docusaurus-lunr-search": "^3.5.0", + "@docusaurus/core": "^3.10.1", + "@docusaurus/plugin-client-redirects": "^3.10.1", + "@docusaurus/plugin-ideal-image": "^3.10.1", + "@docusaurus/preset-classic": "^3.10.1", + "@mdx-js/react": "^3.1.1", + "@types/react": "^19.2.15", + "clsx": "^2.1.1", + "docusaurus-lunr-search": "^3.6.0", "micromatch": "^4.0.8", - "prism-react-renderer": "^2.3.1", - "react": "^18.3.1", - "react-dom": "^18.2.0", - "sharp": "^0.33.5" + "prism-react-renderer": "^2.4.1", + "react": "^19.2.5", + "react-dom": "^19.2.6", + "sharp": "^0.34.5" }, "devDependencies": { - "@docusaurus/module-type-aliases": "^3.5.2", - "@docusaurus/tsconfig": "^3.5.2", - "@docusaurus/types": "^3.5.2", - "css-loader": "^7.1.2", + "@docusaurus/module-type-aliases": "^3.9.2", + "@docusaurus/tsconfig": "^3.10.1", + "@docusaurus/types": "^3.10.1", + "css-loader": "^7.1.4", "style-loader": "^4.0.0", - "typescript": "^5.6.2" + "typescript": "^6.0.3" }, "browserslist": { "production": [ @@ -52,6 +49,9 @@ ] }, "engines": { - "node": ">=16.14" + "node": ">=18.0.0" + }, + "overrides": { + "serialize-javascript": ">=7.0.5" } } diff --git a/site/site/package-lock.json b/site/site/package-lock.json deleted file mode 100644 index ec73fec8ab..0000000000 --- a/site/site/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "site", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/site/src/pages/index.module.css b/site/src/pages/index.module.css index 108e3134d3..4ba57dc4ca 100644 --- a/site/src/pages/index.module.css +++ b/site/src/pages/index.module.css @@ -2,80 +2,3 @@ * CSS files with the .module.css suffix will be treated as CSS modules * and scoped locally. */ - -.heroBanner { - padding: 4rem 0; - text-align: center; - position: relative; - overflow: hidden; -} - -@media screen and (max-width: 996px) { - .heroBanner { - padding: 2rem; - } -} - -@media only screen and (max-width: 600px) { - .columns { - display: flex; - align-items: center; - word-break: break-word; - padding: 4rem 1.5rem; - } - - .column2 { - flex-grow: 2; - font-size: 0.75rem; - } -} - -.buttons { - display: flex; - align-items: center; - justify-content: center; -} - -.columns { - display: flex; - align-items: center; - padding: 4rem 1.5rem; - word-break: break-word -} - -.column1 { - text-align: center; - flex: none; - width: 33.3333%; - font-weight: 600; - font-size: calc(1rem + 1.8vw); -} - -.column2 { - flex-grow: 2; - font-size: calc(1rem + 0.3vw); -} - -.paragraph1 { - margin: 0 0 20px -} - -.features { - margin: 0 2vw; - width: 50%; -} - -.columns2 { - display: flex; - align-items: center; - padding: 4rem 1.5rem; - word-break: break-word; - margin: 0 calc(0rem + 5vw); -} - -.featurelist { - font-size: calc(0rem + 2vw); - flex-grow: 1; - margin-left: calc(0rem + 3vw); - list-style-position: inside; -} diff --git a/test/e2e/common/common.go b/test/e2e/common/common.go index 19a04172b0..4648ffc2b8 100644 --- a/test/e2e/common/common.go +++ b/test/e2e/common/common.go @@ -17,7 +17,6 @@ import ( ) const ( - RetinaPort int = 10093 // netObsRGtag is used to tag resources created by this test suite NetObsRGtag = "-e2e-netobs-" KubeSystemNamespace = "kube-system" @@ -48,6 +47,9 @@ var ( RetinaChartPath = func(rootDir string) string { return filepath.Join(rootDir, "deploy", "standard", "manifests", "controller", "helm", "retina") } + HubbleChartPath = func(rootDir string) string { + return filepath.Join(rootDir, "deploy", "hubble", "manifests", "controller", "helm", "retina") + } RetinaAdvancedProfilePath = func(rootDir string) string { return filepath.Join(rootDir, "test", "profiles", "advanced", "values.yaml") } diff --git a/test/e2e/common/validate-metric.go b/test/e2e/common/validate-metric.go new file mode 100644 index 0000000000..8eb31b5057 --- /dev/null +++ b/test/e2e/common/validate-metric.go @@ -0,0 +1,52 @@ +//nolint:revive // package name "common" is used across the E2E test suite +package common + +import ( + "errors" + "fmt" + "log" + + prom "github.com/microsoft/retina/test/e2e/framework/prometheus" +) + +var ErrMetricFound = errors.New("unexpected metric found") + +type ValidateMetric struct { + ForwardedPort string + MetricName string + ValidMetrics []map[string]string + ExpectMetric bool + PartialMatch bool // If true, only the specified labels need to match (metric can have additional labels) +} + +func (v *ValidateMetric) Run() error { + promAddress := fmt.Sprintf("http://localhost:%s/metrics", v.ForwardedPort) + + for _, validMetric := range v.ValidMetrics { + err := prom.CheckMetric(promAddress, v.MetricName, validMetric, v.PartialMatch) + if err != nil { + // If we expect the metric not to be found, return nil if it's not found. + if !v.ExpectMetric && errors.Is(err, prom.ErrNoMetricFound) { + log.Printf("metric %s not found, as expected\n", v.MetricName) + return nil + } + return fmt.Errorf("failed to verify prometheus metrics: %w", err) + } + + // if we expect the metric not to be found, return an error if it is found + if !v.ExpectMetric { + return fmt.Errorf("did not expect to find metric %s matching %+v: %w", v.MetricName, validMetric, ErrMetricFound) + } + + log.Printf("found metric %s matching %+v\n", v.MetricName, validMetric) + } + return nil +} + +func (v *ValidateMetric) Prevalidate() error { + return nil +} + +func (v *ValidateMetric) Stop() error { + return nil +} diff --git a/test/e2e/framework/azure/create-cluster-with-npm.go b/test/e2e/framework/azure/create-cluster-with-npm.go index 731ff2051c..f560376cba 100644 --- a/test/e2e/framework/azure/create-cluster-with-npm.go +++ b/test/e2e/framework/azure/create-cluster-with-npm.go @@ -18,11 +18,11 @@ var ( ) const ( - clusterTimeout = 15 * time.Minute - clusterCreateTicker = 30 * time.Second - pollFrequency = 5 * time.Second - AgentARMSKU = "Standard_D4pls_v5" - AuxilaryNodeCount = 1 + clusterTimeout = 15 * time.Minute + clusterCreateTicker = 30 * time.Second + pollFrequency = 5 * time.Second + AuxilaryNodeCount = 1 + AuxilaryARMNodeCount = 2 ) type CreateNPMCluster struct { @@ -35,6 +35,7 @@ type CreateNPMCluster struct { PodCidr string DNSServiceIP string ServiceCidr string + PublicIPs []string } func (c *CreateNPMCluster) Prevalidate() error { @@ -53,15 +54,14 @@ func (c *CreateNPMCluster) Run() error { //nolint:appendCombine // separate for verbosity npmCluster.Properties.AgentPoolProfiles = append(npmCluster.Properties.AgentPoolProfiles, &armcontainerservice.ManagedClusterAgentPoolProfile{ //nolint:all - Type: to.Ptr(armcontainerservice.AgentPoolTypeVirtualMachineScaleSets), - // AvailabilityZones: []*string{to.Ptr("1")}, + Type: to.Ptr(armcontainerservice.AgentPoolTypeVirtualMachineScaleSets), Count: to.Ptr[int32](AuxilaryNodeCount), EnableNodePublicIP: to.Ptr(false), Mode: to.Ptr(armcontainerservice.AgentPoolModeUser), OSType: to.Ptr(armcontainerservice.OSTypeWindows), OSSKU: to.Ptr(armcontainerservice.OSSKUWindows2022), ScaleDownMode: to.Ptr(armcontainerservice.ScaleDownModeDelete), - VMSize: to.Ptr(AgentSKU), + VMSize: to.Ptr(AgentWindowsSKU), Name: to.Ptr("ws22"), MaxPods: to.Ptr(int32(MaxPodsPerNode)), }) @@ -69,28 +69,27 @@ func (c *CreateNPMCluster) Run() error { //nolint:appendCombine // separate for verbosity npmCluster.Properties.AgentPoolProfiles = append(npmCluster.Properties.AgentPoolProfiles, &armcontainerservice.ManagedClusterAgentPoolProfile{ Type: to.Ptr(armcontainerservice.AgentPoolTypeVirtualMachineScaleSets), - AvailabilityZones: []*string{to.Ptr("1")}, Count: to.Ptr[int32](AuxilaryNodeCount), EnableNodePublicIP: to.Ptr(false), + EnableFIPS: to.Ptr(true), Mode: to.Ptr(armcontainerservice.AgentPoolModeUser), OSType: to.Ptr(armcontainerservice.OSTypeLinux), OSSKU: to.Ptr(armcontainerservice.OSSKUAzureLinux), ScaleDownMode: to.Ptr(armcontainerservice.ScaleDownModeDelete), - VMSize: to.Ptr(AgentSKU), + VMSize: to.Ptr(AgentLinuxSKU), Name: to.Ptr("azlinux"), MaxPods: to.Ptr(int32(MaxPodsPerNode)), }) //nolint:appendCombine // separate for verbosity npmCluster.Properties.AgentPoolProfiles = append(npmCluster.Properties.AgentPoolProfiles, &armcontainerservice.ManagedClusterAgentPoolProfile{ //nolint:all - Type: to.Ptr(armcontainerservice.AgentPoolTypeVirtualMachineScaleSets), - // AvailabilityZones: []*string{to.Ptr("1")}, - Count: to.Ptr[int32](AuxilaryNodeCount), + Type: to.Ptr(armcontainerservice.AgentPoolTypeVirtualMachineScaleSets), + Count: to.Ptr[int32](AuxilaryARMNodeCount), EnableNodePublicIP: to.Ptr(false), Mode: to.Ptr(armcontainerservice.AgentPoolModeUser), OSType: to.Ptr(armcontainerservice.OSTypeLinux), ScaleDownMode: to.Ptr(armcontainerservice.ScaleDownModeDelete), - VMSize: to.Ptr(AgentARMSKU), + VMSize: to.Ptr(AgentLinuxARMSKU), Name: to.Ptr("arm64"), MaxPods: to.Ptr(int32(MaxPodsPerNode)), }) @@ -99,6 +98,29 @@ func (c *CreateNPMCluster) Run() error { NodeOSUpgradeChannel: to.Ptr(armcontainerservice.NodeOSUpgradeChannelNodeImage), } + if len(c.PublicIPs) > 0 { + publicIPIDs := make([]*armcontainerservice.ResourceReference, 0, len(c.PublicIPs)) + + for _, ipID := range c.PublicIPs { + fmt.Printf("Adding Public IP ID: %s\n", ipID) + publicIPIDs = append(publicIPIDs, &armcontainerservice.ResourceReference{ + ID: to.Ptr(ipID), + }) + } + + for _, ip := range c.PublicIPs { + fmt.Printf("Public IP ID: %s\n", ip) + } + + if npmCluster.Properties.NetworkProfile.LoadBalancerProfile == nil { + npmCluster.Properties.NetworkProfile.LoadBalancerProfile = &armcontainerservice.ManagedClusterLoadBalancerProfile{ + OutboundIPs: &armcontainerservice.ManagedClusterLoadBalancerProfileOutboundIPs{ + PublicIPs: publicIPIDs, + }, + } + } + } + // Deploy cluster cred, err := azidentity.NewAzureCLICredential(nil) if err != nil { diff --git a/test/e2e/framework/azure/create-cluster.go b/test/e2e/framework/azure/create-cluster.go index c6a5b9e50a..abe085d88e 100644 --- a/test/e2e/framework/azure/create-cluster.go +++ b/test/e2e/framework/azure/create-cluster.go @@ -14,7 +14,6 @@ import ( const ( MaxNumberOfNodes = 3 MaxPodsPerNode = 250 - AgentSKU = "Standard_DS4_v2" ) var defaultClusterCreateTimeout = 30 * time.Minute @@ -110,14 +109,13 @@ func GetStarterClusterTemplate(location string) armcontainerservice.ManagedClust */ AgentPoolProfiles: []*armcontainerservice.ManagedClusterAgentPoolProfile{ { - Type: to.Ptr(armcontainerservice.AgentPoolTypeVirtualMachineScaleSets), - // AvailabilityZones: []*string{to.Ptr("1")}, + Type: to.Ptr(armcontainerservice.AgentPoolTypeVirtualMachineScaleSets), Count: to.Ptr[int32](MaxNumberOfNodes), EnableNodePublicIP: to.Ptr(false), Mode: to.Ptr(armcontainerservice.AgentPoolModeSystem), OSType: to.Ptr(armcontainerservice.OSTypeLinux), ScaleDownMode: to.Ptr(armcontainerservice.ScaleDownModeDelete), - VMSize: to.Ptr(AgentSKU), + VMSize: to.Ptr(AgentLinuxSKU), Name: to.Ptr("nodepool1"), MaxPods: to.Ptr(int32(MaxPodsPerNode)), }, diff --git a/test/e2e/framework/azure/create-public-ip.go b/test/e2e/framework/azure/create-public-ip.go new file mode 100644 index 0000000000..135e856da4 --- /dev/null +++ b/test/e2e/framework/azure/create-public-ip.go @@ -0,0 +1,109 @@ +package azure + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v5" +) + +type CreatePublicIP struct { + SubscriptionID string + ResourceGroupName string + Location string + ClusterName string + IPVersion string + IPPrefix string +} + +func (c *CreatePublicIP) Prevalidate() error { + return nil +} + +func (c *CreatePublicIP) Stop() error { + return nil +} + +func (c *CreatePublicIP) Run() error { + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + log.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), clusterTimeout) + defer cancel() + + publicIPClient, err := armnetwork.NewPublicIPAddressesClient(c.SubscriptionID, cred, nil) + if err != nil { + return fmt.Errorf("%w: failed to create public IP client", err) + } + + publicIPParams := armnetwork.PublicIPAddress{ + Location: to.Ptr(c.Location), + SKU: &armnetwork.PublicIPAddressSKU{ + Name: to.Ptr(armnetwork.PublicIPAddressSKUNameStandard), + Tier: to.Ptr(armnetwork.PublicIPAddressSKUTierRegional), + }, + Properties: &armnetwork.PublicIPAddressPropertiesFormat{ + PublicIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodStatic), + PublicIPAddressVersion: to.Ptr(armnetwork.IPVersion(c.IPVersion)), + IPTags: []*armnetwork.IPTag{ + { + IPTagType: to.Ptr("FirstPartyUsage"), + Tag: to.Ptr("/NonProd"), + }, + }, + }, + } + + var version string + switch c.IPVersion { + case string(armnetwork.IPVersionIPv4): + version = "v4" + case string(armnetwork.IPVersionIPv6): + version = "v6" + default: + return fmt.Errorf("%w: invalid IP version: %s", err, c.IPVersion) + } + + ipName := fmt.Sprintf("%s-%s-%s", c.IPPrefix, c.ClusterName, version) + + poller, err := publicIPClient.BeginCreateOrUpdate(ctx, c.ResourceGroupName, ipName, publicIPParams, nil) + if err != nil { + return fmt.Errorf("%w: failed to create public IP address", err) + } + + notifychan := make(chan struct{}) + go func() { + _, err = poller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{ + Frequency: 5 * time.Second, + }) + if err != nil { + log.Printf("failed to create Public IP - %s : %v\n", ipName, err) + } else { + log.Printf("Public IP %s created\n", ipName) + } + close(notifychan) + }() + + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return fmt.Errorf("failed to create Public IP: %w", ctx.Err()) + case <-ticker.C: + log.Printf("waiting for Public IP %s to be ready...\n", ipName) + case <-notifychan: + if err != nil { + return fmt.Errorf("received notification, failed to create public IP address: %w", err) + } + return nil + } + } +} diff --git a/test/e2e/framework/azure/skus.go b/test/e2e/framework/azure/skus.go new file mode 100644 index 0000000000..1ba63b6d25 --- /dev/null +++ b/test/e2e/framework/azure/skus.go @@ -0,0 +1,16 @@ +package azure + +import "os" + +var ( + AgentLinuxSKU = skuFromEnv("AZURE_AGENT_LINUX_SKU", "Standard_D4s_v3") + AgentLinuxARMSKU = skuFromEnv("AZURE_AGENT_LINUX_ARM_SKU", "Standard_D4pds_v5") + AgentWindowsSKU = skuFromEnv("AZURE_AGENT_WINDOWS_SKU", "Standard_D4ds_v4") +) + +func skuFromEnv(key, def string) string { + if v := os.Getenv(key); v != "" { + return v + } + return def +} diff --git a/test/e2e/framework/constants/common.go b/test/e2e/framework/constants/common.go new file mode 100644 index 0000000000..d55571bf89 --- /dev/null +++ b/test/e2e/framework/constants/common.go @@ -0,0 +1,15 @@ +package constants + +const ( + MetricsEndpoint = "metrics" + + TCP = "TCP" + UDP = "UDP" + IPV4 = "IPv4" + IPTableRuleDrop = "IPTABLE_RULE_DROP" + SYN = "SYN" + SYNACK = "SYN-ACK" + ACK = "ACK" + FIN = "FIN" + RST = "RST" +) diff --git a/test/e2e/framework/constants/hubble.go b/test/e2e/framework/constants/hubble.go new file mode 100644 index 0000000000..0efb06b151 --- /dev/null +++ b/test/e2e/framework/constants/hubble.go @@ -0,0 +1,31 @@ +package constants + +const ( + // Metrics Port + HubbleMetricsPort = "9965" + + // MetricsName + HubbleDNSQueryMetricName = "hubble_dns_queries_total" + HubbleDNSResponseMetricName = "hubble_dns_responses_total" + HubbleFlowMetricName = "hubble_flows_processed_total" + HubbleDropMetricName = "hubble_drop_total" + HubbleTCPFlagsMetricName = "hubble_tcp_flags_total" + + // Labels + HubbleDestinationLabel = "destination" + HubbleSourceLabel = "source" + HubbleIPsRetunedLabel = "ips_returned" + HubbleQTypesLabel = "qtypes" + HubbleRCodeLabel = "rcode" + HubbleQueryLabel = "query" + + HubbleProtocolLabel = "protocol" + HubbleReasonLabel = "reason" + + HubbleSubtypeLabel = "subtype" + HubbleTypeLabel = "type" + HubbleVerdictLabel = "verdict" + + HubbleFamilyLabel = "family" + HubbleFlagLabel = "flag" +) diff --git a/test/e2e/framework/constants/retina.go b/test/e2e/framework/constants/retina.go new file mode 100644 index 0000000000..8d2c901a7d --- /dev/null +++ b/test/e2e/framework/constants/retina.go @@ -0,0 +1,17 @@ +package constants + +const ( + // Metrics Port + RetinaMetricsPort = "10093" + + // MetricsName + RetinaDropMetricName = "networkobservability_drop_count" + RetinaForwardMetricName = "networkobservability_forward_count" + + // Labels + RetinaSourceLabel = "source" + RetinaDestinationLabel = "destination" + RetinaProtocolLabel = "protocol" + RetinaReasonLabel = "reason" + RetinaDirectionLabel = "direction" +) diff --git a/test/e2e/framework/kubernetes/create-agnhost-statefulset.go b/test/e2e/framework/kubernetes/create-agnhost-statefulset.go index f9689d9f1d..022d25b443 100644 --- a/test/e2e/framework/kubernetes/create-agnhost-statefulset.go +++ b/test/e2e/framework/kubernetes/create-agnhost-statefulset.go @@ -18,7 +18,6 @@ var ErrLabelMissingFromPod = fmt.Errorf("label missing from pod") const ( AgnhostHTTPPort = 80 - AgnhostReplicas = 1 AgnhostArchAmd64 = "amd64" AgnhostArchArm64 = "arm64" ) @@ -29,6 +28,7 @@ type CreateAgnhostStatefulSet struct { ScheduleOnSameNode bool KubeConfigFilePath string AgnhostArch string + AgnhostReplicas *int } func (c *CreateAgnhostStatefulSet) Run() error { @@ -50,7 +50,13 @@ func (c *CreateAgnhostStatefulSet) Run() error { c.AgnhostArch = AgnhostArchAmd64 } - agnhostStatefulSet := c.getAgnhostDeployment(c.AgnhostArch) + // set default replicas to 1 + replicas := 1 + if c.AgnhostReplicas != nil { + replicas = *c.AgnhostReplicas + } + + agnhostStatefulSet := c.getAgnhostDeployment(c.AgnhostArch, replicas) err = CreateResource(ctx, agnhostStatefulSet, clientset) if err != nil { @@ -79,8 +85,11 @@ func (c *CreateAgnhostStatefulSet) Stop() error { return nil } -func (c *CreateAgnhostStatefulSet) getAgnhostDeployment(arch string) *appsv1.StatefulSet { - reps := int32(AgnhostReplicas) +func (c *CreateAgnhostStatefulSet) getAgnhostDeployment(arch string, replicas int) *appsv1.StatefulSet { + if replicas < 1 { + replicas = 1 + } + reps := int32(replicas) //nolint:gosec // replicas controlled by test code var affinity *v1.Affinity if c.ScheduleOnSameNode { diff --git a/test/e2e/framework/kubernetes/create-network-policy.go b/test/e2e/framework/kubernetes/create-network-policy.go index 996e232215..0aa974c80e 100644 --- a/test/e2e/framework/kubernetes/create-network-policy.go +++ b/test/e2e/framework/kubernetes/create-network-policy.go @@ -36,8 +36,8 @@ func (c *CreateDenyAllNetworkPolicy) Run() error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - agnhostStatefulSet := getNetworkPolicy(c.NetworkPolicyNamespace, c.DenyAllLabelSelector) - err = CreateResource(ctx, agnhostStatefulSet, clientset) + networkPolicy := getNetworkPolicy(c.NetworkPolicyNamespace, c.DenyAllLabelSelector) + err = CreateResource(ctx, networkPolicy, clientset) if err != nil { return fmt.Errorf("error creating simple deny-all network policy: %w", err) } @@ -96,8 +96,8 @@ func (d *DeleteDenyAllNetworkPolicy) Run() error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - agnhostStatefulSet := getNetworkPolicy(d.NetworkPolicyNamespace, d.DenyAllLabelSelector) - err = DeleteResource(ctx, agnhostStatefulSet, clientset) + networkPolicy := getNetworkPolicy(d.NetworkPolicyNamespace, d.DenyAllLabelSelector) + err = DeleteResource(ctx, networkPolicy, clientset) if err != nil { return fmt.Errorf("error creating simple deny-all network policy: %w", err) } diff --git a/test/e2e/framework/kubernetes/install-hubble-helm.go b/test/e2e/framework/kubernetes/install-hubble-helm.go index 5e225d08e6..4cf7f32351 100644 --- a/test/e2e/framework/kubernetes/install-hubble-helm.go +++ b/test/e2e/framework/kubernetes/install-hubble-helm.go @@ -23,7 +23,7 @@ const ( HubbleRelayApp = "hubble-relay" ) -type ValidateHubbleStep struct { +type InstallHubbleHelmChart struct { Namespace string ReleaseName string KubeConfigFilePath string @@ -31,7 +31,7 @@ type ValidateHubbleStep struct { TagEnv string } -func (v *ValidateHubbleStep) Run() error { +func (v *InstallHubbleHelmChart) Run() error { ctx, cancel := context.WithTimeout(context.Background(), defaultTimeoutSeconds*time.Second) defer cancel() @@ -146,10 +146,10 @@ func (v *ValidateHubbleStep) Run() error { return nil } -func (v *ValidateHubbleStep) Prevalidate() error { +func (v *InstallHubbleHelmChart) Prevalidate() error { return nil } -func (v *ValidateHubbleStep) Stop() error { +func (v *InstallHubbleHelmChart) Stop() error { return nil } diff --git a/test/e2e/framework/prometheus/prometheus.go b/test/e2e/framework/prometheus/prometheus.go index f451445939..a2d1ef081f 100644 --- a/test/e2e/framework/prometheus/prometheus.go +++ b/test/e2e/framework/prometheus/prometheus.go @@ -13,6 +13,7 @@ import ( "github.com/microsoft/retina/test/retry" promclient "github.com/prometheus/client_model/go" "github.com/prometheus/common/expfmt" + "github.com/prometheus/common/model" ) var ( @@ -22,13 +23,16 @@ var ( defaultRetryAttempts = 60 ) -func CheckMetric(promAddress, metricName string, validMetric map[string]string) error { +func CheckMetric(promAddress, metricName string, validMetric map[string]string, partial ...bool) error { defaultRetrier := retry.Retrier{Attempts: defaultRetryAttempts, Delay: defaultRetryDelay} ctx := context.Background() pctx, cancel := context.WithCancel(ctx) defer cancel() + // Default partial to false if not provided + usePartial := len(partial) > 0 && partial[0] + metrics := map[string]*promclient.MetricFamily{} scrapeMetricsFn := func() error { log.Printf("checking for metrics on %s", promAddress) @@ -42,7 +46,11 @@ func CheckMetric(promAddress, metricName string, validMetric map[string]string) // loop through each metric to check for a match, // if none is found then log and return an error which will trigger a retry - err = verifyValidMetricPresent(metricName, metrics, validMetric) + if usePartial { + err = verifyValidMetricPresentPartial(metricName, metrics, validMetric) + } else { + err = verifyValidMetricPresent(metricName, metrics, validMetric) + } if err != nil { log.Printf("failed to find metric matching %s: %+v\n", metricName, validMetric) return ErrNoMetricFound @@ -144,14 +152,51 @@ func getAllPrometheusMetricsFromURL(url string) (map[string]*promclient.MetricFa return metrics, nil } +// verifyValidMetricPresentPartial checks if a metric exists with labels that contain +// all the key-value pairs in validMetric (partial matching - the metric can have additional labels) +func verifyValidMetricPresentPartial(metricName string, data map[string]*promclient.MetricFamily, validMetric map[string]string) error { + for _, metric := range data { + if metric.GetName() == metricName { + for _, metric := range metric.GetMetric() { + + // get all labels and values on the metric + metricLabels := map[string]string{} + for _, label := range metric.GetLabel() { + metricLabels[label.GetName()] = label.GetValue() + } + + // if valid metric is empty, then we just need to make sure the metric and value is present + if len(validMetric) == 0 && len(metricLabels) > 0 { + return nil + } + + // Check if all key-value pairs in validMetric exist in metricLabels + allMatch := true + for key, value := range validMetric { + if metricLabels[key] != value { + allMatch = false + break + } + } + + if allMatch { + return nil + } + } + } + } + + return fmt.Errorf("failed to find metric matching: %+v: %w", validMetric, ErrNoMetricFound) +} + func getAllPrometheusMetricsFromBuffer(buf []byte) (map[string]*promclient.MetricFamily, error) { - var parser expfmt.TextParser + parser := expfmt.NewTextParser(model.LegacyValidation) reader := strings.NewReader(string(buf)) return parser.TextToMetricFamilies(reader) //nolint } func ParseReaderPrometheusMetrics(input io.Reader) (map[string]*promclient.MetricFamily, error) { - var parser expfmt.TextParser + parser := expfmt.NewTextParser(model.LegacyValidation) return parser.TextToMetricFamilies(input) //nolint } diff --git a/test/e2e/jobs/jobs.go b/test/e2e/jobs/jobs.go index 37afc13a0d..274a6a973d 100644 --- a/test/e2e/jobs/jobs.go +++ b/test/e2e/jobs/jobs.go @@ -1,22 +1,36 @@ package retina import ( + "fmt" + + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v5" "github.com/microsoft/retina/test/e2e/common" "github.com/microsoft/retina/test/e2e/framework/azure" "github.com/microsoft/retina/test/e2e/framework/generic" "github.com/microsoft/retina/test/e2e/framework/kubernetes" "github.com/microsoft/retina/test/e2e/framework/types" - "github.com/microsoft/retina/test/e2e/hubble" - + "github.com/microsoft/retina/test/e2e/scenarios/capture" "github.com/microsoft/retina/test/e2e/scenarios/dns" "github.com/microsoft/retina/test/e2e/scenarios/drop" + hubble_dns "github.com/microsoft/retina/test/e2e/scenarios/hubble/dns" + hubble_drop "github.com/microsoft/retina/test/e2e/scenarios/hubble/drop" + hubble_flow "github.com/microsoft/retina/test/e2e/scenarios/hubble/flow" + hubble_service "github.com/microsoft/retina/test/e2e/scenarios/hubble/service" + hubble_tcp "github.com/microsoft/retina/test/e2e/scenarios/hubble/tcp" "github.com/microsoft/retina/test/e2e/scenarios/latency" tcp "github.com/microsoft/retina/test/e2e/scenarios/tcp" "github.com/microsoft/retina/test/e2e/scenarios/windows" ) +const IPPrefix = "serviceTaggedIp" + func CreateTestInfra(subID, rg, clusterName, location, kubeConfigFilePath string, createInfra bool) *types.Job { job := types.NewJob("Create e2e test infrastructure") + + publicIPID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/publicIPAddresses", subID, clusterName) + publicIPv4FullName := fmt.Sprintf("%s/%s-%s-v4", publicIPID, IPPrefix, clusterName) + publicIPv6FullName := fmt.Sprintf("%s/%s-%s-v6", publicIPID, IPPrefix, clusterName) + if createInfra { job.AddStep(&azure.CreateResourceGroup{ SubscriptionID: subID, @@ -34,11 +48,31 @@ func CreateTestInfra(subID, rg, clusterName, location, kubeConfigFilePath string SubnetAddressSpace: "10.0.0.0/12", }, nil) + job.AddStep(&azure.CreatePublicIP{ + ClusterName: clusterName, + IPVersion: string(armnetwork.IPVersionIPv4), + IPPrefix: IPPrefix, + }, &types.StepOptions{ + SkipSavingParametersToJob: true, + }) + + job.AddStep(&azure.CreatePublicIP{ + ClusterName: clusterName, + IPVersion: string(armnetwork.IPVersionIPv6), + IPPrefix: IPPrefix, + }, &types.StepOptions{ + SkipSavingParametersToJob: true, + }) + job.AddStep(&azure.CreateNPMCluster{ ClusterName: clusterName, PodCidr: "10.128.0.0/9", DNSServiceIP: "192.168.0.10", ServiceCidr: "192.168.0.0/28", + PublicIPs: []string{ + publicIPv4FullName, + publicIPv6FullName, + }, }, nil) job.AddStep(&azure.GetAKSKubeConfig{ @@ -262,10 +296,10 @@ func UpgradeAndTestRetinaAdvancedMetrics(kubeConfigFilePath, chartPath, valuesFi return job } -func ValidateHubble(kubeConfigFilePath, chartPath string, testPodNamespace string) *types.Job { +func InstallAndTestHubbleMetrics(kubeConfigFilePath, chartPath string) *types.Job { job := types.NewJob("Validate Hubble") - job.AddStep(&kubernetes.ValidateHubbleStep{ + job.AddStep(&kubernetes.InstallHubbleHelmChart{ Namespace: common.KubeSystemNamespace, ReleaseName: "retina", KubeConfigFilePath: kubeConfigFilePath, @@ -273,9 +307,25 @@ func ValidateHubble(kubeConfigFilePath, chartPath string, testPodNamespace strin TagEnv: generic.DefaultTagEnv, }, nil) - job.AddScenario(hubble.ValidateHubbleRelayService()) + hubbleScenarios := []*types.Scenario{ + hubble_service.ValidateHubbleRelayService(), + hubble_service.ValidateHubbleUIService(kubeConfigFilePath), + } - job.AddScenario(hubble.ValidateHubbleUIService(kubeConfigFilePath)) + for _, arch := range common.Architectures { + hubbleScenarios = append(hubbleScenarios, + hubble_dns.ValidateDNSMetric(arch), + hubble_flow.ValidatePodToPodIntraNodeHubbleFlowMetric(arch), + hubble_flow.ValidatePodToPodInterNodeHubbleFlowMetric(arch), + hubble_flow.ValidatePodToWorldHubbleFlowMetric(arch), + hubble_drop.ValidateDropMetric(arch), + hubble_tcp.ValidateTCPMetric(arch), + ) + } + + for _, scenario := range hubbleScenarios { + job.AddScenario(scenario) + } job.AddStep(&kubernetes.EnsureStableComponent{ PodNamespace: common.KubeSystemNamespace, @@ -286,6 +336,16 @@ func ValidateHubble(kubeConfigFilePath, chartPath string, testPodNamespace strin return job } +func ValidateCapture(kubeConfigFilePath, testPodNamespace string) *types.Job { + job := types.NewJob("Validate Capture") + + job.AddScenario(capture.ValidateCapture( + kubeConfigFilePath, + testPodNamespace)) + + return job +} + func LoadGenericFlags() *types.Job { job := types.NewJob("Loading Generic Flags to env") diff --git a/test/e2e/jobs/scale.go b/test/e2e/jobs/scale.go index f4c8ac7d9a..b2eede6b75 100644 --- a/test/e2e/jobs/scale.go +++ b/test/e2e/jobs/scale.go @@ -62,7 +62,7 @@ func GetScaleTestInfra(subID, rg, clusterName, location, kubeConfigFilePath stri Nodes: nodes, }). SetPodCidr("100.64.0.0/10"). - SetVMSize("Standard_D4_v3"). + SetVMSize(azure.AgentLinuxSKU). SetNetworkPluginMode("overlay"), nil) job.AddStep(&azure.GetAKSKubeConfig{ diff --git a/test/e2e/retina_e2e_test.go b/test/e2e/retina_e2e_test.go index 226f254ea1..1c21653add 100644 --- a/test/e2e/retina_e2e_test.go +++ b/test/e2e/retina_e2e_test.go @@ -29,8 +29,6 @@ func TestE2ERetina(t *testing.T) { // Get to root of the repo by going up two directories rootDir := filepath.Dir(filepath.Dir(cwd)) - hubblechartPath := filepath.Join(rootDir, "deploy", "hubble", "manifests", "controller", "helm", "retina") - err = jobs.LoadGenericFlags().Run() require.NoError(t, err, "failed to load generic flags") @@ -74,12 +72,15 @@ func TestE2ERetina(t *testing.T) { unloadAndPinWinBPFJob := types.NewRunner(t, jobs.UnLoadAndPinWinBPFJob(common.KubeConfigFilePath(rootDir))) unloadAndPinWinBPFJob.Run(ctx) - // Install and test Hubble basic metrics - validatehubble := types.NewRunner(t, - jobs.ValidateHubble( + // Install and test Hubble metrics + hubbleMetricsE2E := types.NewRunner(t, jobs.InstallAndTestHubbleMetrics(common.KubeConfigFilePath(rootDir), common.HubbleChartPath(rootDir))) + hubbleMetricsE2E.Run(ctx) + + // Install Retina basic and test captures + captureE2E := types.NewRunner(t, + jobs.ValidateCapture( common.KubeConfigFilePath(rootDir), - hubblechartPath, - common.TestPodNamespace), + "default"), ) - validatehubble.Run(ctx) + captureE2E.Run(ctx) } diff --git a/test/e2e/scenarios/capture/install-retina-plugin.go b/test/e2e/scenarios/capture/install-retina-plugin.go new file mode 100644 index 0000000000..8ac6dc54c0 --- /dev/null +++ b/test/e2e/scenarios/capture/install-retina-plugin.go @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package capture + +import ( + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/pkg/errors" +) + +const ( + // InstallRetinaBinaryDir is the directory where the kubectl-retina binary will be installed + InstallRetinaBinaryDir = "/tmp/retina-bin" +) + +// InstallRetinaPlugin builds and installs the kubectl-retina plugin +// to allow e2e tests to run kubectl retina commands. +type InstallRetinaPlugin struct{} + +// Run builds the kubectl-retina binary and adds it to PATH +func (i *InstallRetinaPlugin) Run() error { + log.Print("Building kubectl-retina plugin...") + + // Create binary directory if it doesn't exist + if err := os.MkdirAll(InstallRetinaBinaryDir, 0o755); err != nil { + return errors.Wrap(err, "failed to create binary directory") + } + + binaryName := "kubectl-retina" + + // Run git rev-parse to find the repository root + cmd := exec.Command("git", "rev-parse", "--show-toplevel") // #nosec + output, err := cmd.Output() + if err != nil { + return errors.Wrap(err, "failed to detect git repository root. Make sure you're running inside a git repository") + } + retinaRepoRoot := strings.TrimSpace(string(output)) + log.Printf("Auto-detected repository root: %s", retinaRepoRoot) + + _, err = os.Stat(retinaRepoRoot) + if err != nil { + return errors.Wrap(err, "invalid RetinaRepoRoot path") + } + + // Check if the cli/main.go file exists + _, err = os.Stat(filepath.Join(retinaRepoRoot, "cli", "main.go")) + if err != nil { + return errors.Wrap(err, "cli/main.go not found in repository root") + } + + // Build the kubectl-retina binary + buildCmd := exec.Command("go", "build", "-o", + filepath.Join(InstallRetinaBinaryDir, binaryName), + filepath.Join(retinaRepoRoot, "cli", "main.go")) // #nosec + + buildCmd.Dir = retinaRepoRoot + var buildOutput []byte + buildOutput, err = buildCmd.CombinedOutput() + if err != nil { + return errors.Wrapf(err, "failed to build kubectl-retina: %s", buildOutput) + } + log.Printf("Successfully built kubectl-retina: %s", buildOutput) + + // Add the binary directory to PATH + currentPath := os.Getenv("PATH") + + // Check if the directory is already in PATH + if !strings.Contains(currentPath, InstallRetinaBinaryDir) { + newPath := fmt.Sprintf("%s:%s", InstallRetinaBinaryDir, currentPath) + err = os.Setenv("PATH", newPath) + if err != nil { + return errors.Wrap(err, "failed to update PATH environment variable") + } + log.Printf("Added %s to PATH", InstallRetinaBinaryDir) + } + + // Verify the plugin is accessible via kubectl + verifyCmd := exec.Command("kubectl", "plugin", "list") // #nosec + verifyOutput, err := verifyCmd.CombinedOutput() + if err != nil { + log.Printf("Warning: kubectl plugin list command failed: %v. Output: %s", err, verifyOutput) + } else { + log.Printf("kubectl plugin list output: %s", verifyOutput) + if !strings.Contains(string(verifyOutput), "retina") { + log.Printf("Warning: retina plugin not found in kubectl plugin list output") + } + } + + return nil +} + +// Prevalidate validates the inputs before running +func (i *InstallRetinaPlugin) Prevalidate() error { + // Check if the repository root exists + + return nil +} + +// Stop is a no-op for this step +func (i *InstallRetinaPlugin) Stop() error { + return nil +} diff --git a/test/e2e/scenarios/capture/scenarios.go b/test/e2e/scenarios/capture/scenarios.go new file mode 100644 index 0000000000..fcda5ebfa1 --- /dev/null +++ b/test/e2e/scenarios/capture/scenarios.go @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package capture + +import ( + "github.com/microsoft/retina/test/e2e/framework/types" + "k8s.io/apimachinery/pkg/util/rand" +) + +func ValidateCapture(kubeConfigPath, namespace string) *types.Scenario { + scenarioName := "Retina Capture" + captureName := "retina-capture-e2e-" + rand.String(5) + steps := []*types.StepWrapper{ + { + Step: &InstallRetinaPlugin{}, + }, + { + Step: &validateCapture{ + CaptureName: captureName, + CaptureNamespace: namespace, + Duration: "5s", + KubeConfigPath: kubeConfigPath, + }, Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + } + return types.NewScenario(scenarioName, steps...) +} diff --git a/test/e2e/scenarios/capture/validate-capture.go b/test/e2e/scenarios/capture/validate-capture.go new file mode 100644 index 0000000000..62aec20c1e --- /dev/null +++ b/test/e2e/scenarios/capture/validate-capture.go @@ -0,0 +1,275 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package capture + +import ( + "context" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/pkg/errors" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" + + captureConstants "github.com/microsoft/retina/pkg/capture/constants" + "github.com/microsoft/retina/pkg/label" + "github.com/microsoft/retina/test/e2e/framework/generic" + "github.com/microsoft/retina/test/retry" +) + +type validateCapture struct { + CaptureName string + CaptureNamespace string + Duration string + KubeConfigPath string +} + +var ( + ErrInvalidCaptureName = errors.New("invalid capture name") + ErrNoCaptureJobsFound = errors.New("no capture jobs found") + ErrFoundNonZeroCaptureJobs = errors.New("found non-zero amount of capture jobs when expecting zero after deletion") + ErrMissingEventOnCaptureJob = errors.New("missing SuccessfulCreate or Completed event on capture job") + ErrCaptureJobFailed = errors.New("capture job failed") +) + +func (v *validateCapture) Run() error { + log.Print("Running retina capture create...") + ctx := context.TODO() + + imageRegistry := os.Getenv(generic.DefaultImageRegistry) + imageNamespace := os.Getenv(generic.DefaultImageNamespace) + imageTag := os.Getenv(generic.DefaultTagEnv) + + os.Setenv("KUBECONFIG", v.KubeConfigPath) + log.Printf("KUBECONFIG: %s\n", os.Getenv("KUBECONFIG")) + + cmd := exec.CommandContext(ctx, "kubectl", "retina", "capture", "create", "--namespace", v.CaptureNamespace, "--name", v.CaptureName, "--duration", v.Duration, "--debug") //#nosec + cmd.Env = append(os.Environ(), "RETINA_AGENT_IMAGE="+filepath.Join(imageRegistry, imageNamespace, "retina-agent:"+imageTag)) + + output, err := cmd.CombinedOutput() + if err != nil { + return errors.Wrapf(err, "failed to execute create capture command: %s", string(output)) + } + log.Printf("Create capture command output: %s\n", output) + + config, err := clientcmd.BuildConfigFromFlags("", v.KubeConfigPath) + if err != nil { + return errors.Wrap(err, "failed to build kubeconfig") + } + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return errors.Wrap(err, "failed to create kubernetes clientset") + } + + retrier := retry.Retrier{Attempts: 5, Delay: 10 * time.Second, ExpBackoff: true} + err = retrier.Do(ctx, func() error { + e := v.verifyJobs(ctx, clientset) + if e != nil { + log.Printf("failed to verify capture jobs: %v, retrying...", e) + return e + } + return nil + }) + if err != nil { + return errors.Wrap(err, "failed to verify capture jobs were created") + } + + err = v.downloadCapture(ctx) + if err != nil { + return errors.Wrap(err, "failed to download and validate capture files") + } + + err = v.deleteJobs(ctx, clientset) + if err != nil { + return errors.Wrap(err, "failed to delete capture jobs") + } + + return nil +} + +// Verify that capture jobs are created (with appropriate labels), and completed successfully +func (v *validateCapture) verifyJobs(ctx context.Context, clientset *kubernetes.Clientset) error { + captureJobSelector := &metav1.LabelSelector{ + MatchLabels: map[string]string{ + label.CaptureNameLabel: v.CaptureName, + label.AppLabel: captureConstants.CaptureAppname, + }, + } + labelSelector, err := labels.Parse(metav1.FormatLabelSelector(captureJobSelector)) + if err != nil { + return errors.Wrap(err, "failed to parse label selector") + } + + jobList, err := clientset.BatchV1().Jobs(v.CaptureNamespace).List(ctx, metav1.ListOptions{ + LabelSelector: labelSelector.String(), + }) + if err != nil { + return errors.Wrap(err, "failed to list capture jobs") + } + + if len(jobList.Items) == 0 { + return errors.Wrap(ErrNoCaptureJobsFound, fmt.Sprintf("with labels %s=%s and %s=%s", + label.CaptureNameLabel, v.CaptureName, + label.AppLabel, captureConstants.CaptureAppname)) + } + + log.Printf("Found %d capture job(s) with appropriate labels.", len(jobList.Items)) + + // Check if all jobs are completed successfully + for i := range jobList.Items { + for _, condition := range jobList.Items[i].Status.Conditions { + if condition.Type == "Complete" && condition.Status == "True" { + log.Printf("Job %s has condition: Complete - True", jobList.Items[i].Name) + } + if condition.Type == "Failed" && condition.Status == "True" { + return errors.Wrap(ErrCaptureJobFailed, jobList.Items[i].Name) + } + } + } + + // Check events for each job to verify SuccessfulCreate and Completed + events, err := clientset.CoreV1().Events(v.CaptureNamespace).List(ctx, metav1.ListOptions{}) + if err != nil { + return fmt.Errorf("failed to list events: %w", err) + } + for i := range jobList.Items { + if err = v.checkJobEvents(jobList.Items[i].Name, events); err != nil { + return fmt.Errorf("failed to verify events for job %s: %w", jobList.Items[i].Name, err) + } + log.Printf("Job %s has both SuccessfulCreate and Completed events.", jobList.Items[i].Name) + } + + return nil +} + +func (v *validateCapture) deleteJobs(ctx context.Context, clientset *kubernetes.Clientset) error { + log.Printf("Running retina capture delete...") + cmd := exec.CommandContext(ctx, "kubectl", "retina", "capture", "delete", "--namespace", v.CaptureNamespace, "--name", v.CaptureName) //#nosec + output, err := cmd.CombinedOutput() + if err != nil { + return errors.Wrapf(err, "failed to execute delete command") + } + log.Printf("Delete command output: %s\n", output) + + captureJobSelector := &metav1.LabelSelector{ + MatchLabels: map[string]string{ + label.CaptureNameLabel: v.CaptureName, + label.AppLabel: captureConstants.CaptureAppname, + }, + } + labelSelector, err := labels.Parse(metav1.FormatLabelSelector(captureJobSelector)) + if err != nil { + return errors.Wrap(err, "failed to parse label selector") + } + // Verify that jobs are deleted + if err := v.verifyDelete(ctx, clientset, labelSelector); err != nil { + return errors.Wrap(err, "failed to verify capture jobs were deleted") + } + + return nil +} + +func (v *validateCapture) checkJobEvents(jobName string, events *v1.EventList) error { + var created, completed bool + for i := range events.Items { + if events.Items[i].InvolvedObject.Kind == "Job" && events.Items[i].InvolvedObject.Name == jobName { + switch events.Items[i].Reason { + case "SuccessfulCreate": + created = true + case "Completed": + completed = true + } + } + } + + if !created || !completed { + return errors.Wrap(ErrMissingEventOnCaptureJob, jobName) + } + + return nil +} + +func (v *validateCapture) verifyDelete(ctx context.Context, clientset *kubernetes.Clientset, labelSelector labels.Selector) error { + // Wait a moment for deletion to propagate + time.Sleep(5 * time.Second) + + jobList, err := clientset.BatchV1().Jobs(v.CaptureNamespace).List(ctx, metav1.ListOptions{ + LabelSelector: labelSelector.String(), + }) + if err != nil { + return errors.Wrap(err, "failed to list jobs during delete verification") + } + + if len(jobList.Items) > 0 { + return ErrFoundNonZeroCaptureJobs + } + + log.Printf("All relevant capture jobs have been successfully deleted.") + return nil +} + +func (v *validateCapture) Prevalidate() error { + return nil +} + +func (v *validateCapture) Stop() error { + return nil +} + +func (v *validateCapture) downloadCapture(ctx context.Context) error { + log.Print("Downloading capture files...") + + outputDir := filepath.Join(".", v.CaptureName) + + // Run the download command + cmd := exec.CommandContext(ctx, "kubectl", "retina", "capture", "download", "--namespace", v.CaptureNamespace, "--name", v.CaptureName) // #nosec + output, err := cmd.CombinedOutput() + if err != nil { + return errors.Wrapf(err, "failed to execute download capture command: %s", string(output)) + } + log.Printf("Download capture command output: %s\n", output) + + // List files in the output directory + files, err := os.ReadDir(outputDir) + if err != nil { + return errors.Wrapf(err, "failed to list files in output directory %s", outputDir) + } + + // Validate the number of files + if len(files) == 0 { + return errors.New("no capture files were downloaded") + } + log.Printf("Downloaded %d capture files", len(files)) + + // Validate file names and content + for _, file := range files { + filePath := filepath.Join(outputDir, file.Name()) + + // Check that the file has the expected tar.gz extension + if !strings.HasSuffix(file.Name(), ".tar.gz") { + return errors.Errorf("downloaded file %s does not have the expected .tar.gz extension", file.Name()) + } + + // Check that the file is not empty + fileInfo, err := os.Stat(filePath) + if err != nil { + return errors.Wrapf(err, "failed to get file info for %s", filePath) + } + + if fileInfo.Size() == 0 { + return errors.Errorf("downloaded file %s is empty", filePath) + } + + log.Printf("Validated file: %s (Size: %d bytes)", file.Name(), fileInfo.Size()) + } + + return nil +} diff --git a/test/e2e/scenarios/dns/scenarios.go b/test/e2e/scenarios/dns/scenarios.go index 4c5c4b9804..4fe2f83f85 100644 --- a/test/e2e/scenarios/dns/scenarios.go +++ b/test/e2e/scenarios/dns/scenarios.go @@ -5,10 +5,10 @@ package dns import ( "fmt" "math/rand" - "strconv" "time" "github.com/microsoft/retina/test/e2e/common" + "github.com/microsoft/retina/test/e2e/framework/constants" "github.com/microsoft/retina/test/e2e/framework/kubernetes" "github.com/microsoft/retina/test/e2e/framework/types" ) @@ -95,8 +95,8 @@ func ValidateBasicDNSMetrics(scenarioName string, req *RequestValidationParams, Step: &kubernetes.PortForward{ Namespace: common.KubeSystemNamespace, LabelSelector: "k8s-app=retina", - LocalPort: strconv.Itoa(common.RetinaPort), - RemotePort: strconv.Itoa(common.RetinaPort), + LocalPort: constants.RetinaMetricsPort, + RemotePort: constants.RetinaMetricsPort, Endpoint: "metrics", OptionalLabelAffinity: "app=" + agnhostName, // port forward to a pod on a node that also has this pod with this label, assuming same namespace }, @@ -209,8 +209,8 @@ func ValidateAdvancedDNSMetrics(scenarioName string, req *RequestValidationParam Step: &kubernetes.PortForward{ Namespace: common.KubeSystemNamespace, LabelSelector: "k8s-app=retina", - LocalPort: strconv.Itoa(common.RetinaPort), - RemotePort: strconv.Itoa(common.RetinaPort), + LocalPort: constants.RetinaMetricsPort, + RemotePort: constants.RetinaMetricsPort, Endpoint: "metrics", OptionalLabelAffinity: "app=" + agnhostName, // port forward to a pod on a node that also has this pod with this label, assuming same namespace }, diff --git a/test/e2e/scenarios/dns/validate-advanced-dns-metric.go b/test/e2e/scenarios/dns/validate-advanced-dns-metric.go index 1c30e343bc..00badbe8bb 100644 --- a/test/e2e/scenarios/dns/validate-advanced-dns-metric.go +++ b/test/e2e/scenarios/dns/validate-advanced-dns-metric.go @@ -6,7 +6,7 @@ import ( "fmt" "log" - "github.com/microsoft/retina/test/e2e/common" + "github.com/microsoft/retina/test/e2e/framework/constants" "github.com/microsoft/retina/test/e2e/framework/kubernetes" prom "github.com/microsoft/retina/test/e2e/framework/prometheus" "github.com/pkg/errors" @@ -29,7 +29,7 @@ type ValidateAdvancedDNSRequestMetrics struct { } func (v *ValidateAdvancedDNSRequestMetrics) Run() error { - metricsEndpoint := fmt.Sprintf("http://localhost:%d/metrics", common.RetinaPort) + metricsEndpoint := fmt.Sprintf("http://localhost:%s/metrics", constants.RetinaMetricsPort) // Get Pod IP address podIP, err := kubernetes.GetPodIP(v.KubeConfigFilePath, v.PodNamespace, v.PodName) if err != nil { @@ -78,7 +78,7 @@ type ValidateAdvanceDNSResponseMetrics struct { } func (v *ValidateAdvanceDNSResponseMetrics) Run() error { - metricsEndpoint := fmt.Sprintf("http://localhost:%d/metrics", common.RetinaPort) + metricsEndpoint := fmt.Sprintf("http://localhost:%s/metrics", constants.RetinaMetricsPort) // Get Pod IP address podIP, err := kubernetes.GetPodIP(v.KubeConfigFilePath, v.PodNamespace, v.PodName) if err != nil { diff --git a/test/e2e/scenarios/dns/validate-basic-dns-metric.go b/test/e2e/scenarios/dns/validate-basic-dns-metric.go index efbcd5af2f..91662e1bb5 100644 --- a/test/e2e/scenarios/dns/validate-basic-dns-metric.go +++ b/test/e2e/scenarios/dns/validate-basic-dns-metric.go @@ -6,7 +6,7 @@ import ( "fmt" "log" - "github.com/microsoft/retina/test/e2e/common" + "github.com/microsoft/retina/test/e2e/framework/constants" prom "github.com/microsoft/retina/test/e2e/framework/prometheus" "github.com/pkg/errors" ) @@ -22,7 +22,7 @@ type validateBasicDNSRequestMetrics struct { } func (v *validateBasicDNSRequestMetrics) Run() error { - metricsEndpoint := fmt.Sprintf("http://localhost:%d/metrics", common.RetinaPort) + metricsEndpoint := fmt.Sprintf("http://localhost:%s/metrics", constants.RetinaMetricsPort) validBasicDNSRequestMetricLabels := map[string]string{} @@ -52,7 +52,7 @@ type validateBasicDNSResponseMetrics struct { } func (v *validateBasicDNSResponseMetrics) Run() error { - metricsEndpoint := fmt.Sprintf("http://localhost:%d/metrics", common.RetinaPort) + metricsEndpoint := fmt.Sprintf("http://localhost:%s/metrics", constants.RetinaMetricsPort) if v.Response == EmptyResponse { v.Response = "" diff --git a/test/e2e/scenarios/hubble/dns/labels.go b/test/e2e/scenarios/hubble/dns/labels.go new file mode 100644 index 0000000000..018dd18c4d --- /dev/null +++ b/test/e2e/scenarios/hubble/dns/labels.go @@ -0,0 +1,26 @@ +package dns + +import ( + "github.com/microsoft/retina/test/e2e/common" + "github.com/microsoft/retina/test/e2e/framework/constants" +) + +var ( + podName = "agnhost-dns-0" + validDNSQueryMetricLabels = map[string]string{ + constants.HubbleDestinationLabel: "", + constants.HubbleSourceLabel: common.TestPodNamespace + "/" + podName, + constants.HubbleIPsRetunedLabel: "0", + constants.HubbleQTypesLabel: "A", + constants.HubbleRCodeLabel: "", + constants.HubbleQueryLabel: "one.one.one.one.", + } + validDNSResponseMetricLabels = map[string]string{ + constants.HubbleDestinationLabel: common.TestPodNamespace + "/" + podName, + constants.HubbleSourceLabel: "", + constants.HubbleIPsRetunedLabel: "2", + constants.HubbleQTypesLabel: "A", + constants.HubbleRCodeLabel: "No Error", + constants.HubbleQueryLabel: "one.one.one.one.", + } +) diff --git a/test/e2e/scenarios/hubble/dns/scenario.go b/test/e2e/scenarios/hubble/dns/scenario.go new file mode 100644 index 0000000000..3eac66907d --- /dev/null +++ b/test/e2e/scenarios/hubble/dns/scenario.go @@ -0,0 +1,93 @@ +package dns + +import ( + "time" + + "github.com/microsoft/retina/test/e2e/common" + "github.com/microsoft/retina/test/e2e/framework/constants" + "github.com/microsoft/retina/test/e2e/framework/kubernetes" + "github.com/microsoft/retina/test/e2e/framework/types" +) + +const ( + sleepDelay = 5 * time.Second +) + +func ValidateDNSMetric(arch string) *types.Scenario { + name := "DNS Metrics - Arch: " + arch + agnhostName := "agnhost-dns" + podName := agnhostName + "-0" + steps := []*types.StepWrapper{ + { + Step: &kubernetes.CreateAgnhostStatefulSet{ + AgnhostName: agnhostName, + AgnhostNamespace: common.TestPodNamespace, + AgnhostArch: arch, + }, + }, + { + Step: &kubernetes.PortForward{ + LabelSelector: "k8s-app=retina", + LocalPort: constants.HubbleMetricsPort, + RemotePort: constants.HubbleMetricsPort, + Namespace: common.KubeSystemNamespace, + Endpoint: "metrics", + OptionalLabelAffinity: "app=" + agnhostName, // port forward hubble metrics to a pod on a node that also has this pod with this label, assuming same namespace + }, + Opts: &types.StepOptions{ + RunInBackgroundWithID: "hubble-dns-port-forward" + arch, + }, + }, + { + Step: &kubernetes.ExecInPod{ + PodName: podName, + PodNamespace: common.TestPodNamespace, + Command: "nslookup -type=a one.one.one.one", + }, + Opts: &types.StepOptions{ + ExpectError: false, + SkipSavingParametersToJob: true, + }, + }, + { + Step: &types.Sleep{ + Duration: sleepDelay, + }, + }, + { + Step: &common.ValidateMetric{ + ForwardedPort: constants.HubbleMetricsPort, + MetricName: constants.HubbleDNSQueryMetricName, + ValidMetrics: []map[string]string{validDNSQueryMetricLabels}, + ExpectMetric: true, + }, + Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + { + Step: &common.ValidateMetric{ + ForwardedPort: constants.HubbleMetricsPort, + MetricName: constants.HubbleDNSResponseMetricName, + ValidMetrics: []map[string]string{validDNSResponseMetricLabels}, + ExpectMetric: true, + }, + Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + { + Step: &types.Stop{ + BackgroundID: "hubble-dns-port-forward" + arch, + }, + }, + { + Step: &kubernetes.DeleteKubernetesResource{ + ResourceType: kubernetes.TypeString(kubernetes.StatefulSet), + ResourceName: agnhostName, + ResourceNamespace: common.TestPodNamespace, + }, + }, + } + return types.NewScenario(name, steps...) +} diff --git a/test/e2e/scenarios/hubble/drop/labels.go b/test/e2e/scenarios/hubble/drop/labels.go new file mode 100644 index 0000000000..2ad1ee7afd --- /dev/null +++ b/test/e2e/scenarios/hubble/drop/labels.go @@ -0,0 +1,27 @@ +package drop + +import ( + "github.com/microsoft/retina/test/e2e/framework/constants" +) + +var ( + podName = "agnhost-drop-0" + agnhostName = "agnhost-drop" + + validRetinaDropMetricLabels = map[string]string{ + constants.RetinaReasonLabel: constants.IPTableRuleDrop, + constants.RetinaDirectionLabel: "unknown", + } + + // Note: When the agnhost pod (with deny-all network policy) tries to curl bing.com, + // it triggers a DNS lookup to CoreDNS. The network policy blocks this egress traffic, + // but Cilium/Hubble records the drop at the destination (CoreDNS) ingress side rather + // than the source (agnhost) egress side. + // 'source:kube-system/agnhost-drop-0' is not recorded in Hubble drop metrics. + // We partially validate this metric. + validHubbleDropMetricLabels = map[string]string{ + constants.HubbleSourceLabel: "", + constants.HubbleProtocolLabel: constants.UDP, + constants.HubbleReasonLabel: "POLICY_DENIED", + } +) diff --git a/test/e2e/scenarios/hubble/drop/scenario.go b/test/e2e/scenarios/hubble/drop/scenario.go new file mode 100644 index 0000000000..8066794af5 --- /dev/null +++ b/test/e2e/scenarios/hubble/drop/scenario.go @@ -0,0 +1,138 @@ +package drop + +import ( + "time" + + "github.com/microsoft/retina/test/e2e/common" + "github.com/microsoft/retina/test/e2e/framework/constants" + "github.com/microsoft/retina/test/e2e/framework/kubernetes" + "github.com/microsoft/retina/test/e2e/framework/types" +) + +const ( + sleepDelay = 5 * time.Second +) + +func ValidateDropMetric(arch string) *types.Scenario { + name := "Drop Metrics - Arch: " + arch + steps := []*types.StepWrapper{ + { + Step: &kubernetes.CreateDenyAllNetworkPolicy{ + NetworkPolicyNamespace: common.TestPodNamespace, + DenyAllLabelSelector: "app=" + agnhostName, + }, + }, + { + Step: &kubernetes.CreateAgnhostStatefulSet{ + AgnhostName: agnhostName, + AgnhostNamespace: common.TestPodNamespace, + AgnhostArch: arch, + }, + }, + // Need this delay to guarantee that the pods will have bpf program attached + { + Step: &types.Sleep{ + Duration: 30 * time.Second, + }, + Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + { + Step: &kubernetes.PortForward{ + LabelSelector: "k8s-app=retina", + LocalPort: constants.RetinaMetricsPort, + RemotePort: constants.RetinaMetricsPort, + Endpoint: constants.MetricsEndpoint, + OptionalLabelAffinity: "app=" + agnhostName, // port forward to a pod on a node that also has this pod with this label, assuming same namespace + }, + Opts: &types.StepOptions{ + RunInBackgroundWithID: "retina-drop-port-forward" + arch, + SkipSavingParametersToJob: true, + }, + }, + { + Step: &kubernetes.PortForward{ + LabelSelector: "k8s-app=retina", + LocalPort: constants.HubbleMetricsPort, + RemotePort: constants.HubbleMetricsPort, + Endpoint: constants.MetricsEndpoint, + OptionalLabelAffinity: "app=" + agnhostName, // port forward hubble metrics to a pod on a node that also has this pod with this label, assuming same namespace + }, + Opts: &types.StepOptions{ + RunInBackgroundWithID: "hubble-drop-port-forward" + arch, + SkipSavingParametersToJob: true, + }, + }, + { + Step: &types.Sleep{ + Duration: sleepDelay, + }, + }, + { + Step: &kubernetes.ExecInPod{ + PodName: podName, + PodNamespace: common.TestPodNamespace, + Command: "curl -s -m 5 bing.com", + }, + Opts: &types.StepOptions{ + ExpectError: true, + SkipSavingParametersToJob: true, + }, + }, + { + Step: &common.ValidateMetric{ + ForwardedPort: constants.RetinaMetricsPort, + MetricName: constants.RetinaDropMetricName, + ValidMetrics: []map[string]string{validRetinaDropMetricLabels}, + ExpectMetric: true, + }, + Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + { + Step: &common.ValidateMetric{ + ForwardedPort: constants.HubbleMetricsPort, + MetricName: constants.HubbleDropMetricName, + ValidMetrics: []map[string]string{validHubbleDropMetricLabels}, + ExpectMetric: true, + PartialMatch: true, + }, + Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + { + Step: &types.Stop{ + BackgroundID: "hubble-drop-port-forward" + arch, + }, + }, + { + Step: &types.Stop{ + BackgroundID: "retina-drop-port-forward" + arch, + }, + }, + { + Step: &kubernetes.DeleteKubernetesResource{ + ResourceType: kubernetes.TypeString(kubernetes.NetworkPolicy), + ResourceName: "deny-all", + ResourceNamespace: common.TestPodNamespace, + }, + Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + { + Step: &kubernetes.DeleteKubernetesResource{ + ResourceType: kubernetes.TypeString(kubernetes.StatefulSet), + ResourceName: agnhostName, + ResourceNamespace: common.TestPodNamespace, + }, + Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + } + return types.NewScenario(name, steps...) +} diff --git a/test/e2e/scenarios/hubble/flow/pod2podInter.go b/test/e2e/scenarios/hubble/flow/pod2podInter.go new file mode 100644 index 0000000000..4662a14398 --- /dev/null +++ b/test/e2e/scenarios/hubble/flow/pod2podInter.go @@ -0,0 +1,184 @@ +package flow + +import ( + "time" + + "github.com/microsoft/retina/test/e2e/common" + "github.com/microsoft/retina/test/e2e/framework/constants" + "github.com/microsoft/retina/test/e2e/framework/kubernetes" + "github.com/microsoft/retina/test/e2e/framework/types" +) + +func ValidatePodToPodInterNodeHubbleFlowMetric(arch string) *types.Scenario { + var ( + podnameSrc = "agnhost-flow-inter-src" + podnameDst = "agnhost-flow-inter-dst" + validHubbleFlowMetricSrcToStackLabel = map[string]string{ + constants.HubbleSourceLabel: common.TestPodNamespace + "/" + podnameSrc + "-0", + constants.HubbleDestinationLabel: "", + constants.HubbleProtocolLabel: constants.TCP, + constants.HubbleSubtypeLabel: "to-stack", + constants.HubbleTypeLabel: "Trace", + constants.HubbleVerdictLabel: "FORWARDED", + } + validHubbleFlowMetricSrcToEndpointLabel = map[string]string{ + constants.HubbleSourceLabel: common.TestPodNamespace + "/" + podnameDst + "-0", + constants.HubbleDestinationLabel: "", + constants.HubbleProtocolLabel: constants.TCP, + constants.HubbleSubtypeLabel: "to-endpoint", + constants.HubbleTypeLabel: "Trace", + constants.HubbleVerdictLabel: "FORWARDED", + } + validHubbleFlowMetricDstToStackLabel = map[string]string{ + constants.HubbleSourceLabel: "", + constants.HubbleDestinationLabel: common.TestPodNamespace + "/" + podnameSrc + "-0", + constants.HubbleProtocolLabel: constants.TCP, + constants.HubbleSubtypeLabel: "to-stack", + constants.HubbleTypeLabel: "Trace", + constants.HubbleVerdictLabel: "FORWARDED", + } + validHubbleFlowMetricDstToEndpointLabel = map[string]string{ + constants.HubbleSourceLabel: "", + constants.HubbleDestinationLabel: common.TestPodNamespace + "/" + podnameDst + "-0", + constants.HubbleProtocolLabel: constants.TCP, + constants.HubbleSubtypeLabel: "to-endpoint", + constants.HubbleTypeLabel: "Trace", + constants.HubbleVerdictLabel: "FORWARDED", + } + validHubbleFlowMetricsSrcLabels = []map[string]string{ + validHubbleFlowMetricSrcToStackLabel, + validHubbleFlowMetricSrcToEndpointLabel, + } + validHubbleFlowMetricsDstLabels = []map[string]string{ + validHubbleFlowMetricDstToStackLabel, + validHubbleFlowMetricDstToEndpointLabel, + } + ) + name := "Validate pod to pod inter node Hubble flow metrics - Arch: " + arch + steps := []*types.StepWrapper{ + { + Step: &kubernetes.CreateAgnhostStatefulSet{ + AgnhostName: podnameSrc, + AgnhostNamespace: common.TestPodNamespace, + AgnhostArch: arch, + }, + Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + { + Step: &kubernetes.CreateAgnhostStatefulSet{ + AgnhostName: podnameDst, + AgnhostNamespace: common.TestPodNamespace, + AgnhostArch: arch, + }, + Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + // Need this delay to guarantee that the pods will have bpf program attached + { + Step: &types.Sleep{ + Duration: 30 * time.Second, + }, + Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + { + Step: &kubernetes.PortForward{ + LabelSelector: "k8s-app=retina", + LocalPort: constants.HubbleMetricsPort, + RemotePort: constants.HubbleMetricsPort, + Endpoint: constants.MetricsEndpoint, + OptionalLabelAffinity: "app=" + podnameSrc, + }, + Opts: &types.StepOptions{ + RunInBackgroundWithID: "hubble-src-flow-port-forward" + arch, + SkipSavingParametersToJob: true, + }, + }, + { + Step: &kubernetes.PortForward{ + LabelSelector: "k8s-app=retina", + LocalPort: "9966", + RemotePort: constants.HubbleMetricsPort, + Endpoint: constants.MetricsEndpoint, + OptionalLabelAffinity: "app=" + podnameDst, + }, + Opts: &types.StepOptions{ + RunInBackgroundWithID: "hubble-dst-flow-port-forward" + arch, + SkipSavingParametersToJob: true, + }, + }, + { + Step: &CurlPod{ + SrcPodName: podnameSrc + "-0", + SrcPodNamespace: common.TestPodNamespace, + DstPodName: podnameDst + "-0", + DstPodNamespace: common.TestPodNamespace, + }, + Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + { + Step: &types.Sleep{ + Duration: sleepDelay, + }, + }, + { + Step: &common.ValidateMetric{ + ForwardedPort: constants.HubbleMetricsPort, + MetricName: constants.HubbleFlowMetricName, + ValidMetrics: validHubbleFlowMetricsSrcLabels, + ExpectMetric: true, + }, + Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + { + Step: &common.ValidateMetric{ + ForwardedPort: "9966", + MetricName: constants.HubbleFlowMetricName, + ValidMetrics: validHubbleFlowMetricsDstLabels, + ExpectMetric: true, + }, + Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + { + Step: &types.Stop{ + BackgroundID: "hubble-src-flow-port-forward" + arch, + }, + Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + { + Step: &types.Stop{ + BackgroundID: "hubble-dst-flow-port-forward" + arch, + }, + }, + { + Step: &kubernetes.DeleteKubernetesResource{ + ResourceType: kubernetes.TypeString(kubernetes.StatefulSet), + ResourceName: podnameSrc, + ResourceNamespace: common.TestPodNamespace, + }, + Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + { + Step: &kubernetes.DeleteKubernetesResource{ + ResourceType: kubernetes.TypeString(kubernetes.StatefulSet), + ResourceName: podnameDst, + ResourceNamespace: common.TestPodNamespace, + }, + }, + } + return types.NewScenario(name, steps...) +} diff --git a/test/e2e/scenarios/hubble/flow/pod2podIntra.go b/test/e2e/scenarios/hubble/flow/pod2podIntra.go new file mode 100644 index 0000000000..10d78e667b --- /dev/null +++ b/test/e2e/scenarios/hubble/flow/pod2podIntra.go @@ -0,0 +1,129 @@ +package flow + +import ( + "time" + + "github.com/microsoft/retina/test/e2e/common" + "github.com/microsoft/retina/test/e2e/framework/constants" + "github.com/microsoft/retina/test/e2e/framework/kubernetes" + "github.com/microsoft/retina/test/e2e/framework/types" +) + +func intPtr(i int) *int { + return &i +} + +func ValidatePodToPodIntraNodeHubbleFlowMetric(arch string) *types.Scenario { + var ( + podname = "agnhost-flow-intra" + validPod0HubbleFlowLabelsToStack = map[string]string{ + constants.HubbleSourceLabel: common.TestPodNamespace + "/" + podname + "-0", + constants.HubbleDestinationLabel: "", + constants.HubbleProtocolLabel: constants.TCP, + constants.HubbleSubtypeLabel: "to-stack", + constants.HubbleTypeLabel: "Trace", + constants.HubbleVerdictLabel: "FORWARDED", + } + validPod0HubbleFlowLablesToEndpoint = map[string]string{ + constants.HubbleSourceLabel: common.TestPodNamespace + "/" + podname + "-0", + constants.HubbleDestinationLabel: "", + constants.HubbleProtocolLabel: constants.TCP, + constants.HubbleSubtypeLabel: "to-endpoint", + constants.HubbleTypeLabel: "Trace", + constants.HubbleVerdictLabel: "FORWARDED", + } + validPod1HubbleFlowLabelsToStack = map[string]string{ + constants.HubbleSourceLabel: common.TestPodNamespace + "/" + podname + "-1", + constants.HubbleDestinationLabel: "", + constants.HubbleProtocolLabel: constants.TCP, + constants.HubbleSubtypeLabel: "to-stack", + constants.HubbleTypeLabel: "Trace", + constants.HubbleVerdictLabel: "FORWARDED", + } + validPod1HubbleFlowLabelsToEndpoint = map[string]string{ + constants.HubbleSourceLabel: common.TestPodNamespace + "/" + podname + "-1", + constants.HubbleDestinationLabel: "", + constants.HubbleProtocolLabel: constants.TCP, + constants.HubbleSubtypeLabel: "to-endpoint", + constants.HubbleTypeLabel: "Trace", + constants.HubbleVerdictLabel: "FORWARDED", + } + validHubbleFlowMetricsLabels = []map[string]string{ + validPod0HubbleFlowLabelsToStack, + validPod0HubbleFlowLablesToEndpoint, + validPod1HubbleFlowLabelsToStack, + validPod1HubbleFlowLabelsToEndpoint, + } + ) + name := "Validate pod to pod intra node Hubble flow metrics - Arch: " + arch + steps := []*types.StepWrapper{ + { + Step: &kubernetes.CreateAgnhostStatefulSet{ + AgnhostName: podname, + AgnhostNamespace: common.TestPodNamespace, + ScheduleOnSameNode: true, + AgnhostReplicas: intPtr(2), + AgnhostArch: arch, + }, + }, + // Need this delay to guarantee that the pods will have bpf program attached + { + Step: &types.Sleep{ + Duration: 30 * time.Second, + }, + Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + { + Step: &kubernetes.PortForward{ + LabelSelector: "k8s-app=retina", + LocalPort: constants.HubbleMetricsPort, + RemotePort: constants.HubbleMetricsPort, + Endpoint: constants.MetricsEndpoint, + OptionalLabelAffinity: "app=" + podname, // port forward to a pod on a node that also has this pod with this label, assuming same namespace + }, + Opts: &types.StepOptions{ + RunInBackgroundWithID: "hubble-flow-intra-port-forward" + arch, + SkipSavingParametersToJob: true, + }, + }, + { + Step: &CurlPod{ + SrcPodName: podname + "-0", + SrcPodNamespace: common.TestPodNamespace, + DstPodName: podname + "-1", + DstPodNamespace: common.TestPodNamespace, + }, + Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + { + Step: &types.Sleep{ + Duration: sleepDelay, + }, + }, + { + Step: &common.ValidateMetric{ + ForwardedPort: constants.HubbleMetricsPort, + MetricName: constants.HubbleFlowMetricName, + ValidMetrics: validHubbleFlowMetricsLabels, + ExpectMetric: true, + }, + }, + { + Step: &types.Stop{ + BackgroundID: "hubble-flow-intra-port-forward" + arch, + }, + }, + { + Step: &kubernetes.DeleteKubernetesResource{ + ResourceType: kubernetes.TypeString(kubernetes.StatefulSet), + ResourceName: podname, + ResourceNamespace: common.TestPodNamespace, + }, + }, + } + return types.NewScenario(name, steps...) +} diff --git a/test/e2e/scenarios/hubble/flow/pod2world.go b/test/e2e/scenarios/hubble/flow/pod2world.go new file mode 100644 index 0000000000..26e341b050 --- /dev/null +++ b/test/e2e/scenarios/hubble/flow/pod2world.go @@ -0,0 +1,104 @@ +package flow + +import ( + "time" + + "github.com/microsoft/retina/test/e2e/common" + "github.com/microsoft/retina/test/e2e/framework/constants" + "github.com/microsoft/retina/test/e2e/framework/kubernetes" + "github.com/microsoft/retina/test/e2e/framework/types" +) + +func ValidatePodToWorldHubbleFlowMetric(arch string) *types.Scenario { + var ( + podName = "agnhost-flow-world" + validHubbleFlowToWorldTCPToStackLabel = map[string]string{ + constants.HubbleSourceLabel: common.TestPodNamespace + "/" + podName + "-0", + constants.HubbleDestinationLabel: "", + constants.HubbleProtocolLabel: constants.TCP, + constants.HubbleSubtypeLabel: "to-stack", + constants.HubbleTypeLabel: "Trace", + constants.HubbleVerdictLabel: "FORWARDED", + } + validHubbleFlowToWorldUDPToStackLabel = map[string]string{ + constants.HubbleSourceLabel: common.TestPodNamespace + "/" + podName + "-0", + constants.HubbleDestinationLabel: "", + constants.HubbleProtocolLabel: constants.UDP, + constants.HubbleSubtypeLabel: "to-stack", + constants.HubbleTypeLabel: "Trace", + constants.HubbleVerdictLabel: "FORWARDED", + } + validHubbleFlowMetricsLabels = []map[string]string{ + validHubbleFlowToWorldTCPToStackLabel, + validHubbleFlowToWorldUDPToStackLabel, + } + ) + name := "Validate pod to world Hubble flow metrics - Arch: " + arch + steps := []*types.StepWrapper{ + { + Step: &kubernetes.CreateAgnhostStatefulSet{ + AgnhostName: podName, + AgnhostNamespace: common.TestPodNamespace, + AgnhostArch: arch, + }, + }, + // Need this delay to guarantee that the pods will have bpf program attached + { + Step: &types.Sleep{ + Duration: 30 * time.Second, + }, + Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + { + Step: &kubernetes.PortForward{ + LabelSelector: "k8s-app=retina", + LocalPort: constants.HubbleMetricsPort, + RemotePort: constants.HubbleMetricsPort, + Endpoint: constants.MetricsEndpoint, + OptionalLabelAffinity: "app=" + podName, + }, + Opts: &types.StepOptions{ + RunInBackgroundWithID: "hubble-flow-to-world-port-forward" + arch, + SkipSavingParametersToJob: true, + }, + }, + { + Step: &kubernetes.ExecInPod{ + PodName: podName + "-0", + PodNamespace: common.TestPodNamespace, + Command: "curl -s -m 5 bing.com", + }, + Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + { + Step: &types.Sleep{ + Duration: sleepDelay, + }, + }, + { + Step: &common.ValidateMetric{ + ForwardedPort: constants.HubbleMetricsPort, + MetricName: constants.HubbleFlowMetricName, + ValidMetrics: validHubbleFlowMetricsLabels, + ExpectMetric: true, + }, + }, + { + Step: &types.Stop{ + BackgroundID: "hubble-flow-to-world-port-forward" + arch, + }, + }, + { + Step: &kubernetes.DeleteKubernetesResource{ + ResourceType: kubernetes.TypeString(kubernetes.StatefulSet), + ResourceName: podName, + ResourceNamespace: common.TestPodNamespace, + }, + }, + } + return types.NewScenario(name, steps...) +} diff --git a/test/e2e/scenarios/hubble/flow/types.go b/test/e2e/scenarios/hubble/flow/types.go new file mode 100644 index 0000000000..9ca8f543c4 --- /dev/null +++ b/test/e2e/scenarios/hubble/flow/types.go @@ -0,0 +1,57 @@ +package flow + +import ( + "context" + "fmt" + "time" + + ossK8s "github.com/microsoft/retina/test/e2e/framework/kubernetes" + "github.com/pkg/errors" + kubernetes "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" +) + +const ( + sleepDelay = 5 * time.Second +) + +type CurlPod struct { + SrcPodName string + SrcPodNamespace string + DstPodName string + DstPodNamespace string + KubeConfigFilePath string +} + +func (c *CurlPod) Run() error { + config, err := clientcmd.BuildConfigFromFlags("", c.KubeConfigFilePath) + if err != nil { + return fmt.Errorf("error building kubeconfig: %w", err) + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return fmt.Errorf("error creating Kubernetes client: %w", err) + } + // Get dst pod IP + dstPodIP, err := ossK8s.GetPodIP(c.KubeConfigFilePath, c.DstPodNamespace, c.DstPodName) + if err != nil { + return errors.Wrap(err, "error getting pod IP") + } + cmd := fmt.Sprintf("curl -s -m 5 %s:80", dstPodIP) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + _, err = ossK8s.ExecPod(ctx, clientset, config, c.SrcPodNamespace, c.SrcPodName, cmd) + if err != nil { + return errors.Wrap(err, "error executing command") + } + return nil +} + +func (c *CurlPod) Prevalidate() error { + return nil +} + +func (c *CurlPod) Stop() error { + return nil +} diff --git a/test/e2e/hubble/scenario.go b/test/e2e/scenarios/hubble/service/scenario.go similarity index 100% rename from test/e2e/hubble/scenario.go rename to test/e2e/scenarios/hubble/service/scenario.go diff --git a/test/e2e/scenarios/hubble/tcp/labels.go b/test/e2e/scenarios/hubble/tcp/labels.go new file mode 100644 index 0000000000..ebf4174663 --- /dev/null +++ b/test/e2e/scenarios/hubble/tcp/labels.go @@ -0,0 +1,27 @@ +package tcp + +import ( + "github.com/microsoft/retina/test/e2e/common" + "github.com/microsoft/retina/test/e2e/framework/constants" +) + +var ( + podName = "agnhost-tcp-0" + validHubbleTCPSYNFlag = map[string]string{ + constants.HubbleSourceLabel: common.TestPodNamespace + "/" + podName, + constants.HubbleDestinationLabel: "", + constants.HubbleFamilyLabel: constants.IPV4, + constants.HubbleFlagLabel: constants.SYN, + } + validHubbleTCPFINFlag = map[string]string{ + constants.HubbleSourceLabel: common.TestPodNamespace + "/" + podName, + constants.HubbleDestinationLabel: "", + constants.HubbleFamilyLabel: constants.IPV4, + constants.HubbleFlagLabel: constants.FIN, + } + + validHubbleTCPMetricsLabels = []map[string]string{ + validHubbleTCPSYNFlag, + validHubbleTCPFINFlag, + } +) diff --git a/test/e2e/scenarios/hubble/tcp/scenario.go b/test/e2e/scenarios/hubble/tcp/scenario.go new file mode 100644 index 0000000000..e268e1a64d --- /dev/null +++ b/test/e2e/scenarios/hubble/tcp/scenario.go @@ -0,0 +1,95 @@ +package tcp + +import ( + "time" + + "github.com/microsoft/retina/test/e2e/common" + "github.com/microsoft/retina/test/e2e/framework/constants" + "github.com/microsoft/retina/test/e2e/framework/kubernetes" + "github.com/microsoft/retina/test/e2e/framework/types" +) + +const ( + sleepDelay = 5 * time.Second +) + +func ValidateTCPMetric(arch string) *types.Scenario { + name := "TCP Flags Metrics - Arch: " + arch + agnhostName := "agnhost-tcp" + podName := agnhostName + "-0" + steps := []*types.StepWrapper{ + { + Step: &kubernetes.CreateAgnhostStatefulSet{ + AgnhostName: agnhostName, + AgnhostNamespace: common.TestPodNamespace, + AgnhostArch: arch, + }, + }, + // Need this delay to guarantee that the pods will have bpf program attached + { + Step: &types.Sleep{ + Duration: 30 * time.Second, + }, + Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + { + Step: &kubernetes.PortForward{ + LabelSelector: "k8s-app=retina", + LocalPort: constants.HubbleMetricsPort, + RemotePort: constants.HubbleMetricsPort, + Namespace: common.KubeSystemNamespace, + Endpoint: constants.MetricsEndpoint, + OptionalLabelAffinity: "app=" + agnhostName, // port forward to a pod on a node that also has this pod with this label, assuming same namespace + }, + Opts: &types.StepOptions{ + RunInBackgroundWithID: "hubble-tcp-port-forward" + arch, + SkipSavingParametersToJob: true, + }, + }, + { + Step: &kubernetes.ExecInPod{ + PodName: podName, + PodNamespace: common.TestPodNamespace, + Command: "curl -s -m 5 bing.com", + }, + Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + { + Step: &types.Sleep{ + Duration: sleepDelay, + }, + }, + { + Step: &common.ValidateMetric{ + ForwardedPort: constants.HubbleMetricsPort, + MetricName: constants.HubbleTCPFlagsMetricName, + ValidMetrics: validHubbleTCPMetricsLabels, + ExpectMetric: true, + }, + Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + { + Step: &types.Stop{ + BackgroundID: "hubble-tcp-port-forward" + arch, + }, + }, + { + Step: &kubernetes.DeleteKubernetesResource{ + ResourceType: kubernetes.TypeString(kubernetes.StatefulSet), + ResourceName: agnhostName, + ResourceNamespace: common.TestPodNamespace, + }, + Opts: &types.StepOptions{ + SkipSavingParametersToJob: true, + }, + }, + } + + return types.NewScenario(name, steps...) +} diff --git a/test/e2e/scenarios/latency/validate-latency-metric.go b/test/e2e/scenarios/latency/validate-latency-metric.go index f45a7f237c..270ae95969 100644 --- a/test/e2e/scenarios/latency/validate-latency-metric.go +++ b/test/e2e/scenarios/latency/validate-latency-metric.go @@ -4,7 +4,7 @@ import ( "fmt" "log" - "github.com/microsoft/retina/test/e2e/common" + "github.com/microsoft/retina/test/e2e/framework/constants" prom "github.com/microsoft/retina/test/e2e/framework/prometheus" "github.com/pkg/errors" ) @@ -18,7 +18,7 @@ func (v *ValidateAPIServerLatencyMetric) Prevalidate() error { } func (v *ValidateAPIServerLatencyMetric) Run() error { - promAddress := fmt.Sprintf("http://localhost:%d/metrics", common.RetinaPort) + promAddress := fmt.Sprintf("http://localhost:%s/metrics", constants.RetinaMetricsPort) metric := map[string]string{} err := prom.CheckMetric(promAddress, latencyBucketMetricName, metric) diff --git a/test/e2e/scenarios/windows/validate-hns-metrics.go b/test/e2e/scenarios/windows/validate-hns-metrics.go index d4bcd0de1a..0cb58cc9fc 100644 --- a/test/e2e/scenarios/windows/validate-hns-metrics.go +++ b/test/e2e/scenarios/windows/validate-hns-metrics.go @@ -7,7 +7,7 @@ import ( "log" "time" - "github.com/microsoft/retina/test/e2e/common" + "github.com/microsoft/retina/test/e2e/framework/constants" k8s "github.com/microsoft/retina/test/e2e/framework/kubernetes" prom "github.com/microsoft/retina/test/e2e/framework/prometheus" "github.com/microsoft/retina/test/retry" @@ -74,7 +74,7 @@ func (v *ValidateHNSMetric) Run() error { // wrap this in a retrier because windows is slow var output []byte err = defaultRetrier.Do(context.TODO(), func() error { - output, err = k8s.ExecPod(context.TODO(), clientset, config, windowsRetinaPod.Namespace, windowsRetinaPod.Name, fmt.Sprintf("curl -s http://localhost:%d/metrics", common.RetinaPort)) + output, err = k8s.ExecPod(context.TODO(), clientset, config, windowsRetinaPod.Namespace, windowsRetinaPod.Name, fmt.Sprintf("curl -s http://localhost:%s/metrics", constants.RetinaMetricsPort)) if err != nil { return fmt.Errorf("error executing command in windows retina pod: %w", err) } diff --git a/test/e2e/test_bpftrace_drops.sh b/test/e2e/test_bpftrace_drops.sh new file mode 100755 index 0000000000..4156c51f86 --- /dev/null +++ b/test/e2e/test_bpftrace_drops.sh @@ -0,0 +1,226 @@ +#!/bin/bash +# Test script for bpftrace - tests drops, RST, socket errors, and retransmits +# Usage: ./test_bpftrace_drops.sh [kubeconfig_path] +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +echo "=== Bpftrace Network Issue Detection Test ===" +echo "" + +# Get first node +NODE=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}') +echo "Target node: $NODE" + +# Clean up any leftover test pods +echo "Cleaning up previous test pods..." +kubectl delete pod drop-gen rst-gen nfqueue-helper --force --grace-period=0 2>/dev/null || true +kubectl delete pod -l app=retina-trace --force --grace-period=0 2>/dev/null || true +sleep 2 + +# Build CLI if needed +if [[ ! -f "$REPO_ROOT/kubectl-retina" ]]; then + echo "Building kubectl-retina..." + cd "$REPO_ROOT" + go build -ldflags "-X github.com/microsoft/retina/internal/buildinfo.Version=v1.0.3" -o kubectl-retina ./cli/main.go +fi + +echo "" +echo "=== Starting bpftrace (70s duration) ===" +echo "This will capture drops, RST, socket errors, retransmits, and NFQUEUE drops on $NODE" +echo "" + +# Create output file +OUTPUT_FILE=$(mktemp) +trap "rm -f $OUTPUT_FILE" EXIT + +# Run trace in background and capture output +# Duration must be long enough for all test phases: RST, DROP/RETRANS, and NFQUEUE +# (each phase takes ~15-30s including pod startup and traffic generation) +"$REPO_ROOT/kubectl-retina" bpftrace "$NODE" --duration 70s --startup-timeout 120s --retina-shell-image-version v1.0.3 > "$OUTPUT_FILE" 2>&1 & +TRACE_PID=$! + +# Wait for trace to start +echo "Waiting for trace pod to start..." +sleep 15 + +echo "" +echo "=== Test 1: RST and SOCK_ERR (connection refused) ===" +# Connect to a closed port to generate RST and socket error (ECONNREFUSED=111) +kubectl run rst-gen --rm -i --restart=Never --image=busybox --overrides="{\"spec\":{\"nodeName\":\"$NODE\"}}" -- sh -c " +for i in 1 2 3; do + echo \"RST attempt \$i: connecting to closed port 9999\" + nc -zv -w1 127.0.0.1 9999 2>&1 || true + sleep 0.5 +done +echo 'RST generation complete' +" 2>&1 || true + +echo "" +echo "=== Test 2: DROP and RETRANS (NetworkPolicy block) ===" +# NetworkPolicy drops will cause TCP to retransmit SYN packets + +# Create a NetworkPolicy to block traffic +cat </dev/null || true +sleep 5 +kubectl wait --for=condition=Ready pod/drop-target --timeout=30s 2>/dev/null || true + +TARGET_IP=$(kubectl get pod drop-target -o jsonpath='{.status.podIP}' 2>/dev/null || echo "10.244.0.99") +echo "Target IP: $TARGET_IP" + +# Generate blocked traffic - connection attempts will retransmit SYN packets +echo "Sending blocked traffic (will cause retransmissions)..." +kubectl run drop-gen --rm -i --restart=Never --image=busybox --overrides="{\"spec\":{\"nodeName\":\"$NODE\"}}" -- sh -c " +for i in 1 2 3 4 5; do + echo \"Attempt \$i to $TARGET_IP:80\" + nc -zv -w2 $TARGET_IP 80 2>&1 || true + sleep 0.5 +done +echo 'Traffic generation complete' +" 2>&1 || true + +echo "" +echo "=== Test 3: NFQ_DROP (iptables NFQUEUE with no consumer) ===" +# Add an iptables -j NFQUEUE rule pointing to a queue with no reader. +# The kernel calls __nf_queue which returns -ESRCH, and fexit catches it. +echo "Creating privileged pod to add NFQUEUE iptables rule..." +cat </dev/null 2>&1 + echo "Adding NFQUEUE rule on OUTPUT to $TARGET_IP:80 queue 42 (no consumer)..." + iptables -I OUTPUT -d $TARGET_IP -p tcp --dport 80 -j NFQUEUE --queue-num 42 + for i in 1 2 3 4 5; do + echo "attempt \$i: connecting to $TARGET_IP:80 via NFQUEUE..." + nc -zv -w2 $TARGET_IP 80 2>&1 || true + sleep 0.5 + done + echo "Removing NFQUEUE rule..." + iptables -D OUTPUT -d $TARGET_IP -p tcp --dport 80 -j NFQUEUE --queue-num 42 2>/dev/null + echo "NFQUEUE test done" +EOF +kubectl wait --for=condition=Ready pod/nfqueue-helper --timeout=60s 2>/dev/null || true +kubectl wait --for=jsonpath='{.status.phase}'=Succeeded pod/nfqueue-helper --timeout=60s 2>/dev/null || true +kubectl logs nfqueue-helper 2>/dev/null || true +kubectl delete pod nfqueue-helper --force --grace-period=0 2>/dev/null || true + +echo "" +echo "=== Waiting for trace to complete ===" +wait $TRACE_PID || true + +echo "" +echo "=== Trace Output ===" +cat "$OUTPUT_FILE" + +echo "" +echo "=== Cleanup ===" +kubectl delete networkpolicy deny-all-test 2>/dev/null || true +kubectl delete pod drop-target --force --grace-period=0 2>/dev/null || true +kubectl delete pod nfqueue-helper --force --grace-period=0 2>/dev/null || true +kubectl delete pod -l app=retina-trace --force --grace-period=0 2>/dev/null || true + +echo "" +echo "=== Test Complete ===" + +# Check results. +# Use precise patterns to match actual event output lines and avoid false +# positives from verbose/startup logs that may mention these keywords. +DROPS_FOUND=false +RST_FOUND=false +SOCK_ERR_FOUND=false +RETRANS_FOUND=false +NFQ_DROP_FOUND=false + +if grep -qP '\bDROP\b.*kfree_skb' "$OUTPUT_FILE"; then + DROPS_FOUND=true + echo "✓ DROP events captured" +else + echo "✗ DROP events NOT captured (requires cluster NetworkPolicy support)" +fi + +if grep -qP '\bRST_(SENT|RECV)\b' "$OUTPUT_FILE"; then + RST_FOUND=true + echo "✓ RST events captured" +else + echo "✗ RST events NOT captured" +fi + +if grep -qP '\bSOCK_ERR\b.*inet_sk_error_report' "$OUTPUT_FILE"; then + SOCK_ERR_FOUND=true + echo "✓ SOCK_ERR events captured" +else + echo "✗ SOCK_ERR events NOT captured" +fi + +if grep -qP '\bRETRANS\b.*tcp_retransmit_skb' "$OUTPUT_FILE"; then + RETRANS_FOUND=true + echo "✓ RETRANS events captured" +else + echo "✗ RETRANS events NOT captured" +fi + +if grep -qP '\bNFQ_DROP\b.*__nf_queue' "$OUTPUT_FILE"; then + NFQ_DROP_FOUND=true + echo "✓ NFQ_DROP events captured" +else + echo "✗ NFQ_DROP events NOT captured (requires iptables/NFQUEUE support)" +fi + +# Count successes +SUCCESSES=0 +$DROPS_FOUND && ((SUCCESSES++)) || true +$RST_FOUND && ((SUCCESSES++)) || true +$SOCK_ERR_FOUND && ((SUCCESSES++)) || true +$RETRANS_FOUND && ((SUCCESSES++)) || true +$NFQ_DROP_FOUND && ((SUCCESSES++)) || true + +if [ $SUCCESSES -ge 3 ]; then + echo "" + echo "SUCCESS: $SUCCESSES/5 event types captured!" + exit 0 +elif [ $SUCCESSES -ge 1 ]; then + echo "" + echo "PARTIAL SUCCESS: $SUCCESSES/5 event types captured" + echo "Note: Some events may not occur depending on kernel behavior" + exit 0 +else + echo "" + echo "NOTE: No events captured. This may be expected if:" + echo " - The kernel doesn't trigger these tracepoints" + echo " - Traffic was blocked before reaching the tracepoints" + echo " - Try running manually with different traffic patterns" + exit 0 +fi diff --git a/test/e2e/test_bpftrace_filter_ip.sh b/test/e2e/test_bpftrace_filter_ip.sh new file mode 100755 index 0000000000..db3a3e2e58 --- /dev/null +++ b/test/e2e/test_bpftrace_filter_ip.sh @@ -0,0 +1,317 @@ +#!/bin/bash +# Test script for bpftrace --ip flag +# This validates that IP filtering works correctly in bpftrace scripts +# Tests drops, RST, socket errors, and retransmits with --ip filtering +# Usage: ./test_bpftrace_filter_ip.sh [kubeconfig_path] +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +echo "=== Bpftrace --ip Flag Test ===" +echo "" + +# Get first node +NODE=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}') +echo "Target node: $NODE" + +# Clean up any leftover test pods +echo "Cleaning up previous test pods..." +kubectl delete pod drop-gen rst-gen traffic-gen filter-test-target nfqueue-helper --force --grace-period=0 2>/dev/null || true +kubectl delete pod -l app=retina-trace --force --grace-period=0 2>/dev/null || true +kubectl delete networkpolicy deny-all-filter-ip-test 2>/dev/null || true +sleep 2 + +# Build CLI if needed +if [[ ! -f "$REPO_ROOT/kubectl-retina" ]]; then + echo "Building kubectl-retina..." + cd "$REPO_ROOT" + go build -ldflags "-X github.com/microsoft/retina/internal/buildinfo.Version=v1.0.3" -o kubectl-retina ./cli/main.go +fi + +# Create a test pod to get a stable IP for filtering +echo "Creating test target pod..." +kubectl run filter-test-target --image=nginx --labels="app=filter-test-target" --overrides="{\"spec\":{\"nodeName\":\"$NODE\"}}" 2>/dev/null || true +sleep 5 +kubectl wait --for=condition=Ready pod/filter-test-target --timeout=60s 2>/dev/null || true + +TARGET_IP=$(kubectl get pod filter-test-target -o jsonpath='{.status.podIP}') +if [[ -z "$TARGET_IP" ]]; then + echo "ERROR: Could not get target pod IP" + exit 1 +fi +echo "Target pod IP: $TARGET_IP" + +echo "" +echo "=== Starting bpftrace with --ip $TARGET_IP (70s duration) ===" +echo "This will capture drops, RST, socket errors, retransmits, and NFQUEUE drops filtered to $TARGET_IP on $NODE" +echo "" + +# Create output files +OUTPUT_FILE=$(mktemp) +ERROR_FILE=$(mktemp) +trap "rm -f $OUTPUT_FILE $ERROR_FILE; kubectl delete networkpolicy deny-all-filter-ip-test 2>/dev/null || true; kubectl delete pod filter-test-target nfqueue-helper --force --grace-period=0 2>/dev/null || true; kubectl delete pod -l app=retina-trace --force --grace-period=0 2>/dev/null || true" EXIT + +# Run trace with IP filter in background and capture output +# Duration must be long enough for all test phases: RST, DROP/RETRANS, NFQUEUE, extra traffic +# (each phase takes ~15-30s including pod startup and traffic generation) +"$REPO_ROOT/kubectl-retina" bpftrace "$NODE" --duration 70s --startup-timeout 120s \ + --ip "$TARGET_IP" \ + --retina-shell-image-version v1.0.3 > "$OUTPUT_FILE" 2>"$ERROR_FILE" & +TRACE_PID=$! + +# Wait for trace to start +echo "Waiting for trace pod to start..." +sleep 15 + +echo "" +echo "=== Test 1: RST and SOCK_ERR (connection refused via target IP) ===" +# Connect to the target pod on a closed port to generate RST and socket error +kubectl run rst-gen --rm -i --restart=Never --image=busybox --overrides="{\"spec\":{\"nodeName\":\"$NODE\"}}" -- sh -c " +for i in 1 2 3; do + echo \"RST attempt \$i: connecting to $TARGET_IP on closed port 9999\" + nc -zv -w1 $TARGET_IP 9999 2>&1 || true + sleep 0.5 +done +echo 'RST generation complete' +" 2>&1 || true + +echo "" +echo "=== Test 2: DROP and RETRANS (NetworkPolicy block on filtered IP) ===" +# Apply a deny-all NetworkPolicy to filter-test-target itself so that +# subsequent traffic to TARGET_IP is dropped and retransmitted. +# This ensures DROP and RETRANS events occur on the IP we're filtering. + +cat <&1 || true + sleep 0.5 +done +echo 'Traffic generation complete' +" 2>&1 || true + +echo "" +echo "=== Test 3: NFQ_DROP with IP filter ===" +# Add an iptables -j NFQUEUE rule for traffic to TARGET_IP; no consumer on queue. +# The fexit:vmlinux:__nf_queue probe should capture the drop and the IP filter +# should ensure only events involving TARGET_IP appear. +# NOTE: This test runs before the "additional traffic" test to ensure it completes +# within the trace window. The nfqueue-helper pod needs time to install iptables. +echo "Creating privileged pod to add NFQUEUE iptables rule..." +cat </dev/null 2>&1 + echo "Adding NFQUEUE rule on OUTPUT to $TARGET_IP:80 queue 42 (no consumer)..." + iptables -I OUTPUT -d $TARGET_IP -p tcp --dport 80 -j NFQUEUE --queue-num 42 + for i in 1 2 3 4 5; do + echo "attempt \$i: connecting to $TARGET_IP:80 via NFQUEUE..." + nc -zv -w2 $TARGET_IP 80 2>&1 || true + sleep 0.5 + done + echo "Removing NFQUEUE rule..." + iptables -D OUTPUT -d $TARGET_IP -p tcp --dport 80 -j NFQUEUE --queue-num 42 2>/dev/null + echo "NFQUEUE test done" +EOF +kubectl wait --for=condition=Ready pod/nfqueue-helper --timeout=60s 2>/dev/null || true +kubectl wait --for=jsonpath='{.status.phase}'=Succeeded pod/nfqueue-helper --timeout=60s 2>/dev/null || true +kubectl logs nfqueue-helper 2>/dev/null || true +kubectl delete pod nfqueue-helper --force --grace-period=0 2>/dev/null || true + +echo "" +echo "=== Test 4: Additional traffic to filtered IP ===" +# Generate extra traffic to the filtered IP to increase chances of capture +echo "Generating additional traffic to $TARGET_IP..." +kubectl run traffic-gen --rm -i --restart=Never --image=busybox --overrides="{\"spec\":{\"nodeName\":\"$NODE\"}}" -- sh -c " +for i in 1 2 3 4 5; do + echo \"Attempt \$i: connecting to $TARGET_IP:80\" + wget -q -O /dev/null -T 2 http://$TARGET_IP:80/ 2>&1 || true + echo \"Attempt \$i: connecting to $TARGET_IP:9999 (closed port)\" + nc -zv -w1 $TARGET_IP 9999 2>&1 || true + sleep 1 +done +echo 'Traffic generation complete' +" 2>&1 || true + +echo "" +echo "=== Waiting for trace to complete ===" +wait $TRACE_PID 2>/dev/null || true + +echo "" +echo "=== Trace Output ===" +cat "$OUTPUT_FILE" + +echo "" +echo "=== Error Output ===" +cat "$ERROR_FILE" + +echo "" +echo "=== Cleanup ===" +kubectl delete networkpolicy deny-all-filter-ip-test 2>/dev/null || true +kubectl delete pod filter-test-target nfqueue-helper --force --grace-period=0 2>/dev/null || true +kubectl delete pod -l app=retina-trace --force --grace-period=0 2>/dev/null || true + +echo "" +echo "=== Test Results ===" + +# Check for the specific casting error that was previously seen +CAST_ERROR=false +if grep -q "Cannot cast from" "$ERROR_FILE" || grep -q "Cannot cast from" "$OUTPUT_FILE"; then + CAST_ERROR=true + echo "✗ FAIL: bpftrace cast error detected" + echo " The --ip flag is generating invalid bpftrace code" +fi + +if [[ "$CAST_ERROR" == "true" ]]; then + echo "" + echo "TEST FAILED: bpftrace casting error" + echo "The --ip flag is generating code that bpftrace cannot execute." + exit 1 +fi + +# Check if the trace started successfully +TRACE_STARTED=false +if grep -q "Tracing network issues" "$OUTPUT_FILE"; then + TRACE_STARTED=true + echo "✓ Trace started successfully with --ip filter" +fi + +# Check for the filtered IP in the output (shows filter was applied) +FILTER_APPLIED=false +if grep -q "$TARGET_IP" "$ERROR_FILE" 2>/dev/null || grep -qi "filter" "$OUTPUT_FILE" 2>/dev/null; then + FILTER_APPLIED=true + echo "✓ IP filter was applied for $TARGET_IP" +fi + +# Verify each event type was captured AND contains the filtered IP on the same line. +DROPS_FOUND=false +RST_FOUND=false +SOCK_ERR_FOUND=false +RETRANS_FOUND=false +NFQ_DROP_FOUND=false + +if grep -qP '\bDROP\b.*kfree_skb' "$OUTPUT_FILE"; then + if grep -P '\bDROP\b.*kfree_skb' "$OUTPUT_FILE" | grep -qF "$TARGET_IP"; then + DROPS_FOUND=true + echo "✓ DROP events captured for $TARGET_IP" + else + echo "✗ DROP events captured but NOT for filtered IP $TARGET_IP — filter may be broken" + fi +else + echo "✗ DROP events NOT captured (requires cluster NetworkPolicy support)" +fi + +if grep -qP '\bRST_(SENT|RECV)\b' "$OUTPUT_FILE"; then + if grep -P '\bRST_(SENT|RECV)\b' "$OUTPUT_FILE" | grep -qF "$TARGET_IP"; then + RST_FOUND=true + echo "✓ RST events captured for $TARGET_IP" + else + echo "✗ RST events captured but NOT for filtered IP $TARGET_IP — filter may be broken" + fi +else + echo "✗ RST events NOT captured" +fi + +if grep -qP '\bSOCK_ERR\b.*inet_sk_error_report' "$OUTPUT_FILE"; then + if grep -P '\bSOCK_ERR\b.*inet_sk_error_report' "$OUTPUT_FILE" | grep -qF "$TARGET_IP"; then + SOCK_ERR_FOUND=true + echo "✓ SOCK_ERR events captured for $TARGET_IP" + else + echo "✗ SOCK_ERR events captured but NOT for filtered IP $TARGET_IP — filter may be broken" + fi +else + echo "✗ SOCK_ERR events NOT captured" +fi + +if grep -qP '\bRETRANS\b.*tcp_retransmit_skb' "$OUTPUT_FILE"; then + if grep -P '\bRETRANS\b.*tcp_retransmit_skb' "$OUTPUT_FILE" | grep -qF "$TARGET_IP"; then + RETRANS_FOUND=true + echo "✓ RETRANS events captured for $TARGET_IP" + else + echo "✗ RETRANS events captured but NOT for filtered IP $TARGET_IP — filter may be broken" + fi +else + echo "✗ RETRANS events NOT captured" +fi + +if grep -qP '\bNFQ_DROP\b.*__nf_queue' "$OUTPUT_FILE"; then + if grep -P '\bNFQ_DROP\b.*__nf_queue' "$OUTPUT_FILE" | grep -qF "$TARGET_IP"; then + NFQ_DROP_FOUND=true + echo "✓ NFQ_DROP events captured for $TARGET_IP" + else + echo "✗ NFQ_DROP events captured but NOT for filtered IP $TARGET_IP — filter may be broken" + fi +else + echo "✗ NFQ_DROP events NOT captured (requires iptables/NFQUEUE support)" +fi + +# Count successes +SUCCESSES=0 +$DROPS_FOUND && ((SUCCESSES++)) || true +$RST_FOUND && ((SUCCESSES++)) || true +$SOCK_ERR_FOUND && ((SUCCESSES++)) || true +$RETRANS_FOUND && ((SUCCESSES++)) || true +$NFQ_DROP_FOUND && ((SUCCESSES++)) || true + +if [[ "$TRACE_STARTED" != "true" ]]; then + echo "" + echo "TEST INCONCLUSIVE: Trace may not have started" + echo "Check the output above for details" + exit 1 +fi + +if [ $SUCCESSES -ge 3 ]; then + echo "" + echo "SUCCESS: $SUCCESSES/5 event types captured with --ip filter!" + exit 0 +elif [ $SUCCESSES -ge 1 ]; then + echo "" + echo "PARTIAL SUCCESS: $SUCCESSES/5 event types captured with --ip filter" + echo "Note: Some events may not occur depending on kernel behavior" + exit 0 +else + echo "" + echo "NOTE: No events captured with --ip filter. This may be expected if:" + echo " - The kernel doesn't trigger these tracepoints for the filtered IP" + echo " - Traffic was blocked before reaching the tracepoints" + echo " - The IP filter is too restrictive for the generated traffic" + echo " - Try running manually with different traffic patterns" + exit 0 +fi diff --git a/test/image/Dockerfile b/test/image/Dockerfile index 08fa9f5bbf..0b2c4f699a 100644 --- a/test/image/Dockerfile +++ b/test/image/Dockerfile @@ -1,7 +1,7 @@ # build stage -# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.24.4-azurelinux3.0 --format "{{.Name}}@{{.Digest}}" -FROM mcr.microsoft.com/oss/go/microsoft/golang:1.24.4-azurelinux3.0@sha256:250d01e55a37bd79d7014ae83f9f50aa6fa5570ca910e7f19faeff4bb0132ae1 AS builder -ENV CGO_ENABLED=0 +# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.26.3-azurelinux3.0 --format "{{.Name}}@{{.Digest}}" +FROM mcr.microsoft.com/oss/go/microsoft/golang:1.26.3-1-azurelinux3.0@sha256:ef480755a4126131197d7311ab1e24d55600407194b45349c4975b7ed0d176e6 AS builder +ENV CGO_ENABLED=1 COPY . /go/src/github.com/microsoft/retina WORKDIR /go/src/github.com/microsoft/retina RUN tdnf install -y clang lld bpftool libbpf-devel make git jq diff --git a/test/managers/filtermanager/main.go b/test/managers/filtermanager/main.go index 7de2a9fd89..919500e15d 100644 --- a/test/managers/filtermanager/main.go +++ b/test/managers/filtermanager/main.go @@ -8,11 +8,13 @@ import ( "bufio" "context" "fmt" + "log/slog" "net" "os" "strings" "time" + kcfg "github.com/microsoft/retina/pkg/config" "github.com/microsoft/retina/pkg/log" "github.com/microsoft/retina/pkg/managers/filtermanager" "github.com/microsoft/retina/pkg/managers/watchermanager" @@ -32,13 +34,13 @@ func main() { log.SetupZapLogger(opts) l := log.Logger().Named("test-packetparser") - metrics.InitializeMetrics() + metrics.InitializeMetrics(slog.Default()) ctx := context.Background() // watcher manager - wm := watchermanager.NewWatcherManager() - wm.Watchers = []watchermanager.IWatcher{endpoint.Watcher(), apiserver.Watcher()} + wm := watchermanager.NewWatcherManager(kcfg.DefaultFilterMapMaxEntries) + wm.Watchers = []watchermanager.IWatcher{endpoint.Watcher(), apiserver.Watcher(kcfg.DefaultFilterMapMaxEntries)} err := wm.Start(ctx) if err != nil { @@ -52,7 +54,7 @@ func main() { }() // Filtermanager. - f, err := filtermanager.Init(5) + f, err := filtermanager.Init(5, kcfg.DefaultFilterMapMaxEntries) if err != nil { l.Error("Failed to start Filtermanager", zap.Error(err)) panic(err) diff --git a/test/multicloud/test/go.mod b/test/multicloud/test/go.mod index e872f06bf7..cd76ece8e3 100644 --- a/test/multicloud/test/go.mod +++ b/test/multicloud/test/go.mod @@ -50,19 +50,19 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/stretchr/testify v1.9.0 // indirect github.com/tmccombs/hcl2json v0.6.4 // indirect - github.com/ulikunitz/xz v0.5.10 // indirect + github.com/ulikunitz/xz v0.5.14 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zclconf/go-cty v1.15.0 // indirect - golang.org/x/crypto v0.36.0 // indirect - golang.org/x/mod v0.21.0 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/oauth2 v0.24.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.27.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.8.0 // indirect - golang.org/x/tools v0.26.0 // indirect + golang.org/x/tools v0.38.0 // indirect google.golang.org/protobuf v1.35.1 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/test/multicloud/test/go.sum b/test/multicloud/test/go.sum index a27d7ceaf5..5334673c05 100644 --- a/test/multicloud/test/go.sum +++ b/test/multicloud/test/go.sum @@ -119,8 +119,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tmccombs/hcl2json v0.6.4 h1:/FWnzS9JCuyZ4MNwrG4vMrFrzRgsWEOVi+1AyYUVLGw= github.com/tmccombs/hcl2json v0.6.4/go.mod h1:+ppKlIW3H5nsAsZddXPy2iMyvld3SHxyjswOZhavRDk= -github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= -github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg= +github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -132,44 +132,44 @@ github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmB golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= -golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/test/plugin/dns/main_linux.go b/test/plugin/dns/main_linux.go index 59cb24f50f..d225de580b 100644 --- a/test/plugin/dns/main_linux.go +++ b/test/plugin/dns/main_linux.go @@ -17,6 +17,7 @@ package main import ( "context" + "log/slog" "time" "github.com/microsoft/retina/pkg/config" @@ -35,7 +36,7 @@ func main() { log.SetupZapLogger(opts) l := log.Logger().Named("test-dns") - metrics.InitializeMetrics() + metrics.InitializeMetrics(slog.Default()) ctx := context.Background() tt := dns.New(&config.Config{ diff --git a/test/plugin/dropreason/main_linux.go b/test/plugin/dropreason/main_linux.go index 06fb3754c7..f1c0ee40e1 100644 --- a/test/plugin/dropreason/main_linux.go +++ b/test/plugin/dropreason/main_linux.go @@ -4,6 +4,7 @@ package main import ( "context" + "log/slog" "net" "time" @@ -23,18 +24,18 @@ func main() { log.SetupZapLogger(opts) l := log.Logger().Named("test-dropreason") - metrics.InitializeMetrics() + metrics.InitializeMetrics(slog.Default()) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() cfg := &kcfg.Config{ MetricsInterval: 1 * time.Second, - EnablePodLevel: true, + EnablePodLevel: true, // Set to false to test fexit programs } // Filtermanager. - f, err := filtermanager.Init(3) + f, err := filtermanager.Init(3, kcfg.DefaultFilterMapMaxEntries) if err != nil { l.Error("Start filtermanager failed", zap.Error(err)) return diff --git a/test/plugin/infiniband/main_linux.go b/test/plugin/infiniband/main_linux.go index 761d70854c..68f1fb4074 100644 --- a/test/plugin/infiniband/main_linux.go +++ b/test/plugin/infiniband/main_linux.go @@ -4,6 +4,7 @@ package main import ( "context" + "log/slog" "time" kcfg "github.com/microsoft/retina/pkg/config" @@ -17,7 +18,7 @@ func main() { log.SetupZapLogger(log.GetDefaultLogOpts()) //nolint std. l := log.Logger().Named("test-infiniband") - metrics.InitializeMetrics() + metrics.InitializeMetrics(slog.Default()) cfg := &kcfg.Config{ MetricsInterval: 1 * time.Second, diff --git a/test/plugin/linuxutil/main_linux.go b/test/plugin/linuxutil/main_linux.go index c46dcf04a7..9de087ccc5 100644 --- a/test/plugin/linuxutil/main_linux.go +++ b/test/plugin/linuxutil/main_linux.go @@ -4,6 +4,7 @@ package main import ( "context" + "log/slog" "time" kcfg "github.com/microsoft/retina/pkg/config" @@ -19,7 +20,7 @@ func main() { log.SetupZapLogger(log.GetDefaultLogOpts()) l := log.Logger().Named("test-linuxutil") - metrics.InitializeMetrics() + metrics.InitializeMetrics(slog.Default()) cfg := &kcfg.Config{ MetricsInterval: 1 * time.Second, diff --git a/test/plugin/packetforward/main_linux.go b/test/plugin/packetforward/main_linux.go index 7ace2e3570..51d20dfb59 100644 --- a/test/plugin/packetforward/main_linux.go +++ b/test/plugin/packetforward/main_linux.go @@ -6,6 +6,7 @@ package main import ( "context" + "log/slog" "time" "github.com/microsoft/retina/pkg/log" @@ -21,7 +22,7 @@ func main() { log.SetupZapLogger(log.GetDefaultLogOpts()) l := log.Logger().Named("test-packetforward") - metrics.InitializeMetrics() + metrics.InitializeMetrics(slog.Default()) ctx := context.Background() diff --git a/test/plugin/packetparser/main_linux.go b/test/plugin/packetparser/main_linux.go index 2596dd1fe7..3ae84bfebb 100644 --- a/test/plugin/packetparser/main_linux.go +++ b/test/plugin/packetparser/main_linux.go @@ -6,6 +6,7 @@ package main import ( "context" + "log/slog" "net" "time" @@ -27,12 +28,12 @@ func main() { log.SetupZapLogger(opts) l := log.Logger().Named("test-packetparser") - metrics.InitializeMetrics() + metrics.InitializeMetrics(slog.Default()) ctxTimeout, cancel := context.WithTimeout(context.Background(), 30*time.Second) // watcher manager - wm := watchermanager.NewWatcherManager() + wm := watchermanager.NewWatcherManager(kcfg.DefaultFilterMapMaxEntries) wm.Watchers = []watchermanager.IWatcher{endpoint.Watcher()} err := wm.Start(ctxTimeout) @@ -46,7 +47,7 @@ func main() { } }() // Filtermanager. - f, err := filtermanager.Init(3) + f, err := filtermanager.Init(3, kcfg.DefaultFilterMapMaxEntries) if err != nil { l.Error("Start filtermanager failed", zap.Error(err)) return diff --git a/test/profiles/advanced/values.yaml b/test/profiles/advanced/values.yaml index 6025ce3539..5c07e6eaaf 100644 --- a/test/profiles/advanced/values.yaml +++ b/test/profiles/advanced/values.yaml @@ -1,5 +1,6 @@ enablePodLevel: true enableAnnotations: true +packetParserRingBuffer: "enabled" operator: enabled: true enableRetinaEndpoint: true diff --git a/test/utsummary/main.go b/test/utsummary/main.go index 3faff60442..647551262e 100644 --- a/test/utsummary/main.go +++ b/test/utsummary/main.go @@ -68,6 +68,10 @@ func run(r io.Reader) (msg string, failures bool, err error) { // Stores output produced by each test. testOutputs := map[string][]string{} + // Tracks packages that had at least one test-level event, + // used to distinguish build failures from normal test failures. + pkgHasTests := map[string]bool{} + start := time.Now() for scanner.Scan() { // When the build fails, go test -json doesn't emit a valid JSON value, only @@ -86,10 +90,23 @@ func run(r io.Reader) (msg string, failures bool, err error) { // The Test field, if non-empty, specifies the test, example, or benchmark // function that caused the event. Events for the overall package test do // not set Test. + if event.Test != "" { + pkgHasTests[event.Package] = true + } if event.Action == "fail" && event.Test != "" { failedTests = append(failedTests, testpath) } + // A package-level fail with no Test field and no test-level events + // indicates a build failure (e.g. [build failed]). These must be + // tracked as failures so that broken packages are not silently ignored. + // If the package had test-level events, the package fail is just a + // summary of individual test failures (already counted above). + if event.Action == "fail" && event.Test == "" && !pkgHasTests[event.Package] { + failedTests = append(failedTests, event.Package+" [build failed]") + counts["fail"]++ + } + if event.Action == "output" { if *verbose { fmt.Print(event.Output) @@ -97,9 +114,9 @@ func run(r io.Reader) (msg string, failures bool, err error) { testOutputs[testpath] = append(testOutputs[testpath], event.Output) } - // We don't want to count package passes/fails because these don't - // represent specific tests being run. However, skips of an entire package - // are not duplicated with individual test skips. + // We don't want to count package passes because these don't represent + // specific tests being run. However, skips of an entire package are not + // duplicated with individual test skips. if event.Test != "" || event.Action == "skip" { counts[event.Action]++ } diff --git a/test/watchers/apiserver/main.go b/test/watchers/apiserver/main.go index 4703e12922..60905500b8 100644 --- a/test/watchers/apiserver/main.go +++ b/test/watchers/apiserver/main.go @@ -8,6 +8,7 @@ import ( "context" "time" + kcfg "github.com/microsoft/retina/pkg/config" "github.com/microsoft/retina/pkg/log" "github.com/microsoft/retina/pkg/managers/filtermanager" "github.com/microsoft/retina/pkg/managers/watchermanager" @@ -24,7 +25,7 @@ func main() { ctx := context.Background() // Filtermanager. - f, err := filtermanager.Init(5) + f, err := filtermanager.Init(5, kcfg.DefaultFilterMapMaxEntries) if err != nil { l.Error("Failed to start Filtermanager", zap.Error(err)) panic(err) @@ -35,8 +36,8 @@ func main() { } }() // watcher manager - wm := watchermanager.NewWatcherManager() - wm.Watchers = []watchermanager.IWatcher{apiserver.Watcher()} + wm := watchermanager.NewWatcherManager(kcfg.DefaultFilterMapMaxEntries) + wm.Watchers = []watchermanager.IWatcher{apiserver.Watcher(kcfg.DefaultFilterMapMaxEntries)} // apiserver watcher. err = wm.Start(ctx) diff --git a/test/watchers/veth/main.go b/test/watchers/veth/main.go index 50a00c1c01..46343de747 100644 --- a/test/watchers/veth/main.go +++ b/test/watchers/veth/main.go @@ -8,6 +8,7 @@ import ( "context" "time" + kcfg "github.com/microsoft/retina/pkg/config" "github.com/microsoft/retina/pkg/log" "github.com/microsoft/retina/pkg/managers/watchermanager" "github.com/microsoft/retina/pkg/watchers/endpoint" @@ -23,7 +24,7 @@ func main() { ctx := context.Background() // watcher manager - wm := watchermanager.NewWatcherManager() + wm := watchermanager.NewWatcherManager(kcfg.DefaultFilterMapMaxEntries) wm.Watchers = []watchermanager.IWatcher{endpoint.Watcher()} err := wm.Start(ctx)