mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-08-27 00:37:20 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4ce199005 | ||
|
|
48c45d7b0a | ||
|
|
2fdd21fca1 | ||
|
|
572cb8e31d | ||
|
|
21948fe959 | ||
|
|
b56b98742e | ||
|
|
b105fa62c7 | ||
|
|
558a8f5110 | ||
|
|
070fd9bab4 | ||
|
|
e89c858bd2 | ||
|
|
483467d8b9 | ||
|
|
27903af638 | ||
|
|
6eb09ee9d5 | ||
|
|
845fdcc8c8 | ||
|
|
a3c453d1d6 | ||
|
|
35d5ad17af | ||
|
|
daeab2dc20 | ||
|
|
5a611b3a6a | ||
|
|
fc72539585 | ||
|
|
69743729a3 | ||
|
|
6d1b1eba49 | ||
|
|
db137b1c33 | ||
|
|
a6915eac63 | ||
|
|
95ef65b08d | ||
|
|
c096e5d075 | ||
|
|
e3903add9f | ||
|
|
ad7d52f7e5 | ||
|
|
670a510a2d | ||
|
|
cb1e39cb61 | ||
|
|
a697d59040 | ||
|
|
b4fd76a5e1 | ||
|
|
11e06c7b37 | ||
|
|
af3e5ab501 | ||
|
|
3ded294ae8 | ||
|
|
e8bf6435e4 | ||
|
|
9293164e4a | ||
|
|
8cc1ac5a53 | ||
|
|
800e46a84c | ||
|
|
18dd879b9c | ||
|
|
743cb07002 | ||
|
|
f5f1910cc4 | ||
|
|
94db56d668 | ||
|
|
596a1f21a6 | ||
|
|
cfe3849bff | ||
|
|
9030fff9d0 | ||
|
|
87169eeb4b | ||
|
|
967c0ffc99 | ||
|
|
f221e02c39 | ||
|
|
26869d06f0 | ||
|
|
01b6240e2e | ||
|
|
1c576cd5d8 | ||
|
|
203f3fc167 | ||
|
|
73017ec48e | ||
|
|
06a8692de5 | ||
|
|
a50bd612e8 | ||
|
|
d5b591d6f1 | ||
|
|
ad8ad1bf74 | ||
|
|
083ec78491 | ||
|
|
bd102623eb | ||
|
|
985416f20c | ||
|
|
128f9311fe | ||
|
|
a9d2180dd6 | ||
|
|
b69a8a9b8c | ||
|
|
cf816f8e26 | ||
|
|
8ed6dbf581 | ||
|
|
8c0be8fd74 | ||
|
|
d3655fa1ab | ||
|
|
9343b43e77 | ||
|
|
e45e2cadd3 | ||
|
|
da51c28767 | ||
|
|
c76b0ab333 | ||
|
|
5aa11c0e4b | ||
|
|
af5bae315e |
@@ -0,0 +1,22 @@
|
||||
# Code Review Guidelines
|
||||
|
||||
## Basic Review
|
||||
|
||||
- **Breaking API changes** — Check for removed/renamed fields in `pkg/apis/`, changed function signatures in public packages, or modified CLI flags and output formats.
|
||||
- **Pattern violations** — New code should follow existing patterns in the codebase (e.g., collector/analyzer structure, error handling conventions, interface usage).
|
||||
- **Security** — Watch for command injection in exec-based collectors, path traversal in file operations, unsanitized user input in specs, and leaked credentials in collected data.
|
||||
- **Go standards** — Issues that linters like `go vet`, `staticcheck`, and `modernize` would catch: deprecated API usage, unnecessary allocations, error shadowing, unchecked errors.
|
||||
- **Test coverage** — New functionality should have tests. Changes to existing code should not reduce coverage compared to the last test run on `main`.
|
||||
- **Error handling** — Errors should wrap context (`fmt.Errorf("... : %w", err)`), not be silently swallowed, and provide actionable messages for operator-facing output.
|
||||
- **Concurrency safety** — Collectors run concurrently. Shared state must be protected. `CollectorResult` map writes from goroutines need synchronization.
|
||||
- **Bundle storage** — Collectors must save data using `CollectorResult.SaveResult` and related methods (`SaveResults`, `SymLinkResult`). Never write files directly — `CollectorResult` handles dual-mode storage (in-memory for preflights, on-disk for support bundles). See `pkg/collect/result.go`.
|
||||
|
||||
## Advanced Review
|
||||
|
||||
- **Cross-feature impact** — Consider whether a change to one collector/analyzer could affect the broader collection pipeline, redaction, or output archive structure.
|
||||
- **CLI vs SDK consumers** — This project is consumed both as CLI tools and as Go packages (SDK). Changes targeting a CLI use case must not break SDK consumers who import `pkg/collect`, `pkg/analyze`, or API types directly.
|
||||
- **Documentation** — Does the change add, modify, or remove user-facing behavior? Check whether https://troubleshoot.sh needs updates. Use https://troubleshoot.sh/llms.txt or https://troubleshoot.sh/llms-full.txt to review current docs.
|
||||
- **Dedicated documentation needs** — For large or complex changes, consider whether CLI users or SDK consumers need standalone documentation (migration guides, new feature walkthroughs, updated examples).
|
||||
- **Backwards compatibility** — Spec changes must consider existing specs in the wild. New fields should have sensible zero-value defaults. Removed fields should not cause parse failures.
|
||||
- **Downstream impact on sbctl** — Changes to public Go packages (`pkg/collect`, `pkg/analyze`, API types, etc.) may require follow-up changes in [replicatedhq/sbctl](https://github.com/replicatedhq/sbctl), which imports this project as a dependency. Flag any breaking or behavioral changes that could affect sbctl.
|
||||
- **Repository docs** — If your changes affect build commands, architecture, project conventions, or review guidelines, update `CLAUDE.md`, `README.md`, and this file accordingly.
|
||||
@@ -1,39 +0,0 @@
|
||||
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)"
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
tidy-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
test-integration:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
@@ -54,12 +54,12 @@ jobs:
|
||||
compile-preflight:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- run: make generate preflight
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: preflight
|
||||
path: bin/preflight
|
||||
@@ -68,13 +68,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: compile-preflight
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- 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@v6
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: preflight
|
||||
path: bin/
|
||||
@@ -84,27 +84,40 @@ jobs:
|
||||
compile-supportbundle:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- run: make generate support-bundle
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: support-bundle
|
||||
path: bin/support-bundle
|
||||
|
||||
compile-collect:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- run: make generate collect
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: collect
|
||||
path: bin/collect
|
||||
|
||||
validate-supportbundle-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
needs: compile-supportbundle
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- 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@v6
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: support-bundle
|
||||
path: bin/
|
||||
@@ -114,17 +127,17 @@ jobs:
|
||||
# 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
|
||||
needs: [compile-supportbundle, compile-collect]
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Download support bundle binary
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: support-bundle
|
||||
path: bin/
|
||||
- run: chmod +x bin/support-bundle
|
||||
- name: Download preflight binary
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: preflight
|
||||
path: bin/
|
||||
|
||||
@@ -21,8 +21,8 @@ jobs:
|
||||
support-bundle: ${{ steps.filter.outputs.support-bundle }}
|
||||
examples: ${{ steps.filter.outputs.examples }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dorny/paths-filter@v3
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dorny/paths-filter@v4
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
@@ -44,8 +44,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: ./.github/actions/setup-go
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Check go mod tidy
|
||||
run: |
|
||||
@@ -71,8 +73,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: ./.github/actions/setup-go
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Setup K3s
|
||||
uses: replicatedhq/action-k3s@main
|
||||
@@ -89,15 +93,145 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: ./.github/actions/setup-go
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- run: make build
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: binaries
|
||||
path: bin/
|
||||
retention-days: 1
|
||||
|
||||
# Minimal linux architecture smoke tests
|
||||
linux-arch-smoke:
|
||||
if: needs.changes.outputs.go-files == 'true'
|
||||
needs: [changes, lint]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- goarch: amd64
|
||||
docker_platform: linux/amd64
|
||||
expected_machine: x86_64
|
||||
- goarch: arm64
|
||||
docker_platform: linux/arm64
|
||||
expected_machine: aarch64
|
||||
- goarch: arm
|
||||
goarm: "7"
|
||||
docker_platform: linux/arm/v7
|
||||
expected_machine: armv7l
|
||||
- goarch: riscv64
|
||||
docker_platform: linux/riscv64
|
||||
expected_machine: riscv64
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- name: Setup K3s
|
||||
uses: replicatedhq/action-k3s@main
|
||||
with:
|
||||
version: v1.31.2-k3s1
|
||||
- uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
platforms: arm64,arm,riscv64
|
||||
|
||||
- name: Build linux/${{ matrix.goarch }} binaries
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOOS: linux
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
run: |
|
||||
if [ -n "${GOARM}" ]; then
|
||||
echo "Building for GOARCH=${GOARCH} GOARM=${GOARM}"
|
||||
else
|
||||
echo "Building for GOARCH=${GOARCH}"
|
||||
fi
|
||||
make build
|
||||
|
||||
- name: Collect a minimal support bundle on ${{ matrix.docker_platform }}
|
||||
env:
|
||||
PREFLIGHT_AUTO_UPDATE: "false"
|
||||
TROUBLESHOOT_AUTO_UPDATE: "false"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tmpdir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
kubeconfig_path="${KUBECONFIG:-$HOME/.kube/config}"
|
||||
|
||||
assert_tar_member() {
|
||||
local pattern="$1"
|
||||
local label="$2"
|
||||
local member
|
||||
member="$(grep -m1 "$pattern" "$tmpdir/archive-members.txt" || true)"
|
||||
if [ -z "$member" ]; then
|
||||
echo "Expected ${label} was not found in ${archive}"
|
||||
exit 1
|
||||
fi
|
||||
echo "Found ${label}: ${member}"
|
||||
}
|
||||
|
||||
echo "Using kubeconfig at ${kubeconfig_path}"
|
||||
test -f "${kubeconfig_path}"
|
||||
cp "${kubeconfig_path}" "$tmpdir/kubeconfig"
|
||||
|
||||
echo "Verifying K3s cluster is reachable on the host"
|
||||
kubectl --kubeconfig "$tmpdir/kubeconfig" get nodes -o wide
|
||||
|
||||
docker run --rm \
|
||||
--platform ${{ matrix.docker_platform }} \
|
||||
--network host \
|
||||
-v "$PWD:/src" \
|
||||
-v "$tmpdir:/tmp/linux-arch-smoke" \
|
||||
-w /src \
|
||||
alpine:3.23 \
|
||||
sh -ec '
|
||||
export PREFLIGHT_AUTO_UPDATE=false
|
||||
export TROUBLESHOOT_AUTO_UPDATE=false
|
||||
/src/bin/support-bundle \
|
||||
--kubeconfig /tmp/linux-arch-smoke/kubeconfig \
|
||||
--interactive=false \
|
||||
/src/test/e2e/support-bundle/spec/linuxArchSmokeLocalHostCollectors.yaml \
|
||||
--output /tmp/linux-arch-smoke/linux-arch-support-bundle.tar.gz
|
||||
'
|
||||
|
||||
archive="$tmpdir/linux-arch-support-bundle.tar.gz"
|
||||
test -f "$archive"
|
||||
echo "Generated support bundle archive: $archive"
|
||||
tar -tf "$archive" > "$tmpdir/archive-members.txt"
|
||||
|
||||
assert_tar_member '/host-collectors/system/cpu.json$' 'cpu.json'
|
||||
assert_tar_member '/host-collectors/system/memory.json$' 'memory.json'
|
||||
assert_tar_member '/host-collectors/run-host/uname.txt$' 'uname.txt'
|
||||
assert_tar_member '/cluster-resources/nodes.json$' 'cluster-resources nodes.json'
|
||||
assert_tar_member '/cluster-resources/resources.json$' 'cluster-resources resources.json'
|
||||
assert_tar_member '/analysis.json$' 'analysis.json'
|
||||
|
||||
echo "Checking uname -m output"
|
||||
uname_path="$(grep -m1 '/host-collectors/run-host/uname.txt$' "$tmpdir/archive-members.txt")"
|
||||
uname_machine="$(tar -xOf "$archive" "$uname_path" | tr -d '\r\n')"
|
||||
if [ "$uname_machine" != '${{ matrix.expected_machine }}' ]; then
|
||||
echo "Unexpected uname -m output: ${uname_machine}"
|
||||
exit 1
|
||||
fi
|
||||
echo "Analyzer input check passed: uname -m reported ${uname_machine}"
|
||||
|
||||
echo "Checking analysis.json"
|
||||
analysis_path="$(grep -m1 '/analysis.json$' "$tmpdir/archive-members.txt")"
|
||||
tar -xOf "$archive" "$analysis_path" > "$tmpdir/analysis.json"
|
||||
jq -e '
|
||||
.[]
|
||||
| select(.insight.primary == "Linux arch smoke node count")
|
||||
| select(.insight.detail == "This cluster has exactly 1 node")
|
||||
| select(.severity == "debug")
|
||||
' "$tmpdir/analysis.json" >/dev/null
|
||||
echo "Found expected analyzer result: Linux arch smoke node count -> This cluster has exactly 1 node"
|
||||
|
||||
# E2E tests
|
||||
e2e:
|
||||
if: needs.changes.outputs.go-files == 'true' || github.event_name == 'push'
|
||||
@@ -118,7 +252,7 @@ jobs:
|
||||
target: support-bundle-e2e-go-test
|
||||
needs-k3s: false
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup K3s
|
||||
if: matrix.needs-k3s
|
||||
@@ -126,7 +260,7 @@ jobs:
|
||||
with:
|
||||
version: v1.31.2-k3s1
|
||||
|
||||
- uses: actions/download-artifact@v6
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: binaries
|
||||
path: bin/
|
||||
@@ -137,7 +271,7 @@ jobs:
|
||||
# Success summary
|
||||
success:
|
||||
if: always()
|
||||
needs: [lint, test, build, e2e]
|
||||
needs: [lint, test, build, linux-arch-smoke, e2e]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check results
|
||||
@@ -146,6 +280,7 @@ jobs:
|
||||
if [[ "${{ needs.lint.result }}" == "failure" ]] || \
|
||||
[[ "${{ needs.test.result }}" == "failure" ]] || \
|
||||
[[ "${{ needs.build.result }}" == "failure" ]] || \
|
||||
[[ "${{ needs.linux-arch-smoke.result }}" == "failure" ]] || \
|
||||
[[ "${{ needs.e2e.result }}" == "failure" ]]; then
|
||||
echo "::error::Some jobs failed or were cancelled"
|
||||
exit 1
|
||||
@@ -155,6 +290,7 @@ jobs:
|
||||
if [[ "${{ needs.lint.result }}" == "cancelled" ]] || \
|
||||
[[ "${{ needs.test.result }}" == "cancelled" ]] || \
|
||||
[[ "${{ needs.build.result }}" == "cancelled" ]] || \
|
||||
[[ "${{ needs.linux-arch-smoke.result }}" == "cancelled" ]] || \
|
||||
[[ "${{ needs.e2e.result }}" == "cancelled" ]]; then
|
||||
echo "::error::Some jobs failed or were cancelled"
|
||||
exit 1
|
||||
|
||||
@@ -13,45 +13,22 @@ on:
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
regression-test:
|
||||
# Build binaries once (shared by all test jobs)
|
||||
build-binaries:
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 25
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
# 1. SETUP
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
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
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
cache-dependency-path: go.sum
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Build binaries
|
||||
run: |
|
||||
@@ -60,84 +37,83 @@ jobs:
|
||||
./bin/preflight version
|
||||
./bin/support-bundle version
|
||||
|
||||
- name: Upload binaries
|
||||
uses: actions/upload-artifact@v7
|
||||
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@v8
|
||||
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
|
||||
run: pip install pyyaml deepdiff
|
||||
|
||||
# 2. EXECUTE SPECS (in parallel)
|
||||
- name: Run all specs in parallel
|
||||
- name: Run preflight v1beta3
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "Running all 3 specs in parallel..."
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
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-v1beta3
|
||||
id: compare
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "Comparing v1beta3 preflight bundle against baseline..."
|
||||
@@ -154,8 +130,93 @@ jobs:
|
||||
--report test/output/diff-report-v1beta3.json \
|
||||
--spec-type preflight
|
||||
|
||||
- name: Upload test artifacts
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
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@v8
|
||||
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-v1beta2
|
||||
id: compare
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "Comparing v1beta2 preflight bundle against baseline..."
|
||||
@@ -172,8 +233,90 @@ jobs:
|
||||
--report test/output/diff-report-v1beta2.json \
|
||||
--spec-type preflight
|
||||
|
||||
- name: Upload test artifacts
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
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@v8
|
||||
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-supportbundle
|
||||
id: compare
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "Comparing support bundle against baseline..."
|
||||
@@ -190,45 +333,116 @@ 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: always()
|
||||
uses: actions/upload-artifact@v5
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: regression-test-results-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
name: test-results-supportbundle-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
test/output/*.tar.gz
|
||||
test/output/*.json
|
||||
test/output/supportbundle.tar.gz
|
||||
test/output/diff-report-supportbundle.json
|
||||
test/output/supportbundle.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
|
||||
|
||||
# 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@v8
|
||||
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 [ "${{ steps.compare-v1beta3.outcome }}" == "failure" ] && [ "${{ steps.compare-v1beta3.outputs.baseline_missing }}" != "true" ]; then
|
||||
echo "❌ v1beta3 comparison failed"
|
||||
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
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
|
||||
if [ "${{ steps.compare-v1beta2.outcome }}" == "failure" ] && [ "${{ steps.compare-v1beta2.outputs.baseline_missing }}" != "true" ]; then
|
||||
echo "❌ v1beta2 comparison failed"
|
||||
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
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
|
||||
if [ "${{ steps.compare-supportbundle.outcome }}" == "failure" ] && [ "${{ steps.compare-supportbundle.outputs.baseline_missing }}" != "true" ]; then
|
||||
echo "❌ Support bundle comparison failed"
|
||||
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
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
|
||||
@@ -241,9 +455,8 @@ jobs:
|
||||
echo "✅ All comparisons passed or skipped (no baseline)"
|
||||
fi
|
||||
|
||||
# 5. UPDATE BASELINES (optional, manual trigger only)
|
||||
- name: Update baselines
|
||||
if: github.event.inputs.update_baselines == 'true' && github.event_name == 'workflow_dispatch'
|
||||
if: ${{ !cancelled() && github.event.inputs.update_baselines == 'true' && github.event_name == 'workflow_dispatch' }}
|
||||
run: |
|
||||
echo "Updating baselines with current bundles..."
|
||||
|
||||
@@ -271,7 +484,7 @@ jobs:
|
||||
{
|
||||
"updated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"git_sha": "${{ github.sha }}",
|
||||
"k8s_version": "v1.28.3",
|
||||
"k8s_version": "v1.31.2-k3s1",
|
||||
"workflow_run": "${{ github.run_id }}"
|
||||
}
|
||||
EOF
|
||||
@@ -282,12 +495,3 @@ 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@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
uses: goreleaser/goreleaser-action@v7
|
||||
with:
|
||||
version: "v2.12.3"
|
||||
args: release --clean --config deploy/.goreleaser.yaml
|
||||
@@ -37,12 +37,12 @@ jobs:
|
||||
|
||||
- name: Update new preflight version in krew-index
|
||||
if: ${{ !contains(github.ref_name, '-') }}
|
||||
uses: rajatjindal/krew-release-bot@v0.0.47
|
||||
uses: rajatjindal/krew-release-bot@v0.0.51
|
||||
with:
|
||||
krew_template_file: deploy/krew/preflight.yaml
|
||||
|
||||
- name: Update new support-bundle version in krew-index
|
||||
if: ${{ !contains(github.ref_name, '-') }}
|
||||
uses: rajatjindal/krew-release-bot@v0.0.47
|
||||
uses: rajatjindal/krew-release-bot@v0.0.51
|
||||
with:
|
||||
krew_template_file: deploy/krew/support-bundle.yaml
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check for Go updates
|
||||
uses: StefMa/Upgrade-Go-Action@v1
|
||||
|
||||
+6
-1
@@ -55,4 +55,9 @@ sbom/
|
||||
/troubleshoot-test
|
||||
cmd/troubleshoot/troubleshoot
|
||||
cmd/*/troubleshoot
|
||||
/support-bundle
|
||||
/support-bundle
|
||||
/.worktrees/
|
||||
|
||||
# IDEs
|
||||
## IntelliJ / GoLand
|
||||
/troubleshoot.iml
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY EPSS RISK PATH
|
||||
{{- range .Matches}}
|
||||
{{.Artifact.Name}} {{.Artifact.Version}} {{join .Vulnerability.Fix.Versions ","}} {{.Artifact.Type}} {{.Vulnerability.ID}} {{.Vulnerability.Severity}} {{.Vulnerability.EPSS}} {{.Vulnerability.Risk}} {{range .Artifact.Locations}}{{.AccessPath}}{{end}}
|
||||
{{- end}}
|
||||
@@ -0,0 +1,3 @@
|
||||
exclude:
|
||||
- ./examples/**
|
||||
- ./bin/**
|
||||
@@ -0,0 +1,3 @@
|
||||
# AGENTS.md
|
||||
|
||||
ALWAYS read and follow the instructions in CLAUDE.md before starting any work in this repository.
|
||||
@@ -0,0 +1,30 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Replicated Troubleshoot is a Kubernetes diagnostic framework providing two kubectl plugins: `preflight` (pre-installation cluster validation) and `support-bundle` (post-installation diagnostics with log collection, redaction, and analysis). Specs use the Kubernetes custom resource format (as a serialization convention, not installed in-cluster) and are defined by application vendors and executed by cluster operators.
|
||||
|
||||
## Build & Test Commands
|
||||
|
||||
```bash
|
||||
make build # Build bin/support-bundle and bin/preflight
|
||||
make test # Unit tests (includes generate, fmt, vet)
|
||||
make test RUN=TestMyFunction # Run a single test
|
||||
make test-integration # Integration tests (requires k8s cluster)
|
||||
make e2e # All e2e tests
|
||||
make generate # Regenerate types/clients after modifying pkg/apis/
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The core data flow is: **Spec loading → Collection → Redaction → Analysis → Results**. The two main workflows are orchestrated by `pkg/supportbundle/` and `pkg/preflight/`.
|
||||
|
||||
Three API versions coexist in `pkg/apis/troubleshoot/`: v1beta1, v1beta2 (primary, all types defined here), and v1beta3 (in-progress, adds `StringOrValueFrom` for Secret/ConfigMap references, converts to v1beta2 at runtime).
|
||||
|
||||
Collectors live in `pkg/collect/`, analyzers in `pkg/analyze/`. When adding either, follow the pattern of existing implementations and run `make generate` after modifying API types.
|
||||
|
||||
## Code Review
|
||||
|
||||
See [.cursor/BUGBOT.md](.cursor/BUGBOT.md) for the full review checklist covering basic checks (API breaks, pattern violations, security, test coverage) and advanced checks (cross-feature impact, CLI vs SDK consumers, documentation needs).
|
||||
@@ -61,6 +61,9 @@ test: generate fmt vet
|
||||
test-integration: generate fmt vet
|
||||
go test -v --tags="integration exclude_graphdriver_devicemapper exclude_graphdriver_btrfs" ${BUILDPATHS}
|
||||
|
||||
.PHONY: e2e
|
||||
e2e: preflight-e2e-test support-bundle-e2e-test support-bundle-e2e-go-test
|
||||
|
||||
.PHONY: preflight-e2e-test
|
||||
preflight-e2e-test:
|
||||
./test/validate-preflight-e2e.sh
|
||||
@@ -158,6 +161,7 @@ generate: controller-gen client-gen
|
||||
--input-base github.com/replicatedhq/troubleshoot/pkg/apis \
|
||||
--input troubleshoot/v1beta1 \
|
||||
--input troubleshoot/v1beta2 \
|
||||
--input troubleshoot/v1beta3 \
|
||||
--go-header-file ./hack/boilerplate.go.txt
|
||||
cp -r troubleshootclientset pkg/client
|
||||
rm -rf troubleshootclientset
|
||||
@@ -254,14 +258,19 @@ sbom: sbom/assets/troubleshoot-sbom.tgz
|
||||
sbom/assets/troubleshoot-sbom.tgz > sbom/assets/troubleshoot-sbom.tgz.sig
|
||||
cosign public-key --key cosign.key --outfile sbom/assets/key.pub
|
||||
|
||||
.PHONY: get-govulncheck
|
||||
get-govulncheck:
|
||||
@command -v govulncheck >/dev/null 2>&1 || go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
|
||||
.PHONY: get-grype
|
||||
get-grype:
|
||||
@command -v grype >/dev/null 2>&1 || curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $(GOPATH)/bin
|
||||
|
||||
.PHONY: scan
|
||||
scan:
|
||||
trivy fs \
|
||||
--scanners vuln \
|
||||
--exit-code=1 \
|
||||
--severity="HIGH,CRITICAL" \
|
||||
--ignore-unfixed \
|
||||
./
|
||||
scan: get-govulncheck get-grype
|
||||
govulncheck ./...
|
||||
grype db update
|
||||
grype dir:. --only-fixed --fail-on high -o template -t .grype.tmpl
|
||||
|
||||
.PHONY: watch
|
||||
watch: npm-install
|
||||
@@ -295,4 +304,4 @@ longhorn:
|
||||
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
|
||||
rm -rf longhorn-manager
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/internal/util"
|
||||
)
|
||||
|
||||
func checkAndSetChroot(newroot string) error {
|
||||
if newroot == "" {
|
||||
return nil
|
||||
}
|
||||
if !util.IsRunningAsRoot() {
|
||||
return errors.New("Can only chroot when run as root")
|
||||
}
|
||||
if err := syscall.Chroot(newroot); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/internal/util"
|
||||
)
|
||||
|
||||
func checkAndSetChroot(newroot string) error {
|
||||
if newroot == "" {
|
||||
return nil
|
||||
}
|
||||
if !util.IsRunningAsRoot() {
|
||||
return errors.New("Can only chroot when run as root")
|
||||
}
|
||||
if err := syscall.Chroot(newroot); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
func checkAndSetChroot(newroot string) error {
|
||||
return errors.New("chroot is only implimented in linux/darwin")
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/cmd/internal/util"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/logger"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
func RootCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "collect [url]",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Short: "Run a collector",
|
||||
Long: `Run a collector and output the results.`,
|
||||
SilenceUsage: true,
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
v := viper.GetViper()
|
||||
v.BindPFlags(cmd.Flags())
|
||||
|
||||
logger.SetupLogger(v)
|
||||
|
||||
if err := util.StartProfiling(); err != nil {
|
||||
klog.Errorf("Failed to start profiling: %v", err)
|
||||
}
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
v := viper.GetViper()
|
||||
|
||||
if err := checkAndSetChroot(v.GetString("chroot")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return runCollect(v, args[0])
|
||||
},
|
||||
PostRun: func(cmd *cobra.Command, args []string) {
|
||||
if err := util.StopProfiling(); err != nil {
|
||||
klog.Errorf("Failed to stop profiling: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
cmd.AddCommand(util.VersionCmd())
|
||||
|
||||
cmd.Flags().StringSlice("redactors", []string{}, "names of the additional redactors to use")
|
||||
cmd.Flags().Bool("redact", true, "enable/disable default redactions")
|
||||
cmd.Flags().String("format", "json", "output format, one of json or raw.")
|
||||
cmd.Flags().String("collector-image", "", "the full name of the collector image to use")
|
||||
cmd.Flags().String("collector-pull-policy", "", "the pull policy of the collector image")
|
||||
cmd.Flags().String("selector", "", "selector (label query) to filter remote collection nodes on.")
|
||||
cmd.Flags().Bool("collect-without-permissions", false, "always generate a support bundle, even if it some require additional permissions")
|
||||
cmd.Flags().Bool("debug", false, "enable debug logging")
|
||||
cmd.Flags().String("chroot", "", "Chroot to path")
|
||||
|
||||
// hidden in favor of the `insecure-skip-tls-verify` flag
|
||||
cmd.Flags().Bool("allow-insecure-connections", false, "when set, do not verify TLS certs when retrieving spec and reporting results")
|
||||
cmd.Flags().MarkHidden("allow-insecure-connections")
|
||||
|
||||
viper.BindPFlags(cmd.Flags())
|
||||
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||||
|
||||
k8sutil.AddFlags(cmd.Flags())
|
||||
|
||||
// Initialize klog flags
|
||||
logger.InitKlogFlags(cmd)
|
||||
|
||||
// CPU and memory profiling flags
|
||||
util.AddProfilingFlags(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func InitAndExecute() {
|
||||
if err := RootCmd().Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
viper.SetEnvPrefix("TROUBLESHOOT")
|
||||
viper.AutomaticEnv()
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/internal/util"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/docrewrite"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/specs"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/supportbundle"
|
||||
"github.com/spf13/viper"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
func runCollect(v *viper.Viper, arg string) error {
|
||||
go func() {
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
signal.Notify(signalChan, os.Interrupt)
|
||||
<-signalChan
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
var collectorContent []byte
|
||||
var err error
|
||||
if strings.HasPrefix(arg, "secret/") {
|
||||
// format secret/namespace-name/secret-name
|
||||
pathParts := strings.Split(arg, "/")
|
||||
if len(pathParts) != 3 {
|
||||
return errors.Errorf("path %s must have 3 components", arg)
|
||||
}
|
||||
|
||||
spec, err := specs.LoadFromSecret(pathParts[1], pathParts[2], "collect-spec")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get spec from secret")
|
||||
}
|
||||
|
||||
collectorContent = spec
|
||||
} else if arg == "-" {
|
||||
b, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collectorContent = b
|
||||
} else if _, err = os.Stat(arg); err == nil {
|
||||
b, err := os.ReadFile(arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collectorContent = b
|
||||
} else {
|
||||
if !util.IsURL(arg) {
|
||||
return fmt.Errorf("%s is not a URL and was not found", arg)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", arg, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Replicated_Collect/v1beta2")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collectorContent = body
|
||||
}
|
||||
|
||||
collectorContent, err = docrewrite.ConvertToV1Beta2(collectorContent)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert to v1beta2")
|
||||
}
|
||||
|
||||
multidocs := strings.Split(string(collectorContent), "\n---\n")
|
||||
|
||||
decode := scheme.Codecs.UniversalDeserializer().Decode
|
||||
|
||||
redactors, err := supportbundle.GetRedactorsFromURIs(v.GetStringSlice("redactors"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get redactors")
|
||||
}
|
||||
|
||||
additionalRedactors := &troubleshootv1beta2.Redactor{
|
||||
Spec: troubleshootv1beta2.RedactorSpec{
|
||||
Redactors: redactors,
|
||||
},
|
||||
}
|
||||
|
||||
for i, additionalDoc := range multidocs {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
additionalDoc, err := docrewrite.ConvertToV1Beta2([]byte(additionalDoc))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert to v1beta2")
|
||||
}
|
||||
obj, _, err := decode(additionalDoc, nil, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to parse additional doc %d", i)
|
||||
}
|
||||
multidocRedactors, ok := obj.(*troubleshootv1beta2.Redactor)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
additionalRedactors.Spec.Redactors = append(additionalRedactors.Spec.Redactors, multidocRedactors.Spec.Redactors...)
|
||||
}
|
||||
|
||||
// make sure we don't block any senders
|
||||
progressCh := make(chan interface{})
|
||||
defer close(progressCh)
|
||||
go func() {
|
||||
for range progressCh {
|
||||
}
|
||||
}()
|
||||
|
||||
restConfig, err := k8sutil.GetRESTConfig()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert kube flags to rest config")
|
||||
}
|
||||
|
||||
labelSelector, err := labels.Parse(v.GetString("selector"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to parse selector")
|
||||
}
|
||||
|
||||
namespace := v.GetString("namespace")
|
||||
if namespace == "" {
|
||||
namespace = "default"
|
||||
}
|
||||
|
||||
timeout := v.GetDuration("request-timeout")
|
||||
if timeout == 0 {
|
||||
timeout = defaultTimeout
|
||||
}
|
||||
|
||||
createOpts := collect.CollectorRunOpts{
|
||||
CollectWithoutPermissions: v.GetBool("collect-without-permissions"),
|
||||
KubernetesRestConfig: restConfig,
|
||||
Image: v.GetString("collector-image"),
|
||||
PullPolicy: v.GetString("collector-pullpolicy"),
|
||||
LabelSelector: labelSelector.String(),
|
||||
Namespace: namespace,
|
||||
Timeout: timeout,
|
||||
ProgressChan: progressCh,
|
||||
}
|
||||
|
||||
// we only support HostCollector or RemoteCollector kinds.
|
||||
hostCollector, err := collect.ParseHostCollectorFromDoc([]byte(multidocs[0]))
|
||||
if err == nil {
|
||||
results, err := collect.CollectHost(hostCollector, additionalRedactors, createOpts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to collect from host")
|
||||
}
|
||||
return showHostStdoutResults(v.GetString("format"), hostCollector.Name, results)
|
||||
}
|
||||
|
||||
remoteCollector, err := collect.ParseRemoteCollectorFromDoc([]byte(multidocs[0]))
|
||||
if err == nil {
|
||||
results, err := collect.CollectRemote(remoteCollector, additionalRedactors, createOpts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to collect from remote host(s)")
|
||||
}
|
||||
return showRemoteStdoutResults(v.GetString("format"), remoteCollector.Name, results)
|
||||
}
|
||||
|
||||
return errors.New("failed to parse hostCollector or remoteCollector")
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
)
|
||||
|
||||
const (
|
||||
// FormatJSON is intended for CLI output.
|
||||
FormatJSON = "json"
|
||||
|
||||
// FormatRaw is intended for consumption by a remote collector. Output is a
|
||||
// string of quoted JSON.
|
||||
FormatRaw = "raw"
|
||||
)
|
||||
|
||||
func showHostStdoutResults(format string, collectName string, results *collect.HostCollectResult) error {
|
||||
switch format {
|
||||
case FormatJSON:
|
||||
return showHostStdoutResultsJSON(collectName, results.AllCollectedData)
|
||||
case FormatRaw:
|
||||
return showHostStdoutResultsRaw(collectName, results.AllCollectedData)
|
||||
default:
|
||||
return errors.Errorf("unknown output format: %q", format)
|
||||
}
|
||||
}
|
||||
|
||||
func showRemoteStdoutResults(format string, collectName string, results *collect.RemoteCollectResult) error {
|
||||
switch format {
|
||||
case FormatJSON:
|
||||
return showRemoteStdoutResultsJSON(collectName, results.AllCollectedData)
|
||||
case FormatRaw:
|
||||
return errors.Errorf("raw format not supported for remote collectors")
|
||||
default:
|
||||
return errors.Errorf("unknown output format: %q", format)
|
||||
}
|
||||
}
|
||||
|
||||
func showHostStdoutResultsJSON(collectName string, results map[string][]byte) error {
|
||||
output := make(map[string]interface{})
|
||||
for file, collectorResult := range results {
|
||||
var collectedItems map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(collectorResult), &collectedItems); err != nil {
|
||||
return errors.Wrap(err, "failed to marshal collector results")
|
||||
}
|
||||
output[file] = collectedItems
|
||||
}
|
||||
|
||||
formatted, err := json.MarshalIndent(output, "", " ")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert output to json")
|
||||
}
|
||||
|
||||
fmt.Print(string(formatted))
|
||||
return nil
|
||||
}
|
||||
|
||||
// showHostStdoutResultsRaw outputs the collector output as a string of quoted json.
|
||||
func showHostStdoutResultsRaw(collectName string, results map[string][]byte) error {
|
||||
strData := map[string]string{}
|
||||
for k, v := range results {
|
||||
strData[k] = string(v)
|
||||
}
|
||||
formatted, err := json.MarshalIndent(strData, "", " ")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert output to json")
|
||||
}
|
||||
fmt.Print(string(formatted))
|
||||
return nil
|
||||
}
|
||||
|
||||
func showRemoteStdoutResultsJSON(collectName string, results map[string][]byte) error {
|
||||
type CollectorResult map[string]interface{}
|
||||
type NodeResult map[string]CollectorResult
|
||||
|
||||
var output = make(map[string]NodeResult)
|
||||
|
||||
for node, result := range results {
|
||||
var nodeResult map[string]string
|
||||
if err := json.Unmarshal(result, &nodeResult); err != nil {
|
||||
return errors.Wrap(err, "failed to marshal node results")
|
||||
}
|
||||
nr := make(NodeResult)
|
||||
for file, collectorResult := range nodeResult {
|
||||
var collectedItems map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(collectorResult), &collectedItems); err != nil {
|
||||
return errors.Wrap(err, "failed to marshal collector results")
|
||||
}
|
||||
nr[file] = collectedItems
|
||||
}
|
||||
output[node] = nr
|
||||
}
|
||||
|
||||
formatted, err := json.MarshalIndent(output, "", " ")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert output to json")
|
||||
}
|
||||
fmt.Print(string(formatted))
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/replicatedhq/troubleshoot/cmd/collect/cli"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cli.InitAndExecute()
|
||||
}
|
||||
@@ -25,7 +25,13 @@ func RootCmd() *cobra.Command {
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Short: "Run and retrieve preflight checks in a cluster",
|
||||
Long: `A preflight check is a set of validations that can and should be run to ensure
|
||||
that a cluster meets the requirements to run an application.`,
|
||||
that a cluster meets the requirements to run an application.
|
||||
|
||||
Unlike support-bundle, preflight does not support --load-cluster-specs because
|
||||
preflight checks are designed to run before an application is installed or
|
||||
upgraded. Since no deployment has occurred yet, there are no in-cluster specs
|
||||
to discover. Preflight specs must be provided via a URL, local file path, or
|
||||
stdin (e.g. "helm template ... | kubectl preflight -").`,
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
@@ -132,10 +132,12 @@ If no arguments are provided, specs are automatically loaded from the cluster by
|
||||
cmd.Flags().Bool("load-cluster-specs", false, "enable/disable loading additional troubleshoot specs found within the cluster. Do not load by default unless no specs are provided in the cli args")
|
||||
cmd.Flags().String("since-time", "", "force pod logs collectors to return logs after a specific date (RFC3339)")
|
||||
cmd.Flags().String("since", "", "force pod logs collectors to return logs newer than a relative duration like 5s, 2m, or 3h.")
|
||||
cmd.Flags().StringP("output", "o", "", "specify the output file path for the support bundle")
|
||||
cmd.Flags().Int("remote-host-collect-timeout", 30, "timeout in seconds for remote host collect operations (e.g. waiting for pods/daemonsets)")
|
||||
cmd.Flags().StringP("output", "o", "", "specify the output file path for the support bundle (.tar.gz extension is added automatically)")
|
||||
cmd.Flags().Bool("debug", false, "enable debug logging. This is equivalent to --v=0")
|
||||
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")
|
||||
cmd.Flags().StringSlice("metadata", []string{}, "user-provided metadata key=value pairs to include in the bundle (can be specified multiple times)")
|
||||
|
||||
// Upload flags
|
||||
cmd.Flags().Bool("auto-upload", false, "automatically upload resulting bundle to replicated.app")
|
||||
|
||||
+44
-11
@@ -166,7 +166,18 @@ func runTroubleshoot(v *viper.Viper, args []string) error {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for msg := range progressChan {
|
||||
klog.Infof("Collecting support bundle: %v", msg)
|
||||
switch msg := msg.(type) {
|
||||
case error:
|
||||
klog.Warningf("Collecting support bundle: %v", msg)
|
||||
case string:
|
||||
if strings.Contains(msg, "skipping collector") {
|
||||
klog.Warningf("Collecting support bundle: %s", msg)
|
||||
} else {
|
||||
klog.Infof("Collecting support bundle: %s", msg)
|
||||
}
|
||||
default:
|
||||
klog.Infof("Collecting support bundle: %v", msg)
|
||||
}
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
@@ -200,17 +211,23 @@ func runTroubleshoot(v *viper.Viper, args []string) error {
|
||||
}()
|
||||
}
|
||||
|
||||
userMetadata, err := parseMetadataFlag(v.GetStringSlice("metadata"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid metadata flag")
|
||||
}
|
||||
|
||||
createOpts := supportbundle.SupportBundleCreateOpts{
|
||||
CollectorProgressCallback: collectorCB,
|
||||
CollectWithoutPermissions: v.GetBool("collect-without-permissions"),
|
||||
KubernetesRestConfig: restConfig,
|
||||
Namespace: v.GetString("namespace"),
|
||||
ProgressChan: progressChan,
|
||||
SinceTime: sinceTime,
|
||||
OutputPath: v.GetString("output"),
|
||||
Redact: v.GetBool("redact"),
|
||||
FromCLI: true,
|
||||
RunHostCollectorsInPod: mainBundle.Spec.RunHostCollectorsInPod,
|
||||
CollectorProgressCallback: collectorCB,
|
||||
CollectWithoutPermissions: v.GetBool("collect-without-permissions"),
|
||||
RemoteHostCollectTimeoutSeconds: v.GetInt("remote-host-collect-timeout"),
|
||||
KubernetesRestConfig: restConfig,
|
||||
Namespace: v.GetString("namespace"),
|
||||
ProgressChan: progressChan,
|
||||
SinceTime: sinceTime,
|
||||
OutputPath: v.GetString("output"),
|
||||
Redact: v.GetBool("redact"),
|
||||
FromCLI: true,
|
||||
RunHostCollectorsInPod: mainBundle.Spec.RunHostCollectorsInPod,
|
||||
|
||||
// Phase 4: Tokenization options
|
||||
Tokenize: v.GetBool("tokenize"),
|
||||
@@ -220,6 +237,7 @@ func runTroubleshoot(v *viper.Viper, args []string) error {
|
||||
VerifyTokenization: v.GetBool("verify-tokenization"),
|
||||
BundleID: v.GetString("bundle-id"),
|
||||
TokenizationStats: v.GetBool("tokenization-stats"),
|
||||
UserMetadata: userMetadata,
|
||||
}
|
||||
|
||||
nonInteractiveOutput := analysisOutput{}
|
||||
@@ -625,3 +643,18 @@ func VerifyTokenizationSetup(v *viper.Viper) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseMetadataFlag(values []string) (map[string]string, error) {
|
||||
if len(values) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
metadata := make(map[string]string, len(values))
|
||||
for _, v := range values {
|
||||
k, val, ok := strings.Cut(v, "=")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid metadata format %q, expected key=value", v)
|
||||
}
|
||||
metadata[k] = val
|
||||
}
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/httputil"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/loader"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
testclient "k8s.io/client-go/kubernetes/fake"
|
||||
@@ -436,3 +437,81 @@ func Test_loadInvalidURISpec(t *testing.T) {
|
||||
assert.Len(t, sb.Spec.Collectors, 3) // default + clusterInfo + clusterResources
|
||||
assert.NotNil(t, sb.Spec.Collectors[0].ConfigMap) // come from the original spec
|
||||
}
|
||||
|
||||
func TestCollectTimeoutFlag(t *testing.T) {
|
||||
const defaultCollectTimeout = 30
|
||||
|
||||
// Parse flags and bind to viper without running the full command (avoids k8s connection).
|
||||
// This verifies the flag is defined and viper receives the correct value.
|
||||
bindFlagsFromArgs := func(t *testing.T, args []string) {
|
||||
t.Helper()
|
||||
cmd := RootCmd()
|
||||
require.NoError(t, cmd.Flags().Parse(args))
|
||||
if cmd.PersistentPreRun != nil {
|
||||
cmd.PersistentPreRun(cmd, nil)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("default value when flag not set", func(t *testing.T) {
|
||||
bindFlagsFromArgs(t, []string{})
|
||||
actualTimeout := viper.GetInt("remote-host-collect-timeout")
|
||||
assert.Equal(t, defaultCollectTimeout, actualTimeout, "remote-host-collect-timeout should default to 30 seconds")
|
||||
})
|
||||
|
||||
t.Run("custom value when flag set", func(t *testing.T) {
|
||||
bindFlagsFromArgs(t, []string{"--remote-host-collect-timeout=90"})
|
||||
actualTimeout := viper.GetInt("remote-host-collect-timeout")
|
||||
assert.Equal(t, 90, actualTimeout, "remote-host-collect-timeout should be 90 when --remote-host-collect-timeout=90 is passed")
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseMetadataFlag(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
values []string
|
||||
want map[string]string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "nil input",
|
||||
values: nil,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
values: []string{},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "single pair",
|
||||
values: []string{"env=staging"},
|
||||
want: map[string]string{"env": "staging"},
|
||||
},
|
||||
{
|
||||
name: "multiple pairs",
|
||||
values: []string{"env=staging", "version=1.2.3"},
|
||||
want: map[string]string{"env": "staging", "version": "1.2.3"},
|
||||
},
|
||||
{
|
||||
name: "value contains equals",
|
||||
values: []string{"config=key=value"},
|
||||
want: map[string]string{"config": "key=value"},
|
||||
},
|
||||
{
|
||||
name: "missing equals",
|
||||
values: []string{"noequals"},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parseMetadataFlag(tt.values)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -880,6 +880,55 @@ spec:
|
||||
- namespace
|
||||
- outcomes
|
||||
type: object
|
||||
ingressClass:
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
checkName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
ingressClassName:
|
||||
type: string
|
||||
outcomes:
|
||||
items:
|
||||
properties:
|
||||
fail:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
pass:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
warn:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
strict:
|
||||
type: BoolString
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
jobStatus:
|
||||
properties:
|
||||
annotations:
|
||||
@@ -1311,6 +1360,8 @@ spec:
|
||||
- key
|
||||
type: object
|
||||
type: object
|
||||
ignoreIfNoFiles:
|
||||
type: boolean
|
||||
outcomes:
|
||||
items:
|
||||
properties:
|
||||
@@ -1563,6 +1614,58 @@ spec:
|
||||
- outcomes
|
||||
- selector
|
||||
type: object
|
||||
s3Status:
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
checkName:
|
||||
type: string
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
fileName:
|
||||
type: string
|
||||
outcomes:
|
||||
items:
|
||||
properties:
|
||||
fail:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
pass:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
warn:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
strict:
|
||||
type: BoolString
|
||||
required:
|
||||
- collectorName
|
||||
- outcomes
|
||||
type: object
|
||||
secret:
|
||||
properties:
|
||||
annotations:
|
||||
@@ -1922,7 +2025,16 @@ spec:
|
||||
items:
|
||||
properties:
|
||||
blockDevices:
|
||||
description: BlockDevicesAnalyze evaluates host-collected block
|
||||
device listings (lsblk-based).
|
||||
properties:
|
||||
additionalDeviceTypes:
|
||||
description: |-
|
||||
AdditionalDeviceTypes are extra lsblk TYPE values (e.g. loop, lvm) that may count toward outcomes,
|
||||
in addition to whole disks and (when IncludeUnmountedPartitions is set) partitions.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
@@ -2725,6 +2837,55 @@ spec:
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
registryImages:
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
checkName:
|
||||
type: string
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
outcomes:
|
||||
items:
|
||||
properties:
|
||||
fail:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
pass:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
warn:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
strict:
|
||||
type: BoolString
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
subnetAvailable:
|
||||
properties:
|
||||
annotations:
|
||||
|
||||
@@ -2674,7 +2674,9 @@ spec:
|
||||
type: integer
|
||||
type: object
|
||||
resizePolicy:
|
||||
description: Resources resize policy for the container.
|
||||
description: |-
|
||||
Resources resize policy for the container.
|
||||
This field cannot be set on ephemeral containers.
|
||||
items:
|
||||
description: ContainerResizePolicy represents
|
||||
resource resize policy for the container.
|
||||
@@ -5933,7 +5935,9 @@ spec:
|
||||
type: integer
|
||||
type: object
|
||||
resizePolicy:
|
||||
description: Resources resize policy for the container.
|
||||
description: |-
|
||||
Resources resize policy for the container.
|
||||
This field cannot be set on ephemeral containers.
|
||||
items:
|
||||
description: ContainerResizePolicy represents
|
||||
resource resize policy for the container.
|
||||
@@ -6723,8 +6727,8 @@ spec:
|
||||
will be made available to those containers which consume them
|
||||
by name.
|
||||
|
||||
This is an alpha field and requires enabling the
|
||||
DynamicResourceAllocation feature gate.
|
||||
This is a stable field but requires that the
|
||||
DynamicResourceAllocation feature gate is enabled.
|
||||
|
||||
This field is immutable.
|
||||
items:
|
||||
@@ -7184,9 +7188,10 @@ spec:
|
||||
operator:
|
||||
description: |-
|
||||
Operator represents a key's relationship to the value.
|
||||
Valid operators are Exists and Equal. Defaults to Equal.
|
||||
Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal.
|
||||
Exists is equivalent to wildcard for value, so that a pod can
|
||||
tolerate all taints of a particular category.
|
||||
Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators).
|
||||
type: string
|
||||
tolerationSeconds:
|
||||
description: |-
|
||||
@@ -7987,7 +7992,7 @@ spec:
|
||||
resources:
|
||||
description: |-
|
||||
resources represents the minimum resources the volume should have.
|
||||
If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements
|
||||
Users are allowed to specify resource requirements
|
||||
that are lower than previous value but must still be higher than capacity recorded in the
|
||||
status field of the claim.
|
||||
More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources
|
||||
@@ -8888,6 +8893,24 @@ spec:
|
||||
CSRs will be addressed to this
|
||||
signer.
|
||||
type: string
|
||||
userAnnotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: |-
|
||||
userAnnotations allow pod authors to pass additional information to
|
||||
the signer implementation. Kubernetes does not restrict or validate this
|
||||
metadata in any way.
|
||||
|
||||
These values are copied verbatim into the `spec.unverifiedUserAnnotations` field of
|
||||
the PodCertificateRequest objects that Kubelet creates.
|
||||
|
||||
Entries are subject to the same validation as object metadata annotations,
|
||||
with the addition that all keys must be domain-prefixed. No restrictions
|
||||
are placed on values, except an overall size limitation on the entire field.
|
||||
|
||||
Signers should document the keys and values they support. Signers should
|
||||
deny requests that contain keys they do not recognize.
|
||||
type: object
|
||||
required:
|
||||
- keyType
|
||||
- signerName
|
||||
@@ -9318,6 +9341,42 @@ spec:
|
||||
x-kubernetes-list-map-keys:
|
||||
- name
|
||||
x-kubernetes-list-type: map
|
||||
workloadRef:
|
||||
description: |-
|
||||
WorkloadRef provides a reference to the Workload object that this Pod belongs to.
|
||||
This field is used by the scheduler to identify the PodGroup and apply the
|
||||
correct group scheduling policies. The Workload object referenced
|
||||
by this field may not exist at the time the Pod is created.
|
||||
This field is immutable, but a Workload object with the same name
|
||||
may be recreated with different policies. Doing this during pod scheduling
|
||||
may result in the placement not conforming to the expected policies.
|
||||
properties:
|
||||
name:
|
||||
description: |-
|
||||
Name defines the name of the Workload object this Pod belongs to.
|
||||
Workload must be in the same namespace as the Pod.
|
||||
If it doesn't match any existing Workload, the Pod will remain unschedulable
|
||||
until a Workload object is created and observed by the kube-scheduler.
|
||||
It must be a DNS subdomain.
|
||||
type: string
|
||||
podGroup:
|
||||
description: |-
|
||||
PodGroup is the name of the PodGroup within the Workload that this Pod
|
||||
belongs to. If it doesn't match any existing PodGroup within the Workload,
|
||||
the Pod will remain unschedulable until the Workload object is recreated
|
||||
and observed by the kube-scheduler. It must be a DNS label.
|
||||
type: string
|
||||
podGroupReplicaKey:
|
||||
description: |-
|
||||
PodGroupReplicaKey specifies the replica key of the PodGroup to which this
|
||||
Pod belongs. It is used to distinguish pods belonging to different replicas
|
||||
of the same pod group. The pod group policy is applied separately to each replica.
|
||||
When set, it must be a DNS label.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- podGroup
|
||||
type: object
|
||||
required:
|
||||
- containers
|
||||
type: object
|
||||
@@ -11199,7 +11258,9 @@ spec:
|
||||
type: integer
|
||||
type: object
|
||||
resizePolicy:
|
||||
description: Resources resize policy for the container.
|
||||
description: |-
|
||||
Resources resize policy for the container.
|
||||
This field cannot be set on ephemeral containers.
|
||||
items:
|
||||
description: ContainerResizePolicy represents
|
||||
resource resize policy for the container.
|
||||
@@ -14458,7 +14519,9 @@ spec:
|
||||
type: integer
|
||||
type: object
|
||||
resizePolicy:
|
||||
description: Resources resize policy for the container.
|
||||
description: |-
|
||||
Resources resize policy for the container.
|
||||
This field cannot be set on ephemeral containers.
|
||||
items:
|
||||
description: ContainerResizePolicy represents
|
||||
resource resize policy for the container.
|
||||
@@ -15248,8 +15311,8 @@ spec:
|
||||
will be made available to those containers which consume them
|
||||
by name.
|
||||
|
||||
This is an alpha field and requires enabling the
|
||||
DynamicResourceAllocation feature gate.
|
||||
This is a stable field but requires that the
|
||||
DynamicResourceAllocation feature gate is enabled.
|
||||
|
||||
This field is immutable.
|
||||
items:
|
||||
@@ -15709,9 +15772,10 @@ spec:
|
||||
operator:
|
||||
description: |-
|
||||
Operator represents a key's relationship to the value.
|
||||
Valid operators are Exists and Equal. Defaults to Equal.
|
||||
Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal.
|
||||
Exists is equivalent to wildcard for value, so that a pod can
|
||||
tolerate all taints of a particular category.
|
||||
Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators).
|
||||
type: string
|
||||
tolerationSeconds:
|
||||
description: |-
|
||||
@@ -16512,7 +16576,7 @@ spec:
|
||||
resources:
|
||||
description: |-
|
||||
resources represents the minimum resources the volume should have.
|
||||
If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements
|
||||
Users are allowed to specify resource requirements
|
||||
that are lower than previous value but must still be higher than capacity recorded in the
|
||||
status field of the claim.
|
||||
More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources
|
||||
@@ -17413,6 +17477,24 @@ spec:
|
||||
CSRs will be addressed to this
|
||||
signer.
|
||||
type: string
|
||||
userAnnotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: |-
|
||||
userAnnotations allow pod authors to pass additional information to
|
||||
the signer implementation. Kubernetes does not restrict or validate this
|
||||
metadata in any way.
|
||||
|
||||
These values are copied verbatim into the `spec.unverifiedUserAnnotations` field of
|
||||
the PodCertificateRequest objects that Kubelet creates.
|
||||
|
||||
Entries are subject to the same validation as object metadata annotations,
|
||||
with the addition that all keys must be domain-prefixed. No restrictions
|
||||
are placed on values, except an overall size limitation on the entire field.
|
||||
|
||||
Signers should document the keys and values they support. Signers should
|
||||
deny requests that contain keys they do not recognize.
|
||||
type: object
|
||||
required:
|
||||
- keyType
|
||||
- signerName
|
||||
@@ -17843,6 +17925,42 @@ spec:
|
||||
x-kubernetes-list-map-keys:
|
||||
- name
|
||||
x-kubernetes-list-type: map
|
||||
workloadRef:
|
||||
description: |-
|
||||
WorkloadRef provides a reference to the Workload object that this Pod belongs to.
|
||||
This field is used by the scheduler to identify the PodGroup and apply the
|
||||
correct group scheduling policies. The Workload object referenced
|
||||
by this field may not exist at the time the Pod is created.
|
||||
This field is immutable, but a Workload object with the same name
|
||||
may be recreated with different policies. Doing this during pod scheduling
|
||||
may result in the placement not conforming to the expected policies.
|
||||
properties:
|
||||
name:
|
||||
description: |-
|
||||
Name defines the name of the Workload object this Pod belongs to.
|
||||
Workload must be in the same namespace as the Pod.
|
||||
If it doesn't match any existing Workload, the Pod will remain unschedulable
|
||||
until a Workload object is created and observed by the kube-scheduler.
|
||||
It must be a DNS subdomain.
|
||||
type: string
|
||||
podGroup:
|
||||
description: |-
|
||||
PodGroup is the name of the PodGroup within the Workload that this Pod
|
||||
belongs to. If it doesn't match any existing PodGroup within the Workload,
|
||||
the Pod will remain unschedulable until the Workload object is recreated
|
||||
and observed by the kube-scheduler. It must be a DNS label.
|
||||
type: string
|
||||
podGroupReplicaKey:
|
||||
description: |-
|
||||
PodGroupReplicaKey specifies the replica key of the PodGroup to which this
|
||||
Pod belongs. It is used to distinguish pods belonging to different replicas
|
||||
of the same pod group. The pod group policy is applied separately to each replica.
|
||||
When set, it must be a DNS label.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- podGroup
|
||||
type: object
|
||||
required:
|
||||
- containers
|
||||
type: object
|
||||
@@ -17851,6 +17969,29 @@ spec:
|
||||
required:
|
||||
- namespace
|
||||
type: object
|
||||
s3Status:
|
||||
properties:
|
||||
accessKeyID:
|
||||
type: string
|
||||
bucketName:
|
||||
type: string
|
||||
collectorName:
|
||||
type: string
|
||||
endpoint:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
insecure:
|
||||
type: boolean
|
||||
region:
|
||||
type: string
|
||||
secretAccessKey:
|
||||
type: string
|
||||
usePathStyle:
|
||||
type: boolean
|
||||
required:
|
||||
- bucketName
|
||||
type: object
|
||||
secret:
|
||||
properties:
|
||||
collectorName:
|
||||
@@ -17881,6 +18022,17 @@ spec:
|
||||
namespace:
|
||||
type: string
|
||||
type: object
|
||||
supportBundleMetadata:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
namespace:
|
||||
type: string
|
||||
required:
|
||||
- namespace
|
||||
type: object
|
||||
sysctl:
|
||||
properties:
|
||||
collectorName:
|
||||
@@ -18333,6 +18485,27 @@ spec:
|
||||
- port
|
||||
- toCIDR
|
||||
type: object
|
||||
registryImages:
|
||||
description: |-
|
||||
HostRegistryImages checks whether images are accessible from the host,
|
||||
without requiring a Kubernetes cluster. Auth can be supplied inline via
|
||||
Username/Password or omitted to rely on ambient credentials (e.g. ~/.docker/config.json).
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
images:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
password:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
required:
|
||||
- images
|
||||
type: object
|
||||
run:
|
||||
properties:
|
||||
args:
|
||||
|
||||
@@ -43,7 +43,16 @@ spec:
|
||||
items:
|
||||
properties:
|
||||
blockDevices:
|
||||
description: BlockDevicesAnalyze evaluates host-collected block
|
||||
device listings (lsblk-based).
|
||||
properties:
|
||||
additionalDeviceTypes:
|
||||
description: |-
|
||||
AdditionalDeviceTypes are extra lsblk TYPE values (e.g. loop, lvm) that may count toward outcomes,
|
||||
in addition to whole disks and (when IncludeUnmountedPartitions is set) partitions.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
@@ -846,6 +855,55 @@ spec:
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
registryImages:
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
checkName:
|
||||
type: string
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
outcomes:
|
||||
items:
|
||||
properties:
|
||||
fail:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
pass:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
warn:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
strict:
|
||||
type: BoolString
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
subnetAvailable:
|
||||
properties:
|
||||
annotations:
|
||||
@@ -1775,6 +1833,27 @@ spec:
|
||||
- port
|
||||
- toCIDR
|
||||
type: object
|
||||
registryImages:
|
||||
description: |-
|
||||
HostRegistryImages checks whether images are accessible from the host,
|
||||
without requiring a Kubernetes cluster. Auth can be supplied inline via
|
||||
Username/Password or omitted to rely on ambient credentials (e.g. ~/.docker/config.json).
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
images:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
password:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
required:
|
||||
- images
|
||||
type: object
|
||||
run:
|
||||
properties:
|
||||
args:
|
||||
|
||||
@@ -43,7 +43,16 @@ spec:
|
||||
items:
|
||||
properties:
|
||||
blockDevices:
|
||||
description: BlockDevicesAnalyze evaluates host-collected block
|
||||
device listings (lsblk-based).
|
||||
properties:
|
||||
additionalDeviceTypes:
|
||||
description: |-
|
||||
AdditionalDeviceTypes are extra lsblk TYPE values (e.g. loop, lvm) that may count toward outcomes,
|
||||
in addition to whole disks and (when IncludeUnmountedPartitions is set) partitions.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
@@ -846,6 +855,55 @@ spec:
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
registryImages:
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
checkName:
|
||||
type: string
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
outcomes:
|
||||
items:
|
||||
properties:
|
||||
fail:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
pass:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
warn:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
strict:
|
||||
type: BoolString
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
subnetAvailable:
|
||||
properties:
|
||||
annotations:
|
||||
@@ -1775,6 +1833,27 @@ spec:
|
||||
- port
|
||||
- toCIDR
|
||||
type: object
|
||||
registryImages:
|
||||
description: |-
|
||||
HostRegistryImages checks whether images are accessible from the host,
|
||||
without requiring a Kubernetes cluster. Auth can be supplied inline via
|
||||
Username/Password or omitted to rely on ambient credentials (e.g. ~/.docker/config.json).
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
images:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
password:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
required:
|
||||
- images
|
||||
type: object
|
||||
run:
|
||||
properties:
|
||||
args:
|
||||
|
||||
@@ -880,6 +880,55 @@ spec:
|
||||
- namespace
|
||||
- outcomes
|
||||
type: object
|
||||
ingressClass:
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
checkName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
ingressClassName:
|
||||
type: string
|
||||
outcomes:
|
||||
items:
|
||||
properties:
|
||||
fail:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
pass:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
warn:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
strict:
|
||||
type: BoolString
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
jobStatus:
|
||||
properties:
|
||||
annotations:
|
||||
@@ -1311,6 +1360,8 @@ spec:
|
||||
- key
|
||||
type: object
|
||||
type: object
|
||||
ignoreIfNoFiles:
|
||||
type: boolean
|
||||
outcomes:
|
||||
items:
|
||||
properties:
|
||||
@@ -1563,6 +1614,58 @@ spec:
|
||||
- outcomes
|
||||
- selector
|
||||
type: object
|
||||
s3Status:
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
checkName:
|
||||
type: string
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
fileName:
|
||||
type: string
|
||||
outcomes:
|
||||
items:
|
||||
properties:
|
||||
fail:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
pass:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
warn:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
strict:
|
||||
type: BoolString
|
||||
required:
|
||||
- collectorName
|
||||
- outcomes
|
||||
type: object
|
||||
secret:
|
||||
properties:
|
||||
annotations:
|
||||
@@ -4522,7 +4625,9 @@ spec:
|
||||
type: integer
|
||||
type: object
|
||||
resizePolicy:
|
||||
description: Resources resize policy for the container.
|
||||
description: |-
|
||||
Resources resize policy for the container.
|
||||
This field cannot be set on ephemeral containers.
|
||||
items:
|
||||
description: ContainerResizePolicy represents
|
||||
resource resize policy for the container.
|
||||
@@ -7781,7 +7886,9 @@ spec:
|
||||
type: integer
|
||||
type: object
|
||||
resizePolicy:
|
||||
description: Resources resize policy for the container.
|
||||
description: |-
|
||||
Resources resize policy for the container.
|
||||
This field cannot be set on ephemeral containers.
|
||||
items:
|
||||
description: ContainerResizePolicy represents
|
||||
resource resize policy for the container.
|
||||
@@ -8571,8 +8678,8 @@ spec:
|
||||
will be made available to those containers which consume them
|
||||
by name.
|
||||
|
||||
This is an alpha field and requires enabling the
|
||||
DynamicResourceAllocation feature gate.
|
||||
This is a stable field but requires that the
|
||||
DynamicResourceAllocation feature gate is enabled.
|
||||
|
||||
This field is immutable.
|
||||
items:
|
||||
@@ -9032,9 +9139,10 @@ spec:
|
||||
operator:
|
||||
description: |-
|
||||
Operator represents a key's relationship to the value.
|
||||
Valid operators are Exists and Equal. Defaults to Equal.
|
||||
Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal.
|
||||
Exists is equivalent to wildcard for value, so that a pod can
|
||||
tolerate all taints of a particular category.
|
||||
Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators).
|
||||
type: string
|
||||
tolerationSeconds:
|
||||
description: |-
|
||||
@@ -9835,7 +9943,7 @@ spec:
|
||||
resources:
|
||||
description: |-
|
||||
resources represents the minimum resources the volume should have.
|
||||
If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements
|
||||
Users are allowed to specify resource requirements
|
||||
that are lower than previous value but must still be higher than capacity recorded in the
|
||||
status field of the claim.
|
||||
More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources
|
||||
@@ -10736,6 +10844,24 @@ spec:
|
||||
CSRs will be addressed to this
|
||||
signer.
|
||||
type: string
|
||||
userAnnotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: |-
|
||||
userAnnotations allow pod authors to pass additional information to
|
||||
the signer implementation. Kubernetes does not restrict or validate this
|
||||
metadata in any way.
|
||||
|
||||
These values are copied verbatim into the `spec.unverifiedUserAnnotations` field of
|
||||
the PodCertificateRequest objects that Kubelet creates.
|
||||
|
||||
Entries are subject to the same validation as object metadata annotations,
|
||||
with the addition that all keys must be domain-prefixed. No restrictions
|
||||
are placed on values, except an overall size limitation on the entire field.
|
||||
|
||||
Signers should document the keys and values they support. Signers should
|
||||
deny requests that contain keys they do not recognize.
|
||||
type: object
|
||||
required:
|
||||
- keyType
|
||||
- signerName
|
||||
@@ -11166,6 +11292,42 @@ spec:
|
||||
x-kubernetes-list-map-keys:
|
||||
- name
|
||||
x-kubernetes-list-type: map
|
||||
workloadRef:
|
||||
description: |-
|
||||
WorkloadRef provides a reference to the Workload object that this Pod belongs to.
|
||||
This field is used by the scheduler to identify the PodGroup and apply the
|
||||
correct group scheduling policies. The Workload object referenced
|
||||
by this field may not exist at the time the Pod is created.
|
||||
This field is immutable, but a Workload object with the same name
|
||||
may be recreated with different policies. Doing this during pod scheduling
|
||||
may result in the placement not conforming to the expected policies.
|
||||
properties:
|
||||
name:
|
||||
description: |-
|
||||
Name defines the name of the Workload object this Pod belongs to.
|
||||
Workload must be in the same namespace as the Pod.
|
||||
If it doesn't match any existing Workload, the Pod will remain unschedulable
|
||||
until a Workload object is created and observed by the kube-scheduler.
|
||||
It must be a DNS subdomain.
|
||||
type: string
|
||||
podGroup:
|
||||
description: |-
|
||||
PodGroup is the name of the PodGroup within the Workload that this Pod
|
||||
belongs to. If it doesn't match any existing PodGroup within the Workload,
|
||||
the Pod will remain unschedulable until the Workload object is recreated
|
||||
and observed by the kube-scheduler. It must be a DNS label.
|
||||
type: string
|
||||
podGroupReplicaKey:
|
||||
description: |-
|
||||
PodGroupReplicaKey specifies the replica key of the PodGroup to which this
|
||||
Pod belongs. It is used to distinguish pods belonging to different replicas
|
||||
of the same pod group. The pod group policy is applied separately to each replica.
|
||||
When set, it must be a DNS label.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- podGroup
|
||||
type: object
|
||||
required:
|
||||
- containers
|
||||
type: object
|
||||
@@ -13047,7 +13209,9 @@ spec:
|
||||
type: integer
|
||||
type: object
|
||||
resizePolicy:
|
||||
description: Resources resize policy for the container.
|
||||
description: |-
|
||||
Resources resize policy for the container.
|
||||
This field cannot be set on ephemeral containers.
|
||||
items:
|
||||
description: ContainerResizePolicy represents
|
||||
resource resize policy for the container.
|
||||
@@ -16306,7 +16470,9 @@ spec:
|
||||
type: integer
|
||||
type: object
|
||||
resizePolicy:
|
||||
description: Resources resize policy for the container.
|
||||
description: |-
|
||||
Resources resize policy for the container.
|
||||
This field cannot be set on ephemeral containers.
|
||||
items:
|
||||
description: ContainerResizePolicy represents
|
||||
resource resize policy for the container.
|
||||
@@ -17096,8 +17262,8 @@ spec:
|
||||
will be made available to those containers which consume them
|
||||
by name.
|
||||
|
||||
This is an alpha field and requires enabling the
|
||||
DynamicResourceAllocation feature gate.
|
||||
This is a stable field but requires that the
|
||||
DynamicResourceAllocation feature gate is enabled.
|
||||
|
||||
This field is immutable.
|
||||
items:
|
||||
@@ -17557,9 +17723,10 @@ spec:
|
||||
operator:
|
||||
description: |-
|
||||
Operator represents a key's relationship to the value.
|
||||
Valid operators are Exists and Equal. Defaults to Equal.
|
||||
Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal.
|
||||
Exists is equivalent to wildcard for value, so that a pod can
|
||||
tolerate all taints of a particular category.
|
||||
Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators).
|
||||
type: string
|
||||
tolerationSeconds:
|
||||
description: |-
|
||||
@@ -18360,7 +18527,7 @@ spec:
|
||||
resources:
|
||||
description: |-
|
||||
resources represents the minimum resources the volume should have.
|
||||
If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements
|
||||
Users are allowed to specify resource requirements
|
||||
that are lower than previous value but must still be higher than capacity recorded in the
|
||||
status field of the claim.
|
||||
More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources
|
||||
@@ -19261,6 +19428,24 @@ spec:
|
||||
CSRs will be addressed to this
|
||||
signer.
|
||||
type: string
|
||||
userAnnotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: |-
|
||||
userAnnotations allow pod authors to pass additional information to
|
||||
the signer implementation. Kubernetes does not restrict or validate this
|
||||
metadata in any way.
|
||||
|
||||
These values are copied verbatim into the `spec.unverifiedUserAnnotations` field of
|
||||
the PodCertificateRequest objects that Kubelet creates.
|
||||
|
||||
Entries are subject to the same validation as object metadata annotations,
|
||||
with the addition that all keys must be domain-prefixed. No restrictions
|
||||
are placed on values, except an overall size limitation on the entire field.
|
||||
|
||||
Signers should document the keys and values they support. Signers should
|
||||
deny requests that contain keys they do not recognize.
|
||||
type: object
|
||||
required:
|
||||
- keyType
|
||||
- signerName
|
||||
@@ -19691,6 +19876,42 @@ spec:
|
||||
x-kubernetes-list-map-keys:
|
||||
- name
|
||||
x-kubernetes-list-type: map
|
||||
workloadRef:
|
||||
description: |-
|
||||
WorkloadRef provides a reference to the Workload object that this Pod belongs to.
|
||||
This field is used by the scheduler to identify the PodGroup and apply the
|
||||
correct group scheduling policies. The Workload object referenced
|
||||
by this field may not exist at the time the Pod is created.
|
||||
This field is immutable, but a Workload object with the same name
|
||||
may be recreated with different policies. Doing this during pod scheduling
|
||||
may result in the placement not conforming to the expected policies.
|
||||
properties:
|
||||
name:
|
||||
description: |-
|
||||
Name defines the name of the Workload object this Pod belongs to.
|
||||
Workload must be in the same namespace as the Pod.
|
||||
If it doesn't match any existing Workload, the Pod will remain unschedulable
|
||||
until a Workload object is created and observed by the kube-scheduler.
|
||||
It must be a DNS subdomain.
|
||||
type: string
|
||||
podGroup:
|
||||
description: |-
|
||||
PodGroup is the name of the PodGroup within the Workload that this Pod
|
||||
belongs to. If it doesn't match any existing PodGroup within the Workload,
|
||||
the Pod will remain unschedulable until the Workload object is recreated
|
||||
and observed by the kube-scheduler. It must be a DNS label.
|
||||
type: string
|
||||
podGroupReplicaKey:
|
||||
description: |-
|
||||
PodGroupReplicaKey specifies the replica key of the PodGroup to which this
|
||||
Pod belongs. It is used to distinguish pods belonging to different replicas
|
||||
of the same pod group. The pod group policy is applied separately to each replica.
|
||||
When set, it must be a DNS label.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- podGroup
|
||||
type: object
|
||||
required:
|
||||
- containers
|
||||
type: object
|
||||
@@ -19699,6 +19920,29 @@ spec:
|
||||
required:
|
||||
- namespace
|
||||
type: object
|
||||
s3Status:
|
||||
properties:
|
||||
accessKeyID:
|
||||
type: string
|
||||
bucketName:
|
||||
type: string
|
||||
collectorName:
|
||||
type: string
|
||||
endpoint:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
insecure:
|
||||
type: boolean
|
||||
region:
|
||||
type: string
|
||||
secretAccessKey:
|
||||
type: string
|
||||
usePathStyle:
|
||||
type: boolean
|
||||
required:
|
||||
- bucketName
|
||||
type: object
|
||||
secret:
|
||||
properties:
|
||||
collectorName:
|
||||
@@ -19729,6 +19973,17 @@ spec:
|
||||
namespace:
|
||||
type: string
|
||||
type: object
|
||||
supportBundleMetadata:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
namespace:
|
||||
type: string
|
||||
required:
|
||||
- namespace
|
||||
type: object
|
||||
sysctl:
|
||||
properties:
|
||||
collectorName:
|
||||
|
||||
@@ -911,6 +911,55 @@ spec:
|
||||
- namespace
|
||||
- outcomes
|
||||
type: object
|
||||
ingressClass:
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
checkName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
ingressClassName:
|
||||
type: string
|
||||
outcomes:
|
||||
items:
|
||||
properties:
|
||||
fail:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
pass:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
warn:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
strict:
|
||||
type: BoolString
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
jobStatus:
|
||||
properties:
|
||||
annotations:
|
||||
@@ -1342,6 +1391,8 @@ spec:
|
||||
- key
|
||||
type: object
|
||||
type: object
|
||||
ignoreIfNoFiles:
|
||||
type: boolean
|
||||
outcomes:
|
||||
items:
|
||||
properties:
|
||||
@@ -1594,6 +1645,58 @@ spec:
|
||||
- outcomes
|
||||
- selector
|
||||
type: object
|
||||
s3Status:
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
checkName:
|
||||
type: string
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
fileName:
|
||||
type: string
|
||||
outcomes:
|
||||
items:
|
||||
properties:
|
||||
fail:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
pass:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
warn:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
strict:
|
||||
type: BoolString
|
||||
required:
|
||||
- collectorName
|
||||
- outcomes
|
||||
type: object
|
||||
secret:
|
||||
properties:
|
||||
annotations:
|
||||
@@ -4553,7 +4656,9 @@ spec:
|
||||
type: integer
|
||||
type: object
|
||||
resizePolicy:
|
||||
description: Resources resize policy for the container.
|
||||
description: |-
|
||||
Resources resize policy for the container.
|
||||
This field cannot be set on ephemeral containers.
|
||||
items:
|
||||
description: ContainerResizePolicy represents
|
||||
resource resize policy for the container.
|
||||
@@ -7812,7 +7917,9 @@ spec:
|
||||
type: integer
|
||||
type: object
|
||||
resizePolicy:
|
||||
description: Resources resize policy for the container.
|
||||
description: |-
|
||||
Resources resize policy for the container.
|
||||
This field cannot be set on ephemeral containers.
|
||||
items:
|
||||
description: ContainerResizePolicy represents
|
||||
resource resize policy for the container.
|
||||
@@ -8602,8 +8709,8 @@ spec:
|
||||
will be made available to those containers which consume them
|
||||
by name.
|
||||
|
||||
This is an alpha field and requires enabling the
|
||||
DynamicResourceAllocation feature gate.
|
||||
This is a stable field but requires that the
|
||||
DynamicResourceAllocation feature gate is enabled.
|
||||
|
||||
This field is immutable.
|
||||
items:
|
||||
@@ -9063,9 +9170,10 @@ spec:
|
||||
operator:
|
||||
description: |-
|
||||
Operator represents a key's relationship to the value.
|
||||
Valid operators are Exists and Equal. Defaults to Equal.
|
||||
Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal.
|
||||
Exists is equivalent to wildcard for value, so that a pod can
|
||||
tolerate all taints of a particular category.
|
||||
Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators).
|
||||
type: string
|
||||
tolerationSeconds:
|
||||
description: |-
|
||||
@@ -9866,7 +9974,7 @@ spec:
|
||||
resources:
|
||||
description: |-
|
||||
resources represents the minimum resources the volume should have.
|
||||
If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements
|
||||
Users are allowed to specify resource requirements
|
||||
that are lower than previous value but must still be higher than capacity recorded in the
|
||||
status field of the claim.
|
||||
More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources
|
||||
@@ -10767,6 +10875,24 @@ spec:
|
||||
CSRs will be addressed to this
|
||||
signer.
|
||||
type: string
|
||||
userAnnotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: |-
|
||||
userAnnotations allow pod authors to pass additional information to
|
||||
the signer implementation. Kubernetes does not restrict or validate this
|
||||
metadata in any way.
|
||||
|
||||
These values are copied verbatim into the `spec.unverifiedUserAnnotations` field of
|
||||
the PodCertificateRequest objects that Kubelet creates.
|
||||
|
||||
Entries are subject to the same validation as object metadata annotations,
|
||||
with the addition that all keys must be domain-prefixed. No restrictions
|
||||
are placed on values, except an overall size limitation on the entire field.
|
||||
|
||||
Signers should document the keys and values they support. Signers should
|
||||
deny requests that contain keys they do not recognize.
|
||||
type: object
|
||||
required:
|
||||
- keyType
|
||||
- signerName
|
||||
@@ -11197,6 +11323,42 @@ spec:
|
||||
x-kubernetes-list-map-keys:
|
||||
- name
|
||||
x-kubernetes-list-type: map
|
||||
workloadRef:
|
||||
description: |-
|
||||
WorkloadRef provides a reference to the Workload object that this Pod belongs to.
|
||||
This field is used by the scheduler to identify the PodGroup and apply the
|
||||
correct group scheduling policies. The Workload object referenced
|
||||
by this field may not exist at the time the Pod is created.
|
||||
This field is immutable, but a Workload object with the same name
|
||||
may be recreated with different policies. Doing this during pod scheduling
|
||||
may result in the placement not conforming to the expected policies.
|
||||
properties:
|
||||
name:
|
||||
description: |-
|
||||
Name defines the name of the Workload object this Pod belongs to.
|
||||
Workload must be in the same namespace as the Pod.
|
||||
If it doesn't match any existing Workload, the Pod will remain unschedulable
|
||||
until a Workload object is created and observed by the kube-scheduler.
|
||||
It must be a DNS subdomain.
|
||||
type: string
|
||||
podGroup:
|
||||
description: |-
|
||||
PodGroup is the name of the PodGroup within the Workload that this Pod
|
||||
belongs to. If it doesn't match any existing PodGroup within the Workload,
|
||||
the Pod will remain unschedulable until the Workload object is recreated
|
||||
and observed by the kube-scheduler. It must be a DNS label.
|
||||
type: string
|
||||
podGroupReplicaKey:
|
||||
description: |-
|
||||
PodGroupReplicaKey specifies the replica key of the PodGroup to which this
|
||||
Pod belongs. It is used to distinguish pods belonging to different replicas
|
||||
of the same pod group. The pod group policy is applied separately to each replica.
|
||||
When set, it must be a DNS label.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- podGroup
|
||||
type: object
|
||||
required:
|
||||
- containers
|
||||
type: object
|
||||
@@ -13078,7 +13240,9 @@ spec:
|
||||
type: integer
|
||||
type: object
|
||||
resizePolicy:
|
||||
description: Resources resize policy for the container.
|
||||
description: |-
|
||||
Resources resize policy for the container.
|
||||
This field cannot be set on ephemeral containers.
|
||||
items:
|
||||
description: ContainerResizePolicy represents
|
||||
resource resize policy for the container.
|
||||
@@ -16337,7 +16501,9 @@ spec:
|
||||
type: integer
|
||||
type: object
|
||||
resizePolicy:
|
||||
description: Resources resize policy for the container.
|
||||
description: |-
|
||||
Resources resize policy for the container.
|
||||
This field cannot be set on ephemeral containers.
|
||||
items:
|
||||
description: ContainerResizePolicy represents
|
||||
resource resize policy for the container.
|
||||
@@ -17127,8 +17293,8 @@ spec:
|
||||
will be made available to those containers which consume them
|
||||
by name.
|
||||
|
||||
This is an alpha field and requires enabling the
|
||||
DynamicResourceAllocation feature gate.
|
||||
This is a stable field but requires that the
|
||||
DynamicResourceAllocation feature gate is enabled.
|
||||
|
||||
This field is immutable.
|
||||
items:
|
||||
@@ -17588,9 +17754,10 @@ spec:
|
||||
operator:
|
||||
description: |-
|
||||
Operator represents a key's relationship to the value.
|
||||
Valid operators are Exists and Equal. Defaults to Equal.
|
||||
Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal.
|
||||
Exists is equivalent to wildcard for value, so that a pod can
|
||||
tolerate all taints of a particular category.
|
||||
Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators).
|
||||
type: string
|
||||
tolerationSeconds:
|
||||
description: |-
|
||||
@@ -18391,7 +18558,7 @@ spec:
|
||||
resources:
|
||||
description: |-
|
||||
resources represents the minimum resources the volume should have.
|
||||
If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements
|
||||
Users are allowed to specify resource requirements
|
||||
that are lower than previous value but must still be higher than capacity recorded in the
|
||||
status field of the claim.
|
||||
More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources
|
||||
@@ -19292,6 +19459,24 @@ spec:
|
||||
CSRs will be addressed to this
|
||||
signer.
|
||||
type: string
|
||||
userAnnotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: |-
|
||||
userAnnotations allow pod authors to pass additional information to
|
||||
the signer implementation. Kubernetes does not restrict or validate this
|
||||
metadata in any way.
|
||||
|
||||
These values are copied verbatim into the `spec.unverifiedUserAnnotations` field of
|
||||
the PodCertificateRequest objects that Kubelet creates.
|
||||
|
||||
Entries are subject to the same validation as object metadata annotations,
|
||||
with the addition that all keys must be domain-prefixed. No restrictions
|
||||
are placed on values, except an overall size limitation on the entire field.
|
||||
|
||||
Signers should document the keys and values they support. Signers should
|
||||
deny requests that contain keys they do not recognize.
|
||||
type: object
|
||||
required:
|
||||
- keyType
|
||||
- signerName
|
||||
@@ -19722,6 +19907,42 @@ spec:
|
||||
x-kubernetes-list-map-keys:
|
||||
- name
|
||||
x-kubernetes-list-type: map
|
||||
workloadRef:
|
||||
description: |-
|
||||
WorkloadRef provides a reference to the Workload object that this Pod belongs to.
|
||||
This field is used by the scheduler to identify the PodGroup and apply the
|
||||
correct group scheduling policies. The Workload object referenced
|
||||
by this field may not exist at the time the Pod is created.
|
||||
This field is immutable, but a Workload object with the same name
|
||||
may be recreated with different policies. Doing this during pod scheduling
|
||||
may result in the placement not conforming to the expected policies.
|
||||
properties:
|
||||
name:
|
||||
description: |-
|
||||
Name defines the name of the Workload object this Pod belongs to.
|
||||
Workload must be in the same namespace as the Pod.
|
||||
If it doesn't match any existing Workload, the Pod will remain unschedulable
|
||||
until a Workload object is created and observed by the kube-scheduler.
|
||||
It must be a DNS subdomain.
|
||||
type: string
|
||||
podGroup:
|
||||
description: |-
|
||||
PodGroup is the name of the PodGroup within the Workload that this Pod
|
||||
belongs to. If it doesn't match any existing PodGroup within the Workload,
|
||||
the Pod will remain unschedulable until the Workload object is recreated
|
||||
and observed by the kube-scheduler. It must be a DNS label.
|
||||
type: string
|
||||
podGroupReplicaKey:
|
||||
description: |-
|
||||
PodGroupReplicaKey specifies the replica key of the PodGroup to which this
|
||||
Pod belongs. It is used to distinguish pods belonging to different replicas
|
||||
of the same pod group. The pod group policy is applied separately to each replica.
|
||||
When set, it must be a DNS label.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- podGroup
|
||||
type: object
|
||||
required:
|
||||
- containers
|
||||
type: object
|
||||
@@ -19730,6 +19951,29 @@ spec:
|
||||
required:
|
||||
- namespace
|
||||
type: object
|
||||
s3Status:
|
||||
properties:
|
||||
accessKeyID:
|
||||
type: string
|
||||
bucketName:
|
||||
type: string
|
||||
collectorName:
|
||||
type: string
|
||||
endpoint:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
insecure:
|
||||
type: boolean
|
||||
region:
|
||||
type: string
|
||||
secretAccessKey:
|
||||
type: string
|
||||
usePathStyle:
|
||||
type: boolean
|
||||
required:
|
||||
- bucketName
|
||||
type: object
|
||||
secret:
|
||||
properties:
|
||||
collectorName:
|
||||
@@ -19760,6 +20004,17 @@ spec:
|
||||
namespace:
|
||||
type: string
|
||||
type: object
|
||||
supportBundleMetadata:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
namespace:
|
||||
type: string
|
||||
required:
|
||||
- namespace
|
||||
type: object
|
||||
sysctl:
|
||||
properties:
|
||||
collectorName:
|
||||
@@ -19797,7 +20052,16 @@ spec:
|
||||
items:
|
||||
properties:
|
||||
blockDevices:
|
||||
description: BlockDevicesAnalyze evaluates host-collected block
|
||||
device listings (lsblk-based).
|
||||
properties:
|
||||
additionalDeviceTypes:
|
||||
description: |-
|
||||
AdditionalDeviceTypes are extra lsblk TYPE values (e.g. loop, lvm) that may count toward outcomes,
|
||||
in addition to whole disks and (when IncludeUnmountedPartitions is set) partitions.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
@@ -20600,6 +20864,55 @@ spec:
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
registryImages:
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
checkName:
|
||||
type: string
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
outcomes:
|
||||
items:
|
||||
properties:
|
||||
fail:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
pass:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
warn:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
strict:
|
||||
type: BoolString
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
subnetAvailable:
|
||||
properties:
|
||||
annotations:
|
||||
@@ -21529,6 +21842,27 @@ spec:
|
||||
- port
|
||||
- toCIDR
|
||||
type: object
|
||||
registryImages:
|
||||
description: |-
|
||||
HostRegistryImages checks whether images are accessible from the host,
|
||||
without requiring a Kubernetes cluster. Auth can be supplied inline via
|
||||
Username/Password or omitted to rely on ambient credentials (e.g. ~/.docker/config.json).
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
images:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
password:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
required:
|
||||
- images
|
||||
type: object
|
||||
run:
|
||||
properties:
|
||||
args:
|
||||
|
||||
+39
-4
@@ -9,10 +9,14 @@ builds:
|
||||
main: ./cmd/preflight/main.go
|
||||
env: [CGO_ENABLED=0]
|
||||
goos: [linux, darwin, windows]
|
||||
goarch: [amd64, arm, arm64]
|
||||
goarch: [amd64, arm, arm64, riscv64]
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: windows
|
||||
goarch: riscv64
|
||||
- goos: darwin
|
||||
goarch: riscv64
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X github.com/replicatedhq/troubleshoot/pkg/version.version={{ .Version }}
|
||||
@@ -32,10 +36,14 @@ builds:
|
||||
main: ./cmd/troubleshoot/main.go
|
||||
env: [CGO_ENABLED=0]
|
||||
goos: [linux, darwin, windows]
|
||||
goarch: [amd64, arm, arm64]
|
||||
goarch: [amd64, arm, arm64, riscv64]
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: windows
|
||||
goarch: riscv64
|
||||
- goos: darwin
|
||||
goarch: riscv64
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X github.com/replicatedhq/troubleshoot/pkg/version.version={{ .Version }}
|
||||
@@ -51,6 +59,33 @@ builds:
|
||||
- -installsuffix=netgo
|
||||
binary: support-bundle
|
||||
|
||||
- id: collect
|
||||
main: ./cmd/collect/main.go
|
||||
env: [CGO_ENABLED=0]
|
||||
goos: [linux, darwin, windows]
|
||||
goarch: [amd64, arm, arm64, riscv64]
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: windows
|
||||
goarch: riscv64
|
||||
- goos: darwin
|
||||
goarch: riscv64
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X github.com/replicatedhq/troubleshoot/pkg/version.version={{ .Version }}
|
||||
- -X github.com/replicatedhq/troubleshoot/pkg/version.gitSHA={{ .Commit }}
|
||||
- -X github.com/replicatedhq/troubleshoot/pkg/version.buildTime={{ .Date }}
|
||||
- -extldflags "-static"
|
||||
flags:
|
||||
- -tags=netgo
|
||||
- -tags=containers_image_ostree_stub
|
||||
- -tags=exclude_graphdriver_devicemapper
|
||||
- -tags=exclude_graphdriver_btrfs
|
||||
- -tags=containers_image_openpgp
|
||||
- -installsuffix=netgo
|
||||
binary: collect
|
||||
|
||||
archives:
|
||||
- id: preflight
|
||||
ids: [preflight]
|
||||
@@ -135,7 +170,7 @@ dockers:
|
||||
ids:
|
||||
- support-bundle
|
||||
- preflight
|
||||
skip_push: true
|
||||
- collect
|
||||
- dockerfile: ./deploy/Dockerfile.troubleshoot
|
||||
image_templates:
|
||||
- "replicated/preflight:latest"
|
||||
@@ -145,7 +180,7 @@ dockers:
|
||||
ids:
|
||||
- support-bundle
|
||||
- preflight
|
||||
skip_push: true
|
||||
- collect
|
||||
|
||||
universal_binaries:
|
||||
- id: preflight-universal
|
||||
|
||||
@@ -7,6 +7,7 @@ RUN apt-get -qq update \
|
||||
|
||||
COPY support-bundle /troubleshoot/support-bundle
|
||||
COPY preflight /troubleshoot/preflight
|
||||
COPY collect /troubleshoot/collect
|
||||
|
||||
ENV PATH="/troubleshoot:${PATH}"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module helm-template
|
||||
|
||||
go 1.24.6
|
||||
go 1.26.1
|
||||
|
||||
// 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.0
|
||||
helm.sh/helm/v3 v3.20.2
|
||||
sigs.k8s.io/yaml v1.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/BurntSushi/toml v1.6.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.4.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.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,7 +28,6 @@ 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
|
||||
@@ -46,28 +45,29 @@ 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.2 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // 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
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/term v0.41.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // 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.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-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
|
||||
k8s.io/api v0.35.3 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.35.3 // indirect
|
||||
k8s.io/apimachinery v0.35.3 // indirect
|
||||
k8s.io/client-go v0.35.3 // indirect
|
||||
k8s.io/klog/v2 v2.140.0 // 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.23.3 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // 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.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.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.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
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/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,8 +36,6 @@ 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=
|
||||
@@ -45,8 +43,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-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
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/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=
|
||||
@@ -55,8 +53,6 @@ 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=
|
||||
@@ -75,17 +71,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.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/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/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.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
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/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=
|
||||
@@ -102,89 +98,66 @@ 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=
|
||||
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/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/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-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=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
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.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/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.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-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=
|
||||
helm.sh/helm/v3 v3.20.2 h1:binM4rvPx5DcNsa1sIt7UZi55lRbu3pZUFmQkSoRh48=
|
||||
helm.sh/helm/v3 v3.20.2/go.mod h1:Fl1kBaWCpkUrM6IYXPjQ3bdZQfFrogKArqptvueZ6Ww=
|
||||
k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ=
|
||||
k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4=
|
||||
k8s.io/apiextensions-apiserver v0.35.3 h1:2fQUhEO7P17sijylbdwt0nBdXP0TvHrHj0KeqHD8FiU=
|
||||
k8s.io/apiextensions-apiserver v0.35.3/go.mod h1:tK4Kz58ykRpwAEkXUb634HD1ZAegEElktz/B3jgETd8=
|
||||
k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8=
|
||||
k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
|
||||
k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg=
|
||||
k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c=
|
||||
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
|
||||
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||
k8s.io/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.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80=
|
||||
sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0=
|
||||
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/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=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
|
||||
@@ -1,131 +1,127 @@
|
||||
module github.com/replicatedhq/troubleshoot
|
||||
|
||||
go 1.25.4
|
||||
go 1.26.2
|
||||
|
||||
require (
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/ahmetalpbalkan/go-cursor v0.0.0-20131010032410-8136607ea412
|
||||
github.com/apparentlymart/go-cidr v1.1.0
|
||||
github.com/apparentlymart/go-cidr v1.1.1
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.6
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.15
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.100.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.1
|
||||
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/cilium/ebpf v0.21.0
|
||||
github.com/containerd/cgroups/v3 v3.1.3
|
||||
github.com/fatih/color v1.19.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.1.0
|
||||
github.com/godbus/dbus/v5 v5.2.2
|
||||
github.com/google/go-containerregistry v0.21.5
|
||||
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.3
|
||||
github.com/hashicorp/go-getter v1.8.6
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/jackc/pgx/v5 v5.7.6
|
||||
github.com/jackc/pgx/v5 v5.9.2
|
||||
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.3
|
||||
github.com/miekg/dns v1.1.68
|
||||
github.com/mattn/go-isatty v0.0.22
|
||||
github.com/microsoft/go-mssqldb v1.9.8
|
||||
github.com/miekg/dns v1.1.72
|
||||
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.10
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/shirou/gopsutil/v4 v4.26.3
|
||||
github.com/spf13/cobra v1.10.2
|
||||
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.38.0
|
||||
go.opentelemetry.io/otel/sdk v1.38.0
|
||||
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67
|
||||
golang.org/x/mod v0.29.0
|
||||
golang.org/x/sync v0.18.0
|
||||
github.com/vmware-tanzu/velero v1.18.0
|
||||
go.opentelemetry.io/otel v1.43.0
|
||||
go.opentelemetry.io/otel/sdk v1.43.0
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
|
||||
golang.org/x/mod v0.35.0
|
||||
golang.org/x/sync v0.20.0
|
||||
gopkg.in/yaml.v2 v2.4.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
|
||||
k8s.io/kubernetes v1.34.1
|
||||
oras.land/oras-go v1.2.7
|
||||
sigs.k8s.io/controller-runtime v0.22.4
|
||||
sigs.k8s.io/e2e-framework v0.6.0
|
||||
k8s.io/api v0.36.0
|
||||
k8s.io/apiextensions-apiserver v0.36.0
|
||||
k8s.io/apimachinery v0.36.0
|
||||
k8s.io/apiserver v0.36.0
|
||||
k8s.io/cli-runtime v0.36.0
|
||||
k8s.io/client-go v0.36.0
|
||||
k8s.io/klog/v2 v2.140.0
|
||||
oras.land/oras-go/v2 v2.6.0
|
||||
sigs.k8s.io/controller-runtime v0.23.3
|
||||
sigs.k8s.io/e2e-framework v0.7.0
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
cloud.google.com/go/auth v0.16.2 // indirect
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
cloud.google.com/go/auth v0.18.2 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.7.0 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.2 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.3 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
filippo.io/edwards25519 v1.1.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
|
||||
filippo.io/edwards25519 v1.1.1 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.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.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/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.42.0 // indirect
|
||||
github.com/aws/smithy-go v1.25.0 // indirect
|
||||
github.com/chai2010/gettext-go v1.0.2 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // 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/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/ebitengine/purego v0.9.0 // 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
|
||||
github.com/coreos/go-systemd/v22 v22.7.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/distribution/distribution/v3 v3.1.0 // indirect
|
||||
github.com/docker/cli v29.4.0+incompatible // indirect
|
||||
github.com/ebitengine/purego v0.10.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
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.0.5 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // 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.3 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // 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.65 // indirect
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72 // 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,76 +130,60 @@ 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/v3 v3.0.1 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // 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
|
||||
github.com/moby/sys/capability v0.4.0 // indirect
|
||||
github.com/moby/sys/user v0.4.0 // indirect
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/rubenv/sql-migrate v1.8.0 // indirect
|
||||
github.com/rubenv/sql-migrate v1.8.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
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.3 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // 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/vladimirvivien/gexe v0.5.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/zeebo/errs v1.4.0 // 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.61.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // 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.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/tools v0.38.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
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
gotest.tools/v3 v3.5.2 // indirect
|
||||
k8s.io/component-base v0.36.0 // indirect
|
||||
k8s.io/kubectl v0.36.0 // indirect
|
||||
k8s.io/streaming v0.36.0 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
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
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
cloud.google.com/go/iam v1.5.3 // indirect
|
||||
cloud.google.com/go/storage v1.61.3 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // 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/BurntSushi/toml v1.6.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.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/containerd/containerd v1.7.30 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/docker/cli v28.5.1+incompatible // indirect
|
||||
github.com/docker/docker v28.5.1+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.5 // indirect
|
||||
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
@@ -212,31 +192,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.14.2 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-version v1.7.0
|
||||
github.com/hashicorp/go-version v1.9.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.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
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/spdystream v0.5.1 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
@@ -245,46 +216,44 @@ require (
|
||||
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.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.22.0 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.20.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.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/vbatts/tar-split v0.12.1 // 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/xlab/treeprint v1.2.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/net v0.47.0
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sys v0.38.0
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/text v0.31.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
|
||||
golang.org/x/crypto v0.50.0 // indirect
|
||||
golang.org/x/net v0.53.0
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sys v0.43.0
|
||||
golang.org/x/term v0.42.0 // indirect
|
||||
golang.org/x/text v0.36.0
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
google.golang.org/api v0.271.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/grpc v1.79.3 // indirect
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
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
|
||||
helm.sh/helm/v3 v3.20.2
|
||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
|
||||
k8s.io/kubelet v0.36.0
|
||||
k8s.io/metrics v0.36.0
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2
|
||||
periph.io/x/host/v3 v3.8.5
|
||||
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/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.21.1 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.21.1 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0
|
||||
)
|
||||
|
||||
|
||||
@@ -194,6 +194,8 @@ func GetAnalyzer(analyzer *troubleshootv1beta2.Analyze) Analyzer {
|
||||
return &AnalyzeClusterVersion{analyzer: analyzer.ClusterVersion}
|
||||
case analyzer.StorageClass != nil:
|
||||
return &AnalyzeStorageClass{analyzer: analyzer.StorageClass}
|
||||
case analyzer.IngressClass != nil:
|
||||
return &AnalyzeIngressClass{analyzer: analyzer.IngressClass}
|
||||
case analyzer.CustomResourceDefinition != nil:
|
||||
return &AnalyzeCustomResourceDefinition{analyzer: analyzer.CustomResourceDefinition}
|
||||
case analyzer.Ingress != nil:
|
||||
@@ -260,6 +262,8 @@ func GetAnalyzer(analyzer *troubleshootv1beta2.Analyze) Analyzer {
|
||||
return &AnalyzeNodeMetrics{analyzer: analyzer.NodeMetrics}
|
||||
case analyzer.HTTP != nil:
|
||||
return &AnalyzeHTTPAnalyze{analyzer: analyzer.HTTP}
|
||||
case analyzer.S3Status != nil:
|
||||
return &AnalyzeS3Status{analyzer: analyzer.S3Status}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -67,6 +67,8 @@ func GetHostAnalyzer(analyzer *troubleshootv1beta2.HostAnalyze) (HostAnalyzer, b
|
||||
return &AnalyzeHostNetworkNamespaceConnectivity{analyzer.NetworkNamespaceConnectivity}, true
|
||||
case analyzer.Sysctl != nil:
|
||||
return &AnalyzeHostSysctl{analyzer.Sysctl}, true
|
||||
case analyzer.RegistryImages != nil:
|
||||
return &AnalyzeHostRegistryImages{analyzer.RegistryImages}, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -16,6 +17,25 @@ type AnalyzeHostBlockDevices struct {
|
||||
hostAnalyzer *troubleshootv1beta2.BlockDevicesAnalyze
|
||||
}
|
||||
|
||||
// blockDevicesMatchConfig carries analyzer settings for counting devices toward a blockDevices when-clause
|
||||
// ("<name-regex> <op> <count>" against host block_devices.json). A device is eligible when its name matches
|
||||
// the regex; its type is "disk", or "part" if includeUnmountedPartitions, or listed in additionalDeviceTypes;
|
||||
// size >= minimumAcceptableSize when that is non-zero; it has no mountpoint or filesystem; it is not
|
||||
// read-only or removable; and no other row has ParentKernelName equal to its KernelName.
|
||||
type blockDevicesMatchConfig struct {
|
||||
minimumAcceptableSize uint64
|
||||
includeUnmountedPartitions bool
|
||||
additionalDeviceTypes []string
|
||||
}
|
||||
|
||||
func matchConfigFromAnalyzer(a *troubleshootv1beta2.BlockDevicesAnalyze) blockDevicesMatchConfig {
|
||||
return blockDevicesMatchConfig{
|
||||
minimumAcceptableSize: a.MinimumAcceptableSize,
|
||||
includeUnmountedPartitions: a.IncludeUnmountedPartitions,
|
||||
additionalDeviceTypes: a.AdditionalDeviceTypes,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostBlockDevices) Title() string {
|
||||
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "Block Devices")
|
||||
}
|
||||
@@ -49,7 +69,7 @@ func (a *AnalyzeHostBlockDevices) Analyze(
|
||||
|
||||
// <regexp> <op> <count>
|
||||
// example: sdb > 0
|
||||
func compareHostBlockDevicesConditionalToActual(conditional string, minimumAcceptableSize uint64, includeUnmountedPartitions bool, devices []collect.BlockDeviceInfo) (res bool, err error) {
|
||||
func compareHostBlockDevicesConditionalToActual(conditional string, cfg blockDevicesMatchConfig, devices []collect.BlockDeviceInfo) (res bool, err error) {
|
||||
parts := strings.Split(conditional, " ")
|
||||
if len(parts) != 3 {
|
||||
return false, fmt.Errorf("Expected exactly 3 parts, got %d", len(parts))
|
||||
@@ -59,7 +79,7 @@ func compareHostBlockDevicesConditionalToActual(conditional string, minimumAccep
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "failed to compile regex %q", parts[0])
|
||||
}
|
||||
count := countEligibleBlockDevices(rx, minimumAcceptableSize, includeUnmountedPartitions, devices)
|
||||
count := countEligibleBlockDevices(rx, cfg, devices)
|
||||
|
||||
desiredInt, err := strconv.Atoi(parts[2])
|
||||
if err != nil {
|
||||
@@ -82,11 +102,11 @@ func compareHostBlockDevicesConditionalToActual(conditional string, minimumAccep
|
||||
return false, fmt.Errorf("Unexpected operator %q", parts[1])
|
||||
}
|
||||
|
||||
func countEligibleBlockDevices(rx *regexp.Regexp, minimumAcceptableSize uint64, includeUnmountedPartitions bool, devices []collect.BlockDeviceInfo) int {
|
||||
func countEligibleBlockDevices(rx *regexp.Regexp, cfg blockDevicesMatchConfig, devices []collect.BlockDeviceInfo) int {
|
||||
count := 0
|
||||
|
||||
for _, device := range devices {
|
||||
if isEligibleBlockDevice(rx, minimumAcceptableSize, includeUnmountedPartitions, device, devices) {
|
||||
if isEligibleBlockDevice(rx, cfg, device, devices) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
@@ -94,23 +114,27 @@ func countEligibleBlockDevices(rx *regexp.Regexp, minimumAcceptableSize uint64,
|
||||
return count
|
||||
}
|
||||
|
||||
func isEligibleBlockDevice(rx *regexp.Regexp, minimumAcceptableSize uint64, includeUnmountedPartitions bool, device collect.BlockDeviceInfo, devices []collect.BlockDeviceInfo) bool {
|
||||
func isEligibleDeviceType(deviceType string, cfg blockDevicesMatchConfig) bool {
|
||||
if deviceType == "disk" {
|
||||
return true
|
||||
}
|
||||
if cfg.includeUnmountedPartitions && deviceType == "part" {
|
||||
return true
|
||||
}
|
||||
return slices.Contains(cfg.additionalDeviceTypes, deviceType)
|
||||
}
|
||||
|
||||
func isEligibleBlockDevice(rx *regexp.Regexp, cfg blockDevicesMatchConfig, device collect.BlockDeviceInfo, devices []collect.BlockDeviceInfo) bool {
|
||||
if !rx.MatchString(device.Name) {
|
||||
return false
|
||||
}
|
||||
|
||||
if includeUnmountedPartitions {
|
||||
if device.Type != "disk" && device.Type != "part" {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if device.Type != "disk" {
|
||||
return false
|
||||
}
|
||||
if !isEligibleDeviceType(device.Type, cfg) {
|
||||
return false
|
||||
}
|
||||
|
||||
if minimumAcceptableSize != 0 {
|
||||
if device.Size < minimumAcceptableSize {
|
||||
if cfg.minimumAcceptableSize != 0 {
|
||||
if device.Size < cfg.minimumAcceptableSize {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -141,12 +165,10 @@ func isEligibleBlockDevice(rx *regexp.Regexp, minimumAcceptableSize uint64, incl
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostBlockDevices) CheckCondition(when string, data []byte) (bool, error) {
|
||||
|
||||
var devices []collect.BlockDeviceInfo
|
||||
if err := json.Unmarshal(data, &devices); err != nil {
|
||||
return false, errors.Wrap(err, "failed to unmarshal block devices info")
|
||||
}
|
||||
|
||||
return compareHostBlockDevicesConditionalToActual(when, a.hostAnalyzer.MinimumAcceptableSize, a.hostAnalyzer.IncludeUnmountedPartitions, devices)
|
||||
|
||||
return compareHostBlockDevicesConditionalToActual(when, matchConfigFromAnalyzer(a.hostAnalyzer), devices)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
// TestHostBlockDevices_deviceTypeEligibility documents type rules in isolation (see blockDevicesMatchConfig in host_block_devices.go).
|
||||
func TestHostBlockDevices_deviceTypeEligibility(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg blockDevicesMatchConfig
|
||||
devType string
|
||||
want bool
|
||||
}{
|
||||
{name: "disk always", cfg: blockDevicesMatchConfig{}, devType: "disk", want: true},
|
||||
{name: "part without flag", cfg: blockDevicesMatchConfig{}, devType: "part", want: false},
|
||||
{name: "part with flag", cfg: blockDevicesMatchConfig{includeUnmountedPartitions: true}, devType: "part", want: true},
|
||||
{name: "loop without additional", cfg: blockDevicesMatchConfig{}, devType: "loop", want: false},
|
||||
{name: "loop with additional", cfg: blockDevicesMatchConfig{additionalDeviceTypes: []string{"loop"}}, devType: "loop", want: true},
|
||||
{name: "lvm with additional", cfg: blockDevicesMatchConfig{additionalDeviceTypes: []string{"lvm"}}, devType: "lvm", want: true},
|
||||
{name: "crypt with additional", cfg: blockDevicesMatchConfig{additionalDeviceTypes: []string{"crypt"}}, devType: "crypt", want: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := isEligibleDeviceType(tt.devType, tt.cfg)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHostBlockDevices_additionalDeviceTypes covers end-to-end analyze behavior for AdditionalDeviceTypes and representative preflights.
|
||||
func TestHostBlockDevices_additionalDeviceTypes(t *testing.T) {
|
||||
const rawStoragePass = "At least one raw block device is available for storage."
|
||||
const rawStorageFail = "No raw block devices found. At least one unformatted, unmounted disk is required for storage. Attach a raw disk and ensure it has no filesystem or mount point."
|
||||
|
||||
rawStorageOutcomes := []*troubleshootv1beta2.Outcome{
|
||||
{Fail: &troubleshootv1beta2.SingleOutcome{When: ".* == 0", Message: rawStorageFail}},
|
||||
{Pass: &troubleshootv1beta2.SingleOutcome{When: ".* >= 1", Message: rawStoragePass}},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
devices []collect.BlockDeviceInfo
|
||||
hostAnalyzer *troubleshootv1beta2.BlockDevicesAnalyze
|
||||
want []*AnalyzeResult
|
||||
}{
|
||||
{
|
||||
name: "preflight-style 10GiB loop0 with includeUnmountedPartitions + additionalDeviceTypes loop",
|
||||
devices: []collect.BlockDeviceInfo{{
|
||||
Name: "loop0", KernelName: "loop0", Type: "loop", Major: 7, Minor: 0,
|
||||
Size: 10737418240, ReadOnly: false, Removable: false,
|
||||
}},
|
||||
hostAnalyzer: &troubleshootv1beta2.BlockDevicesAnalyze{
|
||||
IncludeUnmountedPartitions: true,
|
||||
MinimumAcceptableSize: 10737418240,
|
||||
AdditionalDeviceTypes: []string{"loop"},
|
||||
Outcomes: rawStorageOutcomes,
|
||||
},
|
||||
want: []*AnalyzeResult{{Title: "Block Devices", IsPass: true, Message: rawStoragePass}},
|
||||
},
|
||||
{
|
||||
name: "preflight-style 10GiB LVM with includeUnmountedPartitions + additionalDeviceTypes lvm",
|
||||
devices: []collect.BlockDeviceInfo{{
|
||||
Name: "ceph--vg-lv--osd0", KernelName: "dm-0", Type: "lvm", Major: 252, Minor: 0,
|
||||
Size: 10737418240, ReadOnly: false, Removable: false,
|
||||
}},
|
||||
hostAnalyzer: &troubleshootv1beta2.BlockDevicesAnalyze{
|
||||
IncludeUnmountedPartitions: true,
|
||||
MinimumAcceptableSize: 10737418240,
|
||||
AdditionalDeviceTypes: []string{"lvm"},
|
||||
Outcomes: rawStorageOutcomes,
|
||||
},
|
||||
want: []*AnalyzeResult{{Title: "Block Devices", IsPass: true, Message: rawStoragePass}},
|
||||
},
|
||||
{
|
||||
name: "loop counts with only additionalDeviceTypes (includeUnmountedPartitions false)",
|
||||
devices: []collect.BlockDeviceInfo{{
|
||||
Name: "loop0", KernelName: "loop0", Type: "loop", Major: 7,
|
||||
}},
|
||||
hostAnalyzer: &troubleshootv1beta2.BlockDevicesAnalyze{
|
||||
AdditionalDeviceTypes: []string{"loop"},
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{Pass: &troubleshootv1beta2.SingleOutcome{When: ".* > 0", Message: "Block device available"}},
|
||||
{Fail: &troubleshootv1beta2.SingleOutcome{Message: "No block device available"}},
|
||||
},
|
||||
},
|
||||
want: []*AnalyzeResult{{Title: "Block Devices", IsPass: true, Message: "Block device available"}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := analyzeHostBlockDevicesOutput(t, tt.devices, tt.hostAnalyzer)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -256,23 +256,22 @@ func TestAnalyzeBlockDevices(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
b, err := json.Marshal(test.devices)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
getCollectedFileContents := func(filename string) ([]byte, error) {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
result, err := (&AnalyzeHostBlockDevices{test.hostAnalyzer}).Analyze(getCollectedFileContents, nil)
|
||||
result, err := analyzeHostBlockDevicesOutput(t, test.devices, test.hostAnalyzer)
|
||||
if test.expectErr {
|
||||
req.Error(err)
|
||||
} else {
|
||||
req.NoError(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, test.result, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// analyzeHostBlockDevicesOutput runs the host block device analyzer on marshaled fixture data (shared by match tests).
|
||||
func analyzeHostBlockDevicesOutput(t *testing.T, devices []collect.BlockDeviceInfo, hostAnalyzer *troubleshootv1beta2.BlockDevicesAnalyze) ([]*AnalyzeResult, error) {
|
||||
t.Helper()
|
||||
b, err := json.Marshal(devices)
|
||||
require.NoError(t, err)
|
||||
getCollectedFileContents := func(string) ([]byte, error) { return b, nil }
|
||||
return (&AnalyzeHostBlockDevices{hostAnalyzer}).Analyze(getCollectedFileContents, nil)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/internal/util"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// RegistryImagesSummary is passed as template data when rendering outcome messages.
|
||||
// Fields are exported so Go templates can reference them.
|
||||
//
|
||||
// - Verified: images confirmed to exist in the registry.
|
||||
// - Missing: images confirmed not to exist in the registry.
|
||||
// - Errors: images that could not be checked (parse failures, timeouts, auth errors, etc).
|
||||
// - UnverifiedReasons: map of image name to reason string for every unverified image
|
||||
// (union of Missing and Errors).
|
||||
//
|
||||
// The `when` conditions follow the existing registry images analyzer nomenclature:
|
||||
// "verified", "missing", and "errors" (see https://troubleshoot.sh/docs/analyze/registry-images).
|
||||
type RegistryImagesSummary struct {
|
||||
Verified []string
|
||||
Missing []string
|
||||
Errors []string
|
||||
UnverifiedReasons map[string]string
|
||||
}
|
||||
|
||||
type AnalyzeHostRegistryImages struct {
|
||||
hostAnalyzer *troubleshootv1beta2.HostRegistryImagesAnalyze
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostRegistryImages) Title() string {
|
||||
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "Registry Images")
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostRegistryImages) IsExcluded() (bool, error) {
|
||||
return isExcluded(a.hostAnalyzer.Exclude)
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostRegistryImages) Analyze(
|
||||
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
|
||||
) ([]*AnalyzeResult, error) {
|
||||
collectorName := a.hostAnalyzer.CollectorName
|
||||
if collectorName == "" {
|
||||
collectorName = "images"
|
||||
}
|
||||
|
||||
const nodeBaseDir = "host-collectors/registry-images"
|
||||
localPath := fmt.Sprintf("%s/%s.json", nodeBaseDir, collectorName)
|
||||
fileName := fmt.Sprintf("%s.json", collectorName)
|
||||
|
||||
collectedContents, err := retrieveCollectedContents(
|
||||
getCollectedFileContents,
|
||||
localPath,
|
||||
nodeBaseDir,
|
||||
fileName,
|
||||
)
|
||||
if err != nil {
|
||||
return []*AnalyzeResult{{Title: a.Title()}}, err
|
||||
}
|
||||
|
||||
var results []*AnalyzeResult
|
||||
for _, content := range collectedContents {
|
||||
currentTitle := a.Title()
|
||||
if content.NodeName != "" {
|
||||
currentTitle = fmt.Sprintf("%s - Node %s", a.Title(), content.NodeName)
|
||||
}
|
||||
|
||||
result, err := a.evaluateOutcomesWithTemplate(content.Data, currentTitle)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to analyze host registry images")
|
||||
}
|
||||
if result != nil {
|
||||
klog.V(2).Infof("registry images analysis result: title=%q pass=%t warn=%t fail=%t message=%q",
|
||||
result.Title, result.IsPass, result.IsWarn, result.IsFail, result.Message)
|
||||
results = append(results, result)
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostRegistryImages) evaluateOutcomesWithTemplate(data []byte, title string) (*AnalyzeResult, error) {
|
||||
summary, err := buildRegistryImagesSummary(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, outcome := range a.hostAnalyzer.Outcomes {
|
||||
result := &AnalyzeResult{Title: title}
|
||||
|
||||
switch {
|
||||
case outcome.Fail != nil:
|
||||
if outcome.Fail.When == "" {
|
||||
result.IsFail = true
|
||||
result.Message = renderRegistryMessage(outcome.Fail.Message, summary)
|
||||
result.URI = outcome.Fail.URI
|
||||
return result, nil
|
||||
}
|
||||
isMatch, err := compareRegistryConditionalToActual(outcome.Fail.When, len(summary.Verified), len(summary.Missing), len(summary.Errors))
|
||||
if err != nil {
|
||||
return result, errors.Wrapf(err, "failed to compare %s", outcome.Fail.When)
|
||||
}
|
||||
if isMatch {
|
||||
result.IsFail = true
|
||||
result.Message = renderRegistryMessage(outcome.Fail.Message, summary)
|
||||
result.URI = outcome.Fail.URI
|
||||
return result, nil
|
||||
}
|
||||
|
||||
case outcome.Warn != nil:
|
||||
if outcome.Warn.When == "" {
|
||||
result.IsWarn = true
|
||||
result.Message = renderRegistryMessage(outcome.Warn.Message, summary)
|
||||
result.URI = outcome.Warn.URI
|
||||
return result, nil
|
||||
}
|
||||
isMatch, err := compareRegistryConditionalToActual(outcome.Warn.When, len(summary.Verified), len(summary.Missing), len(summary.Errors))
|
||||
if err != nil {
|
||||
return result, errors.Wrapf(err, "failed to compare %s", outcome.Warn.When)
|
||||
}
|
||||
if isMatch {
|
||||
result.IsWarn = true
|
||||
result.Message = renderRegistryMessage(outcome.Warn.Message, summary)
|
||||
result.URI = outcome.Warn.URI
|
||||
return result, nil
|
||||
}
|
||||
|
||||
case outcome.Pass != nil:
|
||||
if outcome.Pass.When == "" {
|
||||
result.IsPass = true
|
||||
result.Message = renderRegistryMessage(outcome.Pass.Message, summary)
|
||||
result.URI = outcome.Pass.URI
|
||||
return result, nil
|
||||
}
|
||||
isMatch, err := compareRegistryConditionalToActual(outcome.Pass.When, len(summary.Verified), len(summary.Missing), len(summary.Errors))
|
||||
if err != nil {
|
||||
return result, errors.Wrapf(err, "failed to compare %s", outcome.Pass.When)
|
||||
}
|
||||
if isMatch {
|
||||
result.IsPass = true
|
||||
result.Message = renderRegistryMessage(outcome.Pass.Message, summary)
|
||||
result.URI = outcome.Pass.URI
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func buildRegistryImagesSummary(data []byte) (*RegistryImagesSummary, error) {
|
||||
var registryInfo collect.RegistryInfo
|
||||
if err := json.Unmarshal(data, ®istryInfo); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal registry info")
|
||||
}
|
||||
|
||||
summary := &RegistryImagesSummary{
|
||||
UnverifiedReasons: map[string]string{},
|
||||
}
|
||||
for image, info := range registryInfo.Images {
|
||||
if info.Error != "" {
|
||||
summary.Errors = append(summary.Errors, image)
|
||||
summary.UnverifiedReasons[image] = info.Error
|
||||
} else if !info.Exists {
|
||||
summary.Missing = append(summary.Missing, image)
|
||||
summary.UnverifiedReasons[image] = "image not found in registry"
|
||||
} else {
|
||||
summary.Verified = append(summary.Verified, image)
|
||||
}
|
||||
}
|
||||
slices.Sort(summary.Verified)
|
||||
slices.Sort(summary.Missing)
|
||||
slices.Sort(summary.Errors)
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
func renderRegistryMessage(message string, summary *RegistryImagesSummary) string {
|
||||
rendered, err := util.RenderTemplate(message, summary)
|
||||
if err != nil {
|
||||
klog.V(2).Infof("Failed to render registry message template: %v", err)
|
||||
return message
|
||||
}
|
||||
return rendered
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostRegistryImages) CheckCondition(when string, data []byte) (bool, error) {
|
||||
summary, err := buildRegistryImagesSummary(data)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return compareRegistryConditionalToActual(when, len(summary.Verified), len(summary.Missing), len(summary.Errors))
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
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"
|
||||
)
|
||||
|
||||
func TestAnalyzeHostRegistryImagesCheckCondition(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
conditional string
|
||||
data collect.RegistryInfo
|
||||
expected bool
|
||||
expectErr string
|
||||
}{
|
||||
{
|
||||
name: "all images found",
|
||||
conditional: "missing == 0",
|
||||
data: collect.RegistryInfo{
|
||||
Images: map[string]collect.RegistryImage{
|
||||
"registry.example.com/app:v1": {Exists: true},
|
||||
"registry.example.com/app:v2": {Exists: true},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "some images not found",
|
||||
conditional: "missing > 0",
|
||||
data: collect.RegistryInfo{
|
||||
Images: map[string]collect.RegistryImage{
|
||||
"registry.example.com/app:v1": {Exists: true},
|
||||
"registry.example.com/app:v2": {Exists: false},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "verified count matches found",
|
||||
conditional: "verified == 2",
|
||||
data: collect.RegistryInfo{
|
||||
Images: map[string]collect.RegistryImage{
|
||||
"registry.example.com/app:v1": {Exists: true},
|
||||
"registry.example.com/app:v2": {Exists: true},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "errored images counted under errors",
|
||||
conditional: "errors > 0",
|
||||
data: collect.RegistryInfo{
|
||||
Images: map[string]collect.RegistryImage{
|
||||
"registry.example.com/app:v1": {Error: "connection refused"},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "no errors when all found",
|
||||
conditional: "missing == 0",
|
||||
data: collect.RegistryInfo{
|
||||
Images: map[string]collect.RegistryImage{
|
||||
"registry.example.com/app:v1": {Exists: true},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "mixed results - missing and errors counted separately",
|
||||
conditional: "missing == 1",
|
||||
data: collect.RegistryInfo{
|
||||
Images: map[string]collect.RegistryImage{
|
||||
"registry.example.com/app:v1": {Exists: true},
|
||||
"registry.example.com/app:v2": {Exists: false},
|
||||
"registry.example.com/app:v3": {Error: "timeout"},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "invalid conditional format",
|
||||
conditional: "missing",
|
||||
data: collect.RegistryInfo{
|
||||
Images: map[string]collect.RegistryImage{},
|
||||
},
|
||||
expected: false,
|
||||
expectErr: "unable to parse conditional",
|
||||
},
|
||||
{
|
||||
name: "unmarshal error",
|
||||
conditional: "missing == 0",
|
||||
expected: false,
|
||||
expectErr: "failed to unmarshal registry info",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
a := &AnalyzeHostRegistryImages{}
|
||||
|
||||
var data []byte
|
||||
if test.expectErr == "failed to unmarshal registry info" {
|
||||
data = []byte(`{not valid json}`)
|
||||
} else {
|
||||
var err error
|
||||
data, err = json.Marshal(test.data)
|
||||
req.NoError(err)
|
||||
}
|
||||
|
||||
result, err := a.CheckCondition(test.conditional, data)
|
||||
if test.expectErr != "" {
|
||||
req.ErrorContains(err, test.expectErr)
|
||||
} else {
|
||||
req.NoError(err)
|
||||
}
|
||||
assert.Equal(t, test.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyzeHostRegistryImages(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hostAnalyzer *troubleshootv1beta2.HostRegistryImagesAnalyze
|
||||
getCollectedFileContents func(string) ([]byte, error)
|
||||
expectedResults []*AnalyzeResult
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "pass when all images found",
|
||||
hostAnalyzer: &troubleshootv1beta2.HostRegistryImagesAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "missing == 0",
|
||||
Message: "All images are available",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
if path == "host-collectors/registry-images/images.json" {
|
||||
return json.Marshal(collect.RegistryInfo{
|
||||
Images: map[string]collect.RegistryImage{
|
||||
"registry.example.com/app:v1": {Exists: true},
|
||||
},
|
||||
})
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
expectedResults: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Registry Images",
|
||||
IsPass: true,
|
||||
Message: "All images are available",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fail when images not found",
|
||||
hostAnalyzer: &troubleshootv1beta2.HostRegistryImagesAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "missing > 0",
|
||||
Message: "Some images are not available",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
if path == "host-collectors/registry-images/images.json" {
|
||||
return json.Marshal(collect.RegistryInfo{
|
||||
Images: map[string]collect.RegistryImage{
|
||||
"registry.example.com/app:v1": {Exists: false},
|
||||
},
|
||||
})
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
expectedResults: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Registry Images",
|
||||
IsFail: true,
|
||||
Message: "Some images are not available",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "errored images matched by errors condition",
|
||||
hostAnalyzer: &troubleshootv1beta2.HostRegistryImagesAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "errors > 0",
|
||||
Message: "Some images are not available",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
if path == "host-collectors/registry-images/images.json" {
|
||||
return json.Marshal(collect.RegistryInfo{
|
||||
Images: map[string]collect.RegistryImage{
|
||||
"registry.example.com/app:v1": {Error: "connection refused"},
|
||||
},
|
||||
})
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
expectedResults: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Registry Images",
|
||||
IsFail: true,
|
||||
Message: "Some images are not available",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "custom collector name used in path",
|
||||
hostAnalyzer: &troubleshootv1beta2.HostRegistryImagesAnalyze{
|
||||
CollectorName: "my-registry",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "missing == 0",
|
||||
Message: "All images are available",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
if path == "host-collectors/registry-images/my-registry.json" {
|
||||
return json.Marshal(collect.RegistryInfo{
|
||||
Images: map[string]collect.RegistryImage{
|
||||
"registry.example.com/app:v1": {Exists: true},
|
||||
},
|
||||
})
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
expectedResults: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Registry Images",
|
||||
IsPass: true,
|
||||
Message: "All images are available",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "return error when collection data missing",
|
||||
hostAnalyzer: &troubleshootv1beta2.HostRegistryImagesAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "missing == 0",
|
||||
Message: "All images are available",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
expectedResults: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Registry Images",
|
||||
},
|
||||
},
|
||||
expectedError: "file not found",
|
||||
},
|
||||
{
|
||||
name: "template rendering with NotFound list",
|
||||
hostAnalyzer: &troubleshootv1beta2.HostRegistryImagesAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "missing > 0",
|
||||
Message: "Missing: {{ .Missing | join \", \" }}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
if path == "host-collectors/registry-images/images.json" {
|
||||
return json.Marshal(collect.RegistryInfo{
|
||||
Images: map[string]collect.RegistryImage{
|
||||
"registry.example.com/app:v1": {Exists: false},
|
||||
"registry.example.com/app:v2": {Exists: true},
|
||||
},
|
||||
})
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
expectedResults: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Registry Images",
|
||||
IsFail: true,
|
||||
Message: "Missing: registry.example.com/app:v1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "template rendering with NotFoundReasons map",
|
||||
hostAnalyzer: &troubleshootv1beta2.HostRegistryImagesAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "errors > 0",
|
||||
Message: `{{ range $image, $reason := .UnverifiedReasons }}{{ $image }}: {{ $reason }}; {{ end }}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
if path == "host-collectors/registry-images/images.json" {
|
||||
return json.Marshal(collect.RegistryInfo{
|
||||
Images: map[string]collect.RegistryImage{
|
||||
"registry.example.com/app:v1": {Error: "connection refused"},
|
||||
},
|
||||
})
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
expectedResults: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Registry Images",
|
||||
IsFail: true,
|
||||
Message: "registry.example.com/app:v1: connection refused; ",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "template rendering with Found count",
|
||||
hostAnalyzer: &troubleshootv1beta2.HostRegistryImagesAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "missing == 0",
|
||||
Message: "All {{ len .Verified }} images are available",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
if path == "host-collectors/registry-images/images.json" {
|
||||
return json.Marshal(collect.RegistryInfo{
|
||||
Images: map[string]collect.RegistryImage{
|
||||
"registry.example.com/app:v1": {Exists: true},
|
||||
"registry.example.com/app:v2": {Exists: true},
|
||||
},
|
||||
})
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
expectedResults: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Registry Images",
|
||||
IsPass: true,
|
||||
Message: "All 2 images are available",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
a := &AnalyzeHostRegistryImages{
|
||||
hostAnalyzer: test.hostAnalyzer,
|
||||
}
|
||||
|
||||
results, err := a.Analyze(test.getCollectedFileContents, nil)
|
||||
|
||||
if test.expectedError != "" {
|
||||
req.ErrorContains(err, test.expectedError)
|
||||
} else {
|
||||
req.NoError(err)
|
||||
}
|
||||
req.Equal(test.expectedResults, results)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyzeHostRegistryImagesTitle(t *testing.T) {
|
||||
t.Run("default title", func(t *testing.T) {
|
||||
a := &AnalyzeHostRegistryImages{
|
||||
hostAnalyzer: &troubleshootv1beta2.HostRegistryImagesAnalyze{},
|
||||
}
|
||||
assert.Equal(t, "Registry Images", a.Title())
|
||||
})
|
||||
|
||||
t.Run("custom title", func(t *testing.T) {
|
||||
a := &AnalyzeHostRegistryImages{
|
||||
hostAnalyzer: &troubleshootv1beta2.HostRegistryImagesAnalyze{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "My Registry Check",
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, "My Registry Check", a.Title())
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
)
|
||||
|
||||
type AnalyzeIngressClass struct {
|
||||
analyzer *troubleshootv1beta2.IngressClass
|
||||
}
|
||||
|
||||
func (a *AnalyzeIngressClass) Title() string {
|
||||
title := a.analyzer.CheckName
|
||||
if title == "" {
|
||||
if a.analyzer.IngressClassName != "" {
|
||||
title = fmt.Sprintf("Ingress class %s", a.analyzer.IngressClassName)
|
||||
} else {
|
||||
title = "Default Ingress Class"
|
||||
}
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
func (a *AnalyzeIngressClass) IsExcluded() (bool, error) {
|
||||
return isExcluded(a.analyzer.Exclude)
|
||||
}
|
||||
|
||||
func (a *AnalyzeIngressClass) Analyze(getFile getCollectedFileContents, findFiles getChildCollectedFileContents) ([]*AnalyzeResult, error) {
|
||||
result, err := a.analyzeIngressClass(a.analyzer, getFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Strict = a.analyzer.Strict.BoolOrDefaultFalse()
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
func (a *AnalyzeIngressClass) analyzeIngressClass(analyzer *troubleshootv1beta2.IngressClass, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
|
||||
ingressClassesData, err := getCollectedFileContents(fmt.Sprintf("%s/%s.json", constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_INGRESS_CLASS))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ingressClasses networkingv1.IngressClassList
|
||||
if err := json.Unmarshal(ingressClassesData, &ingressClasses); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := AnalyzeResult{
|
||||
Title: a.Title(),
|
||||
IconKey: "kubernetes_ingress_class",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/ingress-class.svg?w=12&h=12",
|
||||
}
|
||||
|
||||
for _, ingressClass := range ingressClasses.Items {
|
||||
val := ingressClass.Annotations["ingressclass.kubernetes.io/is-default-class"]
|
||||
if (ingressClass.Name == analyzer.IngressClassName) || (analyzer.IngressClassName == "" && val == "true") {
|
||||
result.IsPass = true
|
||||
for _, outcome := range analyzer.Outcomes {
|
||||
if outcome.Pass != nil {
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
}
|
||||
}
|
||||
if analyzer.IngressClassName == "" && result.Message == "" {
|
||||
result.Message = "Default Ingress Class found"
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
}
|
||||
|
||||
result.IsFail = true
|
||||
for _, outcome := range analyzer.Outcomes {
|
||||
if outcome.Fail != nil {
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
}
|
||||
}
|
||||
if analyzer.IngressClassName == "" && result.Message == "" {
|
||||
result.Message = "No Default Ingress Class found"
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestAnalyzeIngressClass(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
analyzer *troubleshootv1beta2.IngressClass
|
||||
ingressList *networkingv1.IngressClassList
|
||||
expectResult AnalyzeResult
|
||||
}{
|
||||
{
|
||||
name: "named ingress class found",
|
||||
analyzer: &troubleshootv1beta2.IngressClass{
|
||||
IngressClassName: "nginx",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "nginx ingress class found",
|
||||
},
|
||||
},
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "nginx ingress class not found",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ingressList: &networkingv1.IngressClassList{
|
||||
Items: []networkingv1.IngressClass{
|
||||
{ObjectMeta: metav1.ObjectMeta{Name: "nginx"}},
|
||||
},
|
||||
},
|
||||
expectResult: AnalyzeResult{
|
||||
IsPass: true,
|
||||
Title: "Ingress class nginx",
|
||||
Message: "nginx ingress class found",
|
||||
IconKey: "kubernetes_ingress_class",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/ingress-class.svg?w=12&h=12",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "named ingress class not found",
|
||||
analyzer: &troubleshootv1beta2.IngressClass{
|
||||
IngressClassName: "nginx",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "nginx ingress class found",
|
||||
},
|
||||
},
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "nginx ingress class not found",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ingressList: &networkingv1.IngressClassList{
|
||||
Items: []networkingv1.IngressClass{
|
||||
{ObjectMeta: metav1.ObjectMeta{Name: "traefik"}},
|
||||
},
|
||||
},
|
||||
expectResult: AnalyzeResult{
|
||||
IsFail: true,
|
||||
Title: "Ingress class nginx",
|
||||
Message: "nginx ingress class not found",
|
||||
IconKey: "kubernetes_ingress_class",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/ingress-class.svg?w=12&h=12",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "default ingress class found",
|
||||
analyzer: &troubleshootv1beta2.IngressClass{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "default ingress class exists",
|
||||
},
|
||||
},
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "no default ingress class",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ingressList: &networkingv1.IngressClassList{
|
||||
Items: []networkingv1.IngressClass{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "nginx",
|
||||
Annotations: map[string]string{
|
||||
"ingressclass.kubernetes.io/is-default-class": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectResult: AnalyzeResult{
|
||||
IsPass: true,
|
||||
Title: "Default Ingress Class",
|
||||
Message: "default ingress class exists",
|
||||
IconKey: "kubernetes_ingress_class",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/ingress-class.svg?w=12&h=12",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "default ingress class not found",
|
||||
analyzer: &troubleshootv1beta2.IngressClass{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "default ingress class exists",
|
||||
},
|
||||
},
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "no default ingress class",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ingressList: &networkingv1.IngressClassList{
|
||||
Items: []networkingv1.IngressClass{
|
||||
{ObjectMeta: metav1.ObjectMeta{Name: "nginx"}},
|
||||
},
|
||||
},
|
||||
expectResult: AnalyzeResult{
|
||||
IsFail: true,
|
||||
Title: "Default Ingress Class",
|
||||
Message: "no default ingress class",
|
||||
IconKey: "kubernetes_ingress_class",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/ingress-class.svg?w=12&h=12",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "default ingress class not found with default message",
|
||||
analyzer: &troubleshootv1beta2.IngressClass{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{},
|
||||
},
|
||||
ingressList: &networkingv1.IngressClassList{},
|
||||
expectResult: AnalyzeResult{
|
||||
IsFail: true,
|
||||
Title: "Default Ingress Class",
|
||||
Message: "No Default Ingress Class found",
|
||||
IconKey: "kubernetes_ingress_class",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/ingress-class.svg?w=12&h=12",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "default ingress class found with default message",
|
||||
analyzer: &troubleshootv1beta2.IngressClass{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{},
|
||||
},
|
||||
ingressList: &networkingv1.IngressClassList{
|
||||
Items: []networkingv1.IngressClass{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "nginx",
|
||||
Annotations: map[string]string{
|
||||
"ingressclass.kubernetes.io/is-default-class": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectResult: AnalyzeResult{
|
||||
IsPass: true,
|
||||
Title: "Default Ingress Class",
|
||||
Message: "Default Ingress Class found",
|
||||
IconKey: "kubernetes_ingress_class",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/ingress-class.svg?w=12&h=12",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
b, err := json.Marshal(tt.ingressList)
|
||||
require.NoError(t, err)
|
||||
|
||||
getFile := func(_ string) ([]byte, error) {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
a := AnalyzeIngressClass{analyzer: tt.analyzer}
|
||||
result, err := a.analyzeIngressClass(tt.analyzer, getFile)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expectResult, *result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -17,24 +17,26 @@ import (
|
||||
)
|
||||
|
||||
var Filemap = map[string]string{
|
||||
"deployment": constants.CLUSTER_RESOURCES_DEPLOYMENTS,
|
||||
"daemonset": constants.CLUSTER_RESOURCES_DAEMONSETS,
|
||||
"statefulset": constants.CLUSTER_RESOURCES_STATEFULSETS,
|
||||
"networkpolicy": constants.CLUSTER_RESOURCES_NETWORK_POLICY,
|
||||
"pod": constants.CLUSTER_RESOURCES_PODS,
|
||||
"ingress": constants.CLUSTER_RESOURCES_INGRESS,
|
||||
"service": constants.CLUSTER_RESOURCES_SERVICES,
|
||||
"resourcequota": constants.CLUSTER_RESOURCES_RESOURCE_QUOTA,
|
||||
"job": constants.CLUSTER_RESOURCES_JOBS,
|
||||
"persistentvolumeclaim": constants.CLUSTER_RESOURCES_PVCS,
|
||||
"pvc": constants.CLUSTER_RESOURCES_PVCS,
|
||||
"replicaset": constants.CLUSTER_RESOURCES_REPLICASETS,
|
||||
"configmap": constants.CLUSTER_RESOURCES_CONFIGMAPS,
|
||||
"namespace": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_NAMESPACES),
|
||||
"persistentvolume": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_PVS),
|
||||
"pv": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_PVS),
|
||||
"node": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_NODES),
|
||||
"storageclass": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_STORAGE_CLASS),
|
||||
"deployment": constants.CLUSTER_RESOURCES_DEPLOYMENTS,
|
||||
"daemonset": constants.CLUSTER_RESOURCES_DAEMONSETS,
|
||||
"statefulset": constants.CLUSTER_RESOURCES_STATEFULSETS,
|
||||
"networkpolicy": constants.CLUSTER_RESOURCES_NETWORK_POLICY,
|
||||
"pod": constants.CLUSTER_RESOURCES_PODS,
|
||||
"ingress": constants.CLUSTER_RESOURCES_INGRESS,
|
||||
"service": constants.CLUSTER_RESOURCES_SERVICES,
|
||||
"resourcequota": constants.CLUSTER_RESOURCES_RESOURCE_QUOTA,
|
||||
"job": constants.CLUSTER_RESOURCES_JOBS,
|
||||
"persistentvolumeclaim": constants.CLUSTER_RESOURCES_PVCS,
|
||||
"pvc": constants.CLUSTER_RESOURCES_PVCS,
|
||||
"replicaset": constants.CLUSTER_RESOURCES_REPLICASETS,
|
||||
"configmap": constants.CLUSTER_RESOURCES_CONFIGMAPS,
|
||||
"validatingwebhookconfiguration": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_VALIDATING_WEBHOOK_CONFIGURATIONS),
|
||||
"mutatingwebhookconfiguration": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_MUTATING_WEBHOOK_CONFIGURATIONS),
|
||||
"namespace": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_NAMESPACES),
|
||||
"persistentvolume": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_PVS),
|
||||
"pv": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_PVS),
|
||||
"node": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_NODES),
|
||||
"storageclass": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_STORAGE_CLASS),
|
||||
}
|
||||
|
||||
type AnalyzeClusterResource struct {
|
||||
|
||||
@@ -12,11 +12,12 @@ 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"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/types"
|
||||
)
|
||||
|
||||
type AnalyzeNodeResources struct {
|
||||
@@ -45,6 +46,9 @@ func (a *AnalyzeNodeResources) Analyze(getFile getCollectedFileContents, findFil
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result == nil {
|
||||
return nil, nil
|
||||
}
|
||||
result.Strict = a.analyzer.Strict.BoolOrDefaultFalse()
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
@@ -53,7 +57,19 @@ func (a *AnalyzeNodeResources) analyzeNodeResources(analyzer *troubleshootv1beta
|
||||
|
||||
collected, err := getCollectedFileContents(fmt.Sprintf("%s/%s.json", constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_NODES))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get contents of nodes.json")
|
||||
if _, ok := err.(*types.NotFoundError); !ok {
|
||||
return nil, errors.Wrap(err, "failed to get contents of nodes.json")
|
||||
}
|
||||
if analyzer.IgnoreIfNoFiles {
|
||||
return nil, nil
|
||||
}
|
||||
return &AnalyzeResult{
|
||||
Title: a.Title(),
|
||||
IconKey: "kubernetes_node_resources",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
|
||||
IsWarn: true,
|
||||
Message: "No node resources were collected, unable to analyze node resources",
|
||||
}, nil
|
||||
}
|
||||
|
||||
var nodes corev1.NodeList
|
||||
@@ -453,7 +469,7 @@ func nodeMatchesFilters(node corev1.Node, filters *troubleshootv1beta2.NodeResou
|
||||
}
|
||||
|
||||
if filters.Taint != nil {
|
||||
return taints.TaintExists(node.Spec.Taints, filters.Taint), nil
|
||||
return k8sutil.TaintExists(node.Spec.Taints, filters.Taint), nil
|
||||
}
|
||||
|
||||
if filters.CPUArchitecture != "" {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/types"
|
||||
)
|
||||
|
||||
func Test_compareNodeResourceConditionalToActual(t *testing.T) {
|
||||
@@ -1684,3 +1686,56 @@ func Test_analyzeNodeResources(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_analyzeNodeResources_NoFiles(t *testing.T) {
|
||||
missingFile := func(name string) ([]byte, error) {
|
||||
return nil, &types.NotFoundError{Name: name}
|
||||
}
|
||||
|
||||
t.Run("emits warning when nodes.json is not collected", func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
analyzer := &troubleshootv1beta2.NodeResources{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{Pass: &troubleshootv1beta2.SingleOutcome{Message: "ok"}},
|
||||
},
|
||||
}
|
||||
a := AnalyzeNodeResources{analyzer: analyzer}
|
||||
got, err := a.Analyze(missingFile, nil)
|
||||
req.NoError(err)
|
||||
req.Len(got, 1)
|
||||
req.True(got[0].IsWarn)
|
||||
req.Equal("Node Resources", got[0].Title)
|
||||
})
|
||||
|
||||
t.Run("ignoreIfNoFiles suppresses the warning", func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
analyzer := &troubleshootv1beta2.NodeResources{
|
||||
IgnoreIfNoFiles: true,
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{Pass: &troubleshootv1beta2.SingleOutcome{Message: "ok"}},
|
||||
},
|
||||
}
|
||||
a := AnalyzeNodeResources{analyzer: analyzer}
|
||||
got, err := a.Analyze(missingFile, nil)
|
||||
req.NoError(err)
|
||||
req.Empty(got)
|
||||
})
|
||||
|
||||
t.Run("non-NotFound errors are propagated", func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
analyzer := &troubleshootv1beta2.NodeResources{
|
||||
IgnoreIfNoFiles: true,
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{Pass: &troubleshootv1beta2.SingleOutcome{Message: "ok"}},
|
||||
},
|
||||
}
|
||||
ioErr := func(string) ([]byte, error) {
|
||||
return nil, fmt.Errorf("permission denied")
|
||||
}
|
||||
a := AnalyzeNodeResources{analyzer: analyzer}
|
||||
got, err := a.Analyze(ioErr, nil)
|
||||
req.Error(err)
|
||||
req.Nil(got)
|
||||
req.Contains(err.Error(), "permission denied")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
)
|
||||
|
||||
type AnalyzeS3Status struct {
|
||||
analyzer *troubleshootv1beta2.DatabaseAnalyze
|
||||
}
|
||||
|
||||
func (a *AnalyzeS3Status) Title() string {
|
||||
title := a.analyzer.CheckName
|
||||
if title == "" {
|
||||
title = a.collectorName()
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
func (a *AnalyzeS3Status) IsExcluded() (bool, error) {
|
||||
return isExcluded(a.analyzer.Exclude)
|
||||
}
|
||||
|
||||
func (a *AnalyzeS3Status) Analyze(getFile getCollectedFileContents, findFiles getChildCollectedFileContents) ([]*AnalyzeResult, error) {
|
||||
result, err := a.analyzeS3Status(a.analyzer, getFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Strict = a.analyzer.Strict.BoolOrDefaultFalse()
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
func (a *AnalyzeS3Status) collectorName() string {
|
||||
collectorName := a.analyzer.CollectorName
|
||||
if collectorName == "" {
|
||||
collectorName = "s3Status"
|
||||
}
|
||||
|
||||
return collectorName
|
||||
}
|
||||
|
||||
func (a *AnalyzeS3Status) analyzeS3Status(analyzer *troubleshootv1beta2.DatabaseAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
|
||||
fullPath := path.Join("s3Status", fmt.Sprintf("%s.json", a.collectorName()))
|
||||
|
||||
collected, err := getCollectedFileContents(fullPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to read collected file name: %s", fullPath)
|
||||
}
|
||||
|
||||
databaseConnection := collect.DatabaseConnection{}
|
||||
if err := json.Unmarshal(collected, &databaseConnection); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal s3 status result")
|
||||
}
|
||||
|
||||
result := &AnalyzeResult{
|
||||
Title: a.Title(),
|
||||
IconKey: "kubernetes_s3_analyze",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/s3-analyze.svg",
|
||||
}
|
||||
|
||||
for _, outcome := range analyzer.Outcomes {
|
||||
if outcome.Fail != nil {
|
||||
if outcome.Fail.When == "" {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareDatabaseConditionalToActual(outcome.Fail.When, &databaseConnection)
|
||||
if err != nil {
|
||||
return result, errors.Wrap(err, "failed to compare s3 status conditional")
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsFail = true
|
||||
if databaseConnection.Error != "" {
|
||||
result.Message = outcome.Fail.Message + " " + databaseConnection.Error
|
||||
} else {
|
||||
result.Message = outcome.Fail.Message
|
||||
}
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
return result, nil
|
||||
}
|
||||
} else if outcome.Warn != nil {
|
||||
if outcome.Warn.When == "" {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareDatabaseConditionalToActual(outcome.Warn.When, &databaseConnection)
|
||||
if err != nil {
|
||||
return result, errors.Wrap(err, "failed to compare s3 status conditional")
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
return result, nil
|
||||
}
|
||||
} else if outcome.Pass != nil {
|
||||
if outcome.Pass.When == "" {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareDatabaseConditionalToActual(outcome.Pass.When, &databaseConnection)
|
||||
if err != nil {
|
||||
return result, errors.Wrap(err, "failed to compare s3 status conditional")
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"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"
|
||||
)
|
||||
|
||||
func TestAnalyzeS3Status(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
analyzer *troubleshootv1beta2.DatabaseAnalyze
|
||||
collected *collect.DatabaseConnection
|
||||
wantPass bool
|
||||
wantFail bool
|
||||
wantWarn bool
|
||||
wantMessage string
|
||||
}{
|
||||
{
|
||||
name: "connected, pass",
|
||||
analyzer: &troubleshootv1beta2.DatabaseAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "connected == false",
|
||||
Message: "Cannot access the S3 bucket.",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "connected == true",
|
||||
Message: "S3 bucket is accessible.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
collected: &collect.DatabaseConnection{
|
||||
IsConnected: true,
|
||||
},
|
||||
wantPass: true,
|
||||
wantMessage: "S3 bucket is accessible.",
|
||||
},
|
||||
{
|
||||
name: "not connected, fail with error appended",
|
||||
analyzer: &troubleshootv1beta2.DatabaseAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "connected == false",
|
||||
Message: "Cannot access the S3 bucket.",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "connected == true",
|
||||
Message: "S3 bucket is accessible.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
collected: &collect.DatabaseConnection{
|
||||
IsConnected: false,
|
||||
Error: "operation error S3: HeadBucket, StatusCode: 403",
|
||||
},
|
||||
wantFail: true,
|
||||
wantMessage: "Cannot access the S3 bucket. operation error S3: HeadBucket, StatusCode: 403",
|
||||
},
|
||||
{
|
||||
name: "not connected, fail without error",
|
||||
analyzer: &troubleshootv1beta2.DatabaseAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "connected == false",
|
||||
Message: "Cannot access the S3 bucket.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
collected: &collect.DatabaseConnection{
|
||||
IsConnected: false,
|
||||
},
|
||||
wantFail: true,
|
||||
wantMessage: "Cannot access the S3 bucket.",
|
||||
},
|
||||
{
|
||||
name: "warn outcome",
|
||||
analyzer: &troubleshootv1beta2.DatabaseAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "connected == false",
|
||||
Message: "S3 bucket may be inaccessible.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
collected: &collect.DatabaseConnection{
|
||||
IsConnected: false,
|
||||
},
|
||||
wantWarn: true,
|
||||
wantMessage: "S3 bucket may be inaccessible.",
|
||||
},
|
||||
{
|
||||
name: "unconditional fail",
|
||||
analyzer: &troubleshootv1beta2.DatabaseAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "Always fails.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
collected: &collect.DatabaseConnection{
|
||||
IsConnected: true,
|
||||
},
|
||||
wantFail: true,
|
||||
wantMessage: "Always fails.",
|
||||
},
|
||||
{
|
||||
name: "custom collector name",
|
||||
analyzer: &troubleshootv1beta2.DatabaseAnalyze{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "My S3 Check",
|
||||
},
|
||||
CollectorName: "my-bucket",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "connected == true",
|
||||
Message: "Bucket OK.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
collected: &collect.DatabaseConnection{
|
||||
IsConnected: true,
|
||||
},
|
||||
wantPass: true,
|
||||
wantMessage: "Bucket OK.",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
collectedData, err := json.Marshal(tt.collected)
|
||||
require.NoError(t, err)
|
||||
|
||||
a := &AnalyzeS3Status{analyzer: tt.analyzer}
|
||||
|
||||
getFile := func(path string) ([]byte, error) {
|
||||
return collectedData, nil
|
||||
}
|
||||
|
||||
results, err := a.Analyze(getFile, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 1)
|
||||
|
||||
result := results[0]
|
||||
assert.Equal(t, tt.wantPass, result.IsPass)
|
||||
assert.Equal(t, tt.wantFail, result.IsFail)
|
||||
assert.Equal(t, tt.wantWarn, result.IsWarn)
|
||||
assert.Equal(t, tt.wantMessage, result.Message)
|
||||
|
||||
if tt.analyzer.CheckName != "" {
|
||||
assert.Equal(t, tt.analyzer.CheckName, result.Title)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,12 @@ type StorageClass struct {
|
||||
StorageClassName string `json:"storageClassName,omitempty" yaml:"storageClassName,omitempty"`
|
||||
}
|
||||
|
||||
type IngressClass struct {
|
||||
AnalyzeMeta `json:",inline" yaml:",inline"`
|
||||
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
|
||||
IngressClassName string `json:"ingressClassName,omitempty" yaml:"ingressClassName,omitempty"`
|
||||
}
|
||||
|
||||
type CustomResourceDefinition struct {
|
||||
AnalyzeMeta `json:",inline" yaml:",inline"`
|
||||
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
|
||||
@@ -124,9 +130,10 @@ type Distribution struct {
|
||||
}
|
||||
|
||||
type NodeResources struct {
|
||||
AnalyzeMeta `json:",inline" yaml:",inline"`
|
||||
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
|
||||
Filters *NodeResourceFilters `json:"filters,omitempty" yaml:"filters,omitempty"`
|
||||
AnalyzeMeta `json:",inline" yaml:",inline"`
|
||||
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
|
||||
Filters *NodeResourceFilters `json:"filters,omitempty" yaml:"filters,omitempty"`
|
||||
IgnoreIfNoFiles bool `json:"ignoreIfNoFiles,omitempty" yaml:"ignoreIfNoFiles,omitempty"`
|
||||
}
|
||||
|
||||
type NodeResourceFilters struct {
|
||||
@@ -276,6 +283,7 @@ type PVCRef struct {
|
||||
type Analyze struct {
|
||||
ClusterVersion *ClusterVersion `json:"clusterVersion,omitempty" yaml:"clusterVersion,omitempty"`
|
||||
StorageClass *StorageClass `json:"storageClass,omitempty" yaml:"storageClass,omitempty"`
|
||||
IngressClass *IngressClass `json:"ingressClass,omitempty" yaml:"ingressClass,omitempty"`
|
||||
CustomResourceDefinition *CustomResourceDefinition `json:"customResourceDefinition,omitempty" yaml:"customResourceDefinition,omitempty"`
|
||||
Ingress *Ingress `json:"ingress,omitempty" yaml:"ingress,omitempty"`
|
||||
Secret *AnalyzeSecret `json:"secret,omitempty" yaml:"secret,omitempty"`
|
||||
@@ -309,4 +317,5 @@ type Analyze struct {
|
||||
Event *EventAnalyze `json:"event,omitempty" yaml:"event,omitempty"`
|
||||
NodeMetrics *NodeMetricsAnalyze `json:"nodeMetrics,omitempty" yaml:"nodeMetrics,omitempty"`
|
||||
HTTP *HTTPAnalyze `json:"http,omitempty" yaml:"http,omitempty"`
|
||||
S3Status *DatabaseAnalyze `json:"s3Status,omitempty" yaml:"s3Status,omitempty"`
|
||||
}
|
||||
|
||||
@@ -318,37 +318,55 @@ type Etcd struct {
|
||||
Image string `json:"image" yaml:"image"`
|
||||
}
|
||||
|
||||
type SupportBundleMetadata struct {
|
||||
CollectorMeta `json:",inline" yaml:",inline"`
|
||||
Namespace string `json:"namespace" yaml:"namespace"`
|
||||
}
|
||||
|
||||
type S3Status struct {
|
||||
CollectorMeta `json:",inline" yaml:",inline"`
|
||||
BucketName string `json:"bucketName" yaml:"bucketName"`
|
||||
Region string `json:"region,omitempty" yaml:"region,omitempty"`
|
||||
Endpoint string `json:"endpoint,omitempty" yaml:"endpoint,omitempty"`
|
||||
AccessKeyID string `json:"accessKeyID,omitempty" yaml:"accessKeyID,omitempty"`
|
||||
SecretAccessKey string `json:"secretAccessKey,omitempty" yaml:"secretAccessKey,omitempty"`
|
||||
UsePathStyle bool `json:"usePathStyle,omitempty" yaml:"usePathStyle,omitempty"`
|
||||
Insecure bool `json:"insecure,omitempty" yaml:"insecure,omitempty"`
|
||||
}
|
||||
|
||||
type Collect struct {
|
||||
ClusterInfo *ClusterInfo `json:"clusterInfo,omitempty" yaml:"clusterInfo,omitempty"`
|
||||
ClusterResources *ClusterResources `json:"clusterResources,omitempty" yaml:"clusterResources,omitempty"`
|
||||
Secret *Secret `json:"secret,omitempty" yaml:"secret,omitempty"`
|
||||
CustomMetrics *CustomMetrics `json:"customMetrics,omitempty" yaml:"customMetrics,omitempty"`
|
||||
ConfigMap *ConfigMap `json:"configMap,omitempty" yaml:"configMap,omitempty"`
|
||||
Logs *Logs `json:"logs,omitempty" yaml:"logs,omitempty"`
|
||||
Run *Run `json:"run,omitempty" yaml:"run,omitempty"`
|
||||
RunPod *RunPod `json:"runPod,omitempty" yaml:"runPod,omitempty"`
|
||||
RunDaemonSet *RunDaemonSet `json:"runDaemonSet,omitempty" yaml:"runDaemonSet,omitempty"`
|
||||
Exec *Exec `json:"exec,omitempty" yaml:"exec,omitempty"`
|
||||
Data *Data `json:"data,omitempty" yaml:"data,omitempty"`
|
||||
Copy *Copy `json:"copy,omitempty" yaml:"copy,omitempty"`
|
||||
CopyFromHost *CopyFromHost `json:"copyFromHost,omitempty" yaml:"copyFromHost,omitempty"`
|
||||
HTTP *HTTP `json:"http,omitempty" yaml:"http,omitempty"`
|
||||
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"`
|
||||
Collectd *Collectd `json:"collectd,omitempty" yaml:"collectd,omitempty"`
|
||||
Ceph *Ceph `json:"ceph,omitempty" yaml:"ceph,omitempty"`
|
||||
Longhorn *Longhorn `json:"longhorn,omitempty" yaml:"longhorn,omitempty"`
|
||||
RegistryImages *RegistryImages `json:"registryImages,omitempty" yaml:"registryImages,omitempty"`
|
||||
Sysctl *Sysctl `json:"sysctl,omitempty" yaml:"sysctl,omitempty"`
|
||||
Certificates *Certificates `json:"certificates,omitempty" yaml:"certificates,omitempty"`
|
||||
Helm *Helm `json:"helm,omitempty" yaml:"helm,omitempty"`
|
||||
Goldpinger *Goldpinger `json:"goldpinger,omitempty" yaml:"goldpinger,omitempty"`
|
||||
Sonobuoy *Sonobuoy `json:"sonobuoy,omitempty" yaml:"sonobuoy,omitempty"`
|
||||
NodeMetrics *NodeMetrics `json:"nodeMetrics,omitempty" yaml:"nodeMetrics,omitempty"`
|
||||
DNS *DNS `json:"dns,omitempty" yaml:"dns,omitempty"`
|
||||
Etcd *Etcd `json:"etcd,omitempty" yaml:"etcd,omitempty"`
|
||||
ClusterInfo *ClusterInfo `json:"clusterInfo,omitempty" yaml:"clusterInfo,omitempty"`
|
||||
ClusterResources *ClusterResources `json:"clusterResources,omitempty" yaml:"clusterResources,omitempty"`
|
||||
Secret *Secret `json:"secret,omitempty" yaml:"secret,omitempty"`
|
||||
CustomMetrics *CustomMetrics `json:"customMetrics,omitempty" yaml:"customMetrics,omitempty"`
|
||||
ConfigMap *ConfigMap `json:"configMap,omitempty" yaml:"configMap,omitempty"`
|
||||
Logs *Logs `json:"logs,omitempty" yaml:"logs,omitempty"`
|
||||
Run *Run `json:"run,omitempty" yaml:"run,omitempty"`
|
||||
RunPod *RunPod `json:"runPod,omitempty" yaml:"runPod,omitempty"`
|
||||
RunDaemonSet *RunDaemonSet `json:"runDaemonSet,omitempty" yaml:"runDaemonSet,omitempty"`
|
||||
Exec *Exec `json:"exec,omitempty" yaml:"exec,omitempty"`
|
||||
Data *Data `json:"data,omitempty" yaml:"data,omitempty"`
|
||||
Copy *Copy `json:"copy,omitempty" yaml:"copy,omitempty"`
|
||||
CopyFromHost *CopyFromHost `json:"copyFromHost,omitempty" yaml:"copyFromHost,omitempty"`
|
||||
HTTP *HTTP `json:"http,omitempty" yaml:"http,omitempty"`
|
||||
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"`
|
||||
Collectd *Collectd `json:"collectd,omitempty" yaml:"collectd,omitempty"`
|
||||
Ceph *Ceph `json:"ceph,omitempty" yaml:"ceph,omitempty"`
|
||||
Longhorn *Longhorn `json:"longhorn,omitempty" yaml:"longhorn,omitempty"`
|
||||
RegistryImages *RegistryImages `json:"registryImages,omitempty" yaml:"registryImages,omitempty"`
|
||||
Sysctl *Sysctl `json:"sysctl,omitempty" yaml:"sysctl,omitempty"`
|
||||
Certificates *Certificates `json:"certificates,omitempty" yaml:"certificates,omitempty"`
|
||||
Helm *Helm `json:"helm,omitempty" yaml:"helm,omitempty"`
|
||||
Goldpinger *Goldpinger `json:"goldpinger,omitempty" yaml:"goldpinger,omitempty"`
|
||||
Sonobuoy *Sonobuoy `json:"sonobuoy,omitempty" yaml:"sonobuoy,omitempty"`
|
||||
NodeMetrics *NodeMetrics `json:"nodeMetrics,omitempty" yaml:"nodeMetrics,omitempty"`
|
||||
DNS *DNS `json:"dns,omitempty" yaml:"dns,omitempty"`
|
||||
Etcd *Etcd `json:"etcd,omitempty" yaml:"etcd,omitempty"`
|
||||
SupportBundleMetadata *SupportBundleMetadata `json:"supportBundleMetadata,omitempty" yaml:"supportBundleMetadata,omitempty"`
|
||||
S3Status *S3Status `json:"s3Status,omitempty" yaml:"s3Status,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSubjectAccessReviewSpec {
|
||||
@@ -568,6 +586,21 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
|
||||
})
|
||||
} else if c.Sysctl != nil {
|
||||
// TODO
|
||||
} else if c.SupportBundleMetadata != nil {
|
||||
result = append(result, authorizationv1.SelfSubjectAccessReviewSpec{
|
||||
ResourceAttributes: &authorizationv1.ResourceAttributes{
|
||||
Namespace: pickNamespaceOrDefault(c.SupportBundleMetadata.Namespace, overrideNS),
|
||||
Verb: "get",
|
||||
Group: "",
|
||||
Version: "",
|
||||
Resource: "secrets",
|
||||
Subresource: "",
|
||||
Name: "replicated-support-metadata",
|
||||
},
|
||||
NonResourceAttributes: nil,
|
||||
})
|
||||
} else if c.S3Status != nil {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -671,6 +704,14 @@ func (c *Collect) GetName() string {
|
||||
collector = "certificates"
|
||||
name = c.Certificates.CollectorName
|
||||
}
|
||||
if c.SupportBundleMetadata != nil {
|
||||
collector = "support-bundle-metadata"
|
||||
name = c.SupportBundleMetadata.CollectorName
|
||||
}
|
||||
if c.S3Status != nil {
|
||||
collector = "s3Status"
|
||||
name = c.S3Status.CollectorName
|
||||
}
|
||||
|
||||
if collector == "" {
|
||||
return "<none>"
|
||||
|
||||
@@ -60,12 +60,16 @@ type TimeAnalyze struct {
|
||||
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
|
||||
}
|
||||
|
||||
// BlockDevicesAnalyze evaluates host-collected block device listings (lsblk-based).
|
||||
type BlockDevicesAnalyze struct {
|
||||
AnalyzeMeta `json:",inline" yaml:",inline"`
|
||||
CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
|
||||
MinimumAcceptableSize uint64 `json:"minimumAcceptableSize" yaml:"minimumAcceptableSize"`
|
||||
IncludeUnmountedPartitions bool `json:"includeUnmountedPartitions" yaml:"includeUnmountedPartitions"`
|
||||
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
|
||||
CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
|
||||
MinimumAcceptableSize uint64 `json:"minimumAcceptableSize" yaml:"minimumAcceptableSize"`
|
||||
IncludeUnmountedPartitions bool `json:"includeUnmountedPartitions" yaml:"includeUnmountedPartitions"`
|
||||
// AdditionalDeviceTypes are extra lsblk TYPE values (e.g. loop, lvm) that may count toward outcomes,
|
||||
// in addition to whole disks and (when IncludeUnmountedPartitions is set) partitions.
|
||||
AdditionalDeviceTypes []string `json:"additionalDeviceTypes,omitempty" yaml:"additionalDeviceTypes,omitempty"`
|
||||
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
|
||||
}
|
||||
|
||||
type SystemPackagesAnalyze struct {
|
||||
@@ -149,6 +153,12 @@ type HostSysctlAnalyze struct {
|
||||
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
|
||||
}
|
||||
|
||||
type HostRegistryImagesAnalyze struct {
|
||||
AnalyzeMeta `json:",inline" yaml:",inline"`
|
||||
CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
|
||||
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
|
||||
}
|
||||
|
||||
type HostAnalyze struct {
|
||||
CPU *CPUAnalyze `json:"cpu,omitempty" yaml:"cpu,omitempty"`
|
||||
TCPLoadBalancer *TCPLoadBalancerAnalyze `json:"tcpLoadBalancer,omitempty" yaml:"tcpLoadBalancer,omitempty"`
|
||||
@@ -176,4 +186,5 @@ type HostAnalyze struct {
|
||||
JsonCompare *JsonCompare `json:"jsonCompare,omitempty" yaml:"jsonCompare,omitempty"`
|
||||
NetworkNamespaceConnectivity *NetworkNamespaceConnectivityAnalyze `json:"networkNamespaceConnectivity,omitempty" yaml:"networkNamespaceConnectivity,omitempty"`
|
||||
Sysctl *HostSysctlAnalyze `json:"sysctl,omitempty" yaml:"sysctl,omitempty"`
|
||||
RegistryImages *HostRegistryImagesAnalyze `json:"registryImages,omitempty" yaml:"registryImages,omitempty"`
|
||||
}
|
||||
|
||||
@@ -235,6 +235,16 @@ type HostSysctl struct {
|
||||
HostCollectorMeta `json:",inline" yaml:",inline"`
|
||||
}
|
||||
|
||||
// HostRegistryImages checks whether images are accessible from the host,
|
||||
// without requiring a Kubernetes cluster. Auth can be supplied inline via
|
||||
// Username/Password or omitted to rely on ambient credentials (e.g. ~/.docker/config.json).
|
||||
type HostRegistryImages struct {
|
||||
HostCollectorMeta `json:",inline" yaml:",inline"`
|
||||
Images []string `json:"images" yaml:"images"`
|
||||
Username string `json:"username,omitempty" yaml:"username,omitempty"`
|
||||
Password string `json:"password,omitempty" yaml:"password,omitempty"`
|
||||
}
|
||||
|
||||
type HostCollect struct {
|
||||
CPU *CPU `json:"cpu,omitempty" yaml:"cpu,omitempty"`
|
||||
Memory *Memory `json:"memory,omitempty" yaml:"memory,omitempty"`
|
||||
@@ -265,6 +275,7 @@ type HostCollect struct {
|
||||
HostDNS *HostDNS `json:"dns,omitempty" yaml:"dns,omitempty"`
|
||||
NetworkNamespaceConnectivity *HostNetworkNamespaceConnectivity `json:"networkNamespaceConnectivity,omitempty" yaml:"networkNamespaceConnectivity,omitempty"`
|
||||
HostSysctl *HostSysctl `json:"sysctl,omitempty" yaml:"sysctl,omitempty"`
|
||||
RegistryImages *HostRegistryImages `json:"registryImages,omitempty" yaml:"registryImages,omitempty"`
|
||||
}
|
||||
|
||||
// GetName gets the name of the collector
|
||||
|
||||
@@ -65,6 +65,11 @@ func (in *Analyze) DeepCopyInto(out *Analyze) {
|
||||
*out = new(StorageClass)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.IngressClass != nil {
|
||||
in, out := &in.IngressClass, &out.IngressClass
|
||||
*out = new(IngressClass)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.CustomResourceDefinition != nil {
|
||||
in, out := &in.CustomResourceDefinition, &out.CustomResourceDefinition
|
||||
*out = new(CustomResourceDefinition)
|
||||
@@ -230,6 +235,11 @@ func (in *Analyze) DeepCopyInto(out *Analyze) {
|
||||
*out = new(HTTPAnalyze)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.S3Status != nil {
|
||||
in, out := &in.S3Status, &out.S3Status
|
||||
*out = new(DatabaseAnalyze)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Analyze.
|
||||
@@ -443,6 +453,11 @@ func (in *AnalyzerStatus) DeepCopy() *AnalyzerStatus {
|
||||
func (in *BlockDevicesAnalyze) DeepCopyInto(out *BlockDevicesAnalyze) {
|
||||
*out = *in
|
||||
in.AnalyzeMeta.DeepCopyInto(&out.AnalyzeMeta)
|
||||
if in.AdditionalDeviceTypes != nil {
|
||||
in, out := &in.AdditionalDeviceTypes, &out.AdditionalDeviceTypes
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Outcomes != nil {
|
||||
in, out := &in.Outcomes, &out.Outcomes
|
||||
*out = make([]*Outcome, len(*in))
|
||||
@@ -980,6 +995,16 @@ func (in *Collect) DeepCopyInto(out *Collect) {
|
||||
*out = new(Etcd)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.SupportBundleMetadata != nil {
|
||||
in, out := &in.SupportBundleMetadata, &out.SupportBundleMetadata
|
||||
*out = new(SupportBundleMetadata)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.S3Status != nil {
|
||||
in, out := &in.S3Status, &out.S3Status
|
||||
*out = new(S3Status)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Collect.
|
||||
@@ -1974,6 +1999,11 @@ func (in *HostAnalyze) DeepCopyInto(out *HostAnalyze) {
|
||||
*out = new(HostSysctlAnalyze)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.RegistryImages != nil {
|
||||
in, out := &in.RegistryImages, &out.RegistryImages
|
||||
*out = new(HostRegistryImagesAnalyze)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostAnalyze.
|
||||
@@ -2214,6 +2244,11 @@ func (in *HostCollect) DeepCopyInto(out *HostCollect) {
|
||||
*out = new(HostSysctl)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.RegistryImages != nil {
|
||||
in, out := &in.RegistryImages, &out.RegistryImages
|
||||
*out = new(HostRegistryImages)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostCollect.
|
||||
@@ -2659,6 +2694,54 @@ func (in *HostPreflightStatus) DeepCopy() *HostPreflightStatus {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *HostRegistryImages) DeepCopyInto(out *HostRegistryImages) {
|
||||
*out = *in
|
||||
in.HostCollectorMeta.DeepCopyInto(&out.HostCollectorMeta)
|
||||
if in.Images != nil {
|
||||
in, out := &in.Images, &out.Images
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostRegistryImages.
|
||||
func (in *HostRegistryImages) DeepCopy() *HostRegistryImages {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(HostRegistryImages)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *HostRegistryImagesAnalyze) DeepCopyInto(out *HostRegistryImagesAnalyze) {
|
||||
*out = *in
|
||||
in.AnalyzeMeta.DeepCopyInto(&out.AnalyzeMeta)
|
||||
if in.Outcomes != nil {
|
||||
in, out := &in.Outcomes, &out.Outcomes
|
||||
*out = make([]*Outcome, len(*in))
|
||||
for i := range *in {
|
||||
if (*in)[i] != nil {
|
||||
in, out := &(*in)[i], &(*out)[i]
|
||||
*out = new(Outcome)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostRegistryImagesAnalyze.
|
||||
func (in *HostRegistryImagesAnalyze) DeepCopy() *HostRegistryImagesAnalyze {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(HostRegistryImagesAnalyze)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *HostRun) DeepCopyInto(out *HostRun) {
|
||||
*out = *in
|
||||
@@ -3039,6 +3122,33 @@ func (in *Ingress) DeepCopy() *Ingress {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *IngressClass) DeepCopyInto(out *IngressClass) {
|
||||
*out = *in
|
||||
in.AnalyzeMeta.DeepCopyInto(&out.AnalyzeMeta)
|
||||
if in.Outcomes != nil {
|
||||
in, out := &in.Outcomes, &out.Outcomes
|
||||
*out = make([]*Outcome, len(*in))
|
||||
for i := range *in {
|
||||
if (*in)[i] != nil {
|
||||
in, out := &(*in)[i], &(*out)[i]
|
||||
*out = new(Outcome)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressClass.
|
||||
func (in *IngressClass) DeepCopy() *IngressClass {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(IngressClass)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *JobStatus) DeepCopyInto(out *JobStatus) {
|
||||
*out = *in
|
||||
@@ -4671,6 +4781,22 @@ func (in *RunPod) DeepCopy() *RunPod {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *S3Status) DeepCopyInto(out *S3Status) {
|
||||
*out = *in
|
||||
in.CollectorMeta.DeepCopyInto(&out.CollectorMeta)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3Status.
|
||||
func (in *S3Status) DeepCopy() *S3Status {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(S3Status)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Secret) DeepCopyInto(out *Secret) {
|
||||
*out = *in
|
||||
@@ -4911,6 +5037,22 @@ func (in *SupportBundleList) DeepCopyObject() runtime.Object {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SupportBundleMetadata) DeepCopyInto(out *SupportBundleMetadata) {
|
||||
*out = *in
|
||||
in.CollectorMeta.DeepCopyInto(&out.CollectorMeta)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SupportBundleMetadata.
|
||||
func (in *SupportBundleMetadata) DeepCopy() *SupportBundleMetadata {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SupportBundleMetadata)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
|
||||
troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2"
|
||||
troubleshootv1beta3 "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta3"
|
||||
discovery "k8s.io/client-go/discovery"
|
||||
rest "k8s.io/client-go/rest"
|
||||
flowcontrol "k8s.io/client-go/util/flowcontrol"
|
||||
@@ -32,6 +33,7 @@ type Interface interface {
|
||||
Discovery() discovery.DiscoveryInterface
|
||||
TroubleshootV1beta1() troubleshootv1beta1.TroubleshootV1beta1Interface
|
||||
TroubleshootV1beta2() troubleshootv1beta2.TroubleshootV1beta2Interface
|
||||
TroubleshootV1beta3() troubleshootv1beta3.TroubleshootV1beta3Interface
|
||||
}
|
||||
|
||||
// Clientset contains the clients for groups.
|
||||
@@ -39,6 +41,7 @@ type Clientset struct {
|
||||
*discovery.DiscoveryClient
|
||||
troubleshootV1beta1 *troubleshootv1beta1.TroubleshootV1beta1Client
|
||||
troubleshootV1beta2 *troubleshootv1beta2.TroubleshootV1beta2Client
|
||||
troubleshootV1beta3 *troubleshootv1beta3.TroubleshootV1beta3Client
|
||||
}
|
||||
|
||||
// TroubleshootV1beta1 retrieves the TroubleshootV1beta1Client
|
||||
@@ -51,6 +54,11 @@ func (c *Clientset) TroubleshootV1beta2() troubleshootv1beta2.TroubleshootV1beta
|
||||
return c.troubleshootV1beta2
|
||||
}
|
||||
|
||||
// TroubleshootV1beta3 retrieves the TroubleshootV1beta3Client
|
||||
func (c *Clientset) TroubleshootV1beta3() troubleshootv1beta3.TroubleshootV1beta3Interface {
|
||||
return c.troubleshootV1beta3
|
||||
}
|
||||
|
||||
// Discovery retrieves the DiscoveryClient
|
||||
func (c *Clientset) Discovery() discovery.DiscoveryInterface {
|
||||
if c == nil {
|
||||
@@ -103,6 +111,10 @@ func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cs.troubleshootV1beta3, err = troubleshootv1beta3.NewForConfigAndClient(&configShallowCopy, httpClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient)
|
||||
if err != nil {
|
||||
@@ -126,6 +138,7 @@ func New(c rest.Interface) *Clientset {
|
||||
var cs Clientset
|
||||
cs.troubleshootV1beta1 = troubleshootv1beta1.New(c)
|
||||
cs.troubleshootV1beta2 = troubleshootv1beta2.New(c)
|
||||
cs.troubleshootV1beta3 = troubleshootv1beta3.New(c)
|
||||
|
||||
cs.DiscoveryClient = discovery.NewDiscoveryClient(c)
|
||||
return &cs
|
||||
|
||||
@@ -23,6 +23,8 @@ import (
|
||||
faketroubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/fake"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2"
|
||||
faketroubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake"
|
||||
troubleshootv1beta3 "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta3"
|
||||
faketroubleshootv1beta3 "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta3/fake"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
@@ -98,3 +100,8 @@ func (c *Clientset) TroubleshootV1beta1() troubleshootv1beta1.TroubleshootV1beta
|
||||
func (c *Clientset) TroubleshootV1beta2() troubleshootv1beta2.TroubleshootV1beta2Interface {
|
||||
return &faketroubleshootv1beta2.FakeTroubleshootV1beta2{Fake: &c.Fake}
|
||||
}
|
||||
|
||||
// TroubleshootV1beta3 retrieves the TroubleshootV1beta3Client
|
||||
func (c *Clientset) TroubleshootV1beta3() troubleshootv1beta3.TroubleshootV1beta3Interface {
|
||||
return &faketroubleshootv1beta3.FakeTroubleshootV1beta3{Fake: &c.Fake}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ package fake
|
||||
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"
|
||||
@@ -33,6 +34,7 @@ var codecs = serializer.NewCodecFactory(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
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
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 client-gen. DO NOT EDIT.
|
||||
|
||||
// This package has the automatically generated typed clients.
|
||||
package v1beta3
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
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 client-gen. DO NOT EDIT.
|
||||
|
||||
// Package fake has the automatically generated clients.
|
||||
package fake
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
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 client-gen. DO NOT EDIT.
|
||||
|
||||
package fake
|
||||
|
||||
import (
|
||||
v1beta3 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta3"
|
||||
troubleshootv1beta3 "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta3"
|
||||
gentype "k8s.io/client-go/gentype"
|
||||
)
|
||||
|
||||
// fakeSupportBundles implements SupportBundleInterface
|
||||
type fakeSupportBundles struct {
|
||||
*gentype.FakeClientWithList[*v1beta3.SupportBundle, *v1beta3.SupportBundleList]
|
||||
Fake *FakeTroubleshootV1beta3
|
||||
}
|
||||
|
||||
func newFakeSupportBundles(fake *FakeTroubleshootV1beta3, namespace string) troubleshootv1beta3.SupportBundleInterface {
|
||||
return &fakeSupportBundles{
|
||||
gentype.NewFakeClientWithList[*v1beta3.SupportBundle, *v1beta3.SupportBundleList](
|
||||
fake.Fake,
|
||||
namespace,
|
||||
v1beta3.SchemeGroupVersion.WithResource("supportbundles"),
|
||||
v1beta3.SchemeGroupVersion.WithKind("SupportBundle"),
|
||||
func() *v1beta3.SupportBundle { return &v1beta3.SupportBundle{} },
|
||||
func() *v1beta3.SupportBundleList { return &v1beta3.SupportBundleList{} },
|
||||
func(dst, src *v1beta3.SupportBundleList) { dst.ListMeta = src.ListMeta },
|
||||
func(list *v1beta3.SupportBundleList) []*v1beta3.SupportBundle {
|
||||
return gentype.ToPointerSlice(list.Items)
|
||||
},
|
||||
func(list *v1beta3.SupportBundleList, items []*v1beta3.SupportBundle) {
|
||||
list.Items = gentype.FromPointerSlice(items)
|
||||
},
|
||||
),
|
||||
fake,
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
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 client-gen. DO NOT EDIT.
|
||||
|
||||
package fake
|
||||
|
||||
import (
|
||||
v1beta3 "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta3"
|
||||
rest "k8s.io/client-go/rest"
|
||||
testing "k8s.io/client-go/testing"
|
||||
)
|
||||
|
||||
type FakeTroubleshootV1beta3 struct {
|
||||
*testing.Fake
|
||||
}
|
||||
|
||||
func (c *FakeTroubleshootV1beta3) SupportBundles(namespace string) v1beta3.SupportBundleInterface {
|
||||
return newFakeSupportBundles(c, namespace)
|
||||
}
|
||||
|
||||
// RESTClient returns a RESTClient that is used to communicate
|
||||
// with API server by this client implementation.
|
||||
func (c *FakeTroubleshootV1beta3) RESTClient() rest.Interface {
|
||||
var ret *rest.RESTClient
|
||||
return ret
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
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 client-gen. DO NOT EDIT.
|
||||
|
||||
package v1beta3
|
||||
|
||||
type SupportBundleExpansion interface{}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
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 client-gen. DO NOT EDIT.
|
||||
|
||||
package v1beta3
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
troubleshootv1beta3 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta3"
|
||||
scheme "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
types "k8s.io/apimachinery/pkg/types"
|
||||
watch "k8s.io/apimachinery/pkg/watch"
|
||||
gentype "k8s.io/client-go/gentype"
|
||||
)
|
||||
|
||||
// SupportBundlesGetter has a method to return a SupportBundleInterface.
|
||||
// A group's client should implement this interface.
|
||||
type SupportBundlesGetter interface {
|
||||
SupportBundles(namespace string) SupportBundleInterface
|
||||
}
|
||||
|
||||
// SupportBundleInterface has methods to work with SupportBundle resources.
|
||||
type SupportBundleInterface interface {
|
||||
Create(ctx context.Context, supportBundle *troubleshootv1beta3.SupportBundle, opts v1.CreateOptions) (*troubleshootv1beta3.SupportBundle, error)
|
||||
Update(ctx context.Context, supportBundle *troubleshootv1beta3.SupportBundle, opts v1.UpdateOptions) (*troubleshootv1beta3.SupportBundle, error)
|
||||
// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
|
||||
UpdateStatus(ctx context.Context, supportBundle *troubleshootv1beta3.SupportBundle, opts v1.UpdateOptions) (*troubleshootv1beta3.SupportBundle, error)
|
||||
Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
|
||||
DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
|
||||
Get(ctx context.Context, name string, opts v1.GetOptions) (*troubleshootv1beta3.SupportBundle, error)
|
||||
List(ctx context.Context, opts v1.ListOptions) (*troubleshootv1beta3.SupportBundleList, error)
|
||||
Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
|
||||
Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *troubleshootv1beta3.SupportBundle, err error)
|
||||
SupportBundleExpansion
|
||||
}
|
||||
|
||||
// supportBundles implements SupportBundleInterface
|
||||
type supportBundles struct {
|
||||
*gentype.ClientWithList[*troubleshootv1beta3.SupportBundle, *troubleshootv1beta3.SupportBundleList]
|
||||
}
|
||||
|
||||
// newSupportBundles returns a SupportBundles
|
||||
func newSupportBundles(c *TroubleshootV1beta3Client, namespace string) *supportBundles {
|
||||
return &supportBundles{
|
||||
gentype.NewClientWithList[*troubleshootv1beta3.SupportBundle, *troubleshootv1beta3.SupportBundleList](
|
||||
"supportbundles",
|
||||
c.RESTClient(),
|
||||
scheme.ParameterCodec,
|
||||
namespace,
|
||||
func() *troubleshootv1beta3.SupportBundle { return &troubleshootv1beta3.SupportBundle{} },
|
||||
func() *troubleshootv1beta3.SupportBundleList { return &troubleshootv1beta3.SupportBundleList{} },
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
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 client-gen. DO NOT EDIT.
|
||||
|
||||
package v1beta3
|
||||
|
||||
import (
|
||||
http "net/http"
|
||||
|
||||
troubleshootv1beta3 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta3"
|
||||
scheme "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme"
|
||||
rest "k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
type TroubleshootV1beta3Interface interface {
|
||||
RESTClient() rest.Interface
|
||||
SupportBundlesGetter
|
||||
}
|
||||
|
||||
// TroubleshootV1beta3Client is used to interact with features provided by the troubleshoot.sh group.
|
||||
type TroubleshootV1beta3Client struct {
|
||||
restClient rest.Interface
|
||||
}
|
||||
|
||||
func (c *TroubleshootV1beta3Client) SupportBundles(namespace string) SupportBundleInterface {
|
||||
return newSupportBundles(c, namespace)
|
||||
}
|
||||
|
||||
// NewForConfig creates a new TroubleshootV1beta3Client for the given config.
|
||||
// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient),
|
||||
// where httpClient was generated with rest.HTTPClientFor(c).
|
||||
func NewForConfig(c *rest.Config) (*TroubleshootV1beta3Client, error) {
|
||||
config := *c
|
||||
setConfigDefaults(&config)
|
||||
httpClient, err := rest.HTTPClientFor(&config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewForConfigAndClient(&config, httpClient)
|
||||
}
|
||||
|
||||
// NewForConfigAndClient creates a new TroubleshootV1beta3Client for the given config and http client.
|
||||
// Note the http client provided takes precedence over the configured transport values.
|
||||
func NewForConfigAndClient(c *rest.Config, h *http.Client) (*TroubleshootV1beta3Client, error) {
|
||||
config := *c
|
||||
setConfigDefaults(&config)
|
||||
client, err := rest.RESTClientForConfigAndClient(&config, h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &TroubleshootV1beta3Client{client}, nil
|
||||
}
|
||||
|
||||
// NewForConfigOrDie creates a new TroubleshootV1beta3Client for the given config and
|
||||
// panics if there is an error in the config.
|
||||
func NewForConfigOrDie(c *rest.Config) *TroubleshootV1beta3Client {
|
||||
client, err := NewForConfig(c)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
// New creates a new TroubleshootV1beta3Client for the given RESTClient.
|
||||
func New(c rest.Interface) *TroubleshootV1beta3Client {
|
||||
return &TroubleshootV1beta3Client{c}
|
||||
}
|
||||
|
||||
func setConfigDefaults(config *rest.Config) {
|
||||
gv := troubleshootv1beta3.SchemeGroupVersion
|
||||
config.GroupVersion = &gv
|
||||
config.APIPath = "/apis"
|
||||
config.NegotiatedSerializer = rest.CodecFactoryForGeneratedClient(scheme.Scheme, scheme.Codecs).WithoutConversion()
|
||||
|
||||
if config.UserAgent == "" {
|
||||
config.UserAgent = rest.DefaultKubernetesUserAgent()
|
||||
}
|
||||
}
|
||||
|
||||
// RESTClient returns a RESTClient that is used to communicate
|
||||
// with API server by this client implementation.
|
||||
func (c *TroubleshootV1beta3Client) RESTClient() rest.Interface {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
return c.restClient
|
||||
}
|
||||
@@ -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, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_STATEFULSETS), k), bytes.NewBuffer(v))
|
||||
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_REPLICASETS)), marshalErrors(replicasetsErrors))
|
||||
|
||||
@@ -276,6 +276,11 @@ func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (Coll
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_STORAGE_CLASS)), bytes.NewBuffer(storageClasses))
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_STORAGE_CLASS)), marshalErrors(storageErrors))
|
||||
|
||||
// ingress classes
|
||||
ingressClasses, ingressClassErrors := ingressClasses(ctx, client)
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_INGRESS_CLASS)), bytes.NewBuffer(ingressClasses))
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_INGRESS_CLASS)), marshalErrors(ingressClassErrors))
|
||||
|
||||
// priority classes
|
||||
priorityClasses, priorityErrors := priorityClasses(ctx, client)
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_PRIORITY_CLASS)), bytes.NewBuffer(priorityClasses))
|
||||
@@ -370,9 +375,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_ENDPOINTSICES, k), bytes.NewBuffer(v))
|
||||
_ = 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, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_ENDPOINTSICES)), marshalErrors(endpointslicesErrors))
|
||||
_ = output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_ENDPOINTSLICES)), marshalErrors(endpointslicesErrors))
|
||||
|
||||
// Service Accounts
|
||||
servicesAccounts, servicesAccountsErrors := serviceAccounts(ctx, client, namespaceNames)
|
||||
@@ -393,6 +398,11 @@ func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (Coll
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_VOLUME_ATTACHMENTS)), bytes.NewBuffer(volumeAttachments))
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_VOLUME_ATTACHMENTS)), marshalErrors(volumeAttachmentsErrors))
|
||||
|
||||
// Certificate Signing Requests
|
||||
csrs, csrsErrors := certificateSigningRequests(ctx, client)
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_CERTIFICATE_SIGNING_REQUESTS)), bytes.NewBuffer(csrs))
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_CERTIFICATE_SIGNING_REQUESTS)), marshalErrors(csrsErrors))
|
||||
|
||||
// ConfigMaps
|
||||
configMaps, configMapsErrors := configMaps(ctx, client, namespaceNames)
|
||||
for k, v := range configMaps {
|
||||
@@ -401,6 +411,16 @@ 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))
|
||||
|
||||
// Validating Webhook Configurations
|
||||
validatingWebhookConfigurations, validatingWebhookConfigurationsErrors := validatingWebhookConfigurations(ctx, client)
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_VALIDATING_WEBHOOK_CONFIGURATIONS)), bytes.NewBuffer(validatingWebhookConfigurations))
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_VALIDATING_WEBHOOK_CONFIGURATIONS)), marshalErrors(validatingWebhookConfigurationsErrors))
|
||||
|
||||
// Mutating Webhook Configurations
|
||||
mutatingWebhookConfigurations, mutatingWebhookConfigurationsErrors := mutatingWebhookConfigurations(ctx, client)
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_MUTATING_WEBHOOK_CONFIGURATIONS)), bytes.NewBuffer(mutatingWebhookConfigurations))
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_MUTATING_WEBHOOK_CONFIGURATIONS)), marshalErrors(mutatingWebhookConfigurationsErrors))
|
||||
|
||||
// Replicated License
|
||||
licenseData, licenseErr := replicatedLicense(ctx, client, namespaceNames)
|
||||
if licenseErr == nil {
|
||||
@@ -1112,6 +1132,40 @@ func storageClassesV1beta(ctx context.Context, client *kubernetes.Clientset) ([]
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func ingressClasses(ctx context.Context, client *kubernetes.Clientset) ([]byte, []string) {
|
||||
ok, err := discovery.HasResource(client, "networking.k8s.io/v1", "IngressClass")
|
||||
if err != nil {
|
||||
return nil, []string{err.Error()}
|
||||
}
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ingressClasses, err := client.NetworkingV1().IngressClasses().List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, []string{err.Error()}
|
||||
}
|
||||
|
||||
gvk, err := apiutil.GVKForObject(ingressClasses, scheme.Scheme)
|
||||
if err == nil {
|
||||
ingressClasses.GetObjectKind().SetGroupVersionKind(gvk)
|
||||
}
|
||||
|
||||
for i, o := range ingressClasses.Items {
|
||||
gvk, err := apiutil.GVKForObject(&o, scheme.Scheme)
|
||||
if err == nil {
|
||||
ingressClasses.Items[i].GetObjectKind().SetGroupVersionKind(gvk)
|
||||
}
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(ingressClasses, "", " ")
|
||||
if err != nil {
|
||||
return nil, []string{err.Error()}
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func priorityClasses(ctx context.Context, client *kubernetes.Clientset) ([]byte, []string) {
|
||||
ok, err := discovery.HasResource(client, "scheduling.k8s.io/v1", "PriorityClass")
|
||||
if err != nil {
|
||||
@@ -2129,6 +2183,32 @@ func volumeAttachments(ctx context.Context, client kubernetes.Interface) ([]byte
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func certificateSigningRequests(ctx context.Context, client kubernetes.Interface) ([]byte, []string) {
|
||||
csrs, err := client.CertificatesV1().CertificateSigningRequests().List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, []string{err.Error()}
|
||||
}
|
||||
|
||||
gvk, err := apiutil.GVKForObject(csrs, scheme.Scheme)
|
||||
if err == nil {
|
||||
csrs.GetObjectKind().SetGroupVersionKind(gvk)
|
||||
}
|
||||
|
||||
for i, o := range csrs.Items {
|
||||
gvk, err := apiutil.GVKForObject(&o, scheme.Scheme)
|
||||
if err == nil {
|
||||
csrs.Items[i].GetObjectKind().SetGroupVersionKind(gvk)
|
||||
}
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(csrs, "", " ")
|
||||
if err != nil {
|
||||
return nil, []string{err.Error()}
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func configMaps(ctx context.Context, client kubernetes.Interface, namespaces []string) (map[string][]byte, map[string]string) {
|
||||
configmapByNamespace := make(map[string][]byte)
|
||||
errorsByNamespace := make(map[string]string)
|
||||
@@ -2164,6 +2244,56 @@ func configMaps(ctx context.Context, client kubernetes.Interface, namespaces []s
|
||||
return configmapByNamespace, errorsByNamespace
|
||||
}
|
||||
|
||||
func validatingWebhookConfigurations(ctx context.Context, client kubernetes.Interface) ([]byte, []string) {
|
||||
validatingWebhookConfigurations, err := client.AdmissionregistrationV1().ValidatingWebhookConfigurations().List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, []string{err.Error()}
|
||||
}
|
||||
|
||||
gvk, err := apiutil.GVKForObject(validatingWebhookConfigurations, scheme.Scheme)
|
||||
if err == nil {
|
||||
validatingWebhookConfigurations.GetObjectKind().SetGroupVersionKind(gvk)
|
||||
}
|
||||
|
||||
for i, o := range validatingWebhookConfigurations.Items {
|
||||
gvk, err := apiutil.GVKForObject(&o, scheme.Scheme)
|
||||
if err == nil {
|
||||
validatingWebhookConfigurations.Items[i].GetObjectKind().SetGroupVersionKind(gvk)
|
||||
}
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(validatingWebhookConfigurations, "", " ")
|
||||
if err != nil {
|
||||
return nil, []string{err.Error()}
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func mutatingWebhookConfigurations(ctx context.Context, client kubernetes.Interface) ([]byte, []string) {
|
||||
mutatingWebhookConfigurations, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, []string{err.Error()}
|
||||
}
|
||||
|
||||
gvk, err := apiutil.GVKForObject(mutatingWebhookConfigurations, scheme.Scheme)
|
||||
if err == nil {
|
||||
mutatingWebhookConfigurations.GetObjectKind().SetGroupVersionKind(gvk)
|
||||
}
|
||||
|
||||
for i, o := range mutatingWebhookConfigurations.Items {
|
||||
gvk, err := apiutil.GVKForObject(&o, scheme.Scheme)
|
||||
if err == nil {
|
||||
mutatingWebhookConfigurations.Items[i].GetObjectKind().SetGroupVersionKind(gvk)
|
||||
}
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(mutatingWebhookConfigurations, "", " ")
|
||||
if err != nil {
|
||||
return nil, []string{err.Error()}
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// storeCustomResource stores a custom resource as JSON and YAML
|
||||
// We use both formats for backwards compatibility. This way we
|
||||
// avoid breaking existing tools and analysers that already rely on
|
||||
|
||||
@@ -3,6 +3,7 @@ package collect
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
@@ -11,6 +12,8 @@ import (
|
||||
"github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
certificatesv1 "k8s.io/api/certificates/v1"
|
||||
v1 "k8s.io/api/coordination/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
policyv1 "k8s.io/api/policy/v1"
|
||||
@@ -18,11 +21,13 @@ import (
|
||||
storagev1 "k8s.io/api/storage/v1"
|
||||
apixfake "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
fakediscovery "k8s.io/client-go/discovery/fake"
|
||||
testdynamicclient "k8s.io/client-go/dynamic/fake"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
testclient "k8s.io/client-go/kubernetes/fake"
|
||||
k8stesting "k8s.io/client-go/testing"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
@@ -697,3 +702,242 @@ func createTestPodDisruptionBudgetsV1beta1(client kubernetes.Interface, pdbNames
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Test_CertificateSigningRequests(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
csrNames []string
|
||||
}{
|
||||
{
|
||||
name: "single certificate signing request",
|
||||
csrNames: []string{"test-csr"},
|
||||
},
|
||||
{
|
||||
name: "multiple certificate signing requests",
|
||||
csrNames: []string{"test-csr-1", "test-csr-2", "test-csr-3"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := testclient.NewSimpleClientset()
|
||||
ctx := context.Background()
|
||||
err := createTestCertificateSigningRequests(client, tt.csrNames)
|
||||
assert.NoError(t, err)
|
||||
|
||||
csrs, csrErrors := certificateSigningRequests(ctx, client)
|
||||
assert.Empty(t, csrErrors)
|
||||
assert.NotEmpty(t, csrs)
|
||||
|
||||
var csrList certificatesv1.CertificateSigningRequestList
|
||||
err = json.Unmarshal(csrs, &csrList)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(tt.csrNames), len(csrList.Items))
|
||||
for _, csr := range csrList.Items {
|
||||
assert.Contains(t, tt.csrNames, csr.ObjectMeta.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CertificateSigningRequests_PermissionDenied(t *testing.T) {
|
||||
client := testclient.NewSimpleClientset()
|
||||
ctx := context.Background()
|
||||
|
||||
// Add a reactor to simulate permission denied error
|
||||
client.PrependReactor("list", "certificatesigningrequests", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return true, nil, fmt.Errorf("certificatesigningrequests.certificates.k8s.io is forbidden: User \"system:serviceaccount:default:default\" cannot list resource \"certificatesigningrequests\" in API group \"certificates.k8s.io\" at the cluster scope")
|
||||
})
|
||||
|
||||
csrs, csrErrors := certificateSigningRequests(ctx, client)
|
||||
|
||||
// Verify fail-safe behavior: returns nil data + error string (not panic)
|
||||
assert.Nil(t, csrs)
|
||||
assert.NotEmpty(t, csrErrors)
|
||||
assert.Len(t, csrErrors, 1)
|
||||
// Verify the error is captured as a string
|
||||
assert.IsType(t, "", csrErrors[0])
|
||||
assert.Contains(t, csrErrors[0], "forbidden")
|
||||
}
|
||||
|
||||
func createTestCertificateSigningRequests(client kubernetes.Interface, csrNames []string) error {
|
||||
for _, csrName := range csrNames {
|
||||
_, err := client.CertificatesV1().CertificateSigningRequests().Create(context.Background(), &certificatesv1.CertificateSigningRequest{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: csrName,
|
||||
},
|
||||
Spec: certificatesv1.CertificateSigningRequestSpec{
|
||||
Request: []byte("-----BEGIN CERTIFICATE REQUEST-----\ntest\n-----END CERTIFICATE REQUEST-----"),
|
||||
SignerName: "kubernetes.io/kube-apiserver-client",
|
||||
Usages: []certificatesv1.KeyUsage{certificatesv1.UsageClientAuth},
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Test_ValidatingWebhookConfigurations(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
vwcNames []string
|
||||
}{
|
||||
{
|
||||
name: "single validating webhook configuration",
|
||||
vwcNames: []string{"test-vwc"},
|
||||
},
|
||||
{
|
||||
name: "multiple validating webhook configurations",
|
||||
vwcNames: []string{"vwc-1", "vwc-2", "vwc-3"},
|
||||
},
|
||||
{
|
||||
name: "empty list",
|
||||
vwcNames: []string{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := testclient.NewSimpleClientset()
|
||||
ctx := context.Background()
|
||||
err := createTestValidatingWebhookConfigurations(client, tt.vwcNames)
|
||||
require.NoError(t, err)
|
||||
|
||||
data, errs := validatingWebhookConfigurations(ctx, client)
|
||||
assert.Empty(t, errs)
|
||||
assert.NotNil(t, data)
|
||||
|
||||
var list admissionregistrationv1.ValidatingWebhookConfigurationList
|
||||
err = json.Unmarshal(data, &list)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, list.Items, len(tt.vwcNames))
|
||||
for _, item := range list.Items {
|
||||
assert.Contains(t, tt.vwcNames, item.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ValidatingWebhookConfigurations_PermissionDenied(t *testing.T) {
|
||||
client := testclient.NewSimpleClientset()
|
||||
ctx := context.Background()
|
||||
|
||||
client.PrependReactor("list", "validatingwebhookconfigurations", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return true, nil, fmt.Errorf("validatingwebhookconfigurations.admissionregistration.k8s.io is forbidden: User \"system:serviceaccount:default:default\" cannot list resource \"validatingwebhookconfigurations\" in API group \"admissionregistration.k8s.io\" at the cluster scope")
|
||||
})
|
||||
|
||||
data, errs := validatingWebhookConfigurations(ctx, client)
|
||||
|
||||
assert.Nil(t, data)
|
||||
require.NotEmpty(t, errs)
|
||||
assert.Len(t, errs, 1)
|
||||
assert.Contains(t, errs[0], "forbidden")
|
||||
}
|
||||
|
||||
func createTestValidatingWebhookConfigurations(client kubernetes.Interface, names []string) error {
|
||||
for _, name := range names {
|
||||
_, err := client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(context.Background(), &admissionregistrationv1.ValidatingWebhookConfiguration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Webhooks: []admissionregistrationv1.ValidatingWebhook{
|
||||
{
|
||||
Name: "test-webhook.example.com",
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{
|
||||
Service: &admissionregistrationv1.ServiceReference{
|
||||
Namespace: "default",
|
||||
Name: "webhook-service",
|
||||
},
|
||||
},
|
||||
AdmissionReviewVersions: []string{"v1"},
|
||||
},
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Test_MutatingWebhookConfigurations(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mwcNames []string
|
||||
}{
|
||||
{
|
||||
name: "single mutating webhook configuration",
|
||||
mwcNames: []string{"test-mwc"},
|
||||
},
|
||||
{
|
||||
name: "multiple mutating webhook configurations",
|
||||
mwcNames: []string{"mwc-1", "mwc-2"},
|
||||
},
|
||||
{
|
||||
name: "empty list",
|
||||
mwcNames: []string{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := testclient.NewSimpleClientset()
|
||||
ctx := context.Background()
|
||||
err := createTestMutatingWebhookConfigurations(client, tt.mwcNames)
|
||||
require.NoError(t, err)
|
||||
|
||||
data, errs := mutatingWebhookConfigurations(ctx, client)
|
||||
assert.Empty(t, errs)
|
||||
assert.NotNil(t, data)
|
||||
|
||||
var list admissionregistrationv1.MutatingWebhookConfigurationList
|
||||
err = json.Unmarshal(data, &list)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, list.Items, len(tt.mwcNames))
|
||||
for _, item := range list.Items {
|
||||
assert.Contains(t, tt.mwcNames, item.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_MutatingWebhookConfigurations_PermissionDenied(t *testing.T) {
|
||||
client := testclient.NewSimpleClientset()
|
||||
ctx := context.Background()
|
||||
|
||||
client.PrependReactor("list", "mutatingwebhookconfigurations", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return true, nil, fmt.Errorf("mutatingwebhookconfigurations.admissionregistration.k8s.io is forbidden: User \"system:serviceaccount:default:default\" cannot list resource \"mutatingwebhookconfigurations\" in API group \"admissionregistration.k8s.io\" at the cluster scope")
|
||||
})
|
||||
|
||||
data, errs := mutatingWebhookConfigurations(ctx, client)
|
||||
|
||||
assert.Nil(t, data)
|
||||
require.NotEmpty(t, errs)
|
||||
assert.Len(t, errs, 1)
|
||||
assert.Contains(t, errs[0], "forbidden")
|
||||
}
|
||||
|
||||
func createTestMutatingWebhookConfigurations(client kubernetes.Interface, names []string) error {
|
||||
for _, name := range names {
|
||||
_, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.Background(), &admissionregistrationv1.MutatingWebhookConfiguration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Webhooks: []admissionregistrationv1.MutatingWebhook{
|
||||
{
|
||||
Name: "test-mutating-webhook.example.com",
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{
|
||||
Service: &admissionregistrationv1.ServiceReference{
|
||||
Namespace: "default",
|
||||
Name: "webhook-service",
|
||||
},
|
||||
},
|
||||
AdmissionReviewVersions: []string{"v1"},
|
||||
},
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ func Test_ensureClusterResourcesFirst(t *testing.T) {
|
||||
list []*troubleshootv1beta2.Collect
|
||||
}{
|
||||
{
|
||||
name: "Reorg OK",
|
||||
name: "Reorg OK - clusterResources moved to front",
|
||||
want: []*troubleshootv1beta2.Collect{
|
||||
{
|
||||
ClusterResources: &troubleshootv1beta2.ClusterResources{},
|
||||
@@ -33,6 +33,99 @@ 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) {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package collect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -13,6 +15,7 @@ import (
|
||||
"github.com/replicatedhq/troubleshoot/pkg/multitype"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
type Collector interface {
|
||||
@@ -128,6 +131,10 @@ func GetCollector(collector *troubleshootv1beta2.Collect, bundlePath string, nam
|
||||
return &CollectDNS{collector.DNS, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true
|
||||
case collector.Etcd != nil:
|
||||
return &CollectEtcd{collector.Etcd, bundlePath, clientConfig, client, ctx, RBACErrors}, true
|
||||
case collector.SupportBundleMetadata != nil:
|
||||
return &CollectSupportBundleMetadata{collector.SupportBundleMetadata, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true
|
||||
case collector.S3Status != nil:
|
||||
return &CollectS3Status{collector.S3Status, bundlePath, RBACErrors}, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
@@ -223,10 +230,15 @@ func getCollectorName(c interface{}) string {
|
||||
collector = "dns"
|
||||
case *CollectEtcd:
|
||||
collector = "etcd"
|
||||
case *CollectSupportBundleMetadata:
|
||||
collector = "support-bundle-metadata"
|
||||
name = v.Collector.CollectorName
|
||||
case *CollectS3Status:
|
||||
collector = "s3Status"
|
||||
name = v.Collector.CollectorName
|
||||
default:
|
||||
collector = "<none>"
|
||||
}
|
||||
|
||||
if name != "" {
|
||||
return fmt.Sprintf("%s/%s", collector, name)
|
||||
}
|
||||
@@ -287,6 +299,35 @@ func DedupCollectors(allCollectors []*troubleshootv1beta2.Collect) []*troublesho
|
||||
return finalCollectors
|
||||
}
|
||||
|
||||
// SkippedCollector records information about a collector that was skipped during collection.
|
||||
type SkippedCollector struct {
|
||||
Collector string `json:"collector"`
|
||||
Reason string `json:"reason"`
|
||||
Errors []string `json:"errors"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
|
||||
// WriteSkippedCollectors marshals the skipped collectors list and saves it
|
||||
// using SaveResult which handles both in-memory and on-disk storage.
|
||||
func WriteSkippedCollectors(skipped []SkippedCollector, allCollectedData map[string][]byte, bundlePath string) {
|
||||
if len(skipped) == 0 {
|
||||
return
|
||||
}
|
||||
skippedJSON, err := json.Marshal(skipped)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Either write to bundle path or memory
|
||||
c := CollectorResult{}
|
||||
if err := c.SaveResult(bundlePath, "skipped-collectors.json", bytes.NewReader(skippedJSON)); err != nil {
|
||||
klog.Errorf("Failed to save skipped collectors: %v", err)
|
||||
} else {
|
||||
// Write to collected data to return downstream
|
||||
maps.Copy(allCollectedData, c)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure Copy collectors are last in the list
|
||||
// This is because copy collectors are expected to copy files from other collectors such as Exec, RunPod, RunDaemonSet
|
||||
func EnsureCopyLast(allCollectors []Collector) []Collector {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package collect
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
@@ -44,8 +47,7 @@ pwd=somethinggoeshere;`,
|
||||
want: map[string]string{
|
||||
"data/datacollectorname": ` 123
|
||||
another***HIDDEN***here
|
||||
pwd=***HIDDEN***;
|
||||
`,
|
||||
pwd=***HIDDEN***;`,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -78,8 +80,7 @@ pwd=somethinggoeshere;`,
|
||||
want: map[string]string{
|
||||
"data/datacollectorname": `abc 123
|
||||
another***HIDDEN***here
|
||||
pwd=***HIDDEN***;
|
||||
`,
|
||||
pwd=***HIDDEN***;`,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -112,8 +113,7 @@ pwd=somethinggoeshere;`,
|
||||
want: map[string]string{
|
||||
"data/datacollectorname": `abc 123
|
||||
another line here
|
||||
pwd=***HIDDEN***;
|
||||
`,
|
||||
pwd=***HIDDEN***;`,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -149,8 +149,7 @@ pwd=somethinggoeshere;`,
|
||||
want: map[string]string{
|
||||
"data/datacollectorname": `abc 123
|
||||
another***HIDDEN***here
|
||||
pwd=***HIDDEN***;
|
||||
`,
|
||||
pwd=***HIDDEN***;`,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -186,8 +185,7 @@ pwd=somethinggoeshere;`,
|
||||
want: map[string]string{
|
||||
"data/data/collectorname": `***HIDDEN*** ***HIDDEN***
|
||||
***HIDDEN*** line here
|
||||
pwd=***HIDDEN***;
|
||||
`,
|
||||
pwd=***HIDDEN***;`,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -213,8 +211,7 @@ another line here`,
|
||||
},
|
||||
want: map[string]string{
|
||||
"data/datacollectorname": `abc 123
|
||||
another line here
|
||||
`,
|
||||
another line here`,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -249,8 +246,7 @@ abc`,
|
||||
abc
|
||||
123
|
||||
xyz123
|
||||
abc
|
||||
`,
|
||||
abc`,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -526,3 +522,88 @@ func TestEnsureCopyLast(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteSkippedCollectors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
skipped []SkippedCollector
|
||||
bundlePath string
|
||||
useTempDir bool
|
||||
wantInMap bool
|
||||
wantOnDisk bool
|
||||
wantEntries []SkippedCollector
|
||||
}{
|
||||
{
|
||||
name: "empty skipped list does nothing",
|
||||
skipped: nil,
|
||||
bundlePath: "",
|
||||
wantInMap: false,
|
||||
},
|
||||
{
|
||||
name: "in-memory only when bundlePath is empty",
|
||||
skipped: []SkippedCollector{
|
||||
{Collector: "clusterResources", Reason: "excluded", Timestamp: "2026-01-01T00:00:00Z"},
|
||||
},
|
||||
bundlePath: "",
|
||||
wantInMap: true,
|
||||
wantOnDisk: false,
|
||||
wantEntries: []SkippedCollector{
|
||||
{Collector: "clusterResources", Reason: "excluded", Timestamp: "2026-01-01T00:00:00Z"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writes to disk when bundlePath is set",
|
||||
skipped: []SkippedCollector{
|
||||
{Collector: "clusterResources", Reason: "excluded", Timestamp: "2026-01-01T00:00:00Z"},
|
||||
{Collector: "logs", Reason: "insufficient RBAC permissions", Errors: []string{"pods is forbidden"}, Timestamp: "2026-01-01T00:00:01Z"},
|
||||
},
|
||||
useTempDir: true,
|
||||
wantInMap: true,
|
||||
wantOnDisk: true,
|
||||
wantEntries: []SkippedCollector{
|
||||
{Collector: "clusterResources", Reason: "excluded", Timestamp: "2026-01-01T00:00:00Z"},
|
||||
{Collector: "logs", Reason: "insufficient RBAC permissions", Errors: []string{"pods is forbidden"}, Timestamp: "2026-01-01T00:00:01Z"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
bundlePath := tt.bundlePath
|
||||
if tt.useTempDir {
|
||||
bundlePath = t.TempDir()
|
||||
}
|
||||
|
||||
result := CollectorResult{}
|
||||
WriteSkippedCollectors(tt.skipped, result, bundlePath)
|
||||
|
||||
if !tt.wantInMap {
|
||||
assert.Empty(t, result)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify in-memory entry exists
|
||||
if bundlePath == "" {
|
||||
// In-memory mode: data is stored in the map
|
||||
data, ok := result["skipped-collectors.json"]
|
||||
require.True(t, ok, "skipped-collectors.json should be in result map")
|
||||
require.NotNil(t, data)
|
||||
|
||||
var got []SkippedCollector
|
||||
require.NoError(t, json.Unmarshal(data, &got))
|
||||
assert.Equal(t, tt.wantEntries, got)
|
||||
} else {
|
||||
// On-disk mode: map entry exists with nil value, file is on disk
|
||||
_, ok := result["skipped-collectors.json"]
|
||||
require.True(t, ok, "skipped-collectors.json should be in result map")
|
||||
|
||||
diskData, err := os.ReadFile(filepath.Join(bundlePath, "skipped-collectors.json"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var got []SkippedCollector
|
||||
require.NoError(t, json.Unmarshal(diskData, &got))
|
||||
assert.Equal(t, tt.wantEntries, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
@@ -102,14 +103,14 @@ func copyFilesFromPod(ctx context.Context, dstPath string, clientConfig *restcli
|
||||
Command: command,
|
||||
Container: containerName,
|
||||
Stdin: true,
|
||||
Stdout: false,
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
TTY: false,
|
||||
}, parameterCodec)
|
||||
|
||||
exec, err := remotecommand.NewSPDYExecutor(clientConfig, "POST", req.URL())
|
||||
exec, err := k8sutil.NewFallbackExecutor(clientConfig, req.URL())
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "failed to create SPDY executor")
|
||||
return nil, nil, errors.Wrap(err, "failed to create executor")
|
||||
}
|
||||
|
||||
result := NewResult()
|
||||
|
||||
@@ -304,14 +304,14 @@ func copyFilesFromHost(ctx context.Context, dstPath string, clientConfig *restcl
|
||||
Command: command,
|
||||
Container: containerName,
|
||||
Stdin: true,
|
||||
Stdout: false,
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
TTY: false,
|
||||
}, parameterCodec)
|
||||
|
||||
exec, err := remotecommand.NewSPDYExecutor(clientConfig, "POST", req.URL())
|
||||
exec, err := k8sutil.NewFallbackExecutor(clientConfig, req.URL())
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "failed to create SPDY executor")
|
||||
return nil, nil, errors.Wrap(err, "failed to create executor")
|
||||
}
|
||||
|
||||
result := NewResult()
|
||||
|
||||
+2
-1
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
@@ -325,7 +326,7 @@ func (c *etcdDebug) executeCommand(command string) ([]byte, []byte, error) {
|
||||
TTY: false,
|
||||
}, scheme.ParameterCodec)
|
||||
|
||||
exec, err := remotecommand.NewSPDYExecutor(c.clientConfig, "POST", req.URL())
|
||||
exec, err := k8sutil.NewFallbackExecutor(c.clientConfig, req.URL())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
+16
-5
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
@@ -87,16 +88,26 @@ func execWithoutTimeout(clientConfig *rest.Config, bundlePath string, execCollec
|
||||
pod := pods[0]
|
||||
stdout, stderr, execErrors := getExecOutputs(ctx, clientConfig, client, pod, execCollector)
|
||||
|
||||
container := pod.Spec.Containers[0].Name
|
||||
if execCollector.ContainerName != "" {
|
||||
container = execCollector.ContainerName
|
||||
}
|
||||
|
||||
filePrefix := execCollector.CollectorName
|
||||
if filePrefix == "" {
|
||||
filePrefix = container
|
||||
}
|
||||
|
||||
path := filepath.Join(execCollector.Name, pod.Namespace, pod.Name)
|
||||
if len(stdout) > 0 {
|
||||
output.SaveResult(bundlePath, filepath.Join(path, execCollector.CollectorName+"-stdout.txt"), bytes.NewBuffer(stdout))
|
||||
output.SaveResult(bundlePath, filepath.Join(path, filePrefix+"-stdout.txt"), bytes.NewBuffer(stdout))
|
||||
}
|
||||
if len(stderr) > 0 {
|
||||
output.SaveResult(bundlePath, filepath.Join(path, execCollector.CollectorName+"-stderr.txt"), bytes.NewBuffer(stderr))
|
||||
output.SaveResult(bundlePath, filepath.Join(path, filePrefix+"-stderr.txt"), bytes.NewBuffer(stderr))
|
||||
}
|
||||
|
||||
if len(execErrors) > 0 {
|
||||
output.SaveResult(bundlePath, filepath.Join(path, execCollector.CollectorName+"-errors.json"), marshalErrors(execErrors))
|
||||
output.SaveResult(bundlePath, filepath.Join(path, filePrefix+"-errors.json"), marshalErrors(execErrors))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,12 +133,12 @@ func getExecOutputs(
|
||||
Command: append(execCollector.Command, execCollector.Args...),
|
||||
Container: container,
|
||||
Stdin: true,
|
||||
Stdout: false,
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
TTY: false,
|
||||
}, parameterCodec)
|
||||
|
||||
exec, err := remotecommand.NewSPDYExecutor(clientConfig, "POST", req.URL())
|
||||
exec, err := k8sutil.NewFallbackExecutor(clientConfig, req.URL())
|
||||
if err != nil {
|
||||
return nil, nil, []string{err.Error()}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,8 @@ func GetHostCollector(collector *troubleshootv1beta2.HostCollect, bundlePath str
|
||||
return &CollectHostNetworkNamespaceConnectivity{collector.NetworkNamespaceConnectivity, bundlePath}, true
|
||||
case collector.HostSysctl != nil:
|
||||
return &CollectHostSysctl{collector.HostSysctl, bundlePath}, true
|
||||
case collector.RegistryImages != nil:
|
||||
return &CollectHostRegistryImages{collector.RegistryImages, bundlePath}, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package collect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
type CollectHostRegistryImages struct {
|
||||
hostCollector *troubleshootv1beta2.HostRegistryImages
|
||||
BundlePath string
|
||||
}
|
||||
|
||||
func (c *CollectHostRegistryImages) Title() string {
|
||||
return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "Registry Images")
|
||||
}
|
||||
|
||||
func (c *CollectHostRegistryImages) IsExcluded() (bool, error) {
|
||||
return isExcluded(c.hostCollector.Exclude)
|
||||
}
|
||||
|
||||
func (c *CollectHostRegistryImages) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
|
||||
registryInfo := RegistryInfo{
|
||||
Images: map[string]RegistryImage{},
|
||||
}
|
||||
|
||||
auth := c.resolveAuth()
|
||||
if auth != nil {
|
||||
klog.V(2).Infof("using inline credentials for registry check (username=%s)", c.hostCollector.Username)
|
||||
} else {
|
||||
klog.V(2).Info("no inline credentials provided, using ambient auth")
|
||||
}
|
||||
|
||||
klog.V(2).Infof("checking %d images", len(c.hostCollector.Images))
|
||||
for _, image := range c.hostCollector.Images {
|
||||
klog.V(2).Infof("checking image: %s", image)
|
||||
imageRef, err := parseImageRef(image)
|
||||
if err != nil {
|
||||
klog.Errorf("failed to parse image ref %s: %v", image, err)
|
||||
registryInfo.Images[image] = RegistryImage{Error: err.Error()}
|
||||
continue
|
||||
}
|
||||
exists, err := imageExistsWithAuth(auth, imageRef, image, 10*time.Second)
|
||||
if err != nil {
|
||||
klog.Errorf("image check failed for %s: %v", image, err)
|
||||
registryInfo.Images[image] = RegistryImage{Error: err.Error()}
|
||||
} else {
|
||||
klog.V(2).Infof("image %s exists=%t", image, exists)
|
||||
registryInfo.Images[image] = RegistryImage{Exists: exists}
|
||||
}
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(registryInfo, "", " ")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal registry info")
|
||||
}
|
||||
|
||||
collectorName := c.hostCollector.CollectorName
|
||||
if collectorName == "" {
|
||||
collectorName = "images"
|
||||
}
|
||||
|
||||
name := filepath.Join("host-collectors/registry-images", collectorName+".json")
|
||||
|
||||
output := NewResult()
|
||||
output.SaveResult(c.BundlePath, name, bytes.NewBuffer(b))
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (c *CollectHostRegistryImages) RemoteCollect(progressChan chan<- interface{}) (map[string][]byte, error) {
|
||||
return nil, ErrRemoteCollectorNotImplemented
|
||||
}
|
||||
|
||||
// resolveAuth returns auth config from inline credentials or nil for ambient auth.
|
||||
func (c *CollectHostRegistryImages) resolveAuth() *registryAuthConfig {
|
||||
if c.hostCollector.Username != "" {
|
||||
return ®istryAuthConfig{
|
||||
username: c.hostCollector.Username,
|
||||
password: c.hostCollector.Password,
|
||||
}
|
||||
}
|
||||
// No credentials: rely on ambient auth (~/.docker/config.json)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package collect
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCollectHostRegistryImagesTitle(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
meta troubleshootv1beta2.HostCollectorMeta
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "default title",
|
||||
meta: troubleshootv1beta2.HostCollectorMeta{},
|
||||
expected: "Registry Images",
|
||||
},
|
||||
{
|
||||
name: "custom title",
|
||||
meta: troubleshootv1beta2.HostCollectorMeta{
|
||||
CollectorName: "My Registry",
|
||||
},
|
||||
expected: "My Registry",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
c := &CollectHostRegistryImages{
|
||||
hostCollector: &troubleshootv1beta2.HostRegistryImages{
|
||||
HostCollectorMeta: test.meta,
|
||||
},
|
||||
}
|
||||
assert.Equal(t, test.expected, c.Title())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectHostRegistryImagesResolveAuth(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
username string
|
||||
password string
|
||||
expected *registryAuthConfig
|
||||
}{
|
||||
{
|
||||
name: "nil when no credentials",
|
||||
username: "",
|
||||
password: "",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "returns auth with credentials",
|
||||
username: "user",
|
||||
password: "pass",
|
||||
expected: ®istryAuthConfig{
|
||||
username: "user",
|
||||
password: "pass",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "returns auth with username only",
|
||||
username: "user",
|
||||
password: "",
|
||||
expected: ®istryAuthConfig{
|
||||
username: "user",
|
||||
password: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
c := &CollectHostRegistryImages{
|
||||
hostCollector: &troubleshootv1beta2.HostRegistryImages{
|
||||
Username: test.username,
|
||||
Password: test.password,
|
||||
},
|
||||
}
|
||||
result := c.resolveAuth()
|
||||
assert.Equal(t, test.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectHostRegistryImagesRemoteCollect(t *testing.T) {
|
||||
c := &CollectHostRegistryImages{
|
||||
hostCollector: &troubleshootv1beta2.HostRegistryImages{},
|
||||
}
|
||||
result, err := c.RemoteCollect(nil)
|
||||
require.ErrorIs(t, err, ErrRemoteCollectorNotImplemented)
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
longhornv1beta1types "github.com/replicatedhq/troubleshoot/pkg/longhorn/apis/longhorn/v1beta1"
|
||||
longhornv1beta1 "github.com/replicatedhq/troubleshoot/pkg/longhorn/client/clientset/versioned/typed/longhorn/v1beta1"
|
||||
longhorntypes "github.com/replicatedhq/troubleshoot/pkg/longhorn/types"
|
||||
@@ -391,7 +392,7 @@ func GetLonghornReplicaChecksum(clientConfig *rest.Config, replica longhornv1bet
|
||||
Param("command", "-c").
|
||||
Param("command", fmt.Sprintf("if [ -d %s ]; then md5sum %s/*; fi", dir, dir))
|
||||
|
||||
executor, err := remotecommand.NewSPDYExecutor(clientConfig, "POST", req.URL())
|
||||
executor, err := k8sutil.NewFallbackExecutor(clientConfig, req.URL())
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "create remote exec")
|
||||
}
|
||||
|
||||
+83
-55
@@ -3,18 +3,19 @@ package collect
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"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/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
@@ -91,9 +92,9 @@ func (c *CollectRegistry) Collect(progressChan chan<- interface{}) (CollectorRes
|
||||
}
|
||||
|
||||
func imageExists(namespace string, clientConfig *rest.Config, registryCollector *troubleshootv1beta2.RegistryImages, image string, deadline time.Duration) (bool, error) {
|
||||
imageRef, err := alltransports.ParseImageName(fmt.Sprintf("docker://%s", image))
|
||||
imageRef, err := parseImageRef(image)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "failed to parse image name %s", image)
|
||||
return false, err
|
||||
}
|
||||
|
||||
authConfig, err := getImageAuthConfig(namespace, clientConfig, registryCollector, imageRef)
|
||||
@@ -102,16 +103,29 @@ func imageExists(namespace string, clientConfig *rest.Config, registryCollector
|
||||
return false, errors.Wrap(err, "failed to get auth config")
|
||||
}
|
||||
|
||||
sysCtx := types.SystemContext{
|
||||
DockerDisableV1Ping: true,
|
||||
DockerInsecureSkipTLSVerify: types.OptionalBoolTrue,
|
||||
return imageExistsWithAuth(authConfig, imageRef, image, deadline)
|
||||
}
|
||||
|
||||
func parseImageRef(image string) (name.Reference, error) {
|
||||
ref, err := name.ParseReference(image)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse image name %s", image)
|
||||
}
|
||||
if authConfig != nil {
|
||||
sysCtx.DockerAuthConfig = &types.DockerAuthConfig{
|
||||
Username: authConfig.username,
|
||||
Password: authConfig.password,
|
||||
}
|
||||
return ref, nil
|
||||
}
|
||||
|
||||
// imageExistsWithAuth checks if an image exists in a registry using optional auth credentials.
|
||||
// authConfig may be nil for ambient credentials (e.g. ~/.docker/config.json).
|
||||
// This is the shared core used by both the cluster-level and host-level registry collectors.
|
||||
func imageExistsWithAuth(authConfig *registryAuthConfig, ref name.Reference, image string, deadline time.Duration) (bool, error) {
|
||||
// remote.DefaultTransport includes Proxy (HTTP_PROXY/HTTPS_PROXY), dial/TLS
|
||||
// timeouts, and keepalive; clone it so InsecureSkipVerify does not drop those.
|
||||
defaultTR, ok := remote.DefaultTransport.(*http.Transport)
|
||||
if !ok {
|
||||
return false, errors.New("remote.DefaultTransport is not *http.Transport")
|
||||
}
|
||||
insecureTransport := defaultTR.Clone()
|
||||
insecureTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec
|
||||
|
||||
if deadline == 0 {
|
||||
deadline = 10 * time.Second
|
||||
@@ -122,12 +136,30 @@ func imageExists(namespace string, clientConfig *rest.Config, registryCollector
|
||||
err := func() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), deadline)
|
||||
defer cancel()
|
||||
remoteImage, err := imageRef.NewImage(ctx, &sysCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
opts := []remote.Option{
|
||||
remote.WithContext(ctx),
|
||||
remote.WithTransport(insecureTransport),
|
||||
}
|
||||
remoteImage.Close()
|
||||
return nil
|
||||
if authConfig != nil {
|
||||
opts = append(opts, remote.WithAuth(&authn.Basic{
|
||||
Username: authConfig.username,
|
||||
Password: authConfig.password,
|
||||
}))
|
||||
} else {
|
||||
opts = append(opts, remote.WithAuthFromKeychain(authn.DefaultKeychain))
|
||||
}
|
||||
|
||||
// Use Get (not Head) so 404 responses include a JSON body; the registry
|
||||
// API encodes MANIFEST_UNKNOWN vs NAME_UNKNOWN there, which we need to
|
||||
// distinguish. Head 404s typically have no body, so *transport.Error has
|
||||
// empty Errors and we cannot classify the failure.
|
||||
//
|
||||
// Get fetches the manifest (or list/index) for the tag or digest and does
|
||||
// not pick a per-platform child image, so this checks presence only, not
|
||||
// whether the image runs on a given architecture.
|
||||
_, err := remote.Get(ref, opts...)
|
||||
return err
|
||||
}()
|
||||
if err == nil {
|
||||
klog.V(2).Infof("image %s exists", image)
|
||||
@@ -136,18 +168,10 @@ func imageExists(namespace string, clientConfig *rest.Config, registryCollector
|
||||
|
||||
klog.Errorf("failed to get image %s: %v", image, err)
|
||||
|
||||
// if this is a context timeout, stop here so we dont run this check for too long
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
if stderrors.Is(err, context.DeadlineExceeded) {
|
||||
return false, errors.Wrap(err, "failed to get image manifest")
|
||||
}
|
||||
|
||||
if strings.Contains(err.Error(), "no image found in manifest list for architecture") {
|
||||
// manifest was downloaded, but no matching architecture found in manifest
|
||||
// should this count as image does not exist?
|
||||
// this binary's architecture is not necessarily what will run in the cluster
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if isNotFound(err) {
|
||||
return false, nil
|
||||
}
|
||||
@@ -164,7 +188,7 @@ func imageExists(namespace string, clientConfig *rest.Config, registryCollector
|
||||
return false, errors.Wrap(lastErr, "failed to retry")
|
||||
}
|
||||
|
||||
func getImageAuthConfig(namespace string, clientConfig *rest.Config, registryCollector *troubleshootv1beta2.RegistryImages, imageRef types.ImageReference) (*registryAuthConfig, error) {
|
||||
func getImageAuthConfig(namespace string, clientConfig *rest.Config, registryCollector *troubleshootv1beta2.RegistryImages, imageRef name.Reference) (*registryAuthConfig, error) {
|
||||
if registryCollector.ImagePullSecrets == nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -195,13 +219,13 @@ func getImageAuthConfig(namespace string, clientConfig *rest.Config, registryCol
|
||||
return nil, errors.New("image pull secret spec is not valid")
|
||||
}
|
||||
|
||||
func getImageAuthConfigFromData(imageRef types.ImageReference, pullSecrets *v1beta2.ImagePullSecrets) (*registryAuthConfig, error) {
|
||||
func getImageAuthConfigFromData(imageRef name.Reference, pullSecrets *v1beta2.ImagePullSecrets) (*registryAuthConfig, error) {
|
||||
if pullSecrets.SecretType != "kubernetes.io/dockerconfigjson" {
|
||||
return nil, errors.Errorf("secret type is not supported: %s", pullSecrets.SecretType)
|
||||
}
|
||||
|
||||
configJsonBase64 := pullSecrets.Data[".dockerconfigjson"]
|
||||
registry := dockerref.Domain(imageRef.DockerReference())
|
||||
registry := imageRef.Context().RegistryStr()
|
||||
|
||||
configJson, err := base64.StdEncoding.DecodeString(configJsonBase64)
|
||||
if err != nil {
|
||||
@@ -222,8 +246,14 @@ func getImageAuthConfigFromData(imageRef types.ImageReference, pullSecrets *v1be
|
||||
}
|
||||
|
||||
auth, ok := dockerCfgJSON.Auths[registry]
|
||||
// go-containerregistry normalizes "docker.io" to "index.docker.io"
|
||||
// (name.DefaultRegistry); many dockerconfigjson files key on "docker.io"
|
||||
// instead. Fall back to the alias so existing user secrets keep working.
|
||||
if !ok && registry == name.DefaultRegistry {
|
||||
auth, ok = dockerCfgJSON.Auths["docker.io"]
|
||||
}
|
||||
if !ok {
|
||||
// Suport a mix of public and private images
|
||||
// Support a mix of public and private images
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -258,7 +288,7 @@ func getImageAuthConfigFromData(imageRef types.ImageReference, pullSecrets *v1be
|
||||
return &authConfig, nil
|
||||
}
|
||||
|
||||
func getImageAuthConfigFromSecret(clientConfig *rest.Config, imageRef types.ImageReference, pullSecrets *v1beta2.ImagePullSecrets, namespace string) (*registryAuthConfig, error) {
|
||||
func getImageAuthConfigFromSecret(clientConfig *rest.Config, imageRef name.Reference, pullSecrets *v1beta2.ImagePullSecrets, namespace string) (*registryAuthConfig, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
client, err := kubernetes.NewForConfig(clientConfig)
|
||||
@@ -287,30 +317,28 @@ func getImageAuthConfigFromSecret(clientConfig *rest.Config, imageRef types.Imag
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// isNotFound returns true only when the registry reports MANIFEST_UNKNOWN: the
|
||||
// repository exists but the tag or digest has no manifest. A 404 with
|
||||
// NAME_UNKNOWN (repository missing) is not "not found" in that sense; callers
|
||||
// should see the error to diagnose a wrong image path. Unstructured 404s
|
||||
// (e.g. empty body on HEAD) are not treated as a known-missing image.
|
||||
func isNotFound(err error) bool {
|
||||
switch err := err.(type) {
|
||||
case errcode.Errors:
|
||||
for _, e := range err {
|
||||
if isNotFound(e) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
case errcode.Error:
|
||||
return err.Message == registryv2.ErrorCodeManifestUnknown.Message()
|
||||
}
|
||||
|
||||
// this type will cause panic when compared to error type
|
||||
if _, ok := err.(imagedocker.ErrUnauthorizedForCredentials); ok {
|
||||
var terr *transport.Error
|
||||
if !stderrors.As(err, &terr) || terr.StatusCode != http.StatusNotFound {
|
||||
return false
|
||||
}
|
||||
|
||||
cause := errors.Cause(err)
|
||||
if cause, ok := cause.(error); ok {
|
||||
if cause == err {
|
||||
if len(terr.Errors) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, d := range terr.Errors {
|
||||
if d.Code == transport.NameUnknownErrorCode {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return isNotFound(cause)
|
||||
for _, d := range terr.Errors {
|
||||
if d.Code == transport.ManifestUnknownErrorCode {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -5,15 +5,55 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/containers/image/v5/transports/alltransports"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// fakeRegistry is a minimal Docker Registry v2 TLS stand-in for unit testing
|
||||
// imageExists. The handler returns whatever the caller puts in `manifest`
|
||||
// for /v2/{name}/manifests/{ref}. /v2/ is always a 200.
|
||||
//
|
||||
// We use NewTLSServer so the same tests work against both the current
|
||||
// containers/image implementation (DockerInsecureSkipTLSVerify) and the
|
||||
// new go-containerregistry implementation (InsecureSkipVerify transport).
|
||||
// Plain HTTP would break after Task 3 because go-containerregistry does not
|
||||
// treat 127.0.0.1 as an insecure registry by default.
|
||||
type fakeRegistry struct {
|
||||
server *httptest.Server
|
||||
manifest http.HandlerFunc
|
||||
}
|
||||
|
||||
func newFakeRegistry(t *testing.T, manifest http.HandlerFunc) *fakeRegistry {
|
||||
t.Helper()
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/v2/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/v2/" || r.URL.Path == "/v2" {
|
||||
w.Header().Set("Docker-Distribution-Api-Version", "registry/2.0")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
manifest(w, r)
|
||||
})
|
||||
srv := httptest.NewTLSServer(mux)
|
||||
t.Cleanup(srv.Close)
|
||||
return &fakeRegistry{server: srv, manifest: manifest}
|
||||
}
|
||||
|
||||
// hostPort strips "https://" from the test server URL, leaving "127.0.0.1:NNNN"
|
||||
// suitable for use as the registry portion of an image reference.
|
||||
func (f *fakeRegistry) hostPort() string {
|
||||
return strings.TrimPrefix(f.server.URL, "https://")
|
||||
}
|
||||
|
||||
func TestGetImageAuthConfigFromData(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -57,7 +97,7 @@ func TestGetImageAuthConfigFromData(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
imageRef, err := alltransports.ParseImageName(fmt.Sprintf("docker://%s", test.imageName))
|
||||
imageRef, err := name.ParseReference(test.imageName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
pullSecrets := &v1beta2.ImagePullSecrets{
|
||||
@@ -119,3 +159,101 @@ func TestImageExists_ContextDeadlineExceeded(t *testing.T) {
|
||||
assert.ErrorIs(t, err, context.DeadlineExceeded)
|
||||
assert.False(t, exists)
|
||||
}
|
||||
|
||||
func TestImageExists_Found(t *testing.T) {
|
||||
fr := newFakeRegistry(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
|
||||
w.Header().Set("Docker-Content-Digest", "sha256:1111111111111111111111111111111111111111111111111111111111111111")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","size":1,"digest":"sha256:2222222222222222222222222222222222222222222222222222222222222222"},"layers":[]}`))
|
||||
})
|
||||
|
||||
collector := &v1beta2.RegistryImages{
|
||||
Images: []string{fmt.Sprintf("%s/test:latest", fr.hostPort())},
|
||||
}
|
||||
exists, err := imageExists("default", &rest.Config{}, collector, fmt.Sprintf("%s/test:latest", fr.hostPort()), 5*time.Second)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exists)
|
||||
}
|
||||
|
||||
func TestImageExists_NotFound(t *testing.T) {
|
||||
fr := newFakeRegistry(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = w.Write([]byte(`{"errors":[{"code":"MANIFEST_UNKNOWN","message":"manifest unknown"}]}`))
|
||||
})
|
||||
|
||||
collector := &v1beta2.RegistryImages{
|
||||
Images: []string{fmt.Sprintf("%s/test:latest", fr.hostPort())},
|
||||
}
|
||||
exists, err := imageExists("default", &rest.Config{}, collector, fmt.Sprintf("%s/test:latest", fr.hostPort()), 5*time.Second)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, exists)
|
||||
}
|
||||
|
||||
func TestImageExists_NameUnknown_PropagatesError(t *testing.T) {
|
||||
fr := newFakeRegistry(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = w.Write([]byte(`{"errors":[{"code":"NAME_UNKNOWN","message":"repository name not known to registry"}]}`))
|
||||
})
|
||||
|
||||
collector := &v1beta2.RegistryImages{
|
||||
Images: []string{fmt.Sprintf("%s/nonexistentrepo/image:latest", fr.hostPort())},
|
||||
}
|
||||
exists, err := imageExists("default", &rest.Config{}, collector, fmt.Sprintf("%s/nonexistentrepo/image:latest", fr.hostPort()), 5*time.Second)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.False(t, exists)
|
||||
}
|
||||
|
||||
func TestImageExists_Unauthorized(t *testing.T) {
|
||||
fr := newFakeRegistry(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Www-Authenticate", `Basic realm="registry"`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_, _ = w.Write([]byte(`{"errors":[{"code":"UNAUTHORIZED","message":"authentication required"}]}`))
|
||||
})
|
||||
|
||||
collector := &v1beta2.RegistryImages{
|
||||
Images: []string{fmt.Sprintf("%s/test:latest", fr.hostPort())},
|
||||
}
|
||||
exists, err := imageExists("default", &rest.Config{}, collector, fmt.Sprintf("%s/test:latest", fr.hostPort()), 5*time.Second)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.False(t, exists)
|
||||
}
|
||||
|
||||
func TestImageExists_RetriesOnEOF(t *testing.T) {
|
||||
var attempts int32
|
||||
fr := newFakeRegistry(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
n := atomic.AddInt32(&attempts, 1)
|
||||
if n < 3 {
|
||||
// hijack the connection and slam it shut to cause an EOF on the client
|
||||
hj, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
t.Fatalf("response writer does not support hijacking")
|
||||
}
|
||||
conn, _, err := hj.Hijack()
|
||||
if err != nil {
|
||||
t.Fatalf("hijack: %v", err)
|
||||
}
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
|
||||
w.Header().Set("Docker-Content-Digest", "sha256:1111111111111111111111111111111111111111111111111111111111111111")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","size":1,"digest":"sha256:2222222222222222222222222222222222222222222222222222222222222222"},"layers":[]}`))
|
||||
})
|
||||
|
||||
collector := &v1beta2.RegistryImages{
|
||||
Images: []string{fmt.Sprintf("%s/test:latest", fr.hostPort())},
|
||||
}
|
||||
exists, err := imageExists("default", &rest.Config{}, collector, fmt.Sprintf("%s/test:latest", fr.hostPort()), 5*time.Second)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exists)
|
||||
assert.GreaterOrEqual(t, atomic.LoadInt32(&attempts), int32(3))
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@@ -87,9 +88,7 @@ func (r CollectorResult) SymLinkResult(bundlePath, relativeLinkPath, relativeFil
|
||||
// It also ensures that when operating on the results in memory (e.g preflights),
|
||||
// all files are included.
|
||||
func (r CollectorResult) AddResult(other CollectorResult) {
|
||||
for k, v := range other {
|
||||
r[k] = v
|
||||
}
|
||||
maps.Copy(r, other)
|
||||
}
|
||||
|
||||
// SaveResult saves the collector result to relativePath file on disk. If bundlePath is
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package collect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"crypto/tls"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type S3StatusResult struct {
|
||||
BucketName string `json:"bucketName"`
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
IsConnected bool `json:"isConnected"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type CollectS3Status struct {
|
||||
Collector *troubleshootv1beta2.S3Status
|
||||
BundlePath string
|
||||
RBACErrors
|
||||
}
|
||||
|
||||
func (c *CollectS3Status) Title() string {
|
||||
return getCollectorName(c)
|
||||
}
|
||||
|
||||
func (c *CollectS3Status) IsExcluded() (bool, error) {
|
||||
return isExcluded(c.Collector.Exclude)
|
||||
}
|
||||
|
||||
func (c *CollectS3Status) Collect(progressChan chan<- interface{}) (CollectorResult, error) {
|
||||
result := S3StatusResult{
|
||||
BucketName: c.Collector.BucketName,
|
||||
Endpoint: c.Collector.Endpoint,
|
||||
Region: c.Collector.Region,
|
||||
}
|
||||
|
||||
region := c.Collector.Region
|
||||
if region == "" {
|
||||
region = "us-east-1"
|
||||
}
|
||||
|
||||
opts := s3.Options{
|
||||
Region: region,
|
||||
Credentials: credentials.NewStaticCredentialsProvider(
|
||||
c.Collector.AccessKeyID,
|
||||
c.Collector.SecretAccessKey,
|
||||
"",
|
||||
),
|
||||
UsePathStyle: c.Collector.UsePathStyle,
|
||||
}
|
||||
|
||||
if c.Collector.Endpoint != "" {
|
||||
opts.BaseEndpoint = aws.String(c.Collector.Endpoint)
|
||||
}
|
||||
|
||||
if c.Collector.Insecure {
|
||||
opts.HTTPClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
client := s3.New(opts)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := client.HeadBucket(ctx, &s3.HeadBucketInput{
|
||||
Bucket: aws.String(c.Collector.BucketName),
|
||||
})
|
||||
if err != nil {
|
||||
result.Error = err.Error()
|
||||
} else {
|
||||
result.IsConnected = true
|
||||
}
|
||||
|
||||
b, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal s3 status result")
|
||||
}
|
||||
|
||||
collectorName := c.Collector.CollectorName
|
||||
if collectorName == "" {
|
||||
collectorName = "s3Status"
|
||||
}
|
||||
|
||||
output := NewResult()
|
||||
output.SaveResult(c.BundlePath, fmt.Sprintf("s3Status/%s.json", collectorName), bytes.NewBuffer(b))
|
||||
|
||||
return output, nil
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package collect
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCollectS3Status_Collect(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
handler http.HandlerFunc
|
||||
collectorName string
|
||||
wantConnected bool
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "bucket exists",
|
||||
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodHead && r.URL.Path == "/test-bucket" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}),
|
||||
collectorName: "mybucket",
|
||||
wantConnected: true,
|
||||
},
|
||||
{
|
||||
name: "bucket not found",
|
||||
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}),
|
||||
collectorName: "mybucket",
|
||||
wantConnected: false,
|
||||
wantErrContains: "StatusCode: 404",
|
||||
},
|
||||
{
|
||||
name: "access denied",
|
||||
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
}),
|
||||
collectorName: "mybucket",
|
||||
wantConnected: false,
|
||||
wantErrContains: "StatusCode: 403",
|
||||
},
|
||||
{
|
||||
name: "default collector name",
|
||||
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}),
|
||||
collectorName: "",
|
||||
wantConnected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ts := httptest.NewServer(tt.handler)
|
||||
defer ts.Close()
|
||||
|
||||
collector := &CollectS3Status{
|
||||
Collector: &troubleshootv1beta2.S3Status{
|
||||
CollectorMeta: troubleshootv1beta2.CollectorMeta{
|
||||
CollectorName: tt.collectorName,
|
||||
},
|
||||
BucketName: "test-bucket",
|
||||
Endpoint: ts.URL,
|
||||
Region: "us-east-1",
|
||||
AccessKeyID: "test-key",
|
||||
SecretAccessKey: "test-secret",
|
||||
UsePathStyle: true,
|
||||
},
|
||||
BundlePath: "",
|
||||
}
|
||||
|
||||
result, err := collector.Collect(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedName := tt.collectorName
|
||||
if expectedName == "" {
|
||||
expectedName = "s3Status"
|
||||
}
|
||||
|
||||
key := "s3Status/" + expectedName + ".json"
|
||||
raw, ok := result[key]
|
||||
require.True(t, ok, "expected key %s in result, got keys: %v", key, result)
|
||||
|
||||
var s3Result S3StatusResult
|
||||
err = json.Unmarshal(raw, &s3Result)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wantConnected, s3Result.IsConnected)
|
||||
assert.Equal(t, "test-bucket", s3Result.BucketName)
|
||||
|
||||
if tt.wantErrContains != "" {
|
||||
assert.Contains(t, s3Result.Error, tt.wantErrContains)
|
||||
} else {
|
||||
assert.Empty(t, s3Result.Error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -147,7 +148,7 @@ func sonobuoyRetrieveResults(
|
||||
Stdout: true,
|
||||
Stderr: false,
|
||||
}, scheme.ParameterCodec)
|
||||
executor, err := remotecommand.NewSPDYExecutor(restConfig, "POST", req.URL())
|
||||
executor, err := k8sutil.NewFallbackExecutor(restConfig, req.URL())
|
||||
if err != nil {
|
||||
return nil, ec, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package collect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
type CollectSupportBundleMetadata struct {
|
||||
Collector *troubleshootv1beta2.SupportBundleMetadata
|
||||
BundlePath string
|
||||
Namespace string
|
||||
ClientConfig *rest.Config
|
||||
Client kubernetes.Interface
|
||||
Context context.Context
|
||||
RBACErrors
|
||||
}
|
||||
|
||||
func (c *CollectSupportBundleMetadata) Title() string {
|
||||
return getCollectorName(c)
|
||||
}
|
||||
|
||||
func (c *CollectSupportBundleMetadata) IsExcluded() (bool, error) {
|
||||
return isExcluded(c.Collector.Exclude)
|
||||
}
|
||||
|
||||
func (c *CollectSupportBundleMetadata) Collect(progressChan chan<- interface{}) (CollectorResult, error) {
|
||||
output := NewResult()
|
||||
|
||||
const secretName = "replicated-support-metadata"
|
||||
secret, err := c.Client.CoreV1().Secrets(c.Collector.Namespace).Get(c.Context, secretName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return output, errors.Wrapf(err, "failed to get secret %s/%s", c.Collector.Namespace, secretName)
|
||||
}
|
||||
|
||||
metadata := make(map[string]string)
|
||||
for k, v := range secret.Data {
|
||||
metadata[k] = string(v)
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(metadata, "", " ")
|
||||
if err != nil {
|
||||
return output, errors.Wrap(err, "failed to marshal metadata")
|
||||
}
|
||||
|
||||
output.SaveResult(c.BundlePath, "metadata/cluster.json", bytes.NewBuffer(b))
|
||||
return output, nil
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package collect
|
||||
|
||||
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"
|
||||
testclient "k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestCollectSupportBundleMetadata(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
collector *troubleshootv1beta2.SupportBundleMetadata
|
||||
mockSecrets []corev1.Secret
|
||||
want CollectorResult
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "reads all data fields from secret",
|
||||
collector: &troubleshootv1beta2.SupportBundleMetadata{
|
||||
Namespace: "test-ns",
|
||||
},
|
||||
mockSecrets: []corev1.Secret{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "replicated-support-metadata",
|
||||
Namespace: "test-ns",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"mykey": []byte("myvalue"),
|
||||
"myversion": []byte("1.0.0-example"),
|
||||
"numCrashes": []byte("57"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: CollectorResult{
|
||||
"metadata/cluster.json": mustJSONMarshalIndent(t, map[string]string{
|
||||
"mykey": "myvalue",
|
||||
"myversion": "1.0.0-example",
|
||||
"numCrashes": "57",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty data map",
|
||||
collector: &troubleshootv1beta2.SupportBundleMetadata{
|
||||
Namespace: "test-ns",
|
||||
},
|
||||
mockSecrets: []corev1.Secret{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "replicated-support-metadata",
|
||||
Namespace: "test-ns",
|
||||
},
|
||||
Data: map[string][]byte{},
|
||||
},
|
||||
},
|
||||
want: CollectorResult{
|
||||
"metadata/cluster.json": mustJSONMarshalIndent(t, map[string]string{}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "secret not found returns error",
|
||||
collector: &troubleshootv1beta2.SupportBundleMetadata{
|
||||
Namespace: "test-ns",
|
||||
},
|
||||
mockSecrets: []corev1.Secret{},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := testclient.NewSimpleClientset()
|
||||
for _, secret := range tt.mockSecrets {
|
||||
_, err := client.CoreV1().Secrets(secret.Namespace).Create(ctx, &secret, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
c := &CollectSupportBundleMetadata{tt.collector, "", "", nil, client, ctx, nil}
|
||||
got, err := c.Collect(nil)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+43
-39
@@ -23,45 +23,49 @@ const (
|
||||
ANALYSIS_FILENAME = "analysis.json"
|
||||
|
||||
// Cluster Resources Collector Directories
|
||||
CLUSTER_RESOURCES_DIR = "cluster-resources"
|
||||
CLUSTER_RESOURCES_NAMESPACES = "namespaces"
|
||||
CLUSTER_RESOURCES_AUTH_CANI = "auth-cani-list"
|
||||
CLUSTER_RESOURCES_PODS = "pods"
|
||||
CLUSTER_RESOURCES_PODS_LOGS = "pods/logs"
|
||||
CLUSTER_RESOURCES_POD_DISRUPTION_BUDGETS = "pod-disruption-budgets"
|
||||
CLUSTER_RESOURCES_SERVICES = "services"
|
||||
CLUSTER_RESOURCES_DEPLOYMENTS = "deployments"
|
||||
CLUSTER_RESOURCES_REPLICASETS = "replicasets"
|
||||
CLUSTER_RESOURCES_STATEFULSETS = "statefulsets"
|
||||
CLUSTER_RESOURCES_DAEMONSETS = "daemonsets"
|
||||
CLUSTER_RESOURCES_JOBS = "jobs"
|
||||
CLUSTER_RESOURCES_CRONJOBS = "cronjobs"
|
||||
CLUSTER_RESOURCES_INGRESS = "ingress"
|
||||
CLUSTER_RESOURCES_NETWORK_POLICY = "network-policy"
|
||||
CLUSTER_RESOURCES_RESOURCE_QUOTA = "resource-quota"
|
||||
CLUSTER_RESOURCES_STORAGE_CLASS = "storage-classes"
|
||||
CLUSTER_RESOURCES_CUSTOM_RESOURCE_DEFINITIONS = "custom-resource-definitions"
|
||||
CLUSTER_RESOURCES_CUSTOM_RESOURCES = "custom-resources"
|
||||
CLUSTER_RESOURCES_IMAGE_PULL_SECRETS = "image-pull-secrets" // nolint:gosec
|
||||
CLUSTER_RESOURCES_NODES = "nodes"
|
||||
CLUSTER_RESOURCES_GROUPS = "groups"
|
||||
CLUSTER_RESOURCES_RESOURCES = "resources"
|
||||
CLUSTER_RESOURCES_LIMITRANGES = "limitranges"
|
||||
CLUSTER_RESOURCES_EVENTS = "events"
|
||||
CLUSTER_RESOURCES_PVS = "pvs"
|
||||
CLUSTER_RESOURCES_PVCS = "pvcs"
|
||||
CLUSTER_RESOURCES_ROLES = "roles"
|
||||
CLUSTER_RESOURCES_ROLE_BINDINGS = "rolebindings"
|
||||
CLUSTER_RESOURCES_CLUSTER_ROLES = "clusterroles"
|
||||
CLUSTER_RESOURCES_CLUSTER_ROLE_BINDINGS = "clusterrolebindings"
|
||||
CLUSTER_RESOURCES_PRIORITY_CLASS = "priorityclasses"
|
||||
CLUSTER_RESOURCES_ENDPOINTS = "endpoints"
|
||||
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"
|
||||
CLUSTER_RESOURCES_DIR = "cluster-resources"
|
||||
CLUSTER_RESOURCES_NAMESPACES = "namespaces"
|
||||
CLUSTER_RESOURCES_AUTH_CANI = "auth-cani-list"
|
||||
CLUSTER_RESOURCES_PODS = "pods"
|
||||
CLUSTER_RESOURCES_PODS_LOGS = "pods/logs"
|
||||
CLUSTER_RESOURCES_POD_DISRUPTION_BUDGETS = "pod-disruption-budgets"
|
||||
CLUSTER_RESOURCES_SERVICES = "services"
|
||||
CLUSTER_RESOURCES_DEPLOYMENTS = "deployments"
|
||||
CLUSTER_RESOURCES_REPLICASETS = "replicasets"
|
||||
CLUSTER_RESOURCES_STATEFULSETS = "statefulsets"
|
||||
CLUSTER_RESOURCES_DAEMONSETS = "daemonsets"
|
||||
CLUSTER_RESOURCES_JOBS = "jobs"
|
||||
CLUSTER_RESOURCES_CRONJOBS = "cronjobs"
|
||||
CLUSTER_RESOURCES_INGRESS = "ingress"
|
||||
CLUSTER_RESOURCES_NETWORK_POLICY = "network-policy"
|
||||
CLUSTER_RESOURCES_RESOURCE_QUOTA = "resource-quota"
|
||||
CLUSTER_RESOURCES_STORAGE_CLASS = "storage-classes"
|
||||
CLUSTER_RESOURCES_CUSTOM_RESOURCE_DEFINITIONS = "custom-resource-definitions"
|
||||
CLUSTER_RESOURCES_CUSTOM_RESOURCES = "custom-resources"
|
||||
CLUSTER_RESOURCES_IMAGE_PULL_SECRETS = "image-pull-secrets" // nolint:gosec
|
||||
CLUSTER_RESOURCES_NODES = "nodes"
|
||||
CLUSTER_RESOURCES_GROUPS = "groups"
|
||||
CLUSTER_RESOURCES_RESOURCES = "resources"
|
||||
CLUSTER_RESOURCES_LIMITRANGES = "limitranges"
|
||||
CLUSTER_RESOURCES_EVENTS = "events"
|
||||
CLUSTER_RESOURCES_PVS = "pvs"
|
||||
CLUSTER_RESOURCES_PVCS = "pvcs"
|
||||
CLUSTER_RESOURCES_ROLES = "roles"
|
||||
CLUSTER_RESOURCES_ROLE_BINDINGS = "rolebindings"
|
||||
CLUSTER_RESOURCES_CLUSTER_ROLES = "clusterroles"
|
||||
CLUSTER_RESOURCES_CLUSTER_ROLE_BINDINGS = "clusterrolebindings"
|
||||
CLUSTER_RESOURCES_PRIORITY_CLASS = "priorityclasses"
|
||||
CLUSTER_RESOURCES_ENDPOINTS = "endpoints"
|
||||
CLUSTER_RESOURCES_ENDPOINTSLICES = "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"
|
||||
CLUSTER_RESOURCES_CERTIFICATE_SIGNING_REQUESTS = "certificatesigningrequests"
|
||||
CLUSTER_RESOURCES_INGRESS_CLASS = "ingress-classes"
|
||||
CLUSTER_RESOURCES_VALIDATING_WEBHOOK_CONFIGURATIONS = "validating-webhook-configurations"
|
||||
CLUSTER_RESOURCES_MUTATING_WEBHOOK_CONFIGURATIONS = "mutating-webhook-configurations"
|
||||
|
||||
// SelfSubjectRulesReview evaluation responses
|
||||
SELFSUBJECTRULESREVIEW_ERROR_AUTHORIZATION_WEBHOOK_UNSUPPORTED = "webhook authorizer does not support user rule resolution"
|
||||
|
||||
@@ -6,11 +6,15 @@ 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 err != nil {
|
||||
|
||||
if apiLists == nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Compare the resource api version and kind and find the resource.
|
||||
for _, apiList := range apiLists {
|
||||
if apiList.GroupVersion == apiVersion {
|
||||
@@ -21,5 +25,6 @@ func HasResource(dc discovery.DiscoveryInterface, apiVersion, kind string) (bool
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
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"
|
||||
)
|
||||
@@ -78,3 +81,232 @@ 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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package k8sutil
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/httpstream"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
)
|
||||
|
||||
// NewFallbackExecutor creates an executor that tries WebSocket first and falls
|
||||
// back to SPDY if the server does not support it. Use this in place of
|
||||
// remotecommand.NewSPDYExecutor everywhere.
|
||||
func NewFallbackExecutor(config *restclient.Config, u *url.URL) (remotecommand.Executor, error) {
|
||||
// WebSocket upgrade requires GET per RFC 6455; SPDY uses POST.
|
||||
wsExec, err := remotecommand.NewWebSocketExecutor(config, "GET", u.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
spdyExec, err := remotecommand.NewSPDYExecutor(config, "POST", u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shouldFallback := func(err error) bool {
|
||||
return httpstream.IsUpgradeFailure(err) || httpstream.IsHTTPSProxyError(err)
|
||||
}
|
||||
return remotecommand.NewFallbackExecutor(wsExec, spdyExec, shouldFallback)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package k8sutil
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
func TestNewFallbackExecutor(t *testing.T) {
|
||||
config := &restclient.Config{Host: "http://localhost:8080"}
|
||||
u, err := url.Parse("http://localhost:8080/api/v1/namespaces/default/pods/foo/exec")
|
||||
require.NoError(t, err)
|
||||
|
||||
exec, err := NewFallbackExecutor(config, u)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, exec)
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package k8sutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/portforward"
|
||||
"k8s.io/client-go/transport/spdy"
|
||||
)
|
||||
|
||||
func PortForward(config *restclient.Config, localPort int, remotePort int, namespace string, podName string) (chan struct{}, error) {
|
||||
roundTripper, upgrader, err := spdy.RoundTripperFor(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/portforward", namespace, podName)
|
||||
hostIP := strings.TrimLeft(config.Host, "htps:/")
|
||||
serverURL := url.URL{Scheme: "http", Path: path, Host: hostIP}
|
||||
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: roundTripper}, http.MethodPost, &serverURL)
|
||||
|
||||
stopChan, readyChan := make(chan struct{}, 1), make(chan struct{}, 1)
|
||||
out, errOut := new(bytes.Buffer), new(bytes.Buffer)
|
||||
|
||||
forwarder, err := portforward.New(dialer, []string{fmt.Sprintf("%d:%d", localPort, remotePort)}, stopChan, readyChan, out, errOut)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
for range readyChan { // Kubernetes will close this channel when it has something to tell us.
|
||||
}
|
||||
if errOut.String() != "" {
|
||||
panic(errOut.String())
|
||||
} else if out.String() != "" {
|
||||
// fmt.Println(out.String())
|
||||
}
|
||||
}()
|
||||
|
||||
go func() error {
|
||||
if err = forwarder.ForwardPorts(); err != nil { // Locks until stopChan is closed.
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
// Block until the new service is responding, limited to (math) seconds
|
||||
quickClient := &http.Client{
|
||||
Timeout: time.Millisecond * 200,
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
for {
|
||||
response, err := quickClient.Get(fmt.Sprintf("http://localhost:%d", localPort))
|
||||
if err == nil && response.StatusCode == http.StatusOK {
|
||||
break
|
||||
}
|
||||
if time.Now().Sub(start) > time.Duration(time.Second*5) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
}
|
||||
|
||||
return stopChan, nil
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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
|
||||
}
|
||||
+92
-55
@@ -13,11 +13,12 @@ import (
|
||||
"github.com/replicatedhq/troubleshoot/internal/util"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/version"
|
||||
"k8s.io/klog/v2"
|
||||
"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"
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -75,66 +76,97 @@ func PullSpecsFromOCI(ctx context.Context, uri string) ([]string, error) {
|
||||
}
|
||||
|
||||
func pullFromOCI(ctx context.Context, uri string, mediaType string, imageName string) ([]byte, error) {
|
||||
// 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)
|
||||
// Parse the URI to get the repository reference
|
||||
ref, err := parseURI(uri, imageName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
klog.V(1).Infof("Pulling spec from %q OCI uri", parsedRef)
|
||||
klog.V(1).Infof("Pulling spec from %q OCI uri", ref)
|
||||
|
||||
manifest, err := oras.Copy(ctx, registryStore, parsedRef, memoryStore, "",
|
||||
oras.WithPullEmptyNameAllowed(),
|
||||
oras.WithAllowedMediaTypes(allowedMediaTypes),
|
||||
oras.WithLayerDescriptors(func(l []ocispec.Descriptor) {
|
||||
layers = l
|
||||
}))
|
||||
// Create a repository instance
|
||||
repo, err := remote.NewRepository(ref)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
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") {
|
||||
return nil, ErrNoRelease
|
||||
}
|
||||
|
||||
return nil, errors.Wrap(err, "failed to copy")
|
||||
}
|
||||
|
||||
descriptors = append(descriptors, manifest)
|
||||
descriptors := []ocispec.Descriptor{manifest}
|
||||
descriptors = append(descriptors, layers...)
|
||||
|
||||
// expect 2 descriptors
|
||||
// expect 2 descriptors (manifest + one layer)
|
||||
if len(descriptors) != 2 {
|
||||
return nil, fmt.Errorf("expected 2 descriptor, got %d", len(descriptors))
|
||||
return nil, fmt.Errorf("expected 2 descriptors, got %d", len(descriptors))
|
||||
}
|
||||
|
||||
var matchingDescriptor *ocispec.Descriptor
|
||||
|
||||
for _, descriptor := range descriptors {
|
||||
d := descriptor
|
||||
switch d.MediaType {
|
||||
case mediaType:
|
||||
if d.MediaType == mediaType {
|
||||
matchingDescriptor = &d
|
||||
}
|
||||
}
|
||||
@@ -143,9 +175,17 @@ func pullFromOCI(ctx context.Context, uri string, mediaType string, imageName st
|
||||
return nil, fmt.Errorf("no descriptor found with media type: %s", mediaType)
|
||||
}
|
||||
|
||||
_, matchingSpec, ok := memoryStore.Get(*matchingDescriptor)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to get matching descriptor")
|
||||
// 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")
|
||||
}
|
||||
|
||||
return matchingSpec, nil
|
||||
@@ -172,12 +212,9 @@ func parseURI(in, imageName string) (string, error) {
|
||||
tag = uriParts[1]
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s%s/%s:%s", u.Host, uriParts[0], imageName, tag) // <host>:<port>/path/<imageName>:tag
|
||||
// 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)
|
||||
|
||||
parsedRef, err := registry.ParseReference(uri)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to parse OCI uri reference")
|
||||
}
|
||||
|
||||
return parsedRef.String(), nil
|
||||
return uri, nil
|
||||
}
|
||||
|
||||
@@ -183,6 +183,7 @@ 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 {
|
||||
@@ -193,6 +194,9 @@ 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)
|
||||
}
|
||||
}
|
||||
@@ -200,7 +204,9 @@ func CollectWithContext(ctx context.Context, opts CollectOpts, p *troubleshootv1
|
||||
|
||||
collectorList := map[string]CollectorStatus{}
|
||||
|
||||
for _, collectors := range allCollectorsMap {
|
||||
// Iterate over collector types in the order they appeared in collectSpecs
|
||||
for _, collectorType := range collectorTypeOrder {
|
||||
collectors := allCollectorsMap[collectorType]
|
||||
if mergeCollector, ok := collectors[0].(collect.MergeableCollector); ok {
|
||||
mergedCollectors, err := mergeCollector.Merge(collectors)
|
||||
if err != nil {
|
||||
@@ -239,6 +245,8 @@ func CollectWithContext(ctx context.Context, opts CollectOpts, p *troubleshootv1
|
||||
// move Copy Collectors if any to the end of the execution list
|
||||
allCollectors = collect.EnsureCopyLast(allCollectors)
|
||||
|
||||
var skippedCollectors []collect.SkippedCollector
|
||||
|
||||
for i, collector := range allCollectors {
|
||||
_, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, collector.Title())
|
||||
span.SetAttributes(attribute.String("type", reflect.TypeOf(collector).String()))
|
||||
@@ -248,6 +256,13 @@ func CollectWithContext(ctx context.Context, opts CollectOpts, p *troubleshootv1
|
||||
klog.V(1).Infof("excluding %q collector", collector.Title())
|
||||
span.SetAttributes(attribute.Bool(constants.EXCLUDED, true))
|
||||
span.End()
|
||||
|
||||
skippedCollectors = append(skippedCollectors, collect.SkippedCollector{
|
||||
Collector: collector.Title(),
|
||||
Reason: "excluded",
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -264,6 +279,19 @@ func CollectWithContext(ctx context.Context, opts CollectOpts, p *troubleshootv1
|
||||
}
|
||||
span.SetStatus(codes.Error, "skipping collector, insufficient RBAC permissions")
|
||||
span.End()
|
||||
|
||||
rbacErrors := collector.GetRBACErrors()
|
||||
errorMessages := make([]string, 0, len(rbacErrors))
|
||||
for _, e := range rbacErrors {
|
||||
errorMessages = append(errorMessages, e.Error())
|
||||
}
|
||||
skippedCollectors = append(skippedCollectors, collect.SkippedCollector{
|
||||
Collector: collector.Title(),
|
||||
Reason: "insufficient RBAC permissions",
|
||||
Errors: errorMessages,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -314,6 +342,9 @@ func CollectWithContext(ctx context.Context, opts CollectOpts, p *troubleshootv1
|
||||
span.End()
|
||||
}
|
||||
|
||||
// Write skipped collectors manifest so users can see what was missed
|
||||
collect.WriteSkippedCollectors(skippedCollectors, allCollectedData, opts.BundlePath)
|
||||
|
||||
// The values of map entries will contain the collected data in bytes if the data was not stored to disk
|
||||
collectResult.AllCollectedData = allCollectedData
|
||||
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
-14
@@ -3,6 +3,7 @@ package redact
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
@@ -25,6 +26,12 @@ 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()
|
||||
|
||||
@@ -34,7 +41,8 @@ func (r literalRedactor) Redact(input io.Reader, path string) io.Reader {
|
||||
if err == nil || err == io.EOF {
|
||||
writer.Close()
|
||||
} else {
|
||||
if err == bufio.ErrTooLong {
|
||||
// Check if error is about line exceeding maximum size
|
||||
if errors.Is(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 {
|
||||
@@ -44,17 +52,24 @@ func (r literalRedactor) Redact(input io.Reader, path string) io.Reader {
|
||||
}
|
||||
}()
|
||||
|
||||
buf := make([]byte, constants.BUF_INIT_SIZE)
|
||||
scanner := bufio.NewScanner(input)
|
||||
scanner.Buffer(buf, constants.SCANNER_MAX_SIZE)
|
||||
|
||||
// Use LineReader instead of bufio.Scanner to track newline presence
|
||||
lineReader := NewLineReader(input)
|
||||
tokenizer := GetGlobalTokenizer()
|
||||
lineNum := 0
|
||||
for scanner.Scan() {
|
||||
lineNum++
|
||||
line := scanner.Bytes()
|
||||
|
||||
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
|
||||
lineNum++
|
||||
|
||||
// Perform literal string replacement
|
||||
var clean []byte
|
||||
tokenizer := GetGlobalTokenizer()
|
||||
if tokenizer.IsEnabled() {
|
||||
// For literal redaction, we tokenize the matched value
|
||||
matchStr := string(r.match)
|
||||
@@ -66,12 +81,20 @@ func (r literalRedactor) Redact(input io.Reader, path string) io.Reader {
|
||||
clean = bytes.ReplaceAll(line, r.match, maskTextBytes)
|
||||
}
|
||||
|
||||
// Append newline since scanner strips it
|
||||
err = writeBytes(writer, clean, NEW_LINE)
|
||||
// Write the line (redacted or original)
|
||||
err = writeBytes(writer, clean)
|
||||
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,
|
||||
@@ -81,9 +104,16 @@ func (r literalRedactor) Redact(input io.Reader, path string) io.Reader {
|
||||
IsDefaultRedactor: r.isDefault,
|
||||
})
|
||||
}
|
||||
}
|
||||
if scanErr := scanner.Err(); scanErr != nil {
|
||||
err = scanErr
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}()
|
||||
return out
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
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))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user