mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-08-27 00:37:20 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2192aecf40 | ||
|
|
5bf008f7d2 | ||
|
|
0c6fb2177b | ||
|
|
bea9c2220d | ||
|
|
207cb90b53 | ||
|
|
0bb4a21f22 | ||
|
|
fb82fef6e0 | ||
|
|
3f3906fea1 | ||
|
|
1f1a21b3cf | ||
|
|
e828c21545 | ||
|
|
b158f7d79f | ||
|
|
8be420c42e | ||
|
|
0e2e0cd0ab | ||
|
|
651ec1bfdb | ||
|
|
b9faedfdb0 | ||
|
|
3db6a99d3a | ||
|
|
3c5bf74750 | ||
|
|
95a4a81bce | ||
|
|
5767e9b516 | ||
|
|
102843b433 | ||
|
|
5f09d49e12 | ||
|
|
28d098815b | ||
|
|
55479d963a | ||
|
|
a75c227625 | ||
|
|
80845c8733 | ||
|
|
46cb1b2206 | ||
|
|
f81e4fae5a | ||
|
|
b0c4a23a56 | ||
|
|
bce58cfe56 | ||
|
|
10a2a82d32 | ||
|
|
7fa4497b7f | ||
|
|
b279b2bef9 | ||
|
|
3c59305677 | ||
|
|
2a87b280bf | ||
|
|
5e6f3cdac1 | ||
|
|
65bd747c50 | ||
|
|
e0e45b0dc3 | ||
|
|
eed103b6a5 | ||
|
|
d0c73d4723 | ||
|
|
19e7c9b9c8 | ||
|
|
0edd588c3e | ||
|
|
78ac8f7dc9 | ||
|
|
22ab3cf551 | ||
|
|
dcfddc78da | ||
|
|
aad5f6ef06 | ||
|
|
12b0e741d8 | ||
|
|
baecd3c411 | ||
|
|
1cfa00ed9c | ||
|
|
9600c17454 | ||
|
|
61ef1e1036 | ||
|
|
77e39bc5ed | ||
|
|
edba8f1ee0 | ||
|
|
894158779c | ||
|
|
1cd9b6103c | ||
|
|
ba33b743d5 | ||
|
|
f6c57607ea | ||
|
|
3779868880 | ||
|
|
4c6af55e7c | ||
|
|
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.
|
||||
@@ -0,0 +1,180 @@
|
||||
schema_version: v1
|
||||
name: dependency-update-troubleshoot
|
||||
enabled: true
|
||||
|
||||
trigger:
|
||||
cron:
|
||||
schedule: "0 21 * * *"
|
||||
timezone: "America/Chicago"
|
||||
overlap_policy: skip
|
||||
timeout: 2h
|
||||
|
||||
enable_manual_trigger: true
|
||||
|
||||
tags: ["cron", "dependencies"]
|
||||
color: cyan
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
label: Start
|
||||
entry: true
|
||||
on_enter:
|
||||
inject: |
|
||||
Workflow task: run the Go dependency update maintenance workflow for Troubleshoot.
|
||||
|
||||
This is an unattended dependency maintenance workflow. Scope: Go
|
||||
dependency updates for the top-level go.mod/go.sum plus examples/sdk/helm-template. After updates
|
||||
are applied, you will review the changes, fix any compatibility issues,
|
||||
validate with make build and make test, and open one grouped PR.
|
||||
|
||||
Do not ask for permission to start. Immediately say exactly [PROCEED]
|
||||
to run the dependency updater. Detailed post-update instructions will
|
||||
follow.
|
||||
|
||||
- id: working
|
||||
label: Working
|
||||
triggers:
|
||||
- message_contains: "[PROCEED]"
|
||||
on_enter:
|
||||
run:
|
||||
command: |
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
|
||||
if ! command -v go >/dev/null 2>&1; then
|
||||
nix profile install nixpkgs#go_1_26 || nix-env -iA nixpkgs.go_1_26
|
||||
fi
|
||||
|
||||
go_path="$(command -v go)"
|
||||
go_version="$(go version)"
|
||||
printf "{\"status\":\"ready\",\"go_path\":\"%s\",\"go_version\":\"%s\"}\n" "$go_path" "$go_version"
|
||||
'
|
||||
output: go_toolchain
|
||||
timeout: 10m
|
||||
dependency_updates:
|
||||
ecosystems: [go]
|
||||
paths: ["."]
|
||||
output: dependency_updates
|
||||
timeout: 30m
|
||||
inject: |
|
||||
Dependency updates were applied for the root Go module.
|
||||
|
||||
Scope this workflow to Go dependency updates only. Focus on the
|
||||
top-level go.mod/go.sum. Exclude Node dependencies and any non-Go
|
||||
modules.
|
||||
|
||||
Treat compatibility fixes as part of this workflow. If an updated
|
||||
dependency causes build, lint, type, or test failures, first attempt
|
||||
reasonable scoped code changes within the affected Go module to support
|
||||
the new version. Do not downgrade, pin, or skip an update just because
|
||||
it has breaking API changes unless the required changes are broad,
|
||||
risky, unrelated to this workflow's scope, or cannot be completed within
|
||||
reasonable effort. If you defer an update, explain exactly why.
|
||||
|
||||
Review {{ .Outputs.dependency_updates.files_changed }}.
|
||||
Before opening a PR, validate using the repo make targets in the Nix
|
||||
development environment (nix develop -c). Do not run ad-hoc go
|
||||
commands; use exactly these targets:
|
||||
|
||||
- make build
|
||||
- make test
|
||||
|
||||
Note: make test-integration requires a Kubernetes cluster. Only add it
|
||||
to the validation if the workflow environment provides one.
|
||||
|
||||
Fix any failures and open one grouped PR.
|
||||
Say [DONE] with the PR URL only when the PR is open.
|
||||
If {{ .Outputs.dependency_updates.files_changed }} is empty and no PR is
|
||||
needed, say [NO_CHANGES] instead of [DONE].
|
||||
|
||||
- id: no-changes
|
||||
label: No Changes
|
||||
triggers:
|
||||
- message_contains: "[NO_CHANGES]"
|
||||
- message_contains: "[DONE] No changes"
|
||||
terminal: true
|
||||
|
||||
- id: validate
|
||||
label: Validate
|
||||
triggers:
|
||||
- message_contains: "[DONE]"
|
||||
on_enter:
|
||||
run:
|
||||
command: |
|
||||
nix develop -c bash -lc '
|
||||
set +e
|
||||
tmpdir="$(mktemp -d)"
|
||||
|
||||
make build >"$tmpdir/build.log" 2>&1
|
||||
build_status=$?
|
||||
|
||||
make test >"$tmpdir/test.log" 2>&1
|
||||
test_status=$?
|
||||
|
||||
python3 - "$tmpdir" "$build_status" "$test_status" <<PY
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
tmpdir = pathlib.Path(sys.argv[1])
|
||||
statuses = {
|
||||
"build": int(sys.argv[2]),
|
||||
"test": int(sys.argv[3]),
|
||||
}
|
||||
|
||||
checks = []
|
||||
for name, code in statuses.items():
|
||||
log = (tmpdir / f"{name}.log").read_text(errors="replace")
|
||||
checks.append({
|
||||
"name": name,
|
||||
"status": "passed" if code == 0 else "failed",
|
||||
"exit_code": code,
|
||||
"log_tail": log[-12000:],
|
||||
})
|
||||
|
||||
failed = [check for check in checks if check["exit_code"] != 0]
|
||||
print(json.dumps({
|
||||
"status": "passed" if not failed else "failed",
|
||||
"reason": "all validation checks passed" if not failed else "one or more validation checks failed",
|
||||
"checks": checks,
|
||||
}))
|
||||
PY
|
||||
'
|
||||
output: validation
|
||||
timeout: 45m
|
||||
gate:
|
||||
output: validation
|
||||
pass:
|
||||
path: status
|
||||
values: [passed]
|
||||
fail:
|
||||
path: status
|
||||
values: [failed, error]
|
||||
required: true
|
||||
|
||||
- id: fix-validation
|
||||
label: Fix Validation
|
||||
triggers:
|
||||
- gate_result:
|
||||
stage: validate
|
||||
verdict: fail
|
||||
on_enter:
|
||||
inject: |
|
||||
Validation failed before completion.
|
||||
|
||||
Status: {{ .Outputs.validation.status }}
|
||||
Reason: {{ .Outputs.validation.reason }}
|
||||
|
||||
Checks:
|
||||
{{ .Outputs.validation.checks }}
|
||||
|
||||
Fix the failures above, commit the changes to the same PR branch, and
|
||||
say [DONE] with the PR URL again when the PR is ready for validation.
|
||||
|
||||
- id: complete
|
||||
label: Complete
|
||||
triggers:
|
||||
- gate_result:
|
||||
stage: validate
|
||||
verdict: pass
|
||||
terminal: true
|
||||
@@ -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,8 +30,8 @@ jobs:
|
||||
tidy-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- run: make tidy-diff
|
||||
@@ -39,8 +39,8 @@ jobs:
|
||||
test-integration:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- uses: replicatedhq/action-k3s@main
|
||||
@@ -54,12 +54,12 @@ jobs:
|
||||
compile-preflight:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-go@v7
|
||||
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@v7
|
||||
- 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/setup-go@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-go@v7
|
||||
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@v7
|
||||
- uses: actions/setup-go@v7
|
||||
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@v7
|
||||
- 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, compile-preflight]
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v7
|
||||
- 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@v7
|
||||
- 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@v7
|
||||
- uses: actions/setup-go@v7
|
||||
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@v7
|
||||
- uses: actions/setup-go@v7
|
||||
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@v7
|
||||
- uses: actions/setup-go@v7
|
||||
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@v7
|
||||
- uses: actions/setup-go@v7
|
||||
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@v7
|
||||
|
||||
- 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
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Publish ElasticClaw Workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ".factory-workflows/dependency-update.yaml"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish workflow (${{ matrix.workflow }})
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
workflow:
|
||||
- .factory-workflows/dependency-update.yaml
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Publish workflow (dependency-update)
|
||||
uses: elasticclaw/actions/publish-workflow@main
|
||||
with:
|
||||
hub-endpoint: https://factory.repldev.com
|
||||
token: ${{ secrets.ELASTICCLAW_TOKEN }}
|
||||
workspace: replicated-troubleshoot
|
||||
path: ${{ matrix.workflow }}
|
||||
@@ -0,0 +1,101 @@
|
||||
name: publish-securebuild
|
||||
|
||||
env:
|
||||
SECUREBUILD_CLI_VERSION: v0.0.498
|
||||
SECUREBUILD_CLI_SHA256: d259aa99d85aef957fe1e40c6f26e1983cc7fd72277e08599a82adc6681d3b37
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
description: Stable version tag to build (for example, v0.130.0)
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
SECUREBUILD_API_TOKEN:
|
||||
required: true
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Stable version tag to build (for example, v0.130.0)
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
validate-version:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Require a stable release version
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
if ! [[ "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "SecureBuild only supports stable release versions (for example, v0.130.0): $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
build-package:
|
||||
needs: validate-version
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Set version
|
||||
id: version
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Install SecureBuild CLI
|
||||
run: |
|
||||
curl --fail --silent --show-error --location \
|
||||
"https://github.com/securebuildhq/securebuild/releases/download/${SECUREBUILD_CLI_VERSION}/securebuild-cli-linux-amd64" \
|
||||
--output securebuild
|
||||
echo "${SECUREBUILD_CLI_SHA256} securebuild" | sha256sum --check --strict
|
||||
chmod +x securebuild
|
||||
|
||||
- name: Build package
|
||||
env:
|
||||
SECUREBUILD_API_TOKEN: ${{ secrets.SECUREBUILD_API_TOKEN }}
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
./securebuild build package \
|
||||
--package-family-name troubleshoot \
|
||||
--tag "$VERSION" \
|
||||
--api-token "$SECUREBUILD_API_TOKEN"
|
||||
|
||||
build-image:
|
||||
needs: build-package
|
||||
if: ${{ !cancelled() && needs.build-package.result == 'success' }}
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
image-name:
|
||||
- troubleshoot
|
||||
- preflight
|
||||
steps:
|
||||
- name: Install SecureBuild CLI
|
||||
run: |
|
||||
curl --fail --silent --show-error --location \
|
||||
"https://github.com/securebuildhq/securebuild/releases/download/${SECUREBUILD_CLI_VERSION}/securebuild-cli-linux-amd64" \
|
||||
--output securebuild
|
||||
echo "${SECUREBUILD_CLI_SHA256} securebuild" | sha256sum --check --strict
|
||||
chmod +x securebuild
|
||||
|
||||
- name: Build image
|
||||
env:
|
||||
IMAGE_NAME: ${{ matrix.image-name }}
|
||||
SECUREBUILD_API_TOKEN: ${{ secrets.SECUREBUILD_API_TOKEN }}
|
||||
VERSION: ${{ needs.build-package.outputs.version }}
|
||||
run: |
|
||||
IMAGE_VERSION="${VERSION#v}"
|
||||
MAJOR_MINOR_VERSION="${IMAGE_VERSION%.*}"
|
||||
MAJOR_VERSION="${IMAGE_VERSION%%.*}"
|
||||
|
||||
./securebuild build image \
|
||||
--image-name "$IMAGE_NAME" \
|
||||
--tag "$VERSION" \
|
||||
--image-tag "$MAJOR_MINOR_VERSION" \
|
||||
--image-tag "$MAJOR_VERSION" \
|
||||
--image-tag latest \
|
||||
--api-token "$SECUREBUILD_API_TOKEN"
|
||||
@@ -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@v7
|
||||
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
|
||||
uses: actions/setup-go@v7
|
||||
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@v7
|
||||
|
||||
- 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
|
||||
uses: actions/setup-python@v7
|
||||
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@v7
|
||||
|
||||
- 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@v7
|
||||
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@v7
|
||||
|
||||
- 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@v7
|
||||
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@v7
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v7
|
||||
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,21 +14,28 @@ jobs:
|
||||
runs-on: troubleshoot_release
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: azure/docker-login@v2
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USER }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
go-version: '1.26'
|
||||
check-latest: true
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v4.1.2
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
cosign-release: 'v3.1.3'
|
||||
|
||||
- name: Generate and sign SBOM
|
||||
run: make sbom
|
||||
env:
|
||||
COSIGN_KEY: ${{ secrets.COSIGN_KEY }}
|
||||
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
|
||||
|
||||
- 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 +44,28 @@ 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
|
||||
|
||||
securebuild:
|
||||
if: ${{ !contains(github.ref_name, '-') }}
|
||||
uses: ./.github/workflows/publish-securebuild.yml
|
||||
with:
|
||||
version: ${{ github.ref_name }}
|
||||
secrets:
|
||||
SECUREBUILD_API_TOKEN: ${{ secrets.SECUREBUILD_API_TOKEN }}
|
||||
|
||||
notify:
|
||||
if: ${{ !contains(github.ref_name, '-') }}
|
||||
uses: replicatedhq/reusable-workflows/.github/workflows/notify-release.yml@0432fb838fc83852b78be97a150b35991a85d56f
|
||||
with:
|
||||
tag: ${{ github.ref_name }}
|
||||
secrets:
|
||||
slack_webhook: ${{ secrets.RELEASE_SLACK_WEBHOOK }}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
name: Upgrade Go Version
|
||||
|
||||
on:
|
||||
# Run manually when needed
|
||||
workflow_dispatch:
|
||||
# Run weekly on Mondays at 8am UTC
|
||||
schedule:
|
||||
- cron: "0 8 * * MON"
|
||||
|
||||
jobs:
|
||||
upgrade-go:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Check for Go updates
|
||||
uses: StefMa/Upgrade-Go-Action@v1
|
||||
with:
|
||||
base-branch: 'main'
|
||||
gh-token: ${{ secrets.TROUBLESHOOT_GH_PAT }}
|
||||
+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
|
||||
@@ -149,16 +152,8 @@ vet:
|
||||
|
||||
.PHONY: generate
|
||||
generate: controller-gen client-gen
|
||||
$(CONTROLLER_GEN) \
|
||||
object:headerFile=./hack/boilerplate.go.txt paths=./pkg/apis/...
|
||||
$(CLIENT_GEN) \
|
||||
--output-dir=. \
|
||||
--output-pkg=github.com/replicatedhq/troubleshoot/pkg/client \
|
||||
--clientset-name troubleshootclientset \
|
||||
--input-base github.com/replicatedhq/troubleshoot/pkg/apis \
|
||||
--input troubleshoot/v1beta1 \
|
||||
--input troubleshoot/v1beta2 \
|
||||
--go-header-file ./hack/boilerplate.go.txt
|
||||
$(CONTROLLER_GEN) object:headerFile=./hack/boilerplate.go.txt paths=./pkg/apis/...
|
||||
$(CLIENT_GEN) --output-dir=. --output-pkg=github.com/replicatedhq/troubleshoot/pkg/client --clientset-name troubleshootclientset --input-base github.com/replicatedhq/troubleshoot/pkg/apis --input troubleshoot/v1beta1 --input troubleshoot/v1beta2 --input troubleshoot/v1beta3 --go-header-file ./hack/boilerplate.go.txt
|
||||
cp -r troubleshootclientset pkg/client
|
||||
rm -rf troubleshootclientset
|
||||
|
||||
@@ -190,32 +185,18 @@ bin/docsgen:
|
||||
|
||||
controller-gen:
|
||||
go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.19.0
|
||||
CONTROLLER_GEN=$(shell which controller-gen)
|
||||
CONTROLLER_GEN=$(shell go env GOPATH)/bin/controller-gen
|
||||
|
||||
.PHONY: client-gen
|
||||
client-gen:
|
||||
go install k8s.io/code-generator/cmd/client-gen@v0.34.0
|
||||
CLIENT_GEN=$(shell which client-gen)
|
||||
CLIENT_GEN=$(shell go env GOPATH)/bin/client-gen
|
||||
|
||||
.PHONY: release
|
||||
release: export GITHUB_TOKEN = $(shell echo ${GITHUB_TOKEN_TROUBLESHOOT})
|
||||
release:
|
||||
curl -sL https://git.io/goreleaser | bash -s -- --rm-dist --config deploy/.goreleaser.yml
|
||||
|
||||
.PHONY: snapshot-release
|
||||
snapshot-release:
|
||||
curl -sL https://git.io/goreleaser | bash -s -- --rm-dist --snapshot --config deploy/.goreleaser.snapshot.yml
|
||||
docker push replicated/troubleshoot:alpha
|
||||
docker push replicated/preflight:alpha
|
||||
|
||||
.PHONY: local-release
|
||||
local-release:
|
||||
curl -sL https://git.io/goreleaser | bash -s -- --rm-dist --snapshot --config deploy/.goreleaser.yaml
|
||||
docker tag replicated/troubleshoot:alpha localhost:32000/troubleshoot:alpha
|
||||
docker tag replicated/preflight:alpha localhost:32000/preflight:alpha
|
||||
docker push localhost:32000/troubleshoot:alpha
|
||||
docker push localhost:32000/preflight:alpha
|
||||
|
||||
.PHONY: run-preflight
|
||||
run-preflight: bin/preflight
|
||||
./bin/preflight ./examples/preflight/sample-preflight.yaml
|
||||
@@ -251,17 +232,23 @@ sbom: sbom/assets/troubleshoot-sbom.tgz
|
||||
--tlog-upload \
|
||||
--yes \
|
||||
--rekor-url=https://rekor.sigstore.dev \
|
||||
sbom/assets/troubleshoot-sbom.tgz > sbom/assets/troubleshoot-sbom.tgz.sig
|
||||
--bundle sbom/assets/troubleshoot-sbom.tgz.bundle \
|
||||
sbom/assets/troubleshoot-sbom.tgz
|
||||
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 +282,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
|
||||
|
||||
@@ -56,19 +56,12 @@ For questions about using Troubleshoot, how to contribute and engaging with the
|
||||
# Software Bill of Materials
|
||||
A signed SBOM that includes Troubleshoot dependencies is included in each release.
|
||||
- **troubleshoot-sbom.tgz** contains a software bill of materials for Troubleshoot.
|
||||
- **troubleshoot-sbom.tgz.sig** is the digital signature for troubleshoot-sbom.tgz
|
||||
- **troubleshoot-sbom.tgz.bundle** contains the signature and transparency log material used by Cosign.
|
||||
- **key.pub** is the public key from the key pair used to sign troubleshoot-sbom.tgz
|
||||
|
||||
The following example illustrates using [cosign](https://github.com/sigstore/cosign) to verify that **troubleshoot-sbom.tgz** has
|
||||
not been tampered with.
|
||||
not been tampered with. Install [Cosign v3](https://github.com/sigstore/cosign/releases).
|
||||
```sh
|
||||
$ cosign verify-blob --key key.pub --signature troubleshoot-sbom.tgz.sig troubleshoot-sbom.tgz
|
||||
Verified OK
|
||||
```
|
||||
|
||||
If you were to get an error similar to the one below, it means you are verifying an SBOM signed using cosign `v1` using a newer `v2` of the binary. This version introduced [breaking changes](https://github.com/sigstore/cosign/blob/main/CHANGELOG.md#breaking-changes) which require an additional flag `--insecure-ignore-tlog=true` to successfully verify SBOMs like so.
|
||||
```sh
|
||||
$ cosign verify-blob --key key.pub --signature troubleshoot-sbom.tgz.sig troubleshoot-sbom.tgz --insecure-ignore-tlog=true
|
||||
WARNING: Skipping tlog verification is an insecure practice that lacks of transparency and auditability verification for the blob.
|
||||
$ cosign verify-blob --key key.pub --bundle troubleshoot-sbom.tgz.bundle troubleshoot-sbom.tgz
|
||||
Verified OK
|
||||
```
|
||||
|
||||
@@ -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,88 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// ClickhouseCmd runs the clickhouse collector against a single database and
|
||||
// prints the native result JSON ({"isConnected":..,"version":..,"error":..}).
|
||||
func ClickhouseCmd() *cobra.Command {
|
||||
var (
|
||||
uri string
|
||||
skipVerify bool
|
||||
caCert string
|
||||
clientCert string
|
||||
clientKey string
|
||||
secretName string
|
||||
secretNamespace string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "clickhouse",
|
||||
Short: "Run the clickhouse collector against a database",
|
||||
Long: `Run the clickhouse collector: connect to a ClickHouse server, run "SELECT version()",
|
||||
and print the collector result as JSON.
|
||||
|
||||
--uri is a clickhouse-go DSN:
|
||||
clickhouse://user:password@host:9000/dbname
|
||||
|
||||
TLS material may be provided inline / by file path (--tls-cacert, --tls-client-cert,
|
||||
--tls-client-key) or sourced from a Secret (--tls-secret-name), which requires
|
||||
cluster access.
|
||||
|
||||
Example:
|
||||
collect clickhouse --uri "clickhouse://default:@ch.default.svc:9000/default"`,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
db := &troubleshootv1beta2.Database{
|
||||
CollectorMeta: troubleshootv1beta2.CollectorMeta{CollectorName: "clickhouse"},
|
||||
URI: uri,
|
||||
}
|
||||
|
||||
var (
|
||||
client kubernetes.Interface
|
||||
cfg *rest.Config
|
||||
)
|
||||
if skipVerify || caCert != "" || clientCert != "" || clientKey != "" || secretName != "" {
|
||||
tls := &troubleshootv1beta2.TLSParams{
|
||||
SkipVerify: skipVerify,
|
||||
CACert: caCert,
|
||||
ClientCert: clientCert,
|
||||
ClientKey: clientKey,
|
||||
}
|
||||
if secretName != "" {
|
||||
tls.Secret = &troubleshootv1beta2.TLSSecret{Name: secretName, Namespace: secretNamespace}
|
||||
var err error
|
||||
client, cfg, err = k8sClientForCollectors()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
db.TLS = tls
|
||||
}
|
||||
|
||||
c := &collect.CollectClickhouse{Collector: db, ClientConfig: cfg, Client: client, Context: cmd.Context()}
|
||||
res, err := c.Collect(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printCollectorResult(res)
|
||||
},
|
||||
}
|
||||
|
||||
f := cmd.Flags()
|
||||
f.StringVar(&uri, "uri", "", "ClickHouse connection DSN, e.g. clickhouse://user:pass@host:9000/db (required)")
|
||||
cmd.MarkFlagRequired("uri")
|
||||
f.BoolVar(&skipVerify, "tls-skip-verify", false, "skip TLS certificate verification")
|
||||
f.StringVar(&caCert, "tls-cacert", "", "CA certificate (PEM contents or a file path)")
|
||||
f.StringVar(&clientCert, "tls-client-cert", "", "client certificate (PEM contents or a file path)")
|
||||
f.StringVar(&clientKey, "tls-client-key", "", "client key (PEM contents or a file path)")
|
||||
f.StringVar(&secretName, "tls-secret-name", "", "name of a Secret holding TLS material (requires cluster access)")
|
||||
f.StringVar(&secretNamespace, "tls-secret-namespace", "default", "namespace of the TLS Secret")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Package cli implements the `collect` command and its subcommands.
|
||||
//
|
||||
// The per-collector subcommands (http, postgres, mysql, mssql, redis) run a
|
||||
// single collector and print its native result JSON to stdout. This lets a
|
||||
// collector run inside a Pod using the troubleshoot image — e.g. as a runPod
|
||||
// collector — so the check executes from within the cluster instead of from
|
||||
// wherever the CLI happens to be invoked.
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/version"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// printCollectorResult writes the native collector result JSON to stdout.
|
||||
// The single-collector subcommands run with an empty BundlePath, so the result
|
||||
// is held in memory (map value = the JSON bytes) rather than written to disk.
|
||||
func printCollectorResult(res collect.CollectorResult) error {
|
||||
for _, b := range res {
|
||||
fmt.Println(string(b))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// k8sClientForCollectors builds a Kubernetes client from the ambient kubeconfig
|
||||
// or in-cluster config. Only the collectors that resolve TLS material from a
|
||||
// Secret need this; plain connections and inline/file TLS do not.
|
||||
func k8sClientForCollectors() (kubernetes.Interface, *rest.Config, error) {
|
||||
cfg, err := k8sutil.GetRESTConfig()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
cfg.QPS = constants.DEFAULT_CLIENT_QPS
|
||||
cfg.Burst = constants.DEFAULT_CLIENT_BURST
|
||||
cfg.UserAgent = fmt.Sprintf("%s/%s", constants.DEFAULT_CLIENT_USER_AGENT, version.Version())
|
||||
client, err := kubernetes.NewForConfig(cfg)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return client, cfg, nil
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// HTTPCmd runs the http collector against a single endpoint and prints the
|
||||
// native result JSON ({"response":{"status":...}} or {"error":{...}}).
|
||||
func HTTPCmd() *cobra.Command {
|
||||
var (
|
||||
method string
|
||||
url string
|
||||
headers map[string]string
|
||||
body string
|
||||
timeout string
|
||||
proxy string
|
||||
insecure bool
|
||||
caCert string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "http",
|
||||
Short: "Run the http collector against an endpoint",
|
||||
Long: `Run the http collector: issue an HTTP request and print the collector result as JSON.
|
||||
|
||||
The result contains the response status, body and headers, or an error object.
|
||||
|
||||
Examples:
|
||||
collect http --url http://myapp.default.svc:8080/healthz
|
||||
collect http --method POST --url https://api.internal/ping \
|
||||
--header 'Content-Type=application/json' --body '{"ping":true}'`,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var tls *troubleshootv1beta2.TLSParams
|
||||
if caCert != "" {
|
||||
tls = &troubleshootv1beta2.TLSParams{CACert: caCert}
|
||||
}
|
||||
|
||||
spec := &troubleshootv1beta2.HTTP{
|
||||
CollectorMeta: troubleshootv1beta2.CollectorMeta{CollectorName: "http"},
|
||||
}
|
||||
switch strings.ToUpper(method) {
|
||||
case "GET":
|
||||
spec.Get = &troubleshootv1beta2.Get{URL: url, Headers: headers, Timeout: timeout, Proxy: proxy, InsecureSkipVerify: insecure, TLS: tls}
|
||||
case "POST":
|
||||
spec.Post = &troubleshootv1beta2.Post{URL: url, Headers: headers, Body: body, Timeout: timeout, Proxy: proxy, InsecureSkipVerify: insecure, TLS: tls}
|
||||
case "PUT":
|
||||
spec.Put = &troubleshootv1beta2.Put{URL: url, Headers: headers, Body: body, Timeout: timeout, Proxy: proxy, InsecureSkipVerify: insecure, TLS: tls}
|
||||
default:
|
||||
return fmt.Errorf("unsupported --method %q (use GET, POST, or PUT)", method)
|
||||
}
|
||||
|
||||
c := &collect.CollectHTTP{Collector: spec}
|
||||
res, err := c.Collect(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printCollectorResult(res)
|
||||
},
|
||||
}
|
||||
|
||||
f := cmd.Flags()
|
||||
f.StringVar(&url, "url", "", "request URL (required)")
|
||||
cmd.MarkFlagRequired("url")
|
||||
f.StringVar(&method, "method", "GET", "HTTP method: GET, POST, or PUT")
|
||||
f.StringToStringVar(&headers, "header", nil, "request header as key=value (repeatable)")
|
||||
f.StringVar(&body, "body", "", "request body (POST/PUT only)")
|
||||
f.StringVar(&timeout, "timeout", "", "request timeout, e.g. 15s (empty = no timeout)")
|
||||
f.StringVar(&proxy, "proxy", "", "proxy URL to use for the request")
|
||||
f.BoolVar(&insecure, "insecure-skip-verify", false, "do not verify the server's TLS certificate")
|
||||
f.StringVar(&caCert, "tls-cacert", "", "CA certificate to trust (PEM contents or a file path)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// MssqlCmd runs the mssql collector against a single database and prints the
|
||||
// native result JSON ({"isConnected":..,"version":..,"error":..}).
|
||||
//
|
||||
// Unlike the other database collectors, the mssql collector does not honor a
|
||||
// separate TLS field — TLS is configured through the connection URI query
|
||||
// parameters (e.g. encrypt=true) — so this subcommand only takes --uri.
|
||||
func MssqlCmd() *cobra.Command {
|
||||
var uri string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "mssql",
|
||||
Short: "Run the mssql collector against a database",
|
||||
Long: `Run the mssql collector: connect to a Microsoft SQL Server, run "select @@VERSION",
|
||||
and print the collector result as JSON.
|
||||
|
||||
--uri is a go-mssqldb connection URL; TLS options are passed as query parameters:
|
||||
sqlserver://user:password@host:1433?database=app&encrypt=true
|
||||
|
||||
Example:
|
||||
collect mssql --uri "sqlserver://sa:pass@mssql.default.svc:1433?encrypt=true"`,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
db := &troubleshootv1beta2.Database{
|
||||
CollectorMeta: troubleshootv1beta2.CollectorMeta{CollectorName: "mssql"},
|
||||
URI: uri,
|
||||
}
|
||||
|
||||
c := &collect.CollectMssql{Collector: db, Context: cmd.Context()}
|
||||
res, err := c.Collect(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printCollectorResult(res)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&uri, "uri", "", "SQL Server connection URI, e.g. sqlserver://user:pass@host:1433 (required)")
|
||||
cmd.MarkFlagRequired("uri")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// MysqlCmd runs the mysql collector against a single database and prints the
|
||||
// native result JSON ({"isConnected":..,"version":..,"variables":..,"error":..}).
|
||||
func MysqlCmd() *cobra.Command {
|
||||
var (
|
||||
uri string
|
||||
parameters []string
|
||||
skipVerify bool
|
||||
caCert string
|
||||
clientCert string
|
||||
clientKey string
|
||||
secretName string
|
||||
secretNamespace string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "mysql",
|
||||
Short: "Run the mysql collector against a database",
|
||||
Long: `Run the mysql collector: connect to a MySQL server, run "select version()",
|
||||
optionally collect server variables, and print the collector result as JSON.
|
||||
|
||||
--uri is a go-sql-driver DSN (note: not a URL):
|
||||
user:password@tcp(host:3306)/dbname
|
||||
|
||||
--parameters names server variables to collect via "SHOW VARIABLES"; the matching
|
||||
values are returned under "variables" in the result. This flag is unique to mysql.
|
||||
|
||||
Example:
|
||||
collect mysql --uri "root:pass@tcp(mysql.default.svc:3306)/" --parameters max_connections,version`,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
db := &troubleshootv1beta2.Database{
|
||||
CollectorMeta: troubleshootv1beta2.CollectorMeta{CollectorName: "mysql"},
|
||||
URI: uri,
|
||||
Parameters: parameters,
|
||||
}
|
||||
|
||||
var (
|
||||
client kubernetes.Interface
|
||||
cfg *rest.Config
|
||||
)
|
||||
if skipVerify || caCert != "" || clientCert != "" || clientKey != "" || secretName != "" {
|
||||
tls := &troubleshootv1beta2.TLSParams{
|
||||
SkipVerify: skipVerify,
|
||||
CACert: caCert,
|
||||
ClientCert: clientCert,
|
||||
ClientKey: clientKey,
|
||||
}
|
||||
if secretName != "" {
|
||||
tls.Secret = &troubleshootv1beta2.TLSSecret{Name: secretName, Namespace: secretNamespace}
|
||||
var err error
|
||||
client, cfg, err = k8sClientForCollectors()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
db.TLS = tls
|
||||
}
|
||||
|
||||
c := &collect.CollectMysql{Collector: db, ClientConfig: cfg, Client: client, Context: cmd.Context()}
|
||||
res, err := c.Collect(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printCollectorResult(res)
|
||||
},
|
||||
}
|
||||
|
||||
f := cmd.Flags()
|
||||
f.StringVar(&uri, "uri", "", "MySQL connection DSN, e.g. user:pass@tcp(host:3306)/db (required)")
|
||||
cmd.MarkFlagRequired("uri")
|
||||
f.StringSliceVar(¶meters, "parameters", nil, "server variables to collect via SHOW VARIABLES (comma-separated or repeatable)")
|
||||
f.BoolVar(&skipVerify, "tls-skip-verify", false, "skip TLS certificate verification")
|
||||
f.StringVar(&caCert, "tls-cacert", "", "CA certificate (PEM contents or a file path)")
|
||||
f.StringVar(&clientCert, "tls-client-cert", "", "client certificate (PEM contents or a file path)")
|
||||
f.StringVar(&clientKey, "tls-client-key", "", "client key (PEM contents or a file path)")
|
||||
f.StringVar(&secretName, "tls-secret-name", "", "name of a Secret holding TLS material (requires cluster access)")
|
||||
f.StringVar(&secretNamespace, "tls-secret-namespace", "default", "namespace of the TLS Secret")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// PostgresCmd runs the postgres collector against a single database and prints
|
||||
// the native result JSON ({"isConnected":..,"version":..,"error":..}).
|
||||
func PostgresCmd() *cobra.Command {
|
||||
var (
|
||||
uri string
|
||||
skipVerify bool
|
||||
caCert string
|
||||
clientCert string
|
||||
clientKey string
|
||||
secretName string
|
||||
secretNamespace string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "postgres",
|
||||
Short: "Run the postgres collector against a database",
|
||||
Long: `Run the postgres collector: connect to a PostgreSQL server, run "select version()",
|
||||
and print the collector result as JSON.
|
||||
|
||||
--uri is a libpq/pgx connection URI:
|
||||
postgres://user:password@host:5432/dbname?sslmode=disable
|
||||
|
||||
TLS material may be provided inline / by file path (--tls-cacert, --tls-client-cert,
|
||||
--tls-client-key) or sourced from a Secret (--tls-secret-name), which requires
|
||||
cluster access.
|
||||
|
||||
Example:
|
||||
collect postgres --uri "postgres://user:pass@pg.default.svc:5432/app?sslmode=disable"`,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
db := &troubleshootv1beta2.Database{
|
||||
CollectorMeta: troubleshootv1beta2.CollectorMeta{CollectorName: "postgres"},
|
||||
URI: uri,
|
||||
}
|
||||
|
||||
var (
|
||||
client kubernetes.Interface
|
||||
cfg *rest.Config
|
||||
)
|
||||
if skipVerify || caCert != "" || clientCert != "" || clientKey != "" || secretName != "" {
|
||||
tls := &troubleshootv1beta2.TLSParams{
|
||||
SkipVerify: skipVerify,
|
||||
CACert: caCert,
|
||||
ClientCert: clientCert,
|
||||
ClientKey: clientKey,
|
||||
}
|
||||
if secretName != "" {
|
||||
tls.Secret = &troubleshootv1beta2.TLSSecret{Name: secretName, Namespace: secretNamespace}
|
||||
var err error
|
||||
client, cfg, err = k8sClientForCollectors()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
db.TLS = tls
|
||||
}
|
||||
|
||||
c := &collect.CollectPostgres{Collector: db, ClientConfig: cfg, Client: client, Context: cmd.Context()}
|
||||
res, err := c.Collect(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printCollectorResult(res)
|
||||
},
|
||||
}
|
||||
|
||||
f := cmd.Flags()
|
||||
f.StringVar(&uri, "uri", "", "PostgreSQL connection URI (required)")
|
||||
cmd.MarkFlagRequired("uri")
|
||||
f.BoolVar(&skipVerify, "tls-skip-verify", false, "skip TLS certificate verification")
|
||||
f.StringVar(&caCert, "tls-cacert", "", "CA certificate (PEM contents or a file path)")
|
||||
f.StringVar(&clientCert, "tls-client-cert", "", "client certificate (PEM contents or a file path)")
|
||||
f.StringVar(&clientKey, "tls-client-key", "", "client key (PEM contents or a file path)")
|
||||
f.StringVar(&secretName, "tls-secret-name", "", "name of a Secret holding TLS material (requires cluster access)")
|
||||
f.StringVar(&secretNamespace, "tls-secret-namespace", "default", "namespace of the TLS Secret")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// RedisCmd runs the redis collector against a single instance and prints the
|
||||
// native result JSON ({"isConnected":..,"version":..,"error":..}).
|
||||
func RedisCmd() *cobra.Command {
|
||||
var (
|
||||
uri string
|
||||
skipVerify bool
|
||||
caCert string
|
||||
clientCert string
|
||||
clientKey string
|
||||
secretName string
|
||||
secretNamespace string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "redis",
|
||||
Short: "Run the redis collector against an instance",
|
||||
Long: `Run the redis collector: connect to a Redis server, read "INFO server" (server
|
||||
version), and print the collector result as JSON.
|
||||
|
||||
--uri is a go-redis connection URL:
|
||||
redis://host:6379 (plaintext)
|
||||
rediss://user:pass@host:6379 (TLS)
|
||||
|
||||
TLS material may be provided inline / by file path (--tls-cacert, --tls-client-cert,
|
||||
--tls-client-key) or sourced from a Secret (--tls-secret-name), which requires
|
||||
cluster access.
|
||||
|
||||
Example:
|
||||
collect redis --uri "redis://redis.default.svc:6379"`,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
db := &troubleshootv1beta2.Database{
|
||||
CollectorMeta: troubleshootv1beta2.CollectorMeta{CollectorName: "redis"},
|
||||
URI: uri,
|
||||
}
|
||||
|
||||
var (
|
||||
client kubernetes.Interface
|
||||
cfg *rest.Config
|
||||
)
|
||||
if skipVerify || caCert != "" || clientCert != "" || clientKey != "" || secretName != "" {
|
||||
tls := &troubleshootv1beta2.TLSParams{
|
||||
SkipVerify: skipVerify,
|
||||
CACert: caCert,
|
||||
ClientCert: clientCert,
|
||||
ClientKey: clientKey,
|
||||
}
|
||||
if secretName != "" {
|
||||
tls.Secret = &troubleshootv1beta2.TLSSecret{Name: secretName, Namespace: secretNamespace}
|
||||
var err error
|
||||
client, cfg, err = k8sClientForCollectors()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
db.TLS = tls
|
||||
}
|
||||
|
||||
c := &collect.CollectRedis{Collector: db, ClientConfig: cfg, Client: client, Context: cmd.Context()}
|
||||
res, err := c.Collect(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printCollectorResult(res)
|
||||
},
|
||||
}
|
||||
|
||||
f := cmd.Flags()
|
||||
f.StringVar(&uri, "uri", "", "Redis connection URL, e.g. redis://host:6379 (required)")
|
||||
cmd.MarkFlagRequired("uri")
|
||||
f.BoolVar(&skipVerify, "tls-skip-verify", false, "skip TLS certificate verification")
|
||||
f.StringVar(&caCert, "tls-cacert", "", "CA certificate (PEM contents or a file path)")
|
||||
f.StringVar(&clientCert, "tls-client-cert", "", "client certificate (PEM contents or a file path)")
|
||||
f.StringVar(&clientKey, "tls-client-key", "", "client key (PEM contents or a file path)")
|
||||
f.StringVar(&secretName, "tls-secret-name", "", "name of a Secret holding TLS material (requires cluster access)")
|
||||
f.StringVar(&secretNamespace, "tls-secret-namespace", "default", "namespace of the TLS Secret")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
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,
|
||||
// PersistentPreRun/PersistentPostRun (rather than PreRun/PostRun) so this
|
||||
// setup also runs for the per-collector subcommands, not just `collect [url]`.
|
||||
PersistentPreRun: 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])
|
||||
},
|
||||
PersistentPostRun: 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())
|
||||
|
||||
// Per-collector subcommands: run a single collector and print its native
|
||||
// result JSON. Each has its own flags and help. These let a collector run
|
||||
// inside a Pod via the troubleshoot image (e.g. as a runPod collector), so
|
||||
// the check executes from within the cluster rather than wherever the CLI runs.
|
||||
cmd.AddCommand(HTTPCmd())
|
||||
cmd.AddCommand(PostgresCmd())
|
||||
cmd.AddCommand(MysqlCmd())
|
||||
cmd.AddCommand(MssqlCmd())
|
||||
cmd.AddCommand(RedisCmd())
|
||||
cmd.AddCommand(ClickhouseCmd())
|
||||
|
||||
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.PersistentFlags().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.PersistentFlags())
|
||||
|
||||
// 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:
|
||||
|
||||
+10
-24
@@ -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 }}
|
||||
@@ -125,28 +133,6 @@ archives:
|
||||
dst: .
|
||||
strip_parent: true
|
||||
|
||||
dockers:
|
||||
- dockerfile: ./deploy/Dockerfile.troubleshoot
|
||||
image_templates:
|
||||
- "replicated/troubleshoot:latest"
|
||||
- "replicated/troubleshoot:{{ .Major }}"
|
||||
- "replicated/troubleshoot:{{ .Major }}.{{ .Minor }}"
|
||||
- "replicated/troubleshoot:{{ .Major }}.{{ .Minor }}.{{ .Patch }}"
|
||||
ids:
|
||||
- support-bundle
|
||||
- preflight
|
||||
skip_push: true
|
||||
- dockerfile: ./deploy/Dockerfile.troubleshoot
|
||||
image_templates:
|
||||
- "replicated/preflight:latest"
|
||||
- "replicated/preflight:{{ .Major }}"
|
||||
- "replicated/preflight:{{ .Major }}.{{ .Minor }}"
|
||||
- "replicated/preflight:{{ .Major }}.{{ .Minor }}.{{ .Patch }}"
|
||||
ids:
|
||||
- support-bundle
|
||||
- preflight
|
||||
skip_push: true
|
||||
|
||||
universal_binaries:
|
||||
- id: preflight-universal
|
||||
ids: [preflight] # refers to the build id above
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
FROM debian:bookworm
|
||||
WORKDIR /
|
||||
|
||||
RUN apt-get -qq update \
|
||||
&& apt-get -qq -y install \
|
||||
ca-certificates kmod
|
||||
|
||||
COPY support-bundle /troubleshoot/support-bundle
|
||||
COPY preflight /troubleshoot/preflight
|
||||
|
||||
ENV PATH="/troubleshoot:${PATH}"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module helm-template
|
||||
|
||||
go 1.24.6
|
||||
go 1.26.5
|
||||
|
||||
// Always use the local version of troubleshoot so as to build using
|
||||
// the latest version of the library. This will ensure the example
|
||||
@@ -9,33 +9,41 @@ 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.21.4
|
||||
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/semver/v3 v3.5.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.7.0 // 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
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
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/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
|
||||
github.com/go-logr/logr v1.4.4 // indirect
|
||||
github.com/go-openapi/jsonpointer v1.0.0 // indirect
|
||||
github.com/go-openapi/jsonreference v1.0.0 // indirect
|
||||
github.com/go-openapi/swag v0.28.0 // indirect
|
||||
github.com/go-openapi/swag/cmdutils v0.28.0 // indirect
|
||||
github.com/go-openapi/swag/conv v0.28.0 // indirect
|
||||
github.com/go-openapi/swag/fileutils v0.28.0 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.28.0 // indirect
|
||||
github.com/go-openapi/swag/loading v0.28.0 // indirect
|
||||
github.com/go-openapi/swag/mangling v0.28.0 // indirect
|
||||
github.com/go-openapi/swag/netutils v0.28.0 // indirect
|
||||
github.com/go-openapi/swag/pools v0.28.0 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.28.0 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.28.0 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.28.0 // 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/gnostic-models v0.7.1 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
@@ -46,28 +54,28 @@ require (
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // 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
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/crypto v0.55.0 // indirect
|
||||
golang.org/x/net v0.58.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/term v0.45.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // 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.36.3 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.36.3 // indirect
|
||||
k8s.io/apimachinery v0.36.3 // indirect
|
||||
k8s.io/client-go v0.36.3 // indirect
|
||||
k8s.io/klog/v2 v2.140.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect
|
||||
k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 // indirect
|
||||
sigs.k8s.io/controller-runtime v0.24.1 // 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.4.2 // indirect
|
||||
)
|
||||
|
||||
+106
-109
@@ -2,67 +2,87 @@ 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/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
|
||||
github.com/Masterminds/semver/v3 v3.5.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.7.0 h1:s0Y3ITPy6sQn5xt54DuYvTF8hu134ooYLUb58DX/HjE=
|
||||
github.com/cyphar/filepath-securejoin v0.7.0/go.mod h1:ymLGms/u3BYaviIiuKFnUx8EkQEZeK6cInNoAPJA3o4=
|
||||
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=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
|
||||
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
|
||||
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
||||
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s=
|
||||
github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y=
|
||||
github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY=
|
||||
github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0=
|
||||
github.com/go-openapi/swag v0.28.0 h1:xkgbOSKj6DZziNpyqRRAOt3GJGtgjgsd2RoyT30VWuw=
|
||||
github.com/go-openapi/swag v0.28.0/go.mod h1:4qYnT3Cqr1p1VknOdPo70evN4rgQnAg6jwApHyxSGIg=
|
||||
github.com/go-openapi/swag/cmdutils v0.28.0 h1:7TOeNtkYru1SG8Y34tDh9WBbLsMqGnptuxWiHREPZ4Q=
|
||||
github.com/go-openapi/swag/cmdutils v0.28.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM=
|
||||
github.com/go-openapi/swag/conv v0.28.0 h1:GtqqbyFe7vR5Y7ehxG9W6/OvrSFdf1OLeTGp40TqxH8=
|
||||
github.com/go-openapi/swag/conv v0.28.0/go.mod h1:mbUE+mzctnhxi864m0Q07SpN8OowD9JhxmxuYvZZD/k=
|
||||
github.com/go-openapi/swag/fileutils v0.28.0 h1:Z04XWQD7R8Eq+7GnOrjovBxPPmZzsS4gt2H2GPGIViU=
|
||||
github.com/go-openapi/swag/fileutils v0.28.0/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8=
|
||||
github.com/go-openapi/swag/jsonutils v0.28.0 h1:YIch6FwO7RXzeAnbO8Tu7dWBZeUEH+4nA0HXltVTnv4=
|
||||
github.com/go-openapi/swag/jsonutils v0.28.0/go.mod h1:CYM3WlTUcagR2ZoHdz54di/cbBqt82tuxuXgAjxw+mg=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.28.0 h1:qV+VVUAx5Oro8WjVWpZeql7YReTKhT4smR4zhcOQZr0=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.28.0/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY=
|
||||
github.com/go-openapi/swag/loading v0.28.0 h1:td8QZdZC9MIYGGSnSPKShKiK22I2tU5UQvuUhIBPRLU=
|
||||
github.com/go-openapi/swag/loading v0.28.0/go.mod h1:rXB0QiQX5mMveXEA7ouM4KiiM9jVJe4K6BVbwhD1M4k=
|
||||
github.com/go-openapi/swag/mangling v0.28.0 h1:pH8eyeNO9SLYsTMWJrurnNfKmDa28XrlA+HePVD53VM=
|
||||
github.com/go-openapi/swag/mangling v0.28.0/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w=
|
||||
github.com/go-openapi/swag/netutils v0.28.0 h1:YXN6TALEi2pzts8/8GNm6T61HTAZsieukGZidap989k=
|
||||
github.com/go-openapi/swag/netutils v0.28.0/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ=
|
||||
github.com/go-openapi/swag/pools v0.28.0 h1:HPMZWSAfce3rdVTFcjFiCIBtDg9h4x2QlRrHipwhxeU=
|
||||
github.com/go-openapi/swag/pools v0.28.0/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE=
|
||||
github.com/go-openapi/swag/stringutils v0.28.0 h1:ixsc9iYgDPubHL/8nSkbnryEHpD2VRlBMLKpQyPXcDU=
|
||||
github.com/go-openapi/swag/stringutils v0.28.0/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM=
|
||||
github.com/go-openapi/swag/typeutils v0.28.0 h1:nRBKSBXjDgf01VDPB3fWeD9nQuhCOVeIYAkUx2tbkyY=
|
||||
github.com/go-openapi/swag/typeutils v0.28.0/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ=
|
||||
github.com/go-openapi/swag/yamlutils v0.28.0 h1:TV3JXH6DS46KUroDtMLAYHGkdWf5VDq3wVWFirmzROY=
|
||||
github.com/go-openapi/swag/yamlutils v0.28.0/go.mod h1:x0q/yndZHEgk9Rx3DyDqzFUmHy55KTvIZldvF2dTJXs=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo=
|
||||
github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug=
|
||||
github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/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/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
|
||||
github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/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=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/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=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
@@ -75,17 +95,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.4 h1:fcEcQW/A++6aZAZQNUmNjvA9PSOzefMJBerHJ4t8v8Y=
|
||||
github.com/onsi/ginkgo/v2 v2.27.4/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||
github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q=
|
||||
github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
|
||||
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.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
|
||||
github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
|
||||
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 +122,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/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=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||
golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74=
|
||||
golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
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.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
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.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/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.21.4 h1:T/GcIEXU/gNjJnkITlIZ3e9xqkZjhFTmISuStTZ6+Qg=
|
||||
helm.sh/helm/v3 v3.21.4/go.mod h1:cS2FBb+xfLuaSqvEmbqIeKUVFgHdHVHtVeXb2epof3M=
|
||||
k8s.io/api v0.36.3 h1:NxB+05W2UGqXWFXcLO0RB5cnqnUPP5v5sVlaOH0Iz4w=
|
||||
k8s.io/api v0.36.3/go.mod h1:JzLQKqRHC5+I8RVj/lS3lCg0mg6nWI9Fo/Sk3ElxHzg=
|
||||
k8s.io/apiextensions-apiserver v0.36.3 h1:dPmOAPhwTtqb1bTxbFPsy18KHPhktQeO3WUPXunZIB0=
|
||||
k8s.io/apiextensions-apiserver v0.36.3/go.mod h1:KTXFqgXiuw2pRoL+Wpmttqc+up9Xt/GohadPWeLLOa4=
|
||||
k8s.io/apimachinery v0.36.3 h1:PkzMRBRG8joFD8EhCuQAtNPvJlxb82FwplP26HIzvAM=
|
||||
k8s.io/apimachinery v0.36.3/go.mod h1:cTSjBWgPe/6CQyBKzY/hDIRWCQQQeK0mfLbml0UYFHE=
|
||||
k8s.io/client-go v0.36.3 h1:M4JdVzXxYcZk4fGpfDdYnxSwhLKWCFoQsHW6t+z8Hfg=
|
||||
k8s.io/client-go v0.36.3/go.mod h1:gcPwr0c87vjjG6HB6pWEqOeuYVoXSsREjzux2j6GF30=
|
||||
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-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A=
|
||||
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I=
|
||||
k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 h1:jVkFFVfXdXP74B/zbO3hM3hpSFD0xvhQ5U686DPurkE=
|
||||
k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3/go.mod h1:M2s5JB1lIYP3jzZdorPLHXIPJzt9vv2muW5a6L9DtNM=
|
||||
sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4=
|
||||
sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw=
|
||||
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.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3CIki3aW8c4Q=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.4.2/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=
|
||||
|
||||
Generated
+61
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1783224372,
|
||||
"narHash": "sha256-8i/87eeoqiGE4yOTjwSA3Eh/ziJRQEmd/unYU+K27sk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d407951447dcd00442e97087bf374aad70c04cea",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
description = "Troubleshoot development environment";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in {
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
go_1_26
|
||||
git # required for go VCS stamping during build
|
||||
python3 # required by the dependency-update validation stage
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
echo "Troubleshoot dev — $(go version)" # go 1.26.x required
|
||||
'';
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -1,290 +1,279 @@
|
||||
module github.com/replicatedhq/troubleshoot
|
||||
|
||||
go 1.25.4
|
||||
go 1.26.5
|
||||
|
||||
require (
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.48.0
|
||||
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.43.6
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.36
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.107.2
|
||||
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/go-logr/logr v1.4.3
|
||||
github.com/cilium/ebpf v0.22.0
|
||||
github.com/containerd/cgroups/v3 v3.1.3
|
||||
github.com/fatih/color v1.19.0
|
||||
github.com/go-logr/logr v1.4.4
|
||||
github.com/go-redis/redis/v7 v7.4.1
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/go-sql-driver/mysql v1.10.0
|
||||
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.9
|
||||
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.8
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/jackc/pgx/v5 v5.7.6
|
||||
github.com/longhorn/go-iscsi-helper v0.0.0-20210330030558-49a327fb024e
|
||||
github.com/jackc/pgx/v5 v5.10.0
|
||||
github.com/longhorn/go-common-libs v0.0.0-20260730002911-add09e6eb92c
|
||||
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.24
|
||||
github.com/microsoft/go-mssqldb v1.10.0
|
||||
github.com/miekg/dns v1.1.73
|
||||
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.7
|
||||
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/stretchr/testify v1.12.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.2
|
||||
go.opentelemetry.io/otel v1.45.0
|
||||
go.opentelemetry.io/otel/sdk v1.45.0
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
|
||||
golang.org/x/mod v0.40.0
|
||||
golang.org/x/sync v0.22.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.3
|
||||
k8s.io/apiextensions-apiserver v0.36.3
|
||||
k8s.io/apimachinery v0.36.3
|
||||
k8s.io/apiserver v0.36.3
|
||||
k8s.io/cli-runtime v0.36.3
|
||||
k8s.io/client-go v0.36.3
|
||||
k8s.io/klog/v2 v2.140.0
|
||||
k8s.io/streaming v0.36.3
|
||||
oras.land/oras-go/v2 v2.6.2
|
||||
sigs.k8s.io/controller-runtime v0.24.1
|
||||
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.2 // indirect
|
||||
cloud.google.com/go/auth v0.22.0 // 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.30.0 // 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.2.0 // indirect
|
||||
github.com/ClickHouse/ch-go v0.74.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.35.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.59.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.59.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/semver/v3 v3.5.0 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.4.1 // indirect
|
||||
github.com/andybalholm/brotli v1.2.2 // 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/chai2010/gettext-go v1.0.2 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/avast/retry-go/v4 v4.7.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.35 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.37 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.37 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.37 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.38 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.37 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.38 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.5.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.33.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.45.6 // indirect
|
||||
github.com/aws/smithy-go v1.27.8 // indirect
|
||||
github.com/chai2010/gettext-go v1.0.3 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
|
||||
github.com/cockroachdb/errors v1.14.0 // indirect
|
||||
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect
|
||||
github.com/cockroachdb/redact v1.1.8 // 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.7.0 // indirect
|
||||
github.com/docker/cli v29.7.1+incompatible // indirect
|
||||
github.com/ebitengine/purego v0.10.2 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 // 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/fxamacker/cbor/v2 v2.9.2 // indirect
|
||||
github.com/getsentry/sentry-go v0.48.0 // indirect
|
||||
github.com/go-faster/city v1.0.1 // indirect
|
||||
github.com/go-faster/errors v0.8.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/go-openapi/swag/cmdutils v0.28.0 // indirect
|
||||
github.com/go-openapi/swag/conv v0.28.0 // indirect
|
||||
github.com/go-openapi/swag/fileutils v0.28.0 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.28.0 // indirect
|
||||
github.com/go-openapi/swag/loading v0.28.0 // indirect
|
||||
github.com/go-openapi/swag/mangling v0.28.0 // indirect
|
||||
github.com/go-openapi/swag/netutils v0.28.0 // indirect
|
||||
github.com/go-openapi/swag/pools v0.28.0 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.28.0 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.28.0 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.28.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // 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/gnostic-models v0.7.1 // 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.19 // 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.74 // 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
|
||||
github.com/jmoiron/sqlx v1.4.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/mistifyio/go-zfs/v3 v3.0.1 // indirect
|
||||
github.com/lib/pq v1.12.3 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20260801144041-2fc331e7910f // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.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/paulmach/orb v0.13.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.27 // 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/rogpeppe/go-internal v1.15.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/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||
github.com/segmentio/asm v1.2.1 // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/sylabs/sif/v2 v2.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/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.8.1 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/ulikunitz/xz v0.5.16 // 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.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
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.45.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260729162451-8efbd57d26e0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
gotest.tools/v3 v3.5.2 // indirect
|
||||
k8s.io/component-base v0.36.3 // indirect
|
||||
k8s.io/kubectl v0.36.3 // 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.4.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.12.0 // indirect
|
||||
cloud.google.com/go/storage v1.64.0 // 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/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8 // 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/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.8 // 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
|
||||
github.com/go-errors/errors v1.4.2 // indirect
|
||||
github.com/felixge/httpsnoop v1.1.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.10.1 // indirect
|
||||
github.com/go-errors/errors v1.5.1 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
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/go-openapi/jsonpointer v1.0.0 // indirect
|
||||
github.com/go-openapi/jsonreference v1.0.0 // indirect
|
||||
github.com/go-openapi/swag v0.28.0 // 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.23.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.19.2 // 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/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.27 // 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
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect
|
||||
github.com/nsf/termbox-go v1.1.1 // 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/pelletier/go-toml/v2 v2.4.3 // 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/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // 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.4.0 // indirect
|
||||
github.com/tklauser/numcpus v0.12.0 // 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.55.0 // indirect
|
||||
golang.org/x/net v0.58.0
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.org/x/term v0.45.0 // indirect
|
||||
golang.org/x/text v0.41.0
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
google.golang.org/api v0.292.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260729162451-8efbd57d26e0 // indirect
|
||||
google.golang.org/grpc v1.83.0 // 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.21.4
|
||||
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect
|
||||
k8s.io/kubelet v0.36.3
|
||||
k8s.io/metrics v0.36.3
|
||||
k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3
|
||||
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:
|
||||
@@ -236,6 +238,8 @@ func GetAnalyzer(analyzer *troubleshootv1beta2.Analyze) Analyzer {
|
||||
return &AnalyzeMssql{analyzer: analyzer.Mssql}
|
||||
case analyzer.Redis != nil:
|
||||
return &AnalyzeRedis{analyzer: analyzer.Redis}
|
||||
case analyzer.ClickHouse != nil:
|
||||
return &AnalyzeClickhouse{analyzer: analyzer.ClickHouse}
|
||||
case analyzer.CephStatus != nil:
|
||||
return &AnalyzeCephStatus{analyzer: analyzer.CephStatus}
|
||||
case analyzer.Velero != nil:
|
||||
@@ -260,6 +264,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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
)
|
||||
|
||||
type AnalyzeClickhouse struct {
|
||||
analyzer *troubleshootv1beta2.DatabaseAnalyze
|
||||
}
|
||||
|
||||
func (a *AnalyzeClickhouse) Title() string {
|
||||
title := a.analyzer.CheckName
|
||||
if title == "" {
|
||||
title = a.collectorName()
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
func (a *AnalyzeClickhouse) IsExcluded() (bool, error) {
|
||||
return isExcluded(a.analyzer.Exclude)
|
||||
}
|
||||
|
||||
func (a *AnalyzeClickhouse) Analyze(getFile getCollectedFileContents, findFiles getChildCollectedFileContents) ([]*AnalyzeResult, error) {
|
||||
result, err := a.analyze(a.analyzer, getFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Strict = a.analyzer.Strict.BoolOrDefaultFalse()
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
func (a *AnalyzeClickhouse) collectorName() string {
|
||||
if a.analyzer.CollectorName != "" {
|
||||
return a.analyzer.CollectorName
|
||||
}
|
||||
return "clickhouse"
|
||||
}
|
||||
|
||||
func compareClickhouseConditionalToActual(conditional string, result *collect.DatabaseConnection) (bool, error) {
|
||||
parts := strings.Split(strings.TrimSpace(conditional), " ")
|
||||
|
||||
if len(parts) != 3 {
|
||||
return false, errors.New("unable to parse conditional")
|
||||
}
|
||||
|
||||
switch parts[0] {
|
||||
case "connected":
|
||||
expected, err := strconv.ParseBool(parts[2])
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to parse bool")
|
||||
}
|
||||
|
||||
switch parts[1] {
|
||||
case "=", "==", "===":
|
||||
return expected == result.IsConnected, nil
|
||||
case "!=", "!==":
|
||||
return expected != result.IsConnected, nil
|
||||
}
|
||||
|
||||
return false, errors.New("unable to parse ClickHouse connected analyzer")
|
||||
|
||||
case "version":
|
||||
expected, err := version.NewVersion(strings.ReplaceAll(parts[2], "x", "0"))
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to parse expected version")
|
||||
}
|
||||
|
||||
operation := parts[1]
|
||||
switch operation {
|
||||
case "=", "==", "===":
|
||||
operation = "="
|
||||
case "!=", "!==":
|
||||
operation = "!="
|
||||
}
|
||||
|
||||
actual, err := version.NewVersion(strings.ReplaceAll(result.Version, "x", "0"))
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to parse ClickHouse db actual version")
|
||||
}
|
||||
|
||||
constraints, err := version.NewConstraint(fmt.Sprintf("%s %s", operation, expected))
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to create constraint")
|
||||
}
|
||||
return constraints.Check(actual), nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (a *AnalyzeClickhouse) analyze(analyzer *troubleshootv1beta2.DatabaseAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
|
||||
fullPath := path.Join("", fmt.Sprintf("clickhouse/%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 database connection result")
|
||||
}
|
||||
|
||||
result := &AnalyzeResult{
|
||||
Title: a.Title(),
|
||||
IconKey: "kubernetes_clickhouse_analyze",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/clickhouse-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 := compareClickhouseConditionalToActual(outcome.Fail.When, &databaseConnection)
|
||||
if err != nil {
|
||||
return result, errors.Wrap(err, "failed to compare ClickHouse database conditional")
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
if databaseConnection.Error != "" {
|
||||
result.Message = outcome.Fail.Message + " " + databaseConnection.Error
|
||||
} else {
|
||||
result.Message = outcome.Fail.Message
|
||||
}
|
||||
|
||||
result.IsFail = true
|
||||
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 := compareClickhouseConditionalToActual(outcome.Warn.When, &databaseConnection)
|
||||
if err != nil {
|
||||
return result, errors.Wrap(err, "failed to compare ClickHouse database 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 := compareClickhouseConditionalToActual(outcome.Pass.When, &databaseConnection)
|
||||
if err != nil {
|
||||
return result, errors.Wrap(err, "failed to compare ClickHouse database conditional")
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -699,6 +699,10 @@ func (e *DefaultAnalysisEngine) convertAnalyzerToSpec(analyzer *troubleshootv1be
|
||||
spec.Name = "redis"
|
||||
spec.Type = "database"
|
||||
spec.Config["analyzer"] = analyzer.Redis
|
||||
case analyzer.ClickHouse != nil:
|
||||
spec.Name = "clickhouse"
|
||||
spec.Type = "database"
|
||||
spec.Config["analyzer"] = analyzer.ClickHouse
|
||||
|
||||
// ✅ Storage analyzers
|
||||
case analyzer.CephStatus != 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
|
||||
@@ -203,6 +219,8 @@ func compareNodeResourceConditionalToActual(conditional string, matchingNodes []
|
||||
switch function {
|
||||
case "count":
|
||||
actualValue = len(matchingNodes)
|
||||
case "countDistinct":
|
||||
actualValue = countDistinctLabelValues(matchingNodes, property)
|
||||
case "min":
|
||||
actualValue = findMin(matchingNodes, property, resourceName)
|
||||
case "max":
|
||||
@@ -352,6 +370,19 @@ func getQuantity(node corev1.Node, property string, resourceName string) *resour
|
||||
return nil
|
||||
}
|
||||
|
||||
// countDistinctLabelValues returns the number of distinct values of labelKey
|
||||
// across the given nodes. Nodes missing the label are ignored, so an absent
|
||||
// label yields 0.
|
||||
func countDistinctLabelValues(nodes []corev1.Node, labelKey string) int {
|
||||
seen := map[string]struct{}{}
|
||||
for _, node := range nodes {
|
||||
if v, ok := node.Labels[labelKey]; ok {
|
||||
seen[v] = struct{}{}
|
||||
}
|
||||
}
|
||||
return len(seen)
|
||||
}
|
||||
|
||||
func findSum(nodes []corev1.Node, property string, resourceName string) *resource.Quantity {
|
||||
sum := resource.Quantity{}
|
||||
|
||||
@@ -453,7 +484,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) {
|
||||
@@ -1664,6 +1666,116 @@ func Test_analyzeNodeResources(t *testing.T) {
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "countDistinct spans at least 3 instance types", // countDistinct pass path across all nodes
|
||||
analyzer: &troubleshootv1beta2.NodeResources{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "instance-type spread",
|
||||
},
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "countDistinct(node.kubernetes.io/instance-type) < 3",
|
||||
Message: "Fewer than 3 distinct instance types.",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "At least 3 distinct instance types.",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
IsPass: true,
|
||||
IsFail: false,
|
||||
IsWarn: false,
|
||||
Title: "instance-type spread",
|
||||
Message: "At least 3 distinct instance types.",
|
||||
URI: "",
|
||||
IconKey: "kubernetes_node_resources",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "countDistinct only counts filtered nodes", // filtering to one pool leaves a single distinct value
|
||||
analyzer: &troubleshootv1beta2.NodeResources{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "filtered instance-type spread",
|
||||
},
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "countDistinct(node.kubernetes.io/instance-type) < 3",
|
||||
Message: "Fewer than 3 distinct instance types.",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "At least 3 distinct instance types.",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
Selector: &troubleshootv1beta2.NodeResourceSelectors{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "node.kubernetes.io/instance-type",
|
||||
Operator: metav1.LabelSelectorOpIn,
|
||||
Values: []string{"s-2vcpu-4gb"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
IsPass: false,
|
||||
IsFail: false,
|
||||
IsWarn: true,
|
||||
Title: "filtered instance-type spread",
|
||||
Message: "Fewer than 3 distinct instance types.",
|
||||
URI: "",
|
||||
IconKey: "kubernetes_node_resources",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "countDistinct is 0 when the label is absent", // AIR-238 zone syntax; fixture nodes carry no zone label
|
||||
analyzer: &troubleshootv1beta2.NodeResources{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "zone spread",
|
||||
},
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "countDistinct(topology.kubernetes.io/zone) < 3",
|
||||
Message: "Nodes span fewer than 3 availability zones.",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "Nodes span at least 3 availability zones.",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
IsPass: false,
|
||||
IsFail: false,
|
||||
IsWarn: true,
|
||||
Title: "zone spread",
|
||||
Message: "Nodes span fewer than 3 availability zones.",
|
||||
URI: "",
|
||||
IconKey: "kubernetes_node_resources",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
getExampleNodeContents := func(nodeName string) ([]byte, error) {
|
||||
@@ -1684,3 +1796,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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+54
-25
@@ -30,6 +30,9 @@ func (a *AnalyzeSecret) Analyze(getFile getCollectedFileContents, findFiles getC
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result == nil {
|
||||
return nil, nil
|
||||
}
|
||||
result.Strict = a.analyzer.Strict.BoolOrDefaultFalse()
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
@@ -54,42 +57,68 @@ func (a *AnalyzeSecret) analyzeSecret(analyzer *troubleshootv1beta2.AnalyzeSecre
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The secret analyzer only supports fail (not found) and pass (found) outcomes
|
||||
// per https://troubleshoot.sh/docs/analyze/secrets. If the spec contains
|
||||
// neither, return an explicit error: returning (nil, nil) is swallowed by the
|
||||
// Analyze wrapper into an empty result slice, so the misconfiguration would
|
||||
// surface as neither a result nor an error.
|
||||
// Capture fail and pass independently: a single outcome object may set both,
|
||||
// so an else-if here would silently drop the second one.
|
||||
var failOutcome, passOutcome *troubleshootv1beta2.SingleOutcome
|
||||
for _, outcome := range analyzer.Outcomes {
|
||||
if outcome.Fail != nil {
|
||||
failOutcome = outcome.Fail
|
||||
}
|
||||
if outcome.Pass != nil {
|
||||
passOutcome = outcome.Pass
|
||||
}
|
||||
}
|
||||
if failOutcome == nil && passOutcome == nil {
|
||||
return nil, fmt.Errorf("secret analyzer %s/%s must define at least one pass or fail outcome", analyzer.Namespace, analyzer.SecretName)
|
||||
}
|
||||
|
||||
result := AnalyzeResult{
|
||||
Title: a.Title(),
|
||||
IconKey: "kubernetes_analyze_secret",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/secret.svg?w=13&h=16",
|
||||
}
|
||||
|
||||
var failOutcome *troubleshootv1beta2.Outcome
|
||||
for _, outcome := range analyzer.Outcomes {
|
||||
if outcome.Fail != nil {
|
||||
failOutcome = outcome
|
||||
}
|
||||
secretFound := foundSecret.SecretExists
|
||||
if secretFound && analyzer.Key != "" {
|
||||
secretFound = foundSecret.Key == analyzer.Key && foundSecret.KeyExists
|
||||
}
|
||||
|
||||
if !foundSecret.SecretExists {
|
||||
// Use the matched branch's configured outcome verbatim, tracking whether one
|
||||
// was actually present. A configured outcome with an intentionally empty
|
||||
// message (e.g. a URI-only outcome) is preserved as-is. But when the matched
|
||||
// branch has NO configured outcome at all — e.g. a pass-only spec that took
|
||||
// the fail path, or a fail-only spec that passed — the empty message is not
|
||||
// an intentional choice, so fall back to a default diagnostic. An absent
|
||||
// outcome is not the same as an intentionally empty one.
|
||||
outcomeConfigured := false
|
||||
if secretFound {
|
||||
result.IsPass = true
|
||||
if passOutcome != nil {
|
||||
result.Message = passOutcome.Message
|
||||
result.URI = passOutcome.URI
|
||||
outcomeConfigured = true
|
||||
}
|
||||
} else {
|
||||
result.IsFail = true
|
||||
result.Message = failOutcome.Fail.Message
|
||||
result.URI = failOutcome.Fail.URI
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
if analyzer.Key != "" {
|
||||
if foundSecret.Key != analyzer.Key || !foundSecret.KeyExists {
|
||||
result.IsFail = true
|
||||
result.Message = failOutcome.Fail.Message
|
||||
result.URI = failOutcome.Fail.URI
|
||||
|
||||
return &result, nil
|
||||
if failOutcome != nil {
|
||||
result.Message = failOutcome.Message
|
||||
result.URI = failOutcome.URI
|
||||
outcomeConfigured = true
|
||||
}
|
||||
}
|
||||
|
||||
result.IsPass = true
|
||||
for _, outcome := range analyzer.Outcomes {
|
||||
if outcome.Pass != nil {
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
if !outcomeConfigured {
|
||||
switch {
|
||||
case result.IsPass:
|
||||
result.Message = fmt.Sprintf("Secret %s was found in namespace %s", analyzer.SecretName, analyzer.Namespace)
|
||||
case analyzer.Key != "" && foundSecret.SecretExists:
|
||||
result.Message = fmt.Sprintf("Key %s was not found in secret %s/%s", analyzer.Key, analyzer.Namespace, analyzer.SecretName)
|
||||
default:
|
||||
result.Message = fmt.Sprintf("Secret %s was not found in namespace %s", analyzer.SecretName, analyzer.Namespace)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -166,6 +166,118 @@ func Test_analyzeSecret(t *testing.T) {
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/secret.svg?w=13&h=16",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not found with no fail outcome falls back to a default message (no configured outcome for this branch)",
|
||||
analyzer: &troubleshootv1beta2.AnalyzeSecret{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "Optional Secret",
|
||||
},
|
||||
Namespace: "default",
|
||||
SecretName: "does-not-exist",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "secret found",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mockFiles: map[string][]byte{
|
||||
"secrets/default/does-not-exist.json": mustJSONMarshalIndent(t, collect.SecretOutput{
|
||||
Namespace: "default",
|
||||
Name: "does-not-exist",
|
||||
SecretExists: false,
|
||||
}),
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
IsFail: true,
|
||||
Message: "Secret does-not-exist was not found in namespace default",
|
||||
Title: "Optional Secret",
|
||||
IconKey: "kubernetes_analyze_secret",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/secret.svg?w=13&h=16",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "key not found with no fail outcome falls back to a default message (no configured outcome for this branch)",
|
||||
analyzer: &troubleshootv1beta2.AnalyzeSecret{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "Optional Secret Key",
|
||||
},
|
||||
Namespace: "test-namespace",
|
||||
SecretName: "test-secret",
|
||||
Key: "missing-key",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "key found",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mockFiles: map[string][]byte{
|
||||
"secrets/test-namespace/test-secret/missing-key.json": mustJSONMarshalIndent(t, collect.SecretOutput{
|
||||
Namespace: "test-namespace",
|
||||
Name: "test-secret",
|
||||
Key: "missing-key",
|
||||
SecretExists: true,
|
||||
KeyExists: false,
|
||||
}),
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
IsFail: true,
|
||||
Message: "Key missing-key was not found in secret test-namespace/test-secret",
|
||||
Title: "Optional Secret Key",
|
||||
IconKey: "kubernetes_analyze_secret",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/secret.svg?w=13&h=16",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "found with only fail outcome configured falls back to the default pass message, not the fail outcome's message",
|
||||
analyzer: &troubleshootv1beta2.AnalyzeSecret{
|
||||
Namespace: "test-namespace",
|
||||
SecretName: "test-secret",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "Not found",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mockFiles: map[string][]byte{
|
||||
"secrets/test-namespace/test-secret.json": mustJSONMarshalIndent(t, collect.SecretOutput{
|
||||
Namespace: "test-namespace",
|
||||
Name: "test-secret",
|
||||
SecretExists: true,
|
||||
}),
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
IsPass: true,
|
||||
Message: "Secret test-secret was found in namespace test-namespace",
|
||||
Title: "Secret test-secret",
|
||||
IconKey: "kubernetes_analyze_secret",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/secret.svg?w=13&h=16",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "spec with neither fail nor pass outcome returns an error so the framework surfaces the misconfiguration",
|
||||
analyzer: &troubleshootv1beta2.AnalyzeSecret{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "Misconfigured",
|
||||
},
|
||||
Namespace: "default",
|
||||
SecretName: "does-not-exist",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{},
|
||||
},
|
||||
mockFiles: map[string][]byte{
|
||||
"secrets/default/does-not-exist.json": mustJSONMarshalIndent(t, collect.SecretOutput{
|
||||
Namespace: "default",
|
||||
Name: "does-not-exist",
|
||||
SecretExists: false,
|
||||
}),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "key not found secret not found",
|
||||
analyzer: &troubleshootv1beta2.AnalyzeSecret{
|
||||
@@ -190,6 +302,128 @@ func Test_analyzeSecret(t *testing.T) {
|
||||
},
|
||||
wantErr: true, // TODO: should this be a not found error? This will not work with selectors.
|
||||
},
|
||||
{
|
||||
name: "combined fail and pass in a single outcome, secret found uses the pass outcome",
|
||||
analyzer: &troubleshootv1beta2.AnalyzeSecret{
|
||||
Namespace: "test-namespace",
|
||||
SecretName: "test-secret",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "Not found",
|
||||
},
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "Found",
|
||||
URI: "https://example.com/found",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mockFiles: map[string][]byte{
|
||||
"secrets/test-namespace/test-secret.json": mustJSONMarshalIndent(t, collect.SecretOutput{
|
||||
Namespace: "test-namespace",
|
||||
Name: "test-secret",
|
||||
SecretExists: true,
|
||||
}),
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
IsPass: true,
|
||||
Message: "Found",
|
||||
URI: "https://example.com/found",
|
||||
Title: "Secret test-secret",
|
||||
IconKey: "kubernetes_analyze_secret",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/secret.svg?w=13&h=16",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "combined fail and pass in a single outcome, secret not found uses the fail outcome",
|
||||
analyzer: &troubleshootv1beta2.AnalyzeSecret{
|
||||
Namespace: "test-namespace",
|
||||
SecretName: "test-secret",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "Not found",
|
||||
},
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "Found",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mockFiles: map[string][]byte{
|
||||
"secrets/test-namespace/test-secret.json": mustJSONMarshalIndent(t, collect.SecretOutput{
|
||||
Namespace: "test-namespace",
|
||||
Name: "test-secret",
|
||||
SecretExists: false,
|
||||
}),
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
IsFail: true,
|
||||
Message: "Not found",
|
||||
Title: "Secret test-secret",
|
||||
IconKey: "kubernetes_analyze_secret",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/secret.svg?w=13&h=16",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "secret found with URI-only pass outcome preserves the empty message and URI",
|
||||
analyzer: &troubleshootv1beta2.AnalyzeSecret{
|
||||
Namespace: "test-namespace",
|
||||
SecretName: "test-secret",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
URI: "https://example.com/pass",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mockFiles: map[string][]byte{
|
||||
"secrets/test-namespace/test-secret.json": mustJSONMarshalIndent(t, collect.SecretOutput{
|
||||
Namespace: "test-namespace",
|
||||
Name: "test-secret",
|
||||
SecretExists: true,
|
||||
}),
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
IsPass: true,
|
||||
Message: "",
|
||||
URI: "https://example.com/pass",
|
||||
Title: "Secret test-secret",
|
||||
IconKey: "kubernetes_analyze_secret",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/secret.svg?w=13&h=16",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "secret not found with URI-only fail outcome preserves the empty message and URI",
|
||||
analyzer: &troubleshootv1beta2.AnalyzeSecret{
|
||||
Namespace: "test-namespace",
|
||||
SecretName: "test-secret",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
URI: "https://example.com/fail",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mockFiles: map[string][]byte{
|
||||
"secrets/test-namespace/test-secret.json": mustJSONMarshalIndent(t, collect.SecretOutput{
|
||||
Namespace: "test-namespace",
|
||||
Name: "test-secret",
|
||||
SecretExists: false,
|
||||
}),
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
IsFail: true,
|
||||
Message: "",
|
||||
URI: "https://example.com/fail",
|
||||
Title: "Secret test-secret",
|
||||
IconKey: "kubernetes_analyze_secret",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/secret.svg?w=13&h=16",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -261,7 +261,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
|
||||
result = append(result, authorizationv1.SelfSubjectAccessReviewSpec{
|
||||
ResourceAttributes: &authorizationv1.ResourceAttributes{
|
||||
Namespace: pickNamespaceOrDefault(c.Exec.Namespace, overrideNS),
|
||||
Verb: "get",
|
||||
Verb: "create",
|
||||
Group: "",
|
||||
Version: "",
|
||||
Resource: "pods",
|
||||
@@ -286,7 +286,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
|
||||
result = append(result, authorizationv1.SelfSubjectAccessReviewSpec{
|
||||
ResourceAttributes: &authorizationv1.ResourceAttributes{
|
||||
Namespace: pickNamespaceOrDefault(c.Copy.Namespace, overrideNS),
|
||||
Verb: "get",
|
||||
Verb: "create",
|
||||
Group: "",
|
||||
Version: "",
|
||||
Resource: "pods",
|
||||
|
||||
@@ -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"`
|
||||
@@ -297,6 +305,7 @@ type Analyze struct {
|
||||
Mssql *DatabaseAnalyze `json:"mssql,omitempty" yaml:"mssql,omitempty"`
|
||||
Mysql *DatabaseAnalyze `json:"mysql,omitempty" yaml:"mysql,omitempty"`
|
||||
Redis *DatabaseAnalyze `json:"redis,omitempty" yaml:"redis,omitempty"`
|
||||
ClickHouse *DatabaseAnalyze `json:"clickhouse,omitempty" yaml:"clickhouse,omitempty"`
|
||||
CephStatus *CephStatusAnalyze `json:"cephStatus,omitempty" yaml:"cephStatus,omitempty"`
|
||||
Velero *VeleroAnalyze `json:"velero,omitempty" yaml:"velero,omitempty"`
|
||||
Longhorn *LonghornAnalyze `json:"longhorn,omitempty" yaml:"longhorn,omitempty"`
|
||||
@@ -309,4 +318,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,56 @@ 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"`
|
||||
ClickHouse *Database `json:"clickhouse,omitempty" yaml:"clickhouse,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 {
|
||||
@@ -511,7 +530,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
|
||||
result = append(result, authorizationv1.SelfSubjectAccessReviewSpec{
|
||||
ResourceAttributes: &authorizationv1.ResourceAttributes{
|
||||
Namespace: pickNamespaceOrDefault(c.Exec.Namespace, overrideNS),
|
||||
Verb: "get",
|
||||
Verb: "create",
|
||||
Group: "",
|
||||
Version: "",
|
||||
Resource: "pods",
|
||||
@@ -536,7 +555,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
|
||||
result = append(result, authorizationv1.SelfSubjectAccessReviewSpec{
|
||||
ResourceAttributes: &authorizationv1.ResourceAttributes{
|
||||
Namespace: pickNamespaceOrDefault(c.Copy.Namespace, overrideNS),
|
||||
Verb: "get",
|
||||
Verb: "create",
|
||||
Group: "",
|
||||
Version: "",
|
||||
Resource: "pods",
|
||||
@@ -568,6 +587,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
|
||||
@@ -647,6 +681,10 @@ func (c *Collect) GetName() string {
|
||||
collector = "redis"
|
||||
name = c.Redis.CollectorName
|
||||
}
|
||||
if c.ClickHouse != nil {
|
||||
collector = "clickhouse"
|
||||
name = c.ClickHouse.CollectorName
|
||||
}
|
||||
if c.Collectd != nil {
|
||||
collector = "collectd"
|
||||
name = c.Collectd.CollectorName
|
||||
@@ -671,6 +709,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)
|
||||
@@ -170,6 +175,11 @@ func (in *Analyze) DeepCopyInto(out *Analyze) {
|
||||
*out = new(DatabaseAnalyze)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.ClickHouse != nil {
|
||||
in, out := &in.ClickHouse, &out.ClickHouse
|
||||
*out = new(DatabaseAnalyze)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.CephStatus != nil {
|
||||
in, out := &in.CephStatus, &out.CephStatus
|
||||
*out = new(CephStatusAnalyze)
|
||||
@@ -230,6 +240,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 +458,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))
|
||||
@@ -920,6 +940,11 @@ func (in *Collect) DeepCopyInto(out *Collect) {
|
||||
*out = new(Database)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.ClickHouse != nil {
|
||||
in, out := &in.ClickHouse, &out.ClickHouse
|
||||
*out = new(Database)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Collectd != nil {
|
||||
in, out := &in.Collectd, &out.Collectd
|
||||
*out = new(Collectd)
|
||||
@@ -980,6 +1005,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 +2009,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 +2254,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 +2704,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 +3132,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 +4791,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 +5047,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
|
||||
|
||||
@@ -68,10 +68,11 @@ type HostCollect struct {
|
||||
// For phase 1, we're focusing on Database collectors with StringOrValueFrom support
|
||||
type Collect struct {
|
||||
// Database collectors with v1beta3 StringOrValueFrom support
|
||||
Postgres *Database `json:"postgres,omitempty" yaml:"postgres,omitempty"`
|
||||
Mssql *Database `json:"mssql,omitempty" yaml:"mssql,omitempty"`
|
||||
Mysql *Database `json:"mysql,omitempty" yaml:"mysql,omitempty"`
|
||||
Redis *Database `json:"redis,omitempty" yaml:"redis,omitempty"`
|
||||
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"`
|
||||
ClickHouse *Database `json:"clickhouse,omitempty" yaml:"clickhouse,omitempty"`
|
||||
|
||||
// TODO: Add remaining collector types as we expand v1beta3 support
|
||||
// For now, these are placeholders to make the types compile
|
||||
|
||||
@@ -81,6 +81,14 @@ func convertCollector(
|
||||
v2collector.Redis = db
|
||||
}
|
||||
|
||||
if v3collector.ClickHouse != nil {
|
||||
db, err := convertDatabase(ctx, v3collector.ClickHouse, client, defaultNamespace)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert clickhouse collector: %w", err)
|
||||
}
|
||||
v2collector.ClickHouse = db
|
||||
}
|
||||
|
||||
// TODO: Add conversion for other collector types as v1beta3 support expands
|
||||
|
||||
return v2collector, nil
|
||||
|
||||
@@ -171,7 +171,17 @@ func TestConvertToV1Beta2WithResolution_MultipleDatabases(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
client := fake.NewSimpleClientset(pgSecret, mysqlSecret, redisSecret)
|
||||
clickhouseSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "clickhouse-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"uri": []byte("http://clickhouse.example.com:9000"),
|
||||
},
|
||||
}
|
||||
|
||||
client := fake.NewSimpleClientset(pgSecret, mysqlSecret, redisSecret, clickhouseSecret)
|
||||
|
||||
v3spec := &SupportBundleSpec{
|
||||
Collectors: []*Collect{
|
||||
@@ -211,13 +221,25 @@ func TestConvertToV1Beta2WithResolution_MultipleDatabases(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ClickHouse: &Database{
|
||||
URI: StringOrValueFrom{
|
||||
ValueFrom: &ValueFromSource{
|
||||
SecretKeyRef: &SecretKeyRef{
|
||||
Name: "clickhouse-secret",
|
||||
Key: "uri",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
v2spec, err := ConvertToV1Beta2WithResolution(context.Background(), v3spec, client, "default")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, v2spec)
|
||||
require.Len(t, v2spec.Collectors, 3)
|
||||
require.Len(t, v2spec.Collectors, 4)
|
||||
|
||||
require.NotNil(t, v2spec.Collectors[0].Postgres)
|
||||
assert.Equal(t, "postgresql://user:pass@pg.example.com:5432/db", v2spec.Collectors[0].Postgres.URI)
|
||||
@@ -227,6 +249,8 @@ func TestConvertToV1Beta2WithResolution_MultipleDatabases(t *testing.T) {
|
||||
|
||||
require.NotNil(t, v2spec.Collectors[2].Redis)
|
||||
assert.Equal(t, "redis://redis.example.com:6379", v2spec.Collectors[2].Redis.URI)
|
||||
require.NotNil(t, v2spec.Collectors[3].ClickHouse)
|
||||
assert.Equal(t, "http://clickhouse.example.com:9000", v2spec.Collectors[3].ClickHouse.URI)
|
||||
}
|
||||
|
||||
func TestConvertToV1Beta2WithResolution_SecretNotFound(t *testing.T) {
|
||||
|
||||
@@ -79,6 +79,11 @@ func (in *Collect) DeepCopyInto(out *Collect) {
|
||||
*out = new(Database)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.ClickHouse != nil {
|
||||
in, out := &in.ClickHouse, &out.ClickHouse
|
||||
*out = new(Database)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Collect.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package collect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
var _ Collector = &CollectClickhouse{}
|
||||
|
||||
type CollectClickhouse struct {
|
||||
Collector *troubleshootv1beta2.Database
|
||||
BundlePath string
|
||||
Namespace string
|
||||
ClientConfig *rest.Config
|
||||
Client kubernetes.Interface
|
||||
Context context.Context
|
||||
RBACErrors
|
||||
}
|
||||
|
||||
func (c *CollectClickhouse) Title() string {
|
||||
return getCollectorName(c)
|
||||
}
|
||||
|
||||
func (c *CollectClickhouse) IsExcluded() (bool, error) {
|
||||
return isExcluded(c.Collector.Exclude)
|
||||
}
|
||||
|
||||
func (c *CollectClickhouse) createConnectConfig() (*clickhouse.Options, error) {
|
||||
if c.Collector.URI == "" {
|
||||
return nil, errors.New("clickhouse uri cannot be empty")
|
||||
}
|
||||
|
||||
opts, err := clickhouse.ParseDSN(c.Collector.URI)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "couldn't parse clickhouse URI")
|
||||
}
|
||||
|
||||
if c.Collector.TLS != nil {
|
||||
tlsCfg, err := createTLSConfig(c.Context, c.Client, c.Collector.TLS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts.TLS = tlsCfg
|
||||
}
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func (c *CollectClickhouse) connect() (driver.Conn, error) {
|
||||
config, err := c.createConnectConfig()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create clickhouse connection config")
|
||||
}
|
||||
|
||||
conn, err := clickhouse.Open(config)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to open clickhouse connection")
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *CollectClickhouse) Collect(progressChan chan<- interface{}) (CollectorResult, error) {
|
||||
databaseConnection := DatabaseConnection{}
|
||||
|
||||
conn, err := c.connect()
|
||||
if err != nil {
|
||||
databaseConnection.Error = err.Error()
|
||||
} else {
|
||||
defer conn.Close()
|
||||
|
||||
if err := conn.Ping(c.Context); err != nil {
|
||||
databaseConnection.Error = err.Error()
|
||||
} else {
|
||||
|
||||
var version string
|
||||
err := conn.QueryRow(c.Context, "SELECT version()").Scan(&version)
|
||||
if err != nil {
|
||||
databaseConnection.Error = err.Error()
|
||||
} else {
|
||||
databaseConnection.Version = version
|
||||
databaseConnection.IsConnected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b, err := json.Marshal(databaseConnection)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal database connection")
|
||||
}
|
||||
|
||||
collectorName := c.Collector.CollectorName
|
||||
if collectorName == "" {
|
||||
collectorName = "clickhouse"
|
||||
}
|
||||
|
||||
output := NewResult()
|
||||
output.SaveResult(c.BundlePath, fmt.Sprintf("clickhouse/%s.json", collectorName), bytes.NewBuffer(b))
|
||||
return output, nil
|
||||
}
|
||||
@@ -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))
|
||||
@@ -289,7 +294,18 @@ func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (Coll
|
||||
// crs
|
||||
customResources, crErrors := crs(ctx, dynamicClient, client, c.ClientConfig, namespaceNames)
|
||||
for k, v := range customResources {
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_CUSTOM_RESOURCES, k), bytes.NewBuffer(v))
|
||||
jsonPath := path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_CUSTOM_RESOURCES, k)
|
||||
output.SaveResult(c.BundlePath, jsonPath, bytes.NewBuffer(v))
|
||||
|
||||
// Keep a YAML symlink for backward compatibility. It points at the JSON
|
||||
// file, so any analyzer that expects YAML can still read it, and the
|
||||
// content is redacted through the JSON copy.
|
||||
if strings.HasSuffix(k, ".json") {
|
||||
yamlPath := path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_CUSTOM_RESOURCES, strings.TrimSuffix(k, ".json")+".yaml")
|
||||
if err := output.SymLinkResult(c.BundlePath, yamlPath, jsonPath); err != nil {
|
||||
klog.V(2).Infof("failed to create YAML symlink for %s: %v", jsonPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_CUSTOM_RESOURCES, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_CUSTOM_RESOURCES)), marshalErrors(crErrors))
|
||||
|
||||
@@ -370,9 +386,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 +409,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 +422,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 +1143,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 +2194,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,23 +2255,68 @@ func configMaps(ctx context.Context, client kubernetes.Interface, namespaces []s
|
||||
return configmapByNamespace, errorsByNamespace
|
||||
}
|
||||
|
||||
// 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
|
||||
// the YAML format.
|
||||
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 only.
|
||||
// JSON is valid YAML, so any analyzer expecting YAML can consume the JSON
|
||||
// file directly. We no longer write a separate YAML file because the
|
||||
// built-in redactors are authored for JSON, and the duplicate YAML copy
|
||||
// would otherwise be left unredacted.
|
||||
func storeCustomResource(name string, objects any, m map[string][]byte) error {
|
||||
j, err := json.MarshalIndent(objects, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
y, err := yaml.Marshal(objects)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m[fmt.Sprintf("%s.json", name)] = j
|
||||
m[fmt.Sprintf("%s.yaml", name)] = y
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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,12 +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"
|
||||
"sigs.k8s.io/yaml"
|
||||
k8stesting "k8s.io/client-go/testing"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -507,17 +511,8 @@ func TestCollectClusterResources_CustomResource(t *testing.T) {
|
||||
// Fetch the CR from cluster
|
||||
res, errs := crsV1(ctx, dynamicClient, apixClient.ApiextensionsV1(), []string{"default"})
|
||||
assert.Empty(t, errs)
|
||||
require.Equal(t, 2, len(res))
|
||||
require.Equal(t, 1, len(res))
|
||||
assert.Equal(t, fromJSON(t, res["supportbundles.troubleshoot.sh/default.json"]), sbObject)
|
||||
assert.Equal(t, fromYAML(t, res["supportbundles.troubleshoot.sh/default.yaml"]), sbObject)
|
||||
}
|
||||
|
||||
func fromYAML(t *testing.T, dat []byte) troubleshootv1beta2.SupportBundle {
|
||||
sb := []troubleshootv1beta2.SupportBundle{}
|
||||
err := yaml.Unmarshal(dat, &sb)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(sb))
|
||||
return sb[0]
|
||||
}
|
||||
|
||||
func fromJSON(t *testing.T, dat []byte) troubleshootv1beta2.SupportBundle {
|
||||
@@ -697,3 +692,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 {
|
||||
@@ -104,6 +107,8 @@ func GetCollector(collector *troubleshootv1beta2.Collect, bundlePath string, nam
|
||||
return &CollectMysql{collector.Mysql, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true
|
||||
case collector.Redis != nil:
|
||||
return &CollectRedis{collector.Redis, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true
|
||||
case collector.ClickHouse != nil:
|
||||
return &CollectClickhouse{collector.ClickHouse, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true
|
||||
case collector.Collectd != nil:
|
||||
return &CollectCollectd{collector.Collectd, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true
|
||||
case collector.Ceph != nil:
|
||||
@@ -128,6 +133,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
|
||||
}
|
||||
@@ -194,6 +203,9 @@ func getCollectorName(c interface{}) string {
|
||||
case *CollectRedis:
|
||||
collector = "redis"
|
||||
name = v.Collector.CollectorName
|
||||
case *CollectClickhouse:
|
||||
collector = "clickhouse"
|
||||
name = v.Collector.CollectorName
|
||||
case *CollectCollectd:
|
||||
collector = "collectd"
|
||||
name = v.Collector.CollectorName
|
||||
@@ -223,10 +235,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 +304,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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+8
-4
@@ -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"
|
||||
@@ -97,19 +98,22 @@ func copyFilesFromPod(ctx context.Context, dstPath string, clientConfig *restcli
|
||||
return nil, nil, errors.Wrap(err, "failed to add runtime scheme")
|
||||
}
|
||||
|
||||
// Stdin must be false because StreamOptions.Stdin is nil below.
|
||||
// A mismatch causes the SPDY fallback (after WebSocket fails on RBAC)
|
||||
// to hang: the API server opens a stdin stream but never receives EOF.
|
||||
parameterCodec := runtime.NewParameterCodec(scheme)
|
||||
req.VersionedParams(&corev1.PodExecOptions{
|
||||
Command: command,
|
||||
Container: containerName,
|
||||
Stdin: true,
|
||||
Stdout: false,
|
||||
Stdin: 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()
|
||||
|
||||
@@ -299,19 +299,22 @@ func copyFilesFromHost(ctx context.Context, dstPath string, clientConfig *restcl
|
||||
return nil, nil, errors.Wrap(err, "failed to add runtime scheme")
|
||||
}
|
||||
|
||||
// Stdin must be false because StreamOptions.Stdin is nil below.
|
||||
// A mismatch causes the SPDY fallback (after WebSocket fails on RBAC)
|
||||
// to hang: the API server opens a stdin stream but never receives EOF.
|
||||
parameterCodec := runtime.NewParameterCodec(scheme)
|
||||
req.VersionedParams(&corev1.PodExecOptions{
|
||||
Command: command,
|
||||
Container: containerName,
|
||||
Stdin: true,
|
||||
Stdout: false,
|
||||
Stdin: 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
|
||||
}
|
||||
|
||||
+20
-6
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,16 +129,19 @@ func getExecOutputs(
|
||||
}
|
||||
|
||||
parameterCodec := runtime.NewParameterCodec(scheme)
|
||||
// Stdin must be false because StreamOptions.Stdin is nil below.
|
||||
// A mismatch causes the SPDY fallback (after WebSocket fails on RBAC)
|
||||
// to hang: the API server opens a stdin stream but never receives EOF.
|
||||
req.VersionedParams(&corev1.PodExecOptions{
|
||||
Command: append(execCollector.Command, execCollector.Args...),
|
||||
Container: container,
|
||||
Stdin: true,
|
||||
Stdout: false,
|
||||
Stdin: 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
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@ func loadKConfigs(kernelRelease string) (KConfigs, error) {
|
||||
// https://github.com/torvalds/linux/blob/v4.3/init/Kconfig#L794
|
||||
// https://github.com/torvalds/linux/blob/v4.3/init/Kconfig#L9
|
||||
possiblePaths := []string{
|
||||
"/proc/config.gz",
|
||||
"/boot/config-" + kernelRelease,
|
||||
"/usr/src/linux-" + kernelRelease + "/.config",
|
||||
"/usr/src/linux/.config",
|
||||
@@ -93,6 +92,13 @@ func loadKConfigs(kernelRelease string) (KConfigs, error) {
|
||||
"/lib/modules/" + kernelRelease + "/build/.config",
|
||||
}
|
||||
|
||||
// /proc/config.gz reflects the currently running kernel. Only use it when
|
||||
// the requested kernel release matches the running kernel.
|
||||
currentRelease, err := getKernelRelease()
|
||||
if err == nil && currentRelease == kernelRelease {
|
||||
possiblePaths = append([]string{"/proc/config.gz"}, possiblePaths...)
|
||||
}
|
||||
|
||||
for _, path := range possiblePaths {
|
||||
// open file for reading
|
||||
f, err := os.Open(path)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user