Compare commits

...
83 Commits
Author SHA1 Message Date
Kris ColemanandClaude Opus 4.7 2192aecf40 fix(analyze/secret): guard nil deref when spec omits fail: outcome (#2053) (#2054)
* fix(analyze/secret): guard nil deref when spec omits fail: outcome (#2053)

When a Preflight spec defines only warn: and/or pass: outcomes and the
target Secret is missing, analyzeSecret dereferenced a nil failOutcome and
panicked. Reproduction from replicatedhq/troubleshoot#2053:

  outcomes:
    - warn:
        when: "notFound"
        message: "secret missing (warn)"
    - pass:
        message: "secret found"

This change:
- Collects fail, warn, and warn-when-notFound outcomes up front.
- Routes a missing secret (or missing key) through a single resolver:
  prefer warn(when=notFound), fall back to fail, then any warn,
  then synthesize a benign warn result. Never panics.
- Adds table-driven tests covering the three new shapes.

Same defect shape as #263 (imagePullSecret). The sibling analyzers
configmap.go and image.go have the same pattern but are out of scope
for this PR.

Refs: replicatedhq/troubleshoot#2053

* refactor(analyze/secret): drop warn handling per analyzer contract

Per review feedback (banjoh): the secret analyzer only supports fail
(not found) and pass (found) outcomes — `warn:` and `when:` are not
part of the analyzer's contract (https://troubleshoot.sh/docs/analyze/secrets).

Drop the warn / notFoundWarn branches added in the previous commit and
collapse the nil-fail fallback onto IsFail with a default message. The
core fix — guarding the nil deref at the previous secret.go:72 — stays
in place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(analyze/secret): mirror image_pull_secret pattern; return nil when spec has neither outcome

Per banjoh review: collect failOutcome and passOutcome with non-nil
checks; when the spec contains neither, return nil and let the framework
surface the missing-outcome error rather than fabricating a result.

Structure now mirrors pkg/analyze/image_pull_secret.go: default to
IsFail with fail-outcome message (if set), flip to IsPass with
pass-outcome message when the secret/key check succeeds, fill default
messages at the end only when none were configured. Analyze() now
forwards a nil result through cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(analyze/secret): assign outcome messages inside their own branch

Cursor bugbot caught: when the spec defined only a fail outcome and the
secret was found, the result showed IsPass=true with the fail outcome's
message. Pre-assigning the fail message at the top before flipping
IsPass meant the stale message leaked through whenever no pass outcome
was configured.

Move both message assignments into their respective branches so a pass
result never carries a fail message. Default messages still fill in
when no outcome is set on the active branch. Added a regression test
covering fail-only spec + secret found.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(analyze/secret): address Greptile P1s — combined outcome + empty configured messages

Two fixes flagged by review on #2054:

1. Combined pass outcome dropped: the outcome loop used 'else if' for pass, so a
   single outcome object containing both fail and pass silently dropped the pass.
   Capture fail and pass independently.

2. Empty configured messages overwritten: the default-message fallback fired
   whenever result.Message was empty, clobbering a configured outcome that
   intentionally has an empty message (e.g. URI-only). Track whether the matched
   branch had a configured outcome and only fall back to a default when none was
   supplied, preserving the configured message and URI verbatim.

Adds tests: combined fail+pass captured on both found/not-found paths, and
URI-only pass/fail outcomes preserved without default-message overwrite.

* fix(analyze/secret): drop fabricated default messages; error on missing outcome

Per maintainer review: analyzers do not fabricate default messages — an empty
outcome message is intentional (e.g. a URI-only outcome), so remove the default
message fallback and preserve the configured outcome verbatim. When a matched
branch has no configured outcome, the message stays empty.

Also address the missing-outcome case: when a spec defines neither a pass nor a
fail outcome, analyzeSecret returned (nil, nil), which the Analyze wrapper
swallowed into an empty result slice — the user saw neither a result nor a
config error. Return an explicit error so the framework surfaces the
misconfiguration.

Tests updated: the no-fail-outcome and only-fail-outcome cases now assert an
empty message rather than a fabricated one, and the neither-outcome case asserts
an error.

* fix(analyze/secret): default message only when no outcome is configured for the matched branch

Distinguish an absent matching outcome from an intentionally empty configured
message. A configured outcome with an empty message (e.g. a URI-only outcome) is
still preserved verbatim. But when the matched branch has no configured outcome at
all — a pass-only spec that took the fail path, or a fail-only spec that passed —
fall back to a default diagnostic instead of emitting an empty message.

Addresses the greptile P1 (endorsed by banjoh): dropping the default entirely
conflated the two cases and left users with a pass/fail result and no context.
Tests restore the default-message expectations for the no-configured-outcome
branches; URI-only (configured-empty) and neither-outcome (error) cases unchanged.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-08-24 12:16:33 -04:00
Dmitriy Ivolgin 5bf008f7d2 Use all tags when building with securebuild (#2119) 2026-08-21 11:27:04 -07:00
Xav PaiceandElasticClaw Factory 0c6fb2177b fix(cluster-resources): stop emitting duplicate unredacted YAML copy (#2117)
* fix(cluster-resources): stop emitting duplicate unredacted YAML copy

storeCustomResource was writing both a JSON and a YAML file for every
custom resource. The built-in redactors are authored for JSON, so the
YAML copy was left unredacted. JSON is valid YAML, so analyzers that
expect YAML can still parse the JSON file.

Also convert the built-in kurl installer redactors from YAML-specific
paths to scoped JSON regex redactors so they continue to apply after the
YAML copy is removed.

* fix(cluster-resources): add YAML symlinks and cover cluster-scoped kurl installer

- Add a .yaml symlink for every custom-resource JSON file so existing
  analyzers that look for the old YAML copy keep working. The symlink
  points at the JSON file, so redaction of the JSON also redacts the YAML
  view.
- Fix the built-in kurl installer redactor to match both the cluster-scoped
  file (installers.cluster.kurl.sh.json) and the namespaced file pattern
  (installers.cluster.kurl.sh/*.json). The previous selector only matched
  the namespaced form.

---------

Co-authored-by: ElasticClaw Factory <factory@replicated.com>
2026-08-21 09:29:20 +12:00
replicated-software-factory[bot]andElasticClaw Factory bea9c2220d [deps] Dependency update: troubleshoot (#2112)
deps: update Go dependencies

- github.com/miekg/dns v1.1.72 => v1.1.73
- github.com/stretchr/testify v1.11.1 => v1.12.1
- github.com/stretchr/objx v0.5.2 => v0.5.3 (indirect)

Co-authored-by: ElasticClaw Factory <factory@replicated.com>
2026-08-20 13:16:18 +12:00
Xav PaiceandElasticClaw Factory 207cb90b53 feat(supportbundle): support secret/... redactor URIs (#2111)
Co-authored-by: ElasticClaw Factory <factory@replicated.com>
2026-08-20 08:40:11 +12:00
Dmitriy Ivolgin 0bb4a21f22 Remove dockerfile based release (#2115) 2026-08-19 12:07:36 -07:00
Dmitriy Ivolgin fb82fef6e0 Add securebuild release jobs (#2114) 2026-08-19 09:38:49 -07:00
Dmitriy Ivolgin 3f3906fea1 Use latest go 1.26 in release (#2113) 2026-08-19 08:33:17 -07:00
Dmitriy Ivolgin 1f1a21b3cf Fix SBOM generation (#2109) 2026-08-17 12:48:56 -07:00
Dmitriy Ivolgin e828c21545 Generate and sign SBOM (#2108) 2026-08-17 11:26:40 -07:00
replicated-software-factory[bot]andlnx01 b158f7d79f [deps] Dependency update: troubleshoot (#2107)
chore(deps): update go dependencies for troubleshoot

- helm.sh/helm/v3 v3.21.3 => v3.21.4
- github.com/aws/aws-sdk-go-v2 v1.43.5 => v1.43.6
- github.com/aws/aws-sdk-go-v2/credentials v1.19.35 => v1.19.36
- github.com/aws/aws-sdk-go-v2/service/s3 v1.107.1 => v1.107.2
- golang.org/x/mod v0.39.0 => v0.40.0
- golang.org/x/tools v0.48.0 => v0.49.0 (via tidy)
- golang.org/x/crypto v0.54.0 => v0.55.0 (sdk example)
- golang.org/x/net v0.57.0 => v0.58.0 (sdk example)
- golang.org/x/text v0.40.0 => v0.41.0 (sdk example)

Co-authored-by: replicated-software-factory[bot] <282391587+replicated-software-factory[bot]@users.noreply.github.com>
2026-08-17 08:42:30 +12:00
replicated-software-factory[bot]andDependency Update Bot 8be420c42e [deps] Dependency update: troubleshoot (#2106)
chore(deps): update go dependencies

Co-authored-by: Dependency Update Bot <deps@replicated.com>
2026-08-14 09:46:11 +12:00
Xav Paice 0e2e0cd0ab fix(runpod): strip pod spec before saving collected output (#2105) 2026-08-12 09:13:40 +12:00
Xav Paice 651ec1bfdb ci: remove unmaintained upgrade-go workflow (#2104) 2026-08-11 06:56:48 +12:00
Xav Paice b9faedfdb0 fix(preflight): add tracing span and align error handling for host collector redaction (#2102)
* fix(preflight): add tracing span and align error handling for host collector redaction

Follow-up to #2101.

Adds an OpenTelemetry span around host collector redaction and returns the
unredacted collectResult on redaction failure, matching the behavior of
remote host collectors and in-cluster support-bundle collectors.

* fix(preflight): assign collected data before redaction so errors preserve output
2026-08-10 16:17:10 +01:00
3db6a99d3a feat(analyze): add countDistinct() aggregate to nodeResources (#2079)
Add a countDistinct(<labelKey>) aggregate to the nodeResources analyzer
"when" expression language. It counts distinct values of a node label
across the filtered nodes and returns an int the existing comparison
operators evaluate.

Enables the AIR-238 3-AZ preflight: warn when Keeper-eligible nodes do
not span 3 availability zones, e.g.
"countDistinct(topology.kubernetes.io/zone) < 3".

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Xav Paice <xavpaice@users.noreply.github.com>
2026-08-10 12:54:33 +12:00
Benjamin Reed 3c5bf74750 fix(preflight): redact local host preflight collector output (#2101)
CollectHostWithContext was the only collection path (in-cluster support
bundle, remote/SSH host collectors) that never ran collected data through
the redaction engine. A `run` collector's captured environment in
particular can carry credentials verbatim -- e.g. HTTPS_PROXY with
embedded Basic Auth -- straight into the bundle's <collectorName>-info.json
with no redaction applied.

Wire it through collect.RedactResult the same way CollectRemoteWithContext
and pkg/supportbundle/collect.go already do, so the built-in default
redactors apply to local host preflight output too.

Fixes #2100
2026-08-10 12:09:22 +12:00
95a4a81bce [deps] Dependency update: troubleshoot (#2096)
* chore(deps): update Go dependencies for 2026-08-05

Updates:

- github.com/ClickHouse/clickhouse-go/v2 v2.47.0 -> v2.48.0

- github.com/aws/aws-sdk-go-v2/service/s3 v1.106.3 -> v1.106.4

- go.opentelemetry.io/otel v1.44.0 -> v1.45.0

- go.opentelemetry.io/otel/sdk v1.44.0 -> v1.45.0

* chore(deps): update Go dependencies for 2026-08-07

---------

Co-authored-by: ElasticClaw <elasticclaw@replicated.com>
Co-authored-by: OpenClaw Bot <elasticclaw@openclaw.ai>
2026-08-09 11:28:54 +12:00
replicated-software-factory[bot]andReplicated Developer 5767e9b516 chore(deps): update Go dependencies for 2026-08-02 (#2093)
Updates top-level go.mod/go.sum with latest patch-level dependency
releases. Also refreshes examples/sdk/helm-template go.mod/go.sum
while preserving the local replace directive for troubleshoot.

Validated with:
- make build
- make test
- go build ./... in examples/sdk/helm-template

Co-authored-by: Replicated Developer <dev@replicated.com>
2026-08-03 18:28:31 +12:00
replicated-software-factory[bot]andElasticClaw 102843b433 chore(deps): update Go dependencies (#2091)
* chore(deps): update Go dependencies for top-level and helm-template example

* ci: re-trigger preflight e2e after timeout

* ci: add compile-preflight dependency for validate-supportbundle-e2e-go

---------

Co-authored-by: ElasticClaw <elasticclaw@replicated.com>
2026-07-31 16:43:46 +12:00
dependabot[bot]andlnx01 5f09d49e12 chore(deps): bump github.com/go-logr/logr from 1.4.3 to 1.4.4 in the security group across 1 directory (#2087)
chore(deps): bump github.com/go-logr/logr

Bumps the security group with 1 update in the / directory: [github.com/go-logr/logr](https://github.com/go-logr/logr).


Updates `github.com/go-logr/logr` from 1.4.3 to 1.4.4
- [Release notes](https://github.com/go-logr/logr/releases)
- [Changelog](https://github.com/go-logr/logr/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-logr/logr/compare/v1.4.3...v1.4.4)

---
updated-dependencies:
- dependency-name: github.com/go-logr/logr
  dependency-version: 1.4.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 15:15:11 +12:00
replicated-software-factory[bot]andTroubleshoot Dependency Bot 28d098815b build(deps): automated Go dependency update (#2084)
build(deps): bump github.com/longhorn/go-common-libs

Co-authored-by: Troubleshoot Dependency Bot <troubleshoot-bot@replicated.com>
2026-07-21 16:50:32 +12:00
dependabot[bot]andlnx01 55479d963a chore(deps): bump actions/setup-python from 6 to 7 (#2085)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 16:50:05 +12:00
dependabot[bot]andlnx01 a75c227625 chore(deps): bump actions/setup-go from 6 to 7 (#2086)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6 to 7.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 16:49:41 +12:00
Xav Paice 80845c8733 Xp/factory update (#2072)
* Revert "use CLI rather than action for factory workflow push (#2067)"

This reverts commit 65bd747c50.

* enable Github action for workflow updates

* update workflow
2026-07-21 11:34:18 +12:00
replicated-software-factory[bot]andElasticClaw Bot 46cb1b2206 chore: bump Go dependencies (#2083)
Grouped patch/minor updates from go get -u ./... and go mod tidy:
- github.com/aws/aws-sdk-go-v2/service/s3 v1.105.1 => v1.105.2
- github.com/aws/smithy-go v1.27.3 => v1.27.4
- github.com/docker/cli v29.6.1 => v29.6.2
- github.com/longhorn/go-common-libs v0.0.0-20260712050051 => v0.0.0-20260716070930
- github.com/mattn/go-isatty v0.0.22 => v0.0.23
- google.golang.org/api v0.288.0 => v0.289.0

examples/sdk/helm-template had no net changes (local replace preserved).

Co-authored-by: ElasticClaw Bot <elasticclaw@openclaw.ai>
2026-07-17 16:03:59 +12:00
replicated-software-factory[bot]andTroubleshoot Bot f81e4fae5a chore(deps): grouped Go dependency updates (#2082)
chore(deps): bump Go dependencies

Bumps indirect Google Cloud/GRPC dependencies to latest patch versions:
- google.golang.org/grpc v1.82.0 => v1.82.1
- google.golang.org/genproto v0.0.0-20260713224248-f5fc221cf8c4 => v0.0.0-20260715232425-e75dac1f907d
- google.golang.org/genproto/googleapis/api v0.0.0-20260713224248-f5fc221cf8c4 => v0.0.0-20260715232425-e75dac1f907d
- google.golang.org/genproto/googleapis/rpc v0.0.0-20260713224248-f5fc221cf8c4 => v0.0.0-20260715232425-e75dac1f907d
- cloud.google.com/go/logging v1.18.0 => v1.19.0

Validated with:
- make build
- make test

Co-authored-by: Troubleshoot Bot <troubleshoot-bot@replicated.com>
2026-07-16 15:20:45 +12:00
replicated-software-factory[bot]andElasticClaw b0c4a23a56 chore(deps): update Go dependencies for top-level and helm-template example (#2081)
Updates indirect deps pulled in via the top-level go.mod (notably Google
Cloud, AWS, Sentry, and genproto modules) and refreshes the corresponding
helm-template example sums. Verified with make build and make test.

Co-authored-by: ElasticClaw <elasticclaw@openclaw.ai>
2026-07-15 20:12:21 +12:00
dependabot[bot]andlnx01 bce58cfe56 chore(deps): bump the security group across 1 directory with 2 updates (#2078)
Bumps the security group with 2 updates in the / directory: [github.com/aws/aws-sdk-go-v2/credentials](https://github.com/aws/aws-sdk-go-v2) and [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2).


Updates `github.com/aws/aws-sdk-go-v2/credentials` from 1.19.28 to 1.19.29
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/credentials/v1.19.28...credentials/v1.19.29)

Updates `github.com/aws/aws-sdk-go-v2/service/s3` from 1.105.0 to 1.105.1
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.105.0...service/s3/v1.105.1)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/credentials
  dependency-version: 1.19.29
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: github.com/aws/aws-sdk-go-v2/service/s3
  dependency-version: 1.105.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-14 17:12:47 +12:00
dependabot[bot]andlnx01 10a2a82d32 chore(deps): bump actions/checkout from 4 to 7 (#2075)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-14 13:13:15 +12:00
replicated-software-factory[bot]andElasticClaw Bot 7fa4497b7f chore: update Go dependencies (#2074)
- github.com/longhorn/go-iscsi-helper: replaced by github.com/longhorn/go-common-libs
  and migrated pkg/longhorn/util/iscsi.go to the new namespace executor API
- helm.sh/helm/v3: v3.21.2 -> v3.21.3
- oras.land/oras-go/v2: v2.6.1 -> v2.6.2
- google.golang.org/api: v0.287.1 -> v0.288.0
- golang.org/x/tools: v0.47.0 -> v0.48.0
- github.com/GoogleCloudPlatform/opentelemetry-operations-go/*: v1.33.0/v0.57.0 -> v1.34.0/v0.58.0
- examples/sdk/helm-template: helm.sh/helm/v3 v3.21.2 -> v3.21.3

Fixes required by the update environment:
- Makefile: use $(shell go env GOPATH)/bin for controller-gen/client-gen so
  generate works when the tools are not on PATH
- pkg/collect/host_kernel_configs.go: only use /proc/config.gz when the
  requested kernel release matches the running kernel, making the collector
  robust to hosts that expose a generic /proc/config.gz

Co-authored-by: ElasticClaw Bot <elasticclaw@openclaw.ai>
2026-07-14 13:12:51 +12:00
Xav Paice b279b2bef9 chore: update Go to 1.26.5 (#2073) 2026-07-11 11:12:50 +12:00
replicated-software-factory[bot]andReplicated Dev 3c59305677 chore: update Go dependencies (#2070)
Updates:
- github.com/aws/aws-sdk-go-v2/credentials v1.19.27 => v1.19.28
- github.com/aws/aws-sdk-go-v2/config v1.32.28 => v1.32.29
- github.com/aws/aws-sdk-go-v2/service/signin v1.3.0 => v1.4.0
- golang.org/x/net v0.56.0 => v0.57.0
- examples/sdk/helm-template: various indirect dependency updates

Co-authored-by: Replicated Dev <dev@replicated.com>
2026-07-11 09:48:51 +12:00
Salah Al Saleh 2a87b280bf Add per-collector subcommands to the collect binary (#2071)
* Add per-collector subcommands to the collect binary

Add `collect http|postgres|mysql|mssql|redis` subcommands that run a single
collector and print its native result JSON to stdout, so a collector can run
inside a Pod via the troubleshoot image (e.g. as a runPod collector).

Each has its own flags mapping to the collector's spec fields and its own help.
The Kubernetes client (used only for TLS-from-Secret) is built to match the
production preflight/support-bundle path (QPS/Burst/UserAgent).

* Add clickhouse subcommand to the collect binary

Add `collect clickhouse`, matching the other per-collector subcommands, so the
clickhouse collector can run inside a Pod via the troubleshoot image (e.g. as a
runPod collector). Same flags as the other database collectors (--uri + --tls-*).

* Make global flags work on the collect subcommands

Register --debug and the Kubernetes flags (--kubeconfig, --context, etc.) as
persistent flags, and move the shared setup to PersistentPreRun/PersistentPostRun,
so the per-collector subcommands inherit the global flags and run the same
logger/profiling setup as `collect [url]`. Previously these were local flags with
setup in PreRun, so subcommands rejected --debug/--kubeconfig and ignored the setup.
2026-07-10 09:31:14 -07:00
replicated-software-factory[bot]andTroubleshoot Bot 5e6f3cdac1 chore: update Go dependencies (#2068)
Updated Go dependencies to latest patch/minor versions:
- cloud.google.com/go/auth v0.20.0 -> v0.21.0
- cloud.google.com/go/iam v1.5.3 -> v1.11.0
- cloud.google.com/go/monitoring v1.24.3 -> v1.29.0
- cloud.google.com/go/storage v1.61.3 -> v1.63.0
- github.com/andybalholm/brotli v1.2.1 -> v1.2.2
- github.com/aws/aws-sdk-go-v2/config v1.32.17 -> v1.32.28
- github.com/chai2010/gettext-go v1.0.2 -> v1.0.3
- github.com/cyphar/filepath-securejoin v0.6.1 -> v0.7.0
- github.com/docker/cli v29.5.3 -> v29.6.1
- github.com/fxamacker/cbor/v2 v2.9.0 -> v2.9.2
- github.com/go-viper/mapstructure/v2 v2.4.0 -> v2.5.0
- github.com/google/gnostic-models v0.7.0 -> v0.7.1
- github.com/googleapis/enterprise-certificate-proxy v0.3.16 -> v0.3.18
- github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72 -> v2.0.0-beta.73
- github.com/lufia/plan9stats v0.0.0-20260330125221 -> v0.0.0-20260627054121
- github.com/pelletier/go-toml/v2 v2.2.4 -> v2.4.3
- github.com/sagikazarmark/locafero v0.11.0 -> v0.12.0
- github.com/spiffe/go-spiffe/v2 v2.6.0 -> v2.8.1
- go.opentelemetry.io/contrib/detectors/gcp v1.42.0 -> v1.44.0
- go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 -> v0.69.0
- golang.org/x/crypto v0.53.0 -> v0.54.0
- golang.org/x/mod v0.37.0 -> v0.38.0
- golang.org/x/sync v0.21.0 -> v0.22.0
- golang.org/x/sys v0.46.0 -> v0.47.0
- golang.org/x/term v0.44.0 -> v0.45.0
- golang.org/x/text v0.39.0 -> v0.40.0
- google.golang.org/api v0.283.0 -> v0.287.1
- google.golang.org/genproto/googleapis/api v0.0.0-20260401024825 -> v0.0.0-20260706201446
- google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958 -> v0.0.0-20260706201446
- google.golang.org/grpc v1.81.1 -> v1.82.0
- k8s.io/kube-openapi v0.0.0-20260317180543 -> v0.0.0-20260706235625
- k8s.io/utils v0.0.0-20260210185600 -> v0.0.0-20260707023825
- sigs.k8s.io/structured-merge-diff/v6 v6.3.2 -> v6.4.2

Note: github.com/longhorn/go-iscsi-helper was kept at v0.0.0-20210330030558-49a327fb024e
due to breaking API changes in newer versions.

Build and tests pass successfully.

Co-authored-by: Troubleshoot Bot <troubleshoot-bot@replicated.com>
2026-07-09 09:36:56 +12:00
Xav Paice 65bd747c50 use CLI rather than action for factory workflow push (#2067)
* use CLI rather than action for factory workflow push

* update elasticclaw version

* replace inline elasticclaw token with env var
2026-07-09 07:45:29 +12:00
Xav Paice e0e45b0dc3 add factory workflow (#2066)
* add factory workflow

* add publish workflow for factory

* reduce duplication
2026-07-08 14:37:48 +12:00
dependabot[bot]andlnx01 eed103b6a5 chore(deps): bump the security group across 1 directory with 10 updates (#2065)
Bumps the security group with 10 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [github.com/ClickHouse/clickhouse-go/v2](https://github.com/ClickHouse/clickhouse-go) | `2.46.0` | `2.47.0` |
| [github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) | `1.42.0` | `1.42.1` |
| [github.com/aws/aws-sdk-go-v2/credentials](https://github.com/aws/aws-sdk-go-v2) | `1.19.24` | `1.19.27` |
| [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) | `1.103.3` | `1.105.0` |
| [github.com/cilium/ebpf](https://github.com/cilium/ebpf) | `0.21.0` | `0.22.0` |
| [github.com/google/go-containerregistry](https://github.com/google/go-containerregistry) | `0.21.6` | `0.21.7` |
| [github.com/shirou/gopsutil/v4](https://github.com/shirou/gopsutil) | `4.26.5` | `4.26.6` |
| [github.com/vmware-tanzu/velero](https://github.com/vmware-tanzu/velero) | `1.18.1` | `1.18.2` |
| [golang.org/x/text](https://github.com/golang/text) | `0.38.0` | `0.39.0` |
| [helm.sh/helm/v3](https://github.com/helm/helm) | `3.21.1` | `3.21.2` |



Updates `github.com/ClickHouse/clickhouse-go/v2` from 2.46.0 to 2.47.0
- [Release notes](https://github.com/ClickHouse/clickhouse-go/releases)
- [Changelog](https://github.com/ClickHouse/clickhouse-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ClickHouse/clickhouse-go/compare/v2.46.0...v2.47.0)

Updates `github.com/aws/aws-sdk-go-v2` from 1.42.0 to 1.42.1
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.42.0...v1.42.1)

Updates `github.com/aws/aws-sdk-go-v2/credentials` from 1.19.24 to 1.19.27
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/credentials/v1.19.24...credentials/v1.19.27)

Updates `github.com/aws/aws-sdk-go-v2/service/s3` from 1.103.3 to 1.105.0
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.103.3...service/s3/v1.105.0)

Updates `github.com/cilium/ebpf` from 0.21.0 to 0.22.0
- [Release notes](https://github.com/cilium/ebpf/releases)
- [Commits](https://github.com/cilium/ebpf/compare/v0.21.0...v0.22.0)

Updates `github.com/google/go-containerregistry` from 0.21.6 to 0.21.7
- [Release notes](https://github.com/google/go-containerregistry/releases)
- [Commits](https://github.com/google/go-containerregistry/compare/v0.21.6...v0.21.7)

Updates `github.com/shirou/gopsutil/v4` from 4.26.5 to 4.26.6
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v4.26.5...v4.26.6)

Updates `github.com/vmware-tanzu/velero` from 1.18.1 to 1.18.2
- [Release notes](https://github.com/vmware-tanzu/velero/releases)
- [Changelog](https://github.com/velero-io/velero/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vmware-tanzu/velero/compare/v1.18.1...v1.18.2)

Updates `golang.org/x/text` from 0.38.0 to 0.39.0
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.38.0...v0.39.0)

Updates `helm.sh/helm/v3` from 3.21.1 to 3.21.2
- [Release notes](https://github.com/helm/helm/releases)
- [Commits](https://github.com/helm/helm/compare/v3.21.1...v3.21.2)

---
updated-dependencies:
- dependency-name: github.com/ClickHouse/clickhouse-go/v2
  dependency-version: 2.47.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: github.com/aws/aws-sdk-go-v2
  dependency-version: 1.42.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: github.com/aws/aws-sdk-go-v2/credentials
  dependency-version: 1.19.27
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: github.com/aws/aws-sdk-go-v2/service/s3
  dependency-version: 1.105.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: github.com/cilium/ebpf
  dependency-version: 0.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: github.com/google/go-containerregistry
  dependency-version: 0.21.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: github.com/shirou/gopsutil/v4
  dependency-version: 4.26.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: github.com/vmware-tanzu/velero
  dependency-version: 1.18.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: golang.org/x/text
  dependency-version: 0.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: helm.sh/helm/v3
  dependency-version: 3.21.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 13:56:55 +12:00
dependabot[bot]andlnx01 d0c73d4723 chore(deps): bump actions/checkout from 6 to 7 (#2062)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 08:36:41 +12:00
dependabot[bot]andlnx01 19e7c9b9c8 chore(deps): bump helm.sh/helm/v3 from 3.21.0 to 3.21.2 in /examples/sdk/helm-template in the security group across 1 directory (#2059)
chore(deps): bump helm.sh/helm/v3

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


Updates `helm.sh/helm/v3` from 3.21.0 to 3.21.2
- [Release notes](https://github.com/helm/helm/releases)
- [Commits](https://github.com/helm/helm/compare/v3.21.0...v3.21.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 08:36:12 +12:00
dependabot[bot]andlnx01 0edd588c3e chore(deps): bump github.com/containerd/containerd from 1.7.32 to 1.7.33 (#2061)
Bumps [github.com/containerd/containerd](https://github.com/containerd/containerd) from 1.7.32 to 1.7.33.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v1.7.32...v1.7.33)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd
  dependency-version: 1.7.33
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 09:10:09 +12:00
dependabot[bot]andlnx01 78ac8f7dc9 chore(deps): bump the security group across 1 directory with 19 updates (#2060)
Bumps the security group with 13 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) | `1.41.11` | `1.42.0` |
| [github.com/aws/aws-sdk-go-v2/credentials](https://github.com/aws/aws-sdk-go-v2) | `1.19.21` | `1.19.24` |
| [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) | `1.103.1` | `1.103.3` |
| [golang.org/x/mod](https://github.com/golang/mod) | `0.36.0` | `0.37.0` |
| [golang.org/x/sync](https://github.com/golang/sync) | `0.20.0` | `0.21.0` |
| [k8s.io/api](https://github.com/kubernetes/api) | `0.36.1` | `0.36.2` |
| [k8s.io/apiextensions-apiserver](https://github.com/kubernetes/apiextensions-apiserver) | `0.36.1` | `0.36.2` |
| [k8s.io/cli-runtime](https://github.com/kubernetes/cli-runtime) | `0.36.1` | `0.36.2` |
| [oras.land/oras-go/v2](https://github.com/oras-project/oras-go) | `2.6.0` | `2.6.1` |
| [golang.org/x/net](https://github.com/golang/net) | `0.55.0` | `0.56.0` |
| [helm.sh/helm/v3](https://github.com/helm/helm) | `3.21.0` | `3.21.1` |
| [k8s.io/kubelet](https://github.com/kubernetes/kubelet) | `0.36.1` | `0.36.2` |
| [k8s.io/metrics](https://github.com/kubernetes/metrics) | `0.36.1` | `0.36.2` |



Updates `github.com/aws/aws-sdk-go-v2` from 1.41.11 to 1.42.0
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.41.11...v1.42.0)

Updates `github.com/aws/aws-sdk-go-v2/credentials` from 1.19.21 to 1.19.24
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/credentials/v1.19.21...credentials/v1.19.24)

Updates `github.com/aws/aws-sdk-go-v2/service/s3` from 1.103.1 to 1.103.3
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.103.1...service/s3/v1.103.3)

Updates `golang.org/x/mod` from 0.36.0 to 0.37.0
- [Commits](https://github.com/golang/mod/compare/v0.36.0...v0.37.0)

Updates `golang.org/x/sync` from 0.20.0 to 0.21.0
- [Commits](https://github.com/golang/sync/compare/v0.20.0...v0.21.0)

Updates `k8s.io/api` from 0.36.1 to 0.36.2
- [Commits](https://github.com/kubernetes/api/compare/v0.36.1...v0.36.2)

Updates `k8s.io/apiextensions-apiserver` from 0.36.1 to 0.36.2
- [Release notes](https://github.com/kubernetes/apiextensions-apiserver/releases)
- [Commits](https://github.com/kubernetes/apiextensions-apiserver/compare/v0.36.1...v0.36.2)

Updates `k8s.io/apimachinery` from 0.36.1 to 0.36.2
- [Commits](https://github.com/kubernetes/apimachinery/compare/v0.36.1...v0.36.2)

Updates `k8s.io/apiserver` from 0.36.1 to 0.36.2
- [Commits](https://github.com/kubernetes/apiserver/compare/v0.36.1...v0.36.2)

Updates `k8s.io/cli-runtime` from 0.36.1 to 0.36.2
- [Commits](https://github.com/kubernetes/cli-runtime/compare/v0.36.1...v0.36.2)

Updates `k8s.io/client-go` from 0.36.1 to 0.36.2
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.36.1...v0.36.2)

Updates `k8s.io/streaming` from 0.36.1 to 0.36.2
- [Commits](https://github.com/kubernetes/streaming/compare/v0.36.1...v0.36.2)

Updates `oras.land/oras-go/v2` from 2.6.0 to 2.6.1
- [Release notes](https://github.com/oras-project/oras-go/releases)
- [Commits](https://github.com/oras-project/oras-go/compare/v2.6.0...v2.6.1)

Updates `golang.org/x/net` from 0.55.0 to 0.56.0
- [Commits](https://github.com/golang/net/compare/v0.55.0...v0.56.0)

Updates `golang.org/x/sys` from 0.45.0 to 0.46.0
- [Commits](https://github.com/golang/sys/compare/v0.45.0...v0.46.0)

Updates `golang.org/x/text` from 0.37.0 to 0.38.0
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.37.0...v0.38.0)

Updates `helm.sh/helm/v3` from 3.21.0 to 3.21.1
- [Release notes](https://github.com/helm/helm/releases)
- [Commits](https://github.com/helm/helm/compare/v3.21.0...v3.21.1)

Updates `k8s.io/kubelet` from 0.36.1 to 0.36.2
- [Commits](https://github.com/kubernetes/kubelet/compare/v0.36.1...v0.36.2)

Updates `k8s.io/metrics` from 0.36.1 to 0.36.2
- [Commits](https://github.com/kubernetes/metrics/compare/v0.36.1...v0.36.2)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2
  dependency-version: 1.42.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: github.com/aws/aws-sdk-go-v2/credentials
  dependency-version: 1.19.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: github.com/aws/aws-sdk-go-v2/service/s3
  dependency-version: 1.103.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: golang.org/x/mod
  dependency-version: 0.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: golang.org/x/sync
  dependency-version: 0.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: k8s.io/api
  dependency-version: 0.36.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/apiextensions-apiserver
  dependency-version: 0.36.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/apimachinery
  dependency-version: 0.36.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/apiserver
  dependency-version: 0.36.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/cli-runtime
  dependency-version: 0.36.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/client-go
  dependency-version: 0.36.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/streaming
  dependency-version: 0.36.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: oras.land/oras-go/v2
  dependency-version: 2.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: golang.org/x/net
  dependency-version: 0.56.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: golang.org/x/sys
  dependency-version: 0.46.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: golang.org/x/text
  dependency-version: 0.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: helm.sh/helm/v3
  dependency-version: 3.21.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/kubelet
  dependency-version: 0.36.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/metrics
  dependency-version: 0.36.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 09:34:12 +12:00
Kris Coleman 22ab3cf551 feat(redact): make MAX_CONCURRENT_REDACTORS configurable via env var (sc-138321) (#2057)
Replace the hardcoded MAX_CONCURRENT_REDACTORS = 10 ceiling in
pkg/collect/redact.go with a runtime-resolved value driven by the
TROUBLESHOOT_MAX_CONCURRENT_REDACTORS env var. The default (10) is
unchanged, so behavior is identical unless an operator opts in.

- DefaultMaxConcurrentRedactors exported as the default
- MaxConcurrentRedactorsEnvVar exported as the env var name
- maxConcurrentRedactors() helper parses the env, logs on invalid input,
  and falls back to the default on missing/empty/non-numeric/<=0 values
- Table-driven tests in pkg/collect/redact_test.go cover unset, empty,
  positive override, default-equal, zero, negative, non-numeric, and
  whitespace-padded inputs

Unblocks Pixee's standalone support-bundle pipeline, which hits the
10-concurrent ceiling on large bundles.

Refs: sc-138321, replicated-collab/pixee-replicated#131
2026-06-10 16:07:27 -04:00
replicated-ciandlnx01 dcfddc78da Bump Go from 1.26.3 to 1.26.4 (#2055)
Bump Go to version from 1.26.3 to 1.26.4

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-08 16:38:50 +01:00
dependabot[bot]andlnx01 aad5f6ef06 chore(deps): bump the security group with 7 updates (#2052)
Bumps the security group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) | `1.41.7` | `1.41.11` |
| [github.com/aws/aws-sdk-go-v2/credentials](https://github.com/aws/aws-sdk-go-v2) | `1.19.17` | `1.19.21` |
| [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) | `1.101.0` | `1.103.1` |
| [github.com/jackc/pgx/v5](https://github.com/jackc/pgx) | `5.9.2` | `5.10.0` |
| [github.com/shirou/gopsutil/v4](https://github.com/shirou/gopsutil) | `4.26.4` | `4.26.5` |
| [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) | `1.43.0` | `1.44.0` |
| [go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go) | `1.43.0` | `1.44.0` |


Updates `github.com/aws/aws-sdk-go-v2` from 1.41.7 to 1.41.11
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.41.7...v1.41.11)

Updates `github.com/aws/aws-sdk-go-v2/credentials` from 1.19.17 to 1.19.21
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/credentials/v1.19.17...credentials/v1.19.21)

Updates `github.com/aws/aws-sdk-go-v2/service/s3` from 1.101.0 to 1.103.1
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.101.0...service/s3/v1.103.1)

Updates `github.com/jackc/pgx/v5` from 5.9.2 to 5.10.0
- [Changelog](https://github.com/jackc/pgx/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jackc/pgx/compare/v5.9.2...v5.10.0)

Updates `github.com/shirou/gopsutil/v4` from 4.26.4 to 4.26.5
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v4.26.4...v4.26.5)

Updates `go.opentelemetry.io/otel` from 1.43.0 to 1.44.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.43.0...v1.44.0)

Updates `go.opentelemetry.io/otel/sdk` from 1.43.0 to 1.44.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.43.0...v1.44.0)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2
  dependency-version: 1.41.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: github.com/aws/aws-sdk-go-v2/credentials
  dependency-version: 1.19.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: github.com/aws/aws-sdk-go-v2/service/s3
  dependency-version: 1.103.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: github.com/jackc/pgx/v5
  dependency-version: 5.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: github.com/shirou/gopsutil/v4
  dependency-version: 4.26.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: go.opentelemetry.io/otel
  dependency-version: 1.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: go.opentelemetry.io/otel/sdk
  dependency-version: 1.44.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>
2026-06-04 17:46:47 +12:00
Kris Coleman 12b0e741d8 feat(ci): add Slack release notification via reusable workflow (#2051) 2026-05-29 14:25:15 -04:00
Kris ColemanandClaude Opus 4.6 baecd3c411 chore(deps): bump golang.org/x/net, x/crypto, containerd to patch CVEs (#2049) (#2050)
Bump golang.org/x/crypto v0.51.0 -> v0.52.0 in both root and examples modules.
Bump golang.org/x/net v0.54.0 -> v0.55.0 in examples module (root already at v0.55.0).
containerd already at v1.7.32 in root (no change needed).

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 13:39:20 -04:00
dependabot[bot]andlnx01 1cfa00ed9c chore(deps): bump the security group with 3 updates (#2048)
Bumps the security group with 3 updates: [github.com/aws/aws-sdk-go-v2/credentials](https://github.com/aws/aws-sdk-go-v2), [golang.org/x/net](https://github.com/golang/net) and [golang.org/x/sys](https://github.com/golang/sys).


Updates `github.com/aws/aws-sdk-go-v2/credentials` from 1.19.16 to 1.19.17
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/credentials/v1.19.16...credentials/v1.19.17)

Updates `golang.org/x/net` from 0.54.0 to 0.55.0
- [Commits](https://github.com/golang/net/compare/v0.54.0...v0.55.0)

Updates `golang.org/x/sys` from 0.44.0 to 0.45.0
- [Commits](https://github.com/golang/sys/compare/v0.44.0...v0.45.0)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/credentials
  dependency-version: 1.19.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: golang.org/x/net
  dependency-version: 0.55.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: golang.org/x/sys
  dependency-version: 0.45.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>
2026-05-29 12:33:42 -04:00
dependabot[bot]andlnx01 9600c17454 chore(deps): bump github.com/containerd/containerd from 1.7.30 to 1.7.32 (#2046)
Bumps [github.com/containerd/containerd](https://github.com/containerd/containerd) from 1.7.30 to 1.7.32.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v1.7.30...v1.7.32)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd
  dependency-version: 1.7.32
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-22 15:36:48 +12:00
Gerard NguyenandChuck D'Antonio 61ef1e1036 fix: TLS support in MySQL connector (#2045)
* fix(mysql): respect TLS configuration in MySQL collector

The MySQL collector was ignoring the TLS field in the Database spec,
which caused TLS connections to fail even when properly configured.

This change mirrors the PostgreSQL collector pattern:
- Parse the MySQL DSN using mysql.ParseDSN
- When TLS is configured, create a tls.Config and set cfg.TLS
- Use sql.OpenDB with mysql.NewConnector when TLS is active
- Keep the existing sql.Open path for backward compatibility

Fixes: MySQL preflight fails over TLS connections

* fix(mysql): address review feedback

- Fix gofmt indentation in mysql_test.go
- Remove duplicate DSN parsing in createConnectConfig
- Add CA-cert-only TLS test case

---------

Co-authored-by: Chuck D'Antonio <chuck@replicated.com>
2026-05-22 12:35:15 +10:00
dependabot[bot]andlnx01 77e39bc5ed chore(deps): bump helm.sh/helm/v3 from 3.20.2 to 3.21.0 in /examples/sdk/helm-template in the security group across 1 directory (#2044)
chore(deps): bump helm.sh/helm/v3

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


Updates `helm.sh/helm/v3` from 3.20.2 to 3.21.0
- [Release notes](https://github.com/helm/helm/releases)
- [Commits](https://github.com/helm/helm/compare/v3.20.2...v3.21.0)

---
updated-dependencies:
- dependency-name: helm.sh/helm/v3
  dependency-version: 3.21.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>
2026-05-20 16:13:24 +12:00
dependabot[bot]andlnx01 edba8f1ee0 chore(deps): bump the security group with 14 updates (#2043)
Bumps the security group with 14 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/ClickHouse/clickhouse-go/v2](https://github.com/ClickHouse/clickhouse-go) | `2.42.0` | `2.46.0` |
| [github.com/google/go-containerregistry](https://github.com/google/go-containerregistry) | `0.21.5` | `0.21.6` |
| [github.com/vmware-tanzu/velero](https://github.com/vmware-tanzu/velero) | `1.18.0` | `1.18.1` |
| [k8s.io/api](https://github.com/kubernetes/api) | `0.36.0` | `0.36.1` |
| [k8s.io/apiextensions-apiserver](https://github.com/kubernetes/apiextensions-apiserver) | `0.36.0` | `0.36.1` |
| [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) | `0.36.0` | `0.36.1` |
| [k8s.io/apiserver](https://github.com/kubernetes/apiserver) | `0.36.0` | `0.36.1` |
| [k8s.io/cli-runtime](https://github.com/kubernetes/cli-runtime) | `0.36.0` | `0.36.1` |
| [k8s.io/client-go](https://github.com/kubernetes/client-go) | `0.36.0` | `0.36.1` |
| [k8s.io/streaming](https://github.com/kubernetes/streaming) | `0.36.0` | `0.36.1` |
| [sigs.k8s.io/controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) | `0.24.0` | `0.24.1` |
| [helm.sh/helm/v3](https://github.com/helm/helm) | `3.20.2` | `3.21.0` |
| [k8s.io/kubelet](https://github.com/kubernetes/kubelet) | `0.36.0` | `0.36.1` |
| [k8s.io/metrics](https://github.com/kubernetes/metrics) | `0.36.0` | `0.36.1` |


Updates `github.com/ClickHouse/clickhouse-go/v2` from 2.42.0 to 2.46.0
- [Release notes](https://github.com/ClickHouse/clickhouse-go/releases)
- [Changelog](https://github.com/ClickHouse/clickhouse-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ClickHouse/clickhouse-go/compare/v2.42.0...v2.46.0)

Updates `github.com/google/go-containerregistry` from 0.21.5 to 0.21.6
- [Release notes](https://github.com/google/go-containerregistry/releases)
- [Commits](https://github.com/google/go-containerregistry/compare/v0.21.5...v0.21.6)

Updates `github.com/vmware-tanzu/velero` from 1.18.0 to 1.18.1
- [Release notes](https://github.com/vmware-tanzu/velero/releases)
- [Changelog](https://github.com/velero-io/velero/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vmware-tanzu/velero/compare/v1.18.0...v1.18.1)

Updates `k8s.io/api` from 0.36.0 to 0.36.1
- [Commits](https://github.com/kubernetes/api/compare/v0.36.0...v0.36.1)

Updates `k8s.io/apiextensions-apiserver` from 0.36.0 to 0.36.1
- [Release notes](https://github.com/kubernetes/apiextensions-apiserver/releases)
- [Commits](https://github.com/kubernetes/apiextensions-apiserver/compare/v0.36.0...v0.36.1)

Updates `k8s.io/apimachinery` from 0.36.0 to 0.36.1
- [Commits](https://github.com/kubernetes/apimachinery/compare/v0.36.0...v0.36.1)

Updates `k8s.io/apiserver` from 0.36.0 to 0.36.1
- [Commits](https://github.com/kubernetes/apiserver/compare/v0.36.0...v0.36.1)

Updates `k8s.io/cli-runtime` from 0.36.0 to 0.36.1
- [Commits](https://github.com/kubernetes/cli-runtime/compare/v0.36.0...v0.36.1)

Updates `k8s.io/client-go` from 0.36.0 to 0.36.1
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.36.0...v0.36.1)

Updates `k8s.io/streaming` from 0.36.0 to 0.36.1
- [Commits](https://github.com/kubernetes/streaming/compare/v0.36.0...v0.36.1)

Updates `sigs.k8s.io/controller-runtime` from 0.24.0 to 0.24.1
- [Release notes](https://github.com/kubernetes-sigs/controller-runtime/releases)
- [Changelog](https://github.com/kubernetes-sigs/controller-runtime/blob/main/RELEASE.md)
- [Commits](https://github.com/kubernetes-sigs/controller-runtime/compare/v0.24.0...v0.24.1)

Updates `helm.sh/helm/v3` from 3.20.2 to 3.21.0
- [Release notes](https://github.com/helm/helm/releases)
- [Commits](https://github.com/helm/helm/compare/v3.20.2...v3.21.0)

Updates `k8s.io/kubelet` from 0.36.0 to 0.36.1
- [Commits](https://github.com/kubernetes/kubelet/compare/v0.36.0...v0.36.1)

Updates `k8s.io/metrics` from 0.36.0 to 0.36.1
- [Commits](https://github.com/kubernetes/metrics/compare/v0.36.0...v0.36.1)

---
updated-dependencies:
- dependency-name: github.com/ClickHouse/clickhouse-go/v2
  dependency-version: 2.46.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: github.com/google/go-containerregistry
  dependency-version: 0.21.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: github.com/vmware-tanzu/velero
  dependency-version: 1.18.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/api
  dependency-version: 0.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/apiextensions-apiserver
  dependency-version: 0.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/apimachinery
  dependency-version: 0.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/apiserver
  dependency-version: 0.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/cli-runtime
  dependency-version: 0.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/client-go
  dependency-version: 0.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/streaming
  dependency-version: 0.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: sigs.k8s.io/controller-runtime
  dependency-version: 0.24.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: helm.sh/helm/v3
  dependency-version: 3.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: k8s.io/kubelet
  dependency-version: 0.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/metrics
  dependency-version: 0.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-20 12:31:01 +12:00
replicated-ciandlnx01 894158779c Bump Go from 1.26.2 to 1.26.3 (#2040)
Bump Go to version from 1.26.2 to 1.26.3

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-15 08:19:00 -07:00
Lennard EijsackersandClaude Opus 4.6 1cd9b6103c feat: Add Clickhouse Support (#1967)
* feat: Add Clickhouse Support

* fix: missing case in GetCollector for ClickHouse

* chore: add analyzer

* fix; Findings

* fix: use go-version for ClickHouse version comparison

ClickHouse returns 4-part version numbers (e.g., 25.3.2.39) which
blang/semver.ParseTolerant cannot handle. Use hashicorp/go-version
(which supports arbitrary version parts) via a ClickHouse-specific
comparison function, matching the approach used by the MSSQL analyzer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* change icon

* chore: Resolve comments

- Change icon value
- Update converter to support ClickHouse
- Update loader to support ClickHouse
- Ensure IsConnected behaviour is similar to other db collectors

* fmt 🤦

* chore: Add Clickhouse case to convertAnalyzerToSpec

* resolve CollectorName comment

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-13 22:57:16 +01:00
dependabot[bot]andlnx01 ba33b743d5 chore(deps): bump the security group with 5 updates (#2042)
Bumps the security group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) | `1.100.1` | `1.101.0` |
| [golang.org/x/mod](https://github.com/golang/mod) | `0.35.0` | `0.36.0` |
| [golang.org/x/net](https://github.com/golang/net) | `0.53.0` | `0.54.0` |
| [golang.org/x/sys](https://github.com/golang/sys) | `0.43.0` | `0.44.0` |
| [golang.org/x/text](https://github.com/golang/text) | `0.36.0` | `0.37.0` |


Updates `github.com/aws/aws-sdk-go-v2/service/s3` from 1.100.1 to 1.101.0
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.100.1...service/s3/v1.101.0)

Updates `golang.org/x/mod` from 0.35.0 to 0.36.0
- [Commits](https://github.com/golang/mod/compare/v0.35.0...v0.36.0)

Updates `golang.org/x/net` from 0.53.0 to 0.54.0
- [Commits](https://github.com/golang/net/compare/v0.53.0...v0.54.0)

Updates `golang.org/x/sys` from 0.43.0 to 0.44.0
- [Commits](https://github.com/golang/sys/compare/v0.43.0...v0.44.0)

Updates `golang.org/x/text` from 0.36.0 to 0.37.0
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.36.0...v0.37.0)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/service/s3
  dependency-version: 1.101.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: golang.org/x/mod
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: golang.org/x/net
  dependency-version: 0.54.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: golang.org/x/sys
  dependency-version: 0.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: golang.org/x/text
  dependency-version: 0.37.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>
2026-05-12 20:02:51 -07:00
dependabot[bot]andlnx01 f6c57607ea chore(deps): bump github.com/distribution/distribution/v3 from 3.1.0 to 3.1.1 (#2038)
chore(deps): bump github.com/distribution/distribution/v3

Bumps [github.com/distribution/distribution/v3](https://github.com/distribution/distribution) from 3.1.0 to 3.1.1.
- [Release notes](https://github.com/distribution/distribution/releases)
- [Commits](https://github.com/distribution/distribution/compare/v3.1.0...v3.1.1)

---
updated-dependencies:
- dependency-name: github.com/distribution/distribution/v3
  dependency-version: 3.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 16:06:49 -04:00
dependabot[bot]andlnx01 3779868880 chore(deps): bump the security group with 7 updates (#2039)
Bumps the security group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) | `1.41.6` | `1.41.7` |
| [github.com/aws/aws-sdk-go-v2/credentials](https://github.com/aws/aws-sdk-go-v2) | `1.19.15` | `1.19.16` |
| [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) | `1.100.0` | `1.100.1` |
| [github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql) | `1.9.3` | `1.10.0` |
| [github.com/microsoft/go-mssqldb](https://github.com/microsoft/go-mssqldb) | `1.9.8` | `1.10.0` |
| [github.com/shirou/gopsutil/v4](https://github.com/shirou/gopsutil) | `4.26.3` | `4.26.4` |
| [sigs.k8s.io/controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) | `0.23.3` | `0.24.0` |


Updates `github.com/aws/aws-sdk-go-v2` from 1.41.6 to 1.41.7
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.41.6...v1.41.7)

Updates `github.com/aws/aws-sdk-go-v2/credentials` from 1.19.15 to 1.19.16
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/credentials/v1.19.15...credentials/v1.19.16)

Updates `github.com/aws/aws-sdk-go-v2/service/s3` from 1.100.0 to 1.100.1
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.100.0...service/s3/v1.100.1)

Updates `github.com/go-sql-driver/mysql` from 1.9.3 to 1.10.0
- [Release notes](https://github.com/go-sql-driver/mysql/releases)
- [Changelog](https://github.com/go-sql-driver/mysql/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-sql-driver/mysql/compare/v1.9.3...v1.10.0)

Updates `github.com/microsoft/go-mssqldb` from 1.9.8 to 1.10.0
- [Release notes](https://github.com/microsoft/go-mssqldb/releases)
- [Changelog](https://github.com/microsoft/go-mssqldb/blob/main/CHANGELOG.md)
- [Commits](https://github.com/microsoft/go-mssqldb/compare/v1.9.8...v1.10.0)

Updates `github.com/shirou/gopsutil/v4` from 4.26.3 to 4.26.4
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v4.26.3...v4.26.4)

Updates `sigs.k8s.io/controller-runtime` from 0.23.3 to 0.24.0
- [Release notes](https://github.com/kubernetes-sigs/controller-runtime/releases)
- [Changelog](https://github.com/kubernetes-sigs/controller-runtime/blob/main/RELEASE.md)
- [Commits](https://github.com/kubernetes-sigs/controller-runtime/compare/v0.23.3...v0.24.0)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2
  dependency-version: 1.41.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: github.com/aws/aws-sdk-go-v2/credentials
  dependency-version: 1.19.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: github.com/aws/aws-sdk-go-v2/service/s3
  dependency-version: 1.100.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: github.com/go-sql-driver/mysql
  dependency-version: 1.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: github.com/microsoft/go-mssqldb
  dependency-version: 1.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: github.com/shirou/gopsutil/v4
  dependency-version: 4.26.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: sigs.k8s.io/controller-runtime
  dependency-version: 0.24.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>
2026-05-05 10:01:29 -04:00
Ethan Mosbaugh 4c6af55e7c fix: correct RBAC verb for pods/exec from get to create (#2037)
fix: correct RBAC verb and WebSocket fallback for pods/exec

This commit fixes three related issues that prevented exec collectors from
working with minimal RBAC permissions:

1. RBAC preflight check used wrong verb for pods/exec
   Changed from "get" to "create" in v1beta1 and v1beta2 AccessReviewSpecs.
   The pods/exec subresource requires "create" to execute commands.

2. WebSocket fallback used wrong httpstream package import
   The fallback executor checked IsUpgradeFailure using the apimachinery
   httpstream package, but the roundtripper creates UpgradeFailureError using
   the streaming httpstream package. These are different Go types, so
   errors.As always returned false and fallback to SPDY never triggered.
   Changed import to k8s.io/streaming/pkg/httpstream.

3. Stdin mismatch caused SPDY fallback to hang
   PodExecOptions always set Stdin:true but StreamOptions always passed
   Stdin:nil. When WebSocket failed and fell back to SPDY, the server
   waited for stdin data that never arrived. Changed Stdin to false in
   PodExecOptions for exec, copy, and copy_from_host collectors.
2026-05-01 13:03:42 -07:00
Xav Paice a4ce199005 refactor(collect): replace go.podman.io/image with go-containerregistry (#2034)
* refactor(collect): replace go.podman.io/image with go-containerregistry

Migrate pkg/collect/registry.go off go.podman.io/image/v5 (containers/image)
onto github.com/google/go-containerregistry, eliminating the transitive
github.com/docker/docker +incompatible chain that blocks Docker v29 SDK
adoption.

The RegistryImages collector probes whether each requested image exists in
its registry by issuing a manifest HEAD. We replace alltransports.ParseImageName
+ imageRef.NewImage with name.ParseReference + remote.Head. Auth from a
kubernetes.io/dockerconfigjson Secret is converted to an authn.Authenticator;
error classification switches from errcode.Errors string matching to
transport.Error.StatusCode.

Add httptest.NewTLSServer-backed tests for imageExists covering the
found/not-found/unauthorized/EOF-retry paths to lock in protocol-level
behavior across the migration.

Promote go-containerregistry to a direct require at v0.21.5; go mod tidy
drops go.podman.io/image, distribution/distribution/v3, and the bare
docker/docker entry from the module graph.

Issue: tr-dit

* Removed the unreachable strings.Contains branch so we no longer pretend there is special “architecture mismatch → treat as exists” behavior.
Documented next to remote.Get that the check is manifest presence for the reference, not “runnable on this arch.”

* fix(collect): use DefaultKeychain when registry auth config is nil

Restore ambient credential behavior (~/.docker/config.json, credential
helpers) for registry image checks. Without WithAuthFromKeychain,
remote.Get defaulted to anonymous auth, diverging from containers/image
and from documented nil authConfig semantics.

Made-with: Cursor
2026-04-30 10:12:53 +01:00
Ethan MosbaughandClaude Sonnet 4.6 48c45d7b0a refactor: replace SPDY executor with WebSocket-first fallback executor (#2031)
* docs: add design spec for SPDY executor removal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: add implementation plan for SPDY executor removal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(k8sutil): add NewFallbackExecutor helper

* fix(k8sutil): rename url param to u to avoid shadowing net/url

* refactor(collect): use fallback executor in exec collector

* refactor(collect): use fallback executor in copy collector

* refactor(collect): use fallback executor in copy_from_host collector

* refactor(collect): use fallback executor in sonobuoy_results collector

* refactor(collect): use fallback executor in etcd collector

* refactor(collect): use fallback executor in longhorn collector

* refactor(supportbundle): use fallback executor in collect

* chore(k8sutil): delete unused PortForward function

* fix(k8sutil): use GET for WebSocket executor and broaden fallback predicate

WebSocket upgrade requires GET per RFC 6455. Also add IsHTTPSProxyError to
the fallback predicate so HTTPS proxy environments fall back to SPDY correctly,
matching kubectl's implementation. Remove method param from NewFallbackExecutor
since the methods are now transport-specific and not caller-controlled.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: remove superpowers docs artifacts

* fix(collect): enable stdout capture in exec collector

* fix(collect): enable stdout capture in copy collectors

PodExecOptions.Stdout: false causes the WebSocket API server to discard
stdout entirely, silently breaking tar output for file copy operations.
With SPDY this mismatch was harmless; WebSocket strictly respects the field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 06:21:42 -07:00
Evans Mungai 2fdd21fca1 feat(analyze): warn when nodeResources has no node data, add ignoreIfNoFiles (#2035)
* feat(analyze): warn when nodeResources has no node data, add ignoreIfNoFiles

The nodeResources analyzer previously failed silently when
cluster-resources/nodes.json was not collected (e.g. when the
clusterResources collector was excluded or could not list nodes).
It now emits a warn outcome per nodeResources entry in the spec.

Add an ignoreIfNoFiles top-level field to nodeResources, mirroring
textAnalyze, so users can opt out of the new warning when the
analyzer is intentionally optional.

- Add IgnoreIfNoFiles to v1beta2.NodeResources
- Update CRDs and JSON schemas
- Unit test the warn / ignore paths
- Add an e2e fixture and test that excludes clusterResources and
asserts the analyze output

Signed-off-by: Evans Mungai <evans@replicated.com>

* Fix review comment

Signed-off-by: Evans Mungai <evans@replicated.com>

---------

Signed-off-by: Evans Mungai <evans@replicated.com>
2026-04-29 13:13:24 +01:00
572cb8e31d chore(deps): bump the security group across 1 directory with 12 updates (#2030)
* chore(deps): bump the security group across 1 directory with 12 updates

Bumps the security group with 8 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [github.com/apparentlymart/go-cidr](https://github.com/apparentlymart/go-cidr) | `1.1.0` | `1.1.1` |
| [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) | `1.99.1` | `1.100.0` |
| [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) | `0.0.21` | `0.0.22` |
| [k8s.io/api](https://github.com/kubernetes/api) | `0.35.4` | `0.36.0` |
| [k8s.io/apiextensions-apiserver](https://github.com/kubernetes/apiextensions-apiserver) | `0.35.4` | `0.36.0` |
| [k8s.io/cli-runtime](https://github.com/kubernetes/cli-runtime) | `0.35.4` | `0.36.0` |
| [k8s.io/kubelet](https://github.com/kubernetes/kubelet) | `0.35.4` | `0.36.0` |
| [k8s.io/metrics](https://github.com/kubernetes/metrics) | `0.35.4` | `0.36.0` |



Updates `github.com/apparentlymart/go-cidr` from 1.1.0 to 1.1.1
- [Release notes](https://github.com/apparentlymart/go-cidr/releases)
- [Commits](https://github.com/apparentlymart/go-cidr/compare/v1.1.0...v1.1.1)

Updates `github.com/aws/aws-sdk-go-v2/service/s3` from 1.99.1 to 1.100.0
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.99.1...service/s3/v1.100.0)

Updates `github.com/mattn/go-isatty` from 0.0.21 to 0.0.22
- [Commits](https://github.com/mattn/go-isatty/compare/v0.0.21...v0.0.22)

Updates `k8s.io/api` from 0.35.4 to 0.36.0
- [Commits](https://github.com/kubernetes/api/compare/v0.35.4...v0.36.0)

Updates `k8s.io/apiextensions-apiserver` from 0.35.4 to 0.36.0
- [Release notes](https://github.com/kubernetes/apiextensions-apiserver/releases)
- [Commits](https://github.com/kubernetes/apiextensions-apiserver/compare/v0.35.4...v0.36.0)

Updates `k8s.io/apimachinery` from 0.35.4 to 0.36.0
- [Commits](https://github.com/kubernetes/apimachinery/compare/v0.35.4...v0.36.0)

Updates `k8s.io/apiserver` from 0.35.4 to 0.36.0
- [Commits](https://github.com/kubernetes/apiserver/compare/v0.35.4...v0.36.0)

Updates `k8s.io/cli-runtime` from 0.35.4 to 0.36.0
- [Commits](https://github.com/kubernetes/cli-runtime/compare/v0.35.4...v0.36.0)

Updates `k8s.io/client-go` from 0.35.4 to 0.36.0
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.35.4...v0.36.0)

Updates `k8s.io/kubelet` from 0.35.4 to 0.36.0
- [Commits](https://github.com/kubernetes/kubelet/compare/v0.35.4...v0.36.0)

Updates `k8s.io/metrics` from 0.35.4 to 0.36.0
- [Commits](https://github.com/kubernetes/metrics/compare/v0.35.4...v0.36.0)

Updates `k8s.io/utils` from 0.0.0-20251002143259-bc988d571ff4 to 0.0.0-20260210185600-b8788abfbbc2
- [Commits](https://github.com/kubernetes/utils/commits)

---
updated-dependencies:
- dependency-name: github.com/apparentlymart/go-cidr
  dependency-version: 1.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: github.com/aws/aws-sdk-go-v2/service/s3
  dependency-version: 1.100.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: github.com/mattn/go-isatty
  dependency-version: 0.0.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/api
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: k8s.io/apiextensions-apiserver
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: k8s.io/apimachinery
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: k8s.io/apiserver
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: k8s.io/cli-runtime
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: k8s.io/client-go
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: k8s.io/kubelet
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: k8s.io/metrics
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: k8s.io/utils
  dependency-version: 0.0.0-20260210185600-b8788abfbbc2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
...

Signed-off-by: dependabot[bot] <support@github.com>

* k8s.io/kubectl v0.36.0

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Andrew Lavery <laverya@umich.edu>
2026-04-29 08:12:16 -04:00
Evans MungaiandClaude Opus 4.6 21948fe959 feat: add host registryImages collector and analyzer (#2029)
* feat: add host registryImages collector and analyzer

Adds a host-level registryImages collector and analyzer that can check
image existence in registries without requiring a Kubernetes cluster.
Supports inline username/password auth or ambient credentials from
~/.docker/config.json.

Refactors imageExistsWithAuth from the cluster-level registry collector
to share the core image existence check logic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fixes from manual tests

Signed-off-by: Evans Mungai <evans@replicated.com>

* Updates from manual tests

Signed-off-by: Evans Mungai <evans@replicated.com>

* Sort template lists

Signed-off-by: Evans Mungai <evans@replicated.com>

* More changes

Signed-off-by: Evans Mungai <evans@replicated.com>

---------

Signed-off-by: Evans Mungai <evans@replicated.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-29 10:26:18 +01:00
Xav Paice b56b98742e Revert "refactor(collect): replace go.podman.io/image with go-containerregistry" (#2033)
This reverts commit b105fa62c7.
2026-04-29 13:29:35 +12:00
furiosa b105fa62c7 refactor(collect): replace go.podman.io/image with go-containerregistry
Migrate pkg/collect/registry.go off go.podman.io/image/v5 (containers/image)
onto github.com/google/go-containerregistry, eliminating the transitive
github.com/docker/docker +incompatible chain that blocks Docker v29 SDK
adoption.

The RegistryImages collector probes whether each requested image exists in
its registry by issuing a manifest HEAD. We replace alltransports.ParseImageName
+ imageRef.NewImage with name.ParseReference + remote.Head. Auth from a
kubernetes.io/dockerconfigjson Secret is converted to an authn.Authenticator;
error classification switches from errcode.Errors string matching to
transport.Error.StatusCode.

Add httptest.NewTLSServer-backed tests for imageExists covering the
found/not-found/unauthorized/EOF-retry paths to lock in protocol-level
behavior across the migration.

Promote go-containerregistry to a direct require at v0.21.5; go mod tidy
drops go.podman.io/image, distribution/distribution/v3, and the bare
docker/docker entry from the module graph.

Issue: tr-dit
2026-04-28 16:14:55 +12:00
Xav PaiceandClaude Opus 4.7 558a8f5110 chore(deps): bump docker/docker to v28.5.2 and go-containerregistry to v0.21.5 (#2028)
Addresses vulnerabilities in github.com/docker/docker v28.5.1. Both are
indirect deps; no source imports them directly. Pulled v0.21.5 of
go-containerregistry via `go get ...@latest`, then forced docker/docker
to v28.5.2 since go.podman.io/image/v5 still pins v28.5.1 at its latest
release (v5.39.2).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 13:59:37 +12:00
dependabot[bot]andlnx01 070fd9bab4 chore(deps): bump the security group across 1 directory with 12 updates (#2026)
Bumps the security group with 9 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) | `1.41.5` | `1.41.6` |
| [github.com/aws/aws-sdk-go-v2/credentials](https://github.com/aws/aws-sdk-go-v2) | `1.19.14` | `1.19.15` |
| [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) | `1.99.0` | `1.99.1` |
| [k8s.io/api](https://github.com/kubernetes/api) | `0.35.3` | `0.35.4` |
| [k8s.io/apiextensions-apiserver](https://github.com/kubernetes/apiextensions-apiserver) | `0.35.3` | `0.35.4` |
| [k8s.io/cli-runtime](https://github.com/kubernetes/cli-runtime) | `0.35.3` | `0.35.4` |
| [sigs.k8s.io/e2e-framework](https://github.com/kubernetes-sigs/e2e-framework) | `0.6.0` | `0.7.0` |
| [k8s.io/kubelet](https://github.com/kubernetes/kubelet) | `0.35.3` | `0.35.4` |
| [k8s.io/metrics](https://github.com/kubernetes/metrics) | `0.35.3` | `0.35.4` |



Updates `github.com/aws/aws-sdk-go-v2` from 1.41.5 to 1.41.6
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.41.5...v1.41.6)

Updates `github.com/aws/aws-sdk-go-v2/credentials` from 1.19.14 to 1.19.15
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/credentials/v1.19.14...credentials/v1.19.15)

Updates `github.com/aws/aws-sdk-go-v2/service/s3` from 1.99.0 to 1.99.1
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.99.0...service/s3/v1.99.1)

Updates `k8s.io/api` from 0.35.3 to 0.35.4
- [Commits](https://github.com/kubernetes/api/compare/v0.35.3...v0.35.4)

Updates `k8s.io/apiextensions-apiserver` from 0.35.3 to 0.35.4
- [Release notes](https://github.com/kubernetes/apiextensions-apiserver/releases)
- [Commits](https://github.com/kubernetes/apiextensions-apiserver/compare/v0.35.3...v0.35.4)

Updates `k8s.io/apimachinery` from 0.35.3 to 0.35.4
- [Commits](https://github.com/kubernetes/apimachinery/compare/v0.35.3...v0.35.4)

Updates `k8s.io/apiserver` from 0.35.3 to 0.35.4
- [Commits](https://github.com/kubernetes/apiserver/compare/v0.35.3...v0.35.4)

Updates `k8s.io/cli-runtime` from 0.35.3 to 0.35.4
- [Commits](https://github.com/kubernetes/cli-runtime/compare/v0.35.3...v0.35.4)

Updates `k8s.io/client-go` from 0.35.3 to 0.35.4
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.35.3...v0.35.4)

Updates `sigs.k8s.io/e2e-framework` from 0.6.0 to 0.7.0
- [Release notes](https://github.com/kubernetes-sigs/e2e-framework/releases)
- [Changelog](https://github.com/kubernetes-sigs/e2e-framework/blob/main/RELEASE.md)
- [Commits](https://github.com/kubernetes-sigs/e2e-framework/compare/v0.6.0...v0.7.0)

Updates `k8s.io/kubelet` from 0.35.3 to 0.35.4
- [Commits](https://github.com/kubernetes/kubelet/compare/v0.35.3...v0.35.4)

Updates `k8s.io/metrics` from 0.35.3 to 0.35.4
- [Commits](https://github.com/kubernetes/metrics/compare/v0.35.3...v0.35.4)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2
  dependency-version: 1.41.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: github.com/aws/aws-sdk-go-v2/credentials
  dependency-version: 1.19.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: github.com/aws/aws-sdk-go-v2/service/s3
  dependency-version: 1.99.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/api
  dependency-version: 0.35.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/apiextensions-apiserver
  dependency-version: 0.35.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/apimachinery
  dependency-version: 0.35.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/apiserver
  dependency-version: 0.35.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/cli-runtime
  dependency-version: 0.35.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/client-go
  dependency-version: 0.35.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: sigs.k8s.io/e2e-framework
  dependency-version: 0.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: security
- dependency-name: k8s.io/kubelet
  dependency-version: 0.35.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
- dependency-name: k8s.io/metrics
  dependency-version: 0.35.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: security
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-23 08:21:06 +12:00
dependabot[bot]andlnx01 e89c858bd2 chore(deps): bump docker/setup-qemu-action from 3 to 4 (#2023)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 13:37:56 +12:00
Evans Mungai 483467d8b9 chore: update preflight cli help text (#2027)
Signed-off-by: Evans Mungai <evans@replicated.com>
2026-04-21 17:38:16 +01:00
Ash 27903af638 [chore] bump pgx (#2025) 2026-04-21 11:40:02 +01:00
dependabot[bot]andlnx01 6eb09ee9d5 chore(deps): bump github.com/moby/spdystream from 0.5.0 to 0.5.1 (#2022)
Bumps [github.com/moby/spdystream](https://github.com/moby/spdystream) from 0.5.0 to 0.5.1.
- [Release notes](https://github.com/moby/spdystream/releases)
- [Commits](https://github.com/moby/spdystream/compare/v0.5.0...v0.5.1)

---
updated-dependencies:
- dependency-name: github.com/moby/spdystream
  dependency-version: 0.5.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 13:34:58 +01:00
Evans Mungai 845fdcc8c8 Move REVIEW.md to .cursor/BUGBOT.md (#2021)
* Move REVIEW.md to .cursor/BUGBOT.md

Signed-off-by: Evans Mungai <evans@replicated.com>

* Comment update

Signed-off-by: Evans Mungai <evans@replicated.com>

---------

Signed-off-by: Evans Mungai <evans@replicated.com>
2026-04-16 15:40:45 +01:00
Evans Mungai a3c453d1d6 fix: record skipped collectors in bundle and improve CLI warnings (#2019)
* fix: record skipped collectors in bundle and improve CLI warnings

- Write `skipped-collectors.json` to the support bundle and preflight bundle recording which collectors were skipped (RBAC permissions or spec exclusion), with reason, errors, and timestamp
- Surface RBAC errors and skipped-collector messages as `klog.Warningf` in non-interactive mode so they're visible without `-v` flags
- Exec collector file naming now falls back to the resolved container name (from the pod spec) when `collectorName` is not set, instead of only checking `containerName` from the spec

Signed-off-by: Evans Mungai <evans@replicated.com>

* Address comment

Signed-off-by: Evans Mungai <evans@replicated.com>

* fix bugbot comment

Signed-off-by: Evans Mungai <evans@replicated.com>

* Remove unused var

Signed-off-by: Evans Mungai <evans@replicated.com>

* Address comment

Signed-off-by: Evans Mungai <evans@replicated.com>

* Use results.SaveResult and add automated tests

Signed-off-by: Evans Mungai <evans@replicated.com>

* Improvements

Signed-off-by: Evans Mungai <evans@replicated.com>

* Fix bugbot

Signed-off-by: Evans Mungai <evans@replicated.com>

* Handle bugbot

Signed-off-by: Evans Mungai <evans@replicated.com>

---------

Signed-off-by: Evans Mungai <evans@replicated.com>
2026-04-15 22:29:35 +01:00
Evans Mungai 35d5ad17af feat: add CLAUDE.md and AGENTS.md to project (#2020)
* feat: add CLAUDE.md and AGENTS.md to project

Signed-off-by: Evans Mungai <evans@replicated.com>

* Add missed file

Signed-off-by: Evans Mungai <evans@replicated.com>

* Updates

Signed-off-by: Evans Mungai <evans@replicated.com>

---------

Signed-off-by: Evans Mungai <evans@replicated.com>
2026-04-15 18:46:07 +01:00
Evans MungaiandClaude Opus 4.6 daeab2dc20 fix: improve collector output discoverability (#2018)
* fix: document .tar.gz auto-append in --output flag help text

The --output flag silently appends .tar.gz to the provided path, which
was not mentioned in the help text, causing confusion for users who
expected the exact filename they specified.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: exec collector falls back to containerName for file naming

When collectorName is not set, the exec collector now uses containerName
as the output file prefix instead of producing bare -stdout.txt filenames.
This makes output files identifiable without requiring collectorName to
be explicitly set.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:42:30 +01:00
Andrew LaveryandDaniel Lipovetsky 5a611b3a6a feat: Collect validating and mutating webhook configurations (#2017)
* feat: Collect validating and mutating webhook configurations

* fixup! feat: Collect validating and mutating webhook configurations

Add metadata before marshaling

* explicitly test webhook configuration collection in TestClusterResources

---------

Co-authored-by: Daniel Lipovetsky <daniel.lipovetsky@nutanix.com>
2026-04-14 11:22:55 -04:00
Ludovic HenryandAndrew Lavery fc72539585 ci: Add releases for linux-riscv64 (#2010)
Add releases for linux-riscv64

This project is a dependency of `k0s` which is working on being built, tested and released on `linux-riscv64` [1].

RISC-V is gaining momentum especially in the embedded and edge world. 

[1] https://github.com/k0sproject/k0s/pull/7414

Co-authored-by: Andrew Lavery <laverya@umich.edu>
2026-04-14 11:03:26 -04:00
Andrew Lavery 69743729a3 smoke test all linux target architectures (#2016)
* smoke test all linux target architectures

* go mod update

* use 'real' target cluster

* better logging

* newer alpine
2026-04-14 07:30:26 -07:00
dependabot[bot]andlnx01 6d1b1eba49 chore(deps): bump golang.org/x/net from 0.52.0 to 0.53.0 in the security group (#2015)
chore(deps): bump golang.org/x/net in the security group

Bumps the security group with 1 update: [golang.org/x/net](https://github.com/golang/net).


Updates `golang.org/x/net` from 0.52.0 to 0.53.0
- [Commits](https://github.com/golang/net/compare/v0.52.0...v0.53.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.53.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>
2026-04-14 06:02:54 -04:00
dependabot[bot]andlnx01 db137b1c33 chore(deps): bump helm.sh/helm/v3 from 3.20.1 to 3.20.2 in /examples/sdk/helm-template (#2012)
chore(deps): bump helm.sh/helm/v3 in /examples/sdk/helm-template

Bumps [helm.sh/helm/v3](https://github.com/helm/helm) from 3.20.1 to 3.20.2.
- [Release notes](https://github.com/helm/helm/releases)
- [Commits](https://github.com/helm/helm/compare/v3.20.1...v3.20.2)

---
updated-dependencies:
- dependency-name: helm.sh/helm/v3
  dependency-version: 3.20.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-13 09:48:28 -04:00
dependabot[bot]andlnx01 a6915eac63 chore(deps): bump helm.sh/helm/v3 from 3.20.1 to 3.20.2 (#2013)
Bumps [helm.sh/helm/v3](https://github.com/helm/helm) from 3.20.1 to 3.20.2.
- [Release notes](https://github.com/helm/helm/releases)
- [Commits](https://github.com/helm/helm/compare/v3.20.1...v3.20.2)

---
updated-dependencies:
- dependency-name: helm.sh/helm/v3
  dependency-version: 3.20.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-13 09:48:04 -04:00
replicated-ciandlnx01 95ef65b08d Bump Go from 1.26.1 to 1.26.2 (#2014)
Bump Go to version from 1.26.1 to 1.26.2

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-13 09:47:45 -04:00
Ethan MosbaughandClaude Sonnet 4.6 c096e5d075 fix(supportbundle): ExtractLicenseFromBundle prefers license.json regardless of tar order (#2011)
license.json was being ignored when configmaps containing a license ID
appeared earlier in the tar archive. The function now collects both
candidates in a single pass and returns the license.json result if
present, falling back to the configmap scan only when license.json is
absent.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 11:59:53 -07:00
109 changed files with 6443 additions and 1205 deletions
+22
View File
@@ -0,0 +1,22 @@
# Code Review Guidelines
## Basic Review
- **Breaking API changes** — Check for removed/renamed fields in `pkg/apis/`, changed function signatures in public packages, or modified CLI flags and output formats.
- **Pattern violations** — New code should follow existing patterns in the codebase (e.g., collector/analyzer structure, error handling conventions, interface usage).
- **Security** — Watch for command injection in exec-based collectors, path traversal in file operations, unsanitized user input in specs, and leaked credentials in collected data.
- **Go standards** — Issues that linters like `go vet`, `staticcheck`, and `modernize` would catch: deprecated API usage, unnecessary allocations, error shadowing, unchecked errors.
- **Test coverage** — New functionality should have tests. Changes to existing code should not reduce coverage compared to the last test run on `main`.
- **Error handling** — Errors should wrap context (`fmt.Errorf("... : %w", err)`), not be silently swallowed, and provide actionable messages for operator-facing output.
- **Concurrency safety** — Collectors run concurrently. Shared state must be protected. `CollectorResult` map writes from goroutines need synchronization.
- **Bundle storage** — Collectors must save data using `CollectorResult.SaveResult` and related methods (`SaveResults`, `SymLinkResult`). Never write files directly — `CollectorResult` handles dual-mode storage (in-memory for preflights, on-disk for support bundles). See `pkg/collect/result.go`.
## Advanced Review
- **Cross-feature impact** — Consider whether a change to one collector/analyzer could affect the broader collection pipeline, redaction, or output archive structure.
- **CLI vs SDK consumers** — This project is consumed both as CLI tools and as Go packages (SDK). Changes targeting a CLI use case must not break SDK consumers who import `pkg/collect`, `pkg/analyze`, or API types directly.
- **Documentation** — Does the change add, modify, or remove user-facing behavior? Check whether https://troubleshoot.sh needs updates. Use https://troubleshoot.sh/llms.txt or https://troubleshoot.sh/llms-full.txt to review current docs.
- **Dedicated documentation needs** — For large or complex changes, consider whether CLI users or SDK consumers need standalone documentation (migration guides, new feature walkthroughs, updated examples).
- **Backwards compatibility** — Spec changes must consider existing specs in the wild. New fields should have sensible zero-value defaults. Removed fields should not cause parse failures.
- **Downstream impact on sbctl** — Changes to public Go packages (`pkg/collect`, `pkg/analyze`, API types, etc.) may require follow-up changes in [replicatedhq/sbctl](https://github.com/replicatedhq/sbctl), which imports this project as a dependency. Flag any breaking or behavioral changes that could affect sbctl.
- **Repository docs** — If your changes affect build commands, architecture, project conventions, or review guidelines, update `CLAUDE.md`, `README.md`, and this file accordingly.
+180
View File
@@ -0,0 +1,180 @@
schema_version: v1
name: dependency-update-troubleshoot
enabled: true
trigger:
cron:
schedule: "0 21 * * *"
timezone: "America/Chicago"
overlap_policy: skip
timeout: 2h
enable_manual_trigger: true
tags: ["cron", "dependencies"]
color: cyan
stages:
- id: start
label: Start
entry: true
on_enter:
inject: |
Workflow task: run the Go dependency update maintenance workflow for Troubleshoot.
This is an unattended dependency maintenance workflow. Scope: Go
dependency updates for the top-level go.mod/go.sum plus examples/sdk/helm-template. After updates
are applied, you will review the changes, fix any compatibility issues,
validate with make build and make test, and open one grouped PR.
Do not ask for permission to start. Immediately say exactly [PROCEED]
to run the dependency updater. Detailed post-update instructions will
follow.
- id: working
label: Working
triggers:
- message_contains: "[PROCEED]"
on_enter:
run:
command: |
bash -lc '
set -euo pipefail
if ! command -v go >/dev/null 2>&1; then
nix profile install nixpkgs#go_1_26 || nix-env -iA nixpkgs.go_1_26
fi
go_path="$(command -v go)"
go_version="$(go version)"
printf "{\"status\":\"ready\",\"go_path\":\"%s\",\"go_version\":\"%s\"}\n" "$go_path" "$go_version"
'
output: go_toolchain
timeout: 10m
dependency_updates:
ecosystems: [go]
paths: ["."]
output: dependency_updates
timeout: 30m
inject: |
Dependency updates were applied for the root Go module.
Scope this workflow to Go dependency updates only. Focus on the
top-level go.mod/go.sum. Exclude Node dependencies and any non-Go
modules.
Treat compatibility fixes as part of this workflow. If an updated
dependency causes build, lint, type, or test failures, first attempt
reasonable scoped code changes within the affected Go module to support
the new version. Do not downgrade, pin, or skip an update just because
it has breaking API changes unless the required changes are broad,
risky, unrelated to this workflow's scope, or cannot be completed within
reasonable effort. If you defer an update, explain exactly why.
Review {{ .Outputs.dependency_updates.files_changed }}.
Before opening a PR, validate using the repo make targets in the Nix
development environment (nix develop -c). Do not run ad-hoc go
commands; use exactly these targets:
- make build
- make test
Note: make test-integration requires a Kubernetes cluster. Only add it
to the validation if the workflow environment provides one.
Fix any failures and open one grouped PR.
Say [DONE] with the PR URL only when the PR is open.
If {{ .Outputs.dependency_updates.files_changed }} is empty and no PR is
needed, say [NO_CHANGES] instead of [DONE].
- id: no-changes
label: No Changes
triggers:
- message_contains: "[NO_CHANGES]"
- message_contains: "[DONE] No changes"
terminal: true
- id: validate
label: Validate
triggers:
- message_contains: "[DONE]"
on_enter:
run:
command: |
nix develop -c bash -lc '
set +e
tmpdir="$(mktemp -d)"
make build >"$tmpdir/build.log" 2>&1
build_status=$?
make test >"$tmpdir/test.log" 2>&1
test_status=$?
python3 - "$tmpdir" "$build_status" "$test_status" <<PY
import json
import pathlib
import sys
tmpdir = pathlib.Path(sys.argv[1])
statuses = {
"build": int(sys.argv[2]),
"test": int(sys.argv[3]),
}
checks = []
for name, code in statuses.items():
log = (tmpdir / f"{name}.log").read_text(errors="replace")
checks.append({
"name": name,
"status": "passed" if code == 0 else "failed",
"exit_code": code,
"log_tail": log[-12000:],
})
failed = [check for check in checks if check["exit_code"] != 0]
print(json.dumps({
"status": "passed" if not failed else "failed",
"reason": "all validation checks passed" if not failed else "one or more validation checks failed",
"checks": checks,
}))
PY
'
output: validation
timeout: 45m
gate:
output: validation
pass:
path: status
values: [passed]
fail:
path: status
values: [failed, error]
required: true
- id: fix-validation
label: Fix Validation
triggers:
- gate_result:
stage: validate
verdict: fail
on_enter:
inject: |
Validation failed before completion.
Status: {{ .Outputs.validation.status }}
Reason: {{ .Outputs.validation.reason }}
Checks:
{{ .Outputs.validation.checks }}
Fix the failures above, commit the changes to the same PR branch, and
say [DONE] with the PR URL again when the PR is ready for validation.
- id: complete
label: Complete
triggers:
- gate_result:
stage: validate
verdict: pass
terminal: true
+14 -14
View File
@@ -30,8 +30,8 @@ jobs:
tidy-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: 'go.mod'
- run: make tidy-diff
@@ -39,8 +39,8 @@ jobs:
test-integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: 'go.mod'
- uses: replicatedhq/action-k3s@main
@@ -54,8 +54,8 @@ jobs:
compile-preflight:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: 'go.mod'
- run: make generate preflight
@@ -68,7 +68,7 @@ jobs:
runs-on: ubuntu-latest
needs: compile-preflight
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: replicatedhq/action-k3s@main
id: k3s
with:
@@ -84,8 +84,8 @@ jobs:
compile-supportbundle:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: 'go.mod'
- run: make generate support-bundle
@@ -97,8 +97,8 @@ jobs:
compile-collect:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: 'go.mod'
- run: make generate collect
@@ -111,7 +111,7 @@ jobs:
runs-on: ubuntu-latest
needs: compile-supportbundle
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: replicatedhq/action-k3s@main
id: k3s
with:
@@ -127,9 +127,9 @@ jobs:
# Additional e2e tests for support bundle that run in Go, these create a Kind cluster
validate-supportbundle-e2e-go:
runs-on: ubuntu-latest
needs: [compile-supportbundle, compile-collect]
needs: [compile-supportbundle, compile-collect, compile-preflight]
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Download support bundle binary
uses: actions/download-artifact@v8
with:
+139 -9
View File
@@ -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@v7
- uses: dorny/paths-filter@v4
id: filter
with:
@@ -44,8 +44,8 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: 'go.mod'
@@ -73,8 +73,8 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: 'go.mod'
@@ -93,8 +93,8 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: 'go.mod'
- run: make build
@@ -104,6 +104,134 @@ jobs:
path: bin/
retention-days: 1
# Minimal linux architecture smoke tests
linux-arch-smoke:
if: needs.changes.outputs.go-files == 'true'
needs: [changes, lint]
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- goarch: amd64
docker_platform: linux/amd64
expected_machine: x86_64
- goarch: arm64
docker_platform: linux/arm64
expected_machine: aarch64
- goarch: arm
goarm: "7"
docker_platform: linux/arm/v7
expected_machine: armv7l
- goarch: riscv64
docker_platform: linux/riscv64
expected_machine: riscv64
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: 'go.mod'
- name: Setup K3s
uses: replicatedhq/action-k3s@main
with:
version: v1.31.2-k3s1
- uses: docker/setup-qemu-action@v4
with:
platforms: arm64,arm,riscv64
- name: Build linux/${{ matrix.goarch }} binaries
env:
CGO_ENABLED: "0"
GOOS: linux
GOARCH: ${{ matrix.goarch }}
GOARM: ${{ matrix.goarm }}
run: |
if [ -n "${GOARM}" ]; then
echo "Building for GOARCH=${GOARCH} GOARM=${GOARM}"
else
echo "Building for GOARCH=${GOARCH}"
fi
make build
- name: Collect a minimal support bundle on ${{ matrix.docker_platform }}
env:
PREFLIGHT_AUTO_UPDATE: "false"
TROUBLESHOOT_AUTO_UPDATE: "false"
run: |
set -euo pipefail
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
kubeconfig_path="${KUBECONFIG:-$HOME/.kube/config}"
assert_tar_member() {
local pattern="$1"
local label="$2"
local member
member="$(grep -m1 "$pattern" "$tmpdir/archive-members.txt" || true)"
if [ -z "$member" ]; then
echo "Expected ${label} was not found in ${archive}"
exit 1
fi
echo "Found ${label}: ${member}"
}
echo "Using kubeconfig at ${kubeconfig_path}"
test -f "${kubeconfig_path}"
cp "${kubeconfig_path}" "$tmpdir/kubeconfig"
echo "Verifying K3s cluster is reachable on the host"
kubectl --kubeconfig "$tmpdir/kubeconfig" get nodes -o wide
docker run --rm \
--platform ${{ matrix.docker_platform }} \
--network host \
-v "$PWD:/src" \
-v "$tmpdir:/tmp/linux-arch-smoke" \
-w /src \
alpine:3.23 \
sh -ec '
export PREFLIGHT_AUTO_UPDATE=false
export TROUBLESHOOT_AUTO_UPDATE=false
/src/bin/support-bundle \
--kubeconfig /tmp/linux-arch-smoke/kubeconfig \
--interactive=false \
/src/test/e2e/support-bundle/spec/linuxArchSmokeLocalHostCollectors.yaml \
--output /tmp/linux-arch-smoke/linux-arch-support-bundle.tar.gz
'
archive="$tmpdir/linux-arch-support-bundle.tar.gz"
test -f "$archive"
echo "Generated support bundle archive: $archive"
tar -tf "$archive" > "$tmpdir/archive-members.txt"
assert_tar_member '/host-collectors/system/cpu.json$' 'cpu.json'
assert_tar_member '/host-collectors/system/memory.json$' 'memory.json'
assert_tar_member '/host-collectors/run-host/uname.txt$' 'uname.txt'
assert_tar_member '/cluster-resources/nodes.json$' 'cluster-resources nodes.json'
assert_tar_member '/cluster-resources/resources.json$' 'cluster-resources resources.json'
assert_tar_member '/analysis.json$' 'analysis.json'
echo "Checking uname -m output"
uname_path="$(grep -m1 '/host-collectors/run-host/uname.txt$' "$tmpdir/archive-members.txt")"
uname_machine="$(tar -xOf "$archive" "$uname_path" | tr -d '\r\n')"
if [ "$uname_machine" != '${{ matrix.expected_machine }}' ]; then
echo "Unexpected uname -m output: ${uname_machine}"
exit 1
fi
echo "Analyzer input check passed: uname -m reported ${uname_machine}"
echo "Checking analysis.json"
analysis_path="$(grep -m1 '/analysis.json$' "$tmpdir/archive-members.txt")"
tar -xOf "$archive" "$analysis_path" > "$tmpdir/analysis.json"
jq -e '
.[]
| select(.insight.primary == "Linux arch smoke node count")
| select(.insight.detail == "This cluster has exactly 1 node")
| select(.severity == "debug")
' "$tmpdir/analysis.json" >/dev/null
echo "Found expected analyzer result: Linux arch smoke node count -> This cluster has exactly 1 node"
# E2E tests
e2e:
if: needs.changes.outputs.go-files == 'true' || github.event_name == 'push'
@@ -124,7 +252,7 @@ jobs:
target: support-bundle-e2e-go-test
needs-k3s: false
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Setup K3s
if: matrix.needs-k3s
@@ -143,7 +271,7 @@ jobs:
# Success summary
success:
if: always()
needs: [lint, test, build, e2e]
needs: [lint, test, build, linux-arch-smoke, e2e]
runs-on: ubuntu-latest
steps:
- name: Check results
@@ -152,6 +280,7 @@ jobs:
if [[ "${{ needs.lint.result }}" == "failure" ]] || \
[[ "${{ needs.test.result }}" == "failure" ]] || \
[[ "${{ needs.build.result }}" == "failure" ]] || \
[[ "${{ needs.linux-arch-smoke.result }}" == "failure" ]] || \
[[ "${{ needs.e2e.result }}" == "failure" ]]; then
echo "::error::Some jobs failed or were cancelled"
exit 1
@@ -161,6 +290,7 @@ jobs:
if [[ "${{ needs.lint.result }}" == "cancelled" ]] || \
[[ "${{ needs.test.result }}" == "cancelled" ]] || \
[[ "${{ needs.build.result }}" == "cancelled" ]] || \
[[ "${{ needs.linux-arch-smoke.result }}" == "cancelled" ]] || \
[[ "${{ needs.e2e.result }}" == "cancelled" ]]; then
echo "::error::Some jobs failed or were cancelled"
exit 1
+31
View File
@@ -0,0 +1,31 @@
name: Publish ElasticClaw Workflow
on:
push:
branches:
- main
paths:
- ".factory-workflows/dependency-update.yaml"
workflow_dispatch:
jobs:
publish:
name: Publish workflow (${{ matrix.workflow }})
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
matrix:
workflow:
- .factory-workflows/dependency-update.yaml
steps:
- uses: actions/checkout@v7
- name: Publish workflow (dependency-update)
uses: elasticclaw/actions/publish-workflow@main
with:
hub-endpoint: https://factory.repldev.com
token: ${{ secrets.ELASTICCLAW_TOKEN }}
workspace: replicated-troubleshoot
path: ${{ matrix.workflow }}
+101
View File
@@ -0,0 +1,101 @@
name: publish-securebuild
env:
SECUREBUILD_CLI_VERSION: v0.0.498
SECUREBUILD_CLI_SHA256: d259aa99d85aef957fe1e40c6f26e1983cc7fd72277e08599a82adc6681d3b37
on:
workflow_call:
inputs:
version:
description: Stable version tag to build (for example, v0.130.0)
required: true
type: string
secrets:
SECUREBUILD_API_TOKEN:
required: true
workflow_dispatch:
inputs:
version:
description: Stable version tag to build (for example, v0.130.0)
required: true
type: string
jobs:
validate-version:
runs-on: ubuntu-22.04
steps:
- name: Require a stable release version
env:
VERSION: ${{ inputs.version }}
run: |
if ! [[ "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "SecureBuild only supports stable release versions (for example, v0.130.0): $VERSION"
exit 1
fi
build-package:
needs: validate-version
runs-on: ubuntu-22.04
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Set version
id: version
env:
VERSION: ${{ inputs.version }}
run: echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Install SecureBuild CLI
run: |
curl --fail --silent --show-error --location \
"https://github.com/securebuildhq/securebuild/releases/download/${SECUREBUILD_CLI_VERSION}/securebuild-cli-linux-amd64" \
--output securebuild
echo "${SECUREBUILD_CLI_SHA256} securebuild" | sha256sum --check --strict
chmod +x securebuild
- name: Build package
env:
SECUREBUILD_API_TOKEN: ${{ secrets.SECUREBUILD_API_TOKEN }}
VERSION: ${{ steps.version.outputs.version }}
run: |
./securebuild build package \
--package-family-name troubleshoot \
--tag "$VERSION" \
--api-token "$SECUREBUILD_API_TOKEN"
build-image:
needs: build-package
if: ${{ !cancelled() && needs.build-package.result == 'success' }}
runs-on: ubuntu-22.04
strategy:
matrix:
image-name:
- troubleshoot
- preflight
steps:
- name: Install SecureBuild CLI
run: |
curl --fail --silent --show-error --location \
"https://github.com/securebuildhq/securebuild/releases/download/${SECUREBUILD_CLI_VERSION}/securebuild-cli-linux-amd64" \
--output securebuild
echo "${SECUREBUILD_CLI_SHA256} securebuild" | sha256sum --check --strict
chmod +x securebuild
- name: Build image
env:
IMAGE_NAME: ${{ matrix.image-name }}
SECUREBUILD_API_TOKEN: ${{ secrets.SECUREBUILD_API_TOKEN }}
VERSION: ${{ needs.build-package.outputs.version }}
run: |
IMAGE_VERSION="${VERSION#v}"
MAJOR_MINOR_VERSION="${IMAGE_VERSION%.*}"
MAJOR_VERSION="${IMAGE_VERSION%%.*}"
./securebuild build image \
--image-name "$IMAGE_NAME" \
--tag "$VERSION" \
--image-tag "$MAJOR_MINOR_VERSION" \
--image-tag "$MAJOR_VERSION" \
--image-tag latest \
--api-token "$SECUREBUILD_API_TOKEN"
+10 -10
View File
@@ -21,12 +21,12 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version-file: 'go.mod'
@@ -55,7 +55,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Create output directory
run: mkdir -p test/output
@@ -87,7 +87,7 @@ jobs:
./bin/preflight version
- name: Setup Python for comparison
uses: actions/setup-python@v6
uses: actions/setup-python@v7
with:
python-version: '3.11'
@@ -159,7 +159,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Create output directory
run: mkdir -p test/output
@@ -191,7 +191,7 @@ jobs:
./bin/preflight version
- name: Setup Python for comparison
uses: actions/setup-python@v6
uses: actions/setup-python@v7
with:
python-version: '3.11'
@@ -262,7 +262,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Create output directory
run: mkdir -p test/output
@@ -294,7 +294,7 @@ jobs:
./bin/support-bundle version
- name: Setup Python for comparison
uses: actions/setup-python@v6
uses: actions/setup-python@v7
with:
python-version: '3.11'
@@ -361,10 +361,10 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Setup Python
uses: actions/setup-python@v6
uses: actions/setup-python@v7
with:
python-version: '3.11'
+29 -6
View File
@@ -14,18 +14,25 @@ jobs:
runs-on: troubleshoot_release
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: azure/docker-login@v2
- uses: actions/setup-go@v7
with:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
go-version: '1.26'
check-latest: true
- uses: actions/setup-go@v6
- name: Install Cosign
uses: sigstore/cosign-installer@v4.1.2
with:
go-version-file: 'go.mod'
cosign-release: 'v3.1.3'
- name: Generate and sign SBOM
run: make sbom
env:
COSIGN_KEY: ${{ secrets.COSIGN_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v7
@@ -46,3 +53,19 @@ jobs:
uses: rajatjindal/krew-release-bot@v0.0.51
with:
krew_template_file: deploy/krew/support-bundle.yaml
securebuild:
if: ${{ !contains(github.ref_name, '-') }}
uses: ./.github/workflows/publish-securebuild.yml
with:
version: ${{ github.ref_name }}
secrets:
SECUREBUILD_API_TOKEN: ${{ secrets.SECUREBUILD_API_TOKEN }}
notify:
if: ${{ !contains(github.ref_name, '-') }}
uses: replicatedhq/reusable-workflows/.github/workflows/notify-release.yml@0432fb838fc83852b78be97a150b35991a85d56f
with:
tag: ${{ github.ref_name }}
secrets:
slack_webhook: ${{ secrets.RELEASE_SLACK_WEBHOOK }}
-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 }}
+3
View File
@@ -0,0 +1,3 @@
# AGENTS.md
ALWAYS read and follow the instructions in CLAUDE.md before starting any work in this repository.
+30
View File
@@ -0,0 +1,30 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Replicated Troubleshoot is a Kubernetes diagnostic framework providing two kubectl plugins: `preflight` (pre-installation cluster validation) and `support-bundle` (post-installation diagnostics with log collection, redaction, and analysis). Specs use the Kubernetes custom resource format (as a serialization convention, not installed in-cluster) and are defined by application vendors and executed by cluster operators.
## Build & Test Commands
```bash
make build # Build bin/support-bundle and bin/preflight
make test # Unit tests (includes generate, fmt, vet)
make test RUN=TestMyFunction # Run a single test
make test-integration # Integration tests (requires k8s cluster)
make e2e # All e2e tests
make generate # Regenerate types/clients after modifying pkg/apis/
```
## Architecture
The core data flow is: **Spec loading → Collection → Redaction → Analysis → Results**. The two main workflows are orchestrated by `pkg/supportbundle/` and `pkg/preflight/`.
Three API versions coexist in `pkg/apis/troubleshoot/`: v1beta1, v1beta2 (primary, all types defined here), and v1beta3 (in-progress, adds `StringOrValueFrom` for Secret/ConfigMap references, converts to v1beta2 at runtime).
Collectors live in `pkg/collect/`, analyzers in `pkg/analyze/`. When adding either, follow the pattern of existing implementations and run `make generate` after modifying API types.
## Code Review
See [.cursor/BUGBOT.md](.cursor/BUGBOT.md) for the full review checklist covering basic checks (API breaks, pattern violations, security, test coverage) and advanced checks (cross-feature impact, CLI vs SDK consumers, documentation needs).
+9 -28
View File
@@ -61,6 +61,9 @@ test: generate fmt vet
test-integration: generate fmt vet
go test -v --tags="integration exclude_graphdriver_devicemapper exclude_graphdriver_btrfs" ${BUILDPATHS}
.PHONY: e2e
e2e: preflight-e2e-test support-bundle-e2e-test support-bundle-e2e-go-test
.PHONY: preflight-e2e-test
preflight-e2e-test:
./test/validate-preflight-e2e.sh
@@ -149,17 +152,8 @@ vet:
.PHONY: generate
generate: controller-gen client-gen
$(CONTROLLER_GEN) \
object:headerFile=./hack/boilerplate.go.txt paths=./pkg/apis/...
$(CLIENT_GEN) \
--output-dir=. \
--output-pkg=github.com/replicatedhq/troubleshoot/pkg/client \
--clientset-name troubleshootclientset \
--input-base github.com/replicatedhq/troubleshoot/pkg/apis \
--input troubleshoot/v1beta1 \
--input troubleshoot/v1beta2 \
--input troubleshoot/v1beta3 \
--go-header-file ./hack/boilerplate.go.txt
$(CONTROLLER_GEN) object:headerFile=./hack/boilerplate.go.txt paths=./pkg/apis/...
$(CLIENT_GEN) --output-dir=. --output-pkg=github.com/replicatedhq/troubleshoot/pkg/client --clientset-name troubleshootclientset --input-base github.com/replicatedhq/troubleshoot/pkg/apis --input troubleshoot/v1beta1 --input troubleshoot/v1beta2 --input troubleshoot/v1beta3 --go-header-file ./hack/boilerplate.go.txt
cp -r troubleshootclientset pkg/client
rm -rf troubleshootclientset
@@ -191,32 +185,18 @@ bin/docsgen:
controller-gen:
go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.19.0
CONTROLLER_GEN=$(shell which controller-gen)
CONTROLLER_GEN=$(shell go env GOPATH)/bin/controller-gen
.PHONY: client-gen
client-gen:
go install k8s.io/code-generator/cmd/client-gen@v0.34.0
CLIENT_GEN=$(shell which client-gen)
CLIENT_GEN=$(shell go env GOPATH)/bin/client-gen
.PHONY: release
release: export GITHUB_TOKEN = $(shell echo ${GITHUB_TOKEN_TROUBLESHOOT})
release:
curl -sL https://git.io/goreleaser | bash -s -- --rm-dist --config deploy/.goreleaser.yml
.PHONY: snapshot-release
snapshot-release:
curl -sL https://git.io/goreleaser | bash -s -- --rm-dist --snapshot --config deploy/.goreleaser.snapshot.yml
docker push replicated/troubleshoot:alpha
docker push replicated/preflight:alpha
.PHONY: local-release
local-release:
curl -sL https://git.io/goreleaser | bash -s -- --rm-dist --snapshot --config deploy/.goreleaser.yaml
docker tag replicated/troubleshoot:alpha localhost:32000/troubleshoot:alpha
docker tag replicated/preflight:alpha localhost:32000/preflight:alpha
docker push localhost:32000/troubleshoot:alpha
docker push localhost:32000/preflight:alpha
.PHONY: run-preflight
run-preflight: bin/preflight
./bin/preflight ./examples/preflight/sample-preflight.yaml
@@ -252,7 +232,8 @@ sbom: sbom/assets/troubleshoot-sbom.tgz
--tlog-upload \
--yes \
--rekor-url=https://rekor.sigstore.dev \
sbom/assets/troubleshoot-sbom.tgz > sbom/assets/troubleshoot-sbom.tgz.sig
--bundle sbom/assets/troubleshoot-sbom.tgz.bundle \
sbom/assets/troubleshoot-sbom.tgz
cosign public-key --key cosign.key --outfile sbom/assets/key.pub
.PHONY: get-govulncheck
+3 -10
View File
@@ -56,19 +56,12 @@ For questions about using Troubleshoot, how to contribute and engaging with the
# Software Bill of Materials
A signed SBOM that includes Troubleshoot dependencies is included in each release.
- **troubleshoot-sbom.tgz** contains a software bill of materials for Troubleshoot.
- **troubleshoot-sbom.tgz.sig** is the digital signature for troubleshoot-sbom.tgz
- **troubleshoot-sbom.tgz.bundle** contains the signature and transparency log material used by Cosign.
- **key.pub** is the public key from the key pair used to sign troubleshoot-sbom.tgz
The following example illustrates using [cosign](https://github.com/sigstore/cosign) to verify that **troubleshoot-sbom.tgz** has
not been tampered with.
not been tampered with. Install [Cosign v3](https://github.com/sigstore/cosign/releases).
```sh
$ cosign verify-blob --key key.pub --signature troubleshoot-sbom.tgz.sig troubleshoot-sbom.tgz
Verified OK
```
If you were to get an error similar to the one below, it means you are verifying an SBOM signed using cosign `v1` using a newer `v2` of the binary. This version introduced [breaking changes](https://github.com/sigstore/cosign/blob/main/CHANGELOG.md#breaking-changes) which require an additional flag `--insecure-ignore-tlog=true` to successfully verify SBOMs like so.
```sh
$ cosign verify-blob --key key.pub --signature troubleshoot-sbom.tgz.sig troubleshoot-sbom.tgz --insecure-ignore-tlog=true
WARNING: Skipping tlog verification is an insecure practice that lacks of transparency and auditability verification for the blob.
$ cosign verify-blob --key key.pub --bundle troubleshoot-sbom.tgz.bundle troubleshoot-sbom.tgz
Verified OK
```
+88
View File
@@ -0,0 +1,88 @@
package cli
import (
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
"github.com/spf13/cobra"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
// ClickhouseCmd runs the clickhouse collector against a single database and
// prints the native result JSON ({"isConnected":..,"version":..,"error":..}).
func ClickhouseCmd() *cobra.Command {
var (
uri string
skipVerify bool
caCert string
clientCert string
clientKey string
secretName string
secretNamespace string
)
cmd := &cobra.Command{
Use: "clickhouse",
Short: "Run the clickhouse collector against a database",
Long: `Run the clickhouse collector: connect to a ClickHouse server, run "SELECT version()",
and print the collector result as JSON.
--uri is a clickhouse-go DSN:
clickhouse://user:password@host:9000/dbname
TLS material may be provided inline / by file path (--tls-cacert, --tls-client-cert,
--tls-client-key) or sourced from a Secret (--tls-secret-name), which requires
cluster access.
Example:
collect clickhouse --uri "clickhouse://default:@ch.default.svc:9000/default"`,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
db := &troubleshootv1beta2.Database{
CollectorMeta: troubleshootv1beta2.CollectorMeta{CollectorName: "clickhouse"},
URI: uri,
}
var (
client kubernetes.Interface
cfg *rest.Config
)
if skipVerify || caCert != "" || clientCert != "" || clientKey != "" || secretName != "" {
tls := &troubleshootv1beta2.TLSParams{
SkipVerify: skipVerify,
CACert: caCert,
ClientCert: clientCert,
ClientKey: clientKey,
}
if secretName != "" {
tls.Secret = &troubleshootv1beta2.TLSSecret{Name: secretName, Namespace: secretNamespace}
var err error
client, cfg, err = k8sClientForCollectors()
if err != nil {
return err
}
}
db.TLS = tls
}
c := &collect.CollectClickhouse{Collector: db, ClientConfig: cfg, Client: client, Context: cmd.Context()}
res, err := c.Collect(nil)
if err != nil {
return err
}
return printCollectorResult(res)
},
}
f := cmd.Flags()
f.StringVar(&uri, "uri", "", "ClickHouse connection DSN, e.g. clickhouse://user:pass@host:9000/db (required)")
cmd.MarkFlagRequired("uri")
f.BoolVar(&skipVerify, "tls-skip-verify", false, "skip TLS certificate verification")
f.StringVar(&caCert, "tls-cacert", "", "CA certificate (PEM contents or a file path)")
f.StringVar(&clientCert, "tls-client-cert", "", "client certificate (PEM contents or a file path)")
f.StringVar(&clientKey, "tls-client-key", "", "client key (PEM contents or a file path)")
f.StringVar(&secretName, "tls-secret-name", "", "name of a Secret holding TLS material (requires cluster access)")
f.StringVar(&secretNamespace, "tls-secret-namespace", "default", "namespace of the TLS Secret")
return cmd
}
+47
View File
@@ -0,0 +1,47 @@
// Package cli implements the `collect` command and its subcommands.
//
// The per-collector subcommands (http, postgres, mysql, mssql, redis) run a
// single collector and print its native result JSON to stdout. This lets a
// collector run inside a Pod using the troubleshoot image — e.g. as a runPod
// collector — so the check executes from within the cluster instead of from
// wherever the CLI happens to be invoked.
package cli
import (
"fmt"
"github.com/replicatedhq/troubleshoot/pkg/collect"
"github.com/replicatedhq/troubleshoot/pkg/constants"
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
"github.com/replicatedhq/troubleshoot/pkg/version"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
// printCollectorResult writes the native collector result JSON to stdout.
// The single-collector subcommands run with an empty BundlePath, so the result
// is held in memory (map value = the JSON bytes) rather than written to disk.
func printCollectorResult(res collect.CollectorResult) error {
for _, b := range res {
fmt.Println(string(b))
}
return nil
}
// k8sClientForCollectors builds a Kubernetes client from the ambient kubeconfig
// or in-cluster config. Only the collectors that resolve TLS material from a
// Secret need this; plain connections and inline/file TLS do not.
func k8sClientForCollectors() (kubernetes.Interface, *rest.Config, error) {
cfg, err := k8sutil.GetRESTConfig()
if err != nil {
return nil, nil, err
}
cfg.QPS = constants.DEFAULT_CLIENT_QPS
cfg.Burst = constants.DEFAULT_CLIENT_BURST
cfg.UserAgent = fmt.Sprintf("%s/%s", constants.DEFAULT_CLIENT_USER_AGENT, version.Version())
client, err := kubernetes.NewForConfig(cfg)
if err != nil {
return nil, nil, err
}
return client, cfg, nil
}
+79
View File
@@ -0,0 +1,79 @@
package cli
import (
"fmt"
"strings"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
"github.com/spf13/cobra"
)
// HTTPCmd runs the http collector against a single endpoint and prints the
// native result JSON ({"response":{"status":...}} or {"error":{...}}).
func HTTPCmd() *cobra.Command {
var (
method string
url string
headers map[string]string
body string
timeout string
proxy string
insecure bool
caCert string
)
cmd := &cobra.Command{
Use: "http",
Short: "Run the http collector against an endpoint",
Long: `Run the http collector: issue an HTTP request and print the collector result as JSON.
The result contains the response status, body and headers, or an error object.
Examples:
collect http --url http://myapp.default.svc:8080/healthz
collect http --method POST --url https://api.internal/ping \
--header 'Content-Type=application/json' --body '{"ping":true}'`,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
var tls *troubleshootv1beta2.TLSParams
if caCert != "" {
tls = &troubleshootv1beta2.TLSParams{CACert: caCert}
}
spec := &troubleshootv1beta2.HTTP{
CollectorMeta: troubleshootv1beta2.CollectorMeta{CollectorName: "http"},
}
switch strings.ToUpper(method) {
case "GET":
spec.Get = &troubleshootv1beta2.Get{URL: url, Headers: headers, Timeout: timeout, Proxy: proxy, InsecureSkipVerify: insecure, TLS: tls}
case "POST":
spec.Post = &troubleshootv1beta2.Post{URL: url, Headers: headers, Body: body, Timeout: timeout, Proxy: proxy, InsecureSkipVerify: insecure, TLS: tls}
case "PUT":
spec.Put = &troubleshootv1beta2.Put{URL: url, Headers: headers, Body: body, Timeout: timeout, Proxy: proxy, InsecureSkipVerify: insecure, TLS: tls}
default:
return fmt.Errorf("unsupported --method %q (use GET, POST, or PUT)", method)
}
c := &collect.CollectHTTP{Collector: spec}
res, err := c.Collect(nil)
if err != nil {
return err
}
return printCollectorResult(res)
},
}
f := cmd.Flags()
f.StringVar(&url, "url", "", "request URL (required)")
cmd.MarkFlagRequired("url")
f.StringVar(&method, "method", "GET", "HTTP method: GET, POST, or PUT")
f.StringToStringVar(&headers, "header", nil, "request header as key=value (repeatable)")
f.StringVar(&body, "body", "", "request body (POST/PUT only)")
f.StringVar(&timeout, "timeout", "", "request timeout, e.g. 15s (empty = no timeout)")
f.StringVar(&proxy, "proxy", "", "proxy URL to use for the request")
f.BoolVar(&insecure, "insecure-skip-verify", false, "do not verify the server's TLS certificate")
f.StringVar(&caCert, "tls-cacert", "", "CA certificate to trust (PEM contents or a file path)")
return cmd
}
+49
View File
@@ -0,0 +1,49 @@
package cli
import (
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
"github.com/spf13/cobra"
)
// MssqlCmd runs the mssql collector against a single database and prints the
// native result JSON ({"isConnected":..,"version":..,"error":..}).
//
// Unlike the other database collectors, the mssql collector does not honor a
// separate TLS field — TLS is configured through the connection URI query
// parameters (e.g. encrypt=true) — so this subcommand only takes --uri.
func MssqlCmd() *cobra.Command {
var uri string
cmd := &cobra.Command{
Use: "mssql",
Short: "Run the mssql collector against a database",
Long: `Run the mssql collector: connect to a Microsoft SQL Server, run "select @@VERSION",
and print the collector result as JSON.
--uri is a go-mssqldb connection URL; TLS options are passed as query parameters:
sqlserver://user:password@host:1433?database=app&encrypt=true
Example:
collect mssql --uri "sqlserver://sa:pass@mssql.default.svc:1433?encrypt=true"`,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
db := &troubleshootv1beta2.Database{
CollectorMeta: troubleshootv1beta2.CollectorMeta{CollectorName: "mssql"},
URI: uri,
}
c := &collect.CollectMssql{Collector: db, Context: cmd.Context()}
res, err := c.Collect(nil)
if err != nil {
return err
}
return printCollectorResult(res)
},
}
cmd.Flags().StringVar(&uri, "uri", "", "SQL Server connection URI, e.g. sqlserver://user:pass@host:1433 (required)")
cmd.MarkFlagRequired("uri")
return cmd
}
+90
View File
@@ -0,0 +1,90 @@
package cli
import (
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
"github.com/spf13/cobra"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
// MysqlCmd runs the mysql collector against a single database and prints the
// native result JSON ({"isConnected":..,"version":..,"variables":..,"error":..}).
func MysqlCmd() *cobra.Command {
var (
uri string
parameters []string
skipVerify bool
caCert string
clientCert string
clientKey string
secretName string
secretNamespace string
)
cmd := &cobra.Command{
Use: "mysql",
Short: "Run the mysql collector against a database",
Long: `Run the mysql collector: connect to a MySQL server, run "select version()",
optionally collect server variables, and print the collector result as JSON.
--uri is a go-sql-driver DSN (note: not a URL):
user:password@tcp(host:3306)/dbname
--parameters names server variables to collect via "SHOW VARIABLES"; the matching
values are returned under "variables" in the result. This flag is unique to mysql.
Example:
collect mysql --uri "root:pass@tcp(mysql.default.svc:3306)/" --parameters max_connections,version`,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
db := &troubleshootv1beta2.Database{
CollectorMeta: troubleshootv1beta2.CollectorMeta{CollectorName: "mysql"},
URI: uri,
Parameters: parameters,
}
var (
client kubernetes.Interface
cfg *rest.Config
)
if skipVerify || caCert != "" || clientCert != "" || clientKey != "" || secretName != "" {
tls := &troubleshootv1beta2.TLSParams{
SkipVerify: skipVerify,
CACert: caCert,
ClientCert: clientCert,
ClientKey: clientKey,
}
if secretName != "" {
tls.Secret = &troubleshootv1beta2.TLSSecret{Name: secretName, Namespace: secretNamespace}
var err error
client, cfg, err = k8sClientForCollectors()
if err != nil {
return err
}
}
db.TLS = tls
}
c := &collect.CollectMysql{Collector: db, ClientConfig: cfg, Client: client, Context: cmd.Context()}
res, err := c.Collect(nil)
if err != nil {
return err
}
return printCollectorResult(res)
},
}
f := cmd.Flags()
f.StringVar(&uri, "uri", "", "MySQL connection DSN, e.g. user:pass@tcp(host:3306)/db (required)")
cmd.MarkFlagRequired("uri")
f.StringSliceVar(&parameters, "parameters", nil, "server variables to collect via SHOW VARIABLES (comma-separated or repeatable)")
f.BoolVar(&skipVerify, "tls-skip-verify", false, "skip TLS certificate verification")
f.StringVar(&caCert, "tls-cacert", "", "CA certificate (PEM contents or a file path)")
f.StringVar(&clientCert, "tls-client-cert", "", "client certificate (PEM contents or a file path)")
f.StringVar(&clientKey, "tls-client-key", "", "client key (PEM contents or a file path)")
f.StringVar(&secretName, "tls-secret-name", "", "name of a Secret holding TLS material (requires cluster access)")
f.StringVar(&secretNamespace, "tls-secret-namespace", "default", "namespace of the TLS Secret")
return cmd
}
+88
View File
@@ -0,0 +1,88 @@
package cli
import (
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
"github.com/spf13/cobra"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
// PostgresCmd runs the postgres collector against a single database and prints
// the native result JSON ({"isConnected":..,"version":..,"error":..}).
func PostgresCmd() *cobra.Command {
var (
uri string
skipVerify bool
caCert string
clientCert string
clientKey string
secretName string
secretNamespace string
)
cmd := &cobra.Command{
Use: "postgres",
Short: "Run the postgres collector against a database",
Long: `Run the postgres collector: connect to a PostgreSQL server, run "select version()",
and print the collector result as JSON.
--uri is a libpq/pgx connection URI:
postgres://user:password@host:5432/dbname?sslmode=disable
TLS material may be provided inline / by file path (--tls-cacert, --tls-client-cert,
--tls-client-key) or sourced from a Secret (--tls-secret-name), which requires
cluster access.
Example:
collect postgres --uri "postgres://user:pass@pg.default.svc:5432/app?sslmode=disable"`,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
db := &troubleshootv1beta2.Database{
CollectorMeta: troubleshootv1beta2.CollectorMeta{CollectorName: "postgres"},
URI: uri,
}
var (
client kubernetes.Interface
cfg *rest.Config
)
if skipVerify || caCert != "" || clientCert != "" || clientKey != "" || secretName != "" {
tls := &troubleshootv1beta2.TLSParams{
SkipVerify: skipVerify,
CACert: caCert,
ClientCert: clientCert,
ClientKey: clientKey,
}
if secretName != "" {
tls.Secret = &troubleshootv1beta2.TLSSecret{Name: secretName, Namespace: secretNamespace}
var err error
client, cfg, err = k8sClientForCollectors()
if err != nil {
return err
}
}
db.TLS = tls
}
c := &collect.CollectPostgres{Collector: db, ClientConfig: cfg, Client: client, Context: cmd.Context()}
res, err := c.Collect(nil)
if err != nil {
return err
}
return printCollectorResult(res)
},
}
f := cmd.Flags()
f.StringVar(&uri, "uri", "", "PostgreSQL connection URI (required)")
cmd.MarkFlagRequired("uri")
f.BoolVar(&skipVerify, "tls-skip-verify", false, "skip TLS certificate verification")
f.StringVar(&caCert, "tls-cacert", "", "CA certificate (PEM contents or a file path)")
f.StringVar(&clientCert, "tls-client-cert", "", "client certificate (PEM contents or a file path)")
f.StringVar(&clientKey, "tls-client-key", "", "client key (PEM contents or a file path)")
f.StringVar(&secretName, "tls-secret-name", "", "name of a Secret holding TLS material (requires cluster access)")
f.StringVar(&secretNamespace, "tls-secret-namespace", "default", "namespace of the TLS Secret")
return cmd
}
+89
View File
@@ -0,0 +1,89 @@
package cli
import (
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
"github.com/spf13/cobra"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
// RedisCmd runs the redis collector against a single instance and prints the
// native result JSON ({"isConnected":..,"version":..,"error":..}).
func RedisCmd() *cobra.Command {
var (
uri string
skipVerify bool
caCert string
clientCert string
clientKey string
secretName string
secretNamespace string
)
cmd := &cobra.Command{
Use: "redis",
Short: "Run the redis collector against an instance",
Long: `Run the redis collector: connect to a Redis server, read "INFO server" (server
version), and print the collector result as JSON.
--uri is a go-redis connection URL:
redis://host:6379 (plaintext)
rediss://user:pass@host:6379 (TLS)
TLS material may be provided inline / by file path (--tls-cacert, --tls-client-cert,
--tls-client-key) or sourced from a Secret (--tls-secret-name), which requires
cluster access.
Example:
collect redis --uri "redis://redis.default.svc:6379"`,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
db := &troubleshootv1beta2.Database{
CollectorMeta: troubleshootv1beta2.CollectorMeta{CollectorName: "redis"},
URI: uri,
}
var (
client kubernetes.Interface
cfg *rest.Config
)
if skipVerify || caCert != "" || clientCert != "" || clientKey != "" || secretName != "" {
tls := &troubleshootv1beta2.TLSParams{
SkipVerify: skipVerify,
CACert: caCert,
ClientCert: clientCert,
ClientKey: clientKey,
}
if secretName != "" {
tls.Secret = &troubleshootv1beta2.TLSSecret{Name: secretName, Namespace: secretNamespace}
var err error
client, cfg, err = k8sClientForCollectors()
if err != nil {
return err
}
}
db.TLS = tls
}
c := &collect.CollectRedis{Collector: db, ClientConfig: cfg, Client: client, Context: cmd.Context()}
res, err := c.Collect(nil)
if err != nil {
return err
}
return printCollectorResult(res)
},
}
f := cmd.Flags()
f.StringVar(&uri, "uri", "", "Redis connection URL, e.g. redis://host:6379 (required)")
cmd.MarkFlagRequired("uri")
f.BoolVar(&skipVerify, "tls-skip-verify", false, "skip TLS certificate verification")
f.StringVar(&caCert, "tls-cacert", "", "CA certificate (PEM contents or a file path)")
f.StringVar(&clientCert, "tls-client-cert", "", "client certificate (PEM contents or a file path)")
f.StringVar(&clientKey, "tls-client-key", "", "client key (PEM contents or a file path)")
f.StringVar(&secretName, "tls-secret-name", "", "name of a Secret holding TLS material (requires cluster access)")
f.StringVar(&secretNamespace, "tls-secret-namespace", "default", "namespace of the TLS Secret")
return cmd
}
+17 -4
View File
@@ -19,7 +19,9 @@ func RootCmd() *cobra.Command {
Short: "Run a collector",
Long: `Run a collector and output the results.`,
SilenceUsage: true,
PreRun: func(cmd *cobra.Command, args []string) {
// PersistentPreRun/PersistentPostRun (rather than PreRun/PostRun) so this
// setup also runs for the per-collector subcommands, not just `collect [url]`.
PersistentPreRun: func(cmd *cobra.Command, args []string) {
v := viper.GetViper()
v.BindPFlags(cmd.Flags())
@@ -38,7 +40,7 @@ func RootCmd() *cobra.Command {
return runCollect(v, args[0])
},
PostRun: func(cmd *cobra.Command, args []string) {
PersistentPostRun: func(cmd *cobra.Command, args []string) {
if err := util.StopProfiling(); err != nil {
klog.Errorf("Failed to stop profiling: %v", err)
}
@@ -49,6 +51,17 @@ func RootCmd() *cobra.Command {
cmd.AddCommand(util.VersionCmd())
// Per-collector subcommands: run a single collector and print its native
// result JSON. Each has its own flags and help. These let a collector run
// inside a Pod via the troubleshoot image (e.g. as a runPod collector), so
// the check executes from within the cluster rather than wherever the CLI runs.
cmd.AddCommand(HTTPCmd())
cmd.AddCommand(PostgresCmd())
cmd.AddCommand(MysqlCmd())
cmd.AddCommand(MssqlCmd())
cmd.AddCommand(RedisCmd())
cmd.AddCommand(ClickhouseCmd())
cmd.Flags().StringSlice("redactors", []string{}, "names of the additional redactors to use")
cmd.Flags().Bool("redact", true, "enable/disable default redactions")
cmd.Flags().String("format", "json", "output format, one of json or raw.")
@@ -56,7 +69,7 @@ func RootCmd() *cobra.Command {
cmd.Flags().String("collector-pull-policy", "", "the pull policy of the collector image")
cmd.Flags().String("selector", "", "selector (label query) to filter remote collection nodes on.")
cmd.Flags().Bool("collect-without-permissions", false, "always generate a support bundle, even if it some require additional permissions")
cmd.Flags().Bool("debug", false, "enable debug logging")
cmd.PersistentFlags().Bool("debug", false, "enable debug logging")
cmd.Flags().String("chroot", "", "Chroot to path")
// hidden in favor of the `insecure-skip-tls-verify` flag
@@ -67,7 +80,7 @@ func RootCmd() *cobra.Command {
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
k8sutil.AddFlags(cmd.Flags())
k8sutil.AddFlags(cmd.PersistentFlags())
// Initialize klog flags
logger.InitKlogFlags(cmd)
+7 -1
View File
@@ -25,7 +25,13 @@ func RootCmd() *cobra.Command {
Args: cobra.MinimumNArgs(1),
Short: "Run and retrieve preflight checks in a cluster",
Long: `A preflight check is a set of validations that can and should be run to ensure
that a cluster meets the requirements to run an application.`,
that a cluster meets the requirements to run an application.
Unlike support-bundle, preflight does not support --load-cluster-specs because
preflight checks are designed to run before an application is installed or
upgraded. Since no deployment has occurred yet, there are no in-cluster specs
to discover. Preflight specs must be provided via a URL, local file path, or
stdin (e.g. "helm template ... | kubectl preflight -").`,
SilenceUsage: true,
SilenceErrors: true,
PreRun: func(cmd *cobra.Command, args []string) {
+1 -1
View File
@@ -133,7 +133,7 @@ If no arguments are provided, specs are automatically loaded from the cluster by
cmd.Flags().String("since-time", "", "force pod logs collectors to return logs after a specific date (RFC3339)")
cmd.Flags().String("since", "", "force pod logs collectors to return logs newer than a relative duration like 5s, 2m, or 3h.")
cmd.Flags().Int("remote-host-collect-timeout", 30, "timeout in seconds for remote host collect operations (e.g. waiting for pods/daemonsets)")
cmd.Flags().StringP("output", "o", "", "specify the output file path for the support bundle")
cmd.Flags().StringP("output", "o", "", "specify the output file path for the support bundle (.tar.gz extension is added automatically)")
cmd.Flags().Bool("debug", false, "enable debug logging. This is equivalent to --v=0")
cmd.Flags().Bool("dry-run", false, "print support bundle spec without collecting anything")
cmd.Flags().Bool("auto-update", true, "enable automatic binary self-update check and install")
+12 -1
View File
@@ -166,7 +166,18 @@ func runTroubleshoot(v *viper.Viper, args []string) error {
go func() {
defer wg.Done()
for msg := range progressChan {
klog.Infof("Collecting support bundle: %v", msg)
switch msg := msg.(type) {
case error:
klog.Warningf("Collecting support bundle: %v", msg)
case string:
if strings.Contains(msg, "skipping collector") {
klog.Warningf("Collecting support bundle: %s", msg)
} else {
klog.Infof("Collecting support bundle: %s", msg)
}
default:
klog.Infof("Collecting support bundle: %v", msg)
}
}
}()
} else {
+103
View File
@@ -1360,6 +1360,8 @@ spec:
- key
type: object
type: object
ignoreIfNoFiles:
type: boolean
outcomes:
items:
properties:
@@ -1612,6 +1614,58 @@ spec:
- outcomes
- selector
type: object
s3Status:
properties:
annotations:
additionalProperties:
type: string
type: object
checkName:
type: string
collectorName:
type: string
exclude:
type: BoolString
fileName:
type: string
outcomes:
items:
properties:
fail:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
pass:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
warn:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
type: object
type: array
strict:
type: BoolString
required:
- collectorName
- outcomes
type: object
secret:
properties:
annotations:
@@ -2783,6 +2837,55 @@ spec:
required:
- outcomes
type: object
registryImages:
properties:
annotations:
additionalProperties:
type: string
type: object
checkName:
type: string
collectorName:
type: string
exclude:
type: BoolString
outcomes:
items:
properties:
fail:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
pass:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
warn:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
type: object
type: array
strict:
type: BoolString
required:
- outcomes
type: object
subnetAvailable:
properties:
annotations:
@@ -17969,6 +17969,29 @@ spec:
required:
- namespace
type: object
s3Status:
properties:
accessKeyID:
type: string
bucketName:
type: string
collectorName:
type: string
endpoint:
type: string
exclude:
type: BoolString
insecure:
type: boolean
region:
type: string
secretAccessKey:
type: string
usePathStyle:
type: boolean
required:
- bucketName
type: object
secret:
properties:
collectorName:
@@ -18462,6 +18485,27 @@ spec:
- port
- toCIDR
type: object
registryImages:
description: |-
HostRegistryImages checks whether images are accessible from the host,
without requiring a Kubernetes cluster. Auth can be supplied inline via
Username/Password or omitted to rely on ambient credentials (e.g. ~/.docker/config.json).
properties:
collectorName:
type: string
exclude:
type: BoolString
images:
items:
type: string
type: array
password:
type: string
username:
type: string
required:
- images
type: object
run:
properties:
args:
@@ -855,6 +855,55 @@ spec:
required:
- outcomes
type: object
registryImages:
properties:
annotations:
additionalProperties:
type: string
type: object
checkName:
type: string
collectorName:
type: string
exclude:
type: BoolString
outcomes:
items:
properties:
fail:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
pass:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
warn:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
type: object
type: array
strict:
type: BoolString
required:
- outcomes
type: object
subnetAvailable:
properties:
annotations:
@@ -1784,6 +1833,27 @@ spec:
- port
- toCIDR
type: object
registryImages:
description: |-
HostRegistryImages checks whether images are accessible from the host,
without requiring a Kubernetes cluster. Auth can be supplied inline via
Username/Password or omitted to rely on ambient credentials (e.g. ~/.docker/config.json).
properties:
collectorName:
type: string
exclude:
type: BoolString
images:
items:
type: string
type: array
password:
type: string
username:
type: string
required:
- images
type: object
run:
properties:
args:
@@ -855,6 +855,55 @@ spec:
required:
- outcomes
type: object
registryImages:
properties:
annotations:
additionalProperties:
type: string
type: object
checkName:
type: string
collectorName:
type: string
exclude:
type: BoolString
outcomes:
items:
properties:
fail:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
pass:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
warn:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
type: object
type: array
strict:
type: BoolString
required:
- outcomes
type: object
subnetAvailable:
properties:
annotations:
@@ -1784,6 +1833,27 @@ spec:
- port
- toCIDR
type: object
registryImages:
description: |-
HostRegistryImages checks whether images are accessible from the host,
without requiring a Kubernetes cluster. Auth can be supplied inline via
Username/Password or omitted to rely on ambient credentials (e.g. ~/.docker/config.json).
properties:
collectorName:
type: string
exclude:
type: BoolString
images:
items:
type: string
type: array
password:
type: string
username:
type: string
required:
- images
type: object
run:
properties:
args:
@@ -1360,6 +1360,8 @@ spec:
- key
type: object
type: object
ignoreIfNoFiles:
type: boolean
outcomes:
items:
properties:
@@ -1612,6 +1614,58 @@ spec:
- outcomes
- selector
type: object
s3Status:
properties:
annotations:
additionalProperties:
type: string
type: object
checkName:
type: string
collectorName:
type: string
exclude:
type: BoolString
fileName:
type: string
outcomes:
items:
properties:
fail:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
pass:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
warn:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
type: object
type: array
strict:
type: BoolString
required:
- collectorName
- outcomes
type: object
secret:
properties:
annotations:
@@ -19866,6 +19920,29 @@ spec:
required:
- namespace
type: object
s3Status:
properties:
accessKeyID:
type: string
bucketName:
type: string
collectorName:
type: string
endpoint:
type: string
exclude:
type: BoolString
insecure:
type: boolean
region:
type: string
secretAccessKey:
type: string
usePathStyle:
type: boolean
required:
- bucketName
type: object
secret:
properties:
collectorName:
@@ -1391,6 +1391,8 @@ spec:
- key
type: object
type: object
ignoreIfNoFiles:
type: boolean
outcomes:
items:
properties:
@@ -1643,6 +1645,58 @@ spec:
- outcomes
- selector
type: object
s3Status:
properties:
annotations:
additionalProperties:
type: string
type: object
checkName:
type: string
collectorName:
type: string
exclude:
type: BoolString
fileName:
type: string
outcomes:
items:
properties:
fail:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
pass:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
warn:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
type: object
type: array
strict:
type: BoolString
required:
- collectorName
- outcomes
type: object
secret:
properties:
annotations:
@@ -19897,6 +19951,29 @@ spec:
required:
- namespace
type: object
s3Status:
properties:
accessKeyID:
type: string
bucketName:
type: string
collectorName:
type: string
endpoint:
type: string
exclude:
type: BoolString
insecure:
type: boolean
region:
type: string
secretAccessKey:
type: string
usePathStyle:
type: boolean
required:
- bucketName
type: object
secret:
properties:
collectorName:
@@ -20787,6 +20864,55 @@ spec:
required:
- outcomes
type: object
registryImages:
properties:
annotations:
additionalProperties:
type: string
type: object
checkName:
type: string
collectorName:
type: string
exclude:
type: BoolString
outcomes:
items:
properties:
fail:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
pass:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
warn:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
type: object
type: array
strict:
type: BoolString
required:
- outcomes
type: object
subnetAvailable:
properties:
annotations:
@@ -21716,6 +21842,27 @@ spec:
- port
- toCIDR
type: object
registryImages:
description: |-
HostRegistryImages checks whether images are accessible from the host,
without requiring a Kubernetes cluster. Auth can be supplied inline via
Username/Password or omitted to rely on ambient credentials (e.g. ~/.docker/config.json).
properties:
collectorName:
type: string
exclude:
type: BoolString
images:
items:
type: string
type: array
password:
type: string
username:
type: string
required:
- images
type: object
run:
properties:
args:
+10 -47
View File
@@ -9,10 +9,14 @@ builds:
main: ./cmd/preflight/main.go
env: [CGO_ENABLED=0]
goos: [linux, darwin, windows]
goarch: [amd64, arm, arm64]
goarch: [amd64, arm, arm64, riscv64]
ignore:
- goos: windows
goarch: arm
- goos: windows
goarch: riscv64
- goos: darwin
goarch: riscv64
ldflags:
- -s -w
- -X github.com/replicatedhq/troubleshoot/pkg/version.version={{ .Version }}
@@ -32,10 +36,14 @@ builds:
main: ./cmd/troubleshoot/main.go
env: [CGO_ENABLED=0]
goos: [linux, darwin, windows]
goarch: [amd64, arm, arm64]
goarch: [amd64, arm, arm64, riscv64]
ignore:
- goos: windows
goarch: arm
- goos: windows
goarch: riscv64
- goos: darwin
goarch: riscv64
ldflags:
- -s -w
- -X github.com/replicatedhq/troubleshoot/pkg/version.version={{ .Version }}
@@ -51,29 +59,6 @@ builds:
- -installsuffix=netgo
binary: support-bundle
- id: collect
main: ./cmd/collect/main.go
env: [CGO_ENABLED=0]
goos: [linux, darwin, windows]
goarch: [amd64, arm, arm64]
ignore:
- goos: windows
goarch: arm
ldflags:
- -s -w
- -X github.com/replicatedhq/troubleshoot/pkg/version.version={{ .Version }}
- -X github.com/replicatedhq/troubleshoot/pkg/version.gitSHA={{ .Commit }}
- -X github.com/replicatedhq/troubleshoot/pkg/version.buildTime={{ .Date }}
- -extldflags "-static"
flags:
- -tags=netgo
- -tags=containers_image_ostree_stub
- -tags=exclude_graphdriver_devicemapper
- -tags=exclude_graphdriver_btrfs
- -tags=containers_image_openpgp
- -installsuffix=netgo
binary: collect
archives:
- id: preflight
ids: [preflight]
@@ -148,28 +133,6 @@ archives:
dst: .
strip_parent: true
dockers:
- dockerfile: ./deploy/Dockerfile.troubleshoot
image_templates:
- "replicated/troubleshoot:latest"
- "replicated/troubleshoot:{{ .Major }}"
- "replicated/troubleshoot:{{ .Major }}.{{ .Minor }}"
- "replicated/troubleshoot:{{ .Major }}.{{ .Minor }}.{{ .Patch }}"
ids:
- support-bundle
- preflight
- collect
- dockerfile: ./deploy/Dockerfile.troubleshoot
image_templates:
- "replicated/preflight:latest"
- "replicated/preflight:{{ .Major }}"
- "replicated/preflight:{{ .Major }}.{{ .Minor }}"
- "replicated/preflight:{{ .Major }}.{{ .Minor }}.{{ .Patch }}"
ids:
- support-bundle
- preflight
- collect
universal_binaries:
- id: preflight-universal
ids: [preflight] # refers to the build id above
-13
View File
@@ -1,13 +0,0 @@
FROM debian:bookworm
WORKDIR /
RUN apt-get -qq update \
&& apt-get -qq -y install \
ca-certificates kmod
COPY support-bundle /troubleshoot/support-bundle
COPY preflight /troubleshoot/preflight
COPY collect /troubleshoot/collect
ENV PATH="/troubleshoot:${PATH}"
+40 -32
View File
@@ -1,6 +1,6 @@
module helm-template
go 1.26.1
go 1.26.5
// Always use the local version of troubleshoot so as to build using
// the latest version of the library. This will ensure the example
@@ -9,7 +9,7 @@ replace github.com/replicatedhq/troubleshoot v0.0.0 => ../../../
require (
github.com/replicatedhq/troubleshoot v0.0.0
helm.sh/helm/v3 v3.20.1
helm.sh/helm/v3 v3.21.4
sigs.k8s.io/yaml v1.6.0
)
@@ -17,24 +17,33 @@ require (
dario.cat/mergo v1.0.2 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Masterminds/semver/v3 v3.5.0 // indirect
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.7.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-openapi/jsonpointer v1.0.0 // indirect
github.com/go-openapi/jsonreference v1.0.0 // indirect
github.com/go-openapi/swag v0.28.0 // indirect
github.com/go-openapi/swag/cmdutils v0.28.0 // indirect
github.com/go-openapi/swag/conv v0.28.0 // indirect
github.com/go-openapi/swag/fileutils v0.28.0 // indirect
github.com/go-openapi/swag/jsonutils v0.28.0 // indirect
github.com/go-openapi/swag/loading v0.28.0 // indirect
github.com/go-openapi/swag/mangling v0.28.0 // indirect
github.com/go-openapi/swag/netutils v0.28.0 // indirect
github.com/go-openapi/swag/pools v0.28.0 // indirect
github.com/go-openapi/swag/stringutils v0.28.0 // indirect
github.com/go-openapi/swag/typeutils v0.28.0 // indirect
github.com/go-openapi/swag/yamlutils v0.28.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/gnostic-models v0.7.1 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@@ -45,29 +54,28 @@ require (
github.com/shopspring/decimal v1.4.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/term v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/crypto v0.55.0 // indirect
golang.org/x/net v0.58.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/term v0.45.0 // indirect
golang.org/x/text v0.41.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.35.2 // indirect
k8s.io/apiextensions-apiserver v0.35.2 // indirect
k8s.io/apimachinery v0.35.2 // indirect
k8s.io/client-go v0.35.2 // indirect
k8s.io/api v0.36.3 // indirect
k8s.io/apiextensions-apiserver v0.36.3 // indirect
k8s.io/apimachinery v0.36.3 // indirect
k8s.io/client-go v0.36.3 // indirect
k8s.io/klog/v2 v2.140.0 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
sigs.k8s.io/controller-runtime v0.23.3 // indirect
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect
k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 // indirect
sigs.k8s.io/controller-runtime v0.24.1 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 // indirect
)
+96 -72
View File
@@ -6,38 +6,66 @@ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/cyphar/filepath-securejoin v0.7.0 h1:s0Y3ITPy6sQn5xt54DuYvTF8hu134ooYLUb58DX/HjE=
github.com/cyphar/filepath-securejoin v0.7.0/go.mod h1:ymLGms/u3BYaviIiuKFnUx8EkQEZeK6cInNoAPJA3o4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s=
github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y=
github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY=
github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0=
github.com/go-openapi/swag v0.28.0 h1:xkgbOSKj6DZziNpyqRRAOt3GJGtgjgsd2RoyT30VWuw=
github.com/go-openapi/swag v0.28.0/go.mod h1:4qYnT3Cqr1p1VknOdPo70evN4rgQnAg6jwApHyxSGIg=
github.com/go-openapi/swag/cmdutils v0.28.0 h1:7TOeNtkYru1SG8Y34tDh9WBbLsMqGnptuxWiHREPZ4Q=
github.com/go-openapi/swag/cmdutils v0.28.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM=
github.com/go-openapi/swag/conv v0.28.0 h1:GtqqbyFe7vR5Y7ehxG9W6/OvrSFdf1OLeTGp40TqxH8=
github.com/go-openapi/swag/conv v0.28.0/go.mod h1:mbUE+mzctnhxi864m0Q07SpN8OowD9JhxmxuYvZZD/k=
github.com/go-openapi/swag/fileutils v0.28.0 h1:Z04XWQD7R8Eq+7GnOrjovBxPPmZzsS4gt2H2GPGIViU=
github.com/go-openapi/swag/fileutils v0.28.0/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8=
github.com/go-openapi/swag/jsonutils v0.28.0 h1:YIch6FwO7RXzeAnbO8Tu7dWBZeUEH+4nA0HXltVTnv4=
github.com/go-openapi/swag/jsonutils v0.28.0/go.mod h1:CYM3WlTUcagR2ZoHdz54di/cbBqt82tuxuXgAjxw+mg=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.28.0 h1:qV+VVUAx5Oro8WjVWpZeql7YReTKhT4smR4zhcOQZr0=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.28.0/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY=
github.com/go-openapi/swag/loading v0.28.0 h1:td8QZdZC9MIYGGSnSPKShKiK22I2tU5UQvuUhIBPRLU=
github.com/go-openapi/swag/loading v0.28.0/go.mod h1:rXB0QiQX5mMveXEA7ouM4KiiM9jVJe4K6BVbwhD1M4k=
github.com/go-openapi/swag/mangling v0.28.0 h1:pH8eyeNO9SLYsTMWJrurnNfKmDa28XrlA+HePVD53VM=
github.com/go-openapi/swag/mangling v0.28.0/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w=
github.com/go-openapi/swag/netutils v0.28.0 h1:YXN6TALEi2pzts8/8GNm6T61HTAZsieukGZidap989k=
github.com/go-openapi/swag/netutils v0.28.0/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ=
github.com/go-openapi/swag/pools v0.28.0 h1:HPMZWSAfce3rdVTFcjFiCIBtDg9h4x2QlRrHipwhxeU=
github.com/go-openapi/swag/pools v0.28.0/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE=
github.com/go-openapi/swag/stringutils v0.28.0 h1:ixsc9iYgDPubHL/8nSkbnryEHpD2VRlBMLKpQyPXcDU=
github.com/go-openapi/swag/stringutils v0.28.0/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM=
github.com/go-openapi/swag/typeutils v0.28.0 h1:nRBKSBXjDgf01VDPB3fWeD9nQuhCOVeIYAkUx2tbkyY=
github.com/go-openapi/swag/typeutils v0.28.0/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ=
github.com/go-openapi/swag/yamlutils v0.28.0 h1:TV3JXH6DS46KUroDtMLAYHGkdWf5VDq3wVWFirmzROY=
github.com/go-openapi/swag/yamlutils v0.28.0/go.mod h1:x0q/yndZHEgk9Rx3DyDqzFUmHy55KTvIZldvF2dTJXs=
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0=
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo=
github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug=
github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -49,16 +77,12 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
@@ -71,17 +95,17 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.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.27.4 h1:fcEcQW/A++6aZAZQNUmNjvA9PSOzefMJBerHJ4t8v8Y=
github.com/onsi/ginkgo/v2 v2.27.4/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q=
github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
@@ -98,32 +122,32 @@ 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=
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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
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.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74=
golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -135,29 +159,29 @@ 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.20.1 h1:T8PodUaH1UwNvE+imUA2mIKjJItY8g7CVvLVP5g4NzI=
helm.sh/helm/v3 v3.20.1/go.mod h1:Fl1kBaWCpkUrM6IYXPjQ3bdZQfFrogKArqptvueZ6Ww=
k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw=
k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60=
k8s.io/apiextensions-apiserver v0.35.2 h1:iyStXHoJZsUXPh/nFAsjC29rjJWdSgUmG1XpApE29c0=
k8s.io/apiextensions-apiserver v0.35.2/go.mod h1:OdyGvcO1FtMDWQ+rRh/Ei3b6X3g2+ZDHd0MSRGeS8rU=
k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8=
k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o=
k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g=
helm.sh/helm/v3 v3.21.4 h1:T/GcIEXU/gNjJnkITlIZ3e9xqkZjhFTmISuStTZ6+Qg=
helm.sh/helm/v3 v3.21.4/go.mod h1:cS2FBb+xfLuaSqvEmbqIeKUVFgHdHVHtVeXb2epof3M=
k8s.io/api v0.36.3 h1:NxB+05W2UGqXWFXcLO0RB5cnqnUPP5v5sVlaOH0Iz4w=
k8s.io/api v0.36.3/go.mod h1:JzLQKqRHC5+I8RVj/lS3lCg0mg6nWI9Fo/Sk3ElxHzg=
k8s.io/apiextensions-apiserver v0.36.3 h1:dPmOAPhwTtqb1bTxbFPsy18KHPhktQeO3WUPXunZIB0=
k8s.io/apiextensions-apiserver v0.36.3/go.mod h1:KTXFqgXiuw2pRoL+Wpmttqc+up9Xt/GohadPWeLLOa4=
k8s.io/apimachinery v0.36.3 h1:PkzMRBRG8joFD8EhCuQAtNPvJlxb82FwplP26HIzvAM=
k8s.io/apimachinery v0.36.3/go.mod h1:cTSjBWgPe/6CQyBKzY/hDIRWCQQQeK0mfLbml0UYFHE=
k8s.io/client-go v0.36.3 h1:M4JdVzXxYcZk4fGpfDdYnxSwhLKWCFoQsHW6t+z8Hfg=
k8s.io/client-go v0.36.3/go.mod h1:gcPwr0c87vjjG6HB6pWEqOeuYVoXSsREjzux2j6GF30=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80=
sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0=
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A=
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I=
k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 h1:jVkFFVfXdXP74B/zbO3hM3hpSFD0xvhQ5U686DPurkE=
k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3/go.mod h1:M2s5JB1lIYP3jzZdorPLHXIPJzt9vv2muW5a6L9DtNM=
sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4=
sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs=
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3CIki3aW8c4Q=
sigs.k8s.io/structured-merge-diff/v6 v6.4.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
Generated
+61
View File
@@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1783224372,
"narHash": "sha256-8i/87eeoqiGE4yOTjwSA3Eh/ziJRQEmd/unYU+K27sk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d407951447dcd00442e97087bf374aad70c04cea",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+26
View File
@@ -0,0 +1,26 @@
{
description = "Troubleshoot development environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in {
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
go_1_26
git # required for go VCS stamping during build
python3 # required by the dependency-update validation stage
];
shellHook = ''
echo "Troubleshoot dev $(go version)" # go 1.26.x required
'';
};
});
}
+160 -161
View File
@@ -1,280 +1,279 @@
module github.com/replicatedhq/troubleshoot
go 1.26.1
go 1.26.5
require (
github.com/ClickHouse/clickhouse-go/v2 v2.48.0
github.com/Masterminds/sprig/v3 v3.3.0
github.com/ahmetalpbalkan/go-cursor v0.0.0-20131010032410-8136607ea412
github.com/apparentlymart/go-cidr v1.1.0
github.com/aws/aws-sdk-go-v2 v1.41.5
github.com/aws/aws-sdk-go-v2/credentials v1.19.14
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0
github.com/apparentlymart/go-cidr v1.1.1
github.com/aws/aws-sdk-go-v2 v1.43.6
github.com/aws/aws-sdk-go-v2/credentials v1.19.36
github.com/aws/aws-sdk-go-v2/service/s3 v1.107.2
github.com/blang/semver/v4 v4.0.0
github.com/casbin/govaluate v1.10.0
github.com/cilium/ebpf v0.21.0
github.com/cilium/ebpf v0.22.0
github.com/containerd/cgroups/v3 v3.1.3
github.com/distribution/distribution/v3 v3.1.0
github.com/fatih/color v1.19.0
github.com/go-logr/logr v1.4.3
github.com/go-logr/logr v1.4.4
github.com/go-redis/redis/v7 v7.4.1
github.com/go-sql-driver/mysql v1.9.3
github.com/go-sql-driver/mysql v1.10.0
github.com/gobwas/glob v0.2.3
github.com/godbus/dbus/v5 v5.2.2
github.com/google/go-containerregistry v0.21.9
github.com/google/gofuzz v1.2.0
github.com/google/uuid v1.6.0
github.com/gorilla/handlers v1.5.2
github.com/hashicorp/go-getter v1.8.6
github.com/hashicorp/go-getter v1.8.8
github.com/hashicorp/go-multierror v1.1.1
github.com/jackc/pgx/v5 v5.9.1
github.com/longhorn/go-iscsi-helper v0.0.0-20210330030558-49a327fb024e
github.com/jackc/pgx/v5 v5.10.0
github.com/longhorn/go-common-libs v0.0.0-20260730002911-add09e6eb92c
github.com/manifoldco/promptui v0.9.0
github.com/mattn/go-isatty v0.0.21
github.com/microsoft/go-mssqldb v1.9.8
github.com/miekg/dns v1.1.72
github.com/mattn/go-isatty v0.0.24
github.com/microsoft/go-mssqldb v1.10.0
github.com/miekg/dns v1.1.73
github.com/opencontainers/image-spec v1.1.1
github.com/pkg/errors v0.9.1
github.com/replicatedhq/termui/v3 v3.1.1-0.20200811145416-f40076d26851
github.com/segmentio/ksuid v1.0.4
github.com/shirou/gopsutil/v4 v4.26.3
github.com/shirou/gopsutil/v4 v4.26.7
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/stretchr/testify v1.12.1
github.com/tj/go-spin v1.1.0
github.com/vishvananda/netlink v1.3.1
github.com/vishvananda/netns v0.0.5
github.com/vmware-tanzu/velero v1.18.0
go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/sdk v1.43.0
go.podman.io/image/v5 v5.39.2
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67
golang.org/x/mod v0.35.0
golang.org/x/sync v0.20.0
github.com/vmware-tanzu/velero v1.18.2
go.opentelemetry.io/otel v1.45.0
go.opentelemetry.io/otel/sdk v1.45.0
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
golang.org/x/mod v0.40.0
golang.org/x/sync v0.22.0
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.35.3
k8s.io/apiextensions-apiserver v0.35.3
k8s.io/apimachinery v0.35.3
k8s.io/apiserver v0.35.3
k8s.io/cli-runtime v0.35.3
k8s.io/client-go v0.35.3
k8s.io/api v0.36.3
k8s.io/apiextensions-apiserver v0.36.3
k8s.io/apimachinery v0.36.3
k8s.io/apiserver v0.36.3
k8s.io/cli-runtime v0.36.3
k8s.io/client-go v0.36.3
k8s.io/klog/v2 v2.140.0
oras.land/oras-go/v2 v2.6.0
sigs.k8s.io/controller-runtime v0.23.3
sigs.k8s.io/e2e-framework v0.6.0
k8s.io/streaming v0.36.3
oras.land/oras-go/v2 v2.6.2
sigs.k8s.io/controller-runtime v0.24.1
sigs.k8s.io/e2e-framework v0.7.0
)
require (
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cel.dev/expr v0.25.2 // indirect
cloud.google.com/go/auth v0.22.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/monitoring v1.24.3 // indirect
cyphar.com/go-pathrs v0.2.1 // indirect
cloud.google.com/go/monitoring v1.30.0 // indirect
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.1 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
github.com/ClickHouse/ch-go v0.74.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.35.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.59.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.59.0 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Masterminds/semver/v3 v3.5.0 // indirect
github.com/Masterminds/squirrel v1.5.4 // indirect
github.com/ProtonMail/go-crypto v1.4.1 // indirect
github.com/andybalholm/brotli v1.2.2 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.12 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/avast/retry-go/v4 v4.7.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.18 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.35 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.37 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.37 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.37 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.38 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.30 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.37 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.38 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.5.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.33.6 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.45.6 // indirect
github.com/aws/smithy-go v1.27.8 // indirect
github.com/chai2010/gettext-go v1.0.3 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/cockroachdb/errors v1.14.0 // indirect
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect
github.com/cockroachdb/redact v1.1.8 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/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.10.0 // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.7.0 // indirect
github.com/cyphar/filepath-securejoin v0.7.0 // indirect
github.com/docker/cli v29.7.1+incompatible // indirect
github.com/ebitengine/purego v0.10.2 // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
github.com/getsentry/sentry-go v0.48.0 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.8.0 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/go-openapi/swag/cmdutils v0.28.0 // indirect
github.com/go-openapi/swag/conv v0.28.0 // indirect
github.com/go-openapi/swag/fileutils v0.28.0 // indirect
github.com/go-openapi/swag/jsonutils v0.28.0 // indirect
github.com/go-openapi/swag/loading v0.28.0 // indirect
github.com/go-openapi/swag/mangling v0.28.0 // indirect
github.com/go-openapi/swag/netutils v0.28.0 // indirect
github.com/go-openapi/swag/pools v0.28.0 // indirect
github.com/go-openapi/swag/stringutils v0.28.0 // indirect
github.com/go-openapi/swag/typeutils v0.28.0 // indirect
github.com/go-openapi/swag/yamlutils v0.28.0 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-containerregistry v0.20.6 // indirect
github.com/google/gnostic-models v0.7.1 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.19 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/gosuri/uitable v0.0.4 // indirect
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72 // indirect
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jmoiron/sqlx v1.4.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mistifyio/go-zfs/v4 v4.0.0 // indirect
github.com/lib/pq v1.12.3 // indirect
github.com/lufia/plan9stats v0.0.0-20260801144041-2fc331e7910f // indirect
github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/capability v0.4.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/paulmach/orb v0.13.0 // indirect
github.com/pierrec/lz4/v4 v4.1.27 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/rogpeppe/go-internal v1.15.0 // indirect
github.com/rubenv/sql-migrate v1.8.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/sylabs/sif/v2 v2.22.0 // 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/spiffe/go-spiffe/v2 v2.8.1 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/ulikunitz/xz v0.5.16 // indirect
github.com/vladimirvivien/gexe v0.5.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/tools v0.43.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
go.opentelemetry.io/otel/metric v1.45.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.45.0 // indirect
go.opentelemetry.io/otel/trace v1.45.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260729162451-8efbd57d26e0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
k8s.io/component-base v0.35.3 // indirect
k8s.io/kubectl v0.35.1 // indirect
gotest.tools/v3 v3.5.2 // indirect
k8s.io/component-base v0.36.3 // indirect
k8s.io/kubectl v0.36.3 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 // 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.61.3 // indirect
cloud.google.com/go/iam v1.12.0 // indirect
cloud.google.com/go/storage v1.64.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/c9s/goprocinfo v0.0.0-20170724085704-0010a05ce49f // indirect
github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/containerd/containerd v1.7.30 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
github.com/containers/ocicrypt v1.2.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.5 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/docker/docker-credential-helpers v0.9.8 // indirect
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/felixge/httpsnoop v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/jsonpointer v1.0.0 // indirect
github.com/go-openapi/jsonreference v1.0.0 // indirect
github.com/go-openapi/swag v0.28.0 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-intervals v0.0.2 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/googleapis/gax-go/v2 v2.23.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-version v1.9.0
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/klauspost/compress v1.19.2 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-runewidth v0.0.27 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1
github.com/moby/spdystream v0.5.0 // indirect
github.com/moby/sys/mountinfo v0.7.2 // indirect
github.com/moby/spdystream v0.5.1 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect
github.com/nsf/termbox-go v1.1.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/runtime-spec v1.3.0
github.com/opencontainers/selinux v1.13.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tklauser/go-sysconf v0.3.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.4.0 // indirect
github.com/tklauser/numcpus v0.12.0 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.podman.io/storage v1.62.1-0.20260218215809-4bd29ff8b87e // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0
golang.org/x/crypto v0.55.0 // indirect
golang.org/x/net v0.58.0
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.43.0
golang.org/x/term v0.41.0 // indirect
golang.org/x/text v0.36.0
golang.org/x/sys v0.47.0
golang.org/x/term v0.45.0 // indirect
golang.org/x/text v0.41.0
golang.org/x/time v0.15.0 // indirect
google.golang.org/api v0.271.0 // indirect
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.11 // indirect
google.golang.org/api v0.292.0 // indirect
google.golang.org/genproto v0.0.0-20260729162451-8efbd57d26e0 // indirect
google.golang.org/grpc v1.83.0 // indirect
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
helm.sh/helm/v3 v3.20.1
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
k8s.io/kubelet v0.35.3
k8s.io/metrics v0.35.3
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
helm.sh/helm/v3 v3.21.4
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect
k8s.io/kubelet v0.36.3
k8s.io/metrics v0.36.3
k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3
periph.io/x/host/v3 v3.8.5
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/kustomize/api v0.20.1 // indirect
sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect
sigs.k8s.io/kustomize/api v0.21.1 // indirect
sigs.k8s.io/kustomize/kyaml v0.21.1 // indirect
sigs.k8s.io/yaml v1.6.0
)
+406 -388
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -238,6 +238,8 @@ func GetAnalyzer(analyzer *troubleshootv1beta2.Analyze) Analyzer {
return &AnalyzeMssql{analyzer: analyzer.Mssql}
case analyzer.Redis != nil:
return &AnalyzeRedis{analyzer: analyzer.Redis}
case analyzer.ClickHouse != nil:
return &AnalyzeClickhouse{analyzer: analyzer.ClickHouse}
case analyzer.CephStatus != nil:
return &AnalyzeCephStatus{analyzer: analyzer.CephStatus}
case analyzer.Velero != nil:
+193
View File
@@ -0,0 +1,193 @@
package analyzer
import (
"encoding/json"
"fmt"
"path"
"strconv"
"strings"
"github.com/hashicorp/go-version"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
)
type AnalyzeClickhouse struct {
analyzer *troubleshootv1beta2.DatabaseAnalyze
}
func (a *AnalyzeClickhouse) Title() string {
title := a.analyzer.CheckName
if title == "" {
title = a.collectorName()
}
return title
}
func (a *AnalyzeClickhouse) IsExcluded() (bool, error) {
return isExcluded(a.analyzer.Exclude)
}
func (a *AnalyzeClickhouse) Analyze(getFile getCollectedFileContents, findFiles getChildCollectedFileContents) ([]*AnalyzeResult, error) {
result, err := a.analyze(a.analyzer, getFile)
if err != nil {
return nil, err
}
result.Strict = a.analyzer.Strict.BoolOrDefaultFalse()
return []*AnalyzeResult{result}, nil
}
func (a *AnalyzeClickhouse) collectorName() string {
if a.analyzer.CollectorName != "" {
return a.analyzer.CollectorName
}
return "clickhouse"
}
func compareClickhouseConditionalToActual(conditional string, result *collect.DatabaseConnection) (bool, error) {
parts := strings.Split(strings.TrimSpace(conditional), " ")
if len(parts) != 3 {
return false, errors.New("unable to parse conditional")
}
switch parts[0] {
case "connected":
expected, err := strconv.ParseBool(parts[2])
if err != nil {
return false, errors.Wrap(err, "failed to parse bool")
}
switch parts[1] {
case "=", "==", "===":
return expected == result.IsConnected, nil
case "!=", "!==":
return expected != result.IsConnected, nil
}
return false, errors.New("unable to parse ClickHouse connected analyzer")
case "version":
expected, err := version.NewVersion(strings.ReplaceAll(parts[2], "x", "0"))
if err != nil {
return false, errors.Wrap(err, "failed to parse expected version")
}
operation := parts[1]
switch operation {
case "=", "==", "===":
operation = "="
case "!=", "!==":
operation = "!="
}
actual, err := version.NewVersion(strings.ReplaceAll(result.Version, "x", "0"))
if err != nil {
return false, errors.Wrap(err, "failed to parse ClickHouse db actual version")
}
constraints, err := version.NewConstraint(fmt.Sprintf("%s %s", operation, expected))
if err != nil {
return false, errors.Wrap(err, "failed to create constraint")
}
return constraints.Check(actual), nil
}
return false, nil
}
func (a *AnalyzeClickhouse) analyze(analyzer *troubleshootv1beta2.DatabaseAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
fullPath := path.Join("", fmt.Sprintf("clickhouse/%s.json", a.collectorName()))
collected, err := getCollectedFileContents(fullPath)
if err != nil {
return nil, errors.Wrapf(err, "failed to read collected file name: %s", fullPath)
}
databaseConnection := collect.DatabaseConnection{}
if err := json.Unmarshal(collected, &databaseConnection); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal database connection result")
}
result := &AnalyzeResult{
Title: a.Title(),
IconKey: "kubernetes_clickhouse_analyze",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/clickhouse-analyze.svg",
}
for _, outcome := range analyzer.Outcomes {
if outcome.Fail != nil {
if outcome.Fail.When == "" {
result.IsFail = true
result.Message = outcome.Fail.Message
result.URI = outcome.Fail.URI
return result, nil
}
isMatch, err := compareClickhouseConditionalToActual(outcome.Fail.When, &databaseConnection)
if err != nil {
return result, errors.Wrap(err, "failed to compare ClickHouse database conditional")
}
if isMatch {
if databaseConnection.Error != "" {
result.Message = outcome.Fail.Message + " " + databaseConnection.Error
} else {
result.Message = outcome.Fail.Message
}
result.IsFail = true
result.URI = outcome.Fail.URI
return result, nil
}
} else if outcome.Warn != nil {
if outcome.Warn.When == "" {
result.IsWarn = true
result.Message = outcome.Warn.Message
result.URI = outcome.Warn.URI
return result, nil
}
isMatch, err := compareClickhouseConditionalToActual(outcome.Warn.When, &databaseConnection)
if err != nil {
return result, errors.Wrap(err, "failed to compare ClickHouse database conditional")
}
if isMatch {
result.IsWarn = true
result.Message = outcome.Warn.Message
result.URI = outcome.Warn.URI
return result, nil
}
} else if outcome.Pass != nil {
if outcome.Pass.When == "" {
result.IsPass = true
result.Message = outcome.Pass.Message
result.URI = outcome.Pass.URI
return result, nil
}
isMatch, err := compareClickhouseConditionalToActual(outcome.Pass.When, &databaseConnection)
if err != nil {
return result, errors.Wrap(err, "failed to compare ClickHouse database conditional")
}
if isMatch {
result.IsPass = true
result.Message = outcome.Pass.Message
result.URI = outcome.Pass.URI
return result, nil
}
}
}
return result, nil
}
+4
View File
@@ -699,6 +699,10 @@ func (e *DefaultAnalysisEngine) convertAnalyzerToSpec(analyzer *troubleshootv1be
spec.Name = "redis"
spec.Type = "database"
spec.Config["analyzer"] = analyzer.Redis
case analyzer.ClickHouse != nil:
spec.Name = "clickhouse"
spec.Type = "database"
spec.Config["analyzer"] = analyzer.ClickHouse
// ✅ Storage analyzers
case analyzer.CephStatus != nil:
+2
View File
@@ -67,6 +67,8 @@ func GetHostAnalyzer(analyzer *troubleshootv1beta2.HostAnalyze) (HostAnalyzer, b
return &AnalyzeHostNetworkNamespaceConnectivity{analyzer.NetworkNamespaceConnectivity}, true
case analyzer.Sysctl != nil:
return &AnalyzeHostSysctl{analyzer.Sysctl}, true
case analyzer.RegistryImages != nil:
return &AnalyzeHostRegistryImages{analyzer.RegistryImages}, true
default:
return nil, false
}
+199
View File
@@ -0,0 +1,199 @@
package analyzer
import (
"encoding/json"
"fmt"
"slices"
"github.com/pkg/errors"
"github.com/replicatedhq/troubleshoot/internal/util"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
"k8s.io/klog/v2"
)
// RegistryImagesSummary is passed as template data when rendering outcome messages.
// Fields are exported so Go templates can reference them.
//
// - Verified: images confirmed to exist in the registry.
// - Missing: images confirmed not to exist in the registry.
// - Errors: images that could not be checked (parse failures, timeouts, auth errors, etc).
// - UnverifiedReasons: map of image name to reason string for every unverified image
// (union of Missing and Errors).
//
// The `when` conditions follow the existing registry images analyzer nomenclature:
// "verified", "missing", and "errors" (see https://troubleshoot.sh/docs/analyze/registry-images).
type RegistryImagesSummary struct {
Verified []string
Missing []string
Errors []string
UnverifiedReasons map[string]string
}
type AnalyzeHostRegistryImages struct {
hostAnalyzer *troubleshootv1beta2.HostRegistryImagesAnalyze
}
func (a *AnalyzeHostRegistryImages) Title() string {
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "Registry Images")
}
func (a *AnalyzeHostRegistryImages) IsExcluded() (bool, error) {
return isExcluded(a.hostAnalyzer.Exclude)
}
func (a *AnalyzeHostRegistryImages) Analyze(
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
) ([]*AnalyzeResult, error) {
collectorName := a.hostAnalyzer.CollectorName
if collectorName == "" {
collectorName = "images"
}
const nodeBaseDir = "host-collectors/registry-images"
localPath := fmt.Sprintf("%s/%s.json", nodeBaseDir, collectorName)
fileName := fmt.Sprintf("%s.json", collectorName)
collectedContents, err := retrieveCollectedContents(
getCollectedFileContents,
localPath,
nodeBaseDir,
fileName,
)
if err != nil {
return []*AnalyzeResult{{Title: a.Title()}}, err
}
var results []*AnalyzeResult
for _, content := range collectedContents {
currentTitle := a.Title()
if content.NodeName != "" {
currentTitle = fmt.Sprintf("%s - Node %s", a.Title(), content.NodeName)
}
result, err := a.evaluateOutcomesWithTemplate(content.Data, currentTitle)
if err != nil {
return nil, errors.Wrap(err, "failed to analyze host registry images")
}
if result != nil {
klog.V(2).Infof("registry images analysis result: title=%q pass=%t warn=%t fail=%t message=%q",
result.Title, result.IsPass, result.IsWarn, result.IsFail, result.Message)
results = append(results, result)
}
}
return results, nil
}
func (a *AnalyzeHostRegistryImages) evaluateOutcomesWithTemplate(data []byte, title string) (*AnalyzeResult, error) {
summary, err := buildRegistryImagesSummary(data)
if err != nil {
return nil, err
}
for _, outcome := range a.hostAnalyzer.Outcomes {
result := &AnalyzeResult{Title: title}
switch {
case outcome.Fail != nil:
if outcome.Fail.When == "" {
result.IsFail = true
result.Message = renderRegistryMessage(outcome.Fail.Message, summary)
result.URI = outcome.Fail.URI
return result, nil
}
isMatch, err := compareRegistryConditionalToActual(outcome.Fail.When, len(summary.Verified), len(summary.Missing), len(summary.Errors))
if err != nil {
return result, errors.Wrapf(err, "failed to compare %s", outcome.Fail.When)
}
if isMatch {
result.IsFail = true
result.Message = renderRegistryMessage(outcome.Fail.Message, summary)
result.URI = outcome.Fail.URI
return result, nil
}
case outcome.Warn != nil:
if outcome.Warn.When == "" {
result.IsWarn = true
result.Message = renderRegistryMessage(outcome.Warn.Message, summary)
result.URI = outcome.Warn.URI
return result, nil
}
isMatch, err := compareRegistryConditionalToActual(outcome.Warn.When, len(summary.Verified), len(summary.Missing), len(summary.Errors))
if err != nil {
return result, errors.Wrapf(err, "failed to compare %s", outcome.Warn.When)
}
if isMatch {
result.IsWarn = true
result.Message = renderRegistryMessage(outcome.Warn.Message, summary)
result.URI = outcome.Warn.URI
return result, nil
}
case outcome.Pass != nil:
if outcome.Pass.When == "" {
result.IsPass = true
result.Message = renderRegistryMessage(outcome.Pass.Message, summary)
result.URI = outcome.Pass.URI
return result, nil
}
isMatch, err := compareRegistryConditionalToActual(outcome.Pass.When, len(summary.Verified), len(summary.Missing), len(summary.Errors))
if err != nil {
return result, errors.Wrapf(err, "failed to compare %s", outcome.Pass.When)
}
if isMatch {
result.IsPass = true
result.Message = renderRegistryMessage(outcome.Pass.Message, summary)
result.URI = outcome.Pass.URI
return result, nil
}
}
}
return nil, nil
}
func buildRegistryImagesSummary(data []byte) (*RegistryImagesSummary, error) {
var registryInfo collect.RegistryInfo
if err := json.Unmarshal(data, &registryInfo); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal registry info")
}
summary := &RegistryImagesSummary{
UnverifiedReasons: map[string]string{},
}
for image, info := range registryInfo.Images {
if info.Error != "" {
summary.Errors = append(summary.Errors, image)
summary.UnverifiedReasons[image] = info.Error
} else if !info.Exists {
summary.Missing = append(summary.Missing, image)
summary.UnverifiedReasons[image] = "image not found in registry"
} else {
summary.Verified = append(summary.Verified, image)
}
}
slices.Sort(summary.Verified)
slices.Sort(summary.Missing)
slices.Sort(summary.Errors)
return summary, nil
}
func renderRegistryMessage(message string, summary *RegistryImagesSummary) string {
rendered, err := util.RenderTemplate(message, summary)
if err != nil {
klog.V(2).Infof("Failed to render registry message template: %v", err)
return message
}
return rendered
}
func (a *AnalyzeHostRegistryImages) CheckCondition(when string, data []byte) (bool, error) {
summary, err := buildRegistryImagesSummary(data)
if err != nil {
return false, err
}
return compareRegistryConditionalToActual(when, len(summary.Verified), len(summary.Missing), len(summary.Errors))
}
+412
View File
@@ -0,0 +1,412 @@
package analyzer
import (
"encoding/json"
"testing"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAnalyzeHostRegistryImagesCheckCondition(t *testing.T) {
tests := []struct {
name string
conditional string
data collect.RegistryInfo
expected bool
expectErr string
}{
{
name: "all images found",
conditional: "missing == 0",
data: collect.RegistryInfo{
Images: map[string]collect.RegistryImage{
"registry.example.com/app:v1": {Exists: true},
"registry.example.com/app:v2": {Exists: true},
},
},
expected: true,
},
{
name: "some images not found",
conditional: "missing > 0",
data: collect.RegistryInfo{
Images: map[string]collect.RegistryImage{
"registry.example.com/app:v1": {Exists: true},
"registry.example.com/app:v2": {Exists: false},
},
},
expected: true,
},
{
name: "verified count matches found",
conditional: "verified == 2",
data: collect.RegistryInfo{
Images: map[string]collect.RegistryImage{
"registry.example.com/app:v1": {Exists: true},
"registry.example.com/app:v2": {Exists: true},
},
},
expected: true,
},
{
name: "errored images counted under errors",
conditional: "errors > 0",
data: collect.RegistryInfo{
Images: map[string]collect.RegistryImage{
"registry.example.com/app:v1": {Error: "connection refused"},
},
},
expected: true,
},
{
name: "no errors when all found",
conditional: "missing == 0",
data: collect.RegistryInfo{
Images: map[string]collect.RegistryImage{
"registry.example.com/app:v1": {Exists: true},
},
},
expected: true,
},
{
name: "mixed results - missing and errors counted separately",
conditional: "missing == 1",
data: collect.RegistryInfo{
Images: map[string]collect.RegistryImage{
"registry.example.com/app:v1": {Exists: true},
"registry.example.com/app:v2": {Exists: false},
"registry.example.com/app:v3": {Error: "timeout"},
},
},
expected: true,
},
{
name: "invalid conditional format",
conditional: "missing",
data: collect.RegistryInfo{
Images: map[string]collect.RegistryImage{},
},
expected: false,
expectErr: "unable to parse conditional",
},
{
name: "unmarshal error",
conditional: "missing == 0",
expected: false,
expectErr: "failed to unmarshal registry info",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := require.New(t)
a := &AnalyzeHostRegistryImages{}
var data []byte
if test.expectErr == "failed to unmarshal registry info" {
data = []byte(`{not valid json}`)
} else {
var err error
data, err = json.Marshal(test.data)
req.NoError(err)
}
result, err := a.CheckCondition(test.conditional, data)
if test.expectErr != "" {
req.ErrorContains(err, test.expectErr)
} else {
req.NoError(err)
}
assert.Equal(t, test.expected, result)
})
}
}
func TestAnalyzeHostRegistryImages(t *testing.T) {
tests := []struct {
name string
hostAnalyzer *troubleshootv1beta2.HostRegistryImagesAnalyze
getCollectedFileContents func(string) ([]byte, error)
expectedResults []*AnalyzeResult
expectedError string
}{
{
name: "pass when all images found",
hostAnalyzer: &troubleshootv1beta2.HostRegistryImagesAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "missing == 0",
Message: "All images are available",
},
},
},
},
getCollectedFileContents: func(path string) ([]byte, error) {
if path == "host-collectors/registry-images/images.json" {
return json.Marshal(collect.RegistryInfo{
Images: map[string]collect.RegistryImage{
"registry.example.com/app:v1": {Exists: true},
},
})
}
return nil, errors.New("file not found")
},
expectedResults: []*AnalyzeResult{
{
Title: "Registry Images",
IsPass: true,
Message: "All images are available",
},
},
},
{
name: "fail when images not found",
hostAnalyzer: &troubleshootv1beta2.HostRegistryImagesAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "missing > 0",
Message: "Some images are not available",
},
},
},
},
getCollectedFileContents: func(path string) ([]byte, error) {
if path == "host-collectors/registry-images/images.json" {
return json.Marshal(collect.RegistryInfo{
Images: map[string]collect.RegistryImage{
"registry.example.com/app:v1": {Exists: false},
},
})
}
return nil, errors.New("file not found")
},
expectedResults: []*AnalyzeResult{
{
Title: "Registry Images",
IsFail: true,
Message: "Some images are not available",
},
},
},
{
name: "errored images matched by errors condition",
hostAnalyzer: &troubleshootv1beta2.HostRegistryImagesAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "errors > 0",
Message: "Some images are not available",
},
},
},
},
getCollectedFileContents: func(path string) ([]byte, error) {
if path == "host-collectors/registry-images/images.json" {
return json.Marshal(collect.RegistryInfo{
Images: map[string]collect.RegistryImage{
"registry.example.com/app:v1": {Error: "connection refused"},
},
})
}
return nil, errors.New("file not found")
},
expectedResults: []*AnalyzeResult{
{
Title: "Registry Images",
IsFail: true,
Message: "Some images are not available",
},
},
},
{
name: "custom collector name used in path",
hostAnalyzer: &troubleshootv1beta2.HostRegistryImagesAnalyze{
CollectorName: "my-registry",
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "missing == 0",
Message: "All images are available",
},
},
},
},
getCollectedFileContents: func(path string) ([]byte, error) {
if path == "host-collectors/registry-images/my-registry.json" {
return json.Marshal(collect.RegistryInfo{
Images: map[string]collect.RegistryImage{
"registry.example.com/app:v1": {Exists: true},
},
})
}
return nil, errors.New("file not found")
},
expectedResults: []*AnalyzeResult{
{
Title: "Registry Images",
IsPass: true,
Message: "All images are available",
},
},
},
{
name: "return error when collection data missing",
hostAnalyzer: &troubleshootv1beta2.HostRegistryImagesAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "missing == 0",
Message: "All images are available",
},
},
},
},
getCollectedFileContents: func(path string) ([]byte, error) {
return nil, errors.New("file not found")
},
expectedResults: []*AnalyzeResult{
{
Title: "Registry Images",
},
},
expectedError: "file not found",
},
{
name: "template rendering with NotFound list",
hostAnalyzer: &troubleshootv1beta2.HostRegistryImagesAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "missing > 0",
Message: "Missing: {{ .Missing | join \", \" }}",
},
},
},
},
getCollectedFileContents: func(path string) ([]byte, error) {
if path == "host-collectors/registry-images/images.json" {
return json.Marshal(collect.RegistryInfo{
Images: map[string]collect.RegistryImage{
"registry.example.com/app:v1": {Exists: false},
"registry.example.com/app:v2": {Exists: true},
},
})
}
return nil, errors.New("file not found")
},
expectedResults: []*AnalyzeResult{
{
Title: "Registry Images",
IsFail: true,
Message: "Missing: registry.example.com/app:v1",
},
},
},
{
name: "template rendering with NotFoundReasons map",
hostAnalyzer: &troubleshootv1beta2.HostRegistryImagesAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "errors > 0",
Message: `{{ range $image, $reason := .UnverifiedReasons }}{{ $image }}: {{ $reason }}; {{ end }}`,
},
},
},
},
getCollectedFileContents: func(path string) ([]byte, error) {
if path == "host-collectors/registry-images/images.json" {
return json.Marshal(collect.RegistryInfo{
Images: map[string]collect.RegistryImage{
"registry.example.com/app:v1": {Error: "connection refused"},
},
})
}
return nil, errors.New("file not found")
},
expectedResults: []*AnalyzeResult{
{
Title: "Registry Images",
IsFail: true,
Message: "registry.example.com/app:v1: connection refused; ",
},
},
},
{
name: "template rendering with Found count",
hostAnalyzer: &troubleshootv1beta2.HostRegistryImagesAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "missing == 0",
Message: "All {{ len .Verified }} images are available",
},
},
},
},
getCollectedFileContents: func(path string) ([]byte, error) {
if path == "host-collectors/registry-images/images.json" {
return json.Marshal(collect.RegistryInfo{
Images: map[string]collect.RegistryImage{
"registry.example.com/app:v1": {Exists: true},
"registry.example.com/app:v2": {Exists: true},
},
})
}
return nil, errors.New("file not found")
},
expectedResults: []*AnalyzeResult{
{
Title: "Registry Images",
IsPass: true,
Message: "All 2 images are available",
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := require.New(t)
a := &AnalyzeHostRegistryImages{
hostAnalyzer: test.hostAnalyzer,
}
results, err := a.Analyze(test.getCollectedFileContents, nil)
if test.expectedError != "" {
req.ErrorContains(err, test.expectedError)
} else {
req.NoError(err)
}
req.Equal(test.expectedResults, results)
})
}
}
func TestAnalyzeHostRegistryImagesTitle(t *testing.T) {
t.Run("default title", func(t *testing.T) {
a := &AnalyzeHostRegistryImages{
hostAnalyzer: &troubleshootv1beta2.HostRegistryImagesAnalyze{},
}
assert.Equal(t, "Registry Images", a.Title())
})
t.Run("custom title", func(t *testing.T) {
a := &AnalyzeHostRegistryImages{
hostAnalyzer: &troubleshootv1beta2.HostRegistryImagesAnalyze{
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
CheckName: "My Registry Check",
},
},
}
assert.Equal(t, "My Registry Check", a.Title())
})
}
+20 -18
View File
@@ -17,24 +17,26 @@ import (
)
var Filemap = map[string]string{
"deployment": constants.CLUSTER_RESOURCES_DEPLOYMENTS,
"daemonset": constants.CLUSTER_RESOURCES_DAEMONSETS,
"statefulset": constants.CLUSTER_RESOURCES_STATEFULSETS,
"networkpolicy": constants.CLUSTER_RESOURCES_NETWORK_POLICY,
"pod": constants.CLUSTER_RESOURCES_PODS,
"ingress": constants.CLUSTER_RESOURCES_INGRESS,
"service": constants.CLUSTER_RESOURCES_SERVICES,
"resourcequota": constants.CLUSTER_RESOURCES_RESOURCE_QUOTA,
"job": constants.CLUSTER_RESOURCES_JOBS,
"persistentvolumeclaim": constants.CLUSTER_RESOURCES_PVCS,
"pvc": constants.CLUSTER_RESOURCES_PVCS,
"replicaset": constants.CLUSTER_RESOURCES_REPLICASETS,
"configmap": constants.CLUSTER_RESOURCES_CONFIGMAPS,
"namespace": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_NAMESPACES),
"persistentvolume": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_PVS),
"pv": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_PVS),
"node": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_NODES),
"storageclass": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_STORAGE_CLASS),
"deployment": constants.CLUSTER_RESOURCES_DEPLOYMENTS,
"daemonset": constants.CLUSTER_RESOURCES_DAEMONSETS,
"statefulset": constants.CLUSTER_RESOURCES_STATEFULSETS,
"networkpolicy": constants.CLUSTER_RESOURCES_NETWORK_POLICY,
"pod": constants.CLUSTER_RESOURCES_PODS,
"ingress": constants.CLUSTER_RESOURCES_INGRESS,
"service": constants.CLUSTER_RESOURCES_SERVICES,
"resourcequota": constants.CLUSTER_RESOURCES_RESOURCE_QUOTA,
"job": constants.CLUSTER_RESOURCES_JOBS,
"persistentvolumeclaim": constants.CLUSTER_RESOURCES_PVCS,
"pvc": constants.CLUSTER_RESOURCES_PVCS,
"replicaset": constants.CLUSTER_RESOURCES_REPLICASETS,
"configmap": constants.CLUSTER_RESOURCES_CONFIGMAPS,
"validatingwebhookconfiguration": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_VALIDATING_WEBHOOK_CONFIGURATIONS),
"mutatingwebhookconfiguration": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_MUTATING_WEBHOOK_CONFIGURATIONS),
"namespace": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_NAMESPACES),
"persistentvolume": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_PVS),
"pv": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_PVS),
"node": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_NODES),
"storageclass": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_STORAGE_CLASS),
}
type AnalyzeClusterResource struct {
+32 -1
View File
@@ -17,6 +17,7 @@ import (
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/constants"
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
"github.com/replicatedhq/troubleshoot/pkg/types"
)
type AnalyzeNodeResources struct {
@@ -45,6 +46,9 @@ func (a *AnalyzeNodeResources) Analyze(getFile getCollectedFileContents, findFil
if err != nil {
return nil, err
}
if result == nil {
return nil, nil
}
result.Strict = a.analyzer.Strict.BoolOrDefaultFalse()
return []*AnalyzeResult{result}, nil
}
@@ -53,7 +57,19 @@ func (a *AnalyzeNodeResources) analyzeNodeResources(analyzer *troubleshootv1beta
collected, err := getCollectedFileContents(fmt.Sprintf("%s/%s.json", constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_NODES))
if err != nil {
return nil, errors.Wrap(err, "failed to get contents of nodes.json")
if _, ok := err.(*types.NotFoundError); !ok {
return nil, errors.Wrap(err, "failed to get contents of nodes.json")
}
if analyzer.IgnoreIfNoFiles {
return nil, nil
}
return &AnalyzeResult{
Title: a.Title(),
IconKey: "kubernetes_node_resources",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
IsWarn: true,
Message: "No node resources were collected, unable to analyze node resources",
}, nil
}
var nodes corev1.NodeList
@@ -203,6 +219,8 @@ func compareNodeResourceConditionalToActual(conditional string, matchingNodes []
switch function {
case "count":
actualValue = len(matchingNodes)
case "countDistinct":
actualValue = countDistinctLabelValues(matchingNodes, property)
case "min":
actualValue = findMin(matchingNodes, property, resourceName)
case "max":
@@ -352,6 +370,19 @@ func getQuantity(node corev1.Node, property string, resourceName string) *resour
return nil
}
// countDistinctLabelValues returns the number of distinct values of labelKey
// across the given nodes. Nodes missing the label are ignored, so an absent
// label yields 0.
func countDistinctLabelValues(nodes []corev1.Node, labelKey string) int {
seen := map[string]struct{}{}
for _, node := range nodes {
if v, ok := node.Labels[labelKey]; ok {
seen[v] = struct{}{}
}
}
return len(seen)
}
func findSum(nodes []corev1.Node, property string, resourceName string) *resource.Quantity {
sum := resource.Quantity{}
+165
View File
@@ -1,6 +1,7 @@
package analyzer
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
@@ -10,6 +11,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/types"
)
func Test_compareNodeResourceConditionalToActual(t *testing.T) {
@@ -1664,6 +1666,116 @@ func Test_analyzeNodeResources(t *testing.T) {
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
},
},
{
name: "countDistinct spans at least 3 instance types", // countDistinct pass path across all nodes
analyzer: &troubleshootv1beta2.NodeResources{
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
CheckName: "instance-type spread",
},
Outcomes: []*troubleshootv1beta2.Outcome{
{
Warn: &troubleshootv1beta2.SingleOutcome{
When: "countDistinct(node.kubernetes.io/instance-type) < 3",
Message: "Fewer than 3 distinct instance types.",
URI: "",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "At least 3 distinct instance types.",
URI: "",
},
},
},
},
want: &AnalyzeResult{
IsPass: true,
IsFail: false,
IsWarn: false,
Title: "instance-type spread",
Message: "At least 3 distinct instance types.",
URI: "",
IconKey: "kubernetes_node_resources",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
},
},
{
name: "countDistinct only counts filtered nodes", // filtering to one pool leaves a single distinct value
analyzer: &troubleshootv1beta2.NodeResources{
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
CheckName: "filtered instance-type spread",
},
Outcomes: []*troubleshootv1beta2.Outcome{
{
Warn: &troubleshootv1beta2.SingleOutcome{
When: "countDistinct(node.kubernetes.io/instance-type) < 3",
Message: "Fewer than 3 distinct instance types.",
URI: "",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "At least 3 distinct instance types.",
URI: "",
},
},
},
Filters: &troubleshootv1beta2.NodeResourceFilters{
Selector: &troubleshootv1beta2.NodeResourceSelectors{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "node.kubernetes.io/instance-type",
Operator: metav1.LabelSelectorOpIn,
Values: []string{"s-2vcpu-4gb"},
},
},
},
},
},
want: &AnalyzeResult{
IsPass: false,
IsFail: false,
IsWarn: true,
Title: "filtered instance-type spread",
Message: "Fewer than 3 distinct instance types.",
URI: "",
IconKey: "kubernetes_node_resources",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
},
},
{
name: "countDistinct is 0 when the label is absent", // AIR-238 zone syntax; fixture nodes carry no zone label
analyzer: &troubleshootv1beta2.NodeResources{
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
CheckName: "zone spread",
},
Outcomes: []*troubleshootv1beta2.Outcome{
{
Warn: &troubleshootv1beta2.SingleOutcome{
When: "countDistinct(topology.kubernetes.io/zone) < 3",
Message: "Nodes span fewer than 3 availability zones.",
URI: "",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "Nodes span at least 3 availability zones.",
URI: "",
},
},
},
},
want: &AnalyzeResult{
IsPass: false,
IsFail: false,
IsWarn: true,
Title: "zone spread",
Message: "Nodes span fewer than 3 availability zones.",
URI: "",
IconKey: "kubernetes_node_resources",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
},
},
}
getExampleNodeContents := func(nodeName string) ([]byte, error) {
@@ -1684,3 +1796,56 @@ func Test_analyzeNodeResources(t *testing.T) {
})
}
}
func Test_analyzeNodeResources_NoFiles(t *testing.T) {
missingFile := func(name string) ([]byte, error) {
return nil, &types.NotFoundError{Name: name}
}
t.Run("emits warning when nodes.json is not collected", func(t *testing.T) {
req := require.New(t)
analyzer := &troubleshootv1beta2.NodeResources{
Outcomes: []*troubleshootv1beta2.Outcome{
{Pass: &troubleshootv1beta2.SingleOutcome{Message: "ok"}},
},
}
a := AnalyzeNodeResources{analyzer: analyzer}
got, err := a.Analyze(missingFile, nil)
req.NoError(err)
req.Len(got, 1)
req.True(got[0].IsWarn)
req.Equal("Node Resources", got[0].Title)
})
t.Run("ignoreIfNoFiles suppresses the warning", func(t *testing.T) {
req := require.New(t)
analyzer := &troubleshootv1beta2.NodeResources{
IgnoreIfNoFiles: true,
Outcomes: []*troubleshootv1beta2.Outcome{
{Pass: &troubleshootv1beta2.SingleOutcome{Message: "ok"}},
},
}
a := AnalyzeNodeResources{analyzer: analyzer}
got, err := a.Analyze(missingFile, nil)
req.NoError(err)
req.Empty(got)
})
t.Run("non-NotFound errors are propagated", func(t *testing.T) {
req := require.New(t)
analyzer := &troubleshootv1beta2.NodeResources{
IgnoreIfNoFiles: true,
Outcomes: []*troubleshootv1beta2.Outcome{
{Pass: &troubleshootv1beta2.SingleOutcome{Message: "ok"}},
},
}
ioErr := func(string) ([]byte, error) {
return nil, fmt.Errorf("permission denied")
}
a := AnalyzeNodeResources{analyzer: analyzer}
got, err := a.Analyze(ioErr, nil)
req.Error(err)
req.Nil(got)
req.Contains(err.Error(), "permission denied")
})
}
+54 -25
View File
@@ -30,6 +30,9 @@ func (a *AnalyzeSecret) Analyze(getFile getCollectedFileContents, findFiles getC
if err != nil {
return nil, err
}
if result == nil {
return nil, nil
}
result.Strict = a.analyzer.Strict.BoolOrDefaultFalse()
return []*AnalyzeResult{result}, nil
}
@@ -54,42 +57,68 @@ func (a *AnalyzeSecret) analyzeSecret(analyzer *troubleshootv1beta2.AnalyzeSecre
return nil, err
}
// The secret analyzer only supports fail (not found) and pass (found) outcomes
// per https://troubleshoot.sh/docs/analyze/secrets. If the spec contains
// neither, return an explicit error: returning (nil, nil) is swallowed by the
// Analyze wrapper into an empty result slice, so the misconfiguration would
// surface as neither a result nor an error.
// Capture fail and pass independently: a single outcome object may set both,
// so an else-if here would silently drop the second one.
var failOutcome, passOutcome *troubleshootv1beta2.SingleOutcome
for _, outcome := range analyzer.Outcomes {
if outcome.Fail != nil {
failOutcome = outcome.Fail
}
if outcome.Pass != nil {
passOutcome = outcome.Pass
}
}
if failOutcome == nil && passOutcome == nil {
return nil, fmt.Errorf("secret analyzer %s/%s must define at least one pass or fail outcome", analyzer.Namespace, analyzer.SecretName)
}
result := AnalyzeResult{
Title: a.Title(),
IconKey: "kubernetes_analyze_secret",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/secret.svg?w=13&h=16",
}
var failOutcome *troubleshootv1beta2.Outcome
for _, outcome := range analyzer.Outcomes {
if outcome.Fail != nil {
failOutcome = outcome
}
secretFound := foundSecret.SecretExists
if secretFound && analyzer.Key != "" {
secretFound = foundSecret.Key == analyzer.Key && foundSecret.KeyExists
}
if !foundSecret.SecretExists {
// Use the matched branch's configured outcome verbatim, tracking whether one
// was actually present. A configured outcome with an intentionally empty
// message (e.g. a URI-only outcome) is preserved as-is. But when the matched
// branch has NO configured outcome at all — e.g. a pass-only spec that took
// the fail path, or a fail-only spec that passed — the empty message is not
// an intentional choice, so fall back to a default diagnostic. An absent
// outcome is not the same as an intentionally empty one.
outcomeConfigured := false
if secretFound {
result.IsPass = true
if passOutcome != nil {
result.Message = passOutcome.Message
result.URI = passOutcome.URI
outcomeConfigured = true
}
} else {
result.IsFail = true
result.Message = failOutcome.Fail.Message
result.URI = failOutcome.Fail.URI
return &result, nil
}
if analyzer.Key != "" {
if foundSecret.Key != analyzer.Key || !foundSecret.KeyExists {
result.IsFail = true
result.Message = failOutcome.Fail.Message
result.URI = failOutcome.Fail.URI
return &result, nil
if failOutcome != nil {
result.Message = failOutcome.Message
result.URI = failOutcome.URI
outcomeConfigured = true
}
}
result.IsPass = true
for _, outcome := range analyzer.Outcomes {
if outcome.Pass != nil {
result.Message = outcome.Pass.Message
result.URI = outcome.Pass.URI
if !outcomeConfigured {
switch {
case result.IsPass:
result.Message = fmt.Sprintf("Secret %s was found in namespace %s", analyzer.SecretName, analyzer.Namespace)
case analyzer.Key != "" && foundSecret.SecretExists:
result.Message = fmt.Sprintf("Key %s was not found in secret %s/%s", analyzer.Key, analyzer.Namespace, analyzer.SecretName)
default:
result.Message = fmt.Sprintf("Secret %s was not found in namespace %s", analyzer.SecretName, analyzer.Namespace)
}
}
+234
View File
@@ -166,6 +166,118 @@ func Test_analyzeSecret(t *testing.T) {
IconURI: "https://troubleshoot.sh/images/analyzer-icons/secret.svg?w=13&h=16",
},
},
{
name: "not found with no fail outcome falls back to a default message (no configured outcome for this branch)",
analyzer: &troubleshootv1beta2.AnalyzeSecret{
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
CheckName: "Optional Secret",
},
Namespace: "default",
SecretName: "does-not-exist",
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "secret found",
},
},
},
},
mockFiles: map[string][]byte{
"secrets/default/does-not-exist.json": mustJSONMarshalIndent(t, collect.SecretOutput{
Namespace: "default",
Name: "does-not-exist",
SecretExists: false,
}),
},
want: &AnalyzeResult{
IsFail: true,
Message: "Secret does-not-exist was not found in namespace default",
Title: "Optional Secret",
IconKey: "kubernetes_analyze_secret",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/secret.svg?w=13&h=16",
},
},
{
name: "key not found with no fail outcome falls back to a default message (no configured outcome for this branch)",
analyzer: &troubleshootv1beta2.AnalyzeSecret{
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
CheckName: "Optional Secret Key",
},
Namespace: "test-namespace",
SecretName: "test-secret",
Key: "missing-key",
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "key found",
},
},
},
},
mockFiles: map[string][]byte{
"secrets/test-namespace/test-secret/missing-key.json": mustJSONMarshalIndent(t, collect.SecretOutput{
Namespace: "test-namespace",
Name: "test-secret",
Key: "missing-key",
SecretExists: true,
KeyExists: false,
}),
},
want: &AnalyzeResult{
IsFail: true,
Message: "Key missing-key was not found in secret test-namespace/test-secret",
Title: "Optional Secret Key",
IconKey: "kubernetes_analyze_secret",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/secret.svg?w=13&h=16",
},
},
{
name: "found with only fail outcome configured falls back to the default pass message, not the fail outcome's message",
analyzer: &troubleshootv1beta2.AnalyzeSecret{
Namespace: "test-namespace",
SecretName: "test-secret",
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "Not found",
},
},
},
},
mockFiles: map[string][]byte{
"secrets/test-namespace/test-secret.json": mustJSONMarshalIndent(t, collect.SecretOutput{
Namespace: "test-namespace",
Name: "test-secret",
SecretExists: true,
}),
},
want: &AnalyzeResult{
IsPass: true,
Message: "Secret test-secret was found in namespace test-namespace",
Title: "Secret test-secret",
IconKey: "kubernetes_analyze_secret",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/secret.svg?w=13&h=16",
},
},
{
name: "spec with neither fail nor pass outcome returns an error so the framework surfaces the misconfiguration",
analyzer: &troubleshootv1beta2.AnalyzeSecret{
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
CheckName: "Misconfigured",
},
Namespace: "default",
SecretName: "does-not-exist",
Outcomes: []*troubleshootv1beta2.Outcome{},
},
mockFiles: map[string][]byte{
"secrets/default/does-not-exist.json": mustJSONMarshalIndent(t, collect.SecretOutput{
Namespace: "default",
Name: "does-not-exist",
SecretExists: false,
}),
},
wantErr: true,
},
{
name: "key not found secret not found",
analyzer: &troubleshootv1beta2.AnalyzeSecret{
@@ -190,6 +302,128 @@ func Test_analyzeSecret(t *testing.T) {
},
wantErr: true, // TODO: should this be a not found error? This will not work with selectors.
},
{
name: "combined fail and pass in a single outcome, secret found uses the pass outcome",
analyzer: &troubleshootv1beta2.AnalyzeSecret{
Namespace: "test-namespace",
SecretName: "test-secret",
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "Not found",
},
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "Found",
URI: "https://example.com/found",
},
},
},
},
mockFiles: map[string][]byte{
"secrets/test-namespace/test-secret.json": mustJSONMarshalIndent(t, collect.SecretOutput{
Namespace: "test-namespace",
Name: "test-secret",
SecretExists: true,
}),
},
want: &AnalyzeResult{
IsPass: true,
Message: "Found",
URI: "https://example.com/found",
Title: "Secret test-secret",
IconKey: "kubernetes_analyze_secret",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/secret.svg?w=13&h=16",
},
},
{
name: "combined fail and pass in a single outcome, secret not found uses the fail outcome",
analyzer: &troubleshootv1beta2.AnalyzeSecret{
Namespace: "test-namespace",
SecretName: "test-secret",
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "Not found",
},
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "Found",
},
},
},
},
mockFiles: map[string][]byte{
"secrets/test-namespace/test-secret.json": mustJSONMarshalIndent(t, collect.SecretOutput{
Namespace: "test-namespace",
Name: "test-secret",
SecretExists: false,
}),
},
want: &AnalyzeResult{
IsFail: true,
Message: "Not found",
Title: "Secret test-secret",
IconKey: "kubernetes_analyze_secret",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/secret.svg?w=13&h=16",
},
},
{
name: "secret found with URI-only pass outcome preserves the empty message and URI",
analyzer: &troubleshootv1beta2.AnalyzeSecret{
Namespace: "test-namespace",
SecretName: "test-secret",
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
URI: "https://example.com/pass",
},
},
},
},
mockFiles: map[string][]byte{
"secrets/test-namespace/test-secret.json": mustJSONMarshalIndent(t, collect.SecretOutput{
Namespace: "test-namespace",
Name: "test-secret",
SecretExists: true,
}),
},
want: &AnalyzeResult{
IsPass: true,
Message: "",
URI: "https://example.com/pass",
Title: "Secret test-secret",
IconKey: "kubernetes_analyze_secret",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/secret.svg?w=13&h=16",
},
},
{
name: "secret not found with URI-only fail outcome preserves the empty message and URI",
analyzer: &troubleshootv1beta2.AnalyzeSecret{
Namespace: "test-namespace",
SecretName: "test-secret",
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
URI: "https://example.com/fail",
},
},
},
},
mockFiles: map[string][]byte{
"secrets/test-namespace/test-secret.json": mustJSONMarshalIndent(t, collect.SecretOutput{
Namespace: "test-namespace",
Name: "test-secret",
SecretExists: false,
}),
},
want: &AnalyzeResult{
IsFail: true,
Message: "",
URI: "https://example.com/fail",
Title: "Secret test-secret",
IconKey: "kubernetes_analyze_secret",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/secret.svg?w=13&h=16",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -261,7 +261,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
result = append(result, authorizationv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationv1.ResourceAttributes{
Namespace: pickNamespaceOrDefault(c.Exec.Namespace, overrideNS),
Verb: "get",
Verb: "create",
Group: "",
Version: "",
Resource: "pods",
@@ -286,7 +286,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
result = append(result, authorizationv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationv1.ResourceAttributes{
Namespace: pickNamespaceOrDefault(c.Copy.Namespace, overrideNS),
Verb: "get",
Verb: "create",
Group: "",
Version: "",
Resource: "pods",
@@ -130,9 +130,10 @@ type Distribution struct {
}
type NodeResources struct {
AnalyzeMeta `json:",inline" yaml:",inline"`
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
Filters *NodeResourceFilters `json:"filters,omitempty" yaml:"filters,omitempty"`
AnalyzeMeta `json:",inline" yaml:",inline"`
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
Filters *NodeResourceFilters `json:"filters,omitempty" yaml:"filters,omitempty"`
IgnoreIfNoFiles bool `json:"ignoreIfNoFiles,omitempty" yaml:"ignoreIfNoFiles,omitempty"`
}
type NodeResourceFilters struct {
@@ -304,6 +305,7 @@ type Analyze struct {
Mssql *DatabaseAnalyze `json:"mssql,omitempty" yaml:"mssql,omitempty"`
Mysql *DatabaseAnalyze `json:"mysql,omitempty" yaml:"mysql,omitempty"`
Redis *DatabaseAnalyze `json:"redis,omitempty" yaml:"redis,omitempty"`
ClickHouse *DatabaseAnalyze `json:"clickhouse,omitempty" yaml:"clickhouse,omitempty"`
CephStatus *CephStatusAnalyze `json:"cephStatus,omitempty" yaml:"cephStatus,omitempty"`
Velero *VeleroAnalyze `json:"velero,omitempty" yaml:"velero,omitempty"`
Longhorn *LonghornAnalyze `json:"longhorn,omitempty" yaml:"longhorn,omitempty"`
@@ -353,6 +353,7 @@ type Collect struct {
Mssql *Database `json:"mssql,omitempty" yaml:"mssql,omitempty"`
Mysql *Database `json:"mysql,omitempty" yaml:"mysql,omitempty"`
Redis *Database `json:"redis,omitempty" yaml:"redis,omitempty"`
ClickHouse *Database `json:"clickhouse,omitempty" yaml:"clickhouse,omitempty"`
Collectd *Collectd `json:"collectd,omitempty" yaml:"collectd,omitempty"`
Ceph *Ceph `json:"ceph,omitempty" yaml:"ceph,omitempty"`
Longhorn *Longhorn `json:"longhorn,omitempty" yaml:"longhorn,omitempty"`
@@ -529,7 +530,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
result = append(result, authorizationv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationv1.ResourceAttributes{
Namespace: pickNamespaceOrDefault(c.Exec.Namespace, overrideNS),
Verb: "get",
Verb: "create",
Group: "",
Version: "",
Resource: "pods",
@@ -554,7 +555,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
result = append(result, authorizationv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationv1.ResourceAttributes{
Namespace: pickNamespaceOrDefault(c.Copy.Namespace, overrideNS),
Verb: "get",
Verb: "create",
Group: "",
Version: "",
Resource: "pods",
@@ -680,6 +681,10 @@ func (c *Collect) GetName() string {
collector = "redis"
name = c.Redis.CollectorName
}
if c.ClickHouse != nil {
collector = "clickhouse"
name = c.ClickHouse.CollectorName
}
if c.Collectd != nil {
collector = "collectd"
name = c.Collectd.CollectorName
@@ -153,6 +153,12 @@ type HostSysctlAnalyze struct {
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
}
type HostRegistryImagesAnalyze struct {
AnalyzeMeta `json:",inline" yaml:",inline"`
CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
}
type HostAnalyze struct {
CPU *CPUAnalyze `json:"cpu,omitempty" yaml:"cpu,omitempty"`
TCPLoadBalancer *TCPLoadBalancerAnalyze `json:"tcpLoadBalancer,omitempty" yaml:"tcpLoadBalancer,omitempty"`
@@ -180,4 +186,5 @@ type HostAnalyze struct {
JsonCompare *JsonCompare `json:"jsonCompare,omitempty" yaml:"jsonCompare,omitempty"`
NetworkNamespaceConnectivity *NetworkNamespaceConnectivityAnalyze `json:"networkNamespaceConnectivity,omitempty" yaml:"networkNamespaceConnectivity,omitempty"`
Sysctl *HostSysctlAnalyze `json:"sysctl,omitempty" yaml:"sysctl,omitempty"`
RegistryImages *HostRegistryImagesAnalyze `json:"registryImages,omitempty" yaml:"registryImages,omitempty"`
}
@@ -235,6 +235,16 @@ type HostSysctl struct {
HostCollectorMeta `json:",inline" yaml:",inline"`
}
// HostRegistryImages checks whether images are accessible from the host,
// without requiring a Kubernetes cluster. Auth can be supplied inline via
// Username/Password or omitted to rely on ambient credentials (e.g. ~/.docker/config.json).
type HostRegistryImages struct {
HostCollectorMeta `json:",inline" yaml:",inline"`
Images []string `json:"images" yaml:"images"`
Username string `json:"username,omitempty" yaml:"username,omitempty"`
Password string `json:"password,omitempty" yaml:"password,omitempty"`
}
type HostCollect struct {
CPU *CPU `json:"cpu,omitempty" yaml:"cpu,omitempty"`
Memory *Memory `json:"memory,omitempty" yaml:"memory,omitempty"`
@@ -265,6 +275,7 @@ type HostCollect struct {
HostDNS *HostDNS `json:"dns,omitempty" yaml:"dns,omitempty"`
NetworkNamespaceConnectivity *HostNetworkNamespaceConnectivity `json:"networkNamespaceConnectivity,omitempty" yaml:"networkNamespaceConnectivity,omitempty"`
HostSysctl *HostSysctl `json:"sysctl,omitempty" yaml:"sysctl,omitempty"`
RegistryImages *HostRegistryImages `json:"registryImages,omitempty" yaml:"registryImages,omitempty"`
}
// GetName gets the name of the collector
@@ -175,6 +175,11 @@ func (in *Analyze) DeepCopyInto(out *Analyze) {
*out = new(DatabaseAnalyze)
(*in).DeepCopyInto(*out)
}
if in.ClickHouse != nil {
in, out := &in.ClickHouse, &out.ClickHouse
*out = new(DatabaseAnalyze)
(*in).DeepCopyInto(*out)
}
if in.CephStatus != nil {
in, out := &in.CephStatus, &out.CephStatus
*out = new(CephStatusAnalyze)
@@ -935,6 +940,11 @@ func (in *Collect) DeepCopyInto(out *Collect) {
*out = new(Database)
(*in).DeepCopyInto(*out)
}
if in.ClickHouse != nil {
in, out := &in.ClickHouse, &out.ClickHouse
*out = new(Database)
(*in).DeepCopyInto(*out)
}
if in.Collectd != nil {
in, out := &in.Collectd, &out.Collectd
*out = new(Collectd)
@@ -1999,6 +2009,11 @@ func (in *HostAnalyze) DeepCopyInto(out *HostAnalyze) {
*out = new(HostSysctlAnalyze)
(*in).DeepCopyInto(*out)
}
if in.RegistryImages != nil {
in, out := &in.RegistryImages, &out.RegistryImages
*out = new(HostRegistryImagesAnalyze)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostAnalyze.
@@ -2239,6 +2254,11 @@ func (in *HostCollect) DeepCopyInto(out *HostCollect) {
*out = new(HostSysctl)
(*in).DeepCopyInto(*out)
}
if in.RegistryImages != nil {
in, out := &in.RegistryImages, &out.RegistryImages
*out = new(HostRegistryImages)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostCollect.
@@ -2684,6 +2704,54 @@ func (in *HostPreflightStatus) DeepCopy() *HostPreflightStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HostRegistryImages) DeepCopyInto(out *HostRegistryImages) {
*out = *in
in.HostCollectorMeta.DeepCopyInto(&out.HostCollectorMeta)
if in.Images != nil {
in, out := &in.Images, &out.Images
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostRegistryImages.
func (in *HostRegistryImages) DeepCopy() *HostRegistryImages {
if in == nil {
return nil
}
out := new(HostRegistryImages)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HostRegistryImagesAnalyze) DeepCopyInto(out *HostRegistryImagesAnalyze) {
*out = *in
in.AnalyzeMeta.DeepCopyInto(&out.AnalyzeMeta)
if in.Outcomes != nil {
in, out := &in.Outcomes, &out.Outcomes
*out = make([]*Outcome, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(Outcome)
(*in).DeepCopyInto(*out)
}
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostRegistryImagesAnalyze.
func (in *HostRegistryImagesAnalyze) DeepCopy() *HostRegistryImagesAnalyze {
if in == nil {
return nil
}
out := new(HostRegistryImagesAnalyze)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HostRun) DeepCopyInto(out *HostRun) {
*out = *in
@@ -68,10 +68,11 @@ type HostCollect struct {
// For phase 1, we're focusing on Database collectors with StringOrValueFrom support
type Collect struct {
// Database collectors with v1beta3 StringOrValueFrom support
Postgres *Database `json:"postgres,omitempty" yaml:"postgres,omitempty"`
Mssql *Database `json:"mssql,omitempty" yaml:"mssql,omitempty"`
Mysql *Database `json:"mysql,omitempty" yaml:"mysql,omitempty"`
Redis *Database `json:"redis,omitempty" yaml:"redis,omitempty"`
Postgres *Database `json:"postgres,omitempty" yaml:"postgres,omitempty"`
Mssql *Database `json:"mssql,omitempty" yaml:"mssql,omitempty"`
Mysql *Database `json:"mysql,omitempty" yaml:"mysql,omitempty"`
Redis *Database `json:"redis,omitempty" yaml:"redis,omitempty"`
ClickHouse *Database `json:"clickhouse,omitempty" yaml:"clickhouse,omitempty"`
// TODO: Add remaining collector types as we expand v1beta3 support
// For now, these are placeholders to make the types compile
@@ -81,6 +81,14 @@ func convertCollector(
v2collector.Redis = db
}
if v3collector.ClickHouse != nil {
db, err := convertDatabase(ctx, v3collector.ClickHouse, client, defaultNamespace)
if err != nil {
return nil, fmt.Errorf("failed to convert clickhouse collector: %w", err)
}
v2collector.ClickHouse = db
}
// TODO: Add conversion for other collector types as v1beta3 support expands
return v2collector, nil
@@ -171,7 +171,17 @@ func TestConvertToV1Beta2WithResolution_MultipleDatabases(t *testing.T) {
},
}
client := fake.NewSimpleClientset(pgSecret, mysqlSecret, redisSecret)
clickhouseSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "clickhouse-secret",
Namespace: "default",
},
Data: map[string][]byte{
"uri": []byte("http://clickhouse.example.com:9000"),
},
}
client := fake.NewSimpleClientset(pgSecret, mysqlSecret, redisSecret, clickhouseSecret)
v3spec := &SupportBundleSpec{
Collectors: []*Collect{
@@ -211,13 +221,25 @@ func TestConvertToV1Beta2WithResolution_MultipleDatabases(t *testing.T) {
},
},
},
{
ClickHouse: &Database{
URI: StringOrValueFrom{
ValueFrom: &ValueFromSource{
SecretKeyRef: &SecretKeyRef{
Name: "clickhouse-secret",
Key: "uri",
},
},
},
},
},
},
}
v2spec, err := ConvertToV1Beta2WithResolution(context.Background(), v3spec, client, "default")
require.NoError(t, err)
require.NotNil(t, v2spec)
require.Len(t, v2spec.Collectors, 3)
require.Len(t, v2spec.Collectors, 4)
require.NotNil(t, v2spec.Collectors[0].Postgres)
assert.Equal(t, "postgresql://user:pass@pg.example.com:5432/db", v2spec.Collectors[0].Postgres.URI)
@@ -227,6 +249,8 @@ func TestConvertToV1Beta2WithResolution_MultipleDatabases(t *testing.T) {
require.NotNil(t, v2spec.Collectors[2].Redis)
assert.Equal(t, "redis://redis.example.com:6379", v2spec.Collectors[2].Redis.URI)
require.NotNil(t, v2spec.Collectors[3].ClickHouse)
assert.Equal(t, "http://clickhouse.example.com:9000", v2spec.Collectors[3].ClickHouse.URI)
}
func TestConvertToV1Beta2WithResolution_SecretNotFound(t *testing.T) {
@@ -79,6 +79,11 @@ func (in *Collect) DeepCopyInto(out *Collect) {
*out = new(Database)
(*in).DeepCopyInto(*out)
}
if in.ClickHouse != nil {
in, out := &in.ClickHouse, &out.ClickHouse
*out = new(Database)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Collect.
+109
View File
@@ -0,0 +1,109 @@
package collect
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
var _ Collector = &CollectClickhouse{}
type CollectClickhouse struct {
Collector *troubleshootv1beta2.Database
BundlePath string
Namespace string
ClientConfig *rest.Config
Client kubernetes.Interface
Context context.Context
RBACErrors
}
func (c *CollectClickhouse) Title() string {
return getCollectorName(c)
}
func (c *CollectClickhouse) IsExcluded() (bool, error) {
return isExcluded(c.Collector.Exclude)
}
func (c *CollectClickhouse) createConnectConfig() (*clickhouse.Options, error) {
if c.Collector.URI == "" {
return nil, errors.New("clickhouse uri cannot be empty")
}
opts, err := clickhouse.ParseDSN(c.Collector.URI)
if err != nil {
return nil, errors.Wrap(err, "couldn't parse clickhouse URI")
}
if c.Collector.TLS != nil {
tlsCfg, err := createTLSConfig(c.Context, c.Client, c.Collector.TLS)
if err != nil {
return nil, err
}
opts.TLS = tlsCfg
}
return opts, nil
}
func (c *CollectClickhouse) connect() (driver.Conn, error) {
config, err := c.createConnectConfig()
if err != nil {
return nil, errors.Wrap(err, "failed to create clickhouse connection config")
}
conn, err := clickhouse.Open(config)
if err != nil {
return nil, errors.Wrap(err, "failed to open clickhouse connection")
}
return conn, nil
}
func (c *CollectClickhouse) Collect(progressChan chan<- interface{}) (CollectorResult, error) {
databaseConnection := DatabaseConnection{}
conn, err := c.connect()
if err != nil {
databaseConnection.Error = err.Error()
} else {
defer conn.Close()
if err := conn.Ping(c.Context); err != nil {
databaseConnection.Error = err.Error()
} else {
var version string
err := conn.QueryRow(c.Context, "SELECT version()").Scan(&version)
if err != nil {
databaseConnection.Error = err.Error()
} else {
databaseConnection.Version = version
databaseConnection.IsConnected = true
}
}
}
b, err := json.Marshal(databaseConnection)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal database connection")
}
collectorName := c.Collector.CollectorName
if collectorName == "" {
collectorName = "clickhouse"
}
output := NewResult()
output.SaveResult(c.BundlePath, fmt.Sprintf("clickhouse/%s.json", collectorName), bytes.NewBuffer(b))
return output, nil
}
+77 -11
View File
@@ -294,7 +294,18 @@ func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (Coll
// crs
customResources, crErrors := crs(ctx, dynamicClient, client, c.ClientConfig, namespaceNames)
for k, v := range customResources {
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_CUSTOM_RESOURCES, k), bytes.NewBuffer(v))
jsonPath := path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_CUSTOM_RESOURCES, k)
output.SaveResult(c.BundlePath, jsonPath, bytes.NewBuffer(v))
// Keep a YAML symlink for backward compatibility. It points at the JSON
// file, so any analyzer that expects YAML can still read it, and the
// content is redacted through the JSON copy.
if strings.HasSuffix(k, ".json") {
yamlPath := path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_CUSTOM_RESOURCES, strings.TrimSuffix(k, ".json")+".yaml")
if err := output.SymLinkResult(c.BundlePath, yamlPath, jsonPath); err != nil {
klog.V(2).Infof("failed to create YAML symlink for %s: %v", jsonPath, err)
}
}
}
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_CUSTOM_RESOURCES, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_CUSTOM_RESOURCES)), marshalErrors(crErrors))
@@ -411,6 +422,16 @@ func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (Coll
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_CONFIGMAPS)), marshalErrors(configMapsErrors))
// Validating Webhook Configurations
validatingWebhookConfigurations, validatingWebhookConfigurationsErrors := validatingWebhookConfigurations(ctx, client)
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_VALIDATING_WEBHOOK_CONFIGURATIONS)), bytes.NewBuffer(validatingWebhookConfigurations))
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_VALIDATING_WEBHOOK_CONFIGURATIONS)), marshalErrors(validatingWebhookConfigurationsErrors))
// Mutating Webhook Configurations
mutatingWebhookConfigurations, mutatingWebhookConfigurationsErrors := mutatingWebhookConfigurations(ctx, client)
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_MUTATING_WEBHOOK_CONFIGURATIONS)), bytes.NewBuffer(mutatingWebhookConfigurations))
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_MUTATING_WEBHOOK_CONFIGURATIONS)), marshalErrors(mutatingWebhookConfigurationsErrors))
// Replicated License
licenseData, licenseErr := replicatedLicense(ctx, client, namespaceNames)
if licenseErr == nil {
@@ -2234,23 +2255,68 @@ func configMaps(ctx context.Context, client kubernetes.Interface, namespaces []s
return configmapByNamespace, errorsByNamespace
}
// storeCustomResource stores a custom resource as JSON and YAML
// We use both formats for backwards compatibility. This way we
// avoid breaking existing tools and analysers that already rely on
// the YAML format.
func validatingWebhookConfigurations(ctx context.Context, client kubernetes.Interface) ([]byte, []string) {
validatingWebhookConfigurations, err := client.AdmissionregistrationV1().ValidatingWebhookConfigurations().List(ctx, metav1.ListOptions{})
if err != nil {
return nil, []string{err.Error()}
}
gvk, err := apiutil.GVKForObject(validatingWebhookConfigurations, scheme.Scheme)
if err == nil {
validatingWebhookConfigurations.GetObjectKind().SetGroupVersionKind(gvk)
}
for i, o := range validatingWebhookConfigurations.Items {
gvk, err := apiutil.GVKForObject(&o, scheme.Scheme)
if err == nil {
validatingWebhookConfigurations.Items[i].GetObjectKind().SetGroupVersionKind(gvk)
}
}
b, err := json.MarshalIndent(validatingWebhookConfigurations, "", " ")
if err != nil {
return nil, []string{err.Error()}
}
return b, nil
}
func mutatingWebhookConfigurations(ctx context.Context, client kubernetes.Interface) ([]byte, []string) {
mutatingWebhookConfigurations, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().List(ctx, metav1.ListOptions{})
if err != nil {
return nil, []string{err.Error()}
}
gvk, err := apiutil.GVKForObject(mutatingWebhookConfigurations, scheme.Scheme)
if err == nil {
mutatingWebhookConfigurations.GetObjectKind().SetGroupVersionKind(gvk)
}
for i, o := range mutatingWebhookConfigurations.Items {
gvk, err := apiutil.GVKForObject(&o, scheme.Scheme)
if err == nil {
mutatingWebhookConfigurations.Items[i].GetObjectKind().SetGroupVersionKind(gvk)
}
}
b, err := json.MarshalIndent(mutatingWebhookConfigurations, "", " ")
if err != nil {
return nil, []string{err.Error()}
}
return b, nil
}
// storeCustomResource stores a custom resource as JSON only.
// JSON is valid YAML, so any analyzer expecting YAML can consume the JSON
// file directly. We no longer write a separate YAML file because the
// built-in redactors are authored for JSON, and the duplicate YAML copy
// would otherwise be left unredacted.
func storeCustomResource(name string, objects any, m map[string][]byte) error {
j, err := json.MarshalIndent(objects, "", " ")
if err != nil {
return err
}
y, err := yaml.Marshal(objects)
if err != nil {
return err
}
m[fmt.Sprintf("%s.json", name)] = j
m[fmt.Sprintf("%s.yaml", name)] = y
return nil
}
+166 -11
View File
@@ -12,6 +12,7 @@ import (
"github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
certificatesv1 "k8s.io/api/certificates/v1"
v1 "k8s.io/api/coordination/v1"
corev1 "k8s.io/api/core/v1"
@@ -27,7 +28,6 @@ import (
"k8s.io/client-go/kubernetes"
testclient "k8s.io/client-go/kubernetes/fake"
k8stesting "k8s.io/client-go/testing"
"sigs.k8s.io/yaml"
)
func init() {
@@ -511,17 +511,8 @@ func TestCollectClusterResources_CustomResource(t *testing.T) {
// Fetch the CR from cluster
res, errs := crsV1(ctx, dynamicClient, apixClient.ApiextensionsV1(), []string{"default"})
assert.Empty(t, errs)
require.Equal(t, 2, len(res))
require.Equal(t, 1, len(res))
assert.Equal(t, fromJSON(t, res["supportbundles.troubleshoot.sh/default.json"]), sbObject)
assert.Equal(t, fromYAML(t, res["supportbundles.troubleshoot.sh/default.yaml"]), sbObject)
}
func fromYAML(t *testing.T, dat []byte) troubleshootv1beta2.SupportBundle {
sb := []troubleshootv1beta2.SupportBundle{}
err := yaml.Unmarshal(dat, &sb)
require.NoError(t, err)
require.Equal(t, 1, len(sb))
return sb[0]
}
func fromJSON(t *testing.T, dat []byte) troubleshootv1beta2.SupportBundle {
@@ -776,3 +767,167 @@ func createTestCertificateSigningRequests(client kubernetes.Interface, csrNames
}
return nil
}
func Test_ValidatingWebhookConfigurations(t *testing.T) {
tests := []struct {
name string
vwcNames []string
}{
{
name: "single validating webhook configuration",
vwcNames: []string{"test-vwc"},
},
{
name: "multiple validating webhook configurations",
vwcNames: []string{"vwc-1", "vwc-2", "vwc-3"},
},
{
name: "empty list",
vwcNames: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := testclient.NewSimpleClientset()
ctx := context.Background()
err := createTestValidatingWebhookConfigurations(client, tt.vwcNames)
require.NoError(t, err)
data, errs := validatingWebhookConfigurations(ctx, client)
assert.Empty(t, errs)
assert.NotNil(t, data)
var list admissionregistrationv1.ValidatingWebhookConfigurationList
err = json.Unmarshal(data, &list)
require.NoError(t, err)
assert.Len(t, list.Items, len(tt.vwcNames))
for _, item := range list.Items {
assert.Contains(t, tt.vwcNames, item.Name)
}
})
}
}
func Test_ValidatingWebhookConfigurations_PermissionDenied(t *testing.T) {
client := testclient.NewSimpleClientset()
ctx := context.Background()
client.PrependReactor("list", "validatingwebhookconfigurations", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, fmt.Errorf("validatingwebhookconfigurations.admissionregistration.k8s.io is forbidden: User \"system:serviceaccount:default:default\" cannot list resource \"validatingwebhookconfigurations\" in API group \"admissionregistration.k8s.io\" at the cluster scope")
})
data, errs := validatingWebhookConfigurations(ctx, client)
assert.Nil(t, data)
require.NotEmpty(t, errs)
assert.Len(t, errs, 1)
assert.Contains(t, errs[0], "forbidden")
}
func createTestValidatingWebhookConfigurations(client kubernetes.Interface, names []string) error {
for _, name := range names {
_, err := client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(context.Background(), &admissionregistrationv1.ValidatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Webhooks: []admissionregistrationv1.ValidatingWebhook{
{
Name: "test-webhook.example.com",
ClientConfig: admissionregistrationv1.WebhookClientConfig{
Service: &admissionregistrationv1.ServiceReference{
Namespace: "default",
Name: "webhook-service",
},
},
AdmissionReviewVersions: []string{"v1"},
},
},
}, metav1.CreateOptions{})
if err != nil {
return err
}
}
return nil
}
func Test_MutatingWebhookConfigurations(t *testing.T) {
tests := []struct {
name string
mwcNames []string
}{
{
name: "single mutating webhook configuration",
mwcNames: []string{"test-mwc"},
},
{
name: "multiple mutating webhook configurations",
mwcNames: []string{"mwc-1", "mwc-2"},
},
{
name: "empty list",
mwcNames: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := testclient.NewSimpleClientset()
ctx := context.Background()
err := createTestMutatingWebhookConfigurations(client, tt.mwcNames)
require.NoError(t, err)
data, errs := mutatingWebhookConfigurations(ctx, client)
assert.Empty(t, errs)
assert.NotNil(t, data)
var list admissionregistrationv1.MutatingWebhookConfigurationList
err = json.Unmarshal(data, &list)
require.NoError(t, err)
assert.Len(t, list.Items, len(tt.mwcNames))
for _, item := range list.Items {
assert.Contains(t, tt.mwcNames, item.Name)
}
})
}
}
func Test_MutatingWebhookConfigurations_PermissionDenied(t *testing.T) {
client := testclient.NewSimpleClientset()
ctx := context.Background()
client.PrependReactor("list", "mutatingwebhookconfigurations", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, fmt.Errorf("mutatingwebhookconfigurations.admissionregistration.k8s.io is forbidden: User \"system:serviceaccount:default:default\" cannot list resource \"mutatingwebhookconfigurations\" in API group \"admissionregistration.k8s.io\" at the cluster scope")
})
data, errs := mutatingWebhookConfigurations(ctx, client)
assert.Nil(t, data)
require.NotEmpty(t, errs)
assert.Len(t, errs, 1)
assert.Contains(t, errs[0], "forbidden")
}
func createTestMutatingWebhookConfigurations(client kubernetes.Interface, names []string) error {
for _, name := range names {
_, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.Background(), &admissionregistrationv1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Webhooks: []admissionregistrationv1.MutatingWebhook{
{
Name: "test-mutating-webhook.example.com",
ClientConfig: admissionregistrationv1.WebhookClientConfig{
Service: &admissionregistrationv1.ServiceReference{
Namespace: "default",
Name: "webhook-service",
},
},
AdmissionReviewVersions: []string{"v1"},
},
},
}, metav1.CreateOptions{})
if err != nil {
return err
}
}
return nil
}
+37 -1
View File
@@ -1,9 +1,11 @@
package collect
import (
"bytes"
"context"
"encoding/json"
"fmt"
"maps"
"strconv"
"strings"
"time"
@@ -13,6 +15,7 @@ import (
"github.com/replicatedhq/troubleshoot/pkg/multitype"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/klog/v2"
)
type Collector interface {
@@ -104,6 +107,8 @@ func GetCollector(collector *troubleshootv1beta2.Collect, bundlePath string, nam
return &CollectMysql{collector.Mysql, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true
case collector.Redis != nil:
return &CollectRedis{collector.Redis, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true
case collector.ClickHouse != nil:
return &CollectClickhouse{collector.ClickHouse, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true
case collector.Collectd != nil:
return &CollectCollectd{collector.Collectd, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true
case collector.Ceph != nil:
@@ -198,6 +203,9 @@ func getCollectorName(c interface{}) string {
case *CollectRedis:
collector = "redis"
name = v.Collector.CollectorName
case *CollectClickhouse:
collector = "clickhouse"
name = v.Collector.CollectorName
case *CollectCollectd:
collector = "collectd"
name = v.Collector.CollectorName
@@ -236,7 +244,6 @@ func getCollectorName(c interface{}) string {
default:
collector = "<none>"
}
if name != "" {
return fmt.Sprintf("%s/%s", collector, name)
}
@@ -297,6 +304,35 @@ func DedupCollectors(allCollectors []*troubleshootv1beta2.Collect) []*troublesho
return finalCollectors
}
// SkippedCollector records information about a collector that was skipped during collection.
type SkippedCollector struct {
Collector string `json:"collector"`
Reason string `json:"reason"`
Errors []string `json:"errors"`
Timestamp string `json:"timestamp"`
}
// WriteSkippedCollectors marshals the skipped collectors list and saves it
// using SaveResult which handles both in-memory and on-disk storage.
func WriteSkippedCollectors(skipped []SkippedCollector, allCollectedData map[string][]byte, bundlePath string) {
if len(skipped) == 0 {
return
}
skippedJSON, err := json.Marshal(skipped)
if err != nil {
return
}
// Either write to bundle path or memory
c := CollectorResult{}
if err := c.SaveResult(bundlePath, "skipped-collectors.json", bytes.NewReader(skippedJSON)); err != nil {
klog.Errorf("Failed to save skipped collectors: %v", err)
} else {
// Write to collected data to return downstream
maps.Copy(allCollectedData, c)
}
}
// Ensure Copy collectors are last in the list
// This is because copy collectors are expected to copy files from other collectors such as Exec, RunPod, RunDaemonSet
func EnsureCopyLast(allCollectors []Collector) []Collector {
+88
View File
@@ -1,6 +1,9 @@
package collect
import (
"encoding/json"
"os"
"path/filepath"
"testing"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
@@ -519,3 +522,88 @@ func TestEnsureCopyLast(t *testing.T) {
})
}
}
func TestWriteSkippedCollectors(t *testing.T) {
tests := []struct {
name string
skipped []SkippedCollector
bundlePath string
useTempDir bool
wantInMap bool
wantOnDisk bool
wantEntries []SkippedCollector
}{
{
name: "empty skipped list does nothing",
skipped: nil,
bundlePath: "",
wantInMap: false,
},
{
name: "in-memory only when bundlePath is empty",
skipped: []SkippedCollector{
{Collector: "clusterResources", Reason: "excluded", Timestamp: "2026-01-01T00:00:00Z"},
},
bundlePath: "",
wantInMap: true,
wantOnDisk: false,
wantEntries: []SkippedCollector{
{Collector: "clusterResources", Reason: "excluded", Timestamp: "2026-01-01T00:00:00Z"},
},
},
{
name: "writes to disk when bundlePath is set",
skipped: []SkippedCollector{
{Collector: "clusterResources", Reason: "excluded", Timestamp: "2026-01-01T00:00:00Z"},
{Collector: "logs", Reason: "insufficient RBAC permissions", Errors: []string{"pods is forbidden"}, Timestamp: "2026-01-01T00:00:01Z"},
},
useTempDir: true,
wantInMap: true,
wantOnDisk: true,
wantEntries: []SkippedCollector{
{Collector: "clusterResources", Reason: "excluded", Timestamp: "2026-01-01T00:00:00Z"},
{Collector: "logs", Reason: "insufficient RBAC permissions", Errors: []string{"pods is forbidden"}, Timestamp: "2026-01-01T00:00:01Z"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bundlePath := tt.bundlePath
if tt.useTempDir {
bundlePath = t.TempDir()
}
result := CollectorResult{}
WriteSkippedCollectors(tt.skipped, result, bundlePath)
if !tt.wantInMap {
assert.Empty(t, result)
return
}
// Verify in-memory entry exists
if bundlePath == "" {
// In-memory mode: data is stored in the map
data, ok := result["skipped-collectors.json"]
require.True(t, ok, "skipped-collectors.json should be in result map")
require.NotNil(t, data)
var got []SkippedCollector
require.NoError(t, json.Unmarshal(data, &got))
assert.Equal(t, tt.wantEntries, got)
} else {
// On-disk mode: map entry exists with nil value, file is on disk
_, ok := result["skipped-collectors.json"]
require.True(t, ok, "skipped-collectors.json should be in result map")
diskData, err := os.ReadFile(filepath.Join(bundlePath, "skipped-collectors.json"))
require.NoError(t, err)
var got []SkippedCollector
require.NoError(t, json.Unmarshal(diskData, &got))
assert.Equal(t, tt.wantEntries, got)
}
})
}
}
+8 -4
View File
@@ -11,6 +11,7 @@ import (
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes"
@@ -97,19 +98,22 @@ func copyFilesFromPod(ctx context.Context, dstPath string, clientConfig *restcli
return nil, nil, errors.Wrap(err, "failed to add runtime scheme")
}
// Stdin must be false because StreamOptions.Stdin is nil below.
// A mismatch causes the SPDY fallback (after WebSocket fails on RBAC)
// to hang: the API server opens a stdin stream but never receives EOF.
parameterCodec := runtime.NewParameterCodec(scheme)
req.VersionedParams(&corev1.PodExecOptions{
Command: command,
Container: containerName,
Stdin: true,
Stdout: false,
Stdin: false,
Stdout: true,
Stderr: true,
TTY: false,
}, parameterCodec)
exec, err := remotecommand.NewSPDYExecutor(clientConfig, "POST", req.URL())
exec, err := k8sutil.NewFallbackExecutor(clientConfig, req.URL())
if err != nil {
return nil, nil, errors.Wrap(err, "failed to create SPDY executor")
return nil, nil, errors.Wrap(err, "failed to create executor")
}
result := NewResult()
+7 -4
View File
@@ -299,19 +299,22 @@ func copyFilesFromHost(ctx context.Context, dstPath string, clientConfig *restcl
return nil, nil, errors.Wrap(err, "failed to add runtime scheme")
}
// Stdin must be false because StreamOptions.Stdin is nil below.
// A mismatch causes the SPDY fallback (after WebSocket fails on RBAC)
// to hang: the API server opens a stdin stream but never receives EOF.
parameterCodec := runtime.NewParameterCodec(scheme)
req.VersionedParams(&corev1.PodExecOptions{
Command: command,
Container: containerName,
Stdin: true,
Stdout: false,
Stdin: false,
Stdout: true,
Stderr: true,
TTY: false,
}, parameterCodec)
exec, err := remotecommand.NewSPDYExecutor(clientConfig, "POST", req.URL())
exec, err := k8sutil.NewFallbackExecutor(clientConfig, req.URL())
if err != nil {
return nil, nil, errors.Wrap(err, "failed to create SPDY executor")
return nil, nil, errors.Wrap(err, "failed to create executor")
}
result := NewResult()
+2 -1
View File
@@ -10,6 +10,7 @@ import (
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
@@ -325,7 +326,7 @@ func (c *etcdDebug) executeCommand(command string) ([]byte, []byte, error) {
TTY: false,
}, scheme.ParameterCodec)
exec, err := remotecommand.NewSPDYExecutor(c.clientConfig, "POST", req.URL())
exec, err := k8sutil.NewFallbackExecutor(c.clientConfig, req.URL())
if err != nil {
return nil, nil, err
}
+20 -6
View File
@@ -9,6 +9,7 @@ import (
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes"
@@ -87,16 +88,26 @@ func execWithoutTimeout(clientConfig *rest.Config, bundlePath string, execCollec
pod := pods[0]
stdout, stderr, execErrors := getExecOutputs(ctx, clientConfig, client, pod, execCollector)
container := pod.Spec.Containers[0].Name
if execCollector.ContainerName != "" {
container = execCollector.ContainerName
}
filePrefix := execCollector.CollectorName
if filePrefix == "" {
filePrefix = container
}
path := filepath.Join(execCollector.Name, pod.Namespace, pod.Name)
if len(stdout) > 0 {
output.SaveResult(bundlePath, filepath.Join(path, execCollector.CollectorName+"-stdout.txt"), bytes.NewBuffer(stdout))
output.SaveResult(bundlePath, filepath.Join(path, filePrefix+"-stdout.txt"), bytes.NewBuffer(stdout))
}
if len(stderr) > 0 {
output.SaveResult(bundlePath, filepath.Join(path, execCollector.CollectorName+"-stderr.txt"), bytes.NewBuffer(stderr))
output.SaveResult(bundlePath, filepath.Join(path, filePrefix+"-stderr.txt"), bytes.NewBuffer(stderr))
}
if len(execErrors) > 0 {
output.SaveResult(bundlePath, filepath.Join(path, execCollector.CollectorName+"-errors.json"), marshalErrors(execErrors))
output.SaveResult(bundlePath, filepath.Join(path, filePrefix+"-errors.json"), marshalErrors(execErrors))
}
}
@@ -118,16 +129,19 @@ func getExecOutputs(
}
parameterCodec := runtime.NewParameterCodec(scheme)
// Stdin must be false because StreamOptions.Stdin is nil below.
// A mismatch causes the SPDY fallback (after WebSocket fails on RBAC)
// to hang: the API server opens a stdin stream but never receives EOF.
req.VersionedParams(&corev1.PodExecOptions{
Command: append(execCollector.Command, execCollector.Args...),
Container: container,
Stdin: true,
Stdout: false,
Stdin: false,
Stdout: true,
Stderr: true,
TTY: false,
}, parameterCodec)
exec, err := remotecommand.NewSPDYExecutor(clientConfig, "POST", req.URL())
exec, err := k8sutil.NewFallbackExecutor(clientConfig, req.URL())
if err != nil {
return nil, nil, []string{err.Error()}
}
+2
View File
@@ -105,6 +105,8 @@ func GetHostCollector(collector *troubleshootv1beta2.HostCollect, bundlePath str
return &CollectHostNetworkNamespaceConnectivity{collector.NetworkNamespaceConnectivity, bundlePath}, true
case collector.HostSysctl != nil:
return &CollectHostSysctl{collector.HostSysctl, bundlePath}, true
case collector.RegistryImages != nil:
return &CollectHostRegistryImages{collector.RegistryImages, bundlePath}, true
default:
return nil, false
}
+7 -1
View File
@@ -82,7 +82,6 @@ func loadKConfigs(kernelRelease string) (KConfigs, error) {
// https://github.com/torvalds/linux/blob/v4.3/init/Kconfig#L794
// https://github.com/torvalds/linux/blob/v4.3/init/Kconfig#L9
possiblePaths := []string{
"/proc/config.gz",
"/boot/config-" + kernelRelease,
"/usr/src/linux-" + kernelRelease + "/.config",
"/usr/src/linux/.config",
@@ -93,6 +92,13 @@ func loadKConfigs(kernelRelease string) (KConfigs, error) {
"/lib/modules/" + kernelRelease + "/build/.config",
}
// /proc/config.gz reflects the currently running kernel. Only use it when
// the requested kernel release matches the running kernel.
currentRelease, err := getKernelRelease()
if err == nil && currentRelease == kernelRelease {
possiblePaths = append([]string{"/proc/config.gz"}, possiblePaths...)
}
for _, path := range possiblePaths {
// open file for reading
f, err := os.Open(path)
+90
View File
@@ -0,0 +1,90 @@
package collect
import (
"bytes"
"encoding/json"
"path/filepath"
"time"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"k8s.io/klog/v2"
)
type CollectHostRegistryImages struct {
hostCollector *troubleshootv1beta2.HostRegistryImages
BundlePath string
}
func (c *CollectHostRegistryImages) Title() string {
return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "Registry Images")
}
func (c *CollectHostRegistryImages) IsExcluded() (bool, error) {
return isExcluded(c.hostCollector.Exclude)
}
func (c *CollectHostRegistryImages) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
registryInfo := RegistryInfo{
Images: map[string]RegistryImage{},
}
auth := c.resolveAuth()
if auth != nil {
klog.V(2).Infof("using inline credentials for registry check (username=%s)", c.hostCollector.Username)
} else {
klog.V(2).Info("no inline credentials provided, using ambient auth")
}
klog.V(2).Infof("checking %d images", len(c.hostCollector.Images))
for _, image := range c.hostCollector.Images {
klog.V(2).Infof("checking image: %s", image)
imageRef, err := parseImageRef(image)
if err != nil {
klog.Errorf("failed to parse image ref %s: %v", image, err)
registryInfo.Images[image] = RegistryImage{Error: err.Error()}
continue
}
exists, err := imageExistsWithAuth(auth, imageRef, image, 10*time.Second)
if err != nil {
klog.Errorf("image check failed for %s: %v", image, err)
registryInfo.Images[image] = RegistryImage{Error: err.Error()}
} else {
klog.V(2).Infof("image %s exists=%t", image, exists)
registryInfo.Images[image] = RegistryImage{Exists: exists}
}
}
b, err := json.MarshalIndent(registryInfo, "", " ")
if err != nil {
return nil, errors.Wrap(err, "failed to marshal registry info")
}
collectorName := c.hostCollector.CollectorName
if collectorName == "" {
collectorName = "images"
}
name := filepath.Join("host-collectors/registry-images", collectorName+".json")
output := NewResult()
output.SaveResult(c.BundlePath, name, bytes.NewBuffer(b))
return output, nil
}
func (c *CollectHostRegistryImages) RemoteCollect(progressChan chan<- interface{}) (map[string][]byte, error) {
return nil, ErrRemoteCollectorNotImplemented
}
// resolveAuth returns auth config from inline credentials or nil for ambient auth.
func (c *CollectHostRegistryImages) resolveAuth() *registryAuthConfig {
if c.hostCollector.Username != "" {
return &registryAuthConfig{
username: c.hostCollector.Username,
password: c.hostCollector.Password,
}
}
// No credentials: rely on ambient auth (~/.docker/config.json)
return nil
}
+97
View File
@@ -0,0 +1,97 @@
package collect
import (
"testing"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCollectHostRegistryImagesTitle(t *testing.T) {
tests := []struct {
name string
meta troubleshootv1beta2.HostCollectorMeta
expected string
}{
{
name: "default title",
meta: troubleshootv1beta2.HostCollectorMeta{},
expected: "Registry Images",
},
{
name: "custom title",
meta: troubleshootv1beta2.HostCollectorMeta{
CollectorName: "My Registry",
},
expected: "My Registry",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c := &CollectHostRegistryImages{
hostCollector: &troubleshootv1beta2.HostRegistryImages{
HostCollectorMeta: test.meta,
},
}
assert.Equal(t, test.expected, c.Title())
})
}
}
func TestCollectHostRegistryImagesResolveAuth(t *testing.T) {
tests := []struct {
name string
username string
password string
expected *registryAuthConfig
}{
{
name: "nil when no credentials",
username: "",
password: "",
expected: nil,
},
{
name: "returns auth with credentials",
username: "user",
password: "pass",
expected: &registryAuthConfig{
username: "user",
password: "pass",
},
},
{
name: "returns auth with username only",
username: "user",
password: "",
expected: &registryAuthConfig{
username: "user",
password: "",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c := &CollectHostRegistryImages{
hostCollector: &troubleshootv1beta2.HostRegistryImages{
Username: test.username,
Password: test.password,
},
}
result := c.resolveAuth()
assert.Equal(t, test.expected, result)
})
}
}
func TestCollectHostRegistryImagesRemoteCollect(t *testing.T) {
c := &CollectHostRegistryImages{
hostCollector: &troubleshootv1beta2.HostRegistryImages{},
}
result, err := c.RemoteCollect(nil)
require.ErrorIs(t, err, ErrRemoteCollectorNotImplemented)
assert.Nil(t, result)
}
+2 -1
View File
@@ -12,6 +12,7 @@ import (
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
longhornv1beta1types "github.com/replicatedhq/troubleshoot/pkg/longhorn/apis/longhorn/v1beta1"
longhornv1beta1 "github.com/replicatedhq/troubleshoot/pkg/longhorn/client/clientset/versioned/typed/longhorn/v1beta1"
longhorntypes "github.com/replicatedhq/troubleshoot/pkg/longhorn/types"
@@ -391,7 +392,7 @@ func GetLonghornReplicaChecksum(clientConfig *rest.Config, replica longhornv1bet
Param("command", "-c").
Param("command", fmt.Sprintf("if [ -d %s ]; then md5sum %s/*; fi", dir, dir))
executor, err := remotecommand.NewSPDYExecutor(clientConfig, "POST", req.URL())
executor, err := k8sutil.NewFallbackExecutor(clientConfig, req.URL())
if err != nil {
return "", errors.Wrapf(err, "create remote exec")
}
+51 -3
View File
@@ -7,11 +7,12 @@ import (
"encoding/json"
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/go-sql-driver/mysql"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/klog/v2"
)
type CollectMysql struct {
@@ -32,11 +33,58 @@ func (c *CollectMysql) IsExcluded() (bool, error) {
return isExcluded(c.Collector.Exclude)
}
func (c *CollectMysql) Collect(progressChan chan<- interface{}) (CollectorResult, error) {
databaseConnection := DatabaseConnection{}
func (c *CollectMysql) createConnectConfig() (*mysql.Config, error) {
if c.Collector.URI == "" {
return nil, errors.New("mysql uri cannot be empty")
}
cfg, err := mysql.ParseDSN(c.Collector.URI)
if err != nil {
return nil, errors.Wrap(err, "failed to parse mysql config")
}
if c.Collector.TLS != nil {
klog.V(2).Infof("Connecting to mysql with TLS client config")
tlsConfig, err := createTLSConfig(c.Context, c.Client, c.Collector.TLS)
if err != nil {
return nil, err
}
cfg.TLS = tlsConfig
}
return cfg, nil
}
func (c *CollectMysql) connect() (*sql.DB, error) {
cfg, err := c.createConnectConfig()
if err != nil {
return nil, err
}
if c.Collector.TLS != nil {
connector, err := mysql.NewConnector(cfg)
if err != nil {
return nil, errors.Wrap(err, "failed to create mysql connector")
}
db := sql.OpenDB(connector)
return db, nil
}
db, err := sql.Open("mysql", c.Collector.URI)
if err != nil {
return nil, err
}
return db, nil
}
func (c *CollectMysql) Collect(progressChan chan<- interface{}) (CollectorResult, error) {
databaseConnection := DatabaseConnection{}
db, err := c.connect()
if err != nil {
klog.V(2).Infof("MySQL connection error: %s", err.Error())
databaseConnection.Error = err.Error()
} else {
defer db.Close()
+166
View File
@@ -0,0 +1,166 @@
package collect
import (
"context"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"testing"
"github.com/replicatedhq/troubleshoot/internal/testutils"
"github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
testclient "k8s.io/client-go/kubernetes/fake"
)
func TestCollectMysql_createConnectConfigPlainText(t *testing.T) {
tests := []struct {
name string
uri string
hasError bool
}{
{
name: "valid uri creates mysql connection config successfully",
uri: "user:password@tcp(localhost:3306)/defaultdb",
},
{
name: "empty uri fails to create mysql connection config with error",
uri: "",
hasError: true,
},
{
name: "invalid protocol fails to create mysql connection config with error",
uri: "http://somehost:3306",
hasError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &CollectMysql{
Context: context.Background(),
Collector: &v1beta2.Database{
URI: tt.uri,
},
}
cfg, err := c.createConnectConfig()
assert.Equal(t, tt.hasError, err != nil)
if err == nil {
require.NotNil(t, cfg)
assert.Equal(t, "localhost:3306", cfg.Addr)
assert.Equal(t, "defaultdb", cfg.DBName)
} else {
t.Log(err)
assert.Nil(t, cfg)
}
})
}
}
func TestCollectMysql_createConnectConfigTLS(t *testing.T) {
k8sClient := testclient.NewSimpleClientset()
c := &CollectMysql{
Client: k8sClient,
Context: context.Background(),
Collector: &v1beta2.Database{
URI: "user:password@tcp(localhost:3306)/defaultdb",
TLS: &v1beta2.TLSParams{
CACert: testutils.GetTestFixture(t, "db/ca.pem"),
ClientCert: testutils.GetTestFixture(t, "db/client.pem"),
ClientKey: testutils.GetTestFixture(t, "db/client-key.pem"),
},
},
}
cfg, err := c.createConnectConfig()
assert.NoError(t, err)
assert.NotNil(t, cfg)
assert.Equal(t, "localhost:3306", cfg.Addr)
// Check TLS config exists and is configured correctly
require.NotNil(t, cfg.TLS)
// Check client cert
require.Len(t, cfg.TLS.Certificates, 1)
require.Len(t, cfg.TLS.Certificates[0].Certificate, 1)
cert := cfg.TLS.Certificates[0]
clientCert, err := x509.ParseCertificate(cert.Certificate[0])
require.NoError(t, err)
assert.Equal(t, "CN=client,L=Didcot,ST=Oxfordshire,C=UK", clientCert.Subject.String())
// Check client key
block, _ := pem.Decode([]byte(testutils.GetTestFixture(t, "db/client-key.pem")))
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
require.NoError(t, err)
assert.True(t, key.Equal(cert.PrivateKey.(*rsa.PrivateKey)))
assert.NotNil(t, cfg.TLS.RootCAs)
assert.False(t, cfg.TLS.InsecureSkipVerify)
}
func TestCollectMysql_createConnectConfigTLSSkipVerify(t *testing.T) {
c := &CollectMysql{
Context: context.Background(),
Collector: &v1beta2.Database{
URI: "user:password@tcp(localhost:3306)/defaultdb",
TLS: &v1beta2.TLSParams{
SkipVerify: true,
},
},
}
cfg, err := c.createConnectConfig()
assert.NoError(t, err)
assert.NotNil(t, cfg)
require.NotNil(t, cfg.TLS)
assert.True(t, cfg.TLS.InsecureSkipVerify)
}
func TestCollectMysql_createConnectConfigTLSCACertOnly(t *testing.T) {
c := &CollectMysql{
Context: context.Background(),
Collector: &v1beta2.Database{
URI: "user:password@tcp(localhost:3306)/defaultdb",
TLS: &v1beta2.TLSParams{
CACert: testutils.GetTestFixture(t, "db/ca.pem"),
},
},
}
cfg, err := c.createConnectConfig()
assert.NoError(t, err)
assert.NotNil(t, cfg)
require.NotNil(t, cfg.TLS)
assert.NotNil(t, cfg.TLS.RootCAs)
assert.Empty(t, cfg.TLS.Certificates)
assert.False(t, cfg.TLS.InsecureSkipVerify)
}
func TestCollectMysql_createConnectConfigTLSSecret(t *testing.T) {
k8sClient := testclient.NewSimpleClientset()
c := &CollectMysql{
Client: k8sClient,
Context: context.Background(),
Collector: &v1beta2.Database{
URI: "user:password@tcp(localhost:3306)/defaultdb",
TLS: &v1beta2.TLSParams{
Secret: createTLSSecret(t, k8sClient, map[string]string{
"cacert": testutils.GetTestFixture(t, "db/ca.pem"),
"clientCert": testutils.GetTestFixture(t, "db/client.pem"),
"clientKey": testutils.GetTestFixture(t, "db/client-key.pem"),
}),
},
},
}
cfg, err := c.createConnectConfig()
assert.NoError(t, err)
assert.NotNil(t, cfg)
require.NotNil(t, cfg.TLS)
assert.NotNil(t, cfg.TLS.RootCAs)
assert.NotEmpty(t, cfg.TLS.Certificates)
assert.False(t, cfg.TLS.InsecureSkipVerify)
}
+38 -6
View File
@@ -7,6 +7,7 @@ import (
"io"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
@@ -16,18 +17,49 @@ import (
"k8s.io/klog/v2"
)
// Max number of concurrent redactors to run
// Ensure the number is low enough since each of the redactors
// also spawns goroutines to redact files in tar archives and
// other goroutines for each redactor spec.
const MAX_CONCURRENT_REDACTORS = 10
// Default cap on concurrent file redactors. Each redactor also spawns
// goroutines to redact files in tar archives and goroutines for each
// redactor spec, so the ceiling is intentionally low.
const DefaultMaxConcurrentRedactors = 10
// MaxConcurrentRedactorsEnvVar is the environment variable name used to
// override DefaultMaxConcurrentRedactors at runtime. Operators set this on
// the support-bundle binary (e.g. inside a Job/initContainer) when the
// default ceiling becomes a bottleneck for very large bundles.
const MaxConcurrentRedactorsEnvVar = "TROUBLESHOOT_MAX_CONCURRENT_REDACTORS"
// maxConcurrentRedactors returns the active cap on concurrent redactors.
// It reads MaxConcurrentRedactorsEnvVar; if unset, empty, non-numeric, or
// <= 0 it falls back to DefaultMaxConcurrentRedactors (with a klog warning
// for non-empty invalid input so silent misconfiguration is hard to miss).
func maxConcurrentRedactors() int {
raw, ok := os.LookupEnv(MaxConcurrentRedactorsEnvVar)
if !ok || raw == "" {
return DefaultMaxConcurrentRedactors
}
n, err := strconv.Atoi(raw)
if err != nil {
klog.Warningf("Invalid %s=%q (not an integer); falling back to default %d", MaxConcurrentRedactorsEnvVar, raw, DefaultMaxConcurrentRedactors)
return DefaultMaxConcurrentRedactors
}
if n <= 0 {
klog.Warningf("Invalid %s=%d (must be > 0); falling back to default %d", MaxConcurrentRedactorsEnvVar, n, DefaultMaxConcurrentRedactors)
return DefaultMaxConcurrentRedactors
}
if n != DefaultMaxConcurrentRedactors {
klog.Infof("Overriding concurrent redactor cap: %s=%d (default %d)", MaxConcurrentRedactorsEnvVar, n, DefaultMaxConcurrentRedactors)
}
return n
}
func RedactResult(bundlePath string, input CollectorResult, additionalRedactors []*troubleshootv1beta2.Redact) error {
wg := &sync.WaitGroup{}
// Error channel to capture errors from goroutines
errorCh := make(chan error, len(input))
limitCh := make(chan struct{}, MAX_CONCURRENT_REDACTORS)
limitCh := make(chan struct{}, maxConcurrentRedactors())
defer close(limitCh)
for k, v := range input {
+94
View File
@@ -0,0 +1,94 @@
package collect
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_maxConcurrentRedactors(t *testing.T) {
tests := []struct {
name string
setEnv bool
value string
want int
}{
{
name: "env unset returns default",
setEnv: false,
want: DefaultMaxConcurrentRedactors,
},
{
name: "empty value returns default",
setEnv: true,
value: "",
want: DefaultMaxConcurrentRedactors,
},
{
name: "valid positive int overrides default",
setEnv: true,
value: "50",
want: 50,
},
{
name: "value equal to default is honored",
setEnv: true,
value: "10",
want: DefaultMaxConcurrentRedactors,
},
{
name: "zero falls back to default",
setEnv: true,
value: "0",
want: DefaultMaxConcurrentRedactors,
},
{
name: "negative value falls back to default",
setEnv: true,
value: "-3",
want: DefaultMaxConcurrentRedactors,
},
{
name: "non-numeric value falls back to default",
setEnv: true,
value: "potato",
want: DefaultMaxConcurrentRedactors,
},
{
name: "whitespace-padded value falls back to default",
setEnv: true,
value: " 4 ",
want: DefaultMaxConcurrentRedactors,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Snapshot inherited state so each subtest is hermetic regardless
// of the order Go picks. t.Setenv would mask "env unset" cases
// with an empty value, so we manage the env directly here.
prev, hadPrev := os.LookupEnv(MaxConcurrentRedactorsEnvVar)
t.Cleanup(func() {
if hadPrev {
_ = os.Setenv(MaxConcurrentRedactorsEnvVar, prev)
} else {
_ = os.Unsetenv(MaxConcurrentRedactorsEnvVar)
}
})
if tt.setEnv {
if err := os.Setenv(MaxConcurrentRedactorsEnvVar, tt.value); err != nil {
t.Fatalf("setenv: %v", err)
}
} else {
if err := os.Unsetenv(MaxConcurrentRedactorsEnvVar); err != nil {
t.Fatalf("unsetenv: %v", err)
}
}
got := maxConcurrentRedactors()
assert.Equal(t, tt.want, got)
})
}
}
+83 -55
View File
@@ -3,21 +3,22 @@ package collect
import (
"bytes"
"context"
"crypto/tls"
"encoding/base64"
"encoding/json"
stderrors "errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/distribution/distribution/v3/registry/api/errcode"
registryv2 "github.com/distribution/distribution/v3/registry/api/v2"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
"github.com/pkg/errors"
"github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
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"
@@ -91,9 +92,9 @@ func (c *CollectRegistry) Collect(progressChan chan<- interface{}) (CollectorRes
}
func imageExists(namespace string, clientConfig *rest.Config, registryCollector *troubleshootv1beta2.RegistryImages, image string, deadline time.Duration) (bool, error) {
imageRef, err := alltransports.ParseImageName(fmt.Sprintf("docker://%s", image))
imageRef, err := parseImageRef(image)
if err != nil {
return false, errors.Wrapf(err, "failed to parse image name %s", image)
return false, err
}
authConfig, err := getImageAuthConfig(namespace, clientConfig, registryCollector, imageRef)
@@ -102,16 +103,29 @@ func imageExists(namespace string, clientConfig *rest.Config, registryCollector
return false, errors.Wrap(err, "failed to get auth config")
}
sysCtx := types.SystemContext{
DockerDisableV1Ping: true,
DockerInsecureSkipTLSVerify: types.OptionalBoolTrue,
return imageExistsWithAuth(authConfig, imageRef, image, deadline)
}
func parseImageRef(image string) (name.Reference, error) {
ref, err := name.ParseReference(image)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse image name %s", image)
}
if authConfig != nil {
sysCtx.DockerAuthConfig = &types.DockerAuthConfig{
Username: authConfig.username,
Password: authConfig.password,
}
return ref, nil
}
// imageExistsWithAuth checks if an image exists in a registry using optional auth credentials.
// authConfig may be nil for ambient credentials (e.g. ~/.docker/config.json).
// This is the shared core used by both the cluster-level and host-level registry collectors.
func imageExistsWithAuth(authConfig *registryAuthConfig, ref name.Reference, image string, deadline time.Duration) (bool, error) {
// remote.DefaultTransport includes Proxy (HTTP_PROXY/HTTPS_PROXY), dial/TLS
// timeouts, and keepalive; clone it so InsecureSkipVerify does not drop those.
defaultTR, ok := remote.DefaultTransport.(*http.Transport)
if !ok {
return false, errors.New("remote.DefaultTransport is not *http.Transport")
}
insecureTransport := defaultTR.Clone()
insecureTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec
if deadline == 0 {
deadline = 10 * time.Second
@@ -122,12 +136,30 @@ func imageExists(namespace string, clientConfig *rest.Config, registryCollector
err := func() error {
ctx, cancel := context.WithTimeout(context.Background(), deadline)
defer cancel()
remoteImage, err := imageRef.NewImage(ctx, &sysCtx)
if err != nil {
return err
opts := []remote.Option{
remote.WithContext(ctx),
remote.WithTransport(insecureTransport),
}
remoteImage.Close()
return nil
if authConfig != nil {
opts = append(opts, remote.WithAuth(&authn.Basic{
Username: authConfig.username,
Password: authConfig.password,
}))
} else {
opts = append(opts, remote.WithAuthFromKeychain(authn.DefaultKeychain))
}
// Use Get (not Head) so 404 responses include a JSON body; the registry
// API encodes MANIFEST_UNKNOWN vs NAME_UNKNOWN there, which we need to
// distinguish. Head 404s typically have no body, so *transport.Error has
// empty Errors and we cannot classify the failure.
//
// Get fetches the manifest (or list/index) for the tag or digest and does
// not pick a per-platform child image, so this checks presence only, not
// whether the image runs on a given architecture.
_, err := remote.Get(ref, opts...)
return err
}()
if err == nil {
klog.V(2).Infof("image %s exists", image)
@@ -136,18 +168,10 @@ func imageExists(namespace string, clientConfig *rest.Config, registryCollector
klog.Errorf("failed to get image %s: %v", image, err)
// if this is a context timeout, stop here so we dont run this check for too long
if errors.Is(err, context.DeadlineExceeded) {
if stderrors.Is(err, context.DeadlineExceeded) {
return false, errors.Wrap(err, "failed to get image manifest")
}
if strings.Contains(err.Error(), "no image found in manifest list for architecture") {
// manifest was downloaded, but no matching architecture found in manifest
// should this count as image does not exist?
// this binary's architecture is not necessarily what will run in the cluster
return true, nil
}
if isNotFound(err) {
return false, nil
}
@@ -164,7 +188,7 @@ func imageExists(namespace string, clientConfig *rest.Config, registryCollector
return false, errors.Wrap(lastErr, "failed to retry")
}
func getImageAuthConfig(namespace string, clientConfig *rest.Config, registryCollector *troubleshootv1beta2.RegistryImages, imageRef types.ImageReference) (*registryAuthConfig, error) {
func getImageAuthConfig(namespace string, clientConfig *rest.Config, registryCollector *troubleshootv1beta2.RegistryImages, imageRef name.Reference) (*registryAuthConfig, error) {
if registryCollector.ImagePullSecrets == nil {
return nil, nil
}
@@ -195,13 +219,13 @@ func getImageAuthConfig(namespace string, clientConfig *rest.Config, registryCol
return nil, errors.New("image pull secret spec is not valid")
}
func getImageAuthConfigFromData(imageRef types.ImageReference, pullSecrets *v1beta2.ImagePullSecrets) (*registryAuthConfig, error) {
func getImageAuthConfigFromData(imageRef name.Reference, pullSecrets *v1beta2.ImagePullSecrets) (*registryAuthConfig, error) {
if pullSecrets.SecretType != "kubernetes.io/dockerconfigjson" {
return nil, errors.Errorf("secret type is not supported: %s", pullSecrets.SecretType)
}
configJsonBase64 := pullSecrets.Data[".dockerconfigjson"]
registry := dockerref.Domain(imageRef.DockerReference())
registry := imageRef.Context().RegistryStr()
configJson, err := base64.StdEncoding.DecodeString(configJsonBase64)
if err != nil {
@@ -222,8 +246,14 @@ func getImageAuthConfigFromData(imageRef types.ImageReference, pullSecrets *v1be
}
auth, ok := dockerCfgJSON.Auths[registry]
// go-containerregistry normalizes "docker.io" to "index.docker.io"
// (name.DefaultRegistry); many dockerconfigjson files key on "docker.io"
// instead. Fall back to the alias so existing user secrets keep working.
if !ok && registry == name.DefaultRegistry {
auth, ok = dockerCfgJSON.Auths["docker.io"]
}
if !ok {
// Suport a mix of public and private images
// Support a mix of public and private images
return nil, nil
}
@@ -258,7 +288,7 @@ func getImageAuthConfigFromData(imageRef types.ImageReference, pullSecrets *v1be
return &authConfig, nil
}
func getImageAuthConfigFromSecret(clientConfig *rest.Config, imageRef types.ImageReference, pullSecrets *v1beta2.ImagePullSecrets, namespace string) (*registryAuthConfig, error) {
func getImageAuthConfigFromSecret(clientConfig *rest.Config, imageRef name.Reference, pullSecrets *v1beta2.ImagePullSecrets, namespace string) (*registryAuthConfig, error) {
ctx := context.Background()
client, err := kubernetes.NewForConfig(clientConfig)
@@ -287,30 +317,28 @@ func getImageAuthConfigFromSecret(clientConfig *rest.Config, imageRef types.Imag
return config, nil
}
// isNotFound returns true only when the registry reports MANIFEST_UNKNOWN: the
// repository exists but the tag or digest has no manifest. A 404 with
// NAME_UNKNOWN (repository missing) is not "not found" in that sense; callers
// should see the error to diagnose a wrong image path. Unstructured 404s
// (e.g. empty body on HEAD) are not treated as a known-missing image.
func isNotFound(err error) bool {
switch err := err.(type) {
case errcode.Errors:
for _, e := range err {
if isNotFound(e) {
return true
}
}
return false
case errcode.Error:
return err.Message == registryv2.ErrorCodeManifestUnknown.Message()
}
// this type will cause panic when compared to error type
if _, ok := err.(imagedocker.ErrUnauthorizedForCredentials); ok {
var terr *transport.Error
if !stderrors.As(err, &terr) || terr.StatusCode != http.StatusNotFound {
return false
}
cause := errors.Cause(err)
if cause, ok := cause.(error); ok {
if cause == err {
if len(terr.Errors) == 0 {
return false
}
for _, d := range terr.Errors {
if d.Code == transport.NameUnknownErrorCode {
return false
}
}
return isNotFound(cause)
for _, d := range terr.Errors {
if d.Code == transport.ManifestUnknownErrorCode {
return true
}
}
return false
}
+140 -2
View File
@@ -5,15 +5,55 @@ import (
"encoding/base64"
"fmt"
"net"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/google/go-containerregistry/pkg/name"
"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"
)
// fakeRegistry is a minimal Docker Registry v2 TLS stand-in for unit testing
// imageExists. The handler returns whatever the caller puts in `manifest`
// for /v2/{name}/manifests/{ref}. /v2/ is always a 200.
//
// We use NewTLSServer so the same tests work against both the current
// containers/image implementation (DockerInsecureSkipTLSVerify) and the
// new go-containerregistry implementation (InsecureSkipVerify transport).
// Plain HTTP would break after Task 3 because go-containerregistry does not
// treat 127.0.0.1 as an insecure registry by default.
type fakeRegistry struct {
server *httptest.Server
manifest http.HandlerFunc
}
func newFakeRegistry(t *testing.T, manifest http.HandlerFunc) *fakeRegistry {
t.Helper()
mux := http.NewServeMux()
mux.HandleFunc("/v2/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/v2/" || r.URL.Path == "/v2" {
w.Header().Set("Docker-Distribution-Api-Version", "registry/2.0")
w.WriteHeader(http.StatusOK)
return
}
manifest(w, r)
})
srv := httptest.NewTLSServer(mux)
t.Cleanup(srv.Close)
return &fakeRegistry{server: srv, manifest: manifest}
}
// hostPort strips "https://" from the test server URL, leaving "127.0.0.1:NNNN"
// suitable for use as the registry portion of an image reference.
func (f *fakeRegistry) hostPort() string {
return strings.TrimPrefix(f.server.URL, "https://")
}
func TestGetImageAuthConfigFromData(t *testing.T) {
tests := []struct {
name string
@@ -57,7 +97,7 @@ func TestGetImageAuthConfigFromData(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
imageRef, err := alltransports.ParseImageName(fmt.Sprintf("docker://%s", test.imageName))
imageRef, err := name.ParseReference(test.imageName)
assert.NoError(t, err)
pullSecrets := &v1beta2.ImagePullSecrets{
@@ -119,3 +159,101 @@ func TestImageExists_ContextDeadlineExceeded(t *testing.T) {
assert.ErrorIs(t, err, context.DeadlineExceeded)
assert.False(t, exists)
}
func TestImageExists_Found(t *testing.T) {
fr := newFakeRegistry(t, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
w.Header().Set("Docker-Content-Digest", "sha256:1111111111111111111111111111111111111111111111111111111111111111")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","size":1,"digest":"sha256:2222222222222222222222222222222222222222222222222222222222222222"},"layers":[]}`))
})
collector := &v1beta2.RegistryImages{
Images: []string{fmt.Sprintf("%s/test:latest", fr.hostPort())},
}
exists, err := imageExists("default", &rest.Config{}, collector, fmt.Sprintf("%s/test:latest", fr.hostPort()), 5*time.Second)
assert.NoError(t, err)
assert.True(t, exists)
}
func TestImageExists_NotFound(t *testing.T) {
fr := newFakeRegistry(t, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"errors":[{"code":"MANIFEST_UNKNOWN","message":"manifest unknown"}]}`))
})
collector := &v1beta2.RegistryImages{
Images: []string{fmt.Sprintf("%s/test:latest", fr.hostPort())},
}
exists, err := imageExists("default", &rest.Config{}, collector, fmt.Sprintf("%s/test:latest", fr.hostPort()), 5*time.Second)
assert.NoError(t, err)
assert.False(t, exists)
}
func TestImageExists_NameUnknown_PropagatesError(t *testing.T) {
fr := newFakeRegistry(t, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"errors":[{"code":"NAME_UNKNOWN","message":"repository name not known to registry"}]}`))
})
collector := &v1beta2.RegistryImages{
Images: []string{fmt.Sprintf("%s/nonexistentrepo/image:latest", fr.hostPort())},
}
exists, err := imageExists("default", &rest.Config{}, collector, fmt.Sprintf("%s/nonexistentrepo/image:latest", fr.hostPort()), 5*time.Second)
assert.Error(t, err)
assert.False(t, exists)
}
func TestImageExists_Unauthorized(t *testing.T) {
fr := newFakeRegistry(t, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Www-Authenticate", `Basic realm="registry"`)
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"errors":[{"code":"UNAUTHORIZED","message":"authentication required"}]}`))
})
collector := &v1beta2.RegistryImages{
Images: []string{fmt.Sprintf("%s/test:latest", fr.hostPort())},
}
exists, err := imageExists("default", &rest.Config{}, collector, fmt.Sprintf("%s/test:latest", fr.hostPort()), 5*time.Second)
assert.Error(t, err)
assert.False(t, exists)
}
func TestImageExists_RetriesOnEOF(t *testing.T) {
var attempts int32
fr := newFakeRegistry(t, func(w http.ResponseWriter, r *http.Request) {
n := atomic.AddInt32(&attempts, 1)
if n < 3 {
// hijack the connection and slam it shut to cause an EOF on the client
hj, ok := w.(http.Hijacker)
if !ok {
t.Fatalf("response writer does not support hijacking")
}
conn, _, err := hj.Hijack()
if err != nil {
t.Fatalf("hijack: %v", err)
}
_ = conn.Close()
return
}
w.Header().Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
w.Header().Set("Docker-Content-Digest", "sha256:1111111111111111111111111111111111111111111111111111111111111111")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","size":1,"digest":"sha256:2222222222222222222222222222222222222222222222222222222222222222"},"layers":[]}`))
})
collector := &v1beta2.RegistryImages{
Images: []string{fmt.Sprintf("%s/test:latest", fr.hostPort())},
}
exists, err := imageExists("default", &rest.Config{}, collector, fmt.Sprintf("%s/test:latest", fr.hostPort()), 5*time.Second)
assert.NoError(t, err)
assert.True(t, exists)
assert.GreaterOrEqual(t, atomic.LoadInt32(&attempts), int32(3))
}
+2 -3
View File
@@ -6,6 +6,7 @@ import (
"compress/gzip"
"fmt"
"io"
"maps"
"os"
"path"
"path/filepath"
@@ -87,9 +88,7 @@ func (r CollectorResult) SymLinkResult(bundlePath, relativeLinkPath, relativeFil
// It also ensures that when operating on the results in memory (e.g preflights),
// all files are included.
func (r CollectorResult) AddResult(other CollectorResult) {
for k, v := range other {
r[k] = v
}
maps.Copy(r, other)
}
// SaveResult saves the collector result to relativePath file on disk. If bundlePath is
+8 -3
View File
@@ -69,7 +69,7 @@ func (c *CollectRunPod) Collect(progressChan chan<- interface{}) (result Collect
if err != nil {
return
}
result, err = savePodDetails(ctx, client, result, c.BundlePath, c.ClientConfig, pod, c.Collector)
result, err = savePodDetails(ctx, client, result, c.BundlePath, pod, c.Collector)
if err != nil {
klog.Errorf("failed to save pod details: %v", err)
}
@@ -407,7 +407,7 @@ func RunPodLogsWithOptions(ctx context.Context, client v1.CoreV1Interface, podSp
return io.ReadAll(logs)
}
func savePodDetails(ctx context.Context, client *kubernetes.Clientset, output CollectorResult, bundlePath string, clientConfig *rest.Config, pod *corev1.Pod, runPodCollector *troubleshootv1beta2.RunPod) (CollectorResult, error) {
func savePodDetails(ctx context.Context, client kubernetes.Interface, output CollectorResult, bundlePath string, pod *corev1.Pod, runPodCollector *troubleshootv1beta2.RunPod) (CollectorResult, error) {
podStatus, err := client.CoreV1().Pods(pod.Namespace).Get(ctx, pod.Name, metav1.GetOptions{})
if err != nil {
return nil, errors.Wrap(err, "failed to get pod")
@@ -418,7 +418,12 @@ func savePodDetails(ctx context.Context, client *kubernetes.Clientset, output Co
return nil, errors.Wrap(err, "failed to get pod events")
}
podBytes, err := json.MarshalIndent(podStatus, "", " ")
// The full pod Spec can contain sensitive data such as env vars, commands, args,
// volumes, and image pull secrets. Strip it before saving the pod to the bundle.
sanitizedPod := *podStatus
sanitizedPod.Spec = corev1.PodSpec{}
podBytes, err := json.MarshalIndent(sanitizedPod, "", " ")
if err != nil {
return nil, errors.Wrap(err, "failed to marshal pod status")
}
+97
View File
@@ -2,6 +2,7 @@ package collect
import (
"context"
"encoding/json"
"testing"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
@@ -239,3 +240,99 @@ func Test_deleteImagePullSecret(t *testing.T) {
})
}
}
func TestSavePodDetails_StripsPodSpec(t *testing.T) {
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test-run-pod",
Namespace: "default",
Labels: map[string]string{
"troubleshoot-role": "run-collector",
},
},
Spec: corev1.PodSpec{
NodeName: "test-node",
Containers: []corev1.Container{
{
Name: "collector",
Image: "busybox",
Command: []string{"sh", "-c", "echo secret-command"},
Args: []string{"--token", "super-secret-token"},
Env: []corev1.EnvVar{
{Name: "PASSWORD", Value: "hunter2"},
{
Name: "API_KEY",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: "my-secret"},
Key: "api-key",
},
},
},
},
},
},
ImagePullSecrets: []corev1.LocalObjectReference{{Name: "my-pull-secret"}},
Volumes: []corev1.Volume{
{
Name: "secret-vol",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{SecretName: "my-secret"},
},
},
},
},
Status: corev1.PodStatus{
Phase: corev1.PodSucceeded,
ContainerStatuses: []corev1.ContainerStatus{
{
Name: "collector",
State: corev1.ContainerState{
Terminated: &corev1.ContainerStateTerminated{ExitCode: 0},
},
},
},
},
}
client := fake.NewSimpleClientset(pod)
collector := &troubleshootv1beta2.RunPod{
Name: "test-collector",
Namespace: "default",
}
result, err := savePodDetails(context.Background(), client, NewResult(), "", pod, collector)
require.NoError(t, err)
podKey := "test-collector/test-collector.json"
require.Contains(t, result, podKey)
saved := string(result[podKey])
// Debugging info must still be present
assert.Contains(t, saved, `"name": "test-run-pod"`)
assert.Contains(t, saved, `"phase": "Succeeded"`)
assert.Contains(t, saved, `"exitCode": 0`)
// Sensitive spec data must not be present
assert.NotContains(t, saved, "hunter2")
assert.NotContains(t, saved, "super-secret-token")
assert.NotContains(t, saved, "secret-command")
assert.NotContains(t, saved, "my-secret")
assert.NotContains(t, saved, "my-pull-secret")
assert.NotContains(t, saved, `"command":`)
assert.NotContains(t, saved, `"args":`)
assert.NotContains(t, saved, `"env":`)
// The saved JSON must still unmarshal into a Pod so downstream consumers
// (e.g. goldpinger) can read Name and Status.ContainerStatuses.
var savedPod corev1.Pod
require.NoError(t, json.Unmarshal(result[podKey], &savedPod))
assert.Equal(t, "test-run-pod", savedPod.Name)
assert.Equal(t, corev1.PodSucceeded, savedPod.Status.Phase)
assert.Len(t, savedPod.Status.ContainerStatuses, 1)
assert.Equal(t, int32(0), savedPod.Status.ContainerStatuses[0].State.Terminated.ExitCode)
assert.Empty(t, savedPod.Spec.Containers)
assert.Empty(t, savedPod.Spec.Volumes)
assert.Empty(t, savedPod.Spec.ImagePullSecrets)
}
+2 -1
View File
@@ -12,6 +12,7 @@ import (
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme"
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -147,7 +148,7 @@ func sonobuoyRetrieveResults(
Stdout: true,
Stderr: false,
}, scheme.ParameterCodec)
executor, err := remotecommand.NewSPDYExecutor(restConfig, "POST", req.URL())
executor, err := k8sutil.NewFallbackExecutor(restConfig, req.URL())
if err != nil {
return nil, ec, err
}
+43 -41
View File
@@ -23,47 +23,49 @@ const (
ANALYSIS_FILENAME = "analysis.json"
// Cluster Resources Collector Directories
CLUSTER_RESOURCES_DIR = "cluster-resources"
CLUSTER_RESOURCES_NAMESPACES = "namespaces"
CLUSTER_RESOURCES_AUTH_CANI = "auth-cani-list"
CLUSTER_RESOURCES_PODS = "pods"
CLUSTER_RESOURCES_PODS_LOGS = "pods/logs"
CLUSTER_RESOURCES_POD_DISRUPTION_BUDGETS = "pod-disruption-budgets"
CLUSTER_RESOURCES_SERVICES = "services"
CLUSTER_RESOURCES_DEPLOYMENTS = "deployments"
CLUSTER_RESOURCES_REPLICASETS = "replicasets"
CLUSTER_RESOURCES_STATEFULSETS = "statefulsets"
CLUSTER_RESOURCES_DAEMONSETS = "daemonsets"
CLUSTER_RESOURCES_JOBS = "jobs"
CLUSTER_RESOURCES_CRONJOBS = "cronjobs"
CLUSTER_RESOURCES_INGRESS = "ingress"
CLUSTER_RESOURCES_NETWORK_POLICY = "network-policy"
CLUSTER_RESOURCES_RESOURCE_QUOTA = "resource-quota"
CLUSTER_RESOURCES_STORAGE_CLASS = "storage-classes"
CLUSTER_RESOURCES_INGRESS_CLASS = "ingress-classes"
CLUSTER_RESOURCES_CUSTOM_RESOURCE_DEFINITIONS = "custom-resource-definitions"
CLUSTER_RESOURCES_CUSTOM_RESOURCES = "custom-resources"
CLUSTER_RESOURCES_IMAGE_PULL_SECRETS = "image-pull-secrets" // nolint:gosec
CLUSTER_RESOURCES_NODES = "nodes"
CLUSTER_RESOURCES_GROUPS = "groups"
CLUSTER_RESOURCES_RESOURCES = "resources"
CLUSTER_RESOURCES_LIMITRANGES = "limitranges"
CLUSTER_RESOURCES_EVENTS = "events"
CLUSTER_RESOURCES_PVS = "pvs"
CLUSTER_RESOURCES_PVCS = "pvcs"
CLUSTER_RESOURCES_ROLES = "roles"
CLUSTER_RESOURCES_ROLE_BINDINGS = "rolebindings"
CLUSTER_RESOURCES_CLUSTER_ROLES = "clusterroles"
CLUSTER_RESOURCES_CLUSTER_ROLE_BINDINGS = "clusterrolebindings"
CLUSTER_RESOURCES_PRIORITY_CLASS = "priorityclasses"
CLUSTER_RESOURCES_ENDPOINTS = "endpoints"
CLUSTER_RESOURCES_ENDPOINTSLICES = "endpointslices"
CLUSTER_RESOURCES_SERVICE_ACCOUNTS = "serviceaccounts"
CLUSTER_RESOURCES_LEASES = "leases"
CLUSTER_RESOURCES_VOLUME_ATTACHMENTS = "volumeattachments"
CLUSTER_RESOURCES_CONFIGMAPS = "configmaps"
CLUSTER_RESOURCES_REPLICATED_LICENSE = "license.json"
CLUSTER_RESOURCES_CERTIFICATE_SIGNING_REQUESTS = "certificatesigningrequests"
CLUSTER_RESOURCES_DIR = "cluster-resources"
CLUSTER_RESOURCES_NAMESPACES = "namespaces"
CLUSTER_RESOURCES_AUTH_CANI = "auth-cani-list"
CLUSTER_RESOURCES_PODS = "pods"
CLUSTER_RESOURCES_PODS_LOGS = "pods/logs"
CLUSTER_RESOURCES_POD_DISRUPTION_BUDGETS = "pod-disruption-budgets"
CLUSTER_RESOURCES_SERVICES = "services"
CLUSTER_RESOURCES_DEPLOYMENTS = "deployments"
CLUSTER_RESOURCES_REPLICASETS = "replicasets"
CLUSTER_RESOURCES_STATEFULSETS = "statefulsets"
CLUSTER_RESOURCES_DAEMONSETS = "daemonsets"
CLUSTER_RESOURCES_JOBS = "jobs"
CLUSTER_RESOURCES_CRONJOBS = "cronjobs"
CLUSTER_RESOURCES_INGRESS = "ingress"
CLUSTER_RESOURCES_NETWORK_POLICY = "network-policy"
CLUSTER_RESOURCES_RESOURCE_QUOTA = "resource-quota"
CLUSTER_RESOURCES_STORAGE_CLASS = "storage-classes"
CLUSTER_RESOURCES_CUSTOM_RESOURCE_DEFINITIONS = "custom-resource-definitions"
CLUSTER_RESOURCES_CUSTOM_RESOURCES = "custom-resources"
CLUSTER_RESOURCES_IMAGE_PULL_SECRETS = "image-pull-secrets" // nolint:gosec
CLUSTER_RESOURCES_NODES = "nodes"
CLUSTER_RESOURCES_GROUPS = "groups"
CLUSTER_RESOURCES_RESOURCES = "resources"
CLUSTER_RESOURCES_LIMITRANGES = "limitranges"
CLUSTER_RESOURCES_EVENTS = "events"
CLUSTER_RESOURCES_PVS = "pvs"
CLUSTER_RESOURCES_PVCS = "pvcs"
CLUSTER_RESOURCES_ROLES = "roles"
CLUSTER_RESOURCES_ROLE_BINDINGS = "rolebindings"
CLUSTER_RESOURCES_CLUSTER_ROLES = "clusterroles"
CLUSTER_RESOURCES_CLUSTER_ROLE_BINDINGS = "clusterrolebindings"
CLUSTER_RESOURCES_PRIORITY_CLASS = "priorityclasses"
CLUSTER_RESOURCES_ENDPOINTS = "endpoints"
CLUSTER_RESOURCES_ENDPOINTSLICES = "endpointslices"
CLUSTER_RESOURCES_SERVICE_ACCOUNTS = "serviceaccounts"
CLUSTER_RESOURCES_LEASES = "leases"
CLUSTER_RESOURCES_VOLUME_ATTACHMENTS = "volumeattachments"
CLUSTER_RESOURCES_CONFIGMAPS = "configmaps"
CLUSTER_RESOURCES_REPLICATED_LICENSE = "license.json"
CLUSTER_RESOURCES_CERTIFICATE_SIGNING_REQUESTS = "certificatesigningrequests"
CLUSTER_RESOURCES_INGRESS_CLASS = "ingress-classes"
CLUSTER_RESOURCES_VALIDATING_WEBHOOK_CONFIGURATIONS = "validating-webhook-configurations"
CLUSTER_RESOURCES_MUTATING_WEBHOOK_CONFIGURATIONS = "mutating-webhook-configurations"
// SelfSubjectRulesReview evaluation responses
SELFSUBJECTRULESREVIEW_ERROR_AUTHORIZATION_WEBHOOK_UNSUPPORTED = "webhook authorizer does not support user rule resolution"
+27
View File
@@ -0,0 +1,27 @@
package k8sutil
import (
"net/url"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/remotecommand"
"k8s.io/streaming/pkg/httpstream"
)
// NewFallbackExecutor creates an executor that tries WebSocket first and falls
// back to SPDY if the server does not support it. Use this in place of
// remotecommand.NewSPDYExecutor everywhere.
func NewFallbackExecutor(config *restclient.Config, u *url.URL) (remotecommand.Executor, error) {
// WebSocket upgrade requires GET per RFC 6455; SPDY uses POST.
wsExec, err := remotecommand.NewWebSocketExecutor(config, "GET", u.String())
if err != nil {
return nil, err
}
spdyExec, err := remotecommand.NewSPDYExecutor(config, "POST", u)
if err != nil {
return nil, err
}
return remotecommand.NewFallbackExecutor(wsExec, spdyExec, func(err error) bool {
return httpstream.IsUpgradeFailure(err) || httpstream.IsHTTPSProxyError(err)
})
}
+19
View File
@@ -0,0 +1,19 @@
package k8sutil
import (
"net/url"
"testing"
"github.com/stretchr/testify/require"
restclient "k8s.io/client-go/rest"
)
func TestNewFallbackExecutor(t *testing.T) {
config := &restclient.Config{Host: "http://localhost:8080"}
u, err := url.Parse("http://localhost:8080/api/v1/namespaces/default/pods/foo/exec")
require.NoError(t, err)
exec, err := NewFallbackExecutor(config, u)
require.NoError(t, err)
require.NotNil(t, exec)
}
-72
View File
@@ -1,72 +0,0 @@
package k8sutil
import (
"bytes"
"fmt"
"net/http"
"net/url"
"strings"
"time"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/portforward"
"k8s.io/client-go/transport/spdy"
)
func PortForward(config *restclient.Config, localPort int, remotePort int, namespace string, podName string) (chan struct{}, error) {
roundTripper, upgrader, err := spdy.RoundTripperFor(config)
if err != nil {
return nil, err
}
path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/portforward", namespace, podName)
hostIP := strings.TrimLeft(config.Host, "htps:/")
serverURL := url.URL{Scheme: "http", Path: path, Host: hostIP}
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: roundTripper}, http.MethodPost, &serverURL)
stopChan, readyChan := make(chan struct{}, 1), make(chan struct{}, 1)
out, errOut := new(bytes.Buffer), new(bytes.Buffer)
forwarder, err := portforward.New(dialer, []string{fmt.Sprintf("%d:%d", localPort, remotePort)}, stopChan, readyChan, out, errOut)
if err != nil {
return nil, err
}
go func() {
for range readyChan { // Kubernetes will close this channel when it has something to tell us.
}
if errOut.String() != "" {
panic(errOut.String())
} else if out.String() != "" {
// fmt.Println(out.String())
}
}()
go func() error {
if err = forwarder.ForwardPorts(); err != nil { // Locks until stopChan is closed.
panic(err)
}
return nil
}()
// Block until the new service is responding, limited to (math) seconds
quickClient := &http.Client{
Timeout: time.Millisecond * 200,
}
start := time.Now()
for {
response, err := quickClient.Get(fmt.Sprintf("http://localhost:%d", localPort))
if err == nil && response.StatusCode == http.StatusOK {
break
}
if time.Now().Sub(start) > time.Duration(time.Second*5) {
return nil, err
}
time.Sleep(time.Millisecond * 100)
}
return stopChan, nil
}
+3
View File
@@ -386,6 +386,9 @@ func v1beta3SpecRequiresClient(spec *troubleshootv1beta3.SupportBundleSpec) bool
if c.Redis != nil && databaseRequiresClient(c.Redis) {
return true
}
if c.ClickHouse != nil && databaseRequiresClient(c.ClickHouse) {
return true
}
}
return false
+39 -42
View File
@@ -9,19 +9,23 @@ import (
"path/filepath"
"strings"
longhornns "github.com/longhorn/go-common-libs/ns"
longhornproc "github.com/longhorn/go-common-libs/proc"
lhtypes "github.com/longhorn/go-common-libs/types"
"github.com/pkg/errors"
"k8s.io/klog/v2"
iscsi_util "github.com/longhorn/go-iscsi-helper/util"
)
func newHostNamespaceExecutor() (*longhornns.Executor, error) {
return longhornns.NewNamespaceExecutor(lhtypes.ProcessNone, HostProcPath, []lhtypes.Namespace{lhtypes.NamespaceMnt})
}
func GetDiskInfo(directory string) (info *DiskInfo, err error) {
defer func() {
err = errors.Wrapf(err, "cannot get disk info of directory %v", directory)
}()
initiatorNSPath := iscsi_util.GetHostNamespacePath(HostProcPath)
mountPath := fmt.Sprintf("--mount=%s/mnt", initiatorNSPath)
output, err := Execute([]string{}, "nsenter", mountPath, "stat", "-fc", "{\"path\":\"%n\",\"fsid\":\"%i\",\"type\":\"%T\",\"freeBlock\":%f,\"totalBlock\":%b,\"blockSize\":%S}", directory)
nsDir := longhornproc.GetHostNamespaceDirectory(HostProcPath)
output, err := Execute([]string{}, "nsenter", "--mount="+nsDir+"/mnt", "stat", "-fc", "{\"path\":\"%n\",\"fsid\":\"%i\",\"type\":\"%T\",\"freeBlock\":%f,\"totalBlock\":%b,\"blockSize\":%S}", directory)
if err != nil {
return nil, err
}
@@ -51,17 +55,16 @@ func RemoveHostDirectoryContent(directory string) (err error) {
if strings.Count(dir, "/") < 2 {
return fmt.Errorf("prohibit removing the top level of directory %v", dir)
}
initiatorNSPath := iscsi_util.GetHostNamespacePath(HostProcPath)
nsExec, err := iscsi_util.NewNamespaceExecutor(initiatorNSPath)
nsExec, err := newHostNamespaceExecutor()
if err != nil {
return err
}
// check if the directory already deleted
if _, err := nsExec.Execute("ls", []string{dir}); err != nil {
if _, err := nsExec.Execute(nil, "ls", []string{dir}, lhtypes.ExecuteDefaultTimeout); err != nil {
klog.Warningf("cannot find host directory %v for removal", dir)
return nil
}
if _, err := nsExec.Execute("rm", []string{"-rf", dir}); err != nil {
if _, err := nsExec.Execute(nil, "rm", []string{"-rf", dir}, lhtypes.ExecuteDefaultTimeout); err != nil {
return err
}
return nil
@@ -84,35 +87,33 @@ func CopyHostDirectoryContent(src, dest string) (err error) {
return fmt.Errorf("prohibit copying the content for the top level of directory %v or %v", srcDir, destDir)
}
initiatorNSPath := iscsi_util.GetHostNamespacePath(HostProcPath)
nsExec, err := iscsi_util.NewNamespaceExecutor(initiatorNSPath)
nsExec, err := newHostNamespaceExecutor()
if err != nil {
return err
}
// There can be no src directory, hence returning nil is fine.
if _, err := nsExec.Execute("bash", []string{"-c", fmt.Sprintf("ls %s", filepath.Join(srcDir, "*"))}); err != nil {
if _, err := nsExec.Execute(nil, "bash", []string{"-c", fmt.Sprintf("ls %s", filepath.Join(srcDir, "*"))}, lhtypes.ExecuteDefaultTimeout); err != nil {
klog.V(2).Infof("cannot list the content of the src directory %v for the copy, will do nothing: %v", srcDir, err)
return nil
}
// Check if the dest directory exists.
if _, err := nsExec.Execute("mkdir", []string{"-p", destDir}); err != nil {
if _, err := nsExec.Execute(nil, "mkdir", []string{"-p", destDir}, lhtypes.ExecuteDefaultTimeout); err != nil {
return err
}
// The flag `-n` means not overwriting an existing file.
if _, err := nsExec.Execute("bash", []string{"-c", fmt.Sprintf("cp -an %s %s", filepath.Join(srcDir, "*"), destDir)}); err != nil {
if _, err := nsExec.Execute(nil, "bash", []string{"-c", fmt.Sprintf("cp -an %s %s", filepath.Join(srcDir, "*"), destDir)}, lhtypes.ExecuteDefaultTimeout); err != nil {
return err
}
return nil
}
func CreateDiskPathReplicaSubdirectory(path string) error {
nsPath := iscsi_util.GetHostNamespacePath(HostProcPath)
nsExec, err := iscsi_util.NewNamespaceExecutor(nsPath)
nsExec, err := newHostNamespaceExecutor()
if err != nil {
return err
}
if _, err := nsExec.Execute("mkdir", []string{"-p", filepath.Join(path, ReplicaDirectory)}); err != nil {
if _, err := nsExec.Execute(nil, "mkdir", []string{"-p", filepath.Join(path, ReplicaDirectory)}, lhtypes.ExecuteDefaultTimeout); err != nil {
return errors.Wrapf(err, "error creating data path %v on host", path)
}
@@ -120,22 +121,22 @@ func CreateDiskPathReplicaSubdirectory(path string) error {
}
func DeleteDiskPathReplicaSubdirectoryAndDiskCfgFile(
nsExec *iscsi_util.NamespaceExecutor, path string) error {
nsExec *longhornns.Executor, path string) error {
var err error
dirPath := filepath.Join(path, ReplicaDirectory)
filePath := filepath.Join(path, DiskConfigFile)
// Check if the replica directory exist, delete it
if _, err := nsExec.Execute("ls", []string{dirPath}); err == nil {
if _, err := nsExec.Execute("rmdir", []string{dirPath}); err != nil {
if _, err := nsExec.Execute(nil, "ls", []string{dirPath}, lhtypes.ExecuteDefaultTimeout); err == nil {
if _, err := nsExec.Execute(nil, "rmdir", []string{dirPath}, lhtypes.ExecuteDefaultTimeout); err != nil {
return errors.Wrapf(err, "error deleting data path %v on host", path)
}
}
// Check if the disk cfg file exist, delete it
if _, err := nsExec.Execute("ls", []string{filePath}); err == nil {
if _, err := nsExec.Execute("rm", []string{filePath}); err != nil {
if _, err := nsExec.Execute(nil, "ls", []string{filePath}, lhtypes.ExecuteDefaultTimeout); err == nil {
if _, err := nsExec.Execute(nil, "rm", []string{filePath}, lhtypes.ExecuteDefaultTimeout); err != nil {
err = errors.Wrapf(err, "error deleting disk cfg file %v on host", filePath)
}
}
@@ -145,8 +146,7 @@ func DeleteDiskPathReplicaSubdirectoryAndDiskCfgFile(
func ExpandFileSystem(volumeName string) (err error) {
devicePath := filepath.Join(DeviceDirectory, volumeName)
nsPath := iscsi_util.GetHostNamespacePath(HostProcPath)
nsExec, err := iscsi_util.NewNamespaceExecutor(nsPath)
nsExec, err := newHostNamespaceExecutor()
if err != nil {
return err
}
@@ -162,7 +162,7 @@ func ExpandFileSystem(volumeName string) (err error) {
// make sure there is a mount point for the volume before file system expansion
tmpMountNeeded := true
mountPoint := ""
mountRes, err := nsExec.Execute("bash", []string{"-c", "mount | grep \"/" + volumeName + " \" | awk '{print $3}'"})
mountRes, err := nsExec.Execute(nil, "bash", []string{"-c", "mount | grep \"/" + volumeName + " \" | awk '{print $3}'"}, lhtypes.ExecuteDefaultTimeout)
if err != nil {
klog.Warningf("failed to use command mount to get the mount info of volume %v, consider the volume as unmounted: %v", volumeName, err)
} else {
@@ -185,10 +185,10 @@ func ExpandFileSystem(volumeName string) (err error) {
if tmpMountNeeded {
mountPoint = filepath.Join(TemporaryMountPointDirectory, volumeName)
klog.V(2).Infof("The volume %v is unmounted, hence it will be temporarily mounted on %v for file system expansion", volumeName, mountPoint)
if _, err := nsExec.Execute("mkdir", []string{"-p", mountPoint}); err != nil {
if _, err := nsExec.Execute(nil, "mkdir", []string{"-p", mountPoint}, lhtypes.ExecuteDefaultTimeout); err != nil {
return errors.Wrapf(err, "failed to create a temporary mount point %v before file system expansion", mountPoint)
}
if _, err := nsExec.Execute("mount", []string{devicePath, mountPoint}); err != nil {
if _, err := nsExec.Execute(nil, "mount", []string{devicePath, mountPoint}, lhtypes.ExecuteDefaultTimeout); err != nil {
return errors.Wrapf(err, "failed to temporarily mount volume %v on %v before file system expansion", volumeName, mountPoint)
}
}
@@ -199,11 +199,11 @@ func ExpandFileSystem(volumeName string) (err error) {
case "ext3":
fallthrough
case "ext4":
if _, err = nsExec.Execute("resize2fs", []string{devicePath}); err != nil {
if _, err = nsExec.Execute(nil, "resize2fs", []string{devicePath}, lhtypes.ExecuteDefaultTimeout); err != nil {
return err
}
case "xfs":
if _, err = nsExec.Execute("xfs_growfs", []string{mountPoint}); err != nil {
if _, err = nsExec.Execute(nil, "xfs_growfs", []string{mountPoint}, lhtypes.ExecuteDefaultTimeout); err != nil {
return err
}
default:
@@ -212,10 +212,10 @@ func ExpandFileSystem(volumeName string) (err error) {
// cleanup
if tmpMountNeeded {
if _, err := nsExec.Execute("umount", []string{mountPoint}); err != nil {
if _, err := nsExec.Execute(nil, "umount", []string{mountPoint}, lhtypes.ExecuteDefaultTimeout); err != nil {
return errors.Wrapf(err, "failed to unmount volume %v on the temporary mount point %v after file system expansion", volumeName, mountPoint)
}
if _, err := nsExec.Execute("rm", []string{"-r", mountPoint}); err != nil {
if _, err := nsExec.Execute(nil, "rm", []string{"-r", mountPoint}, lhtypes.ExecuteDefaultTimeout); err != nil {
return errors.Wrapf(err, "failed to remove the temporary mount point %v after file system expansion", mountPoint)
}
}
@@ -225,8 +225,7 @@ func ExpandFileSystem(volumeName string) (err error) {
func DetectFileSystem(volumeName string) (string, error) {
devicePath := filepath.Join(DeviceDirectory, volumeName)
nsPath := iscsi_util.GetHostNamespacePath(HostProcPath)
nsExec, err := iscsi_util.NewNamespaceExecutor(nsPath)
nsExec, err := newHostNamespaceExecutor()
if err != nil {
return "", err
}
@@ -235,7 +234,7 @@ func DetectFileSystem(volumeName string) (string, error) {
// For filesystem `btrfs`, the schema is: `<device path>: UUID="<filesystem UUID>" UUID_SUB="<filesystem UUID_SUB>" TYPE="<filesystem type>"`
// For filesystem `ext4` or `xfs`, the schema is: `<device path>: UUID="<filesystem UUID>" TYPE="<filesystem type>"`
cmd := fmt.Sprintf("blkid %s | sed 's/.*TYPE=//g'", devicePath)
output, err := nsExec.Execute("bash", []string{"-c", cmd})
output, err := nsExec.Execute(nil, "bash", []string{"-c", cmd}, lhtypes.ExecuteDefaultTimeout)
if err != nil {
return "", errors.Wrapf(err, "failed to get the file system info for volume %v, maybe there is no Linux file system on the volume", volumeName)
}
@@ -247,13 +246,12 @@ func DetectFileSystem(volumeName string) (string, error) {
}
func GetDiskConfig(path string) (*DiskConfig, error) {
nsPath := iscsi_util.GetHostNamespacePath(HostProcPath)
nsExec, err := iscsi_util.NewNamespaceExecutor(nsPath)
nsExec, err := newHostNamespaceExecutor()
if err != nil {
return nil, err
}
filePath := filepath.Join(path, DiskConfigFile)
output, err := nsExec.Execute("cat", []string{filePath})
output, err := nsExec.Execute(nil, "cat", []string{filePath}, lhtypes.ExecuteDefaultTimeout)
if err != nil {
return nil, fmt.Errorf("cannot find config file %v on host: %v", filePath, err)
}
@@ -274,13 +272,12 @@ func GenerateDiskConfig(path string) (*DiskConfig, error) {
return nil, fmt.Errorf("BUG: Cannot marshal %+v: %v", cfg, err)
}
nsPath := iscsi_util.GetHostNamespacePath(HostProcPath)
nsExec, err := iscsi_util.NewNamespaceExecutor(nsPath)
nsExec, err := newHostNamespaceExecutor()
if err != nil {
return nil, err
}
filePath := filepath.Join(path, DiskConfigFile)
if _, err := nsExec.Execute("ls", []string{filePath}); err == nil {
if _, err := nsExec.Execute(nil, "ls", []string{filePath}, lhtypes.ExecuteDefaultTimeout); err == nil {
return nil, fmt.Errorf("disk cfg on %v exists, cannot override", filePath)
}
@@ -293,13 +290,13 @@ func GenerateDiskConfig(path string) (*DiskConfig, error) {
}
}()
if _, err := nsExec.ExecuteWithStdin("dd", []string{"of=" + filePath}, string(encoded)); err != nil {
if _, err := nsExec.ExecuteWithStdin(nil, "dd", []string{"of=" + filePath}, string(encoded), lhtypes.ExecuteDefaultTimeout); err != nil {
return nil, fmt.Errorf("cannot write to disk cfg on %v: %v", filePath, err)
}
if err := CreateDiskPathReplicaSubdirectory(path); err != nil {
return nil, err
}
if _, err := nsExec.Execute("sync", []string{filePath}); err != nil {
if _, err := nsExec.Execute(nil, "sync", []string{filePath}, lhtypes.ExecuteDefaultTimeout); err != nil {
return nil, fmt.Errorf("cannot sync disk cfg on %v: %v", filePath, err)
}
+40
View File
@@ -148,6 +148,21 @@ func CollectHostWithContext(
// The values of map entries will contain the collected data in bytes if the data was not stored to disk
collectResult.AllCollectedData = allCollectedData
// Local host preflight collectors are the only collection path (cluster,
// remote host, in-cluster support bundle) that skipped redaction entirely.
// A `run` collector's captured environment in particular can carry
// credentials verbatim (e.g. HTTPS_PROXY with embedded Basic Auth) into
// the bundle. See https://github.com/replicatedhq/troubleshoot/issues/2100.
_, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, "Host collectors")
span.SetAttributes(attribute.String("type", "Redactors"))
if err := collect.RedactResult(opts.BundlePath, collect.CollectorResult(allCollectedData), nil); err != nil {
err = errors.Wrap(err, "failed to redact host collector results")
span.SetStatus(codes.Error, err.Error())
span.End()
return collectResult, err
}
span.End()
return collectResult, nil
}
@@ -245,6 +260,8 @@ func CollectWithContext(ctx context.Context, opts CollectOpts, p *troubleshootv1
// move Copy Collectors if any to the end of the execution list
allCollectors = collect.EnsureCopyLast(allCollectors)
var skippedCollectors []collect.SkippedCollector
for i, collector := range allCollectors {
_, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, collector.Title())
span.SetAttributes(attribute.String("type", reflect.TypeOf(collector).String()))
@@ -254,6 +271,13 @@ func CollectWithContext(ctx context.Context, opts CollectOpts, p *troubleshootv1
klog.V(1).Infof("excluding %q collector", collector.Title())
span.SetAttributes(attribute.Bool(constants.EXCLUDED, true))
span.End()
skippedCollectors = append(skippedCollectors, collect.SkippedCollector{
Collector: collector.Title(),
Reason: "excluded",
Timestamp: time.Now().Format(time.RFC3339),
})
continue
}
@@ -270,6 +294,19 @@ func CollectWithContext(ctx context.Context, opts CollectOpts, p *troubleshootv1
}
span.SetStatus(codes.Error, "skipping collector, insufficient RBAC permissions")
span.End()
rbacErrors := collector.GetRBACErrors()
errorMessages := make([]string, 0, len(rbacErrors))
for _, e := range rbacErrors {
errorMessages = append(errorMessages, e.Error())
}
skippedCollectors = append(skippedCollectors, collect.SkippedCollector{
Collector: collector.Title(),
Reason: "insufficient RBAC permissions",
Errors: errorMessages,
Timestamp: time.Now().Format(time.RFC3339),
})
continue
}
}
@@ -320,6 +357,9 @@ func CollectWithContext(ctx context.Context, opts CollectOpts, p *troubleshootv1
span.End()
}
// Write skipped collectors manifest so users can see what was missed
collect.WriteSkippedCollectors(skippedCollectors, allCollectedData, opts.BundlePath)
// The values of map entries will contain the collected data in bytes if the data was not stored to disk
collectResult.AllCollectedData = allCollectedData
+46
View File
@@ -1,6 +1,9 @@
package preflight
import (
"context"
"os"
"path/filepath"
"reflect"
"testing"
@@ -209,3 +212,46 @@ func TestCollectWithContext_PreservesOrderAfterClusterResources(t *testing.T) {
assert.Less(t, dataIndex, secretIndex, "data collectors should come before secret collectors, preserving relative order")
}
}
// TestCollectHostWithContext_RedactsSensitiveEnvValues verifies that a `run`
// host collector's captured environment is redacted before being written to
// the bundle. CollectHostWithContext is the only collection path (cluster,
// host, remote) that skipped redaction entirely: every env var passed to the
// command -- including credentials embedded in a proxy URL -- landed
// verbatim in <collectorName>-info.json.
func TestCollectHostWithContext_RedactsSensitiveEnvValues(t *testing.T) {
bundlePath := t.TempDir()
hostPreflight := &troubleshootv1beta2.HostPreflight{
Spec: troubleshootv1beta2.HostPreflightSpec{
Collectors: []*troubleshootv1beta2.HostCollect{
{
HostRun: &troubleshootv1beta2.HostRun{
HostCollectorMeta: troubleshootv1beta2.HostCollectorMeta{
CollectorName: "proxy-credential-check",
},
Command: "sh",
Args: []string{"-c", "echo ok"},
IgnoreParentEnvs: true,
Env: []string{
"HTTPS_PROXY=http://alice:sw0rdfish@proxy.example.com:3128",
},
},
},
},
},
}
_, err := CollectHostWithContext(context.Background(), CollectOpts{
ProgressChan: make(chan interface{}, 100),
BundlePath: bundlePath,
}, hostPreflight)
require.NoError(t, err)
infoPath := filepath.Join(bundlePath, "host-collectors/run-host/proxy-credential-check-info.json")
b, err := os.ReadFile(infoPath)
require.NoError(t, err)
assert.NotContains(t, string(b), "sw0rdfish", "proxy credential leaked unredacted into the host preflight bundle")
assert.Contains(t, string(b), "***HIDDEN***", "expected the built-in URL-userinfo redactor to mask the credential")
}
+31 -32
View File
@@ -35,6 +35,32 @@ func init() {
}
}
// kurlInstallerRedactors are built-in redactors scoped to the kurl installer
// custom resource. They previously applied only to the YAML copy of custom
// resources; now they target the JSON file (and the YAML symlink that points to it).
// The installer CRD can be either cluster-scoped (installers.cluster.kurl.sh.json)
// or namespaced (installers.cluster.kurl.sh/<namespace>.json), so both patterns are
// included.
var kurlInstallerRedactors = []*troubleshootv1beta2.Redact{
{
Name: "Redact kurl installer fields",
FileSelector: troubleshootv1beta2.FileSelector{
Files: []string{
fmt.Sprintf("%s/%s/%s.json", constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_CUSTOM_RESOURCES, "installers.cluster.kurl.sh"),
fmt.Sprintf("%s/%s/%s/*.json", constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_CUSTOM_RESOURCES, "installers.cluster.kurl.sh"),
},
},
Removals: troubleshootv1beta2.Removals{
Regex: []troubleshootv1beta2.Regex{
{Redactor: `(?i)("bootstrapToken"\s*:\s*")(?P<mask>[^"]*)(")`},
{Redactor: `(?i)("certKey"\s*:\s*")(?P<mask>[^"]*)(")`},
{Redactor: `(?i)("kubeadmToken"\s*:\s*")(?P<mask>[^"]*)(")`},
{Redactor: `(?i)("kubectl\.kubernetes\.io/last-applied-configuration"\s*:\s*")(?P<mask>(?:\\.|[^"\\])*)(")`},
},
},
},
}
// A regex cache to avoid recompiling the same regexes over and over
func compileRegex(pattern string) (*regexp.Regexp, error) {
regexCacheLock.Lock()
@@ -440,39 +466,12 @@ func getRedactors(path string) ([]Redactor, error) {
redactors = append(redactors, r)
}
customResources := []struct {
resource string
yamlPath string
}{
{
resource: "installers.cluster.kurl.sh",
yamlPath: "*.spec.kubernetes.bootstrapToken",
},
{
resource: "installers.cluster.kurl.sh",
yamlPath: "*.spec.kubernetes.certKey",
},
{
resource: "installers.cluster.kurl.sh",
yamlPath: "*.spec.kubernetes.kubeadmToken",
},
}
uniqueCRs := map[string]bool{}
for _, cr := range customResources {
fileglob := fmt.Sprintf("%s/%s/%s/*.yaml", constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_CUSTOM_RESOURCES, cr.resource)
redactors = append(redactors, NewYamlRedactor(cr.yamlPath, fileglob, ""))
// redact kubectl last applied annotation once for each resource since it contains copies of
// redacted fields
if !uniqueCRs[cr.resource] {
uniqueCRs[cr.resource] = true
redactors = append(redactors, &YamlRedactor{
filePath: fileglob,
maskPath: []string{"*", "metadata", "annotations", "kubectl.kubernetes.io/last-applied-configuration"},
})
}
// Add built-in redactors that are scoped to specific custom resource files.
scopedRedactors, err := buildAdditionalRedactors(path, kurlInstallerRedactors)
if err != nil {
return nil, err
}
redactors = append(redactors, scopedRedactors...)
return redactors, nil
}
+46
View File
@@ -1874,3 +1874,49 @@ func Test_redactMatchesPath(t *testing.T) {
})
}
}
func Test_RedactKurlInstallerJSON(t *testing.T) {
input := `[
{
"metadata": {
"name": "kurl",
"annotations": {
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"cluster.kurl.sh/v1beta1\",\"kind\":\"Installer\",\"spec\":{\"kubernetes\":{\"bootstrapToken\":\"abc\",\"certKey\":\"def\",\"kubeadmToken\":\"ghi\"}}}"
}
},
"spec": {
"kubernetes": {
"bootstrapToken": "abc",
"certKey": "def",
"kubeadmToken": "ghi"
}
}
}
]`
cases := []string{
// Cluster-scoped installer (file directly under custom-resources)
"cluster-resources/custom-resources/installers.cluster.kurl.sh.json",
// Namespaced installer (file under a per-CRD directory)
"cluster-resources/custom-resources/installers.cluster.kurl.sh/default.json",
}
for _, path := range cases {
t.Run(path, func(t *testing.T) {
r, err := Redact(strings.NewReader(input), path, nil)
require.NoError(t, err)
out, err := ioutil.ReadAll(r)
require.NoError(t, err)
outStr := string(out)
require.NotContains(t, outStr, `"abc"`)
require.NotContains(t, outStr, `"def"`)
require.NotContains(t, outStr, `"ghi"`)
require.Contains(t, outStr, `"bootstrapToken": "***HIDDEN***"`)
require.Contains(t, outStr, `"certKey": "***HIDDEN***"`)
require.Contains(t, outStr, `"kubeadmToken": "***HIDDEN***"`)
require.Contains(t, outStr, `"kubectl.kubernetes.io/last-applied-configuration": "***HIDDEN***"`)
})
}
}
+30 -5
View File
@@ -19,6 +19,7 @@ import (
"github.com/replicatedhq/troubleshoot/pkg/collect"
"github.com/replicatedhq/troubleshoot/pkg/constants"
"github.com/replicatedhq/troubleshoot/pkg/convert"
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
"github.com/replicatedhq/troubleshoot/pkg/redact"
"github.com/replicatedhq/troubleshoot/pkg/version"
"go.opentelemetry.io/otel"
@@ -39,9 +40,8 @@ import (
)
const (
selectorLabelKey = "ds-selector-label"
selectorLabelValue = "remote-host-collector"
defaultTimeout = 30
selectorLabelKey = "ds-selector-label"
defaultTimeout = 30
)
func runHostCollectors(ctx context.Context, hostCollectors []*troubleshootv1beta2.HostCollect, additionalRedactors *troubleshootv1beta2.Redactor, bundlePath string, opts SupportBundleCreateOpts) (collect.CollectorResult, error) {
@@ -107,7 +107,7 @@ 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)
allCollectedData := map[string][]byte{}
for _, desiredCollector := range collectSpecs {
if collectorInterface, ok := collect.GetCollector(desiredCollector, bundlePath, opts.Namespace, opts.KubernetesRestConfig, k8sClient, opts.SinceTime); ok {
@@ -155,6 +155,8 @@ func runCollectors(ctx context.Context, collectors []*troubleshootv1beta2.Collec
// move Copy Collectors if any to the end of the execution list
allCollectors = collect.EnsureCopyLast(allCollectors)
var skippedCollectors []collect.SkippedCollector
for _, collector := range allCollectors {
_, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, collector.Title())
span.SetAttributes(attribute.String("type", reflect.TypeOf(collector).String()))
@@ -165,6 +167,13 @@ func runCollectors(ctx context.Context, collectors []*troubleshootv1beta2.Collec
opts.CollectorProgressCallback(opts.ProgressChan, msg)
span.SetAttributes(attribute.Bool(constants.EXCLUDED, true))
span.End()
skippedCollectors = append(skippedCollectors, collect.SkippedCollector{
Collector: collector.Title(),
Reason: "excluded",
Timestamp: time.Now().Format(time.RFC3339),
})
continue
}
@@ -175,6 +184,19 @@ func runCollectors(ctx context.Context, collectors []*troubleshootv1beta2.Collec
opts.CollectorProgressCallback(opts.ProgressChan, msg)
span.SetStatus(codes.Error, "skipping collector, insufficient RBAC permissions")
span.End()
rbacErrors := collector.GetRBACErrors()
errorMessages := make([]string, 0, len(rbacErrors))
for _, e := range rbacErrors {
errorMessages = append(errorMessages, e.Error())
}
skippedCollectors = append(skippedCollectors, collect.SkippedCollector{
Collector: collector.Title(),
Reason: "insufficient RBAC permissions",
Errors: errorMessages,
Timestamp: time.Now().Format(time.RFC3339),
})
continue
}
}
@@ -207,6 +229,9 @@ func runCollectors(ctx context.Context, collectors []*troubleshootv1beta2.Collec
span.End()
}
// Write skipped collectors manifest to the bundle so users can see what was missed
collect.WriteSkippedCollectors(skippedCollectors, allCollectedData, bundlePath)
collectResult := allCollectedData
globalRedactors := []*troubleshootv1beta2.Redact{}
@@ -343,7 +368,7 @@ func getExecOutputs(
TTY: false,
}, parameterCodec)
exec, err := remotecommand.NewSPDYExecutor(clientConfig, "POST", req.URL())
exec, err := k8sutil.NewFallbackExecutor(clientConfig, req.URL())
if err != nil {
return nil, nil, err
}
+29 -21
View File
@@ -34,6 +34,11 @@ func ExtractLicenseFromBundle(bundlePath string) (string, string, error) {
tarReader := tar.NewReader(gzReader)
// Collect results from both sources in a single pass; license.json takes priority
// regardless of its position in the tar, since configmaps often appear earlier.
var licenseJSONID, licenseJSONSlug string
var configmapID, configmapSlug string
for {
header, err := tarReader.Next()
if err == io.EOF {
@@ -43,50 +48,47 @@ 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 {
if header.Typeflag != tar.TypeReg {
continue
}
// First priority: cluster-resources/license.json
if strings.Contains(header.Name, "cluster-resources/license.json") {
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
licenseJSONID = licenseData.LicenseID
licenseJSONSlug = licenseData.AppSlug
}
}
continue
}
// Fallback: process files in cluster-resources/configmaps/
// Fallback: cluster-resources/configmaps/
if configmapID != "" {
continue // already have a configmap result
}
if !strings.Contains(header.Name, "cluster-resources/configmaps/") {
continue
}
// Skip directories
if header.Typeflag != tar.TypeReg {
continue
}
// Process .yaml, .yml, and .json files
if !strings.HasSuffix(header.Name, ".yaml") &&
!strings.HasSuffix(header.Name, ".yml") &&
!strings.HasSuffix(header.Name, ".json") {
continue
}
// Read the file content
content := make([]byte, header.Size)
if _, err := io.ReadFull(tarReader, content); err != nil {
continue // Skip files we can't read
continue
}
// Try to extract license from this configmap
var license string
if strings.HasSuffix(header.Name, ".json") {
license = extractLicenseFromJSON(content)
@@ -95,15 +97,21 @@ func ExtractLicenseFromBundle(bundlePath string) (string, string, error) {
}
if license != "" {
// Extract app slug from filename
filename := filepath.Base(header.Name)
appSlug := strings.TrimSuffix(filename, ".json")
appSlug = strings.TrimSuffix(appSlug, ".yaml")
appSlug = strings.TrimSuffix(appSlug, ".yml")
return license, appSlug, nil
slug := strings.TrimSuffix(filename, ".json")
slug = strings.TrimSuffix(slug, ".yaml")
slug = strings.TrimSuffix(slug, ".yml")
configmapID = license
configmapSlug = slug
}
}
if licenseJSONID != "" {
return licenseJSONID, licenseJSONSlug, nil
}
if configmapID != "" {
return configmapID, configmapSlug, nil
}
return "", "", nil // No license found
}
+139
View File
@@ -0,0 +1,139 @@
package supportbundle
import (
"archive/tar"
"compress/gzip"
"os"
"path/filepath"
"testing"
)
// makeBundleTarGz creates a temporary .tar.gz file containing the given entries.
// Each entry is a (path, content) pair. Returns the file path; caller must remove it.
func makeBundleTarGz(t *testing.T, entries []struct{ name, content string }) string {
t.Helper()
f, err := os.CreateTemp(t.TempDir(), "bundle-*.tar.gz")
if err != nil {
t.Fatal(err)
}
defer f.Close()
gw := gzip.NewWriter(f)
tw := tar.NewWriter(gw)
for _, e := range entries {
hdr := &tar.Header{
Name: e.name,
Typeflag: tar.TypeReg,
Size: int64(len(e.content)),
Mode: 0644,
}
if err := tw.WriteHeader(hdr); err != nil {
t.Fatal(err)
}
if _, err := tw.Write([]byte(e.content)); err != nil {
t.Fatal(err)
}
}
if err := tw.Close(); err != nil {
t.Fatal(err)
}
if err := gw.Close(); err != nil {
t.Fatal(err)
}
return f.Name()
}
func TestExtractLicenseFromBundle_PrefersLicenseJSONOverConfigmap(t *testing.T) {
// The configmap appears first in the tar but license.json should win.
configmapContent := `{
"kind": "ConfigMapList",
"apiVersion": "v1",
"items": [{
"data": {
"license": "configmapLicenseIDAAAAAAAAAAAA"
}
}]
}`
licenseJSONContent := `{"licenseID":"correctLicenseIDAAAAAAAAAAAA","appSlug":"my-app"}`
bundlePath := makeBundleTarGz(t, []struct{ name, content string }{
// configmap comes first in the tar — this is the bug trigger
{"bundle/cluster-resources/configmaps/kotsadm.json", configmapContent},
{"bundle/cluster-resources/license.json", licenseJSONContent},
})
licenseID, appSlug, err := ExtractLicenseFromBundle(bundlePath)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if licenseID != "correctLicenseIDAAAAAAAAAAAA" {
t.Errorf("licenseID = %q, want %q", licenseID, "correctLicenseIDAAAAAAAAAAAA")
}
if appSlug != "my-app" {
t.Errorf("appSlug = %q, want %q", appSlug, "my-app")
}
}
func TestExtractLicenseFromBundle_FallsBackToConfigmap(t *testing.T) {
// No license.json — should fall back to configmap scan.
configmapContent := `{
"kind": "ConfigMapList",
"apiVersion": "v1",
"items": [{
"data": {
"licenseID": "fallbackLicenseIDAAAAAAAAAA"
}
}]
}`
bundlePath := makeBundleTarGz(t, []struct{ name, content string }{
{"bundle/cluster-resources/configmaps/my-app.json", configmapContent},
})
licenseID, appSlug, err := ExtractLicenseFromBundle(bundlePath)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if licenseID != "fallbackLicenseIDAAAAAAAAAA" {
t.Errorf("licenseID = %q, want %q", licenseID, "fallbackLicenseIDAAAAAAAAAA")
}
if appSlug != "my-app" {
t.Errorf("appSlug = %q, want %q", appSlug, "my-app")
}
}
func TestExtractLicenseFromBundle_ReturnsEmptyWhenNotFound(t *testing.T) {
bundlePath := makeBundleTarGz(t, []struct{ name, content string }{
{"bundle/cluster-resources/configmaps/some.json", `{"kind":"ConfigMapList"}`},
})
licenseID, appSlug, err := ExtractLicenseFromBundle(bundlePath)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if licenseID != "" || appSlug != "" {
t.Errorf("expected empty results, got licenseID=%q appSlug=%q", licenseID, appSlug)
}
}
func TestExtractLicenseFromBundle_RealBundle(t *testing.T) {
// Validates against the actual support bundle that triggered the bug.
// The bundle has license.json at tar entry #853 but kotsadm configmap at #94.
bundlePath := filepath.Join("..", "..", "support-bundle-2026-04-10T17_13_13.tar.gz")
if _, err := os.Stat(bundlePath); os.IsNotExist(err) {
t.Skip("real bundle not present")
}
licenseID, appSlug, err := ExtractLicenseFromBundle(bundlePath)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if licenseID != "36G95wTeTQoX7UYcm2QvhssCIkH" {
t.Errorf("licenseID = %q, want %q", licenseID, "36G95wTeTQoX7UYcm2QvhssCIkH")
}
if appSlug != "embedded-cluster-smoke-test-staging-app" {
t.Errorf("appSlug = %q, want %q", appSlug, "embedded-cluster-smoke-test-staging-app")
}
}
+26
View File
@@ -20,6 +20,9 @@ import (
"k8s.io/klog/v2"
)
// loadFromSecret is a package-level hook so tests can stub out cluster access.
var loadFromSecret = specs.LoadFromSecret
// GetSupportBundleFromURI downloads and parses a support bundle from a URI and returns a SupportBundle object
func GetSupportBundleFromURI(bundleURI string) (*troubleshootv1beta2.SupportBundle, error) {
collectorContent, err := LoadSupportBundleSpec(bundleURI)
@@ -165,6 +168,29 @@ func LoadSupportBundleSpec(arg string) ([]byte, error) {
}
func LoadRedactorSpec(arg string) ([]byte, error) {
if strings.HasPrefix(arg, "secret/") {
// format secret/namespace-name/secret-name[/data-key]
pathParts := strings.Split(arg, "/")
if len(pathParts) > 4 {
return nil, errors.Errorf("secret path %s must have at most 4 components", arg)
}
if len(pathParts) < 3 {
return nil, errors.Errorf("secret path %s must have at least 3 components", arg)
}
dataKey := "redactor-spec"
if len(pathParts) == 4 {
dataKey = pathParts[3]
}
spec, err := loadFromSecret(pathParts[1], pathParts[2], dataKey)
if err != nil {
return nil, errors.Wrap(err, "failed to get spec from secret")
}
return spec, nil
}
if strings.HasPrefix(arg, "configmap/") {
// format configmap/namespace-name/configmap-name[/data-key]
pathParts := strings.Split(arg, "/")
+60
View File
@@ -1,7 +1,9 @@
package supportbundle
import (
"fmt"
"reflect"
"strings"
"testing"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
@@ -92,3 +94,61 @@ spec:
})
}
}
func TestLoadRedactorSpec(t *testing.T) {
origLoadFromSecret := loadFromSecret
defer func() { loadFromSecret = origLoadFromSecret }()
loadFromSecret = func(namespace, secretName, key string) ([]byte, error) {
return []byte(fmt.Sprintf("namespace=%s,secret=%s,key=%s", namespace, secretName, key)), nil
}
tests := []struct {
name string
uri string
wantContent string
wantErr string
}{
{
name: "secret URI with default key",
uri: "secret/default/my-redactor",
wantContent: "namespace=default,secret=my-redactor,key=redactor-spec",
},
{
name: "secret URI with custom key",
uri: "secret/default/my-redactor/custom-key",
wantContent: "namespace=default,secret=my-redactor,key=custom-key",
},
{
name: "secret URI with too few components",
uri: "secret/default",
wantErr: "must have at least 3 components",
},
{
name: "secret URI with too many components",
uri: "secret/default/my-redactor/custom-key/extra",
wantErr: "must have at most 4 components",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := LoadRedactorSpec(tt.uri)
if tt.wantErr != "" {
if err == nil {
t.Fatalf("LoadRedactorSpec() expected error, got nil")
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Errorf("LoadRedactorSpec() error = %q, want containing %q", err.Error(), tt.wantErr)
}
return
}
if err != nil {
t.Fatalf("LoadRedactorSpec() unexpected error = %v", err)
}
if string(got) != tt.wantContent {
t.Errorf("LoadRedactorSpec() = %q, want %q", string(got), tt.wantContent)
}
})
}
}
@@ -1836,6 +1836,9 @@
"exclude": {
"oneOf": [{"type": "string"},{"type": "boolean"}]
},
"ignoreIfNoFiles": {
"type": "boolean"
},
"filters": {
"type": "object",
"properties": {
@@ -1836,6 +1836,9 @@
"exclude": {
"oneOf": [{"type": "string"},{"type": "boolean"}]
},
"ignoreIfNoFiles": {
"type": "boolean"
},
"filters": {
"type": "object",
"properties": {

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