Compare commits

..
53 Commits
Author SHA1 Message Date
Noah Campbell 3cd2b9a87b doesnt harcode apiVersion line when looking and figures out which apiVersion to give if none is there 2025-10-13 13:20:01 -05:00
Noah Campbell 898a59fe40 improved --fix capabilities 2025-10-07 16:58:18 -07:00
Noah Campbell e71bf87cc2 added lint subcommand 2025-10-07 16:19:01 -07:00
Noah Campbell 774fd8d935 handle empty structural_compare rules 2025-10-07 09:08:02 -07:00
Noah Campbell 1d4315ced6 simplify comparison to detect code regressions only 2025-10-07 08:58:03 -07:00
Noah Campbell b6b2bf8483 handle node-specific files and multiple report arguments 2025-10-07 08:47:05 -07:00
Noah Campbell ac9a4c9c5b create test/output directory 2025-10-07 08:34:35 -07:00
Noah Campbell c532928cd2 Update regression-test.yaml 2025-10-07 08:21:52 -07:00
Noah Campbell 4b04098689 Update regression-test.yaml 2025-10-07 08:16:38 -07:00
Noah Campbell b3369c2b3e Update regression-test.yaml 2025-10-07 08:13:18 -07:00
Noah Campbell fbfd6c99fe Add regression test suite to github actions 2025-10-07 08:10:32 -07:00
Noah Campbell 12f8bc3daa Improved error handling for support bundles and redactors for windows (#1878)
* improved error handling and window locking

* Delete all-windows-collectors.yaml

* addressing bugbot concerns

* Update host_tcpportstatus.go

* Update redact.go
2025-10-06 14:32:29 -07:00
Noah Campbell b4e3ed278a Removes silent failing (#1877)
* preserves stdout and stderr from collectors

* Delete eliminate-silent-failures.md

* Update host_kernel_modules_test.go

* added error logs when a collector fails to start

* Update host_filesystem_performance_linux.go

* fixed error saving logic inconsistency

* Update collect.go
2025-10-04 15:14:10 -07:00
Benjamin Yang c67775c1a0 feat: clean tokenization system implementation (#1874)
Core tokenization functionality with minimal file changes:

 Core Features:
- Intelligent tokenization engine (tokenizer.go)
- Context-aware secret classification (PASSWORD, APIKEY, DATABASE, etc.)
- Cross-file correlation with deterministic HMAC-SHA256 tokens
- Optional encrypted mapping for token→original value resolution

 Integration:
- CLI flags: --tokenize, --redaction-map, --encrypt-redaction-map
- Updated all redactor types: literal, single-line, multi-line, YAML
- Support bundle integration with auto-upload compatibility
- Backward compatibility: preserves ***HIDDEN*** when disabled

 Production Ready:
- Only 11 essential files (vs 31 in original PR)
- No excessive test files or documentation
- Clean build, all functionality verified
- Maintains existing redaction behavior by default

Token format: ***TOKEN_<TYPE>_<HASH>*** (e.g., ***TOKEN_PASSWORD_A1B2C3***)
2025-09-30 15:04:23 -05:00
83e6cffe5f Cron job clean (#1862)
* created roadmap and yaml claude agent

* Update roadmap.md

* chore(deps): bump sigstore/cosign-installer from 3.9.2 to 3.10.0 (#1857)

Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.9.2 to 3.10.0.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/v3.9.2...v3.10.0)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-version: 3.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump the security group with 2 updates (#1858)

Bumps the security group with 2 updates: [github.com/vmware-tanzu/velero](https://github.com/vmware-tanzu/velero) and [helm.sh/helm/v3](https://github.com/helm/helm).


Updates `github.com/vmware-tanzu/velero` from 1.16.2 to 1.17.0
- [Release notes](https://github.com/vmware-tanzu/velero/releases)
- [Changelog](https://github.com/vmware-tanzu/velero/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vmware-tanzu/velero/compare/v1.16.2...v1.17.0)

Updates `helm.sh/helm/v3` from 3.18.6 to 3.19.0
- [Release notes](https://github.com/helm/helm/releases)
- [Commits](https://github.com/helm/helm/compare/v3.18.6...v3.19.0)

---
updated-dependencies:
- dependency-name: github.com/vmware-tanzu/velero
  dependency-version: 1.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: helm.sh/helm/v3
  dependency-version: 3.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump helm.sh/helm/v3 from 3.18.6 to 3.19.0 in /examples/sdk/helm-template in the security group (#1859)

chore(deps): bump helm.sh/helm/v3

Bumps the security group in /examples/sdk/helm-template with 1 update: [helm.sh/helm/v3](https://github.com/helm/helm).


Updates `helm.sh/helm/v3` from 3.18.6 to 3.19.0
- [Release notes](https://github.com/helm/helm/releases)
- [Commits](https://github.com/helm/helm/compare/v3.18.6...v3.19.0)

---
updated-dependencies:
- dependency-name: helm.sh/helm/v3
  dependency-version: 3.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Add cron job support bundle scheduler

Complete implementation with K8s integration:
- pkg/schedule/job.go: Job management and persistence
- pkg/schedule/daemon.go: Real-time scheduler daemon
- pkg/schedule/cli.go: CLI commands (create, list, delete, daemon)
- pkg/schedule/schedule_test.go: Comprehensive unit tests
- cmd/troubleshoot/cli/root.go: CLI integration

* fixing bugbot

* Fix all bugbot errors: auto-update stability, job cooldown timing, and daemon execution

* Deleting Agent

* removed unused flags

* fixing auto-upload

* fixing markdown files

* namespace not required flag for auto collectors to work

* loosened cron job validation

* writes logs to logfile

* fix: resolve autoFromEnv variable scoping issue for CI

- Ensure autoFromEnv variable and its usage are in correct scope
- Fix build errors: declared and not used / undefined variable
- All functionality preserved and tested locally
- Force add to override gitignore

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Noah Campbell <noah.edward.campbell@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-30 14:11:15 -05:00
Benjamin Yang 84dc8157b9 Auto-Collect (#1867)
* Fix auto-collector missing files issue

- Add KOTS-aware detection for diagnostic files
- Replace silent RBAC filtering with user warnings
- Enhance error file collection for troubleshooting
- Achieve parity with traditional support bundles

Resolves issue where auto-collector was missing:
- KOTS diagnostic files (now 4 vs 3)
- ConfigMaps (now 6 vs 6)
- Maintains superior log collection (24 vs 0)

Final result: [SUCCESS] comprehensive collection achieved

* fixing bugbog

* fix: resolve production readiness issues in auto-collect branch

1. Fix diff test expectations (lines should have newlines for difflib consistency)
2. Fix preflight tests to use existing v1beta3 example file
3. Fix autodiscovery test context parameter (function signature update)

Resolves TestReadLinesFromReader and preflight v1beta3 test failures

* fix: resolve autodiscovery tests and cursor bot image matching issues

1. Fix cursor bot image matching bug in isKotsadmImage:
   - Replace flawed prefix matching with proper image component detection
   - Handle private registries correctly (registry.company.com/kotsadm/kotsadm:v1.0.0)
   - Prevent false positives with proper delimiter checking
   - Add helper functions: containsImageComponent, splitImagePath, removeTagAndDigest

2. Fix autodiscovery test failures:
   - Add TestMode flag to DiscoveryOptions to control KOTS diagnostic collection
   - Tests use TestMode=true to get only foundational collectors (no KOTS diagnostics)
   - Preserves production behavior while enabling clean testing

Resolves failing TestDiscoverer_DiscoverFoundational tests and cursor bot issues
2025-09-30 12:26:10 -05:00
Benjamin YangandNoah Campbell a8fb5210f4 Advanced analysis clean (#1868)
* created roadmap and yaml claude agent

* Update roadmap.md

* feat: Clean advanced analysis implementation - core agents, engine, artifacts

* Remove unrelated files - keep only advanced analysis implementation

* fix: Fix goroutine leak in hosted agent rate limiter

- Added stop channel and stopped flag to RateLimiter struct
- Modified replenishTokens to listen for stop signal and exit cleanly
- Added Stop() method to gracefully shutdown rate limiter
- Added Stop() method to HostedAgent to cleanup rate limiter on shutdown

Fixes cursor bot issue: Rate Limiter Goroutine Leak

* fix: Fix analyzer config and model validation bugs

Bug 1: Analyzer Config Missing File Path
- Added filePath to DeploymentStatus analyzer config in convertAnalyzerToSpec
- Sets namespace-specific path (cluster-resources/deployments/{namespace}.json)
- Falls back to generic path (cluster-resources/deployments.json) if no namespace
- Fixes LocalAgent.analyzeDeploymentStatus backward compatibility

Bug 2: HealthCheck Fails Model Validation
- Changed Ollama model validation from prefix match to exact match
- Prevents false positives where llama2:13b would match request for llama2:7b
- Ensures agent only reports healthy when exact model is available

Both fixes address cursor bot reported issues and maintain backward compatibility.

* fixing lint errors

* fixing lint errors

* adding CLI flags

* fix: resolve linting errors for CI

- Remove unnecessary nil check in host_kernel_configs.go (len() for nil slices is zero)
- Remove unnecessary fmt.Sprintf() calls in ceph.go for static strings
- Apply go fmt formatting fixes

Fixes failing lint CI check

* fix: resolve CI failures in build-test workflow and Ollama tests

1. Fix GitHub Actions workflow logic error:
   - Replace problematic contains() expression with explicit job result checks
   - Properly handle failure and cancelled states for each job
   - Prevents false positive failures in success summary job

2. Fix Ollama agent parseLLMResponse panics:
   - Add proper error handling for malformed JSON in LLM responses
   - Return error when JSON is found but invalid (instead of silent fallback)
   - Add error when no meaningful content can be parsed from response
   - Prevents nil pointer dereference in test assertions

Fixes failing build-test/success and build-test/test CI checks

* fix: resolve all CI failures and cursor bot issues

1. Fix disable-ollama flag logic bug:
   - Remove disable-ollama from advanced analysis trigger condition
   - Prevents unintended advanced analysis mode when no agents registered
   - Allows proper fallback to legacy analysis

2. Fix diff test consistency:
   - Update test expectations to match function behavior (lines with newlines)
   - Ensures consistency between streaming and non-streaming diff paths

3. Fix Ollama agent error handling:
   - Add proper error return for malformed JSON in LLM responses
   - Add meaningful content validation for markdown parsing
   - Prevents nil pointer panics in test assertions

4. Fix analysis engine mock agent:
   - Mock agent now processes and returns results for all provided analyzers
   - Fixes test expectation mismatch (expected 8 results, got 1)

Resolves all failing CI checks: lint, test, and success workflow logic

---------

Co-authored-by: Noah Campbell <noah.edward.campbell@gmail.com>
2025-09-30 11:44:46 -05:00
Marc Campbell d3af4edd31 bump example 2025-09-29 21:34:01 -07:00
Marc Campbell b9dc00cc6c removing risc 2025-09-29 21:19:11 -07:00
Marc Campbell 8990c4b06e some more goreleaser/homebrew stuffs 2025-09-29 21:11:03 -07:00
Marc Campbell e03fa0a046 publish to directory: 2025-09-29 21:00:21 -07:00
Marc Campbell 758d454434 prerelease auto and more 2025-09-29 20:48:28 -07:00
Marc Campbell f0d0eda060 larger 2025-09-29 19:57:57 -07:00
Marc Campbell 4eea31d916 goreleaser 2025-09-29 19:55:14 -07:00
Marc Campbell cfea35e7c0 fix syntax 2025-09-29 19:47:50 -07:00
Marc Campbell 630aa631ee fix syntax 2025-09-29 19:44:50 -07:00
Marc Campbell b61728a11f dropping goreleaser 2025-09-29 19:20:17 -07:00
Marc Campbell 252f9650c9 larger runner for release 2025-09-29 19:02:49 -07:00
Marc Campbell 857a028585 add debug logging 2025-09-29 18:54:17 -07:00
Marc Campbell 6e8704d6f1 remove this check 2025-09-29 18:37:28 -07:00
Marc Campbell c8c246a809 remove collect binary and risc binary 2025-09-29 18:36:36 -07:00
Marc Campbell a41958c173 bump goreleaser to v2 2025-09-29 18:13:11 -07:00
Benjamin YangandNoah Campbell 17518349ff Fix/exec textanalyze path clean (#1865)
* created roadmap and yaml claude agent

* Update roadmap.md

* Fix textAnalyze analyzer to auto-match exec collector nested paths

- Auto-detect exec output files (*-stdout.txt, *-stderr.txt, *-errors.json)
- Convert simple filenames to wildcard patterns automatically
- Preserve existing wildcard patterns
- Fixes 'No matching file' errors for exec + textAnalyze workflows

---------

Co-authored-by: Noah Campbell <noah.edward.campbell@gmail.com>
2025-09-29 17:49:54 -07:00
Benjamin Yang 266647ef2f Fixing testing errors (#1871)
fix: resolve failing unit tests and diff consistency in v1beta3

- Fix readLinesFromReader to return lines WITH newlines (like difflib.SplitLines)
- Update test expectations to match correct function behavior with newlines
- This ensures consistency between streaming and non-streaming diff paths
- Fix timeout test by changing from 10ms to 500ms to eliminate flaky failures

Fixes TestReadLinesFromReader and Test_loadSupportBundleSpecsFromURIs_TimeoutError
Resolves diff output inconsistency between code paths
2025-09-29 17:48:51 -07:00
Marc Campbell 3062dd8d5c adding brew (#1872) 2025-09-29 15:01:22 -07:00
Marc Campbell 91db72b484 removing coverage reporting 2025-09-29 14:36:51 -07:00
Marc Campbell 5f0835f7d0 add a v1beta3 fixture 2025-09-29 11:02:51 -07:00
Marc Campbell 9af9845fb9 removing unused examples 2025-09-29 10:50:00 -07:00
Marc Campbell 007ef32e77 fmt: 2025-09-29 10:43:51 -07:00
Marc Campbell d9904d9632 update ci for v1beta3 (#1870) 2025-09-29 10:40:04 -07:00
Marc Campbell 074fee81f7 V1beta3 cleanup (#1869)
* moving some files around

* more cleanup

* removing more unused
2025-09-29 10:10:41 -07:00
Noah Campbell 7bd7eca528 does not auto update for package managers (#1850) 2025-09-18 12:56:12 -05:00
Noah Campbell b1830ff515 moved v1beta3 examples to proper directory 2025-09-17 17:38:26 -05:00
Noah Campbell 885819ea9a Auto support bundle upload (#1860)
* basic auto uploading support bundles

* added upload command

* added default vendor endpoint

* added auth system from replicated cli

* fixed case sensitivity issue in YAML parsing

* support bundle uploads for end customers

* app slug flag and detection without licenseID
2025-09-17 12:59:41 -05:00
Noah Campbell 20b0c5ed06 fixed gitignore issue 2025-09-17 11:02:53 -05:00
Noah Campbell 71067edb41 Updated yaml spec (#1863)
* v1beta3 spec can be read by preflight

* added test files for ease of testing

* v1beta3 renderer fixes
2025-09-17 10:38:11 -05:00
Noah Campbell d0584a4d4d Updated yaml spec (#1851)
* v1beta3 spec can be read by preflight

* added test files for ease of testing

* updated v1beta3 guide doc and added tests

* fixed not removing tmp files from v1beta3 processing

* created v1beta2 to v1beta3 converter
2025-09-16 15:43:46 -05:00
Noah Campbell acc1aad843 Preflight docs and template subcommands (#1847)
* Added docs and template subcommands with test files

* uses helm templating preflight yaml files

* merge doc requirements for multiple inputs

* Helm aware rendering and markdown output

* v1beta3 yaml structure better mirrors beta2

* Update sample-preflight-templated.yaml

* Added docs and template subcommands with test files

* uses helm templating preflight yaml files

* merge doc requirements for multiple inputs

* Helm aware rendering and markdown output

* v1beta3 yaml structure better mirrors beta2

* Update sample-preflight-templated.yaml

* Added/updated documentation on subcommands

* Update docs.go

* commit to trigger actions
2025-09-16 14:12:09 -05:00
Noah Campbell 8027e273e4 Support bundle diff (#1855)
implemented support bundle diff command
2025-09-15 13:20:35 -05:00
Marc Campbell 3b9d280a95 resetting make targets and github workflows to support v1beta3 releas… (#1853)
* resetting make targets and github workflows to support v1beta3 release later

* removing generate

* remove

* removing

* removing
2025-09-15 09:15:53 -07:00
Benjamin YangandBenjamin Yang 74811c669b Auto-collectors: foundational discovery, image metadata, CLI integrat… (#1845)
* Auto-collectors: foundational discovery, image metadata, CLI integration; reset PRD markers

* Address PR review feedback

- Implement missing namespace exclude patterns functionality
- Fix image facts collector to use empty Data field instead of static string
- Correct APIVersion to use troubleshoot.sh/v1beta2 consistently

* Fix bug bot issues: API parsing, EOF error, and API group corrections

- Fix RBAC API parsing errors in rbac_checker.go (getAPIGroup/getAPIVersion functions)
- Fix FakeReader EOF error to use standard io.EOF instead of custom error
- Fix incorrect API group from troubleshoot.sh to troubleshoot.replicated.com in run.go

These changes address the issues identified by the bug bot and ensure proper
interface compliance and consistent API group usage.

* Fix multiple bug bot issues

- Fix RBAC API parsing errors in rbac_checker.go (getAPIGroup/getAPIVersion functions)
- Fix FakeReader EOF error to use standard io.EOF instead of custom error
- Fix incorrect API group from troubleshoot.sh to troubleshoot.replicated.com in run.go
- Fix image facts collector Data field to contain structured JSON instead of static strings

These changes address all issues identified by the bug bot and ensure proper
interface compliance, consistent API usage, and meaningful data fields.

* Update auto_discovery.go

* Fix TODO comments in Auto-collector section

Fixed 3 of 4 TODOs as requested in PR review:

1. pkg/collect/images/registry_client.go (line 46):
   - Implement custom CA certificate loading
   - Add x509 import and certificate parsing logic
   - Enables image collection from private registries with custom CAs

2. cmd/troubleshoot/cli/diff.go (line 209):
   - Implement bundle file count functionality
   - Add tar/gzip imports and getFileCountFromBundle() function
   - Properly counts files in support bundle archives (.gz/.tgz)

3. cmd/troubleshoot/cli/run.go (line 338):
   - Replace TODO with clarifying comment about RemoteCollectors usage
   - Confirmed RemoteCollectors are still actively used in preflights

The 4th TODO (diff.go line 196) is left as-is since it's explicitly marked
as Phase 4 future work (Support Bundle Differencing implementation).

Addresses PR review feedback about unimplemented TODO comments.

---------

Co-authored-by: Benjamin Yang <benjaminyang@Benjamins-MacBook-Pro.local>
2025-09-12 13:44:26 -06:00
Noah Campbell a1d1a12c1a Auto updater (#1849)
* added auto updater

* updated docs

* commit to trigger actions
2025-09-12 12:30:57 -06:00
Noah Campbell 050dc8499f Change workflow branch from 'main' to 'v1beta3' 2025-09-12 11:08:16 -05:00
103 changed files with 1950 additions and 9746 deletions
+39
View File
@@ -0,0 +1,39 @@
name: 'Setup Go Environment'
description: 'Setup Go with caching and common environment variables'
inputs:
go-version-file:
description: 'Path to go.mod file'
required: false
default: 'go.mod'
outputs:
go-version:
description: 'The Go version that was installed'
value: ${{ steps.setup-go.outputs.go-version }}
cache-hit:
description: 'Whether the Go cache was hit'
value: ${{ steps.setup-go.outputs.cache-hit }}
runs:
using: 'composite'
steps:
- name: Setup Go
id: setup-go
uses: actions/setup-go@v5
with:
go-version-file: ${{ inputs.go-version-file }}
cache: true
- name: Set Go environment variables
shell: bash
run: |
echo "GOMAXPROCS=2" >> $GITHUB_ENV
echo "GOCACHE=$(go env GOCACHE)" >> $GITHUB_ENV
echo "GOMODCACHE=$(go env GOMODCACHE)" >> $GITHUB_ENV
- name: Print Go environment
shell: bash
run: |
echo "Go version: $(go version)"
echo "GOOS: $(go env GOOS)"
echo "GOARCH: $(go env GOARCH)"
echo "Cache directory: $(go env GOCACHE)"
echo "Module cache: $(go env GOMODCACHE)"
-163
View File
@@ -1,163 +0,0 @@
name: build-test-deploy
on:
pull_request:
types:
- opened
- reopened
- synchronize
- ready_for_review
branches:
- main
push:
branches:
- "main"
tags:
- "v*.*.*"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
fail_if_pull_request_is_draft:
if: github.event.pull_request.draft == true
runs-on: ubuntu-latest
steps:
- name: Fails in order to indicate that pull request needs to be marked as ready to review and unit tests workflow needs to pass.
run: exit 1
tidy-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
- run: make tidy-diff
test-integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
- uses: replicatedhq/action-k3s@main
id: k3s
with:
version: v1.31.2-k3s1
# test-integration includes unit tests
- run: make test-integration
compile-preflight:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
- run: make generate preflight
- uses: actions/upload-artifact@v6
with:
name: preflight
path: bin/preflight
validate-preflight-e2e:
runs-on: ubuntu-latest
needs: compile-preflight
steps:
- uses: actions/checkout@v6
- uses: replicatedhq/action-k3s@main
id: k3s
with:
version: v1.31.2-k3s1
- name: Download preflight binary
uses: actions/download-artifact@v7
with:
name: preflight
path: bin/
- run: chmod +x bin/preflight
- run: make preflight-e2e-test
compile-supportbundle:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
- run: make generate support-bundle
- uses: actions/upload-artifact@v6
with:
name: support-bundle
path: bin/support-bundle
validate-supportbundle-e2e:
runs-on: ubuntu-latest
needs: compile-supportbundle
steps:
- uses: actions/checkout@v6
- uses: replicatedhq/action-k3s@main
id: k3s
with:
version: v1.31.2-k3s1
- name: Download support bundle binary
uses: actions/download-artifact@v7
with:
name: support-bundle
path: bin/
- run: chmod +x bin/support-bundle
- run: make support-bundle-e2e-test
# Additional e2e tests for support bundle that run in Go, these create a Kind cluster
validate-supportbundle-e2e-go:
runs-on: ubuntu-latest
needs: compile-supportbundle
steps:
- uses: actions/checkout@v6
- name: Download support bundle binary
uses: actions/download-artifact@v7
with:
name: support-bundle
path: bin/
- run: chmod +x bin/support-bundle
- name: Download preflight binary
uses: actions/download-artifact@v7
with:
name: preflight
path: bin/
- run: chmod +x bin/preflight
- run: make support-bundle-e2e-go-test
# summary jobs, these jobs will only run if all the other jobs have succeeded
validate-pr-tests:
runs-on: ubuntu-latest
needs:
- tidy-check
- test-integration
- validate-preflight-e2e
- validate-supportbundle-e2e
- validate-supportbundle-e2e-go
steps:
- run: echo "All PR tests passed"
# this job will validate that the validation did not fail and that all pr-tests succeed
# it is used for the github branch protection rule
validate-success:
runs-on: ubuntu-latest
needs:
- validate-pr-tests
if: always()
steps:
# https://docs.github.com/en/actions/learn-github-actions/contexts#needs-context
# if the validate-pr-tests job was not successful, this job will fail
- name: fail if validate-pr-tests job was not successful
if: needs.validate-pr-tests.result != 'success'
run: exit 1
# if the validate-pr-tests job was successful, this job will succeed
- name: succeed if validate-pr-tests job succeeded
if: needs.validate-pr-tests.result == 'success'
run: echo "Validation succeeded"
+12 -18
View File
@@ -3,9 +3,9 @@ name: build-test
on:
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
branches: [main]
branches: [v1beta3]
push:
branches: [main]
branches: [v1beta3]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -21,7 +21,7 @@ jobs:
support-bundle: ${{ steps.filter.outputs.support-bundle }}
examples: ${{ steps.filter.outputs.examples }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- uses: dorny/paths-filter@v3
id: filter
with:
@@ -44,10 +44,8 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
- uses: actions/checkout@v5
- uses: ./.github/actions/setup-go
- name: Check go mod tidy
run: |
@@ -73,10 +71,8 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
- uses: actions/checkout@v5
- uses: ./.github/actions/setup-go
- name: Setup K3s
uses: replicatedhq/action-k3s@main
@@ -93,12 +89,10 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
- uses: actions/checkout@v5
- uses: ./.github/actions/setup-go
- run: make build
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v4
with:
name: binaries
path: bin/
@@ -124,7 +118,7 @@ jobs:
target: support-bundle-e2e-go-test
needs-k3s: false
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- name: Setup K3s
if: matrix.needs-k3s
@@ -132,7 +126,7 @@ jobs:
with:
version: v1.31.2-k3s1
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v4
with:
name: binaries
path: bin/
+133 -338
View File
@@ -13,22 +13,44 @@ on:
default: false
jobs:
# Build binaries once (shared by all test jobs)
build-binaries:
if: github.actor != 'dependabot[bot]'
regression-test:
runs-on: ubuntu-22.04
timeout-minutes: 5
timeout-minutes: 25
steps:
# 1. SETUP
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-depth: 0 # Fetch all history for git describe to work
- name: Create output directory
run: mkdir -p test/output
- name: Create k3s cluster
id: create-cluster
uses: replicatedhq/compatibility-actions/create-cluster@v1
with:
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
kubernetes-distribution: k3s
cluster-name: regression-${{ github.run_id }}-${{ github.run_attempt }}
ttl: 25m
timeout-minutes: 5
- name: Configure kubeconfig
run: |
echo "${{ steps.create-cluster.outputs.cluster-kubeconfig }}" > $GITHUB_WORKSPACE/kubeconfig.yaml
echo "KUBECONFIG=$GITHUB_WORKSPACE/kubeconfig.yaml" >> $GITHUB_ENV
- name: Verify cluster access
run: kubectl get nodes -o wide
- name: Setup Go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
go-version-file: go.mod
cache: true
cache-dependency-path: go.sum
- name: Build binaries
run: |
@@ -37,83 +59,84 @@ jobs:
./bin/preflight version
./bin/support-bundle version
- name: Upload binaries
uses: actions/upload-artifact@v6
with:
name: binaries-${{ github.run_id }}
path: |
bin/preflight
bin/support-bundle
retention-days: 1
# Preflight v1beta3 test (parallel job 1)
test-preflight-v1beta3:
needs: [build-binaries]
if: github.actor != 'dependabot[bot]'
runs-on: ubuntu-22.04
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Create output directory
run: mkdir -p test/output
- name: Create k3s cluster
uses: replicatedhq/action-k3s@main
with:
version: v1.31.2-k3s1
- name: Verify cluster access
run: kubectl get nodes -o wide
- name: Wait for all pods to be ready
run: |
echo "Waiting for all pods to be running..."
kubectl get pods --all-namespaces
kubectl wait --for=condition=Ready pods --all --all-namespaces --timeout=300s || true
kubectl get pods --all-namespaces
- name: Download binaries
uses: actions/download-artifact@v7
with:
name: binaries-${{ github.run_id }}
path: bin/
- name: Make binaries executable
run: |
chmod +x bin/preflight bin/support-bundle
./bin/preflight version
- name: Setup Python for comparison
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Python dependencies
run: pip install pyyaml deepdiff
run: |
pip install pyyaml deepdiff
- name: Run preflight v1beta3
# 2. EXECUTE SPECS (in parallel)
- name: Run all specs in parallel
continue-on-error: true
run: |
echo "Running preflight v1beta3..."
./bin/preflight \
examples/preflight/complex-v1beta3.yaml \
--values examples/preflight/values-complex-full.yaml \
--interactive=false \
--format=json \
--auto-update=false \
--output=test/output/preflight-results-v1beta3.json 2>&1 | tee test/output/v1beta3.log || true
echo "Running all 3 specs in parallel..."
BUNDLE=$(ls -t preflightbundle-*.tar.gz 2>/dev/null | head -1)
if [ -n "$BUNDLE" ]; then
mv "$BUNDLE" test/output/preflight-v1beta3-bundle.tar.gz
echo "✓ v1beta3 bundle saved"
fi
# Run v1beta3 in background
(
echo "Starting preflight v1beta3..."
./bin/preflight \
examples/preflight/complex-v1beta3.yaml \
--values examples/preflight/values-complex-full.yaml \
--interactive=false \
--format=json \
--output=test/output/preflight-results-v1beta3.json 2>&1 | tee test/output/v1beta3.log || true
BUNDLE=$(ls -t preflightbundle-*.tar.gz 2>/dev/null | head -1)
if [ -n "$BUNDLE" ]; then
mv "$BUNDLE" test/output/preflight-v1beta3-bundle.tar.gz
echo "✓ v1beta3 bundle saved"
fi
) &
PID_V1BETA3=$!
# Run v1beta2 in background
(
echo "Starting preflight v1beta2..."
./bin/preflight \
examples/preflight/all-analyzers-v1beta2.yaml \
--interactive=false \
--format=json \
--output=test/output/preflight-results-v1beta2.json 2>&1 | tee test/output/v1beta2.log || true
BUNDLE=$(ls -t preflightbundle-*.tar.gz 2>/dev/null | head -1)
if [ -n "$BUNDLE" ]; then
mv "$BUNDLE" test/output/preflight-v1beta2-bundle.tar.gz
echo "✓ v1beta2 bundle saved"
fi
) &
PID_V1BETA2=$!
# Run support bundle in background
(
echo "Starting support bundle..."
./bin/support-bundle \
examples/collect/host/all-kubernetes-collectors.yaml \
--interactive=false \
--output=test/output/supportbundle.tar.gz 2>&1 | tee test/output/supportbundle.log || true
if [ -f test/output/supportbundle.tar.gz ]; then
echo "✓ Support bundle saved"
fi
) &
PID_SUPPORTBUNDLE=$!
# Wait for all to complete
echo "Waiting for all specs to complete..."
wait $PID_V1BETA3
wait $PID_V1BETA2
wait $PID_SUPPORTBUNDLE
echo "All specs completed!"
# Verify bundles exist
ls -lh test/output/*.tar.gz || echo "Warning: Some bundles may be missing"
# 3. COMPARE BUNDLES
- name: Compare preflight v1beta3 bundle
id: compare
id: compare-v1beta3
continue-on-error: true
run: |
echo "Comparing v1beta3 preflight bundle against baseline..."
@@ -130,93 +153,8 @@ jobs:
--report test/output/diff-report-v1beta3.json \
--spec-type preflight
- name: Upload test artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v6
with:
name: test-results-v1beta3-${{ github.run_id }}-${{ github.run_attempt }}
path: |
test/output/preflight-v1beta3-bundle.tar.gz
test/output/preflight-results-v1beta3.json
test/output/diff-report-v1beta3.json
test/output/v1beta3.log
retention-days: 30
- name: Set job outcome
if: ${{ !cancelled() }}
run: |
if [ "${{ steps.compare.outcome }}" == "failure" ] && [ "${{ steps.compare.outputs.baseline_missing }}" != "true" ]; then
echo "comparison_failed=true" >> $GITHUB_OUTPUT
exit 1
fi
# Preflight v1beta2 test (parallel job 2)
test-preflight-v1beta2:
needs: [build-binaries]
if: github.actor != 'dependabot[bot]'
runs-on: ubuntu-22.04
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Create output directory
run: mkdir -p test/output
- name: Create k3s cluster
uses: replicatedhq/action-k3s@main
with:
version: v1.31.2-k3s1
- name: Verify cluster access
run: kubectl get nodes -o wide
- name: Wait for all pods to be ready
run: |
echo "Waiting for all pods to be running..."
kubectl get pods --all-namespaces
kubectl wait --for=condition=Ready pods --all --all-namespaces --timeout=300s || true
kubectl get pods --all-namespaces
- name: Download binaries
uses: actions/download-artifact@v7
with:
name: binaries-${{ github.run_id }}
path: bin/
- name: Make binaries executable
run: |
chmod +x bin/preflight bin/support-bundle
./bin/preflight version
- name: Setup Python for comparison
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Install Python dependencies
run: pip install pyyaml deepdiff
- name: Run preflight v1beta2
continue-on-error: true
run: |
echo "Running preflight v1beta2..."
./bin/preflight \
examples/preflight/all-analyzers-v1beta2.yaml \
--interactive=false \
--format=json \
--auto-update=false \
--output=test/output/preflight-results-v1beta2.json 2>&1 | tee test/output/v1beta2.log || true
BUNDLE=$(ls -t preflightbundle-*.tar.gz 2>/dev/null | head -1)
if [ -n "$BUNDLE" ]; then
mv "$BUNDLE" test/output/preflight-v1beta2-bundle.tar.gz
echo "✓ v1beta2 bundle saved"
fi
- name: Compare preflight v1beta2 bundle
id: compare
id: compare-v1beta2
continue-on-error: true
run: |
echo "Comparing v1beta2 preflight bundle against baseline..."
@@ -233,90 +171,8 @@ jobs:
--report test/output/diff-report-v1beta2.json \
--spec-type preflight
- name: Upload test artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v6
with:
name: test-results-v1beta2-${{ github.run_id }}-${{ github.run_attempt }}
path: |
test/output/preflight-v1beta2-bundle.tar.gz
test/output/preflight-results-v1beta2.json
test/output/diff-report-v1beta2.json
test/output/v1beta2.log
retention-days: 30
- name: Set job outcome
if: ${{ !cancelled() }}
run: |
if [ "${{ steps.compare.outcome }}" == "failure" ] && [ "${{ steps.compare.outputs.baseline_missing }}" != "true" ]; then
echo "comparison_failed=true" >> $GITHUB_OUTPUT
exit 1
fi
# Support bundle test (parallel job 3)
test-supportbundle:
needs: [build-binaries]
if: github.actor != 'dependabot[bot]'
runs-on: ubuntu-22.04
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Create output directory
run: mkdir -p test/output
- name: Create k3s cluster
uses: replicatedhq/action-k3s@main
with:
version: v1.31.2-k3s1
- name: Verify cluster access
run: kubectl get nodes -o wide
- name: Wait for all pods to be ready
run: |
echo "Waiting for all pods to be running..."
kubectl get pods --all-namespaces
kubectl wait --for=condition=Ready pods --all --all-namespaces --timeout=300s || true
kubectl get pods --all-namespaces
- name: Download binaries
uses: actions/download-artifact@v7
with:
name: binaries-${{ github.run_id }}
path: bin/
- name: Make binaries executable
run: |
chmod +x bin/preflight bin/support-bundle
./bin/support-bundle version
- name: Setup Python for comparison
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Install Python dependencies
run: pip install pyyaml deepdiff
- name: Run support bundle
continue-on-error: true
run: |
echo "Running support bundle..."
./bin/support-bundle \
examples/collect/host/all-kubernetes-collectors.yaml \
--interactive=false \
--auto-update=false \
--output=test/output/supportbundle.tar.gz 2>&1 | tee test/output/supportbundle.log || true
if [ -f test/output/supportbundle.tar.gz ]; then
echo "✓ Support bundle saved"
fi
- name: Compare support bundle
id: compare
id: compare-supportbundle
continue-on-error: true
run: |
echo "Comparing support bundle against baseline..."
@@ -333,116 +189,45 @@ jobs:
--report test/output/diff-report-supportbundle.json \
--spec-type supportbundle
# 4. REPORT RESULTS
- name: Generate summary report
if: always()
run: |
python3 scripts/generate_summary.py \
--reports test/output/diff-report-*.json \
--output-file $GITHUB_STEP_SUMMARY \
--output-console
- name: Upload test artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v6
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-supportbundle-${{ github.run_id }}-${{ github.run_attempt }}
name: regression-test-results-${{ github.run_id }}-${{ github.run_attempt }}
path: |
test/output/supportbundle.tar.gz
test/output/diff-report-supportbundle.json
test/output/supportbundle.log
test/output/*.tar.gz
test/output/*.json
retention-days: 30
- name: Set job outcome
if: ${{ !cancelled() }}
run: |
if [ "${{ steps.compare.outcome }}" == "failure" ] && [ "${{ steps.compare.outputs.baseline_missing }}" != "true" ]; then
echo "comparison_failed=true" >> $GITHUB_OUTPUT
exit 1
fi
# Report results (runs after all tests complete)
report-results:
needs: [test-preflight-v1beta3, test-preflight-v1beta2, test-supportbundle]
if: ${{ !cancelled() && github.actor != 'dependabot[bot]' }}
runs-on: ubuntu-22.04
timeout-minutes: 5
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Install Python dependencies
run: pip install pyyaml deepdiff
- name: Download all test artifacts
uses: actions/download-artifact@v7
with:
path: test/output
pattern: test-results-*
- name: Reorganize artifacts
run: |
# Move artifacts from nested directories to test/output
# Use shopt to handle glob patterns that don't match
shopt -s nullglob
for dir in test/output/test-results-*; do
if [ -d "$dir" ]; then
find "$dir" -type f -exec mv {} test/output/ \;
fi
done
# Clean up empty directories
for dir in test/output/test-results-*; do
if [ -d "$dir" ]; then
find "$dir" -type d -empty -delete || true
fi
done
shopt -u nullglob
- name: Generate summary report
run: |
# Handle case where no reports exist
shopt -s nullglob
REPORTS=(test/output/diff-report-*.json)
shopt -u nullglob
if [ ${#REPORTS[@]} -eq 0 ]; then
echo "⚠ No comparison reports found - test jobs may have failed before generating reports"
echo "## Summary Report" >> $GITHUB_STEP_SUMMARY
echo "No comparison reports available. Check individual test job results." >> $GITHUB_STEP_SUMMARY
else
python3 scripts/generate_summary.py \
--reports test/output/diff-report-*.json \
--output-file $GITHUB_STEP_SUMMARY \
--output-console
fi
- name: Check for regressions
if: always()
run: |
echo "Checking comparison results..."
# Check if any comparisons failed
FAILURES=0
if [ "${{ needs.test-preflight-v1beta3.result }}" == "failure" ] || [ "${{ needs.test-preflight-v1beta3.result }}" == "skipped" ]; then
if [ "${{ needs.test-preflight-v1beta3.result }}" == "skipped" ]; then
echo "❌ v1beta3 test was skipped (likely due to build failure)"
else
echo "❌ v1beta3 comparison failed"
fi
if [ "${{ steps.compare-v1beta3.outcome }}" == "failure" ] && [ "${{ steps.compare-v1beta3.outputs.baseline_missing }}" != "true" ]; then
echo "❌ v1beta3 comparison failed"
FAILURES=$((FAILURES + 1))
fi
if [ "${{ needs.test-preflight-v1beta2.result }}" == "failure" ] || [ "${{ needs.test-preflight-v1beta2.result }}" == "skipped" ]; then
if [ "${{ needs.test-preflight-v1beta2.result }}" == "skipped" ]; then
echo "❌ v1beta2 test was skipped (likely due to build failure)"
else
echo "❌ v1beta2 comparison failed"
fi
if [ "${{ steps.compare-v1beta2.outcome }}" == "failure" ] && [ "${{ steps.compare-v1beta2.outputs.baseline_missing }}" != "true" ]; then
echo "❌ v1beta2 comparison failed"
FAILURES=$((FAILURES + 1))
fi
if [ "${{ needs.test-supportbundle.result }}" == "failure" ] || [ "${{ needs.test-supportbundle.result }}" == "skipped" ]; then
if [ "${{ needs.test-supportbundle.result }}" == "skipped" ]; then
echo "❌ Support bundle test was skipped (likely due to build failure)"
else
echo "❌ Support bundle comparison failed"
fi
if [ "${{ steps.compare-supportbundle.outcome }}" == "failure" ] && [ "${{ steps.compare-supportbundle.outputs.baseline_missing }}" != "true" ]; then
echo "❌ Support bundle comparison failed"
FAILURES=$((FAILURES + 1))
fi
@@ -455,8 +240,9 @@ jobs:
echo "✅ All comparisons passed or skipped (no baseline)"
fi
# 5. UPDATE BASELINES (optional, manual trigger only)
- name: Update baselines
if: ${{ !cancelled() && github.event.inputs.update_baselines == 'true' && github.event_name == 'workflow_dispatch' }}
if: github.event.inputs.update_baselines == 'true' && github.event_name == 'workflow_dispatch'
run: |
echo "Updating baselines with current bundles..."
@@ -484,7 +270,7 @@ jobs:
{
"updated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"git_sha": "${{ github.sha }}",
"k8s_version": "v1.31.2-k3s1",
"k8s_version": "v1.28.3",
"workflow_run": "${{ github.run_id }}"
}
EOF
@@ -495,3 +281,12 @@ jobs:
git add test/baselines/
git commit -m "chore: update regression test baselines from run ${{ github.run_id }}"
git push
# 6. CLEANUP
- name: Remove cluster
if: always()
uses: replicatedhq/compatibility-actions/remove-cluster@v1
continue-on-error: true
with:
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
cluster-id: ${{ steps.create-cluster.outputs.cluster-id }}
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
runs-on: troubleshoot_release
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v5
with:
fetch-depth: 0
-24
View File
@@ -1,24 +0,0 @@
name: Upgrade Go Version
on:
# Run manually when needed
workflow_dispatch:
# Run weekly on Mondays at 8am UTC
schedule:
- cron: "0 8 * * MON"
jobs:
upgrade-go:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Check for Go updates
uses: StefMa/Upgrade-Go-Action@v1
with:
base-branch: 'main'
gh-token: ${{ secrets.TROUBLESHOOT_GH_PAT }}
+4 -4
View File
@@ -50,9 +50,9 @@ sbom/
!testdata/supportbundle/*.tar.gz
!test/baselines/**/baseline.tar.gz
# Ignore built binaries (use / prefix to avoid catching source files)
/troubleshoot
/troubleshoot-test
# Ignore built binaries
troubleshoot
troubleshoot-test
cmd/troubleshoot/troubleshoot
cmd/*/troubleshoot
/support-bundle
support-bundle
+4 -190
View File
@@ -38,17 +38,13 @@ BUILDTAGS = "netgo containers_image_ostree_stub exclude_graphdriver_devicemapper
BUILDFLAGS = -tags ${BUILDTAGS} -installsuffix netgo
BUILDPATHS = ./pkg/... ./cmd/... ./internal/...
E2EPATHS = ./test/e2e/...
TESTFLAGS ?= -v -coverprofile cover.out
TESTFLAGS ?=
.DEFAULT_GOAL := all
all: clean build test
.PHONY: ffi
ffi: fmt vet
go build ${BUILDFLAGS} ${LDFLAGS} -o bin/troubleshoot.so -buildmode=c-shared ffi/main.go
.PHONY: test
test: generate fmt vet
test: fmt vet
if [ -n $(RUN) ]; then \
go test ${BUILDFLAGS} ${BUILDPATHS} ${TESTFLAGS} -run $(RUN); \
else \
@@ -58,17 +54,13 @@ test: generate fmt vet
# Go tests that require a K8s instance
# TODOLATER: merge with test, so we get unified coverage reports? it'll add 21~sec to the test job though...
.PHONY: test-integration
test-integration: generate fmt vet
test-integration: fmt vet
go test -v --tags="integration exclude_graphdriver_devicemapper exclude_graphdriver_btrfs" ${BUILDPATHS}
.PHONY: preflight-e2e-test
preflight-e2e-test:
./test/validate-preflight-e2e.sh
.PHONY: run-examples
run-examples:
./test/run-examples.sh
.PHONY: support-bundle-e2e-test
support-bundle-e2e-test:
./test/validate-support-bundle-e2e.sh
@@ -83,31 +75,19 @@ support-bundle-e2e-go-test:
rebuild: clean build
# Build all binaries in parallel ( -j )
build: tidy
@echo "Build cli binaries"
$(MAKE) -j bin/support-bundle bin/preflight
$(MAKE) bin/support-bundle bin/preflight
.PHONY: clean
clean:
@rm -f bin/analyze
@rm -f bin/support-bundle
@rm -f bin/collect
@rm -f bin/preflight
@rm -f bin/troubleshoot.h
@rm -f bin/troubleshoot.so
@rm -f bin/schemagen
@rm -f bin/docsgen
.PHONY: tidy
tidy:
go mod tidy
# Prints the diff of the changes that would be made by `go mod tidy`. Used in CI
.PHONY: tidy-diff
tidy-diff:
go mod tidy -diff
# Only build when any of the files in SOURCES changes, or if bin/<file> is absent
MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
SOURCES := $(shell find $(MAKEFILE_DIR) -type f \( -name "*.go" -o -name "go.mod" -o -name "go.sum" \))
@@ -117,28 +97,12 @@ bin/support-bundle: $(SOURCES)
bin/preflight: $(SOURCES)
go build ${BUILDFLAGS} ${LDFLAGS} -o bin/preflight github.com/replicatedhq/troubleshoot/cmd/preflight
bin/analyze: $(SOURCES)
go build ${BUILDFLAGS} ${LDFLAGS} -o bin/analyze github.com/replicatedhq/troubleshoot/cmd/analyze
bin/collect: $(SOURCES)
go build ${BUILDFLAGS} ${LDFLAGS} -o bin/collect github.com/replicatedhq/troubleshoot/cmd/collect
.PHONY: support-bundle
support-bundle: bin/support-bundle
.PHONY: preflight
preflight: bin/preflight
.PHONY: analyze
analyze: bin/analyze
.PHONY: collect
collect: bin/collect
build-linux: tidy
@echo "Build cli binaries for Linux"
GOOS=linux GOARCH=amd64 $(MAKE) -j bin/support-bundle bin/preflight bin/analyze bin/collect
.PHONY: fmt
fmt:
go fmt ${BUILDPATHS}
@@ -146,153 +110,3 @@ fmt:
.PHONY: vet
vet:
go vet ${BUILDFLAGS} ${BUILDPATHS}
.PHONY: generate
generate: controller-gen client-gen
$(CONTROLLER_GEN) \
object:headerFile=./hack/boilerplate.go.txt paths=./pkg/apis/...
$(CLIENT_GEN) \
--output-dir=. \
--output-pkg=github.com/replicatedhq/troubleshoot/pkg/client \
--clientset-name troubleshootclientset \
--input-base github.com/replicatedhq/troubleshoot/pkg/apis \
--input troubleshoot/v1beta1 \
--input troubleshoot/v1beta2 \
--go-header-file ./hack/boilerplate.go.txt
cp -r troubleshootclientset pkg/client
rm -rf troubleshootclientset
.PHONY: openapischema
openapischema: controller-gen
controller-gen crd +output:dir=./config/crds paths=./pkg/apis/troubleshoot/v1beta1
controller-gen crd +output:dir=./config/crds paths=./pkg/apis/troubleshoot/v1beta2
check-schemas: generate schemas
@if [ -n "$$(git status --short)" ]; then \
echo -e "\033[31mThe git repo is dirty :( Ensure all generated files are committed e.g CRD schema files\033[0;m"; \
git status --short; \
exit 1; \
fi
.PHONY: schemas
schemas: openapischema bin/schemagen
./bin/schemagen --output-dir ./schemas
bin/schemagen:
go build ${LDFLAGS} -o bin/schemagen github.com/replicatedhq/troubleshoot/cmd/schemagen
.PHONY: docs
docs: fmt vet bin/docsgen
./bin/docsgen
bin/docsgen:
go build ${LDFLAGS} -o bin/docsgen github.com/replicatedhq/troubleshoot/cmd/docsgen
controller-gen:
go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.19.0
CONTROLLER_GEN=$(shell which controller-gen)
.PHONY: client-gen
client-gen:
go install k8s.io/code-generator/cmd/client-gen@v0.34.0
CLIENT_GEN=$(shell which client-gen)
.PHONY: release
release: export GITHUB_TOKEN = $(shell echo ${GITHUB_TOKEN_TROUBLESHOOT})
release:
curl -sL https://git.io/goreleaser | bash -s -- --rm-dist --config deploy/.goreleaser.yml
.PHONY: snapshot-release
snapshot-release:
curl -sL https://git.io/goreleaser | bash -s -- --rm-dist --snapshot --config deploy/.goreleaser.snapshot.yml
docker push replicated/troubleshoot:alpha
docker push replicated/preflight:alpha
.PHONY: local-release
local-release:
curl -sL https://git.io/goreleaser | bash -s -- --rm-dist --snapshot --config deploy/.goreleaser.yaml
docker tag replicated/troubleshoot:alpha localhost:32000/troubleshoot:alpha
docker tag replicated/preflight:alpha localhost:32000/preflight:alpha
docker push localhost:32000/troubleshoot:alpha
docker push localhost:32000/preflight:alpha
.PHONY: run-preflight
run-preflight: bin/preflight
./bin/preflight ./examples/preflight/sample-preflight.yaml
.PHONY: run-support-bundle
run-support-bundle: bin/support-bundle
./bin/support-bundle ./examples/support-bundle/sample-supportbundle.yaml
.PHONY: run-analyze
run-analyze: bin/analyze
./bin/analyze --analyzers ./examples/support-bundle/sample-analyzers.yaml ./support-bundle.tar.gz
.PHONY: init-sbom
init-sbom:
mkdir -p sbom/spdx sbom/assets
.PHONY: install-spdx-sbom-generator
install-spdx-sbom-generator: init-sbom
./scripts/initialize-sbom-build.sh
SPDX_GENERATOR=./sbom/spdx-sbom-generator
.PHONY: generate-sbom
generate-sbom: install-spdx-sbom-generator
$(SPDX_GENERATOR) -o ./sbom/spdx
sbom/assets/troubleshoot-sbom.tgz: generate-sbom
tar -czf sbom/assets/troubleshoot-sbom.tgz sbom/spdx/*.spdx
sbom: sbom/assets/troubleshoot-sbom.tgz
cosign sign-blob \
--key ./cosign.key \
--tlog-upload \
--yes \
--rekor-url=https://rekor.sigstore.dev \
sbom/assets/troubleshoot-sbom.tgz > sbom/assets/troubleshoot-sbom.tgz.sig
cosign public-key --key cosign.key --outfile sbom/assets/key.pub
.PHONY: scan
scan:
trivy fs \
--scanners vuln \
--exit-code=1 \
--severity="HIGH,CRITICAL" \
--ignore-unfixed \
./
.PHONY: watch
watch: npm-install
bin/watch.js
## Syncronize the code with a remote server. More info: CONTRIBUTING.md
.PHONY: watchrsync
watchrsync: npm-install
bin/watchrsync.js
.PHONY: npm-install
npm-install:
npm --version 2>&1 >/dev/null || ( echo "npm not installed; install npm to set up watchrsync" && exit 1 )
npm list gaze-run-interrupt || npm install install --no-save gaze-run-interrupt@~2.0.0
######## Lagacy make targets ###########
# Deprecated: These can be removed
.PHONY: run-troubleshoot
run-troubleshoot: run-support-bundle
longhorn:
git clone https://github.com/longhorn/longhorn-manager.git
cd longhorn-manager && git checkout v1.2.2 && cd ..
rm -rf pkg/longhorn
mv longhorn-manager/k8s/pkg pkg/longhorn
mv longhorn-manager/types pkg/longhorn/types
mv longhorn-manager/util pkg/longhorn/util
rm -rf pkg/longhorn/util/daemon
rm -rf pkg/longhorn/util/server
find pkg/longhorn -type f | xargs sed -i "s/github.com\/longhorn\/longhorn-manager\/k8s\/pkg/github.com\/replicatedhq\/troubleshoot\/pkg\/longhorn/g"
find pkg/longhorn -type f | xargs sed -i "s/github.com\/longhorn\/longhorn-manager\/types/github.com\/replicatedhq\/troubleshoot\/pkg\/longhorn\/types/g"
find pkg/longhorn -type f | xargs sed -i "s/github.com\/longhorn\/longhorn-manager\/util/github.com\/replicatedhq\/troubleshoot\/pkg\/longhorn\/util/g"
rm -rf longhorn-manager
-13
View File
@@ -1,13 +0,0 @@
package main
import (
"os"
analyzecli "github.com/replicatedhq/troubleshoot/cmd/analyze/cli"
)
func main() {
if err := analyzecli.RootCmd().Execute(); err != nil {
os.Exit(1)
}
}
+20 -1
View File
@@ -88,7 +88,7 @@ func extractDocs(templateFiles []string, valuesFiles []string, setValues []strin
if err != nil {
return errors.Wrapf(err, "failed to load values file %s", valuesFile)
}
values = preflight.MergeMaps(values, fileValues)
values = mergeMaps(values, fileValues)
}
// Normalize maps for Helm set merging
@@ -331,6 +331,25 @@ func setNestedValue(m map[string]interface{}, keys []string, value interface{})
}
}
func mergeMaps(base, overlay map[string]interface{}) map[string]interface{} {
result := make(map[string]interface{})
for k, v := range base {
result[k] = v
}
for k, v := range overlay {
if baseVal, exists := result[k]; exists {
if baseMap, ok := baseVal.(map[string]interface{}); ok {
if overlayMap, ok := v.(map[string]interface{}); ok {
result[k] = mergeMaps(baseMap, overlayMap)
continue
}
}
}
result[k] = v
}
return result
}
func renderTemplate(templateContent string, values map[string]interface{}) (string, error) {
tmpl := template.New("preflight").Funcs(sprig.FuncMap())
tmpl, err := tmpl.Parse(templateContent)
+17 -19
View File
@@ -7,7 +7,6 @@ import (
"github.com/pkg/errors"
"github.com/replicatedhq/troubleshoot/pkg/constants"
"github.com/replicatedhq/troubleshoot/pkg/lint"
"github.com/replicatedhq/troubleshoot/pkg/types"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -16,17 +15,24 @@ func LintCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "lint [spec-files...]",
Args: cobra.MinimumNArgs(1),
Short: "Lint v1beta2/v1beta3 preflight specs for syntax and structural errors",
Long: `Lint v1beta2/v1beta3 preflight specs for syntax and structural errors.
Short: "Lint preflight specs for syntax and structural errors",
Long: `Lint preflight specs for syntax and structural errors.
This command validates v1beta2/v1beta3 preflight specs and checks for:
- YAML syntax errors
This command validates troubleshoot specs and checks for:
- YAML syntax errors (missing colons, invalid structure)
- Missing required fields (apiVersion, kind, metadata, spec)
- Invalid template syntax ({{ .Values.* }})
- Invalid template syntax ({{ .Values.* }}, {{ .Release.* }}, etc.)
- Missing analyzers or collectors
- Common structural issues
- Missing docStrings (warning)
Both v1beta2 and v1beta3 apiVersions are supported. Use 'convert' if you need a full structural conversion between schema versions.
The --fix flag can automatically repair:
- Missing colons in YAML (e.g., "metadata" → "metadata:")
- Missing or malformed apiVersion line. If templating ({{ }}) or docString fields are detected, apiVersion is set to v1beta3; otherwise v1beta2.
- Template expressions missing leading dot (e.g., "{{ Values.x }}" → "{{ .Values.x }}")
Examples:
# Lint a single spec file
preflight lint my-preflight.yaml
@@ -34,16 +40,12 @@ Examples:
# Lint multiple spec files
preflight lint spec1.yaml spec2.yaml spec3.yaml
# Lint with automatic fixes
# Lint with automatic fixes (may need to run multiple times for complex issues)
preflight lint --fix my-preflight.yaml
# Lint and output as JSON for CI/CD integration
preflight lint --format json my-preflight.yaml
Notes:
- v1beta2 does not support templating; template syntax in v1beta2 files will be flagged as errors.
- v1beta3 supports templating and is linted with template-awareness.
Exit codes:
0 - No errors found
2 - Validation errors found`,
@@ -54,11 +56,9 @@ Exit codes:
v := viper.GetViper()
opts := lint.LintOptions{
FilePaths: args,
Fix: v.GetBool("fix"),
Format: v.GetString("format"),
ValuesFiles: v.GetStringSlice("values"),
SetValues: v.GetStringSlice("set"),
FilePaths: args,
Fix: v.GetBool("fix"),
Format: v.GetString("format"),
}
return runLint(opts)
@@ -67,8 +67,6 @@ Exit codes:
cmd.Flags().Bool("fix", false, "Automatically fix issues where possible")
cmd.Flags().String("format", "text", "Output format: text or json")
cmd.Flags().StringSlice("values", []string{}, "Path to YAML files with template values (required for v1beta3 specs)")
cmd.Flags().StringSlice("set", []string{}, "Set template values via command line (e.g., --set key=value)")
return cmd
}
@@ -93,7 +91,7 @@ func runLint(opts lint.LintOptions) error {
// Return appropriate exit code
if lint.HasErrors(results) {
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, nil)
os.Exit(constants.EXIT_CODE_SPEC_ISSUES)
}
return nil
-100
View File
@@ -1,100 +0,0 @@
package cli
import (
"fmt"
"os"
"github.com/pkg/errors"
"github.com/replicatedhq/troubleshoot/pkg/constants"
"github.com/replicatedhq/troubleshoot/pkg/lint"
"github.com/replicatedhq/troubleshoot/pkg/types"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func LintCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "lint [spec-files...]",
Args: cobra.MinimumNArgs(1),
Short: "Lint v1beta2/v1beta3 troubleshoot specs for syntax and structural errors",
Long: `Lint v1beta2/v1beta3 troubleshoot specs (both preflight and support-bundle) for syntax and structural errors.
This command validates v1beta2/v1beta3 troubleshoot specs and checks for:
- YAML syntax errors
- Missing required fields (apiVersion, kind, metadata, spec)
- Invalid template syntax ({{ .Values.* }})
- Missing collectors or hostCollectors
- Common structural issues
- Missing docStrings (warning)
Examples:
# Lint a single spec file
support-bundle lint my-spec.yaml
# Lint multiple spec files
support-bundle lint spec1.yaml spec2.yaml spec3.yaml
# Lint with automatic fixes
support-bundle lint --fix my-spec.yaml
# Lint and output as JSON for CI/CD integration
support-bundle lint --format json my-spec.yaml
Notes:
- v1beta2 does not support templating; template syntax in v1beta2 files will be flagged as errors.
- v1beta3 supports templating and is linted with template-awareness.
Exit codes:
0 - No errors found
2 - Validation errors found`,
PreRun: func(cmd *cobra.Command, args []string) {
viper.BindPFlags(cmd.Flags())
},
RunE: func(cmd *cobra.Command, args []string) error {
v := viper.GetViper()
opts := lint.LintOptions{
FilePaths: args,
Fix: v.GetBool("fix"),
Format: v.GetString("format"),
ValuesFiles: v.GetStringSlice("values"),
SetValues: v.GetStringSlice("set"),
}
return runLint(opts)
},
}
cmd.Flags().Bool("fix", false, "Automatically fix issues where possible")
cmd.Flags().String("format", "text", "Output format: text or json")
cmd.Flags().StringSlice("values", []string{}, "Path to YAML files with template values (required for v1beta3 specs)")
cmd.Flags().StringSlice("set", []string{}, "Set template values via command line (e.g., --set key=value)")
return cmd
}
func runLint(opts lint.LintOptions) error {
// Validate file paths exist
for _, filePath := range opts.FilePaths {
if _, err := os.Stat(filePath); err != nil {
return errors.Wrapf(err, "file not found: %s", filePath)
}
}
// Run linting
results, err := lint.LintFiles(opts)
if err != nil {
return errors.Wrap(err, "failed to lint files")
}
// Format and print results
output := lint.FormatResults(results, opts.Format)
fmt.Print(output)
// Return appropriate exit code
if lint.HasErrors(results) {
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, nil)
}
return nil
}
+3 -21
View File
@@ -5,14 +5,11 @@ import (
"os"
"strings"
"errors"
"github.com/replicatedhq/troubleshoot/cmd/internal/util"
preflightcli "github.com/replicatedhq/troubleshoot/cmd/preflight/cli"
"github.com/replicatedhq/troubleshoot/internal/traces"
"github.com/replicatedhq/troubleshoot/pkg/constants"
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
"github.com/replicatedhq/troubleshoot/pkg/logger"
"github.com/replicatedhq/troubleshoot/pkg/types"
"github.com/replicatedhq/troubleshoot/pkg/updater"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@@ -112,8 +109,8 @@ If no arguments are provided, specs are automatically loaded from the cluster by
cmd.AddCommand(Diff())
cmd.AddCommand(Schedule())
cmd.AddCommand(UploadCmd())
cmd.AddCommand(LintCmd())
cmd.AddCommand(util.VersionCmd())
cmd.AddCommand(preflightcli.LintCmd())
cmd.Flags().StringSlice("redactors", []string{}, "names of the additional redactors to use")
cmd.Flags().Bool("redact", true, "enable/disable default redactions")
@@ -137,12 +134,6 @@ If no arguments are provided, specs are automatically loaded from the cluster by
cmd.Flags().Bool("dry-run", false, "print support bundle spec without collecting anything")
cmd.Flags().Bool("auto-update", true, "enable automatic binary self-update check and install")
// Upload flags
cmd.Flags().Bool("auto-upload", false, "automatically upload resulting bundle to replicated.app")
cmd.Flags().String("license-id", "", "license ID for authentication when uploading (auto-detected from bundle if not provided)")
cmd.Flags().String("app-slug", "", "application slug when uploading (auto-detected from bundle if not provided)")
cmd.Flags().String("upload-domain", "", "custom domain for upload (default: replicated.app)")
// Auto-discovery flags
cmd.Flags().Bool("auto", false, "enable auto-discovery of foundational collectors. When used with YAML specs, adds foundational collectors to YAML collectors. When used alone, collects only foundational data")
cmd.Flags().Bool("include-images", false, "include container image metadata collection when using auto-discovery")
@@ -172,16 +163,7 @@ If no arguments are provided, specs are automatically loaded from the cluster by
}
func InitAndExecute() {
cmd := RootCmd()
if err := cmd.Execute(); err != nil {
var exitErr types.ExitError
if errors.As(err, &exitErr) {
if exitErr.ExitStatus() != constants.EXIT_CODE_FAIL && exitErr.ExitStatus() != constants.EXIT_CODE_WARN {
cmd.PrintErrln("Error:", err.Error())
}
os.Exit(exitErr.ExitStatus())
}
cmd.PrintErrln("Error:", err.Error())
if err := RootCmd().Execute(); err != nil {
os.Exit(1)
}
}
+2 -23
View File
@@ -242,26 +242,6 @@ func runTroubleshoot(v *viper.Viper, args []string) error {
}
}
// Attempt auto-upload before any early returns
if v.GetBool("auto-upload") && !response.FileUploaded {
licenseID := v.GetString("license-id")
appSlug := v.GetString("app-slug")
uploadDomain := v.GetString("upload-domain")
targetDomain := uploadDomain
if targetDomain == "" {
targetDomain = "replicated.app"
}
fmt.Fprintf(os.Stderr, "Auto-uploading bundle to %s...\n", targetDomain)
if err := supportbundle.UploadBundleAutoDetect(response.ArchivePath, licenseID, appSlug, uploadDomain); err != nil {
fmt.Fprintf(os.Stderr, "Auto-upload failed: %v\n", err)
fmt.Fprintf(os.Stderr, "You can manually upload the bundle using: support-bundle upload %s\n", response.ArchivePath)
} else {
response.FileUploaded = true
}
}
if !response.FileUploaded {
if appName := mainBundle.Labels["applicationName"]; appName != "" {
f := `A support bundle for %s has been created in this directory
@@ -289,12 +269,11 @@ the %s Admin Console to begin analysis.`
fmt.Printf("\r%s\r", cursor.ClearEntireLine())
}
if response.FileUploaded {
fmt.Printf("A support bundle has been created and uploaded to replicated.app for analysis.\n")
fmt.Printf("A support bundle has been created and uploaded to your cluster for analysis. Please visit the Troubleshoot page to continue.\n")
fmt.Printf("A copy of this support bundle was written to the current directory, named %q\n", response.ArchivePath)
} else {
fmt.Printf("A support bundle has been created in the current directory named %q\n", response.ArchivePath)
}
return nil
}
@@ -518,7 +497,7 @@ func (a *analysisOutput) FormattedAnalysisOutput() (outputJson string, err error
formatted, err := json.MarshalIndent(o, "", " ")
if err != nil {
return "", fmt.Errorf("\r * Failed to format analysis: %v", err)
return "", fmt.Errorf("\r * Failed to format analysis: %v\n", err)
}
return string(formatted), nil
}
+2 -7
View File
@@ -26,10 +26,7 @@ Examples:
support-bundle upload bundle.tar.gz --license-id YOUR_LICENSE_ID
# Specify both license and app
support-bundle upload bundle.tar.gz --license-id YOUR_LICENSE_ID --app-slug my-app
# Upload to a custom domain (e.g., development environment)
support-bundle upload bundle.tar.gz --upload-domain replicated-app-dev.example.com`,
support-bundle upload bundle.tar.gz --license-id YOUR_LICENSE_ID --app-slug my-app`,
RunE: func(cmd *cobra.Command, args []string) error {
v := viper.GetViper()
bundlePath := args[0]
@@ -42,10 +39,9 @@ Examples:
// Get upload parameters
licenseID := v.GetString("license-id")
appSlug := v.GetString("app-slug")
uploadDomain := v.GetString("upload-domain")
// Use auto-detection for uploads
if err := supportbundle.UploadBundleAutoDetect(bundlePath, licenseID, appSlug, uploadDomain); err != nil {
if err := supportbundle.UploadBundleAutoDetect(bundlePath, licenseID, appSlug); err != nil {
return errors.Wrap(err, "upload failed")
}
@@ -55,7 +51,6 @@ Examples:
cmd.Flags().String("license-id", "", "license ID for authentication (auto-detected from bundle if not provided)")
cmd.Flags().String("app-slug", "", "application slug (auto-detected from bundle if not provided)")
cmd.Flags().String("upload-domain", "", "custom domain for upload (default: replicated.app)")
return cmd
}
+2 -2
View File
@@ -8,7 +8,7 @@ builds:
- id: preflight
main: ./cmd/preflight/main.go
env: [CGO_ENABLED=0]
goos: [linux, darwin, windows]
goos: [linux, darwin]
goarch: [amd64, arm, arm64]
ignore:
- goos: windows
@@ -31,7 +31,7 @@ builds:
- id: support-bundle
main: ./cmd/troubleshoot/main.go
env: [CGO_ENABLED=0]
goos: [linux, darwin, windows]
goos: [linux, darwin]
goarch: [amd64, arm, arm64]
ignore:
- goos: windows
+2 -2
View File
@@ -42,7 +42,7 @@ spec:
matchLabels:
os: darwin
arch: amd64
{{addURIAndSha "https://github.com/replicatedhq/troubleshoot/releases/download/{{ .TagName }}/preflight_darwin_all.tar.gz" .TagName }}
{{addURIAndSha "https://github.com/replicatedhq/troubleshoot/releases/download/{{ .TagName }}/preflight_darwin_amd64.tar.gz" .TagName }}
files:
- from: preflight
to: .
@@ -53,7 +53,7 @@ spec:
matchLabels:
os: darwin
arch: arm64
{{addURIAndSha "https://github.com/replicatedhq/troubleshoot/releases/download/{{ .TagName }}/preflight_darwin_all.tar.gz" .TagName }}
{{addURIAndSha "https://github.com/replicatedhq/troubleshoot/releases/download/{{ .TagName }}/preflight_darwin_arm64.tar.gz" .TagName }}
files:
- from: preflight
to: .
+2 -2
View File
@@ -42,7 +42,7 @@ spec:
matchLabels:
os: darwin
arch: amd64
{{addURIAndSha "https://github.com/replicatedhq/troubleshoot/releases/download/{{ .TagName }}/support-bundle_darwin_all.tar.gz" .TagName }}
{{addURIAndSha "https://github.com/replicatedhq/troubleshoot/releases/download/{{ .TagName }}/support-bundle_darwin_amd64.tar.gz" .TagName }}
files:
- from: support-bundle
to: .
@@ -53,7 +53,7 @@ spec:
matchLabels:
os: darwin
arch: arm64
{{addURIAndSha "https://github.com/replicatedhq/troubleshoot/releases/download/{{ .TagName }}/support-bundle_darwin_all.tar.gz" .TagName }}
{{addURIAndSha "https://github.com/replicatedhq/troubleshoot/releases/download/{{ .TagName }}/support-bundle_darwin_arm64.tar.gz" .TagName }}
files:
- from: support-bundle
to: .
@@ -1,59 +0,0 @@
apiVersion: troubleshoot.sh/v1beta3
kind: SupportBundle
metadata:
name: test-v1beta3-secretref
spec:
collectors:
# Test 1: PostgreSQL with URI from secret
- postgres:
collectorName: postgres-with-secret
uri:
valueFrom:
secretKeyRef:
name: test-database-credentials
key: postgres-uri
# This will fail to connect (fake server) but that's OK -
# we're testing secret resolution, not actual DB connectivity
# Test 2: PostgreSQL with TLS certs from secret
- postgres:
collectorName: postgres-with-tls
uri:
value: "postgresql://testuser:testpass@localhost:5432/testdb"
tls:
cacert:
valueFrom:
secretKeyRef:
name: test-database-credentials
key: ca.crt
clientCert:
valueFrom:
secretKeyRef:
name: test-database-credentials
key: client.crt
clientKey:
valueFrom:
secretKeyRef:
name: test-database-credentials
key: client.key
# Test 3: MySQL with URI from secret
- mysql:
collectorName: mysql-with-secret
uri:
valueFrom:
secretKeyRef:
name: test-database-credentials
key: mysql-uri
# Test 4: Redis with URI from secret
- redis:
collectorName: redis-with-secret
uri:
valueFrom:
secretKeyRef:
name: test-database-credentials
key: redis-uri
# Test 5: Literal value (no secret) for comparison
- clusterInfo: {}
-39
View File
@@ -1,39 +0,0 @@
---
# Secret containing database credentials
apiVersion: v1
kind: Secret
metadata:
name: test-database-credentials
namespace: default
type: Opaque
stringData:
# PostgreSQL connection URI
postgres-uri: "postgresql://testuser:supersecret@postgres.example.com:5432/testdb?sslmode=require"
# MySQL connection URI
mysql-uri: "mysql://testuser:supersecret@mysql.example.com:3306/testdb"
# Redis connection URI
redis-uri: "redis://:supersecret@redis.example.com:6379"
# TLS certificates (example data)
ca.crt: |
-----BEGIN CERTIFICATE-----
MIICpDCCAYwCCQDU+pQ3ZUD30jANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls
b2NhbGhvc3QwHhcNMjQwMTAxMDAwMDAwWhcNMjUwMTAxMDAwMDAwWjAUMRIwEAYD
VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7
VJTUt9Us8cKjMzEfYyjiWA4R4/M2bS1+fWIcPm15A8IgC0qC1J3xGhE=
-----END CERTIFICATE-----
client.crt: |
-----BEGIN CERTIFICATE-----
MIICpDCCAYwCCQDU+pQ3ZUD30jANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls
b2NhbGhvc3QwHhcNMjQwMTAxMDAwMDAwWhcNMjUwMTAxMDAwMDAwWjAUMRIwEAYD
VQQDDA5jbGllbnQtY2VydA==
-----END CERTIFICATE-----
client.key: |
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7VJTUt9Us8cKj
MzEfYyjiWA4R4/M2bS1+fWIcPm15A8IgC0qC1J3xGhE=
-----END PRIVATE KEY-----
+1 -1
View File
@@ -92,7 +92,7 @@ spec:
- nodeResources:
checkName: Must have 1 node with 2Gi (available) memory and at least 2 cores (on a single node)
filters:
memoryAllocatable: 2Gi
allocatableMemory: 2Gi
cpuCapacity: "2"
outcomes:
- pass:
+3 -3
View File
@@ -28,7 +28,7 @@ spec:
- nodeResources:
checkName: Must have 1 node with 16 GB (available) memory and 10 cores (on a single node)
filters:
memoryAllocatable: 16Gi
allocatableMemory: 16Gi
cpuCapacity: "10"
outcomes:
- fail:
@@ -39,7 +39,7 @@ spec:
- nodeResources:
checkName: Must have 1 node with 16 GB (available) memory and 4 cores of amd64 arch (on a single node)
filters:
memoryAllocatable: 16Gi
allocatableMemory: 16Gi
cpuArchitecture: amd64
cpuCapacity: "4"
outcomes:
@@ -54,7 +54,7 @@ spec:
selector:
matchLabel:
node-role.kubernetes.io/master: ""
memoryAllocatable: 16Gi
allocatableMemory: 16Gi
cpuArchitecture: amd64
cpuCapacity: "6"
outcomes:
+22 -22
View File
@@ -1,6 +1,6 @@
module helm-template
go 1.25.5
go 1.24.6
// Always use the local version of troubleshoot so as to build using
// the latest version of the library. This will ensure the example
@@ -9,17 +9,17 @@ replace github.com/replicatedhq/troubleshoot v0.0.0 => ../../../
require (
github.com/replicatedhq/troubleshoot v0.0.0
helm.sh/helm/v3 v3.19.4
helm.sh/helm/v3 v3.19.0
sigs.k8s.io/yaml v1.6.0
)
require (
dario.cat/mergo v1.0.2 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
@@ -28,6 +28,7 @@ require (
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
@@ -45,29 +46,28 @@ require (
github.com/shopspring/decimal v1.4.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/term v0.35.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/time v0.12.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.35.0 // indirect
k8s.io/apiextensions-apiserver v0.35.0 // indirect
k8s.io/apimachinery v0.35.0 // indirect
k8s.io/client-go v0.35.0 // indirect
k8s.io/api v0.34.1 // indirect
k8s.io/apiextensions-apiserver v0.34.1 // indirect
k8s.io/apimachinery v0.34.1 // indirect
k8s.io/client-go v0.34.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
sigs.k8s.io/controller-runtime v0.22.4 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
sigs.k8s.io/controller-runtime v0.22.1 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
)
+83 -56
View File
@@ -2,16 +2,16 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -36,6 +36,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -43,8 +45,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
@@ -53,6 +55,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -71,17 +75,17 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
@@ -98,63 +102,86 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
helm.sh/helm/v3 v3.19.4 h1:E2yFBejmZBczWr5LblhjZbvAOAwVumfBO1AtN3nqI30=
helm.sh/helm/v3 v3.19.4/go.mod h1:PC1rk7PqacpkV4acUFMLStOOis7QM9Jq3DveHBInu4s=
k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY=
k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA=
k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4=
k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU=
k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8=
k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE=
k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o=
helm.sh/helm/v3 v3.19.0 h1:krVyCGa8fa/wzTZgqw0DUiXuRT5BPdeqE/sQXujQ22k=
helm.sh/helm/v3 v3.19.0/go.mod h1:Lk/SfzN0w3a3C3o+TdAKrLwJ0wcZ//t1/SDXAvfgDdc=
k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI=
k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc=
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg=
sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
+1 -1
View File
@@ -77,7 +77,7 @@ spec:
- nodeResources:
checkName: Must have 1 node with 2Gi (available) memory and at least 2 cores (on a single node)
filters:
memoryAllocatable: 2Gi
allocatableMemory: 2Gi
cpuCapacity: "2"
outcomes:
- pass:
-236
View File
@@ -1,236 +0,0 @@
# v1beta3 Support Bundle Examples
This directory contains example Support Bundle specs using the v1beta3 API, which introduces `StringOrValueFrom` support for securely referencing Kubernetes Secrets and ConfigMaps in collector fields.
## Features
### StringOrValueFrom Pattern
The v1beta3 API introduces a Kubernetes-native pattern for referencing sensitive values:
```yaml
uri:
valueFrom:
secretKeyRef:
name: my-secret
key: connection-uri
```
or
```yaml
uri: "postgresql://localhost:5432/db" # Literal value
```
### Supported Collectors
Currently, v1beta3 supports `StringOrValueFrom` for:
- **Database collectors**: `postgres`, `mysql`, `redis`, `mssql`
- `uri` field - Connection strings from secrets
- `tls` fields - CA cert, client cert, and client key from secrets
## Examples
### 1. postgres-with-secret.yaml
Basic PostgreSQL collector with connection URI from a secret.
**Use case**: Securely store database credentials without hardcoding them in the spec.
```bash
kubectl apply -f postgres-with-secret.yaml
```
### 2. postgres-with-tls.yaml
PostgreSQL with TLS configuration from secrets.
**Use case**: Secure database connections with mutual TLS, storing certificates in secrets.
```bash
kubectl apply -f postgres-with-tls.yaml
```
### 3. multiple-databases.yaml
Multiple database collectors (PostgreSQL, MySQL, Redis, MSSQL) with various configurations.
**Use case**: Collect diagnostics from multiple databases in your application stack.
```bash
kubectl apply -f multiple-databases.yaml
```
### 4. cross-namespace-secrets.yaml
Accessing secrets from different namespaces.
**Use case**: Centralized credential management in a shared namespace.
```bash
kubectl apply -f cross-namespace-secrets.yaml
```
**RBAC Requirements**: The support bundle service account needs `get` permission on secrets in the referenced namespaces.
### 5. optional-secrets.yaml
Using the `optional` field for graceful degradation.
**Use case**: Collect diagnostics even when some credentials are unavailable (e.g., optional secondary databases).
```bash
kubectl apply -f optional-secrets.yaml
```
### 6. configmap-example.yaml
Using ConfigMaps for non-sensitive configuration.
**Use case**: Store non-sensitive connection strings (e.g., development databases) in ConfigMaps.
```bash
kubectl apply -f configmap-example.yaml
```
## Key Concepts
### Secret vs ConfigMap
- **Secrets**: Use for sensitive data (passwords, tokens, certificates)
- **ConfigMaps**: Use for non-sensitive configuration (development endpoints, feature flags)
### Optional Field
```yaml
uri:
valueFrom:
secretKeyRef:
name: my-secret
key: uri
optional: true # Returns empty string if secret/key doesn't exist
```
- `optional: false` (default): Collection fails if secret is missing
- `optional: true`: Returns empty string if secret/key is missing
### Cross-Namespace Access
```yaml
uri:
valueFrom:
secretKeyRef:
name: shared-secret
key: uri
namespace: other-namespace # Access secrets in different namespaces
```
If `namespace` is not specified, uses the support bundle's namespace.
### Backward Compatibility
v1beta3 maintains backward compatibility with v1beta2 TLS configuration:
```yaml
tls:
secret: # v1beta2 style
name: tls-secret
namespace: default
```
## RBAC Configuration
Support bundles need appropriate RBAC permissions to read secrets:
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: troubleshoot-secret-reader
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["postgres-connection", "redis-creds"] # Restrict to specific secrets
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: troubleshoot-secret-reader-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: troubleshoot-secret-reader
subjects:
- kind: ServiceAccount
name: troubleshoot
namespace: default
```
## Migration from v1beta2
### Before (v1beta2):
```yaml
apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
spec:
collectors:
- postgres:
uri: "postgresql://user:password@host:5432/db" # Hardcoded
```
### After (v1beta3):
```yaml
apiVersion: troubleshoot.sh/v1beta3
kind: SupportBundle
spec:
collectors:
- postgres:
uri:
valueFrom:
secretKeyRef:
name: postgres-connection
key: connection-uri
```
## Limitations
1. **No value composition**: Cannot combine multiple secrets into a single value
```yaml
# NOT SUPPORTED
uri: "postgresql://$(USERNAME):$(PASSWORD)@host:5432/db"
```
Store the complete connection string in a single secret key.
2. **Collector scope**: Only database collectors support `StringOrValueFrom` initially
- Future versions will extend to HTTP, Data, and other collectors
3. **No templating**: The entire field value comes from one source
## Security Best Practices
1. **Use resourceNames in RBAC**: Restrict access to specific secrets
2. **Separate secrets**: Don't reuse secrets across applications
3. **Rotate credentials**: Update secrets regularly
4. **Audit access**: Monitor secret access logs
5. **Redact output**: Ensure connection strings are redacted in bundle output
## Troubleshooting
### Error: "failed to get secret default/my-secret"
- **Cause**: Secret doesn't exist or RBAC denied access
- **Solution**: Verify secret exists: `kubectl get secret my-secret`
- **Solution**: Check RBAC: `kubectl auth can-i get secret/my-secret`
### Error: "key 'uri' not found in secret"
- **Cause**: Secret exists but doesn't contain the specified key
- **Solution**: Check secret keys: `kubectl get secret my-secret -o jsonpath='{.data}'`
### Error: "cannot specify both 'value' and 'valueFrom'"
- **Cause**: Both literal value and secret reference provided
- **Solution**: Use only one: either `value: "string"` or `valueFrom: {...}`
## Additional Resources
- [Troubleshoot Documentation](https://troubleshoot.sh)
- [v1beta3 API Reference](https://troubleshoot.sh/docs/v1beta3/)
- [Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/)
- [RBAC Authorization](https://kubernetes.io/docs/reference/access-authn-authz/rbac/)
@@ -1,57 +0,0 @@
apiVersion: troubleshoot.sh/v1beta3
kind: SupportBundle
metadata:
name: configmap-example
spec:
collectors:
# Database URI from ConfigMap (non-sensitive connection string)
- postgres:
collectorName: dev-database
uri:
valueFrom:
configMapKeyRef:
name: database-config
key: dev-connection-uri
# Redis URI from ConfigMap
- redis:
collectorName: dev-redis
uri:
valueFrom:
configMapKeyRef:
name: cache-config
key: redis-uri
# Mixed: URI from ConfigMap, password from Secret
# Note: This shows the limitation - you can't compose values from multiple sources
# The full connection string must be in one place
- mysql:
collectorName: staging-mysql
uri:
valueFrom:
secretKeyRef:
name: mysql-secret
key: complete-connection-string
---
apiVersion: v1
kind: ConfigMap
metadata:
name: database-config
data:
dev-connection-uri: "postgresql://devuser@dev-postgres.default.svc:5432/devdb"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: cache-config
data:
redis-uri: "redis://dev-redis.default.svc:6379/0"
---
apiVersion: v1
kind: Secret
metadata:
name: mysql-secret
type: Opaque
stringData:
# Complete connection string with password included
complete-connection-string: "mysql://staging:stagingpass@staging-mysql:3306/stagingdb"
@@ -1,45 +0,0 @@
apiVersion: troubleshoot.sh/v1beta3
kind: SupportBundle
metadata:
name: cross-namespace-example
spec:
collectors:
# Database in one namespace, secret in another
- postgres:
collectorName: shared-database
uri:
valueFrom:
secretKeyRef:
name: shared-postgres-connection
key: uri
namespace: shared-services # Secret is in a different namespace
# Redis accessing centralized credentials
- redis:
collectorName: shared-cache
uri:
valueFrom:
secretKeyRef:
name: shared-redis-creds
key: uri
namespace: platform-credentials
---
# This secret would be in the 'shared-services' namespace
apiVersion: v1
kind: Secret
metadata:
name: shared-postgres-connection
namespace: shared-services
type: Opaque
stringData:
uri: "postgresql://shared:password@shared-postgres.shared-services.svc:5432/shared_db"
---
# This secret would be in the 'platform-credentials' namespace
apiVersion: v1
kind: Secret
metadata:
name: shared-redis-creds
namespace: platform-credentials
type: Opaque
stringData:
uri: "redis://shared-redis.shared-services.svc:6379/0"
@@ -1,78 +0,0 @@
apiVersion: troubleshoot.sh/v1beta3
kind: SupportBundle
metadata:
name: multi-database-support-bundle
spec:
collectors:
# PostgreSQL with secret reference
- postgres:
collectorName: primary-db
uri:
valueFrom:
secretKeyRef:
name: postgres-primary
key: connection-uri
# PostgreSQL replica with secret reference
- postgres:
collectorName: replica-db
uri:
valueFrom:
secretKeyRef:
name: postgres-replica
key: connection-uri
# Redis cache with secret reference
- redis:
collectorName: cache
uri:
valueFrom:
secretKeyRef:
name: redis-creds
key: uri
# MySQL with literal value (for development/testing)
- mysql:
collectorName: local-mysql
uri: "mysql://root:password@localhost:3306/testdb"
# MSSQL with secret reference
- mssql:
collectorName: legacy-db
uri:
valueFrom:
secretKeyRef:
name: mssql-connection
key: dsn
---
apiVersion: v1
kind: Secret
metadata:
name: postgres-primary
type: Opaque
stringData:
connection-uri: "postgresql://app:secret123@postgres-primary.default.svc:5432/appdb"
---
apiVersion: v1
kind: Secret
metadata:
name: postgres-replica
type: Opaque
stringData:
connection-uri: "postgresql://app:secret123@postgres-replica.default.svc:5432/appdb"
---
apiVersion: v1
kind: Secret
metadata:
name: redis-creds
type: Opaque
stringData:
uri: "redis://:cachesecret@redis.default.svc:6379/0"
---
apiVersion: v1
kind: Secret
metadata:
name: mssql-connection
type: Opaque
stringData:
dsn: "sqlserver://sa:Str0ngP@ssw0rd@mssql.default.svc:1433?database=legacy"
@@ -1,58 +0,0 @@
apiVersion: troubleshoot.sh/v1beta3
kind: SupportBundle
metadata:
name: optional-secrets-example
spec:
collectors:
# Required database - collection will fail if secret doesn't exist
- postgres:
collectorName: required-db
uri:
valueFrom:
secretKeyRef:
name: required-postgres
key: uri
optional: false # Default behavior - secret must exist
# Optional database - collection continues if secret doesn't exist
- postgres:
collectorName: optional-db
uri:
valueFrom:
secretKeyRef:
name: optional-postgres
key: uri
optional: true # Gracefully degrades if secret is missing
# Mixed required and optional TLS
- postgres:
collectorName: partially-optional
uri: "postgresql://localhost:5432/db"
tls:
cacert:
valueFrom:
secretKeyRef:
name: tls-certs
key: ca.crt
optional: false # CA cert is required
clientCert:
valueFrom:
secretKeyRef:
name: tls-certs
key: client.crt
optional: true # Client cert is optional (cert-only TLS)
clientKey:
valueFrom:
secretKeyRef:
name: tls-certs
key: client.key
optional: true # Client key is optional
---
apiVersion: v1
kind: Secret
metadata:
name: required-postgres
type: Opaque
stringData:
uri: "postgresql://user:pass@required-postgres:5432/db"
# Note: optional-postgres secret intentionally not created
@@ -1,21 +0,0 @@
apiVersion: troubleshoot.sh/v1beta3
kind: SupportBundle
metadata:
name: postgres-support-bundle
spec:
collectors:
- postgres:
collectorName: main-database
uri:
valueFrom:
secretKeyRef:
name: postgres-connection
key: connection-uri
---
apiVersion: v1
kind: Secret
metadata:
name: postgres-connection
type: Opaque
stringData:
connection-uri: "postgresql://myuser:mypassword@postgres.default.svc:5432/mydb?sslmode=require"
@@ -1,47 +0,0 @@
apiVersion: troubleshoot.sh/v1beta3
kind: SupportBundle
metadata:
name: postgres-tls-support-bundle
spec:
collectors:
- postgres:
collectorName: secure-database
uri:
valueFrom:
secretKeyRef:
name: postgres-connection
key: connection-uri
tls:
cacert:
valueFrom:
secretKeyRef:
name: postgres-tls
key: ca.crt
clientCert:
valueFrom:
secretKeyRef:
name: postgres-tls
key: tls.crt
clientKey:
valueFrom:
secretKeyRef:
name: postgres-tls
key: tls.key
---
apiVersion: v1
kind: Secret
metadata:
name: postgres-connection
type: Opaque
stringData:
connection-uri: "postgresql://myuser:mypassword@postgres.default.svc:5432/mydb?sslmode=verify-full"
---
apiVersion: v1
kind: Secret
metadata:
name: postgres-tls
type: kubernetes.io/tls
data:
ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi4uLgotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi4uLgotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi4uLgotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t
@@ -1,19 +0,0 @@
apiVersion: troubleshoot.sh/v1beta3
kind: SupportBundle
metadata:
name: invalid-collectors
spec:
collectors:
# Unknown collector type
- notACollector: {}
# Known collector but missing required fields (e.g., ceph requires namespace)
- ceph: {}
# Field exists but wrong type (should be a list)
hostCollectors: "not-a-list"
analyzers:
# Unknown analyzer type
- notAnAnalyzer: {}
# Known analyzer missing required 'outcomes'
- cephStatus:
namespace: default
@@ -1,2 +0,0 @@
# Empty values file for v1beta3 specs without templates
{}
@@ -1 +0,0 @@
minVersion: "1.19.0"
+119 -109
View File
@@ -1,6 +1,6 @@
module github.com/replicatedhq/troubleshoot
go 1.25.5
go 1.24.6
require (
github.com/Masterminds/sprig/v3 v3.3.0
@@ -8,105 +8,105 @@ require (
github.com/apparentlymart/go-cidr v1.1.0
github.com/blang/semver/v4 v4.0.0
github.com/casbin/govaluate v1.10.0
github.com/cilium/ebpf v0.20.0
github.com/containerd/cgroups/v3 v3.1.2
github.com/cilium/ebpf v0.19.0
github.com/containerd/cgroups/v3 v3.0.5
github.com/containers/image/v5 v5.36.2
github.com/distribution/distribution/v3 v3.0.0
github.com/fatih/color v1.18.0
github.com/go-logr/logr v1.4.3
github.com/go-redis/redis/v7 v7.4.1
github.com/go-sql-driver/mysql v1.9.3
github.com/gobwas/glob v0.2.3
github.com/godbus/dbus/v5 v5.2.2
github.com/godbus/dbus/v5 v5.1.0
github.com/google/gofuzz v1.2.0
github.com/google/uuid v1.6.0
github.com/gorilla/handlers v1.5.2
github.com/hashicorp/go-getter v1.8.4
github.com/hashicorp/go-getter v1.8.0
github.com/hashicorp/go-multierror v1.1.1
github.com/jackc/pgx/v5 v5.8.0
github.com/jackc/pgx/v5 v5.7.6
github.com/longhorn/go-iscsi-helper v0.0.0-20210330030558-49a327fb024e
github.com/manifoldco/promptui v0.9.0
github.com/mattn/go-isatty v0.0.20
github.com/microsoft/go-mssqldb v1.9.5
github.com/miekg/dns v1.1.70
github.com/microsoft/go-mssqldb v1.9.3
github.com/miekg/dns v1.1.68
github.com/opencontainers/image-spec v1.1.1
github.com/pkg/errors v0.9.1
github.com/replicatedhq/termui/v3 v3.1.1-0.20200811145416-f40076d26851
github.com/segmentio/ksuid v1.0.4
github.com/shirou/gopsutil/v4 v4.25.12
github.com/spf13/cobra v1.10.2
github.com/shirou/gopsutil/v4 v4.25.8
github.com/spf13/cobra v1.10.1
github.com/spf13/pflag v1.0.10
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/tj/go-spin v1.1.0
github.com/vishvananda/netlink v1.3.1
github.com/vishvananda/netns v0.0.5
github.com/vmware-tanzu/velero v1.17.1
go.opentelemetry.io/otel v1.39.0
go.opentelemetry.io/otel/sdk v1.39.0
go.podman.io/image/v5 v5.38.0
github.com/vmware-tanzu/velero v1.17.0
go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel/sdk v1.38.0
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67
golang.org/x/mod v0.32.0
golang.org/x/sync v0.19.0
golang.org/x/mod v0.28.0
golang.org/x/sync v0.17.0
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.35.0
k8s.io/apiextensions-apiserver v0.35.0
k8s.io/apimachinery v0.35.0
k8s.io/apiserver v0.35.0
k8s.io/cli-runtime v0.35.0
k8s.io/client-go v0.35.0
k8s.io/api v0.34.1
k8s.io/apiextensions-apiserver v0.34.1
k8s.io/apimachinery v0.34.1
k8s.io/apiserver v0.34.1
k8s.io/cli-runtime v0.34.1
k8s.io/client-go v0.34.1
k8s.io/klog/v2 v2.130.1
oras.land/oras-go/v2 v2.6.0
sigs.k8s.io/controller-runtime v0.22.4
k8s.io/kubernetes v1.34.1
oras.land/oras-go v1.2.6
sigs.k8s.io/controller-runtime v0.22.1
sigs.k8s.io/e2e-framework v0.6.0
)
require (
cel.dev/expr v0.24.0 // indirect
cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/auth v0.16.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/compute/metadata v0.7.0 // indirect
cloud.google.com/go/monitoring v1.24.2 // indirect
cyphar.com/go-pathrs v0.2.1 // indirect
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Masterminds/squirrel v1.5.4 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.6 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
github.com/aws/aws-sdk-go-v2/config v1.29.15 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.68 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.80.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.20 // indirect
github.com/aws/smithy-go v1.22.3 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
@@ -114,18 +114,18 @@ require (
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-containerregistry v0.20.6 // indirect
github.com/google/go-containerregistry v0.20.3 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/gosuri/uitable v0.0.4 // indirect
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 // indirect
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.65 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
@@ -134,7 +134,7 @@ require (
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mistifyio/go-zfs/v4 v4.0.0 // indirect
github.com/mistifyio/go-zfs/v3 v3.0.1 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
@@ -148,55 +148,61 @@ require (
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.4-0.20251023124752-b61f268f75b6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/sylabs/sif/v2 v2.22.0 // indirect
github.com/sylabs/sif/v2 v2.21.1 // indirect
github.com/tchap/go-patricia/v2 v2.3.3 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
github.com/vladimirvivien/gexe v0.4.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/errs v1.4.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
k8s.io/component-base v0.35.0 // indirect
k8s.io/kubectl v0.34.2 // indirect
golang.org/x/tools v0.36.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
k8s.io/component-base v0.34.1 // indirect
k8s.io/kubectl v0.34.0 // indirect
oras.land/oras-go/v2 v2.6.0 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
)
require (
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/storage v1.58.0 // indirect
cloud.google.com/go v0.121.1 // indirect
cloud.google.com/go/iam v1.5.2 // indirect
cloud.google.com/go/storage v1.55.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Microsoft/hcsshim v0.13.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/c9s/goprocinfo v0.0.0-20170724085704-0010a05ce49f // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/containerd/containerd v1.7.29 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect
github.com/containerd/containerd v1.7.28 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
github.com/containers/ocicrypt v1.2.1 // indirect
github.com/containers/storage v1.59.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/docker/docker v28.5.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.4 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/cli v28.3.2+incompatible // indirect
github.com/docker/docker v28.3.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.3 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
@@ -206,19 +212,22 @@ require (
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-intervals v0.0.2 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-version v1.8.0
github.com/hashicorp/go-safetemp v1.0.0 // indirect
github.com/hashicorp/go-version v1.7.0
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.9.0 // indirect
@@ -226,6 +235,7 @@ require (
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1
github.com/moby/locker v1.0.1 // indirect
github.com/moby/spdystream v0.5.0 // indirect
github.com/moby/sys/mountinfo v0.7.2 // indirect
github.com/moby/term v0.5.2 // indirect
@@ -235,45 +245,45 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/runtime-spec v1.3.0
github.com/opencontainers/selinux v1.13.1 // indirect
github.com/opencontainers/runtime-spec v1.2.1
github.com/opencontainers/selinux v1.12.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/vbatts/tar-split v0.12.2 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/vbatts/tar-split v0.12.1 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.podman.io/storage v1.61.1-0.20260113143440-ec033ce69f13 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.49.0
golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/sys v0.40.0
golang.org/x/term v0.39.0 // indirect
golang.org/x/text v0.33.0
golang.org/x/time v0.14.0 // indirect
google.golang.org/api v0.256.0 // indirect
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 // indirect
google.golang.org/grpc v1.76.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/net v0.44.0
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.36.0
golang.org/x/term v0.35.0 // indirect
golang.org/x/text v0.29.0
golang.org/x/time v0.12.0 // indirect
google.golang.org/api v0.241.0 // indirect
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect
google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
helm.sh/helm/v3 v3.19.4
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
k8s.io/kubelet v0.35.0
k8s.io/metrics v0.35.0
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
helm.sh/helm/v3 v3.19.0
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
k8s.io/kubelet v0.34.1
k8s.io/metrics v0.34.1
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
periph.io/x/host/v3 v3.8.5
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/kustomize/api v0.20.1 // indirect
sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect
sigs.k8s.io/yaml v1.6.0
+390 -242
View File
@@ -1,27 +1,26 @@
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.121.1 h1:S3kTQSydxmu1JfLRLpKtxRPA7rSrYPRPEUmL/PavVUw=
cloud.google.com/go v0.121.1/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw=
cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4=
cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc=
cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA=
cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=
cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=
cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
cloud.google.com/go/storage v1.58.0 h1:PflFXlmFJjG/nBeR9B7pKddLQWaFaRWx4uUi/LyNxxo=
cloud.google.com/go/storage v1.58.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI=
cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0=
cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY=
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8=
cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
@@ -40,20 +39,21 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuo
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
@@ -66,52 +66,56 @@ github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA=
github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok=
github.com/ahmetalpbalkan/go-cursor v0.0.0-20131010032410-8136607ea412 h1:vOVO0ypMfTt6tZacyI0kp+iCZb1XSNiYDqnzBWYgfe4=
github.com/ahmetalpbalkan/go-cursor v0.0.0-20131010032410-8136607ea412/go.mod h1:AI9hp1tkp10pAlK5TCwL+7yWbRgtDm9jhToq6qij2xs=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU=
github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8=
github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=
github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14=
github.com/aws/aws-sdk-go-v2/config v1.29.15 h1:I5XjesVMpDZXZEZonVfjI12VNMrYa38LtLnw4NtY5Ss=
github.com/aws/aws-sdk-go-v2/config v1.29.15/go.mod h1:tNIp4JIPonlsgaO5hxO372a6gjhN63aSWl2GVl5QoBQ=
github.com/aws/aws-sdk-go-v2/credentials v1.17.68 h1:cFb9yjI02/sWHBSYXAtkamjzCuRymvmeFmt0TC0MbYY=
github.com/aws/aws-sdk-go-v2/credentials v1.17.68/go.mod h1:H6E+jBzyqUu8u0vGaU6POkK3P0NylYEeRZ6ynBpMqIk=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.2 h1:BCG7DCXEXpNCcpwCxg1oi9pkJWH2+eZzTn9MY56MbVw=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.2/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.80.1 h1:xYEAf/6QHiTZDccKnPMbsMwlau13GsDsTgdue3wmHGw=
github.com/aws/aws-sdk-go-v2/service/s3 v1.80.1/go.mod h1:qbn305Je/IofWBJ4bJz/Q7pDEtnnoInw/dGt71v6rHE=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.20 h1:oIaQ1e17CSKaWmUTu62MtraRWVIosn/iONMuZt0gbqc=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.20/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k=
github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
@@ -126,6 +130,7 @@ github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaD
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk=
@@ -139,14 +144,18 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/cilium/ebpf v0.20.0 h1:atwWj9d3NffHyPZzVlx3hmw1on5CLe9eljR8VuHTwhM=
github.com/cilium/ebpf v0.20.0/go.mod h1:pzLjFymM+uZPLk/IXZUL63xdx5VXEo+enTzxkZXdycw=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/containerd/cgroups/v3 v3.1.2 h1:OSosXMtkhI6Qove637tg1XgK4q+DhR0mX8Wi8EhrHa4=
github.com/containerd/cgroups/v3 v3.1.2/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw=
github.com/containerd/containerd v1.7.29 h1:90fWABQsaN9mJhGkoVnuzEY+o1XDPbg9BTC9QTAHnuE=
github.com/containerd/containerd v1.7.29/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs=
github.com/cilium/ebpf v0.19.0 h1:Ro/rE64RmFBeA9FGjcTc+KmCeY6jXmryu6FfnzPRIao=
github.com/cilium/ebpf v0.19.0/go.mod h1:fLCgMo3l8tZmAdM3B2XqdFzXBpwkcSTroaVqN08OWVY=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k=
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo=
github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
github.com/containerd/containerd v1.7.28 h1:Nsgm1AtcmEh4AHAJ4gGlNSaKgXiNccU270Dnf81FQ3c=
github.com/containerd/containerd v1.7.28/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs=
github.com/containerd/continuity v0.4.4 h1:/fNVfTJ7wIl/YPMHjf+5H32uFhl63JucB34PlCpMKII=
github.com/containerd/continuity v0.4.4/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
@@ -155,19 +164,25 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8=
github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q=
github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=
github.com/containers/image/v5 v5.36.2 h1:GcxYQyAHRF/pLqR4p4RpvKllnNL8mOBn0eZnqJbfTwk=
github.com/containers/image/v5 v5.36.2/go.mod h1:b4GMKH2z/5t6/09utbse2ZiLK/c72GuGLFdp7K69eA4=
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA=
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY=
github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpVhSmM=
github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ=
github.com/containers/storage v1.59.1 h1:11Zu68MXsEQGBBd+GadPrHPpWeqjKS8hJDGiAHgIqDs=
github.com/containers/storage v1.59.1/go.mod h1:KoAYHnAjP3/cTsRS+mmWZGkufSY2GACiKQ4V3ZLQnR0=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -180,32 +195,38 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/cli v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY=
github.com/docker/cli v28.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v28.3.2+incompatible h1:mOt9fcLE7zaACbxW1GeS65RI67wIJrTnqS3hP2huFsY=
github.com/docker/cli v28.3.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.9.4 h1:76ItO69/AP/V4yT9V4uuuItG0B1N8hvt0T0c0NN/DzI=
github.com/docker/docker-credential-helpers v0.9.4/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8=
@@ -231,8 +252,11 @@ github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxI
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -255,6 +279,7 @@ github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRf
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
@@ -262,27 +287,49 @@ github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlnd
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU=
github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y=
github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI=
github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI=
github.com/google/go-intervals v0.0.2 h1:FGrVEiUnTRKR8yE04qzXYaJMtnIYqobR5QbblK3ixcM=
github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -290,17 +337,18 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
@@ -313,19 +361,21 @@ github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJr
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 h1:0HADrxxqaQkGycO1JoUUA+B4FnIkuo8d2bz/hSaTFFQ=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70/go.mod h1:fm2FdDCzJdtbXF7WKAMvBb5NEPouXPHFbGNYs9ShFns=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.65 h1:81+kWbE1yErFBMjME0I5k3x3kojjKsWtPYHEAutoPow=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.65/go.mod h1:WtMzv9T++tfWVea+qB2MXoaqxw33S8bpJslzUike2mQ=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-getter v1.8.4 h1:hGEd2xsuVKgwkMtPVufq73fAmZU/x65PPcqH3cb0D9A=
github.com/hashicorp/go-getter v1.8.4/go.mod h1:x27pPGSg9kzoB147QXI8d/nDvp2IgYGcwuRjpaXE9Yg=
github.com/hashicorp/go-getter v1.8.0 h1:GMRdoMBDz12Mim366pWsRVIrrkugJ19rrmykkv0Nhzo=
github.com/hashicorp/go-getter v1.8.0/go.mod h1:/K0O5zR6R72O3r2x3z2UHadiC0XHMbbzHO9pS8ZeJPA=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo=
github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw=
github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU=
github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4=
@@ -340,8 +390,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
@@ -352,13 +402,19 @@ github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtL
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM=
github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -393,18 +449,19 @@ github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/microsoft/go-mssqldb v1.9.5 h1:orwya0X/5bsL1o+KasupTkk2eNTNFkTQG0BEe/HxCn0=
github.com/microsoft/go-mssqldb v1.9.5/go.mod h1:VCP2a0KEZZtGLRHd1PsLavLFYy/3xX2yJUPycv3Sr2Q=
github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/mistifyio/go-zfs/v4 v4.0.0 h1:sU0+5dX45tdDK5xNZ3HBi95nxUc48FS92qbIZEvpAg4=
github.com/mistifyio/go-zfs/v4 v4.0.0/go.mod h1:weotFtXTHvBwhr9Mv96KYnDkTPBOHFUbm9cBmQpesL0=
github.com/microsoft/go-mssqldb v1.9.3 h1:hy4p+LDC8LIGvI3JATnLVmBOLMJbmn5X400mr5j0lPs=
github.com/microsoft/go-mssqldb v1.9.3/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLYN3iFL90lQ+PA=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/mistifyio/go-zfs/v3 v3.0.1 h1:YaoXgBePoMA12+S1u/ddkv+QqxcfiZK4prI6HPnkFiU=
github.com/mistifyio/go-zfs/v3 v3.0.1/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
@@ -416,6 +473,8 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
@@ -424,15 +483,19 @@ github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCnd
github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I=
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
@@ -442,6 +505,7 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840=
@@ -449,19 +513,19 @@ github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg=
github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE=
github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg=
github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww=
github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8=
github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
@@ -470,6 +534,7 @@ github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1H
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -482,14 +547,25 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho=
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U=
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc=
@@ -501,8 +577,8 @@ github.com/replicatedhq/termui/v3 v3.1.1-0.20200811145416-f40076d26851/go.mod h1
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o=
github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@@ -511,27 +587,28 @@ github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDc
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E=
github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEWY=
github.com/sebdah/goldie/v2 v2.5.5/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY=
github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.9.4-0.20251023124752-b61f268f75b6 h1:JEH4GlfEqachL/BWMxNEqYZGst5AGInBdLF/bEx+dZY=
github.com/sirupsen/logrus v1.9.4-0.20251023124752-b61f268f75b6/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -541,60 +618,69 @@ github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8W
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/sylabs/sif/v2 v2.22.0 h1:Y+xXufp4RdgZe02SR3nWEg7S6q4tPWN237WHYzkDSKA=
github.com/sylabs/sif/v2 v2.22.0/go.mod h1:W1XhWTmG1KcG7j5a3KSYdMcUIFvbs240w/MMVW627hs=
github.com/sylabs/sif/v2 v2.21.1 h1:GZ0b5//AFAqJEChd8wHV/uSKx/l1iuGYwjR8nx+4wPI=
github.com/sylabs/sif/v2 v2.21.1/go.mod h1:YoqEGQnb5x/ItV653bawXHZJOXQaEWpGwHsSD3YePJI=
github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc=
github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
github.com/tj/go-spin v1.1.0 h1:lhdWZsvImxvZ3q1C5OIB7d72DuOwP4O2NdBg9PyzNds=
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4=
github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/vladimirvivien/gexe v0.4.1 h1:W9gWkp8vSPjDoXDu04Yp4KljpVMaSt8IQuHswLDd5LY=
github.com/vladimirvivien/gexe v0.4.1/go.mod h1:3gjgTqE2c0VyHnU5UOIwk7gyNzZDGulPb/DJPgcw64E=
github.com/vmware-tanzu/velero v1.17.1 h1:ldKeiTuUwkThOw7zrUucNA1NwnLG66zl13YetWAoE0I=
github.com/vmware-tanzu/velero v1.17.1/go.mod h1:3KTxuUN6Un38JzmYAX+8U6j2k6EexGoNNxa8jrJML8U=
github.com/vmware-tanzu/velero v1.17.0 h1:b+KLlBG+v1YKogP81nAFix2pgJBTmUrnVlXg+OfB5ao=
github.com/vmware-tanzu/velero v1.17.0/go.mod h1:BJRFKei89hSqrazQKiwv5YhhX871X1W1qPyo5OP09zw=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w=
go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk=
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw=
go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4=
go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8=
@@ -613,103 +699,159 @@ go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9Xa
go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0/go.mod h1:zKU4zUgKiaRxrdovSS2amdM5gOc59slmo/zJwGX+YBg=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s=
go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk=
go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs=
go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.podman.io/image/v5 v5.38.0 h1:aUKrCANkPvze1bnhLJsaubcfz0d9v/bSDLnwsXJm6G4=
go.podman.io/image/v5 v5.38.0/go.mod h1:hSIoIUzgBnmc4DjoIdzk63aloqVbD7QXDMkSE/cvG90=
go.podman.io/storage v1.61.1-0.20260113143440-ec033ce69f13 h1:31m5LHEH19ESCYFrvZh4YX1kkfa7sQSapHg9/NHzpok=
go.podman.io/storage v1.61.1-0.20260113143440-ec033ce69f13/go.mod h1:nkKllgCrEviIXYWZNGxWdzU02pUBIfH5oOSrB/QgwIQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo=
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190524152521-dbbf3f1254d4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc=
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.241.0 h1:QKwqWQlkc6O895LchPEDUSYr22Xp3NCxpQRiWTB6avE=
google.golang.org/api v0.241.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
@@ -725,44 +867,50 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
helm.sh/helm/v3 v3.19.4 h1:E2yFBejmZBczWr5LblhjZbvAOAwVumfBO1AtN3nqI30=
helm.sh/helm/v3 v3.19.4/go.mod h1:PC1rk7PqacpkV4acUFMLStOOis7QM9Jq3DveHBInu4s=
k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY=
k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA=
k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4=
k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU=
k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8=
k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4=
k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds=
k8s.io/cli-runtime v0.35.0 h1:PEJtYS/Zr4p20PfZSLCbY6YvaoLrfByd6THQzPworUE=
k8s.io/cli-runtime v0.35.0/go.mod h1:VBRvHzosVAoVdP3XwUQn1Oqkvaa8facnokNkD7jOTMY=
k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE=
k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o=
k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94=
k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0=
helm.sh/helm/v3 v3.19.0 h1:krVyCGa8fa/wzTZgqw0DUiXuRT5BPdeqE/sQXujQ22k=
helm.sh/helm/v3 v3.19.0/go.mod h1:Lk/SfzN0w3a3C3o+TdAKrLwJ0wcZ//t1/SDXAvfgDdc=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI=
k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc=
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA=
k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0=
k8s.io/cli-runtime v0.34.1 h1:btlgAgTrYd4sk8vJTRG6zVtqBKt9ZMDeQZo2PIzbL7M=
k8s.io/cli-runtime v0.34.1/go.mod h1:aVA65c+f0MZiMUPbseU/M9l1Wo2byeaGwUuQEQVVveE=
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A=
k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/kubectl v0.34.2 h1:+fWGrVlDONMUmmQLDaGkQ9i91oszjjRAa94cr37hzqA=
k8s.io/kubectl v0.34.2/go.mod h1:X2KTOdtZZNrTWmUD4oHApJ836pevSl+zvC5sI6oO2YQ=
k8s.io/kubelet v0.35.0 h1:8cgJHCBCKLYuuQ7/Pxb/qWbJfX1LXIw7790ce9xHq7c=
k8s.io/kubelet v0.35.0/go.mod h1:ciRzAXn7C4z5iB7FhG1L2CGPPXLTVCABDlbXt/Zz8YA=
k8s.io/metrics v0.35.0 h1:xVFoqtAGm2dMNJAcB5TFZJPCen0uEqqNt52wW7ABbX8=
k8s.io/metrics v0.35.0/go.mod h1:g2Up4dcBygZi2kQSEQVDByFs+VUwepJMzzQLJJLpq4M=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
k8s.io/kubectl v0.34.0 h1:NcXz4TPTaUwhiX4LU+6r6udrlm0NsVnSkP3R9t0dmxs=
k8s.io/kubectl v0.34.0/go.mod h1:bmd0W5i+HuG7/p5sqicr0Li0rR2iIhXL0oUyLF3OjR4=
k8s.io/kubelet v0.34.1 h1:doAaTA9/Yfzbdq/u/LveZeONp96CwX9giW6b+oHn4m4=
k8s.io/kubelet v0.34.1/go.mod h1:PtV3Ese8iOM19gSooFoQT9iyRisbmJdAPuDImuccbbA=
k8s.io/kubernetes v1.34.1 h1:F3p8dtpv+i8zQoebZeK5zBqM1g9x1aIdnA5vthvcuUk=
k8s.io/kubernetes v1.34.1/go.mod h1:iu+FhII+Oc/1gGWLJcer6wpyih441aNFHl7Pvm8yPto=
k8s.io/metrics v0.34.1 h1:374Rexmp1xxgRt64Bi0TsjAM8cA/Y8skwCoPdjtIslE=
k8s.io/metrics v0.34.1/go.mod h1:Drf5kPfk2NJrlpcNdSiAAHn/7Y9KqxpRNagByM7Ei80=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
oras.land/oras-go v1.2.6 h1:z8cmxQXBU8yZ4mkytWqXfo6tZcamPwjsuxYU81xJ8Lk=
oras.land/oras-go v1.2.6/go.mod h1:OVPc1PegSEe/K8YiLfosrlqlqTN9PUyFvOw5Y9gwrT8=
oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o=
periph.io/x/host/v3 v3.8.5 h1:g4g5xE1XZtDiGl1UAJaUur1aT7uNiFLMkyMEiZ7IHII=
periph.io/x/host/v3 v3.8.5/go.mod h1:hPq8dISZIc+UNfWoRj+bPH3XEBQqJPdFdx218W92mdc=
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg=
sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY=
sigs.k8s.io/e2e-framework v0.6.0 h1:p7hFzHnLKO7eNsWGI2AbC1Mo2IYxidg49BiT4njxkrM=
sigs.k8s.io/e2e-framework v0.6.0/go.mod h1:IREnCHnKgRCioLRmNi0hxSJ1kJ+aAdjEKK/gokcZu4k=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I=
sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM=
sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78=
+3 -9
View File
@@ -191,9 +191,7 @@ func LoadFromCLIArgs(ctx context.Context, client kubernetes.Interface, args []st
// load URL spec first to remove URI key from the spec
urlSpec, err := loader.LoadSpecs(ctx, loader.LoadOptions{
RawSpec: rawURLSpec,
Client: client,
Namespace: vp.GetString("namespace"),
RawSpec: rawURLSpec,
})
if err != nil {
fmt.Println(color.YellowString("failed to load spec from URI %q: %v\n", v, err))
@@ -211,9 +209,7 @@ func LoadFromCLIArgs(ctx context.Context, client kubernetes.Interface, args []st
}
kinds, err := loader.LoadSpecs(ctx, loader.LoadOptions{
RawSpecs: rawSpecs,
Client: client,
Namespace: vp.GetString("namespace"),
RawSpecs: rawSpecs,
})
if err != nil {
return nil, err
@@ -367,9 +363,7 @@ func LoadFromCluster(ctx context.Context, client kubernetes.Interface, selectors
// Load troubleshoot specs from the raw specs
return loader.LoadSpecs(ctx, loader.LoadOptions{
RawSpecs: rawSpecs,
Client: client,
Namespace: ns,
RawSpecs: rawSpecs,
})
}
+105 -644
View File
@@ -421,31 +421,39 @@ func (a *OllamaAgent) Analyze(ctx context.Context, data []byte, analyzers []anal
func (a *OllamaAgent) discoverAnalyzers(bundle *analyzer.SupportBundle) []analyzer.AnalyzerSpec {
var specs []analyzer.AnalyzerSpec
// Collect files by type for aggregation
podFiles := []string{}
deploymentFiles := []string{}
eventFiles := []string{}
nodeFiles := []string{}
// Analyze bundle contents to determine what types of analysis to perform
for filePath := range bundle.Files {
filePathLower := strings.ToLower(filePath)
filePath = strings.ToLower(filePath)
switch {
case strings.Contains(filePathLower, "pods") && strings.HasSuffix(filePathLower, ".json"):
podFiles = append(podFiles, filePath)
case strings.Contains(filePath, "pods") && strings.HasSuffix(filePath, ".json"):
specs = append(specs, analyzer.AnalyzerSpec{
Name: "ai-pod-analysis",
Type: "ai-workload",
Category: "pods",
Priority: 10,
Config: map[string]interface{}{"filePath": filePath, "promptType": "pod-analysis"},
})
case strings.Contains(filePathLower, "deployments") && strings.HasSuffix(filePathLower, ".json"):
deploymentFiles = append(deploymentFiles, filePath)
case strings.Contains(filePath, "deployments") && strings.HasSuffix(filePath, ".json"):
specs = append(specs, analyzer.AnalyzerSpec{
Name: "ai-deployment-analysis",
Type: "ai-workload",
Category: "deployments",
Priority: 9,
Config: map[string]interface{}{"filePath": filePath, "promptType": "deployment-analysis"},
})
case strings.Contains(filePathLower, "events") && strings.HasSuffix(filePathLower, ".json"):
eventFiles = append(eventFiles, filePath)
case strings.Contains(filePath, "events") && strings.HasSuffix(filePath, ".json"):
specs = append(specs, analyzer.AnalyzerSpec{
Name: "ai-event-analysis",
Type: "ai-events",
Category: "events",
Priority: 8,
Config: map[string]interface{}{"filePath": filePath, "promptType": "event-analysis"},
})
case strings.Contains(filePathLower, "nodes") && strings.HasSuffix(filePathLower, ".json"):
nodeFiles = append(nodeFiles, filePath)
case strings.Contains(filePathLower, "logs") && strings.HasSuffix(filePathLower, ".log"):
// Logs are analyzed separately per file (not aggregated)
case strings.Contains(filePath, "logs") && strings.HasSuffix(filePath, ".log"):
specs = append(specs, analyzer.AnalyzerSpec{
Name: "ai-log-analysis",
Type: "ai-logs",
@@ -453,424 +461,50 @@ func (a *OllamaAgent) discoverAnalyzers(bundle *analyzer.SupportBundle) []analyz
Priority: 7,
Config: map[string]interface{}{"filePath": filePath, "promptType": "log-analysis"},
})
case strings.Contains(filePath, "nodes") && strings.HasSuffix(filePath, ".json"):
specs = append(specs, analyzer.AnalyzerSpec{
Name: "ai-resource-analysis",
Type: "ai-resources",
Category: "nodes",
Priority: 8,
Config: map[string]interface{}{"filePath": filePath, "promptType": "resource-analysis"},
})
}
}
// Create aggregated analyzer for ALL pod files (cluster-wide view)
if len(podFiles) > 0 {
specs = append(specs, analyzer.AnalyzerSpec{
Name: "ai-pod-analysis-cluster",
Type: "ai-workload",
Category: "pods",
Priority: 10,
Config: map[string]interface{}{
"filePaths": podFiles,
"promptType": "pod-analysis",
"aggregated": true,
},
})
}
// Create aggregated analyzer for ALL deployment files (cluster-wide view)
if len(deploymentFiles) > 0 {
specs = append(specs, analyzer.AnalyzerSpec{
Name: "ai-deployment-analysis-cluster",
Type: "ai-workload",
Category: "deployments",
Priority: 9,
Config: map[string]interface{}{
"filePaths": deploymentFiles,
"promptType": "deployment-analysis",
"aggregated": true,
},
})
}
// Create aggregated analyzer for ALL event files (cluster-wide view)
if len(eventFiles) > 0 {
specs = append(specs, analyzer.AnalyzerSpec{
Name: "ai-event-analysis-cluster",
Type: "ai-events",
Category: "events",
Priority: 8,
Config: map[string]interface{}{
"filePaths": eventFiles,
"promptType": "event-analysis",
"aggregated": true,
},
})
}
// Create aggregated analyzer for ALL node files (cluster-wide view)
if len(nodeFiles) > 0 {
specs = append(specs, analyzer.AnalyzerSpec{
Name: "ai-resource-analysis-cluster",
Type: "ai-resources",
Category: "nodes",
Priority: 8,
Config: map[string]interface{}{
"filePaths": nodeFiles,
"promptType": "resource-analysis",
"aggregated": true,
},
})
}
return specs
}
// aggregateFiles combines multiple files of the same type into a single summary for analysis
func (a *OllamaAgent) aggregateFiles(bundle *analyzer.SupportBundle, filePaths []string, category string) (string, error) {
var summary strings.Builder
switch category {
case "pods":
return a.aggregatePodFiles(bundle, filePaths)
case "deployments":
return a.aggregateDeploymentFiles(bundle, filePaths)
case "events":
return a.aggregateEventFiles(bundle, filePaths)
case "nodes":
return a.aggregateNodeFiles(bundle, filePaths)
default:
// For other types, just concatenate the files
summary.WriteString(fmt.Sprintf("Aggregated analysis of %d files:\n\n", len(filePaths)))
for _, filePath := range filePaths {
if data, exists := bundle.Files[filePath]; exists {
summary.WriteString(fmt.Sprintf("--- File: %s ---\n", filePath))
summary.Write(data)
summary.WriteString("\n\n")
}
}
}
return summary.String(), nil
}
// aggregatePodFiles creates a cluster-wide summary of pods from multiple namespace files
func (a *OllamaAgent) aggregatePodFiles(bundle *analyzer.SupportBundle, filePaths []string) (string, error) {
var summary strings.Builder
totalPods := 0
runningPods := 0
pendingPods := 0
failedPods := 0
succeededPods := 0
namespaceStats := make(map[string]int)
summary.WriteString("CLUSTER-WIDE POD ANALYSIS\n")
summary.WriteString("Analyzing pods across all namespaces:\n\n")
for _, filePath := range filePaths {
data, exists := bundle.Files[filePath]
if !exists {
continue
}
// Extract namespace from path (e.g., "cluster-resources/pods/kube-system.json")
parts := strings.Split(filePath, "/")
namespace := "unknown"
if len(parts) >= 3 {
namespace = strings.TrimSuffix(parts[len(parts)-1], ".json")
}
// Parse pod data - handle both PodList and single Pod objects
var podList map[string]interface{}
if err := json.Unmarshal(data, &podList); err != nil {
continue
}
// Check if this is a List object with items array
items, ok := podList["items"].([]interface{})
if ok {
// Handle PodList - process all pods in the list
// Initialize namespace for valid PodList (ensures empty namespaces are tracked)
if _, exists := namespaceStats[namespace]; !exists {
namespaceStats[namespace] = 0
}
podCount := len(items)
namespaceStats[namespace] += podCount
totalPods += podCount
// Count pod statuses
for _, item := range items {
pod, ok := item.(map[string]interface{})
if !ok {
continue
}
status, ok := pod["status"].(map[string]interface{})
if !ok {
continue
}
phase, ok := status["phase"].(string)
if !ok {
continue
}
switch phase {
case "Running":
runningPods++
case "Pending":
pendingPods++
case "Failed":
failedPods++
case "Succeeded":
succeededPods++
}
}
} else {
// Handle single Pod object (not a list)
// Check if this is a single Pod object (has "kind": "Pod")
if kind, exists := podList["kind"].(string); exists && kind == "Pod" {
// Initialize namespace only for valid pod data
if _, exists := namespaceStats[namespace]; !exists {
namespaceStats[namespace] = 0
}
// Single pod - increment count for this namespace
namespaceStats[namespace]++
totalPods++
// Extract status for single pod
if status, ok := podList["status"].(map[string]interface{}); ok {
if phase, ok := status["phase"].(string); ok {
switch phase {
case "Running":
runningPods++
case "Pending":
pendingPods++
case "Failed":
failedPods++
case "Succeeded":
succeededPods++
}
}
}
}
// Skip to next file after processing single pod or invalid data
continue
}
}
summary.WriteString(fmt.Sprintf("Total pods in cluster: %d\n", totalPods))
summary.WriteString(fmt.Sprintf(" - Running: %d\n", runningPods))
summary.WriteString(fmt.Sprintf(" - Pending: %d\n", pendingPods))
summary.WriteString(fmt.Sprintf(" - Failed: %d\n", failedPods))
summary.WriteString(fmt.Sprintf(" - Succeeded: %d\n", succeededPods))
summary.WriteString("\nPods by namespace:\n")
for namespace, count := range namespaceStats {
if count > 0 {
summary.WriteString(fmt.Sprintf(" - %s: %d pods\n", namespace, count))
} else {
summary.WriteString(fmt.Sprintf(" - %s: empty (no pods)\n", namespace))
}
}
summary.WriteString("\nIMPORTANT CONTEXT:\n")
summary.WriteString("- Empty namespaces are NORMAL in Kubernetes\n")
summary.WriteString("- Only report issues if there are actual pod failures or critical problems\n")
summary.WriteString("- The presence of empty namespaces is not a problem\n")
return summary.String(), nil
}
// aggregateDeploymentFiles creates a cluster-wide summary of deployments
func (a *OllamaAgent) aggregateDeploymentFiles(bundle *analyzer.SupportBundle, filePaths []string) (string, error) {
var summary strings.Builder
totalDeployments := 0
namespaceStats := make(map[string]int)
summary.WriteString("CLUSTER-WIDE DEPLOYMENT ANALYSIS\n")
summary.WriteString("Analyzing deployments across all namespaces:\n\n")
for _, filePath := range filePaths {
data, exists := bundle.Files[filePath]
if !exists {
continue
}
parts := strings.Split(filePath, "/")
namespace := "unknown"
if len(parts) >= 3 {
namespace = strings.TrimSuffix(parts[len(parts)-1], ".json")
}
// Parse deployment data - handle both DeploymentList and single Deployment objects
var deploymentList map[string]interface{}
if err := json.Unmarshal(data, &deploymentList); err != nil {
continue
}
// Check if this is a List object with items array
items, ok := deploymentList["items"].([]interface{})
if ok {
// Handle DeploymentList - process all deployments in the list
// Initialize namespace for valid DeploymentList (ensures empty namespaces are tracked)
if _, exists := namespaceStats[namespace]; !exists {
namespaceStats[namespace] = 0
}
deployCount := len(items)
namespaceStats[namespace] += deployCount
totalDeployments += deployCount
} else {
// Handle single Deployment object (not a list)
// Check if this is a single Deployment object (has "kind": "Deployment")
if kind, exists := deploymentList["kind"].(string); exists && kind == "Deployment" {
// Initialize namespace only for valid deployment data
if _, exists := namespaceStats[namespace]; !exists {
namespaceStats[namespace] = 0
}
// Single deployment - increment count for this namespace
namespaceStats[namespace]++
totalDeployments++
}
// Skip to next file after processing single deployment or invalid data
continue
}
}
summary.WriteString(fmt.Sprintf("Total deployments in cluster: %d\n", totalDeployments))
summary.WriteString("\nDeployments by namespace:\n")
for namespace, count := range namespaceStats {
if count > 0 {
summary.WriteString(fmt.Sprintf(" - %s: %d deployments\n", namespace, count))
} else {
summary.WriteString(fmt.Sprintf(" - %s: no deployments\n", namespace))
}
}
summary.WriteString("\nIMPORTANT: Empty namespaces are normal. Only flag actual deployment issues.\n")
return summary.String(), nil
}
// aggregateEventFiles creates a cluster-wide summary of events
func (a *OllamaAgent) aggregateEventFiles(bundle *analyzer.SupportBundle, filePaths []string) (string, error) {
var summary strings.Builder
totalEvents := 0
summary.WriteString("CLUSTER-WIDE EVENT ANALYSIS\n")
summary.WriteString("Analyzing events across all namespaces:\n\n")
eventsIncluded := 0
for _, filePath := range filePaths {
data, exists := bundle.Files[filePath]
if !exists {
continue
}
// Parse event data - handle both EventList and single Event objects
var eventList map[string]interface{}
if err := json.Unmarshal(data, &eventList); err != nil {
continue
}
// Check if this is a List object with items array
items, ok := eventList["items"].([]interface{})
if ok {
itemCount := len(items)
totalEvents += itemCount
// Include actual event data for AI analysis (limited to 50 events max for the summary)
// Only include if adding this file wouldn't significantly exceed the limit
if itemCount > 0 && eventsIncluded < 50 && (eventsIncluded+itemCount) <= 60 {
dataStr := string(data)
// Include file if data size is reasonable
if len(dataStr) < 2000 {
summary.WriteString(fmt.Sprintf("\n--- Events from %s ---\n", filePath))
summary.WriteString(dataStr)
summary.WriteString("\n")
eventsIncluded += itemCount
}
}
}
}
summary.WriteString(fmt.Sprintf("\nTotal events collected: %d\n", totalEvents))
return summary.String(), nil
}
// aggregateNodeFiles creates a cluster-wide summary of nodes
func (a *OllamaAgent) aggregateNodeFiles(bundle *analyzer.SupportBundle, filePaths []string) (string, error) {
var summary strings.Builder
summary.WriteString("CLUSTER-WIDE NODE ANALYSIS\n\n")
for _, filePath := range filePaths {
data, exists := bundle.Files[filePath]
if !exists {
continue
}
summary.WriteString(fmt.Sprintf("--- Nodes data from %s ---\n", filePath))
summary.Write(data)
summary.WriteString("\n\n")
}
return summary.String(), nil
}
// runLLMAnalysis executes analysis using LLM for a specific analyzer spec
func (a *OllamaAgent) runLLMAnalysis(ctx context.Context, bundle *analyzer.SupportBundle, spec analyzer.AnalyzerSpec) (*analyzer.AnalyzerResult, error) {
ctx, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, fmt.Sprintf("OllamaAgent.%s", spec.Name))
defer span.End()
var dataStr string
// Smart file detection for enhanced analyzer compatibility
var filePath string
var fileData []byte
var exists bool
// Check if this is an aggregated analyzer (multiple files)
if aggregated, ok := spec.Config["aggregated"].(bool); ok && aggregated {
// Handle aggregated files
if filePaths, ok := spec.Config["filePaths"].([]string); ok && len(filePaths) > 0 {
aggregatedData, err := a.aggregateFiles(bundle, filePaths, spec.Category)
if err != nil {
return &analyzer.AnalyzerResult{
Title: spec.Name,
IsWarn: true,
Message: fmt.Sprintf("Failed to aggregate files: %v", err),
Category: spec.Category,
}, nil
}
dataStr = aggregatedData
} else {
// Missing or invalid filePaths for aggregated analyzer
return &analyzer.AnalyzerResult{
Title: spec.Name,
IsWarn: true,
Message: "Aggregated analyzer missing valid filePaths configuration",
Category: spec.Category,
}, nil
// First try to get explicit filePath from config
if fp, ok := spec.Config["filePath"].(string); ok {
filePath = fp
fileData, exists = bundle.Files[filePath]
}
// If no explicit filePath, auto-detect based on analyzer type
if !exists {
filePath, fileData, exists = a.autoDetectFileForAnalyzer(bundle, spec)
}
if !exists {
result := &analyzer.AnalyzerResult{
Title: spec.Name,
IsWarn: true,
Message: fmt.Sprintf("File not found: %s", filePath),
Category: spec.Category,
}
} else {
// Smart file detection for enhanced analyzer compatibility (single file)
var filePath string
var fileData []byte
var exists bool
// First try to get explicit filePath from config
if fp, ok := spec.Config["filePath"].(string); ok {
filePath = fp
fileData, exists = bundle.Files[filePath]
}
// If no explicit filePath, auto-detect based on analyzer type
if !exists {
filePath, fileData, exists = a.autoDetectFileForAnalyzer(bundle, spec)
}
if !exists {
result := &analyzer.AnalyzerResult{
Title: spec.Name,
IsWarn: true,
Message: fmt.Sprintf("File not found: %s", filePath),
Category: spec.Category,
}
return result, nil
}
dataStr = string(fileData)
return result, nil
}
promptType, _ := spec.Config["promptType"].(string)
@@ -885,6 +519,7 @@ func (a *OllamaAgent) runLLMAnalysis(ctx context.Context, bundle *analyzer.Suppo
}
// Prepare data for analysis (truncate if too large)
dataStr := string(fileData)
if len(dataStr) > 4000 { // Limit input size
if promptType == "log-analysis" {
// For logs, take the last N lines
@@ -1231,182 +866,6 @@ func (a *OllamaAgent) autoDetectFileForAnalyzer(bundle *analyzer.SupportBundle,
return "", nil, false
}
// normalizeInsights converts various JSON formats into a []string array
func (a *OllamaAgent) normalizeInsights(raw json.RawMessage) []string {
if len(raw) == 0 {
return []string{}
}
// Try parsing as array of strings first (expected format)
var arrayInsights []string
if err := json.Unmarshal(raw, &arrayInsights); err == nil {
return arrayInsights
}
// Try parsing as single string
var stringInsight string
if err := json.Unmarshal(raw, &stringInsight); err == nil {
if stringInsight != "" {
return []string{stringInsight}
}
return []string{}
}
// Try parsing as array of objects/maps (common LLM format)
var arrayOfMaps []map[string]interface{}
if err := json.Unmarshal(raw, &arrayOfMaps); err == nil {
insights := []string{}
for _, obj := range arrayOfMaps {
// Extract meaningful text from each object
insightText := a.formatMapAsInsight(obj)
if insightText != "" {
insights = append(insights, insightText)
}
}
return insights
}
// Try parsing as object/map and extract meaningful text
var objInsights map[string]interface{}
if err := json.Unmarshal(raw, &objInsights); err == nil {
insights := []string{}
for key, value := range objInsights {
// Extract meaningful insights from object structure
insightText := a.extractInsightText(key, value)
if insightText != "" {
insights = append(insights, insightText)
}
}
return insights
}
// If all parsing fails, return empty array
return []string{}
}
// formatMapAsInsight converts a map/object into a readable insight string
func (a *OllamaAgent) formatMapAsInsight(obj map[string]interface{}) string {
// Common patterns in LLM responses for insights
// Try to extract description, pattern, message, etc.
// Priority 1: Look for description field
if desc, ok := obj["description"].(string); ok && desc != "" {
if pattern, ok := obj["pattern"].(string); ok && pattern != "" {
return fmt.Sprintf("%s: %s", pattern, desc)
}
return desc
}
// Priority 2: Look for message field
if msg, ok := obj["message"].(string); ok && msg != "" {
return msg
}
// Priority 3: Look for explanation/implication field
if expl, ok := obj["explanation"].(string); ok && expl != "" {
return expl
}
if impl, ok := obj["implication"].(string); ok && impl != "" {
return impl
}
// Priority 4: Combine all string fields
parts := []string{}
for key, value := range obj {
if str, ok := value.(string); ok && str != "" {
parts = append(parts, fmt.Sprintf("%s: %s", key, str))
}
}
if len(parts) > 0 {
return strings.Join(parts, ", ")
}
return ""
}
// extractInsightText extracts readable text from nested JSON structures
func (a *OllamaAgent) extractInsightText(key string, value interface{}) string {
switch v := value.(type) {
case string:
if v != "" {
return fmt.Sprintf("%s: %s", key, v)
}
case map[string]interface{}:
// For nested objects, create a summary
parts := []string{}
for subKey, subValue := range v {
if str, ok := subValue.(string); ok && str != "" {
parts = append(parts, fmt.Sprintf("%s=%s", subKey, str))
}
}
if len(parts) > 0 {
return fmt.Sprintf("%s: %s", key, strings.Join(parts, ", "))
}
case []interface{}:
// For arrays, join elements
parts := []string{}
for _, item := range v {
if str, ok := item.(string); ok && str != "" {
parts = append(parts, str)
}
}
if len(parts) > 0 {
return fmt.Sprintf("%s: %s", key, strings.Join(parts, ", "))
}
case float64, int, bool:
return fmt.Sprintf("%s: %v", key, v)
}
return ""
}
// getStringField extracts a string field from a map, trying multiple key variants
func (a *OllamaAgent) getStringField(m map[string]interface{}, keys ...string) string {
for _, key := range keys {
if val, ok := m[key]; ok {
if str, ok := val.(string); ok {
return str
}
}
}
return ""
}
// extractRemediation extracts remediation info from various JSON structures
func (a *OllamaAgent) extractRemediation(result *analyzer.AnalyzerResult, remData interface{}) {
switch rem := remData.(type) {
case map[string]interface{}:
// Single remediation object
desc := a.getStringField(rem, "description", "Description")
action := a.getStringField(rem, "action", "Action")
command := a.getStringField(rem, "command", "Command")
priority := 5 // default priority
if p, ok := rem["priority"].(float64); ok {
priority = int(p)
} else if p, ok := rem["Priority"].(float64); ok {
priority = int(p)
}
if desc != "" || action != "" {
result.Remediation = &analyzer.RemediationStep{
Description: desc,
Action: action,
Command: command,
Priority: priority,
Category: "ai-suggested",
IsAutomatable: false,
}
}
case []interface{}:
// Array of remediation suggestions - use the first one
if len(rem) > 0 {
if firstRem, ok := rem[0].(map[string]interface{}); ok {
a.extractRemediation(result, firstRem)
}
}
}
}
// parseLLMResponse parses the LLM response into an AnalyzerResult
func (a *OllamaAgent) parseLLMResponse(response string, spec analyzer.AnalyzerSpec) (*analyzer.AnalyzerResult, error) {
// First try JSON parsing
@@ -1416,53 +875,55 @@ func (a *OllamaAgent) parseLLMResponse(response string, spec analyzer.AnalyzerSp
if jsonStart != -1 && jsonEnd != -1 && jsonEnd > jsonStart {
jsonStr := response[jsonStart : jsonEnd+1]
// Try with a flexible map first to handle case-insensitive fields
var jsonMap map[string]interface{}
if err := json.Unmarshal([]byte(jsonStr), &jsonMap); err != nil {
var llmResult struct {
Status string `json:"status"`
Title string `json:"title"`
Message string `json:"message"`
Insights []string `json:"insights"`
Remediation struct {
Description string `json:"description"`
Action string `json:"action"`
Command string `json:"command"`
Priority int `json:"priority"`
} `json:"remediation"`
}
if err := json.Unmarshal([]byte(jsonStr), &llmResult); err == nil {
// Successfully parsed JSON
result := &analyzer.AnalyzerResult{
Title: llmResult.Title,
Message: llmResult.Message,
Category: spec.Category,
Insights: llmResult.Insights,
}
switch strings.ToLower(llmResult.Status) {
case "pass":
result.IsPass = true
case "warn":
result.IsWarn = true
case "fail":
result.IsFail = true
default:
result.IsWarn = true
}
if llmResult.Remediation.Description != "" {
result.Remediation = &analyzer.RemediationStep{
Description: llmResult.Remediation.Description,
Action: llmResult.Remediation.Action,
Command: llmResult.Remediation.Command,
Priority: llmResult.Remediation.Priority,
Category: "ai-suggested",
IsAutomatable: false,
}
}
return result, nil
} else {
// JSON was found but malformed
return nil, errors.Wrap(err, "failed to parse LLM JSON response")
}
// Extract fields in a case-insensitive way
status := a.getStringField(jsonMap, "status", "Status")
title := a.getStringField(jsonMap, "title", "Title")
message := a.getStringField(jsonMap, "message", "Message")
// Get insights field (try both lowercase and uppercase)
var insightsRaw json.RawMessage
if insights, ok := jsonMap["insights"]; ok {
insightsRaw, _ = json.Marshal(insights)
} else if insights, ok := jsonMap["Insights"]; ok {
insightsRaw, _ = json.Marshal(insights)
}
insights := a.normalizeInsights(insightsRaw)
result := &analyzer.AnalyzerResult{
Title: title,
Message: message,
Category: spec.Category,
Insights: insights,
}
switch strings.ToLower(status) {
case "pass":
result.IsPass = true
case "warn":
result.IsWarn = true
case "fail":
result.IsFail = true
default:
result.IsWarn = true
}
// Handle remediation (try both cases)
if rem, ok := jsonMap["remediation"]; ok {
a.extractRemediation(result, rem)
} else if rem, ok := jsonMap["Remediation"]; ok {
a.extractRemediation(result, rem)
}
return result, nil
}
// Fall back to markdown parsing when JSON fails
@@ -206,10 +206,7 @@ func TestOllamaAgent_discoverAnalyzers(t *testing.T) {
assert.NotNil(t, spec.Config)
// Verify AI-specific config
// Aggregated analyzers use "filePaths", single-file analyzers use "filePath"
hasFilePath := spec.Config["filePath"] != nil
hasFilePaths := spec.Config["filePaths"] != nil
assert.True(t, hasFilePath || hasFilePaths, "spec must have either filePath or filePaths")
assert.Contains(t, spec.Config, "filePath")
assert.Contains(t, spec.Config, "promptType")
}
+2 -2
View File
@@ -12,11 +12,11 @@ import (
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/kubernetes/pkg/util/taints"
"github.com/replicatedhq/troubleshoot/internal/util"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/constants"
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
)
type AnalyzeNodeResources struct {
@@ -453,7 +453,7 @@ func nodeMatchesFilters(node corev1.Node, filters *troubleshootv1beta2.NodeResou
}
if filters.Taint != nil {
return k8sutil.TaintExists(node.Spec.Taints, filters.Taint), nil
return taints.TaintExists(node.Spec.Taints, filters.Taint), nil
}
if filters.CPUArchitecture != "" {
+1 -8
View File
@@ -146,7 +146,7 @@ func (h *OllamaHelper) downloadAndInstallWindows() error {
return errors.Wrap(err, "failed to create temporary file")
}
defer os.Remove(tmpFile.Name())
defer tmpFile.Close() // Ensures file is closed in error paths
defer tmpFile.Close()
// Download installer
resp, err := http.Get(h.downloadURL)
@@ -165,13 +165,6 @@ func (h *OllamaHelper) downloadAndInstallWindows() error {
return errors.Wrap(err, "failed to write installer")
}
// Close the file before executing it (required on Windows)
// Note: This will be called twice (here and via defer), but that's safe
// The defer ensures cleanup on error paths, this ensures closure before execution
if err := tmpFile.Close(); err != nil {
return errors.Wrap(err, "failed to close installer file")
}
// Run installer
klog.Info("Running Ollama installer...")
cmd := exec.Command(tmpFile.Name())
@@ -1,78 +0,0 @@
package v1beta3
import (
"github.com/replicatedhq/troubleshoot/pkg/multitype"
)
// CollectorMeta contains metadata for collectors
type CollectorMeta struct {
CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
// +optional
Exclude *multitype.BoolOrString `json:"exclude,omitempty" yaml:"exclude,omitempty"`
}
// Database represents database collectors (PostgreSQL, MySQL, Redis, MSSQL)
// In v1beta3, URI and TLS fields support valueFrom references
type Database struct {
CollectorMeta `json:",inline" yaml:",inline"`
// URI can be a literal value or reference to a Secret/ConfigMap
URI StringOrValueFrom `json:"uri" yaml:"uri"`
// Parameters for the database connection
Parameters []string `json:"parameters,omitempty"`
// TLS configuration with support for valueFrom references
TLS *TLSParams `json:"tls,omitempty" yaml:"tls,omitempty"`
}
// TLSParams contains TLS configuration
// In v1beta3, certificate fields support valueFrom references
type TLSParams struct {
// SkipVerify disables TLS verification
SkipVerify bool `json:"skipVerify,omitempty" yaml:"skipVerify,omitempty"`
// Secret references a Kubernetes Secret containing TLS materials (v1beta2 compatibility)
Secret *TLSSecret `json:"secret,omitempty" yaml:"secret,omitempty"`
// CACert can be a literal value or reference to a Secret/ConfigMap
CACert StringOrValueFrom `json:"cacert,omitempty" yaml:"cacert,omitempty"`
// ClientCert can be a literal value or reference to a Secret/ConfigMap
ClientCert StringOrValueFrom `json:"clientCert,omitempty" yaml:"clientCert,omitempty"`
// ClientKey can be a literal value or reference to a Secret/ConfigMap
ClientKey StringOrValueFrom `json:"clientKey,omitempty" yaml:"clientKey,omitempty"`
}
// TLSSecret references a Kubernetes Secret containing TLS materials
// Maintained for backward compatibility
type TLSSecret struct {
Name string `json:"name" yaml:"name"`
Namespace string `json:"namespace" yaml:"namespace"`
}
// Temporary placeholder types for minimal v1beta3 implementation
// These will be properly defined as we expand v1beta3 support
type AfterCollection struct {
CollectorMeta `json:",inline" yaml:",inline"`
// TODO: Add fields as needed
}
type Analyze struct {
// TODO: Add fields as needed
}
type HostAnalyze struct {
// TODO: Add fields as needed
}
type HostCollect struct {
// TODO: Add fields as needed
}
// Collect contains all collector definitions
// For phase 1, we're focusing on Database collectors with StringOrValueFrom support
type Collect struct {
// Database collectors with v1beta3 StringOrValueFrom support
Postgres *Database `json:"postgres,omitempty" yaml:"postgres,omitempty"`
Mssql *Database `json:"mssql,omitempty" yaml:"mssql,omitempty"`
Mysql *Database `json:"mysql,omitempty" yaml:"mysql,omitempty"`
Redis *Database `json:"redis,omitempty" yaml:"redis,omitempty"`
// TODO: Add remaining collector types as we expand v1beta3 support
// For now, these are placeholders to make the types compile
}
-162
View File
@@ -1,162 +0,0 @@
package v1beta3
import (
"context"
"fmt"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"k8s.io/client-go/kubernetes"
)
// ConvertToV1Beta2WithResolution converts a v1beta3 SupportBundleSpec to v1beta2
// by resolving all StringOrValueFrom fields to their actual values
func ConvertToV1Beta2WithResolution(
ctx context.Context,
v3spec *SupportBundleSpec,
client kubernetes.Interface,
defaultNamespace string,
) (*troubleshootv1beta2.SupportBundleSpec, error) {
v2spec := &troubleshootv1beta2.SupportBundleSpec{
Uri: v3spec.Uri,
RunHostCollectorsInPod: v3spec.RunHostCollectorsInPod,
}
// Convert collectors
if v3spec.Collectors != nil {
v2collectors := make([]*troubleshootv1beta2.Collect, 0, len(v3spec.Collectors))
for _, v3collector := range v3spec.Collectors {
v2collector, err := convertCollector(ctx, v3collector, client, defaultNamespace)
if err != nil {
return nil, fmt.Errorf("failed to convert collector: %w", err)
}
v2collectors = append(v2collectors, v2collector)
}
v2spec.Collectors = v2collectors
}
// TODO: Convert AfterCollection, HostCollectors, Analyzers, HostAnalyzers when v1beta3 support is expanded
return v2spec, nil
}
// convertCollector converts a v1beta3 Collect to v1beta2 Collect
func convertCollector(
ctx context.Context,
v3collector *Collect,
client kubernetes.Interface,
defaultNamespace string,
) (*troubleshootv1beta2.Collect, error) {
v2collector := &troubleshootv1beta2.Collect{}
// Convert database collectors
if v3collector.Postgres != nil {
db, err := convertDatabase(ctx, v3collector.Postgres, client, defaultNamespace)
if err != nil {
return nil, fmt.Errorf("failed to convert postgres collector: %w", err)
}
v2collector.Postgres = db
}
if v3collector.Mysql != nil {
db, err := convertDatabase(ctx, v3collector.Mysql, client, defaultNamespace)
if err != nil {
return nil, fmt.Errorf("failed to convert mysql collector: %w", err)
}
v2collector.Mysql = db
}
if v3collector.Mssql != nil {
db, err := convertDatabase(ctx, v3collector.Mssql, client, defaultNamespace)
if err != nil {
return nil, fmt.Errorf("failed to convert mssql collector: %w", err)
}
v2collector.Mssql = db
}
if v3collector.Redis != nil {
db, err := convertDatabase(ctx, v3collector.Redis, client, defaultNamespace)
if err != nil {
return nil, fmt.Errorf("failed to convert redis collector: %w", err)
}
v2collector.Redis = db
}
// TODO: Add conversion for other collector types as v1beta3 support expands
return v2collector, nil
}
// convertDatabase converts a v1beta3 Database to v1beta2 Database
func convertDatabase(
ctx context.Context,
v3db *Database,
client kubernetes.Interface,
defaultNamespace string,
) (*troubleshootv1beta2.Database, error) {
// Resolve URI
uri, err := ResolveStringOrValueFrom(ctx, v3db.URI, client, defaultNamespace)
if err != nil {
return nil, fmt.Errorf("failed to resolve database URI: %w", err)
}
v2db := &troubleshootv1beta2.Database{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: v3db.CollectorName,
Exclude: v3db.Exclude,
},
URI: uri,
Parameters: v3db.Parameters,
}
// Convert TLS params if present
if v3db.TLS != nil {
tlsParams, err := convertTLSParams(ctx, v3db.TLS, client, defaultNamespace)
if err != nil {
return nil, fmt.Errorf("failed to convert TLS params: %w", err)
}
v2db.TLS = tlsParams
}
return v2db, nil
}
// convertTLSParams converts v1beta3 TLSParams to v1beta2 TLSParams
func convertTLSParams(
ctx context.Context,
v3tls *TLSParams,
client kubernetes.Interface,
defaultNamespace string,
) (*troubleshootv1beta2.TLSParams, error) {
v2tls := &troubleshootv1beta2.TLSParams{
SkipVerify: v3tls.SkipVerify,
}
// Preserve v1beta2 Secret reference if present (backward compatibility)
if v3tls.Secret != nil {
v2tls.Secret = &troubleshootv1beta2.TLSSecret{
Name: v3tls.Secret.Name,
Namespace: v3tls.Secret.Namespace,
}
}
// Resolve v1beta3 StringOrValueFrom fields
caCert, err := ResolveStringOrValueFrom(ctx, v3tls.CACert, client, defaultNamespace)
if err != nil {
return nil, fmt.Errorf("failed to resolve CA cert: %w", err)
}
v2tls.CACert = caCert
clientCert, err := ResolveStringOrValueFrom(ctx, v3tls.ClientCert, client, defaultNamespace)
if err != nil {
return nil, fmt.Errorf("failed to resolve client cert: %w", err)
}
v2tls.ClientCert = clientCert
clientKey, err := ResolveStringOrValueFrom(ctx, v3tls.ClientKey, client, defaultNamespace)
if err != nil {
return nil, fmt.Errorf("failed to resolve client key: %w", err)
}
v2tls.ClientKey = clientKey
return v2tls, nil
}
@@ -1,386 +0,0 @@
package v1beta3
import (
"context"
"testing"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)
func TestConvertToV1Beta2WithResolution_PostgresWithLiteralValue(t *testing.T) {
client := fake.NewSimpleClientset()
uri := "postgresql://user:pass@localhost:5432/db"
v3spec := &SupportBundleSpec{
Collectors: []*Collect{
{
Postgres: &Database{
URI: StringOrValueFrom{
Value: &uri,
},
},
},
},
}
v2spec, err := ConvertToV1Beta2WithResolution(context.Background(), v3spec, client, "default")
require.NoError(t, err)
require.NotNil(t, v2spec)
require.Len(t, v2spec.Collectors, 1)
require.NotNil(t, v2spec.Collectors[0].Postgres)
assert.Equal(t, "postgresql://user:pass@localhost:5432/db", v2spec.Collectors[0].Postgres.URI)
}
func TestConvertToV1Beta2WithResolution_PostgresWithSecretRef(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "db-secret",
Namespace: "default",
},
Data: map[string][]byte{
"uri": []byte("postgresql://user:secret-pass@db.example.com:5432/mydb"),
},
}
client := fake.NewSimpleClientset(secret)
v3spec := &SupportBundleSpec{
Collectors: []*Collect{
{
Postgres: &Database{
URI: StringOrValueFrom{
ValueFrom: &ValueFromSource{
SecretKeyRef: &SecretKeyRef{
Name: "db-secret",
Key: "uri",
},
},
},
},
},
},
}
v2spec, err := ConvertToV1Beta2WithResolution(context.Background(), v3spec, client, "default")
require.NoError(t, err)
require.NotNil(t, v2spec)
require.Len(t, v2spec.Collectors, 1)
require.NotNil(t, v2spec.Collectors[0].Postgres)
assert.Equal(t, "postgresql://user:secret-pass@db.example.com:5432/mydb", v2spec.Collectors[0].Postgres.URI)
}
func TestConvertToV1Beta2WithResolution_PostgresWithTLS(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "tls-secret",
Namespace: "default",
},
Data: map[string][]byte{
"ca.crt": []byte("-----BEGIN CERTIFICATE-----\nCA_CERT_DATA\n-----END CERTIFICATE-----"),
"client.crt": []byte("-----BEGIN CERTIFICATE-----\nCLIENT_CERT_DATA\n-----END CERTIFICATE-----"),
"client.key": []byte("-----BEGIN PRIVATE KEY-----\nCLIENT_KEY_DATA\n-----END PRIVATE KEY-----"),
},
}
client := fake.NewSimpleClientset(secret)
uri := "postgresql://user:pass@localhost:5432/db"
v3spec := &SupportBundleSpec{
Collectors: []*Collect{
{
Postgres: &Database{
URI: StringOrValueFrom{
Value: &uri,
},
TLS: &TLSParams{
CACert: StringOrValueFrom{
ValueFrom: &ValueFromSource{
SecretKeyRef: &SecretKeyRef{
Name: "tls-secret",
Key: "ca.crt",
},
},
},
ClientCert: StringOrValueFrom{
ValueFrom: &ValueFromSource{
SecretKeyRef: &SecretKeyRef{
Name: "tls-secret",
Key: "client.crt",
},
},
},
ClientKey: StringOrValueFrom{
ValueFrom: &ValueFromSource{
SecretKeyRef: &SecretKeyRef{
Name: "tls-secret",
Key: "client.key",
},
},
},
},
},
},
},
}
v2spec, err := ConvertToV1Beta2WithResolution(context.Background(), v3spec, client, "default")
require.NoError(t, err)
require.NotNil(t, v2spec)
require.Len(t, v2spec.Collectors, 1)
require.NotNil(t, v2spec.Collectors[0].Postgres)
require.NotNil(t, v2spec.Collectors[0].Postgres.TLS)
assert.Equal(t, "-----BEGIN CERTIFICATE-----\nCA_CERT_DATA\n-----END CERTIFICATE-----", v2spec.Collectors[0].Postgres.TLS.CACert)
assert.Equal(t, "-----BEGIN CERTIFICATE-----\nCLIENT_CERT_DATA\n-----END CERTIFICATE-----", v2spec.Collectors[0].Postgres.TLS.ClientCert)
assert.Equal(t, "-----BEGIN PRIVATE KEY-----\nCLIENT_KEY_DATA\n-----END PRIVATE KEY-----", v2spec.Collectors[0].Postgres.TLS.ClientKey)
}
func TestConvertToV1Beta2WithResolution_MultipleDatabases(t *testing.T) {
pgSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "postgres-secret",
Namespace: "default",
},
Data: map[string][]byte{
"uri": []byte("postgresql://user:pass@pg.example.com:5432/db"),
},
}
mysqlSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "mysql-secret",
Namespace: "default",
},
Data: map[string][]byte{
"uri": []byte("mysql://user:pass@mysql.example.com:3306/db"),
},
}
redisSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "redis-secret",
Namespace: "default",
},
Data: map[string][]byte{
"uri": []byte("redis://redis.example.com:6379"),
},
}
client := fake.NewSimpleClientset(pgSecret, mysqlSecret, redisSecret)
v3spec := &SupportBundleSpec{
Collectors: []*Collect{
{
Postgres: &Database{
URI: StringOrValueFrom{
ValueFrom: &ValueFromSource{
SecretKeyRef: &SecretKeyRef{
Name: "postgres-secret",
Key: "uri",
},
},
},
},
},
{
Mysql: &Database{
URI: StringOrValueFrom{
ValueFrom: &ValueFromSource{
SecretKeyRef: &SecretKeyRef{
Name: "mysql-secret",
Key: "uri",
},
},
},
},
},
{
Redis: &Database{
URI: StringOrValueFrom{
ValueFrom: &ValueFromSource{
SecretKeyRef: &SecretKeyRef{
Name: "redis-secret",
Key: "uri",
},
},
},
},
},
},
}
v2spec, err := ConvertToV1Beta2WithResolution(context.Background(), v3spec, client, "default")
require.NoError(t, err)
require.NotNil(t, v2spec)
require.Len(t, v2spec.Collectors, 3)
require.NotNil(t, v2spec.Collectors[0].Postgres)
assert.Equal(t, "postgresql://user:pass@pg.example.com:5432/db", v2spec.Collectors[0].Postgres.URI)
require.NotNil(t, v2spec.Collectors[1].Mysql)
assert.Equal(t, "mysql://user:pass@mysql.example.com:3306/db", v2spec.Collectors[1].Mysql.URI)
require.NotNil(t, v2spec.Collectors[2].Redis)
assert.Equal(t, "redis://redis.example.com:6379", v2spec.Collectors[2].Redis.URI)
}
func TestConvertToV1Beta2WithResolution_SecretNotFound(t *testing.T) {
client := fake.NewSimpleClientset()
v3spec := &SupportBundleSpec{
Collectors: []*Collect{
{
Postgres: &Database{
URI: StringOrValueFrom{
ValueFrom: &ValueFromSource{
SecretKeyRef: &SecretKeyRef{
Name: "nonexistent-secret",
Key: "uri",
},
},
},
},
},
},
}
_, err := ConvertToV1Beta2WithResolution(context.Background(), v3spec, client, "default")
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to convert collector")
assert.Contains(t, err.Error(), "failed to resolve database URI")
}
func TestConvertToV1Beta2WithResolution_PreservesCollectorMeta(t *testing.T) {
client := fake.NewSimpleClientset()
uri := "postgresql://user:pass@localhost:5432/db"
v3spec := &SupportBundleSpec{
Collectors: []*Collect{
{
Postgres: &Database{
CollectorMeta: CollectorMeta{
CollectorName: "my-postgres-collector",
},
URI: StringOrValueFrom{
Value: &uri,
},
Parameters: []string{"sslmode=require"},
},
},
},
}
v2spec, err := ConvertToV1Beta2WithResolution(context.Background(), v3spec, client, "default")
require.NoError(t, err)
require.NotNil(t, v2spec)
require.Len(t, v2spec.Collectors, 1)
require.NotNil(t, v2spec.Collectors[0].Postgres)
assert.Equal(t, "my-postgres-collector", v2spec.Collectors[0].Postgres.CollectorName)
assert.Equal(t, []string{"sslmode=require"}, v2spec.Collectors[0].Postgres.Parameters)
}
func TestConvertToV1Beta2WithResolution_TLSBackwardCompatibility(t *testing.T) {
client := fake.NewSimpleClientset()
uri := "postgresql://user:pass@localhost:5432/db"
v3spec := &SupportBundleSpec{
Collectors: []*Collect{
{
Postgres: &Database{
URI: StringOrValueFrom{
Value: &uri,
},
TLS: &TLSParams{
SkipVerify: true,
Secret: &TLSSecret{
Name: "old-tls-secret",
Namespace: "default",
},
},
},
},
},
}
v2spec, err := ConvertToV1Beta2WithResolution(context.Background(), v3spec, client, "default")
require.NoError(t, err)
require.NotNil(t, v2spec)
require.Len(t, v2spec.Collectors, 1)
require.NotNil(t, v2spec.Collectors[0].Postgres)
require.NotNil(t, v2spec.Collectors[0].Postgres.TLS)
assert.True(t, v2spec.Collectors[0].Postgres.TLS.SkipVerify)
require.NotNil(t, v2spec.Collectors[0].Postgres.TLS.Secret)
assert.Equal(t, "old-tls-secret", v2spec.Collectors[0].Postgres.TLS.Secret.Name)
assert.Equal(t, "default", v2spec.Collectors[0].Postgres.TLS.Secret.Namespace)
}
func TestConvertToV1Beta2WithResolution_EmptySpec(t *testing.T) {
client := fake.NewSimpleClientset()
v3spec := &SupportBundleSpec{}
v2spec, err := ConvertToV1Beta2WithResolution(context.Background(), v3spec, client, "default")
require.NoError(t, err)
require.NotNil(t, v2spec)
assert.Nil(t, v2spec.Collectors)
}
func TestConvertDatabase_AllDatabaseTypes(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "db-secret",
Namespace: "default",
},
Data: map[string][]byte{
"uri": []byte("test-uri"),
},
}
client := fake.NewSimpleClientset(secret)
v3db := &Database{
URI: StringOrValueFrom{
ValueFrom: &ValueFromSource{
SecretKeyRef: &SecretKeyRef{
Name: "db-secret",
Key: "uri",
},
},
},
}
// Test that the same database struct works for all DB types
ctx := context.Background()
pgDB, err := convertDatabase(ctx, v3db, client, "default")
require.NoError(t, err)
assert.Equal(t, "test-uri", pgDB.URI)
mysqlDB, err := convertDatabase(ctx, v3db, client, "default")
require.NoError(t, err)
assert.Equal(t, "test-uri", mysqlDB.URI)
mssqlDB, err := convertDatabase(ctx, v3db, client, "default")
require.NoError(t, err)
assert.Equal(t, "test-uri", mssqlDB.URI)
redisDB, err := convertDatabase(ctx, v3db, client, "default")
require.NoError(t, err)
assert.Equal(t, "test-uri", redisDB.URI)
}
// Helper function to convert v2spec back to ensure type compatibility
func ensureV2SpecCompatibility(v2spec *troubleshootv1beta2.SupportBundleSpec) {
// This function just exists to ensure the types are compatible
// If this compiles, we know the conversion produces valid v1beta2 types
_ = v2spec.Uri
_ = v2spec.Collectors
_ = v2spec.Analyzers
}
-6
View File
@@ -1,6 +0,0 @@
// +k8s:deepcopy-gen=package
// +k8s:defaulter-gen=TypeMeta
// +groupName=troubleshoot.sh
// Package v1beta3 is the v1beta3 version of the API.
package v1beta3
-46
View File
@@ -1,46 +0,0 @@
/*
Copyright 2019 Replicated, Inc..
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// NOTE: Boilerplate only. Ignore this file.
// Package v1beta3 contains API Schema definitions for the troubleshoot v1beta3 API group
// +k8s:openapi-gen=true
// +k8s:deepcopy-gen=package,register
// +k8s:conversion-gen=github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot
// +k8s:defaulter-gen=TypeMeta
// +groupName=troubleshoot.sh
package v1beta3
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)
var (
// SchemeGroupVersion is group version used to register these objects
SchemeGroupVersion = schema.GroupVersion{Group: "troubleshoot.sh", Version: "v1beta3"}
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}
// AddToScheme is required by pkg/client/...
AddToScheme = SchemeBuilder.AddToScheme
)
// Resource is required by pkg/client/listers/...
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}
-117
View File
@@ -1,117 +0,0 @@
package v1beta3
import (
"context"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
// ResolveStringOrValueFrom resolves a StringOrValueFrom to its actual string value
// by fetching from Secrets or ConfigMaps as needed.
//
// Parameters:
// - ctx: Context for the resolution operation
// - sov: The StringOrValueFrom to resolve
// - client: Kubernetes client for fetching Secrets/ConfigMaps
// - defaultNamespace: Namespace to use when not specified in the reference
//
// Returns:
// - The resolved string value
// - An error if resolution fails (unless Optional is true)
func ResolveStringOrValueFrom(
ctx context.Context,
sov StringOrValueFrom,
client kubernetes.Interface,
defaultNamespace string,
) (string, error) {
// If Value is directly specified, use it
if sov.Value != nil {
return *sov.Value, nil
}
// If ValueFrom is not specified, return empty string
if sov.ValueFrom == nil {
return "", nil
}
// Resolve from SecretKeyRef
if sov.ValueFrom.SecretKeyRef != nil {
return resolveSecretKeyRef(ctx, sov.ValueFrom.SecretKeyRef, client, defaultNamespace)
}
// Resolve from ConfigMapKeyRef
if sov.ValueFrom.ConfigMapKeyRef != nil {
return resolveConfigMapKeyRef(ctx, sov.ValueFrom.ConfigMapKeyRef, client, defaultNamespace)
}
return "", nil
}
// resolveSecretKeyRef fetches a value from a Kubernetes Secret
func resolveSecretKeyRef(
ctx context.Context,
ref *SecretKeyRef,
client kubernetes.Interface,
defaultNamespace string,
) (string, error) {
namespace := ref.Namespace
if namespace == "" {
namespace = defaultNamespace
}
secret, err := client.CoreV1().Secrets(namespace).Get(ctx, ref.Name, metav1.GetOptions{})
if err != nil {
if isOptional(ref.Optional) {
return "", nil
}
return "", fmt.Errorf("failed to get secret %s/%s: %w", namespace, ref.Name, err)
}
value, ok := secret.Data[ref.Key]
if !ok {
if isOptional(ref.Optional) {
return "", nil
}
return "", fmt.Errorf("key %q not found in secret %s/%s", ref.Key, namespace, ref.Name)
}
return string(value), nil
}
// resolveConfigMapKeyRef fetches a value from a Kubernetes ConfigMap
func resolveConfigMapKeyRef(
ctx context.Context,
ref *ConfigMapKeyRef,
client kubernetes.Interface,
defaultNamespace string,
) (string, error) {
namespace := ref.Namespace
if namespace == "" {
namespace = defaultNamespace
}
configMap, err := client.CoreV1().ConfigMaps(namespace).Get(ctx, ref.Name, metav1.GetOptions{})
if err != nil {
if isOptional(ref.Optional) {
return "", nil
}
return "", fmt.Errorf("failed to get configmap %s/%s: %w", namespace, ref.Name, err)
}
value, ok := configMap.Data[ref.Key]
if !ok {
if isOptional(ref.Optional) {
return "", nil
}
return "", fmt.Errorf("key %q not found in configmap %s/%s", ref.Key, namespace, ref.Name)
}
return value, nil
}
// isOptional checks if the optional flag is set to true
func isOptional(optional *bool) bool {
return optional != nil && *optional
}
@@ -1,333 +0,0 @@
package v1beta3
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)
func TestResolveStringOrValueFrom_LiteralValue(t *testing.T) {
client := fake.NewSimpleClientset()
value := "literal-value"
sov := StringOrValueFrom{
Value: &value,
}
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
require.NoError(t, err)
assert.Equal(t, "literal-value", result)
}
func TestResolveStringOrValueFrom_EmptyValue(t *testing.T) {
client := fake.NewSimpleClientset()
sov := StringOrValueFrom{}
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
require.NoError(t, err)
assert.Equal(t, "", result)
}
func TestResolveStringOrValueFrom_SecretKeyRef(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "default",
},
Data: map[string][]byte{
"password": []byte("super-secret-password"),
},
}
client := fake.NewSimpleClientset(secret)
sov := StringOrValueFrom{
ValueFrom: &ValueFromSource{
SecretKeyRef: &SecretKeyRef{
Name: "test-secret",
Key: "password",
},
},
}
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
require.NoError(t, err)
assert.Equal(t, "super-secret-password", result)
}
func TestResolveStringOrValueFrom_SecretKeyRef_WithNamespace(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "custom-namespace",
},
Data: map[string][]byte{
"password": []byte("secret-from-custom-ns"),
},
}
client := fake.NewSimpleClientset(secret)
sov := StringOrValueFrom{
ValueFrom: &ValueFromSource{
SecretKeyRef: &SecretKeyRef{
Name: "test-secret",
Key: "password",
Namespace: "custom-namespace",
},
},
}
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
require.NoError(t, err)
assert.Equal(t, "secret-from-custom-ns", result)
}
func TestResolveStringOrValueFrom_SecretKeyRef_NotFound(t *testing.T) {
client := fake.NewSimpleClientset()
sov := StringOrValueFrom{
ValueFrom: &ValueFromSource{
SecretKeyRef: &SecretKeyRef{
Name: "nonexistent-secret",
Key: "password",
},
},
}
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to get secret")
assert.Equal(t, "", result)
}
func TestResolveStringOrValueFrom_SecretKeyRef_KeyNotFound(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "default",
},
Data: map[string][]byte{
"password": []byte("secret-value"),
},
}
client := fake.NewSimpleClientset(secret)
sov := StringOrValueFrom{
ValueFrom: &ValueFromSource{
SecretKeyRef: &SecretKeyRef{
Name: "test-secret",
Key: "nonexistent-key",
},
},
}
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
require.Error(t, err)
assert.Contains(t, err.Error(), "key \"nonexistent-key\" not found")
assert.Equal(t, "", result)
}
func TestResolveStringOrValueFrom_SecretKeyRef_Optional(t *testing.T) {
client := fake.NewSimpleClientset()
optional := true
sov := StringOrValueFrom{
ValueFrom: &ValueFromSource{
SecretKeyRef: &SecretKeyRef{
Name: "nonexistent-secret",
Key: "password",
Optional: &optional,
},
},
}
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
require.NoError(t, err)
assert.Equal(t, "", result)
}
func TestResolveStringOrValueFrom_SecretKeyRef_OptionalKeyNotFound(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "default",
},
Data: map[string][]byte{
"password": []byte("secret-value"),
},
}
client := fake.NewSimpleClientset(secret)
optional := true
sov := StringOrValueFrom{
ValueFrom: &ValueFromSource{
SecretKeyRef: &SecretKeyRef{
Name: "test-secret",
Key: "nonexistent-key",
Optional: &optional,
},
},
}
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
require.NoError(t, err)
assert.Equal(t, "", result)
}
func TestResolveStringOrValueFrom_ConfigMapKeyRef(t *testing.T) {
configMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-configmap",
Namespace: "default",
},
Data: map[string]string{
"config-key": "config-value",
},
}
client := fake.NewSimpleClientset(configMap)
sov := StringOrValueFrom{
ValueFrom: &ValueFromSource{
ConfigMapKeyRef: &ConfigMapKeyRef{
Name: "test-configmap",
Key: "config-key",
},
},
}
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
require.NoError(t, err)
assert.Equal(t, "config-value", result)
}
func TestResolveStringOrValueFrom_ConfigMapKeyRef_WithNamespace(t *testing.T) {
configMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-configmap",
Namespace: "custom-namespace",
},
Data: map[string]string{
"config-key": "config-from-custom-ns",
},
}
client := fake.NewSimpleClientset(configMap)
sov := StringOrValueFrom{
ValueFrom: &ValueFromSource{
ConfigMapKeyRef: &ConfigMapKeyRef{
Name: "test-configmap",
Key: "config-key",
Namespace: "custom-namespace",
},
},
}
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
require.NoError(t, err)
assert.Equal(t, "config-from-custom-ns", result)
}
func TestResolveStringOrValueFrom_ConfigMapKeyRef_NotFound(t *testing.T) {
client := fake.NewSimpleClientset()
sov := StringOrValueFrom{
ValueFrom: &ValueFromSource{
ConfigMapKeyRef: &ConfigMapKeyRef{
Name: "nonexistent-configmap",
Key: "config-key",
},
},
}
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to get configmap")
assert.Equal(t, "", result)
}
func TestResolveStringOrValueFrom_ConfigMapKeyRef_KeyNotFound(t *testing.T) {
configMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-configmap",
Namespace: "default",
},
Data: map[string]string{
"config-key": "config-value",
},
}
client := fake.NewSimpleClientset(configMap)
sov := StringOrValueFrom{
ValueFrom: &ValueFromSource{
ConfigMapKeyRef: &ConfigMapKeyRef{
Name: "test-configmap",
Key: "nonexistent-key",
},
},
}
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
require.Error(t, err)
assert.Contains(t, err.Error(), "key \"nonexistent-key\" not found")
assert.Equal(t, "", result)
}
func TestResolveStringOrValueFrom_ConfigMapKeyRef_Optional(t *testing.T) {
client := fake.NewSimpleClientset()
optional := true
sov := StringOrValueFrom{
ValueFrom: &ValueFromSource{
ConfigMapKeyRef: &ConfigMapKeyRef{
Name: "nonexistent-configmap",
Key: "config-key",
Optional: &optional,
},
},
}
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
require.NoError(t, err)
assert.Equal(t, "", result)
}
func TestResolveStringOrValueFrom_ConfigMapKeyRef_OptionalKeyNotFound(t *testing.T) {
configMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-configmap",
Namespace: "default",
},
Data: map[string]string{
"config-key": "config-value",
},
}
client := fake.NewSimpleClientset(configMap)
optional := true
sov := StringOrValueFrom{
ValueFrom: &ValueFromSource{
ConfigMapKeyRef: &ConfigMapKeyRef{
Name: "test-configmap",
Key: "nonexistent-key",
Optional: &optional,
},
},
}
result, err := ResolveStringOrValueFrom(context.Background(), sov, client, "default")
require.NoError(t, err)
assert.Equal(t, "", result)
}
@@ -1,64 +0,0 @@
package v1beta3
// StringOrValueFrom represents a string value that can either be specified
// directly or sourced from a Kubernetes Secret or ConfigMap
type StringOrValueFrom struct {
// Value is a literal string value
// +optional
Value *string `json:"value,omitempty" yaml:"value,omitempty"`
// ValueFrom is a reference to a value in a Secret or ConfigMap
// +optional
ValueFrom *ValueFromSource `json:"valueFrom,omitempty" yaml:"valueFrom,omitempty"`
}
// ValueFromSource represents the source of a value from a Secret or ConfigMap
type ValueFromSource struct {
// SecretKeyRef references a key in a Secret
// +optional
SecretKeyRef *SecretKeyRef `json:"secretKeyRef,omitempty" yaml:"secretKeyRef,omitempty"`
// ConfigMapKeyRef references a key in a ConfigMap
// +optional
ConfigMapKeyRef *ConfigMapKeyRef `json:"configMapKeyRef,omitempty" yaml:"configMapKeyRef,omitempty"`
}
// SecretKeyRef references a specific key in a Kubernetes Secret
type SecretKeyRef struct {
// Name is the name of the Secret
Name string `json:"name" yaml:"name"`
// Key is the key within the Secret to read
Key string `json:"key" yaml:"key"`
// Namespace is the namespace of the Secret
// If not specified, defaults to the namespace where the SupportBundle is running
// +optional
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
// Optional specifies whether the Secret must exist
// If true and the Secret or key doesn't exist, resolves to empty string
// If false (default) and the Secret or key doesn't exist, resolution fails
// +optional
Optional *bool `json:"optional,omitempty" yaml:"optional,omitempty"`
}
// ConfigMapKeyRef references a specific key in a Kubernetes ConfigMap
type ConfigMapKeyRef struct {
// Name is the name of the ConfigMap
Name string `json:"name" yaml:"name"`
// Key is the key within the ConfigMap to read
Key string `json:"key" yaml:"key"`
// Namespace is the namespace of the ConfigMap
// If not specified, defaults to the namespace where the SupportBundle is running
// +optional
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
// Optional specifies whether the ConfigMap must exist
// If true and the ConfigMap or key doesn't exist, resolves to empty string
// If false (default) and the ConfigMap or key doesn't exist, resolution fails
// +optional
Optional *bool `json:"optional,omitempty" yaml:"optional,omitempty"`
}
@@ -1,65 +0,0 @@
/*
Copyright 2019 Replicated, Inc..
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1beta3
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// SupportBundleSpec defines the desired state of SupportBundle
type SupportBundleSpec struct {
AfterCollection []*AfterCollection `json:"afterCollection,omitempty" yaml:"afterCollection,omitempty"`
Collectors []*Collect `json:"collectors,omitempty" yaml:"collectors,omitempty"`
HostCollectors []*HostCollect `json:"hostCollectors,omitempty" yaml:"hostCollectors,omitempty"`
Analyzers []*Analyze `json:"analyzers,omitempty" yaml:"analyzers,omitempty"`
HostAnalyzers []*HostAnalyze `json:"hostAnalyzers,omitempty" yaml:"hostAnalyzers,omitempty"`
// URI optionally defines a location which is the source of this spec to allow updating of the spec at runtime
Uri string `json:"uri,omitempty" yaml:"uri,omitempty"`
RunHostCollectorsInPod bool `json:"runHostCollectorsInPod,omitempty" yaml:"runHostCollectorsInPod,omitempty"`
}
// SupportBundleStatus defines the observed state of SupportBundle
type SupportBundleStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
}
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// SupportBundle is the Schema for the SupportBundles API
// +k8s:openapi-gen=true
type SupportBundle struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"`
Spec SupportBundleSpec `json:"spec,omitempty" yaml:"spec,omitempty"`
Status SupportBundleStatus `json:"status,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// SupportBundleList contains a list of SupportBundle
type SupportBundleList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []SupportBundle `json:"items"`
}
func init() {
SchemeBuilder.Register(&SupportBundle{}, &SupportBundleList{})
}
@@ -1,441 +0,0 @@
//go:build !ignore_autogenerated
/*
Copyright 2019 Replicated, Inc..
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by controller-gen. DO NOT EDIT.
package v1beta3
import (
"github.com/replicatedhq/troubleshoot/pkg/multitype"
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AfterCollection) DeepCopyInto(out *AfterCollection) {
*out = *in
in.CollectorMeta.DeepCopyInto(&out.CollectorMeta)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AfterCollection.
func (in *AfterCollection) DeepCopy() *AfterCollection {
if in == nil {
return nil
}
out := new(AfterCollection)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Analyze) DeepCopyInto(out *Analyze) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Analyze.
func (in *Analyze) DeepCopy() *Analyze {
if in == nil {
return nil
}
out := new(Analyze)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Collect) DeepCopyInto(out *Collect) {
*out = *in
if in.Postgres != nil {
in, out := &in.Postgres, &out.Postgres
*out = new(Database)
(*in).DeepCopyInto(*out)
}
if in.Mssql != nil {
in, out := &in.Mssql, &out.Mssql
*out = new(Database)
(*in).DeepCopyInto(*out)
}
if in.Mysql != nil {
in, out := &in.Mysql, &out.Mysql
*out = new(Database)
(*in).DeepCopyInto(*out)
}
if in.Redis != nil {
in, out := &in.Redis, &out.Redis
*out = new(Database)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Collect.
func (in *Collect) DeepCopy() *Collect {
if in == nil {
return nil
}
out := new(Collect)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CollectorMeta) DeepCopyInto(out *CollectorMeta) {
*out = *in
if in.Exclude != nil {
in, out := &in.Exclude, &out.Exclude
*out = new(multitype.BoolOrString)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CollectorMeta.
func (in *CollectorMeta) DeepCopy() *CollectorMeta {
if in == nil {
return nil
}
out := new(CollectorMeta)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ConfigMapKeyRef) DeepCopyInto(out *ConfigMapKeyRef) {
*out = *in
if in.Optional != nil {
in, out := &in.Optional, &out.Optional
*out = new(bool)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigMapKeyRef.
func (in *ConfigMapKeyRef) DeepCopy() *ConfigMapKeyRef {
if in == nil {
return nil
}
out := new(ConfigMapKeyRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Database) DeepCopyInto(out *Database) {
*out = *in
in.CollectorMeta.DeepCopyInto(&out.CollectorMeta)
in.URI.DeepCopyInto(&out.URI)
if in.Parameters != nil {
in, out := &in.Parameters, &out.Parameters
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.TLS != nil {
in, out := &in.TLS, &out.TLS
*out = new(TLSParams)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Database.
func (in *Database) DeepCopy() *Database {
if in == nil {
return nil
}
out := new(Database)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HostAnalyze) DeepCopyInto(out *HostAnalyze) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostAnalyze.
func (in *HostAnalyze) DeepCopy() *HostAnalyze {
if in == nil {
return nil
}
out := new(HostAnalyze)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HostCollect) DeepCopyInto(out *HostCollect) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostCollect.
func (in *HostCollect) DeepCopy() *HostCollect {
if in == nil {
return nil
}
out := new(HostCollect)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretKeyRef) DeepCopyInto(out *SecretKeyRef) {
*out = *in
if in.Optional != nil {
in, out := &in.Optional, &out.Optional
*out = new(bool)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretKeyRef.
func (in *SecretKeyRef) DeepCopy() *SecretKeyRef {
if in == nil {
return nil
}
out := new(SecretKeyRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StringOrValueFrom) DeepCopyInto(out *StringOrValueFrom) {
*out = *in
if in.Value != nil {
in, out := &in.Value, &out.Value
*out = new(string)
**out = **in
}
if in.ValueFrom != nil {
in, out := &in.ValueFrom, &out.ValueFrom
*out = new(ValueFromSource)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StringOrValueFrom.
func (in *StringOrValueFrom) DeepCopy() *StringOrValueFrom {
if in == nil {
return nil
}
out := new(StringOrValueFrom)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SupportBundle) DeepCopyInto(out *SupportBundle) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
out.Status = in.Status
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SupportBundle.
func (in *SupportBundle) DeepCopy() *SupportBundle {
if in == nil {
return nil
}
out := new(SupportBundle)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *SupportBundle) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SupportBundleList) DeepCopyInto(out *SupportBundleList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]SupportBundle, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SupportBundleList.
func (in *SupportBundleList) DeepCopy() *SupportBundleList {
if in == nil {
return nil
}
out := new(SupportBundleList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *SupportBundleList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SupportBundleSpec) DeepCopyInto(out *SupportBundleSpec) {
*out = *in
if in.AfterCollection != nil {
in, out := &in.AfterCollection, &out.AfterCollection
*out = make([]*AfterCollection, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(AfterCollection)
(*in).DeepCopyInto(*out)
}
}
}
if in.Collectors != nil {
in, out := &in.Collectors, &out.Collectors
*out = make([]*Collect, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(Collect)
(*in).DeepCopyInto(*out)
}
}
}
if in.HostCollectors != nil {
in, out := &in.HostCollectors, &out.HostCollectors
*out = make([]*HostCollect, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(HostCollect)
**out = **in
}
}
}
if in.Analyzers != nil {
in, out := &in.Analyzers, &out.Analyzers
*out = make([]*Analyze, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(Analyze)
**out = **in
}
}
}
if in.HostAnalyzers != nil {
in, out := &in.HostAnalyzers, &out.HostAnalyzers
*out = make([]*HostAnalyze, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(HostAnalyze)
**out = **in
}
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SupportBundleSpec.
func (in *SupportBundleSpec) DeepCopy() *SupportBundleSpec {
if in == nil {
return nil
}
out := new(SupportBundleSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SupportBundleStatus) DeepCopyInto(out *SupportBundleStatus) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SupportBundleStatus.
func (in *SupportBundleStatus) DeepCopy() *SupportBundleStatus {
if in == nil {
return nil
}
out := new(SupportBundleStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TLSParams) DeepCopyInto(out *TLSParams) {
*out = *in
if in.Secret != nil {
in, out := &in.Secret, &out.Secret
*out = new(TLSSecret)
**out = **in
}
in.CACert.DeepCopyInto(&out.CACert)
in.ClientCert.DeepCopyInto(&out.ClientCert)
in.ClientKey.DeepCopyInto(&out.ClientKey)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSParams.
func (in *TLSParams) DeepCopy() *TLSParams {
if in == nil {
return nil
}
out := new(TLSParams)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TLSSecret) DeepCopyInto(out *TLSSecret) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSSecret.
func (in *TLSSecret) DeepCopy() *TLSSecret {
if in == nil {
return nil
}
out := new(TLSSecret)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ValueFromSource) DeepCopyInto(out *ValueFromSource) {
*out = *in
if in.SecretKeyRef != nil {
in, out := &in.SecretKeyRef, &out.SecretKeyRef
*out = new(SecretKeyRef)
(*in).DeepCopyInto(*out)
}
if in.ConfigMapKeyRef != nil {
in, out := &in.ConfigMapKeyRef, &out.ConfigMapKeyRef
*out = new(ConfigMapKeyRef)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValueFromSource.
func (in *ValueFromSource) DeepCopy() *ValueFromSource {
if in == nil {
return nil
}
out := new(ValueFromSource)
in.DeepCopyInto(out)
return out
}
@@ -20,7 +20,6 @@ package scheme
import (
troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
troubleshootv1beta3 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta3"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
@@ -34,7 +33,6 @@ var ParameterCodec = runtime.NewParameterCodec(Scheme)
var localSchemeBuilder = runtime.SchemeBuilder{
troubleshootv1beta1.AddToScheme,
troubleshootv1beta2.AddToScheme,
troubleshootv1beta3.AddToScheme,
}
// AddToScheme adds all types of this clientset into the given scheme. This allows composition
+3 -75
View File
@@ -232,7 +232,7 @@ func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (Coll
// replicasets
replicasets, replicasetsErrors := replicasets(ctx, client, namespaceNames)
for k, v := range replicasets {
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_REPLICASETS, k), bytes.NewBuffer(v))
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_STATEFULSETS), k), bytes.NewBuffer(v))
}
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_REPLICASETS)), marshalErrors(replicasetsErrors))
@@ -370,9 +370,9 @@ func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (Coll
// endpointslices
endpointslices, endpointslicesErrors := endpointslices(ctx, client, namespaceNames)
for k, v := range endpointslices {
_ = output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_ENDPOINTSLICES, k), bytes.NewBuffer(v))
_ = output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_ENDPOINTSICES, k), bytes.NewBuffer(v))
}
_ = output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_ENDPOINTSLICES)), marshalErrors(endpointslicesErrors))
_ = output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_ENDPOINTSICES)), marshalErrors(endpointslicesErrors))
// Service Accounts
servicesAccounts, servicesAccountsErrors := serviceAccounts(ctx, client, namespaceNames)
@@ -400,13 +400,6 @@ func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (Coll
}
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_CONFIGMAPS)), marshalErrors(configMapsErrors))
// Replicated License
licenseData, licenseErr := replicatedLicense(ctx, client, namespaceNames)
if licenseErr == nil {
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_REPLICATED_LICENSE), bytes.NewBuffer(licenseData))
}
return output, nil
}
@@ -2183,68 +2176,3 @@ func storeCustomResource(name string, objects any, m map[string][]byte) error {
m[fmt.Sprintf("%s.yaml", name)] = y
return nil
}
// replicatedLicense searches for the replicated secret across namespaces,
// extracts the config.yaml field, and extracts the licenseID and appSlug.
// Note: secret.Data already contains decoded bytes; no base64 decoding is required.
func replicatedLicense(ctx context.Context, client *kubernetes.Clientset, namespaces []string) ([]byte, error) {
// Structure to parse the config.yaml content
type ConfigYAML struct {
License string `yaml:"license"` // This is a YAML string containing the License object
}
type LicenseSpec struct {
LicenseID string `yaml:"licenseID"`
AppSlug string `yaml:"appSlug"`
}
type License struct {
Spec LicenseSpec `yaml:"spec"`
}
// Search through all namespaces for the replicated secret
for _, namespace := range namespaces {
secret, err := client.CoreV1().Secrets(namespace).Get(ctx, "replicated", metav1.GetOptions{})
if err != nil {
// Secret not found in this namespace, continue to next
continue
}
// Extract the config.yaml field from the secret data
configYAMLBase64, exists := secret.Data["config.yaml"]
if !exists {
continue
}
configYAMLBytes := configYAMLBase64
// Parse the YAML to extract the license field
var config ConfigYAML
if err := yaml.Unmarshal(configYAMLBytes, &config); err != nil {
// Malformed config in this namespace; try the next namespace
continue
}
// Parse the license field (which is a YAML string) to extract licenseID and appSlug
var license License
if err := yaml.Unmarshal([]byte(config.License), &license); err != nil {
// Malformed license in this namespace; try the next namespace
continue
}
// Return both licenseID and appSlug as JSON
licenseData := map[string]string{
"licenseID": license.Spec.LicenseID,
"appSlug": license.Spec.AppSlug,
}
licenseJSON, err := json.Marshal(licenseData)
if err != nil {
return nil, fmt.Errorf("failed to marshal license data: %w", err)
}
return licenseJSON, nil
}
// No replicated secret with a parsable license found in any namespace
return nil, fmt.Errorf("replicated secret with parsable license not found in any namespace")
}
+1 -94
View File
@@ -15,7 +15,7 @@ func Test_ensureClusterResourcesFirst(t *testing.T) {
list []*troubleshootv1beta2.Collect
}{
{
name: "Reorg OK - clusterResources moved to front",
name: "Reorg OK",
want: []*troubleshootv1beta2.Collect{
{
ClusterResources: &troubleshootv1beta2.ClusterResources{},
@@ -33,99 +33,6 @@ func Test_ensureClusterResourcesFirst(t *testing.T) {
},
},
},
{
name: "Already first - no change",
want: []*troubleshootv1beta2.Collect{
{
ClusterResources: &troubleshootv1beta2.ClusterResources{},
},
{
Data: &troubleshootv1beta2.Data{},
},
{
Secret: &troubleshootv1beta2.Secret{},
},
},
list: []*troubleshootv1beta2.Collect{
{
ClusterResources: &troubleshootv1beta2.ClusterResources{},
},
{
Data: &troubleshootv1beta2.Data{},
},
{
Secret: &troubleshootv1beta2.Secret{},
},
},
},
{
name: "Multiple clusterResources - all moved to front",
want: []*troubleshootv1beta2.Collect{
{
ClusterResources: &troubleshootv1beta2.ClusterResources{},
},
{
ClusterResources: &troubleshootv1beta2.ClusterResources{},
},
{
Data: &troubleshootv1beta2.Data{},
},
{
Secret: &troubleshootv1beta2.Secret{},
},
},
list: []*troubleshootv1beta2.Collect{
{
Data: &troubleshootv1beta2.Data{},
},
{
ClusterResources: &troubleshootv1beta2.ClusterResources{},
},
{
Secret: &troubleshootv1beta2.Secret{},
},
{
ClusterResources: &troubleshootv1beta2.ClusterResources{},
},
},
},
{
name: "No clusterResources - no change",
want: []*troubleshootv1beta2.Collect{
{
Data: &troubleshootv1beta2.Data{},
},
{
Secret: &troubleshootv1beta2.Secret{},
},
},
list: []*troubleshootv1beta2.Collect{
{
Data: &troubleshootv1beta2.Data{},
},
{
Secret: &troubleshootv1beta2.Secret{},
},
},
},
{
name: "Empty list - no change",
want: []*troubleshootv1beta2.Collect{},
list: []*troubleshootv1beta2.Collect{},
},
{
name: "Only clusterResources - no change",
want: []*troubleshootv1beta2.Collect{
{
ClusterResources: &troubleshootv1beta2.ClusterResources{},
},
},
list: []*troubleshootv1beta2.Collect{
{
ClusterResources: &troubleshootv1beta2.ClusterResources{},
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
+14 -7
View File
@@ -44,7 +44,8 @@ pwd=somethinggoeshere;`,
want: map[string]string{
"data/datacollectorname": ` 123
another***HIDDEN***here
pwd=***HIDDEN***;`,
pwd=***HIDDEN***;
`,
},
},
{
@@ -77,7 +78,8 @@ pwd=somethinggoeshere;`,
want: map[string]string{
"data/datacollectorname": `abc 123
another***HIDDEN***here
pwd=***HIDDEN***;`,
pwd=***HIDDEN***;
`,
},
},
{
@@ -110,7 +112,8 @@ pwd=somethinggoeshere;`,
want: map[string]string{
"data/datacollectorname": `abc 123
another line here
pwd=***HIDDEN***;`,
pwd=***HIDDEN***;
`,
},
},
{
@@ -146,7 +149,8 @@ pwd=somethinggoeshere;`,
want: map[string]string{
"data/datacollectorname": `abc 123
another***HIDDEN***here
pwd=***HIDDEN***;`,
pwd=***HIDDEN***;
`,
},
},
{
@@ -182,7 +186,8 @@ pwd=somethinggoeshere;`,
want: map[string]string{
"data/data/collectorname": `***HIDDEN*** ***HIDDEN***
***HIDDEN*** line here
pwd=***HIDDEN***;`,
pwd=***HIDDEN***;
`,
},
},
{
@@ -208,7 +213,8 @@ another line here`,
},
want: map[string]string{
"data/datacollectorname": `abc 123
another line here`,
another line here
`,
},
},
{
@@ -243,7 +249,8 @@ abc`,
abc
123
xyz123
abc`,
abc
`,
},
},
{
+3 -46
View File
@@ -10,16 +10,9 @@ import (
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
"helm.sh/helm/v3/pkg/action"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/discovery"
"k8s.io/client-go/discovery/cached/memory"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/restmapper"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/klog/v2"
)
@@ -52,42 +45,6 @@ type VersionInfo struct {
Values map[string]interface{} `json:"values,omitempty"`
}
type configGetter struct {
restConfig *rest.Config
}
// ToDiscoveryClient implements genericclioptions.RESTClientGetter.
func (c configGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
discoveryClient, err := discovery.NewDiscoveryClientForConfig(c.restConfig)
if err != nil {
return nil, err
}
cached := memory.NewMemCacheClient(discoveryClient)
return cached, nil
}
// ToRESTConfig implements genericclioptions.RESTClientGetter.
func (c configGetter) ToRESTConfig() (*rest.Config, error) {
return c.restConfig, nil
}
// ToRESTMapper implements genericclioptions.RESTClientGetter.
func (c configGetter) ToRESTMapper() (meta.RESTMapper, error) {
discoveryClient, err := c.ToDiscoveryClient()
if err != nil {
return nil, err
}
mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
return mapper, nil
}
// ToRawKubeConfigLoader implements genericclioptions.RESTClientGetter.
func (c configGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig {
return k8sutil.GetKubeconfig()
}
var _ genericclioptions.RESTClientGetter = configGetter{}
func (c *CollectHelm) Title() string {
return getCollectorName(c)
}
@@ -100,7 +57,7 @@ func (c *CollectHelm) Collect(progressChan chan<- interface{}) (CollectorResult,
output := NewResult()
releaseInfos, err := helmReleaseHistoryCollector(c.ClientConfig, c.Collector.ReleaseName, c.Collector.Namespace, c.Collector.CollectValues)
releaseInfos, err := helmReleaseHistoryCollector(c.Collector.ReleaseName, c.Collector.Namespace, c.Collector.CollectValues)
if err != nil {
errsToMarhsal := []string{}
for _, e := range err {
@@ -131,12 +88,12 @@ func (c *CollectHelm) Collect(progressChan chan<- interface{}) (CollectorResult,
return output, nil
}
func helmReleaseHistoryCollector(config *rest.Config, releaseName string, namespace string, collectValues bool) ([]ReleaseInfo, []error) {
func helmReleaseHistoryCollector(releaseName string, namespace string, collectValues bool) ([]ReleaseInfo, []error) {
var results []ReleaseInfo
error_list := []error{}
actionConfig := new(action.Configuration)
if err := actionConfig.Init(configGetter{config}, namespace, "", klog.V(2).Infof); err != nil {
if err := actionConfig.Init(nil, namespace, "", klog.V(2).Infof); err != nil {
return nil, []error{err}
}
+4 -4
View File
@@ -9,15 +9,15 @@ import (
"strings"
"time"
imagedocker "github.com/containers/image/v5/docker"
dockerref "github.com/containers/image/v5/docker/reference"
"github.com/containers/image/v5/transports/alltransports"
"github.com/containers/image/v5/types"
"github.com/distribution/distribution/v3/registry/api/errcode"
registryv2 "github.com/distribution/distribution/v3/registry/api/v2"
"github.com/pkg/errors"
"github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
imagedocker "go.podman.io/image/v5/docker"
dockerref "go.podman.io/image/v5/docker/reference"
"go.podman.io/image/v5/transports/alltransports"
"go.podman.io/image/v5/types"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
+1 -1
View File
@@ -8,9 +8,9 @@ import (
"testing"
"time"
"github.com/containers/image/v5/transports/alltransports"
"github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/stretchr/testify/assert"
"go.podman.io/image/v5/transports/alltransports"
"k8s.io/client-go/rest"
)
+1 -13
View File
@@ -80,19 +80,7 @@ func DeterministicIDForCollector(collector *troubleshootv1beta2.Collect) string
}
func selectorToString(selector []string) string {
result := strings.Replace(strings.Join(selector, "-"), "=", "-", -1)
// Sanitize characters that are invalid in Windows filenames: < > : " / \ | ? *
// Replace them with underscores to ensure cross-platform compatibility
result = strings.ReplaceAll(result, "*", "all")
result = strings.ReplaceAll(result, "?", "_")
result = strings.ReplaceAll(result, ":", "_")
result = strings.ReplaceAll(result, "<", "_")
result = strings.ReplaceAll(result, ">", "_")
result = strings.ReplaceAll(result, "|", "_")
result = strings.ReplaceAll(result, "\"", "_")
result = strings.ReplaceAll(result, "/", "_")
result = strings.ReplaceAll(result, "\\", "_")
return result
return strings.Replace(strings.Join(selector, "-"), "=", "-", -1)
}
func pathToString(path string) string {
+1 -2
View File
@@ -56,12 +56,11 @@ const (
CLUSTER_RESOURCES_CLUSTER_ROLE_BINDINGS = "clusterrolebindings"
CLUSTER_RESOURCES_PRIORITY_CLASS = "priorityclasses"
CLUSTER_RESOURCES_ENDPOINTS = "endpoints"
CLUSTER_RESOURCES_ENDPOINTSLICES = "endpointslices"
CLUSTER_RESOURCES_ENDPOINTSICES = "endpointslices"
CLUSTER_RESOURCES_SERVICE_ACCOUNTS = "serviceaccounts"
CLUSTER_RESOURCES_LEASES = "leases"
CLUSTER_RESOURCES_VOLUME_ATTACHMENTS = "volumeattachments"
CLUSTER_RESOURCES_CONFIGMAPS = "configmaps"
CLUSTER_RESOURCES_REPLICATED_LICENSE = "license.json"
// SelfSubjectRulesReview evaluation responses
SELFSUBJECTRULESREVIEW_ERROR_AUTHORIZATION_WEBHOOK_UNSUPPORTED = "webhook authorizer does not support user rule resolution"
+2 -7
View File
@@ -6,15 +6,11 @@ import (
// HasResource takes an api version and a kind of a resource and checks if the resource
// is supported by the k8s api server.
// This function handles partial results from ServerGroupsAndResources(): "The returned group and resource lists might be non-nil with partial
// results even in the case of non-nil error."
func HasResource(dc discovery.DiscoveryInterface, apiVersion, kind string) (bool, error) {
_, apiLists, err := dc.ServerGroupsAndResources()
if apiLists == nil {
if err != nil {
return false, err
}
// Compare the resource api version and kind and find the resource.
for _, apiList := range apiLists {
if apiList.GroupVersion == apiVersion {
@@ -25,6 +21,5 @@ func HasResource(dc discovery.DiscoveryInterface, apiVersion, kind string) (bool
}
}
}
return false, err
return false, nil
}
-232
View File
@@ -1,12 +1,9 @@
package discovery
import (
"errors"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
fakediscovery "k8s.io/client-go/discovery/fake"
fakeclientset "k8s.io/client-go/kubernetes/fake"
)
@@ -81,232 +78,3 @@ func TestHasResource(t *testing.T) {
})
}
}
// TestHasResourceWithPartialDiscoveryFailure verifies that HasResource correctly handles
// partial discovery failures where ServerGroupsAndResources() returns both an error AND
// partial results (non-nil apiLists). This simulates real Kubernetes behavior when some
// API groups fail to load but others succeed.
func TestHasResourceWithPartialDiscoveryFailure(t *testing.T) {
testKind := "Foo"
testKindGroupVersion := "v1"
testcases := []struct {
name string
apiResourceList []*metav1.APIResourceList
discoveryError error
wantResult bool
wantError bool
description string
}{
{
name: "resource found in partial results with discovery error",
apiResourceList: []*metav1.APIResourceList{
{
GroupVersion: "v1",
APIResources: []metav1.APIResource{
{
Kind: "Foo",
},
{
Kind: "Bar",
},
},
},
},
discoveryError: &discovery.ErrGroupDiscoveryFailed{
Groups: map[schema.GroupVersion]error{
{Group: "apps", Version: "v1"}: errors.New("failed to retrieve apps/v1"),
},
},
wantResult: true,
wantError: false,
description: "Should return (true, nil) when resource exists in partial results despite discovery error",
},
{
name: "resource not found in partial results with discovery error",
apiResourceList: []*metav1.APIResourceList{
{
GroupVersion: "v1",
APIResources: []metav1.APIResource{
{
Kind: "Bar",
},
{
Kind: "Baz",
},
},
},
},
discoveryError: &discovery.ErrGroupDiscoveryFailed{
Groups: map[schema.GroupVersion]error{
{Group: "apps", Version: "v1"}: errors.New("failed to retrieve apps/v1"),
},
},
wantResult: false,
wantError: true,
description: "Should return (false, error) when resource not in partial results and discovery error exists",
},
{
name: "nil api resource list with discovery error",
apiResourceList: nil,
discoveryError: &discovery.ErrGroupDiscoveryFailed{
Groups: map[schema.GroupVersion]error{
{Group: "apps", Version: "v1"}: errors.New("failed to retrieve apps/v1"),
},
},
wantResult: false,
wantError: true,
description: "Should return (false, error) when apiLists is nil and discovery error exists",
},
{
name: "empty api resource list with discovery error",
apiResourceList: []*metav1.APIResourceList{},
discoveryError: &discovery.ErrGroupDiscoveryFailed{
Groups: map[schema.GroupVersion]error{
{Group: "apps", Version: "v1"}: errors.New("failed to retrieve apps/v1"),
},
},
wantResult: false,
wantError: true,
description: "Should return (false, error) when apiLists is empty and discovery error exists",
},
{
name: "multiple groups with partial results and discovery error",
apiResourceList: []*metav1.APIResourceList{
{
GroupVersion: "v1",
APIResources: []metav1.APIResource{
{
Kind: "Pod",
},
{
Kind: "Service",
},
},
},
{
GroupVersion: "v2",
APIResources: []metav1.APIResource{
{
Kind: "Foo",
},
},
},
},
discoveryError: &discovery.ErrGroupDiscoveryFailed{
Groups: map[schema.GroupVersion]error{
{Group: "apps", Version: "v1"}: errors.New("failed to retrieve apps/v1"),
{Group: "batch", Version: "v1beta1"}: errors.New("failed to retrieve batch/v1beta1"),
},
},
wantResult: false,
wantError: true,
description: "Should return (false, error) when resource not found across multiple partial groups with discovery error",
},
{
name: "resource found with different version in partial results with discovery error",
apiResourceList: []*metav1.APIResourceList{
{
GroupVersion: "v2",
APIResources: []metav1.APIResource{
{
Kind: "Foo",
},
},
},
},
discoveryError: &discovery.ErrGroupDiscoveryFailed{
Groups: map[schema.GroupVersion]error{
{Group: "apps", Version: "v1"}: errors.New("failed to retrieve apps/v1"),
},
},
wantResult: false,
wantError: true,
description: "Should return (false, error) when resource exists with different version in partial results",
},
{
name: "generic error with partial results containing resource",
apiResourceList: []*metav1.APIResourceList{
{
GroupVersion: "v1",
APIResources: []metav1.APIResource{
{
Kind: "Foo",
},
},
},
},
discoveryError: errors.New("connection timeout"),
wantResult: true,
wantError: false,
description: "Should return (true, nil) when resource exists in partial results even with generic error",
},
{
name: "generic error without resource in partial results",
apiResourceList: []*metav1.APIResourceList{
{
GroupVersion: "v1",
APIResources: []metav1.APIResource{
{
Kind: "Bar",
},
},
},
},
discoveryError: errors.New("connection timeout"),
wantResult: false,
wantError: true,
description: "Should return (false, error) when resource not in partial results with generic error",
},
}
for _, tc := range testcases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
client := fakeclientset.NewSimpleClientset()
fakeDiscovery, ok := client.Discovery().(*fakediscovery.FakeDiscovery)
if !ok {
t.Fatalf("could not convert Discovery() to *FakeDiscovery")
}
// Configure the fake discovery to return both resources and error
fakeDiscovery.Resources = tc.apiResourceList
// Create a mock discovery interface that returns both error and partial results
mockDiscovery := &mockDiscoveryWithPartialFailure{
FakeDiscovery: fakeDiscovery,
errorToReturn: tc.discoveryError,
}
exists, err := HasResource(mockDiscovery, testKindGroupVersion, testKind)
// Verify error expectation
if tc.wantError && err == nil {
t.Errorf("%s: expected error but got nil", tc.description)
}
if !tc.wantError && err != nil {
t.Errorf("%s: expected no error but got: %v", tc.description, err)
}
// Verify result expectation
if exists != tc.wantResult {
t.Errorf("%s: unexpected result for HasResource:\n\t(WANT) %t\n\t(GOT) %t", tc.description, tc.wantResult, exists)
}
})
}
}
// mockDiscoveryWithPartialFailure wraps FakeDiscovery to simulate partial discovery failures
// where ServerGroupsAndResources() returns both an error AND partial results.
type mockDiscoveryWithPartialFailure struct {
*fakediscovery.FakeDiscovery
errorToReturn error
}
// ServerGroupsAndResources simulates the Kubernetes API behavior where partial results
// can be returned even when an error occurs. This happens when some API groups fail to
// load but others succeed.
func (m *mockDiscoveryWithPartialFailure) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
groups, resources, _ := m.FakeDiscovery.ServerGroupsAndResources()
return groups, resources, m.errorToReturn
}
-25
View File
@@ -25,26 +25,6 @@ const (
PodStatusReasonInitCrashLoopBackOff PodStatusReason = "Init:CrashLoopBackOff"
)
// isNativeSidecar checks if an init container is a native sidecar.
// Native sidecars are init containers with restartPolicy: Always (Kubernetes 1.28+).
// They run continuously alongside main containers, unlike traditional init containers
// which must complete before main containers start.
func isNativeSidecar(pod *corev1.Pod, initContainerIndex int) bool {
// Bounds check - ensure the index is valid
if initContainerIndex >= len(pod.Spec.InitContainers) {
return false
}
initContainer := pod.Spec.InitContainers[initContainerIndex]
// Check if RestartPolicy is set to Always
if initContainer.RestartPolicy != nil && *initContainer.RestartPolicy == corev1.ContainerRestartPolicyAlways {
return true
}
return false
}
// reference: https://github.com/kubernetes/kubernetes/blob/e8fcd0de98d50f4019561a6b7a0287f5c059267a/pkg/printers/internalversion/printers.go#L741
func GetPodStatusReason(pod *corev1.Pod) (string, string) {
reason := string(pod.Status.Phase)
@@ -75,11 +55,6 @@ func GetPodStatusReason(pod *corev1.Pod) (string, string) {
case container.State.Waiting != nil && len(container.State.Waiting.Reason) > 0 && container.State.Waiting.Reason != "PodInitializing":
reason = "Init:" + container.State.Waiting.Reason
initializing = true
case isNativeSidecar(pod, i) && container.State.Running != nil:
// Native sidecar running - this is expected, not stuck initializing.
// Native sidecars (init containers with restartPolicy: Always) are designed
// to run continuously, so a Running state means successful initialization.
continue
default:
reason = fmt.Sprintf("Init:%d/%d", i, len(pod.Spec.InitContainers))
initializing = true
+1 -284
View File
@@ -1,11 +1,9 @@
package k8sutil
import (
"testing"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"testing"
)
func TestIsPodUnhealthy(t *testing.T) {
@@ -101,284 +99,3 @@ func TestIsPodUnhealthy(t *testing.T) {
})
}
}
// TestGetPodStatusReason_HealthyNativeSidecar tests that a pod with a running native sidecar
// is correctly reported as "Running" and not stuck initializing.
func TestGetPodStatusReason_HealthyNativeSidecar(t *testing.T) {
startedTrue := true
restartPolicyAlways := corev1.ContainerRestartPolicyAlways
pod := &corev1.Pod{
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{
{
Name: "istio-proxy",
Image: "istio/proxyv2:1.20",
RestartPolicy: &restartPolicyAlways, // Native sidecar!
},
},
Containers: []corev1.Container{
{
Name: "app",
Image: "myapp:latest",
},
},
},
Status: corev1.PodStatus{
Phase: corev1.PodRunning,
Conditions: []corev1.PodCondition{
{
Type: corev1.PodReady,
Status: corev1.ConditionTrue,
},
},
InitContainerStatuses: []corev1.ContainerStatus{
{
Name: "istio-proxy",
Ready: true,
Started: &startedTrue,
State: corev1.ContainerState{
Running: &corev1.ContainerStateRunning{
StartedAt: metav1.Now(),
},
},
},
},
ContainerStatuses: []corev1.ContainerStatus{
{
Name: "app",
Ready: true,
Started: &startedTrue,
State: corev1.ContainerState{
Running: &corev1.ContainerStateRunning{
StartedAt: metav1.Now(),
},
},
},
},
},
}
reason, message := GetPodStatusReason(pod)
// Should report as Running, not Init:0/1
if reason != "Running" {
t.Errorf("Expected reason 'Running', got '%s'", reason)
}
if message != "" {
t.Errorf("Expected empty message, got '%s'", message)
}
}
// TestIsPodUnhealthy_HealthyNativeSidecar tests that a pod with a healthy running native sidecar
// is not marked as unhealthy.
func TestIsPodUnhealthy_HealthyNativeSidecar(t *testing.T) {
startedTrue := true
restartPolicyAlways := corev1.ContainerRestartPolicyAlways
pod := &corev1.Pod{
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{
{
Name: "istio-proxy",
RestartPolicy: &restartPolicyAlways,
},
},
Containers: []corev1.Container{
{
Name: "app",
},
},
},
Status: corev1.PodStatus{
Phase: corev1.PodRunning,
Conditions: []corev1.PodCondition{
{
Type: corev1.PodReady,
Status: corev1.ConditionTrue,
},
},
InitContainerStatuses: []corev1.ContainerStatus{
{
Name: "istio-proxy",
Ready: true,
Started: &startedTrue,
State: corev1.ContainerState{
Running: &corev1.ContainerStateRunning{},
},
},
},
ContainerStatuses: []corev1.ContainerStatus{
{
Name: "app",
Ready: true,
Started: &startedTrue,
State: corev1.ContainerState{
Running: &corev1.ContainerStateRunning{},
},
},
},
},
}
unhealthy := IsPodUnhealthy(pod)
if unhealthy {
t.Error("Pod with healthy native sidecar should not be marked as unhealthy")
}
}
// TestGetPodStatusReason_TraditionalInitAndNativeSidecar tests that a pod with both
// a completed traditional init container and a running native sidecar is reported as "Running".
func TestGetPodStatusReason_TraditionalInitAndNativeSidecar(t *testing.T) {
startedTrue := true
restartPolicyAlways := corev1.ContainerRestartPolicyAlways
pod := &corev1.Pod{
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{
{
Name: "init-setup",
// No RestartPolicy = traditional init container
},
{
Name: "istio-proxy",
RestartPolicy: &restartPolicyAlways, // Native sidecar
},
},
Containers: []corev1.Container{
{
Name: "app",
},
},
},
Status: corev1.PodStatus{
Phase: corev1.PodRunning,
Conditions: []corev1.PodCondition{
{
Type: corev1.PodReady,
Status: corev1.ConditionTrue,
},
},
InitContainerStatuses: []corev1.ContainerStatus{
{
Name: "init-setup",
State: corev1.ContainerState{
Terminated: &corev1.ContainerStateTerminated{
ExitCode: 0,
Reason: "Completed",
},
},
},
{
Name: "istio-proxy",
Ready: true,
Started: &startedTrue,
State: corev1.ContainerState{
Running: &corev1.ContainerStateRunning{},
},
},
},
ContainerStatuses: []corev1.ContainerStatus{
{
Name: "app",
Ready: true,
Started: &startedTrue,
State: corev1.ContainerState{
Running: &corev1.ContainerStateRunning{},
},
},
},
},
}
reason, _ := GetPodStatusReason(pod)
if reason != "Running" {
t.Errorf("Expected reason 'Running', got '%s'", reason)
}
}
// TestGetPodStatusReason_NativeSidecarCrashLoopBackOff tests that a native sidecar
// in CrashLoopBackOff is still correctly detected as an error.
func TestGetPodStatusReason_NativeSidecarCrashLoopBackOff(t *testing.T) {
restartPolicyAlways := corev1.ContainerRestartPolicyAlways
pod := &corev1.Pod{
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{
{
Name: "istio-proxy",
RestartPolicy: &restartPolicyAlways,
},
},
Containers: []corev1.Container{
{
Name: "app",
},
},
},
Status: corev1.PodStatus{
Phase: corev1.PodPending,
InitContainerStatuses: []corev1.ContainerStatus{
{
Name: "istio-proxy",
Ready: false,
State: corev1.ContainerState{
Waiting: &corev1.ContainerStateWaiting{
Reason: "CrashLoopBackOff",
Message: "Back-off 5m0s restarting failed container",
},
},
},
},
},
}
reason, _ := GetPodStatusReason(pod)
// Should still catch the error
if reason != "Init:CrashLoopBackOff" {
t.Errorf("Expected reason 'Init:CrashLoopBackOff', got '%s'", reason)
}
}
// TestGetPodStatusReason_TraditionalInitStuck tests that a traditional init container
// that is stuck running is still correctly detected as stuck initializing.
func TestGetPodStatusReason_TraditionalInitStuck(t *testing.T) {
pod := &corev1.Pod{
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{
{
Name: "init-setup",
// No RestartPolicy = traditional init
},
},
Containers: []corev1.Container{
{
Name: "app",
},
},
},
Status: corev1.PodStatus{
Phase: corev1.PodPending,
InitContainerStatuses: []corev1.ContainerStatus{
{
Name: "init-setup",
Ready: false,
State: corev1.ContainerState{
Running: &corev1.ContainerStateRunning{},
},
},
},
},
}
reason, _ := GetPodStatusReason(pod)
// Traditional init running = stuck
if reason != "Init:0/1" {
t.Errorf("Expected reason 'Init:0/1', got '%s'", reason)
}
}
-17
View File
@@ -1,17 +0,0 @@
package k8sutil
import corev1 "k8s.io/api/core/v1"
// TaintExists checks if the given taint exists in list of taints. Returns true
// if exists false otherwise.
//
// Copied from k8s.io/kubernetes/pkg/util/taints so we don't have to import
// k8s.io/kubernetes.
func TaintExists(taints []corev1.Taint, taintToFind *corev1.Taint) bool {
for _, taint := range taints {
if taint.MatchTaint(taintToFind) {
return true
}
}
return false
}
-345
View File
@@ -1,345 +0,0 @@
package lint
import (
"regexp"
"strings"
"github.com/replicatedhq/troubleshoot/pkg/constants"
)
func applyFixesInMemory(content string, result LintResult) (string, bool, error) {
fixed := false
newContent := content
lines := strings.Split(newContent, "\n")
// Fix A: If templating errors exist in a v1beta2 file, upgrade apiVersion to v1beta3 (minimal, deterministic)
hasTemplateInV1beta2 := false
for _, e := range result.Errors {
if e.Field == "template" && strings.Contains(e.Message, "not supported in v1beta2") {
hasTemplateInV1beta2 = true
break
}
}
if hasTemplateInV1beta2 {
for i, line := range lines {
if strings.HasPrefix(strings.TrimSpace(line), "apiVersion:") && strings.Contains(line, constants.Troubleshootv1beta2Kind) {
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
lines[i] = indent + "apiVersion: " + constants.Troubleshootv1beta3Kind
fixed = true
break
}
}
}
// Sort errors by line number (descending) to avoid line number shifts when editing
errorsByLine := make(map[int][]LintError)
for _, err := range result.Errors {
if err.Line > 0 {
errorsByLine[err.Line] = append(errorsByLine[err.Line], err)
}
}
// Process errors line by line
for lineNum, errs := range errorsByLine {
if lineNum > len(lines) {
continue
}
line := lines[lineNum-1]
originalLine := line
for _, err := range errs {
// Fix 1: Add missing colon
if strings.Contains(err.Message, "could not find expected ':'") {
if !strings.Contains(line, ":") {
trimmed := strings.TrimSpace(line)
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
line = indent + trimmed + ":"
fixed = true
}
}
// Fix 2: Add missing leading dot in template expressions
if strings.Contains(err.Message, "Template expression may be missing leading dot:") {
// Extract the expression from the error message
re := regexp.MustCompile(`Template expression may be missing leading dot: \{\{ (.+?) \}\}`)
matches := re.FindStringSubmatch(err.Message)
if len(matches) > 1 {
badExpr := matches[1]
// Add the leading dot
fixedExpr := "." + badExpr
// Replace in the line
line = strings.Replace(line, "{{ "+badExpr+" }}", "{{ "+fixedExpr+" }}", 1)
line = strings.Replace(line, "{{"+badExpr+"}}", "{{"+fixedExpr+"}}", 1)
line = strings.Replace(line, "{{- "+badExpr+" }}", "{{- "+fixedExpr+" }}", 1)
line = strings.Replace(line, "{{- "+badExpr+" -}}", "{{- "+fixedExpr+" -}}", 1)
fixed = true
}
}
// Fix 3: Fix wrong apiVersion
if strings.Contains(err.Message, "File must contain apiVersion:") && err.Field == "apiVersion" {
if strings.Contains(line, "apiVersion:") && !strings.Contains(line, constants.Troubleshootv1beta3Kind) {
// Replace existing apiVersion with correct one
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
line = indent + "apiVersion: " + constants.Troubleshootv1beta3Kind
fixed = true
}
}
}
// Update the line if it changed
if line != originalLine {
lines[lineNum-1] = line
}
}
// Fix B: Wrap mapping under required list fields (collectors, hostCollectors, analyzers)
for _, err := range result.Errors {
if strings.HasPrefix(err.Message, "Expected 'collectors' to be a list") {
if wrapFirstChildAsList(&lines, "collectors:") {
fixed = true
}
}
if strings.HasPrefix(err.Message, "Expected 'hostCollectors' to be a list") {
if wrapFirstChildAsList(&lines, "hostCollectors:") || convertScalarToEmptyList(&lines, "hostCollectors:") {
fixed = true
}
}
if strings.HasPrefix(err.Message, "Expected 'analyzers' to be a list") {
if wrapFirstChildAsList(&lines, "analyzers:") {
fixed = true
}
}
}
// Fix C: Add missing required fields with empty placeholders (non-assumptive)
// Collectors
for _, err := range result.Errors {
if strings.HasPrefix(err.Message, "Missing required field '") && strings.Contains(err.Message, " for collector '") {
// Parse field and collector type
// e.g., Missing required field 'namespace' for collector 'ceph'
fieldName := between(err.Message, "Missing required field '", "'")
collectorType := betweenAfter(err.Message, "collector '", "'")
if fieldName == "" || collectorType == "" {
continue
}
// Only handle simple case where the list item is in {} form: "- type: {}"
// Find the list item line from current content
cur := strings.Join(lines, "\n")
lineNum := findCollectorLine(cur, "collectors", indexFromField(err.Field))
if lineNum > 0 {
li := lineNum - 1
if strings.Contains(lines[li], "- "+collectorType+": {}") {
indent := lines[li][:len(lines[li])-len(strings.TrimLeft(lines[li], " \t"))]
childIndent := indent + " "
// choose placeholder: outcomes -> [] ; others -> ""
placeholder := "\"\""
if fieldName == "outcomes" {
placeholder = "[]"
}
lines[li] = strings.Replace(lines[li], ": {}", ":\n"+childIndent+fieldName+": "+placeholder, 1)
fixed = true
} else if strings.Contains(lines[li], "- "+collectorType+":") {
// Multi-line mapping; insert missing field under this item
if insertMissingFieldUnderListItem(&lines, li, fieldName) {
fixed = true
}
}
}
}
}
// Analyzers
for _, err := range result.Errors {
if strings.HasPrefix(err.Message, "Missing required field '") && strings.Contains(err.Message, " for analyzer '") {
fieldName := between(err.Message, "Missing required field '", "'")
analyzerType := betweenAfter(err.Message, "analyzer '", "'")
if fieldName == "" || analyzerType == "" {
continue
}
cur := strings.Join(lines, "\n")
lineNum := findAnalyzerLine(cur, indexFromField(err.Field))
if lineNum > 0 {
li := lineNum - 1
if strings.Contains(lines[li], "- "+analyzerType+": {}") {
indent := lines[li][:len(lines[li])-len(strings.TrimLeft(lines[li], " \t"))]
childIndent := indent + " "
placeholder := "\"\""
if fieldName == "outcomes" {
placeholder = "[]"
}
lines[li] = strings.Replace(lines[li], ": {}", ":\n"+childIndent+fieldName+": "+placeholder, 1)
fixed = true
} else if strings.Contains(lines[li], "- "+analyzerType+":") {
if insertMissingFieldUnderListItem(&lines, li, fieldName) {
fixed = true
}
}
}
}
}
// Return fixed content if changes were made
if fixed {
newContent = strings.Join(lines, "\n")
return newContent, true, nil
}
return content, false, nil
}
// wrapFirstChildAsList prefixes the first child mapping line under the given key with '- '
func wrapFirstChildAsList(lines *[]string, key string) bool {
arr := *lines
// find key line index
baseIdx := -1
for i, l := range arr {
if strings.Contains(l, key) {
baseIdx = i
break
}
}
if baseIdx == -1 {
return false
}
baseIndent := arr[baseIdx][:len(arr[baseIdx])-len(strings.TrimLeft(arr[baseIdx], " \t"))]
// find first child line with greater indent
for j := baseIdx + 1; j < len(arr); j++ {
line := arr[j]
if strings.TrimSpace(line) == "" {
continue
}
// stop when indentation goes back to or less than base
if !strings.HasPrefix(line, baseIndent+" ") && !strings.HasPrefix(line, baseIndent+"\t") {
break
}
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "- ") {
// already a list
return false
}
// prefix '- '
childIndent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
arr[j] = childIndent + "- " + strings.TrimSpace(line)
*lines = arr
return true
}
return false
}
// convertScalarToEmptyList changes `key: <scalar>` to `key: []` on the same line
func convertScalarToEmptyList(lines *[]string, key string) bool {
arr := *lines
for i, l := range arr {
trimmed := strings.TrimSpace(l)
if strings.HasPrefix(trimmed, key) {
// If already ends with ':' leave for wrapper; else replace value with []
if strings.HasSuffix(trimmed, ":") {
return false
}
// Replace everything after the first ':' with [] preserving indentation/key
parts := strings.SplitN(l, ":", 2)
if len(parts) == 2 {
arr[i] = parts[0] + ": []"
*lines = arr
return true
}
}
}
return false
}
// indexFromField extracts the numeric index from a path like spec.collectors[1] or spec.analyzers[0]
func indexFromField(field string) int {
// find [number]
start := strings.Index(field, "[")
end := strings.Index(field, "]")
if start == -1 || end == -1 || end <= start+1 {
return 0
}
numStr := field[start+1 : end]
// naive parse
n := 0
for _, ch := range numStr {
if ch < '0' || ch > '9' {
return 0
}
n = n*10 + int(ch-'0')
}
return n
}
// insertMissingFieldUnderListItem inserts "fieldName: <placeholder>" under list item at startIdx
// Placeholder is [] for outcomes, "" otherwise. Preserves indentation by using the next child indentation if available
func insertMissingFieldUnderListItem(lines *[]string, startIdx int, fieldName string) bool {
arr := *lines
baseLine := arr[startIdx]
baseIndent := baseLine[:len(baseLine)-len(strings.TrimLeft(baseLine, " \t"))]
// Determine child indentation: prefer next non-empty line's indent if deeper than base
childIndent := baseIndent + " "
insertPos := startIdx + 1
for j := startIdx + 1; j < len(arr); j++ {
if strings.TrimSpace(arr[j]) == "" {
insertPos = j + 1
continue
}
lineIndent := arr[j][:len(arr[j])-len(strings.TrimLeft(arr[j], " \t"))]
if len(lineIndent) > len(baseIndent) {
childIndent = lineIndent
insertPos = j
}
break
}
// Choose placeholder
placeholder := "\"\""
if fieldName == "outcomes" {
placeholder = "[]"
}
// Insert new line
newLine := childIndent + fieldName + ": " + placeholder
// Avoid duplicate insert if the field already exists within this block
for k := startIdx + 1; k < len(arr); k++ {
if strings.TrimSpace(arr[k]) == "" {
continue
}
// Stop when block ends (indentation returns to base or less)
kIndent := arr[k][:len(arr[k])-len(strings.TrimLeft(arr[k], " \t"))]
if len(kIndent) <= len(baseIndent) {
break
}
if strings.HasPrefix(strings.TrimSpace(arr[k]), fieldName+":") {
return false
}
}
arr = append(arr[:insertPos], append([]string{newLine}, arr[insertPos:]...)...)
*lines = arr
return true
}
// between extracts substring between prefix and suffix (first occurrences)
func between(s, prefix, suffix string) string {
i := strings.Index(s, prefix)
if i == -1 {
return ""
}
s2 := s[i+len(prefix):]
j := strings.Index(s2, suffix)
if j == -1 {
return ""
}
return s2[:j]
}
// betweenAfter extracts substring between prefix and suffix starting search after prefix
func betweenAfter(s, prefix, suffix string) string {
i := strings.Index(s, prefix)
if i == -1 {
return ""
}
s2 := s[i+len(prefix):]
j := strings.Index(s2, suffix)
if j == -1 {
return ""
}
return s2[:j]
}
-103
View File
@@ -1,103 +0,0 @@
package lint
import (
"fmt"
"strings"
)
// FormatResults formats lint results for output
func FormatResults(results []LintResult, format string) string {
if format == "json" {
return formatJSON(results)
}
return formatText(results)
}
func formatText(results []LintResult) string {
var output strings.Builder
totalErrors := 0
totalWarnings := 0
for _, result := range results {
if len(result.Errors) == 0 && len(result.Warnings) == 0 {
output.WriteString(fmt.Sprintf("✓ %s: No issues found\n", result.FilePath))
continue
}
output.WriteString(fmt.Sprintf("\n%s:\n", result.FilePath))
for _, err := range result.Errors {
output.WriteString(fmt.Sprintf(" ✗ Error (line %d): %s\n", err.Line, err.Message))
if err.Field != "" {
output.WriteString(fmt.Sprintf(" Field: %s\n", err.Field))
}
totalErrors++
}
for _, warn := range result.Warnings {
output.WriteString(fmt.Sprintf(" ⚠ Warning (line %d): %s\n", warn.Line, warn.Message))
if warn.Field != "" {
output.WriteString(fmt.Sprintf(" Field: %s\n", warn.Field))
}
totalWarnings++
}
}
output.WriteString(fmt.Sprintf("\nSummary: %d error(s), %d warning(s) across %d file(s)\n", totalErrors, totalWarnings, len(results)))
return output.String()
}
func formatJSON(results []LintResult) string {
// Simple JSON formatting without importing encoding/json
var output strings.Builder
output.WriteString("{\n")
output.WriteString(" \"results\": [\n")
for i, result := range results {
output.WriteString(" {\n")
output.WriteString(fmt.Sprintf(" \"filePath\": %q,\n", result.FilePath))
output.WriteString(" \"errors\": [\n")
for j, err := range result.Errors {
output.WriteString(" {\n")
output.WriteString(fmt.Sprintf(" \"line\": %d,\n", err.Line))
output.WriteString(fmt.Sprintf(" \"column\": %d,\n", err.Column))
output.WriteString(fmt.Sprintf(" \"message\": %q,\n", err.Message))
output.WriteString(fmt.Sprintf(" \"field\": %q\n", err.Field))
output.WriteString(" }")
if j < len(result.Errors)-1 {
output.WriteString(",")
}
output.WriteString("\n")
}
output.WriteString(" ],\n")
output.WriteString(" \"warnings\": [\n")
for j, warn := range result.Warnings {
output.WriteString(" {\n")
output.WriteString(fmt.Sprintf(" \"line\": %d,\n", warn.Line))
output.WriteString(fmt.Sprintf(" \"column\": %d,\n", warn.Column))
output.WriteString(fmt.Sprintf(" \"message\": %q,\n", warn.Message))
output.WriteString(fmt.Sprintf(" \"field\": %q\n", warn.Field))
output.WriteString(" }")
if j < len(result.Warnings)-1 {
output.WriteString(",")
}
output.WriteString("\n")
}
output.WriteString(" ]\n")
output.WriteString(" }")
if i < len(results)-1 {
output.WriteString(",")
}
output.WriteString("\n")
}
output.WriteString(" ]\n")
output.WriteString("}\n")
return output.String()
}
-63
View File
@@ -1,63 +0,0 @@
package lint
import (
"fmt"
"regexp"
"strings"
)
// findLineNumber returns the first 1-based line number containing the search string
func findLineNumber(content, search string) int {
lines := strings.Split(content, "\n")
for i, line := range lines {
if strings.Contains(line, search) {
return i + 1
}
}
return 0
}
func findAnalyzerLine(content string, index int) int {
return findListItemLine(content, "analyzers", index)
}
// findCollectorLine locates the starting line of the Nth entry in a collectors list
func findCollectorLine(content string, field string, index int) int {
return findListItemLine(content, field, index)
}
// findListItemLine locates the starting line of the Nth entry in a list under listKey
func findListItemLine(content, listKey string, index int) int {
lines := strings.Split(content, "\n")
count := 0
inList := false
for i, line := range lines {
if strings.Contains(line, listKey+":") {
inList = true
continue
}
if inList && strings.HasPrefix(strings.TrimSpace(line), "- ") {
if count == index {
return i + 1
}
count++
}
if inList && !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") && strings.TrimSpace(line) != "" {
break
}
}
return 0
}
// extractLineFromError tries to parse a YAML error message for a line number
func extractLineFromError(err error) int {
// Try to extract line number from YAML error message
re := regexp.MustCompile(`line (\d+)`)
matches := re.FindStringSubmatch(err.Error())
if len(matches) > 1 {
var line int
fmt.Sscanf(matches[1], "%d", &line)
return line
}
return 0
}
+727 -284
View File
File diff suppressed because it is too large Load Diff
-466
View File
@@ -1,466 +0,0 @@
package lint
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestLintMultipleFiles(t *testing.T) {
// Get the project root by going up from pkg/lint
projectRoot := filepath.Join("..", "..")
testDir := filepath.Join(projectRoot, "examples", "test-error-messages")
tests := []struct {
name string
files []string
valuesFiles []string // values files for v1beta3 specs
expectErrors map[string][]string // filename -> expected error substrings
expectWarnings map[string][]string // filename -> expected warning substrings
expectPass map[string]bool // filename -> should pass without errors
}{
{
name: "valid v1beta3 with templates",
files: []string{
"helm-builtins-v1beta3.yaml",
},
valuesFiles: []string{
"values-helm-builtins.yaml",
},
expectErrors: map[string][]string{},
expectWarnings: map[string][]string{},
expectPass: map[string]bool{
"helm-builtins-v1beta3.yaml": true,
},
},
{
name: "invalid collectors and analyzers",
files: []string{
"invalid-collectors-analyzers.yaml",
},
valuesFiles: []string{
"values-empty.yaml",
},
expectErrors: map[string][]string{
"invalid-collectors-analyzers.yaml": {
// The linter may stop early due to structural issues
// At minimum, it should catch the hostCollectors type error
"Expected 'hostCollectors' to be a list",
},
},
expectPass: map[string]bool{
"invalid-collectors-analyzers.yaml": false,
},
},
{
name: "missing required fields",
files: []string{
"missing-apiversion-v1beta3.yaml",
"missing-metadata-v1beta3.yaml",
"no-analyzers-v1beta3.yaml",
},
valuesFiles: []string{
"values-empty.yaml",
},
expectErrors: map[string][]string{
"missing-apiversion-v1beta3.yaml": {
"Missing or empty 'apiVersion' field",
},
"missing-metadata-v1beta3.yaml": {
"Missing 'metadata' section",
},
"no-analyzers-v1beta3.yaml": {
"Preflight spec must contain 'analyzers'",
},
},
expectPass: map[string]bool{
"missing-apiversion-v1beta3.yaml": false,
"missing-metadata-v1beta3.yaml": false,
"no-analyzers-v1beta3.yaml": false,
},
},
{
name: "v1beta2 file (valid but with docString warning)",
files: []string{
"wrong-apiversion-v1beta3.yaml", // Actually has v1beta2 which is valid
},
expectErrors: map[string][]string{},
expectWarnings: map[string][]string{
"wrong-apiversion-v1beta3.yaml": {
"Some analyzers are missing docString",
},
},
expectPass: map[string]bool{
"wrong-apiversion-v1beta3.yaml": true, // No errors, just warnings
},
},
{
name: "support bundle specs",
files: []string{
"support-bundle-no-collectors-v1beta3.yaml",
"support-bundle-valid-v1beta3.yaml",
},
valuesFiles: []string{
"values-empty.yaml",
},
expectErrors: map[string][]string{
"support-bundle-no-collectors-v1beta3.yaml": {
"SupportBundle spec must contain 'collectors' or 'hostCollectors'",
},
},
expectPass: map[string]bool{
"support-bundle-no-collectors-v1beta3.yaml": false,
"support-bundle-valid-v1beta3.yaml": true,
},
},
{
name: "multiple files with mixed validity",
files: []string{
"support-bundle-valid-v1beta3.yaml",
"missing-metadata-v1beta3.yaml",
"wrong-apiversion-v1beta3.yaml",
},
valuesFiles: []string{
"values-empty.yaml",
},
expectErrors: map[string][]string{
"missing-metadata-v1beta3.yaml": {
"Missing 'metadata' section",
},
},
expectWarnings: map[string][]string{
"wrong-apiversion-v1beta3.yaml": {
"Some analyzers are missing docString",
},
},
expectPass: map[string]bool{
"support-bundle-valid-v1beta3.yaml": true,
"missing-metadata-v1beta3.yaml": false,
"wrong-apiversion-v1beta3.yaml": true, // No errors, just warnings
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Build full file paths
filePaths := make([]string, len(tt.files))
for i, f := range tt.files {
filePaths[i] = filepath.Join(testDir, f)
// Check file exists
if _, err := os.Stat(filePaths[i]); os.IsNotExist(err) {
t.Skipf("Test file %s does not exist, skipping", filePaths[i])
}
}
// Build values file paths
var valuesFilePaths []string
for _, vf := range tt.valuesFiles {
valuesFilePaths = append(valuesFilePaths, filepath.Join(testDir, vf))
}
// Run linter
opts := LintOptions{
FilePaths: filePaths,
Fix: false,
Format: "text",
ValuesFiles: valuesFilePaths,
}
results, err := LintFiles(opts)
if err != nil {
t.Fatalf("LintFiles failed: %v", err)
}
// Verify we got results for all files
if len(results) != len(filePaths) {
t.Errorf("Expected %d results, got %d", len(filePaths), len(results))
}
// Check each result
for _, result := range results {
filename := filepath.Base(result.FilePath)
// Check expected errors
if expectedErrors, ok := tt.expectErrors[filename]; ok {
if len(expectedErrors) > 0 && len(result.Errors) == 0 {
t.Errorf("File %s: expected errors but got none", filename)
}
for _, expectedErr := range expectedErrors {
found := false
for _, err := range result.Errors {
if strings.Contains(err.Message, expectedErr) {
found = true
break
}
}
if !found {
t.Errorf("File %s: expected error containing '%s' but not found in errors: %v",
filename, expectedErr, getErrorMessages(result.Errors))
}
}
}
// Check expected warnings
if expectedWarnings, ok := tt.expectWarnings[filename]; ok {
for _, expectedWarn := range expectedWarnings {
found := false
for _, warn := range result.Warnings {
if strings.Contains(warn.Message, expectedWarn) {
found = true
break
}
}
if !found {
t.Errorf("File %s: expected warning containing '%s' but not found in warnings: %v",
filename, expectedWarn, getWarningMessages(result.Warnings))
}
}
}
// Check if should pass
if shouldPass, ok := tt.expectPass[filename]; ok {
hasNoErrors := len(result.Errors) == 0
if shouldPass && !hasNoErrors {
t.Errorf("File %s: expected to pass but has errors: %v",
filename, getErrorMessages(result.Errors))
} else if !shouldPass && hasNoErrors && len(tt.expectErrors[filename]) > 0 {
t.Errorf("File %s: expected to fail but passed", filename)
}
}
}
})
}
}
func TestLintWithFix(t *testing.T) {
// Create a temporary directory for test files
tmpDir, err := os.MkdirTemp("", "lint-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
tests := []struct {
name string
content string
expectFix bool
fixedContent string // substring that should appear after fix
}{
{
name: "fix v1beta2 with templates to v1beta3",
content: `apiVersion: troubleshoot.sh/v1beta2
kind: Preflight
metadata:
name: test-{{ .Values.name }}
spec:
analyzers:
- clusterVersion:
outcomes:
- pass:
when: '>= 1.19.0'
message: OK`,
expectFix: true,
fixedContent: "apiVersion: troubleshoot.sh/v1beta3",
},
{
name: "v1beta3 template syntax error is reported",
content: `apiVersion: troubleshoot.sh/v1beta3
kind: Preflight
metadata:
name: test-{{ Values.name }}
spec:
analyzers:
- clusterVersion:
outcomes:
- pass:
when: '>= 1.19.0'
message: OK`,
expectFix: false, // Template errors in v1beta3 are not auto-fixable, rendering will fail
fixedContent: "Failed to render v1beta3 template", // Expect an error message
},
}
// Create empty values file for v1beta3 tests
emptyValuesFile := filepath.Join(tmpDir, "values-empty.yaml")
if err := os.WriteFile(emptyValuesFile, []byte("{}"), 0644); err != nil {
t.Fatalf("Failed to write empty values file: %v", err)
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Write test content to temp file
testFile := filepath.Join(tmpDir, tt.name+".yaml")
if err := os.WriteFile(testFile, []byte(tt.content), 0644); err != nil {
t.Fatalf("Failed to write test file: %v", err)
}
// Run linter with fix enabled
opts := LintOptions{
FilePaths: []string{testFile},
Fix: true,
Format: "text",
ValuesFiles: []string{emptyValuesFile},
}
results, err := LintFiles(opts)
if err != nil {
t.Fatalf("LintFiles failed: %v", err)
}
if len(results) != 1 {
t.Fatalf("Expected 1 result, got %d", len(results))
}
// Read the potentially fixed content
fixedBytes, err := os.ReadFile(testFile)
if err != nil {
t.Fatalf("Failed to read fixed file: %v", err)
}
fixedContent := string(fixedBytes)
// Check if fix was applied or error was reported
if tt.expectFix {
if !strings.Contains(fixedContent, tt.fixedContent) {
t.Errorf("Expected fixed content to contain '%s', but got:\n%s",
tt.fixedContent, fixedContent)
}
} else {
// For tests that don't expect fix, check for errors
if len(results[0].Errors) > 0 {
errorFound := false
for _, err := range results[0].Errors {
if strings.Contains(err.Message, tt.fixedContent) {
errorFound = true
break
}
}
if !errorFound {
t.Errorf("Expected error containing '%s', but got errors: %v",
tt.fixedContent, results[0].Errors)
}
}
}
})
}
}
func TestHasErrors(t *testing.T) {
tests := []struct {
name string
results []LintResult
expected bool
}{
{
name: "no errors",
results: []LintResult{
{
FilePath: "test1.yaml",
Errors: []LintError{},
Warnings: []LintWarning{{Message: "warning"}},
},
{
FilePath: "test2.yaml",
Errors: []LintError{},
},
},
expected: false,
},
{
name: "has errors",
results: []LintResult{
{
FilePath: "test1.yaml",
Errors: []LintError{{Message: "error"}},
},
{
FilePath: "test2.yaml",
Errors: []LintError{},
},
},
expected: true,
},
{
name: "empty results",
results: []LintResult{},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := HasErrors(tt.results)
if result != tt.expected {
t.Errorf("HasErrors() = %v, want %v", result, tt.expected)
}
})
}
}
func TestFormatResults(t *testing.T) {
results := []LintResult{
{
FilePath: "test.yaml",
Errors: []LintError{
{Line: 5, Message: "Missing field", Field: "spec.analyzers"},
},
Warnings: []LintWarning{
{Line: 10, Message: "Consider adding docString", Field: "spec.analyzers[0]"},
},
},
}
t.Run("text format", func(t *testing.T) {
output := FormatResults(results, "text")
// Check for key components in text output
if !strings.Contains(output, "test.yaml") {
t.Error("Text output missing file path")
}
if !strings.Contains(output, "Error (line 5)") {
t.Error("Text output missing error with line number")
}
if !strings.Contains(output, "Warning (line 10)") {
t.Error("Text output missing warning with line number")
}
if !strings.Contains(output, "Summary:") {
t.Error("Text output missing summary")
}
})
t.Run("json format", func(t *testing.T) {
output := FormatResults(results, "json")
// Check for key JSON components
if !strings.Contains(output, `"filePath"`) {
t.Error("JSON output missing filePath field")
}
if !strings.Contains(output, `"errors"`) {
t.Error("JSON output missing errors field")
}
if !strings.Contains(output, `"warnings"`) {
t.Error("JSON output missing warnings field")
}
if !strings.Contains(output, `"line": 5`) {
t.Error("JSON output missing line number")
}
})
}
// Helper functions
func getErrorMessages(errors []LintError) []string {
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Message
}
return messages
}
func getWarningMessages(warnings []LintWarning) []string {
messages := make([]string, len(warnings))
for i, warn := range warnings {
messages[i] = warn.Message
}
return messages
}
-181
View File
@@ -1,181 +0,0 @@
package lint
import (
"fmt"
"regexp"
"sort"
"strings"
)
// detectAPIVersionFromContent tries to extract apiVersion from raw YAML text
func detectAPIVersionFromContent(content string) string {
lines := strings.Split(content, "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "apiVersion:") {
parts := strings.SplitN(trimmed, ":", 2)
if len(parts) == 2 {
val := strings.TrimSpace(parts[1])
// strip quotes if present
val = strings.Trim(val, "'\"")
return val
}
}
}
return ""
}
// addTemplatingErrorsForAllLines records an error for each line containing template braces in versions that do not support templating
func addTemplatingErrorsForAllLines(result *LintResult, content string) {
lines := strings.Split(content, "\n")
for i, line := range lines {
if strings.Contains(line, "{{") && strings.Contains(line, "}}") {
result.Errors = append(result.Errors, LintError{
Line: i + 1,
Message: "Templating is not supported in v1beta2 specs",
Field: "template",
})
}
}
}
func checkTemplateSyntax(content string) ([]LintError, []string) {
errors := []LintError{}
lines := strings.Split(content, "\n")
templateValueRefs := map[string]bool{}
// Check for unmatched braces
for i, line := range lines {
// Count opening and closing braces
opening := strings.Count(line, "{{")
closing := strings.Count(line, "}}")
if opening != closing {
errors = append(errors, LintError{
Line: i + 1,
Message: fmt.Sprintf("Unmatched template braces: %d opening, %d closing", opening, closing),
})
}
// Check for common template syntax issues
// Look for templates that might be missing the leading dot
if strings.Contains(line, "{{") && strings.Contains(line, "}}") {
// Extract template expressions
templateExpr := extractTemplateBetweenBraces(line)
for _, expr := range templateExpr {
trimmed := strings.TrimSpace(expr)
// Skip empty expressions
if trimmed == "" {
continue
}
// Skip comments: {{/* ... */}}
if strings.HasPrefix(trimmed, "/*") || strings.HasPrefix(trimmed, "*/") {
continue
}
// Track template value references for warning (check this before skipping control structures)
if strings.Contains(trimmed, ".Values.") {
// Extract the value path
valuePattern := regexp.MustCompile(`\.Values\.(\w+(?:\.\w+)*)`)
matches := valuePattern.FindAllStringSubmatch(trimmed, -1)
for _, match := range matches {
if len(match) > 1 {
templateValueRefs[match[1]] = true
}
}
}
// Skip control structures (if, else, end, range, with, etc.)
if isControlStructure(trimmed) {
continue
}
// Skip template variables (start with $)
if strings.HasPrefix(trimmed, "$") {
continue
}
// Skip expressions that start with a dot (valid references)
if strings.HasPrefix(trimmed, ".") {
continue
}
// Skip string literals
if strings.HasPrefix(trimmed, "\"") || strings.HasPrefix(trimmed, "'") {
continue
}
// Skip numeric literals
if regexp.MustCompile(`^[0-9]+$`).MatchString(trimmed) {
continue
}
// Skip function calls (contain parentheses or pipes)
if strings.Contains(trimmed, "(") || strings.Contains(trimmed, "|") {
continue
}
// Skip known Helm functions/keywords
helmFunctions := []string{"toYaml", "toJson", "include", "required", "default", "quote", "nindent", "indent", "upper", "lower", "trim"}
isFunction := false
for _, fn := range helmFunctions {
if strings.HasPrefix(trimmed, fn+" ") || trimmed == fn {
isFunction = true
break
}
}
if isFunction {
continue
}
// If we got here, it might be missing a leading dot
errors = append(errors, LintError{
Line: i + 1,
Message: fmt.Sprintf("Template expression may be missing leading dot: {{ %s }}", expr),
})
}
}
}
// Collect template values that need to be provided at runtime
var valueList []string
for val := range templateValueRefs {
valueList = append(valueList, val)
}
// Sort for consistent output
sort.Strings(valueList)
return errors, valueList
}
// extractTemplateBetweenBraces extracts template expressions from a line
func extractTemplateBetweenBraces(line string) []string {
var expressions []string
// Match {{ ... }} with optional whitespace trimming (-), including comments {{/* */}}
re := regexp.MustCompile(`\{\{-?\s*(.+?)\s*-?\}\}`)
matches := re.FindAllStringSubmatch(line, -1)
for _, match := range matches {
if len(match) > 1 {
// Clean up the expression
expr := match[1]
// Remove */ at the end if it's part of a comment
expr = strings.TrimSuffix(strings.TrimSpace(expr), "*/")
expressions = append(expressions, expr)
}
}
return expressions
}
// isControlStructure checks if a template expression is a control structure
func isControlStructure(expr string) bool {
trimmed := strings.TrimSpace(expr)
controlKeywords := []string{"if", "else", "end", "range", "with", "define", "template", "block", "include"}
for _, keyword := range controlKeywords {
if strings.HasPrefix(trimmed, keyword+" ") || trimmed == keyword {
return true
}
}
return false
}
-41
View File
@@ -1,41 +0,0 @@
package lint
// Core types used by the lint package
type LintResult struct {
FilePath string
Errors []LintError
Warnings []LintWarning
}
type LintError struct {
Line int
Column int
Message string
Field string
}
type LintWarning struct {
Line int
Column int
Message string
Field string
}
type LintOptions struct {
FilePaths []string
Fix bool
Format string // "text" or "json"
ValuesFiles []string // Path to YAML files with template values (for v1beta3)
SetValues []string // Template values from command line (for v1beta3)
}
// HasErrors returns true if any of the results contain errors
func HasErrors(results []LintResult) bool {
for _, result := range results {
if len(result.Errors) > 0 {
return true
}
}
return false
}
-452
View File
@@ -1,452 +0,0 @@
package lint
import (
"fmt"
"os"
"path/filepath"
"strings"
"encoding/json"
)
func checkRequiredFields(parsed map[string]interface{}, content string) []LintError {
errors := []LintError{}
// Check apiVersion
if apiVersion, ok := parsed["apiVersion"].(string); !ok || apiVersion == "" {
errors = append(errors, LintError{
Line: findLineNumber(content, "apiVersion"),
Field: "apiVersion",
Message: "Missing 'apiVersion'",
})
}
// Check kind
if kind, ok := parsed["kind"].(string); !ok || kind == "" {
errors = append(errors, LintError{
Line: findLineNumber(content, "kind"),
Field: "kind",
Message: "Missing or empty 'kind' field",
})
} else if kind != "Preflight" && kind != "SupportBundle" {
errors = append(errors, LintError{
Line: findLineNumber(content, "kind"),
Field: "kind",
Message: fmt.Sprintf("Expected kind 'Preflight' or 'SupportBundle' (found '%s')", kind),
})
}
// Check metadata
if _, ok := parsed["metadata"]; !ok {
errors = append(errors, LintError{
Line: findLineNumber(content, "metadata"),
Field: "metadata",
Message: "Missing 'metadata' section",
})
} else if metadata, ok := parsed["metadata"].(map[string]interface{}); ok {
if name, ok := metadata["name"].(string); !ok || name == "" {
errors = append(errors, LintError{
Line: findLineNumber(content, "name"),
Field: "metadata.name",
Message: "Missing 'metadata.name'",
})
}
}
// Check spec
if _, ok := parsed["spec"]; !ok {
errors = append(errors, LintError{
Line: findLineNumber(content, "spec"),
Field: "spec",
Message: "Missing 'spec' section",
})
}
return errors
}
func checkPreflightSpec(parsed map[string]interface{}, content string) []LintError {
errors := []LintError{}
spec, ok := parsed["spec"].(map[string]interface{})
if !ok {
return errors
}
// Check for analyzers
analyzers, hasAnalyzers := spec["analyzers"]
if !hasAnalyzers {
errors = append(errors, LintError{
Line: findLineNumber(content, "spec:"),
Field: "spec.analyzers",
Message: "Preflight spec must contain 'analyzers'",
})
} else if analyzersList, ok := analyzers.([]interface{}); ok {
if len(analyzersList) == 0 {
errors = append(errors, LintError{
Line: findLineNumber(content, "analyzers"),
Field: "spec.analyzers",
Message: "Preflight spec must have at least one analyzer",
})
}
}
return errors
}
func checkSupportBundleSpec(parsed map[string]interface{}, content string) []LintError {
errors := []LintError{}
spec, ok := parsed["spec"].(map[string]interface{})
if !ok {
return errors
}
// Check for collectors
collectors, hasCollectors := spec["collectors"]
_, hasHostCollectors := spec["hostCollectors"]
if !hasCollectors && !hasHostCollectors {
errors = append(errors, LintError{
Line: findLineNumber(content, "spec:"),
Field: "spec.collectors",
Message: "SupportBundle spec must contain 'collectors' or 'hostCollectors'",
})
} else {
// Check if collectors list is empty
if hasCollectors {
if collectorsList, ok := collectors.([]interface{}); ok && len(collectorsList) == 0 {
errors = append(errors, LintError{
Line: findLineNumber(content, "collectors"),
Field: "spec.collectors",
Message: "Collectors list is empty",
})
}
}
}
return errors
}
// checkCommonIssues aggregates advisory warnings based on best practices
func checkCommonIssues(parsed map[string]interface{}, content string, apiVersion string, templateValueRefs []string) []LintWarning {
warnings := []LintWarning{}
// Check for missing docStrings in analyzers and collectors
spec, ok := parsed["spec"].(map[string]interface{})
if !ok {
return warnings
}
// Check if any analyzers are missing docString
analyzersMissingDocString := false
if analyzers, ok := spec["analyzers"].([]interface{}); ok {
for _, analyzer := range analyzers {
if analyzerMap, ok := analyzer.(map[string]interface{}); ok {
// Check if docString exists at the analyzer level (v1beta3)
if _, hasDocString := analyzerMap["docString"]; !hasDocString {
analyzersMissingDocString = true
break
}
}
}
}
// Check if any collectors are missing docString
collectorsMissingDocString := false
if collectors, ok := spec["collectors"].([]interface{}); ok {
for _, collector := range collectors {
if collectorMap, ok := collector.(map[string]interface{}); ok {
// Get the actual collector type (first key-value pair)
for _, collectorSpec := range collectorMap {
if specMap, ok := collectorSpec.(map[string]interface{}); ok {
if _, hasDocString := specMap["docString"]; !hasDocString {
collectorsMissingDocString = true
break
}
}
// Only check the first key since collectors have single type
break
}
}
if collectorsMissingDocString {
break
}
}
}
// Add consolidated warnings if any items are missing docString
if analyzersMissingDocString && collectorsMissingDocString {
warnings = append(warnings, LintWarning{
Line: findLineNumber(content, "spec:"),
Field: "spec",
Message: "Some analyzers and collectors are missing docString (recommended for v1beta3)",
})
} else if analyzersMissingDocString {
warnings = append(warnings, LintWarning{
Line: findLineNumber(content, "analyzers:"),
Field: "spec.analyzers",
Message: "Some analyzers are missing docString (recommended for v1beta3)",
})
} else if collectorsMissingDocString {
warnings = append(warnings, LintWarning{
Line: findLineNumber(content, "collectors:"),
Field: "spec.collectors",
Message: "Some collectors are missing docString (recommended for v1beta3)",
})
}
// Add warning about template values that need to be provided at runtime (v1beta3 only)
if apiVersion == "troubleshoot.sh/v1beta3" && len(templateValueRefs) > 0 {
warnings = append(warnings, LintWarning{
Line: 1,
Field: "template-values",
Message: fmt.Sprintf("Template values that must be provided at runtime: %s", strings.Join(templateValueRefs, ", ")),
})
}
return warnings
}
// --- Schema-backed quick validation (best-effort) ---
type schemaTypeInfo struct {
required map[string]struct{}
properties map[string]struct{}
}
var (
knownAnalyzerTypes map[string]struct{}
knownCollectorTypes map[string]struct{}
analyzerTypeInfo map[string]schemaTypeInfo
collectorTypeInfo map[string]schemaTypeInfo
knownTypesLoaded bool
)
func ensureKnownTypesLoaded() {
if knownTypesLoaded {
return
}
knownAnalyzerTypes = map[string]struct{}{}
knownCollectorTypes = map[string]struct{}{}
analyzerTypeInfo = map[string]schemaTypeInfo{}
collectorTypeInfo = map[string]schemaTypeInfo{}
// Analyzer schema (v1beta2)
loadKeysFromSchema(
filepath.Join("schemas", "analyzer-troubleshoot-v1beta2.json"),
[]string{"properties", "spec", "properties", "analyzers", "items", "properties"},
knownAnalyzerTypes,
)
loadTypeInfoFromSchema(
filepath.Join("schemas", "analyzer-troubleshoot-v1beta2.json"),
[]string{"properties", "spec", "properties", "analyzers", "items", "properties"},
analyzerTypeInfo,
)
// Collector schema (v1beta2)
loadKeysFromSchema(
filepath.Join("schemas", "collector-troubleshoot-v1beta2.json"),
[]string{"properties", "spec", "properties", "collectors", "items", "properties"},
knownCollectorTypes,
)
loadTypeInfoFromSchema(
filepath.Join("schemas", "collector-troubleshoot-v1beta2.json"),
[]string{"properties", "spec", "properties", "collectors", "items", "properties"},
collectorTypeInfo,
)
knownTypesLoaded = true
}
// loadKeysFromSchema walks a JSON object by keysPath and adds map keys at that node into dest
func loadKeysFromSchema(schemaPath string, keysPath []string, dest map[string]struct{}) {
data, err := os.ReadFile(schemaPath)
if err != nil {
return
}
var obj map[string]interface{}
if err := json.Unmarshal(data, &obj); err != nil {
return
}
node := interface{}(obj)
for _, key := range keysPath {
m, ok := node.(map[string]interface{})
if !ok {
return
}
node, ok = m[key]
if !ok {
return
}
}
props, ok := node.(map[string]interface{})
if !ok {
return
}
for k := range props {
dest[k] = struct{}{}
}
}
// loadTypeInfoFromSchema records required/properties for each type under the node
func loadTypeInfoFromSchema(schemaPath string, keysPath []string, dest map[string]schemaTypeInfo) {
data, err := os.ReadFile(schemaPath)
if err != nil {
return
}
var obj map[string]interface{}
if err := json.Unmarshal(data, &obj); err != nil {
return
}
node := interface{}(obj)
for _, key := range keysPath {
m, ok := node.(map[string]interface{})
if !ok {
return
}
node, ok = m[key]
if !ok {
return
}
}
typesNode, ok := node.(map[string]interface{})
if !ok {
return
}
for typeName, raw := range typesNode {
m, ok := raw.(map[string]interface{})
if !ok {
continue
}
info := schemaTypeInfo{required: map[string]struct{}{}, properties: map[string]struct{}{}}
if req, ok := m["required"].([]interface{}); ok {
for _, r := range req {
if s, ok := r.(string); ok {
info.required[s] = struct{}{}
}
}
}
if props, ok := m["properties"].(map[string]interface{}); ok {
for prop := range props {
info.properties[prop] = struct{}{}
}
}
dest[typeName] = info
}
}
// validateAnalyzers delegates to the generic typed-list validator
func validateAnalyzers(parsed map[string]interface{}, content string) []LintError {
return validateTypedList(parsed, content, "analyzers", "analyzer", knownAnalyzerTypes, analyzerTypeInfo)
}
func validateCollectors(parsed map[string]interface{}, content string, field string) []LintError {
return validateTypedList(parsed, content, field, "collector", knownCollectorTypes, collectorTypeInfo)
}
// validateTypedList provides generic validation for lists of typed single-key objects
func validateTypedList(
parsed map[string]interface{},
content string,
listKey string,
subject string,
knownTypes map[string]struct{},
typeInfo map[string]schemaTypeInfo,
) []LintError {
var errs []LintError
spec, ok := parsed["spec"].(map[string]interface{})
if !ok {
return errs
}
raw, exists := spec[listKey]
if !exists {
return errs
}
list, ok := raw.([]interface{})
if !ok {
errs = append(errs, LintError{
Line: findLineNumber(content, listKey+":"),
Field: "spec." + listKey,
Message: fmt.Sprintf("Expected '%s' to be a list", listKey),
})
return errs
}
for i, item := range list {
m, ok := item.(map[string]interface{})
if !ok {
errs = append(errs, LintError{
Line: findListItemLine(content, listKey, i),
Field: fmt.Sprintf("spec.%s[%d]", listKey, i),
Message: fmt.Sprintf("Expected %s entry to be a mapping", subject),
})
continue
}
// Count non-docString keys (docString is metadata in v1beta3, not a type)
typeCount := 0
var typ string
var body interface{}
for k, v := range m {
if k == "docString" {
// docString is metadata, not a type - skip it
continue
} else {
typeCount++
typ, body = k, v
}
}
// Check that we have exactly one type (excluding docString)
if typeCount != 1 {
errs = append(errs, LintError{
Line: findListItemLine(content, listKey, i),
Field: fmt.Sprintf("spec.%s[%d]", listKey, i),
Message: fmt.Sprintf("%s entry must specify exactly one %s type", strings.Title(subject), subject),
})
continue
}
// If no actual type was found (only docString), skip further validation
if typ == "" {
continue
}
if len(knownTypes) > 0 {
if _, ok := knownTypes[typ]; !ok {
errs = append(errs, LintError{
Line: findListItemLine(content, listKey, i),
Field: fmt.Sprintf("spec.%s[%d]", listKey, i),
Message: fmt.Sprintf("Unknown %s type '%s'", subject, typ),
})
}
}
bodyMap, ok := body.(map[string]interface{})
if !ok {
errs = append(errs, LintError{
Line: findListItemLine(content, listKey, i),
Field: fmt.Sprintf("spec.%s[%d].%s", listKey, i, typ),
Message: fmt.Sprintf("Expected %s definition to be a mapping", subject),
})
continue
}
if ti, ok := typeInfo[typ]; ok {
for req := range ti.required {
if _, ok := bodyMap[req]; !ok {
errs = append(errs, LintError{
Line: findListItemLine(content, listKey, i),
Field: fmt.Sprintf("spec.%s[%d].%s.%s", listKey, i, typ, req),
Message: fmt.Sprintf("Missing required field '%s' for %s '%s'", req, subject, typ),
})
}
}
for k := range bodyMap {
if _, ok := ti.properties[k]; !ok {
errs = append(errs, LintError{
Line: findListItemLine(content, listKey, i),
Field: fmt.Sprintf("spec.%s[%d].%s.%s", listKey, i, typ, k),
Message: fmt.Sprintf("Unknown field '%s' for %s '%s'", k, subject, typ),
})
}
}
}
}
return errs
}
+2 -157
View File
@@ -8,14 +8,12 @@ import (
"github.com/pkg/errors"
"github.com/replicatedhq/troubleshoot/internal/util"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
troubleshootv1beta3 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta3"
"github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme"
"github.com/replicatedhq/troubleshoot/pkg/constants"
"github.com/replicatedhq/troubleshoot/pkg/docrewrite"
"github.com/replicatedhq/troubleshoot/pkg/types"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes"
"k8s.io/klog/v2"
"sigs.k8s.io/yaml"
)
@@ -25,8 +23,6 @@ var decoder runtime.Decoder
func init() {
// Allow serializing Secrets and ConfigMaps
_ = v1.AddToScheme(scheme.Scheme)
// Ensure v1beta3 Troubleshoot types are registered for decoding
_ = troubleshootv1beta3.AddToScheme(scheme.Scheme)
decoder = scheme.Codecs.UniversalDeserializer()
}
@@ -44,14 +40,6 @@ type LoadOptions struct {
// If true, the loader will return an error if any of the specs are not valid
// else the invalid specs will be ignored
Strict bool
// Client is the kubernetes client used for resolving v1beta3 StringOrValueFrom fields
// If not provided, v1beta3 specs with secretKeyRef will fail to load
Client kubernetes.Interface
// Namespace is the default namespace for resolving v1beta3 secret references
// Defaults to "default" if not provided
Namespace string
}
// TODO: Additional requirements needed in this package
@@ -76,18 +64,8 @@ type LoadOptions struct {
// the documents are not valid, else the invalid documents will be ignored.
func LoadSpecs(ctx context.Context, opt LoadOptions) (*TroubleshootKinds, error) {
opt.RawSpecs = append(opt.RawSpecs, opt.RawSpec)
// Default namespace to "default" if not provided
namespace := opt.Namespace
if namespace == "" {
namespace = "default"
}
l := specLoader{
strict: opt.Strict,
client: opt.Client,
namespace: namespace,
ctx: ctx,
strict: opt.Strict,
}
return l.loadFromStrings(opt.RawSpecs...)
@@ -162,10 +140,7 @@ func NewTroubleshootKinds() *TroubleshootKinds {
}
type specLoader struct {
strict bool
client kubernetes.Interface
namespace string
ctx context.Context
strict bool
}
// loadFromStrings accepts a list of strings (exploded) which should be yaml documents
@@ -241,23 +216,6 @@ func (l *specLoader) loadFromSplitDocs(splitdocs []string) (*TroubleshootKinds,
kinds := NewTroubleshootKinds()
for _, doc := range splitdocs {
// Check if this is a v1beta3 spec
var parsed parsedDoc
if err := yaml.Unmarshal([]byte(doc), &parsed); err == nil && parsed.APIVersion == constants.Troubleshootv1beta3Kind {
// Only handle v1beta3 SupportBundle specially (to resolve valueFrom and convert)
if parsed.Kind == "SupportBundle" {
if err := l.loadV1Beta3Spec(doc, kinds); err != nil {
// Always surface v1beta3 SupportBundle errors so users get actionable guidance
return nil, err
}
// handled as support bundle; move to next doc
continue
}
// For other v1beta3 kinds (e.g., Preflight), fall through to the generic
// v1beta3->v1beta2 conversion path below to preserve prior behavior.
}
// Handle v1beta2 and v1beta1 specs
converted, err := docrewrite.ConvertToV1Beta2([]byte(doc))
if err != nil {
if !l.strict {
@@ -305,119 +263,6 @@ func (l *specLoader) loadFromSplitDocs(splitdocs []string) (*TroubleshootKinds,
return kinds, nil
}
// loadV1Beta3Spec handles loading and resolving v1beta3 specs
func (l *specLoader) loadV1Beta3Spec(doc string, kinds *TroubleshootKinds) error {
// Unmarshal to v1beta3 types
obj, _, err := decoder.Decode([]byte(doc), nil, nil)
if err != nil {
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES,
errors.Wrapf(err, "failed to decode v1beta3 spec: '%s'", doc),
)
}
switch v3spec := obj.(type) {
case *troubleshootv1beta3.SupportBundle:
// Resolve secrets and convert to v1beta2
requiresClient := v1beta3SpecRequiresClient(&v3spec.Spec)
if requiresClient && l.client == nil {
return types.NewExitCodeError(
constants.EXIT_CODE_SPEC_ISSUES,
errors.New("kubernetes client required"),
)
}
v2spec, err := troubleshootv1beta3.ConvertToV1Beta2WithResolution(l.ctx, &v3spec.Spec, l.client, l.namespace)
if err != nil {
// When secret/configmap references are present, show a clear guidance message
// instead of leaking underlying RBAC or lookup errors.
if requiresClient {
return types.NewExitCodeError(
constants.EXIT_CODE_SPEC_ISSUES,
errors.New("this v1beta3 SupportBundle uses secret/configmap references and must be run in a cluster"),
)
}
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES,
errors.Wrap(err, "failed to resolve and convert v1beta3 support bundle spec"),
)
}
// Create v1beta2 support bundle
v2bundle := troubleshootv1beta2.SupportBundle{
TypeMeta: v3spec.TypeMeta,
ObjectMeta: v3spec.ObjectMeta,
Spec: *v2spec,
}
// Update apiVersion to v1beta2
v2bundle.APIVersion = constants.Troubleshootv1beta2Kind
kinds.SupportBundlesV1Beta2 = append(kinds.SupportBundlesV1Beta2, v2bundle)
// TODO: Add other v1beta3 types as they are implemented
default:
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES,
errors.Errorf("unsupported v1beta3 kind: %T", v3spec),
)
}
return nil
}
// v1beta3SpecRequiresClient returns true if the v1beta3 spec contains any
// StringOrValueFrom references that require fetching from the cluster.
func v1beta3SpecRequiresClient(spec *troubleshootv1beta3.SupportBundleSpec) bool {
if spec == nil || spec.Collectors == nil {
return false
}
for _, c := range spec.Collectors {
if c == nil {
continue
}
// Database collectors
if c.Postgres != nil && databaseRequiresClient(c.Postgres) {
return true
}
if c.Mysql != nil && databaseRequiresClient(c.Mysql) {
return true
}
if c.Mssql != nil && databaseRequiresClient(c.Mssql) {
return true
}
if c.Redis != nil && databaseRequiresClient(c.Redis) {
return true
}
}
return false
}
func databaseRequiresClient(db *troubleshootv1beta3.Database) bool {
if db == nil {
return false
}
if stringOrValueFromHasRef(db.URI) {
return true
}
if db.TLS != nil {
if stringOrValueFromHasRef(db.TLS.CACert) ||
stringOrValueFromHasRef(db.TLS.ClientCert) ||
stringOrValueFromHasRef(db.TLS.ClientKey) {
return true
}
}
return false
}
func stringOrValueFromHasRef(s troubleshootv1beta3.StringOrValueFrom) bool {
if s.ValueFrom == nil {
return false
}
return s.ValueFrom.SecretKeyRef != nil || s.ValueFrom.ConfigMapKeyRef != nil
}
func isSecret(parsedDocHead parsedDoc) bool {
if parsedDocHead.Kind == "Secret" && parsedDocHead.APIVersion == "v1" {
return true
-289
View File
@@ -1,289 +0,0 @@
package loader
import (
"context"
"testing"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)
func TestLoadSpecs_V1Beta3WithSecretRef(t *testing.T) {
// Create test secret
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "postgres-secret",
Namespace: "default",
},
Data: map[string][]byte{
"uri": []byte("postgresql://user:password@localhost:5432/mydb"),
},
}
client := fake.NewSimpleClientset(secret)
spec := `
apiVersion: troubleshoot.sh/v1beta3
kind: SupportBundle
metadata:
name: test-bundle
spec:
collectors:
- postgres:
collectorName: main-db
uri:
valueFrom:
secretKeyRef:
name: postgres-secret
key: uri
`
kinds, err := LoadSpecs(context.Background(), LoadOptions{
RawSpec: spec,
Client: client,
Namespace: "default",
})
require.NoError(t, err)
require.NotNil(t, kinds)
require.Len(t, kinds.SupportBundlesV1Beta2, 1)
bundle := kinds.SupportBundlesV1Beta2[0]
assert.Equal(t, "test-bundle", bundle.Name)
assert.Equal(t, "troubleshoot.sh/v1beta2", bundle.APIVersion)
require.Len(t, bundle.Spec.Collectors, 1)
require.NotNil(t, bundle.Spec.Collectors[0].Postgres)
assert.Equal(t, "main-db", bundle.Spec.Collectors[0].Postgres.CollectorName)
assert.Equal(t, "postgresql://user:password@localhost:5432/mydb", bundle.Spec.Collectors[0].Postgres.URI)
}
func TestLoadSpecs_V1Beta3WithTLSSecrets(t *testing.T) {
// Create test secret with TLS certs
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "tls-secret",
Namespace: "default",
},
Data: map[string][]byte{
"ca.crt": []byte("CA_CERT_DATA"),
"client.crt": []byte("CLIENT_CERT_DATA"),
"client.key": []byte("CLIENT_KEY_DATA"),
},
}
client := fake.NewSimpleClientset(secret)
spec := `
apiVersion: troubleshoot.sh/v1beta3
kind: SupportBundle
metadata:
name: test-bundle
spec:
collectors:
- postgres:
uri:
value: "postgresql://localhost:5432/db"
tls:
cacert:
valueFrom:
secretKeyRef:
name: tls-secret
key: ca.crt
clientCert:
valueFrom:
secretKeyRef:
name: tls-secret
key: client.crt
clientKey:
valueFrom:
secretKeyRef:
name: tls-secret
key: client.key
`
kinds, err := LoadSpecs(context.Background(), LoadOptions{
RawSpec: spec,
Client: client,
Namespace: "default",
})
require.NoError(t, err)
require.NotNil(t, kinds)
require.Len(t, kinds.SupportBundlesV1Beta2, 1)
bundle := kinds.SupportBundlesV1Beta2[0]
require.Len(t, bundle.Spec.Collectors, 1)
require.NotNil(t, bundle.Spec.Collectors[0].Postgres)
require.NotNil(t, bundle.Spec.Collectors[0].Postgres.TLS)
assert.Equal(t, "CA_CERT_DATA", bundle.Spec.Collectors[0].Postgres.TLS.CACert)
assert.Equal(t, "CLIENT_CERT_DATA", bundle.Spec.Collectors[0].Postgres.TLS.ClientCert)
assert.Equal(t, "CLIENT_KEY_DATA", bundle.Spec.Collectors[0].Postgres.TLS.ClientKey)
}
func TestLoadSpecs_V1Beta3MultipleCollectors(t *testing.T) {
pgSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "postgres-secret",
Namespace: "default",
},
Data: map[string][]byte{
"uri": []byte("postgresql://localhost:5432/db"),
},
}
mysqlSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "mysql-secret",
Namespace: "default",
},
Data: map[string][]byte{
"uri": []byte("mysql://localhost:3306/db"),
},
}
client := fake.NewSimpleClientset(pgSecret, mysqlSecret)
spec := `
apiVersion: troubleshoot.sh/v1beta3
kind: SupportBundle
metadata:
name: test-bundle
spec:
collectors:
- postgres:
uri:
valueFrom:
secretKeyRef:
name: postgres-secret
key: uri
- mysql:
uri:
valueFrom:
secretKeyRef:
name: mysql-secret
key: uri
`
kinds, err := LoadSpecs(context.Background(), LoadOptions{
RawSpec: spec,
Client: client,
Namespace: "default",
})
require.NoError(t, err)
require.NotNil(t, kinds)
require.Len(t, kinds.SupportBundlesV1Beta2, 1)
bundle := kinds.SupportBundlesV1Beta2[0]
require.Len(t, bundle.Spec.Collectors, 2)
require.NotNil(t, bundle.Spec.Collectors[0].Postgres)
assert.Equal(t, "postgresql://localhost:5432/db", bundle.Spec.Collectors[0].Postgres.URI)
require.NotNil(t, bundle.Spec.Collectors[1].Mysql)
assert.Equal(t, "mysql://localhost:3306/db", bundle.Spec.Collectors[1].Mysql.URI)
}
func TestLoadSpecs_V1Beta3WithoutClient(t *testing.T) {
spec := `
apiVersion: troubleshoot.sh/v1beta3
kind: SupportBundle
metadata:
name: test-bundle
spec:
collectors:
- postgres:
uri:
valueFrom:
secretKeyRef:
name: postgres-secret
key: uri
`
_, err := LoadSpecs(context.Background(), LoadOptions{
RawSpec: spec,
Strict: true, // Enable strict mode to get error instead of warning
// No client provided
})
require.Error(t, err)
assert.Contains(t, err.Error(), "kubernetes client required")
}
func TestLoadSpecs_V1Beta3MixedWithV1Beta2(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "postgres-secret",
Namespace: "default",
},
Data: map[string][]byte{
"uri": []byte("postgresql://localhost:5432/db"),
},
}
client := fake.NewSimpleClientset(secret)
specs := `
---
apiVersion: troubleshoot.sh/v1beta3
kind: SupportBundle
metadata:
name: v1beta3-bundle
spec:
collectors:
- postgres:
uri:
valueFrom:
secretKeyRef:
name: postgres-secret
key: uri
---
apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: v1beta2-bundle
spec:
collectors:
- clusterInfo: {}
`
kinds, err := LoadSpecs(context.Background(), LoadOptions{
RawSpec: specs,
Client: client,
Namespace: "default",
})
require.NoError(t, err)
require.NotNil(t, kinds)
require.Len(t, kinds.SupportBundlesV1Beta2, 2)
// Find the v1beta3-converted bundle
var v3Bundle *troubleshootv1beta2.SupportBundle
var v2Bundle *troubleshootv1beta2.SupportBundle
for i := range kinds.SupportBundlesV1Beta2 {
if kinds.SupportBundlesV1Beta2[i].Name == "v1beta3-bundle" {
v3Bundle = &kinds.SupportBundlesV1Beta2[i]
}
if kinds.SupportBundlesV1Beta2[i].Name == "v1beta2-bundle" {
v2Bundle = &kinds.SupportBundlesV1Beta2[i]
}
}
require.NotNil(t, v3Bundle, "v1beta3 bundle should be converted and loaded")
require.NotNil(t, v2Bundle, "v1beta2 bundle should be loaded")
// Verify v1beta3 bundle was resolved correctly
require.Len(t, v3Bundle.Spec.Collectors, 1)
require.NotNil(t, v3Bundle.Spec.Collectors[0].Postgres)
assert.Equal(t, "postgresql://localhost:5432/db", v3Bundle.Spec.Collectors[0].Postgres.URI)
// Verify v1beta2 bundle was loaded correctly
require.Len(t, v2Bundle.Spec.Collectors, 1)
require.NotNil(t, v2Bundle.Spec.Collectors[0].ClusterInfo)
}
+2 -3
View File
@@ -5,7 +5,6 @@ package namespaces
import (
"fmt"
"net"
"strconv"
"time"
)
@@ -52,7 +51,7 @@ func (n *NamespacePinger) PingUDP(dst net.IP) error {
func (n *NamespacePinger) PingTCP(dst net.IP) error {
n.cfg.Logf("reaching to %q from %q with tcp", dst, n.InternalIP)
pinger := func() error {
addr := net.JoinHostPort(dst.String(), strconv.Itoa(n.cfg.Port))
addr := fmt.Sprintf("%s:%d", dst, n.cfg.Port)
conn, err := net.DialTimeout("tcp", addr, n.cfg.Timeout)
if err != nil {
return fmt.Errorf("error dialing tcp: %w", err)
@@ -91,7 +90,7 @@ func (n *NamespacePinger) StartTCPEchoServer(errors chan error) {
// received, the server ends. Callers must wait until the ready channel is
// closed before they can start sending packets.
func (n *NamespacePinger) startTCPEchoServer(ready chan struct{}) (err error) {
addr := net.JoinHostPort(n.InternalIP.String(), strconv.Itoa(n.cfg.Port))
addr := fmt.Sprintf("%s:%d", n.InternalIP, n.cfg.Port)
n.cfg.Logf("starting tcp echo server on namespace %q(%q)", n.name, addr)
if err = n.Join(); err != nil {
+55 -92
View File
@@ -13,12 +13,11 @@ import (
"github.com/replicatedhq/troubleshoot/internal/util"
"github.com/replicatedhq/troubleshoot/pkg/version"
"k8s.io/klog/v2"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content/memory"
"oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/credentials"
"oras.land/oras-go/v2/registry/remote/retry"
"oras.land/oras-go/pkg/auth"
dockerauth "oras.land/oras-go/pkg/auth/docker"
"oras.land/oras-go/pkg/content"
"oras.land/oras-go/pkg/oras"
"oras.land/oras-go/pkg/registry"
)
const (
@@ -76,97 +75,66 @@ func PullSpecsFromOCI(ctx context.Context, uri string) ([]string, error) {
}
func pullFromOCI(ctx context.Context, uri string, mediaType string, imageName string) ([]byte, error) {
// Parse the URI to get the repository reference
ref, err := parseURI(uri, imageName)
// helm credentials
helmCredentialsFile := filepath.Join(util.HomeDir(), HelmCredentialsFileBasename)
dockerauthClient, err := dockerauth.NewClientWithDockerFallback(helmCredentialsFile)
if err != nil {
return nil, errors.Wrap(err, "failed to create auth client")
}
authClient := dockerauthClient
headers := http.Header{}
headers.Set("User-Agent", version.GetUserAgent())
opts := []auth.ResolverOption{auth.WithResolverHeaders(headers)}
resolver, err := authClient.ResolverWithOpts(opts...)
if err != nil {
return nil, errors.Wrap(err, "failed to create resolver")
}
memoryStore := content.NewMemory()
allowedMediaTypes := []string{
mediaType,
}
var descriptors, layers []ocispec.Descriptor
registryStore := content.Registry{Resolver: resolver}
parsedRef, err := parseURI(uri, imageName)
if err != nil {
return nil, err
}
klog.V(1).Infof("Pulling spec from %q OCI uri", ref)
klog.V(1).Infof("Pulling spec from %q OCI uri", parsedRef)
// Create a repository instance
repo, err := remote.NewRepository(ref)
manifest, err := oras.Copy(ctx, registryStore, parsedRef, memoryStore, "",
oras.WithPullEmptyNameAllowed(),
oras.WithAllowedMediaTypes(allowedMediaTypes),
oras.WithLayerDescriptors(func(l []ocispec.Descriptor) {
layers = l
}))
if err != nil {
return nil, errors.Wrap(err, "failed to create repository")
}
// Set up authentication with Docker credentials fallback
helmCredentialsFile := filepath.Join(util.HomeDir(), HelmCredentialsFileBasename)
storeOpts := credentials.StoreOptions{}
// Try to load credentials from Helm config first, fall back to Docker config
var credStore credentials.Store
helmStore, helmErr := credentials.NewStore(helmCredentialsFile, storeOpts)
if helmErr == nil {
credStore = helmStore
} else {
// Fall back to Docker credentials if helm credentials are not available
dockerStore, dockerErr := credentials.NewStoreFromDocker(storeOpts)
if dockerErr != nil {
return nil, errors.Wrap(dockerErr, "failed to create credential store")
}
credStore = dockerStore
}
// Configure the repository client with authentication and custom headers
repo.Client = &auth.Client{
Client: retry.DefaultClient,
Cache: auth.NewCache(),
Credential: credentials.Credential(credStore),
Header: http.Header{
"User-Agent": []string{version.GetUserAgent()},
},
}
// Create in-memory storage for the pulled content
memoryStore := memory.New()
// Track layers for filtering
var layers []ocispec.Descriptor
// Set up copy options to capture layer descriptors
copyOpts := oras.CopyOptions{}
copyOpts.CopyGraphOptions.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
// Filter by media type - only copy layers with the specified media type
if desc.MediaType == mediaType {
layers = append(layers, desc)
return nil
}
// Allow manifest and other necessary descriptors
if strings.Contains(desc.MediaType, "manifest") || strings.Contains(desc.MediaType, "config") {
return nil
}
// Skip other media types
return oras.SkipNode
}
// Copy from the repository to memory
tag := repo.Reference.Reference
if tag == "" {
tag = "latest"
}
manifest, err := oras.Copy(ctx, repo, tag, memoryStore, tag, copyOpts)
if err != nil {
if strings.Contains(err.Error(), "not found") || strings.Contains(err.Error(), "manifest unknown") {
if strings.Contains(err.Error(), "not found") {
return nil, ErrNoRelease
}
return nil, errors.Wrap(err, "failed to copy")
}
descriptors := []ocispec.Descriptor{manifest}
descriptors = append(descriptors, manifest)
descriptors = append(descriptors, layers...)
// expect 2 descriptors (manifest + one layer)
// expect 2 descriptors
if len(descriptors) != 2 {
return nil, fmt.Errorf("expected 2 descriptors, got %d", len(descriptors))
return nil, fmt.Errorf("expected 2 descriptor, got %d", len(descriptors))
}
var matchingDescriptor *ocispec.Descriptor
for _, descriptor := range descriptors {
d := descriptor
if d.MediaType == mediaType {
switch d.MediaType {
case mediaType:
matchingDescriptor = &d
}
}
@@ -175,17 +143,9 @@ func pullFromOCI(ctx context.Context, uri string, mediaType string, imageName st
return nil, fmt.Errorf("no descriptor found with media type: %s", mediaType)
}
// Fetch the content from memory store
reader, err := memoryStore.Fetch(ctx, *matchingDescriptor)
if err != nil {
return nil, errors.Wrap(err, "failed to fetch matching descriptor")
}
defer reader.Close()
// Read all content
matchingSpec := make([]byte, matchingDescriptor.Size)
if _, err := reader.Read(matchingSpec); err != nil {
return nil, errors.Wrap(err, "failed to read content")
_, matchingSpec, ok := memoryStore.Get(*matchingDescriptor)
if !ok {
return nil, fmt.Errorf("failed to get matching descriptor")
}
return matchingSpec, nil
@@ -212,9 +172,12 @@ func parseURI(in, imageName string) (string, error) {
tag = uriParts[1]
}
// Format as: <host>:<port>/path/<imageName>:tag
// The remote.NewRepository() function in v2 can handle this format directly
uri := fmt.Sprintf("%s%s/%s:%s", u.Host, uriParts[0], imageName, tag)
uri := fmt.Sprintf("%s%s/%s:%s", u.Host, uriParts[0], imageName, tag) // <host>:<port>/path/<imageName>:tag
return uri, nil
parsedRef, err := registry.ParseReference(uri)
if err != nil {
return "", errors.Wrap(err, "failed to parse OCI uri reference")
}
return parsedRef.String(), nil
}
+1 -7
View File
@@ -183,7 +183,6 @@ func CollectWithContext(ctx context.Context, opts CollectOpts, p *troubleshootv1
}
allCollectorsMap := make(map[reflect.Type][]collect.Collector)
collectorTypeOrder := make([]reflect.Type, 0) // Preserve order of collector types
allCollectedData := make(map[string][]byte)
for _, desiredCollector := range collectSpecs {
@@ -194,9 +193,6 @@ func CollectWithContext(ctx context.Context, opts CollectOpts, p *troubleshootv1
return nil, errors.Wrap(err, "failed to check RBAC for collectors")
}
collectorType := reflect.TypeOf(collector)
if _, exists := allCollectorsMap[collectorType]; !exists {
collectorTypeOrder = append(collectorTypeOrder, collectorType)
}
allCollectorsMap[collectorType] = append(allCollectorsMap[collectorType], collector)
}
}
@@ -204,9 +200,7 @@ func CollectWithContext(ctx context.Context, opts CollectOpts, p *troubleshootv1
collectorList := map[string]CollectorStatus{}
// Iterate over collector types in the order they appeared in collectSpecs
for _, collectorType := range collectorTypeOrder {
collectors := allCollectorsMap[collectorType]
for _, collectors := range allCollectorsMap {
if mergeCollector, ok := collectors[0].(collect.MergeableCollector); ok {
mergedCollectors, err := mergeCollector.Merge(collectors)
if err != nil {
-211
View File
@@ -1,211 +0,0 @@
package preflight
import (
"reflect"
"testing"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/rest"
)
// TestCollectWithContext_ClusterResourcesFirst verifies that clusterResources
// collector runs first, even when it's not first in the spec.
func TestCollectWithContext_ClusterResourcesFirst(t *testing.T) {
// Create a preflight spec with collectors in a specific order
// where clusterResources is NOT first
preflight := &troubleshootv1beta2.Preflight{
Spec: troubleshootv1beta2.PreflightSpec{
Collectors: []*troubleshootv1beta2.Collect{
{
Data: &troubleshootv1beta2.Data{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "test-data",
},
Name: "test.json",
Data: `{"test": "data"}`,
},
},
{
ClusterResources: &troubleshootv1beta2.ClusterResources{},
},
},
},
}
// Use a fake Kubernetes client to avoid network calls
fakeClient := fake.NewSimpleClientset()
restConfig := &rest.Config{
Host: "https://fake-host",
}
opts := CollectOpts{
Namespace: "default",
KubernetesRestConfig: restConfig,
ProgressChan: make(chan interface{}, 100),
BundlePath: t.TempDir(),
IgnorePermissionErrors: true, // Ignore RBAC errors in tests
}
// Manually test the ordering logic by simulating what CollectWithContext does
collectSpecs := make([]*troubleshootv1beta2.Collect, 0)
if preflight.Spec.Collectors != nil {
collectSpecs = append(collectSpecs, preflight.Spec.Collectors...)
}
collectSpecs = collect.EnsureCollectorInList(
collectSpecs, troubleshootv1beta2.Collect{ClusterInfo: &troubleshootv1beta2.ClusterInfo{}},
)
collectSpecs = collect.EnsureCollectorInList(
collectSpecs, troubleshootv1beta2.Collect{ClusterResources: &troubleshootv1beta2.ClusterResources{}},
)
collectSpecs = collect.DedupCollectors(collectSpecs)
collectSpecs = collect.EnsureClusterResourcesFirst(collectSpecs)
// Verify clusterResources is first in the specs
require.NotEmpty(t, collectSpecs, "should have collectors")
require.NotNil(t, collectSpecs[0].ClusterResources, "first collector should be clusterResources")
// Now simulate the map grouping and order preservation
allCollectorsMap := make(map[reflect.Type][]collect.Collector)
collectorTypeOrder := make([]reflect.Type, 0)
for _, desiredCollector := range collectSpecs {
if collectorInterface, ok := collect.GetCollector(desiredCollector, opts.BundlePath, opts.Namespace, opts.KubernetesRestConfig, fakeClient, nil); ok {
if collector, ok := collectorInterface.(collect.Collector); ok {
// Skip RBAC check for this unit test
collectorType := reflect.TypeOf(collector)
if _, exists := allCollectorsMap[collectorType]; !exists {
collectorTypeOrder = append(collectorTypeOrder, collectorType)
}
allCollectorsMap[collectorType] = append(allCollectorsMap[collectorType], collector)
}
}
}
// Verify that clusterResources type is first in the order
require.NotEmpty(t, collectorTypeOrder, "should have collector types")
// Find the clusterResources type by checking the actual collectors
var clusterResourcesType reflect.Type
for collectorType, collectors := range allCollectorsMap {
if len(collectors) > 0 {
if _, ok := collectors[0].(*collect.CollectClusterResources); ok {
clusterResourcesType = collectorType
break
}
}
}
require.NotNil(t, clusterResourcesType, "should find clusterResources type")
assert.Equal(t, clusterResourcesType, collectorTypeOrder[0], "clusterResources type should be first in collectorTypeOrder")
}
// TestCollectWithContext_PreservesOrderAfterClusterResources verifies that
// after clusterResources, other collectors maintain their relative order.
func TestCollectWithContext_PreservesOrderAfterClusterResources(t *testing.T) {
// Create a preflight spec with multiple collectors in a specific order
preflight := &troubleshootv1beta2.Preflight{
Spec: troubleshootv1beta2.PreflightSpec{
Collectors: []*troubleshootv1beta2.Collect{
{
Data: &troubleshootv1beta2.Data{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "data-first",
},
Name: "first.json",
Data: `{"first": "data"}`,
},
},
{
Secret: &troubleshootv1beta2.Secret{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "secret-second",
},
},
},
},
},
}
// Use a fake Kubernetes client
fakeClient := fake.NewSimpleClientset()
restConfig := &rest.Config{
Host: "https://fake-host",
}
opts := CollectOpts{
Namespace: "default",
KubernetesRestConfig: restConfig,
ProgressChan: make(chan interface{}, 100),
BundlePath: t.TempDir(),
IgnorePermissionErrors: true,
}
// Simulate the ordering logic
collectSpecs := make([]*troubleshootv1beta2.Collect, 0)
if preflight.Spec.Collectors != nil {
collectSpecs = append(collectSpecs, preflight.Spec.Collectors...)
}
collectSpecs = collect.EnsureCollectorInList(
collectSpecs, troubleshootv1beta2.Collect{ClusterInfo: &troubleshootv1beta2.ClusterInfo{}},
)
collectSpecs = collect.EnsureCollectorInList(
collectSpecs, troubleshootv1beta2.Collect{ClusterResources: &troubleshootv1beta2.ClusterResources{}},
)
collectSpecs = collect.DedupCollectors(collectSpecs)
collectSpecs = collect.EnsureClusterResourcesFirst(collectSpecs)
// Group collectors by type and track order
allCollectorsMap := make(map[reflect.Type][]collect.Collector)
collectorTypeOrder := make([]reflect.Type, 0)
for _, desiredCollector := range collectSpecs {
if collectorInterface, ok := collect.GetCollector(desiredCollector, opts.BundlePath, opts.Namespace, opts.KubernetesRestConfig, fakeClient, nil); ok {
if collector, ok := collectorInterface.(collect.Collector); ok {
collectorType := reflect.TypeOf(collector)
if _, exists := allCollectorsMap[collectorType]; !exists {
collectorTypeOrder = append(collectorTypeOrder, collectorType)
}
allCollectorsMap[collectorType] = append(allCollectorsMap[collectorType], collector)
}
}
}
// Verify clusterResources is first
require.NotEmpty(t, collectorTypeOrder, "should have collector types")
// Find the actual types from the collectors
var clusterResourcesType, dataType, secretType reflect.Type
for collectorType, collectors := range allCollectorsMap {
if len(collectors) > 0 {
switch collectors[0].(type) {
case *collect.CollectClusterResources:
clusterResourcesType = collectorType
case *collect.CollectData:
dataType = collectorType
case *collect.CollectSecret:
secretType = collectorType
}
}
}
require.NotNil(t, clusterResourcesType, "should find clusterResources type")
assert.Equal(t, clusterResourcesType, collectorTypeOrder[0], "clusterResources should be first")
dataIndex := -1
secretIndex := -1
for i, ct := range collectorTypeOrder {
if ct == dataType {
dataIndex = i
}
if ct == secretType {
secretIndex = i
}
}
if dataIndex >= 0 && secretIndex >= 0 {
assert.Less(t, dataIndex, secretIndex, "data collectors should come before secret collectors, preserving relative order")
}
}
+1 -47
View File
@@ -88,52 +88,6 @@ func preprocessV1Beta3Specs(args []string) ([]string, []string, error) {
valuesFiles := viper.GetStringSlice("values")
setValues := viper.GetStringSlice("set")
// Check if any args contain v1beta3 specs
hasV1Beta3 := false
for _, arg := range args {
// Skip non-file arguments
if arg == "-" || strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") ||
strings.HasPrefix(arg, "secret/") || strings.HasPrefix(arg, "configmap/") {
continue
}
// Check if file exists
if _, err := os.Stat(arg); err != nil {
continue
}
// Read the file
content, err := os.ReadFile(arg)
if err != nil {
continue
}
// Check if it's a v1beta3 spec with templates
contentStr := string(content)
var parsed map[string]interface{}
if err := yaml.Unmarshal(content, &parsed); err == nil {
if apiVersion, ok := parsed["apiVersion"]; ok && apiVersion == constants.Troubleshootv1beta3Kind {
// Only require values if the spec has Helm templates
if strings.Contains(contentStr, "{{") && strings.Contains(contentStr, "}}") {
hasV1Beta3 = true
break
}
}
} else {
// If YAML parsing fails, check raw content for v1beta3 with templates
if strings.Contains(contentStr, "apiVersion: troubleshoot.sh/v1beta3") &&
strings.Contains(contentStr, "{{") && strings.Contains(contentStr, "}}") {
hasV1Beta3 = true
break
}
}
}
// If v1beta3 spec with templates found but no values provided, return error
if hasV1Beta3 && len(valuesFiles) == 0 && len(setValues) == 0 {
return nil, nil, errors.New("v1beta3 specs with Helm templates require a values file. Please provide values using --values or --set flags")
}
// If no values provided, return args unchanged
if len(valuesFiles) == 0 && len(setValues) == 0 {
return args, nil, nil
@@ -155,7 +109,7 @@ func preprocessV1Beta3Specs(args []string) ([]string, []string, error) {
return nil, nil, errors.Wrapf(err, "failed to parse values file %s", valuesFile)
}
values = MergeMaps(values, fileValues)
values = mergeMaps(values, fileValues)
}
// Apply --set values
-73
View File
@@ -8,7 +8,6 @@ import (
"github.com/replicatedhq/troubleshoot/internal/testutils"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/loader"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -411,75 +410,3 @@ func singleTestPreflightSpecsRead(t *testing.T, tt *PreflightSpecsReadTest) (*lo
return kinds, err
}
func TestPreprocessV1Beta3Specs_RequiresValues(t *testing.T) {
// Save and restore viper state
oldValues := viper.Get("values")
oldSet := viper.Get("set")
defer func() {
viper.Set("values", oldValues)
viper.Set("set", oldSet)
}()
t.Run("v1beta3 without values should error", func(t *testing.T) {
// Clear viper values
viper.Set("values", []string{})
viper.Set("set", []string{})
v1beta3File := filepath.Join(testutils.FileDir(), "../../examples/preflight/simple-v1beta3.yaml")
_, _, err := preprocessV1Beta3Specs([]string{v1beta3File})
require.Error(t, err)
assert.Contains(t, err.Error(), "v1beta3 specs with Helm templates require a values file")
})
t.Run("v1beta3 with values file should succeed", func(t *testing.T) {
valuesFile := filepath.Join(testutils.FileDir(), "../../examples/preflight/values-v1beta3-1.yaml")
viper.Set("values", []string{valuesFile})
viper.Set("set", []string{})
v1beta3File := filepath.Join(testutils.FileDir(), "../../examples/preflight/simple-v1beta3.yaml")
processedArgs, tempFiles, err := preprocessV1Beta3Specs([]string{v1beta3File})
// Clean up temp files
defer func() {
for _, f := range tempFiles {
_ = os.Remove(f)
}
}()
require.NoError(t, err)
assert.NotNil(t, processedArgs)
})
t.Run("v1beta3 with --set values should succeed", func(t *testing.T) {
viper.Set("values", []string{})
viper.Set("set", []string{"kubernetes.enabled=true"})
v1beta3File := filepath.Join(testutils.FileDir(), "../../examples/preflight/simple-v1beta3.yaml")
processedArgs, tempFiles, err := preprocessV1Beta3Specs([]string{v1beta3File})
// Clean up temp files
defer func() {
for _, f := range tempFiles {
_ = os.Remove(f)
}
}()
require.NoError(t, err)
assert.NotNil(t, processedArgs)
})
t.Run("v1beta2 without values should succeed", func(t *testing.T) {
viper.Set("values", []string{})
viper.Set("set", []string{})
v1beta2File := filepath.Join(testutils.FileDir(), "../../testdata/preflightspec/troubleshoot_v1beta2_preflight_gotest.yaml")
processedArgs, tempFiles, err := preprocessV1Beta3Specs([]string{v1beta2File})
require.NoError(t, err)
assert.NotNil(t, processedArgs)
assert.Empty(t, tempFiles)
assert.Equal(t, []string{v1beta2File}, processedArgs)
})
}
+4 -4
View File
@@ -33,7 +33,7 @@ func RunTemplate(templateFile string, valuesFiles []string, setValues []string,
if err != nil {
return errors.Wrapf(err, "failed to load values file %s", valuesFile)
}
values = MergeMaps(values, fileValues)
values = mergeMaps(values, fileValues)
}
// Apply --set values (Helm semantics)
@@ -165,8 +165,8 @@ func cleanRenderedYAML(content string) string {
return strings.Join(cleaned, "\n") + "\n"
}
// MergeMaps recursively merges two maps, with overlay taking precedence
func MergeMaps(base, overlay map[string]interface{}) map[string]interface{} {
// mergeMaps recursively merges two maps
func mergeMaps(base, overlay map[string]interface{}) map[string]interface{} {
result := make(map[string]interface{})
// Copy base map
@@ -180,7 +180,7 @@ func MergeMaps(base, overlay map[string]interface{}) map[string]interface{} {
// If both are maps, merge recursively
if baseMap, ok := baseVal.(map[string]interface{}); ok {
if overlayMap, ok := v.(map[string]interface{}); ok {
result[k] = MergeMaps(baseMap, overlayMap)
result[k] = mergeMaps(baseMap, overlayMap)
continue
}
}
+1 -1
View File
@@ -430,7 +430,7 @@ func TestRender_V1Beta3_MergeMultipleValuesFiles_And_SetPrecedence(t *testing.T)
for _, f := range []string{minimalFile, file1, file3} {
m, err := loadValuesFile(f)
require.NoError(t, err)
vals = MergeMaps(vals, m)
vals = mergeMaps(vals, m)
}
// First render without --set; expect NO kubernetes analyzer
-104
View File
@@ -1,104 +0,0 @@
package redact
import (
"bufio"
"io"
"github.com/replicatedhq/troubleshoot/pkg/constants"
)
// LineReader reads lines from an io.Reader while tracking whether each line
// ended with a newline character. This is essential for preserving the exact
// structure of input files during redaction - binary files and text files
// without trailing newlines should not have newlines added to them.
//
// Unlike bufio.Scanner which strips newlines and requires the caller to add
// them back, LineReader explicitly tracks the presence of newlines so callers
// can conditionally restore them only when they were originally present.
type LineReader struct {
reader *bufio.Reader
}
// NewLineReader creates a new LineReader that reads from the given io.Reader.
// The reader is wrapped in a bufio.Reader for efficient byte-by-byte reading.
func NewLineReader(r io.Reader) *LineReader {
return &LineReader{
reader: bufio.NewReader(r),
}
}
// ReadLine reads the next line from the reader and returns:
// - line content (without the newline character if present)
// - whether the line ended with a newline (\n)
// - any error encountered
//
// Return values:
// - (content, true, nil) - line ended with \n, more content may follow
// - (content, false, io.EOF) - last line without \n (file doesn't end with newline)
// - (nil, false, io.EOF) - reached EOF with no content (empty file or end of file)
// - (content, false, error) - encountered a non-EOF error
//
// The function respects constants.SCANNER_MAX_SIZE and returns an error if a single
// line exceeds this limit. This prevents memory exhaustion on files with extremely
// long lines or binary files without newlines that are larger than the limit.
//
// Example usage:
//
// lr := NewLineReader(input)
// for {
// line, hadNewline, err := lr.ReadLine()
// if err == io.EOF && len(line) == 0 {
// break // End of file, no more content
// }
//
// // Process line...
// fmt.Print(string(line))
// if hadNewline {
// fmt.Print("\n")
// }
//
// if err == io.EOF {
// break // Last line processed
// }
// if err != nil {
// return err
// }
// }
func (lr *LineReader) ReadLine() ([]byte, bool, error) {
// Initialize line as empty slice (not nil) to ensure consistent return values
// Empty lines (just \n) should return []byte{}, not nil
line := []byte{}
for {
b, err := lr.reader.ReadByte()
// Handle errors
if err == io.EOF {
if len(line) > 0 {
// Last line without newline - return the content we have
return line, false, io.EOF
}
// Nothing left to read - empty file or end of content
return nil, false, io.EOF
}
if err != nil {
// Non-EOF error encountered
return line, false, err
}
// Found newline character
if b == '\n' {
// Return the line (may be empty for blank lines)
return line, true, nil
}
// Accumulate byte into line buffer
line = append(line, b)
// Check buffer limit to prevent memory exhaustion
// This is especially important for binary files without newlines
if len(line) > constants.SCANNER_MAX_SIZE {
return nil, false, bufio.ErrTooLong
}
}
}
-302
View File
@@ -1,302 +0,0 @@
package redact
import (
"bufio"
"bytes"
"io"
"strings"
"testing"
"github.com/replicatedhq/troubleshoot/pkg/constants"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Test 1.2 & 1.3: NewLineReader creates instance correctly
func TestNewLineReader(t *testing.T) {
input := strings.NewReader("test")
lr := NewLineReader(input)
require.NotNil(t, lr)
require.NotNil(t, lr.reader)
}
// Test 1.8: Empty file → (nil, false, io.EOF)
func TestLineReader_EmptyFile(t *testing.T) {
lr := NewLineReader(strings.NewReader(""))
line, hadNewline, err := lr.ReadLine()
assert.Nil(t, line)
assert.False(t, hadNewline)
assert.Equal(t, io.EOF, err)
}
// Test 1.9: Single line with \n → (content, true, nil)
func TestLineReader_SingleLineWithNewline(t *testing.T) {
lr := NewLineReader(strings.NewReader("hello world\n"))
line, hadNewline, err := lr.ReadLine()
assert.Equal(t, []byte("hello world"), line)
assert.True(t, hadNewline)
assert.NoError(t, err)
// Second read should return EOF
line, hadNewline, err = lr.ReadLine()
assert.Nil(t, line)
assert.False(t, hadNewline)
assert.Equal(t, io.EOF, err)
}
// Test 1.10: Single line without \n → (content, false, io.EOF)
func TestLineReader_SingleLineWithoutNewline(t *testing.T) {
lr := NewLineReader(strings.NewReader("hello world"))
line, hadNewline, err := lr.ReadLine()
assert.Equal(t, []byte("hello world"), line)
assert.False(t, hadNewline)
assert.Equal(t, io.EOF, err)
}
// Test 1.11: Multiple lines with \n → correct for each
func TestLineReader_MultipleLinesWithNewlines(t *testing.T) {
input := "line1\nline2\nline3\n"
lr := NewLineReader(strings.NewReader(input))
// First line
line, hadNewline, err := lr.ReadLine()
assert.Equal(t, []byte("line1"), line)
assert.True(t, hadNewline)
assert.NoError(t, err)
// Second line
line, hadNewline, err = lr.ReadLine()
assert.Equal(t, []byte("line2"), line)
assert.True(t, hadNewline)
assert.NoError(t, err)
// Third line
line, hadNewline, err = lr.ReadLine()
assert.Equal(t, []byte("line3"), line)
assert.True(t, hadNewline)
assert.NoError(t, err)
// EOF
line, hadNewline, err = lr.ReadLine()
assert.Nil(t, line)
assert.False(t, hadNewline)
assert.Equal(t, io.EOF, err)
}
// Test 1.12: Last line without \n → (content, false, io.EOF)
func TestLineReader_LastLineWithoutNewline(t *testing.T) {
input := "line1\nline2\nline3"
lr := NewLineReader(strings.NewReader(input))
// First line
line, hadNewline, err := lr.ReadLine()
assert.Equal(t, []byte("line1"), line)
assert.True(t, hadNewline)
assert.NoError(t, err)
// Second line
line, hadNewline, err = lr.ReadLine()
assert.Equal(t, []byte("line2"), line)
assert.True(t, hadNewline)
assert.NoError(t, err)
// Third line (no trailing newline)
line, hadNewline, err = lr.ReadLine()
assert.Equal(t, []byte("line3"), line)
assert.False(t, hadNewline)
assert.Equal(t, io.EOF, err)
}
// Test 1.13: Binary data (no \n) → (all content, false, io.EOF)
func TestLineReader_BinaryData(t *testing.T) {
binaryData := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0xFF, 0xFE}
lr := NewLineReader(bytes.NewReader(binaryData))
line, hadNewline, err := lr.ReadLine()
assert.Equal(t, binaryData, line)
assert.False(t, hadNewline)
assert.Equal(t, io.EOF, err)
}
// Test 1.14: Line exceeding max size → error
func TestLineReader_LineExceedingMaxSize(t *testing.T) {
// Create a line that exceeds SCANNER_MAX_SIZE
largeData := make([]byte, constants.SCANNER_MAX_SIZE+100)
for i := range largeData {
largeData[i] = 'a'
}
lr := NewLineReader(bytes.NewReader(largeData))
line, hadNewline, err := lr.ReadLine()
assert.Nil(t, line)
assert.False(t, hadNewline)
assert.Error(t, err)
assert.ErrorIs(t, err, bufio.ErrTooLong)
}
// Test 1.15: File with only \n → ([], true, nil)
func TestLineReader_OnlyNewline(t *testing.T) {
lr := NewLineReader(strings.NewReader("\n"))
line, hadNewline, err := lr.ReadLine()
assert.Equal(t, []byte{}, line) // Empty line
assert.True(t, hadNewline)
assert.NoError(t, err)
// Second read should return EOF
line, hadNewline, err = lr.ReadLine()
assert.Nil(t, line)
assert.False(t, hadNewline)
assert.Equal(t, io.EOF, err)
}
// Additional test: File with empty lines (multiple newlines)
func TestLineReader_EmptyLines(t *testing.T) {
input := "\n\n\n"
lr := NewLineReader(strings.NewReader(input))
// First empty line
line, hadNewline, err := lr.ReadLine()
assert.Equal(t, []byte{}, line)
assert.True(t, hadNewline)
assert.NoError(t, err)
// Second empty line
line, hadNewline, err = lr.ReadLine()
assert.Equal(t, []byte{}, line)
assert.True(t, hadNewline)
assert.NoError(t, err)
// Third empty line
line, hadNewline, err = lr.ReadLine()
assert.Equal(t, []byte{}, line)
assert.True(t, hadNewline)
assert.NoError(t, err)
// EOF
line, hadNewline, err = lr.ReadLine()
assert.Nil(t, line)
assert.False(t, hadNewline)
assert.Equal(t, io.EOF, err)
}
// Additional test: Mixed content with and without newlines
func TestLineReader_MixedContent(t *testing.T) {
input := "line1\n\nline3"
lr := NewLineReader(strings.NewReader(input))
// First line
line, hadNewline, err := lr.ReadLine()
assert.Equal(t, []byte("line1"), line)
assert.True(t, hadNewline)
assert.NoError(t, err)
// Empty line
line, hadNewline, err = lr.ReadLine()
assert.Equal(t, []byte{}, line)
assert.True(t, hadNewline)
assert.NoError(t, err)
// Last line without newline
line, hadNewline, err = lr.ReadLine()
assert.Equal(t, []byte("line3"), line)
assert.False(t, hadNewline)
assert.Equal(t, io.EOF, err)
}
// Additional test: Large but valid file (under max size)
func TestLineReader_LargeValidFile(t *testing.T) {
// Create a line that's large but under the limit
largeData := make([]byte, constants.SCANNER_MAX_SIZE-100)
for i := range largeData {
largeData[i] = 'x'
}
largeData = append(largeData, '\n')
lr := NewLineReader(bytes.NewReader(largeData))
line, hadNewline, err := lr.ReadLine()
assert.Equal(t, constants.SCANNER_MAX_SIZE-100, len(line))
assert.True(t, hadNewline)
assert.NoError(t, err)
}
// Additional test: Binary file with embedded newlines
func TestLineReader_BinaryWithEmbeddedNewlines(t *testing.T) {
binaryData := []byte{0x01, 0x02, '\n', 0x03, 0x04, '\n', 0x05}
lr := NewLineReader(bytes.NewReader(binaryData))
// First "line" (up to first \n)
line, hadNewline, err := lr.ReadLine()
assert.Equal(t, []byte{0x01, 0x02}, line)
assert.True(t, hadNewline)
assert.NoError(t, err)
// Second "line"
line, hadNewline, err = lr.ReadLine()
assert.Equal(t, []byte{0x03, 0x04}, line)
assert.True(t, hadNewline)
assert.NoError(t, err)
// Last "line" without newline
line, hadNewline, err = lr.ReadLine()
assert.Equal(t, []byte{0x05}, line)
assert.False(t, hadNewline)
assert.Equal(t, io.EOF, err)
}
// Test edge case: Very small reads
func TestLineReader_SingleByteReads(t *testing.T) {
input := "a\nb\nc"
lr := NewLineReader(strings.NewReader(input))
line, hadNewline, err := lr.ReadLine()
assert.Equal(t, []byte("a"), line)
assert.True(t, hadNewline)
assert.NoError(t, err)
line, hadNewline, err = lr.ReadLine()
assert.Equal(t, []byte("b"), line)
assert.True(t, hadNewline)
assert.NoError(t, err)
line, hadNewline, err = lr.ReadLine()
assert.Equal(t, []byte("c"), line)
assert.False(t, hadNewline)
assert.Equal(t, io.EOF, err)
}
// Benchmark: LineReader vs bufio.Scanner performance
func BenchmarkLineReader(b *testing.B) {
// Create test data
var buf bytes.Buffer
for i := 0; i < 1000; i++ {
buf.WriteString("This is line number ")
buf.WriteString(string(rune(i)))
buf.WriteString(" with some content\n")
}
data := buf.Bytes()
b.ResetTimer()
for i := 0; i < b.N; i++ {
lr := NewLineReader(bytes.NewReader(data))
for {
_, _, err := lr.ReadLine()
if err == io.EOF {
break
}
}
}
}
+13 -43
View File
@@ -3,7 +3,6 @@ package redact
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
@@ -26,12 +25,6 @@ func literalString(match []byte, path, name string) Redactor {
}
}
// Redact processes the input reader line-by-line, replacing literal string matches.
// Unlike the previous implementation using bufio.Scanner, this now uses LineReader
// to preserve the exact newline structure of the input file. Lines that originally
// ended with \n will have \n added back, while lines without \n (like the last line
// of a file without a trailing newline, or binary files) will not have \n added.
// This ensures binary files and text files without trailing newlines are not corrupted.
func (r literalRedactor) Redact(input io.Reader, path string) io.Reader {
out, writer := io.Pipe()
@@ -41,8 +34,7 @@ func (r literalRedactor) Redact(input io.Reader, path string) io.Reader {
if err == nil || err == io.EOF {
writer.Close()
} else {
// Check if error is about line exceeding maximum size
if errors.Is(err, bufio.ErrTooLong) {
if err == bufio.ErrTooLong {
s := fmt.Sprintf("Error redacting %q. A line in the file exceeded %d MB max length", path, constants.SCANNER_MAX_SIZE/1024/1024)
klog.V(2).Info(s)
} else {
@@ -52,24 +44,17 @@ func (r literalRedactor) Redact(input io.Reader, path string) io.Reader {
}
}()
// Use LineReader instead of bufio.Scanner to track newline presence
lineReader := NewLineReader(input)
tokenizer := GetGlobalTokenizer()
buf := make([]byte, constants.BUF_INIT_SIZE)
scanner := bufio.NewScanner(input)
scanner.Buffer(buf, constants.SCANNER_MAX_SIZE)
lineNum := 0
for {
line, hadNewline, readErr := lineReader.ReadLine()
// Handle EOF with no content - we're done
if readErr == io.EOF && len(line) == 0 {
break
}
// We have content to process
for scanner.Scan() {
lineNum++
line := scanner.Bytes()
// Perform literal string replacement
var clean []byte
tokenizer := GetGlobalTokenizer()
if tokenizer.IsEnabled() {
// For literal redaction, we tokenize the matched value
matchStr := string(r.match)
@@ -81,20 +66,12 @@ func (r literalRedactor) Redact(input io.Reader, path string) io.Reader {
clean = bytes.ReplaceAll(line, r.match, maskTextBytes)
}
// Write the line (redacted or original)
err = writeBytes(writer, clean)
// Append newline since scanner strips it
err = writeBytes(writer, clean, NEW_LINE)
if err != nil {
return
}
// Only add newline if original line had one
if hadNewline {
err = writeBytes(writer, NEW_LINE)
if err != nil {
return
}
}
// Track redaction if content changed
if !bytes.Equal(clean, line) {
addRedaction(Redaction{
RedactorName: r.redactName,
@@ -104,16 +81,9 @@ func (r literalRedactor) Redact(input io.Reader, path string) io.Reader {
IsDefaultRedactor: r.isDefault,
})
}
// Check if we hit EOF after processing this line
if readErr == io.EOF {
break
}
// Check for non-EOF errors
if readErr != nil {
err = readErr
return
}
}
if scanErr := scanner.Err(); scanErr != nil {
err = scanErr
}
}()
return out
-386
View File
@@ -1,386 +0,0 @@
package redact
import (
"bytes"
"io"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
// Test basic literal redaction functionality
func TestLiteralRedactor_BasicRedaction(t *testing.T) {
tests := []struct {
name string
match string
inputString string
wantString string
}{
{
name: "Simple literal match",
match: "secret123",
inputString: "password=secret123",
wantString: "password=***HIDDEN***", // No trailing newline in input
},
{
name: "Multiple occurrences",
match: "secret",
inputString: "secret is secret here secret",
wantString: "***HIDDEN*** is ***HIDDEN*** here ***HIDDEN***",
},
{
name: "No match",
match: "xyz",
inputString: "no match here",
wantString: "no match here",
},
{
name: "With trailing newline",
match: "secret",
inputString: "secret\n",
wantString: "***HIDDEN***\n",
},
{
name: "Multiline with newlines",
match: "secret",
inputString: "line1 secret\nline2 secret\n",
wantString: "line1 ***HIDDEN***\nline2 ***HIDDEN***\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
redactor := literalString([]byte(tt.match), "testfile", tt.name)
out := redactor.Redact(bytes.NewReader([]byte(tt.inputString)), "")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, tt.wantString, string(result))
})
}
}
// Test 4.12: Binary file → unchanged
func TestLiteralRedactor_BinaryFile(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
// Binary content with no newlines and no match
binaryData := []byte{0x01, 0x02, 0x03, 0x04, 0x00, 0xFF, 0xFE, 0xAB, 0xCD}
redactor := literalString([]byte("notfound"), "testfile", t.Name())
out := redactor.Redact(bytes.NewReader(binaryData), "test.bin")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, binaryData, result, "Binary file should be unchanged")
}
// Test: Binary file with every single byte value (0x00 -> 0xFF)
func TestLiteralRedactor_AllSingleByteValues(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
// Create binary data with every possible byte value
binaryData := make([]byte, 256)
for i := 0; i < 256; i++ {
binaryData[i] = byte(i)
}
redactor := literalString([]byte("notfound"), "testfile", t.Name())
out := redactor.Redact(bytes.NewReader(binaryData), "test.bin")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, binaryData, result, "Binary file with all byte values should be unchanged")
require.Len(t, result, 256, "Should preserve all 256 bytes")
}
// Test: Binary file with every two-byte combination (0x00+0x00 -> 0xFF+0xFF)
func TestLiteralRedactor_AllTwoByteValues(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
// Create binary data with all 65536 two-byte combinations (128KB)
binaryData := make([]byte, 256*256*2)
pos := 0
for i := 0; i < 256; i++ {
for j := 0; j < 256; j++ {
binaryData[pos] = byte(i)
binaryData[pos+1] = byte(j)
pos += 2
}
}
redactor := literalString([]byte("notfound"), "testfile", t.Name())
out := redactor.Redact(bytes.NewReader(binaryData), "test.bin")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, binaryData, result, "Binary file with all two-byte combinations should be unchanged")
require.Len(t, result, 256*256*2, "Should preserve all 131072 bytes")
}
// Test 4.12 (variant): Binary file with literal match → redacted, no extra newlines
func TestLiteralRedactor_BinaryFileWithMatch(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
// Binary content with a literal match (0xFF 0xFE sequence)
binaryData := []byte{0x01, 0x02, 0xFF, 0xFE, 0x03, 0x04}
redactor := literalString([]byte{0xFF, 0xFE}, "testfile", t.Name())
// We need to mock maskTextBytes for this test to work predictably
// For now, test that no newlines are added
out := redactor.Redact(bytes.NewReader(binaryData), "test.bin")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.NotEqual(t, binaryData, result, "Binary should be redacted")
require.NotContains(t, result, []byte{0xFF, 0xFE}, "Match should be replaced")
// Most importantly: no trailing newline added to binary file
require.NotEqual(t, byte('\n'), result[len(result)-1], "Should not add trailing newline")
}
// Test 4.13: Text with trailing \n → preserved
func TestLiteralRedactor_TextWithTrailingNewline(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := "hello world\n"
redactor := literalString([]byte("xyz"), "testfile", t.Name())
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, "hello world\n", string(result), "Trailing newline should be preserved")
}
// Test 4.14: Text without trailing \n → preserved
func TestLiteralRedactor_TextWithoutTrailingNewline(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := "hello world"
redactor := literalString([]byte("xyz"), "testfile", t.Name())
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, "hello world", string(result), "No newline should be added")
}
// Test 4.15: Empty file → unchanged
func TestLiteralRedactor_EmptyFile(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := ""
redactor := literalString([]byte("secret"), "testfile", t.Name())
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, "", string(result), "Empty file should remain empty")
}
// Test 4.16: Literal match and replacement works
func TestLiteralRedactor_LiteralMatch(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := "password=secret123"
redactor := literalString([]byte("secret123"), "testfile", t.Name())
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, "password=***HIDDEN***", string(result))
}
// Test 4.17: Multiple occurrences replaced
func TestLiteralRedactor_MultipleOccurrences(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := "secret here and secret there and secret everywhere"
redactor := literalString([]byte("secret"), "testfile", t.Name())
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, "***HIDDEN*** here and ***HIDDEN*** there and ***HIDDEN*** everywhere", string(result))
}
// Test 4.17 (variant): Multiple occurrences across lines
func TestLiteralRedactor_MultipleOccurrencesMultiline(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := "line1 secret\nline2 secret\nline3 secret\n"
redactor := literalString([]byte("secret"), "testfile", t.Name())
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
expected := "line1 ***HIDDEN***\nline2 ***HIDDEN***\nline3 ***HIDDEN***\n"
require.Equal(t, expected, string(result))
}
// Test 4.18: Tokenization works
func TestLiteralRedactor_Tokenization(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
// Enable tokenization for this test
EnableTokenization()
defer DisableTokenization()
input := "password=secret123"
redactor := literalString([]byte("secret123"), "testfile", t.Name())
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
// Result should contain a token, not the original or ***HIDDEN***
require.NotContains(t, string(result), "secret123")
require.NotContains(t, string(result), "***HIDDEN***")
require.Contains(t, string(result), "password=")
}
// Test 4.19: Redaction count accurate
func TestLiteralRedactor_RedactionCount(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := "secret here\nsecret there"
// Use unique redactor name and filename to avoid pollution from parallel tests
uniqueFile := "TestLiteralRedactor_RedactionCount_file"
uniqueRedactor := "TestLiteralRedactor_RedactionCount_redactor"
redactor := literalString([]byte("secret"), uniqueFile, uniqueRedactor)
out := redactor.Redact(bytes.NewReader([]byte(input)), "")
_, err := io.ReadAll(out)
require.NoError(t, err)
redactions := GetRedactionList()
// Two lines, each with one match = 2 redaction events
require.Len(t, redactions.ByRedactor[uniqueRedactor], 2, "Should record 2 redactions (one per line)")
require.Len(t, redactions.ByFile[uniqueFile], 2, "Should record 2 redactions for file")
}
// Test 4.20: Backward compatibility - existing behavior preserved for text with newlines
func TestLiteralRedactor_BackwardCompatibility(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := "line1 secret\nline2 secret\nline3\n"
redactor := literalString([]byte("secret"), "testfile", t.Name())
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
expected := "line1 ***HIDDEN***\nline2 ***HIDDEN***\nline3\n"
require.Equal(t, expected, string(result), "Behavior for text with newlines should be unchanged")
}
// Test 4.20 (variant): Literal match on last line without \n
func TestLiteralRedactor_LastLineWithoutNewline(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := "line1\nline2 secret"
redactor := literalString([]byte("secret"), "testfile", t.Name())
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
expected := "line1\nline2 ***HIDDEN***"
require.Equal(t, expected, string(result), "Should not add newline to last line")
}
// Additional test: Empty line handling
func TestLiteralRedactor_EmptyLines(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := "\n\n\n"
redactor := literalString([]byte("secret"), "testfile", t.Name())
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, "\n\n\n", string(result), "Empty lines should be preserved")
}
// Additional test: Large file with many matches
func TestLiteralRedactor_LargeFile(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
// Create large file with many occurrences
var input strings.Builder
for i := 0; i < 1000; i++ {
input.WriteString("line ")
input.WriteString("secret")
input.WriteString(" here\n")
}
redactor := literalString([]byte("secret"), "testfile", t.Name())
out := redactor.Redact(strings.NewReader(input.String()), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.NotContains(t, string(result), "secret", "All secrets should be redacted")
require.Contains(t, string(result), "***HIDDEN***")
}
// Additional test: Partial match should not be replaced
func TestLiteralRedactor_PartialMatchNotReplaced(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := "secret secretive secrets"
// Should only replace exact literal "secret", not "secretive" or "secrets"
redactor := literalString([]byte("secret"), "testfile", t.Name())
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, "***HIDDEN*** ***HIDDEN***ive ***HIDDEN***s", string(result))
}
+34 -127
View File
@@ -1,18 +1,12 @@
package redact
import (
"bufio"
"bytes"
"io"
"regexp"
)
// lineState represents a line and whether it ended with a newline character.
// This is used by MultiLineRedactor to track newline state for line pairs.
type lineState struct {
content []byte
hadNewline bool
}
type MultiLineRedactor struct {
scan *regexp.Regexp
re1 *regexp.Regexp
@@ -45,16 +39,6 @@ func NewMultiLineRedactor(re1 LineRedactor, re2 string, maskText, path, name str
return &MultiLineRedactor{scan: scanCompiled, re1: compiled1, re2: compiled2, maskText: maskText, filePath: path, redactName: name, isDefault: isDefault}, nil
}
// Redact processes the input reader in pairs of lines, applying redaction patterns.
// Unlike the previous implementation using bufio.Reader with readLine(), this now
// uses LineReader to preserve the exact newline structure of the input file.
//
// The MultiLineRedactor works by:
// 1. Reading pairs of lines (line1, line2)
// 2. If line1 matches the selector pattern (re1), redact line2 using re2
// 3. Write both lines with their original newline structure preserved
//
// This ensures binary files and text files without trailing newlines are not corrupted.
func (r *MultiLineRedactor) Redact(input io.Reader, path string) io.Reader {
out, writer := io.Pipe()
go func() {
@@ -64,74 +48,51 @@ func (r *MultiLineRedactor) Redact(input io.Reader, path string) io.Reader {
}()
tokenizer := GetGlobalTokenizer()
lineReader := NewLineReader(input)
// Try to read first two lines
line1, nl1, line2, nl2, readErr := getNextTwoLines(lineReader, nil)
// Handle case where we can't read 2 lines (empty file or single line)
// Note: We check line1 == nil (not len(line1) == 0) because:
// - nil means truly empty file with no content
// - []byte{} (len==0) means an empty line that had a newline (e.g., "\n")
if readErr != nil && line1 == nil {
// Empty file - nothing to write
// Propagate non-EOF errors (EOF is expected for empty files)
if readErr != io.EOF {
err = readErr
}
return
}
if readErr != nil {
// Only 1 line available (or empty line with newline) - write it and exit
// FIX: This is the bug fix - only add newline if original had one
// Also handles empty lines (line1 == []byte{} with nl1 == true)
err = writeLine(writer, line1, nl1)
reader := bufio.NewReader(input)
line1, line2, err := getNextTwoLines(reader, nil)
if err != nil {
// this will print 2 blank lines for empty input...
// Append newlines since scanner strips them
err = writeBytes(writer, line1, NEW_LINE, line2, NEW_LINE)
if err != nil {
return
}
// Propagate non-EOF errors (EOF is expected for single-line files)
if readErr != io.EOF {
err = readErr
}
return
}
// Process line pairs
flushLastLine := false
lineNum := 1
for readErr == nil {
for err == nil {
lineNum++ // the first line that can be redacted is line 2
// Pre-filter: if scan is not nil, check if line1 matches scan by lowercasing it
// is scan is not nil, then check if line1 matches scan by lowercasing it
if r.scan != nil {
lowerLine1 := bytes.ToLower(line1)
if !r.scan.Match(lowerLine1) {
// No match - write line1 and advance
err = writeLine(writer, line1, nl1)
// Append newline since scanner strips it
err = writeBytes(writer, line1, NEW_LINE)
if err != nil {
return
}
line1, nl1, line2, nl2, readErr = getNextTwoLines(lineReader, &lineState{line2, nl2})
line1, line2, err = getNextTwoLines(reader, &line2)
flushLastLine = true
continue
}
}
// Check if line1 matches the selector pattern (re1)
// If line1 matches re1, then transform line2 using re2
if !r.re1.Match(line1) {
// No match - write line1 and advance
err = writeLine(writer, line1, nl1)
// Append newline since scanner strips it
err = writeBytes(writer, line1, NEW_LINE)
if err != nil {
return
}
line1, nl1, line2, nl2, readErr = getNextTwoLines(lineReader, &lineState{line2, nl2})
line1, line2, err = getNextTwoLines(reader, &line2)
flushLastLine = true
continue
}
// line1 matched selector - redact line2
flushLastLine = false
var clean []byte
if tokenizer.IsEnabled() {
@@ -144,17 +105,13 @@ func (r *MultiLineRedactor) Redact(input io.Reader, path string) io.Reader {
clean = r.re2.ReplaceAll(line2, substStr)
}
// Write line1 (selector line) and line2 (redacted line)
err = writeLine(writer, line1, nl1)
if err != nil {
return
}
err = writeLine(writer, clean, nl2)
// Append newlines since scanner strips them
err = writeBytes(writer, line1, NEW_LINE, clean, NEW_LINE)
if err != nil {
return
}
// Track redaction if content changed
// if clean is not equal to line2, a redaction was performed
if !bytes.Equal(clean, line2) {
addRedaction(Redaction{
RedactorName: r.redactName,
@@ -165,92 +122,42 @@ func (r *MultiLineRedactor) Redact(input io.Reader, path string) io.Reader {
})
}
// Get next pair
line1, nl1, line2, nl2, readErr = getNextTwoLines(lineReader, nil)
line1, line2, err = getNextTwoLines(reader, nil)
}
// After loop exits (readErr != nil), check if we have an unwritten line1
// This happens in two cases:
// 1. flushLastLine=true: line1 was advanced but not written (scan/re1 didn't match)
// 2. line1 != nil: we read line1 but couldn't get line2 (unpaired line at end)
// Note: We check line1 != nil (not len(line1) > 0) to handle empty lines ([]byte{})
if flushLastLine || line1 != nil {
err = writeLine(writer, line1, nl1)
if flushLastLine {
// Append newline since scanner strip it
err = writeBytes(writer, line1, NEW_LINE)
if err != nil {
return
}
}
// Propagate non-EOF read errors to the caller
// EOF is expected (end of file) and not an error condition
// Note: readErr is always non-nil here (loop exited), but we only propagate non-EOF errors
if readErr != io.EOF {
err = readErr
}
}()
return out
}
// getNextTwoLines reads the next pair of lines from the LineReader.
// It returns the content and newline state for both lines.
//
// If curLine2 is provided, it's used as line1 (optimization for advancing through file).
// Otherwise, both lines are read fresh from the reader.
//
// Returns:
// - line1, hadNewline1: First line content and newline state
// - line2, hadNewline2: Second line content and newline state
// - err: Error only if we couldn't read line1, or if line2 read failed with non-EOF error
//
// Note: If line2 returns (content, false, io.EOF), we treat this as SUCCESS because
// we got the content. The EOF just means it didn't have a trailing newline.
func getNextTwoLines(lr *LineReader, curLine2 *lineState) (
line1 []byte, hadNewline1 bool,
line2 []byte, hadNewline2 bool,
err error,
) {
func getNextTwoLines(reader *bufio.Reader, curLine2 *[]byte) (line1 []byte, line2 []byte, err error) {
line2 = []byte{}
if curLine2 == nil {
// Read both lines fresh
line1, hadNewline1, err = lr.ReadLine()
line1, err = readLine(reader)
if err != nil {
return
}
line2, hadNewline2, err = lr.ReadLine()
// If we got line2 content but hit EOF, that's OK - it just means no trailing newline
if err == io.EOF && len(line2) > 0 {
err = nil // Clear the error - we successfully read both lines
}
line2, err = readLine(reader)
return
}
// Use cached line2 as new line1 (optimization)
line1 = curLine2.content
hadNewline1 = curLine2.hadNewline
// Read new line2
line2, hadNewline2, err = lr.ReadLine()
// If we got line2 content but hit EOF, that's OK - it just means no trailing newline
if err == io.EOF && len(line2) > 0 {
err = nil // Clear the error - we successfully read both lines
line1 = *curLine2
line2, err = readLine(reader)
if err != nil {
return
}
return
}
// writeLine writes a line to the writer, optionally adding a newline if hadNewline is true.
// This helper reduces code duplication in the Redact function.
func writeLine(w io.Writer, line []byte, hadNewline bool) error {
if err := writeBytes(w, line); err != nil {
return err
}
if hadNewline {
if err := writeBytes(w, NEW_LINE); err != nil {
return err
}
}
return nil
}
// writeBytes writes all byte slices to the writer
// in the order they are passed in the variadic argument
func writeBytes(w io.Writer, bs ...[]byte) error {
+13 -285
View File
@@ -28,7 +28,8 @@ func Test_NewMultiLineRedactor(t *testing.T) {
inputString: `"name": "secret_access_key"
"value": "dfeadsfsdfe"`,
wantString: `"name": "secret_access_key"
"value": "***HIDDEN***"`, // No trailing newline in input, so none in output
"value": "***HIDDEN***"
`,
},
{
name: "Redact multiline with AWS secret id",
@@ -39,7 +40,8 @@ func Test_NewMultiLineRedactor(t *testing.T) {
inputString: `"name": "ACCESS_KEY_ID"
"value": "dfeadsfsdfe"`,
wantString: `"name": "ACCESS_KEY_ID"
"value": "***HIDDEN***"`, // No trailing newline in input, so none in output
"value": "***HIDDEN***"
`,
},
{
name: "Redact multiline with OSD",
@@ -50,7 +52,8 @@ func Test_NewMultiLineRedactor(t *testing.T) {
inputString: `"entity": "osd.1abcdef"
"key": "Gjt8s0WkfPtxZUo7gI8a0awbQGHgzuprdaedfb=="`,
wantString: `"entity": "osd.1abcdef"
"key": "***HIDDEN***"`, // No trailing newline in input, so none in output
"key": "***HIDDEN***"
`,
},
{
name: "Redact multiline with AWS secret access key and scan regex",
@@ -62,7 +65,8 @@ func Test_NewMultiLineRedactor(t *testing.T) {
inputString: `"name": "secret_access_key"
"value": "dfeadsfsdfe"`,
wantString: `"name": "secret_access_key"
"value": "***HIDDEN***"`, // No trailing newline in input, so none in output
"value": "***HIDDEN***"
`,
},
{
name: "Redact multiline with AWS secret id and scan regex",
@@ -74,7 +78,8 @@ func Test_NewMultiLineRedactor(t *testing.T) {
inputString: `"name": "ACCESS_KEY_ID"
"value": "dfeadsfsdfe"`,
wantString: `"name": "ACCESS_KEY_ID"
"value": "***HIDDEN***"`, // No trailing newline in input, so none in output
"value": "***HIDDEN***"
`,
},
{
name: "Redact multiline with OSD and scan regex",
@@ -86,7 +91,8 @@ func Test_NewMultiLineRedactor(t *testing.T) {
inputString: `"entity": "osd.1abcdef"
"key": "Gjt8s0WkfPtxZUo7gI8a0awbQGHgzuprdaedfb=="`,
wantString: `"entity": "osd.1abcdef"
"key": "***HIDDEN***"`, // No trailing newline in input, so none in output
"key": "***HIDDEN***"
`,
},
{
name: "Multiple newlines with no match",
@@ -96,7 +102,7 @@ func Test_NewMultiLineRedactor(t *testing.T) {
},
redactor: `(?i)("value": *")(?P<mask>.*[^\"]*)(")`,
inputString: "no match\n\n no match \n\n",
wantString: "no match\n\n no match \n\n", // Input has trailing newline, should be preserved
wantString: "no match\n\n no match \n\n",
},
}
for _, tt := range tests {
@@ -152,281 +158,3 @@ func Test_writeBytes(t *testing.T) {
})
}
}
// Test 3.16: Binary file (no newlines) → unchanged
func TestMultiLineRedactor_BinaryFile(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
// Binary content with no newlines - the bug that caused 2 extra bytes
binaryData := []byte{0x01, 0x02, 0x03, 0x04, 0x00, 0xFF, 0xFE, 0xAB, 0xCD}
redactor, err := NewMultiLineRedactor(
LineRedactor{regex: `"name":`},
`"value":`,
MASK_TEXT, "testfile", "test", false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader(binaryData), "test.bin")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, binaryData, result, "Binary file should be unchanged (no extra newlines)")
}
// Test: Binary file with every single byte value (0x00 -> 0xFF)
func TestMultiLineRedactor_AllSingleByteValues(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
// Create binary data with every possible byte value
binaryData := make([]byte, 256)
for i := 0; i < 256; i++ {
binaryData[i] = byte(i)
}
redactor, err := NewMultiLineRedactor(
LineRedactor{regex: `"name":`},
`"value":`,
MASK_TEXT, "testfile", t.Name(), false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader(binaryData), "test.bin")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, binaryData, result, "Binary file with all byte values should be unchanged")
require.Len(t, result, 256, "Should preserve all 256 bytes")
}
// Test: Binary file with every two-byte combination (0x00+0x00 -> 0xFF+0xFF)
func TestMultiLineRedactor_AllTwoByteValues(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
// Create binary data with all 65,536 two-byte combinations (128KB)
binaryData := make([]byte, 256*256*2)
pos := 0
for i := 0; i < 256; i++ {
for j := 0; j < 256; j++ {
binaryData[pos] = byte(i)
binaryData[pos+1] = byte(j)
pos += 2
}
}
redactor, err := NewMultiLineRedactor(
LineRedactor{regex: `"name":`},
`"value":`,
MASK_TEXT, "testfile", t.Name(), false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader(binaryData), "test.bin")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, binaryData, result, "Binary file with all two-byte combinations should be unchanged")
require.Len(t, result, 256*256*2, "Should preserve all 131,072 bytes (64k combinations)")
}
// Test 3.17: Single line with \n → unchanged
func TestMultiLineRedactor_SingleLineWithNewline(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := "single line\n"
redactor, err := NewMultiLineRedactor(
LineRedactor{regex: `"name":`},
`"value":`,
MASK_TEXT, "testfile", "test", false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, "single line\n", string(result))
}
// Test 3.18: Single line without \n → unchanged
func TestMultiLineRedactor_SingleLineWithoutNewline(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := "single line"
redactor, err := NewMultiLineRedactor(
LineRedactor{regex: `"name":`},
`"value":`,
MASK_TEXT, "testfile", "test", false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, "single line", string(result), "No newline should be added")
}
// Test 3.19: Empty file → unchanged
func TestMultiLineRedactor_EmptyFile(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := ""
redactor, err := NewMultiLineRedactor(
LineRedactor{regex: `"name":`},
`"value":`,
MASK_TEXT, "testfile", "test", false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, "", string(result), "Empty file should remain empty")
}
// Test 3.20: Two lines, matches selector → line2 redacted
func TestMultiLineRedactor_TwoLinesMatch(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := `"name": "PASSWORD"
"value": "secret123"`
redactor, err := NewMultiLineRedactor(
LineRedactor{regex: `(?i)"name": *"PASSWORD"`},
`(?i)("value": *")(?P<mask>[^"]*)(")`,
MASK_TEXT, "testfile", "test", false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
expected := `"name": "PASSWORD"
"value": "***HIDDEN***"`
require.Equal(t, expected, string(result))
}
// Test 3.21: Two lines, no selector match → unchanged
func TestMultiLineRedactor_TwoLinesNoMatch(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := `"name": "USERNAME"
"value": "admin"`
redactor, err := NewMultiLineRedactor(
LineRedactor{regex: `(?i)"name": *"PASSWORD"`},
`(?i)("value": *")(?P<mask>[^"]*)(")`,
MASK_TEXT, "testfile", "test", false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
expected := `"name": "USERNAME"
"value": "admin"`
require.Equal(t, expected, string(result))
}
// Test 3.22: Multiple line pairs → correct redactions
func TestMultiLineRedactor_MultiplePairs(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := `"name": "PASSWORD"
"value": "secret1"
"name": "TOKEN"
"value": "secret2"
"name": "USERNAME"
"value": "admin"
`
redactor, err := NewMultiLineRedactor(
LineRedactor{regex: `(?i)"name": *"(PASSWORD|TOKEN)"`},
`(?i)("value": *")(?P<mask>[^"]*)(")`,
MASK_TEXT, "testfile", "test", false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
expected := `"name": "PASSWORD"
"value": "***HIDDEN***"
"name": "TOKEN"
"value": "***HIDDEN***"
"name": "USERNAME"
"value": "admin"
`
require.Equal(t, expected, string(result))
}
// Test 3.23: Three lines (pair + unpaired)
func TestMultiLineRedactor_ThreeLines(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := `"name": "PASSWORD"
"value": "secret"
unpaired line`
redactor, err := NewMultiLineRedactor(
LineRedactor{regex: `(?i)"name": *"PASSWORD"`},
`(?i)("value": *")(?P<mask>[^"]*)(")`,
MASK_TEXT, "testfile", "test", false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
expected := `"name": "PASSWORD"
"value": "***HIDDEN***"
unpaired line`
require.Equal(t, expected, string(result))
}
// Test 3.24: Large file with selector matches
func TestMultiLineRedactor_LargeFile(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
var input strings.Builder
for i := 0; i < 1000; i++ {
input.WriteString(`"name": "PASSWORD"` + "\n")
input.WriteString(`"value": "secret"` + "\n")
}
redactor, err := NewMultiLineRedactor(
LineRedactor{regex: `(?i)"name": *"PASSWORD"`},
`(?i)("value": *")(?P<mask>[^"]*)(")`,
MASK_TEXT, "testfile", "test", false,
)
require.NoError(t, err)
out := redactor.Redact(strings.NewReader(input.String()), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
// Verify all secrets were redacted
require.NotContains(t, string(result), `"value": "secret"`)
require.Contains(t, string(result), `"value": "***HIDDEN***"`)
}
-4
View File
@@ -104,10 +104,6 @@ func GetRedactionList() RedactionList {
}
func ResetRedactionList() {
// Wait for all pending redaction goroutines to complete before resetting
// This prevents race conditions where goroutines write to the map after reset
pendingRedactions.Wait()
redactionListMut.Lock()
defer redactionListMut.Unlock()
allRedactions = RedactionList{
-1
View File
@@ -1724,7 +1724,6 @@ func Test_Redactors(t *testing.T) {
t.Run("test default redactors", func(t *testing.T) {
req := require.New(t)
ResetRedactionList() // Ensure clean state before test
redactors, err := getRedactors("testpath")
req.NoError(err)
+45 -75
View File
@@ -3,7 +3,6 @@ package redact
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"regexp"
@@ -40,13 +39,6 @@ func NewSingleLineRedactor(re LineRedactor, maskText, path, name string, isDefau
return &SingleLineRedactor{scan: scanCompiled, re: compiled, maskText: maskText, filePath: path, redactName: name, isDefault: isDefault}, nil
}
// Redact processes the input reader line-by-line, applying redaction patterns.
// Unlike the previous implementation using bufio.Scanner, this now uses LineReader
// to preserve the exact newline structure of the input file. Lines that originally
// ended with \n will have \n added back, while lines without \n (like the last line
// of a file without a trailing newline, or binary files) will not have \n added.
// This ensures binary files and text files without trailing newlines are not corrupted.
func (r *SingleLineRedactor) Redact(input io.Reader, path string) io.Reader {
out, writer := io.Pipe()
@@ -56,8 +48,7 @@ func (r *SingleLineRedactor) Redact(input io.Reader, path string) io.Reader {
if err == nil || err == io.EOF {
writer.Close()
} else {
// Check if error is about line exceeding maximum size
if errors.Is(err, bufio.ErrTooLong) {
if err == bufio.ErrTooLong {
s := fmt.Sprintf("Error redacting %q. A line in the file exceeded %d MB max length", path, constants.SCANNER_MAX_SIZE/1024/1024)
klog.V(2).Info(s)
} else {
@@ -67,89 +58,68 @@ func (r *SingleLineRedactor) Redact(input io.Reader, path string) io.Reader {
}
}()
// Use LineReader instead of bufio.Scanner to track newline presence
lineReader := NewLineReader(input)
buf := make([]byte, constants.BUF_INIT_SIZE)
scanner := bufio.NewScanner(input)
scanner.Buffer(buf, constants.SCANNER_MAX_SIZE)
tokenizer := GetGlobalTokenizer()
lineNum := 0
for {
line, hadNewline, readErr := lineReader.ReadLine()
// Handle EOF with no content - we're done
if readErr == io.EOF && len(line) == 0 {
break
}
// We have content to process
for scanner.Scan() {
lineNum++
line := scanner.Bytes()
// Determine if we should redact this line
shouldRedact := true
// Pre-filter: if scan is not nil, check if line matches scan by lowercasing it
// is scan is not nil, then check if line matches scan by lowercasing it
if r.scan != nil {
lowerLine := bytes.ToLower(line)
if !r.scan.Match(lowerLine) {
shouldRedact = false
// Append newline since scanner strips it
err = writeBytes(writer, line, NEW_LINE)
if err != nil {
return
}
continue
}
}
// Check if line matches the main redaction pattern
if shouldRedact && !r.re.Match(line) {
shouldRedact = false
}
// Process the line (redact or pass through)
var outputLine []byte
if shouldRedact {
// Line matches - perform redaction
if tokenizer.IsEnabled() {
// Use tokenized replacement - context comes from the redactor name
context := r.redactName
outputLine = getTokenizedReplacementPatternWithPath(r.re, line, context, r.filePath)
} else {
// Use original masking behavior
substStr := []byte(getReplacementPattern(r.re, r.maskText))
outputLine = r.re.ReplaceAll(line, substStr)
}
// Track redaction if content changed
if !bytes.Equal(outputLine, line) {
addRedaction(Redaction{
RedactorName: r.redactName,
CharactersRemoved: len(line) - len(outputLine),
Line: lineNum,
File: r.filePath,
IsDefaultRedactor: r.isDefault,
})
}
} else {
// No match - use original line
outputLine = line
}
// Write the line
err = writeBytes(writer, outputLine)
if err != nil {
return
}
// Only add newline if original line had one
if hadNewline {
err = writeBytes(writer, NEW_LINE)
// if scan matches, but re does not, do not redact
if !r.re.Match(line) {
// Append newline since scanner strips it
err = writeBytes(writer, line, NEW_LINE)
if err != nil {
return
}
continue
}
// Check if we hit EOF after processing this line
if readErr == io.EOF {
break
var clean []byte
if tokenizer.IsEnabled() {
// Use tokenized replacement - context comes from the redactor name which often indicates the secret type
context := r.redactName
clean = getTokenizedReplacementPatternWithPath(r.re, line, context, r.filePath)
} else {
// Use original masking behavior
substStr := []byte(getReplacementPattern(r.re, r.maskText))
clean = r.re.ReplaceAll(line, substStr)
}
// Check for non-EOF errors
if readErr != nil {
err = readErr
// Append newline since scanner strips it
err = writeBytes(writer, clean, NEW_LINE)
if err != nil {
return
}
// if clean is not equal to line, a redaction was performed
if !bytes.Equal(clean, line) {
addRedaction(Redaction{
RedactorName: r.redactName,
CharactersRemoved: len(line) - len(clean),
Line: lineNum,
File: r.filePath,
IsDefaultRedactor: r.isDefault,
})
}
}
if scanErr := scanner.Err(); scanErr != nil {
err = scanErr
}
}()
return out
+16 -325
View File
@@ -21,7 +21,7 @@ func TestNewSingleLineRedactor(t *testing.T) {
name: "copied from default redactors",
re: `(?i)(Pwd *= *)(?P<mask>[^\;]+)(;)`,
inputString: `pwd = abcdef;`,
wantString: "pwd = ***HIDDEN***;", // No trailing newline in input, so none in output
wantString: "pwd = ***HIDDEN***;\n",
wantRedactions: RedactionList{
ByRedactor: map[string][]Redaction{
"copied from default redactors": []Redaction{
@@ -49,7 +49,7 @@ func TestNewSingleLineRedactor(t *testing.T) {
name: "no leading matching group", // this is not the ideal behavior - why are we dropping ungrouped match components?
re: `(?i)Pwd *= *(?P<mask>[^\;]+)(;)`,
inputString: `pwd = abcdef;`,
wantString: "***HIDDEN***;", // No trailing newline in input, so none in output
wantString: "***HIDDEN***;\n",
wantRedactions: RedactionList{
ByRedactor: map[string][]Redaction{
"no leading matching group": []Redaction{
@@ -77,7 +77,7 @@ func TestNewSingleLineRedactor(t *testing.T) {
name: "multiple matching literals",
re: `(?i)(Pwd *= *)(?P<mask>[^\;]+)(;)`,
inputString: `pwd = abcdef;abcdef`,
wantString: "pwd = ***HIDDEN***;abcdef", // No trailing newline in input, so none in output
wantString: "pwd = ***HIDDEN***;abcdef\n",
wantRedactions: RedactionList{
ByRedactor: map[string][]Redaction{
"multiple matching literals": []Redaction{
@@ -105,7 +105,8 @@ func TestNewSingleLineRedactor(t *testing.T) {
name: "Redact values for environment variables that look like AWS Secret Access Keys",
re: `(?i)("name":"[^\"]*SECRET_?ACCESS_?KEY","value":")(?P<mask>[^\"]*)(")`,
inputString: `{"name":"SECRET_ACCESS_KEY","value":"123"}`,
wantString: `{"name":"SECRET_ACCESS_KEY","value":"***HIDDEN***"}`, // No trailing newline in input, so none in output
wantString: `{"name":"SECRET_ACCESS_KEY","value":"***HIDDEN***"}
`,
wantRedactions: RedactionList{
ByRedactor: map[string][]Redaction{
"Redact values for environment variables that look like AWS Secret Access Keys": []Redaction{
@@ -133,7 +134,7 @@ func TestNewSingleLineRedactor(t *testing.T) {
name: "Redact connection strings with username and password",
re: `(?i)(https?|ftp)(:\/\/)(?P<mask>[^:\"\/]+){1}(:)(?P<mask>[^@\"\/]+){1}(?P<host>@[^:\/\s\"]+){1}(?P<port>:[\d]+)?`,
inputString: `http://user:password@host:8888`,
wantString: "http://***HIDDEN***:***HIDDEN***@host:8888", // No trailing newline in input, so none in output
wantString: "http://***HIDDEN***:***HIDDEN***@host:8888\n",
wantRedactions: RedactionList{
ByRedactor: map[string][]Redaction{
"Redact connection strings with username and password": []Redaction{
@@ -162,7 +163,8 @@ func TestNewSingleLineRedactor(t *testing.T) {
re: `(?i)("name":"[^\"]*SECRET_?ACCESS_?KEY","value":")(?P<mask>[^\"]*)(")`,
scan: `secret_?access_?key`,
inputString: `{"name":"SECRET_ACCESS_KEY","value":"123"}`,
wantString: `{"name":"SECRET_ACCESS_KEY","value":"***HIDDEN***"}`, // No trailing newline in input, so none in output
wantString: `{"name":"SECRET_ACCESS_KEY","value":"***HIDDEN***"}
`,
wantRedactions: RedactionList{
ByRedactor: map[string][]Redaction{
"Redact values for environment variables that look like AWS Secret Access Keys With Scan": {
@@ -191,7 +193,8 @@ func TestNewSingleLineRedactor(t *testing.T) {
re: `(?i)("name":"[^\"]*ACCESS_?KEY_?ID","value":")(?P<mask>[^\"]*)(")`,
scan: `access_?key_?id`,
inputString: `{"name":"ACCESS_KEY_ID","value":"123"}`,
wantString: `{"name":"ACCESS_KEY_ID","value":"***HIDDEN***"}`, // No trailing newline in input, so none in output
wantString: `{"name":"ACCESS_KEY_ID","value":"***HIDDEN***"}
`,
wantRedactions: RedactionList{
ByRedactor: map[string][]Redaction{
"Redact values for environment variables that look like Access Keys ID With Scan": {
@@ -220,7 +223,8 @@ func TestNewSingleLineRedactor(t *testing.T) {
re: `(?i)("name":"[^\"]*OWNER_?ACCOUNT","value":")(?P<mask>[^\"]*)(")`,
scan: `owner_?account`,
inputString: `{"name":"OWNER_ACCOUNT","value":"123"}`,
wantString: `{"name":"OWNER_ACCOUNT","value":"***HIDDEN***"}`, // No trailing newline in input, so none in output
wantString: `{"name":"OWNER_ACCOUNT","value":"***HIDDEN***"}
`,
wantRedactions: RedactionList{
ByRedactor: map[string][]Redaction{
"Redact values for environment variables that look like Owner Account With Scan": {
@@ -249,7 +253,8 @@ func TestNewSingleLineRedactor(t *testing.T) {
re: `(?i)(Data Source *= *)(?P<mask>[^\;]+)(;)`,
scan: `data source`,
inputString: `Data Source = abcdef;`,
wantString: `Data Source = ***HIDDEN***;`, // No trailing newline in input, so none in output
wantString: `Data Source = ***HIDDEN***;
`,
wantRedactions: RedactionList{
ByRedactor: map[string][]Redaction{
"Redact 'Data Source' values With Scan": {
@@ -278,7 +283,8 @@ func TestNewSingleLineRedactor(t *testing.T) {
re: `(?i)(https?|ftp)(:\/\/)(?P<mask>[^:\"\/]+){1}(:)(?P<mask>[^@\"\/]+){1}(?P<host>@[^:\/\s\"]+){1}(?P<port>:[\d]+)?`,
scan: `https?|ftp`,
inputString: `http://user:password@host:8888;`,
wantString: `http://***HIDDEN***:***HIDDEN***@host:8888;`, // No trailing newline in input, so none in output
wantString: `http://***HIDDEN***:***HIDDEN***@host:8888;
`,
wantRedactions: RedactionList{
ByRedactor: map[string][]Redaction{
"Redact connection strings With Scan": {
@@ -334,318 +340,3 @@ func TestNewSingleLineRedactor(t *testing.T) {
})
}
}
// Test 2.15: Binary file (no newlines) → unchanged
func TestSingleLineRedactor_BinaryFile(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
// Binary content with no newlines
binaryData := []byte{0x01, 0x02, 0x03, 0x04, 0x00, 0xFF, 0xFE, 0xAB, 0xCD}
redactor, err := NewSingleLineRedactor(
LineRedactor{regex: "password"}, // Pattern that won't match binary
MASK_TEXT, "testfile", "test", false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader(binaryData), "test.bin")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, binaryData, result, "Binary file should be unchanged")
}
// Test: Binary file with every single byte value (0x00 -> 0xFF)
func TestSingleLineRedactor_AllSingleByteValues(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
// Create binary data with every possible byte value
binaryData := make([]byte, 256)
for i := 0; i < 256; i++ {
binaryData[i] = byte(i)
}
redactor, err := NewSingleLineRedactor(
LineRedactor{regex: `secret`},
MASK_TEXT, "testfile", t.Name(), false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader(binaryData), "test.bin")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, binaryData, result, "Binary file with all byte values should be unchanged")
require.Len(t, result, 256, "Should preserve all 256 bytes")
}
// Test: Binary file with every two-byte combination (0x00+0x00 -> 0xFF+0xFF)
func TestSingleLineRedactor_AllTwoByteValues(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
// Create binary data with all 65,536 two-byte combinations (128KB)
binaryData := make([]byte, 256*256*2)
pos := 0
for i := 0; i < 256; i++ {
for j := 0; j < 256; j++ {
binaryData[pos] = byte(i)
binaryData[pos+1] = byte(j)
pos += 2
}
}
redactor, err := NewSingleLineRedactor(
LineRedactor{regex: `secret`},
MASK_TEXT, "testfile", t.Name(), false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader(binaryData), "test.bin")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, binaryData, result, "Binary file with all two-byte combinations should be unchanged")
require.Len(t, result, 256*256*2, "Should preserve all 131,072 bytes (64k combinations)")
}
// Test 2.16: Text file with \n → preserved
func TestSingleLineRedactor_TextFileWithNewline(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := "hello world\n"
redactor, err := NewSingleLineRedactor(
LineRedactor{regex: "xyz"}, // No match
MASK_TEXT, "testfile", "test", false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, "hello world\n", string(result), "Trailing newline should be preserved")
}
// Test 2.17: Text file without \n → preserved
func TestSingleLineRedactor_TextFileWithoutNewline(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := "hello world"
redactor, err := NewSingleLineRedactor(
LineRedactor{regex: "xyz"}, // No match
MASK_TEXT, "testfile", "test", false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, "hello world", string(result), "No newline should be added")
}
// Test 2.18: Empty file → unchanged
func TestSingleLineRedactor_EmptyFile(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := ""
redactor, err := NewSingleLineRedactor(
LineRedactor{regex: "password"},
MASK_TEXT, "testfile", "test", false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, "", string(result), "Empty file should remain empty")
}
// Test 2.19: Single line with secret → redacted correctly
func TestSingleLineRedactor_SingleLineWithSecret(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := "password=secret123"
redactor, err := NewSingleLineRedactor(
LineRedactor{regex: `(?i)(password=)(?P<mask>.*)`},
MASK_TEXT, "testfile", "test", false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, "password=***HIDDEN***", string(result))
}
// Test 2.20: Multiple lines with secrets → all redacted
func TestSingleLineRedactor_MultipleLinesWithSecrets(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := "password=secret1\npassword=secret2\npassword=secret3\n"
redactor, err := NewSingleLineRedactor(
LineRedactor{regex: `(?i)(password=)(?P<mask>.*)`},
MASK_TEXT, "testfile", "test", false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
expected := "password=***HIDDEN***\npassword=***HIDDEN***\npassword=***HIDDEN***\n"
require.Equal(t, expected, string(result))
}
// Test 2.21: Scan pattern filters correctly
func TestSingleLineRedactor_ScanPatternFilters(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := "password=secret\nusername=admin\n"
redactor, err := NewSingleLineRedactor(
LineRedactor{
regex: `(?i)(password=)(?P<mask>.*)`,
scan: `password`, // Only process lines containing "password"
},
MASK_TEXT, "testfile", "test", false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
expected := "password=***HIDDEN***\nusername=admin\n"
require.Equal(t, expected, string(result))
}
// Test 2.22: File with only one newline \n → one newline out
func TestSingleLineRedactor_OnlyNewline(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
input := "\n"
redactor, err := NewSingleLineRedactor(
LineRedactor{regex: "password"},
MASK_TEXT, "testfile", "test", false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, "\n", string(result))
}
// Test 2.23: Mixed binary/text content
func TestSingleLineRedactor_MixedContent(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
// Binary data with embedded newline
input := []byte{0x01, 0x02, '\n', 0x03, 0x04}
redactor, err := NewSingleLineRedactor(
LineRedactor{regex: "password"},
MASK_TEXT, "testfile", "test", false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader(input), "test.bin")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, input, result, "Mixed content should be preserved")
}
// Test: Windows CRLF (\r\n) line endings preserved
func TestSingleLineRedactor_WindowsLineEndings(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
// Windows-style line endings with no secrets
input := "line1\r\nline2\r\nline3\r\n"
redactor, err := NewSingleLineRedactor(
LineRedactor{regex: `secret`},
MASK_TEXT, "testfile", t.Name(), false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
// Windows line endings should be preserved exactly
require.Equal(t, "line1\r\nline2\r\nline3\r\n", string(result),
"Windows CRLF line endings should be preserved, not converted to LF")
}
// Test: Windows CRLF with redaction
func TestSingleLineRedactor_WindowsLineEndingsWithRedaction(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
// Windows-style line endings with a secret
// Note: LineReader splits on \n, so line content includes \r
input := "password=secret123\r\nusername=admin\r\n"
// Use a regex that doesn't capture the \r
redactor, err := NewSingleLineRedactor(
LineRedactor{regex: `(password=)(?P<mask>[^\r\n]+)`},
MASK_TEXT, "testfile", t.Name(), false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader([]byte(input)), "test.txt")
result, err := io.ReadAll(out)
require.NoError(t, err)
// \r\n should be preserved: regex doesn't capture \r, so it stays in output
expected := "password=***HIDDEN***\r\nusername=admin\r\n"
require.Equal(t, expected, string(result),
"Windows CRLF should be preserved - \\r not captured by regex, \\n added by LineReader")
}
// Test 2.24: Large binary file (1MB, no newlines) → preserved
func TestSingleLineRedactor_LargeBinaryFile(t *testing.T) {
ResetRedactionList()
defer ResetRedactionList()
// Create 1MB of binary data
largeData := make([]byte, 1024*1024)
for i := range largeData {
largeData[i] = byte(i % 256)
}
redactor, err := NewSingleLineRedactor(
LineRedactor{regex: "password"},
MASK_TEXT, "testfile", "test", false,
)
require.NoError(t, err)
out := redactor.Redact(bytes.NewReader(largeData), "large.bin")
result, err := io.ReadAll(out)
require.NoError(t, err)
require.Equal(t, largeData, result, "Large binary file should be unchanged")
}
+1 -7
View File
@@ -106,7 +106,6 @@ func runCollectors(ctx context.Context, collectors []*troubleshootv1beta2.Collec
}
allCollectorsMap := make(map[reflect.Type][]collect.Collector)
collectorTypeOrder := make([]reflect.Type, 0) // Preserve order of collector types
allCollectedData := make(map[string][]byte)
for _, desiredCollector := range collectSpecs {
@@ -117,17 +116,12 @@ func runCollectors(ctx context.Context, collectors []*troubleshootv1beta2.Collec
return nil, errors.Wrap(err, "failed to check RBAC for collectors")
}
collectorType := reflect.TypeOf(collector)
if _, exists := allCollectorsMap[collectorType]; !exists {
collectorTypeOrder = append(collectorTypeOrder, collectorType)
}
allCollectorsMap[collectorType] = append(allCollectorsMap[collectorType], collector)
}
}
}
// Iterate over collector types in the order they appeared in collectSpecs
for _, collectorType := range collectorTypeOrder {
collectors := allCollectorsMap[collectorType]
for _, collectors := range allCollectorsMap {
if mergeCollector, ok := collectors[0].(collect.MergeableCollector); ok {
mergedCollectors, err := mergeCollector.Merge(collectors)
if err != nil {
+3 -24
View File
@@ -16,9 +16,8 @@ import (
)
// ExtractLicenseFromBundle extracts the license ID from a support bundle
// It first looks for cluster-resources/license.json, then falls back to searching
// cluster-resources/configmaps/* for a license field
// Returns both the license ID and the app slug
// It looks in cluster-resources/configmaps/* for a license field
// Returns both the license ID and the app slug (from the filename where license was found)
func ExtractLicenseFromBundle(bundlePath string) (string, string, error) {
file, err := os.Open(bundlePath)
if err != nil {
@@ -43,27 +42,7 @@ func ExtractLicenseFromBundle(bundlePath string) (string, string, error) {
return "", "", errors.Wrap(err, "failed to read tar header")
}
// First priority: check for the new license.json file
if strings.Contains(header.Name, "cluster-resources/license.json") && header.Typeflag == tar.TypeReg {
content := make([]byte, header.Size)
if _, err := io.ReadFull(tarReader, content); err != nil {
continue
}
// Parse the license.json file
var licenseData struct {
LicenseID string `json:"licenseID"`
AppSlug string `json:"appSlug"`
}
if err := json.Unmarshal(content, &licenseData); err == nil {
if licenseData.LicenseID != "" && licenseData.AppSlug != "" {
return licenseData.LicenseID, licenseData.AppSlug, nil
}
}
continue
}
// Fallback: process files in cluster-resources/configmaps/
// Only process files in cluster-resources/configmaps/ (may be nested under bundle directory)
if !strings.Contains(header.Name, "cluster-resources/configmaps/") {
continue
}
+5 -17
View File
@@ -10,7 +10,7 @@ import (
// UploadToReplicatedApp uploads a support bundle directly to replicated.app
// using the app slug as the upload path
func UploadToReplicatedApp(bundlePath, licenseID, appSlug, uploadDomain string) error {
func UploadToReplicatedApp(bundlePath, licenseID, appSlug string) error {
// Open the bundle file
file, err := os.Open(bundlePath)
if err != nil {
@@ -23,14 +23,8 @@ func UploadToReplicatedApp(bundlePath, licenseID, appSlug, uploadDomain string)
return errors.Wrap(err, "failed to stat file")
}
// Use custom domain if provided, otherwise default to replicated.app
domain := uploadDomain
if domain == "" {
domain = "replicated.app"
}
// Build the upload URL using the app slug
uploadURL := fmt.Sprintf("https://%s/supportbundle/upload/%s", domain, appSlug)
uploadURL := fmt.Sprintf("https://replicated.app/supportbundle/upload/%s", appSlug)
// Create the request
req, err := http.NewRequest("POST", uploadURL, file)
@@ -59,7 +53,7 @@ func UploadToReplicatedApp(bundlePath, licenseID, appSlug, uploadDomain string)
}
// UploadBundleAutoDetect uploads a support bundle with automatic license and app slug detection
func UploadBundleAutoDetect(bundlePath string, providedLicenseID, providedAppSlug, uploadDomain string) error {
func UploadBundleAutoDetect(bundlePath string, providedLicenseID, providedAppSlug string) error {
licenseID := providedLicenseID
// Always extract from bundle to get app slug (and license if not provided)
@@ -85,15 +79,9 @@ func UploadBundleAutoDetect(bundlePath string, providedLicenseID, providedAppSlu
appSlug = extractedAppSlug
}
// Determine target domain for upload message
targetDomain := uploadDomain
if targetDomain == "" {
targetDomain = "replicated.app"
}
// Upload the bundle
fmt.Printf("Uploading support bundle to %s...\n", targetDomain)
if err := UploadToReplicatedApp(bundlePath, licenseID, appSlug, uploadDomain); err != nil {
fmt.Printf("Uploading support bundle to replicated.app...\n")
if err := UploadToReplicatedApp(bundlePath, licenseID, appSlug); err != nil {
return errors.Wrap(err, "failed to upload bundle")
}
+21 -21
View File
@@ -48,12 +48,14 @@ trap "rm -rf $TEMP_DIR" EXIT
echo -e "\n${BLUE}Step 1: Downloading artifacts...${NC}"
# Download all test-results artifacts from the run
# Try downloading all artifacts (will download test-results-v1beta3-*, test-results-v1beta2-*, test-results-supportbundle-*)
if ! gh run download "$RUN_ID" --dir "$TEMP_DIR" 2>/dev/null; then
echo -e "${RED}Error: Failed to download artifacts from run ${RUN_ID}${NC}"
echo "Make sure the workflow run completed successfully."
exit 1
# Download artifacts from the run
cd "$TEMP_DIR"
if ! gh run download "$RUN_ID" --name "regression-test-results-${RUN_ID}-1" 2>/dev/null; then
# Try without attempt suffix
if ! gh run download "$RUN_ID" 2>/dev/null; then
echo -e "${RED}Error: Failed to download artifacts from run ${RUN_ID}${NC}"
exit 1
fi
fi
echo -e "${GREEN}✓ Artifacts downloaded${NC}"
@@ -61,29 +63,27 @@ echo -e "${GREEN}✓ Artifacts downloaded${NC}"
# Check which bundles are present
echo -e "\n${BLUE}Step 2: Checking available bundles...${NC}"
# Use find to locate bundles in artifact subdirectories
V1BETA3_BUNDLE=$(find "$TEMP_DIR" -name "preflight-v1beta3-bundle.tar.gz" | head -1)
V1BETA2_BUNDLE=$(find "$TEMP_DIR" -name "preflight-v1beta2-bundle.tar.gz" | head -1)
SUPPORTBUNDLE=$(find "$TEMP_DIR" -name "supportbundle.tar.gz" | head -1)
V1BETA3_BUNDLE=""
V1BETA2_BUNDLE=""
SUPPORTBUNDLE=""
if [ -n "$V1BETA3_BUNDLE" ]; then
if [ -f "preflight-v1beta3-bundle.tar.gz" ] || [ -f "test/output/preflight-v1beta3-bundle.tar.gz" ]; then
V1BETA3_BUNDLE=$(find . -name "preflight-v1beta3-bundle.tar.gz" | head -1)
echo -e "${GREEN}${NC} Found v1beta3 preflight bundle"
fi
if [ -n "$V1BETA2_BUNDLE" ]; then
if [ -f "preflight-v1beta2-bundle.tar.gz" ] || [ -f "test/output/preflight-v1beta2-bundle.tar.gz" ]; then
V1BETA2_BUNDLE=$(find . -name "preflight-v1beta2-bundle.tar.gz" | head -1)
echo -e "${GREEN}${NC} Found v1beta2 preflight bundle"
fi
if [ -n "$SUPPORTBUNDLE" ]; then
if [ -f "supportbundle.tar.gz" ] || [ -f "test/output/supportbundle.tar.gz" ]; then
SUPPORTBUNDLE=$(find . -name "supportbundle.tar.gz" | head -1)
echo -e "${GREEN}${NC} Found support bundle"
fi
if [ -z "$V1BETA3_BUNDLE" ] && [ -z "$V1BETA2_BUNDLE" ] && [ -z "$SUPPORTBUNDLE" ]; then
echo -e "${RED}Error: No bundles found in artifacts${NC}"
echo "Downloaded artifacts:"
ls -la "$TEMP_DIR"
echo -e "\nSearching for bundles:"
find "$TEMP_DIR" -name "*.tar.gz" -o -name "*.json"
exit 1
fi
@@ -110,21 +110,21 @@ echo -e "\n${BLUE}Step 3: Updating baselines...${NC}"
# Update v1beta3 baseline
if [ -n "$V1BETA3_BUNDLE" ]; then
mkdir -p test/baselines/preflight-v1beta3
cp "$V1BETA3_BUNDLE" test/baselines/preflight-v1beta3/baseline.tar.gz
cp "$TEMP_DIR/$V1BETA3_BUNDLE" test/baselines/preflight-v1beta3/baseline.tar.gz
echo -e "${GREEN}${NC} Updated preflight-v1beta3 baseline"
fi
# Update v1beta2 baseline
if [ -n "$V1BETA2_BUNDLE" ]; then
mkdir -p test/baselines/preflight-v1beta2
cp "$V1BETA2_BUNDLE" test/baselines/preflight-v1beta2/baseline.tar.gz
cp "$TEMP_DIR/$V1BETA2_BUNDLE" test/baselines/preflight-v1beta2/baseline.tar.gz
echo -e "${GREEN}${NC} Updated preflight-v1beta2 baseline"
fi
# Update support bundle baseline
if [ -n "$SUPPORTBUNDLE" ]; then
mkdir -p test/baselines/supportbundle
cp "$SUPPORTBUNDLE" test/baselines/supportbundle/baseline.tar.gz
cp "$TEMP_DIR/$SUPPORTBUNDLE" test/baselines/supportbundle/baseline.tar.gz
echo -e "${GREEN}${NC} Updated supportbundle baseline"
fi
@@ -139,7 +139,7 @@ cat > test/baselines/metadata.json <<EOF
"updated_at": "$CURRENT_DATE",
"git_sha": "$GIT_SHA",
"workflow_run_id": "$RUN_ID",
"k8s_version": "v1.31.2-k3s1",
"k8s_version": "v1.28.3",
"updated_by": "$(git config user.name) <$(git config user.email)>"
}
EOF
-7
View File
@@ -1,7 +0,0 @@
{
"updated_at": "2025-12-10T17:29:30Z",
"git_sha": "7d73318d1ea65a72cfe2ffa7ea52940cf4b98f18",
"workflow_run_id": "20107431959",
"k8s_version": "v1.31.2-k3s1",
"updated_by": "hedge-sparrow <sparrow@spooky.academy>"
}
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More