mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-08-27 00:37:20 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cd2b9a87b | ||
|
|
898a59fe40 | ||
|
|
e71bf87cc2 | ||
|
|
774fd8d935 | ||
|
|
1d4315ced6 | ||
|
|
b6b2bf8483 | ||
|
|
ac9a4c9c5b | ||
|
|
c532928cd2 | ||
|
|
4b04098689 | ||
|
|
b3369c2b3e | ||
|
|
fbfd6c99fe | ||
|
|
12f8bc3daa | ||
|
|
b4e3ed278a | ||
|
|
c67775c1a0 | ||
|
|
83e6cffe5f | ||
|
|
84dc8157b9 | ||
|
|
a8fb5210f4 | ||
|
|
d3af4edd31 | ||
|
|
b9dc00cc6c | ||
|
|
8990c4b06e | ||
|
|
e03fa0a046 | ||
|
|
758d454434 | ||
|
|
f0d0eda060 | ||
|
|
4eea31d916 | ||
|
|
cfea35e7c0 | ||
|
|
630aa631ee | ||
|
|
b61728a11f | ||
|
|
252f9650c9 | ||
|
|
857a028585 | ||
|
|
6e8704d6f1 | ||
|
|
c8c246a809 | ||
|
|
a41958c173 | ||
|
|
17518349ff | ||
|
|
266647ef2f | ||
|
|
3062dd8d5c | ||
|
|
91db72b484 | ||
|
|
5f0835f7d0 | ||
|
|
9af9845fb9 | ||
|
|
007ef32e77 | ||
|
|
d9904d9632 | ||
|
|
074fee81f7 | ||
|
|
7bd7eca528 | ||
|
|
b1830ff515 | ||
|
|
885819ea9a | ||
|
|
20b0c5ed06 | ||
|
|
71067edb41 | ||
|
|
d0584a4d4d | ||
|
|
acc1aad843 | ||
|
|
8027e273e4 | ||
|
|
3b9d280a95 | ||
|
|
74811c669b | ||
|
|
a1d1a12c1a | ||
|
|
050dc8499f |
@@ -0,0 +1,39 @@
|
||||
name: 'Setup Go Environment'
|
||||
description: 'Setup Go with caching and common environment variables'
|
||||
inputs:
|
||||
go-version-file:
|
||||
description: 'Path to go.mod file'
|
||||
required: false
|
||||
default: 'go.mod'
|
||||
outputs:
|
||||
go-version:
|
||||
description: 'The Go version that was installed'
|
||||
value: ${{ steps.setup-go.outputs.go-version }}
|
||||
cache-hit:
|
||||
description: 'Whether the Go cache was hit'
|
||||
value: ${{ steps.setup-go.outputs.cache-hit }}
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Setup Go
|
||||
id: setup-go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: ${{ inputs.go-version-file }}
|
||||
cache: true
|
||||
|
||||
- name: Set Go environment variables
|
||||
shell: bash
|
||||
run: |
|
||||
echo "GOMAXPROCS=2" >> $GITHUB_ENV
|
||||
echo "GOCACHE=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||
echo "GOMODCACHE=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||
|
||||
- name: Print Go environment
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Go version: $(go version)"
|
||||
echo "GOOS: $(go env GOOS)"
|
||||
echo "GOARCH: $(go env GOARCH)"
|
||||
echo "Cache directory: $(go env GOCACHE)"
|
||||
echo "Module cache: $(go env GOMODCACHE)"
|
||||
@@ -1,163 +0,0 @@
|
||||
name: build-test-deploy
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
fail_if_pull_request_is_draft:
|
||||
if: github.event.pull_request.draft == true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Fails in order to indicate that pull request needs to be marked as ready to review and unit tests workflow needs to pass.
|
||||
run: exit 1
|
||||
|
||||
tidy-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- run: make tidy-diff
|
||||
|
||||
test-integration:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- uses: replicatedhq/action-k3s@main
|
||||
id: k3s
|
||||
with:
|
||||
version: v1.31.2-k3s1
|
||||
# test-integration includes unit tests
|
||||
- run: make test-integration
|
||||
|
||||
|
||||
compile-preflight:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- run: make generate preflight
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: preflight
|
||||
path: bin/preflight
|
||||
|
||||
validate-preflight-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
needs: compile-preflight
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: replicatedhq/action-k3s@main
|
||||
id: k3s
|
||||
with:
|
||||
version: v1.31.2-k3s1
|
||||
- name: Download preflight binary
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: preflight
|
||||
path: bin/
|
||||
- run: chmod +x bin/preflight
|
||||
- run: make preflight-e2e-test
|
||||
|
||||
compile-supportbundle:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- run: make generate support-bundle
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: support-bundle
|
||||
path: bin/support-bundle
|
||||
|
||||
validate-supportbundle-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
needs: compile-supportbundle
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: replicatedhq/action-k3s@main
|
||||
id: k3s
|
||||
with:
|
||||
version: v1.31.2-k3s1
|
||||
- name: Download support bundle binary
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: support-bundle
|
||||
path: bin/
|
||||
- run: chmod +x bin/support-bundle
|
||||
- run: make support-bundle-e2e-test
|
||||
|
||||
# Additional e2e tests for support bundle that run in Go, these create a Kind cluster
|
||||
validate-supportbundle-e2e-go:
|
||||
runs-on: ubuntu-latest
|
||||
needs: compile-supportbundle
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Download support bundle binary
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: support-bundle
|
||||
path: bin/
|
||||
- run: chmod +x bin/support-bundle
|
||||
- name: Download preflight binary
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: preflight
|
||||
path: bin/
|
||||
- run: chmod +x bin/preflight
|
||||
- run: make support-bundle-e2e-go-test
|
||||
|
||||
# summary jobs, these jobs will only run if all the other jobs have succeeded
|
||||
validate-pr-tests:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- tidy-check
|
||||
- test-integration
|
||||
- validate-preflight-e2e
|
||||
- validate-supportbundle-e2e
|
||||
- validate-supportbundle-e2e-go
|
||||
steps:
|
||||
- run: echo "All PR tests passed"
|
||||
|
||||
|
||||
# this job will validate that the validation did not fail and that all pr-tests succeed
|
||||
# it is used for the github branch protection rule
|
||||
validate-success:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- validate-pr-tests
|
||||
if: always()
|
||||
steps:
|
||||
# https://docs.github.com/en/actions/learn-github-actions/contexts#needs-context
|
||||
# if the validate-pr-tests job was not successful, this job will fail
|
||||
- name: fail if validate-pr-tests job was not successful
|
||||
if: needs.validate-pr-tests.result != 'success'
|
||||
run: exit 1
|
||||
# if the validate-pr-tests job was successful, this job will succeed
|
||||
- name: succeed if validate-pr-tests job succeeded
|
||||
if: needs.validate-pr-tests.result == 'success'
|
||||
run: echo "Validation succeeded"
|
||||
@@ -3,9 +3,9 @@ name: build-test
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
branches: [main]
|
||||
branches: [v1beta3]
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [v1beta3]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
support-bundle: ${{ steps.filter.outputs.support-bundle }}
|
||||
examples: ${{ steps.filter.outputs.examples }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
@@ -44,10 +44,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- uses: actions/checkout@v5
|
||||
- uses: ./.github/actions/setup-go
|
||||
|
||||
- name: Check go mod tidy
|
||||
run: |
|
||||
@@ -73,10 +71,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- uses: actions/checkout@v5
|
||||
- uses: ./.github/actions/setup-go
|
||||
|
||||
- name: Setup K3s
|
||||
uses: replicatedhq/action-k3s@main
|
||||
@@ -93,12 +89,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- uses: actions/checkout@v5
|
||||
- uses: ./.github/actions/setup-go
|
||||
- run: make build
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binaries
|
||||
path: bin/
|
||||
@@ -124,7 +118,7 @@ jobs:
|
||||
target: support-bundle-e2e-go-test
|
||||
needs-k3s: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup K3s
|
||||
if: matrix.needs-k3s
|
||||
@@ -132,7 +126,7 @@ jobs:
|
||||
with:
|
||||
version: v1.31.2-k3s1
|
||||
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: binaries
|
||||
path: bin/
|
||||
|
||||
@@ -13,22 +13,44 @@ on:
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
# Build binaries once (shared by all test jobs)
|
||||
build-binaries:
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
regression-test:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 5
|
||||
timeout-minutes: 25
|
||||
|
||||
steps:
|
||||
# 1. SETUP
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-depth: 0 # Fetch all history for git describe to work
|
||||
|
||||
- name: Create output directory
|
||||
run: mkdir -p test/output
|
||||
|
||||
- name: Create k3s cluster
|
||||
id: create-cluster
|
||||
uses: replicatedhq/compatibility-actions/create-cluster@v1
|
||||
with:
|
||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||
kubernetes-distribution: k3s
|
||||
cluster-name: regression-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
ttl: 25m
|
||||
timeout-minutes: 5
|
||||
|
||||
- name: Configure kubeconfig
|
||||
run: |
|
||||
echo "${{ steps.create-cluster.outputs.cluster-kubeconfig }}" > $GITHUB_WORKSPACE/kubeconfig.yaml
|
||||
echo "KUBECONFIG=$GITHUB_WORKSPACE/kubeconfig.yaml" >> $GITHUB_ENV
|
||||
|
||||
- name: Verify cluster access
|
||||
run: kubectl get nodes -o wide
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
cache-dependency-path: go.sum
|
||||
|
||||
- name: Build binaries
|
||||
run: |
|
||||
@@ -37,83 +59,84 @@ jobs:
|
||||
./bin/preflight version
|
||||
./bin/support-bundle version
|
||||
|
||||
- name: Upload binaries
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: binaries-${{ github.run_id }}
|
||||
path: |
|
||||
bin/preflight
|
||||
bin/support-bundle
|
||||
retention-days: 1
|
||||
|
||||
# Preflight v1beta3 test (parallel job 1)
|
||||
test-preflight-v1beta3:
|
||||
needs: [build-binaries]
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Create output directory
|
||||
run: mkdir -p test/output
|
||||
|
||||
- name: Create k3s cluster
|
||||
uses: replicatedhq/action-k3s@main
|
||||
with:
|
||||
version: v1.31.2-k3s1
|
||||
|
||||
- name: Verify cluster access
|
||||
run: kubectl get nodes -o wide
|
||||
|
||||
- name: Wait for all pods to be ready
|
||||
run: |
|
||||
echo "Waiting for all pods to be running..."
|
||||
kubectl get pods --all-namespaces
|
||||
kubectl wait --for=condition=Ready pods --all --all-namespaces --timeout=300s || true
|
||||
kubectl get pods --all-namespaces
|
||||
|
||||
- name: Download binaries
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: binaries-${{ github.run_id }}
|
||||
path: bin/
|
||||
|
||||
- name: Make binaries executable
|
||||
run: |
|
||||
chmod +x bin/preflight bin/support-bundle
|
||||
./bin/preflight version
|
||||
|
||||
- name: Setup Python for comparison
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: pip install pyyaml deepdiff
|
||||
run: |
|
||||
pip install pyyaml deepdiff
|
||||
|
||||
- name: Run preflight v1beta3
|
||||
# 2. EXECUTE SPECS (in parallel)
|
||||
- name: Run all specs in parallel
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "Running preflight v1beta3..."
|
||||
./bin/preflight \
|
||||
examples/preflight/complex-v1beta3.yaml \
|
||||
--values examples/preflight/values-complex-full.yaml \
|
||||
--interactive=false \
|
||||
--format=json \
|
||||
--auto-update=false \
|
||||
--output=test/output/preflight-results-v1beta3.json 2>&1 | tee test/output/v1beta3.log || true
|
||||
echo "Running all 3 specs in parallel..."
|
||||
|
||||
BUNDLE=$(ls -t preflightbundle-*.tar.gz 2>/dev/null | head -1)
|
||||
if [ -n "$BUNDLE" ]; then
|
||||
mv "$BUNDLE" test/output/preflight-v1beta3-bundle.tar.gz
|
||||
echo "✓ v1beta3 bundle saved"
|
||||
fi
|
||||
# Run v1beta3 in background
|
||||
(
|
||||
echo "Starting preflight v1beta3..."
|
||||
./bin/preflight \
|
||||
examples/preflight/complex-v1beta3.yaml \
|
||||
--values examples/preflight/values-complex-full.yaml \
|
||||
--interactive=false \
|
||||
--format=json \
|
||||
--output=test/output/preflight-results-v1beta3.json 2>&1 | tee test/output/v1beta3.log || true
|
||||
|
||||
BUNDLE=$(ls -t preflightbundle-*.tar.gz 2>/dev/null | head -1)
|
||||
if [ -n "$BUNDLE" ]; then
|
||||
mv "$BUNDLE" test/output/preflight-v1beta3-bundle.tar.gz
|
||||
echo "✓ v1beta3 bundle saved"
|
||||
fi
|
||||
) &
|
||||
PID_V1BETA3=$!
|
||||
|
||||
# Run v1beta2 in background
|
||||
(
|
||||
echo "Starting preflight v1beta2..."
|
||||
./bin/preflight \
|
||||
examples/preflight/all-analyzers-v1beta2.yaml \
|
||||
--interactive=false \
|
||||
--format=json \
|
||||
--output=test/output/preflight-results-v1beta2.json 2>&1 | tee test/output/v1beta2.log || true
|
||||
|
||||
BUNDLE=$(ls -t preflightbundle-*.tar.gz 2>/dev/null | head -1)
|
||||
if [ -n "$BUNDLE" ]; then
|
||||
mv "$BUNDLE" test/output/preflight-v1beta2-bundle.tar.gz
|
||||
echo "✓ v1beta2 bundle saved"
|
||||
fi
|
||||
) &
|
||||
PID_V1BETA2=$!
|
||||
|
||||
# Run support bundle in background
|
||||
(
|
||||
echo "Starting support bundle..."
|
||||
./bin/support-bundle \
|
||||
examples/collect/host/all-kubernetes-collectors.yaml \
|
||||
--interactive=false \
|
||||
--output=test/output/supportbundle.tar.gz 2>&1 | tee test/output/supportbundle.log || true
|
||||
|
||||
if [ -f test/output/supportbundle.tar.gz ]; then
|
||||
echo "✓ Support bundle saved"
|
||||
fi
|
||||
) &
|
||||
PID_SUPPORTBUNDLE=$!
|
||||
|
||||
# Wait for all to complete
|
||||
echo "Waiting for all specs to complete..."
|
||||
wait $PID_V1BETA3
|
||||
wait $PID_V1BETA2
|
||||
wait $PID_SUPPORTBUNDLE
|
||||
|
||||
echo "All specs completed!"
|
||||
|
||||
# Verify bundles exist
|
||||
ls -lh test/output/*.tar.gz || echo "Warning: Some bundles may be missing"
|
||||
|
||||
# 3. COMPARE BUNDLES
|
||||
- name: Compare preflight v1beta3 bundle
|
||||
id: compare
|
||||
id: compare-v1beta3
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "Comparing v1beta3 preflight bundle against baseline..."
|
||||
@@ -130,93 +153,8 @@ jobs:
|
||||
--report test/output/diff-report-v1beta3.json \
|
||||
--spec-type preflight
|
||||
|
||||
- name: Upload test artifacts
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: test-results-v1beta3-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
test/output/preflight-v1beta3-bundle.tar.gz
|
||||
test/output/preflight-results-v1beta3.json
|
||||
test/output/diff-report-v1beta3.json
|
||||
test/output/v1beta3.log
|
||||
retention-days: 30
|
||||
|
||||
- name: Set job outcome
|
||||
if: ${{ !cancelled() }}
|
||||
run: |
|
||||
if [ "${{ steps.compare.outcome }}" == "failure" ] && [ "${{ steps.compare.outputs.baseline_missing }}" != "true" ]; then
|
||||
echo "comparison_failed=true" >> $GITHUB_OUTPUT
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Preflight v1beta2 test (parallel job 2)
|
||||
test-preflight-v1beta2:
|
||||
needs: [build-binaries]
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Create output directory
|
||||
run: mkdir -p test/output
|
||||
|
||||
- name: Create k3s cluster
|
||||
uses: replicatedhq/action-k3s@main
|
||||
with:
|
||||
version: v1.31.2-k3s1
|
||||
|
||||
- name: Verify cluster access
|
||||
run: kubectl get nodes -o wide
|
||||
|
||||
- name: Wait for all pods to be ready
|
||||
run: |
|
||||
echo "Waiting for all pods to be running..."
|
||||
kubectl get pods --all-namespaces
|
||||
kubectl wait --for=condition=Ready pods --all --all-namespaces --timeout=300s || true
|
||||
kubectl get pods --all-namespaces
|
||||
|
||||
- name: Download binaries
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: binaries-${{ github.run_id }}
|
||||
path: bin/
|
||||
|
||||
- name: Make binaries executable
|
||||
run: |
|
||||
chmod +x bin/preflight bin/support-bundle
|
||||
./bin/preflight version
|
||||
|
||||
- name: Setup Python for comparison
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: pip install pyyaml deepdiff
|
||||
|
||||
- name: Run preflight v1beta2
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "Running preflight v1beta2..."
|
||||
./bin/preflight \
|
||||
examples/preflight/all-analyzers-v1beta2.yaml \
|
||||
--interactive=false \
|
||||
--format=json \
|
||||
--auto-update=false \
|
||||
--output=test/output/preflight-results-v1beta2.json 2>&1 | tee test/output/v1beta2.log || true
|
||||
|
||||
BUNDLE=$(ls -t preflightbundle-*.tar.gz 2>/dev/null | head -1)
|
||||
if [ -n "$BUNDLE" ]; then
|
||||
mv "$BUNDLE" test/output/preflight-v1beta2-bundle.tar.gz
|
||||
echo "✓ v1beta2 bundle saved"
|
||||
fi
|
||||
|
||||
- name: Compare preflight v1beta2 bundle
|
||||
id: compare
|
||||
id: compare-v1beta2
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "Comparing v1beta2 preflight bundle against baseline..."
|
||||
@@ -233,90 +171,8 @@ jobs:
|
||||
--report test/output/diff-report-v1beta2.json \
|
||||
--spec-type preflight
|
||||
|
||||
- name: Upload test artifacts
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: test-results-v1beta2-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
test/output/preflight-v1beta2-bundle.tar.gz
|
||||
test/output/preflight-results-v1beta2.json
|
||||
test/output/diff-report-v1beta2.json
|
||||
test/output/v1beta2.log
|
||||
retention-days: 30
|
||||
|
||||
- name: Set job outcome
|
||||
if: ${{ !cancelled() }}
|
||||
run: |
|
||||
if [ "${{ steps.compare.outcome }}" == "failure" ] && [ "${{ steps.compare.outputs.baseline_missing }}" != "true" ]; then
|
||||
echo "comparison_failed=true" >> $GITHUB_OUTPUT
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Support bundle test (parallel job 3)
|
||||
test-supportbundle:
|
||||
needs: [build-binaries]
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Create output directory
|
||||
run: mkdir -p test/output
|
||||
|
||||
- name: Create k3s cluster
|
||||
uses: replicatedhq/action-k3s@main
|
||||
with:
|
||||
version: v1.31.2-k3s1
|
||||
|
||||
- name: Verify cluster access
|
||||
run: kubectl get nodes -o wide
|
||||
|
||||
- name: Wait for all pods to be ready
|
||||
run: |
|
||||
echo "Waiting for all pods to be running..."
|
||||
kubectl get pods --all-namespaces
|
||||
kubectl wait --for=condition=Ready pods --all --all-namespaces --timeout=300s || true
|
||||
kubectl get pods --all-namespaces
|
||||
|
||||
- name: Download binaries
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: binaries-${{ github.run_id }}
|
||||
path: bin/
|
||||
|
||||
- name: Make binaries executable
|
||||
run: |
|
||||
chmod +x bin/preflight bin/support-bundle
|
||||
./bin/support-bundle version
|
||||
|
||||
- name: Setup Python for comparison
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: pip install pyyaml deepdiff
|
||||
|
||||
- name: Run support bundle
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "Running support bundle..."
|
||||
./bin/support-bundle \
|
||||
examples/collect/host/all-kubernetes-collectors.yaml \
|
||||
--interactive=false \
|
||||
--auto-update=false \
|
||||
--output=test/output/supportbundle.tar.gz 2>&1 | tee test/output/supportbundle.log || true
|
||||
|
||||
if [ -f test/output/supportbundle.tar.gz ]; then
|
||||
echo "✓ Support bundle saved"
|
||||
fi
|
||||
|
||||
- name: Compare support bundle
|
||||
id: compare
|
||||
id: compare-supportbundle
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "Comparing support bundle against baseline..."
|
||||
@@ -333,116 +189,45 @@ jobs:
|
||||
--report test/output/diff-report-supportbundle.json \
|
||||
--spec-type supportbundle
|
||||
|
||||
# 4. REPORT RESULTS
|
||||
- name: Generate summary report
|
||||
if: always()
|
||||
run: |
|
||||
python3 scripts/generate_summary.py \
|
||||
--reports test/output/diff-report-*.json \
|
||||
--output-file $GITHUB_STEP_SUMMARY \
|
||||
--output-console
|
||||
|
||||
- name: Upload test artifacts
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v6
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-results-supportbundle-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
name: regression-test-results-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
test/output/supportbundle.tar.gz
|
||||
test/output/diff-report-supportbundle.json
|
||||
test/output/supportbundle.log
|
||||
test/output/*.tar.gz
|
||||
test/output/*.json
|
||||
retention-days: 30
|
||||
|
||||
- name: Set job outcome
|
||||
if: ${{ !cancelled() }}
|
||||
run: |
|
||||
if [ "${{ steps.compare.outcome }}" == "failure" ] && [ "${{ steps.compare.outputs.baseline_missing }}" != "true" ]; then
|
||||
echo "comparison_failed=true" >> $GITHUB_OUTPUT
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Report results (runs after all tests complete)
|
||||
report-results:
|
||||
needs: [test-preflight-v1beta3, test-preflight-v1beta2, test-supportbundle]
|
||||
if: ${{ !cancelled() && github.actor != 'dependabot[bot]' }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: pip install pyyaml deepdiff
|
||||
|
||||
- name: Download all test artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: test/output
|
||||
pattern: test-results-*
|
||||
|
||||
- name: Reorganize artifacts
|
||||
run: |
|
||||
# Move artifacts from nested directories to test/output
|
||||
# Use shopt to handle glob patterns that don't match
|
||||
shopt -s nullglob
|
||||
for dir in test/output/test-results-*; do
|
||||
if [ -d "$dir" ]; then
|
||||
find "$dir" -type f -exec mv {} test/output/ \;
|
||||
fi
|
||||
done
|
||||
# Clean up empty directories
|
||||
for dir in test/output/test-results-*; do
|
||||
if [ -d "$dir" ]; then
|
||||
find "$dir" -type d -empty -delete || true
|
||||
fi
|
||||
done
|
||||
shopt -u nullglob
|
||||
|
||||
- name: Generate summary report
|
||||
run: |
|
||||
# Handle case where no reports exist
|
||||
shopt -s nullglob
|
||||
REPORTS=(test/output/diff-report-*.json)
|
||||
shopt -u nullglob
|
||||
|
||||
if [ ${#REPORTS[@]} -eq 0 ]; then
|
||||
echo "⚠ No comparison reports found - test jobs may have failed before generating reports"
|
||||
echo "## Summary Report" >> $GITHUB_STEP_SUMMARY
|
||||
echo "No comparison reports available. Check individual test job results." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
python3 scripts/generate_summary.py \
|
||||
--reports test/output/diff-report-*.json \
|
||||
--output-file $GITHUB_STEP_SUMMARY \
|
||||
--output-console
|
||||
fi
|
||||
|
||||
- name: Check for regressions
|
||||
if: always()
|
||||
run: |
|
||||
echo "Checking comparison results..."
|
||||
|
||||
# Check if any comparisons failed
|
||||
FAILURES=0
|
||||
|
||||
if [ "${{ needs.test-preflight-v1beta3.result }}" == "failure" ] || [ "${{ needs.test-preflight-v1beta3.result }}" == "skipped" ]; then
|
||||
if [ "${{ needs.test-preflight-v1beta3.result }}" == "skipped" ]; then
|
||||
echo "❌ v1beta3 test was skipped (likely due to build failure)"
|
||||
else
|
||||
echo "❌ v1beta3 comparison failed"
|
||||
fi
|
||||
if [ "${{ steps.compare-v1beta3.outcome }}" == "failure" ] && [ "${{ steps.compare-v1beta3.outputs.baseline_missing }}" != "true" ]; then
|
||||
echo "❌ v1beta3 comparison failed"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
|
||||
if [ "${{ needs.test-preflight-v1beta2.result }}" == "failure" ] || [ "${{ needs.test-preflight-v1beta2.result }}" == "skipped" ]; then
|
||||
if [ "${{ needs.test-preflight-v1beta2.result }}" == "skipped" ]; then
|
||||
echo "❌ v1beta2 test was skipped (likely due to build failure)"
|
||||
else
|
||||
echo "❌ v1beta2 comparison failed"
|
||||
fi
|
||||
if [ "${{ steps.compare-v1beta2.outcome }}" == "failure" ] && [ "${{ steps.compare-v1beta2.outputs.baseline_missing }}" != "true" ]; then
|
||||
echo "❌ v1beta2 comparison failed"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
|
||||
if [ "${{ needs.test-supportbundle.result }}" == "failure" ] || [ "${{ needs.test-supportbundle.result }}" == "skipped" ]; then
|
||||
if [ "${{ needs.test-supportbundle.result }}" == "skipped" ]; then
|
||||
echo "❌ Support bundle test was skipped (likely due to build failure)"
|
||||
else
|
||||
echo "❌ Support bundle comparison failed"
|
||||
fi
|
||||
if [ "${{ steps.compare-supportbundle.outcome }}" == "failure" ] && [ "${{ steps.compare-supportbundle.outputs.baseline_missing }}" != "true" ]; then
|
||||
echo "❌ Support bundle comparison failed"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
|
||||
@@ -455,8 +240,9 @@ jobs:
|
||||
echo "✅ All comparisons passed or skipped (no baseline)"
|
||||
fi
|
||||
|
||||
# 5. UPDATE BASELINES (optional, manual trigger only)
|
||||
- name: Update baselines
|
||||
if: ${{ !cancelled() && github.event.inputs.update_baselines == 'true' && github.event_name == 'workflow_dispatch' }}
|
||||
if: github.event.inputs.update_baselines == 'true' && github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
echo "Updating baselines with current bundles..."
|
||||
|
||||
@@ -484,7 +270,7 @@ jobs:
|
||||
{
|
||||
"updated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"git_sha": "${{ github.sha }}",
|
||||
"k8s_version": "v1.31.2-k3s1",
|
||||
"k8s_version": "v1.28.3",
|
||||
"workflow_run": "${{ github.run_id }}"
|
||||
}
|
||||
EOF
|
||||
@@ -495,3 +281,12 @@ jobs:
|
||||
git add test/baselines/
|
||||
git commit -m "chore: update regression test baselines from run ${{ github.run_id }}"
|
||||
git push
|
||||
|
||||
# 6. CLEANUP
|
||||
- name: Remove cluster
|
||||
if: always()
|
||||
uses: replicatedhq/compatibility-actions/remove-cluster@v1
|
||||
continue-on-error: true
|
||||
with:
|
||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||
cluster-id: ${{ steps.create-cluster.outputs.cluster-id }}
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: troubleshoot_release
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
name: Upgrade Go Version
|
||||
|
||||
on:
|
||||
# Run manually when needed
|
||||
workflow_dispatch:
|
||||
# Run weekly on Mondays at 8am UTC
|
||||
schedule:
|
||||
- cron: "0 8 * * MON"
|
||||
|
||||
jobs:
|
||||
upgrade-go:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check for Go updates
|
||||
uses: StefMa/Upgrade-Go-Action@v1
|
||||
with:
|
||||
base-branch: 'main'
|
||||
gh-token: ${{ secrets.TROUBLESHOOT_GH_PAT }}
|
||||
+4
-4
@@ -50,9 +50,9 @@ sbom/
|
||||
!testdata/supportbundle/*.tar.gz
|
||||
!test/baselines/**/baseline.tar.gz
|
||||
|
||||
# Ignore built binaries (use / prefix to avoid catching source files)
|
||||
/troubleshoot
|
||||
/troubleshoot-test
|
||||
# Ignore built binaries
|
||||
troubleshoot
|
||||
troubleshoot-test
|
||||
cmd/troubleshoot/troubleshoot
|
||||
cmd/*/troubleshoot
|
||||
/support-bundle
|
||||
support-bundle
|
||||
@@ -38,17 +38,13 @@ BUILDTAGS = "netgo containers_image_ostree_stub exclude_graphdriver_devicemapper
|
||||
BUILDFLAGS = -tags ${BUILDTAGS} -installsuffix netgo
|
||||
BUILDPATHS = ./pkg/... ./cmd/... ./internal/...
|
||||
E2EPATHS = ./test/e2e/...
|
||||
TESTFLAGS ?= -v -coverprofile cover.out
|
||||
TESTFLAGS ?=
|
||||
|
||||
.DEFAULT_GOAL := all
|
||||
all: clean build test
|
||||
|
||||
.PHONY: ffi
|
||||
ffi: fmt vet
|
||||
go build ${BUILDFLAGS} ${LDFLAGS} -o bin/troubleshoot.so -buildmode=c-shared ffi/main.go
|
||||
|
||||
.PHONY: test
|
||||
test: generate fmt vet
|
||||
test: fmt vet
|
||||
if [ -n $(RUN) ]; then \
|
||||
go test ${BUILDFLAGS} ${BUILDPATHS} ${TESTFLAGS} -run $(RUN); \
|
||||
else \
|
||||
@@ -58,17 +54,13 @@ test: generate fmt vet
|
||||
# Go tests that require a K8s instance
|
||||
# TODOLATER: merge with test, so we get unified coverage reports? it'll add 21~sec to the test job though...
|
||||
.PHONY: test-integration
|
||||
test-integration: generate fmt vet
|
||||
test-integration: fmt vet
|
||||
go test -v --tags="integration exclude_graphdriver_devicemapper exclude_graphdriver_btrfs" ${BUILDPATHS}
|
||||
|
||||
.PHONY: preflight-e2e-test
|
||||
preflight-e2e-test:
|
||||
./test/validate-preflight-e2e.sh
|
||||
|
||||
.PHONY: run-examples
|
||||
run-examples:
|
||||
./test/run-examples.sh
|
||||
|
||||
.PHONY: support-bundle-e2e-test
|
||||
support-bundle-e2e-test:
|
||||
./test/validate-support-bundle-e2e.sh
|
||||
@@ -83,31 +75,19 @@ support-bundle-e2e-go-test:
|
||||
|
||||
rebuild: clean build
|
||||
|
||||
# Build all binaries in parallel ( -j )
|
||||
build: tidy
|
||||
@echo "Build cli binaries"
|
||||
$(MAKE) -j bin/support-bundle bin/preflight
|
||||
$(MAKE) bin/support-bundle bin/preflight
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
@rm -f bin/analyze
|
||||
@rm -f bin/support-bundle
|
||||
@rm -f bin/collect
|
||||
@rm -f bin/preflight
|
||||
@rm -f bin/troubleshoot.h
|
||||
@rm -f bin/troubleshoot.so
|
||||
@rm -f bin/schemagen
|
||||
@rm -f bin/docsgen
|
||||
|
||||
.PHONY: tidy
|
||||
tidy:
|
||||
go mod tidy
|
||||
|
||||
# Prints the diff of the changes that would be made by `go mod tidy`. Used in CI
|
||||
.PHONY: tidy-diff
|
||||
tidy-diff:
|
||||
go mod tidy -diff
|
||||
|
||||
# Only build when any of the files in SOURCES changes, or if bin/<file> is absent
|
||||
MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
SOURCES := $(shell find $(MAKEFILE_DIR) -type f \( -name "*.go" -o -name "go.mod" -o -name "go.sum" \))
|
||||
@@ -117,28 +97,12 @@ bin/support-bundle: $(SOURCES)
|
||||
bin/preflight: $(SOURCES)
|
||||
go build ${BUILDFLAGS} ${LDFLAGS} -o bin/preflight github.com/replicatedhq/troubleshoot/cmd/preflight
|
||||
|
||||
bin/analyze: $(SOURCES)
|
||||
go build ${BUILDFLAGS} ${LDFLAGS} -o bin/analyze github.com/replicatedhq/troubleshoot/cmd/analyze
|
||||
|
||||
bin/collect: $(SOURCES)
|
||||
go build ${BUILDFLAGS} ${LDFLAGS} -o bin/collect github.com/replicatedhq/troubleshoot/cmd/collect
|
||||
|
||||
.PHONY: support-bundle
|
||||
support-bundle: bin/support-bundle
|
||||
|
||||
.PHONY: preflight
|
||||
preflight: bin/preflight
|
||||
|
||||
.PHONY: analyze
|
||||
analyze: bin/analyze
|
||||
|
||||
.PHONY: collect
|
||||
collect: bin/collect
|
||||
|
||||
build-linux: tidy
|
||||
@echo "Build cli binaries for Linux"
|
||||
GOOS=linux GOARCH=amd64 $(MAKE) -j bin/support-bundle bin/preflight bin/analyze bin/collect
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
go fmt ${BUILDPATHS}
|
||||
@@ -146,153 +110,3 @@ fmt:
|
||||
.PHONY: vet
|
||||
vet:
|
||||
go vet ${BUILDFLAGS} ${BUILDPATHS}
|
||||
|
||||
.PHONY: generate
|
||||
generate: controller-gen client-gen
|
||||
$(CONTROLLER_GEN) \
|
||||
object:headerFile=./hack/boilerplate.go.txt paths=./pkg/apis/...
|
||||
$(CLIENT_GEN) \
|
||||
--output-dir=. \
|
||||
--output-pkg=github.com/replicatedhq/troubleshoot/pkg/client \
|
||||
--clientset-name troubleshootclientset \
|
||||
--input-base github.com/replicatedhq/troubleshoot/pkg/apis \
|
||||
--input troubleshoot/v1beta1 \
|
||||
--input troubleshoot/v1beta2 \
|
||||
--go-header-file ./hack/boilerplate.go.txt
|
||||
cp -r troubleshootclientset pkg/client
|
||||
rm -rf troubleshootclientset
|
||||
|
||||
.PHONY: openapischema
|
||||
openapischema: controller-gen
|
||||
controller-gen crd +output:dir=./config/crds paths=./pkg/apis/troubleshoot/v1beta1
|
||||
controller-gen crd +output:dir=./config/crds paths=./pkg/apis/troubleshoot/v1beta2
|
||||
|
||||
check-schemas: generate schemas
|
||||
@if [ -n "$$(git status --short)" ]; then \
|
||||
echo -e "\033[31mThe git repo is dirty :( Ensure all generated files are committed e.g CRD schema files\033[0;m"; \
|
||||
git status --short; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: schemas
|
||||
schemas: openapischema bin/schemagen
|
||||
./bin/schemagen --output-dir ./schemas
|
||||
|
||||
bin/schemagen:
|
||||
go build ${LDFLAGS} -o bin/schemagen github.com/replicatedhq/troubleshoot/cmd/schemagen
|
||||
|
||||
.PHONY: docs
|
||||
docs: fmt vet bin/docsgen
|
||||
./bin/docsgen
|
||||
|
||||
bin/docsgen:
|
||||
go build ${LDFLAGS} -o bin/docsgen github.com/replicatedhq/troubleshoot/cmd/docsgen
|
||||
|
||||
controller-gen:
|
||||
go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.19.0
|
||||
CONTROLLER_GEN=$(shell which controller-gen)
|
||||
|
||||
.PHONY: client-gen
|
||||
client-gen:
|
||||
go install k8s.io/code-generator/cmd/client-gen@v0.34.0
|
||||
CLIENT_GEN=$(shell which client-gen)
|
||||
|
||||
.PHONY: release
|
||||
release: export GITHUB_TOKEN = $(shell echo ${GITHUB_TOKEN_TROUBLESHOOT})
|
||||
release:
|
||||
curl -sL https://git.io/goreleaser | bash -s -- --rm-dist --config deploy/.goreleaser.yml
|
||||
|
||||
.PHONY: snapshot-release
|
||||
snapshot-release:
|
||||
curl -sL https://git.io/goreleaser | bash -s -- --rm-dist --snapshot --config deploy/.goreleaser.snapshot.yml
|
||||
docker push replicated/troubleshoot:alpha
|
||||
docker push replicated/preflight:alpha
|
||||
|
||||
.PHONY: local-release
|
||||
local-release:
|
||||
curl -sL https://git.io/goreleaser | bash -s -- --rm-dist --snapshot --config deploy/.goreleaser.yaml
|
||||
docker tag replicated/troubleshoot:alpha localhost:32000/troubleshoot:alpha
|
||||
docker tag replicated/preflight:alpha localhost:32000/preflight:alpha
|
||||
docker push localhost:32000/troubleshoot:alpha
|
||||
docker push localhost:32000/preflight:alpha
|
||||
|
||||
.PHONY: run-preflight
|
||||
run-preflight: bin/preflight
|
||||
./bin/preflight ./examples/preflight/sample-preflight.yaml
|
||||
|
||||
.PHONY: run-support-bundle
|
||||
run-support-bundle: bin/support-bundle
|
||||
./bin/support-bundle ./examples/support-bundle/sample-supportbundle.yaml
|
||||
|
||||
.PHONY: run-analyze
|
||||
run-analyze: bin/analyze
|
||||
./bin/analyze --analyzers ./examples/support-bundle/sample-analyzers.yaml ./support-bundle.tar.gz
|
||||
|
||||
.PHONY: init-sbom
|
||||
init-sbom:
|
||||
mkdir -p sbom/spdx sbom/assets
|
||||
|
||||
.PHONY: install-spdx-sbom-generator
|
||||
install-spdx-sbom-generator: init-sbom
|
||||
./scripts/initialize-sbom-build.sh
|
||||
|
||||
SPDX_GENERATOR=./sbom/spdx-sbom-generator
|
||||
|
||||
.PHONY: generate-sbom
|
||||
generate-sbom: install-spdx-sbom-generator
|
||||
$(SPDX_GENERATOR) -o ./sbom/spdx
|
||||
|
||||
sbom/assets/troubleshoot-sbom.tgz: generate-sbom
|
||||
tar -czf sbom/assets/troubleshoot-sbom.tgz sbom/spdx/*.spdx
|
||||
|
||||
sbom: sbom/assets/troubleshoot-sbom.tgz
|
||||
cosign sign-blob \
|
||||
--key ./cosign.key \
|
||||
--tlog-upload \
|
||||
--yes \
|
||||
--rekor-url=https://rekor.sigstore.dev \
|
||||
sbom/assets/troubleshoot-sbom.tgz > sbom/assets/troubleshoot-sbom.tgz.sig
|
||||
cosign public-key --key cosign.key --outfile sbom/assets/key.pub
|
||||
|
||||
.PHONY: scan
|
||||
scan:
|
||||
trivy fs \
|
||||
--scanners vuln \
|
||||
--exit-code=1 \
|
||||
--severity="HIGH,CRITICAL" \
|
||||
--ignore-unfixed \
|
||||
./
|
||||
|
||||
.PHONY: watch
|
||||
watch: npm-install
|
||||
bin/watch.js
|
||||
|
||||
## Syncronize the code with a remote server. More info: CONTRIBUTING.md
|
||||
.PHONY: watchrsync
|
||||
watchrsync: npm-install
|
||||
bin/watchrsync.js
|
||||
|
||||
.PHONY: npm-install
|
||||
npm-install:
|
||||
npm --version 2>&1 >/dev/null || ( echo "npm not installed; install npm to set up watchrsync" && exit 1 )
|
||||
npm list gaze-run-interrupt || npm install install --no-save gaze-run-interrupt@~2.0.0
|
||||
|
||||
|
||||
######## Lagacy make targets ###########
|
||||
# Deprecated: These can be removed
|
||||
.PHONY: run-troubleshoot
|
||||
run-troubleshoot: run-support-bundle
|
||||
|
||||
longhorn:
|
||||
git clone https://github.com/longhorn/longhorn-manager.git
|
||||
cd longhorn-manager && git checkout v1.2.2 && cd ..
|
||||
rm -rf pkg/longhorn
|
||||
mv longhorn-manager/k8s/pkg pkg/longhorn
|
||||
mv longhorn-manager/types pkg/longhorn/types
|
||||
mv longhorn-manager/util pkg/longhorn/util
|
||||
rm -rf pkg/longhorn/util/daemon
|
||||
rm -rf pkg/longhorn/util/server
|
||||
find pkg/longhorn -type f | xargs sed -i "s/github.com\/longhorn\/longhorn-manager\/k8s\/pkg/github.com\/replicatedhq\/troubleshoot\/pkg\/longhorn/g"
|
||||
find pkg/longhorn -type f | xargs sed -i "s/github.com\/longhorn\/longhorn-manager\/types/github.com\/replicatedhq\/troubleshoot\/pkg\/longhorn\/types/g"
|
||||
find pkg/longhorn -type f | xargs sed -i "s/github.com\/longhorn\/longhorn-manager\/util/github.com\/replicatedhq\/troubleshoot\/pkg\/longhorn\/util/g"
|
||||
rm -rf longhorn-manager
|
||||
@@ -1,13 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
analyzecli "github.com/replicatedhq/troubleshoot/cmd/analyze/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := analyzecli.RootCmd().Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@ func extractDocs(templateFiles []string, valuesFiles []string, setValues []strin
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to load values file %s", valuesFile)
|
||||
}
|
||||
values = preflight.MergeMaps(values, fileValues)
|
||||
values = mergeMaps(values, fileValues)
|
||||
}
|
||||
|
||||
// Normalize maps for Helm set merging
|
||||
@@ -331,6 +331,25 @@ func setNestedValue(m map[string]interface{}, keys []string, value interface{})
|
||||
}
|
||||
}
|
||||
|
||||
func mergeMaps(base, overlay map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
for k, v := range base {
|
||||
result[k] = v
|
||||
}
|
||||
for k, v := range overlay {
|
||||
if baseVal, exists := result[k]; exists {
|
||||
if baseMap, ok := baseVal.(map[string]interface{}); ok {
|
||||
if overlayMap, ok := v.(map[string]interface{}); ok {
|
||||
result[k] = mergeMaps(baseMap, overlayMap)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func renderTemplate(templateContent string, values map[string]interface{}) (string, error) {
|
||||
tmpl := template.New("preflight").Funcs(sprig.FuncMap())
|
||||
tmpl, err := tmpl.Parse(templateContent)
|
||||
|
||||
+17
-19
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/lint"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/types"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
@@ -16,17 +15,24 @@ func LintCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "lint [spec-files...]",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Short: "Lint v1beta2/v1beta3 preflight specs for syntax and structural errors",
|
||||
Long: `Lint v1beta2/v1beta3 preflight specs for syntax and structural errors.
|
||||
Short: "Lint preflight specs for syntax and structural errors",
|
||||
Long: `Lint preflight specs for syntax and structural errors.
|
||||
|
||||
This command validates v1beta2/v1beta3 preflight specs and checks for:
|
||||
- YAML syntax errors
|
||||
This command validates troubleshoot specs and checks for:
|
||||
- YAML syntax errors (missing colons, invalid structure)
|
||||
- Missing required fields (apiVersion, kind, metadata, spec)
|
||||
- Invalid template syntax ({{ .Values.* }})
|
||||
- Invalid template syntax ({{ .Values.* }}, {{ .Release.* }}, etc.)
|
||||
- Missing analyzers or collectors
|
||||
- Common structural issues
|
||||
- Missing docStrings (warning)
|
||||
|
||||
Both v1beta2 and v1beta3 apiVersions are supported. Use 'convert' if you need a full structural conversion between schema versions.
|
||||
|
||||
The --fix flag can automatically repair:
|
||||
- Missing colons in YAML (e.g., "metadata" → "metadata:")
|
||||
- Missing or malformed apiVersion line. If templating ({{ }}) or docString fields are detected, apiVersion is set to v1beta3; otherwise v1beta2.
|
||||
- Template expressions missing leading dot (e.g., "{{ Values.x }}" → "{{ .Values.x }}")
|
||||
|
||||
Examples:
|
||||
# Lint a single spec file
|
||||
preflight lint my-preflight.yaml
|
||||
@@ -34,16 +40,12 @@ Examples:
|
||||
# Lint multiple spec files
|
||||
preflight lint spec1.yaml spec2.yaml spec3.yaml
|
||||
|
||||
# Lint with automatic fixes
|
||||
# Lint with automatic fixes (may need to run multiple times for complex issues)
|
||||
preflight lint --fix my-preflight.yaml
|
||||
|
||||
# Lint and output as JSON for CI/CD integration
|
||||
preflight lint --format json my-preflight.yaml
|
||||
|
||||
Notes:
|
||||
- v1beta2 does not support templating; template syntax in v1beta2 files will be flagged as errors.
|
||||
- v1beta3 supports templating and is linted with template-awareness.
|
||||
|
||||
Exit codes:
|
||||
0 - No errors found
|
||||
2 - Validation errors found`,
|
||||
@@ -54,11 +56,9 @@ Exit codes:
|
||||
v := viper.GetViper()
|
||||
|
||||
opts := lint.LintOptions{
|
||||
FilePaths: args,
|
||||
Fix: v.GetBool("fix"),
|
||||
Format: v.GetString("format"),
|
||||
ValuesFiles: v.GetStringSlice("values"),
|
||||
SetValues: v.GetStringSlice("set"),
|
||||
FilePaths: args,
|
||||
Fix: v.GetBool("fix"),
|
||||
Format: v.GetString("format"),
|
||||
}
|
||||
|
||||
return runLint(opts)
|
||||
@@ -67,8 +67,6 @@ Exit codes:
|
||||
|
||||
cmd.Flags().Bool("fix", false, "Automatically fix issues where possible")
|
||||
cmd.Flags().String("format", "text", "Output format: text or json")
|
||||
cmd.Flags().StringSlice("values", []string{}, "Path to YAML files with template values (required for v1beta3 specs)")
|
||||
cmd.Flags().StringSlice("set", []string{}, "Set template values via command line (e.g., --set key=value)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -93,7 +91,7 @@ func runLint(opts lint.LintOptions) error {
|
||||
|
||||
// Return appropriate exit code
|
||||
if lint.HasErrors(results) {
|
||||
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, nil)
|
||||
os.Exit(constants.EXIT_CODE_SPEC_ISSUES)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/lint"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/types"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func LintCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "lint [spec-files...]",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Short: "Lint v1beta2/v1beta3 troubleshoot specs for syntax and structural errors",
|
||||
Long: `Lint v1beta2/v1beta3 troubleshoot specs (both preflight and support-bundle) for syntax and structural errors.
|
||||
|
||||
This command validates v1beta2/v1beta3 troubleshoot specs and checks for:
|
||||
- YAML syntax errors
|
||||
- Missing required fields (apiVersion, kind, metadata, spec)
|
||||
- Invalid template syntax ({{ .Values.* }})
|
||||
- Missing collectors or hostCollectors
|
||||
- Common structural issues
|
||||
- Missing docStrings (warning)
|
||||
|
||||
Examples:
|
||||
# Lint a single spec file
|
||||
support-bundle lint my-spec.yaml
|
||||
|
||||
# Lint multiple spec files
|
||||
support-bundle lint spec1.yaml spec2.yaml spec3.yaml
|
||||
|
||||
# Lint with automatic fixes
|
||||
support-bundle lint --fix my-spec.yaml
|
||||
|
||||
# Lint and output as JSON for CI/CD integration
|
||||
support-bundle lint --format json my-spec.yaml
|
||||
|
||||
Notes:
|
||||
- v1beta2 does not support templating; template syntax in v1beta2 files will be flagged as errors.
|
||||
- v1beta3 supports templating and is linted with template-awareness.
|
||||
|
||||
Exit codes:
|
||||
0 - No errors found
|
||||
2 - Validation errors found`,
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
viper.BindPFlags(cmd.Flags())
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
v := viper.GetViper()
|
||||
|
||||
opts := lint.LintOptions{
|
||||
FilePaths: args,
|
||||
Fix: v.GetBool("fix"),
|
||||
Format: v.GetString("format"),
|
||||
ValuesFiles: v.GetStringSlice("values"),
|
||||
SetValues: v.GetStringSlice("set"),
|
||||
}
|
||||
|
||||
return runLint(opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Bool("fix", false, "Automatically fix issues where possible")
|
||||
cmd.Flags().String("format", "text", "Output format: text or json")
|
||||
cmd.Flags().StringSlice("values", []string{}, "Path to YAML files with template values (required for v1beta3 specs)")
|
||||
cmd.Flags().StringSlice("set", []string{}, "Set template values via command line (e.g., --set key=value)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runLint(opts lint.LintOptions) error {
|
||||
// Validate file paths exist
|
||||
for _, filePath := range opts.FilePaths {
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
return errors.Wrapf(err, "file not found: %s", filePath)
|
||||
}
|
||||
}
|
||||
|
||||
// Run linting
|
||||
results, err := lint.LintFiles(opts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to lint files")
|
||||
}
|
||||
|
||||
// Format and print results
|
||||
output := lint.FormatResults(results, opts.Format)
|
||||
fmt.Print(output)
|
||||
|
||||
// Return appropriate exit code
|
||||
if lint.HasErrors(results) {
|
||||
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -5,14 +5,11 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"errors"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/cmd/internal/util"
|
||||
preflightcli "github.com/replicatedhq/troubleshoot/cmd/preflight/cli"
|
||||
"github.com/replicatedhq/troubleshoot/internal/traces"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/logger"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/types"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/updater"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
@@ -112,8 +109,8 @@ If no arguments are provided, specs are automatically loaded from the cluster by
|
||||
cmd.AddCommand(Diff())
|
||||
cmd.AddCommand(Schedule())
|
||||
cmd.AddCommand(UploadCmd())
|
||||
cmd.AddCommand(LintCmd())
|
||||
cmd.AddCommand(util.VersionCmd())
|
||||
cmd.AddCommand(preflightcli.LintCmd())
|
||||
|
||||
cmd.Flags().StringSlice("redactors", []string{}, "names of the additional redactors to use")
|
||||
cmd.Flags().Bool("redact", true, "enable/disable default redactions")
|
||||
@@ -137,12 +134,6 @@ If no arguments are provided, specs are automatically loaded from the cluster by
|
||||
cmd.Flags().Bool("dry-run", false, "print support bundle spec without collecting anything")
|
||||
cmd.Flags().Bool("auto-update", true, "enable automatic binary self-update check and install")
|
||||
|
||||
// Upload flags
|
||||
cmd.Flags().Bool("auto-upload", false, "automatically upload resulting bundle to replicated.app")
|
||||
cmd.Flags().String("license-id", "", "license ID for authentication when uploading (auto-detected from bundle if not provided)")
|
||||
cmd.Flags().String("app-slug", "", "application slug when uploading (auto-detected from bundle if not provided)")
|
||||
cmd.Flags().String("upload-domain", "", "custom domain for upload (default: replicated.app)")
|
||||
|
||||
// Auto-discovery flags
|
||||
cmd.Flags().Bool("auto", false, "enable auto-discovery of foundational collectors. When used with YAML specs, adds foundational collectors to YAML collectors. When used alone, collects only foundational data")
|
||||
cmd.Flags().Bool("include-images", false, "include container image metadata collection when using auto-discovery")
|
||||
@@ -172,16 +163,7 @@ If no arguments are provided, specs are automatically loaded from the cluster by
|
||||
}
|
||||
|
||||
func InitAndExecute() {
|
||||
cmd := RootCmd()
|
||||
if err := cmd.Execute(); err != nil {
|
||||
var exitErr types.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
if exitErr.ExitStatus() != constants.EXIT_CODE_FAIL && exitErr.ExitStatus() != constants.EXIT_CODE_WARN {
|
||||
cmd.PrintErrln("Error:", err.Error())
|
||||
}
|
||||
os.Exit(exitErr.ExitStatus())
|
||||
}
|
||||
cmd.PrintErrln("Error:", err.Error())
|
||||
if err := RootCmd().Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,26 +242,6 @@ func runTroubleshoot(v *viper.Viper, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt auto-upload before any early returns
|
||||
if v.GetBool("auto-upload") && !response.FileUploaded {
|
||||
licenseID := v.GetString("license-id")
|
||||
appSlug := v.GetString("app-slug")
|
||||
uploadDomain := v.GetString("upload-domain")
|
||||
|
||||
targetDomain := uploadDomain
|
||||
if targetDomain == "" {
|
||||
targetDomain = "replicated.app"
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "Auto-uploading bundle to %s...\n", targetDomain)
|
||||
if err := supportbundle.UploadBundleAutoDetect(response.ArchivePath, licenseID, appSlug, uploadDomain); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Auto-upload failed: %v\n", err)
|
||||
fmt.Fprintf(os.Stderr, "You can manually upload the bundle using: support-bundle upload %s\n", response.ArchivePath)
|
||||
} else {
|
||||
response.FileUploaded = true
|
||||
}
|
||||
}
|
||||
|
||||
if !response.FileUploaded {
|
||||
if appName := mainBundle.Labels["applicationName"]; appName != "" {
|
||||
f := `A support bundle for %s has been created in this directory
|
||||
@@ -289,12 +269,11 @@ the %s Admin Console to begin analysis.`
|
||||
fmt.Printf("\r%s\r", cursor.ClearEntireLine())
|
||||
}
|
||||
if response.FileUploaded {
|
||||
fmt.Printf("A support bundle has been created and uploaded to replicated.app for analysis.\n")
|
||||
fmt.Printf("A support bundle has been created and uploaded to your cluster for analysis. Please visit the Troubleshoot page to continue.\n")
|
||||
fmt.Printf("A copy of this support bundle was written to the current directory, named %q\n", response.ArchivePath)
|
||||
} else {
|
||||
fmt.Printf("A support bundle has been created in the current directory named %q\n", response.ArchivePath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -518,7 +497,7 @@ func (a *analysisOutput) FormattedAnalysisOutput() (outputJson string, err error
|
||||
|
||||
formatted, err := json.MarshalIndent(o, "", " ")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("\r * Failed to format analysis: %v", err)
|
||||
return "", fmt.Errorf("\r * Failed to format analysis: %v\n", err)
|
||||
}
|
||||
return string(formatted), nil
|
||||
}
|
||||
|
||||
@@ -26,10 +26,7 @@ Examples:
|
||||
support-bundle upload bundle.tar.gz --license-id YOUR_LICENSE_ID
|
||||
|
||||
# Specify both license and app
|
||||
support-bundle upload bundle.tar.gz --license-id YOUR_LICENSE_ID --app-slug my-app
|
||||
|
||||
# Upload to a custom domain (e.g., development environment)
|
||||
support-bundle upload bundle.tar.gz --upload-domain replicated-app-dev.example.com`,
|
||||
support-bundle upload bundle.tar.gz --license-id YOUR_LICENSE_ID --app-slug my-app`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
v := viper.GetViper()
|
||||
bundlePath := args[0]
|
||||
@@ -42,10 +39,9 @@ Examples:
|
||||
// Get upload parameters
|
||||
licenseID := v.GetString("license-id")
|
||||
appSlug := v.GetString("app-slug")
|
||||
uploadDomain := v.GetString("upload-domain")
|
||||
|
||||
// Use auto-detection for uploads
|
||||
if err := supportbundle.UploadBundleAutoDetect(bundlePath, licenseID, appSlug, uploadDomain); err != nil {
|
||||
if err := supportbundle.UploadBundleAutoDetect(bundlePath, licenseID, appSlug); err != nil {
|
||||
return errors.Wrap(err, "upload failed")
|
||||
}
|
||||
|
||||
@@ -55,7 +51,6 @@ Examples:
|
||||
|
||||
cmd.Flags().String("license-id", "", "license ID for authentication (auto-detected from bundle if not provided)")
|
||||
cmd.Flags().String("app-slug", "", "application slug (auto-detected from bundle if not provided)")
|
||||
cmd.Flags().String("upload-domain", "", "custom domain for upload (default: replicated.app)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ builds:
|
||||
- id: preflight
|
||||
main: ./cmd/preflight/main.go
|
||||
env: [CGO_ENABLED=0]
|
||||
goos: [linux, darwin, windows]
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm, arm64]
|
||||
ignore:
|
||||
- goos: windows
|
||||
@@ -31,7 +31,7 @@ builds:
|
||||
- id: support-bundle
|
||||
main: ./cmd/troubleshoot/main.go
|
||||
env: [CGO_ENABLED=0]
|
||||
goos: [linux, darwin, windows]
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm, arm64]
|
||||
ignore:
|
||||
- goos: windows
|
||||
|
||||
@@ -42,7 +42,7 @@ spec:
|
||||
matchLabels:
|
||||
os: darwin
|
||||
arch: amd64
|
||||
{{addURIAndSha "https://github.com/replicatedhq/troubleshoot/releases/download/{{ .TagName }}/preflight_darwin_all.tar.gz" .TagName }}
|
||||
{{addURIAndSha "https://github.com/replicatedhq/troubleshoot/releases/download/{{ .TagName }}/preflight_darwin_amd64.tar.gz" .TagName }}
|
||||
files:
|
||||
- from: preflight
|
||||
to: .
|
||||
@@ -53,7 +53,7 @@ spec:
|
||||
matchLabels:
|
||||
os: darwin
|
||||
arch: arm64
|
||||
{{addURIAndSha "https://github.com/replicatedhq/troubleshoot/releases/download/{{ .TagName }}/preflight_darwin_all.tar.gz" .TagName }}
|
||||
{{addURIAndSha "https://github.com/replicatedhq/troubleshoot/releases/download/{{ .TagName }}/preflight_darwin_arm64.tar.gz" .TagName }}
|
||||
files:
|
||||
- from: preflight
|
||||
to: .
|
||||
|
||||
@@ -42,7 +42,7 @@ spec:
|
||||
matchLabels:
|
||||
os: darwin
|
||||
arch: amd64
|
||||
{{addURIAndSha "https://github.com/replicatedhq/troubleshoot/releases/download/{{ .TagName }}/support-bundle_darwin_all.tar.gz" .TagName }}
|
||||
{{addURIAndSha "https://github.com/replicatedhq/troubleshoot/releases/download/{{ .TagName }}/support-bundle_darwin_amd64.tar.gz" .TagName }}
|
||||
files:
|
||||
- from: support-bundle
|
||||
to: .
|
||||
@@ -53,7 +53,7 @@ spec:
|
||||
matchLabels:
|
||||
os: darwin
|
||||
arch: arm64
|
||||
{{addURIAndSha "https://github.com/replicatedhq/troubleshoot/releases/download/{{ .TagName }}/support-bundle_darwin_all.tar.gz" .TagName }}
|
||||
{{addURIAndSha "https://github.com/replicatedhq/troubleshoot/releases/download/{{ .TagName }}/support-bundle_darwin_arm64.tar.gz" .TagName }}
|
||||
files:
|
||||
- from: support-bundle
|
||||
to: .
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: test-v1beta3-secretref
|
||||
spec:
|
||||
collectors:
|
||||
# Test 1: PostgreSQL with URI from secret
|
||||
- postgres:
|
||||
collectorName: postgres-with-secret
|
||||
uri:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: test-database-credentials
|
||||
key: postgres-uri
|
||||
# This will fail to connect (fake server) but that's OK -
|
||||
# we're testing secret resolution, not actual DB connectivity
|
||||
|
||||
# Test 2: PostgreSQL with TLS certs from secret
|
||||
- postgres:
|
||||
collectorName: postgres-with-tls
|
||||
uri:
|
||||
value: "postgresql://testuser:testpass@localhost:5432/testdb"
|
||||
tls:
|
||||
cacert:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: test-database-credentials
|
||||
key: ca.crt
|
||||
clientCert:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: test-database-credentials
|
||||
key: client.crt
|
||||
clientKey:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: test-database-credentials
|
||||
key: client.key
|
||||
|
||||
# Test 3: MySQL with URI from secret
|
||||
- mysql:
|
||||
collectorName: mysql-with-secret
|
||||
uri:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: test-database-credentials
|
||||
key: mysql-uri
|
||||
|
||||
# Test 4: Redis with URI from secret
|
||||
- redis:
|
||||
collectorName: redis-with-secret
|
||||
uri:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: test-database-credentials
|
||||
key: redis-uri
|
||||
|
||||
# Test 5: Literal value (no secret) for comparison
|
||||
- clusterInfo: {}
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
# Secret containing database credentials
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: test-database-credentials
|
||||
namespace: default
|
||||
type: Opaque
|
||||
stringData:
|
||||
# PostgreSQL connection URI
|
||||
postgres-uri: "postgresql://testuser:supersecret@postgres.example.com:5432/testdb?sslmode=require"
|
||||
|
||||
# MySQL connection URI
|
||||
mysql-uri: "mysql://testuser:supersecret@mysql.example.com:3306/testdb"
|
||||
|
||||
# Redis connection URI
|
||||
redis-uri: "redis://:supersecret@redis.example.com:6379"
|
||||
|
||||
# TLS certificates (example data)
|
||||
ca.crt: |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICpDCCAYwCCQDU+pQ3ZUD30jANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls
|
||||
b2NhbGhvc3QwHhcNMjQwMTAxMDAwMDAwWhcNMjUwMTAxMDAwMDAwWjAUMRIwEAYD
|
||||
VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7
|
||||
VJTUt9Us8cKjMzEfYyjiWA4R4/M2bS1+fWIcPm15A8IgC0qC1J3xGhE=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
client.crt: |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICpDCCAYwCCQDU+pQ3ZUD30jANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls
|
||||
b2NhbGhvc3QwHhcNMjQwMTAxMDAwMDAwWhcNMjUwMTAxMDAwMDAwWjAUMRIwEAYD
|
||||
VQQDDA5jbGllbnQtY2VydA==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
client.key: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7VJTUt9Us8cKj
|
||||
MzEfYyjiWA4R4/M2bS1+fWIcPm15A8IgC0qC1J3xGhE=
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -92,7 +92,7 @@ spec:
|
||||
- nodeResources:
|
||||
checkName: Must have 1 node with 2Gi (available) memory and at least 2 cores (on a single node)
|
||||
filters:
|
||||
memoryAllocatable: 2Gi
|
||||
allocatableMemory: 2Gi
|
||||
cpuCapacity: "2"
|
||||
outcomes:
|
||||
- pass:
|
||||
|
||||
@@ -28,7 +28,7 @@ spec:
|
||||
- nodeResources:
|
||||
checkName: Must have 1 node with 16 GB (available) memory and 10 cores (on a single node)
|
||||
filters:
|
||||
memoryAllocatable: 16Gi
|
||||
allocatableMemory: 16Gi
|
||||
cpuCapacity: "10"
|
||||
outcomes:
|
||||
- fail:
|
||||
@@ -39,7 +39,7 @@ spec:
|
||||
- nodeResources:
|
||||
checkName: Must have 1 node with 16 GB (available) memory and 4 cores of amd64 arch (on a single node)
|
||||
filters:
|
||||
memoryAllocatable: 16Gi
|
||||
allocatableMemory: 16Gi
|
||||
cpuArchitecture: amd64
|
||||
cpuCapacity: "4"
|
||||
outcomes:
|
||||
@@ -54,7 +54,7 @@ spec:
|
||||
selector:
|
||||
matchLabel:
|
||||
node-role.kubernetes.io/master: ""
|
||||
memoryAllocatable: 16Gi
|
||||
allocatableMemory: 16Gi
|
||||
cpuArchitecture: amd64
|
||||
cpuCapacity: "6"
|
||||
outcomes:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module helm-template
|
||||
|
||||
go 1.25.5
|
||||
go 1.24.6
|
||||
|
||||
// Always use the local version of troubleshoot so as to build using
|
||||
// the latest version of the library. This will ensure the example
|
||||
@@ -9,17 +9,17 @@ replace github.com/replicatedhq/troubleshoot v0.0.0 => ../../../
|
||||
|
||||
require (
|
||||
github.com/replicatedhq/troubleshoot v0.0.0
|
||||
helm.sh/helm/v3 v3.19.4
|
||||
helm.sh/helm/v3 v3.19.0
|
||||
sigs.k8s.io/yaml v1.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
@@ -28,6 +28,7 @@ require (
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
@@ -45,29 +46,28 @@ require (
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/oauth2 v0.33.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/term v0.38.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/net v0.44.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/term v0.35.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/api v0.35.0 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.35.0 // indirect
|
||||
k8s.io/apimachinery v0.35.0 // indirect
|
||||
k8s.io/client-go v0.35.0 // indirect
|
||||
k8s.io/api v0.34.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.34.1 // indirect
|
||||
k8s.io/apimachinery v0.34.1 // indirect
|
||||
k8s.io/client-go v0.34.1 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
||||
sigs.k8s.io/controller-runtime v0.22.4 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
|
||||
sigs.k8s.io/controller-runtime v0.22.1 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
)
|
||||
|
||||
@@ -2,16 +2,16 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
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.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/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
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=
|
||||
@@ -36,6 +36,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
@@ -43,8 +45,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
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-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
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/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
@@ -53,6 +55,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
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/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -71,17 +75,17 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
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/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||
github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
|
||||
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||
github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
|
||||
github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
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/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
@@ -98,63 +102,86 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
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.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
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-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.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
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/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.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
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.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
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.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
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=
|
||||
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/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
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/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/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
helm.sh/helm/v3 v3.19.4 h1:E2yFBejmZBczWr5LblhjZbvAOAwVumfBO1AtN3nqI30=
|
||||
helm.sh/helm/v3 v3.19.4/go.mod h1:PC1rk7PqacpkV4acUFMLStOOis7QM9Jq3DveHBInu4s=
|
||||
k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY=
|
||||
k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA=
|
||||
k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4=
|
||||
k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU=
|
||||
k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8=
|
||||
k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
|
||||
k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE=
|
||||
k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o=
|
||||
helm.sh/helm/v3 v3.19.0 h1:krVyCGa8fa/wzTZgqw0DUiXuRT5BPdeqE/sQXujQ22k=
|
||||
helm.sh/helm/v3 v3.19.0/go.mod h1:Lk/SfzN0w3a3C3o+TdAKrLwJ0wcZ//t1/SDXAvfgDdc=
|
||||
k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
|
||||
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
|
||||
k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI=
|
||||
k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc=
|
||||
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
|
||||
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
|
||||
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
|
||||
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-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
|
||||
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
|
||||
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=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg=
|
||||
sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY=
|
||||
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=
|
||||
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/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
|
||||
|
||||
@@ -77,7 +77,7 @@ spec:
|
||||
- nodeResources:
|
||||
checkName: Must have 1 node with 2Gi (available) memory and at least 2 cores (on a single node)
|
||||
filters:
|
||||
memoryAllocatable: 2Gi
|
||||
allocatableMemory: 2Gi
|
||||
cpuCapacity: "2"
|
||||
outcomes:
|
||||
- pass:
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
# v1beta3 Support Bundle Examples
|
||||
|
||||
This directory contains example Support Bundle specs using the v1beta3 API, which introduces `StringOrValueFrom` support for securely referencing Kubernetes Secrets and ConfigMaps in collector fields.
|
||||
|
||||
## Features
|
||||
|
||||
### StringOrValueFrom Pattern
|
||||
|
||||
The v1beta3 API introduces a Kubernetes-native pattern for referencing sensitive values:
|
||||
|
||||
```yaml
|
||||
uri:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: my-secret
|
||||
key: connection-uri
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```yaml
|
||||
uri: "postgresql://localhost:5432/db" # Literal value
|
||||
```
|
||||
|
||||
### Supported Collectors
|
||||
|
||||
Currently, v1beta3 supports `StringOrValueFrom` for:
|
||||
|
||||
- **Database collectors**: `postgres`, `mysql`, `redis`, `mssql`
|
||||
- `uri` field - Connection strings from secrets
|
||||
- `tls` fields - CA cert, client cert, and client key from secrets
|
||||
|
||||
## Examples
|
||||
|
||||
### 1. postgres-with-secret.yaml
|
||||
Basic PostgreSQL collector with connection URI from a secret.
|
||||
|
||||
**Use case**: Securely store database credentials without hardcoding them in the spec.
|
||||
|
||||
```bash
|
||||
kubectl apply -f postgres-with-secret.yaml
|
||||
```
|
||||
|
||||
### 2. postgres-with-tls.yaml
|
||||
PostgreSQL with TLS configuration from secrets.
|
||||
|
||||
**Use case**: Secure database connections with mutual TLS, storing certificates in secrets.
|
||||
|
||||
```bash
|
||||
kubectl apply -f postgres-with-tls.yaml
|
||||
```
|
||||
|
||||
### 3. multiple-databases.yaml
|
||||
Multiple database collectors (PostgreSQL, MySQL, Redis, MSSQL) with various configurations.
|
||||
|
||||
**Use case**: Collect diagnostics from multiple databases in your application stack.
|
||||
|
||||
```bash
|
||||
kubectl apply -f multiple-databases.yaml
|
||||
```
|
||||
|
||||
### 4. cross-namespace-secrets.yaml
|
||||
Accessing secrets from different namespaces.
|
||||
|
||||
**Use case**: Centralized credential management in a shared namespace.
|
||||
|
||||
```bash
|
||||
kubectl apply -f cross-namespace-secrets.yaml
|
||||
```
|
||||
|
||||
**RBAC Requirements**: The support bundle service account needs `get` permission on secrets in the referenced namespaces.
|
||||
|
||||
### 5. optional-secrets.yaml
|
||||
Using the `optional` field for graceful degradation.
|
||||
|
||||
**Use case**: Collect diagnostics even when some credentials are unavailable (e.g., optional secondary databases).
|
||||
|
||||
```bash
|
||||
kubectl apply -f optional-secrets.yaml
|
||||
```
|
||||
|
||||
### 6. configmap-example.yaml
|
||||
Using ConfigMaps for non-sensitive configuration.
|
||||
|
||||
**Use case**: Store non-sensitive connection strings (e.g., development databases) in ConfigMaps.
|
||||
|
||||
```bash
|
||||
kubectl apply -f configmap-example.yaml
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Secret vs ConfigMap
|
||||
|
||||
- **Secrets**: Use for sensitive data (passwords, tokens, certificates)
|
||||
- **ConfigMaps**: Use for non-sensitive configuration (development endpoints, feature flags)
|
||||
|
||||
### Optional Field
|
||||
|
||||
```yaml
|
||||
uri:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: my-secret
|
||||
key: uri
|
||||
optional: true # Returns empty string if secret/key doesn't exist
|
||||
```
|
||||
|
||||
- `optional: false` (default): Collection fails if secret is missing
|
||||
- `optional: true`: Returns empty string if secret/key is missing
|
||||
|
||||
### Cross-Namespace Access
|
||||
|
||||
```yaml
|
||||
uri:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: shared-secret
|
||||
key: uri
|
||||
namespace: other-namespace # Access secrets in different namespaces
|
||||
```
|
||||
|
||||
If `namespace` is not specified, uses the support bundle's namespace.
|
||||
|
||||
### Backward Compatibility
|
||||
|
||||
v1beta3 maintains backward compatibility with v1beta2 TLS configuration:
|
||||
|
||||
```yaml
|
||||
tls:
|
||||
secret: # v1beta2 style
|
||||
name: tls-secret
|
||||
namespace: default
|
||||
```
|
||||
|
||||
## RBAC Configuration
|
||||
|
||||
Support bundles need appropriate RBAC permissions to read secrets:
|
||||
|
||||
```yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: troubleshoot-secret-reader
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
resourceNames: ["postgres-connection", "redis-creds"] # Restrict to specific secrets
|
||||
verbs: ["get"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: troubleshoot-secret-reader-binding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: troubleshoot-secret-reader
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: troubleshoot
|
||||
namespace: default
|
||||
```
|
||||
|
||||
## Migration from v1beta2
|
||||
|
||||
### Before (v1beta2):
|
||||
```yaml
|
||||
apiVersion: troubleshoot.sh/v1beta2
|
||||
kind: SupportBundle
|
||||
spec:
|
||||
collectors:
|
||||
- postgres:
|
||||
uri: "postgresql://user:password@host:5432/db" # Hardcoded
|
||||
```
|
||||
|
||||
### After (v1beta3):
|
||||
```yaml
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: SupportBundle
|
||||
spec:
|
||||
collectors:
|
||||
- postgres:
|
||||
uri:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-connection
|
||||
key: connection-uri
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
1. **No value composition**: Cannot combine multiple secrets into a single value
|
||||
```yaml
|
||||
# NOT SUPPORTED
|
||||
uri: "postgresql://$(USERNAME):$(PASSWORD)@host:5432/db"
|
||||
```
|
||||
Store the complete connection string in a single secret key.
|
||||
|
||||
2. **Collector scope**: Only database collectors support `StringOrValueFrom` initially
|
||||
- Future versions will extend to HTTP, Data, and other collectors
|
||||
|
||||
3. **No templating**: The entire field value comes from one source
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Use resourceNames in RBAC**: Restrict access to specific secrets
|
||||
2. **Separate secrets**: Don't reuse secrets across applications
|
||||
3. **Rotate credentials**: Update secrets regularly
|
||||
4. **Audit access**: Monitor secret access logs
|
||||
5. **Redact output**: Ensure connection strings are redacted in bundle output
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: "failed to get secret default/my-secret"
|
||||
|
||||
- **Cause**: Secret doesn't exist or RBAC denied access
|
||||
- **Solution**: Verify secret exists: `kubectl get secret my-secret`
|
||||
- **Solution**: Check RBAC: `kubectl auth can-i get secret/my-secret`
|
||||
|
||||
### Error: "key 'uri' not found in secret"
|
||||
|
||||
- **Cause**: Secret exists but doesn't contain the specified key
|
||||
- **Solution**: Check secret keys: `kubectl get secret my-secret -o jsonpath='{.data}'`
|
||||
|
||||
### Error: "cannot specify both 'value' and 'valueFrom'"
|
||||
|
||||
- **Cause**: Both literal value and secret reference provided
|
||||
- **Solution**: Use only one: either `value: "string"` or `valueFrom: {...}`
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Troubleshoot Documentation](https://troubleshoot.sh)
|
||||
- [v1beta3 API Reference](https://troubleshoot.sh/docs/v1beta3/)
|
||||
- [Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/)
|
||||
- [RBAC Authorization](https://kubernetes.io/docs/reference/access-authn-authz/rbac/)
|
||||
@@ -1,57 +0,0 @@
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: configmap-example
|
||||
spec:
|
||||
collectors:
|
||||
# Database URI from ConfigMap (non-sensitive connection string)
|
||||
- postgres:
|
||||
collectorName: dev-database
|
||||
uri:
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: database-config
|
||||
key: dev-connection-uri
|
||||
|
||||
# Redis URI from ConfigMap
|
||||
- redis:
|
||||
collectorName: dev-redis
|
||||
uri:
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: cache-config
|
||||
key: redis-uri
|
||||
|
||||
# Mixed: URI from ConfigMap, password from Secret
|
||||
# Note: This shows the limitation - you can't compose values from multiple sources
|
||||
# The full connection string must be in one place
|
||||
- mysql:
|
||||
collectorName: staging-mysql
|
||||
uri:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mysql-secret
|
||||
key: complete-connection-string
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: database-config
|
||||
data:
|
||||
dev-connection-uri: "postgresql://devuser@dev-postgres.default.svc:5432/devdb"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: cache-config
|
||||
data:
|
||||
redis-uri: "redis://dev-redis.default.svc:6379/0"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: mysql-secret
|
||||
type: Opaque
|
||||
stringData:
|
||||
# Complete connection string with password included
|
||||
complete-connection-string: "mysql://staging:stagingpass@staging-mysql:3306/stagingdb"
|
||||
@@ -1,45 +0,0 @@
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: cross-namespace-example
|
||||
spec:
|
||||
collectors:
|
||||
# Database in one namespace, secret in another
|
||||
- postgres:
|
||||
collectorName: shared-database
|
||||
uri:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: shared-postgres-connection
|
||||
key: uri
|
||||
namespace: shared-services # Secret is in a different namespace
|
||||
|
||||
# Redis accessing centralized credentials
|
||||
- redis:
|
||||
collectorName: shared-cache
|
||||
uri:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: shared-redis-creds
|
||||
key: uri
|
||||
namespace: platform-credentials
|
||||
---
|
||||
# This secret would be in the 'shared-services' namespace
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: shared-postgres-connection
|
||||
namespace: shared-services
|
||||
type: Opaque
|
||||
stringData:
|
||||
uri: "postgresql://shared:password@shared-postgres.shared-services.svc:5432/shared_db"
|
||||
---
|
||||
# This secret would be in the 'platform-credentials' namespace
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: shared-redis-creds
|
||||
namespace: platform-credentials
|
||||
type: Opaque
|
||||
stringData:
|
||||
uri: "redis://shared-redis.shared-services.svc:6379/0"
|
||||
@@ -1,78 +0,0 @@
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: multi-database-support-bundle
|
||||
spec:
|
||||
collectors:
|
||||
# PostgreSQL with secret reference
|
||||
- postgres:
|
||||
collectorName: primary-db
|
||||
uri:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-primary
|
||||
key: connection-uri
|
||||
|
||||
# PostgreSQL replica with secret reference
|
||||
- postgres:
|
||||
collectorName: replica-db
|
||||
uri:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-replica
|
||||
key: connection-uri
|
||||
|
||||
# Redis cache with secret reference
|
||||
- redis:
|
||||
collectorName: cache
|
||||
uri:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: redis-creds
|
||||
key: uri
|
||||
|
||||
# MySQL with literal value (for development/testing)
|
||||
- mysql:
|
||||
collectorName: local-mysql
|
||||
uri: "mysql://root:password@localhost:3306/testdb"
|
||||
|
||||
# MSSQL with secret reference
|
||||
- mssql:
|
||||
collectorName: legacy-db
|
||||
uri:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mssql-connection
|
||||
key: dsn
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: postgres-primary
|
||||
type: Opaque
|
||||
stringData:
|
||||
connection-uri: "postgresql://app:secret123@postgres-primary.default.svc:5432/appdb"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: postgres-replica
|
||||
type: Opaque
|
||||
stringData:
|
||||
connection-uri: "postgresql://app:secret123@postgres-replica.default.svc:5432/appdb"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: redis-creds
|
||||
type: Opaque
|
||||
stringData:
|
||||
uri: "redis://:cachesecret@redis.default.svc:6379/0"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: mssql-connection
|
||||
type: Opaque
|
||||
stringData:
|
||||
dsn: "sqlserver://sa:Str0ngP@ssw0rd@mssql.default.svc:1433?database=legacy"
|
||||
@@ -1,58 +0,0 @@
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: optional-secrets-example
|
||||
spec:
|
||||
collectors:
|
||||
# Required database - collection will fail if secret doesn't exist
|
||||
- postgres:
|
||||
collectorName: required-db
|
||||
uri:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: required-postgres
|
||||
key: uri
|
||||
optional: false # Default behavior - secret must exist
|
||||
|
||||
# Optional database - collection continues if secret doesn't exist
|
||||
- postgres:
|
||||
collectorName: optional-db
|
||||
uri:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: optional-postgres
|
||||
key: uri
|
||||
optional: true # Gracefully degrades if secret is missing
|
||||
|
||||
# Mixed required and optional TLS
|
||||
- postgres:
|
||||
collectorName: partially-optional
|
||||
uri: "postgresql://localhost:5432/db"
|
||||
tls:
|
||||
cacert:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tls-certs
|
||||
key: ca.crt
|
||||
optional: false # CA cert is required
|
||||
clientCert:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tls-certs
|
||||
key: client.crt
|
||||
optional: true # Client cert is optional (cert-only TLS)
|
||||
clientKey:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tls-certs
|
||||
key: client.key
|
||||
optional: true # Client key is optional
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: required-postgres
|
||||
type: Opaque
|
||||
stringData:
|
||||
uri: "postgresql://user:pass@required-postgres:5432/db"
|
||||
# Note: optional-postgres secret intentionally not created
|
||||
@@ -1,21 +0,0 @@
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: postgres-support-bundle
|
||||
spec:
|
||||
collectors:
|
||||
- postgres:
|
||||
collectorName: main-database
|
||||
uri:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-connection
|
||||
key: connection-uri
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: postgres-connection
|
||||
type: Opaque
|
||||
stringData:
|
||||
connection-uri: "postgresql://myuser:mypassword@postgres.default.svc:5432/mydb?sslmode=require"
|
||||
@@ -1,47 +0,0 @@
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: postgres-tls-support-bundle
|
||||
spec:
|
||||
collectors:
|
||||
- postgres:
|
||||
collectorName: secure-database
|
||||
uri:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-connection
|
||||
key: connection-uri
|
||||
tls:
|
||||
cacert:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-tls
|
||||
key: ca.crt
|
||||
clientCert:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-tls
|
||||
key: tls.crt
|
||||
clientKey:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-tls
|
||||
key: tls.key
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: postgres-connection
|
||||
type: Opaque
|
||||
stringData:
|
||||
connection-uri: "postgresql://myuser:mypassword@postgres.default.svc:5432/mydb?sslmode=verify-full"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: postgres-tls
|
||||
type: kubernetes.io/tls
|
||||
data:
|
||||
ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi4uLgotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t
|
||||
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi4uLgotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t
|
||||
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi4uLgotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t
|
||||
@@ -1,19 +0,0 @@
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: invalid-collectors
|
||||
spec:
|
||||
collectors:
|
||||
# Unknown collector type
|
||||
- notACollector: {}
|
||||
# Known collector but missing required fields (e.g., ceph requires namespace)
|
||||
- ceph: {}
|
||||
# Field exists but wrong type (should be a list)
|
||||
hostCollectors: "not-a-list"
|
||||
analyzers:
|
||||
# Unknown analyzer type
|
||||
- notAnAnalyzer: {}
|
||||
# Known analyzer missing required 'outcomes'
|
||||
- cephStatus:
|
||||
namespace: default
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
# Empty values file for v1beta3 specs without templates
|
||||
{}
|
||||
@@ -1 +0,0 @@
|
||||
minVersion: "1.19.0"
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/replicatedhq/troubleshoot
|
||||
|
||||
go 1.25.5
|
||||
go 1.24.6
|
||||
|
||||
require (
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
@@ -8,105 +8,105 @@ require (
|
||||
github.com/apparentlymart/go-cidr v1.1.0
|
||||
github.com/blang/semver/v4 v4.0.0
|
||||
github.com/casbin/govaluate v1.10.0
|
||||
github.com/cilium/ebpf v0.20.0
|
||||
github.com/containerd/cgroups/v3 v3.1.2
|
||||
github.com/cilium/ebpf v0.19.0
|
||||
github.com/containerd/cgroups/v3 v3.0.5
|
||||
github.com/containers/image/v5 v5.36.2
|
||||
github.com/distribution/distribution/v3 v3.0.0
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/go-logr/logr v1.4.3
|
||||
github.com/go-redis/redis/v7 v7.4.1
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/gobwas/glob v0.2.3
|
||||
github.com/godbus/dbus/v5 v5.2.2
|
||||
github.com/godbus/dbus/v5 v5.1.0
|
||||
github.com/google/gofuzz v1.2.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/handlers v1.5.2
|
||||
github.com/hashicorp/go-getter v1.8.4
|
||||
github.com/hashicorp/go-getter v1.8.0
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/jackc/pgx/v5 v5.8.0
|
||||
github.com/jackc/pgx/v5 v5.7.6
|
||||
github.com/longhorn/go-iscsi-helper v0.0.0-20210330030558-49a327fb024e
|
||||
github.com/manifoldco/promptui v0.9.0
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/microsoft/go-mssqldb v1.9.5
|
||||
github.com/miekg/dns v1.1.70
|
||||
github.com/microsoft/go-mssqldb v1.9.3
|
||||
github.com/miekg/dns v1.1.68
|
||||
github.com/opencontainers/image-spec v1.1.1
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/replicatedhq/termui/v3 v3.1.1-0.20200811145416-f40076d26851
|
||||
github.com/segmentio/ksuid v1.0.4
|
||||
github.com/shirou/gopsutil/v4 v4.25.12
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/shirou/gopsutil/v4 v4.25.8
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tj/go-spin v1.1.0
|
||||
github.com/vishvananda/netlink v1.3.1
|
||||
github.com/vishvananda/netns v0.0.5
|
||||
github.com/vmware-tanzu/velero v1.17.1
|
||||
go.opentelemetry.io/otel v1.39.0
|
||||
go.opentelemetry.io/otel/sdk v1.39.0
|
||||
go.podman.io/image/v5 v5.38.0
|
||||
github.com/vmware-tanzu/velero v1.17.0
|
||||
go.opentelemetry.io/otel v1.38.0
|
||||
go.opentelemetry.io/otel/sdk v1.38.0
|
||||
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67
|
||||
golang.org/x/mod v0.32.0
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/mod v0.28.0
|
||||
golang.org/x/sync v0.17.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
k8s.io/api v0.35.0
|
||||
k8s.io/apiextensions-apiserver v0.35.0
|
||||
k8s.io/apimachinery v0.35.0
|
||||
k8s.io/apiserver v0.35.0
|
||||
k8s.io/cli-runtime v0.35.0
|
||||
k8s.io/client-go v0.35.0
|
||||
k8s.io/api v0.34.1
|
||||
k8s.io/apiextensions-apiserver v0.34.1
|
||||
k8s.io/apimachinery v0.34.1
|
||||
k8s.io/apiserver v0.34.1
|
||||
k8s.io/cli-runtime v0.34.1
|
||||
k8s.io/client-go v0.34.1
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
oras.land/oras-go/v2 v2.6.0
|
||||
sigs.k8s.io/controller-runtime v0.22.4
|
||||
k8s.io/kubernetes v1.34.1
|
||||
oras.land/oras-go v1.2.6
|
||||
sigs.k8s.io/controller-runtime v0.22.1
|
||||
sigs.k8s.io/e2e-framework v0.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
cloud.google.com/go/auth v0.17.0 // indirect
|
||||
cloud.google.com/go/auth v0.16.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/compute/metadata v0.7.0 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.2 // indirect
|
||||
cyphar.com/go-pathrs v0.2.1 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.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/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect
|
||||
github.com/MakeNowJust/heredoc v1.0.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
|
||||
github.com/aws/smithy-go v1.24.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.68 // 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.2 // 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/s3 v1.80.1 // 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.20 // indirect
|
||||
github.com/aws/smithy-go v1.22.3 // indirect
|
||||
github.com/chai2010/gettext-go v1.0.2 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/containerd/typeurl/v2 v2.2.3 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
|
||||
@@ -114,18 +114,18 @@ require (
|
||||
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/go-containerregistry v0.20.6 // indirect
|
||||
github.com/google/go-containerregistry v0.20.3 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||
github.com/gosuri/uitable v0.0.4 // indirect
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 // indirect
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.65 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
@@ -134,7 +134,7 @@ require (
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/mistifyio/go-zfs/v4 v4.0.0 // indirect
|
||||
github.com/mistifyio/go-zfs/v3 v3.0.1 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
@@ -148,55 +148,61 @@ require (
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4-0.20251023124752-b61f268f75b6 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/sylabs/sif/v2 v2.22.0 // indirect
|
||||
github.com/sylabs/sif/v2 v2.21.1 // indirect
|
||||
github.com/tchap/go-patricia/v2 v2.3.3 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/vladimirvivien/gexe v0.4.1 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/zeebo/errs v1.4.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
k8s.io/component-base v0.35.0 // indirect
|
||||
k8s.io/kubectl v0.34.2 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
k8s.io/component-base v0.34.1 // indirect
|
||||
k8s.io/kubectl v0.34.0 // indirect
|
||||
oras.land/oras-go/v2 v2.6.0 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
cloud.google.com/go/iam v1.5.3 // indirect
|
||||
cloud.google.com/go/storage v1.58.0 // indirect
|
||||
cloud.google.com/go v0.121.1 // indirect
|
||||
cloud.google.com/go/iam v1.5.2 // indirect
|
||||
cloud.google.com/go/storage v1.55.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/Microsoft/hcsshim v0.13.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
|
||||
github.com/c9s/goprocinfo v0.0.0-20170724085704-0010a05ce49f // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/containerd/containerd v1.7.29 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect
|
||||
github.com/containerd/containerd v1.7.28 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
|
||||
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
|
||||
github.com/containers/ocicrypt v1.2.1 // indirect
|
||||
github.com/containers/storage v1.59.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/docker/docker v28.5.1+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.4 // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/cli v28.3.2+incompatible // indirect
|
||||
github.com/docker/docker v28.3.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
@@ -206,19 +212,22 @@ require (
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/go-intervals v0.0.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // 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-version v1.8.0
|
||||
github.com/hashicorp/go-safetemp v1.0.0 // indirect
|
||||
github.com/hashicorp/go-version v1.7.0
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
@@ -226,6 +235,7 @@ require (
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1
|
||||
github.com/moby/locker v1.0.1 // indirect
|
||||
github.com/moby/spdystream v0.5.0 // indirect
|
||||
github.com/moby/sys/mountinfo v0.7.2 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
@@ -235,45 +245,45 @@ require (
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.3.0
|
||||
github.com/opencontainers/selinux v1.13.1 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.2.1
|
||||
github.com/opencontainers/selinux v1.12.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_golang v1.22.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/vbatts/tar-split v0.12.2 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/vbatts/tar-split v0.12.1 // indirect
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.podman.io/storage v1.61.1-0.20260113143440-ec033ce69f13 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/net v0.49.0
|
||||
golang.org/x/oauth2 v0.33.0 // indirect
|
||||
golang.org/x/sys v0.40.0
|
||||
golang.org/x/term v0.39.0 // indirect
|
||||
golang.org/x/text v0.33.0
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/api v0.256.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 // indirect
|
||||
google.golang.org/grpc v1.76.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/net v0.44.0
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sys v0.36.0
|
||||
golang.org/x/term v0.35.0 // indirect
|
||||
golang.org/x/text v0.29.0
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
google.golang.org/api v0.241.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect
|
||||
google.golang.org/grpc v1.73.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
helm.sh/helm/v3 v3.19.4
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
|
||||
k8s.io/kubelet v0.35.0
|
||||
k8s.io/metrics v0.35.0
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
|
||||
helm.sh/helm/v3 v3.19.0
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
|
||||
k8s.io/kubelet v0.34.1
|
||||
k8s.io/metrics v0.34.1
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
|
||||
periph.io/x/host/v3 v3.8.5
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.20.1 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
|
||||
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
||||
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.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
|
||||
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.121.1 h1:S3kTQSydxmu1JfLRLpKtxRPA7rSrYPRPEUmL/PavVUw=
|
||||
cloud.google.com/go v0.121.1/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw=
|
||||
cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4=
|
||||
cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA=
|
||||
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/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
|
||||
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
|
||||
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
|
||||
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
|
||||
cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc=
|
||||
cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA=
|
||||
cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=
|
||||
cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
|
||||
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
|
||||
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
|
||||
cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=
|
||||
cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
|
||||
cloud.google.com/go/storage v1.58.0 h1:PflFXlmFJjG/nBeR9B7pKddLQWaFaRWx4uUi/LyNxxo=
|
||||
cloud.google.com/go/storage v1.58.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI=
|
||||
cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0=
|
||||
cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY=
|
||||
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
|
||||
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
|
||||
cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8=
|
||||
cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
@@ -40,20 +39,21 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuo
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=
|
||||
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/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
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/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.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/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
|
||||
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/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0=
|
||||
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=
|
||||
@@ -66,52 +66,56 @@ github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8
|
||||
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||
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.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA=
|
||||
github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok=
|
||||
github.com/ahmetalpbalkan/go-cursor v0.0.0-20131010032410-8136607ea412 h1:vOVO0ypMfTt6tZacyI0kp+iCZb1XSNiYDqnzBWYgfe4=
|
||||
github.com/ahmetalpbalkan/go-cursor v0.0.0-20131010032410-8136607ea412/go.mod h1:AI9hp1tkp10pAlK5TCwL+7yWbRgtDm9jhToq6qij2xs=
|
||||
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/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU=
|
||||
github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=
|
||||
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/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
|
||||
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
||||
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
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.15 h1:I5XjesVMpDZXZEZonVfjI12VNMrYa38LtLnw4NtY5Ss=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.15/go.mod h1:tNIp4JIPonlsgaO5hxO372a6gjhN63aSWl2GVl5QoBQ=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.68 h1:cFb9yjI02/sWHBSYXAtkamjzCuRymvmeFmt0TC0MbYY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.68/go.mod h1:H6E+jBzyqUu8u0vGaU6POkK3P0NylYEeRZ6ynBpMqIk=
|
||||
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/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/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.2 h1:BCG7DCXEXpNCcpwCxg1oi9pkJWH2+eZzTn9MY56MbVw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.2/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/s3 v1.80.1 h1:xYEAf/6QHiTZDccKnPMbsMwlau13GsDsTgdue3wmHGw=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.80.1/go.mod h1:qbn305Je/IofWBJ4bJz/Q7pDEtnnoInw/dGt71v6rHE=
|
||||
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.20 h1:oIaQ1e17CSKaWmUTu62MtraRWVIosn/iONMuZt0gbqc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.20/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/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/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
|
||||
@@ -126,6 +130,7 @@ github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaD
|
||||
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
|
||||
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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
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=
|
||||
@@ -139,14 +144,18 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/cilium/ebpf v0.20.0 h1:atwWj9d3NffHyPZzVlx3hmw1on5CLe9eljR8VuHTwhM=
|
||||
github.com/cilium/ebpf v0.20.0/go.mod h1:pzLjFymM+uZPLk/IXZUL63xdx5VXEo+enTzxkZXdycw=
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||
github.com/containerd/cgroups/v3 v3.1.2 h1:OSosXMtkhI6Qove637tg1XgK4q+DhR0mX8Wi8EhrHa4=
|
||||
github.com/containerd/cgroups/v3 v3.1.2/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw=
|
||||
github.com/containerd/containerd v1.7.29 h1:90fWABQsaN9mJhGkoVnuzEY+o1XDPbg9BTC9QTAHnuE=
|
||||
github.com/containerd/containerd v1.7.29/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs=
|
||||
github.com/cilium/ebpf v0.19.0 h1:Ro/rE64RmFBeA9FGjcTc+KmCeY6jXmryu6FfnzPRIao=
|
||||
github.com/cilium/ebpf v0.19.0/go.mod h1:fLCgMo3l8tZmAdM3B2XqdFzXBpwkcSTroaVqN08OWVY=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k=
|
||||
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||
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.28 h1:Nsgm1AtcmEh4AHAJ4gGlNSaKgXiNccU270Dnf81FQ3c=
|
||||
github.com/containerd/containerd v1.7.28/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs=
|
||||
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 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=
|
||||
@@ -155,19 +164,25 @@ 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.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
|
||||
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/containers/image/v5 v5.36.2 h1:GcxYQyAHRF/pLqR4p4RpvKllnNL8mOBn0eZnqJbfTwk=
|
||||
github.com/containers/image/v5 v5.36.2/go.mod h1:b4GMKH2z/5t6/09utbse2ZiLK/c72GuGLFdp7K69eA4=
|
||||
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA=
|
||||
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY=
|
||||
github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpVhSmM=
|
||||
github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ=
|
||||
github.com/containers/storage v1.59.1 h1:11Zu68MXsEQGBBd+GadPrHPpWeqjKS8hJDGiAHgIqDs=
|
||||
github.com/containers/storage v1.59.1/go.mod h1:KoAYHnAjP3/cTsRS+mmWZGkufSY2GACiKQ4V3ZLQnR0=
|
||||
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/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
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/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
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=
|
||||
@@ -180,32 +195,38 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
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 v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY=
|
||||
github.com/docker/cli v28.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v28.3.2+incompatible h1:mOt9fcLE7zaACbxW1GeS65RI67wIJrTnqS3hP2huFsY=
|
||||
github.com/docker/cli v28.3.2+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.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
|
||||
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.9.4 h1:76ItO69/AP/V4yT9V4uuuItG0B1N8hvt0T0c0NN/DzI=
|
||||
github.com/docker/docker-credential-helpers v0.9.4/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
||||
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/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
|
||||
github.com/docker/docker v28.3.3+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.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
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/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
|
||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
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/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.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/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/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8=
|
||||
@@ -231,8 +252,11 @@ github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxI
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
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.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
|
||||
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
|
||||
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.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -255,6 +279,7 @@ github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRf
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
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-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
@@ -262,27 +287,49 @@ github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlnd
|
||||
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.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
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/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/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||
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.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=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
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/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.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU=
|
||||
github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y=
|
||||
github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI=
|
||||
github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI=
|
||||
github.com/google/go-intervals v0.0.2 h1:FGrVEiUnTRKR8yE04qzXYaJMtnIYqobR5QbblK3ixcM=
|
||||
github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@@ -290,17 +337,18 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
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-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
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/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.2.0/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/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
|
||||
github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
|
||||
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=
|
||||
@@ -313,19 +361,21 @@ github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJr
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 h1:0HADrxxqaQkGycO1JoUUA+B4FnIkuo8d2bz/hSaTFFQ=
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70/go.mod h1:fm2FdDCzJdtbXF7WKAMvBb5NEPouXPHFbGNYs9ShFns=
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.65 h1:81+kWbE1yErFBMjME0I5k3x3kojjKsWtPYHEAutoPow=
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.65/go.mod h1:WtMzv9T++tfWVea+qB2MXoaqxw33S8bpJslzUike2mQ=
|
||||
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=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-getter v1.8.4 h1:hGEd2xsuVKgwkMtPVufq73fAmZU/x65PPcqH3cb0D9A=
|
||||
github.com/hashicorp/go-getter v1.8.4/go.mod h1:x27pPGSg9kzoB147QXI8d/nDvp2IgYGcwuRjpaXE9Yg=
|
||||
github.com/hashicorp/go-getter v1.8.0 h1:GMRdoMBDz12Mim366pWsRVIrrkugJ19rrmykkv0Nhzo=
|
||||
github.com/hashicorp/go-getter v1.8.0/go.mod h1:/K0O5zR6R72O3r2x3z2UHadiC0XHMbbzHO9pS8ZeJPA=
|
||||
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-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
|
||||
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo=
|
||||
github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=
|
||||
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/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw=
|
||||
github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4=
|
||||
@@ -340,8 +390,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
||||
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.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
|
||||
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
|
||||
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
|
||||
github.com/jackc/pgx/v5 v5.7.6/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/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||
@@ -352,13 +402,19 @@ github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtL
|
||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
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/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
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/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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
@@ -393,18 +449,19 @@ github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp
|
||||
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/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
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/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
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/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
||||
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||
github.com/microsoft/go-mssqldb v1.9.5 h1:orwya0X/5bsL1o+KasupTkk2eNTNFkTQG0BEe/HxCn0=
|
||||
github.com/microsoft/go-mssqldb v1.9.5/go.mod h1:VCP2a0KEZZtGLRHd1PsLavLFYy/3xX2yJUPycv3Sr2Q=
|
||||
github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
|
||||
github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/mistifyio/go-zfs/v4 v4.0.0 h1:sU0+5dX45tdDK5xNZ3HBi95nxUc48FS92qbIZEvpAg4=
|
||||
github.com/mistifyio/go-zfs/v4 v4.0.0/go.mod h1:weotFtXTHvBwhr9Mv96KYnDkTPBOHFUbm9cBmQpesL0=
|
||||
github.com/microsoft/go-mssqldb v1.9.3 h1:hy4p+LDC8LIGvI3JATnLVmBOLMJbmn5X400mr5j0lPs=
|
||||
github.com/microsoft/go-mssqldb v1.9.3/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLYN3iFL90lQ+PA=
|
||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/mistifyio/go-zfs/v3 v3.0.1 h1:YaoXgBePoMA12+S1u/ddkv+QqxcfiZK4prI6HPnkFiU=
|
||||
github.com/mistifyio/go-zfs/v3 v3.0.1/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
@@ -416,6 +473,8 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
|
||||
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/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/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
@@ -424,15 +483,19 @@ github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCnd
|
||||
github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I=
|
||||
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
|
||||
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
|
||||
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/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
|
||||
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
|
||||
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/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.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/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=
|
||||
@@ -442,6 +505,7 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
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/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840=
|
||||
@@ -449,19 +513,19 @@ github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
|
||||
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||
github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
|
||||
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||
github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
|
||||
github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
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.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg=
|
||||
github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE=
|
||||
github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg=
|
||||
github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww=
|
||||
github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8=
|
||||
github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U=
|
||||
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.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
|
||||
@@ -470,6 +534,7 @@ github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1H
|
||||
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
|
||||
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=
|
||||
@@ -482,14 +547,25 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
|
||||
github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
|
||||
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_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_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
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.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
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.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
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.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho=
|
||||
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U=
|
||||
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc=
|
||||
@@ -501,8 +577,8 @@ github.com/replicatedhq/termui/v3 v3.1.1-0.20200811145416-f40076d26851/go.mod h1
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
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/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o=
|
||||
github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
@@ -511,27 +587,28 @@ github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDc
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
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/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E=
|
||||
github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||
github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEWY=
|
||||
github.com/sebdah/goldie/v2 v2.5.5/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
|
||||
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
|
||||
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/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY=
|
||||
github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
|
||||
github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.9.4-0.20251023124752-b61f268f75b6 h1:JEH4GlfEqachL/BWMxNEqYZGst5AGInBdLF/bEx+dZY=
|
||||
github.com/sirupsen/logrus v1.9.4-0.20251023124752-b61f268f75b6/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
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/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
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=
|
||||
@@ -541,60 +618,69 @@ github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8W
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
|
||||
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=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
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/sylabs/sif/v2 v2.22.0 h1:Y+xXufp4RdgZe02SR3nWEg7S6q4tPWN237WHYzkDSKA=
|
||||
github.com/sylabs/sif/v2 v2.22.0/go.mod h1:W1XhWTmG1KcG7j5a3KSYdMcUIFvbs240w/MMVW627hs=
|
||||
github.com/sylabs/sif/v2 v2.21.1 h1:GZ0b5//AFAqJEChd8wHV/uSKx/l1iuGYwjR8nx+4wPI=
|
||||
github.com/sylabs/sif/v2 v2.21.1/go.mod h1:YoqEGQnb5x/ItV653bawXHZJOXQaEWpGwHsSD3YePJI=
|
||||
github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc=
|
||||
github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
|
||||
github.com/tj/go-spin v1.1.0 h1:lhdWZsvImxvZ3q1C5OIB7d72DuOwP4O2NdBg9PyzNds=
|
||||
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
|
||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||
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/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/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
|
||||
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
|
||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/vladimirvivien/gexe v0.4.1 h1:W9gWkp8vSPjDoXDu04Yp4KljpVMaSt8IQuHswLDd5LY=
|
||||
github.com/vladimirvivien/gexe v0.4.1/go.mod h1:3gjgTqE2c0VyHnU5UOIwk7gyNzZDGulPb/DJPgcw64E=
|
||||
github.com/vmware-tanzu/velero v1.17.1 h1:ldKeiTuUwkThOw7zrUucNA1NwnLG66zl13YetWAoE0I=
|
||||
github.com/vmware-tanzu/velero v1.17.1/go.mod h1:3KTxuUN6Un38JzmYAX+8U6j2k6EexGoNNxa8jrJML8U=
|
||||
github.com/vmware-tanzu/velero v1.17.0 h1:b+KLlBG+v1YKogP81nAFix2pgJBTmUrnVlXg+OfB5ao=
|
||||
github.com/vmware-tanzu/velero v1.17.0/go.mod h1:BJRFKei89hSqrazQKiwv5YhhX871X1W1qPyo5OP09zw=
|
||||
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/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
|
||||
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
|
||||
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
|
||||
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.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/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8=
|
||||
@@ -613,103 +699,159 @@ go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9Xa
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0/go.mod h1:zKU4zUgKiaRxrdovSS2amdM5gOc59slmo/zJwGX+YBg=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s=
|
||||
go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk=
|
||||
go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs=
|
||||
go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
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.podman.io/image/v5 v5.38.0 h1:aUKrCANkPvze1bnhLJsaubcfz0d9v/bSDLnwsXJm6G4=
|
||||
go.podman.io/image/v5 v5.38.0/go.mod h1:hSIoIUzgBnmc4DjoIdzk63aloqVbD7QXDMkSE/cvG90=
|
||||
go.podman.io/storage v1.61.1-0.20260113143440-ec033ce69f13 h1:31m5LHEH19ESCYFrvZh4YX1kkfa7sQSapHg9/NHzpok=
|
||||
go.podman.io/storage v1.61.1-0.20260113143440-ec033ce69f13/go.mod h1:nkKllgCrEviIXYWZNGxWdzU02pUBIfH5oOSrB/QgwIQ=
|
||||
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/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
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.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo=
|
||||
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
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=
|
||||
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.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
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-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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
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.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
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-20181122145206-62eef0e2fa9b/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190524152521-dbbf3f1254d4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
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.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
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=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
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.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
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=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
|
||||
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
|
||||
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc=
|
||||
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.241.0 h1:QKwqWQlkc6O895LchPEDUSYr22Xp3NCxpQRiWTB6avE=
|
||||
google.golang.org/api v0.241.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
|
||||
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-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
|
||||
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
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.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||
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=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
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.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=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
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/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/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
@@ -725,44 +867,50 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
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.19.4 h1:E2yFBejmZBczWr5LblhjZbvAOAwVumfBO1AtN3nqI30=
|
||||
helm.sh/helm/v3 v3.19.4/go.mod h1:PC1rk7PqacpkV4acUFMLStOOis7QM9Jq3DveHBInu4s=
|
||||
k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY=
|
||||
k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA=
|
||||
k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4=
|
||||
k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU=
|
||||
k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8=
|
||||
k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
|
||||
k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4=
|
||||
k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds=
|
||||
k8s.io/cli-runtime v0.35.0 h1:PEJtYS/Zr4p20PfZSLCbY6YvaoLrfByd6THQzPworUE=
|
||||
k8s.io/cli-runtime v0.35.0/go.mod h1:VBRvHzosVAoVdP3XwUQn1Oqkvaa8facnokNkD7jOTMY=
|
||||
k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE=
|
||||
k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o=
|
||||
k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94=
|
||||
k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0=
|
||||
helm.sh/helm/v3 v3.19.0 h1:krVyCGa8fa/wzTZgqw0DUiXuRT5BPdeqE/sQXujQ22k=
|
||||
helm.sh/helm/v3 v3.19.0/go.mod h1:Lk/SfzN0w3a3C3o+TdAKrLwJ0wcZ//t1/SDXAvfgDdc=
|
||||
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=
|
||||
k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
|
||||
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
|
||||
k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI=
|
||||
k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc=
|
||||
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
|
||||
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA=
|
||||
k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0=
|
||||
k8s.io/cli-runtime v0.34.1 h1:btlgAgTrYd4sk8vJTRG6zVtqBKt9ZMDeQZo2PIzbL7M=
|
||||
k8s.io/cli-runtime v0.34.1/go.mod h1:aVA65c+f0MZiMUPbseU/M9l1Wo2byeaGwUuQEQVVveE=
|
||||
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
|
||||
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
|
||||
k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A=
|
||||
k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0=
|
||||
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-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||
k8s.io/kubectl v0.34.2 h1:+fWGrVlDONMUmmQLDaGkQ9i91oszjjRAa94cr37hzqA=
|
||||
k8s.io/kubectl v0.34.2/go.mod h1:X2KTOdtZZNrTWmUD4oHApJ836pevSl+zvC5sI6oO2YQ=
|
||||
k8s.io/kubelet v0.35.0 h1:8cgJHCBCKLYuuQ7/Pxb/qWbJfX1LXIw7790ce9xHq7c=
|
||||
k8s.io/kubelet v0.35.0/go.mod h1:ciRzAXn7C4z5iB7FhG1L2CGPPXLTVCABDlbXt/Zz8YA=
|
||||
k8s.io/metrics v0.35.0 h1:xVFoqtAGm2dMNJAcB5TFZJPCen0uEqqNt52wW7ABbX8=
|
||||
k8s.io/metrics v0.35.0/go.mod h1:g2Up4dcBygZi2kQSEQVDByFs+VUwepJMzzQLJJLpq4M=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
|
||||
k8s.io/kubectl v0.34.0 h1:NcXz4TPTaUwhiX4LU+6r6udrlm0NsVnSkP3R9t0dmxs=
|
||||
k8s.io/kubectl v0.34.0/go.mod h1:bmd0W5i+HuG7/p5sqicr0Li0rR2iIhXL0oUyLF3OjR4=
|
||||
k8s.io/kubelet v0.34.1 h1:doAaTA9/Yfzbdq/u/LveZeONp96CwX9giW6b+oHn4m4=
|
||||
k8s.io/kubelet v0.34.1/go.mod h1:PtV3Ese8iOM19gSooFoQT9iyRisbmJdAPuDImuccbbA=
|
||||
k8s.io/kubernetes v1.34.1 h1:F3p8dtpv+i8zQoebZeK5zBqM1g9x1aIdnA5vthvcuUk=
|
||||
k8s.io/kubernetes v1.34.1/go.mod h1:iu+FhII+Oc/1gGWLJcer6wpyih441aNFHl7Pvm8yPto=
|
||||
k8s.io/metrics v0.34.1 h1:374Rexmp1xxgRt64Bi0TsjAM8cA/Y8skwCoPdjtIslE=
|
||||
k8s.io/metrics v0.34.1/go.mod h1:Drf5kPfk2NJrlpcNdSiAAHn/7Y9KqxpRNagByM7Ei80=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
oras.land/oras-go v1.2.6 h1:z8cmxQXBU8yZ4mkytWqXfo6tZcamPwjsuxYU81xJ8Lk=
|
||||
oras.land/oras-go v1.2.6/go.mod h1:OVPc1PegSEe/K8YiLfosrlqlqTN9PUyFvOw5Y9gwrT8=
|
||||
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=
|
||||
periph.io/x/host/v3 v3.8.5 h1:g4g5xE1XZtDiGl1UAJaUur1aT7uNiFLMkyMEiZ7IHII=
|
||||
periph.io/x/host/v3 v3.8.5/go.mod h1:hPq8dISZIc+UNfWoRj+bPH3XEBQqJPdFdx218W92mdc=
|
||||
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
|
||||
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
|
||||
sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg=
|
||||
sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY=
|
||||
sigs.k8s.io/e2e-framework v0.6.0 h1:p7hFzHnLKO7eNsWGI2AbC1Mo2IYxidg49BiT4njxkrM=
|
||||
sigs.k8s.io/e2e-framework v0.6.0/go.mod h1:IREnCHnKgRCioLRmNi0hxSJ1kJ+aAdjEKK/gokcZu4k=
|
||||
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/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
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=
|
||||
|
||||
@@ -191,9 +191,7 @@ func LoadFromCLIArgs(ctx context.Context, client kubernetes.Interface, args []st
|
||||
|
||||
// load URL spec first to remove URI key from the spec
|
||||
urlSpec, err := loader.LoadSpecs(ctx, loader.LoadOptions{
|
||||
RawSpec: rawURLSpec,
|
||||
Client: client,
|
||||
Namespace: vp.GetString("namespace"),
|
||||
RawSpec: rawURLSpec,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println(color.YellowString("failed to load spec from URI %q: %v\n", v, err))
|
||||
@@ -211,9 +209,7 @@ func LoadFromCLIArgs(ctx context.Context, client kubernetes.Interface, args []st
|
||||
}
|
||||
|
||||
kinds, err := loader.LoadSpecs(ctx, loader.LoadOptions{
|
||||
RawSpecs: rawSpecs,
|
||||
Client: client,
|
||||
Namespace: vp.GetString("namespace"),
|
||||
RawSpecs: rawSpecs,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -367,9 +363,7 @@ func LoadFromCluster(ctx context.Context, client kubernetes.Interface, selectors
|
||||
|
||||
// Load troubleshoot specs from the raw specs
|
||||
return loader.LoadSpecs(ctx, loader.LoadOptions{
|
||||
RawSpecs: rawSpecs,
|
||||
Client: client,
|
||||
Namespace: ns,
|
||||
RawSpecs: rawSpecs,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -421,31 +421,39 @@ func (a *OllamaAgent) Analyze(ctx context.Context, data []byte, analyzers []anal
|
||||
func (a *OllamaAgent) discoverAnalyzers(bundle *analyzer.SupportBundle) []analyzer.AnalyzerSpec {
|
||||
var specs []analyzer.AnalyzerSpec
|
||||
|
||||
// Collect files by type for aggregation
|
||||
podFiles := []string{}
|
||||
deploymentFiles := []string{}
|
||||
eventFiles := []string{}
|
||||
nodeFiles := []string{}
|
||||
|
||||
// Analyze bundle contents to determine what types of analysis to perform
|
||||
for filePath := range bundle.Files {
|
||||
filePathLower := strings.ToLower(filePath)
|
||||
filePath = strings.ToLower(filePath)
|
||||
|
||||
switch {
|
||||
case strings.Contains(filePathLower, "pods") && strings.HasSuffix(filePathLower, ".json"):
|
||||
podFiles = append(podFiles, filePath)
|
||||
case strings.Contains(filePath, "pods") && strings.HasSuffix(filePath, ".json"):
|
||||
specs = append(specs, analyzer.AnalyzerSpec{
|
||||
Name: "ai-pod-analysis",
|
||||
Type: "ai-workload",
|
||||
Category: "pods",
|
||||
Priority: 10,
|
||||
Config: map[string]interface{}{"filePath": filePath, "promptType": "pod-analysis"},
|
||||
})
|
||||
|
||||
case strings.Contains(filePathLower, "deployments") && strings.HasSuffix(filePathLower, ".json"):
|
||||
deploymentFiles = append(deploymentFiles, filePath)
|
||||
case strings.Contains(filePath, "deployments") && strings.HasSuffix(filePath, ".json"):
|
||||
specs = append(specs, analyzer.AnalyzerSpec{
|
||||
Name: "ai-deployment-analysis",
|
||||
Type: "ai-workload",
|
||||
Category: "deployments",
|
||||
Priority: 9,
|
||||
Config: map[string]interface{}{"filePath": filePath, "promptType": "deployment-analysis"},
|
||||
})
|
||||
|
||||
case strings.Contains(filePathLower, "events") && strings.HasSuffix(filePathLower, ".json"):
|
||||
eventFiles = append(eventFiles, filePath)
|
||||
case strings.Contains(filePath, "events") && strings.HasSuffix(filePath, ".json"):
|
||||
specs = append(specs, analyzer.AnalyzerSpec{
|
||||
Name: "ai-event-analysis",
|
||||
Type: "ai-events",
|
||||
Category: "events",
|
||||
Priority: 8,
|
||||
Config: map[string]interface{}{"filePath": filePath, "promptType": "event-analysis"},
|
||||
})
|
||||
|
||||
case strings.Contains(filePathLower, "nodes") && strings.HasSuffix(filePathLower, ".json"):
|
||||
nodeFiles = append(nodeFiles, filePath)
|
||||
|
||||
case strings.Contains(filePathLower, "logs") && strings.HasSuffix(filePathLower, ".log"):
|
||||
// Logs are analyzed separately per file (not aggregated)
|
||||
case strings.Contains(filePath, "logs") && strings.HasSuffix(filePath, ".log"):
|
||||
specs = append(specs, analyzer.AnalyzerSpec{
|
||||
Name: "ai-log-analysis",
|
||||
Type: "ai-logs",
|
||||
@@ -453,424 +461,50 @@ func (a *OllamaAgent) discoverAnalyzers(bundle *analyzer.SupportBundle) []analyz
|
||||
Priority: 7,
|
||||
Config: map[string]interface{}{"filePath": filePath, "promptType": "log-analysis"},
|
||||
})
|
||||
|
||||
case strings.Contains(filePath, "nodes") && strings.HasSuffix(filePath, ".json"):
|
||||
specs = append(specs, analyzer.AnalyzerSpec{
|
||||
Name: "ai-resource-analysis",
|
||||
Type: "ai-resources",
|
||||
Category: "nodes",
|
||||
Priority: 8,
|
||||
Config: map[string]interface{}{"filePath": filePath, "promptType": "resource-analysis"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Create aggregated analyzer for ALL pod files (cluster-wide view)
|
||||
if len(podFiles) > 0 {
|
||||
specs = append(specs, analyzer.AnalyzerSpec{
|
||||
Name: "ai-pod-analysis-cluster",
|
||||
Type: "ai-workload",
|
||||
Category: "pods",
|
||||
Priority: 10,
|
||||
Config: map[string]interface{}{
|
||||
"filePaths": podFiles,
|
||||
"promptType": "pod-analysis",
|
||||
"aggregated": true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Create aggregated analyzer for ALL deployment files (cluster-wide view)
|
||||
if len(deploymentFiles) > 0 {
|
||||
specs = append(specs, analyzer.AnalyzerSpec{
|
||||
Name: "ai-deployment-analysis-cluster",
|
||||
Type: "ai-workload",
|
||||
Category: "deployments",
|
||||
Priority: 9,
|
||||
Config: map[string]interface{}{
|
||||
"filePaths": deploymentFiles,
|
||||
"promptType": "deployment-analysis",
|
||||
"aggregated": true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Create aggregated analyzer for ALL event files (cluster-wide view)
|
||||
if len(eventFiles) > 0 {
|
||||
specs = append(specs, analyzer.AnalyzerSpec{
|
||||
Name: "ai-event-analysis-cluster",
|
||||
Type: "ai-events",
|
||||
Category: "events",
|
||||
Priority: 8,
|
||||
Config: map[string]interface{}{
|
||||
"filePaths": eventFiles,
|
||||
"promptType": "event-analysis",
|
||||
"aggregated": true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Create aggregated analyzer for ALL node files (cluster-wide view)
|
||||
if len(nodeFiles) > 0 {
|
||||
specs = append(specs, analyzer.AnalyzerSpec{
|
||||
Name: "ai-resource-analysis-cluster",
|
||||
Type: "ai-resources",
|
||||
Category: "nodes",
|
||||
Priority: 8,
|
||||
Config: map[string]interface{}{
|
||||
"filePaths": nodeFiles,
|
||||
"promptType": "resource-analysis",
|
||||
"aggregated": true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return specs
|
||||
}
|
||||
|
||||
// aggregateFiles combines multiple files of the same type into a single summary for analysis
|
||||
func (a *OllamaAgent) aggregateFiles(bundle *analyzer.SupportBundle, filePaths []string, category string) (string, error) {
|
||||
var summary strings.Builder
|
||||
|
||||
switch category {
|
||||
case "pods":
|
||||
return a.aggregatePodFiles(bundle, filePaths)
|
||||
case "deployments":
|
||||
return a.aggregateDeploymentFiles(bundle, filePaths)
|
||||
case "events":
|
||||
return a.aggregateEventFiles(bundle, filePaths)
|
||||
case "nodes":
|
||||
return a.aggregateNodeFiles(bundle, filePaths)
|
||||
default:
|
||||
// For other types, just concatenate the files
|
||||
summary.WriteString(fmt.Sprintf("Aggregated analysis of %d files:\n\n", len(filePaths)))
|
||||
for _, filePath := range filePaths {
|
||||
if data, exists := bundle.Files[filePath]; exists {
|
||||
summary.WriteString(fmt.Sprintf("--- File: %s ---\n", filePath))
|
||||
summary.Write(data)
|
||||
summary.WriteString("\n\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return summary.String(), nil
|
||||
}
|
||||
|
||||
// aggregatePodFiles creates a cluster-wide summary of pods from multiple namespace files
|
||||
func (a *OllamaAgent) aggregatePodFiles(bundle *analyzer.SupportBundle, filePaths []string) (string, error) {
|
||||
var summary strings.Builder
|
||||
totalPods := 0
|
||||
runningPods := 0
|
||||
pendingPods := 0
|
||||
failedPods := 0
|
||||
succeededPods := 0
|
||||
namespaceStats := make(map[string]int)
|
||||
|
||||
summary.WriteString("CLUSTER-WIDE POD ANALYSIS\n")
|
||||
summary.WriteString("Analyzing pods across all namespaces:\n\n")
|
||||
|
||||
for _, filePath := range filePaths {
|
||||
data, exists := bundle.Files[filePath]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract namespace from path (e.g., "cluster-resources/pods/kube-system.json")
|
||||
parts := strings.Split(filePath, "/")
|
||||
namespace := "unknown"
|
||||
if len(parts) >= 3 {
|
||||
namespace = strings.TrimSuffix(parts[len(parts)-1], ".json")
|
||||
}
|
||||
|
||||
// Parse pod data - handle both PodList and single Pod objects
|
||||
var podList map[string]interface{}
|
||||
if err := json.Unmarshal(data, &podList); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this is a List object with items array
|
||||
items, ok := podList["items"].([]interface{})
|
||||
if ok {
|
||||
// Handle PodList - process all pods in the list
|
||||
// Initialize namespace for valid PodList (ensures empty namespaces are tracked)
|
||||
if _, exists := namespaceStats[namespace]; !exists {
|
||||
namespaceStats[namespace] = 0
|
||||
}
|
||||
|
||||
podCount := len(items)
|
||||
namespaceStats[namespace] += podCount
|
||||
totalPods += podCount
|
||||
|
||||
// Count pod statuses
|
||||
for _, item := range items {
|
||||
pod, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
status, ok := pod["status"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
phase, ok := status["phase"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch phase {
|
||||
case "Running":
|
||||
runningPods++
|
||||
case "Pending":
|
||||
pendingPods++
|
||||
case "Failed":
|
||||
failedPods++
|
||||
case "Succeeded":
|
||||
succeededPods++
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle single Pod object (not a list)
|
||||
// Check if this is a single Pod object (has "kind": "Pod")
|
||||
if kind, exists := podList["kind"].(string); exists && kind == "Pod" {
|
||||
// Initialize namespace only for valid pod data
|
||||
if _, exists := namespaceStats[namespace]; !exists {
|
||||
namespaceStats[namespace] = 0
|
||||
}
|
||||
// Single pod - increment count for this namespace
|
||||
namespaceStats[namespace]++
|
||||
totalPods++
|
||||
// Extract status for single pod
|
||||
if status, ok := podList["status"].(map[string]interface{}); ok {
|
||||
if phase, ok := status["phase"].(string); ok {
|
||||
switch phase {
|
||||
case "Running":
|
||||
runningPods++
|
||||
case "Pending":
|
||||
pendingPods++
|
||||
case "Failed":
|
||||
failedPods++
|
||||
case "Succeeded":
|
||||
succeededPods++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Skip to next file after processing single pod or invalid data
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
summary.WriteString(fmt.Sprintf("Total pods in cluster: %d\n", totalPods))
|
||||
summary.WriteString(fmt.Sprintf(" - Running: %d\n", runningPods))
|
||||
summary.WriteString(fmt.Sprintf(" - Pending: %d\n", pendingPods))
|
||||
summary.WriteString(fmt.Sprintf(" - Failed: %d\n", failedPods))
|
||||
summary.WriteString(fmt.Sprintf(" - Succeeded: %d\n", succeededPods))
|
||||
summary.WriteString("\nPods by namespace:\n")
|
||||
|
||||
for namespace, count := range namespaceStats {
|
||||
if count > 0 {
|
||||
summary.WriteString(fmt.Sprintf(" - %s: %d pods\n", namespace, count))
|
||||
} else {
|
||||
summary.WriteString(fmt.Sprintf(" - %s: empty (no pods)\n", namespace))
|
||||
}
|
||||
}
|
||||
|
||||
summary.WriteString("\nIMPORTANT CONTEXT:\n")
|
||||
summary.WriteString("- Empty namespaces are NORMAL in Kubernetes\n")
|
||||
summary.WriteString("- Only report issues if there are actual pod failures or critical problems\n")
|
||||
summary.WriteString("- The presence of empty namespaces is not a problem\n")
|
||||
|
||||
return summary.String(), nil
|
||||
}
|
||||
|
||||
// aggregateDeploymentFiles creates a cluster-wide summary of deployments
|
||||
func (a *OllamaAgent) aggregateDeploymentFiles(bundle *analyzer.SupportBundle, filePaths []string) (string, error) {
|
||||
var summary strings.Builder
|
||||
totalDeployments := 0
|
||||
namespaceStats := make(map[string]int)
|
||||
|
||||
summary.WriteString("CLUSTER-WIDE DEPLOYMENT ANALYSIS\n")
|
||||
summary.WriteString("Analyzing deployments across all namespaces:\n\n")
|
||||
|
||||
for _, filePath := range filePaths {
|
||||
data, exists := bundle.Files[filePath]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Split(filePath, "/")
|
||||
namespace := "unknown"
|
||||
if len(parts) >= 3 {
|
||||
namespace = strings.TrimSuffix(parts[len(parts)-1], ".json")
|
||||
}
|
||||
|
||||
// Parse deployment data - handle both DeploymentList and single Deployment objects
|
||||
var deploymentList map[string]interface{}
|
||||
if err := json.Unmarshal(data, &deploymentList); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this is a List object with items array
|
||||
items, ok := deploymentList["items"].([]interface{})
|
||||
if ok {
|
||||
// Handle DeploymentList - process all deployments in the list
|
||||
// Initialize namespace for valid DeploymentList (ensures empty namespaces are tracked)
|
||||
if _, exists := namespaceStats[namespace]; !exists {
|
||||
namespaceStats[namespace] = 0
|
||||
}
|
||||
|
||||
deployCount := len(items)
|
||||
namespaceStats[namespace] += deployCount
|
||||
totalDeployments += deployCount
|
||||
} else {
|
||||
// Handle single Deployment object (not a list)
|
||||
// Check if this is a single Deployment object (has "kind": "Deployment")
|
||||
if kind, exists := deploymentList["kind"].(string); exists && kind == "Deployment" {
|
||||
// Initialize namespace only for valid deployment data
|
||||
if _, exists := namespaceStats[namespace]; !exists {
|
||||
namespaceStats[namespace] = 0
|
||||
}
|
||||
// Single deployment - increment count for this namespace
|
||||
namespaceStats[namespace]++
|
||||
totalDeployments++
|
||||
}
|
||||
// Skip to next file after processing single deployment or invalid data
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
summary.WriteString(fmt.Sprintf("Total deployments in cluster: %d\n", totalDeployments))
|
||||
summary.WriteString("\nDeployments by namespace:\n")
|
||||
|
||||
for namespace, count := range namespaceStats {
|
||||
if count > 0 {
|
||||
summary.WriteString(fmt.Sprintf(" - %s: %d deployments\n", namespace, count))
|
||||
} else {
|
||||
summary.WriteString(fmt.Sprintf(" - %s: no deployments\n", namespace))
|
||||
}
|
||||
}
|
||||
|
||||
summary.WriteString("\nIMPORTANT: Empty namespaces are normal. Only flag actual deployment issues.\n")
|
||||
|
||||
return summary.String(), nil
|
||||
}
|
||||
|
||||
// aggregateEventFiles creates a cluster-wide summary of events
|
||||
func (a *OllamaAgent) aggregateEventFiles(bundle *analyzer.SupportBundle, filePaths []string) (string, error) {
|
||||
var summary strings.Builder
|
||||
totalEvents := 0
|
||||
|
||||
summary.WriteString("CLUSTER-WIDE EVENT ANALYSIS\n")
|
||||
summary.WriteString("Analyzing events across all namespaces:\n\n")
|
||||
|
||||
eventsIncluded := 0
|
||||
for _, filePath := range filePaths {
|
||||
data, exists := bundle.Files[filePath]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse event data - handle both EventList and single Event objects
|
||||
var eventList map[string]interface{}
|
||||
if err := json.Unmarshal(data, &eventList); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this is a List object with items array
|
||||
items, ok := eventList["items"].([]interface{})
|
||||
if ok {
|
||||
itemCount := len(items)
|
||||
totalEvents += itemCount
|
||||
// Include actual event data for AI analysis (limited to 50 events max for the summary)
|
||||
// Only include if adding this file wouldn't significantly exceed the limit
|
||||
if itemCount > 0 && eventsIncluded < 50 && (eventsIncluded+itemCount) <= 60 {
|
||||
dataStr := string(data)
|
||||
// Include file if data size is reasonable
|
||||
if len(dataStr) < 2000 {
|
||||
summary.WriteString(fmt.Sprintf("\n--- Events from %s ---\n", filePath))
|
||||
summary.WriteString(dataStr)
|
||||
summary.WriteString("\n")
|
||||
eventsIncluded += itemCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
summary.WriteString(fmt.Sprintf("\nTotal events collected: %d\n", totalEvents))
|
||||
|
||||
return summary.String(), nil
|
||||
}
|
||||
|
||||
// aggregateNodeFiles creates a cluster-wide summary of nodes
|
||||
func (a *OllamaAgent) aggregateNodeFiles(bundle *analyzer.SupportBundle, filePaths []string) (string, error) {
|
||||
var summary strings.Builder
|
||||
|
||||
summary.WriteString("CLUSTER-WIDE NODE ANALYSIS\n\n")
|
||||
|
||||
for _, filePath := range filePaths {
|
||||
data, exists := bundle.Files[filePath]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
summary.WriteString(fmt.Sprintf("--- Nodes data from %s ---\n", filePath))
|
||||
summary.Write(data)
|
||||
summary.WriteString("\n\n")
|
||||
}
|
||||
|
||||
return summary.String(), nil
|
||||
}
|
||||
|
||||
// runLLMAnalysis executes analysis using LLM for a specific analyzer spec
|
||||
func (a *OllamaAgent) runLLMAnalysis(ctx context.Context, bundle *analyzer.SupportBundle, spec analyzer.AnalyzerSpec) (*analyzer.AnalyzerResult, error) {
|
||||
ctx, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, fmt.Sprintf("OllamaAgent.%s", spec.Name))
|
||||
defer span.End()
|
||||
|
||||
var dataStr string
|
||||
// Smart file detection for enhanced analyzer compatibility
|
||||
var filePath string
|
||||
var fileData []byte
|
||||
var exists bool
|
||||
|
||||
// Check if this is an aggregated analyzer (multiple files)
|
||||
if aggregated, ok := spec.Config["aggregated"].(bool); ok && aggregated {
|
||||
// Handle aggregated files
|
||||
if filePaths, ok := spec.Config["filePaths"].([]string); ok && len(filePaths) > 0 {
|
||||
aggregatedData, err := a.aggregateFiles(bundle, filePaths, spec.Category)
|
||||
if err != nil {
|
||||
return &analyzer.AnalyzerResult{
|
||||
Title: spec.Name,
|
||||
IsWarn: true,
|
||||
Message: fmt.Sprintf("Failed to aggregate files: %v", err),
|
||||
Category: spec.Category,
|
||||
}, nil
|
||||
}
|
||||
dataStr = aggregatedData
|
||||
} else {
|
||||
// Missing or invalid filePaths for aggregated analyzer
|
||||
return &analyzer.AnalyzerResult{
|
||||
Title: spec.Name,
|
||||
IsWarn: true,
|
||||
Message: "Aggregated analyzer missing valid filePaths configuration",
|
||||
Category: spec.Category,
|
||||
}, nil
|
||||
// First try to get explicit filePath from config
|
||||
if fp, ok := spec.Config["filePath"].(string); ok {
|
||||
filePath = fp
|
||||
fileData, exists = bundle.Files[filePath]
|
||||
}
|
||||
|
||||
// If no explicit filePath, auto-detect based on analyzer type
|
||||
if !exists {
|
||||
filePath, fileData, exists = a.autoDetectFileForAnalyzer(bundle, spec)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
result := &analyzer.AnalyzerResult{
|
||||
Title: spec.Name,
|
||||
IsWarn: true,
|
||||
Message: fmt.Sprintf("File not found: %s", filePath),
|
||||
Category: spec.Category,
|
||||
}
|
||||
} else {
|
||||
// Smart file detection for enhanced analyzer compatibility (single file)
|
||||
var filePath string
|
||||
var fileData []byte
|
||||
var exists bool
|
||||
|
||||
// First try to get explicit filePath from config
|
||||
if fp, ok := spec.Config["filePath"].(string); ok {
|
||||
filePath = fp
|
||||
fileData, exists = bundle.Files[filePath]
|
||||
}
|
||||
|
||||
// If no explicit filePath, auto-detect based on analyzer type
|
||||
if !exists {
|
||||
filePath, fileData, exists = a.autoDetectFileForAnalyzer(bundle, spec)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
result := &analyzer.AnalyzerResult{
|
||||
Title: spec.Name,
|
||||
IsWarn: true,
|
||||
Message: fmt.Sprintf("File not found: %s", filePath),
|
||||
Category: spec.Category,
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
dataStr = string(fileData)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
promptType, _ := spec.Config["promptType"].(string)
|
||||
@@ -885,6 +519,7 @@ func (a *OllamaAgent) runLLMAnalysis(ctx context.Context, bundle *analyzer.Suppo
|
||||
}
|
||||
|
||||
// Prepare data for analysis (truncate if too large)
|
||||
dataStr := string(fileData)
|
||||
if len(dataStr) > 4000 { // Limit input size
|
||||
if promptType == "log-analysis" {
|
||||
// For logs, take the last N lines
|
||||
@@ -1231,182 +866,6 @@ func (a *OllamaAgent) autoDetectFileForAnalyzer(bundle *analyzer.SupportBundle,
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
// normalizeInsights converts various JSON formats into a []string array
|
||||
func (a *OllamaAgent) normalizeInsights(raw json.RawMessage) []string {
|
||||
if len(raw) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// Try parsing as array of strings first (expected format)
|
||||
var arrayInsights []string
|
||||
if err := json.Unmarshal(raw, &arrayInsights); err == nil {
|
||||
return arrayInsights
|
||||
}
|
||||
|
||||
// Try parsing as single string
|
||||
var stringInsight string
|
||||
if err := json.Unmarshal(raw, &stringInsight); err == nil {
|
||||
if stringInsight != "" {
|
||||
return []string{stringInsight}
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// Try parsing as array of objects/maps (common LLM format)
|
||||
var arrayOfMaps []map[string]interface{}
|
||||
if err := json.Unmarshal(raw, &arrayOfMaps); err == nil {
|
||||
insights := []string{}
|
||||
for _, obj := range arrayOfMaps {
|
||||
// Extract meaningful text from each object
|
||||
insightText := a.formatMapAsInsight(obj)
|
||||
if insightText != "" {
|
||||
insights = append(insights, insightText)
|
||||
}
|
||||
}
|
||||
return insights
|
||||
}
|
||||
|
||||
// Try parsing as object/map and extract meaningful text
|
||||
var objInsights map[string]interface{}
|
||||
if err := json.Unmarshal(raw, &objInsights); err == nil {
|
||||
insights := []string{}
|
||||
for key, value := range objInsights {
|
||||
// Extract meaningful insights from object structure
|
||||
insightText := a.extractInsightText(key, value)
|
||||
if insightText != "" {
|
||||
insights = append(insights, insightText)
|
||||
}
|
||||
}
|
||||
return insights
|
||||
}
|
||||
|
||||
// If all parsing fails, return empty array
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// formatMapAsInsight converts a map/object into a readable insight string
|
||||
func (a *OllamaAgent) formatMapAsInsight(obj map[string]interface{}) string {
|
||||
// Common patterns in LLM responses for insights
|
||||
// Try to extract description, pattern, message, etc.
|
||||
|
||||
// Priority 1: Look for description field
|
||||
if desc, ok := obj["description"].(string); ok && desc != "" {
|
||||
if pattern, ok := obj["pattern"].(string); ok && pattern != "" {
|
||||
return fmt.Sprintf("%s: %s", pattern, desc)
|
||||
}
|
||||
return desc
|
||||
}
|
||||
|
||||
// Priority 2: Look for message field
|
||||
if msg, ok := obj["message"].(string); ok && msg != "" {
|
||||
return msg
|
||||
}
|
||||
|
||||
// Priority 3: Look for explanation/implication field
|
||||
if expl, ok := obj["explanation"].(string); ok && expl != "" {
|
||||
return expl
|
||||
}
|
||||
if impl, ok := obj["implication"].(string); ok && impl != "" {
|
||||
return impl
|
||||
}
|
||||
|
||||
// Priority 4: Combine all string fields
|
||||
parts := []string{}
|
||||
for key, value := range obj {
|
||||
if str, ok := value.(string); ok && str != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", key, str))
|
||||
}
|
||||
}
|
||||
|
||||
if len(parts) > 0 {
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractInsightText extracts readable text from nested JSON structures
|
||||
func (a *OllamaAgent) extractInsightText(key string, value interface{}) string {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
if v != "" {
|
||||
return fmt.Sprintf("%s: %s", key, v)
|
||||
}
|
||||
case map[string]interface{}:
|
||||
// For nested objects, create a summary
|
||||
parts := []string{}
|
||||
for subKey, subValue := range v {
|
||||
if str, ok := subValue.(string); ok && str != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s=%s", subKey, str))
|
||||
}
|
||||
}
|
||||
if len(parts) > 0 {
|
||||
return fmt.Sprintf("%s: %s", key, strings.Join(parts, ", "))
|
||||
}
|
||||
case []interface{}:
|
||||
// For arrays, join elements
|
||||
parts := []string{}
|
||||
for _, item := range v {
|
||||
if str, ok := item.(string); ok && str != "" {
|
||||
parts = append(parts, str)
|
||||
}
|
||||
}
|
||||
if len(parts) > 0 {
|
||||
return fmt.Sprintf("%s: %s", key, strings.Join(parts, ", "))
|
||||
}
|
||||
case float64, int, bool:
|
||||
return fmt.Sprintf("%s: %v", key, v)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getStringField extracts a string field from a map, trying multiple key variants
|
||||
func (a *OllamaAgent) getStringField(m map[string]interface{}, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if val, ok := m[key]; ok {
|
||||
if str, ok := val.(string); ok {
|
||||
return str
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractRemediation extracts remediation info from various JSON structures
|
||||
func (a *OllamaAgent) extractRemediation(result *analyzer.AnalyzerResult, remData interface{}) {
|
||||
switch rem := remData.(type) {
|
||||
case map[string]interface{}:
|
||||
// Single remediation object
|
||||
desc := a.getStringField(rem, "description", "Description")
|
||||
action := a.getStringField(rem, "action", "Action")
|
||||
command := a.getStringField(rem, "command", "Command")
|
||||
priority := 5 // default priority
|
||||
if p, ok := rem["priority"].(float64); ok {
|
||||
priority = int(p)
|
||||
} else if p, ok := rem["Priority"].(float64); ok {
|
||||
priority = int(p)
|
||||
}
|
||||
|
||||
if desc != "" || action != "" {
|
||||
result.Remediation = &analyzer.RemediationStep{
|
||||
Description: desc,
|
||||
Action: action,
|
||||
Command: command,
|
||||
Priority: priority,
|
||||
Category: "ai-suggested",
|
||||
IsAutomatable: false,
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
// Array of remediation suggestions - use the first one
|
||||
if len(rem) > 0 {
|
||||
if firstRem, ok := rem[0].(map[string]interface{}); ok {
|
||||
a.extractRemediation(result, firstRem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseLLMResponse parses the LLM response into an AnalyzerResult
|
||||
func (a *OllamaAgent) parseLLMResponse(response string, spec analyzer.AnalyzerSpec) (*analyzer.AnalyzerResult, error) {
|
||||
// First try JSON parsing
|
||||
@@ -1416,53 +875,55 @@ func (a *OllamaAgent) parseLLMResponse(response string, spec analyzer.AnalyzerSp
|
||||
if jsonStart != -1 && jsonEnd != -1 && jsonEnd > jsonStart {
|
||||
jsonStr := response[jsonStart : jsonEnd+1]
|
||||
|
||||
// Try with a flexible map first to handle case-insensitive fields
|
||||
var jsonMap map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(jsonStr), &jsonMap); err != nil {
|
||||
var llmResult struct {
|
||||
Status string `json:"status"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Insights []string `json:"insights"`
|
||||
Remediation struct {
|
||||
Description string `json:"description"`
|
||||
Action string `json:"action"`
|
||||
Command string `json:"command"`
|
||||
Priority int `json:"priority"`
|
||||
} `json:"remediation"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(jsonStr), &llmResult); err == nil {
|
||||
// Successfully parsed JSON
|
||||
result := &analyzer.AnalyzerResult{
|
||||
Title: llmResult.Title,
|
||||
Message: llmResult.Message,
|
||||
Category: spec.Category,
|
||||
Insights: llmResult.Insights,
|
||||
}
|
||||
|
||||
switch strings.ToLower(llmResult.Status) {
|
||||
case "pass":
|
||||
result.IsPass = true
|
||||
case "warn":
|
||||
result.IsWarn = true
|
||||
case "fail":
|
||||
result.IsFail = true
|
||||
default:
|
||||
result.IsWarn = true
|
||||
}
|
||||
|
||||
if llmResult.Remediation.Description != "" {
|
||||
result.Remediation = &analyzer.RemediationStep{
|
||||
Description: llmResult.Remediation.Description,
|
||||
Action: llmResult.Remediation.Action,
|
||||
Command: llmResult.Remediation.Command,
|
||||
Priority: llmResult.Remediation.Priority,
|
||||
Category: "ai-suggested",
|
||||
IsAutomatable: false,
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
} else {
|
||||
// JSON was found but malformed
|
||||
return nil, errors.Wrap(err, "failed to parse LLM JSON response")
|
||||
}
|
||||
|
||||
// Extract fields in a case-insensitive way
|
||||
status := a.getStringField(jsonMap, "status", "Status")
|
||||
title := a.getStringField(jsonMap, "title", "Title")
|
||||
message := a.getStringField(jsonMap, "message", "Message")
|
||||
|
||||
// Get insights field (try both lowercase and uppercase)
|
||||
var insightsRaw json.RawMessage
|
||||
if insights, ok := jsonMap["insights"]; ok {
|
||||
insightsRaw, _ = json.Marshal(insights)
|
||||
} else if insights, ok := jsonMap["Insights"]; ok {
|
||||
insightsRaw, _ = json.Marshal(insights)
|
||||
}
|
||||
|
||||
insights := a.normalizeInsights(insightsRaw)
|
||||
|
||||
result := &analyzer.AnalyzerResult{
|
||||
Title: title,
|
||||
Message: message,
|
||||
Category: spec.Category,
|
||||
Insights: insights,
|
||||
}
|
||||
|
||||
switch strings.ToLower(status) {
|
||||
case "pass":
|
||||
result.IsPass = true
|
||||
case "warn":
|
||||
result.IsWarn = true
|
||||
case "fail":
|
||||
result.IsFail = true
|
||||
default:
|
||||
result.IsWarn = true
|
||||
}
|
||||
|
||||
// Handle remediation (try both cases)
|
||||
if rem, ok := jsonMap["remediation"]; ok {
|
||||
a.extractRemediation(result, rem)
|
||||
} else if rem, ok := jsonMap["Remediation"]; ok {
|
||||
a.extractRemediation(result, rem)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Fall back to markdown parsing when JSON fails
|
||||
|
||||
@@ -206,10 +206,7 @@ func TestOllamaAgent_discoverAnalyzers(t *testing.T) {
|
||||
assert.NotNil(t, spec.Config)
|
||||
|
||||
// Verify AI-specific config
|
||||
// Aggregated analyzers use "filePaths", single-file analyzers use "filePath"
|
||||
hasFilePath := spec.Config["filePath"] != nil
|
||||
hasFilePaths := spec.Config["filePaths"] != nil
|
||||
assert.True(t, hasFilePath || hasFilePaths, "spec must have either filePath or filePaths")
|
||||
assert.Contains(t, spec.Config, "filePath")
|
||||
assert.Contains(t, spec.Config, "promptType")
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ import (
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/kubernetes/pkg/util/taints"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/internal/util"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
)
|
||||
|
||||
type AnalyzeNodeResources struct {
|
||||
@@ -453,7 +453,7 @@ func nodeMatchesFilters(node corev1.Node, filters *troubleshootv1beta2.NodeResou
|
||||
}
|
||||
|
||||
if filters.Taint != nil {
|
||||
return k8sutil.TaintExists(node.Spec.Taints, filters.Taint), nil
|
||||
return taints.TaintExists(node.Spec.Taints, filters.Taint), nil
|
||||
}
|
||||
|
||||
if filters.CPUArchitecture != "" {
|
||||
|
||||
@@ -146,7 +146,7 @@ func (h *OllamaHelper) downloadAndInstallWindows() error {
|
||||
return errors.Wrap(err, "failed to create temporary file")
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
defer tmpFile.Close() // Ensures file is closed in error paths
|
||||
defer tmpFile.Close()
|
||||
|
||||
// Download installer
|
||||
resp, err := http.Get(h.downloadURL)
|
||||
@@ -165,13 +165,6 @@ func (h *OllamaHelper) downloadAndInstallWindows() error {
|
||||
return errors.Wrap(err, "failed to write installer")
|
||||
}
|
||||
|
||||
// Close the file before executing it (required on Windows)
|
||||
// Note: This will be called twice (here and via defer), but that's safe
|
||||
// The defer ensures cleanup on error paths, this ensures closure before execution
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
return errors.Wrap(err, "failed to close installer file")
|
||||
}
|
||||
|
||||
// Run installer
|
||||
klog.Info("Running Ollama installer...")
|
||||
cmd := exec.Command(tmpFile.Name())
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
package v1beta3
|
||||
|
||||
import (
|
||||
"github.com/replicatedhq/troubleshoot/pkg/multitype"
|
||||
)
|
||||
|
||||
// CollectorMeta contains metadata for collectors
|
||||
type CollectorMeta struct {
|
||||
CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
|
||||
// +optional
|
||||
Exclude *multitype.BoolOrString `json:"exclude,omitempty" yaml:"exclude,omitempty"`
|
||||
}
|
||||
|
||||
// Database represents database collectors (PostgreSQL, MySQL, Redis, MSSQL)
|
||||
// In v1beta3, URI and TLS fields support valueFrom references
|
||||
type Database struct {
|
||||
CollectorMeta `json:",inline" yaml:",inline"`
|
||||
// URI can be a literal value or reference to a Secret/ConfigMap
|
||||
URI StringOrValueFrom `json:"uri" yaml:"uri"`
|
||||
// Parameters for the database connection
|
||||
Parameters []string `json:"parameters,omitempty"`
|
||||
// TLS configuration with support for valueFrom references
|
||||
TLS *TLSParams `json:"tls,omitempty" yaml:"tls,omitempty"`
|
||||
}
|
||||
|
||||
// TLSParams contains TLS configuration
|
||||
// In v1beta3, certificate fields support valueFrom references
|
||||
type TLSParams struct {
|
||||
// SkipVerify disables TLS verification
|
||||
SkipVerify bool `json:"skipVerify,omitempty" yaml:"skipVerify,omitempty"`
|
||||
// Secret references a Kubernetes Secret containing TLS materials (v1beta2 compatibility)
|
||||
Secret *TLSSecret `json:"secret,omitempty" yaml:"secret,omitempty"`
|
||||
// CACert can be a literal value or reference to a Secret/ConfigMap
|
||||
CACert StringOrValueFrom `json:"cacert,omitempty" yaml:"cacert,omitempty"`
|
||||
// ClientCert can be a literal value or reference to a Secret/ConfigMap
|
||||
ClientCert StringOrValueFrom `json:"clientCert,omitempty" yaml:"clientCert,omitempty"`
|
||||
// ClientKey can be a literal value or reference to a Secret/ConfigMap
|
||||
ClientKey StringOrValueFrom `json:"clientKey,omitempty" yaml:"clientKey,omitempty"`
|
||||
}
|
||||
|
||||
// TLSSecret references a Kubernetes Secret containing TLS materials
|
||||
// Maintained for backward compatibility
|
||||
type TLSSecret struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Namespace string `json:"namespace" yaml:"namespace"`
|
||||
}
|
||||
|
||||
// Temporary placeholder types for minimal v1beta3 implementation
|
||||
// These will be properly defined as we expand v1beta3 support
|
||||
type AfterCollection struct {
|
||||
CollectorMeta `json:",inline" yaml:",inline"`
|
||||
// TODO: Add fields as needed
|
||||
}
|
||||
|
||||
type Analyze struct {
|
||||
// TODO: Add fields as needed
|
||||
}
|
||||
|
||||
type HostAnalyze struct {
|
||||
// TODO: Add fields as needed
|
||||
}
|
||||
|
||||
type HostCollect struct {
|
||||
// TODO: Add fields as needed
|
||||
}
|
||||
|
||||
// Collect contains all collector definitions
|
||||
// For phase 1, we're focusing on Database collectors with StringOrValueFrom support
|
||||
type Collect struct {
|
||||
// Database collectors with v1beta3 StringOrValueFrom support
|
||||
Postgres *Database `json:"postgres,omitempty" yaml:"postgres,omitempty"`
|
||||
Mssql *Database `json:"mssql,omitempty" yaml:"mssql,omitempty"`
|
||||
Mysql *Database `json:"mysql,omitempty" yaml:"mysql,omitempty"`
|
||||
Redis *Database `json:"redis,omitempty" yaml:"redis,omitempty"`
|
||||
|
||||
// TODO: Add remaining collector types as we expand v1beta3 support
|
||||
// For now, these are placeholders to make the types compile
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
package v1beta3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
// ConvertToV1Beta2WithResolution converts a v1beta3 SupportBundleSpec to v1beta2
|
||||
// by resolving all StringOrValueFrom fields to their actual values
|
||||
func ConvertToV1Beta2WithResolution(
|
||||
ctx context.Context,
|
||||
v3spec *SupportBundleSpec,
|
||||
client kubernetes.Interface,
|
||||
defaultNamespace string,
|
||||
) (*troubleshootv1beta2.SupportBundleSpec, error) {
|
||||
v2spec := &troubleshootv1beta2.SupportBundleSpec{
|
||||
Uri: v3spec.Uri,
|
||||
RunHostCollectorsInPod: v3spec.RunHostCollectorsInPod,
|
||||
}
|
||||
|
||||
// Convert collectors
|
||||
if v3spec.Collectors != nil {
|
||||
v2collectors := make([]*troubleshootv1beta2.Collect, 0, len(v3spec.Collectors))
|
||||
for _, v3collector := range v3spec.Collectors {
|
||||
v2collector, err := convertCollector(ctx, v3collector, client, defaultNamespace)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert collector: %w", err)
|
||||
}
|
||||
v2collectors = append(v2collectors, v2collector)
|
||||
}
|
||||
v2spec.Collectors = v2collectors
|
||||
}
|
||||
|
||||
// TODO: Convert AfterCollection, HostCollectors, Analyzers, HostAnalyzers when v1beta3 support is expanded
|
||||
|
||||
return v2spec, nil
|
||||
}
|
||||
|
||||
// convertCollector converts a v1beta3 Collect to v1beta2 Collect
|
||||
func convertCollector(
|
||||
ctx context.Context,
|
||||
v3collector *Collect,
|
||||
client kubernetes.Interface,
|
||||
defaultNamespace string,
|
||||
) (*troubleshootv1beta2.Collect, error) {
|
||||
v2collector := &troubleshootv1beta2.Collect{}
|
||||
|
||||
// Convert database collectors
|
||||
if v3collector.Postgres != nil {
|
||||
db, err := convertDatabase(ctx, v3collector.Postgres, client, defaultNamespace)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert postgres collector: %w", err)
|
||||
}
|
||||
v2collector.Postgres = db
|
||||
}
|
||||
|
||||
if v3collector.Mysql != nil {
|
||||
db, err := convertDatabase(ctx, v3collector.Mysql, client, defaultNamespace)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert mysql collector: %w", err)
|
||||
}
|
||||
v2collector.Mysql = db
|
||||
}
|
||||
|
||||
if v3collector.Mssql != nil {
|
||||
db, err := convertDatabase(ctx, v3collector.Mssql, client, defaultNamespace)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert mssql collector: %w", err)
|
||||
}
|
||||
v2collector.Mssql = db
|
||||
}
|
||||
|
||||
if v3collector.Redis != nil {
|
||||
db, err := convertDatabase(ctx, v3collector.Redis, client, defaultNamespace)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert redis collector: %w", err)
|
||||
}
|
||||
v2collector.Redis = db
|
||||
}
|
||||
|
||||
// TODO: Add conversion for other collector types as v1beta3 support expands
|
||||
|
||||
return v2collector, nil
|
||||
}
|
||||
|
||||
// convertDatabase converts a v1beta3 Database to v1beta2 Database
|
||||
func convertDatabase(
|
||||
ctx context.Context,
|
||||
v3db *Database,
|
||||
client kubernetes.Interface,
|
||||
defaultNamespace string,
|
||||
) (*troubleshootv1beta2.Database, error) {
|
||||
// Resolve URI
|
||||
uri, err := ResolveStringOrValueFrom(ctx, v3db.URI, client, defaultNamespace)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve database URI: %w", err)
|
||||
}
|
||||
|
||||
v2db := &troubleshootv1beta2.Database{
|
||||
CollectorMeta: troubleshootv1beta2.CollectorMeta{
|
||||
CollectorName: v3db.CollectorName,
|
||||
Exclude: v3db.Exclude,
|
||||
},
|
||||
URI: uri,
|
||||
Parameters: v3db.Parameters,
|
||||
}
|
||||
|
||||
// Convert TLS params if present
|
||||
if v3db.TLS != nil {
|
||||
tlsParams, err := convertTLSParams(ctx, v3db.TLS, client, defaultNamespace)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert TLS params: %w", err)
|
||||
}
|
||||
v2db.TLS = tlsParams
|
||||
}
|
||||
|
||||
return v2db, nil
|
||||
}
|
||||
|
||||
// convertTLSParams converts v1beta3 TLSParams to v1beta2 TLSParams
|
||||
func convertTLSParams(
|
||||
ctx context.Context,
|
||||
v3tls *TLSParams,
|
||||
client kubernetes.Interface,
|
||||
defaultNamespace string,
|
||||
) (*troubleshootv1beta2.TLSParams, error) {
|
||||
v2tls := &troubleshootv1beta2.TLSParams{
|
||||
SkipVerify: v3tls.SkipVerify,
|
||||
}
|
||||
|
||||
// Preserve v1beta2 Secret reference if present (backward compatibility)
|
||||
if v3tls.Secret != nil {
|
||||
v2tls.Secret = &troubleshootv1beta2.TLSSecret{
|
||||
Name: v3tls.Secret.Name,
|
||||
Namespace: v3tls.Secret.Namespace,
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve v1beta3 StringOrValueFrom fields
|
||||
caCert, err := ResolveStringOrValueFrom(ctx, v3tls.CACert, client, defaultNamespace)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve CA cert: %w", err)
|
||||
}
|
||||
v2tls.CACert = caCert
|
||||
|
||||
clientCert, err := ResolveStringOrValueFrom(ctx, v3tls.ClientCert, client, defaultNamespace)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve client cert: %w", err)
|
||||
}
|
||||
v2tls.ClientCert = clientCert
|
||||
|
||||
clientKey, err := ResolveStringOrValueFrom(ctx, v3tls.ClientKey, client, defaultNamespace)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve client key: %w", err)
|
||||
}
|
||||
v2tls.ClientKey = clientKey
|
||||
|
||||
return v2tls, nil
|
||||
}
|
||||
@@ -1,386 +0,0 @@
|
||||
package v1beta3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestConvertToV1Beta2WithResolution_PostgresWithLiteralValue(t *testing.T) {
|
||||
client := fake.NewSimpleClientset()
|
||||
uri := "postgresql://user:pass@localhost:5432/db"
|
||||
|
||||
v3spec := &SupportBundleSpec{
|
||||
Collectors: []*Collect{
|
||||
{
|
||||
Postgres: &Database{
|
||||
URI: StringOrValueFrom{
|
||||
Value: &uri,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
v2spec, err := ConvertToV1Beta2WithResolution(context.Background(), v3spec, client, "default")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, v2spec)
|
||||
require.Len(t, v2spec.Collectors, 1)
|
||||
require.NotNil(t, v2spec.Collectors[0].Postgres)
|
||||
assert.Equal(t, "postgresql://user:pass@localhost:5432/db", v2spec.Collectors[0].Postgres.URI)
|
||||
}
|
||||
|
||||
func TestConvertToV1Beta2WithResolution_PostgresWithSecretRef(t *testing.T) {
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "db-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"uri": []byte("postgresql://user:secret-pass@db.example.com:5432/mydb"),
|
||||
},
|
||||
}
|
||||
|
||||
client := fake.NewSimpleClientset(secret)
|
||||
|
||||
v3spec := &SupportBundleSpec{
|
||||
Collectors: []*Collect{
|
||||
{
|
||||
Postgres: &Database{
|
||||
URI: StringOrValueFrom{
|
||||
ValueFrom: &ValueFromSource{
|
||||
SecretKeyRef: &SecretKeyRef{
|
||||
Name: "db-secret",
|
||||
Key: "uri",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
v2spec, err := ConvertToV1Beta2WithResolution(context.Background(), v3spec, client, "default")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, v2spec)
|
||||
require.Len(t, v2spec.Collectors, 1)
|
||||
require.NotNil(t, v2spec.Collectors[0].Postgres)
|
||||
assert.Equal(t, "postgresql://user:secret-pass@db.example.com:5432/mydb", v2spec.Collectors[0].Postgres.URI)
|
||||
}
|
||||
|
||||
func TestConvertToV1Beta2WithResolution_PostgresWithTLS(t *testing.T) {
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tls-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"ca.crt": []byte("-----BEGIN CERTIFICATE-----\nCA_CERT_DATA\n-----END CERTIFICATE-----"),
|
||||
"client.crt": []byte("-----BEGIN CERTIFICATE-----\nCLIENT_CERT_DATA\n-----END CERTIFICATE-----"),
|
||||
"client.key": []byte("-----BEGIN PRIVATE KEY-----\nCLIENT_KEY_DATA\n-----END PRIVATE KEY-----"),
|
||||
},
|
||||
}
|
||||
|
||||
client := fake.NewSimpleClientset(secret)
|
||||
uri := "postgresql://user:pass@localhost:5432/db"
|
||||
|
||||
v3spec := &SupportBundleSpec{
|
||||
Collectors: []*Collect{
|
||||
{
|
||||
Postgres: &Database{
|
||||
URI: StringOrValueFrom{
|
||||
Value: &uri,
|
||||
},
|
||||
TLS: &TLSParams{
|
||||
CACert: StringOrValueFrom{
|
||||
ValueFrom: &ValueFromSource{
|
||||
SecretKeyRef: &SecretKeyRef{
|
||||
Name: "tls-secret",
|
||||
Key: "ca.crt",
|
||||
},
|
||||
},
|
||||
},
|
||||
ClientCert: StringOrValueFrom{
|
||||
ValueFrom: &ValueFromSource{
|
||||
SecretKeyRef: &SecretKeyRef{
|
||||
Name: "tls-secret",
|
||||
Key: "client.crt",
|
||||
},
|
||||
},
|
||||
},
|
||||
ClientKey: StringOrValueFrom{
|
||||
ValueFrom: &ValueFromSource{
|
||||
SecretKeyRef: &SecretKeyRef{
|
||||
Name: "tls-secret",
|
||||
Key: "client.key",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
v2spec, err := ConvertToV1Beta2WithResolution(context.Background(), v3spec, client, "default")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, v2spec)
|
||||
require.Len(t, v2spec.Collectors, 1)
|
||||
require.NotNil(t, v2spec.Collectors[0].Postgres)
|
||||
require.NotNil(t, v2spec.Collectors[0].Postgres.TLS)
|
||||
|
||||
assert.Equal(t, "-----BEGIN CERTIFICATE-----\nCA_CERT_DATA\n-----END CERTIFICATE-----", v2spec.Collectors[0].Postgres.TLS.CACert)
|
||||
assert.Equal(t, "-----BEGIN CERTIFICATE-----\nCLIENT_CERT_DATA\n-----END CERTIFICATE-----", v2spec.Collectors[0].Postgres.TLS.ClientCert)
|
||||
assert.Equal(t, "-----BEGIN PRIVATE KEY-----\nCLIENT_KEY_DATA\n-----END PRIVATE KEY-----", v2spec.Collectors[0].Postgres.TLS.ClientKey)
|
||||
}
|
||||
|
||||
func TestConvertToV1Beta2WithResolution_MultipleDatabases(t *testing.T) {
|
||||
pgSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "postgres-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"uri": []byte("postgresql://user:pass@pg.example.com:5432/db"),
|
||||
},
|
||||
}
|
||||
|
||||
mysqlSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "mysql-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"uri": []byte("mysql://user:pass@mysql.example.com:3306/db"),
|
||||
},
|
||||
}
|
||||
|
||||
redisSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "redis-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"uri": []byte("redis://redis.example.com:6379"),
|
||||
},
|
||||
}
|
||||
|
||||
client := fake.NewSimpleClientset(pgSecret, mysqlSecret, redisSecret)
|
||||
|
||||
v3spec := &SupportBundleSpec{
|
||||
Collectors: []*Collect{
|
||||
{
|
||||
Postgres: &Database{
|
||||
URI: StringOrValueFrom{
|
||||
ValueFrom: &ValueFromSource{
|
||||
SecretKeyRef: &SecretKeyRef{
|
||||
Name: "postgres-secret",
|
||||
Key: "uri",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Mysql: &Database{
|
||||
URI: StringOrValueFrom{
|
||||
ValueFrom: &ValueFromSource{
|
||||
SecretKeyRef: &SecretKeyRef{
|
||||
Name: "mysql-secret",
|
||||
Key: "uri",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Redis: &Database{
|
||||
URI: StringOrValueFrom{
|
||||
ValueFrom: &ValueFromSource{
|
||||
SecretKeyRef: &SecretKeyRef{
|
||||
Name: "redis-secret",
|
||||
Key: "uri",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
v2spec, err := ConvertToV1Beta2WithResolution(context.Background(), v3spec, client, "default")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, v2spec)
|
||||
require.Len(t, v2spec.Collectors, 3)
|
||||
|
||||
require.NotNil(t, v2spec.Collectors[0].Postgres)
|
||||
assert.Equal(t, "postgresql://user:pass@pg.example.com:5432/db", v2spec.Collectors[0].Postgres.URI)
|
||||
|
||||
require.NotNil(t, v2spec.Collectors[1].Mysql)
|
||||
assert.Equal(t, "mysql://user:pass@mysql.example.com:3306/db", v2spec.Collectors[1].Mysql.URI)
|
||||
|
||||
require.NotNil(t, v2spec.Collectors[2].Redis)
|
||||
assert.Equal(t, "redis://redis.example.com:6379", v2spec.Collectors[2].Redis.URI)
|
||||
}
|
||||
|
||||
func TestConvertToV1Beta2WithResolution_SecretNotFound(t *testing.T) {
|
||||
client := fake.NewSimpleClientset()
|
||||
|
||||
v3spec := &SupportBundleSpec{
|
||||
Collectors: []*Collect{
|
||||
{
|
||||
Postgres: &Database{
|
||||
URI: StringOrValueFrom{
|
||||
ValueFrom: &ValueFromSource{
|
||||
SecretKeyRef: &SecretKeyRef{
|
||||
Name: "nonexistent-secret",
|
||||
Key: "uri",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := ConvertToV1Beta2WithResolution(context.Background(), v3spec, client, "default")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to convert collector")
|
||||
assert.Contains(t, err.Error(), "failed to resolve database URI")
|
||||
}
|
||||
|
||||
func TestConvertToV1Beta2WithResolution_PreservesCollectorMeta(t *testing.T) {
|
||||
client := fake.NewSimpleClientset()
|
||||
uri := "postgresql://user:pass@localhost:5432/db"
|
||||
|
||||
v3spec := &SupportBundleSpec{
|
||||
Collectors: []*Collect{
|
||||
{
|
||||
Postgres: &Database{
|
||||
CollectorMeta: CollectorMeta{
|
||||
CollectorName: "my-postgres-collector",
|
||||
},
|
||||
URI: StringOrValueFrom{
|
||||
Value: &uri,
|
||||
},
|
||||
Parameters: []string{"sslmode=require"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
v2spec, err := ConvertToV1Beta2WithResolution(context.Background(), v3spec, client, "default")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, v2spec)
|
||||
require.Len(t, v2spec.Collectors, 1)
|
||||
require.NotNil(t, v2spec.Collectors[0].Postgres)
|
||||
|
||||
assert.Equal(t, "my-postgres-collector", v2spec.Collectors[0].Postgres.CollectorName)
|
||||
assert.Equal(t, []string{"sslmode=require"}, v2spec.Collectors[0].Postgres.Parameters)
|
||||
}
|
||||
|
||||
func TestConvertToV1Beta2WithResolution_TLSBackwardCompatibility(t *testing.T) {
|
||||
client := fake.NewSimpleClientset()
|
||||
uri := "postgresql://user:pass@localhost:5432/db"
|
||||
|
||||
v3spec := &SupportBundleSpec{
|
||||
Collectors: []*Collect{
|
||||
{
|
||||
Postgres: &Database{
|
||||
URI: StringOrValueFrom{
|
||||
Value: &uri,
|
||||
},
|
||||
TLS: &TLSParams{
|
||||
SkipVerify: true,
|
||||
Secret: &TLSSecret{
|
||||
Name: "old-tls-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
v2spec, err := ConvertToV1Beta2WithResolution(context.Background(), v3spec, client, "default")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, v2spec)
|
||||
require.Len(t, v2spec.Collectors, 1)
|
||||
require.NotNil(t, v2spec.Collectors[0].Postgres)
|
||||
require.NotNil(t, v2spec.Collectors[0].Postgres.TLS)
|
||||
|
||||
assert.True(t, v2spec.Collectors[0].Postgres.TLS.SkipVerify)
|
||||
require.NotNil(t, v2spec.Collectors[0].Postgres.TLS.Secret)
|
||||
assert.Equal(t, "old-tls-secret", v2spec.Collectors[0].Postgres.TLS.Secret.Name)
|
||||
assert.Equal(t, "default", v2spec.Collectors[0].Postgres.TLS.Secret.Namespace)
|
||||
}
|
||||
|
||||
func TestConvertToV1Beta2WithResolution_EmptySpec(t *testing.T) {
|
||||
client := fake.NewSimpleClientset()
|
||||
|
||||
v3spec := &SupportBundleSpec{}
|
||||
|
||||
v2spec, err := ConvertToV1Beta2WithResolution(context.Background(), v3spec, client, "default")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, v2spec)
|
||||
assert.Nil(t, v2spec.Collectors)
|
||||
}
|
||||
|
||||
func TestConvertDatabase_AllDatabaseTypes(t *testing.T) {
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "db-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"uri": []byte("test-uri"),
|
||||
},
|
||||
}
|
||||
|
||||
client := fake.NewSimpleClientset(secret)
|
||||
|
||||
v3db := &Database{
|
||||
URI: StringOrValueFrom{
|
||||
ValueFrom: &ValueFromSource{
|
||||
SecretKeyRef: &SecretKeyRef{
|
||||
Name: "db-secret",
|
||||
Key: "uri",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test that the same database struct works for all DB types
|
||||
ctx := context.Background()
|
||||
|
||||
pgDB, err := convertDatabase(ctx, v3db, client, "default")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "test-uri", pgDB.URI)
|
||||
|
||||
mysqlDB, err := convertDatabase(ctx, v3db, client, "default")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "test-uri", mysqlDB.URI)
|
||||
|
||||
mssqlDB, err := convertDatabase(ctx, v3db, client, "default")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "test-uri", mssqlDB.URI)
|
||||
|
||||
redisDB, err := convertDatabase(ctx, v3db, client, "default")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "test-uri", redisDB.URI)
|
||||
}
|
||||
|
||||
// Helper function to convert v2spec back to ensure type compatibility
|
||||
func ensureV2SpecCompatibility(v2spec *troubleshootv1beta2.SupportBundleSpec) {
|
||||
// This function just exists to ensure the types are compatible
|
||||
// If this compiles, we know the conversion produces valid v1beta2 types
|
||||
_ = v2spec.Uri
|
||||
_ = v2spec.Collectors
|
||||
_ = v2spec.Analyzers
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +k8s:defaulter-gen=TypeMeta
|
||||
// +groupName=troubleshoot.sh
|
||||
|
||||
// Package v1beta3 is the v1beta3 version of the API.
|
||||
package v1beta3
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
Copyright 2019 Replicated, Inc..
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// NOTE: Boilerplate only. Ignore this file.
|
||||
|
||||
// Package v1beta3 contains API Schema definitions for the troubleshoot v1beta3 API group
|
||||
// +k8s:openapi-gen=true
|
||||
// +k8s:deepcopy-gen=package,register
|
||||
// +k8s:conversion-gen=github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot
|
||||
// +k8s:defaulter-gen=TypeMeta
|
||||
// +groupName=troubleshoot.sh
|
||||
package v1beta3
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/controller-runtime/pkg/scheme"
|
||||
)
|
||||
|
||||
var (
|
||||
// SchemeGroupVersion is group version used to register these objects
|
||||
SchemeGroupVersion = schema.GroupVersion{Group: "troubleshoot.sh", Version: "v1beta3"}
|
||||
|
||||
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
|
||||
SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}
|
||||
|
||||
// AddToScheme is required by pkg/client/...
|
||||
AddToScheme = SchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
// Resource is required by pkg/client/listers/...
|
||||
func Resource(resource string) schema.GroupResource {
|
||||
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package v1beta3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
// ResolveStringOrValueFrom resolves a StringOrValueFrom to its actual string value
|
||||
// by fetching from Secrets or ConfigMaps as needed.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: Context for the resolution operation
|
||||
// - sov: The StringOrValueFrom to resolve
|
||||
// - client: Kubernetes client for fetching Secrets/ConfigMaps
|
||||
// - defaultNamespace: Namespace to use when not specified in the reference
|
||||
//
|
||||
// Returns:
|
||||
// - The resolved string value
|
||||
// - An error if resolution fails (unless Optional is true)
|
||||
func ResolveStringOrValueFrom(
|
||||
ctx context.Context,
|
||||
sov StringOrValueFrom,
|
||||
client kubernetes.Interface,
|
||||
defaultNamespace string,
|
||||
) (string, error) {
|
||||
// If Value is directly specified, use it
|
||||
if sov.Value != nil {
|
||||
return *sov.Value, nil
|
||||
}
|
||||
|
||||
// If ValueFrom is not specified, return empty string
|
||||
if sov.ValueFrom == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Resolve from SecretKeyRef
|
||||
if sov.ValueFrom.SecretKeyRef != nil {
|
||||
return resolveSecretKeyRef(ctx, sov.ValueFrom.SecretKeyRef, client, defaultNamespace)
|
||||
}
|
||||
|
||||
// Resolve from ConfigMapKeyRef
|
||||
if sov.ValueFrom.ConfigMapKeyRef != nil {
|
||||
return resolveConfigMapKeyRef(ctx, sov.ValueFrom.ConfigMapKeyRef, client, defaultNamespace)
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// resolveSecretKeyRef fetches a value from a Kubernetes Secret
|
||||
func resolveSecretKeyRef(
|
||||
ctx context.Context,
|
||||
ref *SecretKeyRef,
|
||||
client kubernetes.Interface,
|
||||
defaultNamespace string,
|
||||
) (string, error) {
|
||||
namespace := ref.Namespace
|
||||
if namespace == "" {
|
||||
namespace = defaultNamespace
|
||||
}
|
||||
|
||||
secret, err := client.CoreV1().Secrets(namespace).Get(ctx, ref.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if isOptional(ref.Optional) {
|
||||
return "", nil
|
||||
}
|
||||
return "", fmt.Errorf("failed to get secret %s/%s: %w", namespace, ref.Name, err)
|
||||
}
|
||||
|
||||
value, ok := secret.Data[ref.Key]
|
||||
if !ok {
|
||||
if isOptional(ref.Optional) {
|
||||
return "", nil
|
||||
}
|
||||
return "", fmt.Errorf("key %q not found in secret %s/%s", ref.Key, namespace, ref.Name)
|
||||
}
|
||||
|
||||
return string(value), nil
|
||||
}
|
||||
|
||||
// resolveConfigMapKeyRef fetches a value from a Kubernetes ConfigMap
|
||||
func resolveConfigMapKeyRef(
|
||||
ctx context.Context,
|
||||
ref *ConfigMapKeyRef,
|
||||
client kubernetes.Interface,
|
||||
defaultNamespace string,
|
||||
) (string, error) {
|
||||
namespace := ref.Namespace
|
||||
if namespace == "" {
|
||||
namespace = defaultNamespace
|
||||
}
|
||||
|
||||
configMap, err := client.CoreV1().ConfigMaps(namespace).Get(ctx, ref.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if isOptional(ref.Optional) {
|
||||
return "", nil
|
||||
}
|
||||
return "", fmt.Errorf("failed to get configmap %s/%s: %w", namespace, ref.Name, err)
|
||||
}
|
||||
|
||||
value, ok := configMap.Data[ref.Key]
|
||||
if !ok {
|
||||
if isOptional(ref.Optional) {
|
||||
return "", nil
|
||||
}
|
||||
return "", fmt.Errorf("key %q not found in configmap %s/%s", ref.Key, namespace, ref.Name)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// isOptional checks if the optional flag is set to true
|
||||
func isOptional(optional *bool) bool {
|
||||
return optional != nil && *optional
|
||||
}
|
||||
@@ -1,333 +0,0 @@
|
||||
package v1beta3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestResolveStringOrValueFrom_LiteralValue(t *testing.T) {
|
||||
client := fake.NewSimpleClientset()
|
||||
value := "literal-value"
|
||||
|
||||
sov := StringOrValueFrom{
|
||||
Value: &value,
|
||||
}
|
||||
|
||||
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "literal-value", result)
|
||||
}
|
||||
|
||||
func TestResolveStringOrValueFrom_EmptyValue(t *testing.T) {
|
||||
client := fake.NewSimpleClientset()
|
||||
|
||||
sov := StringOrValueFrom{}
|
||||
|
||||
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", result)
|
||||
}
|
||||
|
||||
func TestResolveStringOrValueFrom_SecretKeyRef(t *testing.T) {
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"password": []byte("super-secret-password"),
|
||||
},
|
||||
}
|
||||
|
||||
client := fake.NewSimpleClientset(secret)
|
||||
|
||||
sov := StringOrValueFrom{
|
||||
ValueFrom: &ValueFromSource{
|
||||
SecretKeyRef: &SecretKeyRef{
|
||||
Name: "test-secret",
|
||||
Key: "password",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "super-secret-password", result)
|
||||
}
|
||||
|
||||
func TestResolveStringOrValueFrom_SecretKeyRef_WithNamespace(t *testing.T) {
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-secret",
|
||||
Namespace: "custom-namespace",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"password": []byte("secret-from-custom-ns"),
|
||||
},
|
||||
}
|
||||
|
||||
client := fake.NewSimpleClientset(secret)
|
||||
|
||||
sov := StringOrValueFrom{
|
||||
ValueFrom: &ValueFromSource{
|
||||
SecretKeyRef: &SecretKeyRef{
|
||||
Name: "test-secret",
|
||||
Key: "password",
|
||||
Namespace: "custom-namespace",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "secret-from-custom-ns", result)
|
||||
}
|
||||
|
||||
func TestResolveStringOrValueFrom_SecretKeyRef_NotFound(t *testing.T) {
|
||||
client := fake.NewSimpleClientset()
|
||||
|
||||
sov := StringOrValueFrom{
|
||||
ValueFrom: &ValueFromSource{
|
||||
SecretKeyRef: &SecretKeyRef{
|
||||
Name: "nonexistent-secret",
|
||||
Key: "password",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to get secret")
|
||||
assert.Equal(t, "", result)
|
||||
}
|
||||
|
||||
func TestResolveStringOrValueFrom_SecretKeyRef_KeyNotFound(t *testing.T) {
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"password": []byte("secret-value"),
|
||||
},
|
||||
}
|
||||
|
||||
client := fake.NewSimpleClientset(secret)
|
||||
|
||||
sov := StringOrValueFrom{
|
||||
ValueFrom: &ValueFromSource{
|
||||
SecretKeyRef: &SecretKeyRef{
|
||||
Name: "test-secret",
|
||||
Key: "nonexistent-key",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "key \"nonexistent-key\" not found")
|
||||
assert.Equal(t, "", result)
|
||||
}
|
||||
|
||||
func TestResolveStringOrValueFrom_SecretKeyRef_Optional(t *testing.T) {
|
||||
client := fake.NewSimpleClientset()
|
||||
optional := true
|
||||
|
||||
sov := StringOrValueFrom{
|
||||
ValueFrom: &ValueFromSource{
|
||||
SecretKeyRef: &SecretKeyRef{
|
||||
Name: "nonexistent-secret",
|
||||
Key: "password",
|
||||
Optional: &optional,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", result)
|
||||
}
|
||||
|
||||
func TestResolveStringOrValueFrom_SecretKeyRef_OptionalKeyNotFound(t *testing.T) {
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"password": []byte("secret-value"),
|
||||
},
|
||||
}
|
||||
|
||||
client := fake.NewSimpleClientset(secret)
|
||||
optional := true
|
||||
|
||||
sov := StringOrValueFrom{
|
||||
ValueFrom: &ValueFromSource{
|
||||
SecretKeyRef: &SecretKeyRef{
|
||||
Name: "test-secret",
|
||||
Key: "nonexistent-key",
|
||||
Optional: &optional,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", result)
|
||||
}
|
||||
|
||||
func TestResolveStringOrValueFrom_ConfigMapKeyRef(t *testing.T) {
|
||||
configMap := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-configmap",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"config-key": "config-value",
|
||||
},
|
||||
}
|
||||
|
||||
client := fake.NewSimpleClientset(configMap)
|
||||
|
||||
sov := StringOrValueFrom{
|
||||
ValueFrom: &ValueFromSource{
|
||||
ConfigMapKeyRef: &ConfigMapKeyRef{
|
||||
Name: "test-configmap",
|
||||
Key: "config-key",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "config-value", result)
|
||||
}
|
||||
|
||||
func TestResolveStringOrValueFrom_ConfigMapKeyRef_WithNamespace(t *testing.T) {
|
||||
configMap := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-configmap",
|
||||
Namespace: "custom-namespace",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"config-key": "config-from-custom-ns",
|
||||
},
|
||||
}
|
||||
|
||||
client := fake.NewSimpleClientset(configMap)
|
||||
|
||||
sov := StringOrValueFrom{
|
||||
ValueFrom: &ValueFromSource{
|
||||
ConfigMapKeyRef: &ConfigMapKeyRef{
|
||||
Name: "test-configmap",
|
||||
Key: "config-key",
|
||||
Namespace: "custom-namespace",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "config-from-custom-ns", result)
|
||||
}
|
||||
|
||||
func TestResolveStringOrValueFrom_ConfigMapKeyRef_NotFound(t *testing.T) {
|
||||
client := fake.NewSimpleClientset()
|
||||
|
||||
sov := StringOrValueFrom{
|
||||
ValueFrom: &ValueFromSource{
|
||||
ConfigMapKeyRef: &ConfigMapKeyRef{
|
||||
Name: "nonexistent-configmap",
|
||||
Key: "config-key",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to get configmap")
|
||||
assert.Equal(t, "", result)
|
||||
}
|
||||
|
||||
func TestResolveStringOrValueFrom_ConfigMapKeyRef_KeyNotFound(t *testing.T) {
|
||||
configMap := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-configmap",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"config-key": "config-value",
|
||||
},
|
||||
}
|
||||
|
||||
client := fake.NewSimpleClientset(configMap)
|
||||
|
||||
sov := StringOrValueFrom{
|
||||
ValueFrom: &ValueFromSource{
|
||||
ConfigMapKeyRef: &ConfigMapKeyRef{
|
||||
Name: "test-configmap",
|
||||
Key: "nonexistent-key",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "key \"nonexistent-key\" not found")
|
||||
assert.Equal(t, "", result)
|
||||
}
|
||||
|
||||
func TestResolveStringOrValueFrom_ConfigMapKeyRef_Optional(t *testing.T) {
|
||||
client := fake.NewSimpleClientset()
|
||||
optional := true
|
||||
|
||||
sov := StringOrValueFrom{
|
||||
ValueFrom: &ValueFromSource{
|
||||
ConfigMapKeyRef: &ConfigMapKeyRef{
|
||||
Name: "nonexistent-configmap",
|
||||
Key: "config-key",
|
||||
Optional: &optional,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", result)
|
||||
}
|
||||
|
||||
func TestResolveStringOrValueFrom_ConfigMapKeyRef_OptionalKeyNotFound(t *testing.T) {
|
||||
configMap := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-configmap",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"config-key": "config-value",
|
||||
},
|
||||
}
|
||||
|
||||
client := fake.NewSimpleClientset(configMap)
|
||||
optional := true
|
||||
|
||||
sov := StringOrValueFrom{
|
||||
ValueFrom: &ValueFromSource{
|
||||
ConfigMapKeyRef: &ConfigMapKeyRef{
|
||||
Name: "test-configmap",
|
||||
Key: "nonexistent-key",
|
||||
Optional: &optional,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", result)
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package v1beta3
|
||||
|
||||
// StringOrValueFrom represents a string value that can either be specified
|
||||
// directly or sourced from a Kubernetes Secret or ConfigMap
|
||||
type StringOrValueFrom struct {
|
||||
// Value is a literal string value
|
||||
// +optional
|
||||
Value *string `json:"value,omitempty" yaml:"value,omitempty"`
|
||||
|
||||
// ValueFrom is a reference to a value in a Secret or ConfigMap
|
||||
// +optional
|
||||
ValueFrom *ValueFromSource `json:"valueFrom,omitempty" yaml:"valueFrom,omitempty"`
|
||||
}
|
||||
|
||||
// ValueFromSource represents the source of a value from a Secret or ConfigMap
|
||||
type ValueFromSource struct {
|
||||
// SecretKeyRef references a key in a Secret
|
||||
// +optional
|
||||
SecretKeyRef *SecretKeyRef `json:"secretKeyRef,omitempty" yaml:"secretKeyRef,omitempty"`
|
||||
|
||||
// ConfigMapKeyRef references a key in a ConfigMap
|
||||
// +optional
|
||||
ConfigMapKeyRef *ConfigMapKeyRef `json:"configMapKeyRef,omitempty" yaml:"configMapKeyRef,omitempty"`
|
||||
}
|
||||
|
||||
// SecretKeyRef references a specific key in a Kubernetes Secret
|
||||
type SecretKeyRef struct {
|
||||
// Name is the name of the Secret
|
||||
Name string `json:"name" yaml:"name"`
|
||||
|
||||
// Key is the key within the Secret to read
|
||||
Key string `json:"key" yaml:"key"`
|
||||
|
||||
// Namespace is the namespace of the Secret
|
||||
// If not specified, defaults to the namespace where the SupportBundle is running
|
||||
// +optional
|
||||
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
|
||||
|
||||
// Optional specifies whether the Secret must exist
|
||||
// If true and the Secret or key doesn't exist, resolves to empty string
|
||||
// If false (default) and the Secret or key doesn't exist, resolution fails
|
||||
// +optional
|
||||
Optional *bool `json:"optional,omitempty" yaml:"optional,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigMapKeyRef references a specific key in a Kubernetes ConfigMap
|
||||
type ConfigMapKeyRef struct {
|
||||
// Name is the name of the ConfigMap
|
||||
Name string `json:"name" yaml:"name"`
|
||||
|
||||
// Key is the key within the ConfigMap to read
|
||||
Key string `json:"key" yaml:"key"`
|
||||
|
||||
// Namespace is the namespace of the ConfigMap
|
||||
// If not specified, defaults to the namespace where the SupportBundle is running
|
||||
// +optional
|
||||
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
|
||||
|
||||
// Optional specifies whether the ConfigMap must exist
|
||||
// If true and the ConfigMap or key doesn't exist, resolves to empty string
|
||||
// If false (default) and the ConfigMap or key doesn't exist, resolution fails
|
||||
// +optional
|
||||
Optional *bool `json:"optional,omitempty" yaml:"optional,omitempty"`
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
Copyright 2019 Replicated, Inc..
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1beta3
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// SupportBundleSpec defines the desired state of SupportBundle
|
||||
type SupportBundleSpec struct {
|
||||
AfterCollection []*AfterCollection `json:"afterCollection,omitempty" yaml:"afterCollection,omitempty"`
|
||||
Collectors []*Collect `json:"collectors,omitempty" yaml:"collectors,omitempty"`
|
||||
HostCollectors []*HostCollect `json:"hostCollectors,omitempty" yaml:"hostCollectors,omitempty"`
|
||||
Analyzers []*Analyze `json:"analyzers,omitempty" yaml:"analyzers,omitempty"`
|
||||
HostAnalyzers []*HostAnalyze `json:"hostAnalyzers,omitempty" yaml:"hostAnalyzers,omitempty"`
|
||||
// URI optionally defines a location which is the source of this spec to allow updating of the spec at runtime
|
||||
Uri string `json:"uri,omitempty" yaml:"uri,omitempty"`
|
||||
RunHostCollectorsInPod bool `json:"runHostCollectorsInPod,omitempty" yaml:"runHostCollectorsInPod,omitempty"`
|
||||
}
|
||||
|
||||
// SupportBundleStatus defines the observed state of SupportBundle
|
||||
type SupportBundleStatus struct {
|
||||
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
|
||||
// Important: Run "make" to regenerate code after modifying this file
|
||||
}
|
||||
|
||||
// +genclient
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
// SupportBundle is the Schema for the SupportBundles API
|
||||
// +k8s:openapi-gen=true
|
||||
type SupportBundle struct {
|
||||
metav1.TypeMeta `json:",inline" yaml:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"`
|
||||
|
||||
Spec SupportBundleSpec `json:"spec,omitempty" yaml:"spec,omitempty"`
|
||||
Status SupportBundleStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
// SupportBundleList contains a list of SupportBundle
|
||||
type SupportBundleList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []SupportBundle `json:"items"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&SupportBundle{}, &SupportBundleList{})
|
||||
}
|
||||
@@ -1,441 +0,0 @@
|
||||
//go:build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2019 Replicated, Inc..
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Code generated by controller-gen. DO NOT EDIT.
|
||||
|
||||
package v1beta3
|
||||
|
||||
import (
|
||||
"github.com/replicatedhq/troubleshoot/pkg/multitype"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *AfterCollection) DeepCopyInto(out *AfterCollection) {
|
||||
*out = *in
|
||||
in.CollectorMeta.DeepCopyInto(&out.CollectorMeta)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AfterCollection.
|
||||
func (in *AfterCollection) DeepCopy() *AfterCollection {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(AfterCollection)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Analyze) DeepCopyInto(out *Analyze) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Analyze.
|
||||
func (in *Analyze) DeepCopy() *Analyze {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Analyze)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Collect) DeepCopyInto(out *Collect) {
|
||||
*out = *in
|
||||
if in.Postgres != nil {
|
||||
in, out := &in.Postgres, &out.Postgres
|
||||
*out = new(Database)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Mssql != nil {
|
||||
in, out := &in.Mssql, &out.Mssql
|
||||
*out = new(Database)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Mysql != nil {
|
||||
in, out := &in.Mysql, &out.Mysql
|
||||
*out = new(Database)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Redis != nil {
|
||||
in, out := &in.Redis, &out.Redis
|
||||
*out = new(Database)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Collect.
|
||||
func (in *Collect) DeepCopy() *Collect {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Collect)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CollectorMeta) DeepCopyInto(out *CollectorMeta) {
|
||||
*out = *in
|
||||
if in.Exclude != nil {
|
||||
in, out := &in.Exclude, &out.Exclude
|
||||
*out = new(multitype.BoolOrString)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CollectorMeta.
|
||||
func (in *CollectorMeta) DeepCopy() *CollectorMeta {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CollectorMeta)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ConfigMapKeyRef) DeepCopyInto(out *ConfigMapKeyRef) {
|
||||
*out = *in
|
||||
if in.Optional != nil {
|
||||
in, out := &in.Optional, &out.Optional
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigMapKeyRef.
|
||||
func (in *ConfigMapKeyRef) DeepCopy() *ConfigMapKeyRef {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ConfigMapKeyRef)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Database) DeepCopyInto(out *Database) {
|
||||
*out = *in
|
||||
in.CollectorMeta.DeepCopyInto(&out.CollectorMeta)
|
||||
in.URI.DeepCopyInto(&out.URI)
|
||||
if in.Parameters != nil {
|
||||
in, out := &in.Parameters, &out.Parameters
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.TLS != nil {
|
||||
in, out := &in.TLS, &out.TLS
|
||||
*out = new(TLSParams)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Database.
|
||||
func (in *Database) DeepCopy() *Database {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Database)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *HostAnalyze) DeepCopyInto(out *HostAnalyze) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostAnalyze.
|
||||
func (in *HostAnalyze) DeepCopy() *HostAnalyze {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(HostAnalyze)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *HostCollect) DeepCopyInto(out *HostCollect) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostCollect.
|
||||
func (in *HostCollect) DeepCopy() *HostCollect {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(HostCollect)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SecretKeyRef) DeepCopyInto(out *SecretKeyRef) {
|
||||
*out = *in
|
||||
if in.Optional != nil {
|
||||
in, out := &in.Optional, &out.Optional
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretKeyRef.
|
||||
func (in *SecretKeyRef) DeepCopy() *SecretKeyRef {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SecretKeyRef)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *StringOrValueFrom) DeepCopyInto(out *StringOrValueFrom) {
|
||||
*out = *in
|
||||
if in.Value != nil {
|
||||
in, out := &in.Value, &out.Value
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
if in.ValueFrom != nil {
|
||||
in, out := &in.ValueFrom, &out.ValueFrom
|
||||
*out = new(ValueFromSource)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StringOrValueFrom.
|
||||
func (in *StringOrValueFrom) DeepCopy() *StringOrValueFrom {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(StringOrValueFrom)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SupportBundle) DeepCopyInto(out *SupportBundle) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
out.Status = in.Status
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SupportBundle.
|
||||
func (in *SupportBundle) DeepCopy() *SupportBundle {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SupportBundle)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *SupportBundle) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SupportBundleList) DeepCopyInto(out *SupportBundleList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]SupportBundle, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SupportBundleList.
|
||||
func (in *SupportBundleList) DeepCopy() *SupportBundleList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SupportBundleList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *SupportBundleList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SupportBundleSpec) DeepCopyInto(out *SupportBundleSpec) {
|
||||
*out = *in
|
||||
if in.AfterCollection != nil {
|
||||
in, out := &in.AfterCollection, &out.AfterCollection
|
||||
*out = make([]*AfterCollection, len(*in))
|
||||
for i := range *in {
|
||||
if (*in)[i] != nil {
|
||||
in, out := &(*in)[i], &(*out)[i]
|
||||
*out = new(AfterCollection)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
}
|
||||
if in.Collectors != nil {
|
||||
in, out := &in.Collectors, &out.Collectors
|
||||
*out = make([]*Collect, len(*in))
|
||||
for i := range *in {
|
||||
if (*in)[i] != nil {
|
||||
in, out := &(*in)[i], &(*out)[i]
|
||||
*out = new(Collect)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
}
|
||||
if in.HostCollectors != nil {
|
||||
in, out := &in.HostCollectors, &out.HostCollectors
|
||||
*out = make([]*HostCollect, len(*in))
|
||||
for i := range *in {
|
||||
if (*in)[i] != nil {
|
||||
in, out := &(*in)[i], &(*out)[i]
|
||||
*out = new(HostCollect)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
}
|
||||
if in.Analyzers != nil {
|
||||
in, out := &in.Analyzers, &out.Analyzers
|
||||
*out = make([]*Analyze, len(*in))
|
||||
for i := range *in {
|
||||
if (*in)[i] != nil {
|
||||
in, out := &(*in)[i], &(*out)[i]
|
||||
*out = new(Analyze)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
}
|
||||
if in.HostAnalyzers != nil {
|
||||
in, out := &in.HostAnalyzers, &out.HostAnalyzers
|
||||
*out = make([]*HostAnalyze, len(*in))
|
||||
for i := range *in {
|
||||
if (*in)[i] != nil {
|
||||
in, out := &(*in)[i], &(*out)[i]
|
||||
*out = new(HostAnalyze)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SupportBundleSpec.
|
||||
func (in *SupportBundleSpec) DeepCopy() *SupportBundleSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SupportBundleSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SupportBundleStatus) DeepCopyInto(out *SupportBundleStatus) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SupportBundleStatus.
|
||||
func (in *SupportBundleStatus) DeepCopy() *SupportBundleStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SupportBundleStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *TLSParams) DeepCopyInto(out *TLSParams) {
|
||||
*out = *in
|
||||
if in.Secret != nil {
|
||||
in, out := &in.Secret, &out.Secret
|
||||
*out = new(TLSSecret)
|
||||
**out = **in
|
||||
}
|
||||
in.CACert.DeepCopyInto(&out.CACert)
|
||||
in.ClientCert.DeepCopyInto(&out.ClientCert)
|
||||
in.ClientKey.DeepCopyInto(&out.ClientKey)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSParams.
|
||||
func (in *TLSParams) DeepCopy() *TLSParams {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(TLSParams)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *TLSSecret) DeepCopyInto(out *TLSSecret) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSSecret.
|
||||
func (in *TLSSecret) DeepCopy() *TLSSecret {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(TLSSecret)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ValueFromSource) DeepCopyInto(out *ValueFromSource) {
|
||||
*out = *in
|
||||
if in.SecretKeyRef != nil {
|
||||
in, out := &in.SecretKeyRef, &out.SecretKeyRef
|
||||
*out = new(SecretKeyRef)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.ConfigMapKeyRef != nil {
|
||||
in, out := &in.ConfigMapKeyRef, &out.ConfigMapKeyRef
|
||||
*out = new(ConfigMapKeyRef)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValueFromSource.
|
||||
func (in *ValueFromSource) DeepCopy() *ValueFromSource {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ValueFromSource)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
@@ -20,7 +20,6 @@ package scheme
|
||||
import (
|
||||
troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
troubleshootv1beta3 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta3"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
schema "k8s.io/apimachinery/pkg/runtime/schema"
|
||||
@@ -34,7 +33,6 @@ var ParameterCodec = runtime.NewParameterCodec(Scheme)
|
||||
var localSchemeBuilder = runtime.SchemeBuilder{
|
||||
troubleshootv1beta1.AddToScheme,
|
||||
troubleshootv1beta2.AddToScheme,
|
||||
troubleshootv1beta3.AddToScheme,
|
||||
}
|
||||
|
||||
// AddToScheme adds all types of this clientset into the given scheme. This allows composition
|
||||
|
||||
@@ -232,7 +232,7 @@ func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (Coll
|
||||
// replicasets
|
||||
replicasets, replicasetsErrors := replicasets(ctx, client, namespaceNames)
|
||||
for k, v := range replicasets {
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_REPLICASETS, k), bytes.NewBuffer(v))
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_STATEFULSETS), k), bytes.NewBuffer(v))
|
||||
}
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_REPLICASETS)), marshalErrors(replicasetsErrors))
|
||||
|
||||
@@ -370,9 +370,9 @@ func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (Coll
|
||||
// endpointslices
|
||||
endpointslices, endpointslicesErrors := endpointslices(ctx, client, namespaceNames)
|
||||
for k, v := range endpointslices {
|
||||
_ = output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_ENDPOINTSLICES, k), bytes.NewBuffer(v))
|
||||
_ = output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_ENDPOINTSICES, k), bytes.NewBuffer(v))
|
||||
}
|
||||
_ = output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_ENDPOINTSLICES)), marshalErrors(endpointslicesErrors))
|
||||
_ = output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_ENDPOINTSICES)), marshalErrors(endpointslicesErrors))
|
||||
|
||||
// Service Accounts
|
||||
servicesAccounts, servicesAccountsErrors := serviceAccounts(ctx, client, namespaceNames)
|
||||
@@ -400,13 +400,6 @@ func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (Coll
|
||||
}
|
||||
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_CONFIGMAPS)), marshalErrors(configMapsErrors))
|
||||
|
||||
// Replicated License
|
||||
licenseData, licenseErr := replicatedLicense(ctx, client, namespaceNames)
|
||||
if licenseErr == nil {
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_REPLICATED_LICENSE), bytes.NewBuffer(licenseData))
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
@@ -2183,68 +2176,3 @@ func storeCustomResource(name string, objects any, m map[string][]byte) error {
|
||||
m[fmt.Sprintf("%s.yaml", name)] = y
|
||||
return nil
|
||||
}
|
||||
|
||||
// replicatedLicense searches for the replicated secret across namespaces,
|
||||
// extracts the config.yaml field, and extracts the licenseID and appSlug.
|
||||
// Note: secret.Data already contains decoded bytes; no base64 decoding is required.
|
||||
func replicatedLicense(ctx context.Context, client *kubernetes.Clientset, namespaces []string) ([]byte, error) {
|
||||
// Structure to parse the config.yaml content
|
||||
type ConfigYAML struct {
|
||||
License string `yaml:"license"` // This is a YAML string containing the License object
|
||||
}
|
||||
|
||||
type LicenseSpec struct {
|
||||
LicenseID string `yaml:"licenseID"`
|
||||
AppSlug string `yaml:"appSlug"`
|
||||
}
|
||||
|
||||
type License struct {
|
||||
Spec LicenseSpec `yaml:"spec"`
|
||||
}
|
||||
|
||||
// Search through all namespaces for the replicated secret
|
||||
for _, namespace := range namespaces {
|
||||
secret, err := client.CoreV1().Secrets(namespace).Get(ctx, "replicated", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
// Secret not found in this namespace, continue to next
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract the config.yaml field from the secret data
|
||||
configYAMLBase64, exists := secret.Data["config.yaml"]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
configYAMLBytes := configYAMLBase64
|
||||
|
||||
// Parse the YAML to extract the license field
|
||||
var config ConfigYAML
|
||||
if err := yaml.Unmarshal(configYAMLBytes, &config); err != nil {
|
||||
// Malformed config in this namespace; try the next namespace
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse the license field (which is a YAML string) to extract licenseID and appSlug
|
||||
var license License
|
||||
if err := yaml.Unmarshal([]byte(config.License), &license); err != nil {
|
||||
// Malformed license in this namespace; try the next namespace
|
||||
continue
|
||||
}
|
||||
|
||||
// Return both licenseID and appSlug as JSON
|
||||
licenseData := map[string]string{
|
||||
"licenseID": license.Spec.LicenseID,
|
||||
"appSlug": license.Spec.AppSlug,
|
||||
}
|
||||
licenseJSON, err := json.Marshal(licenseData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal license data: %w", err)
|
||||
}
|
||||
|
||||
return licenseJSON, nil
|
||||
}
|
||||
|
||||
// No replicated secret with a parsable license found in any namespace
|
||||
return nil, fmt.Errorf("replicated secret with parsable license not found in any namespace")
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ func Test_ensureClusterResourcesFirst(t *testing.T) {
|
||||
list []*troubleshootv1beta2.Collect
|
||||
}{
|
||||
{
|
||||
name: "Reorg OK - clusterResources moved to front",
|
||||
name: "Reorg OK",
|
||||
want: []*troubleshootv1beta2.Collect{
|
||||
{
|
||||
ClusterResources: &troubleshootv1beta2.ClusterResources{},
|
||||
@@ -33,99 +33,6 @@ func Test_ensureClusterResourcesFirst(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Already first - no change",
|
||||
want: []*troubleshootv1beta2.Collect{
|
||||
{
|
||||
ClusterResources: &troubleshootv1beta2.ClusterResources{},
|
||||
},
|
||||
{
|
||||
Data: &troubleshootv1beta2.Data{},
|
||||
},
|
||||
{
|
||||
Secret: &troubleshootv1beta2.Secret{},
|
||||
},
|
||||
},
|
||||
list: []*troubleshootv1beta2.Collect{
|
||||
{
|
||||
ClusterResources: &troubleshootv1beta2.ClusterResources{},
|
||||
},
|
||||
{
|
||||
Data: &troubleshootv1beta2.Data{},
|
||||
},
|
||||
{
|
||||
Secret: &troubleshootv1beta2.Secret{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Multiple clusterResources - all moved to front",
|
||||
want: []*troubleshootv1beta2.Collect{
|
||||
{
|
||||
ClusterResources: &troubleshootv1beta2.ClusterResources{},
|
||||
},
|
||||
{
|
||||
ClusterResources: &troubleshootv1beta2.ClusterResources{},
|
||||
},
|
||||
{
|
||||
Data: &troubleshootv1beta2.Data{},
|
||||
},
|
||||
{
|
||||
Secret: &troubleshootv1beta2.Secret{},
|
||||
},
|
||||
},
|
||||
list: []*troubleshootv1beta2.Collect{
|
||||
{
|
||||
Data: &troubleshootv1beta2.Data{},
|
||||
},
|
||||
{
|
||||
ClusterResources: &troubleshootv1beta2.ClusterResources{},
|
||||
},
|
||||
{
|
||||
Secret: &troubleshootv1beta2.Secret{},
|
||||
},
|
||||
{
|
||||
ClusterResources: &troubleshootv1beta2.ClusterResources{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "No clusterResources - no change",
|
||||
want: []*troubleshootv1beta2.Collect{
|
||||
{
|
||||
Data: &troubleshootv1beta2.Data{},
|
||||
},
|
||||
{
|
||||
Secret: &troubleshootv1beta2.Secret{},
|
||||
},
|
||||
},
|
||||
list: []*troubleshootv1beta2.Collect{
|
||||
{
|
||||
Data: &troubleshootv1beta2.Data{},
|
||||
},
|
||||
{
|
||||
Secret: &troubleshootv1beta2.Secret{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Empty list - no change",
|
||||
want: []*troubleshootv1beta2.Collect{},
|
||||
list: []*troubleshootv1beta2.Collect{},
|
||||
},
|
||||
{
|
||||
name: "Only clusterResources - no change",
|
||||
want: []*troubleshootv1beta2.Collect{
|
||||
{
|
||||
ClusterResources: &troubleshootv1beta2.ClusterResources{},
|
||||
},
|
||||
},
|
||||
list: []*troubleshootv1beta2.Collect{
|
||||
{
|
||||
ClusterResources: &troubleshootv1beta2.ClusterResources{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
@@ -44,7 +44,8 @@ pwd=somethinggoeshere;`,
|
||||
want: map[string]string{
|
||||
"data/datacollectorname": ` 123
|
||||
another***HIDDEN***here
|
||||
pwd=***HIDDEN***;`,
|
||||
pwd=***HIDDEN***;
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -77,7 +78,8 @@ pwd=somethinggoeshere;`,
|
||||
want: map[string]string{
|
||||
"data/datacollectorname": `abc 123
|
||||
another***HIDDEN***here
|
||||
pwd=***HIDDEN***;`,
|
||||
pwd=***HIDDEN***;
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -110,7 +112,8 @@ pwd=somethinggoeshere;`,
|
||||
want: map[string]string{
|
||||
"data/datacollectorname": `abc 123
|
||||
another line here
|
||||
pwd=***HIDDEN***;`,
|
||||
pwd=***HIDDEN***;
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -146,7 +149,8 @@ pwd=somethinggoeshere;`,
|
||||
want: map[string]string{
|
||||
"data/datacollectorname": `abc 123
|
||||
another***HIDDEN***here
|
||||
pwd=***HIDDEN***;`,
|
||||
pwd=***HIDDEN***;
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -182,7 +186,8 @@ pwd=somethinggoeshere;`,
|
||||
want: map[string]string{
|
||||
"data/data/collectorname": `***HIDDEN*** ***HIDDEN***
|
||||
***HIDDEN*** line here
|
||||
pwd=***HIDDEN***;`,
|
||||
pwd=***HIDDEN***;
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -208,7 +213,8 @@ another line here`,
|
||||
},
|
||||
want: map[string]string{
|
||||
"data/datacollectorname": `abc 123
|
||||
another line here`,
|
||||
another line here
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -243,7 +249,8 @@ abc`,
|
||||
abc
|
||||
123
|
||||
xyz123
|
||||
abc`,
|
||||
abc
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
+3
-46
@@ -10,16 +10,9 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/discovery/cached/memory"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/restmapper"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
@@ -52,42 +45,6 @@ type VersionInfo struct {
|
||||
Values map[string]interface{} `json:"values,omitempty"`
|
||||
}
|
||||
|
||||
type configGetter struct {
|
||||
restConfig *rest.Config
|
||||
}
|
||||
|
||||
// ToDiscoveryClient implements genericclioptions.RESTClientGetter.
|
||||
func (c configGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
|
||||
discoveryClient, err := discovery.NewDiscoveryClientForConfig(c.restConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cached := memory.NewMemCacheClient(discoveryClient)
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
// ToRESTConfig implements genericclioptions.RESTClientGetter.
|
||||
func (c configGetter) ToRESTConfig() (*rest.Config, error) {
|
||||
return c.restConfig, nil
|
||||
}
|
||||
|
||||
// ToRESTMapper implements genericclioptions.RESTClientGetter.
|
||||
func (c configGetter) ToRESTMapper() (meta.RESTMapper, error) {
|
||||
discoveryClient, err := c.ToDiscoveryClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
|
||||
return mapper, nil
|
||||
}
|
||||
|
||||
// ToRawKubeConfigLoader implements genericclioptions.RESTClientGetter.
|
||||
func (c configGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig {
|
||||
return k8sutil.GetKubeconfig()
|
||||
}
|
||||
|
||||
var _ genericclioptions.RESTClientGetter = configGetter{}
|
||||
|
||||
func (c *CollectHelm) Title() string {
|
||||
return getCollectorName(c)
|
||||
}
|
||||
@@ -100,7 +57,7 @@ func (c *CollectHelm) Collect(progressChan chan<- interface{}) (CollectorResult,
|
||||
|
||||
output := NewResult()
|
||||
|
||||
releaseInfos, err := helmReleaseHistoryCollector(c.ClientConfig, c.Collector.ReleaseName, c.Collector.Namespace, c.Collector.CollectValues)
|
||||
releaseInfos, err := helmReleaseHistoryCollector(c.Collector.ReleaseName, c.Collector.Namespace, c.Collector.CollectValues)
|
||||
if err != nil {
|
||||
errsToMarhsal := []string{}
|
||||
for _, e := range err {
|
||||
@@ -131,12 +88,12 @@ func (c *CollectHelm) Collect(progressChan chan<- interface{}) (CollectorResult,
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func helmReleaseHistoryCollector(config *rest.Config, releaseName string, namespace string, collectValues bool) ([]ReleaseInfo, []error) {
|
||||
func helmReleaseHistoryCollector(releaseName string, namespace string, collectValues bool) ([]ReleaseInfo, []error) {
|
||||
var results []ReleaseInfo
|
||||
error_list := []error{}
|
||||
|
||||
actionConfig := new(action.Configuration)
|
||||
if err := actionConfig.Init(configGetter{config}, namespace, "", klog.V(2).Infof); err != nil {
|
||||
if err := actionConfig.Init(nil, namespace, "", klog.V(2).Infof); err != nil {
|
||||
return nil, []error{err}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,15 +9,15 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
imagedocker "github.com/containers/image/v5/docker"
|
||||
dockerref "github.com/containers/image/v5/docker/reference"
|
||||
"github.com/containers/image/v5/transports/alltransports"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/distribution/distribution/v3/registry/api/errcode"
|
||||
registryv2 "github.com/distribution/distribution/v3/registry/api/v2"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
imagedocker "go.podman.io/image/v5/docker"
|
||||
dockerref "go.podman.io/image/v5/docker/reference"
|
||||
"go.podman.io/image/v5/transports/alltransports"
|
||||
"go.podman.io/image/v5/types"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/containers/image/v5/transports/alltransports"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.podman.io/image/v5/transports/alltransports"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
|
||||
+1
-13
@@ -80,19 +80,7 @@ func DeterministicIDForCollector(collector *troubleshootv1beta2.Collect) string
|
||||
}
|
||||
|
||||
func selectorToString(selector []string) string {
|
||||
result := strings.Replace(strings.Join(selector, "-"), "=", "-", -1)
|
||||
// Sanitize characters that are invalid in Windows filenames: < > : " / \ | ? *
|
||||
// Replace them with underscores to ensure cross-platform compatibility
|
||||
result = strings.ReplaceAll(result, "*", "all")
|
||||
result = strings.ReplaceAll(result, "?", "_")
|
||||
result = strings.ReplaceAll(result, ":", "_")
|
||||
result = strings.ReplaceAll(result, "<", "_")
|
||||
result = strings.ReplaceAll(result, ">", "_")
|
||||
result = strings.ReplaceAll(result, "|", "_")
|
||||
result = strings.ReplaceAll(result, "\"", "_")
|
||||
result = strings.ReplaceAll(result, "/", "_")
|
||||
result = strings.ReplaceAll(result, "\\", "_")
|
||||
return result
|
||||
return strings.Replace(strings.Join(selector, "-"), "=", "-", -1)
|
||||
}
|
||||
|
||||
func pathToString(path string) string {
|
||||
|
||||
@@ -56,12 +56,11 @@ const (
|
||||
CLUSTER_RESOURCES_CLUSTER_ROLE_BINDINGS = "clusterrolebindings"
|
||||
CLUSTER_RESOURCES_PRIORITY_CLASS = "priorityclasses"
|
||||
CLUSTER_RESOURCES_ENDPOINTS = "endpoints"
|
||||
CLUSTER_RESOURCES_ENDPOINTSLICES = "endpointslices"
|
||||
CLUSTER_RESOURCES_ENDPOINTSICES = "endpointslices"
|
||||
CLUSTER_RESOURCES_SERVICE_ACCOUNTS = "serviceaccounts"
|
||||
CLUSTER_RESOURCES_LEASES = "leases"
|
||||
CLUSTER_RESOURCES_VOLUME_ATTACHMENTS = "volumeattachments"
|
||||
CLUSTER_RESOURCES_CONFIGMAPS = "configmaps"
|
||||
CLUSTER_RESOURCES_REPLICATED_LICENSE = "license.json"
|
||||
|
||||
// SelfSubjectRulesReview evaluation responses
|
||||
SELFSUBJECTRULESREVIEW_ERROR_AUTHORIZATION_WEBHOOK_UNSUPPORTED = "webhook authorizer does not support user rule resolution"
|
||||
|
||||
@@ -6,15 +6,11 @@ import (
|
||||
|
||||
// HasResource takes an api version and a kind of a resource and checks if the resource
|
||||
// is supported by the k8s api server.
|
||||
// This function handles partial results from ServerGroupsAndResources(): "The returned group and resource lists might be non-nil with partial
|
||||
// results even in the case of non-nil error."
|
||||
func HasResource(dc discovery.DiscoveryInterface, apiVersion, kind string) (bool, error) {
|
||||
_, apiLists, err := dc.ServerGroupsAndResources()
|
||||
|
||||
if apiLists == nil {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Compare the resource api version and kind and find the resource.
|
||||
for _, apiList := range apiLists {
|
||||
if apiList.GroupVersion == apiVersion {
|
||||
@@ -25,6 +21,5 @@ func HasResource(dc discovery.DiscoveryInterface, apiVersion, kind string) (bool
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, err
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/discovery"
|
||||
fakediscovery "k8s.io/client-go/discovery/fake"
|
||||
fakeclientset "k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
@@ -81,232 +78,3 @@ func TestHasResource(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHasResourceWithPartialDiscoveryFailure verifies that HasResource correctly handles
|
||||
// partial discovery failures where ServerGroupsAndResources() returns both an error AND
|
||||
// partial results (non-nil apiLists). This simulates real Kubernetes behavior when some
|
||||
// API groups fail to load but others succeed.
|
||||
func TestHasResourceWithPartialDiscoveryFailure(t *testing.T) {
|
||||
testKind := "Foo"
|
||||
testKindGroupVersion := "v1"
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
apiResourceList []*metav1.APIResourceList
|
||||
discoveryError error
|
||||
wantResult bool
|
||||
wantError bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "resource found in partial results with discovery error",
|
||||
apiResourceList: []*metav1.APIResourceList{
|
||||
{
|
||||
GroupVersion: "v1",
|
||||
APIResources: []metav1.APIResource{
|
||||
{
|
||||
Kind: "Foo",
|
||||
},
|
||||
{
|
||||
Kind: "Bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
discoveryError: &discovery.ErrGroupDiscoveryFailed{
|
||||
Groups: map[schema.GroupVersion]error{
|
||||
{Group: "apps", Version: "v1"}: errors.New("failed to retrieve apps/v1"),
|
||||
},
|
||||
},
|
||||
wantResult: true,
|
||||
wantError: false,
|
||||
description: "Should return (true, nil) when resource exists in partial results despite discovery error",
|
||||
},
|
||||
{
|
||||
name: "resource not found in partial results with discovery error",
|
||||
apiResourceList: []*metav1.APIResourceList{
|
||||
{
|
||||
GroupVersion: "v1",
|
||||
APIResources: []metav1.APIResource{
|
||||
{
|
||||
Kind: "Bar",
|
||||
},
|
||||
{
|
||||
Kind: "Baz",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
discoveryError: &discovery.ErrGroupDiscoveryFailed{
|
||||
Groups: map[schema.GroupVersion]error{
|
||||
{Group: "apps", Version: "v1"}: errors.New("failed to retrieve apps/v1"),
|
||||
},
|
||||
},
|
||||
wantResult: false,
|
||||
wantError: true,
|
||||
description: "Should return (false, error) when resource not in partial results and discovery error exists",
|
||||
},
|
||||
{
|
||||
name: "nil api resource list with discovery error",
|
||||
apiResourceList: nil,
|
||||
discoveryError: &discovery.ErrGroupDiscoveryFailed{
|
||||
Groups: map[schema.GroupVersion]error{
|
||||
{Group: "apps", Version: "v1"}: errors.New("failed to retrieve apps/v1"),
|
||||
},
|
||||
},
|
||||
wantResult: false,
|
||||
wantError: true,
|
||||
description: "Should return (false, error) when apiLists is nil and discovery error exists",
|
||||
},
|
||||
{
|
||||
name: "empty api resource list with discovery error",
|
||||
apiResourceList: []*metav1.APIResourceList{},
|
||||
discoveryError: &discovery.ErrGroupDiscoveryFailed{
|
||||
Groups: map[schema.GroupVersion]error{
|
||||
{Group: "apps", Version: "v1"}: errors.New("failed to retrieve apps/v1"),
|
||||
},
|
||||
},
|
||||
wantResult: false,
|
||||
wantError: true,
|
||||
description: "Should return (false, error) when apiLists is empty and discovery error exists",
|
||||
},
|
||||
{
|
||||
name: "multiple groups with partial results and discovery error",
|
||||
apiResourceList: []*metav1.APIResourceList{
|
||||
{
|
||||
GroupVersion: "v1",
|
||||
APIResources: []metav1.APIResource{
|
||||
{
|
||||
Kind: "Pod",
|
||||
},
|
||||
{
|
||||
Kind: "Service",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
GroupVersion: "v2",
|
||||
APIResources: []metav1.APIResource{
|
||||
{
|
||||
Kind: "Foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
discoveryError: &discovery.ErrGroupDiscoveryFailed{
|
||||
Groups: map[schema.GroupVersion]error{
|
||||
{Group: "apps", Version: "v1"}: errors.New("failed to retrieve apps/v1"),
|
||||
{Group: "batch", Version: "v1beta1"}: errors.New("failed to retrieve batch/v1beta1"),
|
||||
},
|
||||
},
|
||||
wantResult: false,
|
||||
wantError: true,
|
||||
description: "Should return (false, error) when resource not found across multiple partial groups with discovery error",
|
||||
},
|
||||
{
|
||||
name: "resource found with different version in partial results with discovery error",
|
||||
apiResourceList: []*metav1.APIResourceList{
|
||||
{
|
||||
GroupVersion: "v2",
|
||||
APIResources: []metav1.APIResource{
|
||||
{
|
||||
Kind: "Foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
discoveryError: &discovery.ErrGroupDiscoveryFailed{
|
||||
Groups: map[schema.GroupVersion]error{
|
||||
{Group: "apps", Version: "v1"}: errors.New("failed to retrieve apps/v1"),
|
||||
},
|
||||
},
|
||||
wantResult: false,
|
||||
wantError: true,
|
||||
description: "Should return (false, error) when resource exists with different version in partial results",
|
||||
},
|
||||
{
|
||||
name: "generic error with partial results containing resource",
|
||||
apiResourceList: []*metav1.APIResourceList{
|
||||
{
|
||||
GroupVersion: "v1",
|
||||
APIResources: []metav1.APIResource{
|
||||
{
|
||||
Kind: "Foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
discoveryError: errors.New("connection timeout"),
|
||||
wantResult: true,
|
||||
wantError: false,
|
||||
description: "Should return (true, nil) when resource exists in partial results even with generic error",
|
||||
},
|
||||
{
|
||||
name: "generic error without resource in partial results",
|
||||
apiResourceList: []*metav1.APIResourceList{
|
||||
{
|
||||
GroupVersion: "v1",
|
||||
APIResources: []metav1.APIResource{
|
||||
{
|
||||
Kind: "Bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
discoveryError: errors.New("connection timeout"),
|
||||
wantResult: false,
|
||||
wantError: true,
|
||||
description: "Should return (false, error) when resource not in partial results with generic error",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
client := fakeclientset.NewSimpleClientset()
|
||||
fakeDiscovery, ok := client.Discovery().(*fakediscovery.FakeDiscovery)
|
||||
if !ok {
|
||||
t.Fatalf("could not convert Discovery() to *FakeDiscovery")
|
||||
}
|
||||
|
||||
// Configure the fake discovery to return both resources and error
|
||||
fakeDiscovery.Resources = tc.apiResourceList
|
||||
|
||||
// Create a mock discovery interface that returns both error and partial results
|
||||
mockDiscovery := &mockDiscoveryWithPartialFailure{
|
||||
FakeDiscovery: fakeDiscovery,
|
||||
errorToReturn: tc.discoveryError,
|
||||
}
|
||||
|
||||
exists, err := HasResource(mockDiscovery, testKindGroupVersion, testKind)
|
||||
|
||||
// Verify error expectation
|
||||
if tc.wantError && err == nil {
|
||||
t.Errorf("%s: expected error but got nil", tc.description)
|
||||
}
|
||||
if !tc.wantError && err != nil {
|
||||
t.Errorf("%s: expected no error but got: %v", tc.description, err)
|
||||
}
|
||||
|
||||
// Verify result expectation
|
||||
if exists != tc.wantResult {
|
||||
t.Errorf("%s: unexpected result for HasResource:\n\t(WANT) %t\n\t(GOT) %t", tc.description, tc.wantResult, exists)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// mockDiscoveryWithPartialFailure wraps FakeDiscovery to simulate partial discovery failures
|
||||
// where ServerGroupsAndResources() returns both an error AND partial results.
|
||||
type mockDiscoveryWithPartialFailure struct {
|
||||
*fakediscovery.FakeDiscovery
|
||||
errorToReturn error
|
||||
}
|
||||
|
||||
// ServerGroupsAndResources simulates the Kubernetes API behavior where partial results
|
||||
// can be returned even when an error occurs. This happens when some API groups fail to
|
||||
// load but others succeed.
|
||||
func (m *mockDiscoveryWithPartialFailure) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
|
||||
groups, resources, _ := m.FakeDiscovery.ServerGroupsAndResources()
|
||||
return groups, resources, m.errorToReturn
|
||||
}
|
||||
|
||||
@@ -25,26 +25,6 @@ const (
|
||||
PodStatusReasonInitCrashLoopBackOff PodStatusReason = "Init:CrashLoopBackOff"
|
||||
)
|
||||
|
||||
// isNativeSidecar checks if an init container is a native sidecar.
|
||||
// Native sidecars are init containers with restartPolicy: Always (Kubernetes 1.28+).
|
||||
// They run continuously alongside main containers, unlike traditional init containers
|
||||
// which must complete before main containers start.
|
||||
func isNativeSidecar(pod *corev1.Pod, initContainerIndex int) bool {
|
||||
// Bounds check - ensure the index is valid
|
||||
if initContainerIndex >= len(pod.Spec.InitContainers) {
|
||||
return false
|
||||
}
|
||||
|
||||
initContainer := pod.Spec.InitContainers[initContainerIndex]
|
||||
|
||||
// Check if RestartPolicy is set to Always
|
||||
if initContainer.RestartPolicy != nil && *initContainer.RestartPolicy == corev1.ContainerRestartPolicyAlways {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// reference: https://github.com/kubernetes/kubernetes/blob/e8fcd0de98d50f4019561a6b7a0287f5c059267a/pkg/printers/internalversion/printers.go#L741
|
||||
func GetPodStatusReason(pod *corev1.Pod) (string, string) {
|
||||
reason := string(pod.Status.Phase)
|
||||
@@ -75,11 +55,6 @@ func GetPodStatusReason(pod *corev1.Pod) (string, string) {
|
||||
case container.State.Waiting != nil && len(container.State.Waiting.Reason) > 0 && container.State.Waiting.Reason != "PodInitializing":
|
||||
reason = "Init:" + container.State.Waiting.Reason
|
||||
initializing = true
|
||||
case isNativeSidecar(pod, i) && container.State.Running != nil:
|
||||
// Native sidecar running - this is expected, not stuck initializing.
|
||||
// Native sidecars (init containers with restartPolicy: Always) are designed
|
||||
// to run continuously, so a Running state means successful initialization.
|
||||
continue
|
||||
default:
|
||||
reason = fmt.Sprintf("Init:%d/%d", i, len(pod.Spec.InitContainers))
|
||||
initializing = true
|
||||
|
||||
+1
-284
@@ -1,11 +1,9 @@
|
||||
package k8sutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsPodUnhealthy(t *testing.T) {
|
||||
@@ -101,284 +99,3 @@ func TestIsPodUnhealthy(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetPodStatusReason_HealthyNativeSidecar tests that a pod with a running native sidecar
|
||||
// is correctly reported as "Running" and not stuck initializing.
|
||||
func TestGetPodStatusReason_HealthyNativeSidecar(t *testing.T) {
|
||||
startedTrue := true
|
||||
restartPolicyAlways := corev1.ContainerRestartPolicyAlways
|
||||
|
||||
pod := &corev1.Pod{
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "istio-proxy",
|
||||
Image: "istio/proxyv2:1.20",
|
||||
RestartPolicy: &restartPolicyAlways, // Native sidecar!
|
||||
},
|
||||
},
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "app",
|
||||
Image: "myapp:latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
Phase: corev1.PodRunning,
|
||||
Conditions: []corev1.PodCondition{
|
||||
{
|
||||
Type: corev1.PodReady,
|
||||
Status: corev1.ConditionTrue,
|
||||
},
|
||||
},
|
||||
InitContainerStatuses: []corev1.ContainerStatus{
|
||||
{
|
||||
Name: "istio-proxy",
|
||||
Ready: true,
|
||||
Started: &startedTrue,
|
||||
State: corev1.ContainerState{
|
||||
Running: &corev1.ContainerStateRunning{
|
||||
StartedAt: metav1.Now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ContainerStatuses: []corev1.ContainerStatus{
|
||||
{
|
||||
Name: "app",
|
||||
Ready: true,
|
||||
Started: &startedTrue,
|
||||
State: corev1.ContainerState{
|
||||
Running: &corev1.ContainerStateRunning{
|
||||
StartedAt: metav1.Now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reason, message := GetPodStatusReason(pod)
|
||||
|
||||
// Should report as Running, not Init:0/1
|
||||
if reason != "Running" {
|
||||
t.Errorf("Expected reason 'Running', got '%s'", reason)
|
||||
}
|
||||
|
||||
if message != "" {
|
||||
t.Errorf("Expected empty message, got '%s'", message)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsPodUnhealthy_HealthyNativeSidecar tests that a pod with a healthy running native sidecar
|
||||
// is not marked as unhealthy.
|
||||
func TestIsPodUnhealthy_HealthyNativeSidecar(t *testing.T) {
|
||||
startedTrue := true
|
||||
restartPolicyAlways := corev1.ContainerRestartPolicyAlways
|
||||
|
||||
pod := &corev1.Pod{
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "istio-proxy",
|
||||
RestartPolicy: &restartPolicyAlways,
|
||||
},
|
||||
},
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "app",
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
Phase: corev1.PodRunning,
|
||||
Conditions: []corev1.PodCondition{
|
||||
{
|
||||
Type: corev1.PodReady,
|
||||
Status: corev1.ConditionTrue,
|
||||
},
|
||||
},
|
||||
InitContainerStatuses: []corev1.ContainerStatus{
|
||||
{
|
||||
Name: "istio-proxy",
|
||||
Ready: true,
|
||||
Started: &startedTrue,
|
||||
State: corev1.ContainerState{
|
||||
Running: &corev1.ContainerStateRunning{},
|
||||
},
|
||||
},
|
||||
},
|
||||
ContainerStatuses: []corev1.ContainerStatus{
|
||||
{
|
||||
Name: "app",
|
||||
Ready: true,
|
||||
Started: &startedTrue,
|
||||
State: corev1.ContainerState{
|
||||
Running: &corev1.ContainerStateRunning{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
unhealthy := IsPodUnhealthy(pod)
|
||||
|
||||
if unhealthy {
|
||||
t.Error("Pod with healthy native sidecar should not be marked as unhealthy")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetPodStatusReason_TraditionalInitAndNativeSidecar tests that a pod with both
|
||||
// a completed traditional init container and a running native sidecar is reported as "Running".
|
||||
func TestGetPodStatusReason_TraditionalInitAndNativeSidecar(t *testing.T) {
|
||||
startedTrue := true
|
||||
restartPolicyAlways := corev1.ContainerRestartPolicyAlways
|
||||
|
||||
pod := &corev1.Pod{
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "init-setup",
|
||||
// No RestartPolicy = traditional init container
|
||||
},
|
||||
{
|
||||
Name: "istio-proxy",
|
||||
RestartPolicy: &restartPolicyAlways, // Native sidecar
|
||||
},
|
||||
},
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "app",
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
Phase: corev1.PodRunning,
|
||||
Conditions: []corev1.PodCondition{
|
||||
{
|
||||
Type: corev1.PodReady,
|
||||
Status: corev1.ConditionTrue,
|
||||
},
|
||||
},
|
||||
InitContainerStatuses: []corev1.ContainerStatus{
|
||||
{
|
||||
Name: "init-setup",
|
||||
State: corev1.ContainerState{
|
||||
Terminated: &corev1.ContainerStateTerminated{
|
||||
ExitCode: 0,
|
||||
Reason: "Completed",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "istio-proxy",
|
||||
Ready: true,
|
||||
Started: &startedTrue,
|
||||
State: corev1.ContainerState{
|
||||
Running: &corev1.ContainerStateRunning{},
|
||||
},
|
||||
},
|
||||
},
|
||||
ContainerStatuses: []corev1.ContainerStatus{
|
||||
{
|
||||
Name: "app",
|
||||
Ready: true,
|
||||
Started: &startedTrue,
|
||||
State: corev1.ContainerState{
|
||||
Running: &corev1.ContainerStateRunning{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reason, _ := GetPodStatusReason(pod)
|
||||
|
||||
if reason != "Running" {
|
||||
t.Errorf("Expected reason 'Running', got '%s'", reason)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetPodStatusReason_NativeSidecarCrashLoopBackOff tests that a native sidecar
|
||||
// in CrashLoopBackOff is still correctly detected as an error.
|
||||
func TestGetPodStatusReason_NativeSidecarCrashLoopBackOff(t *testing.T) {
|
||||
restartPolicyAlways := corev1.ContainerRestartPolicyAlways
|
||||
|
||||
pod := &corev1.Pod{
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "istio-proxy",
|
||||
RestartPolicy: &restartPolicyAlways,
|
||||
},
|
||||
},
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "app",
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
Phase: corev1.PodPending,
|
||||
InitContainerStatuses: []corev1.ContainerStatus{
|
||||
{
|
||||
Name: "istio-proxy",
|
||||
Ready: false,
|
||||
State: corev1.ContainerState{
|
||||
Waiting: &corev1.ContainerStateWaiting{
|
||||
Reason: "CrashLoopBackOff",
|
||||
Message: "Back-off 5m0s restarting failed container",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reason, _ := GetPodStatusReason(pod)
|
||||
|
||||
// Should still catch the error
|
||||
if reason != "Init:CrashLoopBackOff" {
|
||||
t.Errorf("Expected reason 'Init:CrashLoopBackOff', got '%s'", reason)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetPodStatusReason_TraditionalInitStuck tests that a traditional init container
|
||||
// that is stuck running is still correctly detected as stuck initializing.
|
||||
func TestGetPodStatusReason_TraditionalInitStuck(t *testing.T) {
|
||||
pod := &corev1.Pod{
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "init-setup",
|
||||
// No RestartPolicy = traditional init
|
||||
},
|
||||
},
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "app",
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
Phase: corev1.PodPending,
|
||||
InitContainerStatuses: []corev1.ContainerStatus{
|
||||
{
|
||||
Name: "init-setup",
|
||||
Ready: false,
|
||||
State: corev1.ContainerState{
|
||||
Running: &corev1.ContainerStateRunning{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reason, _ := GetPodStatusReason(pod)
|
||||
|
||||
// Traditional init running = stuck
|
||||
if reason != "Init:0/1" {
|
||||
t.Errorf("Expected reason 'Init:0/1', got '%s'", reason)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package k8sutil
|
||||
|
||||
import corev1 "k8s.io/api/core/v1"
|
||||
|
||||
// TaintExists checks if the given taint exists in list of taints. Returns true
|
||||
// if exists false otherwise.
|
||||
//
|
||||
// Copied from k8s.io/kubernetes/pkg/util/taints so we don't have to import
|
||||
// k8s.io/kubernetes.
|
||||
func TaintExists(taints []corev1.Taint, taintToFind *corev1.Taint) bool {
|
||||
for _, taint := range taints {
|
||||
if taint.MatchTaint(taintToFind) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,345 +0,0 @@
|
||||
package lint
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
)
|
||||
|
||||
func applyFixesInMemory(content string, result LintResult) (string, bool, error) {
|
||||
fixed := false
|
||||
newContent := content
|
||||
lines := strings.Split(newContent, "\n")
|
||||
|
||||
// Fix A: If templating errors exist in a v1beta2 file, upgrade apiVersion to v1beta3 (minimal, deterministic)
|
||||
hasTemplateInV1beta2 := false
|
||||
for _, e := range result.Errors {
|
||||
if e.Field == "template" && strings.Contains(e.Message, "not supported in v1beta2") {
|
||||
hasTemplateInV1beta2 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasTemplateInV1beta2 {
|
||||
for i, line := range lines {
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "apiVersion:") && strings.Contains(line, constants.Troubleshootv1beta2Kind) {
|
||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||
lines[i] = indent + "apiVersion: " + constants.Troubleshootv1beta3Kind
|
||||
fixed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort errors by line number (descending) to avoid line number shifts when editing
|
||||
errorsByLine := make(map[int][]LintError)
|
||||
for _, err := range result.Errors {
|
||||
if err.Line > 0 {
|
||||
errorsByLine[err.Line] = append(errorsByLine[err.Line], err)
|
||||
}
|
||||
}
|
||||
|
||||
// Process errors line by line
|
||||
for lineNum, errs := range errorsByLine {
|
||||
if lineNum > len(lines) {
|
||||
continue
|
||||
}
|
||||
|
||||
line := lines[lineNum-1]
|
||||
originalLine := line
|
||||
|
||||
for _, err := range errs {
|
||||
// Fix 1: Add missing colon
|
||||
if strings.Contains(err.Message, "could not find expected ':'") {
|
||||
if !strings.Contains(line, ":") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||
line = indent + trimmed + ":"
|
||||
fixed = true
|
||||
}
|
||||
}
|
||||
|
||||
// Fix 2: Add missing leading dot in template expressions
|
||||
if strings.Contains(err.Message, "Template expression may be missing leading dot:") {
|
||||
// Extract the expression from the error message
|
||||
re := regexp.MustCompile(`Template expression may be missing leading dot: \{\{ (.+?) \}\}`)
|
||||
matches := re.FindStringSubmatch(err.Message)
|
||||
if len(matches) > 1 {
|
||||
badExpr := matches[1]
|
||||
// Add the leading dot
|
||||
fixedExpr := "." + badExpr
|
||||
// Replace in the line
|
||||
line = strings.Replace(line, "{{ "+badExpr+" }}", "{{ "+fixedExpr+" }}", 1)
|
||||
line = strings.Replace(line, "{{"+badExpr+"}}", "{{"+fixedExpr+"}}", 1)
|
||||
line = strings.Replace(line, "{{- "+badExpr+" }}", "{{- "+fixedExpr+" }}", 1)
|
||||
line = strings.Replace(line, "{{- "+badExpr+" -}}", "{{- "+fixedExpr+" -}}", 1)
|
||||
fixed = true
|
||||
}
|
||||
}
|
||||
|
||||
// Fix 3: Fix wrong apiVersion
|
||||
if strings.Contains(err.Message, "File must contain apiVersion:") && err.Field == "apiVersion" {
|
||||
if strings.Contains(line, "apiVersion:") && !strings.Contains(line, constants.Troubleshootv1beta3Kind) {
|
||||
// Replace existing apiVersion with correct one
|
||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||
line = indent + "apiVersion: " + constants.Troubleshootv1beta3Kind
|
||||
fixed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the line if it changed
|
||||
if line != originalLine {
|
||||
lines[lineNum-1] = line
|
||||
}
|
||||
}
|
||||
|
||||
// Fix B: Wrap mapping under required list fields (collectors, hostCollectors, analyzers)
|
||||
for _, err := range result.Errors {
|
||||
if strings.HasPrefix(err.Message, "Expected 'collectors' to be a list") {
|
||||
if wrapFirstChildAsList(&lines, "collectors:") {
|
||||
fixed = true
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(err.Message, "Expected 'hostCollectors' to be a list") {
|
||||
if wrapFirstChildAsList(&lines, "hostCollectors:") || convertScalarToEmptyList(&lines, "hostCollectors:") {
|
||||
fixed = true
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(err.Message, "Expected 'analyzers' to be a list") {
|
||||
if wrapFirstChildAsList(&lines, "analyzers:") {
|
||||
fixed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fix C: Add missing required fields with empty placeholders (non-assumptive)
|
||||
// Collectors
|
||||
for _, err := range result.Errors {
|
||||
if strings.HasPrefix(err.Message, "Missing required field '") && strings.Contains(err.Message, " for collector '") {
|
||||
// Parse field and collector type
|
||||
// e.g., Missing required field 'namespace' for collector 'ceph'
|
||||
fieldName := between(err.Message, "Missing required field '", "'")
|
||||
collectorType := betweenAfter(err.Message, "collector '", "'")
|
||||
if fieldName == "" || collectorType == "" {
|
||||
continue
|
||||
}
|
||||
// Only handle simple case where the list item is in {} form: "- type: {}"
|
||||
// Find the list item line from current content
|
||||
cur := strings.Join(lines, "\n")
|
||||
lineNum := findCollectorLine(cur, "collectors", indexFromField(err.Field))
|
||||
if lineNum > 0 {
|
||||
li := lineNum - 1
|
||||
if strings.Contains(lines[li], "- "+collectorType+": {}") {
|
||||
indent := lines[li][:len(lines[li])-len(strings.TrimLeft(lines[li], " \t"))]
|
||||
childIndent := indent + " "
|
||||
// choose placeholder: outcomes -> [] ; others -> ""
|
||||
placeholder := "\"\""
|
||||
if fieldName == "outcomes" {
|
||||
placeholder = "[]"
|
||||
}
|
||||
lines[li] = strings.Replace(lines[li], ": {}", ":\n"+childIndent+fieldName+": "+placeholder, 1)
|
||||
fixed = true
|
||||
} else if strings.Contains(lines[li], "- "+collectorType+":") {
|
||||
// Multi-line mapping; insert missing field under this item
|
||||
if insertMissingFieldUnderListItem(&lines, li, fieldName) {
|
||||
fixed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Analyzers
|
||||
for _, err := range result.Errors {
|
||||
if strings.HasPrefix(err.Message, "Missing required field '") && strings.Contains(err.Message, " for analyzer '") {
|
||||
fieldName := between(err.Message, "Missing required field '", "'")
|
||||
analyzerType := betweenAfter(err.Message, "analyzer '", "'")
|
||||
if fieldName == "" || analyzerType == "" {
|
||||
continue
|
||||
}
|
||||
cur := strings.Join(lines, "\n")
|
||||
lineNum := findAnalyzerLine(cur, indexFromField(err.Field))
|
||||
if lineNum > 0 {
|
||||
li := lineNum - 1
|
||||
if strings.Contains(lines[li], "- "+analyzerType+": {}") {
|
||||
indent := lines[li][:len(lines[li])-len(strings.TrimLeft(lines[li], " \t"))]
|
||||
childIndent := indent + " "
|
||||
placeholder := "\"\""
|
||||
if fieldName == "outcomes" {
|
||||
placeholder = "[]"
|
||||
}
|
||||
lines[li] = strings.Replace(lines[li], ": {}", ":\n"+childIndent+fieldName+": "+placeholder, 1)
|
||||
fixed = true
|
||||
} else if strings.Contains(lines[li], "- "+analyzerType+":") {
|
||||
if insertMissingFieldUnderListItem(&lines, li, fieldName) {
|
||||
fixed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return fixed content if changes were made
|
||||
if fixed {
|
||||
newContent = strings.Join(lines, "\n")
|
||||
return newContent, true, nil
|
||||
}
|
||||
|
||||
return content, false, nil
|
||||
}
|
||||
|
||||
// wrapFirstChildAsList prefixes the first child mapping line under the given key with '- '
|
||||
func wrapFirstChildAsList(lines *[]string, key string) bool {
|
||||
arr := *lines
|
||||
// find key line index
|
||||
baseIdx := -1
|
||||
for i, l := range arr {
|
||||
if strings.Contains(l, key) {
|
||||
baseIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if baseIdx == -1 {
|
||||
return false
|
||||
}
|
||||
baseIndent := arr[baseIdx][:len(arr[baseIdx])-len(strings.TrimLeft(arr[baseIdx], " \t"))]
|
||||
// find first child line with greater indent
|
||||
for j := baseIdx + 1; j < len(arr); j++ {
|
||||
line := arr[j]
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
// stop when indentation goes back to or less than base
|
||||
if !strings.HasPrefix(line, baseIndent+" ") && !strings.HasPrefix(line, baseIndent+"\t") {
|
||||
break
|
||||
}
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "- ") {
|
||||
// already a list
|
||||
return false
|
||||
}
|
||||
// prefix '- '
|
||||
childIndent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||
arr[j] = childIndent + "- " + strings.TrimSpace(line)
|
||||
*lines = arr
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// convertScalarToEmptyList changes `key: <scalar>` to `key: []` on the same line
|
||||
func convertScalarToEmptyList(lines *[]string, key string) bool {
|
||||
arr := *lines
|
||||
for i, l := range arr {
|
||||
trimmed := strings.TrimSpace(l)
|
||||
if strings.HasPrefix(trimmed, key) {
|
||||
// If already ends with ':' leave for wrapper; else replace value with []
|
||||
if strings.HasSuffix(trimmed, ":") {
|
||||
return false
|
||||
}
|
||||
// Replace everything after the first ':' with [] preserving indentation/key
|
||||
parts := strings.SplitN(l, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
arr[i] = parts[0] + ": []"
|
||||
*lines = arr
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// indexFromField extracts the numeric index from a path like spec.collectors[1] or spec.analyzers[0]
|
||||
func indexFromField(field string) int {
|
||||
// find [number]
|
||||
start := strings.Index(field, "[")
|
||||
end := strings.Index(field, "]")
|
||||
if start == -1 || end == -1 || end <= start+1 {
|
||||
return 0
|
||||
}
|
||||
numStr := field[start+1 : end]
|
||||
// naive parse
|
||||
n := 0
|
||||
for _, ch := range numStr {
|
||||
if ch < '0' || ch > '9' {
|
||||
return 0
|
||||
}
|
||||
n = n*10 + int(ch-'0')
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// insertMissingFieldUnderListItem inserts "fieldName: <placeholder>" under list item at startIdx
|
||||
// Placeholder is [] for outcomes, "" otherwise. Preserves indentation by using the next child indentation if available
|
||||
func insertMissingFieldUnderListItem(lines *[]string, startIdx int, fieldName string) bool {
|
||||
arr := *lines
|
||||
baseLine := arr[startIdx]
|
||||
baseIndent := baseLine[:len(baseLine)-len(strings.TrimLeft(baseLine, " \t"))]
|
||||
// Determine child indentation: prefer next non-empty line's indent if deeper than base
|
||||
childIndent := baseIndent + " "
|
||||
insertPos := startIdx + 1
|
||||
for j := startIdx + 1; j < len(arr); j++ {
|
||||
if strings.TrimSpace(arr[j]) == "" {
|
||||
insertPos = j + 1
|
||||
continue
|
||||
}
|
||||
lineIndent := arr[j][:len(arr[j])-len(strings.TrimLeft(arr[j], " \t"))]
|
||||
if len(lineIndent) > len(baseIndent) {
|
||||
childIndent = lineIndent
|
||||
insertPos = j
|
||||
}
|
||||
break
|
||||
}
|
||||
// Choose placeholder
|
||||
placeholder := "\"\""
|
||||
if fieldName == "outcomes" {
|
||||
placeholder = "[]"
|
||||
}
|
||||
// Insert new line
|
||||
newLine := childIndent + fieldName + ": " + placeholder
|
||||
// Avoid duplicate insert if the field already exists within this block
|
||||
for k := startIdx + 1; k < len(arr); k++ {
|
||||
if strings.TrimSpace(arr[k]) == "" {
|
||||
continue
|
||||
}
|
||||
// Stop when block ends (indentation returns to base or less)
|
||||
kIndent := arr[k][:len(arr[k])-len(strings.TrimLeft(arr[k], " \t"))]
|
||||
if len(kIndent) <= len(baseIndent) {
|
||||
break
|
||||
}
|
||||
if strings.HasPrefix(strings.TrimSpace(arr[k]), fieldName+":") {
|
||||
return false
|
||||
}
|
||||
}
|
||||
arr = append(arr[:insertPos], append([]string{newLine}, arr[insertPos:]...)...)
|
||||
*lines = arr
|
||||
return true
|
||||
}
|
||||
|
||||
// between extracts substring between prefix and suffix (first occurrences)
|
||||
func between(s, prefix, suffix string) string {
|
||||
i := strings.Index(s, prefix)
|
||||
if i == -1 {
|
||||
return ""
|
||||
}
|
||||
s2 := s[i+len(prefix):]
|
||||
j := strings.Index(s2, suffix)
|
||||
if j == -1 {
|
||||
return ""
|
||||
}
|
||||
return s2[:j]
|
||||
}
|
||||
|
||||
// betweenAfter extracts substring between prefix and suffix starting search after prefix
|
||||
func betweenAfter(s, prefix, suffix string) string {
|
||||
i := strings.Index(s, prefix)
|
||||
if i == -1 {
|
||||
return ""
|
||||
}
|
||||
s2 := s[i+len(prefix):]
|
||||
j := strings.Index(s2, suffix)
|
||||
if j == -1 {
|
||||
return ""
|
||||
}
|
||||
return s2[:j]
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package lint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FormatResults formats lint results for output
|
||||
func FormatResults(results []LintResult, format string) string {
|
||||
if format == "json" {
|
||||
return formatJSON(results)
|
||||
}
|
||||
return formatText(results)
|
||||
}
|
||||
|
||||
func formatText(results []LintResult) string {
|
||||
var output strings.Builder
|
||||
totalErrors := 0
|
||||
totalWarnings := 0
|
||||
|
||||
for _, result := range results {
|
||||
if len(result.Errors) == 0 && len(result.Warnings) == 0 {
|
||||
output.WriteString(fmt.Sprintf("✓ %s: No issues found\n", result.FilePath))
|
||||
continue
|
||||
}
|
||||
|
||||
output.WriteString(fmt.Sprintf("\n%s:\n", result.FilePath))
|
||||
|
||||
for _, err := range result.Errors {
|
||||
output.WriteString(fmt.Sprintf(" ✗ Error (line %d): %s\n", err.Line, err.Message))
|
||||
if err.Field != "" {
|
||||
output.WriteString(fmt.Sprintf(" Field: %s\n", err.Field))
|
||||
}
|
||||
totalErrors++
|
||||
}
|
||||
|
||||
for _, warn := range result.Warnings {
|
||||
output.WriteString(fmt.Sprintf(" ⚠ Warning (line %d): %s\n", warn.Line, warn.Message))
|
||||
if warn.Field != "" {
|
||||
output.WriteString(fmt.Sprintf(" Field: %s\n", warn.Field))
|
||||
}
|
||||
totalWarnings++
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString(fmt.Sprintf("\nSummary: %d error(s), %d warning(s) across %d file(s)\n", totalErrors, totalWarnings, len(results)))
|
||||
|
||||
return output.String()
|
||||
}
|
||||
|
||||
func formatJSON(results []LintResult) string {
|
||||
// Simple JSON formatting without importing encoding/json
|
||||
var output strings.Builder
|
||||
output.WriteString("{\n")
|
||||
output.WriteString(" \"results\": [\n")
|
||||
|
||||
for i, result := range results {
|
||||
output.WriteString(" {\n")
|
||||
output.WriteString(fmt.Sprintf(" \"filePath\": %q,\n", result.FilePath))
|
||||
output.WriteString(" \"errors\": [\n")
|
||||
|
||||
for j, err := range result.Errors {
|
||||
output.WriteString(" {\n")
|
||||
output.WriteString(fmt.Sprintf(" \"line\": %d,\n", err.Line))
|
||||
output.WriteString(fmt.Sprintf(" \"column\": %d,\n", err.Column))
|
||||
output.WriteString(fmt.Sprintf(" \"message\": %q,\n", err.Message))
|
||||
output.WriteString(fmt.Sprintf(" \"field\": %q\n", err.Field))
|
||||
output.WriteString(" }")
|
||||
if j < len(result.Errors)-1 {
|
||||
output.WriteString(",")
|
||||
}
|
||||
output.WriteString("\n")
|
||||
}
|
||||
|
||||
output.WriteString(" ],\n")
|
||||
output.WriteString(" \"warnings\": [\n")
|
||||
|
||||
for j, warn := range result.Warnings {
|
||||
output.WriteString(" {\n")
|
||||
output.WriteString(fmt.Sprintf(" \"line\": %d,\n", warn.Line))
|
||||
output.WriteString(fmt.Sprintf(" \"column\": %d,\n", warn.Column))
|
||||
output.WriteString(fmt.Sprintf(" \"message\": %q,\n", warn.Message))
|
||||
output.WriteString(fmt.Sprintf(" \"field\": %q\n", warn.Field))
|
||||
output.WriteString(" }")
|
||||
if j < len(result.Warnings)-1 {
|
||||
output.WriteString(",")
|
||||
}
|
||||
output.WriteString("\n")
|
||||
}
|
||||
|
||||
output.WriteString(" ]\n")
|
||||
output.WriteString(" }")
|
||||
if i < len(results)-1 {
|
||||
output.WriteString(",")
|
||||
}
|
||||
output.WriteString("\n")
|
||||
}
|
||||
|
||||
output.WriteString(" ]\n")
|
||||
output.WriteString("}\n")
|
||||
|
||||
return output.String()
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package lint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// findLineNumber returns the first 1-based line number containing the search string
|
||||
func findLineNumber(content, search string) int {
|
||||
lines := strings.Split(content, "\n")
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, search) {
|
||||
return i + 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func findAnalyzerLine(content string, index int) int {
|
||||
return findListItemLine(content, "analyzers", index)
|
||||
}
|
||||
|
||||
// findCollectorLine locates the starting line of the Nth entry in a collectors list
|
||||
func findCollectorLine(content string, field string, index int) int {
|
||||
return findListItemLine(content, field, index)
|
||||
}
|
||||
|
||||
// findListItemLine locates the starting line of the Nth entry in a list under listKey
|
||||
func findListItemLine(content, listKey string, index int) int {
|
||||
lines := strings.Split(content, "\n")
|
||||
count := 0
|
||||
inList := false
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, listKey+":") {
|
||||
inList = true
|
||||
continue
|
||||
}
|
||||
if inList && strings.HasPrefix(strings.TrimSpace(line), "- ") {
|
||||
if count == index {
|
||||
return i + 1
|
||||
}
|
||||
count++
|
||||
}
|
||||
if inList && !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") && strings.TrimSpace(line) != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// extractLineFromError tries to parse a YAML error message for a line number
|
||||
func extractLineFromError(err error) int {
|
||||
// Try to extract line number from YAML error message
|
||||
re := regexp.MustCompile(`line (\d+)`)
|
||||
matches := re.FindStringSubmatch(err.Error())
|
||||
if len(matches) > 1 {
|
||||
var line int
|
||||
fmt.Sscanf(matches[1], "%d", &line)
|
||||
return line
|
||||
}
|
||||
return 0
|
||||
}
|
||||
+727
-284
File diff suppressed because it is too large
Load Diff
@@ -1,466 +0,0 @@
|
||||
package lint
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLintMultipleFiles(t *testing.T) {
|
||||
// Get the project root by going up from pkg/lint
|
||||
projectRoot := filepath.Join("..", "..")
|
||||
testDir := filepath.Join(projectRoot, "examples", "test-error-messages")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
files []string
|
||||
valuesFiles []string // values files for v1beta3 specs
|
||||
expectErrors map[string][]string // filename -> expected error substrings
|
||||
expectWarnings map[string][]string // filename -> expected warning substrings
|
||||
expectPass map[string]bool // filename -> should pass without errors
|
||||
}{
|
||||
{
|
||||
name: "valid v1beta3 with templates",
|
||||
files: []string{
|
||||
"helm-builtins-v1beta3.yaml",
|
||||
},
|
||||
valuesFiles: []string{
|
||||
"values-helm-builtins.yaml",
|
||||
},
|
||||
expectErrors: map[string][]string{},
|
||||
expectWarnings: map[string][]string{},
|
||||
expectPass: map[string]bool{
|
||||
"helm-builtins-v1beta3.yaml": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid collectors and analyzers",
|
||||
files: []string{
|
||||
"invalid-collectors-analyzers.yaml",
|
||||
},
|
||||
valuesFiles: []string{
|
||||
"values-empty.yaml",
|
||||
},
|
||||
expectErrors: map[string][]string{
|
||||
"invalid-collectors-analyzers.yaml": {
|
||||
// The linter may stop early due to structural issues
|
||||
// At minimum, it should catch the hostCollectors type error
|
||||
"Expected 'hostCollectors' to be a list",
|
||||
},
|
||||
},
|
||||
expectPass: map[string]bool{
|
||||
"invalid-collectors-analyzers.yaml": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing required fields",
|
||||
files: []string{
|
||||
"missing-apiversion-v1beta3.yaml",
|
||||
"missing-metadata-v1beta3.yaml",
|
||||
"no-analyzers-v1beta3.yaml",
|
||||
},
|
||||
valuesFiles: []string{
|
||||
"values-empty.yaml",
|
||||
},
|
||||
expectErrors: map[string][]string{
|
||||
"missing-apiversion-v1beta3.yaml": {
|
||||
"Missing or empty 'apiVersion' field",
|
||||
},
|
||||
"missing-metadata-v1beta3.yaml": {
|
||||
"Missing 'metadata' section",
|
||||
},
|
||||
"no-analyzers-v1beta3.yaml": {
|
||||
"Preflight spec must contain 'analyzers'",
|
||||
},
|
||||
},
|
||||
expectPass: map[string]bool{
|
||||
"missing-apiversion-v1beta3.yaml": false,
|
||||
"missing-metadata-v1beta3.yaml": false,
|
||||
"no-analyzers-v1beta3.yaml": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "v1beta2 file (valid but with docString warning)",
|
||||
files: []string{
|
||||
"wrong-apiversion-v1beta3.yaml", // Actually has v1beta2 which is valid
|
||||
},
|
||||
expectErrors: map[string][]string{},
|
||||
expectWarnings: map[string][]string{
|
||||
"wrong-apiversion-v1beta3.yaml": {
|
||||
"Some analyzers are missing docString",
|
||||
},
|
||||
},
|
||||
expectPass: map[string]bool{
|
||||
"wrong-apiversion-v1beta3.yaml": true, // No errors, just warnings
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "support bundle specs",
|
||||
files: []string{
|
||||
"support-bundle-no-collectors-v1beta3.yaml",
|
||||
"support-bundle-valid-v1beta3.yaml",
|
||||
},
|
||||
valuesFiles: []string{
|
||||
"values-empty.yaml",
|
||||
},
|
||||
expectErrors: map[string][]string{
|
||||
"support-bundle-no-collectors-v1beta3.yaml": {
|
||||
"SupportBundle spec must contain 'collectors' or 'hostCollectors'",
|
||||
},
|
||||
},
|
||||
expectPass: map[string]bool{
|
||||
"support-bundle-no-collectors-v1beta3.yaml": false,
|
||||
"support-bundle-valid-v1beta3.yaml": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple files with mixed validity",
|
||||
files: []string{
|
||||
"support-bundle-valid-v1beta3.yaml",
|
||||
"missing-metadata-v1beta3.yaml",
|
||||
"wrong-apiversion-v1beta3.yaml",
|
||||
},
|
||||
valuesFiles: []string{
|
||||
"values-empty.yaml",
|
||||
},
|
||||
expectErrors: map[string][]string{
|
||||
"missing-metadata-v1beta3.yaml": {
|
||||
"Missing 'metadata' section",
|
||||
},
|
||||
},
|
||||
expectWarnings: map[string][]string{
|
||||
"wrong-apiversion-v1beta3.yaml": {
|
||||
"Some analyzers are missing docString",
|
||||
},
|
||||
},
|
||||
expectPass: map[string]bool{
|
||||
"support-bundle-valid-v1beta3.yaml": true,
|
||||
"missing-metadata-v1beta3.yaml": false,
|
||||
"wrong-apiversion-v1beta3.yaml": true, // No errors, just warnings
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Build full file paths
|
||||
filePaths := make([]string, len(tt.files))
|
||||
for i, f := range tt.files {
|
||||
filePaths[i] = filepath.Join(testDir, f)
|
||||
// Check file exists
|
||||
if _, err := os.Stat(filePaths[i]); os.IsNotExist(err) {
|
||||
t.Skipf("Test file %s does not exist, skipping", filePaths[i])
|
||||
}
|
||||
}
|
||||
|
||||
// Build values file paths
|
||||
var valuesFilePaths []string
|
||||
for _, vf := range tt.valuesFiles {
|
||||
valuesFilePaths = append(valuesFilePaths, filepath.Join(testDir, vf))
|
||||
}
|
||||
|
||||
// Run linter
|
||||
opts := LintOptions{
|
||||
FilePaths: filePaths,
|
||||
Fix: false,
|
||||
Format: "text",
|
||||
ValuesFiles: valuesFilePaths,
|
||||
}
|
||||
|
||||
results, err := LintFiles(opts)
|
||||
if err != nil {
|
||||
t.Fatalf("LintFiles failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify we got results for all files
|
||||
if len(results) != len(filePaths) {
|
||||
t.Errorf("Expected %d results, got %d", len(filePaths), len(results))
|
||||
}
|
||||
|
||||
// Check each result
|
||||
for _, result := range results {
|
||||
filename := filepath.Base(result.FilePath)
|
||||
|
||||
// Check expected errors
|
||||
if expectedErrors, ok := tt.expectErrors[filename]; ok {
|
||||
if len(expectedErrors) > 0 && len(result.Errors) == 0 {
|
||||
t.Errorf("File %s: expected errors but got none", filename)
|
||||
}
|
||||
for _, expectedErr := range expectedErrors {
|
||||
found := false
|
||||
for _, err := range result.Errors {
|
||||
if strings.Contains(err.Message, expectedErr) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("File %s: expected error containing '%s' but not found in errors: %v",
|
||||
filename, expectedErr, getErrorMessages(result.Errors))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check expected warnings
|
||||
if expectedWarnings, ok := tt.expectWarnings[filename]; ok {
|
||||
for _, expectedWarn := range expectedWarnings {
|
||||
found := false
|
||||
for _, warn := range result.Warnings {
|
||||
if strings.Contains(warn.Message, expectedWarn) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("File %s: expected warning containing '%s' but not found in warnings: %v",
|
||||
filename, expectedWarn, getWarningMessages(result.Warnings))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if should pass
|
||||
if shouldPass, ok := tt.expectPass[filename]; ok {
|
||||
hasNoErrors := len(result.Errors) == 0
|
||||
if shouldPass && !hasNoErrors {
|
||||
t.Errorf("File %s: expected to pass but has errors: %v",
|
||||
filename, getErrorMessages(result.Errors))
|
||||
} else if !shouldPass && hasNoErrors && len(tt.expectErrors[filename]) > 0 {
|
||||
t.Errorf("File %s: expected to fail but passed", filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLintWithFix(t *testing.T) {
|
||||
// Create a temporary directory for test files
|
||||
tmpDir, err := os.MkdirTemp("", "lint-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
expectFix bool
|
||||
fixedContent string // substring that should appear after fix
|
||||
}{
|
||||
{
|
||||
name: "fix v1beta2 with templates to v1beta3",
|
||||
content: `apiVersion: troubleshoot.sh/v1beta2
|
||||
kind: Preflight
|
||||
metadata:
|
||||
name: test-{{ .Values.name }}
|
||||
spec:
|
||||
analyzers:
|
||||
- clusterVersion:
|
||||
outcomes:
|
||||
- pass:
|
||||
when: '>= 1.19.0'
|
||||
message: OK`,
|
||||
expectFix: true,
|
||||
fixedContent: "apiVersion: troubleshoot.sh/v1beta3",
|
||||
},
|
||||
{
|
||||
name: "v1beta3 template syntax error is reported",
|
||||
content: `apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: Preflight
|
||||
metadata:
|
||||
name: test-{{ Values.name }}
|
||||
spec:
|
||||
analyzers:
|
||||
- clusterVersion:
|
||||
outcomes:
|
||||
- pass:
|
||||
when: '>= 1.19.0'
|
||||
message: OK`,
|
||||
expectFix: false, // Template errors in v1beta3 are not auto-fixable, rendering will fail
|
||||
fixedContent: "Failed to render v1beta3 template", // Expect an error message
|
||||
},
|
||||
}
|
||||
|
||||
// Create empty values file for v1beta3 tests
|
||||
emptyValuesFile := filepath.Join(tmpDir, "values-empty.yaml")
|
||||
if err := os.WriteFile(emptyValuesFile, []byte("{}"), 0644); err != nil {
|
||||
t.Fatalf("Failed to write empty values file: %v", err)
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Write test content to temp file
|
||||
testFile := filepath.Join(tmpDir, tt.name+".yaml")
|
||||
if err := os.WriteFile(testFile, []byte(tt.content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test file: %v", err)
|
||||
}
|
||||
|
||||
// Run linter with fix enabled
|
||||
opts := LintOptions{
|
||||
FilePaths: []string{testFile},
|
||||
Fix: true,
|
||||
Format: "text",
|
||||
ValuesFiles: []string{emptyValuesFile},
|
||||
}
|
||||
|
||||
results, err := LintFiles(opts)
|
||||
if err != nil {
|
||||
t.Fatalf("LintFiles failed: %v", err)
|
||||
}
|
||||
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("Expected 1 result, got %d", len(results))
|
||||
}
|
||||
|
||||
// Read the potentially fixed content
|
||||
fixedBytes, err := os.ReadFile(testFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read fixed file: %v", err)
|
||||
}
|
||||
fixedContent := string(fixedBytes)
|
||||
|
||||
// Check if fix was applied or error was reported
|
||||
if tt.expectFix {
|
||||
if !strings.Contains(fixedContent, tt.fixedContent) {
|
||||
t.Errorf("Expected fixed content to contain '%s', but got:\n%s",
|
||||
tt.fixedContent, fixedContent)
|
||||
}
|
||||
} else {
|
||||
// For tests that don't expect fix, check for errors
|
||||
if len(results[0].Errors) > 0 {
|
||||
errorFound := false
|
||||
for _, err := range results[0].Errors {
|
||||
if strings.Contains(err.Message, tt.fixedContent) {
|
||||
errorFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !errorFound {
|
||||
t.Errorf("Expected error containing '%s', but got errors: %v",
|
||||
tt.fixedContent, results[0].Errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
results []LintResult
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "no errors",
|
||||
results: []LintResult{
|
||||
{
|
||||
FilePath: "test1.yaml",
|
||||
Errors: []LintError{},
|
||||
Warnings: []LintWarning{{Message: "warning"}},
|
||||
},
|
||||
{
|
||||
FilePath: "test2.yaml",
|
||||
Errors: []LintError{},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "has errors",
|
||||
results: []LintResult{
|
||||
{
|
||||
FilePath: "test1.yaml",
|
||||
Errors: []LintError{{Message: "error"}},
|
||||
},
|
||||
{
|
||||
FilePath: "test2.yaml",
|
||||
Errors: []LintError{},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "empty results",
|
||||
results: []LintResult{},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := HasErrors(tt.results)
|
||||
if result != tt.expected {
|
||||
t.Errorf("HasErrors() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatResults(t *testing.T) {
|
||||
results := []LintResult{
|
||||
{
|
||||
FilePath: "test.yaml",
|
||||
Errors: []LintError{
|
||||
{Line: 5, Message: "Missing field", Field: "spec.analyzers"},
|
||||
},
|
||||
Warnings: []LintWarning{
|
||||
{Line: 10, Message: "Consider adding docString", Field: "spec.analyzers[0]"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("text format", func(t *testing.T) {
|
||||
output := FormatResults(results, "text")
|
||||
|
||||
// Check for key components in text output
|
||||
if !strings.Contains(output, "test.yaml") {
|
||||
t.Error("Text output missing file path")
|
||||
}
|
||||
if !strings.Contains(output, "Error (line 5)") {
|
||||
t.Error("Text output missing error with line number")
|
||||
}
|
||||
if !strings.Contains(output, "Warning (line 10)") {
|
||||
t.Error("Text output missing warning with line number")
|
||||
}
|
||||
if !strings.Contains(output, "Summary:") {
|
||||
t.Error("Text output missing summary")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("json format", func(t *testing.T) {
|
||||
output := FormatResults(results, "json")
|
||||
|
||||
// Check for key JSON components
|
||||
if !strings.Contains(output, `"filePath"`) {
|
||||
t.Error("JSON output missing filePath field")
|
||||
}
|
||||
if !strings.Contains(output, `"errors"`) {
|
||||
t.Error("JSON output missing errors field")
|
||||
}
|
||||
if !strings.Contains(output, `"warnings"`) {
|
||||
t.Error("JSON output missing warnings field")
|
||||
}
|
||||
if !strings.Contains(output, `"line": 5`) {
|
||||
t.Error("JSON output missing line number")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func getErrorMessages(errors []LintError) []string {
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Message
|
||||
}
|
||||
return messages
|
||||
}
|
||||
|
||||
func getWarningMessages(warnings []LintWarning) []string {
|
||||
messages := make([]string, len(warnings))
|
||||
for i, warn := range warnings {
|
||||
messages[i] = warn.Message
|
||||
}
|
||||
return messages
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
package lint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// detectAPIVersionFromContent tries to extract apiVersion from raw YAML text
|
||||
func detectAPIVersionFromContent(content string) string {
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "apiVersion:") {
|
||||
parts := strings.SplitN(trimmed, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
val := strings.TrimSpace(parts[1])
|
||||
// strip quotes if present
|
||||
val = strings.Trim(val, "'\"")
|
||||
return val
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// addTemplatingErrorsForAllLines records an error for each line containing template braces in versions that do not support templating
|
||||
func addTemplatingErrorsForAllLines(result *LintResult, content string) {
|
||||
lines := strings.Split(content, "\n")
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, "{{") && strings.Contains(line, "}}") {
|
||||
result.Errors = append(result.Errors, LintError{
|
||||
Line: i + 1,
|
||||
Message: "Templating is not supported in v1beta2 specs",
|
||||
Field: "template",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkTemplateSyntax(content string) ([]LintError, []string) {
|
||||
errors := []LintError{}
|
||||
lines := strings.Split(content, "\n")
|
||||
templateValueRefs := map[string]bool{}
|
||||
|
||||
// Check for unmatched braces
|
||||
for i, line := range lines {
|
||||
// Count opening and closing braces
|
||||
opening := strings.Count(line, "{{")
|
||||
closing := strings.Count(line, "}}")
|
||||
|
||||
if opening != closing {
|
||||
errors = append(errors, LintError{
|
||||
Line: i + 1,
|
||||
Message: fmt.Sprintf("Unmatched template braces: %d opening, %d closing", opening, closing),
|
||||
})
|
||||
}
|
||||
|
||||
// Check for common template syntax issues
|
||||
// Look for templates that might be missing the leading dot
|
||||
if strings.Contains(line, "{{") && strings.Contains(line, "}}") {
|
||||
// Extract template expressions
|
||||
templateExpr := extractTemplateBetweenBraces(line)
|
||||
for _, expr := range templateExpr {
|
||||
trimmed := strings.TrimSpace(expr)
|
||||
|
||||
// Skip empty expressions
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip comments: {{/* ... */}}
|
||||
if strings.HasPrefix(trimmed, "/*") || strings.HasPrefix(trimmed, "*/") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Track template value references for warning (check this before skipping control structures)
|
||||
if strings.Contains(trimmed, ".Values.") {
|
||||
// Extract the value path
|
||||
valuePattern := regexp.MustCompile(`\.Values\.(\w+(?:\.\w+)*)`)
|
||||
matches := valuePattern.FindAllStringSubmatch(trimmed, -1)
|
||||
for _, match := range matches {
|
||||
if len(match) > 1 {
|
||||
templateValueRefs[match[1]] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip control structures (if, else, end, range, with, etc.)
|
||||
if isControlStructure(trimmed) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip template variables (start with $)
|
||||
if strings.HasPrefix(trimmed, "$") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip expressions that start with a dot (valid references)
|
||||
if strings.HasPrefix(trimmed, ".") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip string literals
|
||||
if strings.HasPrefix(trimmed, "\"") || strings.HasPrefix(trimmed, "'") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip numeric literals
|
||||
if regexp.MustCompile(`^[0-9]+$`).MatchString(trimmed) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip function calls (contain parentheses or pipes)
|
||||
if strings.Contains(trimmed, "(") || strings.Contains(trimmed, "|") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip known Helm functions/keywords
|
||||
helmFunctions := []string{"toYaml", "toJson", "include", "required", "default", "quote", "nindent", "indent", "upper", "lower", "trim"}
|
||||
isFunction := false
|
||||
for _, fn := range helmFunctions {
|
||||
if strings.HasPrefix(trimmed, fn+" ") || trimmed == fn {
|
||||
isFunction = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isFunction {
|
||||
continue
|
||||
}
|
||||
|
||||
// If we got here, it might be missing a leading dot
|
||||
errors = append(errors, LintError{
|
||||
Line: i + 1,
|
||||
Message: fmt.Sprintf("Template expression may be missing leading dot: {{ %s }}", expr),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect template values that need to be provided at runtime
|
||||
var valueList []string
|
||||
for val := range templateValueRefs {
|
||||
valueList = append(valueList, val)
|
||||
}
|
||||
// Sort for consistent output
|
||||
sort.Strings(valueList)
|
||||
|
||||
return errors, valueList
|
||||
}
|
||||
|
||||
// extractTemplateBetweenBraces extracts template expressions from a line
|
||||
func extractTemplateBetweenBraces(line string) []string {
|
||||
var expressions []string
|
||||
// Match {{ ... }} with optional whitespace trimming (-), including comments {{/* */}}
|
||||
re := regexp.MustCompile(`\{\{-?\s*(.+?)\s*-?\}\}`)
|
||||
matches := re.FindAllStringSubmatch(line, -1)
|
||||
for _, match := range matches {
|
||||
if len(match) > 1 {
|
||||
// Clean up the expression
|
||||
expr := match[1]
|
||||
// Remove */ at the end if it's part of a comment
|
||||
expr = strings.TrimSuffix(strings.TrimSpace(expr), "*/")
|
||||
expressions = append(expressions, expr)
|
||||
}
|
||||
}
|
||||
return expressions
|
||||
}
|
||||
|
||||
// isControlStructure checks if a template expression is a control structure
|
||||
func isControlStructure(expr string) bool {
|
||||
trimmed := strings.TrimSpace(expr)
|
||||
controlKeywords := []string{"if", "else", "end", "range", "with", "define", "template", "block", "include"}
|
||||
for _, keyword := range controlKeywords {
|
||||
if strings.HasPrefix(trimmed, keyword+" ") || trimmed == keyword {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package lint
|
||||
|
||||
// Core types used by the lint package
|
||||
|
||||
type LintResult struct {
|
||||
FilePath string
|
||||
Errors []LintError
|
||||
Warnings []LintWarning
|
||||
}
|
||||
|
||||
type LintError struct {
|
||||
Line int
|
||||
Column int
|
||||
Message string
|
||||
Field string
|
||||
}
|
||||
|
||||
type LintWarning struct {
|
||||
Line int
|
||||
Column int
|
||||
Message string
|
||||
Field string
|
||||
}
|
||||
|
||||
type LintOptions struct {
|
||||
FilePaths []string
|
||||
Fix bool
|
||||
Format string // "text" or "json"
|
||||
ValuesFiles []string // Path to YAML files with template values (for v1beta3)
|
||||
SetValues []string // Template values from command line (for v1beta3)
|
||||
}
|
||||
|
||||
// HasErrors returns true if any of the results contain errors
|
||||
func HasErrors(results []LintResult) bool {
|
||||
for _, result := range results {
|
||||
if len(result.Errors) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,452 +0,0 @@
|
||||
package lint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
func checkRequiredFields(parsed map[string]interface{}, content string) []LintError {
|
||||
errors := []LintError{}
|
||||
|
||||
// Check apiVersion
|
||||
if apiVersion, ok := parsed["apiVersion"].(string); !ok || apiVersion == "" {
|
||||
errors = append(errors, LintError{
|
||||
Line: findLineNumber(content, "apiVersion"),
|
||||
Field: "apiVersion",
|
||||
Message: "Missing 'apiVersion'",
|
||||
})
|
||||
}
|
||||
|
||||
// Check kind
|
||||
if kind, ok := parsed["kind"].(string); !ok || kind == "" {
|
||||
errors = append(errors, LintError{
|
||||
Line: findLineNumber(content, "kind"),
|
||||
Field: "kind",
|
||||
Message: "Missing or empty 'kind' field",
|
||||
})
|
||||
} else if kind != "Preflight" && kind != "SupportBundle" {
|
||||
errors = append(errors, LintError{
|
||||
Line: findLineNumber(content, "kind"),
|
||||
Field: "kind",
|
||||
Message: fmt.Sprintf("Expected kind 'Preflight' or 'SupportBundle' (found '%s')", kind),
|
||||
})
|
||||
}
|
||||
|
||||
// Check metadata
|
||||
if _, ok := parsed["metadata"]; !ok {
|
||||
errors = append(errors, LintError{
|
||||
Line: findLineNumber(content, "metadata"),
|
||||
Field: "metadata",
|
||||
Message: "Missing 'metadata' section",
|
||||
})
|
||||
} else if metadata, ok := parsed["metadata"].(map[string]interface{}); ok {
|
||||
if name, ok := metadata["name"].(string); !ok || name == "" {
|
||||
errors = append(errors, LintError{
|
||||
Line: findLineNumber(content, "name"),
|
||||
Field: "metadata.name",
|
||||
Message: "Missing 'metadata.name'",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Check spec
|
||||
if _, ok := parsed["spec"]; !ok {
|
||||
errors = append(errors, LintError{
|
||||
Line: findLineNumber(content, "spec"),
|
||||
Field: "spec",
|
||||
Message: "Missing 'spec' section",
|
||||
})
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
func checkPreflightSpec(parsed map[string]interface{}, content string) []LintError {
|
||||
errors := []LintError{}
|
||||
|
||||
spec, ok := parsed["spec"].(map[string]interface{})
|
||||
if !ok {
|
||||
return errors
|
||||
}
|
||||
|
||||
// Check for analyzers
|
||||
analyzers, hasAnalyzers := spec["analyzers"]
|
||||
if !hasAnalyzers {
|
||||
errors = append(errors, LintError{
|
||||
Line: findLineNumber(content, "spec:"),
|
||||
Field: "spec.analyzers",
|
||||
Message: "Preflight spec must contain 'analyzers'",
|
||||
})
|
||||
} else if analyzersList, ok := analyzers.([]interface{}); ok {
|
||||
if len(analyzersList) == 0 {
|
||||
errors = append(errors, LintError{
|
||||
Line: findLineNumber(content, "analyzers"),
|
||||
Field: "spec.analyzers",
|
||||
Message: "Preflight spec must have at least one analyzer",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
func checkSupportBundleSpec(parsed map[string]interface{}, content string) []LintError {
|
||||
errors := []LintError{}
|
||||
|
||||
spec, ok := parsed["spec"].(map[string]interface{})
|
||||
if !ok {
|
||||
return errors
|
||||
}
|
||||
|
||||
// Check for collectors
|
||||
collectors, hasCollectors := spec["collectors"]
|
||||
_, hasHostCollectors := spec["hostCollectors"]
|
||||
|
||||
if !hasCollectors && !hasHostCollectors {
|
||||
errors = append(errors, LintError{
|
||||
Line: findLineNumber(content, "spec:"),
|
||||
Field: "spec.collectors",
|
||||
Message: "SupportBundle spec must contain 'collectors' or 'hostCollectors'",
|
||||
})
|
||||
} else {
|
||||
// Check if collectors list is empty
|
||||
if hasCollectors {
|
||||
if collectorsList, ok := collectors.([]interface{}); ok && len(collectorsList) == 0 {
|
||||
errors = append(errors, LintError{
|
||||
Line: findLineNumber(content, "collectors"),
|
||||
Field: "spec.collectors",
|
||||
Message: "Collectors list is empty",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// checkCommonIssues aggregates advisory warnings based on best practices
|
||||
func checkCommonIssues(parsed map[string]interface{}, content string, apiVersion string, templateValueRefs []string) []LintWarning {
|
||||
warnings := []LintWarning{}
|
||||
|
||||
// Check for missing docStrings in analyzers and collectors
|
||||
spec, ok := parsed["spec"].(map[string]interface{})
|
||||
if !ok {
|
||||
return warnings
|
||||
}
|
||||
|
||||
// Check if any analyzers are missing docString
|
||||
analyzersMissingDocString := false
|
||||
if analyzers, ok := spec["analyzers"].([]interface{}); ok {
|
||||
for _, analyzer := range analyzers {
|
||||
if analyzerMap, ok := analyzer.(map[string]interface{}); ok {
|
||||
// Check if docString exists at the analyzer level (v1beta3)
|
||||
if _, hasDocString := analyzerMap["docString"]; !hasDocString {
|
||||
analyzersMissingDocString = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any collectors are missing docString
|
||||
collectorsMissingDocString := false
|
||||
if collectors, ok := spec["collectors"].([]interface{}); ok {
|
||||
for _, collector := range collectors {
|
||||
if collectorMap, ok := collector.(map[string]interface{}); ok {
|
||||
// Get the actual collector type (first key-value pair)
|
||||
for _, collectorSpec := range collectorMap {
|
||||
if specMap, ok := collectorSpec.(map[string]interface{}); ok {
|
||||
if _, hasDocString := specMap["docString"]; !hasDocString {
|
||||
collectorsMissingDocString = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// Only check the first key since collectors have single type
|
||||
break
|
||||
}
|
||||
}
|
||||
if collectorsMissingDocString {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add consolidated warnings if any items are missing docString
|
||||
if analyzersMissingDocString && collectorsMissingDocString {
|
||||
warnings = append(warnings, LintWarning{
|
||||
Line: findLineNumber(content, "spec:"),
|
||||
Field: "spec",
|
||||
Message: "Some analyzers and collectors are missing docString (recommended for v1beta3)",
|
||||
})
|
||||
} else if analyzersMissingDocString {
|
||||
warnings = append(warnings, LintWarning{
|
||||
Line: findLineNumber(content, "analyzers:"),
|
||||
Field: "spec.analyzers",
|
||||
Message: "Some analyzers are missing docString (recommended for v1beta3)",
|
||||
})
|
||||
} else if collectorsMissingDocString {
|
||||
warnings = append(warnings, LintWarning{
|
||||
Line: findLineNumber(content, "collectors:"),
|
||||
Field: "spec.collectors",
|
||||
Message: "Some collectors are missing docString (recommended for v1beta3)",
|
||||
})
|
||||
}
|
||||
|
||||
// Add warning about template values that need to be provided at runtime (v1beta3 only)
|
||||
if apiVersion == "troubleshoot.sh/v1beta3" && len(templateValueRefs) > 0 {
|
||||
warnings = append(warnings, LintWarning{
|
||||
Line: 1,
|
||||
Field: "template-values",
|
||||
Message: fmt.Sprintf("Template values that must be provided at runtime: %s", strings.Join(templateValueRefs, ", ")),
|
||||
})
|
||||
}
|
||||
|
||||
return warnings
|
||||
}
|
||||
|
||||
// --- Schema-backed quick validation (best-effort) ---
|
||||
|
||||
type schemaTypeInfo struct {
|
||||
required map[string]struct{}
|
||||
properties map[string]struct{}
|
||||
}
|
||||
|
||||
var (
|
||||
knownAnalyzerTypes map[string]struct{}
|
||||
knownCollectorTypes map[string]struct{}
|
||||
analyzerTypeInfo map[string]schemaTypeInfo
|
||||
collectorTypeInfo map[string]schemaTypeInfo
|
||||
knownTypesLoaded bool
|
||||
)
|
||||
|
||||
func ensureKnownTypesLoaded() {
|
||||
if knownTypesLoaded {
|
||||
return
|
||||
}
|
||||
knownAnalyzerTypes = map[string]struct{}{}
|
||||
knownCollectorTypes = map[string]struct{}{}
|
||||
analyzerTypeInfo = map[string]schemaTypeInfo{}
|
||||
collectorTypeInfo = map[string]schemaTypeInfo{}
|
||||
|
||||
// Analyzer schema (v1beta2)
|
||||
loadKeysFromSchema(
|
||||
filepath.Join("schemas", "analyzer-troubleshoot-v1beta2.json"),
|
||||
[]string{"properties", "spec", "properties", "analyzers", "items", "properties"},
|
||||
knownAnalyzerTypes,
|
||||
)
|
||||
loadTypeInfoFromSchema(
|
||||
filepath.Join("schemas", "analyzer-troubleshoot-v1beta2.json"),
|
||||
[]string{"properties", "spec", "properties", "analyzers", "items", "properties"},
|
||||
analyzerTypeInfo,
|
||||
)
|
||||
// Collector schema (v1beta2)
|
||||
loadKeysFromSchema(
|
||||
filepath.Join("schemas", "collector-troubleshoot-v1beta2.json"),
|
||||
[]string{"properties", "spec", "properties", "collectors", "items", "properties"},
|
||||
knownCollectorTypes,
|
||||
)
|
||||
loadTypeInfoFromSchema(
|
||||
filepath.Join("schemas", "collector-troubleshoot-v1beta2.json"),
|
||||
[]string{"properties", "spec", "properties", "collectors", "items", "properties"},
|
||||
collectorTypeInfo,
|
||||
)
|
||||
|
||||
knownTypesLoaded = true
|
||||
}
|
||||
|
||||
// loadKeysFromSchema walks a JSON object by keysPath and adds map keys at that node into dest
|
||||
func loadKeysFromSchema(schemaPath string, keysPath []string, dest map[string]struct{}) {
|
||||
data, err := os.ReadFile(schemaPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var obj map[string]interface{}
|
||||
if err := json.Unmarshal(data, &obj); err != nil {
|
||||
return
|
||||
}
|
||||
node := interface{}(obj)
|
||||
for _, key := range keysPath {
|
||||
m, ok := node.(map[string]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
node, ok = m[key]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
props, ok := node.(map[string]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for k := range props {
|
||||
dest[k] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// loadTypeInfoFromSchema records required/properties for each type under the node
|
||||
func loadTypeInfoFromSchema(schemaPath string, keysPath []string, dest map[string]schemaTypeInfo) {
|
||||
data, err := os.ReadFile(schemaPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var obj map[string]interface{}
|
||||
if err := json.Unmarshal(data, &obj); err != nil {
|
||||
return
|
||||
}
|
||||
node := interface{}(obj)
|
||||
for _, key := range keysPath {
|
||||
m, ok := node.(map[string]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
node, ok = m[key]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
typesNode, ok := node.(map[string]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for typeName, raw := range typesNode {
|
||||
m, ok := raw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
info := schemaTypeInfo{required: map[string]struct{}{}, properties: map[string]struct{}{}}
|
||||
if req, ok := m["required"].([]interface{}); ok {
|
||||
for _, r := range req {
|
||||
if s, ok := r.(string); ok {
|
||||
info.required[s] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
if props, ok := m["properties"].(map[string]interface{}); ok {
|
||||
for prop := range props {
|
||||
info.properties[prop] = struct{}{}
|
||||
}
|
||||
}
|
||||
dest[typeName] = info
|
||||
}
|
||||
}
|
||||
|
||||
// validateAnalyzers delegates to the generic typed-list validator
|
||||
func validateAnalyzers(parsed map[string]interface{}, content string) []LintError {
|
||||
return validateTypedList(parsed, content, "analyzers", "analyzer", knownAnalyzerTypes, analyzerTypeInfo)
|
||||
}
|
||||
|
||||
func validateCollectors(parsed map[string]interface{}, content string, field string) []LintError {
|
||||
return validateTypedList(parsed, content, field, "collector", knownCollectorTypes, collectorTypeInfo)
|
||||
}
|
||||
|
||||
// validateTypedList provides generic validation for lists of typed single-key objects
|
||||
func validateTypedList(
|
||||
parsed map[string]interface{},
|
||||
content string,
|
||||
listKey string,
|
||||
subject string,
|
||||
knownTypes map[string]struct{},
|
||||
typeInfo map[string]schemaTypeInfo,
|
||||
) []LintError {
|
||||
var errs []LintError
|
||||
spec, ok := parsed["spec"].(map[string]interface{})
|
||||
if !ok {
|
||||
return errs
|
||||
}
|
||||
raw, exists := spec[listKey]
|
||||
if !exists {
|
||||
return errs
|
||||
}
|
||||
list, ok := raw.([]interface{})
|
||||
if !ok {
|
||||
errs = append(errs, LintError{
|
||||
Line: findLineNumber(content, listKey+":"),
|
||||
Field: "spec." + listKey,
|
||||
Message: fmt.Sprintf("Expected '%s' to be a list", listKey),
|
||||
})
|
||||
return errs
|
||||
}
|
||||
for i, item := range list {
|
||||
m, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
errs = append(errs, LintError{
|
||||
Line: findListItemLine(content, listKey, i),
|
||||
Field: fmt.Sprintf("spec.%s[%d]", listKey, i),
|
||||
Message: fmt.Sprintf("Expected %s entry to be a mapping", subject),
|
||||
})
|
||||
continue
|
||||
}
|
||||
// Count non-docString keys (docString is metadata in v1beta3, not a type)
|
||||
typeCount := 0
|
||||
var typ string
|
||||
var body interface{}
|
||||
for k, v := range m {
|
||||
if k == "docString" {
|
||||
// docString is metadata, not a type - skip it
|
||||
continue
|
||||
} else {
|
||||
typeCount++
|
||||
typ, body = k, v
|
||||
}
|
||||
}
|
||||
|
||||
// Check that we have exactly one type (excluding docString)
|
||||
if typeCount != 1 {
|
||||
errs = append(errs, LintError{
|
||||
Line: findListItemLine(content, listKey, i),
|
||||
Field: fmt.Sprintf("spec.%s[%d]", listKey, i),
|
||||
Message: fmt.Sprintf("%s entry must specify exactly one %s type", strings.Title(subject), subject),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// If no actual type was found (only docString), skip further validation
|
||||
if typ == "" {
|
||||
continue
|
||||
}
|
||||
if len(knownTypes) > 0 {
|
||||
if _, ok := knownTypes[typ]; !ok {
|
||||
errs = append(errs, LintError{
|
||||
Line: findListItemLine(content, listKey, i),
|
||||
Field: fmt.Sprintf("spec.%s[%d]", listKey, i),
|
||||
Message: fmt.Sprintf("Unknown %s type '%s'", subject, typ),
|
||||
})
|
||||
}
|
||||
}
|
||||
bodyMap, ok := body.(map[string]interface{})
|
||||
if !ok {
|
||||
errs = append(errs, LintError{
|
||||
Line: findListItemLine(content, listKey, i),
|
||||
Field: fmt.Sprintf("spec.%s[%d].%s", listKey, i, typ),
|
||||
Message: fmt.Sprintf("Expected %s definition to be a mapping", subject),
|
||||
})
|
||||
continue
|
||||
}
|
||||
if ti, ok := typeInfo[typ]; ok {
|
||||
for req := range ti.required {
|
||||
if _, ok := bodyMap[req]; !ok {
|
||||
errs = append(errs, LintError{
|
||||
Line: findListItemLine(content, listKey, i),
|
||||
Field: fmt.Sprintf("spec.%s[%d].%s.%s", listKey, i, typ, req),
|
||||
Message: fmt.Sprintf("Missing required field '%s' for %s '%s'", req, subject, typ),
|
||||
})
|
||||
}
|
||||
}
|
||||
for k := range bodyMap {
|
||||
if _, ok := ti.properties[k]; !ok {
|
||||
errs = append(errs, LintError{
|
||||
Line: findListItemLine(content, listKey, i),
|
||||
Field: fmt.Sprintf("spec.%s[%d].%s.%s", listKey, i, typ, k),
|
||||
Message: fmt.Sprintf("Unknown field '%s' for %s '%s'", k, subject, typ),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
+2
-157
@@ -8,14 +8,12 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/internal/util"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
troubleshootv1beta3 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta3"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/docrewrite"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/types"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
@@ -25,8 +23,6 @@ var decoder runtime.Decoder
|
||||
func init() {
|
||||
// Allow serializing Secrets and ConfigMaps
|
||||
_ = v1.AddToScheme(scheme.Scheme)
|
||||
// Ensure v1beta3 Troubleshoot types are registered for decoding
|
||||
_ = troubleshootv1beta3.AddToScheme(scheme.Scheme)
|
||||
decoder = scheme.Codecs.UniversalDeserializer()
|
||||
}
|
||||
|
||||
@@ -44,14 +40,6 @@ type LoadOptions struct {
|
||||
// If true, the loader will return an error if any of the specs are not valid
|
||||
// else the invalid specs will be ignored
|
||||
Strict bool
|
||||
|
||||
// Client is the kubernetes client used for resolving v1beta3 StringOrValueFrom fields
|
||||
// If not provided, v1beta3 specs with secretKeyRef will fail to load
|
||||
Client kubernetes.Interface
|
||||
|
||||
// Namespace is the default namespace for resolving v1beta3 secret references
|
||||
// Defaults to "default" if not provided
|
||||
Namespace string
|
||||
}
|
||||
|
||||
// TODO: Additional requirements needed in this package
|
||||
@@ -76,18 +64,8 @@ type LoadOptions struct {
|
||||
// the documents are not valid, else the invalid documents will be ignored.
|
||||
func LoadSpecs(ctx context.Context, opt LoadOptions) (*TroubleshootKinds, error) {
|
||||
opt.RawSpecs = append(opt.RawSpecs, opt.RawSpec)
|
||||
|
||||
// Default namespace to "default" if not provided
|
||||
namespace := opt.Namespace
|
||||
if namespace == "" {
|
||||
namespace = "default"
|
||||
}
|
||||
|
||||
l := specLoader{
|
||||
strict: opt.Strict,
|
||||
client: opt.Client,
|
||||
namespace: namespace,
|
||||
ctx: ctx,
|
||||
strict: opt.Strict,
|
||||
}
|
||||
|
||||
return l.loadFromStrings(opt.RawSpecs...)
|
||||
@@ -162,10 +140,7 @@ func NewTroubleshootKinds() *TroubleshootKinds {
|
||||
}
|
||||
|
||||
type specLoader struct {
|
||||
strict bool
|
||||
client kubernetes.Interface
|
||||
namespace string
|
||||
ctx context.Context
|
||||
strict bool
|
||||
}
|
||||
|
||||
// loadFromStrings accepts a list of strings (exploded) which should be yaml documents
|
||||
@@ -241,23 +216,6 @@ func (l *specLoader) loadFromSplitDocs(splitdocs []string) (*TroubleshootKinds,
|
||||
kinds := NewTroubleshootKinds()
|
||||
|
||||
for _, doc := range splitdocs {
|
||||
// Check if this is a v1beta3 spec
|
||||
var parsed parsedDoc
|
||||
if err := yaml.Unmarshal([]byte(doc), &parsed); err == nil && parsed.APIVersion == constants.Troubleshootv1beta3Kind {
|
||||
// Only handle v1beta3 SupportBundle specially (to resolve valueFrom and convert)
|
||||
if parsed.Kind == "SupportBundle" {
|
||||
if err := l.loadV1Beta3Spec(doc, kinds); err != nil {
|
||||
// Always surface v1beta3 SupportBundle errors so users get actionable guidance
|
||||
return nil, err
|
||||
}
|
||||
// handled as support bundle; move to next doc
|
||||
continue
|
||||
}
|
||||
// For other v1beta3 kinds (e.g., Preflight), fall through to the generic
|
||||
// v1beta3->v1beta2 conversion path below to preserve prior behavior.
|
||||
}
|
||||
|
||||
// Handle v1beta2 and v1beta1 specs
|
||||
converted, err := docrewrite.ConvertToV1Beta2([]byte(doc))
|
||||
if err != nil {
|
||||
if !l.strict {
|
||||
@@ -305,119 +263,6 @@ func (l *specLoader) loadFromSplitDocs(splitdocs []string) (*TroubleshootKinds,
|
||||
return kinds, nil
|
||||
}
|
||||
|
||||
// loadV1Beta3Spec handles loading and resolving v1beta3 specs
|
||||
func (l *specLoader) loadV1Beta3Spec(doc string, kinds *TroubleshootKinds) error {
|
||||
// Unmarshal to v1beta3 types
|
||||
obj, _, err := decoder.Decode([]byte(doc), nil, nil)
|
||||
if err != nil {
|
||||
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES,
|
||||
errors.Wrapf(err, "failed to decode v1beta3 spec: '%s'", doc),
|
||||
)
|
||||
}
|
||||
|
||||
switch v3spec := obj.(type) {
|
||||
case *troubleshootv1beta3.SupportBundle:
|
||||
// Resolve secrets and convert to v1beta2
|
||||
requiresClient := v1beta3SpecRequiresClient(&v3spec.Spec)
|
||||
if requiresClient && l.client == nil {
|
||||
return types.NewExitCodeError(
|
||||
constants.EXIT_CODE_SPEC_ISSUES,
|
||||
errors.New("kubernetes client required"),
|
||||
)
|
||||
}
|
||||
|
||||
v2spec, err := troubleshootv1beta3.ConvertToV1Beta2WithResolution(l.ctx, &v3spec.Spec, l.client, l.namespace)
|
||||
if err != nil {
|
||||
// When secret/configmap references are present, show a clear guidance message
|
||||
// instead of leaking underlying RBAC or lookup errors.
|
||||
if requiresClient {
|
||||
return types.NewExitCodeError(
|
||||
constants.EXIT_CODE_SPEC_ISSUES,
|
||||
errors.New("this v1beta3 SupportBundle uses secret/configmap references and must be run in a cluster"),
|
||||
)
|
||||
}
|
||||
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES,
|
||||
errors.Wrap(err, "failed to resolve and convert v1beta3 support bundle spec"),
|
||||
)
|
||||
}
|
||||
|
||||
// Create v1beta2 support bundle
|
||||
v2bundle := troubleshootv1beta2.SupportBundle{
|
||||
TypeMeta: v3spec.TypeMeta,
|
||||
ObjectMeta: v3spec.ObjectMeta,
|
||||
Spec: *v2spec,
|
||||
}
|
||||
// Update apiVersion to v1beta2
|
||||
v2bundle.APIVersion = constants.Troubleshootv1beta2Kind
|
||||
kinds.SupportBundlesV1Beta2 = append(kinds.SupportBundlesV1Beta2, v2bundle)
|
||||
|
||||
// TODO: Add other v1beta3 types as they are implemented
|
||||
default:
|
||||
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES,
|
||||
errors.Errorf("unsupported v1beta3 kind: %T", v3spec),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// v1beta3SpecRequiresClient returns true if the v1beta3 spec contains any
|
||||
// StringOrValueFrom references that require fetching from the cluster.
|
||||
func v1beta3SpecRequiresClient(spec *troubleshootv1beta3.SupportBundleSpec) bool {
|
||||
if spec == nil || spec.Collectors == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, c := range spec.Collectors {
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Database collectors
|
||||
if c.Postgres != nil && databaseRequiresClient(c.Postgres) {
|
||||
return true
|
||||
}
|
||||
if c.Mysql != nil && databaseRequiresClient(c.Mysql) {
|
||||
return true
|
||||
}
|
||||
if c.Mssql != nil && databaseRequiresClient(c.Mssql) {
|
||||
return true
|
||||
}
|
||||
if c.Redis != nil && databaseRequiresClient(c.Redis) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func databaseRequiresClient(db *troubleshootv1beta3.Database) bool {
|
||||
if db == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if stringOrValueFromHasRef(db.URI) {
|
||||
return true
|
||||
}
|
||||
|
||||
if db.TLS != nil {
|
||||
if stringOrValueFromHasRef(db.TLS.CACert) ||
|
||||
stringOrValueFromHasRef(db.TLS.ClientCert) ||
|
||||
stringOrValueFromHasRef(db.TLS.ClientKey) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func stringOrValueFromHasRef(s troubleshootv1beta3.StringOrValueFrom) bool {
|
||||
if s.ValueFrom == nil {
|
||||
return false
|
||||
}
|
||||
return s.ValueFrom.SecretKeyRef != nil || s.ValueFrom.ConfigMapKeyRef != nil
|
||||
}
|
||||
|
||||
func isSecret(parsedDocHead parsedDoc) bool {
|
||||
if parsedDocHead.Kind == "Secret" && parsedDocHead.APIVersion == "v1" {
|
||||
return true
|
||||
|
||||
@@ -1,289 +0,0 @@
|
||||
package loader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestLoadSpecs_V1Beta3WithSecretRef(t *testing.T) {
|
||||
// Create test secret
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "postgres-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"uri": []byte("postgresql://user:password@localhost:5432/mydb"),
|
||||
},
|
||||
}
|
||||
|
||||
client := fake.NewSimpleClientset(secret)
|
||||
|
||||
spec := `
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: test-bundle
|
||||
spec:
|
||||
collectors:
|
||||
- postgres:
|
||||
collectorName: main-db
|
||||
uri:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-secret
|
||||
key: uri
|
||||
`
|
||||
|
||||
kinds, err := LoadSpecs(context.Background(), LoadOptions{
|
||||
RawSpec: spec,
|
||||
Client: client,
|
||||
Namespace: "default",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, kinds)
|
||||
require.Len(t, kinds.SupportBundlesV1Beta2, 1)
|
||||
|
||||
bundle := kinds.SupportBundlesV1Beta2[0]
|
||||
assert.Equal(t, "test-bundle", bundle.Name)
|
||||
assert.Equal(t, "troubleshoot.sh/v1beta2", bundle.APIVersion)
|
||||
|
||||
require.Len(t, bundle.Spec.Collectors, 1)
|
||||
require.NotNil(t, bundle.Spec.Collectors[0].Postgres)
|
||||
assert.Equal(t, "main-db", bundle.Spec.Collectors[0].Postgres.CollectorName)
|
||||
assert.Equal(t, "postgresql://user:password@localhost:5432/mydb", bundle.Spec.Collectors[0].Postgres.URI)
|
||||
}
|
||||
|
||||
func TestLoadSpecs_V1Beta3WithTLSSecrets(t *testing.T) {
|
||||
// Create test secret with TLS certs
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tls-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"ca.crt": []byte("CA_CERT_DATA"),
|
||||
"client.crt": []byte("CLIENT_CERT_DATA"),
|
||||
"client.key": []byte("CLIENT_KEY_DATA"),
|
||||
},
|
||||
}
|
||||
|
||||
client := fake.NewSimpleClientset(secret)
|
||||
|
||||
spec := `
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: test-bundle
|
||||
spec:
|
||||
collectors:
|
||||
- postgres:
|
||||
uri:
|
||||
value: "postgresql://localhost:5432/db"
|
||||
tls:
|
||||
cacert:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tls-secret
|
||||
key: ca.crt
|
||||
clientCert:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tls-secret
|
||||
key: client.crt
|
||||
clientKey:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tls-secret
|
||||
key: client.key
|
||||
`
|
||||
|
||||
kinds, err := LoadSpecs(context.Background(), LoadOptions{
|
||||
RawSpec: spec,
|
||||
Client: client,
|
||||
Namespace: "default",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, kinds)
|
||||
require.Len(t, kinds.SupportBundlesV1Beta2, 1)
|
||||
|
||||
bundle := kinds.SupportBundlesV1Beta2[0]
|
||||
require.Len(t, bundle.Spec.Collectors, 1)
|
||||
require.NotNil(t, bundle.Spec.Collectors[0].Postgres)
|
||||
require.NotNil(t, bundle.Spec.Collectors[0].Postgres.TLS)
|
||||
|
||||
assert.Equal(t, "CA_CERT_DATA", bundle.Spec.Collectors[0].Postgres.TLS.CACert)
|
||||
assert.Equal(t, "CLIENT_CERT_DATA", bundle.Spec.Collectors[0].Postgres.TLS.ClientCert)
|
||||
assert.Equal(t, "CLIENT_KEY_DATA", bundle.Spec.Collectors[0].Postgres.TLS.ClientKey)
|
||||
}
|
||||
|
||||
func TestLoadSpecs_V1Beta3MultipleCollectors(t *testing.T) {
|
||||
pgSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "postgres-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"uri": []byte("postgresql://localhost:5432/db"),
|
||||
},
|
||||
}
|
||||
|
||||
mysqlSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "mysql-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"uri": []byte("mysql://localhost:3306/db"),
|
||||
},
|
||||
}
|
||||
|
||||
client := fake.NewSimpleClientset(pgSecret, mysqlSecret)
|
||||
|
||||
spec := `
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: test-bundle
|
||||
spec:
|
||||
collectors:
|
||||
- postgres:
|
||||
uri:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-secret
|
||||
key: uri
|
||||
- mysql:
|
||||
uri:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mysql-secret
|
||||
key: uri
|
||||
`
|
||||
|
||||
kinds, err := LoadSpecs(context.Background(), LoadOptions{
|
||||
RawSpec: spec,
|
||||
Client: client,
|
||||
Namespace: "default",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, kinds)
|
||||
require.Len(t, kinds.SupportBundlesV1Beta2, 1)
|
||||
|
||||
bundle := kinds.SupportBundlesV1Beta2[0]
|
||||
require.Len(t, bundle.Spec.Collectors, 2)
|
||||
|
||||
require.NotNil(t, bundle.Spec.Collectors[0].Postgres)
|
||||
assert.Equal(t, "postgresql://localhost:5432/db", bundle.Spec.Collectors[0].Postgres.URI)
|
||||
|
||||
require.NotNil(t, bundle.Spec.Collectors[1].Mysql)
|
||||
assert.Equal(t, "mysql://localhost:3306/db", bundle.Spec.Collectors[1].Mysql.URI)
|
||||
}
|
||||
|
||||
func TestLoadSpecs_V1Beta3WithoutClient(t *testing.T) {
|
||||
spec := `
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: test-bundle
|
||||
spec:
|
||||
collectors:
|
||||
- postgres:
|
||||
uri:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-secret
|
||||
key: uri
|
||||
`
|
||||
|
||||
_, err := LoadSpecs(context.Background(), LoadOptions{
|
||||
RawSpec: spec,
|
||||
Strict: true, // Enable strict mode to get error instead of warning
|
||||
// No client provided
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "kubernetes client required")
|
||||
}
|
||||
|
||||
func TestLoadSpecs_V1Beta3MixedWithV1Beta2(t *testing.T) {
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "postgres-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"uri": []byte("postgresql://localhost:5432/db"),
|
||||
},
|
||||
}
|
||||
|
||||
client := fake.NewSimpleClientset(secret)
|
||||
|
||||
specs := `
|
||||
---
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: v1beta3-bundle
|
||||
spec:
|
||||
collectors:
|
||||
- postgres:
|
||||
uri:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-secret
|
||||
key: uri
|
||||
---
|
||||
apiVersion: troubleshoot.sh/v1beta2
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: v1beta2-bundle
|
||||
spec:
|
||||
collectors:
|
||||
- clusterInfo: {}
|
||||
`
|
||||
|
||||
kinds, err := LoadSpecs(context.Background(), LoadOptions{
|
||||
RawSpec: specs,
|
||||
Client: client,
|
||||
Namespace: "default",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, kinds)
|
||||
require.Len(t, kinds.SupportBundlesV1Beta2, 2)
|
||||
|
||||
// Find the v1beta3-converted bundle
|
||||
var v3Bundle *troubleshootv1beta2.SupportBundle
|
||||
var v2Bundle *troubleshootv1beta2.SupportBundle
|
||||
for i := range kinds.SupportBundlesV1Beta2 {
|
||||
if kinds.SupportBundlesV1Beta2[i].Name == "v1beta3-bundle" {
|
||||
v3Bundle = &kinds.SupportBundlesV1Beta2[i]
|
||||
}
|
||||
if kinds.SupportBundlesV1Beta2[i].Name == "v1beta2-bundle" {
|
||||
v2Bundle = &kinds.SupportBundlesV1Beta2[i]
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(t, v3Bundle, "v1beta3 bundle should be converted and loaded")
|
||||
require.NotNil(t, v2Bundle, "v1beta2 bundle should be loaded")
|
||||
|
||||
// Verify v1beta3 bundle was resolved correctly
|
||||
require.Len(t, v3Bundle.Spec.Collectors, 1)
|
||||
require.NotNil(t, v3Bundle.Spec.Collectors[0].Postgres)
|
||||
assert.Equal(t, "postgresql://localhost:5432/db", v3Bundle.Spec.Collectors[0].Postgres.URI)
|
||||
|
||||
// Verify v1beta2 bundle was loaded correctly
|
||||
require.Len(t, v2Bundle.Spec.Collectors, 1)
|
||||
require.NotNil(t, v2Bundle.Spec.Collectors[0].ClusterInfo)
|
||||
}
|
||||
@@ -5,7 +5,6 @@ package namespaces
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -52,7 +51,7 @@ func (n *NamespacePinger) PingUDP(dst net.IP) error {
|
||||
func (n *NamespacePinger) PingTCP(dst net.IP) error {
|
||||
n.cfg.Logf("reaching to %q from %q with tcp", dst, n.InternalIP)
|
||||
pinger := func() error {
|
||||
addr := net.JoinHostPort(dst.String(), strconv.Itoa(n.cfg.Port))
|
||||
addr := fmt.Sprintf("%s:%d", dst, n.cfg.Port)
|
||||
conn, err := net.DialTimeout("tcp", addr, n.cfg.Timeout)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error dialing tcp: %w", err)
|
||||
@@ -91,7 +90,7 @@ func (n *NamespacePinger) StartTCPEchoServer(errors chan error) {
|
||||
// received, the server ends. Callers must wait until the ready channel is
|
||||
// closed before they can start sending packets.
|
||||
func (n *NamespacePinger) startTCPEchoServer(ready chan struct{}) (err error) {
|
||||
addr := net.JoinHostPort(n.InternalIP.String(), strconv.Itoa(n.cfg.Port))
|
||||
addr := fmt.Sprintf("%s:%d", n.InternalIP, n.cfg.Port)
|
||||
n.cfg.Logf("starting tcp echo server on namespace %q(%q)", n.name, addr)
|
||||
|
||||
if err = n.Join(); err != nil {
|
||||
|
||||
+55
-92
@@ -13,12 +13,11 @@ import (
|
||||
"github.com/replicatedhq/troubleshoot/internal/util"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/version"
|
||||
"k8s.io/klog/v2"
|
||||
"oras.land/oras-go/v2"
|
||||
"oras.land/oras-go/v2/content/memory"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
"oras.land/oras-go/v2/registry/remote/auth"
|
||||
"oras.land/oras-go/v2/registry/remote/credentials"
|
||||
"oras.land/oras-go/v2/registry/remote/retry"
|
||||
"oras.land/oras-go/pkg/auth"
|
||||
dockerauth "oras.land/oras-go/pkg/auth/docker"
|
||||
"oras.land/oras-go/pkg/content"
|
||||
"oras.land/oras-go/pkg/oras"
|
||||
"oras.land/oras-go/pkg/registry"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -76,97 +75,66 @@ func PullSpecsFromOCI(ctx context.Context, uri string) ([]string, error) {
|
||||
}
|
||||
|
||||
func pullFromOCI(ctx context.Context, uri string, mediaType string, imageName string) ([]byte, error) {
|
||||
// Parse the URI to get the repository reference
|
||||
ref, err := parseURI(uri, imageName)
|
||||
// helm credentials
|
||||
helmCredentialsFile := filepath.Join(util.HomeDir(), HelmCredentialsFileBasename)
|
||||
dockerauthClient, err := dockerauth.NewClientWithDockerFallback(helmCredentialsFile)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create auth client")
|
||||
}
|
||||
|
||||
authClient := dockerauthClient
|
||||
|
||||
headers := http.Header{}
|
||||
headers.Set("User-Agent", version.GetUserAgent())
|
||||
opts := []auth.ResolverOption{auth.WithResolverHeaders(headers)}
|
||||
resolver, err := authClient.ResolverWithOpts(opts...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create resolver")
|
||||
}
|
||||
|
||||
memoryStore := content.NewMemory()
|
||||
allowedMediaTypes := []string{
|
||||
mediaType,
|
||||
}
|
||||
|
||||
var descriptors, layers []ocispec.Descriptor
|
||||
registryStore := content.Registry{Resolver: resolver}
|
||||
|
||||
parsedRef, err := parseURI(uri, imageName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
klog.V(1).Infof("Pulling spec from %q OCI uri", ref)
|
||||
klog.V(1).Infof("Pulling spec from %q OCI uri", parsedRef)
|
||||
|
||||
// Create a repository instance
|
||||
repo, err := remote.NewRepository(ref)
|
||||
manifest, err := oras.Copy(ctx, registryStore, parsedRef, memoryStore, "",
|
||||
oras.WithPullEmptyNameAllowed(),
|
||||
oras.WithAllowedMediaTypes(allowedMediaTypes),
|
||||
oras.WithLayerDescriptors(func(l []ocispec.Descriptor) {
|
||||
layers = l
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create repository")
|
||||
}
|
||||
|
||||
// Set up authentication with Docker credentials fallback
|
||||
helmCredentialsFile := filepath.Join(util.HomeDir(), HelmCredentialsFileBasename)
|
||||
storeOpts := credentials.StoreOptions{}
|
||||
|
||||
// Try to load credentials from Helm config first, fall back to Docker config
|
||||
var credStore credentials.Store
|
||||
helmStore, helmErr := credentials.NewStore(helmCredentialsFile, storeOpts)
|
||||
if helmErr == nil {
|
||||
credStore = helmStore
|
||||
} else {
|
||||
// Fall back to Docker credentials if helm credentials are not available
|
||||
dockerStore, dockerErr := credentials.NewStoreFromDocker(storeOpts)
|
||||
if dockerErr != nil {
|
||||
return nil, errors.Wrap(dockerErr, "failed to create credential store")
|
||||
}
|
||||
credStore = dockerStore
|
||||
}
|
||||
|
||||
// Configure the repository client with authentication and custom headers
|
||||
repo.Client = &auth.Client{
|
||||
Client: retry.DefaultClient,
|
||||
Cache: auth.NewCache(),
|
||||
Credential: credentials.Credential(credStore),
|
||||
Header: http.Header{
|
||||
"User-Agent": []string{version.GetUserAgent()},
|
||||
},
|
||||
}
|
||||
|
||||
// Create in-memory storage for the pulled content
|
||||
memoryStore := memory.New()
|
||||
|
||||
// Track layers for filtering
|
||||
var layers []ocispec.Descriptor
|
||||
|
||||
// Set up copy options to capture layer descriptors
|
||||
copyOpts := oras.CopyOptions{}
|
||||
copyOpts.CopyGraphOptions.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
|
||||
// Filter by media type - only copy layers with the specified media type
|
||||
if desc.MediaType == mediaType {
|
||||
layers = append(layers, desc)
|
||||
return nil
|
||||
}
|
||||
// Allow manifest and other necessary descriptors
|
||||
if strings.Contains(desc.MediaType, "manifest") || strings.Contains(desc.MediaType, "config") {
|
||||
return nil
|
||||
}
|
||||
// Skip other media types
|
||||
return oras.SkipNode
|
||||
}
|
||||
|
||||
// Copy from the repository to memory
|
||||
tag := repo.Reference.Reference
|
||||
if tag == "" {
|
||||
tag = "latest"
|
||||
}
|
||||
|
||||
manifest, err := oras.Copy(ctx, repo, tag, memoryStore, tag, copyOpts)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") || strings.Contains(err.Error(), "manifest unknown") {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
return nil, ErrNoRelease
|
||||
}
|
||||
|
||||
return nil, errors.Wrap(err, "failed to copy")
|
||||
}
|
||||
|
||||
descriptors := []ocispec.Descriptor{manifest}
|
||||
descriptors = append(descriptors, manifest)
|
||||
descriptors = append(descriptors, layers...)
|
||||
|
||||
// expect 2 descriptors (manifest + one layer)
|
||||
// expect 2 descriptors
|
||||
if len(descriptors) != 2 {
|
||||
return nil, fmt.Errorf("expected 2 descriptors, got %d", len(descriptors))
|
||||
return nil, fmt.Errorf("expected 2 descriptor, got %d", len(descriptors))
|
||||
}
|
||||
|
||||
var matchingDescriptor *ocispec.Descriptor
|
||||
|
||||
for _, descriptor := range descriptors {
|
||||
d := descriptor
|
||||
if d.MediaType == mediaType {
|
||||
switch d.MediaType {
|
||||
case mediaType:
|
||||
matchingDescriptor = &d
|
||||
}
|
||||
}
|
||||
@@ -175,17 +143,9 @@ func pullFromOCI(ctx context.Context, uri string, mediaType string, imageName st
|
||||
return nil, fmt.Errorf("no descriptor found with media type: %s", mediaType)
|
||||
}
|
||||
|
||||
// Fetch the content from memory store
|
||||
reader, err := memoryStore.Fetch(ctx, *matchingDescriptor)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to fetch matching descriptor")
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
// Read all content
|
||||
matchingSpec := make([]byte, matchingDescriptor.Size)
|
||||
if _, err := reader.Read(matchingSpec); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read content")
|
||||
_, matchingSpec, ok := memoryStore.Get(*matchingDescriptor)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to get matching descriptor")
|
||||
}
|
||||
|
||||
return matchingSpec, nil
|
||||
@@ -212,9 +172,12 @@ func parseURI(in, imageName string) (string, error) {
|
||||
tag = uriParts[1]
|
||||
}
|
||||
|
||||
// Format as: <host>:<port>/path/<imageName>:tag
|
||||
// The remote.NewRepository() function in v2 can handle this format directly
|
||||
uri := fmt.Sprintf("%s%s/%s:%s", u.Host, uriParts[0], imageName, tag)
|
||||
uri := fmt.Sprintf("%s%s/%s:%s", u.Host, uriParts[0], imageName, tag) // <host>:<port>/path/<imageName>:tag
|
||||
|
||||
return uri, nil
|
||||
parsedRef, err := registry.ParseReference(uri)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to parse OCI uri reference")
|
||||
}
|
||||
|
||||
return parsedRef.String(), nil
|
||||
}
|
||||
|
||||
@@ -183,7 +183,6 @@ func CollectWithContext(ctx context.Context, opts CollectOpts, p *troubleshootv1
|
||||
}
|
||||
|
||||
allCollectorsMap := make(map[reflect.Type][]collect.Collector)
|
||||
collectorTypeOrder := make([]reflect.Type, 0) // Preserve order of collector types
|
||||
allCollectedData := make(map[string][]byte)
|
||||
|
||||
for _, desiredCollector := range collectSpecs {
|
||||
@@ -194,9 +193,6 @@ func CollectWithContext(ctx context.Context, opts CollectOpts, p *troubleshootv1
|
||||
return nil, errors.Wrap(err, "failed to check RBAC for collectors")
|
||||
}
|
||||
collectorType := reflect.TypeOf(collector)
|
||||
if _, exists := allCollectorsMap[collectorType]; !exists {
|
||||
collectorTypeOrder = append(collectorTypeOrder, collectorType)
|
||||
}
|
||||
allCollectorsMap[collectorType] = append(allCollectorsMap[collectorType], collector)
|
||||
}
|
||||
}
|
||||
@@ -204,9 +200,7 @@ func CollectWithContext(ctx context.Context, opts CollectOpts, p *troubleshootv1
|
||||
|
||||
collectorList := map[string]CollectorStatus{}
|
||||
|
||||
// Iterate over collector types in the order they appeared in collectSpecs
|
||||
for _, collectorType := range collectorTypeOrder {
|
||||
collectors := allCollectorsMap[collectorType]
|
||||
for _, collectors := range allCollectorsMap {
|
||||
if mergeCollector, ok := collectors[0].(collect.MergeableCollector); ok {
|
||||
mergedCollectors, err := mergeCollector.Merge(collectors)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
package preflight
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// TestCollectWithContext_ClusterResourcesFirst verifies that clusterResources
|
||||
// collector runs first, even when it's not first in the spec.
|
||||
func TestCollectWithContext_ClusterResourcesFirst(t *testing.T) {
|
||||
// Create a preflight spec with collectors in a specific order
|
||||
// where clusterResources is NOT first
|
||||
preflight := &troubleshootv1beta2.Preflight{
|
||||
Spec: troubleshootv1beta2.PreflightSpec{
|
||||
Collectors: []*troubleshootv1beta2.Collect{
|
||||
{
|
||||
Data: &troubleshootv1beta2.Data{
|
||||
CollectorMeta: troubleshootv1beta2.CollectorMeta{
|
||||
CollectorName: "test-data",
|
||||
},
|
||||
Name: "test.json",
|
||||
Data: `{"test": "data"}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
ClusterResources: &troubleshootv1beta2.ClusterResources{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Use a fake Kubernetes client to avoid network calls
|
||||
fakeClient := fake.NewSimpleClientset()
|
||||
restConfig := &rest.Config{
|
||||
Host: "https://fake-host",
|
||||
}
|
||||
|
||||
opts := CollectOpts{
|
||||
Namespace: "default",
|
||||
KubernetesRestConfig: restConfig,
|
||||
ProgressChan: make(chan interface{}, 100),
|
||||
BundlePath: t.TempDir(),
|
||||
IgnorePermissionErrors: true, // Ignore RBAC errors in tests
|
||||
}
|
||||
|
||||
// Manually test the ordering logic by simulating what CollectWithContext does
|
||||
collectSpecs := make([]*troubleshootv1beta2.Collect, 0)
|
||||
if preflight.Spec.Collectors != nil {
|
||||
collectSpecs = append(collectSpecs, preflight.Spec.Collectors...)
|
||||
}
|
||||
collectSpecs = collect.EnsureCollectorInList(
|
||||
collectSpecs, troubleshootv1beta2.Collect{ClusterInfo: &troubleshootv1beta2.ClusterInfo{}},
|
||||
)
|
||||
collectSpecs = collect.EnsureCollectorInList(
|
||||
collectSpecs, troubleshootv1beta2.Collect{ClusterResources: &troubleshootv1beta2.ClusterResources{}},
|
||||
)
|
||||
collectSpecs = collect.DedupCollectors(collectSpecs)
|
||||
collectSpecs = collect.EnsureClusterResourcesFirst(collectSpecs)
|
||||
|
||||
// Verify clusterResources is first in the specs
|
||||
require.NotEmpty(t, collectSpecs, "should have collectors")
|
||||
require.NotNil(t, collectSpecs[0].ClusterResources, "first collector should be clusterResources")
|
||||
|
||||
// Now simulate the map grouping and order preservation
|
||||
allCollectorsMap := make(map[reflect.Type][]collect.Collector)
|
||||
collectorTypeOrder := make([]reflect.Type, 0)
|
||||
|
||||
for _, desiredCollector := range collectSpecs {
|
||||
if collectorInterface, ok := collect.GetCollector(desiredCollector, opts.BundlePath, opts.Namespace, opts.KubernetesRestConfig, fakeClient, nil); ok {
|
||||
if collector, ok := collectorInterface.(collect.Collector); ok {
|
||||
// Skip RBAC check for this unit test
|
||||
collectorType := reflect.TypeOf(collector)
|
||||
if _, exists := allCollectorsMap[collectorType]; !exists {
|
||||
collectorTypeOrder = append(collectorTypeOrder, collectorType)
|
||||
}
|
||||
allCollectorsMap[collectorType] = append(allCollectorsMap[collectorType], collector)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that clusterResources type is first in the order
|
||||
require.NotEmpty(t, collectorTypeOrder, "should have collector types")
|
||||
|
||||
// Find the clusterResources type by checking the actual collectors
|
||||
var clusterResourcesType reflect.Type
|
||||
for collectorType, collectors := range allCollectorsMap {
|
||||
if len(collectors) > 0 {
|
||||
if _, ok := collectors[0].(*collect.CollectClusterResources); ok {
|
||||
clusterResourcesType = collectorType
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
require.NotNil(t, clusterResourcesType, "should find clusterResources type")
|
||||
assert.Equal(t, clusterResourcesType, collectorTypeOrder[0], "clusterResources type should be first in collectorTypeOrder")
|
||||
}
|
||||
|
||||
// TestCollectWithContext_PreservesOrderAfterClusterResources verifies that
|
||||
// after clusterResources, other collectors maintain their relative order.
|
||||
func TestCollectWithContext_PreservesOrderAfterClusterResources(t *testing.T) {
|
||||
// Create a preflight spec with multiple collectors in a specific order
|
||||
preflight := &troubleshootv1beta2.Preflight{
|
||||
Spec: troubleshootv1beta2.PreflightSpec{
|
||||
Collectors: []*troubleshootv1beta2.Collect{
|
||||
{
|
||||
Data: &troubleshootv1beta2.Data{
|
||||
CollectorMeta: troubleshootv1beta2.CollectorMeta{
|
||||
CollectorName: "data-first",
|
||||
},
|
||||
Name: "first.json",
|
||||
Data: `{"first": "data"}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Secret: &troubleshootv1beta2.Secret{
|
||||
CollectorMeta: troubleshootv1beta2.CollectorMeta{
|
||||
CollectorName: "secret-second",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Use a fake Kubernetes client
|
||||
fakeClient := fake.NewSimpleClientset()
|
||||
restConfig := &rest.Config{
|
||||
Host: "https://fake-host",
|
||||
}
|
||||
|
||||
opts := CollectOpts{
|
||||
Namespace: "default",
|
||||
KubernetesRestConfig: restConfig,
|
||||
ProgressChan: make(chan interface{}, 100),
|
||||
BundlePath: t.TempDir(),
|
||||
IgnorePermissionErrors: true,
|
||||
}
|
||||
|
||||
// Simulate the ordering logic
|
||||
collectSpecs := make([]*troubleshootv1beta2.Collect, 0)
|
||||
if preflight.Spec.Collectors != nil {
|
||||
collectSpecs = append(collectSpecs, preflight.Spec.Collectors...)
|
||||
}
|
||||
collectSpecs = collect.EnsureCollectorInList(
|
||||
collectSpecs, troubleshootv1beta2.Collect{ClusterInfo: &troubleshootv1beta2.ClusterInfo{}},
|
||||
)
|
||||
collectSpecs = collect.EnsureCollectorInList(
|
||||
collectSpecs, troubleshootv1beta2.Collect{ClusterResources: &troubleshootv1beta2.ClusterResources{}},
|
||||
)
|
||||
collectSpecs = collect.DedupCollectors(collectSpecs)
|
||||
collectSpecs = collect.EnsureClusterResourcesFirst(collectSpecs)
|
||||
|
||||
// Group collectors by type and track order
|
||||
allCollectorsMap := make(map[reflect.Type][]collect.Collector)
|
||||
collectorTypeOrder := make([]reflect.Type, 0)
|
||||
|
||||
for _, desiredCollector := range collectSpecs {
|
||||
if collectorInterface, ok := collect.GetCollector(desiredCollector, opts.BundlePath, opts.Namespace, opts.KubernetesRestConfig, fakeClient, nil); ok {
|
||||
if collector, ok := collectorInterface.(collect.Collector); ok {
|
||||
collectorType := reflect.TypeOf(collector)
|
||||
if _, exists := allCollectorsMap[collectorType]; !exists {
|
||||
collectorTypeOrder = append(collectorTypeOrder, collectorType)
|
||||
}
|
||||
allCollectorsMap[collectorType] = append(allCollectorsMap[collectorType], collector)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify clusterResources is first
|
||||
require.NotEmpty(t, collectorTypeOrder, "should have collector types")
|
||||
|
||||
// Find the actual types from the collectors
|
||||
var clusterResourcesType, dataType, secretType reflect.Type
|
||||
for collectorType, collectors := range allCollectorsMap {
|
||||
if len(collectors) > 0 {
|
||||
switch collectors[0].(type) {
|
||||
case *collect.CollectClusterResources:
|
||||
clusterResourcesType = collectorType
|
||||
case *collect.CollectData:
|
||||
dataType = collectorType
|
||||
case *collect.CollectSecret:
|
||||
secretType = collectorType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(t, clusterResourcesType, "should find clusterResources type")
|
||||
assert.Equal(t, clusterResourcesType, collectorTypeOrder[0], "clusterResources should be first")
|
||||
|
||||
dataIndex := -1
|
||||
secretIndex := -1
|
||||
for i, ct := range collectorTypeOrder {
|
||||
if ct == dataType {
|
||||
dataIndex = i
|
||||
}
|
||||
if ct == secretType {
|
||||
secretIndex = i
|
||||
}
|
||||
}
|
||||
|
||||
if dataIndex >= 0 && secretIndex >= 0 {
|
||||
assert.Less(t, dataIndex, secretIndex, "data collectors should come before secret collectors, preserving relative order")
|
||||
}
|
||||
}
|
||||
@@ -88,52 +88,6 @@ func preprocessV1Beta3Specs(args []string) ([]string, []string, error) {
|
||||
valuesFiles := viper.GetStringSlice("values")
|
||||
setValues := viper.GetStringSlice("set")
|
||||
|
||||
// Check if any args contain v1beta3 specs
|
||||
hasV1Beta3 := false
|
||||
for _, arg := range args {
|
||||
// Skip non-file arguments
|
||||
if arg == "-" || strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") ||
|
||||
strings.HasPrefix(arg, "secret/") || strings.HasPrefix(arg, "configmap/") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
if _, err := os.Stat(arg); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Read the file
|
||||
content, err := os.ReadFile(arg)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if it's a v1beta3 spec with templates
|
||||
contentStr := string(content)
|
||||
var parsed map[string]interface{}
|
||||
if err := yaml.Unmarshal(content, &parsed); err == nil {
|
||||
if apiVersion, ok := parsed["apiVersion"]; ok && apiVersion == constants.Troubleshootv1beta3Kind {
|
||||
// Only require values if the spec has Helm templates
|
||||
if strings.Contains(contentStr, "{{") && strings.Contains(contentStr, "}}") {
|
||||
hasV1Beta3 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If YAML parsing fails, check raw content for v1beta3 with templates
|
||||
if strings.Contains(contentStr, "apiVersion: troubleshoot.sh/v1beta3") &&
|
||||
strings.Contains(contentStr, "{{") && strings.Contains(contentStr, "}}") {
|
||||
hasV1Beta3 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If v1beta3 spec with templates found but no values provided, return error
|
||||
if hasV1Beta3 && len(valuesFiles) == 0 && len(setValues) == 0 {
|
||||
return nil, nil, errors.New("v1beta3 specs with Helm templates require a values file. Please provide values using --values or --set flags")
|
||||
}
|
||||
|
||||
// If no values provided, return args unchanged
|
||||
if len(valuesFiles) == 0 && len(setValues) == 0 {
|
||||
return args, nil, nil
|
||||
@@ -155,7 +109,7 @@ func preprocessV1Beta3Specs(args []string) ([]string, []string, error) {
|
||||
return nil, nil, errors.Wrapf(err, "failed to parse values file %s", valuesFile)
|
||||
}
|
||||
|
||||
values = MergeMaps(values, fileValues)
|
||||
values = mergeMaps(values, fileValues)
|
||||
}
|
||||
|
||||
// Apply --set values
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/replicatedhq/troubleshoot/internal/testutils"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/loader"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -411,75 +410,3 @@ func singleTestPreflightSpecsRead(t *testing.T, tt *PreflightSpecsReadTest) (*lo
|
||||
|
||||
return kinds, err
|
||||
}
|
||||
|
||||
func TestPreprocessV1Beta3Specs_RequiresValues(t *testing.T) {
|
||||
// Save and restore viper state
|
||||
oldValues := viper.Get("values")
|
||||
oldSet := viper.Get("set")
|
||||
defer func() {
|
||||
viper.Set("values", oldValues)
|
||||
viper.Set("set", oldSet)
|
||||
}()
|
||||
|
||||
t.Run("v1beta3 without values should error", func(t *testing.T) {
|
||||
// Clear viper values
|
||||
viper.Set("values", []string{})
|
||||
viper.Set("set", []string{})
|
||||
|
||||
v1beta3File := filepath.Join(testutils.FileDir(), "../../examples/preflight/simple-v1beta3.yaml")
|
||||
_, _, err := preprocessV1Beta3Specs([]string{v1beta3File})
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "v1beta3 specs with Helm templates require a values file")
|
||||
})
|
||||
|
||||
t.Run("v1beta3 with values file should succeed", func(t *testing.T) {
|
||||
valuesFile := filepath.Join(testutils.FileDir(), "../../examples/preflight/values-v1beta3-1.yaml")
|
||||
viper.Set("values", []string{valuesFile})
|
||||
viper.Set("set", []string{})
|
||||
|
||||
v1beta3File := filepath.Join(testutils.FileDir(), "../../examples/preflight/simple-v1beta3.yaml")
|
||||
processedArgs, tempFiles, err := preprocessV1Beta3Specs([]string{v1beta3File})
|
||||
|
||||
// Clean up temp files
|
||||
defer func() {
|
||||
for _, f := range tempFiles {
|
||||
_ = os.Remove(f)
|
||||
}
|
||||
}()
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, processedArgs)
|
||||
})
|
||||
|
||||
t.Run("v1beta3 with --set values should succeed", func(t *testing.T) {
|
||||
viper.Set("values", []string{})
|
||||
viper.Set("set", []string{"kubernetes.enabled=true"})
|
||||
|
||||
v1beta3File := filepath.Join(testutils.FileDir(), "../../examples/preflight/simple-v1beta3.yaml")
|
||||
processedArgs, tempFiles, err := preprocessV1Beta3Specs([]string{v1beta3File})
|
||||
|
||||
// Clean up temp files
|
||||
defer func() {
|
||||
for _, f := range tempFiles {
|
||||
_ = os.Remove(f)
|
||||
}
|
||||
}()
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, processedArgs)
|
||||
})
|
||||
|
||||
t.Run("v1beta2 without values should succeed", func(t *testing.T) {
|
||||
viper.Set("values", []string{})
|
||||
viper.Set("set", []string{})
|
||||
|
||||
v1beta2File := filepath.Join(testutils.FileDir(), "../../testdata/preflightspec/troubleshoot_v1beta2_preflight_gotest.yaml")
|
||||
processedArgs, tempFiles, err := preprocessV1Beta3Specs([]string{v1beta2File})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, processedArgs)
|
||||
assert.Empty(t, tempFiles)
|
||||
assert.Equal(t, []string{v1beta2File}, processedArgs)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func RunTemplate(templateFile string, valuesFiles []string, setValues []string,
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to load values file %s", valuesFile)
|
||||
}
|
||||
values = MergeMaps(values, fileValues)
|
||||
values = mergeMaps(values, fileValues)
|
||||
}
|
||||
|
||||
// Apply --set values (Helm semantics)
|
||||
@@ -165,8 +165,8 @@ func cleanRenderedYAML(content string) string {
|
||||
return strings.Join(cleaned, "\n") + "\n"
|
||||
}
|
||||
|
||||
// MergeMaps recursively merges two maps, with overlay taking precedence
|
||||
func MergeMaps(base, overlay map[string]interface{}) map[string]interface{} {
|
||||
// mergeMaps recursively merges two maps
|
||||
func mergeMaps(base, overlay map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
// Copy base map
|
||||
@@ -180,7 +180,7 @@ func MergeMaps(base, overlay map[string]interface{}) map[string]interface{} {
|
||||
// If both are maps, merge recursively
|
||||
if baseMap, ok := baseVal.(map[string]interface{}); ok {
|
||||
if overlayMap, ok := v.(map[string]interface{}); ok {
|
||||
result[k] = MergeMaps(baseMap, overlayMap)
|
||||
result[k] = mergeMaps(baseMap, overlayMap)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,7 +430,7 @@ func TestRender_V1Beta3_MergeMultipleValuesFiles_And_SetPrecedence(t *testing.T)
|
||||
for _, f := range []string{minimalFile, file1, file3} {
|
||||
m, err := loadValuesFile(f)
|
||||
require.NoError(t, err)
|
||||
vals = MergeMaps(vals, m)
|
||||
vals = mergeMaps(vals, m)
|
||||
}
|
||||
|
||||
// First render without --set; expect NO kubernetes analyzer
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
package redact
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
)
|
||||
|
||||
// LineReader reads lines from an io.Reader while tracking whether each line
|
||||
// ended with a newline character. This is essential for preserving the exact
|
||||
// structure of input files during redaction - binary files and text files
|
||||
// without trailing newlines should not have newlines added to them.
|
||||
//
|
||||
// Unlike bufio.Scanner which strips newlines and requires the caller to add
|
||||
// them back, LineReader explicitly tracks the presence of newlines so callers
|
||||
// can conditionally restore them only when they were originally present.
|
||||
type LineReader struct {
|
||||
reader *bufio.Reader
|
||||
}
|
||||
|
||||
// NewLineReader creates a new LineReader that reads from the given io.Reader.
|
||||
// The reader is wrapped in a bufio.Reader for efficient byte-by-byte reading.
|
||||
func NewLineReader(r io.Reader) *LineReader {
|
||||
return &LineReader{
|
||||
reader: bufio.NewReader(r),
|
||||
}
|
||||
}
|
||||
|
||||
// ReadLine reads the next line from the reader and returns:
|
||||
// - line content (without the newline character if present)
|
||||
// - whether the line ended with a newline (\n)
|
||||
// - any error encountered
|
||||
//
|
||||
// Return values:
|
||||
// - (content, true, nil) - line ended with \n, more content may follow
|
||||
// - (content, false, io.EOF) - last line without \n (file doesn't end with newline)
|
||||
// - (nil, false, io.EOF) - reached EOF with no content (empty file or end of file)
|
||||
// - (content, false, error) - encountered a non-EOF error
|
||||
//
|
||||
// The function respects constants.SCANNER_MAX_SIZE and returns an error if a single
|
||||
// line exceeds this limit. This prevents memory exhaustion on files with extremely
|
||||
// long lines or binary files without newlines that are larger than the limit.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// lr := NewLineReader(input)
|
||||
// for {
|
||||
// line, hadNewline, err := lr.ReadLine()
|
||||
// if err == io.EOF && len(line) == 0 {
|
||||
// break // End of file, no more content
|
||||
// }
|
||||
//
|
||||
// // Process line...
|
||||
// fmt.Print(string(line))
|
||||
// if hadNewline {
|
||||
// fmt.Print("\n")
|
||||
// }
|
||||
//
|
||||
// if err == io.EOF {
|
||||
// break // Last line processed
|
||||
// }
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
func (lr *LineReader) ReadLine() ([]byte, bool, error) {
|
||||
// Initialize line as empty slice (not nil) to ensure consistent return values
|
||||
// Empty lines (just \n) should return []byte{}, not nil
|
||||
line := []byte{}
|
||||
|
||||
for {
|
||||
b, err := lr.reader.ReadByte()
|
||||
|
||||
// Handle errors
|
||||
if err == io.EOF {
|
||||
if len(line) > 0 {
|
||||
// Last line without newline - return the content we have
|
||||
return line, false, io.EOF
|
||||
}
|
||||
// Nothing left to read - empty file or end of content
|
||||
return nil, false, io.EOF
|
||||
}
|
||||
if err != nil {
|
||||
// Non-EOF error encountered
|
||||
return line, false, err
|
||||
}
|
||||
|
||||
// Found newline character
|
||||
if b == '\n' {
|
||||
// Return the line (may be empty for blank lines)
|
||||
return line, true, nil
|
||||
}
|
||||
|
||||
// Accumulate byte into line buffer
|
||||
line = append(line, b)
|
||||
|
||||
// Check buffer limit to prevent memory exhaustion
|
||||
// This is especially important for binary files without newlines
|
||||
if len(line) > constants.SCANNER_MAX_SIZE {
|
||||
return nil, false, bufio.ErrTooLong
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
package redact
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Test 1.2 & 1.3: NewLineReader creates instance correctly
|
||||
func TestNewLineReader(t *testing.T) {
|
||||
input := strings.NewReader("test")
|
||||
lr := NewLineReader(input)
|
||||
|
||||
require.NotNil(t, lr)
|
||||
require.NotNil(t, lr.reader)
|
||||
}
|
||||
|
||||
// Test 1.8: Empty file → (nil, false, io.EOF)
|
||||
func TestLineReader_EmptyFile(t *testing.T) {
|
||||
lr := NewLineReader(strings.NewReader(""))
|
||||
|
||||
line, hadNewline, err := lr.ReadLine()
|
||||
|
||||
assert.Nil(t, line)
|
||||
assert.False(t, hadNewline)
|
||||
assert.Equal(t, io.EOF, err)
|
||||
}
|
||||
|
||||
// Test 1.9: Single line with \n → (content, true, nil)
|
||||
func TestLineReader_SingleLineWithNewline(t *testing.T) {
|
||||
lr := NewLineReader(strings.NewReader("hello world\n"))
|
||||
|
||||
line, hadNewline, err := lr.ReadLine()
|
||||
|
||||
assert.Equal(t, []byte("hello world"), line)
|
||||
assert.True(t, hadNewline)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Second read should return EOF
|
||||
line, hadNewline, err = lr.ReadLine()
|
||||
assert.Nil(t, line)
|
||||
assert.False(t, hadNewline)
|
||||
assert.Equal(t, io.EOF, err)
|
||||
}
|
||||
|
||||
// Test 1.10: Single line without \n → (content, false, io.EOF)
|
||||
func TestLineReader_SingleLineWithoutNewline(t *testing.T) {
|
||||
lr := NewLineReader(strings.NewReader("hello world"))
|
||||
|
||||
line, hadNewline, err := lr.ReadLine()
|
||||
|
||||
assert.Equal(t, []byte("hello world"), line)
|
||||
assert.False(t, hadNewline)
|
||||
assert.Equal(t, io.EOF, err)
|
||||
}
|
||||
|
||||
// Test 1.11: Multiple lines with \n → correct for each
|
||||
func TestLineReader_MultipleLinesWithNewlines(t *testing.T) {
|
||||
input := "line1\nline2\nline3\n"
|
||||
lr := NewLineReader(strings.NewReader(input))
|
||||
|
||||
// First line
|
||||
line, hadNewline, err := lr.ReadLine()
|
||||
assert.Equal(t, []byte("line1"), line)
|
||||
assert.True(t, hadNewline)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Second line
|
||||
line, hadNewline, err = lr.ReadLine()
|
||||
assert.Equal(t, []byte("line2"), line)
|
||||
assert.True(t, hadNewline)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Third line
|
||||
line, hadNewline, err = lr.ReadLine()
|
||||
assert.Equal(t, []byte("line3"), line)
|
||||
assert.True(t, hadNewline)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// EOF
|
||||
line, hadNewline, err = lr.ReadLine()
|
||||
assert.Nil(t, line)
|
||||
assert.False(t, hadNewline)
|
||||
assert.Equal(t, io.EOF, err)
|
||||
}
|
||||
|
||||
// Test 1.12: Last line without \n → (content, false, io.EOF)
|
||||
func TestLineReader_LastLineWithoutNewline(t *testing.T) {
|
||||
input := "line1\nline2\nline3"
|
||||
lr := NewLineReader(strings.NewReader(input))
|
||||
|
||||
// First line
|
||||
line, hadNewline, err := lr.ReadLine()
|
||||
assert.Equal(t, []byte("line1"), line)
|
||||
assert.True(t, hadNewline)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Second line
|
||||
line, hadNewline, err = lr.ReadLine()
|
||||
assert.Equal(t, []byte("line2"), line)
|
||||
assert.True(t, hadNewline)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Third line (no trailing newline)
|
||||
line, hadNewline, err = lr.ReadLine()
|
||||
assert.Equal(t, []byte("line3"), line)
|
||||
assert.False(t, hadNewline)
|
||||
assert.Equal(t, io.EOF, err)
|
||||
}
|
||||
|
||||
// Test 1.13: Binary data (no \n) → (all content, false, io.EOF)
|
||||
func TestLineReader_BinaryData(t *testing.T) {
|
||||
binaryData := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0xFF, 0xFE}
|
||||
lr := NewLineReader(bytes.NewReader(binaryData))
|
||||
|
||||
line, hadNewline, err := lr.ReadLine()
|
||||
|
||||
assert.Equal(t, binaryData, line)
|
||||
assert.False(t, hadNewline)
|
||||
assert.Equal(t, io.EOF, err)
|
||||
}
|
||||
|
||||
// Test 1.14: Line exceeding max size → error
|
||||
func TestLineReader_LineExceedingMaxSize(t *testing.T) {
|
||||
// Create a line that exceeds SCANNER_MAX_SIZE
|
||||
largeData := make([]byte, constants.SCANNER_MAX_SIZE+100)
|
||||
for i := range largeData {
|
||||
largeData[i] = 'a'
|
||||
}
|
||||
|
||||
lr := NewLineReader(bytes.NewReader(largeData))
|
||||
|
||||
line, hadNewline, err := lr.ReadLine()
|
||||
|
||||
assert.Nil(t, line)
|
||||
assert.False(t, hadNewline)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, bufio.ErrTooLong)
|
||||
}
|
||||
|
||||
// Test 1.15: File with only \n → ([], true, nil)
|
||||
func TestLineReader_OnlyNewline(t *testing.T) {
|
||||
lr := NewLineReader(strings.NewReader("\n"))
|
||||
|
||||
line, hadNewline, err := lr.ReadLine()
|
||||
|
||||
assert.Equal(t, []byte{}, line) // Empty line
|
||||
assert.True(t, hadNewline)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Second read should return EOF
|
||||
line, hadNewline, err = lr.ReadLine()
|
||||
assert.Nil(t, line)
|
||||
assert.False(t, hadNewline)
|
||||
assert.Equal(t, io.EOF, err)
|
||||
}
|
||||
|
||||
// Additional test: File with empty lines (multiple newlines)
|
||||
func TestLineReader_EmptyLines(t *testing.T) {
|
||||
input := "\n\n\n"
|
||||
lr := NewLineReader(strings.NewReader(input))
|
||||
|
||||
// First empty line
|
||||
line, hadNewline, err := lr.ReadLine()
|
||||
assert.Equal(t, []byte{}, line)
|
||||
assert.True(t, hadNewline)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Second empty line
|
||||
line, hadNewline, err = lr.ReadLine()
|
||||
assert.Equal(t, []byte{}, line)
|
||||
assert.True(t, hadNewline)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Third empty line
|
||||
line, hadNewline, err = lr.ReadLine()
|
||||
assert.Equal(t, []byte{}, line)
|
||||
assert.True(t, hadNewline)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// EOF
|
||||
line, hadNewline, err = lr.ReadLine()
|
||||
assert.Nil(t, line)
|
||||
assert.False(t, hadNewline)
|
||||
assert.Equal(t, io.EOF, err)
|
||||
}
|
||||
|
||||
// Additional test: Mixed content with and without newlines
|
||||
func TestLineReader_MixedContent(t *testing.T) {
|
||||
input := "line1\n\nline3"
|
||||
lr := NewLineReader(strings.NewReader(input))
|
||||
|
||||
// First line
|
||||
line, hadNewline, err := lr.ReadLine()
|
||||
assert.Equal(t, []byte("line1"), line)
|
||||
assert.True(t, hadNewline)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Empty line
|
||||
line, hadNewline, err = lr.ReadLine()
|
||||
assert.Equal(t, []byte{}, line)
|
||||
assert.True(t, hadNewline)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Last line without newline
|
||||
line, hadNewline, err = lr.ReadLine()
|
||||
assert.Equal(t, []byte("line3"), line)
|
||||
assert.False(t, hadNewline)
|
||||
assert.Equal(t, io.EOF, err)
|
||||
}
|
||||
|
||||
// Additional test: Large but valid file (under max size)
|
||||
func TestLineReader_LargeValidFile(t *testing.T) {
|
||||
// Create a line that's large but under the limit
|
||||
largeData := make([]byte, constants.SCANNER_MAX_SIZE-100)
|
||||
for i := range largeData {
|
||||
largeData[i] = 'x'
|
||||
}
|
||||
largeData = append(largeData, '\n')
|
||||
|
||||
lr := NewLineReader(bytes.NewReader(largeData))
|
||||
|
||||
line, hadNewline, err := lr.ReadLine()
|
||||
|
||||
assert.Equal(t, constants.SCANNER_MAX_SIZE-100, len(line))
|
||||
assert.True(t, hadNewline)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// Additional test: Binary file with embedded newlines
|
||||
func TestLineReader_BinaryWithEmbeddedNewlines(t *testing.T) {
|
||||
binaryData := []byte{0x01, 0x02, '\n', 0x03, 0x04, '\n', 0x05}
|
||||
lr := NewLineReader(bytes.NewReader(binaryData))
|
||||
|
||||
// First "line" (up to first \n)
|
||||
line, hadNewline, err := lr.ReadLine()
|
||||
assert.Equal(t, []byte{0x01, 0x02}, line)
|
||||
assert.True(t, hadNewline)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Second "line"
|
||||
line, hadNewline, err = lr.ReadLine()
|
||||
assert.Equal(t, []byte{0x03, 0x04}, line)
|
||||
assert.True(t, hadNewline)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Last "line" without newline
|
||||
line, hadNewline, err = lr.ReadLine()
|
||||
assert.Equal(t, []byte{0x05}, line)
|
||||
assert.False(t, hadNewline)
|
||||
assert.Equal(t, io.EOF, err)
|
||||
}
|
||||
|
||||
// Test edge case: Very small reads
|
||||
func TestLineReader_SingleByteReads(t *testing.T) {
|
||||
input := "a\nb\nc"
|
||||
lr := NewLineReader(strings.NewReader(input))
|
||||
|
||||
line, hadNewline, err := lr.ReadLine()
|
||||
assert.Equal(t, []byte("a"), line)
|
||||
assert.True(t, hadNewline)
|
||||
assert.NoError(t, err)
|
||||
|
||||
line, hadNewline, err = lr.ReadLine()
|
||||
assert.Equal(t, []byte("b"), line)
|
||||
assert.True(t, hadNewline)
|
||||
assert.NoError(t, err)
|
||||
|
||||
line, hadNewline, err = lr.ReadLine()
|
||||
assert.Equal(t, []byte("c"), line)
|
||||
assert.False(t, hadNewline)
|
||||
assert.Equal(t, io.EOF, err)
|
||||
}
|
||||
|
||||
// Benchmark: LineReader vs bufio.Scanner performance
|
||||
func BenchmarkLineReader(b *testing.B) {
|
||||
// Create test data
|
||||
var buf bytes.Buffer
|
||||
for i := 0; i < 1000; i++ {
|
||||
buf.WriteString("This is line number ")
|
||||
buf.WriteString(string(rune(i)))
|
||||
buf.WriteString(" with some content\n")
|
||||
}
|
||||
data := buf.Bytes()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
lr := NewLineReader(bytes.NewReader(data))
|
||||
for {
|
||||
_, _, err := lr.ReadLine()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-43
@@ -3,7 +3,6 @@ package redact
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
@@ -26,12 +25,6 @@ func literalString(match []byte, path, name string) Redactor {
|
||||
}
|
||||
}
|
||||
|
||||
// Redact processes the input reader line-by-line, replacing literal string matches.
|
||||
// Unlike the previous implementation using bufio.Scanner, this now uses LineReader
|
||||
// to preserve the exact newline structure of the input file. Lines that originally
|
||||
// ended with \n will have \n added back, while lines without \n (like the last line
|
||||
// of a file without a trailing newline, or binary files) will not have \n added.
|
||||
// This ensures binary files and text files without trailing newlines are not corrupted.
|
||||
func (r literalRedactor) Redact(input io.Reader, path string) io.Reader {
|
||||
out, writer := io.Pipe()
|
||||
|
||||
@@ -41,8 +34,7 @@ func (r literalRedactor) Redact(input io.Reader, path string) io.Reader {
|
||||
if err == nil || err == io.EOF {
|
||||
writer.Close()
|
||||
} else {
|
||||
// Check if error is about line exceeding maximum size
|
||||
if errors.Is(err, bufio.ErrTooLong) {
|
||||
if err == bufio.ErrTooLong {
|
||||
s := fmt.Sprintf("Error redacting %q. A line in the file exceeded %d MB max length", path, constants.SCANNER_MAX_SIZE/1024/1024)
|
||||
klog.V(2).Info(s)
|
||||
} else {
|
||||
@@ -52,24 +44,17 @@ func (r literalRedactor) Redact(input io.Reader, path string) io.Reader {
|
||||
}
|
||||
}()
|
||||
|
||||
// Use LineReader instead of bufio.Scanner to track newline presence
|
||||
lineReader := NewLineReader(input)
|
||||
tokenizer := GetGlobalTokenizer()
|
||||
buf := make([]byte, constants.BUF_INIT_SIZE)
|
||||
scanner := bufio.NewScanner(input)
|
||||
scanner.Buffer(buf, constants.SCANNER_MAX_SIZE)
|
||||
|
||||
lineNum := 0
|
||||
|
||||
for {
|
||||
line, hadNewline, readErr := lineReader.ReadLine()
|
||||
|
||||
// Handle EOF with no content - we're done
|
||||
if readErr == io.EOF && len(line) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// We have content to process
|
||||
for scanner.Scan() {
|
||||
lineNum++
|
||||
line := scanner.Bytes()
|
||||
|
||||
// Perform literal string replacement
|
||||
var clean []byte
|
||||
tokenizer := GetGlobalTokenizer()
|
||||
if tokenizer.IsEnabled() {
|
||||
// For literal redaction, we tokenize the matched value
|
||||
matchStr := string(r.match)
|
||||
@@ -81,20 +66,12 @@ func (r literalRedactor) Redact(input io.Reader, path string) io.Reader {
|
||||
clean = bytes.ReplaceAll(line, r.match, maskTextBytes)
|
||||
}
|
||||
|
||||
// Write the line (redacted or original)
|
||||
err = writeBytes(writer, clean)
|
||||
// Append newline since scanner strips it
|
||||
err = writeBytes(writer, clean, NEW_LINE)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Only add newline if original line had one
|
||||
if hadNewline {
|
||||
err = writeBytes(writer, NEW_LINE)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Track redaction if content changed
|
||||
if !bytes.Equal(clean, line) {
|
||||
addRedaction(Redaction{
|
||||
RedactorName: r.redactName,
|
||||
@@ -104,16 +81,9 @@ func (r literalRedactor) Redact(input io.Reader, path string) io.Reader {
|
||||
IsDefaultRedactor: r.isDefault,
|
||||
})
|
||||
}
|
||||
|
||||
// Check if we hit EOF after processing this line
|
||||
if readErr == io.EOF {
|
||||
break
|
||||
}
|
||||
// Check for non-EOF errors
|
||||
if readErr != nil {
|
||||
err = readErr
|
||||
return
|
||||
}
|
||||
}
|
||||
if scanErr := scanner.Err(); scanErr != nil {
|
||||
err = scanErr
|
||||
}
|
||||
}()
|
||||
return out
|
||||
|
||||
@@ -1,386 +0,0 @@
|
||||
package redact
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Test basic literal redaction functionality
|
||||
func TestLiteralRedactor_BasicRedaction(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
match string
|
||||
inputString string
|
||||
wantString string
|
||||
}{
|
||||
{
|
||||
name: "Simple literal match",
|
||||
match: "secret123",
|
||||
inputString: "password=secret123",
|
||||
wantString: "password=***HIDDEN***", // No trailing newline in input
|
||||
},
|
||||
{
|
||||
name: "Multiple occurrences",
|
||||
match: "secret",
|
||||
inputString: "secret is secret here secret",
|
||||
wantString: "***HIDDEN*** is ***HIDDEN*** here ***HIDDEN***",
|
||||
},
|
||||
{
|
||||
name: "No match",
|
||||
match: "xyz",
|
||||
inputString: "no match here",
|
||||
wantString: "no match here",
|
||||
},
|
||||
{
|
||||
name: "With trailing newline",
|
||||
match: "secret",
|
||||
inputString: "secret\n",
|
||||
wantString: "***HIDDEN***\n",
|
||||
},
|
||||
{
|
||||
name: "Multiline with newlines",
|
||||
match: "secret",
|
||||
inputString: "line1 secret\nline2 secret\n",
|
||||
wantString: "line1 ***HIDDEN***\nline2 ***HIDDEN***\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
redactor := literalString([]byte(tt.match), "testfile", tt.name)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(tt.inputString)), "")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.wantString, string(result))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test 4.12: Binary file → unchanged
|
||||
func TestLiteralRedactor_BinaryFile(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
// Binary content with no newlines and no match
|
||||
binaryData := []byte{0x01, 0x02, 0x03, 0x04, 0x00, 0xFF, 0xFE, 0xAB, 0xCD}
|
||||
|
||||
redactor := literalString([]byte("notfound"), "testfile", t.Name())
|
||||
|
||||
out := redactor.Redact(bytes.NewReader(binaryData), "test.bin")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, binaryData, result, "Binary file should be unchanged")
|
||||
}
|
||||
|
||||
// Test: Binary file with every single byte value (0x00 -> 0xFF)
|
||||
func TestLiteralRedactor_AllSingleByteValues(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
// Create binary data with every possible byte value
|
||||
binaryData := make([]byte, 256)
|
||||
for i := 0; i < 256; i++ {
|
||||
binaryData[i] = byte(i)
|
||||
}
|
||||
|
||||
redactor := literalString([]byte("notfound"), "testfile", t.Name())
|
||||
|
||||
out := redactor.Redact(bytes.NewReader(binaryData), "test.bin")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, binaryData, result, "Binary file with all byte values should be unchanged")
|
||||
require.Len(t, result, 256, "Should preserve all 256 bytes")
|
||||
}
|
||||
|
||||
// Test: Binary file with every two-byte combination (0x00+0x00 -> 0xFF+0xFF)
|
||||
func TestLiteralRedactor_AllTwoByteValues(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
// Create binary data with all 65536 two-byte combinations (128KB)
|
||||
binaryData := make([]byte, 256*256*2)
|
||||
pos := 0
|
||||
for i := 0; i < 256; i++ {
|
||||
for j := 0; j < 256; j++ {
|
||||
binaryData[pos] = byte(i)
|
||||
binaryData[pos+1] = byte(j)
|
||||
pos += 2
|
||||
}
|
||||
}
|
||||
|
||||
redactor := literalString([]byte("notfound"), "testfile", t.Name())
|
||||
|
||||
out := redactor.Redact(bytes.NewReader(binaryData), "test.bin")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, binaryData, result, "Binary file with all two-byte combinations should be unchanged")
|
||||
require.Len(t, result, 256*256*2, "Should preserve all 131072 bytes")
|
||||
}
|
||||
|
||||
// Test 4.12 (variant): Binary file with literal match → redacted, no extra newlines
|
||||
func TestLiteralRedactor_BinaryFileWithMatch(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
// Binary content with a literal match (0xFF 0xFE sequence)
|
||||
binaryData := []byte{0x01, 0x02, 0xFF, 0xFE, 0x03, 0x04}
|
||||
|
||||
redactor := literalString([]byte{0xFF, 0xFE}, "testfile", t.Name())
|
||||
|
||||
// We need to mock maskTextBytes for this test to work predictably
|
||||
// For now, test that no newlines are added
|
||||
out := redactor.Redact(bytes.NewReader(binaryData), "test.bin")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, binaryData, result, "Binary should be redacted")
|
||||
require.NotContains(t, result, []byte{0xFF, 0xFE}, "Match should be replaced")
|
||||
// Most importantly: no trailing newline added to binary file
|
||||
require.NotEqual(t, byte('\n'), result[len(result)-1], "Should not add trailing newline")
|
||||
}
|
||||
|
||||
// Test 4.13: Text with trailing \n → preserved
|
||||
func TestLiteralRedactor_TextWithTrailingNewline(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := "hello world\n"
|
||||
|
||||
redactor := literalString([]byte("xyz"), "testfile", t.Name())
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello world\n", string(result), "Trailing newline should be preserved")
|
||||
}
|
||||
|
||||
// Test 4.14: Text without trailing \n → preserved
|
||||
func TestLiteralRedactor_TextWithoutTrailingNewline(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := "hello world"
|
||||
|
||||
redactor := literalString([]byte("xyz"), "testfile", t.Name())
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello world", string(result), "No newline should be added")
|
||||
}
|
||||
|
||||
// Test 4.15: Empty file → unchanged
|
||||
func TestLiteralRedactor_EmptyFile(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := ""
|
||||
|
||||
redactor := literalString([]byte("secret"), "testfile", t.Name())
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "", string(result), "Empty file should remain empty")
|
||||
}
|
||||
|
||||
// Test 4.16: Literal match and replacement works
|
||||
func TestLiteralRedactor_LiteralMatch(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := "password=secret123"
|
||||
|
||||
redactor := literalString([]byte("secret123"), "testfile", t.Name())
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "password=***HIDDEN***", string(result))
|
||||
}
|
||||
|
||||
// Test 4.17: Multiple occurrences replaced
|
||||
func TestLiteralRedactor_MultipleOccurrences(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := "secret here and secret there and secret everywhere"
|
||||
|
||||
redactor := literalString([]byte("secret"), "testfile", t.Name())
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "***HIDDEN*** here and ***HIDDEN*** there and ***HIDDEN*** everywhere", string(result))
|
||||
}
|
||||
|
||||
// Test 4.17 (variant): Multiple occurrences across lines
|
||||
func TestLiteralRedactor_MultipleOccurrencesMultiline(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := "line1 secret\nline2 secret\nline3 secret\n"
|
||||
|
||||
redactor := literalString([]byte("secret"), "testfile", t.Name())
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
expected := "line1 ***HIDDEN***\nline2 ***HIDDEN***\nline3 ***HIDDEN***\n"
|
||||
require.Equal(t, expected, string(result))
|
||||
}
|
||||
|
||||
// Test 4.18: Tokenization works
|
||||
func TestLiteralRedactor_Tokenization(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
// Enable tokenization for this test
|
||||
EnableTokenization()
|
||||
defer DisableTokenization()
|
||||
|
||||
input := "password=secret123"
|
||||
|
||||
redactor := literalString([]byte("secret123"), "testfile", t.Name())
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
// Result should contain a token, not the original or ***HIDDEN***
|
||||
require.NotContains(t, string(result), "secret123")
|
||||
require.NotContains(t, string(result), "***HIDDEN***")
|
||||
require.Contains(t, string(result), "password=")
|
||||
}
|
||||
|
||||
// Test 4.19: Redaction count accurate
|
||||
func TestLiteralRedactor_RedactionCount(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := "secret here\nsecret there"
|
||||
|
||||
// Use unique redactor name and filename to avoid pollution from parallel tests
|
||||
uniqueFile := "TestLiteralRedactor_RedactionCount_file"
|
||||
uniqueRedactor := "TestLiteralRedactor_RedactionCount_redactor"
|
||||
|
||||
redactor := literalString([]byte("secret"), uniqueFile, uniqueRedactor)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "")
|
||||
_, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
redactions := GetRedactionList()
|
||||
// Two lines, each with one match = 2 redaction events
|
||||
require.Len(t, redactions.ByRedactor[uniqueRedactor], 2, "Should record 2 redactions (one per line)")
|
||||
require.Len(t, redactions.ByFile[uniqueFile], 2, "Should record 2 redactions for file")
|
||||
}
|
||||
|
||||
// Test 4.20: Backward compatibility - existing behavior preserved for text with newlines
|
||||
func TestLiteralRedactor_BackwardCompatibility(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := "line1 secret\nline2 secret\nline3\n"
|
||||
|
||||
redactor := literalString([]byte("secret"), "testfile", t.Name())
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
expected := "line1 ***HIDDEN***\nline2 ***HIDDEN***\nline3\n"
|
||||
require.Equal(t, expected, string(result), "Behavior for text with newlines should be unchanged")
|
||||
}
|
||||
|
||||
// Test 4.20 (variant): Literal match on last line without \n
|
||||
func TestLiteralRedactor_LastLineWithoutNewline(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := "line1\nline2 secret"
|
||||
|
||||
redactor := literalString([]byte("secret"), "testfile", t.Name())
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
expected := "line1\nline2 ***HIDDEN***"
|
||||
require.Equal(t, expected, string(result), "Should not add newline to last line")
|
||||
}
|
||||
|
||||
// Additional test: Empty line handling
|
||||
func TestLiteralRedactor_EmptyLines(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := "\n\n\n"
|
||||
|
||||
redactor := literalString([]byte("secret"), "testfile", t.Name())
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "\n\n\n", string(result), "Empty lines should be preserved")
|
||||
}
|
||||
|
||||
// Additional test: Large file with many matches
|
||||
func TestLiteralRedactor_LargeFile(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
// Create large file with many occurrences
|
||||
var input strings.Builder
|
||||
for i := 0; i < 1000; i++ {
|
||||
input.WriteString("line ")
|
||||
input.WriteString("secret")
|
||||
input.WriteString(" here\n")
|
||||
}
|
||||
|
||||
redactor := literalString([]byte("secret"), "testfile", t.Name())
|
||||
|
||||
out := redactor.Redact(strings.NewReader(input.String()), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotContains(t, string(result), "secret", "All secrets should be redacted")
|
||||
require.Contains(t, string(result), "***HIDDEN***")
|
||||
}
|
||||
|
||||
// Additional test: Partial match should not be replaced
|
||||
func TestLiteralRedactor_PartialMatchNotReplaced(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := "secret secretive secrets"
|
||||
|
||||
// Should only replace exact literal "secret", not "secretive" or "secrets"
|
||||
redactor := literalString([]byte("secret"), "testfile", t.Name())
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "***HIDDEN*** ***HIDDEN***ive ***HIDDEN***s", string(result))
|
||||
}
|
||||
+34
-127
@@ -1,18 +1,12 @@
|
||||
package redact
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// lineState represents a line and whether it ended with a newline character.
|
||||
// This is used by MultiLineRedactor to track newline state for line pairs.
|
||||
type lineState struct {
|
||||
content []byte
|
||||
hadNewline bool
|
||||
}
|
||||
|
||||
type MultiLineRedactor struct {
|
||||
scan *regexp.Regexp
|
||||
re1 *regexp.Regexp
|
||||
@@ -45,16 +39,6 @@ func NewMultiLineRedactor(re1 LineRedactor, re2 string, maskText, path, name str
|
||||
return &MultiLineRedactor{scan: scanCompiled, re1: compiled1, re2: compiled2, maskText: maskText, filePath: path, redactName: name, isDefault: isDefault}, nil
|
||||
}
|
||||
|
||||
// Redact processes the input reader in pairs of lines, applying redaction patterns.
|
||||
// Unlike the previous implementation using bufio.Reader with readLine(), this now
|
||||
// uses LineReader to preserve the exact newline structure of the input file.
|
||||
//
|
||||
// The MultiLineRedactor works by:
|
||||
// 1. Reading pairs of lines (line1, line2)
|
||||
// 2. If line1 matches the selector pattern (re1), redact line2 using re2
|
||||
// 3. Write both lines with their original newline structure preserved
|
||||
//
|
||||
// This ensures binary files and text files without trailing newlines are not corrupted.
|
||||
func (r *MultiLineRedactor) Redact(input io.Reader, path string) io.Reader {
|
||||
out, writer := io.Pipe()
|
||||
go func() {
|
||||
@@ -64,74 +48,51 @@ func (r *MultiLineRedactor) Redact(input io.Reader, path string) io.Reader {
|
||||
}()
|
||||
|
||||
tokenizer := GetGlobalTokenizer()
|
||||
lineReader := NewLineReader(input)
|
||||
|
||||
// Try to read first two lines
|
||||
line1, nl1, line2, nl2, readErr := getNextTwoLines(lineReader, nil)
|
||||
|
||||
// Handle case where we can't read 2 lines (empty file or single line)
|
||||
// Note: We check line1 == nil (not len(line1) == 0) because:
|
||||
// - nil means truly empty file with no content
|
||||
// - []byte{} (len==0) means an empty line that had a newline (e.g., "\n")
|
||||
if readErr != nil && line1 == nil {
|
||||
// Empty file - nothing to write
|
||||
// Propagate non-EOF errors (EOF is expected for empty files)
|
||||
if readErr != io.EOF {
|
||||
err = readErr
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if readErr != nil {
|
||||
// Only 1 line available (or empty line with newline) - write it and exit
|
||||
// FIX: This is the bug fix - only add newline if original had one
|
||||
// Also handles empty lines (line1 == []byte{} with nl1 == true)
|
||||
err = writeLine(writer, line1, nl1)
|
||||
reader := bufio.NewReader(input)
|
||||
line1, line2, err := getNextTwoLines(reader, nil)
|
||||
if err != nil {
|
||||
// this will print 2 blank lines for empty input...
|
||||
// Append newlines since scanner strips them
|
||||
err = writeBytes(writer, line1, NEW_LINE, line2, NEW_LINE)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Propagate non-EOF errors (EOF is expected for single-line files)
|
||||
if readErr != io.EOF {
|
||||
err = readErr
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Process line pairs
|
||||
flushLastLine := false
|
||||
lineNum := 1
|
||||
|
||||
for readErr == nil {
|
||||
for err == nil {
|
||||
lineNum++ // the first line that can be redacted is line 2
|
||||
|
||||
// Pre-filter: if scan is not nil, check if line1 matches scan by lowercasing it
|
||||
// is scan is not nil, then check if line1 matches scan by lowercasing it
|
||||
if r.scan != nil {
|
||||
lowerLine1 := bytes.ToLower(line1)
|
||||
if !r.scan.Match(lowerLine1) {
|
||||
// No match - write line1 and advance
|
||||
err = writeLine(writer, line1, nl1)
|
||||
// Append newline since scanner strips it
|
||||
err = writeBytes(writer, line1, NEW_LINE)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
line1, nl1, line2, nl2, readErr = getNextTwoLines(lineReader, &lineState{line2, nl2})
|
||||
line1, line2, err = getNextTwoLines(reader, &line2)
|
||||
flushLastLine = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Check if line1 matches the selector pattern (re1)
|
||||
// If line1 matches re1, then transform line2 using re2
|
||||
if !r.re1.Match(line1) {
|
||||
// No match - write line1 and advance
|
||||
err = writeLine(writer, line1, nl1)
|
||||
// Append newline since scanner strips it
|
||||
err = writeBytes(writer, line1, NEW_LINE)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
line1, nl1, line2, nl2, readErr = getNextTwoLines(lineReader, &lineState{line2, nl2})
|
||||
line1, line2, err = getNextTwoLines(reader, &line2)
|
||||
flushLastLine = true
|
||||
continue
|
||||
}
|
||||
|
||||
// line1 matched selector - redact line2
|
||||
flushLastLine = false
|
||||
var clean []byte
|
||||
if tokenizer.IsEnabled() {
|
||||
@@ -144,17 +105,13 @@ func (r *MultiLineRedactor) Redact(input io.Reader, path string) io.Reader {
|
||||
clean = r.re2.ReplaceAll(line2, substStr)
|
||||
}
|
||||
|
||||
// Write line1 (selector line) and line2 (redacted line)
|
||||
err = writeLine(writer, line1, nl1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = writeLine(writer, clean, nl2)
|
||||
// Append newlines since scanner strips them
|
||||
err = writeBytes(writer, line1, NEW_LINE, clean, NEW_LINE)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Track redaction if content changed
|
||||
// if clean is not equal to line2, a redaction was performed
|
||||
if !bytes.Equal(clean, line2) {
|
||||
addRedaction(Redaction{
|
||||
RedactorName: r.redactName,
|
||||
@@ -165,92 +122,42 @@ func (r *MultiLineRedactor) Redact(input io.Reader, path string) io.Reader {
|
||||
})
|
||||
}
|
||||
|
||||
// Get next pair
|
||||
line1, nl1, line2, nl2, readErr = getNextTwoLines(lineReader, nil)
|
||||
line1, line2, err = getNextTwoLines(reader, nil)
|
||||
}
|
||||
|
||||
// After loop exits (readErr != nil), check if we have an unwritten line1
|
||||
// This happens in two cases:
|
||||
// 1. flushLastLine=true: line1 was advanced but not written (scan/re1 didn't match)
|
||||
// 2. line1 != nil: we read line1 but couldn't get line2 (unpaired line at end)
|
||||
// Note: We check line1 != nil (not len(line1) > 0) to handle empty lines ([]byte{})
|
||||
if flushLastLine || line1 != nil {
|
||||
err = writeLine(writer, line1, nl1)
|
||||
if flushLastLine {
|
||||
// Append newline since scanner strip it
|
||||
err = writeBytes(writer, line1, NEW_LINE)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Propagate non-EOF read errors to the caller
|
||||
// EOF is expected (end of file) and not an error condition
|
||||
// Note: readErr is always non-nil here (loop exited), but we only propagate non-EOF errors
|
||||
if readErr != io.EOF {
|
||||
err = readErr
|
||||
}
|
||||
}()
|
||||
return out
|
||||
}
|
||||
|
||||
// getNextTwoLines reads the next pair of lines from the LineReader.
|
||||
// It returns the content and newline state for both lines.
|
||||
//
|
||||
// If curLine2 is provided, it's used as line1 (optimization for advancing through file).
|
||||
// Otherwise, both lines are read fresh from the reader.
|
||||
//
|
||||
// Returns:
|
||||
// - line1, hadNewline1: First line content and newline state
|
||||
// - line2, hadNewline2: Second line content and newline state
|
||||
// - err: Error only if we couldn't read line1, or if line2 read failed with non-EOF error
|
||||
//
|
||||
// Note: If line2 returns (content, false, io.EOF), we treat this as SUCCESS because
|
||||
// we got the content. The EOF just means it didn't have a trailing newline.
|
||||
func getNextTwoLines(lr *LineReader, curLine2 *lineState) (
|
||||
line1 []byte, hadNewline1 bool,
|
||||
line2 []byte, hadNewline2 bool,
|
||||
err error,
|
||||
) {
|
||||
func getNextTwoLines(reader *bufio.Reader, curLine2 *[]byte) (line1 []byte, line2 []byte, err error) {
|
||||
line2 = []byte{}
|
||||
|
||||
if curLine2 == nil {
|
||||
// Read both lines fresh
|
||||
line1, hadNewline1, err = lr.ReadLine()
|
||||
line1, err = readLine(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
line2, hadNewline2, err = lr.ReadLine()
|
||||
// If we got line2 content but hit EOF, that's OK - it just means no trailing newline
|
||||
if err == io.EOF && len(line2) > 0 {
|
||||
err = nil // Clear the error - we successfully read both lines
|
||||
}
|
||||
line2, err = readLine(reader)
|
||||
return
|
||||
}
|
||||
|
||||
// Use cached line2 as new line1 (optimization)
|
||||
line1 = curLine2.content
|
||||
hadNewline1 = curLine2.hadNewline
|
||||
|
||||
// Read new line2
|
||||
line2, hadNewline2, err = lr.ReadLine()
|
||||
// If we got line2 content but hit EOF, that's OK - it just means no trailing newline
|
||||
if err == io.EOF && len(line2) > 0 {
|
||||
err = nil // Clear the error - we successfully read both lines
|
||||
line1 = *curLine2
|
||||
line2, err = readLine(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// writeLine writes a line to the writer, optionally adding a newline if hadNewline is true.
|
||||
// This helper reduces code duplication in the Redact function.
|
||||
func writeLine(w io.Writer, line []byte, hadNewline bool) error {
|
||||
if err := writeBytes(w, line); err != nil {
|
||||
return err
|
||||
}
|
||||
if hadNewline {
|
||||
if err := writeBytes(w, NEW_LINE); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeBytes writes all byte slices to the writer
|
||||
// in the order they are passed in the variadic argument
|
||||
func writeBytes(w io.Writer, bs ...[]byte) error {
|
||||
|
||||
+13
-285
@@ -28,7 +28,8 @@ func Test_NewMultiLineRedactor(t *testing.T) {
|
||||
inputString: `"name": "secret_access_key"
|
||||
"value": "dfeadsfsdfe"`,
|
||||
wantString: `"name": "secret_access_key"
|
||||
"value": "***HIDDEN***"`, // No trailing newline in input, so none in output
|
||||
"value": "***HIDDEN***"
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Redact multiline with AWS secret id",
|
||||
@@ -39,7 +40,8 @@ func Test_NewMultiLineRedactor(t *testing.T) {
|
||||
inputString: `"name": "ACCESS_KEY_ID"
|
||||
"value": "dfeadsfsdfe"`,
|
||||
wantString: `"name": "ACCESS_KEY_ID"
|
||||
"value": "***HIDDEN***"`, // No trailing newline in input, so none in output
|
||||
"value": "***HIDDEN***"
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Redact multiline with OSD",
|
||||
@@ -50,7 +52,8 @@ func Test_NewMultiLineRedactor(t *testing.T) {
|
||||
inputString: `"entity": "osd.1abcdef"
|
||||
"key": "Gjt8s0WkfPtxZUo7gI8a0awbQGHgzuprdaedfb=="`,
|
||||
wantString: `"entity": "osd.1abcdef"
|
||||
"key": "***HIDDEN***"`, // No trailing newline in input, so none in output
|
||||
"key": "***HIDDEN***"
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Redact multiline with AWS secret access key and scan regex",
|
||||
@@ -62,7 +65,8 @@ func Test_NewMultiLineRedactor(t *testing.T) {
|
||||
inputString: `"name": "secret_access_key"
|
||||
"value": "dfeadsfsdfe"`,
|
||||
wantString: `"name": "secret_access_key"
|
||||
"value": "***HIDDEN***"`, // No trailing newline in input, so none in output
|
||||
"value": "***HIDDEN***"
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Redact multiline with AWS secret id and scan regex",
|
||||
@@ -74,7 +78,8 @@ func Test_NewMultiLineRedactor(t *testing.T) {
|
||||
inputString: `"name": "ACCESS_KEY_ID"
|
||||
"value": "dfeadsfsdfe"`,
|
||||
wantString: `"name": "ACCESS_KEY_ID"
|
||||
"value": "***HIDDEN***"`, // No trailing newline in input, so none in output
|
||||
"value": "***HIDDEN***"
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Redact multiline with OSD and scan regex",
|
||||
@@ -86,7 +91,8 @@ func Test_NewMultiLineRedactor(t *testing.T) {
|
||||
inputString: `"entity": "osd.1abcdef"
|
||||
"key": "Gjt8s0WkfPtxZUo7gI8a0awbQGHgzuprdaedfb=="`,
|
||||
wantString: `"entity": "osd.1abcdef"
|
||||
"key": "***HIDDEN***"`, // No trailing newline in input, so none in output
|
||||
"key": "***HIDDEN***"
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Multiple newlines with no match",
|
||||
@@ -96,7 +102,7 @@ func Test_NewMultiLineRedactor(t *testing.T) {
|
||||
},
|
||||
redactor: `(?i)("value": *")(?P<mask>.*[^\"]*)(")`,
|
||||
inputString: "no match\n\n no match \n\n",
|
||||
wantString: "no match\n\n no match \n\n", // Input has trailing newline, should be preserved
|
||||
wantString: "no match\n\n no match \n\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -152,281 +158,3 @@ func Test_writeBytes(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test 3.16: Binary file (no newlines) → unchanged
|
||||
func TestMultiLineRedactor_BinaryFile(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
// Binary content with no newlines - the bug that caused 2 extra bytes
|
||||
binaryData := []byte{0x01, 0x02, 0x03, 0x04, 0x00, 0xFF, 0xFE, 0xAB, 0xCD}
|
||||
|
||||
redactor, err := NewMultiLineRedactor(
|
||||
LineRedactor{regex: `"name":`},
|
||||
`"value":`,
|
||||
MASK_TEXT, "testfile", "test", false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader(binaryData), "test.bin")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, binaryData, result, "Binary file should be unchanged (no extra newlines)")
|
||||
}
|
||||
|
||||
// Test: Binary file with every single byte value (0x00 -> 0xFF)
|
||||
func TestMultiLineRedactor_AllSingleByteValues(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
// Create binary data with every possible byte value
|
||||
binaryData := make([]byte, 256)
|
||||
for i := 0; i < 256; i++ {
|
||||
binaryData[i] = byte(i)
|
||||
}
|
||||
|
||||
redactor, err := NewMultiLineRedactor(
|
||||
LineRedactor{regex: `"name":`},
|
||||
`"value":`,
|
||||
MASK_TEXT, "testfile", t.Name(), false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader(binaryData), "test.bin")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, binaryData, result, "Binary file with all byte values should be unchanged")
|
||||
require.Len(t, result, 256, "Should preserve all 256 bytes")
|
||||
}
|
||||
|
||||
// Test: Binary file with every two-byte combination (0x00+0x00 -> 0xFF+0xFF)
|
||||
func TestMultiLineRedactor_AllTwoByteValues(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
// Create binary data with all 65,536 two-byte combinations (128KB)
|
||||
binaryData := make([]byte, 256*256*2)
|
||||
pos := 0
|
||||
for i := 0; i < 256; i++ {
|
||||
for j := 0; j < 256; j++ {
|
||||
binaryData[pos] = byte(i)
|
||||
binaryData[pos+1] = byte(j)
|
||||
pos += 2
|
||||
}
|
||||
}
|
||||
|
||||
redactor, err := NewMultiLineRedactor(
|
||||
LineRedactor{regex: `"name":`},
|
||||
`"value":`,
|
||||
MASK_TEXT, "testfile", t.Name(), false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader(binaryData), "test.bin")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, binaryData, result, "Binary file with all two-byte combinations should be unchanged")
|
||||
require.Len(t, result, 256*256*2, "Should preserve all 131,072 bytes (64k combinations)")
|
||||
}
|
||||
|
||||
// Test 3.17: Single line with \n → unchanged
|
||||
func TestMultiLineRedactor_SingleLineWithNewline(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := "single line\n"
|
||||
|
||||
redactor, err := NewMultiLineRedactor(
|
||||
LineRedactor{regex: `"name":`},
|
||||
`"value":`,
|
||||
MASK_TEXT, "testfile", "test", false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "single line\n", string(result))
|
||||
}
|
||||
|
||||
// Test 3.18: Single line without \n → unchanged
|
||||
func TestMultiLineRedactor_SingleLineWithoutNewline(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := "single line"
|
||||
|
||||
redactor, err := NewMultiLineRedactor(
|
||||
LineRedactor{regex: `"name":`},
|
||||
`"value":`,
|
||||
MASK_TEXT, "testfile", "test", false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "single line", string(result), "No newline should be added")
|
||||
}
|
||||
|
||||
// Test 3.19: Empty file → unchanged
|
||||
func TestMultiLineRedactor_EmptyFile(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := ""
|
||||
|
||||
redactor, err := NewMultiLineRedactor(
|
||||
LineRedactor{regex: `"name":`},
|
||||
`"value":`,
|
||||
MASK_TEXT, "testfile", "test", false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "", string(result), "Empty file should remain empty")
|
||||
}
|
||||
|
||||
// Test 3.20: Two lines, matches selector → line2 redacted
|
||||
func TestMultiLineRedactor_TwoLinesMatch(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := `"name": "PASSWORD"
|
||||
"value": "secret123"`
|
||||
|
||||
redactor, err := NewMultiLineRedactor(
|
||||
LineRedactor{regex: `(?i)"name": *"PASSWORD"`},
|
||||
`(?i)("value": *")(?P<mask>[^"]*)(")`,
|
||||
MASK_TEXT, "testfile", "test", false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
expected := `"name": "PASSWORD"
|
||||
"value": "***HIDDEN***"`
|
||||
require.Equal(t, expected, string(result))
|
||||
}
|
||||
|
||||
// Test 3.21: Two lines, no selector match → unchanged
|
||||
func TestMultiLineRedactor_TwoLinesNoMatch(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := `"name": "USERNAME"
|
||||
"value": "admin"`
|
||||
|
||||
redactor, err := NewMultiLineRedactor(
|
||||
LineRedactor{regex: `(?i)"name": *"PASSWORD"`},
|
||||
`(?i)("value": *")(?P<mask>[^"]*)(")`,
|
||||
MASK_TEXT, "testfile", "test", false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
expected := `"name": "USERNAME"
|
||||
"value": "admin"`
|
||||
require.Equal(t, expected, string(result))
|
||||
}
|
||||
|
||||
// Test 3.22: Multiple line pairs → correct redactions
|
||||
func TestMultiLineRedactor_MultiplePairs(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := `"name": "PASSWORD"
|
||||
"value": "secret1"
|
||||
"name": "TOKEN"
|
||||
"value": "secret2"
|
||||
"name": "USERNAME"
|
||||
"value": "admin"
|
||||
`
|
||||
|
||||
redactor, err := NewMultiLineRedactor(
|
||||
LineRedactor{regex: `(?i)"name": *"(PASSWORD|TOKEN)"`},
|
||||
`(?i)("value": *")(?P<mask>[^"]*)(")`,
|
||||
MASK_TEXT, "testfile", "test", false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
expected := `"name": "PASSWORD"
|
||||
"value": "***HIDDEN***"
|
||||
"name": "TOKEN"
|
||||
"value": "***HIDDEN***"
|
||||
"name": "USERNAME"
|
||||
"value": "admin"
|
||||
`
|
||||
require.Equal(t, expected, string(result))
|
||||
}
|
||||
|
||||
// Test 3.23: Three lines (pair + unpaired)
|
||||
func TestMultiLineRedactor_ThreeLines(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := `"name": "PASSWORD"
|
||||
"value": "secret"
|
||||
unpaired line`
|
||||
|
||||
redactor, err := NewMultiLineRedactor(
|
||||
LineRedactor{regex: `(?i)"name": *"PASSWORD"`},
|
||||
`(?i)("value": *")(?P<mask>[^"]*)(")`,
|
||||
MASK_TEXT, "testfile", "test", false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
expected := `"name": "PASSWORD"
|
||||
"value": "***HIDDEN***"
|
||||
unpaired line`
|
||||
require.Equal(t, expected, string(result))
|
||||
}
|
||||
|
||||
// Test 3.24: Large file with selector matches
|
||||
func TestMultiLineRedactor_LargeFile(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
var input strings.Builder
|
||||
for i := 0; i < 1000; i++ {
|
||||
input.WriteString(`"name": "PASSWORD"` + "\n")
|
||||
input.WriteString(`"value": "secret"` + "\n")
|
||||
}
|
||||
|
||||
redactor, err := NewMultiLineRedactor(
|
||||
LineRedactor{regex: `(?i)"name": *"PASSWORD"`},
|
||||
`(?i)("value": *")(?P<mask>[^"]*)(")`,
|
||||
MASK_TEXT, "testfile", "test", false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(strings.NewReader(input.String()), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify all secrets were redacted
|
||||
require.NotContains(t, string(result), `"value": "secret"`)
|
||||
require.Contains(t, string(result), `"value": "***HIDDEN***"`)
|
||||
}
|
||||
|
||||
@@ -104,10 +104,6 @@ func GetRedactionList() RedactionList {
|
||||
}
|
||||
|
||||
func ResetRedactionList() {
|
||||
// Wait for all pending redaction goroutines to complete before resetting
|
||||
// This prevents race conditions where goroutines write to the map after reset
|
||||
pendingRedactions.Wait()
|
||||
|
||||
redactionListMut.Lock()
|
||||
defer redactionListMut.Unlock()
|
||||
allRedactions = RedactionList{
|
||||
|
||||
@@ -1724,7 +1724,6 @@ func Test_Redactors(t *testing.T) {
|
||||
|
||||
t.Run("test default redactors", func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
ResetRedactionList() // Ensure clean state before test
|
||||
redactors, err := getRedactors("testpath")
|
||||
req.NoError(err)
|
||||
|
||||
|
||||
+45
-75
@@ -3,7 +3,6 @@ package redact
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
@@ -40,13 +39,6 @@ func NewSingleLineRedactor(re LineRedactor, maskText, path, name string, isDefau
|
||||
return &SingleLineRedactor{scan: scanCompiled, re: compiled, maskText: maskText, filePath: path, redactName: name, isDefault: isDefault}, nil
|
||||
}
|
||||
|
||||
// Redact processes the input reader line-by-line, applying redaction patterns.
|
||||
|
||||
// Unlike the previous implementation using bufio.Scanner, this now uses LineReader
|
||||
// to preserve the exact newline structure of the input file. Lines that originally
|
||||
// ended with \n will have \n added back, while lines without \n (like the last line
|
||||
// of a file without a trailing newline, or binary files) will not have \n added.
|
||||
// This ensures binary files and text files without trailing newlines are not corrupted.
|
||||
func (r *SingleLineRedactor) Redact(input io.Reader, path string) io.Reader {
|
||||
out, writer := io.Pipe()
|
||||
|
||||
@@ -56,8 +48,7 @@ func (r *SingleLineRedactor) Redact(input io.Reader, path string) io.Reader {
|
||||
if err == nil || err == io.EOF {
|
||||
writer.Close()
|
||||
} else {
|
||||
// Check if error is about line exceeding maximum size
|
||||
if errors.Is(err, bufio.ErrTooLong) {
|
||||
if err == bufio.ErrTooLong {
|
||||
s := fmt.Sprintf("Error redacting %q. A line in the file exceeded %d MB max length", path, constants.SCANNER_MAX_SIZE/1024/1024)
|
||||
klog.V(2).Info(s)
|
||||
} else {
|
||||
@@ -67,89 +58,68 @@ func (r *SingleLineRedactor) Redact(input io.Reader, path string) io.Reader {
|
||||
}
|
||||
}()
|
||||
|
||||
// Use LineReader instead of bufio.Scanner to track newline presence
|
||||
lineReader := NewLineReader(input)
|
||||
buf := make([]byte, constants.BUF_INIT_SIZE)
|
||||
scanner := bufio.NewScanner(input)
|
||||
scanner.Buffer(buf, constants.SCANNER_MAX_SIZE)
|
||||
|
||||
tokenizer := GetGlobalTokenizer()
|
||||
lineNum := 0
|
||||
|
||||
for {
|
||||
line, hadNewline, readErr := lineReader.ReadLine()
|
||||
|
||||
// Handle EOF with no content - we're done
|
||||
if readErr == io.EOF && len(line) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// We have content to process
|
||||
for scanner.Scan() {
|
||||
lineNum++
|
||||
line := scanner.Bytes()
|
||||
|
||||
// Determine if we should redact this line
|
||||
shouldRedact := true
|
||||
|
||||
// Pre-filter: if scan is not nil, check if line matches scan by lowercasing it
|
||||
// is scan is not nil, then check if line matches scan by lowercasing it
|
||||
if r.scan != nil {
|
||||
lowerLine := bytes.ToLower(line)
|
||||
if !r.scan.Match(lowerLine) {
|
||||
shouldRedact = false
|
||||
// Append newline since scanner strips it
|
||||
err = writeBytes(writer, line, NEW_LINE)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Check if line matches the main redaction pattern
|
||||
if shouldRedact && !r.re.Match(line) {
|
||||
shouldRedact = false
|
||||
}
|
||||
|
||||
// Process the line (redact or pass through)
|
||||
var outputLine []byte
|
||||
if shouldRedact {
|
||||
// Line matches - perform redaction
|
||||
if tokenizer.IsEnabled() {
|
||||
// Use tokenized replacement - context comes from the redactor name
|
||||
context := r.redactName
|
||||
outputLine = getTokenizedReplacementPatternWithPath(r.re, line, context, r.filePath)
|
||||
} else {
|
||||
// Use original masking behavior
|
||||
substStr := []byte(getReplacementPattern(r.re, r.maskText))
|
||||
outputLine = r.re.ReplaceAll(line, substStr)
|
||||
}
|
||||
|
||||
// Track redaction if content changed
|
||||
if !bytes.Equal(outputLine, line) {
|
||||
addRedaction(Redaction{
|
||||
RedactorName: r.redactName,
|
||||
CharactersRemoved: len(line) - len(outputLine),
|
||||
Line: lineNum,
|
||||
File: r.filePath,
|
||||
IsDefaultRedactor: r.isDefault,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// No match - use original line
|
||||
outputLine = line
|
||||
}
|
||||
|
||||
// Write the line
|
||||
err = writeBytes(writer, outputLine)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Only add newline if original line had one
|
||||
if hadNewline {
|
||||
err = writeBytes(writer, NEW_LINE)
|
||||
// if scan matches, but re does not, do not redact
|
||||
if !r.re.Match(line) {
|
||||
// Append newline since scanner strips it
|
||||
err = writeBytes(writer, line, NEW_LINE)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if we hit EOF after processing this line
|
||||
if readErr == io.EOF {
|
||||
break
|
||||
var clean []byte
|
||||
if tokenizer.IsEnabled() {
|
||||
// Use tokenized replacement - context comes from the redactor name which often indicates the secret type
|
||||
context := r.redactName
|
||||
clean = getTokenizedReplacementPatternWithPath(r.re, line, context, r.filePath)
|
||||
} else {
|
||||
// Use original masking behavior
|
||||
substStr := []byte(getReplacementPattern(r.re, r.maskText))
|
||||
clean = r.re.ReplaceAll(line, substStr)
|
||||
}
|
||||
// Check for non-EOF errors
|
||||
if readErr != nil {
|
||||
err = readErr
|
||||
// Append newline since scanner strips it
|
||||
err = writeBytes(writer, clean, NEW_LINE)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// if clean is not equal to line, a redaction was performed
|
||||
if !bytes.Equal(clean, line) {
|
||||
addRedaction(Redaction{
|
||||
RedactorName: r.redactName,
|
||||
CharactersRemoved: len(line) - len(clean),
|
||||
Line: lineNum,
|
||||
File: r.filePath,
|
||||
IsDefaultRedactor: r.isDefault,
|
||||
})
|
||||
}
|
||||
}
|
||||
if scanErr := scanner.Err(); scanErr != nil {
|
||||
err = scanErr
|
||||
}
|
||||
}()
|
||||
return out
|
||||
|
||||
+16
-325
@@ -21,7 +21,7 @@ func TestNewSingleLineRedactor(t *testing.T) {
|
||||
name: "copied from default redactors",
|
||||
re: `(?i)(Pwd *= *)(?P<mask>[^\;]+)(;)`,
|
||||
inputString: `pwd = abcdef;`,
|
||||
wantString: "pwd = ***HIDDEN***;", // No trailing newline in input, so none in output
|
||||
wantString: "pwd = ***HIDDEN***;\n",
|
||||
wantRedactions: RedactionList{
|
||||
ByRedactor: map[string][]Redaction{
|
||||
"copied from default redactors": []Redaction{
|
||||
@@ -49,7 +49,7 @@ func TestNewSingleLineRedactor(t *testing.T) {
|
||||
name: "no leading matching group", // this is not the ideal behavior - why are we dropping ungrouped match components?
|
||||
re: `(?i)Pwd *= *(?P<mask>[^\;]+)(;)`,
|
||||
inputString: `pwd = abcdef;`,
|
||||
wantString: "***HIDDEN***;", // No trailing newline in input, so none in output
|
||||
wantString: "***HIDDEN***;\n",
|
||||
wantRedactions: RedactionList{
|
||||
ByRedactor: map[string][]Redaction{
|
||||
"no leading matching group": []Redaction{
|
||||
@@ -77,7 +77,7 @@ func TestNewSingleLineRedactor(t *testing.T) {
|
||||
name: "multiple matching literals",
|
||||
re: `(?i)(Pwd *= *)(?P<mask>[^\;]+)(;)`,
|
||||
inputString: `pwd = abcdef;abcdef`,
|
||||
wantString: "pwd = ***HIDDEN***;abcdef", // No trailing newline in input, so none in output
|
||||
wantString: "pwd = ***HIDDEN***;abcdef\n",
|
||||
wantRedactions: RedactionList{
|
||||
ByRedactor: map[string][]Redaction{
|
||||
"multiple matching literals": []Redaction{
|
||||
@@ -105,7 +105,8 @@ func TestNewSingleLineRedactor(t *testing.T) {
|
||||
name: "Redact values for environment variables that look like AWS Secret Access Keys",
|
||||
re: `(?i)("name":"[^\"]*SECRET_?ACCESS_?KEY","value":")(?P<mask>[^\"]*)(")`,
|
||||
inputString: `{"name":"SECRET_ACCESS_KEY","value":"123"}`,
|
||||
wantString: `{"name":"SECRET_ACCESS_KEY","value":"***HIDDEN***"}`, // No trailing newline in input, so none in output
|
||||
wantString: `{"name":"SECRET_ACCESS_KEY","value":"***HIDDEN***"}
|
||||
`,
|
||||
wantRedactions: RedactionList{
|
||||
ByRedactor: map[string][]Redaction{
|
||||
"Redact values for environment variables that look like AWS Secret Access Keys": []Redaction{
|
||||
@@ -133,7 +134,7 @@ func TestNewSingleLineRedactor(t *testing.T) {
|
||||
name: "Redact connection strings with username and password",
|
||||
re: `(?i)(https?|ftp)(:\/\/)(?P<mask>[^:\"\/]+){1}(:)(?P<mask>[^@\"\/]+){1}(?P<host>@[^:\/\s\"]+){1}(?P<port>:[\d]+)?`,
|
||||
inputString: `http://user:password@host:8888`,
|
||||
wantString: "http://***HIDDEN***:***HIDDEN***@host:8888", // No trailing newline in input, so none in output
|
||||
wantString: "http://***HIDDEN***:***HIDDEN***@host:8888\n",
|
||||
wantRedactions: RedactionList{
|
||||
ByRedactor: map[string][]Redaction{
|
||||
"Redact connection strings with username and password": []Redaction{
|
||||
@@ -162,7 +163,8 @@ func TestNewSingleLineRedactor(t *testing.T) {
|
||||
re: `(?i)("name":"[^\"]*SECRET_?ACCESS_?KEY","value":")(?P<mask>[^\"]*)(")`,
|
||||
scan: `secret_?access_?key`,
|
||||
inputString: `{"name":"SECRET_ACCESS_KEY","value":"123"}`,
|
||||
wantString: `{"name":"SECRET_ACCESS_KEY","value":"***HIDDEN***"}`, // No trailing newline in input, so none in output
|
||||
wantString: `{"name":"SECRET_ACCESS_KEY","value":"***HIDDEN***"}
|
||||
`,
|
||||
wantRedactions: RedactionList{
|
||||
ByRedactor: map[string][]Redaction{
|
||||
"Redact values for environment variables that look like AWS Secret Access Keys With Scan": {
|
||||
@@ -191,7 +193,8 @@ func TestNewSingleLineRedactor(t *testing.T) {
|
||||
re: `(?i)("name":"[^\"]*ACCESS_?KEY_?ID","value":")(?P<mask>[^\"]*)(")`,
|
||||
scan: `access_?key_?id`,
|
||||
inputString: `{"name":"ACCESS_KEY_ID","value":"123"}`,
|
||||
wantString: `{"name":"ACCESS_KEY_ID","value":"***HIDDEN***"}`, // No trailing newline in input, so none in output
|
||||
wantString: `{"name":"ACCESS_KEY_ID","value":"***HIDDEN***"}
|
||||
`,
|
||||
wantRedactions: RedactionList{
|
||||
ByRedactor: map[string][]Redaction{
|
||||
"Redact values for environment variables that look like Access Keys ID With Scan": {
|
||||
@@ -220,7 +223,8 @@ func TestNewSingleLineRedactor(t *testing.T) {
|
||||
re: `(?i)("name":"[^\"]*OWNER_?ACCOUNT","value":")(?P<mask>[^\"]*)(")`,
|
||||
scan: `owner_?account`,
|
||||
inputString: `{"name":"OWNER_ACCOUNT","value":"123"}`,
|
||||
wantString: `{"name":"OWNER_ACCOUNT","value":"***HIDDEN***"}`, // No trailing newline in input, so none in output
|
||||
wantString: `{"name":"OWNER_ACCOUNT","value":"***HIDDEN***"}
|
||||
`,
|
||||
wantRedactions: RedactionList{
|
||||
ByRedactor: map[string][]Redaction{
|
||||
"Redact values for environment variables that look like Owner Account With Scan": {
|
||||
@@ -249,7 +253,8 @@ func TestNewSingleLineRedactor(t *testing.T) {
|
||||
re: `(?i)(Data Source *= *)(?P<mask>[^\;]+)(;)`,
|
||||
scan: `data source`,
|
||||
inputString: `Data Source = abcdef;`,
|
||||
wantString: `Data Source = ***HIDDEN***;`, // No trailing newline in input, so none in output
|
||||
wantString: `Data Source = ***HIDDEN***;
|
||||
`,
|
||||
wantRedactions: RedactionList{
|
||||
ByRedactor: map[string][]Redaction{
|
||||
"Redact 'Data Source' values With Scan": {
|
||||
@@ -278,7 +283,8 @@ func TestNewSingleLineRedactor(t *testing.T) {
|
||||
re: `(?i)(https?|ftp)(:\/\/)(?P<mask>[^:\"\/]+){1}(:)(?P<mask>[^@\"\/]+){1}(?P<host>@[^:\/\s\"]+){1}(?P<port>:[\d]+)?`,
|
||||
scan: `https?|ftp`,
|
||||
inputString: `http://user:password@host:8888;`,
|
||||
wantString: `http://***HIDDEN***:***HIDDEN***@host:8888;`, // No trailing newline in input, so none in output
|
||||
wantString: `http://***HIDDEN***:***HIDDEN***@host:8888;
|
||||
`,
|
||||
wantRedactions: RedactionList{
|
||||
ByRedactor: map[string][]Redaction{
|
||||
"Redact connection strings With Scan": {
|
||||
@@ -334,318 +340,3 @@ func TestNewSingleLineRedactor(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2.15: Binary file (no newlines) → unchanged
|
||||
func TestSingleLineRedactor_BinaryFile(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
// Binary content with no newlines
|
||||
binaryData := []byte{0x01, 0x02, 0x03, 0x04, 0x00, 0xFF, 0xFE, 0xAB, 0xCD}
|
||||
|
||||
redactor, err := NewSingleLineRedactor(
|
||||
LineRedactor{regex: "password"}, // Pattern that won't match binary
|
||||
MASK_TEXT, "testfile", "test", false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader(binaryData), "test.bin")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, binaryData, result, "Binary file should be unchanged")
|
||||
}
|
||||
|
||||
// Test: Binary file with every single byte value (0x00 -> 0xFF)
|
||||
func TestSingleLineRedactor_AllSingleByteValues(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
// Create binary data with every possible byte value
|
||||
binaryData := make([]byte, 256)
|
||||
for i := 0; i < 256; i++ {
|
||||
binaryData[i] = byte(i)
|
||||
}
|
||||
|
||||
redactor, err := NewSingleLineRedactor(
|
||||
LineRedactor{regex: `secret`},
|
||||
MASK_TEXT, "testfile", t.Name(), false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader(binaryData), "test.bin")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, binaryData, result, "Binary file with all byte values should be unchanged")
|
||||
require.Len(t, result, 256, "Should preserve all 256 bytes")
|
||||
}
|
||||
|
||||
// Test: Binary file with every two-byte combination (0x00+0x00 -> 0xFF+0xFF)
|
||||
func TestSingleLineRedactor_AllTwoByteValues(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
// Create binary data with all 65,536 two-byte combinations (128KB)
|
||||
binaryData := make([]byte, 256*256*2)
|
||||
pos := 0
|
||||
for i := 0; i < 256; i++ {
|
||||
for j := 0; j < 256; j++ {
|
||||
binaryData[pos] = byte(i)
|
||||
binaryData[pos+1] = byte(j)
|
||||
pos += 2
|
||||
}
|
||||
}
|
||||
|
||||
redactor, err := NewSingleLineRedactor(
|
||||
LineRedactor{regex: `secret`},
|
||||
MASK_TEXT, "testfile", t.Name(), false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader(binaryData), "test.bin")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, binaryData, result, "Binary file with all two-byte combinations should be unchanged")
|
||||
require.Len(t, result, 256*256*2, "Should preserve all 131,072 bytes (64k combinations)")
|
||||
}
|
||||
|
||||
// Test 2.16: Text file with \n → preserved
|
||||
func TestSingleLineRedactor_TextFileWithNewline(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := "hello world\n"
|
||||
|
||||
redactor, err := NewSingleLineRedactor(
|
||||
LineRedactor{regex: "xyz"}, // No match
|
||||
MASK_TEXT, "testfile", "test", false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello world\n", string(result), "Trailing newline should be preserved")
|
||||
}
|
||||
|
||||
// Test 2.17: Text file without \n → preserved
|
||||
func TestSingleLineRedactor_TextFileWithoutNewline(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := "hello world"
|
||||
|
||||
redactor, err := NewSingleLineRedactor(
|
||||
LineRedactor{regex: "xyz"}, // No match
|
||||
MASK_TEXT, "testfile", "test", false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello world", string(result), "No newline should be added")
|
||||
}
|
||||
|
||||
// Test 2.18: Empty file → unchanged
|
||||
func TestSingleLineRedactor_EmptyFile(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := ""
|
||||
|
||||
redactor, err := NewSingleLineRedactor(
|
||||
LineRedactor{regex: "password"},
|
||||
MASK_TEXT, "testfile", "test", false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "", string(result), "Empty file should remain empty")
|
||||
}
|
||||
|
||||
// Test 2.19: Single line with secret → redacted correctly
|
||||
func TestSingleLineRedactor_SingleLineWithSecret(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := "password=secret123"
|
||||
|
||||
redactor, err := NewSingleLineRedactor(
|
||||
LineRedactor{regex: `(?i)(password=)(?P<mask>.*)`},
|
||||
MASK_TEXT, "testfile", "test", false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "password=***HIDDEN***", string(result))
|
||||
}
|
||||
|
||||
// Test 2.20: Multiple lines with secrets → all redacted
|
||||
func TestSingleLineRedactor_MultipleLinesWithSecrets(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := "password=secret1\npassword=secret2\npassword=secret3\n"
|
||||
|
||||
redactor, err := NewSingleLineRedactor(
|
||||
LineRedactor{regex: `(?i)(password=)(?P<mask>.*)`},
|
||||
MASK_TEXT, "testfile", "test", false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
expected := "password=***HIDDEN***\npassword=***HIDDEN***\npassword=***HIDDEN***\n"
|
||||
require.Equal(t, expected, string(result))
|
||||
}
|
||||
|
||||
// Test 2.21: Scan pattern filters correctly
|
||||
func TestSingleLineRedactor_ScanPatternFilters(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := "password=secret\nusername=admin\n"
|
||||
|
||||
redactor, err := NewSingleLineRedactor(
|
||||
LineRedactor{
|
||||
regex: `(?i)(password=)(?P<mask>.*)`,
|
||||
scan: `password`, // Only process lines containing "password"
|
||||
},
|
||||
MASK_TEXT, "testfile", "test", false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
expected := "password=***HIDDEN***\nusername=admin\n"
|
||||
require.Equal(t, expected, string(result))
|
||||
}
|
||||
|
||||
// Test 2.22: File with only one newline \n → one newline out
|
||||
func TestSingleLineRedactor_OnlyNewline(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
input := "\n"
|
||||
|
||||
redactor, err := NewSingleLineRedactor(
|
||||
LineRedactor{regex: "password"},
|
||||
MASK_TEXT, "testfile", "test", false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "\n", string(result))
|
||||
}
|
||||
|
||||
// Test 2.23: Mixed binary/text content
|
||||
func TestSingleLineRedactor_MixedContent(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
// Binary data with embedded newline
|
||||
input := []byte{0x01, 0x02, '\n', 0x03, 0x04}
|
||||
|
||||
redactor, err := NewSingleLineRedactor(
|
||||
LineRedactor{regex: "password"},
|
||||
MASK_TEXT, "testfile", "test", false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader(input), "test.bin")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, input, result, "Mixed content should be preserved")
|
||||
}
|
||||
|
||||
// Test: Windows CRLF (\r\n) line endings preserved
|
||||
func TestSingleLineRedactor_WindowsLineEndings(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
// Windows-style line endings with no secrets
|
||||
input := "line1\r\nline2\r\nline3\r\n"
|
||||
|
||||
redactor, err := NewSingleLineRedactor(
|
||||
LineRedactor{regex: `secret`},
|
||||
MASK_TEXT, "testfile", t.Name(), false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
// Windows line endings should be preserved exactly
|
||||
require.Equal(t, "line1\r\nline2\r\nline3\r\n", string(result),
|
||||
"Windows CRLF line endings should be preserved, not converted to LF")
|
||||
}
|
||||
|
||||
// Test: Windows CRLF with redaction
|
||||
func TestSingleLineRedactor_WindowsLineEndingsWithRedaction(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
// Windows-style line endings with a secret
|
||||
// Note: LineReader splits on \n, so line content includes \r
|
||||
input := "password=secret123\r\nusername=admin\r\n"
|
||||
|
||||
// Use a regex that doesn't capture the \r
|
||||
redactor, err := NewSingleLineRedactor(
|
||||
LineRedactor{regex: `(password=)(?P<mask>[^\r\n]+)`},
|
||||
MASK_TEXT, "testfile", t.Name(), false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
// \r\n should be preserved: regex doesn't capture \r, so it stays in output
|
||||
expected := "password=***HIDDEN***\r\nusername=admin\r\n"
|
||||
require.Equal(t, expected, string(result),
|
||||
"Windows CRLF should be preserved - \\r not captured by regex, \\n added by LineReader")
|
||||
}
|
||||
|
||||
// Test 2.24: Large binary file (1MB, no newlines) → preserved
|
||||
func TestSingleLineRedactor_LargeBinaryFile(t *testing.T) {
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
// Create 1MB of binary data
|
||||
largeData := make([]byte, 1024*1024)
|
||||
for i := range largeData {
|
||||
largeData[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
redactor, err := NewSingleLineRedactor(
|
||||
LineRedactor{regex: "password"},
|
||||
MASK_TEXT, "testfile", "test", false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := redactor.Redact(bytes.NewReader(largeData), "large.bin")
|
||||
result, err := io.ReadAll(out)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, largeData, result, "Large binary file should be unchanged")
|
||||
}
|
||||
|
||||
@@ -106,7 +106,6 @@ func runCollectors(ctx context.Context, collectors []*troubleshootv1beta2.Collec
|
||||
}
|
||||
|
||||
allCollectorsMap := make(map[reflect.Type][]collect.Collector)
|
||||
collectorTypeOrder := make([]reflect.Type, 0) // Preserve order of collector types
|
||||
allCollectedData := make(map[string][]byte)
|
||||
|
||||
for _, desiredCollector := range collectSpecs {
|
||||
@@ -117,17 +116,12 @@ func runCollectors(ctx context.Context, collectors []*troubleshootv1beta2.Collec
|
||||
return nil, errors.Wrap(err, "failed to check RBAC for collectors")
|
||||
}
|
||||
collectorType := reflect.TypeOf(collector)
|
||||
if _, exists := allCollectorsMap[collectorType]; !exists {
|
||||
collectorTypeOrder = append(collectorTypeOrder, collectorType)
|
||||
}
|
||||
allCollectorsMap[collectorType] = append(allCollectorsMap[collectorType], collector)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate over collector types in the order they appeared in collectSpecs
|
||||
for _, collectorType := range collectorTypeOrder {
|
||||
collectors := allCollectorsMap[collectorType]
|
||||
for _, collectors := range allCollectorsMap {
|
||||
if mergeCollector, ok := collectors[0].(collect.MergeableCollector); ok {
|
||||
mergedCollectors, err := mergeCollector.Merge(collectors)
|
||||
if err != nil {
|
||||
|
||||
@@ -16,9 +16,8 @@ import (
|
||||
)
|
||||
|
||||
// ExtractLicenseFromBundle extracts the license ID from a support bundle
|
||||
// It first looks for cluster-resources/license.json, then falls back to searching
|
||||
// cluster-resources/configmaps/* for a license field
|
||||
// Returns both the license ID and the app slug
|
||||
// It looks in cluster-resources/configmaps/* for a license field
|
||||
// Returns both the license ID and the app slug (from the filename where license was found)
|
||||
func ExtractLicenseFromBundle(bundlePath string) (string, string, error) {
|
||||
file, err := os.Open(bundlePath)
|
||||
if err != nil {
|
||||
@@ -43,27 +42,7 @@ func ExtractLicenseFromBundle(bundlePath string) (string, string, error) {
|
||||
return "", "", errors.Wrap(err, "failed to read tar header")
|
||||
}
|
||||
|
||||
// First priority: check for the new license.json file
|
||||
if strings.Contains(header.Name, "cluster-resources/license.json") && header.Typeflag == tar.TypeReg {
|
||||
content := make([]byte, header.Size)
|
||||
if _, err := io.ReadFull(tarReader, content); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse the license.json file
|
||||
var licenseData struct {
|
||||
LicenseID string `json:"licenseID"`
|
||||
AppSlug string `json:"appSlug"`
|
||||
}
|
||||
if err := json.Unmarshal(content, &licenseData); err == nil {
|
||||
if licenseData.LicenseID != "" && licenseData.AppSlug != "" {
|
||||
return licenseData.LicenseID, licenseData.AppSlug, nil
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Fallback: process files in cluster-resources/configmaps/
|
||||
// Only process files in cluster-resources/configmaps/ (may be nested under bundle directory)
|
||||
if !strings.Contains(header.Name, "cluster-resources/configmaps/") {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
// UploadToReplicatedApp uploads a support bundle directly to replicated.app
|
||||
// using the app slug as the upload path
|
||||
func UploadToReplicatedApp(bundlePath, licenseID, appSlug, uploadDomain string) error {
|
||||
func UploadToReplicatedApp(bundlePath, licenseID, appSlug string) error {
|
||||
// Open the bundle file
|
||||
file, err := os.Open(bundlePath)
|
||||
if err != nil {
|
||||
@@ -23,14 +23,8 @@ func UploadToReplicatedApp(bundlePath, licenseID, appSlug, uploadDomain string)
|
||||
return errors.Wrap(err, "failed to stat file")
|
||||
}
|
||||
|
||||
// Use custom domain if provided, otherwise default to replicated.app
|
||||
domain := uploadDomain
|
||||
if domain == "" {
|
||||
domain = "replicated.app"
|
||||
}
|
||||
|
||||
// Build the upload URL using the app slug
|
||||
uploadURL := fmt.Sprintf("https://%s/supportbundle/upload/%s", domain, appSlug)
|
||||
uploadURL := fmt.Sprintf("https://replicated.app/supportbundle/upload/%s", appSlug)
|
||||
|
||||
// Create the request
|
||||
req, err := http.NewRequest("POST", uploadURL, file)
|
||||
@@ -59,7 +53,7 @@ func UploadToReplicatedApp(bundlePath, licenseID, appSlug, uploadDomain string)
|
||||
}
|
||||
|
||||
// UploadBundleAutoDetect uploads a support bundle with automatic license and app slug detection
|
||||
func UploadBundleAutoDetect(bundlePath string, providedLicenseID, providedAppSlug, uploadDomain string) error {
|
||||
func UploadBundleAutoDetect(bundlePath string, providedLicenseID, providedAppSlug string) error {
|
||||
licenseID := providedLicenseID
|
||||
|
||||
// Always extract from bundle to get app slug (and license if not provided)
|
||||
@@ -85,15 +79,9 @@ func UploadBundleAutoDetect(bundlePath string, providedLicenseID, providedAppSlu
|
||||
appSlug = extractedAppSlug
|
||||
}
|
||||
|
||||
// Determine target domain for upload message
|
||||
targetDomain := uploadDomain
|
||||
if targetDomain == "" {
|
||||
targetDomain = "replicated.app"
|
||||
}
|
||||
|
||||
// Upload the bundle
|
||||
fmt.Printf("Uploading support bundle to %s...\n", targetDomain)
|
||||
if err := UploadToReplicatedApp(bundlePath, licenseID, appSlug, uploadDomain); err != nil {
|
||||
fmt.Printf("Uploading support bundle to replicated.app...\n")
|
||||
if err := UploadToReplicatedApp(bundlePath, licenseID, appSlug); err != nil {
|
||||
return errors.Wrap(err, "failed to upload bundle")
|
||||
}
|
||||
|
||||
|
||||
+21
-21
@@ -48,12 +48,14 @@ trap "rm -rf $TEMP_DIR" EXIT
|
||||
|
||||
echo -e "\n${BLUE}Step 1: Downloading artifacts...${NC}"
|
||||
|
||||
# Download all test-results artifacts from the run
|
||||
# Try downloading all artifacts (will download test-results-v1beta3-*, test-results-v1beta2-*, test-results-supportbundle-*)
|
||||
if ! gh run download "$RUN_ID" --dir "$TEMP_DIR" 2>/dev/null; then
|
||||
echo -e "${RED}Error: Failed to download artifacts from run ${RUN_ID}${NC}"
|
||||
echo "Make sure the workflow run completed successfully."
|
||||
exit 1
|
||||
# Download artifacts from the run
|
||||
cd "$TEMP_DIR"
|
||||
if ! gh run download "$RUN_ID" --name "regression-test-results-${RUN_ID}-1" 2>/dev/null; then
|
||||
# Try without attempt suffix
|
||||
if ! gh run download "$RUN_ID" 2>/dev/null; then
|
||||
echo -e "${RED}Error: Failed to download artifacts from run ${RUN_ID}${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✓ Artifacts downloaded${NC}"
|
||||
@@ -61,29 +63,27 @@ echo -e "${GREEN}✓ Artifacts downloaded${NC}"
|
||||
# Check which bundles are present
|
||||
echo -e "\n${BLUE}Step 2: Checking available bundles...${NC}"
|
||||
|
||||
# Use find to locate bundles in artifact subdirectories
|
||||
V1BETA3_BUNDLE=$(find "$TEMP_DIR" -name "preflight-v1beta3-bundle.tar.gz" | head -1)
|
||||
V1BETA2_BUNDLE=$(find "$TEMP_DIR" -name "preflight-v1beta2-bundle.tar.gz" | head -1)
|
||||
SUPPORTBUNDLE=$(find "$TEMP_DIR" -name "supportbundle.tar.gz" | head -1)
|
||||
V1BETA3_BUNDLE=""
|
||||
V1BETA2_BUNDLE=""
|
||||
SUPPORTBUNDLE=""
|
||||
|
||||
if [ -n "$V1BETA3_BUNDLE" ]; then
|
||||
if [ -f "preflight-v1beta3-bundle.tar.gz" ] || [ -f "test/output/preflight-v1beta3-bundle.tar.gz" ]; then
|
||||
V1BETA3_BUNDLE=$(find . -name "preflight-v1beta3-bundle.tar.gz" | head -1)
|
||||
echo -e "${GREEN}✓${NC} Found v1beta3 preflight bundle"
|
||||
fi
|
||||
|
||||
if [ -n "$V1BETA2_BUNDLE" ]; then
|
||||
if [ -f "preflight-v1beta2-bundle.tar.gz" ] || [ -f "test/output/preflight-v1beta2-bundle.tar.gz" ]; then
|
||||
V1BETA2_BUNDLE=$(find . -name "preflight-v1beta2-bundle.tar.gz" | head -1)
|
||||
echo -e "${GREEN}✓${NC} Found v1beta2 preflight bundle"
|
||||
fi
|
||||
|
||||
if [ -n "$SUPPORTBUNDLE" ]; then
|
||||
if [ -f "supportbundle.tar.gz" ] || [ -f "test/output/supportbundle.tar.gz" ]; then
|
||||
SUPPORTBUNDLE=$(find . -name "supportbundle.tar.gz" | head -1)
|
||||
echo -e "${GREEN}✓${NC} Found support bundle"
|
||||
fi
|
||||
|
||||
if [ -z "$V1BETA3_BUNDLE" ] && [ -z "$V1BETA2_BUNDLE" ] && [ -z "$SUPPORTBUNDLE" ]; then
|
||||
echo -e "${RED}Error: No bundles found in artifacts${NC}"
|
||||
echo "Downloaded artifacts:"
|
||||
ls -la "$TEMP_DIR"
|
||||
echo -e "\nSearching for bundles:"
|
||||
find "$TEMP_DIR" -name "*.tar.gz" -o -name "*.json"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -110,21 +110,21 @@ echo -e "\n${BLUE}Step 3: Updating baselines...${NC}"
|
||||
# Update v1beta3 baseline
|
||||
if [ -n "$V1BETA3_BUNDLE" ]; then
|
||||
mkdir -p test/baselines/preflight-v1beta3
|
||||
cp "$V1BETA3_BUNDLE" test/baselines/preflight-v1beta3/baseline.tar.gz
|
||||
cp "$TEMP_DIR/$V1BETA3_BUNDLE" test/baselines/preflight-v1beta3/baseline.tar.gz
|
||||
echo -e "${GREEN}✓${NC} Updated preflight-v1beta3 baseline"
|
||||
fi
|
||||
|
||||
# Update v1beta2 baseline
|
||||
if [ -n "$V1BETA2_BUNDLE" ]; then
|
||||
mkdir -p test/baselines/preflight-v1beta2
|
||||
cp "$V1BETA2_BUNDLE" test/baselines/preflight-v1beta2/baseline.tar.gz
|
||||
cp "$TEMP_DIR/$V1BETA2_BUNDLE" test/baselines/preflight-v1beta2/baseline.tar.gz
|
||||
echo -e "${GREEN}✓${NC} Updated preflight-v1beta2 baseline"
|
||||
fi
|
||||
|
||||
# Update support bundle baseline
|
||||
if [ -n "$SUPPORTBUNDLE" ]; then
|
||||
mkdir -p test/baselines/supportbundle
|
||||
cp "$SUPPORTBUNDLE" test/baselines/supportbundle/baseline.tar.gz
|
||||
cp "$TEMP_DIR/$SUPPORTBUNDLE" test/baselines/supportbundle/baseline.tar.gz
|
||||
echo -e "${GREEN}✓${NC} Updated supportbundle baseline"
|
||||
fi
|
||||
|
||||
@@ -139,7 +139,7 @@ cat > test/baselines/metadata.json <<EOF
|
||||
"updated_at": "$CURRENT_DATE",
|
||||
"git_sha": "$GIT_SHA",
|
||||
"workflow_run_id": "$RUN_ID",
|
||||
"k8s_version": "v1.31.2-k3s1",
|
||||
"k8s_version": "v1.28.3",
|
||||
"updated_by": "$(git config user.name) <$(git config user.email)>"
|
||||
}
|
||||
EOF
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"updated_at": "2025-12-10T17:29:30Z",
|
||||
"git_sha": "7d73318d1ea65a72cfe2ffa7ea52940cf4b98f18",
|
||||
"workflow_run_id": "20107431959",
|
||||
"k8s_version": "v1.31.2-k3s1",
|
||||
"updated_by": "hedge-sparrow <sparrow@spooky.academy>"
|
||||
}
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user