mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-08-27 00:37:20 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86b7e54466 | ||
|
|
f6f51acbd5 | ||
|
|
64ee9e5596 | ||
|
|
277272249b | ||
|
|
607124381c | ||
|
|
b9b20753e9 | ||
|
|
3f1314c00d | ||
|
|
fcfdc63634 | ||
|
|
d67cdb7b90 | ||
|
|
a6fbf144b8 | ||
|
|
b38a6a0389 | ||
|
|
de15d02f39 | ||
|
|
8e2647077d | ||
|
|
6e5db6d895 | ||
|
|
ecc92b1e3e | ||
|
|
9f5f0633cf | ||
|
|
6167fd8a5e | ||
|
|
91bf0242d4 | ||
|
|
5a820c02ff | ||
|
|
cf10bd0e82 | ||
|
|
7bb88e6b83 | ||
|
|
be2c2128c6 | ||
|
|
9ef117f080 | ||
|
|
1a828fa90b | ||
|
|
9970fb1371 | ||
|
|
0213fbb124 | ||
|
|
deeeea7cec | ||
|
|
197f6de425 | ||
|
|
2f62240ed3 | ||
|
|
40eef3cdc8 | ||
|
|
d25aa7d0ea | ||
|
|
77c9968ff6 | ||
|
|
06506ed95d | ||
|
|
e272683bce | ||
|
|
ea900a1881 | ||
|
|
f0b8de68ae | ||
|
|
544a700062 | ||
|
|
059b5d14d2 | ||
|
|
b33d5538e9 | ||
|
|
deda4ce98c | ||
|
|
350418c6e9 | ||
|
|
eacff7112f | ||
|
|
7ed2f4bff2 | ||
|
|
af8f682244 | ||
|
|
c968fca125 | ||
|
|
0fb0a07e55 | ||
|
|
0d21eed5f8 | ||
|
|
b88bc8ddf7 | ||
|
|
9c24ab6067 | ||
|
|
289102f16d | ||
|
|
77fd7da315 | ||
|
|
78ee833f45 | ||
|
|
781853251d | ||
|
|
ffa1c040e2 | ||
|
|
0113624352 | ||
|
|
52efd167ad | ||
|
|
402d111745 | ||
|
|
8105fa00e9 | ||
|
|
e7c07a7d79 | ||
|
|
0240a632c9 | ||
|
|
f58f02560f | ||
|
|
2efbc20b7c | ||
|
|
c1c4b612a4 | ||
|
|
d60f9a6b76 | ||
|
|
668b7ed0b2 | ||
|
|
2bb611cda1 | ||
|
|
142015cce3 | ||
|
|
83f02f4705 | ||
|
|
2f78fee284 | ||
|
|
e97b9613a5 | ||
|
|
8823f7d99e | ||
|
|
d73082ac93 | ||
|
|
cf8c08b913 | ||
|
|
aea4f7c87c | ||
|
|
05dcae2388 | ||
|
|
0c63880528 | ||
|
|
7484b10914 | ||
|
|
f66216154a | ||
|
|
bd6c5b976d | ||
|
|
0a2c9c74ab | ||
|
|
e685cb9cf3 | ||
|
|
3aaffb424b | ||
|
|
1b1efa133e | ||
|
|
ff31f5af0b | ||
|
|
8ab2b5e247 | ||
|
|
55608c12ed | ||
|
|
fa14616009 | ||
|
|
d83d8ebfc6 | ||
|
|
47656a8e6f | ||
|
|
60263caf78 | ||
|
|
87cedca468 | ||
|
|
16237e4459 | ||
|
|
a57171a918 | ||
|
|
01d5804977 | ||
|
|
22cc02cd29 | ||
|
|
1444c01725 | ||
|
|
0020c1129e | ||
|
|
1788376472 | ||
|
|
04e656a0a5 | ||
|
|
790c8d4bde | ||
|
|
4e999d6bfb | ||
|
|
403b1ceccf | ||
|
|
8173759e52 | ||
|
|
f5f02f5a80 | ||
|
|
e882f44ae9 | ||
|
|
d79ff950ca | ||
|
|
191ebdb598 | ||
|
|
edfa01c5c4 | ||
|
|
18ec6adc12 | ||
|
|
15b908f9e0 | ||
|
|
a9c5baca14 | ||
|
|
5778d4656a | ||
|
|
905c9771bc | ||
|
|
ce155270c8 | ||
|
|
91da8f13bb | ||
|
|
1a8aff1d79 | ||
|
|
ece20d3c2c | ||
|
|
80e5fac07c | ||
|
|
51c07b42c3 | ||
|
|
bd7bae6302 | ||
|
|
9d41460d33 | ||
|
|
fb0f81d076 |
@@ -7,7 +7,7 @@ labels: 'epic'
|
||||
|
||||
# Design Proposal
|
||||
|
||||
Link to the [proposal](https://github.com/replicatedhq/troubleshoot/tree/main/design/template.md)
|
||||
Link to the [proposal](https://github.com/replicatedhq/troubleshoot/tree/main/docs/design/template.md)
|
||||
|
||||
# Definition of done
|
||||
|
||||
|
||||
@@ -22,34 +22,17 @@ concurrency:
|
||||
jobs:
|
||||
fail_if_pull_request_is_draft:
|
||||
if: github.event.pull_request.draft == true
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Fails in order to indicate that pull request needs to be marked as ready to review and unit tests workflow needs to pass.
|
||||
run: exit 1
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21"
|
||||
|
||||
- name: setup env
|
||||
run: |
|
||||
echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV
|
||||
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
|
||||
shell: bash
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- run: make test
|
||||
|
||||
test-integration:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21"
|
||||
go-version: "1.23"
|
||||
|
||||
- name: setup env
|
||||
run: |
|
||||
@@ -61,8 +44,8 @@ jobs:
|
||||
- uses: replicatedhq/action-k3s@main
|
||||
id: k3s
|
||||
with:
|
||||
version: v1.24.1-k3s1
|
||||
|
||||
version: v1.31.2-k3s1
|
||||
# test-integration includes unit tests
|
||||
- run: make test-integration
|
||||
|
||||
ensure-schemas-are-generated:
|
||||
@@ -70,7 +53,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21"
|
||||
go-version: "1.23"
|
||||
- name: setup env
|
||||
run: |
|
||||
echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV
|
||||
@@ -79,14 +62,17 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: make check-schemas
|
||||
path: github.com/replicatedhq/troubleshoot
|
||||
- run: |
|
||||
cd github.com/replicatedhq/troubleshoot
|
||||
make check-schemas
|
||||
|
||||
compile-preflight:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21"
|
||||
go-version: "1.23"
|
||||
- name: setup env
|
||||
run: |
|
||||
echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV
|
||||
@@ -99,54 +85,6 @@ jobs:
|
||||
name: preflight
|
||||
path: bin/preflight
|
||||
|
||||
validate-preflight:
|
||||
runs-on: ubuntu-latest
|
||||
needs: compile-preflight
|
||||
steps:
|
||||
- uses: replicatedhq/action-k3s@main
|
||||
id: k3s
|
||||
with:
|
||||
version: v1.23.6-k3s1
|
||||
- name: Download preflight binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: preflight
|
||||
path: bin/
|
||||
- run: chmod +x bin/preflight
|
||||
- run: |
|
||||
set +e
|
||||
./bin/preflight --interactive=false --format=json https://preflight.replicated.com > result.json
|
||||
EXIT_CODE=$?
|
||||
cat result.json
|
||||
|
||||
EXIT_STATUS=0
|
||||
if [ $EXIT_CODE -ne 3 ]; then
|
||||
echo "Expected exit code of 3 (some checks failed), got $EXIT_CODE"
|
||||
EXIT_STATUS=1
|
||||
fi
|
||||
|
||||
if grep -q "was not collected" result.json; then
|
||||
echo "Some files were not collected"
|
||||
EXIT_STATUS=1
|
||||
fi
|
||||
|
||||
if (( `jq '.pass | length' result.json` < 1 )); then
|
||||
echo "No passing preflights found"
|
||||
EXIT_STATUS=1
|
||||
fi
|
||||
|
||||
if (( `jq '.warn | length' result.json` < 1 )); then
|
||||
echo "No warnings found"
|
||||
EXIT_STATUS=1
|
||||
fi
|
||||
|
||||
if (( `jq '.fail | length' result.json` < 1 )); then
|
||||
echo "No failed preflights found"
|
||||
EXIT_STATUS=1
|
||||
fi
|
||||
|
||||
exit $EXIT_STATUS
|
||||
|
||||
validate-preflight-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
needs: compile-preflight
|
||||
@@ -155,7 +93,7 @@ jobs:
|
||||
- uses: replicatedhq/action-k3s@main
|
||||
id: k3s
|
||||
with:
|
||||
version: v1.23.6-k3s1
|
||||
version: v1.31.2-k3s1
|
||||
- name: Download preflight binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -175,7 +113,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21"
|
||||
go-version: "1.23"
|
||||
- name: setup env
|
||||
run: |
|
||||
echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV
|
||||
@@ -188,25 +126,6 @@ jobs:
|
||||
name: support-bundle
|
||||
path: bin/support-bundle
|
||||
|
||||
validate-supportbundle:
|
||||
runs-on: ubuntu-latest
|
||||
needs: compile-supportbundle
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: replicatedhq/action-k3s@main
|
||||
id: k3s
|
||||
with:
|
||||
version: v1.23.6-k3s1
|
||||
- name: Download support-bundle binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: support-bundle
|
||||
path: bin/
|
||||
- run: chmod +x bin/support-bundle
|
||||
- run: ./bin/support-bundle ./examples/support-bundle/sample-collectors.yaml
|
||||
- run: ./bin/support-bundle ./examples/support-bundle/sample-supportbundle.yaml
|
||||
- run: ./bin/support-bundle https://kots.io
|
||||
|
||||
validate-supportbundle-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
needs: compile-supportbundle
|
||||
@@ -215,7 +134,7 @@ jobs:
|
||||
- uses: replicatedhq/action-k3s@main
|
||||
id: k3s
|
||||
with:
|
||||
version: v1.23.6-k3s1
|
||||
version: v1.31.2-k3s1
|
||||
- name: Download support bundle binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -223,19 +142,12 @@ jobs:
|
||||
path: bin/
|
||||
- run: chmod +x bin/support-bundle
|
||||
- run: make support-bundle-e2e-test
|
||||
|
||||
validate-supportbundle-e2e-go-test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: compile-supportbundle
|
||||
steps:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21"
|
||||
- name: setup env
|
||||
run: |
|
||||
echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV
|
||||
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
|
||||
shell: bash
|
||||
|
||||
# Additional e2e tests for support bundle that run in Go, these create a Kind cluster
|
||||
validate-supportbundle-e2e-go:
|
||||
runs-on: ubuntu-latest
|
||||
needs: compile-supportbundle
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Download support bundle binary
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -243,6 +155,12 @@ jobs:
|
||||
name: support-bundle
|
||||
path: bin/
|
||||
- run: chmod +x bin/support-bundle
|
||||
- name: Download preflight binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: preflight
|
||||
path: bin/
|
||||
- run: chmod +x bin/preflight
|
||||
- run: make support-bundle-e2e-go-test
|
||||
|
||||
compile-collect:
|
||||
@@ -250,7 +168,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21"
|
||||
go-version: "1.23"
|
||||
- name: setup env
|
||||
run: |
|
||||
echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV
|
||||
@@ -283,10 +201,10 @@ jobs:
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21"
|
||||
go-version: "1.23"
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: "v0.183.0"
|
||||
args: build --rm-dist --snapshot --config deploy/.goreleaser.yaml --single-target
|
||||
@@ -297,9 +215,8 @@ jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- validate-preflight
|
||||
- validate-preflight-e2e
|
||||
- validate-supportbundle
|
||||
- validate-supportbundle-e2e
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -307,14 +224,14 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: azure/docker-login@v1
|
||||
- uses: azure/docker-login@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USER }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21"
|
||||
go-version: "1.23"
|
||||
|
||||
- uses: sigstore/cosign-installer@v3
|
||||
with:
|
||||
@@ -335,7 +252,7 @@ jobs:
|
||||
COSIGN_KEY: ${{ secrets.COSIGN_KEY }}
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: "v1.x" # Binary version to install
|
||||
args: release --rm-dist --config deploy/.goreleaser.yaml
|
||||
@@ -343,11 +260,11 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Update new preflight version in krew-index
|
||||
uses: rajatjindal/krew-release-bot@v0.0.46
|
||||
uses: rajatjindal/krew-release-bot@v0.0.47
|
||||
with:
|
||||
krew_template_file: deploy/krew/preflight.yaml
|
||||
- name: Update new support-bundle version in krew-index
|
||||
uses: rajatjindal/krew-release-bot@v0.0.46
|
||||
uses: rajatjindal/krew-release-bot@v0.0.47
|
||||
with:
|
||||
krew_template_file: deploy/krew/support-bundle.yaml
|
||||
|
||||
@@ -356,15 +273,12 @@ jobs:
|
||||
validate-pr-tests:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- test
|
||||
- test-integration
|
||||
- run-examples
|
||||
- compile-collect
|
||||
- validate-preflight
|
||||
- validate-preflight-e2e
|
||||
- validate-supportbundle
|
||||
- validate-supportbundle-e2e
|
||||
- validate-supportbundle-e2e-go-test
|
||||
- validate-supportbundle-e2e-go
|
||||
- ensure-schemas-are-generated
|
||||
steps:
|
||||
- run: echo "All PR tests passed"
|
||||
@@ -386,4 +300,4 @@ jobs:
|
||||
# if the validate-pr-tests job was successful, this job will succeed
|
||||
- name: succeed if validate-pr-tests job succeeded
|
||||
if: needs.validate-pr-tests.result == 'success'
|
||||
run: echo "Validation succeeded"
|
||||
run: echo "Validation succeeded"
|
||||
|
||||
@@ -14,15 +14,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21
|
||||
|
||||
- name: Install Go deps
|
||||
run: go mod download
|
||||
|
||||
- name: Install trivy
|
||||
run: |
|
||||
wget https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.deb
|
||||
|
||||
+3
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
run:
|
||||
allow-parallel-runners: true
|
||||
timeout: 30s
|
||||
timeout: 10m
|
||||
|
||||
linters:
|
||||
enable:
|
||||
@@ -11,3 +11,5 @@ linters:
|
||||
- gofmt
|
||||
- gosec
|
||||
- govet
|
||||
disable:
|
||||
- errcheck
|
||||
|
||||
@@ -7,6 +7,7 @@ VERSION_PACKAGE = github.com/replicatedhq/troubleshoot/pkg/version
|
||||
VERSION ?=`git describe --tags --dirty`
|
||||
DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"`
|
||||
RUN?=""
|
||||
GOLANGCI_LINT_VERSION ?= "v1.61.0"
|
||||
|
||||
GIT_TREE = $(shell git rev-parse --is-inside-work-tree 2>/dev/null)
|
||||
ifneq "$(GIT_TREE)" ""
|
||||
@@ -34,7 +35,8 @@ define LDFLAGS
|
||||
"
|
||||
endef
|
||||
|
||||
BUILDFLAGS = -tags "netgo containers_image_ostree_stub exclude_graphdriver_devicemapper exclude_graphdriver_btrfs containers_image_openpgp" -installsuffix netgo
|
||||
BUILDTAGS = "netgo containers_image_ostree_stub exclude_graphdriver_devicemapper exclude_graphdriver_btrfs containers_image_openpgp"
|
||||
BUILDFLAGS = -tags ${BUILDTAGS} -installsuffix netgo
|
||||
BUILDPATHS = ./pkg/... ./cmd/... ./internal/...
|
||||
E2EPATHS = ./test/e2e/...
|
||||
TESTFLAGS ?= -v -coverprofile cover.out
|
||||
@@ -57,8 +59,8 @@ test: generate fmt vet
|
||||
# Go tests that require a K8s instance
|
||||
# TODOLATER: merge with test, so we get unified coverage reports? it'll add 21~sec to the test job though...
|
||||
.PHONY: test-integration
|
||||
test-integration:
|
||||
go test -v --tags "integration exclude_graphdriver_devicemapper exclude_graphdriver_btrfs" ${BUILDPATHS}
|
||||
test-integration: generate fmt vet
|
||||
go test -v --tags="integration exclude_graphdriver_devicemapper exclude_graphdriver_btrfs" ${BUILDPATHS}
|
||||
|
||||
.PHONY: preflight-e2e-test
|
||||
preflight-e2e-test:
|
||||
@@ -114,6 +116,10 @@ bin/analyze:
|
||||
bin/collect:
|
||||
go build ${BUILDFLAGS} ${LDFLAGS} -o bin/collect github.com/replicatedhq/troubleshoot/cmd/collect
|
||||
|
||||
build-linux: tidy
|
||||
@echo "Build cli binaries for Linux"
|
||||
GOOS=linux GOARCH=amd64 $(MAKE) -j bin/support-bundle bin/preflight bin/analyze bin/collect
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
go fmt ${BUILDPATHS}
|
||||
@@ -127,13 +133,15 @@ generate: controller-gen client-gen
|
||||
$(CONTROLLER_GEN) \
|
||||
object:headerFile=./hack/boilerplate.go.txt paths=./pkg/apis/...
|
||||
$(CLIENT_GEN) \
|
||||
--output-base=./../../../ \
|
||||
--output-base=. \
|
||||
--output-package=github.com/replicatedhq/troubleshoot/pkg/client \
|
||||
--clientset-name troubleshootclientset \
|
||||
--input-base github.com/replicatedhq/troubleshoot/pkg/apis \
|
||||
--input troubleshoot/v1beta1 \
|
||||
--input troubleshoot/v1beta2 \
|
||||
-h ./hack/boilerplate.go.txt
|
||||
cp -r github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset pkg/client
|
||||
rm -rf github.com
|
||||
|
||||
.PHONY: openapischema
|
||||
openapischema: controller-gen
|
||||
@@ -141,14 +149,14 @@ openapischema: controller-gen
|
||||
controller-gen crd +output:dir=./config/crds paths=./pkg/apis/troubleshoot/v1beta2
|
||||
|
||||
check-schemas: generate schemas
|
||||
@if [ -n "$(shell git status --short)" ]; then \
|
||||
@if [ -n "$$(git status --short)" ]; then \
|
||||
echo -e "\033[31mThe git repo is dirty :( Ensure all generated files are committed e.g CRD schema files\033[0;m"; \
|
||||
git status --short; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: schemas
|
||||
schemas: fmt vet openapischema bin/schemagen
|
||||
schemas: openapischema bin/schemagen
|
||||
./bin/schemagen --output-dir ./schemas
|
||||
|
||||
bin/schemagen:
|
||||
@@ -162,12 +170,12 @@ bin/docsgen:
|
||||
go build ${LDFLAGS} -o bin/docsgen github.com/replicatedhq/troubleshoot/cmd/docsgen
|
||||
|
||||
controller-gen:
|
||||
go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.14.0
|
||||
go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.16.2
|
||||
CONTROLLER_GEN=$(shell which controller-gen)
|
||||
|
||||
.PHONY: client-gen
|
||||
client-gen:
|
||||
go install k8s.io/code-generator/cmd/client-gen@v0.28.8
|
||||
go install k8s.io/code-generator/cmd/client-gen@v0.28.13
|
||||
CLIENT_GEN=$(shell which client-gen)
|
||||
|
||||
.PHONY: release
|
||||
@@ -231,14 +239,6 @@ scan:
|
||||
--ignore-unfixed \
|
||||
./
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
golangci-lint run --new -c .golangci.yaml ${BUILDPATHS}
|
||||
|
||||
.PHONY: lint-and-fix
|
||||
lint-and-fix:
|
||||
golangci-lint run --new --fix -c .golangci.yaml ${BUILDPATHS}
|
||||
|
||||
.PHONY: watch
|
||||
watch: npm-install
|
||||
bin/watch.js
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/internal/util"
|
||||
)
|
||||
|
||||
func checkAndSetChroot(newroot string) error {
|
||||
if newroot == "" {
|
||||
return nil
|
||||
}
|
||||
if !util.IsRunningAsRoot() {
|
||||
return errors.New("Can only chroot when run as root")
|
||||
}
|
||||
if err := syscall.Chroot(newroot); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/internal/util"
|
||||
)
|
||||
|
||||
func checkAndSetChroot(newroot string) error {
|
||||
if newroot == "" {
|
||||
return nil
|
||||
}
|
||||
if !util.IsRunningAsRoot() {
|
||||
return errors.New("Can only chroot when run as root")
|
||||
}
|
||||
if err := syscall.Chroot(newroot); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
func checkAndSetChroot(newroot string) error {
|
||||
return errors.New("chroot is only implimented in linux/darwin")
|
||||
}
|
||||
@@ -32,6 +32,10 @@ func RootCmd() *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
v := viper.GetViper()
|
||||
|
||||
if err := checkAndSetChroot(v.GetString("chroot")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return runCollect(v, args[0])
|
||||
},
|
||||
PostRun: func(cmd *cobra.Command, args []string) {
|
||||
@@ -53,6 +57,7 @@ func RootCmd() *cobra.Command {
|
||||
cmd.Flags().String("selector", "", "selector (label query) to filter remote collection nodes on.")
|
||||
cmd.Flags().Bool("collect-without-permissions", false, "always generate a support bundle, even if it some require additional permissions")
|
||||
cmd.Flags().Bool("debug", false, "enable debug logging")
|
||||
cmd.Flags().String("chroot", "", "Chroot to path")
|
||||
|
||||
// hidden in favor of the `insecure-skip-tls-verify` flag
|
||||
cmd.Flags().Bool("allow-insecure-connections", false, "when set, do not verify TLS certs when retrieving spec and reporting results")
|
||||
|
||||
@@ -2,6 +2,7 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -49,6 +50,13 @@ func runCollect(v *viper.Viper, arg string) error {
|
||||
}
|
||||
|
||||
collectorContent = spec
|
||||
} else if arg == "-" {
|
||||
b, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collectorContent = b
|
||||
} else if _, err = os.Stat(arg); err == nil {
|
||||
b, err := os.ReadFile(arg)
|
||||
if err != nil {
|
||||
|
||||
@@ -50,7 +50,7 @@ that a cluster meets the requirements to run an application.`,
|
||||
|
||||
err = preflight.RunPreflights(v.GetBool("interactive"), v.GetString("output"), v.GetString("format"), args)
|
||||
if !v.GetBool("dry-run") && (v.GetBool("debug") || v.IsSet("v")) {
|
||||
fmt.Printf("\n%s", traces.GetExporterInstance().GetSummary())
|
||||
fmt.Fprintf(os.Stderr, "\n%s", traces.GetExporterInstance().GetSummary())
|
||||
}
|
||||
|
||||
return err
|
||||
@@ -71,6 +71,7 @@ that a cluster meets the requirements to run an application.`,
|
||||
// Dry run flag should be in cmd.PersistentFlags() flags made available to all subcommands
|
||||
// Adding here to avoid that
|
||||
cmd.Flags().Bool("dry-run", false, "print the preflight spec without running preflight checks")
|
||||
cmd.Flags().Bool("no-uri", false, "When this flag is used, Preflight does not attempt to retrieve the spec referenced by the uri: field`")
|
||||
|
||||
k8sutil.AddFlags(cmd.Flags())
|
||||
|
||||
|
||||
@@ -2,10 +2,6 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/go-wordwrap"
|
||||
"github.com/pkg/errors"
|
||||
@@ -19,15 +15,14 @@ import (
|
||||
var (
|
||||
selectedResult = 0
|
||||
table = widgets.NewTable()
|
||||
isShowingSaved = false
|
||||
)
|
||||
|
||||
func showInteractiveResults(supportBundleName string, analyzeResults []*analyzerunner.AnalyzeResult) error {
|
||||
func showInteractiveResults(supportBundleName string, analyzeResults []*analyzerunner.AnalyzeResult, archivePath string) error {
|
||||
if err := ui.Init(); err != nil {
|
||||
return errors.Wrap(err, "failed to create terminal ui")
|
||||
}
|
||||
defer ui.Close()
|
||||
drawUI(supportBundleName, analyzeResults)
|
||||
drawUI(supportBundleName, analyzeResults, archivePath)
|
||||
|
||||
uiEvents := ui.PollEvents()
|
||||
for {
|
||||
@@ -37,29 +32,10 @@ func showInteractiveResults(supportBundleName string, analyzeResults []*analyzer
|
||||
case "<C-c>":
|
||||
return nil
|
||||
case "q":
|
||||
if isShowingSaved == true {
|
||||
isShowingSaved = false
|
||||
ui.Clear()
|
||||
drawUI(supportBundleName, analyzeResults)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
case "s":
|
||||
filename, err := save(analyzeResults)
|
||||
if err != nil {
|
||||
// show
|
||||
} else {
|
||||
showSaved(filename)
|
||||
go func() {
|
||||
time.Sleep(time.Second * 5)
|
||||
isShowingSaved = false
|
||||
ui.Clear()
|
||||
drawUI(supportBundleName, analyzeResults)
|
||||
}()
|
||||
}
|
||||
return nil
|
||||
case "<Resize>":
|
||||
ui.Clear()
|
||||
drawUI(supportBundleName, analyzeResults)
|
||||
drawUI(supportBundleName, analyzeResults, archivePath)
|
||||
case "<Down>":
|
||||
if selectedResult < len(analyzeResults)-1 {
|
||||
selectedResult++
|
||||
@@ -69,7 +45,7 @@ func showInteractiveResults(supportBundleName string, analyzeResults []*analyzer
|
||||
}
|
||||
table.ScrollDown()
|
||||
ui.Clear()
|
||||
drawUI(supportBundleName, analyzeResults)
|
||||
drawUI(supportBundleName, analyzeResults, archivePath)
|
||||
case "<Up>":
|
||||
if selectedResult > 0 {
|
||||
selectedResult--
|
||||
@@ -79,16 +55,16 @@ func showInteractiveResults(supportBundleName string, analyzeResults []*analyzer
|
||||
}
|
||||
table.ScrollUp()
|
||||
ui.Clear()
|
||||
drawUI(supportBundleName, analyzeResults)
|
||||
drawUI(supportBundleName, analyzeResults, archivePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func drawUI(supportBundleName string, analyzeResults []*analyzerunner.AnalyzeResult) {
|
||||
func drawUI(supportBundleName string, analyzeResults []*analyzerunner.AnalyzeResult, archivePath string) {
|
||||
drawGrid(analyzeResults)
|
||||
drawHeader(supportBundleName)
|
||||
drawFooter()
|
||||
drawFooter(archivePath)
|
||||
}
|
||||
|
||||
func drawGrid(analyzeResults []*analyzerunner.AnalyzeResult) {
|
||||
@@ -101,9 +77,7 @@ func drawHeader(supportBundleName string) {
|
||||
|
||||
title := widgets.NewParagraph()
|
||||
title.Text = fmt.Sprintf("%s Support Bundle Analysis", util.AppName(supportBundleName))
|
||||
title.TextStyle.Fg = ui.ColorWhite
|
||||
title.TextStyle.Bg = ui.ColorClear
|
||||
title.TextStyle.Modifier = ui.ModifierBold
|
||||
title.TextStyle = ui.NewStyle(ui.ColorWhite, ui.ColorClear, ui.ModifierBold)
|
||||
title.Border = false
|
||||
|
||||
left := termWidth/2 - 2*len(title.Text)/3
|
||||
@@ -113,11 +87,18 @@ func drawHeader(supportBundleName string) {
|
||||
ui.Render(title)
|
||||
}
|
||||
|
||||
func drawFooter() {
|
||||
func drawFooter(archivePath string) {
|
||||
termWidth, termHeight := ui.TerminalDimensions()
|
||||
|
||||
archivePathMsg := widgets.NewParagraph()
|
||||
archivePathMsg.Text = "Support bundle archive: " + archivePath
|
||||
archivePathMsg.Border = false
|
||||
archivePathMsg.TextStyle = ui.NewStyle(ui.ColorWhite, ui.ColorClear, ui.ModifierBold)
|
||||
|
||||
archivePathMsg.SetRect(0, termHeight-3, termWidth, termHeight-2)
|
||||
|
||||
instructions := widgets.NewParagraph()
|
||||
instructions.Text = "[q] quit [s] save [↑][↓] scroll"
|
||||
instructions.Text = "[q] quit [↑][↓] scroll"
|
||||
instructions.Border = false
|
||||
|
||||
left := 0
|
||||
@@ -126,13 +107,13 @@ func drawFooter() {
|
||||
bottom := termHeight
|
||||
|
||||
instructions.SetRect(left, top, right, bottom)
|
||||
ui.Render(instructions)
|
||||
ui.Render(archivePathMsg, instructions)
|
||||
}
|
||||
|
||||
func drawAnalyzersTable(analyzeResults []*analyzerunner.AnalyzeResult) {
|
||||
termWidth, termHeight := ui.TerminalDimensions()
|
||||
|
||||
table.SetRect(0, 3, termWidth/2, termHeight-6)
|
||||
table.SetRect(0, 3, termWidth/2, termHeight-4)
|
||||
table.FillRow = true
|
||||
table.Border = true
|
||||
table.Rows = [][]string{}
|
||||
@@ -213,60 +194,3 @@ func drawDetails(analysisResult *analyzerunner.AnalyzeResult) {
|
||||
message.SetRect(termWidth/2, currentTop, termWidth, currentTop+height)
|
||||
ui.Render(message)
|
||||
}
|
||||
|
||||
func showSaved(filename string) {
|
||||
termWidth, termHeight := ui.TerminalDimensions()
|
||||
|
||||
savedMessage := widgets.NewParagraph()
|
||||
savedMessage.Text = fmt.Sprintf("Support Bundle analysis results saved to\n\n%s", filename)
|
||||
savedMessage.WrapText = true
|
||||
savedMessage.Border = true
|
||||
|
||||
left := termWidth/2 - 20
|
||||
right := termWidth/2 + 20
|
||||
top := termHeight/2 - 4
|
||||
bottom := termHeight/2 + 4
|
||||
|
||||
savedMessage.SetRect(left, top, right, bottom)
|
||||
ui.Render(savedMessage)
|
||||
|
||||
isShowingSaved = true
|
||||
}
|
||||
|
||||
func save(analyzeResults []*analyzerunner.AnalyzeResult) (string, error) {
|
||||
filename := path.Join(util.HomeDir(), fmt.Sprintf("%s-results.txt", "support-bundle"))
|
||||
_, err := os.Stat(filename)
|
||||
if err == nil {
|
||||
os.Remove(filename)
|
||||
}
|
||||
|
||||
results := ""
|
||||
for _, analyzeResult := range analyzeResults {
|
||||
result := ""
|
||||
|
||||
if analyzeResult.IsPass {
|
||||
result = "Check PASS\n"
|
||||
} else if analyzeResult.IsWarn {
|
||||
result = "Check WARN\n"
|
||||
} else if analyzeResult.IsFail {
|
||||
result = "Check FAIL\n"
|
||||
}
|
||||
|
||||
result = result + fmt.Sprintf("Title: %s\n", analyzeResult.Title)
|
||||
result = result + fmt.Sprintf("Message: %s\n", analyzeResult.Message)
|
||||
|
||||
if analyzeResult.URI != "" {
|
||||
result = result + fmt.Sprintf("URI: %s\n", analyzeResult.URI)
|
||||
}
|
||||
|
||||
result = result + "\n------------\n"
|
||||
|
||||
results = results + result
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(filename, []byte(results), 0644); err != nil {
|
||||
return "", errors.Wrap(err, "failed to save preflight results")
|
||||
}
|
||||
|
||||
return filename, nil
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ For more information on redactors visit https://troubleshoot.sh/docs/redact/
|
||||
if output == "" {
|
||||
output = fmt.Sprintf("redacted-support-bundle-%s.tar.gz", time.Now().Format("2006-01-02T15_04_05"))
|
||||
}
|
||||
err = collectorResult.ArchiveSupportBundle(bundleDir, output)
|
||||
err = collectorResult.ArchiveBundle(bundleDir, output)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create support bundle archive")
|
||||
}
|
||||
|
||||
@@ -18,9 +18,17 @@ func RootCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "support-bundle [urls...]",
|
||||
Args: cobra.MinimumNArgs(0),
|
||||
Short: "Generate a support bundle",
|
||||
Long: `A support bundle is an archive of files, output, metrics and state
|
||||
from a server that can be used to assist when troubleshooting a Kubernetes cluster.`,
|
||||
Short: "Generate a support bundle from a Kubernetes cluster or specified sources",
|
||||
Long: `Generate a support bundle, an archive containing files, output, metrics, and cluster state to aid in troubleshooting Kubernetes clusters.
|
||||
|
||||
If no arguments are provided, specs are automatically loaded from the cluster by default.
|
||||
|
||||
**Argument Types**:
|
||||
1. **Secret**: Load specs from a Kubernetes Secret. Format: "secret/namespace-name/secret-name[/data-key]"
|
||||
2. **ConfigMap**: Load specs from a Kubernetes ConfigMap. Format: "configmap/namespace-name/configmap-name[/data-key]"
|
||||
3. **File**: Load specs from a local file. Format: Local file path
|
||||
4. **Standard Input**: Read specs from stdin. Format: "-"
|
||||
5. **URL**: Load specs from a URL. Supports HTTP and OCI registry URLs.`,
|
||||
SilenceUsage: true,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
v := viper.GetViper()
|
||||
@@ -36,6 +44,18 @@ from a server that can be used to assist when troubleshooting a Kubernetes clust
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
v := viper.GetViper()
|
||||
|
||||
// If there are no locations to load specs from passed in the cli args, we should
|
||||
// load them from the cluster by setting "load-cluster-specs=true". If the caller
|
||||
// provided "--load-cluster-specs" cli option, we should respect it.
|
||||
if len(args) == 0 {
|
||||
// Check if --load-cluster-specs was set by the cli caller to avoid overriding it
|
||||
flg := cmd.Flags().Lookup("load-cluster-specs")
|
||||
if flg != nil && !flg.Changed {
|
||||
// Load specs from the cluster if no spec(s) is(are) provided in the cli args
|
||||
v.Set("load-cluster-specs", true)
|
||||
}
|
||||
}
|
||||
|
||||
closer, err := traces.ConfigureTracing("support-bundle")
|
||||
if err != nil {
|
||||
// Do not fail running support-bundle if tracing fails
|
||||
@@ -46,7 +66,7 @@ from a server that can be used to assist when troubleshooting a Kubernetes clust
|
||||
|
||||
err = runTroubleshoot(v, args)
|
||||
if !v.IsSet("dry-run") && (v.GetBool("debug") || v.IsSet("v")) {
|
||||
fmt.Printf("\n%s", traces.GetExporterInstance().GetSummary())
|
||||
fmt.Fprintf(os.Stderr, "\n%s", traces.GetExporterInstance().GetSummary())
|
||||
}
|
||||
|
||||
return err
|
||||
@@ -69,7 +89,7 @@ from a server that can be used to assist when troubleshooting a Kubernetes clust
|
||||
cmd.Flags().Bool("interactive", true, "enable/disable interactive mode")
|
||||
cmd.Flags().Bool("collect-without-permissions", true, "always generate a support bundle, even if it some require additional permissions")
|
||||
cmd.Flags().StringSliceP("selector", "l", []string{"troubleshoot.sh/kind=support-bundle"}, "selector to filter on for loading additional support bundle specs found in secrets within the cluster")
|
||||
cmd.Flags().Bool("load-cluster-specs", false, "enable/disable loading additional troubleshoot specs found within the cluster. required when no specs are provided on the command line")
|
||||
cmd.Flags().Bool("load-cluster-specs", false, "enable/disable loading additional troubleshoot specs found within the cluster. Do not load by default unless no specs are provided in the cli args")
|
||||
cmd.Flags().String("since-time", "", "force pod logs collectors to return logs after a specific date (RFC3339)")
|
||||
cmd.Flags().String("since", "", "force pod logs collectors to return logs newer than a relative duration like 5s, 2m, or 3h.")
|
||||
cmd.Flags().StringP("output", "o", "", "specify the output file path for the support bundle")
|
||||
|
||||
+34
-11
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -38,9 +39,6 @@ import (
|
||||
|
||||
func runTroubleshoot(v *viper.Viper, args []string) error {
|
||||
ctx := context.Background()
|
||||
if !v.GetBool("load-cluster-specs") && len(args) < 1 {
|
||||
return errors.New("flag load-cluster-specs must be set if no specs are provided on the command line")
|
||||
}
|
||||
|
||||
restConfig, err := k8sutil.GetRESTConfig()
|
||||
if err != nil {
|
||||
@@ -112,7 +110,7 @@ func runTroubleshoot(v *viper.Viper, args []string) error {
|
||||
}
|
||||
|
||||
if interactive {
|
||||
if len(mainBundle.Spec.HostCollectors) > 0 && !util.IsRunningAsRoot() {
|
||||
if len(mainBundle.Spec.HostCollectors) > 0 && !util.IsRunningAsRoot() && !mainBundle.Spec.RunHostCollectorsInPod {
|
||||
fmt.Print(cursor.Show())
|
||||
if util.PromptYesNo(util.HOST_COLLECTORS_RUN_AS_ROOT_PROMPT) {
|
||||
fmt.Println("Exiting...")
|
||||
@@ -186,6 +184,7 @@ func runTroubleshoot(v *viper.Viper, args []string) error {
|
||||
OutputPath: v.GetString("output"),
|
||||
Redact: v.GetBool("redact"),
|
||||
FromCLI: true,
|
||||
RunHostCollectorsInPod: mainBundle.Spec.RunHostCollectorsInPod,
|
||||
}
|
||||
|
||||
nonInteractiveOutput := analysisOutput{}
|
||||
@@ -200,7 +199,7 @@ func runTroubleshoot(v *viper.Viper, args []string) error {
|
||||
|
||||
if len(response.AnalyzerResults) > 0 {
|
||||
if interactive {
|
||||
if err := showInteractiveResults(mainBundle.Name, response.AnalyzerResults); err != nil {
|
||||
if err := showInteractiveResults(mainBundle.Name, response.AnalyzerResults, response.ArchivePath); err != nil {
|
||||
interactive = false
|
||||
}
|
||||
} else {
|
||||
@@ -227,7 +226,7 @@ the %s Admin Console to begin analysis.`
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("\n%s\n", response.ArchivePath)
|
||||
fmt.Printf("\nA support bundle was generated and saved at %s. Please send this file to your software vendor for support.\n", response.ArchivePath)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -285,14 +284,21 @@ func loadSupportBundleSpecsFromURIs(ctx context.Context, kinds *loader.Troublesh
|
||||
}
|
||||
|
||||
func loadSpecs(ctx context.Context, args []string, client kubernetes.Interface) (*troubleshootv1beta2.SupportBundle, *troubleshootv1beta2.Redactor, error) {
|
||||
// Append redactor uris to the args
|
||||
allArgs := append(args, viper.GetStringSlice("redactors")...)
|
||||
kinds, err := specs.LoadFromCLIArgs(ctx, client, allArgs, viper.GetViper())
|
||||
var (
|
||||
kinds = loader.NewTroubleshootKinds()
|
||||
vp = viper.GetViper()
|
||||
redactors = vp.GetStringSlice("redactors")
|
||||
allArgs = append(args, redactors...)
|
||||
err error
|
||||
)
|
||||
|
||||
kinds, err = specs.LoadFromCLIArgs(ctx, client, allArgs, vp)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, errors.Wrap(err, "failed to load specs from CLI args")
|
||||
}
|
||||
|
||||
// Load additional specs from support bundle URIs
|
||||
// only when no-uri flag is not set and no URLs are provided in the args
|
||||
if !viper.GetBool("no-uri") {
|
||||
err := loadSupportBundleSpecsFromURIs(ctx, kinds)
|
||||
if err != nil {
|
||||
@@ -305,7 +311,10 @@ func loadSpecs(ctx context.Context, args []string, client kubernetes.Interface)
|
||||
if len(kinds.CollectorsV1Beta2) == 0 &&
|
||||
len(kinds.HostCollectorsV1Beta2) == 0 &&
|
||||
len(kinds.SupportBundlesV1Beta2) == 0 {
|
||||
return nil, nil, errors.New("no collectors specified to run")
|
||||
return nil, nil, types.NewExitCodeError(
|
||||
constants.EXIT_CODE_CATCH_ALL,
|
||||
errors.New("no collectors specified to run. Use --debug and/or -v=2 to see more information"),
|
||||
)
|
||||
}
|
||||
|
||||
// Merge specs
|
||||
@@ -320,10 +329,18 @@ func loadSpecs(ctx context.Context, args []string, client kubernetes.Interface)
|
||||
Name: "merged-support-bundle-spec",
|
||||
},
|
||||
}
|
||||
|
||||
var enableRunHostCollectorsInPod bool
|
||||
|
||||
for _, sb := range kinds.SupportBundlesV1Beta2 {
|
||||
sb := sb
|
||||
mainBundle = supportbundle.ConcatSpec(mainBundle, &sb)
|
||||
//check if sb has metadata and if it has RunHostCollectorsInPod set to true
|
||||
if !reflect.DeepEqual(sb.ObjectMeta, metav1.ObjectMeta{}) && sb.Spec.RunHostCollectorsInPod {
|
||||
enableRunHostCollectorsInPod = sb.Spec.RunHostCollectorsInPod
|
||||
}
|
||||
}
|
||||
mainBundle.Spec.RunHostCollectorsInPod = enableRunHostCollectorsInPod
|
||||
|
||||
for _, c := range kinds.CollectorsV1Beta2 {
|
||||
mainBundle.Spec.Collectors = util.Append(mainBundle.Spec.Collectors, c.Spec.Collectors)
|
||||
@@ -362,6 +379,12 @@ func loadSpecs(ctx context.Context, args []string, client kubernetes.Interface)
|
||||
additionalRedactors.Spec.Redactors = util.Append(additionalRedactors.Spec.Redactors, r.Spec.Redactors)
|
||||
}
|
||||
|
||||
// dedupe specs
|
||||
mainBundle.Spec.Collectors = util.Dedup(mainBundle.Spec.Collectors)
|
||||
mainBundle.Spec.Analyzers = util.Dedup(mainBundle.Spec.Analyzers)
|
||||
mainBundle.Spec.HostCollectors = util.Dedup(mainBundle.Spec.HostCollectors)
|
||||
mainBundle.Spec.HostAnalyzers = util.Dedup(mainBundle.Spec.HostAnalyzers)
|
||||
|
||||
return mainBundle, additionalRedactors, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -343,3 +343,77 @@ spec:
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_loadDuplicatedBundleSpecs(t *testing.T) {
|
||||
spec := testutils.ServeFromFilePath(t, `
|
||||
apiVersion: troubleshoot.sh/v1beta2
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: sb
|
||||
spec:
|
||||
collectors:
|
||||
- helm: {}
|
||||
analyzers:
|
||||
- clusterVersion: {}
|
||||
hostCollectors:
|
||||
- cpu: {}
|
||||
hostAnalyzers:
|
||||
- cpu: {}
|
||||
`)
|
||||
args := []string{spec, spec}
|
||||
|
||||
ctx := context.Background()
|
||||
client := testclient.NewSimpleClientset()
|
||||
sb, _, err := loadSpecs(ctx, args, client)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, sb.Spec.Collectors, 1+2) // default clusterInfo + clusterResources
|
||||
assert.Len(t, sb.Spec.Analyzers, 1)
|
||||
assert.Len(t, sb.Spec.HostCollectors, 1)
|
||||
assert.Len(t, sb.Spec.HostAnalyzers, 1)
|
||||
}
|
||||
|
||||
func Test_loadSpecsFromURL(t *testing.T) {
|
||||
// Run a webserver to serve the URI spec
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`
|
||||
apiVersion: troubleshoot.sh/v1beta2
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: sb-2
|
||||
spec:
|
||||
collectors:
|
||||
- logs:
|
||||
name: podlogs/kotsadm
|
||||
selector:
|
||||
- app=kotsadm`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
// update URI spec with the server URL
|
||||
orig := strings.ReplaceAll(templSpec(), "$MY_URI", srv.URL)
|
||||
|
||||
// now create a webserver to serve the spec with URI
|
||||
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(orig))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
fileSpec := testutils.ServeFromFilePath(t, `
|
||||
apiVersion: troubleshoot.sh/v1beta2
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: sb
|
||||
spec:
|
||||
collectors:
|
||||
- helm: {}`)
|
||||
|
||||
// test and ensure that URI spec is not loaded
|
||||
ctx := context.Background()
|
||||
client := testclient.NewSimpleClientset()
|
||||
sb, _, err := loadSpecs(ctx, []string{fileSpec, srv.URL}, client)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, sb.Spec.Collectors, 2+2) // default + clusterInfo + clusterResources
|
||||
assert.NotNil(t, sb.Spec.Collectors[0].Helm) // come from the file spec
|
||||
assert.NotNil(t, sb.Spec.Collectors[1].ConfigMap) // come from the original spec
|
||||
assert.Nil(t, sb.Spec.Collectors[1].Logs) // come from the URI spec
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.14.0
|
||||
controller-gen.kubebuilder.io/version: v0.16.2
|
||||
name: analyzers.troubleshoot.replicated.com
|
||||
spec:
|
||||
group: troubleshoot.replicated.com
|
||||
@@ -417,6 +417,12 @@ spec:
|
||||
type: string
|
||||
podCapacity:
|
||||
type: string
|
||||
resourceAllocatable:
|
||||
type: string
|
||||
resourceCapacity:
|
||||
type: string
|
||||
resourceName:
|
||||
type: string
|
||||
selector:
|
||||
properties:
|
||||
matchLabel:
|
||||
|
||||
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.14.0
|
||||
controller-gen.kubebuilder.io/version: v0.16.2
|
||||
name: collectors.troubleshoot.replicated.com
|
||||
spec:
|
||||
group: troubleshoot.replicated.com
|
||||
|
||||
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.14.0
|
||||
controller-gen.kubebuilder.io/version: v0.16.2
|
||||
name: preflights.troubleshoot.replicated.com
|
||||
spec:
|
||||
group: troubleshoot.replicated.com
|
||||
@@ -417,6 +417,12 @@ spec:
|
||||
type: string
|
||||
podCapacity:
|
||||
type: string
|
||||
resourceAllocatable:
|
||||
type: string
|
||||
resourceCapacity:
|
||||
type: string
|
||||
resourceName:
|
||||
type: string
|
||||
selector:
|
||||
properties:
|
||||
matchLabel:
|
||||
|
||||
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.14.0
|
||||
controller-gen.kubebuilder.io/version: v0.16.2
|
||||
name: redactors.troubleshoot.replicated.com
|
||||
spec:
|
||||
group: troubleshoot.replicated.com
|
||||
|
||||
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.14.0
|
||||
controller-gen.kubebuilder.io/version: v0.16.2
|
||||
name: supportbundles.troubleshoot.replicated.com
|
||||
spec:
|
||||
group: troubleshoot.replicated.com
|
||||
@@ -448,6 +448,12 @@ spec:
|
||||
type: string
|
||||
podCapacity:
|
||||
type: string
|
||||
resourceAllocatable:
|
||||
type: string
|
||||
resourceCapacity:
|
||||
type: string
|
||||
resourceName:
|
||||
type: string
|
||||
selector:
|
||||
properties:
|
||||
matchLabel:
|
||||
|
||||
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.14.0
|
||||
controller-gen.kubebuilder.io/version: v0.16.2
|
||||
name: analyzers.troubleshoot.sh
|
||||
spec:
|
||||
group: troubleshoot.sh
|
||||
@@ -141,6 +141,61 @@ spec:
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
clusterContainerStatuses:
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
checkName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
namespaces:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
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
|
||||
restartCount:
|
||||
format: int32
|
||||
type: integer
|
||||
strict:
|
||||
type: BoolString
|
||||
required:
|
||||
- outcomes
|
||||
- restartCount
|
||||
type: object
|
||||
clusterPodStatuses:
|
||||
properties:
|
||||
annotations:
|
||||
@@ -673,6 +728,55 @@ spec:
|
||||
required:
|
||||
- collectorName
|
||||
type: object
|
||||
http:
|
||||
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
|
||||
imagePullSecret:
|
||||
properties:
|
||||
annotations:
|
||||
@@ -1117,10 +1221,10 @@ spec:
|
||||
type: BoolString
|
||||
filters:
|
||||
properties:
|
||||
architecture:
|
||||
type: string
|
||||
cpuAllocatable:
|
||||
type: string
|
||||
cpuArchitecture:
|
||||
type: string
|
||||
cpuCapacity:
|
||||
type: string
|
||||
ephemeralStorageAllocatable:
|
||||
@@ -1135,8 +1239,44 @@ spec:
|
||||
type: string
|
||||
podCapacity:
|
||||
type: string
|
||||
resourceAllocatable:
|
||||
type: string
|
||||
resourceCapacity:
|
||||
type: string
|
||||
resourceName:
|
||||
type: string
|
||||
selector:
|
||||
properties:
|
||||
matchExpressions:
|
||||
items:
|
||||
description: |-
|
||||
A label selector requirement is a selector that contains values, a key, and an operator that
|
||||
relates the key and values.
|
||||
properties:
|
||||
key:
|
||||
description: key is the label key that the
|
||||
selector applies to.
|
||||
type: string
|
||||
operator:
|
||||
description: |-
|
||||
operator represents a key's relationship to a set of values.
|
||||
Valid operators are In, NotIn, Exists and DoesNotExist.
|
||||
type: string
|
||||
values:
|
||||
description: |-
|
||||
values is an array of string values. If the operator is In or NotIn,
|
||||
the values array must be non-empty. If the operator is Exists or DoesNotExist,
|
||||
the values array must be empty. This array is replaced during a strategic
|
||||
merge patch.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
required:
|
||||
- key
|
||||
- operator
|
||||
type: object
|
||||
type: array
|
||||
matchLabel:
|
||||
additionalProperties:
|
||||
type: string
|
||||
@@ -2299,6 +2439,117 @@ spec:
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
jsonCompare:
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
checkName:
|
||||
type: string
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
fileName:
|
||||
type: string
|
||||
jsonPath:
|
||||
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
|
||||
path:
|
||||
type: string
|
||||
strict:
|
||||
type: BoolString
|
||||
value:
|
||||
type: string
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
kernelConfigs:
|
||||
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
|
||||
selectedConfigs:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
strict:
|
||||
type: BoolString
|
||||
required:
|
||||
- outcomes
|
||||
- selectedConfigs
|
||||
type: object
|
||||
kernelModules:
|
||||
properties:
|
||||
annotations:
|
||||
@@ -2397,6 +2648,55 @@ spec:
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
networkNamespaceConnectivity:
|
||||
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:
|
||||
@@ -2446,6 +2746,55 @@ spec:
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
sysctl:
|
||||
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
|
||||
systemPackages:
|
||||
properties:
|
||||
annotations:
|
||||
@@ -2803,6 +3152,8 @@ spec:
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
uri:
|
||||
type: string
|
||||
type: object
|
||||
status:
|
||||
description: AnalyzerStatus defines the observed state of Analyzer
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.14.0
|
||||
controller-gen.kubebuilder.io/version: v0.16.2
|
||||
name: hostcollectors.troubleshoot.sh
|
||||
spec:
|
||||
group: troubleshoot.sh
|
||||
@@ -588,6 +588,117 @@ spec:
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
jsonCompare:
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
checkName:
|
||||
type: string
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
fileName:
|
||||
type: string
|
||||
jsonPath:
|
||||
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
|
||||
path:
|
||||
type: string
|
||||
strict:
|
||||
type: BoolString
|
||||
value:
|
||||
type: string
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
kernelConfigs:
|
||||
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
|
||||
selectedConfigs:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
strict:
|
||||
type: BoolString
|
||||
required:
|
||||
- outcomes
|
||||
- selectedConfigs
|
||||
type: object
|
||||
kernelModules:
|
||||
properties:
|
||||
annotations:
|
||||
@@ -686,6 +797,55 @@ spec:
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
networkNamespaceConnectivity:
|
||||
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:
|
||||
@@ -735,6 +895,55 @@ spec:
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
sysctl:
|
||||
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
|
||||
systemPackages:
|
||||
properties:
|
||||
annotations:
|
||||
@@ -1129,6 +1338,15 @@ spec:
|
||||
required:
|
||||
- paths
|
||||
type: object
|
||||
cgroups:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
mountPoint:
|
||||
type: string
|
||||
type: object
|
||||
copy:
|
||||
properties:
|
||||
collectorName:
|
||||
@@ -1158,6 +1376,19 @@ spec:
|
||||
required:
|
||||
- path
|
||||
type: object
|
||||
dns:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
hostnames:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- hostnames
|
||||
type: object
|
||||
filesystemPerformance:
|
||||
description: |-
|
||||
FilesystemPerformance benchmarks sequential write latency on a single file.
|
||||
@@ -1217,6 +1448,12 @@ spec:
|
||||
background IOPS feature if enabled, since those must be fixed at 4096.
|
||||
format: int64
|
||||
type: integer
|
||||
runTime:
|
||||
description: |-
|
||||
Limit runtime. The test will run until it completes the configured I/O workload or until it
|
||||
has run for this specified amount of time, whichever occurs first. When the unit is omitted,
|
||||
the value is interpreted in seconds. Defaults to 120 seconds. Set to "0" to disable.
|
||||
type: string
|
||||
sync:
|
||||
description: Whether to call sync on the file after each
|
||||
write. Does not apply to background IOPS task.
|
||||
@@ -1261,11 +1498,34 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
proxy:
|
||||
type: string
|
||||
timeout:
|
||||
description: |-
|
||||
Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.
|
||||
Missing value or empty string or means no timeout.
|
||||
type: string
|
||||
tls:
|
||||
properties:
|
||||
cacert:
|
||||
type: string
|
||||
clientCert:
|
||||
type: string
|
||||
clientKey:
|
||||
type: string
|
||||
secret:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
namespace:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- namespace
|
||||
type: object
|
||||
skipVerify:
|
||||
type: boolean
|
||||
type: object
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -1281,11 +1541,34 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
proxy:
|
||||
type: string
|
||||
timeout:
|
||||
description: |-
|
||||
Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.
|
||||
Missing value or empty string or means no timeout.
|
||||
type: string
|
||||
tls:
|
||||
properties:
|
||||
cacert:
|
||||
type: string
|
||||
clientCert:
|
||||
type: string
|
||||
clientKey:
|
||||
type: string
|
||||
secret:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
namespace:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- namespace
|
||||
type: object
|
||||
skipVerify:
|
||||
type: boolean
|
||||
type: object
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -1301,11 +1584,34 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
proxy:
|
||||
type: string
|
||||
timeout:
|
||||
description: |-
|
||||
Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.
|
||||
Missing value or empty string or means no timeout.
|
||||
type: string
|
||||
tls:
|
||||
properties:
|
||||
cacert:
|
||||
type: string
|
||||
clientCert:
|
||||
type: string
|
||||
clientKey:
|
||||
type: string
|
||||
secret:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
namespace:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- namespace
|
||||
type: object
|
||||
skipVerify:
|
||||
type: boolean
|
||||
type: object
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -1338,6 +1644,42 @@ spec:
|
||||
exclude:
|
||||
type: BoolString
|
||||
type: object
|
||||
journald:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
dmesg:
|
||||
type: boolean
|
||||
exclude:
|
||||
type: BoolString
|
||||
lines:
|
||||
type: integer
|
||||
output:
|
||||
type: string
|
||||
reverse:
|
||||
type: boolean
|
||||
since:
|
||||
type: string
|
||||
system:
|
||||
type: boolean
|
||||
timeout:
|
||||
type: string
|
||||
units:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
until:
|
||||
type: string
|
||||
utc:
|
||||
type: boolean
|
||||
type: object
|
||||
kernelConfigs:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
type: object
|
||||
kernelModules:
|
||||
properties:
|
||||
collectorName:
|
||||
@@ -1359,6 +1701,25 @@ spec:
|
||||
exclude:
|
||||
type: BoolString
|
||||
type: object
|
||||
networkNamespaceConnectivity:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
fromCIDR:
|
||||
type: string
|
||||
port:
|
||||
type: integer
|
||||
timeout:
|
||||
type: string
|
||||
toCIDR:
|
||||
type: string
|
||||
required:
|
||||
- fromCIDR
|
||||
- port
|
||||
- toCIDR
|
||||
type: object
|
||||
run:
|
||||
properties:
|
||||
args:
|
||||
@@ -1407,6 +1768,13 @@ spec:
|
||||
- CIDRRangeAlloc
|
||||
- desiredCIDR
|
||||
type: object
|
||||
sysctl:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
type: object
|
||||
systemPackages:
|
||||
properties:
|
||||
amzn:
|
||||
@@ -1562,6 +1930,8 @@ spec:
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
uri:
|
||||
type: string
|
||||
type: object
|
||||
status:
|
||||
description: HostCollectorStatus defines the observed state of HostCollector
|
||||
|
||||
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.14.0
|
||||
controller-gen.kubebuilder.io/version: v0.16.2
|
||||
name: hostpreflights.troubleshoot.sh
|
||||
spec:
|
||||
group: troubleshoot.sh
|
||||
@@ -588,6 +588,117 @@ spec:
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
jsonCompare:
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
checkName:
|
||||
type: string
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
fileName:
|
||||
type: string
|
||||
jsonPath:
|
||||
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
|
||||
path:
|
||||
type: string
|
||||
strict:
|
||||
type: BoolString
|
||||
value:
|
||||
type: string
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
kernelConfigs:
|
||||
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
|
||||
selectedConfigs:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
strict:
|
||||
type: BoolString
|
||||
required:
|
||||
- outcomes
|
||||
- selectedConfigs
|
||||
type: object
|
||||
kernelModules:
|
||||
properties:
|
||||
annotations:
|
||||
@@ -686,6 +797,55 @@ spec:
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
networkNamespaceConnectivity:
|
||||
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:
|
||||
@@ -735,6 +895,55 @@ spec:
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
sysctl:
|
||||
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
|
||||
systemPackages:
|
||||
properties:
|
||||
annotations:
|
||||
@@ -1129,6 +1338,15 @@ spec:
|
||||
required:
|
||||
- paths
|
||||
type: object
|
||||
cgroups:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
mountPoint:
|
||||
type: string
|
||||
type: object
|
||||
copy:
|
||||
properties:
|
||||
collectorName:
|
||||
@@ -1158,6 +1376,19 @@ spec:
|
||||
required:
|
||||
- path
|
||||
type: object
|
||||
dns:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
hostnames:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- hostnames
|
||||
type: object
|
||||
filesystemPerformance:
|
||||
description: |-
|
||||
FilesystemPerformance benchmarks sequential write latency on a single file.
|
||||
@@ -1217,6 +1448,12 @@ spec:
|
||||
background IOPS feature if enabled, since those must be fixed at 4096.
|
||||
format: int64
|
||||
type: integer
|
||||
runTime:
|
||||
description: |-
|
||||
Limit runtime. The test will run until it completes the configured I/O workload or until it
|
||||
has run for this specified amount of time, whichever occurs first. When the unit is omitted,
|
||||
the value is interpreted in seconds. Defaults to 120 seconds. Set to "0" to disable.
|
||||
type: string
|
||||
sync:
|
||||
description: Whether to call sync on the file after each
|
||||
write. Does not apply to background IOPS task.
|
||||
@@ -1261,11 +1498,34 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
proxy:
|
||||
type: string
|
||||
timeout:
|
||||
description: |-
|
||||
Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.
|
||||
Missing value or empty string or means no timeout.
|
||||
type: string
|
||||
tls:
|
||||
properties:
|
||||
cacert:
|
||||
type: string
|
||||
clientCert:
|
||||
type: string
|
||||
clientKey:
|
||||
type: string
|
||||
secret:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
namespace:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- namespace
|
||||
type: object
|
||||
skipVerify:
|
||||
type: boolean
|
||||
type: object
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -1281,11 +1541,34 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
proxy:
|
||||
type: string
|
||||
timeout:
|
||||
description: |-
|
||||
Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.
|
||||
Missing value or empty string or means no timeout.
|
||||
type: string
|
||||
tls:
|
||||
properties:
|
||||
cacert:
|
||||
type: string
|
||||
clientCert:
|
||||
type: string
|
||||
clientKey:
|
||||
type: string
|
||||
secret:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
namespace:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- namespace
|
||||
type: object
|
||||
skipVerify:
|
||||
type: boolean
|
||||
type: object
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -1301,11 +1584,34 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
proxy:
|
||||
type: string
|
||||
timeout:
|
||||
description: |-
|
||||
Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.
|
||||
Missing value or empty string or means no timeout.
|
||||
type: string
|
||||
tls:
|
||||
properties:
|
||||
cacert:
|
||||
type: string
|
||||
clientCert:
|
||||
type: string
|
||||
clientKey:
|
||||
type: string
|
||||
secret:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
namespace:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- namespace
|
||||
type: object
|
||||
skipVerify:
|
||||
type: boolean
|
||||
type: object
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -1338,6 +1644,42 @@ spec:
|
||||
exclude:
|
||||
type: BoolString
|
||||
type: object
|
||||
journald:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
dmesg:
|
||||
type: boolean
|
||||
exclude:
|
||||
type: BoolString
|
||||
lines:
|
||||
type: integer
|
||||
output:
|
||||
type: string
|
||||
reverse:
|
||||
type: boolean
|
||||
since:
|
||||
type: string
|
||||
system:
|
||||
type: boolean
|
||||
timeout:
|
||||
type: string
|
||||
units:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
until:
|
||||
type: string
|
||||
utc:
|
||||
type: boolean
|
||||
type: object
|
||||
kernelConfigs:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
type: object
|
||||
kernelModules:
|
||||
properties:
|
||||
collectorName:
|
||||
@@ -1359,6 +1701,25 @@ spec:
|
||||
exclude:
|
||||
type: BoolString
|
||||
type: object
|
||||
networkNamespaceConnectivity:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
fromCIDR:
|
||||
type: string
|
||||
port:
|
||||
type: integer
|
||||
timeout:
|
||||
type: string
|
||||
toCIDR:
|
||||
type: string
|
||||
required:
|
||||
- fromCIDR
|
||||
- port
|
||||
- toCIDR
|
||||
type: object
|
||||
run:
|
||||
properties:
|
||||
args:
|
||||
@@ -1407,6 +1768,13 @@ spec:
|
||||
- CIDRRangeAlloc
|
||||
- desiredCIDR
|
||||
type: object
|
||||
sysctl:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
type: object
|
||||
systemPackages:
|
||||
properties:
|
||||
amzn:
|
||||
@@ -1663,6 +2031,12 @@ spec:
|
||||
background IOPS feature if enabled, since those must be fixed at 4096.
|
||||
format: int64
|
||||
type: integer
|
||||
runTime:
|
||||
description: |-
|
||||
Limit runtime. The test will run until it completes the configured I/O workload or until it
|
||||
has run for this specified amount of time, whichever occurs first. When the unit is omitted,
|
||||
the value is interpreted in seconds. Defaults to 120 seconds. Set to "0" to disable.
|
||||
type: string
|
||||
sync:
|
||||
description: Whether to call sync on the file after each
|
||||
write. Does not apply to background IOPS task.
|
||||
@@ -1679,6 +2053,13 @@ spec:
|
||||
- backgroundWriteIOPSJobs
|
||||
- enableBackgroundIOPS
|
||||
type: object
|
||||
hostOS:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
type: object
|
||||
hostServices:
|
||||
properties:
|
||||
collectorName:
|
||||
@@ -1700,11 +2081,34 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
proxy:
|
||||
type: string
|
||||
timeout:
|
||||
description: |-
|
||||
Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.
|
||||
Missing value or empty string or means no timeout.
|
||||
type: string
|
||||
tls:
|
||||
properties:
|
||||
cacert:
|
||||
type: string
|
||||
clientCert:
|
||||
type: string
|
||||
clientKey:
|
||||
type: string
|
||||
secret:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
namespace:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- namespace
|
||||
type: object
|
||||
skipVerify:
|
||||
type: boolean
|
||||
type: object
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -1720,11 +2124,34 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
proxy:
|
||||
type: string
|
||||
timeout:
|
||||
description: |-
|
||||
Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.
|
||||
Missing value or empty string or means no timeout.
|
||||
type: string
|
||||
tls:
|
||||
properties:
|
||||
cacert:
|
||||
type: string
|
||||
clientCert:
|
||||
type: string
|
||||
clientKey:
|
||||
type: string
|
||||
secret:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
namespace:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- namespace
|
||||
type: object
|
||||
skipVerify:
|
||||
type: boolean
|
||||
type: object
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -1740,11 +2167,34 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
proxy:
|
||||
type: string
|
||||
timeout:
|
||||
description: |-
|
||||
Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.
|
||||
Missing value or empty string or means no timeout.
|
||||
type: string
|
||||
tls:
|
||||
properties:
|
||||
cacert:
|
||||
type: string
|
||||
clientCert:
|
||||
type: string
|
||||
clientKey:
|
||||
type: string
|
||||
secret:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
namespace:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- namespace
|
||||
type: object
|
||||
skipVerify:
|
||||
type: boolean
|
||||
type: object
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -1876,6 +2326,8 @@ spec:
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
uri:
|
||||
type: string
|
||||
type: object
|
||||
status:
|
||||
description: HostPreflightStatus defines the observed state of HostPreflight
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.14.0
|
||||
controller-gen.kubebuilder.io/version: v0.16.2
|
||||
name: redactors.troubleshoot.sh
|
||||
spec:
|
||||
group: troubleshoot.sh
|
||||
@@ -75,6 +75,8 @@ spec:
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
uri:
|
||||
type: string
|
||||
type: object
|
||||
status:
|
||||
description: RedactorStatus defines the observed state of Redactor
|
||||
|
||||
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.14.0
|
||||
controller-gen.kubebuilder.io/version: v0.16.2
|
||||
name: remotecollectors.troubleshoot.sh
|
||||
spec:
|
||||
group: troubleshoot.sh
|
||||
@@ -171,6 +171,12 @@ spec:
|
||||
background IOPS feature if enabled, since those must be fixed at 4096.
|
||||
format: int64
|
||||
type: integer
|
||||
runTime:
|
||||
description: |-
|
||||
Limit runtime. The test will run until it completes the configured I/O workload or until it
|
||||
has run for this specified amount of time, whichever occurs first. When the unit is omitted,
|
||||
the value is interpreted in seconds. Defaults to 120 seconds. Set to "0" to disable.
|
||||
type: string
|
||||
sync:
|
||||
description: Whether to call sync on the file after each
|
||||
write. Does not apply to background IOPS task.
|
||||
@@ -187,6 +193,13 @@ spec:
|
||||
- backgroundWriteIOPSJobs
|
||||
- enableBackgroundIOPS
|
||||
type: object
|
||||
hostOS:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
type: object
|
||||
hostServices:
|
||||
properties:
|
||||
collectorName:
|
||||
@@ -208,11 +221,34 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
proxy:
|
||||
type: string
|
||||
timeout:
|
||||
description: |-
|
||||
Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.
|
||||
Missing value or empty string or means no timeout.
|
||||
type: string
|
||||
tls:
|
||||
properties:
|
||||
cacert:
|
||||
type: string
|
||||
clientCert:
|
||||
type: string
|
||||
clientKey:
|
||||
type: string
|
||||
secret:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
namespace:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- namespace
|
||||
type: object
|
||||
skipVerify:
|
||||
type: boolean
|
||||
type: object
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -228,11 +264,34 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
proxy:
|
||||
type: string
|
||||
timeout:
|
||||
description: |-
|
||||
Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.
|
||||
Missing value or empty string or means no timeout.
|
||||
type: string
|
||||
tls:
|
||||
properties:
|
||||
cacert:
|
||||
type: string
|
||||
clientCert:
|
||||
type: string
|
||||
clientKey:
|
||||
type: string
|
||||
secret:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
namespace:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- namespace
|
||||
type: object
|
||||
skipVerify:
|
||||
type: boolean
|
||||
type: object
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -248,11 +307,34 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
proxy:
|
||||
type: string
|
||||
timeout:
|
||||
description: |-
|
||||
Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.
|
||||
Missing value or empty string or means no timeout.
|
||||
type: string
|
||||
tls:
|
||||
properties:
|
||||
cacert:
|
||||
type: string
|
||||
clientCert:
|
||||
type: string
|
||||
clientKey:
|
||||
type: string
|
||||
secret:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
namespace:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- namespace
|
||||
type: object
|
||||
skipVerify:
|
||||
type: boolean
|
||||
type: object
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -388,6 +470,8 @@ spec:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
uri:
|
||||
type: string
|
||||
type: object
|
||||
status:
|
||||
description: CollectorStatus defines the observed state of Collector
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -37,6 +37,7 @@ preflight [url] [flags]
|
||||
--kubeconfig string Path to the kubeconfig file to use for CLI requests.
|
||||
--memprofile string File path to write memory profiling data
|
||||
-n, --namespace string If present, the namespace scope for this CLI request
|
||||
--no-uri When this flag is used, Preflight does not attempt to retrieve the spec referenced by the uri: field`
|
||||
-o, --output string specify the output file path for the preflight checks
|
||||
--request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0")
|
||||
--selector string selector (label query) to filter remote collection nodes on.
|
||||
@@ -54,4 +55,4 @@ preflight [url] [flags]
|
||||
* [preflight oci-fetch](preflight_oci-fetch.md) - Fetch a preflight from an OCI registry and print it to standard out
|
||||
* [preflight version](preflight_version.md) - Print the current version and exit
|
||||
|
||||
###### Auto generated by spf13/cobra on 25-Jan-2024
|
||||
###### Auto generated by spf13/cobra on 23-Aug-2024
|
||||
|
||||
@@ -34,4 +34,4 @@ preflight oci-fetch [URI] [flags]
|
||||
|
||||
* [preflight](preflight.md) - Run and retrieve preflight checks in a cluster
|
||||
|
||||
###### Auto generated by spf13/cobra on 25-Jan-2024
|
||||
###### Auto generated by spf13/cobra on 23-Aug-2024
|
||||
|
||||
@@ -37,4 +37,4 @@ preflight version [flags]
|
||||
|
||||
* [preflight](preflight.md) - Run and retrieve preflight checks in a cluster
|
||||
|
||||
###### Auto generated by spf13/cobra on 25-Jan-2024
|
||||
###### Auto generated by spf13/cobra on 23-Aug-2024
|
||||
|
||||
+13
-5
@@ -1,11 +1,19 @@
|
||||
## support-bundle
|
||||
|
||||
Generate a support bundle
|
||||
Generate a support bundle from a Kubernetes cluster or specified sources
|
||||
|
||||
### Synopsis
|
||||
|
||||
A support bundle is an archive of files, output, metrics and state
|
||||
from a server that can be used to assist when troubleshooting a Kubernetes cluster.
|
||||
Generate a support bundle, an archive containing files, output, metrics, and cluster state to aid in troubleshooting Kubernetes clusters.
|
||||
|
||||
If no arguments are provided, specs are automatically loaded from the cluster by default.
|
||||
|
||||
**Argument Types**:
|
||||
1. **Secret**: Load specs from a Kubernetes Secret. Format: "secret/namespace-name/secret-name[/data-key]"
|
||||
2. **ConfigMap**: Load specs from a Kubernetes ConfigMap. Format: "configmap/namespace-name/configmap-name[/data-key]"
|
||||
3. **File**: Load specs from a local file. Format: Local file path
|
||||
4. **Standard Input**: Read specs from stdin. Format: "-"
|
||||
5. **URL**: Load specs from a URL. Supports HTTP and OCI registry URLs.
|
||||
|
||||
```
|
||||
support-bundle [urls...] [flags]
|
||||
@@ -32,7 +40,7 @@ support-bundle [urls...] [flags]
|
||||
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
|
||||
--interactive enable/disable interactive mode (default true)
|
||||
--kubeconfig string Path to the kubeconfig file to use for CLI requests.
|
||||
--load-cluster-specs enable/disable loading additional troubleshoot specs found within the cluster. required when no specs are provided on the command line
|
||||
--load-cluster-specs enable/disable loading additional troubleshoot specs found within the cluster. This is the default behavior if no spec is provided as an argument
|
||||
--memprofile string File path to write memory profiling data
|
||||
-n, --namespace string If present, the namespace scope for this CLI request
|
||||
--no-uri When this flag is used, Troubleshoot does not attempt to retrieve the spec referenced by the uri: field`
|
||||
@@ -56,4 +64,4 @@ support-bundle [urls...] [flags]
|
||||
* [support-bundle redact](support-bundle_redact.md) - Redact information from a generated support bundle archive
|
||||
* [support-bundle version](support-bundle_version.md) - Print the current version and exit
|
||||
|
||||
###### Auto generated by spf13/cobra on 25-Jan-2024
|
||||
###### Auto generated by spf13/cobra on 23-Aug-2024
|
||||
|
||||
@@ -28,6 +28,6 @@ support-bundle analyze [url] [flags]
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [support-bundle](support-bundle.md) - Generate a support bundle
|
||||
* [support-bundle](support-bundle.md) - Generate a support bundle from a Kubernetes cluster or specified sources
|
||||
|
||||
###### Auto generated by spf13/cobra on 25-Jan-2024
|
||||
###### Auto generated by spf13/cobra on 23-Aug-2024
|
||||
|
||||
@@ -13,7 +13,7 @@ be stored in the current directory in the path provided by the --output flag.
|
||||
The [urls...] argument is a list of either oci://.., http://.., https://.. or local paths to yaml files.
|
||||
|
||||
For more information on redactors visit https://troubleshoot.sh/docs/redact/
|
||||
|
||||
|
||||
|
||||
```
|
||||
support-bundle redact [urls...] [flags]
|
||||
@@ -37,6 +37,6 @@ support-bundle redact [urls...] [flags]
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [support-bundle](support-bundle.md) - Generate a support bundle
|
||||
* [support-bundle](support-bundle.md) - Generate a support bundle from a Kubernetes cluster or specified sources
|
||||
|
||||
###### Auto generated by spf13/cobra on 25-Jan-2024
|
||||
###### Auto generated by spf13/cobra on 23-Aug-2024
|
||||
|
||||
@@ -25,6 +25,6 @@ support-bundle version [flags]
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [support-bundle](support-bundle.md) - Generate a support bundle
|
||||
* [support-bundle](support-bundle.md) - Generate a support bundle from a Kubernetes cluster or specified sources
|
||||
|
||||
###### Auto generated by spf13/cobra on 25-Jan-2024
|
||||
###### Auto generated by spf13/cobra on 23-Aug-2024
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
apiVersion: troubleshoot.sh/v1beta2
|
||||
kind: HostCollector
|
||||
metadata:
|
||||
name: sysctl
|
||||
spec:
|
||||
collectors:
|
||||
- sysctl:
|
||||
collectorName: sysctl
|
||||
@@ -0,0 +1,18 @@
|
||||
apiVersion: troubleshoot.sh/v1beta2
|
||||
kind: HostPreflight
|
||||
metadata:
|
||||
name: sysctl
|
||||
spec:
|
||||
collectors:
|
||||
- sysctl:
|
||||
collectorName: host sysctl
|
||||
analyzers:
|
||||
- sysctl:
|
||||
collectorName: host sysctl
|
||||
outcomes:
|
||||
- warn:
|
||||
when: 'kern.ostype == Darwin'
|
||||
message: "Running sysctl on a Darwin host"
|
||||
- pass:
|
||||
when: 'net.ipv4.conf.default.arp_ignore > 0'
|
||||
message: "ARP ignore is enabled for the default interfaces interfaces on the host."
|
||||
@@ -1,6 +1,8 @@
|
||||
module helm-template
|
||||
|
||||
go 1.22.0
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.4
|
||||
|
||||
// Always use the local version of troubleshoot so as to build using
|
||||
// the latest version of the library. This will ensure the example
|
||||
@@ -9,30 +11,32 @@ replace github.com/replicatedhq/troubleshoot v0.0.0 => ../../../
|
||||
|
||||
require (
|
||||
github.com/replicatedhq/troubleshoot v0.0.0
|
||||
helm.sh/helm/v3 v3.14.4
|
||||
helm.sh/helm/v3 v3.16.4
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.3.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.10 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // 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.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
github.com/imdario/mergo v0.3.15 // 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.7.7 // indirect
|
||||
@@ -42,31 +46,31 @@ require (
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
golang.org/x/crypto v0.22.0 // indirect
|
||||
golang.org/x/net v0.24.0 // indirect
|
||||
golang.org/x/oauth2 v0.18.0 // indirect
|
||||
golang.org/x/sys v0.19.0 // indirect
|
||||
golang.org/x/term v0.19.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/oauth2 v0.23.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/term v0.27.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
google.golang.org/protobuf v1.35.2 // 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.30.0 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.30.0 // indirect
|
||||
k8s.io/apimachinery v0.30.0 // indirect
|
||||
k8s.io/client-go v0.30.0 // indirect
|
||||
k8s.io/klog/v2 v2.120.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
|
||||
sigs.k8s.io/controller-runtime v0.17.3 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
|
||||
k8s.io/api v0.32.0 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.32.0 // indirect
|
||||
k8s.io/apimachinery v0.32.0 // indirect
|
||||
k8s.io/client-go v0.32.0 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
|
||||
sigs.k8s.io/controller-runtime v0.19.3 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
|
||||
)
|
||||
|
||||
@@ -1,83 +1,69 @@
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||
github.com/BurntSushi/toml v1.4.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.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
|
||||
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
||||
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
|
||||
github.com/Masterminds/semver/v3 v3.3.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.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8=
|
||||
github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM=
|
||||
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/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI=
|
||||
github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
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/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.22.10 h1:4y86NVn7Z2yYd6pfS4Z+Nyh3aAUL3Nul+LMbhFKy0gA=
|
||||
github.com/go-openapi/swag v0.22.10/go.mod h1:Cnn8BYtRlx6BNE3DPN86f/xkapGIcLWzh3CLEb4C1jI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/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.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
|
||||
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
|
||||
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
|
||||
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
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.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
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.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -87,36 +73,29 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||
github.com/modern-go/reflect2 v1.0.2/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.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY=
|
||||
github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw=
|
||||
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
|
||||
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
|
||||
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
|
||||
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
||||
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
@@ -126,112 +105,81 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
|
||||
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
|
||||
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
|
||||
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
|
||||
golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
|
||||
golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
|
||||
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
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.14.4 h1:6FSpEfqyDalHq3kUr4gOMThhgY55kXUEjdQoyODYnrM=
|
||||
helm.sh/helm/v3 v3.14.4/go.mod h1:Tje7LL4gprZpuBNTbG34d1Xn5NmRT3OWfBRwpOSer9I=
|
||||
k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw=
|
||||
k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80=
|
||||
k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE=
|
||||
k8s.io/apiextensions-apiserver v0.29.3 h1:9HF+EtZaVpFjStakF4yVufnXGPRppWFEQ87qnO91YeI=
|
||||
k8s.io/apiextensions-apiserver v0.29.3/go.mod h1:po0XiY5scnpJfFizNGo6puNU6Fq6D70UJY2Cb2KwAVc=
|
||||
k8s.io/apiextensions-apiserver v0.30.0/go.mod h1:N9ogQFGcrbWqAY9p2mUAL5mGxsLqwgtUce127VtRX5Y=
|
||||
k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU=
|
||||
k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU=
|
||||
k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc=
|
||||
k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg=
|
||||
k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0=
|
||||
k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY=
|
||||
k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
|
||||
k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780=
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/controller-runtime v0.17.3 h1:65QmN7r3FWgTxDMz9fvGnO1kbf2nu+acg9p2R9oYYYk=
|
||||
sigs.k8s.io/controller-runtime v0.17.3/go.mod h1:N0jpP5Lo7lMTF9aL56Z/B2oWBJjey6StQM0jRbKQXtY=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
|
||||
helm.sh/helm/v3 v3.16.4 h1:rBn/h9MACw+QlhxQTjpl8Ifx+VTWaYsw3rguGBYBzr0=
|
||||
helm.sh/helm/v3 v3.16.4/go.mod h1:k8QPotUt57wWbi90w3LNmg3/MWcLPigVv+0/X4B8BzA=
|
||||
k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE=
|
||||
k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0=
|
||||
k8s.io/apiextensions-apiserver v0.32.0 h1:S0Xlqt51qzzqjKPxfgX1xh4HBZE+p8KKBq+k2SWNOE0=
|
||||
k8s.io/apiextensions-apiserver v0.32.0/go.mod h1:86hblMvN5yxMvZrZFX2OhIHAuFIMJIZ19bTvzkP+Fmw=
|
||||
k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg=
|
||||
k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
|
||||
k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8=
|
||||
k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
|
||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/controller-runtime v0.19.3 h1:XO2GvC9OPftRst6xWCpTgBZO04S2cbp0Qqkj8bX1sPw=
|
||||
sigs.k8s.io/controller-runtime v0.19.3/go.mod h1:j4j87DqtsThvwTv5/Tc5NFRyyF/RF0ip4+62tbTSIUM=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
module github.com/replicatedhq/troubleshoot
|
||||
|
||||
go 1.22.0
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.4
|
||||
|
||||
require (
|
||||
github.com/ahmetalpbalkan/go-cursor v0.0.0-20131010032410-8136607ea412
|
||||
github.com/apparentlymart/go-cidr v1.1.0
|
||||
github.com/blang/semver/v4 v4.0.0
|
||||
github.com/containers/image/v5 v5.30.1
|
||||
github.com/distribution/distribution/v3 v3.0.0-alpha.1
|
||||
github.com/fatih/color v1.17.0
|
||||
github.com/go-logr/logr v1.4.1
|
||||
github.com/cilium/ebpf v0.17.1
|
||||
github.com/containerd/cgroups/v3 v3.0.5
|
||||
github.com/containers/image/v5 v5.33.0
|
||||
github.com/distribution/distribution/v3 v3.0.0-rc.2
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/go-logr/logr v1.4.2
|
||||
github.com/go-redis/redis/v7 v7.4.1
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/gobwas/glob v0.2.3
|
||||
@@ -17,152 +21,179 @@ require (
|
||||
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.7.4
|
||||
github.com/hashicorp/go-getter v1.7.6
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/jackc/pgx/v5 v5.5.5
|
||||
github.com/jackc/pgx/v5 v5.7.2
|
||||
github.com/longhorn/go-iscsi-helper v0.0.0-20210330030558-49a327fb024e
|
||||
github.com/manifoldco/promptui v0.9.0
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/mholt/archiver/v3 v3.5.1
|
||||
github.com/microsoft/go-mssqldb v1.7.1
|
||||
github.com/microsoft/go-mssqldb v1.8.0
|
||||
github.com/miekg/dns v1.1.62
|
||||
github.com/opencontainers/image-spec v1.1.0
|
||||
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/v3 v3.24.4
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/shirou/gopsutil/v3 v3.24.5
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.18.2
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/spf13/viper v1.19.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tj/go-spin v1.1.0
|
||||
github.com/vmware-tanzu/velero v1.13.2
|
||||
go.opentelemetry.io/otel v1.26.0
|
||||
go.opentelemetry.io/otel/sdk v1.26.0
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225
|
||||
golang.org/x/mod v0.17.0
|
||||
golang.org/x/sync v0.7.0
|
||||
github.com/vishvananda/netlink v1.3.0
|
||||
github.com/vishvananda/netns v0.0.5
|
||||
github.com/vmware-tanzu/velero v1.15.1
|
||||
go.opentelemetry.io/otel v1.33.0
|
||||
go.opentelemetry.io/otel/sdk v1.33.0
|
||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
|
||||
golang.org/x/mod v0.22.0
|
||||
golang.org/x/sync v0.10.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
k8s.io/api v0.30.0
|
||||
k8s.io/apiextensions-apiserver v0.30.0
|
||||
k8s.io/apimachinery v0.30.0
|
||||
k8s.io/apiserver v0.30.0
|
||||
k8s.io/cli-runtime v0.30.0
|
||||
k8s.io/client-go v0.30.0
|
||||
k8s.io/klog/v2 v2.120.1
|
||||
oras.land/oras-go v1.2.5
|
||||
sigs.k8s.io/controller-runtime v0.18.2
|
||||
sigs.k8s.io/e2e-framework v0.3.0
|
||||
k8s.io/api v0.32.0
|
||||
k8s.io/apiextensions-apiserver v0.32.0
|
||||
k8s.io/apimachinery v0.32.0
|
||||
k8s.io/apiserver v0.32.0
|
||||
k8s.io/cli-runtime v0.32.0
|
||||
k8s.io/client-go v0.32.0
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
oras.land/oras-go v1.2.6
|
||||
sigs.k8s.io/controller-runtime v0.19.3
|
||||
sigs.k8s.io/e2e-framework v0.5.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
cel.dev/expr v0.18.0 // indirect
|
||||
cloud.google.com/go/auth v0.9.3 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.5.1 // indirect
|
||||
cloud.google.com/go/monitoring v1.21.0 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.1 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 // indirect
|
||||
github.com/MakeNowJust/heredoc v1.0.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
|
||||
github.com/chai2010/gettext-go v1.0.2 // indirect
|
||||
github.com/containerd/cgroups/v3 v3.0.2 // indirect
|
||||
github.com/containerd/errdefs v0.1.0 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect
|
||||
github.com/containerd/errdefs v0.3.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
|
||||
github.com/distribution/reference v0.5.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/containerd/typeurl/v2 v2.2.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/envoyproxy/go-control-plane v0.13.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
|
||||
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
|
||||
github.com/go-logr/stdr v1.2.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.6.8 // indirect
|
||||
github.com/google/go-containerregistry v0.19.0 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/google/go-containerregistry v0.20.2 // indirect
|
||||
github.com/google/s2a-go v0.1.8 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/gosuri/uitable v0.0.4 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jmoiron/sqlx v1.4.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/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
github.com/mistifyio/go-zfs/v3 v3.0.1 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/sys/user v0.1.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/sys/capability v0.3.0 // indirect
|
||||
github.com/moby/sys/user v0.3.0 // indirect
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/rubenv/sql-migrate v1.5.2 // indirect
|
||||
github.com/rubenv/sql-migrate v1.7.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/sylabs/sif/v2 v2.15.1 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/sylabs/sif/v2 v2.19.1 // indirect
|
||||
github.com/tchap/go-patricia/v2 v2.3.1 // indirect
|
||||
github.com/vladimirvivien/gexe v0.2.0 // indirect
|
||||
github.com/vladimirvivien/gexe v0.3.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.26.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.26.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.29.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.33.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/tools v0.18.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
|
||||
k8s.io/component-base v0.30.0 // indirect
|
||||
k8s.io/kubectl v0.29.0 // indirect
|
||||
golang.org/x/tools v0.27.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect
|
||||
google.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
k8s.io/component-base v0.32.0 // indirect
|
||||
k8s.io/kubectl v0.31.3 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.110.10 // indirect
|
||||
cloud.google.com/go/compute v1.23.3 // indirect
|
||||
cloud.google.com/go/iam v1.1.5 // indirect
|
||||
cloud.google.com/go/storage v1.35.1 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/Microsoft/hcsshim v0.12.0-rc.3 // indirect
|
||||
cloud.google.com/go v0.115.1 // indirect
|
||||
cloud.google.com/go/iam v1.2.1 // indirect
|
||||
cloud.google.com/go/storage v1.45.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/Microsoft/hcsshim v0.12.9 // indirect
|
||||
github.com/andybalholm/brotli v1.0.1 // indirect
|
||||
github.com/aws/aws-sdk-go v1.48.10 // indirect
|
||||
github.com/aws/aws-sdk-go v1.55.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
|
||||
github.com/c9s/goprocinfo v0.0.0-20170724085704-0010a05ce49f // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/containerd/containerd v1.7.13 // indirect
|
||||
github.com/containerd/containerd v1.7.23 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect
|
||||
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
|
||||
github.com/containers/ocicrypt v1.1.9 // indirect
|
||||
github.com/containers/storage v1.53.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
github.com/containers/ocicrypt v1.2.0 // indirect
|
||||
github.com/containers/storage v1.56.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.3.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/docker/cli v25.0.3+incompatible // indirect
|
||||
github.com/docker/docker v25.0.5+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.8.1 // indirect
|
||||
github.com/docker/cli v27.3.1+incompatible // indirect
|
||||
github.com/docker/docker v27.3.1+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.8.2 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
||||
github.com/evanphx/json-patch v5.7.0+incompatible // indirect
|
||||
github.com/evanphx/json-patch v5.9.0+incompatible // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-errors/errors v1.4.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.10 // 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.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
@@ -171,34 +202,32 @@ require (
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/go-intervals v0.0.2 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-safetemp v1.0.0 // indirect
|
||||
github.com/hashicorp/go-version v1.6.0
|
||||
github.com/hashicorp/go-version v1.7.0
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/imdario/mergo v0.3.15 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.7 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mattn/go-shellwords v1.0.12 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moby/locker v1.0.1 // indirect
|
||||
github.com/moby/spdystream v0.2.0 // indirect
|
||||
github.com/moby/sys/mountinfo v0.7.1 // indirect
|
||||
github.com/moby/spdystream v0.5.0 // indirect
|
||||
github.com/moby/sys/mountinfo v0.7.2 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
@@ -207,57 +236,53 @@ require (
|
||||
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect
|
||||
github.com/nwaples/rardecode v1.1.2 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.2.0 // indirect
|
||||
github.com/opencontainers/selinux v1.11.0 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.2.0
|
||||
github.com/opencontainers/selinux v1.11.1 // indirect
|
||||
github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.18.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.45.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/prometheus/client_golang v1.20.5 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.60.1 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/ulikunitz/xz v0.5.11 // indirect
|
||||
github.com/vbatts/tar-split v0.11.5 // indirect
|
||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||
github.com/vbatts/tar-split v0.11.6 // indirect
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/net v0.25.0
|
||||
golang.org/x/oauth2 v0.18.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/term v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/api v0.153.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect
|
||||
google.golang.org/grpc v1.59.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/net v0.33.0
|
||||
golang.org/x/oauth2 v0.23.0 // indirect
|
||||
golang.org/x/sys v0.28.0
|
||||
golang.org/x/term v0.27.0 // indirect
|
||||
golang.org/x/text v0.21.0
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
google.golang.org/api v0.197.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
google.golang.org/grpc v1.68.0 // indirect
|
||||
google.golang.org/protobuf v1.35.2 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
helm.sh/helm/v3 v3.14.4
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
|
||||
k8s.io/kubelet v0.30.0
|
||||
k8s.io/metrics v0.30.0
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b
|
||||
helm.sh/helm/v3 v3.16.4
|
||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
|
||||
k8s.io/kubelet v0.32.0
|
||||
k8s.io/metrics v0.32.0
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
|
||||
periph.io/x/host/v3 v3.8.2
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.18.0 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
)
|
||||
|
||||
|
||||
+117
-17
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/internal/util"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
@@ -78,6 +79,7 @@ func LoadFromCLIArgs(ctx context.Context, client kubernetes.Interface, args []st
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
allURLSpecs := loader.NewTroubleshootKinds()
|
||||
rawSpecs := []string{}
|
||||
|
||||
for _, v := range args {
|
||||
@@ -174,22 +176,34 @@ func LoadFromCLIArgs(ctx context.Context, client kubernetes.Interface, args []st
|
||||
if err != nil {
|
||||
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
|
||||
}
|
||||
|
||||
var rawURLSpec string
|
||||
headers := map[string]string{}
|
||||
|
||||
if parsedURL.Host == "kots.io" {
|
||||
// To download specs from kots.io, we need to set the User-Agent header
|
||||
rawSpec, err := downloadFromHttpURL(ctx, v, map[string]string{
|
||||
"User-Agent": "Replicated_Troubleshoot/v1beta1",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawSpecs = append(rawSpecs, rawSpec)
|
||||
} else {
|
||||
rawSpec, err := downloadFromHttpURL(ctx, v, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawSpecs = append(rawSpecs, rawSpec)
|
||||
headers["User-Agent"] = "Replicated_Troubleshoot/v1beta1"
|
||||
}
|
||||
rawURLSpec, err = downloadFromHttpURL(ctx, v, headers)
|
||||
if err != nil {
|
||||
fmt.Println(color.YellowString("failed to download spec from URI %q: %v\n", v, err))
|
||||
continue
|
||||
}
|
||||
|
||||
// load URL spec first to remove URI key from the spec
|
||||
urlSpec, err := loader.LoadSpecs(ctx, loader.LoadOptions{
|
||||
RawSpec: rawURLSpec,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println(color.YellowString("failed to load spec from URI %q: %v\n", v, err))
|
||||
continue
|
||||
}
|
||||
// remove URI key from the spec if any
|
||||
for i := range urlSpec.SupportBundlesV1Beta2 {
|
||||
urlSpec.SupportBundlesV1Beta2[i].Spec.Uri = ""
|
||||
}
|
||||
|
||||
allURLSpecs.Add(urlSpec)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,14 +214,20 @@ func LoadFromCLIArgs(ctx context.Context, client kubernetes.Interface, args []st
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if allURLSpecs.Len() > 0 {
|
||||
kinds.Add(allURLSpecs)
|
||||
}
|
||||
|
||||
if vp.GetBool("load-cluster-specs") {
|
||||
clusterKinds, err := LoadFromCluster(ctx, client, vp.GetStringSlice("selector"), vp.GetString("namespace"))
|
||||
if err != nil {
|
||||
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
|
||||
if kinds.IsEmpty() {
|
||||
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
|
||||
}
|
||||
fmt.Println(color.YellowString("failed to load specs from cluster: %v\n", err))
|
||||
} else {
|
||||
kinds.Add(clusterKinds)
|
||||
}
|
||||
|
||||
kinds.Add(clusterKinds)
|
||||
}
|
||||
|
||||
return kinds, nil
|
||||
@@ -251,6 +271,8 @@ func downloadFromHttpURL(ctx context.Context, url string, headers map[string]str
|
||||
// to list & read secrets and configmaps from all namespaces, we will fallback to trying each
|
||||
// namespace individually, and eventually default to the configured kubeconfig namespace.
|
||||
func LoadFromCluster(ctx context.Context, client kubernetes.Interface, selectors []string, ns string) (*loader.TroubleshootKinds, error) {
|
||||
klog.V(1).Infof("Load troubleshoot specs from the cluster using selectors: %v", selectors)
|
||||
|
||||
if reflect.DeepEqual(selectors, []string{"troubleshoot.sh/kind=support-bundle"}) {
|
||||
// Its the default selector so we append troubleshoot.io/kind=support-bundle to it due to backwards compatibility
|
||||
selectors = append(selectors, "troubleshoot.io/kind=support-bundle")
|
||||
@@ -344,3 +366,81 @@ func LoadFromCluster(ctx context.Context, client kubernetes.Interface, selectors
|
||||
RawSpecs: rawSpecs,
|
||||
})
|
||||
}
|
||||
|
||||
// LoadAdditionalSpecFromURIs loads additional specs from the URIs provided in the kinds.
|
||||
// This function will modify kinds in place.
|
||||
func LoadAdditionalSpecFromURIs(ctx context.Context, kinds *loader.TroubleshootKinds) {
|
||||
|
||||
obj := reflect.ValueOf(*kinds)
|
||||
|
||||
// iterate over all fields of the TroubleshootKinds
|
||||
// e.g. SupportBundlesV1Beta2, PreflightsV1Beta2, etc.
|
||||
for i := 0; i < obj.NumField(); i++ {
|
||||
field := obj.Field(i)
|
||||
if field.Kind() != reflect.Slice {
|
||||
continue
|
||||
}
|
||||
|
||||
// look at each spec in the slice
|
||||
// e.g. each spec in []PreflightsV1Beta2
|
||||
for count := 0; count < field.Len(); count++ {
|
||||
currentSpec := field.Index(count)
|
||||
specName := currentSpec.Type().Name()
|
||||
|
||||
// check if .Spec.Uri exists
|
||||
specField := currentSpec.FieldByName("Spec")
|
||||
if !specField.IsValid() {
|
||||
continue
|
||||
}
|
||||
uriField := specField.FieldByName("Uri")
|
||||
if uriField.Kind() != reflect.String {
|
||||
continue
|
||||
}
|
||||
|
||||
// download spec from URI
|
||||
uri := uriField.String()
|
||||
if uri == "" {
|
||||
continue
|
||||
}
|
||||
rawSpec, err := downloadFromHttpURL(ctx, uri, nil)
|
||||
if err != nil {
|
||||
klog.Warningf("failed to download spec from URI %q: %v", uri, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// load spec from raw spec
|
||||
uriKinds, err := loader.LoadSpecs(ctx, loader.LoadOptions{RawSpec: string(rawSpec)})
|
||||
if err != nil {
|
||||
klog.Warningf("failed to load spec from URI %q: %v", uri, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// replace original spec with the loaded spec from URI
|
||||
newSpec := getFirstSpecOf(uriKinds, specName)
|
||||
if !newSpec.IsValid() {
|
||||
klog.Warningf("failed to read spec of type %s in URI %s", specName, uri)
|
||||
continue
|
||||
}
|
||||
currentSpec.Set(newSpec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dynamically get spec from kinds of given name
|
||||
// return first element of the spec slice
|
||||
func getFirstSpecOf(kinds *loader.TroubleshootKinds, name string) reflect.Value {
|
||||
obj := reflect.ValueOf(*kinds)
|
||||
for i := 0; i < obj.NumField(); i++ {
|
||||
field := obj.Field(i)
|
||||
if field.Kind() != reflect.Slice {
|
||||
continue
|
||||
}
|
||||
if field.Len() > 0 {
|
||||
if field.Index(0).Type().Name() == name {
|
||||
// return first element
|
||||
return field.Index(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
return reflect.Value{}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package specs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
@@ -224,3 +227,79 @@ spec:
|
||||
require.NoError(t, err)
|
||||
require.Len(t, specs.HostCollectorsV1Beta2, 1)
|
||||
}
|
||||
|
||||
func TestLoadFromMultipleURIs(t *testing.T) {
|
||||
server1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`apiVersion: troubleshoot.sh/v1beta2
|
||||
kind: HostCollector
|
||||
metadata:
|
||||
name: cpu
|
||||
spec:
|
||||
collectors:
|
||||
- cpu: {}
|
||||
`))
|
||||
}))
|
||||
defer server1.Close()
|
||||
|
||||
server2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}))
|
||||
// immediately close the server to force the error
|
||||
server2.Close()
|
||||
|
||||
// Capture stdout
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
client := testclient.NewSimpleClientset()
|
||||
specs, err := LoadFromCLIArgs(context.Background(), client, []string{server1.URL, server2.URL}, viper.New())
|
||||
|
||||
// Restore stdout
|
||||
w.Close()
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
output := buf.String()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, specs.HostCollectorsV1Beta2, 1)
|
||||
assert.Contains(t, output, "failed to download spec from URI")
|
||||
}
|
||||
|
||||
func TestLoadAdditionalSpecFromURIs(t *testing.T) {
|
||||
m := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`apiVersion: troubleshoot.sh/v1beta2
|
||||
apiVersion: troubleshoot.sh/v1beta2
|
||||
kind: Preflight
|
||||
metadata:
|
||||
name: preflight-2
|
||||
spec:
|
||||
collectors:
|
||||
- ceph: {}
|
||||
`))
|
||||
}))
|
||||
defer m.Close()
|
||||
kinds := loader.NewTroubleshootKinds()
|
||||
kinds.PreflightsV1Beta2 = []troubleshootv1beta2.Preflight{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "preflight-1",
|
||||
},
|
||||
Spec: troubleshootv1beta2.PreflightSpec{
|
||||
Uri: m.URL,
|
||||
Collectors: []*troubleshootv1beta2.Collect{
|
||||
{
|
||||
DNS: &troubleshootv1beta2.DNS{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
LoadAdditionalSpecFromURIs(context.Background(), kinds)
|
||||
require.Len(t, kinds.PreflightsV1Beta2, 1)
|
||||
require.Len(t, kinds.PreflightsV1Beta2[0].Spec.Collectors, 1)
|
||||
require.NotNil(t, kinds.PreflightsV1Beta2[0].Spec.Collectors[0].Ceph)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package util
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -145,3 +146,26 @@ func PromptYesNo(question string) bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Dedup[T any](objs []T) []T {
|
||||
seen := make(map[string]bool)
|
||||
out := []T{}
|
||||
|
||||
if len(objs) == 0 {
|
||||
return objs
|
||||
}
|
||||
|
||||
for _, o := range objs {
|
||||
data, err := json.Marshal(o)
|
||||
if err != nil {
|
||||
out = append(out, o)
|
||||
continue
|
||||
}
|
||||
key := string(data)
|
||||
if _, ok := seen[key]; !ok {
|
||||
out = append(out, o)
|
||||
seen[key] = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
+17
-7
@@ -89,6 +89,11 @@ func HostAnalyze(
|
||||
if err != nil {
|
||||
return NewAnalyzeResultError(analyzer, errors.Wrap(err, "analyze"))
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
klog.Errorf("no outcome matched for %q host analyzer", analyzer.Title())
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -117,13 +122,10 @@ func Analyze(
|
||||
return nil, errors.New("nil analyzer")
|
||||
}
|
||||
|
||||
analyzerInst := getAnalyzer(analyzer)
|
||||
analyzerInst := GetAnalyzer(analyzer)
|
||||
if analyzerInst == nil {
|
||||
return []*AnalyzeResult{{
|
||||
IsFail: true,
|
||||
Title: "nonexistent analyzer",
|
||||
Message: "Analyzer not found",
|
||||
}}, nil
|
||||
klog.Info("Non-existent analyzer found in the spec. Please double-check the spelling and indentation of the analyzers in the spec.")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
_, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, analyzerInst.Title())
|
||||
@@ -151,6 +153,10 @@ func Analyze(
|
||||
results = []*AnalyzeResult{}
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
klog.Errorf("no outcome matched for %q analyzer", analyzerInst.Title())
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
@@ -182,7 +188,7 @@ type Analyzer interface {
|
||||
Analyze(getFile getCollectedFileContents, findFiles getChildCollectedFileContents) ([]*AnalyzeResult, error)
|
||||
}
|
||||
|
||||
func getAnalyzer(analyzer *troubleshootv1beta2.Analyze) Analyzer {
|
||||
func GetAnalyzer(analyzer *troubleshootv1beta2.Analyze) Analyzer {
|
||||
switch {
|
||||
case analyzer.ClusterVersion != nil:
|
||||
return &AnalyzeClusterVersion{analyzer: analyzer.ClusterVersion}
|
||||
@@ -208,6 +214,8 @@ func getAnalyzer(analyzer *troubleshootv1beta2.Analyze) Analyzer {
|
||||
return &AnalyzeReplicaSetStatus{analyzer: analyzer.ReplicaSetStatus}
|
||||
case analyzer.ClusterPodStatuses != nil:
|
||||
return &AnalyzeClusterPodStatuses{analyzer: analyzer.ClusterPodStatuses}
|
||||
case analyzer.ClusterContainerStatuses != nil:
|
||||
return &AnalyzeClusterContainerStatuses{analyzer: analyzer.ClusterContainerStatuses}
|
||||
case analyzer.ContainerRuntime != nil:
|
||||
return &AnalyzeContainerRuntime{analyzer: analyzer.ContainerRuntime}
|
||||
case analyzer.Distribution != nil:
|
||||
@@ -250,6 +258,8 @@ func getAnalyzer(analyzer *troubleshootv1beta2.Analyze) Analyzer {
|
||||
return &AnalyzeEvent{analyzer: analyzer.Event}
|
||||
case analyzer.NodeMetrics != nil:
|
||||
return &AnalyzeNodeMetrics{analyzer: analyzer.NodeMetrics}
|
||||
case analyzer.HTTP != nil:
|
||||
return &AnalyzeHTTPAnalyze{analyzer: analyzer.HTTP}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
type AnalyzeClusterContainerStatuses struct {
|
||||
analyzer *troubleshootv1beta2.ClusterContainerStatuses
|
||||
}
|
||||
|
||||
type podsWithContainers map[string]struct {
|
||||
name string
|
||||
namespace string
|
||||
containerStatuses []corev1.ContainerStatus
|
||||
}
|
||||
|
||||
type matchedContainerInfo struct {
|
||||
Namespace string
|
||||
PodName string
|
||||
ContainerName string
|
||||
Ready bool
|
||||
RestartCount int32
|
||||
Message string
|
||||
}
|
||||
|
||||
func (a *AnalyzeClusterContainerStatuses) Title() string {
|
||||
if a.analyzer.CheckName != "" {
|
||||
return a.analyzer.CheckName
|
||||
}
|
||||
return "Cluster Container Status"
|
||||
}
|
||||
|
||||
func (a *AnalyzeClusterContainerStatuses) IsExcluded() (bool, error) {
|
||||
return isExcluded(a.analyzer.Exclude)
|
||||
}
|
||||
|
||||
func (a *AnalyzeClusterContainerStatuses) Analyze(getFile getCollectedFileContents, findFiles getChildCollectedFileContents) ([]*AnalyzeResult, error) {
|
||||
// get all pod list files from clusterResources collector directory
|
||||
excludeFiles := []string{}
|
||||
podListFiles, err := findFiles(filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_PODS, "*.json"), excludeFiles)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read collected pods")
|
||||
}
|
||||
|
||||
// get pods matched analyzer filters
|
||||
pods, err := a.getPodsMatchingFilters(podListFiles)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get pods matching filters")
|
||||
}
|
||||
|
||||
results, err := a.analyzeContainerStatuses(pods)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to analyze container statuses")
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (a *AnalyzeClusterContainerStatuses) getPodsMatchingFilters(podListFiles map[string][]byte) (podsWithContainers, error) {
|
||||
var podsMatchedNamespace []corev1.Pod
|
||||
matchedPods := podsWithContainers{}
|
||||
|
||||
// filter pods matched namespace selector
|
||||
for fileName, fileContent := range podListFiles {
|
||||
// pod list fileName is the namespace name, e.g. default.json
|
||||
currentNamespace := strings.TrimSuffix(filepath.Base(fileName), ".json")
|
||||
selectedNamespaces := a.analyzer.Namespaces
|
||||
if len(selectedNamespaces) > 0 {
|
||||
if !slices.Contains(selectedNamespaces, currentNamespace) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// filter pods by namespace
|
||||
var podList corev1.PodList
|
||||
if err := json.Unmarshal(fileContent, &podList); err != nil {
|
||||
var pods []corev1.Pod
|
||||
// fallback to old format
|
||||
if err := json.Unmarshal(fileContent, &pods); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal pods list for namespace %s", currentNamespace)
|
||||
}
|
||||
podsMatchedNamespace = append(podsMatchedNamespace, pods...)
|
||||
} else {
|
||||
podsMatchedNamespace = append(podsMatchedNamespace, podList.Items...)
|
||||
}
|
||||
}
|
||||
|
||||
// filter pods by container criteria
|
||||
for _, pod := range podsMatchedNamespace {
|
||||
for _, containerStatus := range pod.Status.ContainerStatuses {
|
||||
if containerStatus.RestartCount < a.analyzer.RestartCount {
|
||||
continue
|
||||
}
|
||||
// check if the pod has already been matched
|
||||
key := string(pod.UID)
|
||||
if _, ok := matchedPods[key]; !ok {
|
||||
matchedPods[key] = struct {
|
||||
name string
|
||||
namespace string
|
||||
containerStatuses []corev1.ContainerStatus
|
||||
}{
|
||||
name: pod.Name,
|
||||
namespace: pod.Namespace,
|
||||
containerStatuses: []corev1.ContainerStatus{containerStatus},
|
||||
}
|
||||
continue
|
||||
}
|
||||
entry := matchedPods[key]
|
||||
entry.containerStatuses = append(entry.containerStatuses, containerStatus)
|
||||
}
|
||||
}
|
||||
|
||||
return matchedPods, nil
|
||||
}
|
||||
|
||||
func (a *AnalyzeClusterContainerStatuses) analyzeContainerStatuses(podContainers podsWithContainers) ([]*AnalyzeResult, error) {
|
||||
results := []*AnalyzeResult{}
|
||||
|
||||
// for each outcome, iterate over the pods and match the outcome against the container statues
|
||||
for _, outcome := range a.analyzer.Outcomes {
|
||||
r := AnalyzeResult{
|
||||
Title: a.Title(),
|
||||
IconKey: "kubernetes_container_statuses",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/kubernetes.svg?w=16&h=16",
|
||||
}
|
||||
when := ""
|
||||
|
||||
switch {
|
||||
case outcome.Fail != nil:
|
||||
r.IsFail = true
|
||||
r.Message = outcome.Fail.Message
|
||||
r.URI = outcome.Fail.URI
|
||||
when = outcome.Fail.When
|
||||
case outcome.Warn != nil:
|
||||
r.IsWarn = true
|
||||
r.Message = outcome.Warn.Message
|
||||
r.URI = outcome.Warn.URI
|
||||
when = outcome.Warn.When
|
||||
case outcome.Pass != nil:
|
||||
r.IsPass = true
|
||||
r.Message = outcome.Pass.Message
|
||||
r.URI = outcome.Pass.URI
|
||||
when = outcome.Pass.When
|
||||
default:
|
||||
klog.Warning("unexpected outcome in clusterContainerStatuses analyzer")
|
||||
continue
|
||||
}
|
||||
|
||||
// empty when indicates final case, let's return the result
|
||||
if when == "" {
|
||||
// return collected results if any
|
||||
if len(results) > 0 {
|
||||
return results, nil
|
||||
}
|
||||
return []*AnalyzeResult{&r}, nil
|
||||
}
|
||||
|
||||
// continue matching with when condition
|
||||
reason, isEqualityOp, err := parseWhen(when)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse when")
|
||||
}
|
||||
|
||||
for _, pod := range podContainers {
|
||||
matched, matchedContainerInfo := matchContainerReason(reason, pod.name, pod.namespace, pod.containerStatuses)
|
||||
if matched != isEqualityOp {
|
||||
continue
|
||||
}
|
||||
r.Message = renderContainerMessage(r.Message, &matchedContainerInfo)
|
||||
results = append(results, &r)
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// matchContainerReason iterates over the containerStatuses and returns true on the first reason that matches
|
||||
func matchContainerReason(reason string, podName string, namespace string, containerStatuses []corev1.ContainerStatus) (bool, matchedContainerInfo) {
|
||||
var matched bool
|
||||
info := matchedContainerInfo{}
|
||||
info.Namespace = namespace
|
||||
info.PodName = podName
|
||||
|
||||
for _, containerStatus := range containerStatuses {
|
||||
state := containerStatus.State
|
||||
info.ContainerName = containerStatus.Name
|
||||
info.Ready = containerStatus.Ready
|
||||
info.RestartCount = containerStatus.RestartCount
|
||||
|
||||
switch {
|
||||
case containerStatus.LastTerminationState.Terminated != nil && strings.EqualFold(containerStatus.LastTerminationState.Terminated.Reason, reason):
|
||||
matched = true
|
||||
info.Message = containerStatus.LastTerminationState.Terminated.Message
|
||||
case state.Terminated != nil && strings.EqualFold(state.Terminated.Reason, reason):
|
||||
info.Message = state.Terminated.Message
|
||||
matched = true
|
||||
case state.Waiting != nil && strings.EqualFold(state.Waiting.Reason, reason):
|
||||
matched = true
|
||||
info.Message = state.Waiting.Message
|
||||
}
|
||||
}
|
||||
return matched, info
|
||||
}
|
||||
|
||||
// parseWhen parses the when string into operator and reason
|
||||
// return error if reason is not in the expected format
|
||||
func parseWhen(when string) (string, bool, error) {
|
||||
parts := strings.Split(strings.TrimSpace(when), " ")
|
||||
if len(parts) != 2 {
|
||||
return "", false, errors.Errorf("expected 2 parts in when %q", when)
|
||||
}
|
||||
operator := parts[0]
|
||||
reason := parts[1]
|
||||
var isEqualityOp bool
|
||||
|
||||
switch operator {
|
||||
case "=", "==", "===":
|
||||
isEqualityOp = true
|
||||
case "!=", "!==":
|
||||
isEqualityOp = false
|
||||
default:
|
||||
return "", false, errors.Errorf("unexpected operator %q in containerStatuses reason", operator)
|
||||
}
|
||||
|
||||
return reason, isEqualityOp, nil
|
||||
}
|
||||
|
||||
func renderContainerMessage(message string, info *matchedContainerInfo) string {
|
||||
if info == nil {
|
||||
return message
|
||||
}
|
||||
out := fmt.Sprintf("Container matched. Container: %s, Namespace: %s, Pod: %s", info.ContainerName, info.Namespace, info.PodName)
|
||||
|
||||
tmpl := template.New("container")
|
||||
msgTmpl, err := tmpl.Parse(message)
|
||||
if err != nil {
|
||||
klog.V(2).Infof("failed to parse message template: %v", err)
|
||||
return out
|
||||
}
|
||||
|
||||
var m bytes.Buffer
|
||||
err = msgTmpl.Execute(&m, info)
|
||||
if err != nil {
|
||||
klog.V(2).Infof("failed to render message template: %v", err)
|
||||
return out
|
||||
}
|
||||
|
||||
return strings.TrimSpace(m.String())
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_analyzeContainerStatuses(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
analyzer troubleshootv1beta2.ClusterContainerStatuses
|
||||
expectResult []*AnalyzeResult
|
||||
files map[string][]byte
|
||||
}{
|
||||
{
|
||||
name: "fail when there is OOMKilled container",
|
||||
analyzer: troubleshootv1beta2.ClusterContainerStatuses{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "oomkilled-container",
|
||||
},
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "== OOMKilled",
|
||||
Message: "Container {{ .ContainerName }} from pod {{ .Namespace }}/{{ .PodName }} has OOMKilled",
|
||||
},
|
||||
},
|
||||
},
|
||||
Namespaces: []string{"message-oomkill-pod"},
|
||||
},
|
||||
expectResult: []*AnalyzeResult{
|
||||
{
|
||||
IsFail: true,
|
||||
IsWarn: false,
|
||||
IsPass: false,
|
||||
Title: "oomkilled-container",
|
||||
Message: "Container memory-eater from pod message-oomkill-pod/oom-kill-job3-gbb89 has OOMKilled",
|
||||
IconKey: "kubernetes_container_statuses",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/kubernetes.svg?w=16&h=16",
|
||||
},
|
||||
},
|
||||
files: map[string][]byte{
|
||||
"cluster-resources/pods/message-oomkill-pod.json": []byte(messageOOMKillPod),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pass when there is no status detected",
|
||||
analyzer: troubleshootv1beta2.ClusterContainerStatuses{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "oomkilled-container",
|
||||
},
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "== OOMKilled",
|
||||
Message: "Container {{ .ContainerName }} from pod {{ .Namespace }}/{{ .PodName }} has OOMKilled",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "No OOMKilled container found",
|
||||
},
|
||||
},
|
||||
},
|
||||
Namespaces: []string{"default"},
|
||||
},
|
||||
expectResult: []*AnalyzeResult{
|
||||
{
|
||||
IsFail: false,
|
||||
IsWarn: false,
|
||||
IsPass: true,
|
||||
Title: "oomkilled-container",
|
||||
Message: "No OOMKilled container found",
|
||||
IconKey: "kubernetes_container_statuses",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/kubernetes.svg?w=16&h=16",
|
||||
},
|
||||
},
|
||||
files: map[string][]byte{
|
||||
"cluster-resources/pods/default.json": []byte(defaultPods),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
getFiles := func(n string, _ []string) (map[string][]byte, error) {
|
||||
return test.files, nil
|
||||
}
|
||||
|
||||
a := AnalyzeClusterContainerStatuses{
|
||||
analyzer: &test.analyzer,
|
||||
}
|
||||
|
||||
actual, err := a.Analyze(nil, getFiles)
|
||||
req.NoError(err)
|
||||
req.Equal(test.expectResult, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/types"
|
||||
)
|
||||
|
||||
type collectedContent struct {
|
||||
NodeName string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
type nodeNames struct {
|
||||
Nodes []string `json:"nodes"`
|
||||
}
|
||||
|
||||
func retrieveCollectedContents(
|
||||
getCollectedFileContents func(string) ([]byte, error),
|
||||
localPath string, remoteNodeBaseDir string, remoteFileName string,
|
||||
) ([]collectedContent, error) {
|
||||
var collectedContents []collectedContent
|
||||
|
||||
// Try to retrieve local data first
|
||||
if contents, err := getCollectedFileContents(localPath); err == nil {
|
||||
collectedContents = append(collectedContents, collectedContent{NodeName: "", Data: contents})
|
||||
// Return immediately if local content is available
|
||||
return collectedContents, nil
|
||||
}
|
||||
|
||||
// Local data not available, move to remote collection
|
||||
nodeListContents, err := getCollectedFileContents(constants.NODE_LIST_FILE)
|
||||
if err != nil {
|
||||
if _, ok := err.(*types.NotFoundError); ok {
|
||||
return collectedContents, nil
|
||||
}
|
||||
return nil, errors.Wrap(err, "failed to get node list")
|
||||
}
|
||||
|
||||
var nodeNames nodeNames
|
||||
if err := json.Unmarshal(nodeListContents, &nodeNames); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal node names")
|
||||
}
|
||||
|
||||
// Collect data for each node
|
||||
for _, node := range nodeNames.Nodes {
|
||||
nodeFilePath := fmt.Sprintf("%s/%s/%s", remoteNodeBaseDir, node, remoteFileName)
|
||||
nodeContents, err := getCollectedFileContents(nodeFilePath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to retrieve content for node %s", node)
|
||||
}
|
||||
|
||||
collectedContents = append(collectedContents, collectedContent{NodeName: node, Data: nodeContents})
|
||||
}
|
||||
|
||||
return collectedContents, nil
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRetrieveCollectedContents(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
getCollectedFileContents func(string) ([]byte, error) // Mock function
|
||||
localPath string
|
||||
remoteNodeBaseDir string
|
||||
remoteFileName string
|
||||
expectedResult []collectedContent
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "successfully retrieve local content",
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
if path == "localPath" {
|
||||
return []byte("localContent"), nil
|
||||
}
|
||||
return nil, &types.NotFoundError{Name: path}
|
||||
},
|
||||
localPath: "localPath",
|
||||
remoteNodeBaseDir: "remoteBaseDir",
|
||||
remoteFileName: "remoteFileName",
|
||||
expectedResult: []collectedContent{
|
||||
{
|
||||
NodeName: "",
|
||||
Data: []byte("localContent"),
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "local content not found, retrieve remote node content successfully",
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
if path == constants.NODE_LIST_FILE {
|
||||
nodeNames := nodeNames{Nodes: []string{"node1", "node2"}}
|
||||
return json.Marshal(nodeNames)
|
||||
}
|
||||
if path == "remoteBaseDir/node1/remoteFileName" {
|
||||
return []byte("remoteContent1"), nil
|
||||
}
|
||||
if path == "remoteBaseDir/node2/remoteFileName" {
|
||||
return []byte("remoteContent2"), nil
|
||||
}
|
||||
return nil, &types.NotFoundError{Name: path}
|
||||
},
|
||||
localPath: "localPath",
|
||||
remoteNodeBaseDir: "remoteBaseDir",
|
||||
remoteFileName: "remoteFileName",
|
||||
expectedResult: []collectedContent{
|
||||
{
|
||||
NodeName: "node1",
|
||||
Data: []byte("remoteContent1"),
|
||||
},
|
||||
{
|
||||
NodeName: "node2",
|
||||
Data: []byte("remoteContent2"),
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "fail to retrieve local content and node list",
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
return nil, &types.NotFoundError{Name: path}
|
||||
},
|
||||
localPath: "localPath",
|
||||
remoteNodeBaseDir: "remoteBaseDir",
|
||||
remoteFileName: "remoteFileName",
|
||||
expectedResult: nil,
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "fail to retrieve content for one of the nodes",
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
if path == constants.NODE_LIST_FILE {
|
||||
nodeNames := nodeNames{Nodes: []string{"node1", "node2"}}
|
||||
return json.Marshal(nodeNames)
|
||||
}
|
||||
if path == "remoteBaseDir/node1/remoteFileName" {
|
||||
return []byte("remoteContent1"), nil
|
||||
}
|
||||
if path == "remoteBaseDir/node2/remoteFileName" {
|
||||
return nil, &types.NotFoundError{Name: path}
|
||||
}
|
||||
return nil, &types.NotFoundError{Name: path}
|
||||
},
|
||||
localPath: "localPath",
|
||||
remoteNodeBaseDir: "remoteBaseDir",
|
||||
remoteFileName: "remoteFileName",
|
||||
expectedResult: nil,
|
||||
expectedError: "failed to retrieve content for node node2",
|
||||
},
|
||||
{
|
||||
name: "fail to unmarshal node list",
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
if path == constants.NODE_LIST_FILE {
|
||||
return []byte("invalidJSON"), nil
|
||||
}
|
||||
return nil, &types.NotFoundError{Name: path}
|
||||
},
|
||||
localPath: "localPath",
|
||||
remoteNodeBaseDir: "remoteBaseDir",
|
||||
remoteFileName: "remoteFileName",
|
||||
expectedResult: nil,
|
||||
expectedError: "failed to unmarshal node names",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result, err := retrieveCollectedContents(
|
||||
test.getCollectedFileContents,
|
||||
test.localPath,
|
||||
test.remoteNodeBaseDir,
|
||||
test.remoteFileName,
|
||||
)
|
||||
|
||||
if test.expectedError != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), test.expectedError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expectedResult, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -66,11 +66,15 @@ func (a *AnalyzeCustomResourceDefinition) analyzeCustomResourceDefinition(analyz
|
||||
}
|
||||
}
|
||||
|
||||
result.IsFail = true
|
||||
for _, outcome := range analyzer.Outcomes {
|
||||
if outcome.Fail != nil {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
} else if outcome.Warn != nil {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestAnalyzeCustomResourceDefinition(t *testing.T) {
|
||||
getFile := func(_ string) ([]byte, error) {
|
||||
crdsList := v1beta1.CustomResourceDefinitionList{
|
||||
Items: []v1beta1.CustomResourceDefinition{
|
||||
{ObjectMeta: metav1.ObjectMeta{Name: "servicemonitors.monitoring.coreos.com"}},
|
||||
{ObjectMeta: metav1.ObjectMeta{Name: "probes.monitoring.coreos.com"}},
|
||||
},
|
||||
}
|
||||
return json.Marshal(crdsList)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
analyzer *troubleshootv1beta2.CustomResourceDefinition
|
||||
expectedResult *AnalyzeResult
|
||||
}{
|
||||
{
|
||||
name: "CRD exists and pass",
|
||||
analyzer: &troubleshootv1beta2.CustomResourceDefinition{
|
||||
CustomResourceDefinitionName: "servicemonitors.monitoring.coreos.com",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "The ServiceMonitor CRD is installed and available.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: &AnalyzeResult{
|
||||
Title: "Custom resource definition servicemonitors.monitoring.coreos.com",
|
||||
IconKey: "kubernetes_custom_resource_definition",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/custom-resource-definition.svg?w=13&h=16",
|
||||
IsPass: true,
|
||||
Message: "The ServiceMonitor CRD is installed and available.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "CRD not exists and warn",
|
||||
analyzer: &troubleshootv1beta2.CustomResourceDefinition{
|
||||
CustomResourceDefinitionName: "podmonitors.monitoring.coreos.com",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "The Prometheus PodMonitor CRD was not found in the cluster.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: &AnalyzeResult{
|
||||
Title: "Custom resource definition podmonitors.monitoring.coreos.com",
|
||||
IconKey: "kubernetes_custom_resource_definition",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/custom-resource-definition.svg?w=13&h=16",
|
||||
IsWarn: true,
|
||||
Message: "The Prometheus PodMonitor CRD was not found in the cluster.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "CRD not exists and fail",
|
||||
analyzer: &troubleshootv1beta2.CustomResourceDefinition{
|
||||
CustomResourceDefinitionName: "backupstoragelocations.velero.io",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "The BackupStorageLocation CRD was not found in the cluster.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: &AnalyzeResult{
|
||||
Title: "Custom resource definition backupstoragelocations.velero.io",
|
||||
IconKey: "kubernetes_custom_resource_definition",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/custom-resource-definition.svg?w=13&h=16",
|
||||
IsFail: true,
|
||||
Message: "The BackupStorageLocation CRD was not found in the cluster.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := &AnalyzeCustomResourceDefinition{
|
||||
analyzer: tt.analyzer,
|
||||
}
|
||||
result, err := a.analyzeCustomResourceDefinition(tt.analyzer, getFile)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedResult, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
type providers struct {
|
||||
@@ -75,7 +76,8 @@ func (a *AnalyzeDistribution) IsExcluded() (bool, error) {
|
||||
func (a *AnalyzeDistribution) Analyze(getFile getCollectedFileContents, findFiles getChildCollectedFileContents) ([]*AnalyzeResult, error) {
|
||||
result, err := a.analyzeDistribution(a.analyzer, getFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
klog.Errorf("failed to analyze distribution: %v", err)
|
||||
return nil, errors.Wrapf(err, "failed to analyze distribution")
|
||||
}
|
||||
result.Strict = a.analyzer.Strict.BoolOrDefaultFalse()
|
||||
return []*AnalyzeResult{result}, nil
|
||||
|
||||
@@ -2865,6 +2865,391 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Node",
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"flannel.alpha.coreos.com/backend-data": "{\"VNI\":1,\"VtepMAC\":\"c6:c2:b9:1b:49:2e\"}",
|
||||
"flannel.alpha.coreos.com/backend-type": "vxlan",
|
||||
"flannel.alpha.coreos.com/kube-subnet-manager": "true",
|
||||
"flannel.alpha.coreos.com/public-ip": "172.31.21.92",
|
||||
"kubeadm.alpha.kubernetes.io/cri-socket": "unix:///run/containerd/containerd.sock",
|
||||
"nfd.node.kubernetes.io/feature-labels": "cpu-cpuid.ADX,cpu-cpuid.AESNI,cpu-cpuid.AVX,cpu-cpuid.AVX2,cpu-cpuid.AVX512BW,cpu-cpuid.AVX512CD,cpu-cpuid.AVX512DQ,cpu-cpuid.AVX512F,cpu-cpuid.AVX512VL,cpu-cpuid.AVX512VNNI,cpu-cpuid.CMPXCHG8,cpu-cpuid.FMA3,cpu-cpuid.FXSR,cpu-cpuid.FXSROPT,cpu-cpuid.HYPERVISOR,cpu-cpuid.LAHF,cpu-cpuid.MOVBE,cpu-cpuid.MPX,cpu-cpuid.OSXSAVE,cpu-cpuid.SYSCALL,cpu-cpuid.SYSEE,cpu-cpuid.X87,cpu-cpuid.XGETBV1,cpu-cpuid.XSAVE,cpu-cpuid.XSAVEC,cpu-cpuid.XSAVEOPT,cpu-cpuid.XSAVES,cpu-hardware_multithreading,cpu-model.family,cpu-model.id,cpu-model.vendor_id,kernel-config.NO_HZ,kernel-config.NO_HZ_FULL,kernel-version.full,kernel-version.major,kernel-version.minor,kernel-version.revision,nvidia.com/cuda.driver-version.full,nvidia.com/cuda.driver-version.major,nvidia.com/cuda.driver-version.minor,nvidia.com/cuda.driver-version.revision,nvidia.com/cuda.driver.major,nvidia.com/cuda.driver.minor,nvidia.com/cuda.driver.rev,nvidia.com/cuda.runtime-version.full,nvidia.com/cuda.runtime-version.major,nvidia.com/cuda.runtime-version.minor,nvidia.com/cuda.runtime.major,nvidia.com/cuda.runtime.minor,nvidia.com/gfd.timestamp,nvidia.com/gpu.compute.major,nvidia.com/gpu.compute.minor,nvidia.com/gpu.count,nvidia.com/gpu.family,nvidia.com/gpu.machine,nvidia.com/gpu.memory,nvidia.com/gpu.mode,nvidia.com/gpu.product,nvidia.com/gpu.replicas,nvidia.com/gpu.sharing-strategy,nvidia.com/mig.capable,nvidia.com/mig.strategy,nvidia.com/mps.capable,nvidia.com/vgpu.present,pci-10de.present,pci-1d0f.present,storage-nonrotationaldisk,system-os_release.ID,system-os_release.VERSION_ID,system-os_release.VERSION_ID.major,system-os_release.VERSION_ID.minor",
|
||||
"node.alpha.kubernetes.io/ttl": "0",
|
||||
"nvidia.com/gpu-driver-upgrade-enabled": "true",
|
||||
"volumes.kubernetes.io/controller-managed-attach-detach": "true"
|
||||
},
|
||||
"creationTimestamp": "2024-12-18T22:04:55Z",
|
||||
"labels": {
|
||||
"beta.kubernetes.io/arch": "amd64",
|
||||
"beta.kubernetes.io/os": "linux",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.ADX": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.AESNI": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.AVX": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.AVX2": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.AVX512BW": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.AVX512CD": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.AVX512DQ": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.AVX512F": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.AVX512VL": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.AVX512VNNI": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.CMPXCHG8": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.FMA3": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.FXSR": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.FXSROPT": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.HYPERVISOR": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.LAHF": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.MOVBE": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.MPX": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.OSXSAVE": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.SYSCALL": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.SYSEE": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.X87": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.XGETBV1": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.XSAVE": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.XSAVEC": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.XSAVEOPT": "true",
|
||||
"feature.node.kubernetes.io/cpu-cpuid.XSAVES": "true",
|
||||
"feature.node.kubernetes.io/cpu-hardware_multithreading": "true",
|
||||
"feature.node.kubernetes.io/cpu-model.family": "6",
|
||||
"feature.node.kubernetes.io/cpu-model.id": "85",
|
||||
"feature.node.kubernetes.io/cpu-model.vendor_id": "Intel",
|
||||
"feature.node.kubernetes.io/kernel-config.NO_HZ": "true",
|
||||
"feature.node.kubernetes.io/kernel-config.NO_HZ_FULL": "true",
|
||||
"feature.node.kubernetes.io/kernel-version.full": "6.8.0-1015-aws",
|
||||
"feature.node.kubernetes.io/kernel-version.major": "6",
|
||||
"feature.node.kubernetes.io/kernel-version.minor": "8",
|
||||
"feature.node.kubernetes.io/kernel-version.revision": "0",
|
||||
"feature.node.kubernetes.io/pci-10de.present": "true",
|
||||
"feature.node.kubernetes.io/pci-1d0f.present": "true",
|
||||
"feature.node.kubernetes.io/storage-nonrotationaldisk": "true",
|
||||
"feature.node.kubernetes.io/system-os_release.ID": "ubuntu",
|
||||
"feature.node.kubernetes.io/system-os_release.VERSION_ID": "22.04",
|
||||
"feature.node.kubernetes.io/system-os_release.VERSION_ID.major": "22",
|
||||
"feature.node.kubernetes.io/system-os_release.VERSION_ID.minor": "04",
|
||||
"kubernetes.io/arch": "amd64",
|
||||
"kubernetes.io/hostname": "ip-172-31-21-92",
|
||||
"kubernetes.io/os": "linux",
|
||||
"kurl.sh/cluster": "true",
|
||||
"node-role.kubernetes.io/control-plane": "",
|
||||
"node-role.kubernetes.io/master": "",
|
||||
"node.kubernetes.io/exclude-from-external-load-balancers": "",
|
||||
"nvidia.com/cuda.driver-version.full": "550.127.08",
|
||||
"nvidia.com/cuda.driver-version.major": "550",
|
||||
"nvidia.com/cuda.driver-version.minor": "127",
|
||||
"nvidia.com/cuda.driver-version.revision": "08",
|
||||
"nvidia.com/cuda.driver.major": "550",
|
||||
"nvidia.com/cuda.driver.minor": "127",
|
||||
"nvidia.com/cuda.driver.rev": "08",
|
||||
"nvidia.com/cuda.runtime-version.full": "12.4",
|
||||
"nvidia.com/cuda.runtime-version.major": "12",
|
||||
"nvidia.com/cuda.runtime-version.minor": "4",
|
||||
"nvidia.com/cuda.runtime.major": "12",
|
||||
"nvidia.com/cuda.runtime.minor": "4",
|
||||
"nvidia.com/gfd.timestamp": "1734575658",
|
||||
"nvidia.com/gpu-driver-upgrade-state": "upgrade-done",
|
||||
"nvidia.com/gpu.compute.major": "7",
|
||||
"nvidia.com/gpu.compute.minor": "5",
|
||||
"nvidia.com/gpu.count": "1",
|
||||
"nvidia.com/gpu.deploy.container-toolkit": "true",
|
||||
"nvidia.com/gpu.deploy.dcgm": "true",
|
||||
"nvidia.com/gpu.deploy.dcgm-exporter": "true",
|
||||
"nvidia.com/gpu.deploy.device-plugin": "true",
|
||||
"nvidia.com/gpu.deploy.driver": "true",
|
||||
"nvidia.com/gpu.deploy.gpu-feature-discovery": "true",
|
||||
"nvidia.com/gpu.deploy.node-status-exporter": "true",
|
||||
"nvidia.com/gpu.deploy.nvsm": "",
|
||||
"nvidia.com/gpu.deploy.operator-validator": "true",
|
||||
"nvidia.com/gpu.family": "turing",
|
||||
"nvidia.com/gpu.machine": "g4dn.xlarge",
|
||||
"nvidia.com/gpu.memory": "15360",
|
||||
"nvidia.com/gpu.mode": "compute",
|
||||
"nvidia.com/gpu.present": "true",
|
||||
"nvidia.com/gpu.product": "Tesla-T4",
|
||||
"nvidia.com/gpu.replicas": "1",
|
||||
"nvidia.com/gpu.sharing-strategy": "none",
|
||||
"nvidia.com/mig.capable": "false",
|
||||
"nvidia.com/mig.strategy": "single",
|
||||
"nvidia.com/mps.capable": "false",
|
||||
"nvidia.com/vgpu.present": "false"
|
||||
},
|
||||
"name": "ip-172-31-21-92",
|
||||
"resourceVersion": "36620",
|
||||
"uid": "aed93ed6-f460-4578-a7b0-574574237a0a"
|
||||
},
|
||||
"spec": {
|
||||
"podCIDR": "10.32.0.0/24",
|
||||
"podCIDRs": [
|
||||
"10.32.0.0/24"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"addresses": [
|
||||
{
|
||||
"address": "172.31.21.92",
|
||||
"type": "InternalIP"
|
||||
},
|
||||
{
|
||||
"address": "ip-172-31-21-92",
|
||||
"type": "Hostname"
|
||||
}
|
||||
],
|
||||
"allocatable": {
|
||||
"cpu": "1",
|
||||
"ephemeral-storage": "93478772582",
|
||||
"hugepages-1Gi": "0",
|
||||
"hugepages-2Mi": "0",
|
||||
"memory": "16064760Ki",
|
||||
"nvidia.com/gpu": "1",
|
||||
"pods": "110"
|
||||
},
|
||||
"capacity": {
|
||||
"cpu": "1",
|
||||
"ephemeral-storage": "101430960Ki",
|
||||
"hugepages-1Gi": "0",
|
||||
"hugepages-2Mi": "0",
|
||||
"memory": "16167160Ki",
|
||||
"nvidia.com/gpu": "1",
|
||||
"pods": "110"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"lastHeartbeatTime": "2024-12-19T02:27:57Z",
|
||||
"lastTransitionTime": "2024-12-19T02:27:57Z",
|
||||
"message": "Flannel is running on this node",
|
||||
"reason": "FlannelIsUp",
|
||||
"status": "False",
|
||||
"type": "NetworkUnavailable"
|
||||
},
|
||||
{
|
||||
"lastHeartbeatTime": "2024-12-19T02:59:48Z",
|
||||
"lastTransitionTime": "2024-12-18T22:04:54Z",
|
||||
"message": "kubelet has sufficient memory available",
|
||||
"reason": "KubeletHasSufficientMemory",
|
||||
"status": "False",
|
||||
"type": "MemoryPressure"
|
||||
},
|
||||
{
|
||||
"lastHeartbeatTime": "2024-12-19T02:59:48Z",
|
||||
"lastTransitionTime": "2024-12-18T22:04:54Z",
|
||||
"message": "kubelet has no disk pressure",
|
||||
"reason": "KubeletHasNoDiskPressure",
|
||||
"status": "False",
|
||||
"type": "DiskPressure"
|
||||
},
|
||||
{
|
||||
"lastHeartbeatTime": "2024-12-19T02:59:48Z",
|
||||
"lastTransitionTime": "2024-12-18T22:04:54Z",
|
||||
"message": "kubelet has sufficient PID available",
|
||||
"reason": "KubeletHasSufficientPID",
|
||||
"status": "False",
|
||||
"type": "PIDPressure"
|
||||
},
|
||||
{
|
||||
"lastHeartbeatTime": "2024-12-19T02:59:48Z",
|
||||
"lastTransitionTime": "2024-12-19T02:27:52Z",
|
||||
"message": "kubelet is posting ready status. AppArmor enabled",
|
||||
"reason": "KubeletReady",
|
||||
"status": "True",
|
||||
"type": "Ready"
|
||||
}
|
||||
],
|
||||
"daemonEndpoints": {
|
||||
"kubeletEndpoint": {
|
||||
"Port": 10250
|
||||
}
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"names": [
|
||||
"nvcr.io/nvidia/driver@sha256:495706c38ad8afcd6c9bca1b436e23d3990631511645933325d0340ed2a87794",
|
||||
"nvcr.io/nvidia/driver:550.127.08-ubuntu22.04"
|
||||
],
|
||||
"sizeBytes": 676042363
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"docker.io/replicated/kurl-util:v2024.12.04-0"
|
||||
],
|
||||
"sizeBytes": 606430157
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"nvcr.io/nvidia/cloud-native/k8s-driver-manager@sha256:b072c5793be65eee556eaff1b9cbbd115a1ef29982be95b2959adfcb4bc72382",
|
||||
"nvcr.io/nvidia/cloud-native/k8s-driver-manager:v0.7.0"
|
||||
],
|
||||
"sizeBytes": 225657726
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"nvcr.io/nvidia/gpu-operator@sha256:db8451bc4861b11c2c18a8b439357178b43e9f831dd5908868d0e8dea73804a6",
|
||||
"nvcr.io/nvidia/gpu-operator:v24.9.1"
|
||||
],
|
||||
"sizeBytes": 221606441
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"docker.io/replicated/ekco:v0.28.7"
|
||||
],
|
||||
"sizeBytes": 194820836
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"nvcr.io/nvidia/k8s-device-plugin@sha256:7089559ce6153018806857f5049085bae15b3bf6f1c8bd19d8b12f707d087dea",
|
||||
"nvcr.io/nvidia/k8s-device-plugin:v0.17.0"
|
||||
],
|
||||
"sizeBytes": 187560257
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"nvcr.io/nvidia/cloud-native/gpu-operator-validator@sha256:420869a5601e0bff799ffe57ca1883c429ef9071d380050de0223d0a5278517c",
|
||||
"nvcr.io/nvidia/cloud-native/gpu-operator-validator:v24.9.1"
|
||||
],
|
||||
"sizeBytes": 183027141
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"docker.io/minio/minio:RELEASE.2024-11-07T00-52-20Z"
|
||||
],
|
||||
"sizeBytes": 165802842
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"docker.io/envoyproxy/envoy:v1.31.0"
|
||||
],
|
||||
"sizeBytes": 156987178
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"k8s.gcr.io/etcd:3.5.16-0",
|
||||
"registry.k8s.io/etcd:3.5.16-0"
|
||||
],
|
||||
"sizeBytes": 151015693
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"nvcr.io/nvidia/k8s/container-toolkit@sha256:35b40720f7009eec5acfb5318ac118cb4aed4dda39b190d0b75da2e8b3830383",
|
||||
"nvcr.io/nvidia/k8s/container-toolkit:v1.17.3-ubuntu20.04"
|
||||
],
|
||||
"sizeBytes": 132728008
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"k8s.gcr.io/kube-apiserver:v1.29.11",
|
||||
"registry.k8s.io/kube-apiserver:v1.29.11"
|
||||
],
|
||||
"sizeBytes": 128856468
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"nvcr.io/nvidia/k8s/dcgm-exporter@sha256:3d4e0dfa5fc4d7d12689d29fc6b56cd6c610750e8d187a393882e341fbba6c12",
|
||||
"nvcr.io/nvidia/k8s/dcgm-exporter:3.3.9-3.6.1-ubuntu22.04"
|
||||
],
|
||||
"sizeBytes": 127723011
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"k8s.gcr.io/kube-controller-manager:v1.29.11",
|
||||
"registry.k8s.io/kube-controller-manager:v1.29.11"
|
||||
],
|
||||
"sizeBytes": 123998458
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"k8s.gcr.io/kube-proxy:v1.29.11",
|
||||
"registry.k8s.io/kube-proxy:v1.29.11"
|
||||
],
|
||||
"sizeBytes": 84241069
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"docker.io/flannel/flannel:v0.26.1"
|
||||
],
|
||||
"sizeBytes": 83929242
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"docker.io/openebs/provisioner-localpv:4.1.0"
|
||||
],
|
||||
"sizeBytes": 77454375
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"registry.k8s.io/nfd/node-feature-discovery@sha256:19ebca8b3804bfe2ee7324de4873875ab0a9112b51e0ace9dfd7c470beecf4a9",
|
||||
"registry.k8s.io/nfd/node-feature-discovery:v0.16.6"
|
||||
],
|
||||
"sizeBytes": 69405547
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"docker.io/openebs/linux-utils:4.1.0"
|
||||
],
|
||||
"sizeBytes": 65541306
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"k8s.gcr.io/kube-scheduler:v1.29.11",
|
||||
"registry.k8s.io/kube-scheduler:v1.29.11"
|
||||
],
|
||||
"sizeBytes": 61288697
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"k8s.gcr.io/coredns/coredns:v1.11.1",
|
||||
"k8s.gcr.io/coredns:v1.11.1",
|
||||
"registry.k8s.io/coredns/coredns:v1.11.1",
|
||||
"registry.k8s.io/coredns:v1.11.1"
|
||||
],
|
||||
"sizeBytes": 61240402
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"ghcr.io/projectcontour/contour:v1.30.0"
|
||||
],
|
||||
"sizeBytes": 53207021
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"docker.io/library/haproxy:2.9.7-alpine3.20"
|
||||
],
|
||||
"sizeBytes": 26917938
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"docker.io/flannel/flannel-cni-plugin:v1.5.1-flannel2"
|
||||
],
|
||||
"sizeBytes": 10918841
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"k8s.gcr.io/pause:3.9",
|
||||
"registry.k8s.io/pause:3.9"
|
||||
],
|
||||
"sizeBytes": 747356
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"k8s.gcr.io/pause:3.6",
|
||||
"registry.k8s.io/pause:3.6"
|
||||
],
|
||||
"sizeBytes": 685844
|
||||
}
|
||||
],
|
||||
"nodeInfo": {
|
||||
"architecture": "x86_64",
|
||||
"bootID": "b20715e0-443d-4368-9b23-10a8882c9896",
|
||||
"containerRuntimeVersion": "containerd://1.6.33",
|
||||
"kernelVersion": "6.8.0-1015-aws",
|
||||
"kubeProxyVersion": "v1.29.11",
|
||||
"kubeletVersion": "v1.29.11",
|
||||
"machineID": "ec29e5a82f5ff79a06bc334c099a4ca4",
|
||||
"operatingSystem": "linux",
|
||||
"osImage": "Ubuntu 22.04.5 LTS",
|
||||
"systemUUID": "ec29e5a8-2f5f-f79a-06bc-334c099a4ca4"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"kind": "ConfigMapList",
|
||||
"apiVersion": "v1",
|
||||
"metadata": {
|
||||
"resourceVersion": "4825753"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"kind": "ConfigMap",
|
||||
"apiVersion": "v1",
|
||||
"metadata": {
|
||||
"name": "kube-root-ca.crt",
|
||||
"namespace": "kube-public",
|
||||
"uid": "37a912b2-a666-480f-a1a8-05ab012432e4",
|
||||
"resourceVersion": "332",
|
||||
"creationTimestamp": "2023-05-29T23:33:07Z",
|
||||
"annotations": {
|
||||
"kubernetes.io/description": "Contains a CA bundle that can be used to verify the kube-apiserver when using internal endpoints such as the internal service IP or kubernetes.default.svc. No other usage is guaranteed across distributions of Kubernetes clusters."
|
||||
},
|
||||
"managedFields": [
|
||||
{
|
||||
"manager": "kube-controller-manager",
|
||||
"operation": "Update",
|
||||
"apiVersion": "v1",
|
||||
"time": "2023-05-29T23:33:07Z",
|
||||
"fieldsType": "FieldsV1",
|
||||
"fieldsV1": {
|
||||
"f:data": {
|
||||
".": {},
|
||||
"f:ca.crt": {}
|
||||
},
|
||||
"f:metadata": {
|
||||
"f:annotations": {
|
||||
".": {},
|
||||
"f:kubernetes.io/description": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"ca.crt": "-----BEGIN CERTIFICATE-----\nMIIDBjCCAe6gAwIBAgIBATANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwptaW5p\na3ViZUNBMB4XDTIzMDUyODIzMzIzOVoXDTMzMDUyNjIzMzIzOVowFTETMBEGA1UE\nAxMKbWluaWt1YmVDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOpU\nswd15tD+QCelgYlu5MRB260+Ke63gEDTkXIyAr+mR+TjW/v9TPPIP9iidvTUg+jq\nryrf1PwURVx3EUSZWtd9NpAEDT1ov/ggx16xxj2El7KLen0SXQutSF28zjCFXxYG\nMkPxXu+qYsn2mLJX5i1kaCZNffyToCJ0n2bxxx83rOS+fgz11JntAwcgC8V3Mtq6\nIv+2Xb9PSxPs38ef7r15j5KSTOrmWCR5texPFz/WU/YbZ3W42pj9T/EiuGwamfmI\ngevuwv7AxlfjKutp5UQEth5GhY6V4kJyVIUExN3ddEsTPLQD9zvdsP4DlkGpZmiR\n/Ip3rXY/ldxeeGZ0HUUCAwEAAaNhMF8wDgYDVR0PAQH/BAQDAgKkMB0GA1UdJQQW\nMBQGCCsGAQUFBwMCBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW\nBBS0XUXhCu/DOH8/0W5L0n4BOw9b6zANBgkqhkiG9w0BAQsFAAOCAQEAHrXOxKdy\n+DvPtvgfzzV3qqmIvTuuFyW0BdZdXV8yo1tZajkEO0B0HvLUyn8ljKgoK5YtCQcr\nzSj6QEewgP+JCBTsWCKzbOhMcDKw1pa6bSeLcQWwMxox+1Zcj7edMPlPcQ3SVLxZ\n6y7fD7BArTKRBKCr8Uwudwox5Vm0URWLRAvb+8jPv9BDuC/uMPJ4UrexL/Q2QQs8\nQkyWYeSk4mBCM3qAahQhYc0WSHbk+a/5iua/y+VUaa208CUbm5glBoAroHDk5eTN\nYstOSLUQAlzTdr4kCEVi+a3+NgmuvzYXWubAdy/PT860aFBJlmVNuDhy8V/bGpnS\nKxmG7B3yJmbmzg==\n-----END CERTIFICATE-----\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -96,6 +96,18 @@ func TestAnalyzeGoldpinger_podPingsAnalysis(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no ping errors for one pod",
|
||||
cao: caoFixture(t, "goldpinger/checkall-one-pod.json"),
|
||||
want: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Pings to \"gp-goldpinger-xn9rg\" pod succeeded",
|
||||
Message: "Pings to \"gp-goldpinger-xn9rg\" pod from all other pods in the cluster succeeded",
|
||||
IconKey: "kubernetes",
|
||||
IsPass: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package analyzer
|
||||
|
||||
import troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
)
|
||||
|
||||
type HostAnalyzer interface {
|
||||
Title() string
|
||||
@@ -52,6 +57,14 @@ func GetHostAnalyzer(analyzer *troubleshootv1beta2.HostAnalyze) (HostAnalyzer, b
|
||||
return &AnalyzeHostOS{analyzer.HostOS}, true
|
||||
case analyzer.TextAnalyze != nil:
|
||||
return &AnalyzeHostTextAnalyze{analyzer.TextAnalyze}, true
|
||||
case analyzer.KernelConfigs != nil:
|
||||
return &AnalyzeHostKernelConfigs{analyzer.KernelConfigs}, true
|
||||
case analyzer.JsonCompare != nil:
|
||||
return &AnalyzeHostJsonCompare{analyzer.JsonCompare}, true
|
||||
case analyzer.NetworkNamespaceConnectivity != nil:
|
||||
return &AnalyzeHostNetworkNamespaceConnectivity{analyzer.NetworkNamespaceConnectivity}, true
|
||||
case analyzer.Sysctl != nil:
|
||||
return &AnalyzeHostSysctl{analyzer.Sysctl}, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
@@ -79,3 +92,102 @@ func (c *resultCollector) get(title string) []*AnalyzeResult {
|
||||
}
|
||||
return []*AnalyzeResult{{Title: title, IsWarn: true, Message: "no results"}}
|
||||
}
|
||||
|
||||
func analyzeHostCollectorResults(collectedContent []collectedContent, outcomes []*troubleshootv1beta2.Outcome, checkCondition func(string, []byte) (bool, error), title string) ([]*AnalyzeResult, error) {
|
||||
var results []*AnalyzeResult
|
||||
for _, content := range collectedContent {
|
||||
currentTitle := title
|
||||
if content.NodeName != "" {
|
||||
currentTitle = fmt.Sprintf("%s - Node %s", title, content.NodeName)
|
||||
}
|
||||
|
||||
analyzeResult, err := evaluateOutcomes(outcomes, checkCondition, content.Data, currentTitle)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to evaluate outcomes")
|
||||
}
|
||||
if analyzeResult != nil {
|
||||
results = append(results, analyzeResult...)
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func evaluateOutcomes(outcomes []*troubleshootv1beta2.Outcome, checkCondition func(string, []byte) (bool, error), data []byte, title string) ([]*AnalyzeResult, error) {
|
||||
var results []*AnalyzeResult
|
||||
|
||||
for _, outcome := range outcomes {
|
||||
result := AnalyzeResult{
|
||||
Title: title,
|
||||
}
|
||||
|
||||
switch {
|
||||
case outcome.Fail != nil:
|
||||
if outcome.Fail.When == "" {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
results = append(results, &result)
|
||||
return results, nil
|
||||
}
|
||||
|
||||
isMatch, err := checkCondition(outcome.Fail.When, data)
|
||||
if err != nil {
|
||||
return []*AnalyzeResult{&result}, errors.Wrapf(err, "failed to compare %s", outcome.Fail.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
results = append(results, &result)
|
||||
return results, nil
|
||||
}
|
||||
|
||||
case outcome.Warn != nil:
|
||||
if outcome.Warn.When == "" {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
results = append(results, &result)
|
||||
return results, nil
|
||||
}
|
||||
|
||||
isMatch, err := checkCondition(outcome.Warn.When, data)
|
||||
if err != nil {
|
||||
return []*AnalyzeResult{&result}, errors.Wrapf(err, "failed to compare %s", outcome.Warn.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
results = append(results, &result)
|
||||
return results, nil
|
||||
}
|
||||
|
||||
case outcome.Pass != nil:
|
||||
if outcome.Pass.When == "" {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
results = append(results, &result)
|
||||
return results, nil
|
||||
}
|
||||
|
||||
isMatch, err := checkCondition(outcome.Pass.When, data)
|
||||
if err != nil {
|
||||
return []*AnalyzeResult{&result}, errors.Wrapf(err, "failed to compare %s", outcome.Pass.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
results = append(results, &result)
|
||||
return results, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
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 collectorToBytes(t *testing.T, collector any) []byte {
|
||||
jsonData, err := json.Marshal(collector)
|
||||
require.NoError(t, err)
|
||||
return jsonData
|
||||
}
|
||||
|
||||
func TestAnalyzeHostCollectorResults(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
outcomes []*troubleshootv1beta2.Outcome
|
||||
collectedContent []collectedContent
|
||||
expectResult []*AnalyzeResult
|
||||
}{
|
||||
{
|
||||
name: "pass if ubuntu >= 00.1.2",
|
||||
collectedContent: []collectedContent{
|
||||
{
|
||||
NodeName: "node1",
|
||||
Data: collectorToBytes(t, collect.HostOSInfo{
|
||||
Name: "myhost",
|
||||
KernelVersion: "5.4.0-1034-gcp",
|
||||
PlatformVersion: "00.1.2",
|
||||
Platform: "ubuntu",
|
||||
}),
|
||||
},
|
||||
},
|
||||
outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "ubuntu >= 00.1.2",
|
||||
Message: "supported distribution matches ubuntu >= 00.1.2",
|
||||
},
|
||||
},
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "unsupported distribution",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectResult: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Host OS Info - Node node1",
|
||||
IsPass: true,
|
||||
Message: "supported distribution matches ubuntu >= 00.1.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fail if ubuntu <= 11.04",
|
||||
collectedContent: []collectedContent{
|
||||
{
|
||||
NodeName: "node1",
|
||||
Data: collectorToBytes(t, collect.HostOSInfo{
|
||||
Name: "myhost",
|
||||
KernelVersion: "5.4.0-1034-gcp",
|
||||
PlatformVersion: "11.04",
|
||||
Platform: "ubuntu",
|
||||
}),
|
||||
},
|
||||
{
|
||||
NodeName: "node2",
|
||||
Data: collectorToBytes(t, collect.HostOSInfo{
|
||||
Name: "myhost",
|
||||
KernelVersion: "5.4.0-1034-gcp",
|
||||
PlatformVersion: "11.04",
|
||||
Platform: "ubuntu",
|
||||
}),
|
||||
},
|
||||
},
|
||||
outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "ubuntu <= 11.04",
|
||||
Message: "unsupported ubuntu version 11.04",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "supported distribution",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectResult: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Host OS Info - Node node1",
|
||||
IsFail: true,
|
||||
Message: "unsupported ubuntu version 11.04",
|
||||
},
|
||||
{
|
||||
Title: "Host OS Info - Node node2",
|
||||
IsFail: true,
|
||||
Message: "unsupported ubuntu version 11.04",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "title does not include node name if empty",
|
||||
collectedContent: []collectedContent{
|
||||
{
|
||||
NodeName: "",
|
||||
Data: collectorToBytes(t, collect.HostOSInfo{
|
||||
Name: "myhost",
|
||||
KernelVersion: "5.4.0-1034-gcp",
|
||||
PlatformVersion: "20.04",
|
||||
Platform: "ubuntu",
|
||||
}),
|
||||
},
|
||||
},
|
||||
outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "ubuntu >= 20.04",
|
||||
Message: "supported distribution matches ubuntu >= 20.04",
|
||||
},
|
||||
},
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "unsupported distribution",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectResult: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Host OS Info", // Ensuring the title does not include node name if it's empty
|
||||
IsPass: true,
|
||||
Message: "supported distribution matches ubuntu >= 20.04",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
a := AnalyzeHostOS{}
|
||||
// Call the new analyzeHostCollectorResults function with the test data
|
||||
result, err := analyzeHostCollectorResults(test.collectedContent, test.outcomes, a.CheckCondition, "Host OS Info")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expectResult, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateOutcomes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
outcomes []*troubleshootv1beta2.Outcome
|
||||
checkCondition func(string, []byte) (bool, error)
|
||||
data []byte
|
||||
expectedResult []*AnalyzeResult
|
||||
}{
|
||||
{
|
||||
name: "fail condition matches",
|
||||
outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "failCondition",
|
||||
Message: "failure condition met",
|
||||
},
|
||||
},
|
||||
},
|
||||
checkCondition: func(when string, data []byte) (bool, error) {
|
||||
// Return true if the condition being checked matches "failCondition"
|
||||
return when == "failCondition", nil
|
||||
},
|
||||
data: []byte("someData"),
|
||||
expectedResult: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Test Title",
|
||||
IsFail: true,
|
||||
Message: "failure condition met",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "warn condition matches",
|
||||
outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "warnCondition",
|
||||
Message: "warning condition met",
|
||||
},
|
||||
},
|
||||
},
|
||||
checkCondition: func(when string, data []byte) (bool, error) {
|
||||
// Return true if the condition being checked matches "warnCondition"
|
||||
return when == "warnCondition", nil
|
||||
},
|
||||
data: []byte("someData"),
|
||||
expectedResult: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Test Title",
|
||||
IsWarn: true,
|
||||
Message: "warning condition met",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pass condition matches",
|
||||
outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "passCondition",
|
||||
Message: "pass condition met",
|
||||
},
|
||||
},
|
||||
},
|
||||
checkCondition: func(when string, data []byte) (bool, error) {
|
||||
// Return true if the condition being checked matches "passCondition"
|
||||
return when == "passCondition", nil
|
||||
},
|
||||
data: []byte("someData"),
|
||||
expectedResult: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Test Title",
|
||||
IsPass: true,
|
||||
Message: "pass condition met",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no condition matches",
|
||||
outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "failCondition",
|
||||
Message: "failure condition met",
|
||||
},
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "warnCondition",
|
||||
Message: "warning condition met",
|
||||
},
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "passCondition",
|
||||
Message: "pass condition met",
|
||||
},
|
||||
},
|
||||
},
|
||||
checkCondition: func(when string, data []byte) (bool, error) {
|
||||
// Always return false to simulate no condition matching
|
||||
return false, nil
|
||||
},
|
||||
data: []byte("someData"),
|
||||
expectedResult: nil, // No condition matches, so we expect no results
|
||||
},
|
||||
{
|
||||
name: "error in checkCondition",
|
||||
outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "failCondition",
|
||||
Message: "failure condition met",
|
||||
},
|
||||
},
|
||||
},
|
||||
checkCondition: func(when string, data []byte) (bool, error) {
|
||||
// Simulate an error occurring during condition evaluation
|
||||
return false, errors.New("mock error")
|
||||
},
|
||||
data: []byte("someData"),
|
||||
expectedResult: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Test Title",
|
||||
IsFail: false, // Error occurred, so no success flag
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result, err := evaluateOutcomes(test.outcomes, test.checkCondition, test.data, "Test Title")
|
||||
|
||||
if test.name == "error in checkCondition" {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expectedResult, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -27,90 +27,24 @@ func (a *AnalyzeHostBlockDevices) IsExcluded() (bool, error) {
|
||||
func (a *AnalyzeHostBlockDevices) Analyze(
|
||||
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
|
||||
) ([]*AnalyzeResult, error) {
|
||||
hostAnalyzer := a.hostAnalyzer
|
||||
result := AnalyzeResult{Title: a.Title()}
|
||||
|
||||
contents, err := getCollectedFileContents(collect.HostBlockDevicesPath)
|
||||
collectedContents, err := retrieveCollectedContents(
|
||||
getCollectedFileContents,
|
||||
collect.HostBlockDevicesPath,
|
||||
collect.NodeInfoBaseDir,
|
||||
collect.HostBlockDevicesFileName,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get collected file")
|
||||
return []*AnalyzeResult{&result}, err
|
||||
}
|
||||
|
||||
var devices []collect.BlockDeviceInfo
|
||||
if err := json.Unmarshal(contents, &devices); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal block devices info")
|
||||
results, err := analyzeHostCollectorResults(collectedContents, a.hostAnalyzer.Outcomes, a.CheckCondition, a.Title())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to analyze block devices")
|
||||
}
|
||||
|
||||
result := AnalyzeResult{}
|
||||
|
||||
result.Title = a.Title()
|
||||
|
||||
for _, outcome := range hostAnalyzer.Outcomes {
|
||||
if outcome.Fail != nil {
|
||||
if outcome.Fail.When == "" {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
return []*AnalyzeResult{&result}, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareHostBlockDevicesConditionalToActual(outcome.Fail.When, hostAnalyzer.MinimumAcceptableSize, hostAnalyzer.IncludeUnmountedPartitions, devices)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Fail.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
return []*AnalyzeResult{&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 []*AnalyzeResult{&result}, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareHostBlockDevicesConditionalToActual(outcome.Warn.When, hostAnalyzer.MinimumAcceptableSize, hostAnalyzer.IncludeUnmountedPartitions, devices)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Warn.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
return []*AnalyzeResult{&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 []*AnalyzeResult{&result}, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareHostBlockDevicesConditionalToActual(outcome.Pass.When, hostAnalyzer.MinimumAcceptableSize, hostAnalyzer.IncludeUnmountedPartitions, devices)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Pass.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
return []*AnalyzeResult{&result}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return []*AnalyzeResult{&result}, nil
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// <regexp> <op> <count>
|
||||
@@ -205,3 +139,14 @@ func isEligibleBlockDevice(rx *regexp.Regexp, minimumAcceptableSize uint64, incl
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostBlockDevices) CheckCondition(when string, data []byte) (bool, error) {
|
||||
|
||||
var devices []collect.BlockDeviceInfo
|
||||
if err := json.Unmarshal(data, &devices); err != nil {
|
||||
return false, errors.Wrap(err, "failed to unmarshal block devices info")
|
||||
}
|
||||
|
||||
return compareHostBlockDevicesConditionalToActual(when, a.hostAnalyzer.MinimumAcceptableSize, a.hostAnalyzer.IncludeUnmountedPartitions, devices)
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -23,52 +23,34 @@ func (a *AnalyzeHostCertificate) IsExcluded() (bool, error) {
|
||||
func (a *AnalyzeHostCertificate) Analyze(
|
||||
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
|
||||
) ([]*AnalyzeResult, error) {
|
||||
hostAnalyzer := a.hostAnalyzer
|
||||
|
||||
collectorName := hostAnalyzer.CollectorName
|
||||
collectorName := a.hostAnalyzer.CollectorName
|
||||
if collectorName == "" {
|
||||
collectorName = "certificate"
|
||||
}
|
||||
name := filepath.Join("host-collectors/certificate", collectorName+".json")
|
||||
contents, err := getCollectedFileContents(name)
|
||||
|
||||
const nodeBaseDir = "host-collectors/certificate"
|
||||
localPath := fmt.Sprintf("%s/%s", nodeBaseDir, collectorName)
|
||||
fileName := fmt.Sprintf("%s.json", collectorName)
|
||||
|
||||
collectedContents, err := retrieveCollectedContents(
|
||||
getCollectedFileContents,
|
||||
localPath,
|
||||
nodeBaseDir,
|
||||
fileName,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get collected file")
|
||||
}
|
||||
status := string(contents)
|
||||
|
||||
trimmedStatus := strings.TrimSpace(status)
|
||||
|
||||
var coll resultCollector
|
||||
|
||||
for _, outcome := range hostAnalyzer.Outcomes {
|
||||
result := &AnalyzeResult{Title: a.Title()}
|
||||
|
||||
if outcome.Fail != nil {
|
||||
if outcome.Fail.When == "" || outcome.Fail.When == trimmedStatus {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
coll.push(result)
|
||||
}
|
||||
} else if outcome.Warn != nil {
|
||||
if outcome.Warn.When == "" || outcome.Warn.When == trimmedStatus {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
coll.push(result)
|
||||
}
|
||||
} else if outcome.Pass != nil {
|
||||
if outcome.Pass.When == "" || outcome.Pass.When == trimmedStatus {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
coll.push(result)
|
||||
}
|
||||
}
|
||||
return []*AnalyzeResult{{Title: a.Title()}}, err
|
||||
}
|
||||
|
||||
return coll.get(a.Title()), nil
|
||||
results, err := analyzeHostCollectorResults(collectedContents, a.hostAnalyzer.Outcomes, a.CheckCondition, a.Title())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to analyze certificate")
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostCertificate) CheckCondition(when string, data []byte) (bool, error) {
|
||||
return strings.TrimSpace(string(data)) == when, nil
|
||||
}
|
||||
|
||||
+110
-80
@@ -2,6 +2,8 @@ package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -10,6 +12,16 @@ import (
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
)
|
||||
|
||||
// microarchs holds a list of features present in each microarchitecture.
|
||||
// ref: https://gitlab.com/x86-psABIs/x86-64-ABI
|
||||
// ref: https://developers.redhat.com/blog/2021/01/05/building-red-hat-enterprise-linux-9-for-the-x86-64-v2-microarchitecture-level
|
||||
var microarchs = map[string][]string{
|
||||
"x86-64": {"cmov", "cx8", "fpu", "fxsr", "mmx", "syscall", "sse", "sse2"},
|
||||
"x86-64-v2": {"cx16", "lahf_lm", "popcnt", "ssse3", "sse4_1", "sse4_2", "ssse3"},
|
||||
"x86-64-v3": {"avx", "avx2", "bmi1", "bmi2", "f16c", "fma", "lzcnt", "movbe", "xsave"},
|
||||
"x86-64-v4": {"avx512f", "avx512bw", "avx512cd", "avx512dq", "avx512vl"},
|
||||
}
|
||||
|
||||
type AnalyzeHostCPU struct {
|
||||
hostAnalyzer *troubleshootv1beta2.CPUAnalyze
|
||||
}
|
||||
@@ -22,104 +34,96 @@ func (a *AnalyzeHostCPU) IsExcluded() (bool, error) {
|
||||
return isExcluded(a.hostAnalyzer.Exclude)
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostCPU) CheckCondition(when string, data []byte) (bool, error) {
|
||||
|
||||
cpuInfo := collect.CPUInfo{}
|
||||
if err := json.Unmarshal(data, &cpuInfo); err != nil {
|
||||
return false, fmt.Errorf("failed to unmarshal data into CPUInfo: %v", err)
|
||||
}
|
||||
|
||||
return compareHostCPUConditionalToActual(when, cpuInfo.LogicalCount, cpuInfo.PhysicalCount, cpuInfo.Flags, cpuInfo.MachineArch)
|
||||
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostCPU) Analyze(
|
||||
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
|
||||
) ([]*AnalyzeResult, error) {
|
||||
hostAnalyzer := a.hostAnalyzer
|
||||
result := AnalyzeResult{Title: a.Title()}
|
||||
|
||||
contents, err := getCollectedFileContents(collect.HostCPUPath)
|
||||
// Use the generic function to collect both local and remote data
|
||||
collectedContents, err := retrieveCollectedContents(
|
||||
getCollectedFileContents,
|
||||
collect.HostCPUPath, // Local path
|
||||
collect.NodeInfoBaseDir, // Remote base directory
|
||||
collect.HostCPUFileName, // Remote file name
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get collected file")
|
||||
return []*AnalyzeResult{&result}, err
|
||||
}
|
||||
|
||||
cpuInfo := collect.CPUInfo{}
|
||||
if err := json.Unmarshal(contents, &cpuInfo); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal cpu info")
|
||||
results, err := analyzeHostCollectorResults(collectedContents, a.hostAnalyzer.Outcomes, a.CheckCondition, a.Title())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to analyze OS version")
|
||||
}
|
||||
|
||||
result := AnalyzeResult{
|
||||
Title: a.Title(),
|
||||
}
|
||||
return results, nil
|
||||
|
||||
for _, outcome := range hostAnalyzer.Outcomes {
|
||||
|
||||
if outcome.Fail != nil {
|
||||
if outcome.Fail.When == "" {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
return []*AnalyzeResult{&result}, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareHostCPUConditionalToActual(outcome.Fail.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to compare")
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
return []*AnalyzeResult{&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 []*AnalyzeResult{&result}, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareHostCPUConditionalToActual(outcome.Warn.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to compare")
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
return []*AnalyzeResult{&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 []*AnalyzeResult{&result}, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareHostCPUConditionalToActual(outcome.Pass.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to compare")
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
return []*AnalyzeResult{&result}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return []*AnalyzeResult{&result}, nil
|
||||
}
|
||||
|
||||
func compareHostCPUConditionalToActual(conditional string, logicalCount int, physicalCount int) (res bool, err error) {
|
||||
func doCompareHostCPUMicroArchitecture(microarch string, flags []string) (res bool, err error) {
|
||||
specifics := make([]string, 0)
|
||||
switch microarch {
|
||||
case "x86-64-v4":
|
||||
specifics = append(specifics, microarchs["x86-64-v4"]...)
|
||||
fallthrough
|
||||
case "x86-64-v3":
|
||||
specifics = append(specifics, microarchs["x86-64-v3"]...)
|
||||
fallthrough
|
||||
case "x86-64-v2":
|
||||
specifics = append(specifics, microarchs["x86-64-v2"]...)
|
||||
fallthrough
|
||||
case "x86-64":
|
||||
specifics = append(specifics, microarchs["x86-64"]...)
|
||||
default:
|
||||
return false, errors.Errorf("troubleshoot does not yet support microarchitecture %q", microarch)
|
||||
}
|
||||
|
||||
for _, flag := range specifics {
|
||||
if slices.Contains(flags, flag) {
|
||||
continue
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func doCompareHostCPUFlags(expected string, flags []string) (res bool, err error) {
|
||||
expectedFlags := strings.Split(expected, ",")
|
||||
if len(expectedFlags) == 0 {
|
||||
return false, errors.New("expected flags cannot be empty")
|
||||
}
|
||||
for _, flag := range expectedFlags {
|
||||
if slices.Contains(flags, flag) {
|
||||
continue
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func compareHostCPUConditionalToActual(conditional string, logicalCount int, physicalCount int, flags []string, machineArch string) (res bool, err error) {
|
||||
compareLogical := false
|
||||
comparePhysical := false
|
||||
compareUnspecified := false
|
||||
compareMachineArch := false
|
||||
|
||||
comparator := ""
|
||||
desired := ""
|
||||
|
||||
/* When the conditional is in the format of "logical <comparator> <desired>"
|
||||
example: when: "count < 2"
|
||||
*/
|
||||
|
||||
parts := strings.Split(conditional, " ")
|
||||
if len(parts) == 3 {
|
||||
comparator = parts[1]
|
||||
@@ -130,6 +134,8 @@ func compareHostCPUConditionalToActual(conditional string, logicalCount int, phy
|
||||
comparePhysical = true
|
||||
} else if strings.ToLower(parts[0]) == "count" {
|
||||
compareUnspecified = true
|
||||
} else if strings.ToLower(parts[0]) == "machinearch" {
|
||||
compareMachineArch = true
|
||||
}
|
||||
} else if len(parts) == 2 {
|
||||
compareUnspecified = true
|
||||
@@ -137,7 +143,17 @@ func compareHostCPUConditionalToActual(conditional string, logicalCount int, phy
|
||||
desired = parts[1]
|
||||
}
|
||||
|
||||
if !compareLogical && !comparePhysical && !compareUnspecified {
|
||||
// analyze if the cpu supports a specific set of features, aka as micrarchitecture.
|
||||
if strings.ToLower(comparator) == "supports" {
|
||||
return doCompareHostCPUMicroArchitecture(desired, flags)
|
||||
}
|
||||
|
||||
// hasFlags allows users to query for specific flags on the CPU.
|
||||
if strings.ToLower(comparator) == "hasflags" {
|
||||
return doCompareHostCPUFlags(desired, flags)
|
||||
}
|
||||
|
||||
if !compareLogical && !comparePhysical && !compareUnspecified && !compareMachineArch {
|
||||
return false, errors.New("unable to parse conditional")
|
||||
}
|
||||
|
||||
@@ -145,6 +161,8 @@ func compareHostCPUConditionalToActual(conditional string, logicalCount int, phy
|
||||
return doCompareHostCPU(comparator, desired, logicalCount)
|
||||
} else if comparePhysical {
|
||||
return doCompareHostCPU(comparator, desired, physicalCount)
|
||||
} else if compareMachineArch {
|
||||
return doCompareMachineArch(comparator, desired, machineArch)
|
||||
} else {
|
||||
actual := logicalCount
|
||||
if physicalCount > logicalCount {
|
||||
@@ -155,6 +173,16 @@ func compareHostCPUConditionalToActual(conditional string, logicalCount int, phy
|
||||
}
|
||||
}
|
||||
|
||||
func doCompareMachineArch(operator string, desired string, actual string) (bool, error) {
|
||||
switch operator {
|
||||
case "=", "==", "===":
|
||||
return actual == desired, nil
|
||||
case "!=", "!==":
|
||||
return actual != desired, nil
|
||||
}
|
||||
return false, errors.New("unknown operator")
|
||||
}
|
||||
|
||||
func doCompareHostCPU(operator string, desired string, actual int) (bool, error) {
|
||||
desiredInt, err := strconv.ParseInt(desired, 10, 64)
|
||||
if err != nil {
|
||||
@@ -172,6 +200,8 @@ func doCompareHostCPU(operator string, desired string, actual int) (bool, error)
|
||||
return actual >= int(desiredInt), nil
|
||||
case "=", "==", "===":
|
||||
return actual == int(desiredInt), nil
|
||||
case "!=", "!==":
|
||||
return actual != int(desiredInt), nil
|
||||
}
|
||||
|
||||
return false, errors.New("unknown operator")
|
||||
|
||||
@@ -81,6 +81,8 @@ func Test_compareHostCPUConditionalToActual(t *testing.T) {
|
||||
when string
|
||||
logicalCount int
|
||||
physicalCount int
|
||||
flags []string
|
||||
machineArch string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
@@ -139,12 +141,42 @@ func Test_compareHostCPUConditionalToActual(t *testing.T) {
|
||||
physicalCount: 4,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "supports x86-64-v2 microarchitecture",
|
||||
when: "supports x86-64-v2",
|
||||
flags: []string{""},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "supports x86-64-v2 microarchitecture",
|
||||
when: "supports x86-64-v2",
|
||||
flags: []string{"cmov", "cx8", "fpu", "fxsr", "mmx", "syscall", "sse", "sse2", "cx16", "lahf_lm", "popcnt", "ssse3", "sse4_1", "sse4_2", "ssse3"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "has a non existent flag",
|
||||
when: "hasFlags cmov,doesNotExist",
|
||||
flags: []string{"cmov", "cx8", "fpu", "fxsr", "mmx", "syscall", "sse", "sse2", "cx16", "lahf_lm", "popcnt", "ssse3", "sse4_1", "sse4_2", "ssse3"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "has flags",
|
||||
when: "hasFlags a,b,c",
|
||||
flags: []string{"a", "b", "c", "d", "e"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "machine arch matches",
|
||||
when: "machineArch == x86_64",
|
||||
machineArch: "x86_64",
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
actual, err := compareHostCPUConditionalToActual(test.when, test.logicalCount, test.physicalCount)
|
||||
actual, err := compareHostCPUConditionalToActual(test.when, test.logicalCount, test.physicalCount, test.flags, test.machineArch)
|
||||
req.NoError(err)
|
||||
|
||||
assert.Equal(t, test.expected, actual)
|
||||
|
||||
@@ -3,7 +3,6 @@ package analyzer
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -28,97 +27,32 @@ func (a *AnalyzeHostDiskUsage) IsExcluded() (bool, error) {
|
||||
func (a *AnalyzeHostDiskUsage) Analyze(
|
||||
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
|
||||
) ([]*AnalyzeResult, error) {
|
||||
hostAnalyzer := a.hostAnalyzer
|
||||
|
||||
name := filepath.Join("host-collectors/diskUsage", "diskUsage.json")
|
||||
if hostAnalyzer.CollectorName != "" {
|
||||
name = filepath.Join("host-collectors/diskUsage", hostAnalyzer.CollectorName+".json")
|
||||
collectorName := a.hostAnalyzer.CollectorName
|
||||
if collectorName == "" {
|
||||
collectorName = "diskUsage"
|
||||
}
|
||||
contents, err := getCollectedFileContents(name)
|
||||
|
||||
const nodeBaseDir = "host-collectors/diskUsage"
|
||||
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 nil, errors.Wrapf(err, "failed to get collected file %s", name)
|
||||
return []*AnalyzeResult{{Title: a.Title()}}, err
|
||||
}
|
||||
|
||||
diskUsageInfo := collect.DiskUsageInfo{}
|
||||
if err := json.Unmarshal(contents, &diskUsageInfo); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal disk usage info from %s", name)
|
||||
results, err := analyzeHostCollectorResults(collectedContents, a.hostAnalyzer.Outcomes, a.CheckCondition, a.Title())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to analyze disk usage")
|
||||
}
|
||||
|
||||
result := AnalyzeResult{
|
||||
Title: a.Title(),
|
||||
}
|
||||
|
||||
for _, outcome := range hostAnalyzer.Outcomes {
|
||||
result := &AnalyzeResult{Title: a.Title()}
|
||||
|
||||
if outcome.Fail != nil {
|
||||
if outcome.Fail.When == "" {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareHostDiskUsageConditionalToActual(outcome.Fail.When, diskUsageInfo.TotalBytes, diskUsageInfo.UsedBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %q", outcome.Fail.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
return []*AnalyzeResult{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 []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareHostDiskUsageConditionalToActual(outcome.Warn.When, diskUsageInfo.TotalBytes, diskUsageInfo.UsedBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %q", outcome.Warn.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
return []*AnalyzeResult{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 []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareHostDiskUsageConditionalToActual(outcome.Pass.When, diskUsageInfo.TotalBytes, diskUsageInfo.UsedBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %q", outcome.Pass.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return []*AnalyzeResult{&result}, nil
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func compareHostDiskUsageConditionalToActual(conditional string, totalBytes uint64, usedBytes uint64) (res bool, err error) {
|
||||
@@ -202,3 +136,13 @@ func doCompareHostDiskUsagePercent(operator string, desired string, actual float
|
||||
|
||||
return false, errors.New("unknown operator")
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostDiskUsage) CheckCondition(when string, data []byte) (bool, error) {
|
||||
|
||||
var diskUsageInfo collect.DiskUsageInfo
|
||||
if err := json.Unmarshal(data, &diskUsageInfo); err != nil {
|
||||
return false, fmt.Errorf("failed to unmarshal data into DiskUsageInfo: %v", err)
|
||||
}
|
||||
|
||||
return compareHostDiskUsageConditionalToActual(when, diskUsageInfo.TotalBytes, diskUsageInfo.UsedBytes)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
@@ -30,18 +29,23 @@ func (a *AnalyzeHostFilesystemPerformance) IsExcluded() (bool, error) {
|
||||
func (a *AnalyzeHostFilesystemPerformance) Analyze(
|
||||
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
|
||||
) ([]*AnalyzeResult, error) {
|
||||
result := &AnalyzeResult{Title: a.Title()}
|
||||
hostAnalyzer := a.hostAnalyzer
|
||||
collectorName := a.hostAnalyzer.CollectorName
|
||||
|
||||
result := &AnalyzeResult{
|
||||
Title: a.Title(),
|
||||
}
|
||||
|
||||
collectorName := hostAnalyzer.CollectorName
|
||||
if collectorName == "" {
|
||||
collectorName = "filesystemPerformance"
|
||||
}
|
||||
name := filepath.Join("host-collectors/filesystemPerformance", collectorName+".json")
|
||||
contents, err := getCollectedFileContents(name)
|
||||
const nodeBaseDir = "host-collectors/filesystemPerformance"
|
||||
localPath := fmt.Sprintf("%s/%s.json", nodeBaseDir, collectorName)
|
||||
fileName := fmt.Sprintf("%s.json", collectorName)
|
||||
|
||||
collectedContents, err := retrieveCollectedContents(
|
||||
getCollectedFileContents,
|
||||
localPath,
|
||||
nodeBaseDir,
|
||||
fileName,
|
||||
)
|
||||
if err != nil {
|
||||
if len(hostAnalyzer.Outcomes) >= 1 {
|
||||
// if the very first outcome is FILE_NOT_COLLECTED', then use that outcome
|
||||
@@ -64,97 +68,26 @@ func (a *AnalyzeHostFilesystemPerformance) Analyze(
|
||||
result.URI = hostAnalyzer.Outcomes[0].Pass.URI
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.Wrapf(err, "failed to get collected file %s", name)
|
||||
}
|
||||
|
||||
fioResult := collect.FioResult{}
|
||||
if err := json.Unmarshal(contents, &fioResult); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal fio results from %s", name)
|
||||
}
|
||||
|
||||
if len(fioResult.Jobs) == 0 {
|
||||
return nil, errors.Errorf("no jobs found in fio results from %s", name)
|
||||
}
|
||||
|
||||
fioWriteLatency := fioResult.Jobs[0].Sync
|
||||
|
||||
fsPerf := fioWriteLatency.FSPerfResults()
|
||||
if err := json.Unmarshal(contents, &fsPerf); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal filesystem performance results from %s", name)
|
||||
}
|
||||
|
||||
for _, outcome := range hostAnalyzer.Outcomes {
|
||||
|
||||
if outcome.Fail != nil {
|
||||
if outcome.Fail.When == "" {
|
||||
result.IsFail = true
|
||||
result.Message = renderFSPerfOutcome(outcome.Fail.Message, fsPerf)
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareHostFilesystemPerformanceConditionalToActual(outcome.Fail.When, fsPerf)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %q", outcome.Fail.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsFail = true
|
||||
result.Message = renderFSPerfOutcome(outcome.Fail.Message, fsPerf)
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
} else if outcome.Warn != nil {
|
||||
if outcome.Warn.When == "" {
|
||||
result.IsWarn = true
|
||||
result.Message = renderFSPerfOutcome(outcome.Warn.Message, fsPerf)
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareHostFilesystemPerformanceConditionalToActual(outcome.Warn.When, fsPerf)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %q", outcome.Warn.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsWarn = true
|
||||
result.Message = renderFSPerfOutcome(outcome.Warn.Message, fsPerf)
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
} else if outcome.Pass != nil {
|
||||
if outcome.Pass.When == "" {
|
||||
result.IsPass = true
|
||||
result.Message = renderFSPerfOutcome(outcome.Pass.Message, fsPerf)
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareHostFilesystemPerformanceConditionalToActual(outcome.Pass.When, fsPerf)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %q", outcome.Pass.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsPass = true
|
||||
result.Message = renderFSPerfOutcome(outcome.Pass.Message, fsPerf)
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
return nil, errors.Wrapf(err, "failed to get collected file %s", localPath)
|
||||
}
|
||||
}
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
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.analyzeSingleNode(content, currentTitle)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to analyze filesystem performance for %s", currentTitle)
|
||||
}
|
||||
if result != nil {
|
||||
results = append(results, result...)
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func compareHostFilesystemPerformanceConditionalToActual(conditional string, fsPerf collect.FSPerfResults) (res bool, err error) {
|
||||
@@ -251,3 +184,105 @@ func renderFSPerfOutcome(outcome string, fsPerf collect.FSPerfResults) string {
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostFilesystemPerformance) analyzeSingleNode(content collectedContent, currentTitle string) ([]*AnalyzeResult, error) {
|
||||
hostAnalyzer := a.hostAnalyzer
|
||||
result := &AnalyzeResult{
|
||||
Title: currentTitle,
|
||||
}
|
||||
fioResult := collect.FioResult{}
|
||||
if err := json.Unmarshal(content.Data, &fioResult); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal fio results from %s", currentTitle)
|
||||
}
|
||||
|
||||
var job *collect.FioJobs
|
||||
for _, j := range fioResult.Jobs {
|
||||
if j.JobName == collect.FioJobName {
|
||||
job = &j
|
||||
break
|
||||
}
|
||||
}
|
||||
if job == nil {
|
||||
return nil, errors.Errorf("no job named 'fsperf' found in fio results from %s", currentTitle)
|
||||
}
|
||||
|
||||
fioWriteLatency := job.Sync
|
||||
|
||||
fsPerf := fioWriteLatency.FSPerfResults()
|
||||
if err := json.Unmarshal(content.Data, &fsPerf); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal filesystem performance results from %s", currentTitle)
|
||||
}
|
||||
|
||||
for _, outcome := range hostAnalyzer.Outcomes {
|
||||
|
||||
switch {
|
||||
case outcome.Fail != nil:
|
||||
if outcome.Fail.When == "" {
|
||||
result.IsFail = true
|
||||
result.Message = renderFSPerfOutcome(outcome.Fail.Message, fsPerf)
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareHostFilesystemPerformanceConditionalToActual(outcome.Fail.When, fsPerf)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %q", outcome.Fail.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsFail = true
|
||||
result.Message = renderFSPerfOutcome(outcome.Fail.Message, fsPerf)
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
case outcome.Warn != nil:
|
||||
if outcome.Warn.When == "" {
|
||||
result.IsWarn = true
|
||||
result.Message = renderFSPerfOutcome(outcome.Warn.Message, fsPerf)
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareHostFilesystemPerformanceConditionalToActual(outcome.Warn.When, fsPerf)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %q", outcome.Warn.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsWarn = true
|
||||
result.Message = renderFSPerfOutcome(outcome.Warn.Message, fsPerf)
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
case outcome.Pass != nil:
|
||||
if outcome.Pass.When == "" {
|
||||
result.IsPass = true
|
||||
result.Message = renderFSPerfOutcome(outcome.Pass.Message, fsPerf)
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareHostFilesystemPerformanceConditionalToActual(outcome.Pass.When, fsPerf)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %q", outcome.Pass.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsPass = true
|
||||
result.Message = renderFSPerfOutcome(outcome.Pass.Message, fsPerf)
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("unexpected outcome")
|
||||
}
|
||||
}
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
+64
-33
@@ -3,7 +3,6 @@ package analyzer
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -32,13 +31,65 @@ func (a *AnalyzeHostHTTP) IsExcluded() (bool, error) {
|
||||
func (a *AnalyzeHostHTTP) Analyze(
|
||||
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
|
||||
) ([]*AnalyzeResult, error) {
|
||||
hostAnalyzer := a.hostAnalyzer
|
||||
|
||||
name := filepath.Join("host-collectors/http", "result.json")
|
||||
if hostAnalyzer.CollectorName != "" {
|
||||
name = filepath.Join("host-collectors/http", hostAnalyzer.CollectorName+".json")
|
||||
collectorName := a.hostAnalyzer.CollectorName
|
||||
if collectorName == "" {
|
||||
collectorName = "result"
|
||||
}
|
||||
contents, err := getCollectedFileContents(name)
|
||||
|
||||
const nodeBaseDir = "host-collectors/http"
|
||||
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
|
||||
}
|
||||
|
||||
results, err := analyzeHostCollectorResults(collectedContents, a.hostAnalyzer.Outcomes, a.CheckCondition, a.Title())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to analyze http request")
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func compareHostHTTPConditionalToActual(conditional string, result *httpResult) (res bool, err error) {
|
||||
if conditional == "error" {
|
||||
return result.Error != nil, nil
|
||||
}
|
||||
|
||||
parts := strings.Split(conditional, " ")
|
||||
if len(parts) != 3 {
|
||||
return false, fmt.Errorf("Failed to parse conditional: got %d parts", len(parts))
|
||||
}
|
||||
|
||||
if parts[0] != "statusCode" {
|
||||
return false, errors.New(`Conditional must begin with keyword "statusCode"`)
|
||||
}
|
||||
|
||||
if parts[1] != "=" && parts[1] != "==" && parts[1] != "===" {
|
||||
return false, errors.New(`Only supported operator is "=="`)
|
||||
}
|
||||
|
||||
i, err := strconv.Atoi(parts[2])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if result.Response == nil {
|
||||
return false, err
|
||||
}
|
||||
return result.Response.Status == i, nil
|
||||
}
|
||||
|
||||
func analyzeHTTPResult(analyzer *troubleshootv1beta2.HTTPAnalyze, fileName string, getCollectedFileContents getCollectedFileContents, title string) ([]*AnalyzeResult, error) {
|
||||
contents, err := getCollectedFileContents(fileName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get collected file")
|
||||
}
|
||||
@@ -49,10 +100,10 @@ func (a *AnalyzeHostHTTP) Analyze(
|
||||
}
|
||||
|
||||
result := &AnalyzeResult{
|
||||
Title: a.Title(),
|
||||
Title: title,
|
||||
}
|
||||
|
||||
for _, outcome := range hostAnalyzer.Outcomes {
|
||||
for _, outcome := range analyzer.Outcomes {
|
||||
if outcome.Fail != nil {
|
||||
if outcome.Fail.When == "" {
|
||||
result.IsFail = true
|
||||
@@ -123,31 +174,11 @@ func (a *AnalyzeHostHTTP) Analyze(
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
func compareHostHTTPConditionalToActual(conditional string, result *httpResult) (res bool, err error) {
|
||||
if conditional == "error" {
|
||||
return result.Error != nil, nil
|
||||
}
|
||||
func (a *AnalyzeHostHTTP) CheckCondition(when string, data []byte) (bool, error) {
|
||||
|
||||
parts := strings.Split(conditional, " ")
|
||||
if len(parts) != 3 {
|
||||
return false, fmt.Errorf("Failed to parse conditional: got %d parts", len(parts))
|
||||
var httpInfo httpResult
|
||||
if err := json.Unmarshal(data, &httpInfo); err != nil {
|
||||
return false, fmt.Errorf("failed to unmarshal data into httpResult: %v", err)
|
||||
}
|
||||
|
||||
if parts[0] != "statusCode" {
|
||||
return false, errors.New(`Conditional must begin with keyword "statusCode"`)
|
||||
}
|
||||
|
||||
if parts[1] != "=" && parts[1] != "==" && parts[1] != "===" {
|
||||
return false, errors.New(`Only supported operator is "=="`)
|
||||
}
|
||||
|
||||
i, err := strconv.Atoi(parts[2])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if result.Response == nil {
|
||||
return false, err
|
||||
}
|
||||
return result.Response.Status == i, nil
|
||||
return compareHostHTTPConditionalToActual(when, &httpInfo)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package analyzer
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
@@ -25,82 +24,43 @@ func (a *AnalyzeHostHTTPLoadBalancer) IsExcluded() (bool, error) {
|
||||
func (a *AnalyzeHostHTTPLoadBalancer) Analyze(
|
||||
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
|
||||
) ([]*AnalyzeResult, error) {
|
||||
hostAnalyzer := a.hostAnalyzer
|
||||
|
||||
collectorName := hostAnalyzer.CollectorName
|
||||
collectorName := a.hostAnalyzer.CollectorName
|
||||
if collectorName == "" {
|
||||
collectorName = "httpLoadBalancer"
|
||||
}
|
||||
fullPath := path.Join("host-collectors/httpLoadBalancer", fmt.Sprintf("%s.json", collectorName))
|
||||
|
||||
collected, err := getCollectedFileContents(fullPath)
|
||||
const nodeBaseDir = "host-collectors/httpLoadBalancer"
|
||||
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 nil, errors.Wrapf(err, "failed to read collected file name: %s", fullPath)
|
||||
}
|
||||
actual := collect.NetworkStatusResult{}
|
||||
if err := json.Unmarshal(collected, &actual); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal collected")
|
||||
return []*AnalyzeResult{{Title: a.Title()}}, err
|
||||
}
|
||||
|
||||
var coll resultCollector
|
||||
|
||||
for _, outcome := range hostAnalyzer.Outcomes {
|
||||
result := &AnalyzeResult{Title: a.Title()}
|
||||
|
||||
if outcome.Fail != nil {
|
||||
if outcome.Fail.When == "" {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
coll.push(result)
|
||||
continue
|
||||
}
|
||||
|
||||
if string(actual.Status) == outcome.Fail.When {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
coll.push(result)
|
||||
}
|
||||
} else if outcome.Warn != nil {
|
||||
if outcome.Warn.When == "" {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
coll.push(result)
|
||||
continue
|
||||
}
|
||||
|
||||
if string(actual.Status) == outcome.Warn.When {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
coll.push(result)
|
||||
}
|
||||
} else if outcome.Pass != nil {
|
||||
if outcome.Pass.When == "" {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
coll.push(result)
|
||||
continue
|
||||
}
|
||||
|
||||
if string(actual.Status) == outcome.Pass.When {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
coll.push(result)
|
||||
}
|
||||
|
||||
}
|
||||
results, err := analyzeHostCollectorResults(collectedContents, a.hostAnalyzer.Outcomes, a.CheckCondition, a.Title())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to analyze HTTP Load Balancer")
|
||||
}
|
||||
|
||||
return coll.get(a.Title()), nil
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostHTTPLoadBalancer) CheckCondition(when string, data []byte) (bool, error) {
|
||||
|
||||
var httpLoadBalancer collect.NetworkStatusResult
|
||||
if err := json.Unmarshal(data, &httpLoadBalancer); err != nil {
|
||||
return false, fmt.Errorf("failed to unmarshal data into NetworkStatusResult: %v", err)
|
||||
}
|
||||
|
||||
if string(httpLoadBalancer.Status) == when {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAnalyzeHTTPLoadBalancer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
info *collect.NetworkStatusResult
|
||||
hostAnalyzer *troubleshootv1beta2.HTTPLoadBalancerAnalyze
|
||||
result []*AnalyzeResult
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "connection refused, fail",
|
||||
info: &collect.NetworkStatusResult{
|
||||
Status: collect.NetworkStatusConnectionRefused,
|
||||
},
|
||||
hostAnalyzer: &troubleshootv1beta2.HTTPLoadBalancerAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "connection-refused",
|
||||
Message: "Connection was refused",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "connected",
|
||||
Message: "Connection was successful",
|
||||
},
|
||||
},
|
||||
{
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "Unexpected HTTP connection status",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
result: []*AnalyzeResult{
|
||||
{
|
||||
Title: "HTTP Load Balancer",
|
||||
IsFail: true,
|
||||
Message: "Connection was refused",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "connected, success",
|
||||
info: &collect.NetworkStatusResult{
|
||||
Status: collect.NetworkStatusConnected,
|
||||
},
|
||||
hostAnalyzer: &troubleshootv1beta2.HTTPLoadBalancerAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "connection-refused",
|
||||
Message: "Connection was refused",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "connected",
|
||||
Message: "Connection was successful",
|
||||
},
|
||||
},
|
||||
{
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "Unexpected HTTP connection status",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
result: []*AnalyzeResult{
|
||||
{
|
||||
Title: "HTTP Load Balancer",
|
||||
IsPass: true,
|
||||
Message: "Connection was successful",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
b, err := json.Marshal(test.info)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
getCollectedFileContents := func(filename string) ([]byte, error) {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
result, err := (&AnalyzeHostHTTPLoadBalancer{test.hostAnalyzer}).Analyze(getCollectedFileContents, nil)
|
||||
if test.expectErr {
|
||||
req.Error(err)
|
||||
} else {
|
||||
req.NoError(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, test.result, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -27,94 +27,24 @@ func (a *AnalyzeHostIPV4Interfaces) IsExcluded() (bool, error) {
|
||||
func (a *AnalyzeHostIPV4Interfaces) Analyze(
|
||||
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
|
||||
) ([]*AnalyzeResult, error) {
|
||||
hostAnalyzer := a.hostAnalyzer
|
||||
result := AnalyzeResult{Title: a.Title()}
|
||||
|
||||
contents, err := getCollectedFileContents(collect.HostIPV4InterfacesPath)
|
||||
collectedContents, err := retrieveCollectedContents(
|
||||
getCollectedFileContents,
|
||||
collect.HostIPV4InterfacesPath,
|
||||
collect.NodeInfoBaseDir,
|
||||
collect.HostIPV4FileName,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get collected file")
|
||||
return []*AnalyzeResult{&result}, err
|
||||
}
|
||||
|
||||
var ipv4Interfaces []net.Interface
|
||||
if err := json.Unmarshal(contents, &ipv4Interfaces); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal ipv4Interfaces")
|
||||
results, err := analyzeHostCollectorResults(collectedContents, a.hostAnalyzer.Outcomes, a.CheckCondition, a.Title())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to analyze IPv4 interfaces")
|
||||
}
|
||||
|
||||
var coll resultCollector
|
||||
|
||||
for _, outcome := range hostAnalyzer.Outcomes {
|
||||
result := &AnalyzeResult{Title: a.Title()}
|
||||
|
||||
if outcome.Fail != nil {
|
||||
if outcome.Fail.When == "" {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
coll.push(result)
|
||||
continue
|
||||
}
|
||||
|
||||
isMatch, err := compareHostIPV4InterfacesConditionalToActual(outcome.Fail.When, ipv4Interfaces)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Fail.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
coll.push(result)
|
||||
}
|
||||
} else if outcome.Warn != nil {
|
||||
if outcome.Warn.When == "" {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
coll.push(result)
|
||||
continue
|
||||
}
|
||||
|
||||
isMatch, err := compareHostIPV4InterfacesConditionalToActual(outcome.Warn.When, ipv4Interfaces)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Warn.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
coll.push(result)
|
||||
}
|
||||
} else if outcome.Pass != nil {
|
||||
if outcome.Pass.When == "" {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
coll.push(result)
|
||||
continue
|
||||
}
|
||||
|
||||
isMatch, err := compareHostIPV4InterfacesConditionalToActual(outcome.Pass.When, ipv4Interfaces)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Pass.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
coll.push(result)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return coll.get(a.Title()), nil
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func compareHostIPV4InterfacesConditionalToActual(conditional string, ipv4Interfaces []net.Interface) (res bool, err error) {
|
||||
@@ -153,3 +83,13 @@ func compareHostIPV4InterfacesConditionalToActual(conditional string, ipv4Interf
|
||||
|
||||
return false, fmt.Errorf("Unknown operator %q. Supported operators are: <, <=, ==, >=, >", operator)
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostIPV4Interfaces) CheckCondition(when string, data []byte) (bool, error) {
|
||||
|
||||
var ipv4Interfaces []net.Interface
|
||||
if err := json.Unmarshal(data, &ipv4Interfaces); err != nil {
|
||||
return false, fmt.Errorf("failed to unmarshal data into []net.Interface: %v", err)
|
||||
}
|
||||
|
||||
return compareHostIPV4InterfacesConditionalToActual(when, ipv4Interfaces)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
)
|
||||
|
||||
type AnalyzeHostJsonCompare struct {
|
||||
hostAnalyzer *troubleshootv1beta2.JsonCompare
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostJsonCompare) Title() string {
|
||||
return jsonCompareTitle(a.hostAnalyzer)
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostJsonCompare) IsExcluded() (bool, error) {
|
||||
return isExcluded(a.hostAnalyzer.Exclude)
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostJsonCompare) Analyze(
|
||||
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
|
||||
) ([]*AnalyzeResult, error) {
|
||||
result, err := analyzeJsonCompare(a.hostAnalyzer, getCollectedFileContents, a.Title())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Strict = a.hostAnalyzer.Strict.BoolOrDefaultFalse()
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
type AnalyzeHostKernelConfigs struct {
|
||||
hostAnalyzer *troubleshootv1beta2.KernelConfigsAnalyze
|
||||
}
|
||||
|
||||
var kConfigRegex = regexp.MustCompile("^(CONFIG_[A-Z0-9_]+)=([ymn]+)$")
|
||||
|
||||
func (a *AnalyzeHostKernelConfigs) Title() string {
|
||||
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "Kernel Configs")
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostKernelConfigs) IsExcluded() (bool, error) {
|
||||
return isExcluded(a.hostAnalyzer.Exclude)
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostKernelConfigs) Analyze(
|
||||
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
|
||||
) ([]*AnalyzeResult, error) {
|
||||
collectedContents, err := retrieveCollectedContents(
|
||||
getCollectedFileContents,
|
||||
collect.HostKernelConfigsPath,
|
||||
collect.NodeInfoBaseDir,
|
||||
collect.HostKernelConfigsFileName,
|
||||
)
|
||||
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.analyzeSingleNode(content, currentTitle)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to analyze kernel configs for %s", currentTitle)
|
||||
}
|
||||
if result != nil {
|
||||
results = append(results, result...)
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func addMissingKernelConfigs(message string, missingConfigs []string) string {
|
||||
if message == "" && len(missingConfigs) == 0 {
|
||||
return message
|
||||
}
|
||||
return strings.ReplaceAll(message, "{{ .ConfigsNotFound }}", strings.Join(missingConfigs, ", "))
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostKernelConfigs) analyzeSingleNode(content collectedContent, currentTitle string) ([]*AnalyzeResult, error) {
|
||||
hostAnalyzer := a.hostAnalyzer
|
||||
kConfigs := collect.KConfigs{}
|
||||
if err := json.Unmarshal(content.Data, &kConfigs); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read kernel configs")
|
||||
}
|
||||
|
||||
var configsNotFound []string
|
||||
for _, config := range hostAnalyzer.SelectedConfigs {
|
||||
matches := kConfigRegex.FindStringSubmatch(config)
|
||||
// zero tolerance for invalid kernel config
|
||||
if matches == nil || len(matches) < 3 {
|
||||
return nil, errors.Errorf("invalid kernel config: %s", config)
|
||||
}
|
||||
|
||||
key := matches[1]
|
||||
values := matches[2] // values can contain multiple values in any order y, m, n
|
||||
|
||||
// check if the kernel config exists
|
||||
if _, ok := kConfigs[key]; !ok {
|
||||
configsNotFound = append(configsNotFound, config)
|
||||
continue
|
||||
}
|
||||
// check if the kernel config value matches
|
||||
if !strings.Contains(values, kConfigs[key]) {
|
||||
klog.V(2).Infof("collected kernel config %s=%s does not in expected values %s", key, kConfigs[key], values)
|
||||
configsNotFound = append(configsNotFound, config)
|
||||
}
|
||||
}
|
||||
|
||||
var results []*AnalyzeResult
|
||||
for _, outcome := range hostAnalyzer.Outcomes {
|
||||
result := &AnalyzeResult{
|
||||
Title: currentTitle,
|
||||
Strict: a.hostAnalyzer.Strict.BoolOrDefaultFalse(),
|
||||
}
|
||||
|
||||
if outcome.Pass != nil && len(configsNotFound) == 0 {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
results = append(results, result)
|
||||
break
|
||||
}
|
||||
|
||||
if outcome.Fail != nil && len(configsNotFound) > 0 {
|
||||
result.IsFail = true
|
||||
result.Message = addMissingKernelConfigs(outcome.Fail.Message, configsNotFound)
|
||||
results = append(results, result)
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAnalyzeKernelConfigs(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
selectedConfigs []string
|
||||
outcomes []*troubleshootv1beta2.Outcome
|
||||
results []*AnalyzeResult
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "all pass",
|
||||
selectedConfigs: []string{"CONFIG_CGROUP_FREEZER=y", "CONFIG_NETFILTER_XTABLES=m"},
|
||||
outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "required kernel configs are available",
|
||||
},
|
||||
},
|
||||
},
|
||||
results: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Kernel Configs",
|
||||
IsPass: true,
|
||||
Message: "required kernel configs are available",
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "has fail",
|
||||
selectedConfigs: []string{"CONFIG_UTS_NS=y"},
|
||||
outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "missing kernel config(s): {{ .ConfigsNotFound }}",
|
||||
},
|
||||
},
|
||||
},
|
||||
results: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Kernel Configs",
|
||||
IsFail: true,
|
||||
Message: "missing kernel config(s): CONFIG_UTS_NS=y",
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "kernel config disabled",
|
||||
selectedConfigs: []string{"CONFIG_CGROUP_FREEZER=n"},
|
||||
outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "missing kernel config(s): {{ .ConfigsNotFound }}",
|
||||
},
|
||||
},
|
||||
},
|
||||
results: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Kernel Configs",
|
||||
IsFail: true,
|
||||
Message: "missing kernel config(s): CONFIG_CGROUP_FREEZER=n",
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid kernel config",
|
||||
selectedConfigs: []string{"foobar=n"},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "select multiple kernel config values",
|
||||
selectedConfigs: []string{"CONFIG_BRIDGE=my"},
|
||||
outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "required kernel configs are available",
|
||||
},
|
||||
},
|
||||
},
|
||||
results: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Kernel Configs",
|
||||
IsPass: true,
|
||||
Message: "required kernel configs are available",
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
mockKernelFile := func(_ string) ([]byte, error) {
|
||||
return []byte(`{"CONFIG_CGROUP_FREEZER": "y", "CONFIG_NETFILTER_XTABLES": "m", "CONFIG_BRIDGE": "y"}`), nil
|
||||
}
|
||||
|
||||
analyzer := AnalyzeHostKernelConfigs{
|
||||
hostAnalyzer: &troubleshootv1beta2.KernelConfigsAnalyze{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "Kernel Configs",
|
||||
},
|
||||
SelectedConfigs: tt.selectedConfigs,
|
||||
Outcomes: tt.outcomes,
|
||||
},
|
||||
}
|
||||
|
||||
results, err := analyzer.Analyze(mockKernelFile, nil)
|
||||
|
||||
if tt.expectErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.results, results)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+32
-87
@@ -26,95 +26,36 @@ func (a *AnalyzeHostMemory) IsExcluded() (bool, error) {
|
||||
func (a *AnalyzeHostMemory) Analyze(
|
||||
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
|
||||
) ([]*AnalyzeResult, error) {
|
||||
hostAnalyzer := a.hostAnalyzer
|
||||
result := AnalyzeResult{Title: a.Title()}
|
||||
|
||||
contents, err := getCollectedFileContents(collect.HostMemoryPath)
|
||||
// Use the generic function to collect both local and remote data
|
||||
collectedContents, err := retrieveCollectedContents(
|
||||
getCollectedFileContents,
|
||||
collect.HostMemoryPath, // Local path
|
||||
collect.NodeInfoBaseDir, // Remote base directory
|
||||
collect.HostMemoryFileName, // Remote file name
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get collected file")
|
||||
return []*AnalyzeResult{&result}, err
|
||||
}
|
||||
|
||||
memoryInfo := collect.MemoryInfo{}
|
||||
if err := json.Unmarshal(contents, &memoryInfo); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal memory info")
|
||||
results, err := analyzeHostCollectorResults(collectedContents, a.hostAnalyzer.Outcomes, a.CheckCondition, a.Title())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to analyze OS version")
|
||||
}
|
||||
|
||||
result := AnalyzeResult{
|
||||
Title: a.Title(),
|
||||
}
|
||||
|
||||
for _, outcome := range hostAnalyzer.Outcomes {
|
||||
|
||||
if outcome.Fail != nil {
|
||||
if outcome.Fail.When == "" {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
return []*AnalyzeResult{&result}, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareHostMemoryConditionalToActual(outcome.Fail.When, memoryInfo.Total)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Fail.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
return []*AnalyzeResult{&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 []*AnalyzeResult{&result}, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareHostMemoryConditionalToActual(outcome.Warn.When, memoryInfo.Total)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Warn.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
return []*AnalyzeResult{&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 []*AnalyzeResult{&result}, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareHostMemoryConditionalToActual(outcome.Pass.When, memoryInfo.Total)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Pass.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
return []*AnalyzeResult{&result}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return []*AnalyzeResult{&result}, nil
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func compareHostMemoryConditionalToActual(conditional string, total uint64) (res bool, err error) {
|
||||
parts := strings.Split(conditional, " ")
|
||||
// checkCondition checks the condition of the when clause
|
||||
func (a *AnalyzeHostMemory) CheckCondition(when string, data []byte) (bool, error) {
|
||||
|
||||
var memInfo collect.MemoryInfo
|
||||
if err := json.Unmarshal(data, &memInfo); err != nil {
|
||||
return false, fmt.Errorf("failed to unmarshal data into MemoryInfo: %v", err)
|
||||
}
|
||||
|
||||
parts := strings.Split(when, " ")
|
||||
if len(parts) != 2 {
|
||||
return false, fmt.Errorf("Expected 2 parts in conditional, got %d", len(parts))
|
||||
}
|
||||
@@ -129,19 +70,23 @@ func compareHostMemoryConditionalToActual(conditional string, total uint64) (res
|
||||
if !ok {
|
||||
return false, fmt.Errorf("could not parse quantity %q", desired)
|
||||
}
|
||||
if desiredInt < 0 {
|
||||
return false, fmt.Errorf("desired value must be a positive integer, got %d", desiredInt)
|
||||
}
|
||||
|
||||
switch operator {
|
||||
case "<":
|
||||
return total < uint64(desiredInt), nil
|
||||
return memInfo.Total < uint64(desiredInt), nil
|
||||
case "<=":
|
||||
return total <= uint64(desiredInt), nil
|
||||
return memInfo.Total <= uint64(desiredInt), nil
|
||||
case ">":
|
||||
return total > uint64(desiredInt), nil
|
||||
return memInfo.Total > uint64(desiredInt), nil
|
||||
case ">=":
|
||||
return total >= uint64(desiredInt), nil
|
||||
return memInfo.Total >= uint64(desiredInt), nil
|
||||
case "=", "==", "===":
|
||||
return total == uint64(desiredInt), nil
|
||||
return memInfo.Total == uint64(desiredInt), nil
|
||||
default:
|
||||
return false, fmt.Errorf("unsupported operator: %q", operator)
|
||||
}
|
||||
|
||||
return false, errors.New("unknown operator")
|
||||
}
|
||||
|
||||
+167
-108
@@ -2,91 +2,110 @@ package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_doCompareHostMemory(t *testing.T) {
|
||||
func TestAnalyzeHostMemoryCheckCondition(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
conditional string
|
||||
actual uint64
|
||||
expected bool
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "< 16Gi when actual is 8Gi",
|
||||
conditional: "< 16Gi",
|
||||
actual: 8 * 1024 * 1024 * 1024,
|
||||
actual: 8 * 1024 * 1024 * 1024, // 8GiB
|
||||
expected: true,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "< 8Gi when actual is 8Gi",
|
||||
conditional: "< 8Gi",
|
||||
actual: 8 * 1024 * 1024 * 1024,
|
||||
actual: 8 * 1024 * 1024 * 1024, // 8GiB
|
||||
expected: false,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "<= 8Gi when actual is 8Gi",
|
||||
conditional: "<= 8Gi",
|
||||
actual: 8 * 1024 * 1024 * 1024,
|
||||
actual: 8 * 1024 * 1024 * 1024, // 8GiB
|
||||
expected: true,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "<= 8Gi when actual is 16Gi",
|
||||
conditional: "<= 8Gi",
|
||||
actual: 16 * 1024 * 1024 * 1024,
|
||||
actual: 16 * 1024 * 1024 * 1024, // 16GiB
|
||||
expected: false,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "== 8Gi when actual is 16Gi",
|
||||
conditional: "== 8Gi",
|
||||
actual: 16 * 1024 * 1024 * 1024,
|
||||
actual: 16 * 1024 * 1024 * 1024, // 16GiB
|
||||
expected: false,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "== 8Gi when actual is 8Gi",
|
||||
conditional: "== 8Gi",
|
||||
actual: 8 * 1024 * 1024 * 1024,
|
||||
actual: 8 * 1024 * 1024 * 1024, // 8GiB
|
||||
expected: true,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "== 8000000000 when actual is 8000000000",
|
||||
conditional: "== 8000000000",
|
||||
actual: 8 * 1000 * 1000 * 1000,
|
||||
actual: 8 * 1000 * 1000 * 1000, // 8GB in decimal
|
||||
expected: true,
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
// Create the AnalyzeHostMemory object
|
||||
analyzeHostMemory := AnalyzeHostMemory{}
|
||||
|
||||
actual, err := compareHostMemoryConditionalToActual(test.conditional, test.actual)
|
||||
req.NoError(err)
|
||||
|
||||
assert.Equal(t, test.expected, actual)
|
||||
// Simulate the memory info as JSON-encoded data
|
||||
memInfo := collect.MemoryInfo{
|
||||
Total: test.actual,
|
||||
}
|
||||
rawData, err := json.Marshal(memInfo)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Call the CheckCondition method
|
||||
result, err := analyzeHostMemory.CheckCondition(test.conditional, rawData)
|
||||
if test.expectErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyzeHostMemory(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
memoryInfo *collect.MemoryInfo
|
||||
hostAnalyzer *troubleshootv1beta2.MemoryAnalyze
|
||||
result []*AnalyzeResult
|
||||
expectErr bool
|
||||
name string
|
||||
hostAnalyzer *troubleshootv1beta2.MemoryAnalyze
|
||||
getCollectedFileContents func(string) ([]byte, error)
|
||||
expectedResults []*AnalyzeResult
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "Pass on memory available",
|
||||
memoryInfo: &collect.MemoryInfo{
|
||||
Total: 8 * 1024 * 1024 * 1024,
|
||||
},
|
||||
name: "Pass on memory available (local)",
|
||||
hostAnalyzer: &troubleshootv1beta2.MemoryAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
@@ -97,19 +116,27 @@ func TestAnalyzeHostMemory(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
result: []*AnalyzeResult{
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
// Simulate local memory content retrieval
|
||||
if path == collect.HostMemoryPath {
|
||||
memoryInfo := collect.MemoryInfo{
|
||||
Total: 8 * 1024 * 1024 * 1024, // 8GiB
|
||||
}
|
||||
return json.Marshal(memoryInfo)
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
expectedResults: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Amount of Memory",
|
||||
IsPass: true,
|
||||
Message: "System has at least 4Gi of memory",
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "Fail on memory available",
|
||||
memoryInfo: &collect.MemoryInfo{
|
||||
Total: 8 * 1024 * 1024 * 1024,
|
||||
},
|
||||
name: "Fail on memory available (remote node)",
|
||||
hostAnalyzer: &troubleshootv1beta2.MemoryAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
@@ -120,27 +147,33 @@ func TestAnalyzeHostMemory(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
result: []*AnalyzeResult{
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
// Simulate remote node list and memory content retrieval
|
||||
if path == constants.NODE_LIST_FILE {
|
||||
nodeNames := nodeNames{Nodes: []string{"node1"}}
|
||||
return json.Marshal(nodeNames)
|
||||
}
|
||||
if path == fmt.Sprintf("%s/node1/%s", collect.NodeInfoBaseDir, collect.HostMemoryFileName) {
|
||||
memoryInfo := collect.MemoryInfo{
|
||||
Total: 8 * 1024 * 1024 * 1024, // 8GiB for remote node
|
||||
}
|
||||
return json.Marshal(memoryInfo)
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
expectedResults: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Amount of Memory",
|
||||
Title: "Amount of Memory - Node node1",
|
||||
IsFail: true,
|
||||
Message: "System requires at least 16Gi of memory",
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "Warn on memory available",
|
||||
memoryInfo: &collect.MemoryInfo{
|
||||
Total: 8 * 1024 * 1024 * 1024,
|
||||
},
|
||||
name: "Warn on memory available (remote node)",
|
||||
hostAnalyzer: &troubleshootv1beta2.MemoryAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "< 4Gi",
|
||||
Message: "System requires at least 4Gi of memory",
|
||||
},
|
||||
},
|
||||
{
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "<= 8Gi",
|
||||
@@ -149,97 +182,123 @@ func TestAnalyzeHostMemory(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
result: []*AnalyzeResult{
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
// Simulate remote node list and memory content retrieval
|
||||
if path == constants.NODE_LIST_FILE {
|
||||
nodeNames := nodeNames{Nodes: []string{"node1"}}
|
||||
return json.Marshal(nodeNames)
|
||||
}
|
||||
if path == fmt.Sprintf("%s/node1/%s", collect.NodeInfoBaseDir, collect.HostMemoryFileName) {
|
||||
memoryInfo := collect.MemoryInfo{
|
||||
Total: 8 * 1024 * 1024 * 1024, // 8GiB for remote node
|
||||
}
|
||||
return json.Marshal(memoryInfo)
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
expectedResults: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Amount of Memory",
|
||||
Title: "Amount of Memory - Node node1",
|
||||
IsWarn: true,
|
||||
Message: "System performs best with more than 8Gi of memory",
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
b, err := json.Marshal(test.memoryInfo)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
getCollectedFileContents := func(filename string) ([]byte, error) {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
result, err := (&AnalyzeHostMemory{test.hostAnalyzer}).Analyze(getCollectedFileContents, nil)
|
||||
if test.expectErr {
|
||||
req.Error(err)
|
||||
} else {
|
||||
req.NoError(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, test.result, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostMemoryAnalyze(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
memoryInfo collect.MemoryInfo
|
||||
outcomes []*troubleshootv1beta2.Outcome
|
||||
results []*AnalyzeResult
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "fix for empty pass predicate",
|
||||
memoryInfo: collect.MemoryInfo{
|
||||
Total: 16 * 1024 * 1024 * 1024,
|
||||
},
|
||||
outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "< 8Gi",
|
||||
Message: "oops",
|
||||
name: "Pass on empty pass predicate (local)",
|
||||
hostAnalyzer: &troubleshootv1beta2.MemoryAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "< 8Gi",
|
||||
Message: "System requires at least 8Gi of memory",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "",
|
||||
Message: "it passed",
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "",
|
||||
Message: "Memory is sufficient",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
results: []*AnalyzeResult{
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
// Simulate local memory content retrieval
|
||||
if path == collect.HostMemoryPath {
|
||||
memoryInfo := collect.MemoryInfo{
|
||||
Total: 16 * 1024 * 1024 * 1024, // 16GiB
|
||||
}
|
||||
return json.Marshal(memoryInfo)
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
expectedResults: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Amount of Memory",
|
||||
IsPass: true,
|
||||
Message: "Memory is sufficient",
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "Fix for empty pass predicate",
|
||||
hostAnalyzer: &troubleshootv1beta2.MemoryAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "< 8Gi",
|
||||
Message: "oops",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "",
|
||||
Message: "it passed",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
// Simulate local memory content retrieval
|
||||
if path == collect.HostMemoryPath {
|
||||
memoryInfo := collect.MemoryInfo{
|
||||
Total: 16 * 1024 * 1024 * 1024, // 16GiB
|
||||
}
|
||||
return json.Marshal(memoryInfo)
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
expectedResults: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Amount of Memory",
|
||||
IsPass: true,
|
||||
Message: "it passed",
|
||||
Title: "Memory Test",
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fn := func(_ string) ([]byte, error) {
|
||||
return json.Marshal(&tc.memoryInfo)
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// Set up the AnalyzeHostMemory object
|
||||
analyzeHostMemory := AnalyzeHostMemory{
|
||||
hostAnalyzer: test.hostAnalyzer,
|
||||
}
|
||||
|
||||
analyzer := AnalyzeHostMemory{
|
||||
hostAnalyzer: &troubleshootv1beta2.MemoryAnalyze{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "Memory Test",
|
||||
},
|
||||
Outcomes: tc.outcomes,
|
||||
},
|
||||
// Call the Analyze function
|
||||
results, err := analyzeHostMemory.Analyze(test.getCollectedFileContents, nil)
|
||||
|
||||
// Check for errors and compare results
|
||||
if test.expectedError != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), test.expectedError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expectedResults, results)
|
||||
}
|
||||
results, err := analyzer.Analyze(fn, nil)
|
||||
if tc.wantErr {
|
||||
require.NotNil(t, err)
|
||||
return
|
||||
}
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, tc.results, results)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
util "github.com/replicatedhq/troubleshoot/internal/util"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
)
|
||||
|
||||
type AnalyzeHostNetworkNamespaceConnectivity struct {
|
||||
hostAnalyzer *troubleshootv1beta2.NetworkNamespaceConnectivityAnalyze
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostNetworkNamespaceConnectivity) Title() string {
|
||||
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "Network Namespace Connectivity")
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostNetworkNamespaceConnectivity) IsExcluded() (bool, error) {
|
||||
return isExcluded(a.hostAnalyzer.Exclude)
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostNetworkNamespaceConnectivity) Analyze(
|
||||
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
|
||||
) ([]*AnalyzeResult, error) {
|
||||
hostAnalyzer := a.hostAnalyzer
|
||||
|
||||
collectedPath := filepath.Join("host-collectors/system", "networkNamespaceConnectivity.json")
|
||||
fileName := "networkNamespaceConnectivity.json"
|
||||
if hostAnalyzer.CollectorName != "" {
|
||||
collectedPath = filepath.Join("host-collectors/system", hostAnalyzer.CollectorName+".json")
|
||||
fileName = hostAnalyzer.CollectorName + ".json"
|
||||
}
|
||||
|
||||
collectedContents, err := retrieveCollectedContents(
|
||||
getCollectedFileContents,
|
||||
collectedPath,
|
||||
collect.NodeInfoBaseDir,
|
||||
fileName,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve collected contents: %w", err)
|
||||
}
|
||||
|
||||
var results []*AnalyzeResult
|
||||
for _, collected := range collectedContents {
|
||||
var info collect.NetworkNamespaceConnectivityInfo
|
||||
if err := json.Unmarshal(collected.Data, &info); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal disk usage info: %w", err)
|
||||
}
|
||||
|
||||
for _, outcome := range hostAnalyzer.Outcomes {
|
||||
result := &AnalyzeResult{Title: a.Title()}
|
||||
|
||||
if outcome.Pass != nil && info.Success {
|
||||
result.IsPass = true
|
||||
result.Message, err = util.RenderTemplate(outcome.Pass.Message, &info)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to render template on outcome message: %w", err)
|
||||
}
|
||||
results = append(results, result)
|
||||
break
|
||||
}
|
||||
|
||||
if outcome.Fail != nil && !info.Success {
|
||||
result.IsFail = true
|
||||
result.Message, err = util.RenderTemplate(outcome.Fail.Message, &info)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to render template on outcome message: %w", err)
|
||||
}
|
||||
results = append(results, result)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
+60
-76
@@ -27,103 +27,87 @@ func (a *AnalyzeHostOS) IsExcluded() (bool, error) {
|
||||
func (a *AnalyzeHostOS) Analyze(
|
||||
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
|
||||
) ([]*AnalyzeResult, error) {
|
||||
result := AnalyzeResult{Title: a.Title()}
|
||||
|
||||
result := AnalyzeResult{}
|
||||
result.Title = a.Title()
|
||||
|
||||
contents, err := getCollectedFileContents(collect.HostOSInfoPath)
|
||||
// Use the generic function to collect both local and remote data
|
||||
collectedContents, err := retrieveCollectedContents(
|
||||
getCollectedFileContents,
|
||||
collect.HostOSInfoPath, // Local path
|
||||
collect.NodeInfoBaseDir, // Remote base directory
|
||||
collect.HostInfoFileName, // Remote file name
|
||||
)
|
||||
if err != nil {
|
||||
return []*AnalyzeResult{&result}, errors.Wrap(err, "failed to get collected file")
|
||||
return []*AnalyzeResult{&result}, err
|
||||
}
|
||||
|
||||
var osInfo collect.HostOSInfo
|
||||
if err := json.Unmarshal(contents, &osInfo); err != nil {
|
||||
return []*AnalyzeResult{&result}, errors.Wrap(err, "failed to unmarshal host os info")
|
||||
results, err := analyzeHostCollectorResults(collectedContents, a.hostAnalyzer.Outcomes, a.CheckCondition, a.Title())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to analyze OS version")
|
||||
}
|
||||
|
||||
return analyzeOSVersionResult(osInfo, a.hostAnalyzer.Outcomes, a.Title())
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func analyzeOSVersionResult(osInfo collect.HostOSInfo, outcomes []*troubleshootv1beta2.Outcome, title string) ([]*AnalyzeResult, error) {
|
||||
// checkCondition checks the condition of the when clause
|
||||
func (a *AnalyzeHostOS) CheckCondition(when string, data []byte) (bool, error) {
|
||||
|
||||
if title == "" {
|
||||
title = "Host OS Info"
|
||||
var osInfo collect.HostOSInfo
|
||||
if err := json.Unmarshal(data, &osInfo); err != nil {
|
||||
return false, fmt.Errorf("failed to unmarshal data into HostOSInfo: %v", err)
|
||||
}
|
||||
|
||||
for _, outcome := range outcomes {
|
||||
parts := strings.Split(when, " ")
|
||||
if len(parts) < 3 {
|
||||
return false, errors.New("when condition must have at least 3 parts")
|
||||
}
|
||||
expectedVer := fixVersion(parts[2])
|
||||
toleratedVer, err := semver.ParseTolerant(expectedVer)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "failed to parse version: %s", expectedVer)
|
||||
}
|
||||
when = fmt.Sprintf("%s %v", parts[1], toleratedVer)
|
||||
whenRange, err := semver.ParseRange(when)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "failed to parse version range: %s", when)
|
||||
}
|
||||
|
||||
result := AnalyzeResult{
|
||||
Title: title,
|
||||
}
|
||||
when := ""
|
||||
message := ""
|
||||
uri := ""
|
||||
|
||||
if outcome.Fail != nil {
|
||||
result.IsFail = true
|
||||
when = outcome.Fail.When
|
||||
message = outcome.Fail.Message
|
||||
uri = outcome.Fail.URI
|
||||
} else if outcome.Warn != nil {
|
||||
result.IsWarn = true
|
||||
when = outcome.Warn.When
|
||||
message = outcome.Warn.Message
|
||||
uri = outcome.Warn.URI
|
||||
} else if outcome.Pass != nil {
|
||||
result.IsPass = true
|
||||
when = outcome.Pass.When
|
||||
message = outcome.Pass.Message
|
||||
uri = outcome.Pass.URI
|
||||
} else {
|
||||
return nil, errors.New("empty outcome")
|
||||
}
|
||||
|
||||
result.Message = message
|
||||
result.URI = uri
|
||||
// When is usually empty as the final case and should be treated as true
|
||||
if when == "" {
|
||||
return []*AnalyzeResult{&result}, nil
|
||||
}
|
||||
|
||||
parts := strings.Split(when, " ")
|
||||
platform := parts[0]
|
||||
expectedVer := fixVersion(parts[2])
|
||||
toleratedVer, err := semver.ParseTolerant(expectedVer)
|
||||
// Match the kernel version regardless of the platform
|
||||
if parts[0] == "kernelVersion" {
|
||||
fixedKernelVer := fixVersion(osInfo.KernelVersion)
|
||||
toleratedKernelVer, err := semver.ParseTolerant(fixedKernelVer)
|
||||
if err != nil {
|
||||
return []*AnalyzeResult{&result}, errors.Wrapf(err, "failed to parse tolerant: %s", expectedVer)
|
||||
return false, errors.Wrapf(err, "failed to parse tolerant: %v", fixedKernelVer)
|
||||
}
|
||||
|
||||
when = fmt.Sprintf("%s %v", parts[1], toleratedVer)
|
||||
whenRange, err := semver.ParseRange(when)
|
||||
if err != nil {
|
||||
return []*AnalyzeResult{&result}, errors.Wrapf(err, "failed to parse range: %s", when)
|
||||
if whenRange(toleratedKernelVer) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
kernelInfo := fmt.Sprintf("%s-%s-kernel", osInfo.Platform, osInfo.PlatformVersion)
|
||||
if len(strings.Split(platform, "-")) == 3 && strings.Split(platform, "-")[2] == "kernel" {
|
||||
if platform == kernelInfo {
|
||||
fixedKernelVer := fixVersion(osInfo.KernelVersion)
|
||||
toleratedKernelVer, err := semver.ParseTolerant(fixedKernelVer)
|
||||
if err != nil {
|
||||
return []*AnalyzeResult{&result}, errors.Wrapf(err, "failed to parse tolerant: %v", fixedKernelVer)
|
||||
}
|
||||
if whenRange(toleratedKernelVer) {
|
||||
return []*AnalyzeResult{&result}, nil
|
||||
}
|
||||
}
|
||||
} else if platform == osInfo.Platform {
|
||||
fixedDistVer := fixVersion(osInfo.PlatformVersion)
|
||||
toleratedDistVer, err := semver.ParseTolerant(fixedDistVer)
|
||||
// Match platform version and kernel version, such as "centos-8.2-kernel == 8.2"
|
||||
platform := parts[0]
|
||||
kernelInfo := fmt.Sprintf("%s-%s-kernel", osInfo.Platform, osInfo.PlatformVersion)
|
||||
if len(strings.Split(platform, "-")) == 3 && strings.Split(platform, "-")[2] == "kernel" {
|
||||
if platform == kernelInfo {
|
||||
fixedKernelVer := fixVersion(osInfo.KernelVersion)
|
||||
toleratedKernelVer, err := semver.ParseTolerant(fixedKernelVer)
|
||||
if err != nil {
|
||||
return []*AnalyzeResult{&result}, errors.Wrapf(err, "failed to parse tolerant: %v", fixedDistVer)
|
||||
return false, errors.Wrapf(err, "failed to parse tolerant: %v", fixedKernelVer)
|
||||
}
|
||||
if whenRange(toleratedDistVer) {
|
||||
return []*AnalyzeResult{&result}, nil
|
||||
if whenRange(toleratedKernelVer) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
} else if platform == osInfo.Platform {
|
||||
fixedDistVer := fixVersion(osInfo.PlatformVersion)
|
||||
toleratedDistVer, err := semver.ParseTolerant(fixedDistVer)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "failed to parse tolerant: %v", fixedDistVer)
|
||||
}
|
||||
if whenRange(toleratedDistVer) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return []*AnalyzeResult{}, nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var rx = regexp.MustCompile(`^[0-9]+\.?[0-9]*\.?[0-9]*`)
|
||||
|
||||
+248
-217
@@ -2,30 +2,106 @@ package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAnalyzeHostOS(t *testing.T) {
|
||||
func TestAnalyzeHostOSCheckCondition(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hostInfo collect.HostOSInfo
|
||||
hostAnalyzer *troubleshootv1beta2.HostOSAnalyze
|
||||
result []*AnalyzeResult
|
||||
expectErr bool
|
||||
name string
|
||||
conditional string
|
||||
osInfo collect.HostOSInfo
|
||||
expected bool
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "pass if ubuntu >= 0.1.2",
|
||||
hostInfo: collect.HostOSInfo{
|
||||
Name: "myhost",
|
||||
KernelVersion: "5.4.0-1034-gcp",
|
||||
PlatformVersion: "00.1.2",
|
||||
Platform: "ubuntu",
|
||||
name: "kernelVersion == 4.15 when actual is 4.15",
|
||||
conditional: "kernelVersion == 4.15",
|
||||
osInfo: collect.HostOSInfo{
|
||||
KernelVersion: "4.15.0",
|
||||
},
|
||||
expected: true,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "kernelVersion < 4.15 when actual is 4.16",
|
||||
conditional: "kernelVersion < 4.15",
|
||||
osInfo: collect.HostOSInfo{
|
||||
KernelVersion: "4.16.0",
|
||||
},
|
||||
expected: false,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "centos == 8.2 when actual is 8.2",
|
||||
conditional: "centos == 8.2",
|
||||
osInfo: collect.HostOSInfo{
|
||||
Platform: "centos",
|
||||
PlatformVersion: "8.2",
|
||||
},
|
||||
expected: true,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "ubuntu == 20.04 when actual is 18.04",
|
||||
conditional: "ubuntu == 20.04",
|
||||
osInfo: collect.HostOSInfo{
|
||||
Platform: "ubuntu",
|
||||
PlatformVersion: "18.04",
|
||||
},
|
||||
expected: false,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid conditional format",
|
||||
conditional: "invalid conditional",
|
||||
osInfo: collect.HostOSInfo{
|
||||
Platform: "ubuntu",
|
||||
PlatformVersion: "18.04",
|
||||
},
|
||||
expected: false,
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// Create the AnalyzeHostOS object
|
||||
analyzeHostOS := AnalyzeHostOS{}
|
||||
|
||||
// Simulate the OS info as JSON-encoded data
|
||||
rawData, err := json.Marshal(test.osInfo)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Call the CheckCondition method
|
||||
result, err := analyzeHostOS.CheckCondition(test.conditional, rawData)
|
||||
if test.expectErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyzeHostOS(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hostAnalyzer *troubleshootv1beta2.HostOSAnalyze
|
||||
getCollectedFileContents func(string) ([]byte, error)
|
||||
result []*AnalyzeResult
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "successfully retrieve local content and analyze",
|
||||
hostAnalyzer: &troubleshootv1beta2.HostOSAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
@@ -41,6 +117,17 @@ func TestAnalyzeHostOS(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
if path == collect.HostOSInfoPath {
|
||||
return json.Marshal(collect.HostOSInfo{
|
||||
Name: "myhost",
|
||||
KernelVersion: "5.4.0-1034-gcp",
|
||||
PlatformVersion: "00.1.2",
|
||||
Platform: "ubuntu",
|
||||
})
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
result: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Host OS Info",
|
||||
@@ -48,22 +135,16 @@ func TestAnalyzeHostOS(t *testing.T) {
|
||||
Message: "supported distribution matches ubuntu >= 00.1.2",
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
|
||||
{
|
||||
name: "pass if ubuntu >= 1.0.2",
|
||||
hostInfo: collect.HostOSInfo{
|
||||
Name: "myhost",
|
||||
KernelVersion: "5.4.0-1034-gcp",
|
||||
PlatformVersion: "1.0.2",
|
||||
Platform: "ubuntu",
|
||||
},
|
||||
name: "local content not found, retrieve and analyze remote content",
|
||||
hostAnalyzer: &troubleshootv1beta2.HostOSAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "ubuntu >= 1.0.2",
|
||||
Message: "supported distribution matches ubuntu >= 1.0.2",
|
||||
When: "ubuntu == 18.04",
|
||||
Message: "supported remote ubuntu 18.04",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -73,53 +154,75 @@ func TestAnalyzeHostOS(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
if path == constants.NODE_LIST_FILE {
|
||||
return json.Marshal(nodeNames{Nodes: []string{"node1"}})
|
||||
}
|
||||
if path == fmt.Sprintf("%s/node1/%s", collect.NodeInfoBaseDir, collect.HostInfoFileName) {
|
||||
return json.Marshal(collect.HostOSInfo{
|
||||
Name: "nodehost",
|
||||
KernelVersion: "4.15.0-1034-aws",
|
||||
PlatformVersion: "18.04",
|
||||
Platform: "ubuntu",
|
||||
})
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
result: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Host OS Info",
|
||||
Title: "Host OS Info - Node node1",
|
||||
IsPass: true,
|
||||
Message: "supported distribution matches ubuntu >= 1.0.2",
|
||||
Message: "supported remote ubuntu 18.04",
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "pass if ubuntu >= 1.2.0",
|
||||
hostInfo: collect.HostOSInfo{
|
||||
Name: "myhost",
|
||||
KernelVersion: "5.4.0-1034-gcp",
|
||||
PlatformVersion: "1.2.0",
|
||||
Platform: "ubuntu",
|
||||
},
|
||||
name: "fail to retrieve both local and remote content",
|
||||
hostAnalyzer: &troubleshootv1beta2.HostOSAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "ubuntu >= 1.0.2",
|
||||
Message: "supported distribution matches ubuntu >= 1.2.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "unsupported distribution",
|
||||
Message: "failed analysis",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
result: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Host OS Info",
|
||||
IsPass: true,
|
||||
Message: "supported distribution matches ubuntu >= 1.2.0",
|
||||
Title: "Host OS Info",
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "pass if ubuntu-1.2.0-kernel >= 1.2.0",
|
||||
hostInfo: collect.HostOSInfo{
|
||||
Name: "myhost",
|
||||
KernelVersion: "1.2.0-1034-gcp",
|
||||
PlatformVersion: "1.2.0",
|
||||
Platform: "centos",
|
||||
name: "error during remote content analysis",
|
||||
hostAnalyzer: &troubleshootv1beta2.HostOSAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "analysis failed",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
if path == constants.NODE_LIST_FILE {
|
||||
return json.Marshal(nodeNames{Nodes: []string{"node1"}})
|
||||
}
|
||||
if path == fmt.Sprintf("%s/node1/%s", collect.NodeInfoBaseDir, collect.HostInfoFileName) {
|
||||
return nil, errors.New("file not found")
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
result: nil,
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "pass if centos-1.2.0-kernel >= 1.2.0",
|
||||
hostAnalyzer: &troubleshootv1beta2.HostOSAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
@@ -135,211 +238,139 @@ func TestAnalyzeHostOS(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
if path == constants.NODE_LIST_FILE {
|
||||
return json.Marshal(nodeNames{Nodes: []string{"node1"}})
|
||||
}
|
||||
if path == fmt.Sprintf("%s/node1/%s", collect.NodeInfoBaseDir, collect.HostInfoFileName) {
|
||||
return json.Marshal(collect.HostOSInfo{
|
||||
Name: "nodehost",
|
||||
KernelVersion: "1.2.0-1034-aws",
|
||||
PlatformVersion: "1.2.0",
|
||||
Platform: "centos",
|
||||
})
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
result: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Host OS Info",
|
||||
Title: "Host OS Info - Node node1",
|
||||
IsPass: true,
|
||||
Message: "supported kernel matches centos-1.2.0-kernel >= 1.2.0",
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "pass if ubuntu-0.1.2-kernel >= 0.1.2",
|
||||
hostInfo: collect.HostOSInfo{
|
||||
Name: "myhost",
|
||||
KernelVersion: "0.01.2-1034-gcp",
|
||||
PlatformVersion: "8.2",
|
||||
Platform: "centos",
|
||||
},
|
||||
name: "warn if ubuntu <= 16.04",
|
||||
hostAnalyzer: &troubleshootv1beta2.HostOSAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "centos-8.2-kernel >= 0.01.2",
|
||||
Message: "supported kernel matches centos-8.2-kernel >= 0.01.2",
|
||||
},
|
||||
},
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "unsupported distribution",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
result: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Host OS Info",
|
||||
IsPass: true,
|
||||
Message: "supported kernel matches centos-8.2-kernel >= 0.01.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "fail if ubuntu <= 11.04",
|
||||
hostInfo: collect.HostOSInfo{
|
||||
Name: "myhost",
|
||||
KernelVersion: "5.4.0-1034-gcp",
|
||||
PlatformVersion: "11.04",
|
||||
Platform: "ubuntu",
|
||||
},
|
||||
hostAnalyzer: &troubleshootv1beta2.HostOSAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "ubuntu <= 11.04",
|
||||
Message: "unsupported ubuntu version 11.04",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "supported distribution",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
result: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Host OS Info",
|
||||
IsFail: true,
|
||||
Message: "unsupported ubuntu version 11.04",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fail if none of the kernel distribution versions match",
|
||||
hostInfo: collect.HostOSInfo{
|
||||
Name: "my-host",
|
||||
KernelVersion: "4.4",
|
||||
PlatformVersion: "18.04",
|
||||
Platform: "ubuntu",
|
||||
},
|
||||
hostAnalyzer: &troubleshootv1beta2.HostOSAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "centos-18.04-kernel > 4.15",
|
||||
Message: "supported distribution matches centos-18.04-kernel >= 4.15",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "ubuntu-18.04-kernel > 4.15",
|
||||
Message: "supported distribution matches ubuntu-18.04-kernel >= 4.15",
|
||||
},
|
||||
},
|
||||
{
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "ubuntu-16.04-kernel == 4.15",
|
||||
Message: "supported distribution matches ubuntu-16.04-kernel == 4.15 ",
|
||||
When: "ubuntu <= 16.04",
|
||||
Message: "System performs best with Ubuntu version higher than 16.04",
|
||||
},
|
||||
},
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "None matched, centos-18.04-kernel >= 4.15,ubuntu-18.04-kernel >= 4.15, supported distribution",
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "ubuntu > 16.04",
|
||||
Message: "Ubuntu version is sufficient",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
if path == collect.HostOSInfoPath {
|
||||
return json.Marshal(collect.HostOSInfo{
|
||||
Name: "myhost",
|
||||
KernelVersion: "4.15.0-1234-gcp",
|
||||
PlatformVersion: "16.04",
|
||||
Platform: "ubuntu",
|
||||
})
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
result: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Host OS Info",
|
||||
IsWarn: true,
|
||||
Message: "System performs best with Ubuntu version higher than 16.04",
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "analyze multiple nodes with different OS info",
|
||||
hostAnalyzer: &troubleshootv1beta2.HostOSAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "ubuntu == 18.04",
|
||||
Message: "supported ubuntu version",
|
||||
},
|
||||
},
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "unsupported ubuntu version",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
if path == constants.NODE_LIST_FILE {
|
||||
return json.Marshal(nodeNames{Nodes: []string{"node1", "node2"}})
|
||||
}
|
||||
if path == fmt.Sprintf("%s/node1/%s", collect.NodeInfoBaseDir, collect.HostInfoFileName) {
|
||||
return json.Marshal(collect.HostOSInfo{
|
||||
Name: "nodehost",
|
||||
KernelVersion: "4.15.0-1034-aws",
|
||||
PlatformVersion: "18.04",
|
||||
Platform: "ubuntu",
|
||||
})
|
||||
}
|
||||
if path == fmt.Sprintf("%s/node2/%s", collect.NodeInfoBaseDir, collect.HostInfoFileName) {
|
||||
return json.Marshal(collect.HostOSInfo{
|
||||
Name: "nodehost",
|
||||
KernelVersion: "4.15.0-1034-aws",
|
||||
PlatformVersion: "16.04",
|
||||
Platform: "ubuntu",
|
||||
})
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
result: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Host OS Info - Node node1",
|
||||
IsPass: true,
|
||||
Message: "supported ubuntu version",
|
||||
},
|
||||
{
|
||||
Title: "Host OS Info - Node node2",
|
||||
IsFail: true,
|
||||
Message: "None matched, centos-18.04-kernel >= 4.15,ubuntu-18.04-kernel >= 4.15, supported distribution",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test if centos kernel > 4.15",
|
||||
hostInfo: collect.HostOSInfo{
|
||||
Name: "my-host",
|
||||
KernelVersion: "4.15",
|
||||
PlatformVersion: "18.04",
|
||||
Platform: "centos",
|
||||
},
|
||||
hostAnalyzer: &troubleshootv1beta2.HostOSAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "centos-18.04-kernel >= 4.15",
|
||||
Message: "supported distribution matches centos-18.04-kernel >= 4.15",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "ubuntu-18.04-kernel > 4.15",
|
||||
Message: "supported distribution matches ubuntu-18.04-kernel >= 4.15",
|
||||
},
|
||||
},
|
||||
{
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "ubuntu-16.04-kernel == 4.15",
|
||||
Message: "supported distribution matches ubuntu-16.04-kernel == 4.15 ",
|
||||
},
|
||||
},
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "None matched, centos-18.04-kernel >= 4.15,ubuntu-18.04-kernel >= 4.15, supported distribution",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
result: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Host OS Info",
|
||||
IsPass: true,
|
||||
Message: "supported distribution matches centos-18.04-kernel >= 4.15",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test ubuntu 16 kernel >= 4.15-abc",
|
||||
hostInfo: collect.HostOSInfo{
|
||||
Name: "my-host",
|
||||
KernelVersion: "4.14-abc",
|
||||
PlatformVersion: "16.04",
|
||||
Platform: "ubuntu",
|
||||
},
|
||||
hostAnalyzer: &troubleshootv1beta2.HostOSAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "ubuntu-16.04-kernel >= 4.14",
|
||||
Message: "supported distribution match 4.14",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
result: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Host OS Info",
|
||||
IsPass: true,
|
||||
Message: "supported distribution match 4.14",
|
||||
Message: "unsupported ubuntu version",
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
b, err := json.Marshal(test.hostInfo)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
// Set up the AnalyzeHostOS object with the custom hostAnalyzer per test
|
||||
analyzeHostOS := AnalyzeHostOS{
|
||||
hostAnalyzer: test.hostAnalyzer,
|
||||
}
|
||||
|
||||
getCollectedFileContents := func(filename string) ([]byte, error) {
|
||||
return b, nil
|
||||
}
|
||||
// Call the Analyze function
|
||||
results, err := analyzeHostOS.Analyze(test.getCollectedFileContents, nil)
|
||||
|
||||
result, err := (&AnalyzeHostOS{test.hostAnalyzer}).Analyze(getCollectedFileContents, nil)
|
||||
// Check for errors and compare results
|
||||
if test.expectErr {
|
||||
req.Error(err)
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
req.NoError(err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.result, results)
|
||||
}
|
||||
|
||||
assert.Equal(t, test.result, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,94 +25,24 @@ func (a *AnalyzeHostServices) IsExcluded() (bool, error) {
|
||||
func (a *AnalyzeHostServices) Analyze(
|
||||
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
|
||||
) ([]*AnalyzeResult, error) {
|
||||
hostAnalyzer := a.hostAnalyzer
|
||||
result := AnalyzeResult{Title: a.Title()}
|
||||
|
||||
contents, err := getCollectedFileContents(collect.HostServicesPath)
|
||||
collectedContents, err := retrieveCollectedContents(
|
||||
getCollectedFileContents,
|
||||
collect.HostServicesPath,
|
||||
collect.NodeInfoBaseDir,
|
||||
collect.HostServicesFileName,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get collected file")
|
||||
return []*AnalyzeResult{&result}, err
|
||||
}
|
||||
|
||||
var services []collect.ServiceInfo
|
||||
if err := json.Unmarshal(contents, &services); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal systemctl service info")
|
||||
results, err := analyzeHostCollectorResults(collectedContents, a.hostAnalyzer.Outcomes, a.CheckCondition, a.Title())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to analyze host services")
|
||||
}
|
||||
|
||||
var coll resultCollector
|
||||
|
||||
for _, outcome := range hostAnalyzer.Outcomes {
|
||||
result := &AnalyzeResult{Title: a.Title()}
|
||||
|
||||
if outcome.Fail != nil {
|
||||
if outcome.Fail.When == "" {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
coll.push(result)
|
||||
continue
|
||||
}
|
||||
|
||||
isMatch, err := compareHostServicesConditionalToActual(outcome.Fail.When, services)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Fail.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
coll.push(result)
|
||||
}
|
||||
} else if outcome.Warn != nil {
|
||||
if outcome.Warn.When == "" {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
coll.push(result)
|
||||
continue
|
||||
}
|
||||
|
||||
isMatch, err := compareHostServicesConditionalToActual(outcome.Warn.When, services)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Warn.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
coll.push(result)
|
||||
}
|
||||
} else if outcome.Pass != nil {
|
||||
if outcome.Pass.When == "" {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
coll.push(result)
|
||||
continue
|
||||
}
|
||||
|
||||
isMatch, err := compareHostServicesConditionalToActual(outcome.Pass.When, services)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Pass.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
coll.push(result)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return coll.get(a.Title()), nil
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// <service> <op> <state>
|
||||
@@ -187,3 +117,13 @@ func isServiceMatch(serviceName string, matchName string) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostServices) CheckCondition(when string, data []byte) (bool, error) {
|
||||
|
||||
var services []collect.ServiceInfo
|
||||
if err := json.Unmarshal(data, &services); err != nil {
|
||||
return false, fmt.Errorf("failed to unmarshal data into ServiceInfo: %v", err)
|
||||
}
|
||||
|
||||
return compareHostServicesConditionalToActual(when, services)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
@@ -24,61 +24,38 @@ func (a *AnalyzeHostSubnetAvailable) IsExcluded() (bool, error) {
|
||||
func (a *AnalyzeHostSubnetAvailable) Analyze(
|
||||
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
|
||||
) ([]*AnalyzeResult, error) {
|
||||
hostAnalyzer := a.hostAnalyzer
|
||||
|
||||
name := filepath.Join("host-collectors/subnetAvailable", "result.json")
|
||||
if hostAnalyzer.CollectorName != "" {
|
||||
name = filepath.Join("host-collectors/subnetAvailable", hostAnalyzer.CollectorName+".json")
|
||||
collectorName := a.hostAnalyzer.CollectorName
|
||||
if collectorName == "" {
|
||||
collectorName = "result"
|
||||
}
|
||||
contents, err := getCollectedFileContents(name)
|
||||
|
||||
const nodeBaseDir = "host-collectors/subnetAvailable"
|
||||
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 nil, errors.Wrap(err, "failed to get collected file")
|
||||
return []*AnalyzeResult{{Title: a.Title()}}, err
|
||||
}
|
||||
|
||||
isSubnetAvailable := &collect.SubnetAvailableResult{}
|
||||
if err := json.Unmarshal(contents, isSubnetAvailable); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal subnetAvailable result")
|
||||
results, err := analyzeHostCollectorResults(collectedContents, a.hostAnalyzer.Outcomes, a.CheckCondition, a.Title())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to analyze HTTP Load Balancer")
|
||||
}
|
||||
|
||||
result := &AnalyzeResult{
|
||||
Title: a.Title(),
|
||||
}
|
||||
|
||||
for _, outcome := range hostAnalyzer.Outcomes {
|
||||
if outcome.Fail != nil {
|
||||
if outcome.Fail.When == "" {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
if string(isSubnetAvailable.Status) == outcome.Fail.When {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
return []*AnalyzeResult{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 []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
if string(isSubnetAvailable.Status) == outcome.Pass.When {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
return results, nil
|
||||
}
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
}
|
||||
func (a *AnalyzeHostSubnetAvailable) CheckCondition(when string, data []byte) (bool, error) {
|
||||
isSubnetAvailable := &collect.SubnetAvailableResult{}
|
||||
if err := json.Unmarshal(data, isSubnetAvailable); err != nil {
|
||||
return false, errors.Wrap(err, "failed to unmarshal subnetAvailable result")
|
||||
}
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
return string(isSubnetAvailable.Status) == when, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAnalyzeSubnetAvailable(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
info *collect.SubnetAvailableResult
|
||||
hostAnalyzer *troubleshootv1beta2.SubnetAvailableAnalyze
|
||||
result []*AnalyzeResult
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
name: "subnet available",
|
||||
info: &collect.SubnetAvailableResult{
|
||||
CIDRRangeAlloc: "10.0.0.0/8",
|
||||
DesiredCIDR: 22,
|
||||
Status: collect.SubnetStatusAvailable,
|
||||
},
|
||||
hostAnalyzer: &troubleshootv1beta2.SubnetAvailableAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "a-subnet-is-available",
|
||||
Message: "available /22 subnet found",
|
||||
},
|
||||
},
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "no-subnet-available",
|
||||
Message: "failed to find available subnet",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
result: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Subnet Available",
|
||||
IsPass: true,
|
||||
Message: "available /22 subnet found",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "subnet not available",
|
||||
info: &collect.SubnetAvailableResult{
|
||||
CIDRRangeAlloc: "10.0.0.0/8",
|
||||
DesiredCIDR: 22,
|
||||
Status: collect.SubnetStatusNoneAvailable,
|
||||
},
|
||||
hostAnalyzer: &troubleshootv1beta2.SubnetAvailableAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "a-subnet-is-available",
|
||||
Message: "available /22 subnet found",
|
||||
},
|
||||
},
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "no-subnet-available",
|
||||
Message: "failed to find available subnet",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
result: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Subnet Available",
|
||||
IsFail: true,
|
||||
Message: "failed to find available subnet",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
b, err := json.Marshal(test.info)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
getCollectedFileContents := func(filename string) ([]byte, error) {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
result, err := (&AnalyzeHostSubnetAvailable{test.hostAnalyzer}).Analyze(getCollectedFileContents, nil)
|
||||
if test.expectedErr {
|
||||
req.Error(err)
|
||||
} else {
|
||||
req.NoError(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, test.result, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
)
|
||||
|
||||
// Ensure `AnalyzeHostSysctl` implements `HostAnalyzer` interface at compile time.
|
||||
var _ HostAnalyzer = (*AnalyzeHostSysctl)(nil)
|
||||
|
||||
type AnalyzeHostSysctl struct {
|
||||
hostAnalyzer *troubleshootv1beta2.HostSysctlAnalyze
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostSysctl) Title() string {
|
||||
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "Sysctl")
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostSysctl) IsExcluded() (bool, error) {
|
||||
return isExcluded(a.hostAnalyzer.Exclude)
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostSysctl) Analyze(
|
||||
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
|
||||
) ([]*AnalyzeResult, error) {
|
||||
result := AnalyzeResult{Title: a.Title()}
|
||||
|
||||
// Use the generic function to collect both local and remote data
|
||||
collectedContents, err := retrieveCollectedContents(
|
||||
getCollectedFileContents,
|
||||
collect.HostSysctlPath, // Local path
|
||||
collect.NodeInfoBaseDir, // Remote base directory
|
||||
collect.HostSysctlFileName, // Remote file name
|
||||
)
|
||||
if err != nil {
|
||||
return []*AnalyzeResult{&result}, err
|
||||
}
|
||||
|
||||
results, err := analyzeHostCollectorResults(collectedContents, a.hostAnalyzer.Outcomes, a.CheckCondition, a.Title())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to analyze sysctl output")
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// checkCondition checks the condition of the when clause
|
||||
func (a *AnalyzeHostSysctl) CheckCondition(when string, data []byte) (bool, error) {
|
||||
|
||||
sysctl := map[string]string{}
|
||||
if err := json.Unmarshal(data, &sysctl); err != nil {
|
||||
return false, errors.Wrap(err, "failed to unmarshal data")
|
||||
}
|
||||
|
||||
// <1:key> <2:operator> <3:value>
|
||||
matches := sysctlWhenRX.FindStringSubmatch(when)
|
||||
if len(matches) < 4 {
|
||||
return false, fmt.Errorf("expected 3 parts in when %q", when)
|
||||
}
|
||||
|
||||
param := matches[1]
|
||||
expected := matches[3]
|
||||
opString := matches[2]
|
||||
operator, err := ParseComparisonOperator(opString)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, fmt.Sprintf("failed to parse comparison operator %q", opString))
|
||||
}
|
||||
|
||||
if _, ok := sysctl[param]; !ok {
|
||||
return false, fmt.Errorf("kernel parameter %q does not exist on collected sysctl output", param)
|
||||
}
|
||||
|
||||
switch operator {
|
||||
case Equal:
|
||||
return expected == sysctl[param], nil
|
||||
}
|
||||
|
||||
// operator used is an inequality operator, the only valid inputs should be ints, if not we'll error out
|
||||
value, err := strconv.Atoi(sysctl[param])
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("collected sysctl param %q has value %q, cannot be used with provided operator %q", param, sysctl[param], opString)
|
||||
}
|
||||
expectedInt, err := strconv.Atoi(expected)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("expected value for sysctl param %q has value %q, cannot be used with provided operator %q", param, expected, opString)
|
||||
}
|
||||
|
||||
switch operator {
|
||||
case LessThan:
|
||||
return value < expectedInt, nil
|
||||
case LessThanOrEqual:
|
||||
return value <= expectedInt, nil
|
||||
case GreaterThan:
|
||||
return value > expectedInt, nil
|
||||
case GreaterThanOrEqual:
|
||||
return value >= expectedInt, nil
|
||||
default:
|
||||
return false, fmt.Errorf("unsupported operator %q", opString)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAnalyzeHostSysctlCheckCondition(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
conditional string
|
||||
collected string
|
||||
expected bool
|
||||
expectErr string
|
||||
}{
|
||||
{
|
||||
name: "errors out if we can't unmarshal data",
|
||||
conditional: "net.ipv4.conf.all.arp_filter = 0",
|
||||
collected: `{not JSON}`,
|
||||
expected: false,
|
||||
expectErr: "failed to unmarshal data",
|
||||
},
|
||||
{
|
||||
name: "errors out if the matched conditional is missing elements",
|
||||
conditional: "net.ipv4.conf.all.arp_filter =",
|
||||
collected: `{}`,
|
||||
expected: false,
|
||||
expectErr: `expected 3 parts in when "net.ipv4.conf.all.arp_filter ="`,
|
||||
},
|
||||
{
|
||||
name: "errors out if the parameter in the condition was not collected",
|
||||
conditional: "net.ipv4.conf.all.arp_filter = 0",
|
||||
collected: `{"net.ipv4.conf.all.arp_ignore": "0"}`,
|
||||
expected: false,
|
||||
expectErr: `"net.ipv4.conf.all.arp_filter" does not exist on collected sysctl`,
|
||||
},
|
||||
{
|
||||
name: "errors out if the collected parameter does not support inequalities",
|
||||
conditional: "net.ipv4.tcp_available_congestion_control > 0",
|
||||
collected: `{"net.ipv4.tcp_available_congestion_control": "reno cubic"}`,
|
||||
expected: false,
|
||||
expectErr: `has value "reno cubic", cannot be used with provided operator ">"`,
|
||||
},
|
||||
{
|
||||
name: "errors out if the provided value for the conditional does not support inequalities",
|
||||
conditional: "net.ipv4.conf.all.arp_filter > broken",
|
||||
collected: `{"net.ipv4.conf.all.arp_filter": "0"}`,
|
||||
expected: false,
|
||||
expectErr: `has value "broken", cannot be used with provided operator ">"`,
|
||||
},
|
||||
{
|
||||
name: "errors out if the provided operator is unsupported",
|
||||
conditional: "net.ipv4.conf.all.arp_filter <== 0",
|
||||
collected: `{"net.ipv4.conf.all.arp_filter": "0"}`,
|
||||
expected: false,
|
||||
expectErr: `failed to parse comparison operator "<=="`,
|
||||
},
|
||||
{
|
||||
name: "equals with ints",
|
||||
conditional: "net.ipv4.conf.all.arp_filter = 0",
|
||||
collected: `{"net.ipv4.conf.all.arp_filter": "0"}`,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "equals with different data types",
|
||||
conditional: "net.ipv4.conf.all.arp_filter = will be false",
|
||||
collected: `{"net.ipv4.conf.all.arp_filter": "0"}`,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "equals with strings",
|
||||
conditional: "net.ipv4.tcp_available_congestion_control = reno cubic",
|
||||
collected: `{"net.ipv4.tcp_available_congestion_control": "reno cubic"}`,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "triple equals works",
|
||||
conditional: "net.ipv4.tcp_available_congestion_control === reno cubic",
|
||||
collected: `{"net.ipv4.tcp_available_congestion_control": "reno cubic"}`,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "double equals works",
|
||||
conditional: "net.ipv4.tcp_available_congestion_control == reno cubic",
|
||||
collected: `{"net.ipv4.tcp_available_congestion_control": "reno cubic"}`,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "lower than succeeds",
|
||||
conditional: "net.ipv4.conf.default.arp_ignore < 1",
|
||||
collected: `{"net.ipv4.conf.default.arp_ignore": "0"}`,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "lower than fails",
|
||||
conditional: "net.ipv4.conf.default.arp_ignore < 1",
|
||||
collected: `{"net.ipv4.conf.default.arp_ignore": "1"}`,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "lower than or equals succeeds",
|
||||
conditional: "net.ipv4.conf.default.arp_ignore <= 1",
|
||||
collected: `{"net.ipv4.conf.default.arp_ignore": "1"}`,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "lower than or equals fails",
|
||||
conditional: "net.ipv4.conf.default.arp_ignore <= 1",
|
||||
collected: `{"net.ipv4.conf.default.arp_ignore": "2"}`,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "higher than succeeds",
|
||||
conditional: "net.ipv4.conf.default.arp_ignore > 1",
|
||||
collected: `{"net.ipv4.conf.default.arp_ignore": "2"}`,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "higher than fails",
|
||||
conditional: "net.ipv4.conf.default.arp_ignore > 1",
|
||||
collected: `{"net.ipv4.conf.default.arp_ignore": "1"}`,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "higher than or equals succeeds",
|
||||
conditional: "net.ipv4.conf.default.arp_ignore >= 1",
|
||||
collected: `{"net.ipv4.conf.default.arp_ignore": "1"}`,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "higher than or equals fails",
|
||||
conditional: "net.ipv4.conf.default.arp_ignore >= 2",
|
||||
collected: `{"net.ipv4.conf.default.arp_ignore": "1"}`,
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
analyzeHostSysctl := AnalyzeHostSysctl{}
|
||||
|
||||
// JSON encoded sysctl collected output
|
||||
data := []byte(test.collected)
|
||||
|
||||
// Call the CheckCondition method
|
||||
result, err := analyzeHostSysctl.CheckCondition(test.conditional, data)
|
||||
if test.expectErr != "" {
|
||||
req.ErrorContains(err, test.expectErr)
|
||||
} else {
|
||||
req.NoError(err)
|
||||
}
|
||||
assert.Equal(t, test.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyzeHostSysctl(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hostAnalyzer *troubleshootv1beta2.HostSysctlAnalyze
|
||||
getCollectedFileContents func(string) ([]byte, error)
|
||||
expectedResults []*AnalyzeResult
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "Pass on successful condition (local)",
|
||||
hostAnalyzer: &troubleshootv1beta2.HostSysctlAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "net.ipv4.conf.default.arp_ignore >= 1",
|
||||
Message: "ARP ignore is enabled",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
// Simulate local sysctl content retrieval
|
||||
if path == collect.HostSysctlPath {
|
||||
|
||||
data := map[string]string{
|
||||
"net.ipv4.conf.default.arp_ignore": "2",
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
expectedResults: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Sysctl",
|
||||
IsPass: true,
|
||||
Message: "ARP ignore is enabled",
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "Fail on condition (remote node)",
|
||||
hostAnalyzer: &troubleshootv1beta2.HostSysctlAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "net.ipv4.conf.default.arp_filter = 0",
|
||||
Message: "ARP filter is disabled, please enable it via `sysctl net.ipv4.conf.default.arp_filter=1`",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
// Simulate remote node list and sysctl content retrieval
|
||||
if path == constants.NODE_LIST_FILE {
|
||||
nodeNames := nodeNames{Nodes: []string{"node1"}}
|
||||
return json.Marshal(nodeNames)
|
||||
}
|
||||
if path == fmt.Sprintf("%s/node1/%s", collect.NodeInfoBaseDir, collect.HostSysctlFileName) {
|
||||
data := map[string]string{
|
||||
"net.ipv4.conf.default.arp_filter": "0",
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
expectedResults: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Sysctl - Node node1",
|
||||
IsFail: true,
|
||||
Message: "ARP filter is disabled, please enable it via `sysctl net.ipv4.conf.default.arp_filter=1`",
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "Warn on condition(remote node)",
|
||||
hostAnalyzer: &troubleshootv1beta2.HostSysctlAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "net.ipv4.tcp_available_congestion_control = reno cubic",
|
||||
Message: "Unexpected TCP congestion control algorithm available",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
// Simulate remote node list and sysctl content retrieval
|
||||
if path == constants.NODE_LIST_FILE {
|
||||
nodeNames := nodeNames{Nodes: []string{"node1"}}
|
||||
return json.Marshal(nodeNames)
|
||||
}
|
||||
if path == fmt.Sprintf("%s/node1/%s", collect.NodeInfoBaseDir, collect.HostSysctlFileName) {
|
||||
data := map[string]string{
|
||||
"net.ipv4.tcp_available_congestion_control": "reno cubic",
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
expectedResults: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Sysctl - Node node1",
|
||||
IsWarn: true,
|
||||
Message: "Unexpected TCP congestion control algorithm available",
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "Return error if collection fails",
|
||||
hostAnalyzer: &troubleshootv1beta2.HostSysctlAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "net.ipv4.tcp_available_congestion_control = reno cubic",
|
||||
Message: "Unexpected TCP congestion control algorithm available",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getCollectedFileContents: func(path string) ([]byte, error) {
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
expectedResults: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Sysctl",
|
||||
},
|
||||
},
|
||||
expectedError: "file not found",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
analyzeHostSysctl := AnalyzeHostSysctl{
|
||||
hostAnalyzer: test.hostAnalyzer,
|
||||
}
|
||||
|
||||
results, err := analyzeHostSysctl.Analyze(test.getCollectedFileContents, nil)
|
||||
|
||||
if test.expectedError != "" {
|
||||
req.ErrorContains(err, test.expectedError)
|
||||
} else {
|
||||
req.NoError(err)
|
||||
}
|
||||
req.Equal(test.expectedResults, results)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -32,93 +32,40 @@ func (a *AnalyzeHostSystemPackages) Analyze(
|
||||
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
|
||||
) ([]*AnalyzeResult, error) {
|
||||
hostAnalyzer := a.hostAnalyzer
|
||||
|
||||
packagesFileName := "host-collectors/system/packages.json"
|
||||
if a.hostAnalyzer.CollectorName != "" {
|
||||
packagesFileName = fmt.Sprintf("host-collectors/system/%s-packages.json", a.hostAnalyzer.CollectorName)
|
||||
collectorName := hostAnalyzer.CollectorName
|
||||
if collectorName == "" {
|
||||
collectorName = "packages"
|
||||
}
|
||||
|
||||
contents, err := getCollectedFileContents(packagesFileName)
|
||||
localPath := fmt.Sprintf("%s/%s-packages.json", collect.NodeInfoBaseDir, collectorName)
|
||||
packagesFileName := fmt.Sprintf("%s-packages.json", collectorName)
|
||||
|
||||
collectedContents, err := retrieveCollectedContents(
|
||||
getCollectedFileContents,
|
||||
localPath,
|
||||
collect.NodeInfoBaseDir,
|
||||
packagesFileName,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get collected file")
|
||||
return []*AnalyzeResult{{Title: a.Title()}}, err
|
||||
}
|
||||
|
||||
var info collect.SystemPackagesInfo
|
||||
if err := json.Unmarshal(contents, &info); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal system packages info")
|
||||
}
|
||||
|
||||
allResults := []*AnalyzeResult{}
|
||||
|
||||
for _, pkg := range info.Packages {
|
||||
templateMap := getSystemPackageTemplateMap(pkg, info.OS, info.OSVersion)
|
||||
|
||||
for _, outcome := range hostAnalyzer.Outcomes {
|
||||
r := AnalyzeResult{}
|
||||
when := ""
|
||||
|
||||
if outcome.Fail != nil {
|
||||
r.IsFail = true
|
||||
r.Message = outcome.Fail.Message
|
||||
r.URI = outcome.Fail.URI
|
||||
when = outcome.Fail.When
|
||||
} else if outcome.Warn != nil {
|
||||
r.IsWarn = true
|
||||
r.Message = outcome.Warn.Message
|
||||
r.URI = outcome.Warn.URI
|
||||
when = outcome.Warn.When
|
||||
} else if outcome.Pass != nil {
|
||||
r.IsPass = true
|
||||
r.Message = outcome.Pass.Message
|
||||
r.URI = outcome.Pass.URI
|
||||
when = outcome.Pass.When
|
||||
} else {
|
||||
println("error: found an empty outcome in a systemPackages analyzer") // don't stop
|
||||
continue
|
||||
}
|
||||
|
||||
match, err := compareSystemPackagesConditionalToActual(when, templateMap)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %s", when)
|
||||
}
|
||||
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
|
||||
tmpl := template.New("package")
|
||||
|
||||
// template the title
|
||||
titleTmpl, err := tmpl.Parse(a.Title())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create new title template")
|
||||
}
|
||||
var t bytes.Buffer
|
||||
err = titleTmpl.Execute(&t, templateMap)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to execute title template")
|
||||
}
|
||||
r.Title = t.String()
|
||||
|
||||
// template the message
|
||||
msgTmpl, err := tmpl.Parse(r.Message)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create new message template")
|
||||
}
|
||||
var m bytes.Buffer
|
||||
err = msgTmpl.Execute(&m, templateMap)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to execute message template")
|
||||
}
|
||||
r.Message = m.String()
|
||||
|
||||
// add to results, break and check the next pod
|
||||
allResults = append(allResults, &r)
|
||||
break
|
||||
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.analyzeSingleNode(content, currentTitle)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to analyze system packages for %s", currentTitle)
|
||||
}
|
||||
if result != nil {
|
||||
results = append(results, result...)
|
||||
}
|
||||
}
|
||||
|
||||
return allResults, nil
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func getSystemPackageTemplateMap(pkg collect.SystemPackage, osName string, osVersion string) map[string]interface{} {
|
||||
@@ -231,3 +178,72 @@ func compareSystemPackagesConditionalToActual(conditional string, templateMap ma
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostSystemPackages) analyzeSingleNode(content collectedContent, currentTitle string) ([]*AnalyzeResult, error) {
|
||||
hostAnalyzer := a.hostAnalyzer
|
||||
var info collect.SystemPackagesInfo
|
||||
if err := json.Unmarshal(content.Data, &info); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal system packages info from %s", currentTitle)
|
||||
}
|
||||
|
||||
allResults := []*AnalyzeResult{}
|
||||
|
||||
for _, pkg := range info.Packages {
|
||||
templateMap := getSystemPackageTemplateMap(pkg, info.OS, info.OSVersion)
|
||||
|
||||
for _, outcome := range hostAnalyzer.Outcomes {
|
||||
r := AnalyzeResult{}
|
||||
when := ""
|
||||
switch {
|
||||
case outcome.Fail != nil:
|
||||
r.IsFail = true
|
||||
r.Message = outcome.Fail.Message
|
||||
r.URI = outcome.Fail.URI
|
||||
when = outcome.Fail.When
|
||||
case outcome.Warn != nil:
|
||||
r.IsWarn = true
|
||||
r.Message = outcome.Warn.Message
|
||||
r.URI = outcome.Warn.URI
|
||||
when = outcome.Warn.When
|
||||
case outcome.Pass != nil:
|
||||
r.IsPass = true
|
||||
r.Message = outcome.Pass.Message
|
||||
r.URI = outcome.Pass.URI
|
||||
when = outcome.Pass.When
|
||||
default:
|
||||
return nil, errors.New("invalid outcome")
|
||||
}
|
||||
|
||||
match, err := compareSystemPackagesConditionalToActual(when, templateMap)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %s", when)
|
||||
}
|
||||
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
|
||||
tmpl := template.New("package")
|
||||
|
||||
r.Title = currentTitle
|
||||
|
||||
// template the message
|
||||
msgTmpl, err := tmpl.Parse(r.Message)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create new message template")
|
||||
}
|
||||
var m bytes.Buffer
|
||||
err = msgTmpl.Execute(&m, templateMap)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to execute message template")
|
||||
}
|
||||
r.Message = m.String()
|
||||
|
||||
// add to results, break and check the next pod
|
||||
allResults = append(allResults, &r)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return allResults, nil
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
@@ -24,84 +24,39 @@ func (a *AnalyzeHostTCPConnect) IsExcluded() (bool, error) {
|
||||
func (a *AnalyzeHostTCPConnect) Analyze(
|
||||
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
|
||||
) ([]*AnalyzeResult, error) {
|
||||
hostAnalyzer := a.hostAnalyzer
|
||||
|
||||
collectorName := hostAnalyzer.CollectorName
|
||||
collectorName := a.hostAnalyzer.CollectorName
|
||||
if collectorName == "" {
|
||||
collectorName = "connect"
|
||||
}
|
||||
fullPath := filepath.Join("host-collectors/connect", collectorName+".json")
|
||||
|
||||
collected, err := getCollectedFileContents(fullPath)
|
||||
const nodeBaseDir = "host-collectors/connect"
|
||||
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 nil, errors.Wrapf(err, "failed to read collected file name: %s", fullPath)
|
||||
}
|
||||
actual := collect.NetworkStatusResult{}
|
||||
if err := json.Unmarshal(collected, &actual); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal collected")
|
||||
return []*AnalyzeResult{{Title: a.Title()}}, err
|
||||
}
|
||||
|
||||
var coll resultCollector
|
||||
result := &AnalyzeResult{Title: a.Title()}
|
||||
|
||||
for _, outcome := range hostAnalyzer.Outcomes {
|
||||
|
||||
if outcome.Fail != nil {
|
||||
if outcome.Fail.When == "" {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
coll.push(result)
|
||||
break
|
||||
}
|
||||
|
||||
if string(actual.Status) == outcome.Fail.When {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
coll.push(result)
|
||||
break
|
||||
}
|
||||
} else if outcome.Warn != nil {
|
||||
if outcome.Warn.When == "" {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
coll.push(result)
|
||||
break
|
||||
}
|
||||
|
||||
if string(actual.Status) == outcome.Warn.When {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
coll.push(result)
|
||||
break
|
||||
}
|
||||
} else if outcome.Pass != nil {
|
||||
if outcome.Pass.When == "" {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
coll.push(result)
|
||||
break
|
||||
}
|
||||
|
||||
if string(actual.Status) == outcome.Pass.When {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
coll.push(result)
|
||||
break
|
||||
}
|
||||
}
|
||||
results, err := analyzeHostCollectorResults(collectedContents, a.hostAnalyzer.Outcomes, a.CheckCondition, a.Title())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to analyze tcp connect")
|
||||
}
|
||||
|
||||
return coll.get(a.Title()), nil
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostTCPConnect) CheckCondition(when string, data []byte) (bool, error) {
|
||||
|
||||
var tcpConnect collect.NetworkStatusResult
|
||||
if err := json.Unmarshal(data, &tcpConnect); err != nil {
|
||||
return false, fmt.Errorf("failed to unmarshal data into NetworkStatusResult: %v", err)
|
||||
}
|
||||
|
||||
return string(tcpConnect.Status) == when, nil
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package analyzer
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
@@ -25,85 +24,45 @@ func (a *AnalyzeHostTCPLoadBalancer) IsExcluded() (bool, error) {
|
||||
func (a *AnalyzeHostTCPLoadBalancer) Analyze(
|
||||
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
|
||||
) ([]*AnalyzeResult, error) {
|
||||
hostAnalyzer := a.hostAnalyzer
|
||||
|
||||
collectorName := hostAnalyzer.CollectorName
|
||||
collectorName := a.hostAnalyzer.CollectorName
|
||||
if collectorName == "" {
|
||||
collectorName = "tcpLoadBalancer"
|
||||
}
|
||||
|
||||
fullPath := path.Join("host-collectors/tcpLoadBalancer", fmt.Sprintf("%s.json", collectorName))
|
||||
const nodeBaseDir = "host-collectors/tcpLoadBalancer"
|
||||
localPath := fmt.Sprintf("%s/%s.json", nodeBaseDir, collectorName)
|
||||
fileName := fmt.Sprintf("%s.json", collectorName)
|
||||
|
||||
collected, err := getCollectedFileContents(fullPath)
|
||||
collectedContents, err := retrieveCollectedContents(
|
||||
getCollectedFileContents,
|
||||
localPath,
|
||||
nodeBaseDir,
|
||||
fileName,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to read collected file name: %s", fullPath)
|
||||
}
|
||||
actual := collect.NetworkStatusResult{}
|
||||
if err := json.Unmarshal(collected, &actual); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal collected")
|
||||
return []*AnalyzeResult{{Title: a.Title()}}, err
|
||||
}
|
||||
|
||||
var coll resultCollector
|
||||
result := &AnalyzeResult{Title: a.Title()}
|
||||
|
||||
for _, outcome := range hostAnalyzer.Outcomes {
|
||||
|
||||
if outcome.Fail != nil {
|
||||
if outcome.Fail.When == "" {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
coll.push(result)
|
||||
break
|
||||
}
|
||||
|
||||
if string(actual.Status) == outcome.Fail.When {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
coll.push(result)
|
||||
break
|
||||
}
|
||||
} else if outcome.Warn != nil {
|
||||
if outcome.Warn.When == "" {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
coll.push(result)
|
||||
break
|
||||
}
|
||||
|
||||
if string(actual.Status) == outcome.Warn.When {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
coll.push(result)
|
||||
break
|
||||
}
|
||||
} else if outcome.Pass != nil {
|
||||
if outcome.Pass.When == "" {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
coll.push(result)
|
||||
break
|
||||
}
|
||||
|
||||
if string(actual.Status) == outcome.Pass.When {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
coll.push(result)
|
||||
break
|
||||
}
|
||||
}
|
||||
results, err := analyzeHostCollectorResults(collectedContents, a.hostAnalyzer.Outcomes, a.CheckCondition, a.Title())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to analyze TCP Load Balancer")
|
||||
}
|
||||
|
||||
return coll.get(a.Title()), nil
|
||||
return results, nil
|
||||
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostTCPLoadBalancer) CheckCondition(when string, data []byte) (bool, error) {
|
||||
|
||||
var tcpLoadBalancer collect.NetworkStatusResult
|
||||
if err := json.Unmarshal(data, &tcpLoadBalancer); err != nil {
|
||||
return false, fmt.Errorf("failed to unmarshal data into NetworkStatusResult: %v", err)
|
||||
}
|
||||
|
||||
if string(tcpLoadBalancer.Status) == when {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package analyzer
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
@@ -27,76 +26,40 @@ func (a *AnalyzeHostTCPPortStatus) IsExcluded() (bool, error) {
|
||||
func (a *AnalyzeHostTCPPortStatus) Analyze(
|
||||
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
|
||||
) ([]*AnalyzeResult, error) {
|
||||
hostAnalyzer := a.hostAnalyzer
|
||||
|
||||
fullPath := path.Join("host-collectors/tcpPortStatus", "tcpPortStatus.json")
|
||||
if hostAnalyzer.CollectorName != "" {
|
||||
fullPath = path.Join("host-collectors/tcpPortStatus", fmt.Sprintf("%s.json", hostAnalyzer.CollectorName))
|
||||
collectorName := a.hostAnalyzer.CollectorName
|
||||
if collectorName == "" {
|
||||
collectorName = "tcpPortStatus"
|
||||
}
|
||||
|
||||
collected, err := getCollectedFileContents(fullPath)
|
||||
const nodeBaseDir = "host-collectors/tcpPortStatus"
|
||||
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 nil, errors.Wrapf(err, "failed to read collected file name: %s", fullPath)
|
||||
}
|
||||
actual := collect.NetworkStatusResult{}
|
||||
if err := json.Unmarshal(collected, &actual); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal collected")
|
||||
return []*AnalyzeResult{{Title: a.Title()}}, err
|
||||
}
|
||||
|
||||
result := &AnalyzeResult{Title: a.Title()}
|
||||
|
||||
for _, outcome := range hostAnalyzer.Outcomes {
|
||||
|
||||
if outcome.Fail != nil {
|
||||
if outcome.Fail.When == "" {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
if string(actual.Status) == outcome.Fail.When {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
return []*AnalyzeResult{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 []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
if string(actual.Status) == outcome.Warn.When {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
return []*AnalyzeResult{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 []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
if string(actual.Status) == outcome.Pass.When {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
}
|
||||
results, err := analyzeHostCollectorResults(collectedContents, a.hostAnalyzer.Outcomes, a.CheckCondition, a.Title())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to analyze tcp port status")
|
||||
}
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
return results, nil
|
||||
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostTCPPortStatus) CheckCondition(when string, data []byte) (bool, error) {
|
||||
|
||||
var tcpPortStatus collect.NetworkStatusResult
|
||||
if err := json.Unmarshal(data, &tcpPortStatus); err != nil {
|
||||
return false, fmt.Errorf("failed to unmarshal data into NetworkStatusResult: %v", err)
|
||||
}
|
||||
|
||||
return string(tcpPortStatus.Status) == when, nil
|
||||
}
|
||||
|
||||
+22
-82
@@ -32,95 +32,25 @@ func (a *AnalyzeHostTime) IsExcluded() (bool, error) {
|
||||
func (a *AnalyzeHostTime) Analyze(
|
||||
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
|
||||
) ([]*AnalyzeResult, error) {
|
||||
hostAnalyzer := a.hostAnalyzer
|
||||
result := AnalyzeResult{Title: a.Title()}
|
||||
|
||||
contents, err := getCollectedFileContents(collect.HostTimePath)
|
||||
collectedContents, err := retrieveCollectedContents(
|
||||
getCollectedFileContents,
|
||||
collect.HostTimePath,
|
||||
collect.NodeInfoBaseDir,
|
||||
collect.HostTimeFileName,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get collected file")
|
||||
return []*AnalyzeResult{&result}, err
|
||||
}
|
||||
|
||||
timeInfo := collect.TimeInfo{}
|
||||
if err := json.Unmarshal(contents, &timeInfo); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal time info")
|
||||
results, err := analyzeHostCollectorResults(collectedContents, a.hostAnalyzer.Outcomes, a.CheckCondition, a.Title())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to analyze OS version")
|
||||
}
|
||||
|
||||
var coll resultCollector
|
||||
return results, nil
|
||||
|
||||
for _, outcome := range hostAnalyzer.Outcomes {
|
||||
result := &AnalyzeResult{Title: a.Title()}
|
||||
|
||||
if outcome.Fail != nil {
|
||||
if outcome.Fail.When == "" {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
coll.push(result)
|
||||
continue
|
||||
}
|
||||
|
||||
isMatch, err := compareHostTimeStatusToActual(outcome.Fail.When, timeInfo)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Fail.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
coll.push(result)
|
||||
}
|
||||
} else if outcome.Warn != nil {
|
||||
if outcome.Warn.When == "" {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
coll.push(result)
|
||||
continue
|
||||
}
|
||||
|
||||
isMatch, err := compareHostTimeStatusToActual(outcome.Warn.When, timeInfo)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Warn.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
coll.push(result)
|
||||
}
|
||||
|
||||
} else if outcome.Pass != nil {
|
||||
if outcome.Pass.When == "" {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
coll.push(result)
|
||||
continue
|
||||
}
|
||||
|
||||
isMatch, err := compareHostTimeStatusToActual(outcome.Pass.When, timeInfo)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Pass.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
coll.push(result)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return coll.get(a.Title()), nil
|
||||
}
|
||||
|
||||
func compareHostTimeStatusToActual(status string, timeInfo collect.TimeInfo) (res bool, err error) {
|
||||
@@ -160,3 +90,13 @@ func compareHostTimeStatusToActual(status string, timeInfo collect.TimeInfo) (re
|
||||
|
||||
return false, fmt.Errorf("Unknown keyword: %s", parts[0])
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostTime) CheckCondition(when string, data []byte) (bool, error) {
|
||||
|
||||
var timeInfo collect.TimeInfo
|
||||
if err := json.Unmarshal(data, &timeInfo); err != nil {
|
||||
return false, fmt.Errorf("failed to unmarshal data into TimeInfo: %v", err)
|
||||
}
|
||||
|
||||
return compareHostTimeStatusToActual(when, timeInfo)
|
||||
}
|
||||
|
||||
@@ -222,54 +222,6 @@ func TestAnalyzeHostTime(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple issues",
|
||||
timeInfo: &collect.TimeInfo{
|
||||
Timezone: "PST",
|
||||
NTPSynchronized: false,
|
||||
NTPActive: true,
|
||||
},
|
||||
hostAnalyzer: &troubleshootv1beta2.TimeAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "timezone != UTC",
|
||||
Message: "timezone is not set to UTC",
|
||||
},
|
||||
},
|
||||
{
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "ntp == unsynchronized+active",
|
||||
Message: "System clock not yet synchronized",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "timezone == UTC",
|
||||
Message: "Timezone is set to UTC",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "ntp == synchronized+active",
|
||||
Message: "System clock is synchronized",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
result: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Time",
|
||||
IsFail: true,
|
||||
Message: "timezone is not set to UTC",
|
||||
},
|
||||
{
|
||||
Title: "Time",
|
||||
IsWarn: true,
|
||||
Message: "System clock not yet synchronized",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
@@ -3,7 +3,6 @@ package analyzer
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
@@ -27,76 +26,40 @@ func (a *AnalyzeHostUDPPortStatus) IsExcluded() (bool, error) {
|
||||
func (a *AnalyzeHostUDPPortStatus) Analyze(
|
||||
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
|
||||
) ([]*AnalyzeResult, error) {
|
||||
hostAnalyzer := a.hostAnalyzer
|
||||
|
||||
fullPath := path.Join("host-collectors/udpPortStatus", "udpPortStatus.json")
|
||||
if hostAnalyzer.CollectorName != "" {
|
||||
fullPath = path.Join("host-collectors/udpPortStatus", fmt.Sprintf("%s.json", hostAnalyzer.CollectorName))
|
||||
collectorName := a.hostAnalyzer.CollectorName
|
||||
if collectorName == "" {
|
||||
collectorName = "udpPortStatus"
|
||||
}
|
||||
|
||||
collected, err := getCollectedFileContents(fullPath)
|
||||
const nodeBaseDir = "host-collectors/udpPortStatus"
|
||||
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 nil, errors.Wrapf(err, "failed to read collected file name: %s", fullPath)
|
||||
}
|
||||
actual := collect.NetworkStatusResult{}
|
||||
if err := json.Unmarshal(collected, &actual); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal collected")
|
||||
return []*AnalyzeResult{{Title: a.Title()}}, err
|
||||
}
|
||||
|
||||
result := &AnalyzeResult{Title: a.Title()}
|
||||
|
||||
for _, outcome := range hostAnalyzer.Outcomes {
|
||||
|
||||
if outcome.Fail != nil {
|
||||
if outcome.Fail.When == "" {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
if string(actual.Status) == outcome.Fail.When {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
return []*AnalyzeResult{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 []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
if string(actual.Status) == outcome.Warn.When {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
return []*AnalyzeResult{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 []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
if string(actual.Status) == outcome.Pass.When {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
}
|
||||
results, err := analyzeHostCollectorResults(collectedContents, a.hostAnalyzer.Outcomes, a.CheckCondition, a.Title())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to analyze udp port status")
|
||||
}
|
||||
|
||||
return []*AnalyzeResult{result}, nil
|
||||
return results, nil
|
||||
|
||||
}
|
||||
|
||||
func (a *AnalyzeHostUDPPortStatus) CheckCondition(when string, data []byte) (bool, error) {
|
||||
|
||||
var udpPort collect.NetworkStatusResult
|
||||
if err := json.Unmarshal(data, &udpPort); err != nil {
|
||||
return false, fmt.Errorf("failed to unmarshal data into NetworkStatusResult: %v", err)
|
||||
}
|
||||
|
||||
return string(udpPort.Status) == when, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
)
|
||||
|
||||
type AnalyzeHTTPAnalyze struct {
|
||||
analyzer *troubleshootv1beta2.HTTPAnalyze
|
||||
}
|
||||
|
||||
func (a *AnalyzeHTTPAnalyze) Title() string {
|
||||
checkName := a.analyzer.CheckName
|
||||
if checkName == "" {
|
||||
checkName = a.analyzer.CollectorName
|
||||
}
|
||||
|
||||
return checkName
|
||||
}
|
||||
|
||||
func (a *AnalyzeHTTPAnalyze) IsExcluded() (bool, error) {
|
||||
return isExcluded(a.analyzer.Exclude)
|
||||
}
|
||||
|
||||
func (a *AnalyzeHTTPAnalyze) Analyze(getFile getCollectedFileContents, findFiles getChildCollectedFileContents) ([]*AnalyzeResult, error) {
|
||||
fileName := "result.json"
|
||||
if a.analyzer.CollectorName != "" {
|
||||
fileName = a.analyzer.CollectorName + ".json"
|
||||
}
|
||||
return analyzeHTTPResult(a.analyzer, fileName, getFile, a.Title())
|
||||
}
|
||||
@@ -21,9 +21,16 @@ type AnalyzeJsonCompare struct {
|
||||
}
|
||||
|
||||
func (a *AnalyzeJsonCompare) Title() string {
|
||||
title := a.analyzer.CheckName
|
||||
return jsonCompareTitle(a.analyzer)
|
||||
}
|
||||
|
||||
func jsonCompareTitle(analyser *troubleshootv1beta2.JsonCompare) string {
|
||||
title := analyser.CheckName
|
||||
if title == "" {
|
||||
title = a.analyzer.CollectorName
|
||||
title = analyser.CollectorName
|
||||
}
|
||||
if title == "" {
|
||||
title = "Json Compare"
|
||||
}
|
||||
|
||||
return title
|
||||
@@ -34,7 +41,7 @@ func (a *AnalyzeJsonCompare) IsExcluded() (bool, error) {
|
||||
}
|
||||
|
||||
func (a *AnalyzeJsonCompare) Analyze(getFile getCollectedFileContents, findFiles getChildCollectedFileContents) ([]*AnalyzeResult, error) {
|
||||
result, err := a.analyzeJsonCompare(a.analyzer, getFile)
|
||||
result, err := analyzeJsonCompare(a.analyzer, getFile, a.Title())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -42,7 +49,7 @@ func (a *AnalyzeJsonCompare) Analyze(getFile getCollectedFileContents, findFiles
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
func (a *AnalyzeJsonCompare) analyzeJsonCompare(analyzer *troubleshootv1beta2.JsonCompare, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
|
||||
func analyzeJsonCompare(analyzer *troubleshootv1beta2.JsonCompare, getCollectedFileContents func(string) ([]byte, error), title string) (*AnalyzeResult, error) {
|
||||
fullPath := filepath.Join(analyzer.CollectorName, analyzer.FileName)
|
||||
collected, err := getCollectedFileContents(fullPath)
|
||||
if err != nil {
|
||||
@@ -97,7 +104,7 @@ func (a *AnalyzeJsonCompare) analyzeJsonCompare(analyzer *troubleshootv1beta2.Js
|
||||
}
|
||||
|
||||
result := &AnalyzeResult{
|
||||
Title: a.Title(),
|
||||
Title: title,
|
||||
IconKey: "kubernetes_text_analyze",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
|
||||
}
|
||||
@@ -173,7 +180,7 @@ func (a *AnalyzeJsonCompare) analyzeJsonCompare(analyzer *troubleshootv1beta2.Js
|
||||
}
|
||||
|
||||
return &AnalyzeResult{
|
||||
Title: a.Title(),
|
||||
Title: title,
|
||||
IconKey: "kubernetes_text_analyze",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
|
||||
IsFail: true,
|
||||
|
||||
@@ -808,11 +808,7 @@ func Test_jsonCompare(t *testing.T) {
|
||||
return test.fileContents, nil
|
||||
}
|
||||
|
||||
a := AnalyzeJsonCompare{
|
||||
analyzer: &test.analyzer,
|
||||
}
|
||||
|
||||
actual, err := a.analyzeJsonCompare(&test.analyzer, getCollectedFileContents)
|
||||
actual, err := analyzeJsonCompare(&test.analyzer, getCollectedFileContents, test.analyzer.CollectorName)
|
||||
if !test.isError {
|
||||
req.NoError(err)
|
||||
req.Equal(test.expectResult, *actual)
|
||||
|
||||
@@ -17,22 +17,23 @@ import (
|
||||
)
|
||||
|
||||
var Filemap = map[string]string{
|
||||
"Deployment": constants.CLUSTER_RESOURCES_DEPLOYMENTS,
|
||||
"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,
|
||||
"deployment": constants.CLUSTER_RESOURCES_DEPLOYMENTS,
|
||||
"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,
|
||||
"Namespace": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_NAMESPACES),
|
||||
"PersistentVolume": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_PVS),
|
||||
"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),
|
||||
"node": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_NODES),
|
||||
"storageclass": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_STORAGE_CLASS),
|
||||
}
|
||||
|
||||
type AnalyzeClusterResource struct {
|
||||
@@ -66,6 +67,9 @@ func FindResource(kind string, clusterScoped bool, namespace string, name string
|
||||
|
||||
var datapath string
|
||||
|
||||
// lowercase the kind to avoid case sensitivity
|
||||
kind = strings.ToLower(kind)
|
||||
|
||||
resourceLocation, ok := Filemap[kind]
|
||||
|
||||
if !ok {
|
||||
|
||||
@@ -21,13 +21,13 @@ func Test_findResource(t *testing.T) {
|
||||
resourceExists: true,
|
||||
analyzer: troubleshootv1beta2.ClusterResource{
|
||||
CollectorName: "Check namespaced resource",
|
||||
Kind: "Deployment",
|
||||
Kind: "deployment",
|
||||
Namespace: "kube-system",
|
||||
Name: "coredns",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "check default fallthrough",
|
||||
name: "check default fallthrough with case insensitivity",
|
||||
resourceExists: true,
|
||||
analyzer: troubleshootv1beta2.ClusterResource{
|
||||
CollectorName: "Check namespaced resource",
|
||||
@@ -40,7 +40,7 @@ func Test_findResource(t *testing.T) {
|
||||
resourceExists: true,
|
||||
analyzer: troubleshootv1beta2.ClusterResource{
|
||||
CollectorName: "Check namespaced resource",
|
||||
Kind: "Node",
|
||||
Kind: "node",
|
||||
ClusterScoped: true,
|
||||
Name: "repldev-marc",
|
||||
},
|
||||
@@ -50,11 +50,21 @@ func Test_findResource(t *testing.T) {
|
||||
resourceExists: false,
|
||||
analyzer: troubleshootv1beta2.ClusterResource{
|
||||
CollectorName: "Check namespaced resource",
|
||||
Kind: "Node",
|
||||
Kind: "node",
|
||||
ClusterScoped: true,
|
||||
Name: "resource-does-not-exist",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "configmap does exist",
|
||||
resourceExists: true,
|
||||
analyzer: troubleshootv1beta2.ClusterResource{
|
||||
CollectorName: "Check namespaced resource",
|
||||
Kind: "configmap",
|
||||
Namespace: "kube-public",
|
||||
Name: "kube-root-ca.crt",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
+107
-29
@@ -8,22 +8,30 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/internal/util"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
)
|
||||
|
||||
type AnalyzeNodeResources struct {
|
||||
analyzer *troubleshootv1beta2.NodeResources
|
||||
}
|
||||
|
||||
type NodeResourceMsg struct {
|
||||
*troubleshootv1beta2.NodeResourceFilters
|
||||
NodeCount int
|
||||
}
|
||||
|
||||
func (a *AnalyzeNodeResources) Title() string {
|
||||
title := a.analyzer.CheckName
|
||||
if title == "" {
|
||||
title = "Node Resources"
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
@@ -41,6 +49,7 @@ func (a *AnalyzeNodeResources) Analyze(getFile getCollectedFileContents, findFil
|
||||
}
|
||||
|
||||
func (a *AnalyzeNodeResources) analyzeNodeResources(analyzer *troubleshootv1beta2.NodeResources, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
|
||||
|
||||
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")
|
||||
@@ -70,42 +79,55 @@ func (a *AnalyzeNodeResources) analyzeNodeResources(analyzer *troubleshootv1beta
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
|
||||
}
|
||||
|
||||
nodeMsg := NodeResourceMsg{
|
||||
analyzer.Filters, len(matchingNodes),
|
||||
}
|
||||
|
||||
for _, outcome := range analyzer.Outcomes {
|
||||
if outcome.Fail != nil {
|
||||
isWhenMatch, err := compareNodeResourceConditionalToActual(outcome.Fail.When, matchingNodes)
|
||||
isWhenMatch, err := compareNodeResourceConditionalToActual(outcome.Fail.When, matchingNodes, analyzer.Filters)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse when")
|
||||
}
|
||||
|
||||
if isWhenMatch {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.Message, err = util.RenderTemplate(outcome.Fail.Message, nodeMsg)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to render message template")
|
||||
}
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
return result, nil
|
||||
}
|
||||
} else if outcome.Warn != nil {
|
||||
isWhenMatch, err := compareNodeResourceConditionalToActual(outcome.Warn.When, matchingNodes)
|
||||
isWhenMatch, err := compareNodeResourceConditionalToActual(outcome.Warn.When, matchingNodes, analyzer.Filters)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse when")
|
||||
}
|
||||
|
||||
if isWhenMatch {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.Message, err = util.RenderTemplate(outcome.Warn.Message, nodeMsg)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to render message template")
|
||||
}
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
return result, nil
|
||||
}
|
||||
} else if outcome.Pass != nil {
|
||||
isWhenMatch, err := compareNodeResourceConditionalToActual(outcome.Pass.When, matchingNodes)
|
||||
isWhenMatch, err := compareNodeResourceConditionalToActual(outcome.Pass.When, matchingNodes, analyzer.Filters)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse when")
|
||||
}
|
||||
|
||||
if isWhenMatch {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.Message, err = util.RenderTemplate(outcome.Pass.Message, nodeMsg)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to render message template")
|
||||
}
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
return result, nil
|
||||
@@ -116,7 +138,7 @@ func (a *AnalyzeNodeResources) analyzeNodeResources(analyzer *troubleshootv1beta
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func compareNodeResourceConditionalToActual(conditional string, matchingNodes []corev1.Node) (res bool, err error) {
|
||||
func compareNodeResourceConditionalToActual(conditional string, matchingNodes []corev1.Node, filters *troubleshootv1beta2.NodeResourceFilters) (res bool, err error) {
|
||||
res = false
|
||||
err = nil
|
||||
|
||||
@@ -169,6 +191,11 @@ func compareNodeResourceConditionalToActual(conditional string, matchingNodes []
|
||||
|
||||
function := match[1]
|
||||
property := match[2]
|
||||
resourceName := ""
|
||||
|
||||
if filters != nil {
|
||||
resourceName = filters.ResourceName
|
||||
}
|
||||
|
||||
var actualValue interface{}
|
||||
|
||||
@@ -176,11 +203,11 @@ func compareNodeResourceConditionalToActual(conditional string, matchingNodes []
|
||||
case "count":
|
||||
actualValue = len(matchingNodes)
|
||||
case "min":
|
||||
actualValue = findMin(matchingNodes, property)
|
||||
actualValue = findMin(matchingNodes, property, resourceName)
|
||||
case "max":
|
||||
actualValue = findMax(matchingNodes, property)
|
||||
actualValue = findMax(matchingNodes, property, resourceName)
|
||||
case "sum":
|
||||
actualValue = findSum(matchingNodes, property)
|
||||
actualValue = findSum(matchingNodes, property, resourceName)
|
||||
case "nodeCondition":
|
||||
operatorChecker := regexp.MustCompile(`={1,3}`)
|
||||
if !operatorChecker.MatchString(operator) {
|
||||
@@ -290,7 +317,7 @@ func compareNodeResourceConditionalToActual(conditional string, matchingNodes []
|
||||
return
|
||||
}
|
||||
|
||||
func getQuantity(node corev1.Node, property string) *resource.Quantity {
|
||||
func getQuantity(node corev1.Node, property string, resourceName string) *resource.Quantity {
|
||||
switch property {
|
||||
case "cpuCapacity":
|
||||
return node.Status.Capacity.Cpu()
|
||||
@@ -308,15 +335,27 @@ func getQuantity(node corev1.Node, property string) *resource.Quantity {
|
||||
return node.Status.Capacity.StorageEphemeral()
|
||||
case "ephemeralStorageAllocatable":
|
||||
return node.Status.Allocatable.StorageEphemeral()
|
||||
case "resourceCapacity":
|
||||
capacity, ok := node.Status.Capacity[corev1.ResourceName(resourceName)]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &capacity
|
||||
case "resourceAllocatable":
|
||||
allocatable, ok := node.Status.Allocatable[corev1.ResourceName(resourceName)]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &allocatable
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findSum(nodes []corev1.Node, property string) *resource.Quantity {
|
||||
func findSum(nodes []corev1.Node, property string, resourceName string) *resource.Quantity {
|
||||
sum := resource.Quantity{}
|
||||
|
||||
for _, node := range nodes {
|
||||
if quant := getQuantity(node, property); quant != nil {
|
||||
if quant := getQuantity(node, property, resourceName); quant != nil {
|
||||
sum.Add(*quant)
|
||||
}
|
||||
}
|
||||
@@ -324,11 +363,11 @@ func findSum(nodes []corev1.Node, property string) *resource.Quantity {
|
||||
return &sum
|
||||
}
|
||||
|
||||
func findMin(nodes []corev1.Node, property string) *resource.Quantity {
|
||||
func findMin(nodes []corev1.Node, property string, resourceName string) *resource.Quantity {
|
||||
var min *resource.Quantity
|
||||
|
||||
for _, node := range nodes {
|
||||
if quant := getQuantity(node, property); quant != nil {
|
||||
if quant := getQuantity(node, property, resourceName); quant != nil {
|
||||
if min == nil {
|
||||
min = quant
|
||||
} else if quant.Cmp(*min) == -1 {
|
||||
@@ -340,11 +379,11 @@ func findMin(nodes []corev1.Node, property string) *resource.Quantity {
|
||||
return min
|
||||
}
|
||||
|
||||
func findMax(nodes []corev1.Node, property string) *resource.Quantity {
|
||||
func findMax(nodes []corev1.Node, property string, resourceName string) *resource.Quantity {
|
||||
var max *resource.Quantity
|
||||
|
||||
for _, node := range nodes {
|
||||
if quant := getQuantity(node, property); quant != nil {
|
||||
if quant := getQuantity(node, property, resourceName); quant != nil {
|
||||
if max == nil {
|
||||
max = quant
|
||||
} else if quant.Cmp(*max) == 1 {
|
||||
@@ -361,20 +400,59 @@ func nodeMatchesFilters(node corev1.Node, filters *troubleshootv1beta2.NodeResou
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// all filters must pass for this to pass
|
||||
if filters.Selector != nil {
|
||||
for k, v := range filters.Selector.MatchLabel {
|
||||
l, found := node.Labels[k]
|
||||
if !found {
|
||||
if filters.ResourceName != "" {
|
||||
capacity, capacityExists := node.Status.Capacity[corev1.ResourceName(filters.ResourceName)]
|
||||
allocatable, allocatableExists := node.Status.Allocatable[corev1.ResourceName(filters.ResourceName)]
|
||||
|
||||
if !capacityExists && !allocatableExists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if filters.ResourceCapacity != "" {
|
||||
parsed, err := resource.ParseQuantity(filters.ResourceCapacity)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to parse resource capacity")
|
||||
}
|
||||
|
||||
// Compare the capacity value with the parsed value
|
||||
if capacity.Cmp(parsed) == -1 {
|
||||
return false, nil
|
||||
} else if l != v {
|
||||
}
|
||||
}
|
||||
|
||||
if filters.ResourceAllocatable != "" {
|
||||
parsed, err := resource.ParseQuantity(filters.ResourceAllocatable)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to parse resource allocatable")
|
||||
}
|
||||
|
||||
// Compare the allocatable value with the parsed value
|
||||
if allocatable.Cmp(parsed) == -1 {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if filters.Architecture != "" {
|
||||
parsed := filters.Architecture
|
||||
// all filters must pass for this to pass
|
||||
if filters.Selector != nil {
|
||||
selector, err := metav1.LabelSelectorAsSelector(
|
||||
&metav1.LabelSelector{
|
||||
MatchLabels: filters.Selector.MatchLabel,
|
||||
MatchExpressions: filters.Selector.MatchExpressions,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to create label selector")
|
||||
}
|
||||
|
||||
found := selector.Matches(labels.Set(node.Labels))
|
||||
if !found {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
if filters.CPUArchitecture != "" {
|
||||
parsed := filters.CPUArchitecture
|
||||
|
||||
if !strings.EqualFold(node.Status.NodeInfo.Architecture, parsed) {
|
||||
return false, nil
|
||||
|
||||
@@ -3,12 +3,13 @@ package analyzer
|
||||
import (
|
||||
"testing"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
)
|
||||
|
||||
func Test_compareNodeResourceConditionalToActual(t *testing.T) {
|
||||
@@ -33,6 +34,7 @@ func Test_compareNodeResourceConditionalToActual(t *testing.T) {
|
||||
"ephemeral-storage": resource.MustParse("19316009748"),
|
||||
"memory": resource.MustParse("16Ki"),
|
||||
"pods": resource.MustParse("14"),
|
||||
"nvidia.com/gpu": resource.MustParse("1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -56,6 +58,7 @@ func Test_compareNodeResourceConditionalToActual(t *testing.T) {
|
||||
"ephemeral-storage": resource.MustParse("12316009748"),
|
||||
"memory": resource.MustParse("7848976Ki"),
|
||||
"pods": resource.MustParse("12"),
|
||||
"nvidia.com/gpu": resource.MustParse("1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -64,6 +67,7 @@ func Test_compareNodeResourceConditionalToActual(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
conditional string
|
||||
filters *troubleshootv1beta2.NodeResourceFilters
|
||||
totalNodeCount int
|
||||
matchingNodes []corev1.Node
|
||||
expected bool
|
||||
@@ -365,13 +369,58 @@ func Test_compareNodeResourceConditionalToActual(t *testing.T) {
|
||||
expected: false,
|
||||
isError: true,
|
||||
},
|
||||
{
|
||||
name: "GPU min(resourceAllocatable) == 1 (true)",
|
||||
conditional: "min(resourceAllocatable) == 1",
|
||||
filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
ResourceName: "nvidia.com/gpu",
|
||||
},
|
||||
matchingNodes: nodeData,
|
||||
totalNodeCount: len(nodeData),
|
||||
expected: true,
|
||||
isError: false,
|
||||
},
|
||||
{
|
||||
name: "GPU max(resourceAllocatable) > 1 (false)",
|
||||
conditional: "max(resourceAllocatable) > 1",
|
||||
filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
ResourceName: "nvidia.com/gpu",
|
||||
},
|
||||
matchingNodes: nodeData,
|
||||
totalNodeCount: 0,
|
||||
expected: false,
|
||||
isError: false,
|
||||
},
|
||||
{
|
||||
name: "GPU count() == 2 (true)",
|
||||
conditional: "count() == 2",
|
||||
filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
ResourceName: "nvidia.com/gpu",
|
||||
ResourceAllocatable: "1",
|
||||
},
|
||||
matchingNodes: nodeData,
|
||||
totalNodeCount: len(nodeData),
|
||||
expected: true,
|
||||
isError: false,
|
||||
},
|
||||
{
|
||||
name: "GPU count() == 1 (false)",
|
||||
conditional: "count() == 1",
|
||||
filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
ResourceName: "gpu.intel.com/i915",
|
||||
},
|
||||
matchingNodes: nodeData,
|
||||
totalNodeCount: 0,
|
||||
expected: false,
|
||||
isError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
actual, err := compareNodeResourceConditionalToActual(test.conditional, test.matchingNodes)
|
||||
actual, err := compareNodeResourceConditionalToActual(test.conditional, test.matchingNodes, test.filters)
|
||||
if test.isError {
|
||||
req.Error(err)
|
||||
} else {
|
||||
@@ -403,6 +452,7 @@ func Test_nodeMatchesFilters(t *testing.T) {
|
||||
"hugepages-2Mi": resource.MustParse("0"),
|
||||
"memory": resource.MustParse("7951376Ki"),
|
||||
"pods": resource.MustParse("29"),
|
||||
"nvidia.com/gpu": resource.MustParse("1"),
|
||||
},
|
||||
Allocatable: corev1.ResourceList{
|
||||
"attachable-volumes-aws-ebs": resource.MustParse("25"),
|
||||
@@ -412,6 +462,7 @@ func Test_nodeMatchesFilters(t *testing.T) {
|
||||
"hugepages-2Mi": resource.MustParse("0"),
|
||||
"memory": resource.MustParse("7848976Ki"),
|
||||
"pods": resource.MustParse("29"),
|
||||
"nvidia.com/gpu": resource.MustParse("1"),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -445,7 +496,7 @@ func Test_nodeMatchesFilters(t *testing.T) {
|
||||
name: "true when cpu arch is amd64",
|
||||
node: node,
|
||||
filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
Architecture: "amd64",
|
||||
CPUArchitecture: "amd64",
|
||||
},
|
||||
expectResult: true,
|
||||
},
|
||||
@@ -453,7 +504,7 @@ func Test_nodeMatchesFilters(t *testing.T) {
|
||||
name: "false when cpu arch is not amd64",
|
||||
node: node,
|
||||
filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
Architecture: "armhf",
|
||||
CPUArchitecture: "armhf",
|
||||
},
|
||||
expectResult: false,
|
||||
},
|
||||
@@ -501,6 +552,156 @@ func Test_nodeMatchesFilters(t *testing.T) {
|
||||
},
|
||||
expectResult: true,
|
||||
},
|
||||
{
|
||||
name: "true when the label expression matches with operator In",
|
||||
node: node,
|
||||
filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
Selector: &troubleshootv1beta2.NodeResourceSelectors{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "label",
|
||||
Operator: metav1.LabelSelectorOpIn,
|
||||
Values: []string{"value"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectResult: true,
|
||||
},
|
||||
{
|
||||
name: "false when the label expression does not match with operator In",
|
||||
node: node,
|
||||
filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
Selector: &troubleshootv1beta2.NodeResourceSelectors{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "label",
|
||||
Operator: metav1.LabelSelectorOpIn,
|
||||
Values: []string{"value2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectResult: false,
|
||||
},
|
||||
{
|
||||
name: "true when the label expression matches with operator NotIn",
|
||||
node: node,
|
||||
filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
Selector: &troubleshootv1beta2.NodeResourceSelectors{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "label",
|
||||
Operator: metav1.LabelSelectorOpNotIn,
|
||||
Values: []string{"value2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectResult: true,
|
||||
},
|
||||
{
|
||||
name: "false when the label expression does not match with operator NotIn",
|
||||
node: node,
|
||||
filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
Selector: &troubleshootv1beta2.NodeResourceSelectors{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "label",
|
||||
Operator: metav1.LabelSelectorOpNotIn,
|
||||
Values: []string{"value"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectResult: false,
|
||||
},
|
||||
{
|
||||
name: "true when the label expression matches with operator Exists",
|
||||
node: node,
|
||||
filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
Selector: &troubleshootv1beta2.NodeResourceSelectors{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "label",
|
||||
Operator: metav1.LabelSelectorOpExists,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectResult: true,
|
||||
},
|
||||
{
|
||||
name: "false when the label expression matches with operator Exists",
|
||||
node: node,
|
||||
filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
Selector: &troubleshootv1beta2.NodeResourceSelectors{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "label2",
|
||||
Operator: metav1.LabelSelectorOpExists,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectResult: false,
|
||||
},
|
||||
{
|
||||
name: "true when the label expression matches with operator DoesNotExist",
|
||||
node: node,
|
||||
filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
Selector: &troubleshootv1beta2.NodeResourceSelectors{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "label2",
|
||||
Operator: metav1.LabelSelectorOpDoesNotExist,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectResult: true,
|
||||
},
|
||||
{
|
||||
name: "false when the label expression does not match with operator DoesNotExist",
|
||||
node: node,
|
||||
filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
Selector: &troubleshootv1beta2.NodeResourceSelectors{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "label",
|
||||
Operator: metav1.LabelSelectorOpDoesNotExist,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectResult: false,
|
||||
},
|
||||
{
|
||||
name: "true when allocatable gpu is available",
|
||||
node: node,
|
||||
filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
ResourceName: "nvidia.com/gpu",
|
||||
ResourceAllocatable: "1",
|
||||
},
|
||||
expectResult: true,
|
||||
},
|
||||
{
|
||||
name: "true when gpu capacity is available",
|
||||
node: node,
|
||||
filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
ResourceName: "nvidia.com/gpu",
|
||||
ResourceCapacity: "1",
|
||||
},
|
||||
expectResult: true,
|
||||
},
|
||||
{
|
||||
name: "false when no gpu is available",
|
||||
node: node,
|
||||
filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
ResourceName: "gpu.intel.com/i915",
|
||||
},
|
||||
expectResult: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -751,7 +952,44 @@ func Test_analyzeNodeResources(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
Architecture: "amd64",
|
||||
CPUArchitecture: "amd64",
|
||||
},
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
IsPass: true,
|
||||
IsFail: false,
|
||||
IsWarn: false,
|
||||
Title: "amd64-exists",
|
||||
Message: "There is a node with at least 8 cores on amd64 arch",
|
||||
URI: "",
|
||||
IconKey: "kubernetes_node_resources",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "at least 8 cores on amd64 with message templating", // filter for a node with enough amd64 cores with message templating
|
||||
analyzer: &troubleshootv1beta2.NodeResources{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "amd64-exists",
|
||||
},
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "max(cpuCapacity) < 8",
|
||||
Message: "There isn't a node with 8 or more cores on {{ .CPUArchitecture }} arch",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "max(cpuCapacity) >= 8",
|
||||
Message: "There is a node with at least 8 cores on {{ .CPUArchitecture }} arch",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
CPUArchitecture: "amd64",
|
||||
},
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
@@ -898,6 +1136,162 @@ func Test_analyzeNodeResources(t *testing.T) {
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "8 cores in nodes with at least 8gb of ram with message templating", // validate that filtering based on memory capacity works with message templating
|
||||
analyzer: &troubleshootv1beta2.NodeResources{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "memory filter",
|
||||
},
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "sum(cpuCapacity) < 8",
|
||||
Message: "less than 8 CPUs in nodes with {{ .MemoryCapacity }} of ram",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "sum(cpuCapacity) = 8",
|
||||
Message: "exactly 8 CPUs total in nodes with {{ .MemoryCapacity }} of ram",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "sum(cpuCapacity) > 8",
|
||||
Message: "more than 8 CPUs in nodes with {{ .MemoryCapacity }} of ram",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
MemoryCapacity: "8Gi",
|
||||
},
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
IsPass: true,
|
||||
IsFail: false,
|
||||
IsWarn: false,
|
||||
Title: "memory filter",
|
||||
Message: "more than 8 CPUs in nodes with 8Gi of ram",
|
||||
URI: "",
|
||||
IconKey: "kubernetes_node_resources",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "at least 1 node on arm64 with message templating", // filter for arm64 nodes with message templating
|
||||
analyzer: &troubleshootv1beta2.NodeResources{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "arm64-exists",
|
||||
},
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "count() < 3",
|
||||
Message: "This application requires at least 3 nodes. {{ .CPUArchitecture }}, it should only return the {{ .NodeCount }} nodes that match that filter",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "count() >= 3",
|
||||
Message: "There are {{ .NodeCount }} nodes that match that filter",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
CPUArchitecture: "arm64",
|
||||
},
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
IsPass: false,
|
||||
IsFail: true,
|
||||
IsWarn: false,
|
||||
Title: "arm64-exists",
|
||||
Message: "This application requires at least 3 nodes. arm64, it should only return the 0 nodes that match that filter",
|
||||
URI: "",
|
||||
IconKey: "kubernetes_node_resources",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "at least 1 node on amd64 with message templating", // filter for amd64 nodes with message templating
|
||||
analyzer: &troubleshootv1beta2.NodeResources{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "amd64-exists",
|
||||
},
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "count() < 3",
|
||||
Message: "This application requires at least 3 nodes. {{ .CPUArchitecture }}, it should only return the {{ .NodeCount }} nodes that match that filter",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "count() >= 3",
|
||||
Message: "There are {{ .NodeCount }} nodes that match that filter",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
CPUArchitecture: "amd64",
|
||||
},
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
IsPass: true,
|
||||
IsFail: false,
|
||||
IsWarn: false,
|
||||
Title: "amd64-exists",
|
||||
Message: "There are 6 nodes that match that filter",
|
||||
URI: "",
|
||||
IconKey: "kubernetes_node_resources",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Only 5 Nodes with amd64 and 2 CPU with message templating", // filter for amd64 and 2 CPU nodes with message templating
|
||||
analyzer: &troubleshootv1beta2.NodeResources{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "amd64-exists",
|
||||
},
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "count() < 3",
|
||||
Message: "This application requires at least 3 nodes. {{ .CPUArchitecture }}, it should only return the {{ .NodeCount }} nodes that match that filter",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "count() >= 3",
|
||||
Message: "There are {{ .NodeCount }} nodes that match that filter {{ .CPUArchitecture }} and {{ .CPUCapacity }} CPU cores",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
CPUArchitecture: "amd64",
|
||||
CPUCapacity: "2",
|
||||
},
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
IsPass: true,
|
||||
IsFail: false,
|
||||
IsWarn: false,
|
||||
Title: "amd64-exists",
|
||||
Message: "There are 5 nodes that match that filter amd64 and 2 CPU cores",
|
||||
URI: "",
|
||||
IconKey: "kubernetes_node_resources",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "no pass or fail", // validate that the pass message is not always shown
|
||||
@@ -926,6 +1320,165 @@ func Test_analyzeNodeResources(t *testing.T) {
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "1 GPU in nodes", // validate that the pass message is not always shown
|
||||
analyzer: &troubleshootv1beta2.NodeResources{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "GPU filter",
|
||||
},
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "count() >= 1",
|
||||
Message: "There is a node with at least 1 GPU",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
ResourceName: "nvidia.com/gpu",
|
||||
ResourceAllocatable: "1",
|
||||
},
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
IsPass: true,
|
||||
IsFail: false,
|
||||
IsWarn: false,
|
||||
Title: "GPU filter",
|
||||
Message: "There is a node with at least 1 GPU",
|
||||
URI: "",
|
||||
IconKey: "kubernetes_node_resources",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "1 GPU in nodes filtered by ResourceAllocatable", // validate that the pass message is not always shown
|
||||
analyzer: &troubleshootv1beta2.NodeResources{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "GPU filter by ResourceAllocatable",
|
||||
},
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "count() >= 1",
|
||||
Message: "There is a node with at least 1 GPU",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
ResourceName: "nvidia.com/gpu",
|
||||
ResourceAllocatable: "1",
|
||||
},
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
IsPass: true,
|
||||
IsFail: false,
|
||||
IsWarn: false,
|
||||
Title: "GPU filter by ResourceAllocatable",
|
||||
Message: "There is a node with at least 1 GPU",
|
||||
URI: "",
|
||||
IconKey: "kubernetes_node_resources",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "1 GPU in nodes filtered by ResourceCapacity", // validate that the pass message is not always shown
|
||||
analyzer: &troubleshootv1beta2.NodeResources{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "GPU filter by ResourceCapacity",
|
||||
},
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "count() >= 1",
|
||||
Message: "There is a node with at least 1 GPU",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
ResourceName: "nvidia.com/gpu",
|
||||
ResourceCapacity: "1",
|
||||
},
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
IsPass: true,
|
||||
IsFail: false,
|
||||
IsWarn: false,
|
||||
Title: "GPU filter by ResourceCapacity",
|
||||
Message: "There is a node with at least 1 GPU",
|
||||
URI: "",
|
||||
IconKey: "kubernetes_node_resources",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Sum 1 GPU in nodes", // validate that the pass message is not always shown
|
||||
analyzer: &troubleshootv1beta2.NodeResources{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "GPU sum",
|
||||
},
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "sum(resourceAllocatable) >= 1",
|
||||
Message: "There is a node with at least 1 GPU",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
ResourceName: "nvidia.com/gpu",
|
||||
ResourceAllocatable: "1",
|
||||
},
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
IsPass: true,
|
||||
IsFail: false,
|
||||
IsWarn: false,
|
||||
Title: "GPU sum",
|
||||
Message: "There is a node with at least 1 GPU",
|
||||
URI: "",
|
||||
IconKey: "kubernetes_node_resources",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Count 0 Intel GPU in nodes", // validate that the pass message is not always shown
|
||||
analyzer: &troubleshootv1beta2.NodeResources{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "GPU Intel Count",
|
||||
},
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "count() >= 1",
|
||||
Message: "There is a node with at least 1 Intel GPU",
|
||||
URI: "",
|
||||
},
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "There is no node with at least 1 Intel GPU",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
ResourceName: "gpu.intel.com/i915",
|
||||
},
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
IsPass: false,
|
||||
IsFail: true,
|
||||
IsWarn: false,
|
||||
Title: "GPU Intel Count",
|
||||
Message: "There is no node with at least 1 Intel GPU",
|
||||
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) {
|
||||
|
||||
+51
-39
@@ -115,54 +115,66 @@ func analyzeRegexPattern(pattern string, collected []byte, outcomes []*troublesh
|
||||
return nil, errors.Wrapf(err, "failed to compile regex: %s", pattern)
|
||||
}
|
||||
|
||||
var failOutcome *troubleshootv1beta2.SingleOutcome
|
||||
var passOutcome *troubleshootv1beta2.SingleOutcome
|
||||
for _, outcome := range outcomes {
|
||||
if outcome.Fail != nil {
|
||||
failOutcome = outcome.Fail
|
||||
} else if outcome.Pass != nil {
|
||||
passOutcome = outcome.Pass
|
||||
}
|
||||
}
|
||||
result := AnalyzeResult{
|
||||
Title: checkName,
|
||||
IconKey: "kubernetes_text_analyze",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
|
||||
}
|
||||
|
||||
reMatch := re.MatchString(string(collected))
|
||||
failWhen := false
|
||||
if failOutcome != nil && failOutcome.When != "" {
|
||||
failWhen, err = strconv.ParseBool(failOutcome.When)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to process when statement: %s", failOutcome.When)
|
||||
}
|
||||
}
|
||||
passWhen := true
|
||||
if passOutcome != nil && passOutcome.When != "" {
|
||||
passWhen, err = strconv.ParseBool(passOutcome.When)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to process when statement: %s", passOutcome.When)
|
||||
}
|
||||
}
|
||||
isMatch := re.MatchString(string(collected))
|
||||
|
||||
if passWhen == failWhen {
|
||||
return nil, errors.Wrap(err, "outcome when conditions for fail and pass are equal")
|
||||
}
|
||||
for _, outcome := range outcomes {
|
||||
if outcome.Fail != nil {
|
||||
|
||||
if reMatch == passWhen {
|
||||
result.IsPass = true
|
||||
if passOutcome != nil {
|
||||
result.Message = passOutcome.Message
|
||||
result.URI = passOutcome.URI
|
||||
// if the outcome.Fail.When is not set, default to false
|
||||
if outcome.Fail.When == "" {
|
||||
outcome.Fail.When = "false"
|
||||
}
|
||||
|
||||
failWhen, err := strconv.ParseBool(outcome.Fail.When)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to process when statement: %s", outcome.Fail.When)
|
||||
}
|
||||
|
||||
if isMatch == failWhen {
|
||||
result.IsFail = true
|
||||
result.IsWarn = false
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
}
|
||||
} else if outcome.Warn != nil {
|
||||
// if the outcome.Warn.When is not set, default to false
|
||||
if outcome.Warn.When == "" {
|
||||
outcome.Warn.When = "false"
|
||||
}
|
||||
|
||||
warnWhen, err := strconv.ParseBool(outcome.Warn.When)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to process when statement: %s", outcome.Warn.When)
|
||||
}
|
||||
|
||||
if isMatch == warnWhen {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
}
|
||||
} else if outcome.Pass != nil {
|
||||
// if the outcome.Pass.When is not set, default to true
|
||||
if outcome.Pass.When == "" {
|
||||
outcome.Pass.When = "true"
|
||||
}
|
||||
|
||||
passWhen, err := strconv.ParseBool(outcome.Pass.When)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to process when statement: %s", outcome.Pass.When)
|
||||
}
|
||||
|
||||
if isMatch == passWhen {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
}
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
result.IsFail = true
|
||||
if failOutcome != nil {
|
||||
result.Message = failOutcome.Message
|
||||
result.URI = failOutcome.URI
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
@@ -222,6 +222,40 @@ func Test_textAnalyze(t *testing.T) {
|
||||
"text-collector-6/cfile-6.txt": []byte("A different message"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "warn case 1",
|
||||
analyzer: troubleshootv1beta2.TextAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "success",
|
||||
},
|
||||
},
|
||||
{
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "warning",
|
||||
},
|
||||
},
|
||||
},
|
||||
CollectorName: "text-collector-6",
|
||||
FileName: "cfile-6.txt",
|
||||
RegexPattern: "([a-zA-Z0-9\\-_:*\\s])*succe([a-zA-Z0-9\\-_:*\\s!])*",
|
||||
},
|
||||
expectResult: []AnalyzeResult{
|
||||
{
|
||||
IsPass: false,
|
||||
IsWarn: true,
|
||||
IsFail: false,
|
||||
Title: "text-collector-6",
|
||||
Message: "warning",
|
||||
IconKey: "kubernetes_text_analyze",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
|
||||
},
|
||||
},
|
||||
files: map[string][]byte{
|
||||
"text-collector-6/cfile-6.txt": []byte("A different message"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple results case 1",
|
||||
analyzer: troubleshootv1beta2.TextAnalyze{
|
||||
@@ -303,6 +337,56 @@ func Test_textAnalyze(t *testing.T) {
|
||||
"text-collector-2/cfile-3.txt": []byte("Yes it all succeeded"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple results with both warn and fail case 1, only fail",
|
||||
analyzer: troubleshootv1beta2.TextAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "pass",
|
||||
},
|
||||
},
|
||||
{
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "warning",
|
||||
},
|
||||
},
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "fail",
|
||||
},
|
||||
},
|
||||
},
|
||||
CollectorName: "text-collector-1",
|
||||
FileName: "cfile",
|
||||
RegexPattern: "succeeded",
|
||||
},
|
||||
expectResult: []AnalyzeResult{
|
||||
{
|
||||
IsPass: true,
|
||||
IsWarn: false,
|
||||
IsFail: false,
|
||||
Title: "text-collector-1",
|
||||
Message: "pass",
|
||||
IconKey: "kubernetes_text_analyze",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
|
||||
},
|
||||
{
|
||||
IsPass: false,
|
||||
IsWarn: false,
|
||||
IsFail: true,
|
||||
Title: "text-collector-1",
|
||||
Message: "fail",
|
||||
IconKey: "kubernetes_text_analyze",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
|
||||
},
|
||||
},
|
||||
files: map[string][]byte{
|
||||
"text-collector-1/cfile-1.txt": []byte("Yes it all succeeded"),
|
||||
"text-collector-1/cfile-2.txt": []byte("no success here"),
|
||||
"text-collector-2/cfile-3.txt": []byte("Yes it all succeeded"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Fail on error case 1", // regexes are not case insensitive by default
|
||||
analyzer: troubleshootv1beta2.TextAnalyze{
|
||||
|
||||
@@ -93,6 +93,9 @@ type NodeResourceFilters struct {
|
||||
EphemeralStorageCapacity string `json:"ephemeralStorageCapacity,omitempty" yaml:"ephemeralStorageCapacity,omitempty"`
|
||||
EphemeralStorageAllocatable string `json:"ephemeralStorageAllocatable,omitempty" yaml:"ephemeralStorageAllocatable,omitempty"`
|
||||
Selector *NodeResourceSelectors `json:"selector,omitempty" yaml:"selector,omitempty"`
|
||||
ResourceName string `json:"resourceName,omitempty" yaml:"resourceName,omitempty"`
|
||||
ResourceAllocatable string `json:"resourceAllocatable,omitempty" yaml:"resourceAllocatable,omitempty"`
|
||||
ResourceCapacity string `json:"resourceCapacity,omitempty" yaml:"resourceCapacity,omitempty"`
|
||||
}
|
||||
|
||||
type NodeResourceSelectors struct {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package v1beta2
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/pkg/multitype"
|
||||
)
|
||||
|
||||
@@ -103,6 +105,13 @@ type ClusterPodStatuses struct {
|
||||
Namespaces []string `json:"namespaces,omitempty" yaml:"namespaces,omitempty"`
|
||||
}
|
||||
|
||||
type ClusterContainerStatuses struct {
|
||||
AnalyzeMeta `json:",inline" yaml:",inline"`
|
||||
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
|
||||
Namespaces []string `json:"namespaces,omitempty" yaml:"namespaces,omitempty"`
|
||||
RestartCount int32 `json:"restartCount" yaml:"restartCount"`
|
||||
}
|
||||
|
||||
type ContainerRuntime struct {
|
||||
AnalyzeMeta `json:",inline" yaml:",inline"`
|
||||
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
|
||||
@@ -120,7 +129,7 @@ type NodeResources struct {
|
||||
}
|
||||
|
||||
type NodeResourceFilters struct {
|
||||
Architecture string `json:"architecture,omitempty" yaml:"cpuArchitecture,omitempty"`
|
||||
CPUArchitecture string `json:"cpuArchitecture,omitempty" yaml:"cpuArchitecture,omitempty"`
|
||||
CPUCapacity string `json:"cpuCapacity,omitempty" yaml:"cpuCapacity,omitempty"`
|
||||
CPUAllocatable string `json:"cpuAllocatable,omitempty" yaml:"cpuAllocatable,omitempty"`
|
||||
MemoryCapacity string `json:"memoryCapacity,omitempty" yaml:"memoryCapacity,omitempty"`
|
||||
@@ -130,10 +139,14 @@ type NodeResourceFilters struct {
|
||||
EphemeralStorageCapacity string `json:"ephemeralStorageCapacity,omitempty" yaml:"ephemeralStorageCapacity,omitempty"`
|
||||
EphemeralStorageAllocatable string `json:"ephemeralStorageAllocatable,omitempty" yaml:"ephemeralStorageAllocatable,omitempty"`
|
||||
Selector *NodeResourceSelectors `json:"selector,omitempty" yaml:"selector,omitempty"`
|
||||
ResourceName string `json:"resourceName,omitempty" yaml:"resourceName,omitempty"`
|
||||
ResourceAllocatable string `json:"resourceAllocatable,omitempty" yaml:"resourceAllocatable,omitempty"`
|
||||
ResourceCapacity string `json:"resourceCapacity,omitempty" yaml:"resourceCapacity,omitempty"`
|
||||
}
|
||||
|
||||
type NodeResourceSelectors struct {
|
||||
MatchLabel map[string]string `json:"matchLabel,omitempty" yaml:"matchLabel,omitempty"`
|
||||
MatchLabel map[string]string `json:"matchLabel,omitempty" yaml:"matchLabel,omitempty"`
|
||||
MatchExpressions []metav1.LabelSelectorRequirement `json:"matchExpressions,omitempty" yaml:"matchExpressions,omitempty"`
|
||||
}
|
||||
|
||||
type TextAnalyze struct {
|
||||
@@ -271,6 +284,7 @@ type Analyze struct {
|
||||
JobStatus *JobStatus `json:"jobStatus,omitempty" yaml:"jobStatus,omitempty"`
|
||||
ReplicaSetStatus *ReplicaSetStatus `json:"replicasetStatus,omitempty" yaml:"replicasetStatus,omitempty"`
|
||||
ClusterPodStatuses *ClusterPodStatuses `json:"clusterPodStatuses,omitempty" yaml:"clusterPodStatuses,omitempty"`
|
||||
ClusterContainerStatuses *ClusterContainerStatuses `json:"clusterContainerStatuses,omitempty" yaml:"clusterContainerStatuses,omitempty"`
|
||||
ContainerRuntime *ContainerRuntime `json:"containerRuntime,omitempty" yaml:"containerRuntime,omitempty"`
|
||||
Distribution *Distribution `json:"distribution,omitempty" yaml:"distribution,omitempty"`
|
||||
NodeResources *NodeResources `json:"nodeResources,omitempty" yaml:"nodeResources,omitempty"`
|
||||
@@ -292,4 +306,5 @@ type Analyze struct {
|
||||
Goldpinger *GoldpingerAnalyze `json:"goldpinger,omitempty" yaml:"goldpinger,omitempty"`
|
||||
Event *EventAnalyze `json:"event,omitempty" yaml:"event,omitempty"`
|
||||
NodeMetrics *NodeMetricsAnalyze `json:"nodeMetrics,omitempty" yaml:"nodeMetrics,omitempty"`
|
||||
HTTP *HTTPAnalyze `json:"http,omitempty" yaml:"http,omitempty"`
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
type AnalyzerSpec struct {
|
||||
Analyzers []*Analyze `json:"analyzers,omitempty"`
|
||||
HostAnalyzers []*HostAnalyze `json:"hostAnalyzers,omitempty" yaml:"hostAnalyzers,omitempty"`
|
||||
Uri string `json:"uri,omitempty" yaml:"uri,omitempty"`
|
||||
}
|
||||
|
||||
// AnalyzerStatus defines the observed state of Analyzer
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user