mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-08-27 00:37:20 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4899dc2709 | ||
|
|
2c9d9ea4c8 | ||
|
|
8603a2e58f | ||
|
|
b0102719f9 | ||
|
|
35759c47af | ||
|
|
c2f839971d | ||
|
|
fcf46d44f0 | ||
|
|
a96c9d5ff3 | ||
|
|
b8c3a65bd5 | ||
|
|
4551bc257b | ||
|
|
9dc7baafa8 |
@@ -0,0 +1,248 @@
|
||||
name: build-test-deploy
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
fail_if_pull_request_is_draft:
|
||||
if: github.event.pull_request.draft == true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Fails in order to indicate that pull request needs to be marked as ready to review and unit tests workflow needs to pass.
|
||||
run: exit 1
|
||||
|
||||
tidy-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- run: make tidy-diff
|
||||
|
||||
test-integration:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- uses: replicatedhq/action-k3s@main
|
||||
id: k3s
|
||||
with:
|
||||
version: v1.31.2-k3s1
|
||||
# test-integration includes unit tests
|
||||
- run: make test-integration
|
||||
|
||||
|
||||
compile-preflight:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- run: make generate preflight
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: preflight
|
||||
path: bin/preflight
|
||||
|
||||
validate-preflight-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
needs: compile-preflight
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: replicatedhq/action-k3s@main
|
||||
id: k3s
|
||||
with:
|
||||
version: v1.31.2-k3s1
|
||||
- name: Download preflight binary
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: preflight
|
||||
path: bin/
|
||||
- run: chmod +x bin/preflight
|
||||
- run: make preflight-e2e-test
|
||||
|
||||
compile-supportbundle:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- run: make generate support-bundle
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: support-bundle
|
||||
path: bin/support-bundle
|
||||
|
||||
validate-supportbundle-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
needs: compile-supportbundle
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: replicatedhq/action-k3s@main
|
||||
id: k3s
|
||||
with:
|
||||
version: v1.31.2-k3s1
|
||||
- name: Download support bundle binary
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: support-bundle
|
||||
path: bin/
|
||||
- run: chmod +x bin/support-bundle
|
||||
- run: make support-bundle-e2e-test
|
||||
|
||||
# Additional e2e tests for support bundle that run in Go, these create a Kind cluster
|
||||
validate-supportbundle-e2e-go:
|
||||
runs-on: ubuntu-latest
|
||||
needs: compile-supportbundle
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Download support bundle binary
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: support-bundle
|
||||
path: bin/
|
||||
- run: chmod +x bin/support-bundle
|
||||
- name: Download preflight binary
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: preflight
|
||||
path: bin/
|
||||
- run: chmod +x bin/preflight
|
||||
- run: make support-bundle-e2e-go-test
|
||||
|
||||
goreleaser-test:
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/v') != true
|
||||
strategy:
|
||||
matrix:
|
||||
goarch: [amd64, arm64]
|
||||
goos: [darwin, linux]
|
||||
include:
|
||||
- goarch: arm
|
||||
goos: linux
|
||||
- goarch: riscv64
|
||||
goos: linux
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: "v2.12.3"
|
||||
args: build --clean --snapshot --config deploy/.goreleaser.yaml --single-target
|
||||
env:
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: ${{ matrix.goos }}
|
||||
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- validate-preflight-e2e
|
||||
- validate-supportbundle-e2e
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: azure/docker-login@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USER }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- uses: sigstore/cosign-installer@v3.10.0
|
||||
|
||||
- name: Get Cosign Key
|
||||
run: |
|
||||
echo $COSIGN_KEY | base64 -d > ./cosign.key
|
||||
env:
|
||||
COSIGN_KEY: ${{secrets.COSIGN_KEY}}
|
||||
|
||||
- name: Generate SBOM
|
||||
run: |
|
||||
make sbom
|
||||
env:
|
||||
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
|
||||
COSIGN_KEY: ${{ secrets.COSIGN_KEY }}
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: "v2.12.3" # Binary version to install
|
||||
args: release --clean --config deploy/.goreleaser.yaml
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Update new preflight version in krew-index
|
||||
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.47
|
||||
with:
|
||||
krew_template_file: deploy/krew/support-bundle.yaml
|
||||
|
||||
|
||||
# summary jobs, these jobs will only run if all the other jobs have succeeded
|
||||
validate-pr-tests:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- tidy-check
|
||||
- test-integration
|
||||
- validate-preflight-e2e
|
||||
- validate-supportbundle-e2e
|
||||
- validate-supportbundle-e2e-go
|
||||
steps:
|
||||
- run: echo "All PR tests passed"
|
||||
|
||||
|
||||
# this job will validate that the validation did not fail and that all pr-tests succeed
|
||||
# it is used for the github branch protection rule
|
||||
validate-success:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- validate-pr-tests
|
||||
if: always()
|
||||
steps:
|
||||
# https://docs.github.com/en/actions/learn-github-actions/contexts#needs-context
|
||||
# if the validate-pr-tests job was not successful, this job will fail
|
||||
- name: fail if validate-pr-tests job was not successful
|
||||
if: needs.validate-pr-tests.result != 'success'
|
||||
run: exit 1
|
||||
# if the validate-pr-tests job was successful, this job will succeed
|
||||
- name: succeed if validate-pr-tests job succeeded
|
||||
if: needs.validate-pr-tests.result == 'success'
|
||||
run: echo "Validation succeeded"
|
||||
@@ -3,9 +3,9 @@ name: build-test
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
branches: [v1beta3]
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [v1beta3]
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
@@ -38,13 +38,17 @@ BUILDTAGS = "netgo containers_image_ostree_stub exclude_graphdriver_devicemapper
|
||||
BUILDFLAGS = -tags ${BUILDTAGS} -installsuffix netgo
|
||||
BUILDPATHS = ./pkg/... ./cmd/... ./internal/...
|
||||
E2EPATHS = ./test/e2e/...
|
||||
TESTFLAGS ?=
|
||||
TESTFLAGS ?= -v -coverprofile cover.out
|
||||
|
||||
.DEFAULT_GOAL := all
|
||||
all: clean build test
|
||||
|
||||
.PHONY: ffi
|
||||
ffi: fmt vet
|
||||
go build ${BUILDFLAGS} ${LDFLAGS} -o bin/troubleshoot.so -buildmode=c-shared ffi/main.go
|
||||
|
||||
.PHONY: test
|
||||
test: fmt vet
|
||||
test: generate fmt vet
|
||||
if [ -n $(RUN) ]; then \
|
||||
go test ${BUILDFLAGS} ${BUILDPATHS} ${TESTFLAGS} -run $(RUN); \
|
||||
else \
|
||||
@@ -54,13 +58,17 @@ test: 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: fmt vet
|
||||
test-integration: generate fmt vet
|
||||
go test -v --tags="integration exclude_graphdriver_devicemapper exclude_graphdriver_btrfs" ${BUILDPATHS}
|
||||
|
||||
.PHONY: preflight-e2e-test
|
||||
preflight-e2e-test:
|
||||
./test/validate-preflight-e2e.sh
|
||||
|
||||
.PHONY: run-examples
|
||||
run-examples:
|
||||
./test/run-examples.sh
|
||||
|
||||
.PHONY: support-bundle-e2e-test
|
||||
support-bundle-e2e-test:
|
||||
./test/validate-support-bundle-e2e.sh
|
||||
@@ -75,19 +83,31 @@ support-bundle-e2e-go-test:
|
||||
|
||||
rebuild: clean build
|
||||
|
||||
# Build all binaries in parallel ( -j )
|
||||
build: tidy
|
||||
@echo "Build cli binaries"
|
||||
$(MAKE) bin/support-bundle bin/preflight
|
||||
$(MAKE) -j bin/support-bundle bin/preflight
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
@rm -f bin/analyze
|
||||
@rm -f bin/support-bundle
|
||||
@rm -f bin/collect
|
||||
@rm -f bin/preflight
|
||||
@rm -f bin/troubleshoot.h
|
||||
@rm -f bin/troubleshoot.so
|
||||
@rm -f bin/schemagen
|
||||
@rm -f bin/docsgen
|
||||
|
||||
.PHONY: tidy
|
||||
tidy:
|
||||
go mod tidy
|
||||
|
||||
# Prints the diff of the changes that would be made by `go mod tidy`. Used in CI
|
||||
.PHONY: tidy-diff
|
||||
tidy-diff:
|
||||
go mod tidy -diff
|
||||
|
||||
# Only build when any of the files in SOURCES changes, or if bin/<file> is absent
|
||||
MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
SOURCES := $(shell find $(MAKEFILE_DIR) -type f \( -name "*.go" -o -name "go.mod" -o -name "go.sum" \))
|
||||
@@ -97,12 +117,28 @@ bin/support-bundle: $(SOURCES)
|
||||
bin/preflight: $(SOURCES)
|
||||
go build ${BUILDFLAGS} ${LDFLAGS} -o bin/preflight github.com/replicatedhq/troubleshoot/cmd/preflight
|
||||
|
||||
bin/analyze: $(SOURCES)
|
||||
go build ${BUILDFLAGS} ${LDFLAGS} -o bin/analyze github.com/replicatedhq/troubleshoot/cmd/analyze
|
||||
|
||||
bin/collect: $(SOURCES)
|
||||
go build ${BUILDFLAGS} ${LDFLAGS} -o bin/collect github.com/replicatedhq/troubleshoot/cmd/collect
|
||||
|
||||
.PHONY: support-bundle
|
||||
support-bundle: bin/support-bundle
|
||||
|
||||
.PHONY: preflight
|
||||
preflight: bin/preflight
|
||||
|
||||
.PHONY: analyze
|
||||
analyze: bin/analyze
|
||||
|
||||
.PHONY: collect
|
||||
collect: bin/collect
|
||||
|
||||
build-linux: tidy
|
||||
@echo "Build cli binaries for Linux"
|
||||
GOOS=linux GOARCH=amd64 $(MAKE) -j bin/support-bundle bin/preflight bin/analyze bin/collect
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
go fmt ${BUILDPATHS}
|
||||
@@ -110,3 +146,153 @@ fmt:
|
||||
.PHONY: vet
|
||||
vet:
|
||||
go vet ${BUILDFLAGS} ${BUILDPATHS}
|
||||
|
||||
.PHONY: generate
|
||||
generate: controller-gen client-gen
|
||||
$(CONTROLLER_GEN) \
|
||||
object:headerFile=./hack/boilerplate.go.txt paths=./pkg/apis/...
|
||||
$(CLIENT_GEN) \
|
||||
--output-dir=. \
|
||||
--output-pkg=github.com/replicatedhq/troubleshoot/pkg/client \
|
||||
--clientset-name troubleshootclientset \
|
||||
--input-base github.com/replicatedhq/troubleshoot/pkg/apis \
|
||||
--input troubleshoot/v1beta1 \
|
||||
--input troubleshoot/v1beta2 \
|
||||
--go-header-file ./hack/boilerplate.go.txt
|
||||
cp -r troubleshootclientset pkg/client
|
||||
rm -rf troubleshootclientset
|
||||
|
||||
.PHONY: openapischema
|
||||
openapischema: controller-gen
|
||||
controller-gen crd +output:dir=./config/crds paths=./pkg/apis/troubleshoot/v1beta1
|
||||
controller-gen crd +output:dir=./config/crds paths=./pkg/apis/troubleshoot/v1beta2
|
||||
|
||||
check-schemas: generate schemas
|
||||
@if [ -n "$$(git status --short)" ]; then \
|
||||
echo -e "\033[31mThe git repo is dirty :( Ensure all generated files are committed e.g CRD schema files\033[0;m"; \
|
||||
git status --short; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: schemas
|
||||
schemas: openapischema bin/schemagen
|
||||
./bin/schemagen --output-dir ./schemas
|
||||
|
||||
bin/schemagen:
|
||||
go build ${LDFLAGS} -o bin/schemagen github.com/replicatedhq/troubleshoot/cmd/schemagen
|
||||
|
||||
.PHONY: docs
|
||||
docs: fmt vet bin/docsgen
|
||||
./bin/docsgen
|
||||
|
||||
bin/docsgen:
|
||||
go build ${LDFLAGS} -o bin/docsgen github.com/replicatedhq/troubleshoot/cmd/docsgen
|
||||
|
||||
controller-gen:
|
||||
go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.19.0
|
||||
CONTROLLER_GEN=$(shell which controller-gen)
|
||||
|
||||
.PHONY: client-gen
|
||||
client-gen:
|
||||
go install k8s.io/code-generator/cmd/client-gen@v0.34.0
|
||||
CLIENT_GEN=$(shell which client-gen)
|
||||
|
||||
.PHONY: release
|
||||
release: export GITHUB_TOKEN = $(shell echo ${GITHUB_TOKEN_TROUBLESHOOT})
|
||||
release:
|
||||
curl -sL https://git.io/goreleaser | bash -s -- --rm-dist --config deploy/.goreleaser.yml
|
||||
|
||||
.PHONY: snapshot-release
|
||||
snapshot-release:
|
||||
curl -sL https://git.io/goreleaser | bash -s -- --rm-dist --snapshot --config deploy/.goreleaser.snapshot.yml
|
||||
docker push replicated/troubleshoot:alpha
|
||||
docker push replicated/preflight:alpha
|
||||
|
||||
.PHONY: local-release
|
||||
local-release:
|
||||
curl -sL https://git.io/goreleaser | bash -s -- --rm-dist --snapshot --config deploy/.goreleaser.yaml
|
||||
docker tag replicated/troubleshoot:alpha localhost:32000/troubleshoot:alpha
|
||||
docker tag replicated/preflight:alpha localhost:32000/preflight:alpha
|
||||
docker push localhost:32000/troubleshoot:alpha
|
||||
docker push localhost:32000/preflight:alpha
|
||||
|
||||
.PHONY: run-preflight
|
||||
run-preflight: bin/preflight
|
||||
./bin/preflight ./examples/preflight/sample-preflight.yaml
|
||||
|
||||
.PHONY: run-support-bundle
|
||||
run-support-bundle: bin/support-bundle
|
||||
./bin/support-bundle ./examples/support-bundle/sample-supportbundle.yaml
|
||||
|
||||
.PHONY: run-analyze
|
||||
run-analyze: bin/analyze
|
||||
./bin/analyze --analyzers ./examples/support-bundle/sample-analyzers.yaml ./support-bundle.tar.gz
|
||||
|
||||
.PHONY: init-sbom
|
||||
init-sbom:
|
||||
mkdir -p sbom/spdx sbom/assets
|
||||
|
||||
.PHONY: install-spdx-sbom-generator
|
||||
install-spdx-sbom-generator: init-sbom
|
||||
./scripts/initialize-sbom-build.sh
|
||||
|
||||
SPDX_GENERATOR=./sbom/spdx-sbom-generator
|
||||
|
||||
.PHONY: generate-sbom
|
||||
generate-sbom: install-spdx-sbom-generator
|
||||
$(SPDX_GENERATOR) -o ./sbom/spdx
|
||||
|
||||
sbom/assets/troubleshoot-sbom.tgz: generate-sbom
|
||||
tar -czf sbom/assets/troubleshoot-sbom.tgz sbom/spdx/*.spdx
|
||||
|
||||
sbom: sbom/assets/troubleshoot-sbom.tgz
|
||||
cosign sign-blob \
|
||||
--key ./cosign.key \
|
||||
--tlog-upload \
|
||||
--yes \
|
||||
--rekor-url=https://rekor.sigstore.dev \
|
||||
sbom/assets/troubleshoot-sbom.tgz > sbom/assets/troubleshoot-sbom.tgz.sig
|
||||
cosign public-key --key cosign.key --outfile sbom/assets/key.pub
|
||||
|
||||
.PHONY: scan
|
||||
scan:
|
||||
trivy fs \
|
||||
--scanners vuln \
|
||||
--exit-code=1 \
|
||||
--severity="HIGH,CRITICAL" \
|
||||
--ignore-unfixed \
|
||||
./
|
||||
|
||||
.PHONY: watch
|
||||
watch: npm-install
|
||||
bin/watch.js
|
||||
|
||||
## Syncronize the code with a remote server. More info: CONTRIBUTING.md
|
||||
.PHONY: watchrsync
|
||||
watchrsync: npm-install
|
||||
bin/watchrsync.js
|
||||
|
||||
.PHONY: npm-install
|
||||
npm-install:
|
||||
npm --version 2>&1 >/dev/null || ( echo "npm not installed; install npm to set up watchrsync" && exit 1 )
|
||||
npm list gaze-run-interrupt || npm install install --no-save gaze-run-interrupt@~2.0.0
|
||||
|
||||
|
||||
######## Lagacy make targets ###########
|
||||
# Deprecated: These can be removed
|
||||
.PHONY: run-troubleshoot
|
||||
run-troubleshoot: run-support-bundle
|
||||
|
||||
longhorn:
|
||||
git clone https://github.com/longhorn/longhorn-manager.git
|
||||
cd longhorn-manager && git checkout v1.2.2 && cd ..
|
||||
rm -rf pkg/longhorn
|
||||
mv longhorn-manager/k8s/pkg pkg/longhorn
|
||||
mv longhorn-manager/types pkg/longhorn/types
|
||||
mv longhorn-manager/util pkg/longhorn/util
|
||||
rm -rf pkg/longhorn/util/daemon
|
||||
rm -rf pkg/longhorn/util/server
|
||||
find pkg/longhorn -type f | xargs sed -i "s/github.com\/longhorn\/longhorn-manager\/k8s\/pkg/github.com\/replicatedhq\/troubleshoot\/pkg\/longhorn/g"
|
||||
find pkg/longhorn -type f | xargs sed -i "s/github.com\/longhorn\/longhorn-manager\/types/github.com\/replicatedhq\/troubleshoot\/pkg\/longhorn\/types/g"
|
||||
find pkg/longhorn -type f | xargs sed -i "s/github.com\/longhorn\/longhorn-manager\/util/github.com\/replicatedhq\/troubleshoot\/pkg\/longhorn\/util/g"
|
||||
rm -rf longhorn-manager
|
||||
@@ -1,98 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/lint"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func LintCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "lint [spec-files...]",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Short: "Lint preflight specs for syntax and structural errors",
|
||||
Long: `Lint preflight specs for syntax and structural errors.
|
||||
|
||||
This command validates troubleshoot specs and checks for:
|
||||
- YAML syntax errors (missing colons, invalid structure)
|
||||
- Missing required fields (apiVersion, kind, metadata, spec)
|
||||
- Invalid template syntax ({{ .Values.* }}, {{ .Release.* }}, etc.)
|
||||
- Missing analyzers or collectors
|
||||
- Common structural issues
|
||||
- Missing docStrings (warning)
|
||||
|
||||
Both v1beta2 and v1beta3 apiVersions are supported. Use 'convert' if you need a full structural conversion between schema versions.
|
||||
|
||||
The --fix flag can automatically repair:
|
||||
- Missing colons in YAML (e.g., "metadata" → "metadata:")
|
||||
- Missing or malformed apiVersion line. If templating ({{ }}) or docString fields are detected, apiVersion is set to v1beta3; otherwise v1beta2.
|
||||
- Template expressions missing leading dot (e.g., "{{ Values.x }}" → "{{ .Values.x }}")
|
||||
|
||||
Examples:
|
||||
# Lint a single spec file
|
||||
preflight lint my-preflight.yaml
|
||||
|
||||
# Lint multiple spec files
|
||||
preflight lint spec1.yaml spec2.yaml spec3.yaml
|
||||
|
||||
# Lint with automatic fixes (may need to run multiple times for complex issues)
|
||||
preflight lint --fix my-preflight.yaml
|
||||
|
||||
# Lint and output as JSON for CI/CD integration
|
||||
preflight lint --format json my-preflight.yaml
|
||||
|
||||
Exit codes:
|
||||
0 - No errors found
|
||||
2 - Validation errors found`,
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
viper.BindPFlags(cmd.Flags())
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
v := viper.GetViper()
|
||||
|
||||
opts := lint.LintOptions{
|
||||
FilePaths: args,
|
||||
Fix: v.GetBool("fix"),
|
||||
Format: v.GetString("format"),
|
||||
}
|
||||
|
||||
return runLint(opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Bool("fix", false, "Automatically fix issues where possible")
|
||||
cmd.Flags().String("format", "text", "Output format: text or json")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runLint(opts lint.LintOptions) error {
|
||||
// Validate file paths exist
|
||||
for _, filePath := range opts.FilePaths {
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
return errors.Wrapf(err, "file not found: %s", filePath)
|
||||
}
|
||||
}
|
||||
|
||||
// Run linting
|
||||
results, err := lint.LintFiles(opts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to lint files")
|
||||
}
|
||||
|
||||
// Format and print results
|
||||
output := lint.FormatResults(results, opts.Format)
|
||||
fmt.Print(output)
|
||||
|
||||
// Return appropriate exit code
|
||||
if lint.HasErrors(results) {
|
||||
os.Exit(constants.EXIT_CODE_SPEC_ISSUES)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -89,7 +89,6 @@ that a cluster meets the requirements to run an application.`,
|
||||
cmd.AddCommand(TemplateCmd())
|
||||
cmd.AddCommand(DocsCmd())
|
||||
cmd.AddCommand(ConvertCmd())
|
||||
cmd.AddCommand(LintCmd())
|
||||
|
||||
preflight.AddFlags(cmd.PersistentFlags())
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/cmd/internal/util"
|
||||
preflightcli "github.com/replicatedhq/troubleshoot/cmd/preflight/cli"
|
||||
"github.com/replicatedhq/troubleshoot/internal/traces"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/logger"
|
||||
@@ -110,7 +109,6 @@ If no arguments are provided, specs are automatically loaded from the cluster by
|
||||
cmd.AddCommand(Schedule())
|
||||
cmd.AddCommand(UploadCmd())
|
||||
cmd.AddCommand(util.VersionCmd())
|
||||
cmd.AddCommand(preflightcli.LintCmd())
|
||||
|
||||
cmd.Flags().StringSlice("redactors", []string{}, "names of the additional redactors to use")
|
||||
cmd.Flags().Bool("redact", true, "enable/disable default redactions")
|
||||
|
||||
+8
-45
@@ -13,6 +13,8 @@ builds:
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X github.com/replicatedhq/troubleshoot/pkg/version.version={{ .Version }}
|
||||
@@ -36,6 +38,8 @@ builds:
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X github.com/replicatedhq/troubleshoot/pkg/version.version={{ .Version }}
|
||||
@@ -91,39 +95,7 @@ archives:
|
||||
dst: .
|
||||
strip_parent: true
|
||||
|
||||
- id: preflight-universal
|
||||
ids: [preflight-universal]
|
||||
formats: [tar.gz]
|
||||
name_template: "preflight_{{ .Os }}_{{ .Arch }}"
|
||||
files:
|
||||
- licence*
|
||||
- LICENCE*
|
||||
- license*
|
||||
- LICENSE*
|
||||
- readme*
|
||||
- README*
|
||||
- changelog*
|
||||
- CHANGELOG*
|
||||
- src: "sbom/assets/*"
|
||||
dst: .
|
||||
strip_parent: true
|
||||
|
||||
- id: support-bundle-universal
|
||||
ids: [support-bundle-universal]
|
||||
formats: [tar.gz]
|
||||
name_template: "support-bundle_{{ .Os }}_{{ .Arch }}"
|
||||
files:
|
||||
- licence*
|
||||
- LICENCE*
|
||||
- license*
|
||||
- LICENSE*
|
||||
- readme*
|
||||
- README*
|
||||
- changelog*
|
||||
- CHANGELOG*
|
||||
- src: "sbom/assets/*"
|
||||
dst: .
|
||||
strip_parent: true
|
||||
|
||||
|
||||
dockers:
|
||||
- dockerfile: ./deploy/Dockerfile.troubleshoot
|
||||
@@ -147,20 +119,11 @@ dockers:
|
||||
- preflight
|
||||
skip_push: true
|
||||
|
||||
universal_binaries:
|
||||
- id: preflight-universal
|
||||
ids: [preflight] # refers to the build id above
|
||||
replace: true
|
||||
name_template: preflight
|
||||
|
||||
- id: support-bundle-universal
|
||||
ids: [support-bundle] # refers to the build id above
|
||||
replace: true
|
||||
name_template: support-bundle
|
||||
|
||||
|
||||
brews:
|
||||
- name: preflight
|
||||
ids: [preflight, preflight-universal]
|
||||
ids: [preflight]
|
||||
homepage: https://docs.replicated.com/reference/preflight-overview/
|
||||
description: "A preflight checker and conformance test for Kubernetes clusters."
|
||||
repository:
|
||||
@@ -170,7 +133,7 @@ brews:
|
||||
directory: HomebrewFormula
|
||||
install: bin.install "preflight"
|
||||
- name: support-bundle
|
||||
ids: [support-bundle, support-bundle-universal]
|
||||
ids: [support-bundle]
|
||||
homepage: https://docs.replicated.com/reference/support-bundle-overview/
|
||||
description: "Collect and redact support bundles for Kubernetes clusters."
|
||||
repository:
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: Preflight
|
||||
metadata:
|
||||
name: helm-builtins-example
|
||||
labels:
|
||||
release: {{ .Release.Name }}
|
||||
spec:
|
||||
analyzers:
|
||||
- docString: |
|
||||
Title: Example using Helm builtin objects
|
||||
Requirement: Demonstrates .Values, .Release, .Chart, etc.
|
||||
|
||||
Supported Helm builtin objects:
|
||||
- .Values.* - User-provided values
|
||||
- .Release.Name - Release name (default: "preflight")
|
||||
- .Release.Namespace - Release namespace (default: "default")
|
||||
- .Release.IsInstall - Whether this is an install (true)
|
||||
- .Release.IsUpgrade - Whether this is an upgrade (false)
|
||||
- .Release.Revision - Release revision (1)
|
||||
- .Chart.Name - Chart name
|
||||
- .Chart.Version - Chart version
|
||||
- .Capabilities.KubeVersion - Kubernetes version capabilities
|
||||
clusterVersion:
|
||||
checkName: Kubernetes version check in {{ .Release.Namespace }}
|
||||
outcomes:
|
||||
- fail:
|
||||
when: '< {{ .Values.minVersion | default "1.19.0" }}'
|
||||
message: |
|
||||
Release {{ .Release.Name }} requires Kubernetes {{ .Values.minVersion | default "1.19.0" }} or later.
|
||||
Chart: {{ .Chart.Name }}
|
||||
- pass:
|
||||
when: '>= {{ .Values.minVersion | default "1.19.0" }}'
|
||||
message: Kubernetes version is supported for release {{ .Release.Name }}
|
||||
@@ -1,8 +0,0 @@
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: Preflight
|
||||
metadata
|
||||
name: invalid-yaml
|
||||
spec:
|
||||
analyzers:
|
||||
- clusterVersion:
|
||||
checkName: Kubernetes version
|
||||
@@ -1,11 +0,0 @@
|
||||
kind: Preflight
|
||||
metadata:
|
||||
name: missing-apiversion
|
||||
spec:
|
||||
analyzers:
|
||||
- clusterVersion:
|
||||
checkName: Kubernetes version
|
||||
outcomes:
|
||||
- pass:
|
||||
when: '>= 1.19.0'
|
||||
message: Kubernetes version is supported
|
||||
@@ -1,10 +0,0 @@
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: Preflight
|
||||
spec:
|
||||
analyzers:
|
||||
- clusterVersion:
|
||||
checkName: Kubernetes version
|
||||
outcomes:
|
||||
- pass:
|
||||
when: '>= 1.19.0'
|
||||
message: Kubernetes version is supported
|
||||
@@ -1,7 +0,0 @@
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: Preflight
|
||||
metadata:
|
||||
name: no-analyzers
|
||||
spec:
|
||||
collectors:
|
||||
- clusterInfo: {}
|
||||
@@ -1,18 +0,0 @@
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: Preflight
|
||||
metadata:
|
||||
name: simple-no-template
|
||||
spec:
|
||||
analyzers:
|
||||
- docString: |
|
||||
Title: Kubernetes Version Check
|
||||
Requirement: Kubernetes 1.19.0 or later
|
||||
clusterVersion:
|
||||
checkName: Kubernetes version
|
||||
outcomes:
|
||||
- fail:
|
||||
when: '< 1.19.0'
|
||||
message: Kubernetes version must be at least 1.19.0
|
||||
- pass:
|
||||
when: '>= 1.19.0'
|
||||
message: Kubernetes version is supported
|
||||
@@ -1,12 +0,0 @@
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: no-collectors
|
||||
spec:
|
||||
analyzers:
|
||||
- clusterVersion:
|
||||
checkName: Kubernetes version
|
||||
outcomes:
|
||||
- pass:
|
||||
when: '>= 1.19.0'
|
||||
message: Kubernetes version is supported
|
||||
@@ -1,15 +0,0 @@
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: valid-support-bundle
|
||||
spec:
|
||||
collectors:
|
||||
- clusterInfo: {}
|
||||
- clusterResources: {}
|
||||
analyzers:
|
||||
- clusterVersion:
|
||||
checkName: Kubernetes version
|
||||
outcomes:
|
||||
- pass:
|
||||
when: '>= 1.19.0'
|
||||
message: Kubernetes version is supported
|
||||
@@ -1,15 +0,0 @@
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: Preflight
|
||||
metadata:
|
||||
name: valid-preflight
|
||||
spec:
|
||||
analyzers:
|
||||
- docString: |
|
||||
Title: Test Analyzer
|
||||
Requirement: Test requirement
|
||||
clusterVersion:
|
||||
checkName: Kubernetes version
|
||||
outcomes:
|
||||
- pass:
|
||||
when: '>= 1.19.0'
|
||||
message: Kubernetes version is supported
|
||||
@@ -1,12 +0,0 @@
|
||||
apiVersion: troubleshoot.sh/v1beta2
|
||||
kind: Preflight
|
||||
metadata:
|
||||
name: wrong-version
|
||||
spec:
|
||||
analyzers:
|
||||
- clusterVersion:
|
||||
checkName: Kubernetes version
|
||||
outcomes:
|
||||
- pass:
|
||||
when: '>= 1.19.0'
|
||||
message: Kubernetes version is supported
|
||||
@@ -21,7 +21,7 @@ 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.8.0
|
||||
github.com/hashicorp/go-getter v1.8.2
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/jackc/pgx/v5 v5.7.6
|
||||
github.com/longhorn/go-iscsi-helper v0.0.0-20210330030558-49a327fb024e
|
||||
@@ -33,7 +33,7 @@ require (
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/replicatedhq/termui/v3 v3.1.1-0.20200811145416-f40076d26851
|
||||
github.com/segmentio/ksuid v1.0.4
|
||||
github.com/shirou/gopsutil/v4 v4.25.8
|
||||
github.com/shirou/gopsutil/v4 v4.25.9
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/spf13/viper v1.21.0
|
||||
@@ -57,7 +57,7 @@ require (
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
k8s.io/kubernetes v1.34.1
|
||||
oras.land/oras-go v1.2.6
|
||||
sigs.k8s.io/controller-runtime v0.22.1
|
||||
sigs.k8s.io/controller-runtime v0.22.2
|
||||
sigs.k8s.io/e2e-framework v0.6.0
|
||||
)
|
||||
|
||||
@@ -106,7 +106,7 @@ require (
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/ebitengine/purego v0.9.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
|
||||
@@ -222,7 +222,6 @@ require (
|
||||
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.7.0
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
|
||||
@@ -213,8 +213,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
|
||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
|
||||
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
@@ -368,12 +368,10 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-getter v1.8.0 h1:GMRdoMBDz12Mim366pWsRVIrrkugJ19rrmykkv0Nhzo=
|
||||
github.com/hashicorp/go-getter v1.8.0/go.mod h1:/K0O5zR6R72O3r2x3z2UHadiC0XHMbbzHO9pS8ZeJPA=
|
||||
github.com/hashicorp/go-getter v1.8.2 h1:CGCK+bZQLl44PYiwJweVzfpjg7bBwtuXu3AGcLiod2o=
|
||||
github.com/hashicorp/go-getter v1.8.2/go.mod h1:CUTt9x2bCtJ/sV8ihgrITL3IUE+0BE1j/e4n5P/GIM4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo=
|
||||
github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=
|
||||
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw=
|
||||
@@ -593,8 +591,8 @@ github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c
|
||||
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
|
||||
github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI=
|
||||
github.com/shirou/gopsutil/v4 v4.25.9 h1:JImNpf6gCVhKgZhtaAHJ0serfFGtlfIlSC08eaKdTrU=
|
||||
github.com/shirou/gopsutil/v4 v4.25.9/go.mod h1:gxIxoC+7nQRwUl/xNhutXlD8lq+jxTgpIkEf3rADHL8=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
@@ -905,8 +903,8 @@ oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
|
||||
oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o=
|
||||
periph.io/x/host/v3 v3.8.5 h1:g4g5xE1XZtDiGl1UAJaUur1aT7uNiFLMkyMEiZ7IHII=
|
||||
periph.io/x/host/v3 v3.8.5/go.mod h1:hPq8dISZIc+UNfWoRj+bPH3XEBQqJPdFdx218W92mdc=
|
||||
sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg=
|
||||
sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY=
|
||||
sigs.k8s.io/controller-runtime v0.22.2 h1:cK2l8BGWsSWkXz09tcS4rJh95iOLney5eawcK5A33r4=
|
||||
sigs.k8s.io/controller-runtime v0.22.2/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
|
||||
sigs.k8s.io/e2e-framework v0.6.0 h1:p7hFzHnLKO7eNsWGI2AbC1Mo2IYxidg49BiT4njxkrM=
|
||||
sigs.k8s.io/e2e-framework v0.6.0/go.mod h1:IREnCHnKgRCioLRmNi0hxSJ1kJ+aAdjEKK/gokcZu4k=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
|
||||
|
||||
+46
-3
@@ -10,9 +10,16 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/discovery/cached/memory"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/restmapper"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
@@ -45,6 +52,42 @@ type VersionInfo struct {
|
||||
Values map[string]interface{} `json:"values,omitempty"`
|
||||
}
|
||||
|
||||
type configGetter struct {
|
||||
restConfig *rest.Config
|
||||
}
|
||||
|
||||
// ToDiscoveryClient implements genericclioptions.RESTClientGetter.
|
||||
func (c configGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
|
||||
discoveryClient, err := discovery.NewDiscoveryClientForConfig(c.restConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cached := memory.NewMemCacheClient(discoveryClient)
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
// ToRESTConfig implements genericclioptions.RESTClientGetter.
|
||||
func (c configGetter) ToRESTConfig() (*rest.Config, error) {
|
||||
return c.restConfig, nil
|
||||
}
|
||||
|
||||
// ToRESTMapper implements genericclioptions.RESTClientGetter.
|
||||
func (c configGetter) ToRESTMapper() (meta.RESTMapper, error) {
|
||||
discoveryClient, err := c.ToDiscoveryClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
|
||||
return mapper, nil
|
||||
}
|
||||
|
||||
// ToRawKubeConfigLoader implements genericclioptions.RESTClientGetter.
|
||||
func (c configGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig {
|
||||
return k8sutil.GetKubeconfig()
|
||||
}
|
||||
|
||||
var _ genericclioptions.RESTClientGetter = configGetter{}
|
||||
|
||||
func (c *CollectHelm) Title() string {
|
||||
return getCollectorName(c)
|
||||
}
|
||||
@@ -57,7 +100,7 @@ func (c *CollectHelm) Collect(progressChan chan<- interface{}) (CollectorResult,
|
||||
|
||||
output := NewResult()
|
||||
|
||||
releaseInfos, err := helmReleaseHistoryCollector(c.Collector.ReleaseName, c.Collector.Namespace, c.Collector.CollectValues)
|
||||
releaseInfos, err := helmReleaseHistoryCollector(c.ClientConfig, c.Collector.ReleaseName, c.Collector.Namespace, c.Collector.CollectValues)
|
||||
if err != nil {
|
||||
errsToMarhsal := []string{}
|
||||
for _, e := range err {
|
||||
@@ -88,12 +131,12 @@ func (c *CollectHelm) Collect(progressChan chan<- interface{}) (CollectorResult,
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func helmReleaseHistoryCollector(releaseName string, namespace string, collectValues bool) ([]ReleaseInfo, []error) {
|
||||
func helmReleaseHistoryCollector(config *rest.Config, releaseName string, namespace string, collectValues bool) ([]ReleaseInfo, []error) {
|
||||
var results []ReleaseInfo
|
||||
error_list := []error{}
|
||||
|
||||
actionConfig := new(action.Configuration)
|
||||
if err := actionConfig.Init(nil, namespace, "", klog.V(2).Infof); err != nil {
|
||||
if err := actionConfig.Init(configGetter{config}, namespace, "", klog.V(2).Infof); err != nil {
|
||||
return nil, []error{err}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,768 +0,0 @@
|
||||
package lint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
type LintResult struct {
|
||||
FilePath string
|
||||
Errors []LintError
|
||||
Warnings []LintWarning
|
||||
}
|
||||
|
||||
type LintError struct {
|
||||
Line int
|
||||
Column int
|
||||
Message string
|
||||
Field string
|
||||
}
|
||||
|
||||
type LintWarning struct {
|
||||
Line int
|
||||
Column int
|
||||
Message string
|
||||
Field string
|
||||
}
|
||||
|
||||
type LintOptions struct {
|
||||
FilePaths []string
|
||||
Fix bool
|
||||
Format string // "text" or "json"
|
||||
}
|
||||
|
||||
// LintFiles validates v1beta3 troubleshoot specs for syntax and structural errors
|
||||
func LintFiles(opts LintOptions) ([]LintResult, error) {
|
||||
results := []LintResult{}
|
||||
|
||||
for _, filePath := range opts.FilePaths {
|
||||
result, err := lintFile(filePath, opts.Fix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func lintFile(filePath string, fix bool) (LintResult, error) {
|
||||
result := LintResult{
|
||||
FilePath: filePath,
|
||||
Errors: []LintError{},
|
||||
Warnings: []LintWarning{},
|
||||
}
|
||||
|
||||
// Read file
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return result, errors.Wrapf(err, "failed to read file %s", filePath)
|
||||
}
|
||||
|
||||
// Check if file contains template expressions
|
||||
hasTemplates := strings.Contains(string(content), "{{") && strings.Contains(string(content), "}}")
|
||||
|
||||
// Validate YAML syntax (but be lenient with templated files)
|
||||
var parsed map[string]interface{}
|
||||
yamlParseErr := yaml.Unmarshal(content, &parsed)
|
||||
if yamlParseErr != nil {
|
||||
// If the file has templates, YAML parsing may fail - that's expected
|
||||
// We'll still try to validate what we can
|
||||
if !hasTemplates {
|
||||
result.Errors = append(result.Errors, LintError{
|
||||
Line: extractLineFromError(yamlParseErr),
|
||||
Message: fmt.Sprintf("YAML syntax error: %s", yamlParseErr.Error()),
|
||||
})
|
||||
// Don't return yet - we want to try to fix this error
|
||||
// Continue to applyFixes at the end
|
||||
// Try to surface apiVersion issues even if YAML failed to parse
|
||||
// Detect via simple textual scan
|
||||
avLine, avValue := findAPIVersionLineAndValue(string(content))
|
||||
if avLine == 0 {
|
||||
result.Errors = append(result.Errors, LintError{
|
||||
Line: 0,
|
||||
Field: "apiVersion",
|
||||
Message: "Missing or empty 'apiVersion' field",
|
||||
})
|
||||
} else if avValue != constants.Troubleshootv1beta2Kind && avValue != constants.Troubleshootv1beta3Kind {
|
||||
result.Errors = append(result.Errors, LintError{
|
||||
Line: avLine,
|
||||
Field: "apiVersion",
|
||||
Message: fmt.Sprintf("Invalid 'apiVersion' value %q; expected %s or %s", avValue, constants.Troubleshootv1beta2Kind, constants.Troubleshootv1beta3Kind),
|
||||
})
|
||||
}
|
||||
if fix {
|
||||
fixed, err := applyFixes(filePath, string(content), result)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
if fixed {
|
||||
// Re-lint to verify fixes
|
||||
return lintFile(filePath, false)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
// For templated files, we can't parse YAML strictly, so just check template syntax
|
||||
result.Errors = append(result.Errors, checkTemplateSyntax(string(content))...)
|
||||
// Surface apiVersion issues via textual scan for templated files
|
||||
avLine, avValue := findAPIVersionLineAndValue(string(content))
|
||||
if avLine == 0 {
|
||||
result.Errors = append(result.Errors, LintError{
|
||||
Line: 0,
|
||||
Field: "apiVersion",
|
||||
Message: "Missing or empty 'apiVersion' field",
|
||||
})
|
||||
} else if avValue != constants.Troubleshootv1beta2Kind && avValue != constants.Troubleshootv1beta3Kind {
|
||||
result.Errors = append(result.Errors, LintError{
|
||||
Line: avLine,
|
||||
Field: "apiVersion",
|
||||
Message: fmt.Sprintf("Invalid 'apiVersion' value %q; expected %s or %s", avValue, constants.Troubleshootv1beta2Kind, constants.Troubleshootv1beta3Kind),
|
||||
})
|
||||
}
|
||||
// Continue to applyFixes for templates too
|
||||
if fix {
|
||||
fixed, err := applyFixes(filePath, string(content), result)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
if fixed {
|
||||
// Re-lint to verify fixes
|
||||
return lintFile(filePath, false)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Check required fields
|
||||
result.Errors = append(result.Errors, checkRequiredFields(parsed, string(content))...)
|
||||
|
||||
// Check template syntax
|
||||
result.Errors = append(result.Errors, checkTemplateSyntax(string(content))...)
|
||||
|
||||
// Check for kind-specific requirements
|
||||
if kind, ok := parsed["kind"].(string); ok {
|
||||
switch kind {
|
||||
case "Preflight":
|
||||
result.Errors = append(result.Errors, checkPreflightSpec(parsed, string(content))...)
|
||||
case "SupportBundle":
|
||||
result.Errors = append(result.Errors, checkSupportBundleSpec(parsed, string(content))...)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate apiVersion value if present
|
||||
if apiVersion, ok := parsed["apiVersion"].(string); ok && apiVersion != "" {
|
||||
if apiVersion != constants.Troubleshootv1beta2Kind && apiVersion != constants.Troubleshootv1beta3Kind {
|
||||
result.Errors = append(result.Errors, LintError{
|
||||
Line: findLineNumber(string(content), "apiVersion"),
|
||||
Field: "apiVersion",
|
||||
Message: fmt.Sprintf("Invalid 'apiVersion' value %q; expected %s or %s", apiVersion, constants.Troubleshootv1beta2Kind, constants.Troubleshootv1beta3Kind),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Check for common issues
|
||||
result.Warnings = append(result.Warnings, checkCommonIssues(parsed, string(content))...)
|
||||
|
||||
// Apply fixes if requested
|
||||
if fix && (len(result.Errors) > 0 || len(result.Warnings) > 0) {
|
||||
fixed, err := applyFixes(filePath, string(content), result)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
if fixed {
|
||||
// Re-lint to verify fixes
|
||||
return lintFile(filePath, false)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func checkRequiredFields(parsed map[string]interface{}, content string) []LintError {
|
||||
errors := []LintError{}
|
||||
|
||||
// Check apiVersion
|
||||
if apiVersion, ok := parsed["apiVersion"].(string); !ok || apiVersion == "" {
|
||||
errors = append(errors, LintError{
|
||||
Line: findLineNumber(content, "apiVersion"),
|
||||
Field: "apiVersion",
|
||||
Message: "Missing or empty 'apiVersion' field",
|
||||
})
|
||||
}
|
||||
|
||||
// Check kind
|
||||
if kind, ok := parsed["kind"].(string); !ok || kind == "" {
|
||||
errors = append(errors, LintError{
|
||||
Line: findLineNumber(content, "kind"),
|
||||
Field: "kind",
|
||||
Message: "Missing or empty 'kind' field",
|
||||
})
|
||||
} else if kind != "Preflight" && kind != "SupportBundle" {
|
||||
errors = append(errors, LintError{
|
||||
Line: findLineNumber(content, "kind"),
|
||||
Field: "kind",
|
||||
Message: fmt.Sprintf("Invalid kind '%s'. Must be 'Preflight' or 'SupportBundle'", kind),
|
||||
})
|
||||
}
|
||||
|
||||
// Check metadata
|
||||
if _, ok := parsed["metadata"]; !ok {
|
||||
errors = append(errors, LintError{
|
||||
Line: findLineNumber(content, "metadata"),
|
||||
Field: "metadata",
|
||||
Message: "Missing 'metadata' section",
|
||||
})
|
||||
} else if metadata, ok := parsed["metadata"].(map[string]interface{}); ok {
|
||||
if name, ok := metadata["name"].(string); !ok || name == "" {
|
||||
errors = append(errors, LintError{
|
||||
Line: findLineNumber(content, "name"),
|
||||
Field: "metadata.name",
|
||||
Message: "Missing or empty 'metadata.name' field",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Check spec
|
||||
if _, ok := parsed["spec"]; !ok {
|
||||
errors = append(errors, LintError{
|
||||
Line: findLineNumber(content, "spec"),
|
||||
Field: "spec",
|
||||
Message: "Missing 'spec' section",
|
||||
})
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
func checkTemplateSyntax(content string) []LintError {
|
||||
errors := []LintError{}
|
||||
lines := strings.Split(content, "\n")
|
||||
|
||||
// Check for unmatched braces
|
||||
for i, line := range lines {
|
||||
// Count opening and closing braces
|
||||
opening := strings.Count(line, "{{")
|
||||
closing := strings.Count(line, "}}")
|
||||
|
||||
if opening != closing {
|
||||
errors = append(errors, LintError{
|
||||
Line: i + 1,
|
||||
Message: fmt.Sprintf("Unmatched template braces: %d opening, %d closing", opening, closing),
|
||||
})
|
||||
}
|
||||
|
||||
// Check for common template syntax issues
|
||||
// Look for templates that might be missing the leading dot
|
||||
if strings.Contains(line, "{{") && strings.Contains(line, "}}") {
|
||||
// Extract template expressions
|
||||
templateExpr := extractTemplateBetweenBraces(line)
|
||||
for _, expr := range templateExpr {
|
||||
trimmed := strings.TrimSpace(expr)
|
||||
|
||||
// Skip empty expressions
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip control structures (if, else, end, range, with, etc.)
|
||||
if isControlStructure(trimmed) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip comments: {{/* ... */}}
|
||||
if strings.HasPrefix(trimmed, "/*") || strings.HasPrefix(trimmed, "*/") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip template variables (start with $)
|
||||
if strings.HasPrefix(trimmed, "$") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip expressions that start with a dot (valid references)
|
||||
if strings.HasPrefix(trimmed, ".") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip string literals
|
||||
if strings.HasPrefix(trimmed, "\"") || strings.HasPrefix(trimmed, "'") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip numeric literals
|
||||
if regexp.MustCompile(`^[0-9]+$`).MatchString(trimmed) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip function calls (contain parentheses or pipes)
|
||||
if strings.Contains(trimmed, "(") || strings.Contains(trimmed, "|") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip known Helm functions/keywords
|
||||
helmFunctions := []string{"toYaml", "toJson", "include", "required", "default", "quote", "nindent", "indent", "upper", "lower", "trim"}
|
||||
isFunction := false
|
||||
for _, fn := range helmFunctions {
|
||||
if strings.HasPrefix(trimmed, fn+" ") || trimmed == fn {
|
||||
isFunction = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isFunction {
|
||||
continue
|
||||
}
|
||||
|
||||
// If we got here, it might be missing a leading dot
|
||||
errors = append(errors, LintError{
|
||||
Line: i + 1,
|
||||
Message: fmt.Sprintf("Template expression may be missing leading dot: {{ %s }}", expr),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
func checkPreflightSpec(parsed map[string]interface{}, content string) []LintError {
|
||||
errors := []LintError{}
|
||||
|
||||
spec, ok := parsed["spec"].(map[string]interface{})
|
||||
if !ok {
|
||||
return errors
|
||||
}
|
||||
|
||||
// Check for analyzers
|
||||
analyzers, hasAnalyzers := spec["analyzers"]
|
||||
if !hasAnalyzers {
|
||||
errors = append(errors, LintError{
|
||||
Line: findLineNumber(content, "spec:"),
|
||||
Field: "spec.analyzers",
|
||||
Message: "Preflight spec must contain 'analyzers'",
|
||||
})
|
||||
} else if analyzersList, ok := analyzers.([]interface{}); ok {
|
||||
if len(analyzersList) == 0 {
|
||||
errors = append(errors, LintError{
|
||||
Line: findLineNumber(content, "analyzers"),
|
||||
Field: "spec.analyzers",
|
||||
Message: "Preflight spec must have at least one analyzer",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
func checkSupportBundleSpec(parsed map[string]interface{}, content string) []LintError {
|
||||
errors := []LintError{}
|
||||
|
||||
spec, ok := parsed["spec"].(map[string]interface{})
|
||||
if !ok {
|
||||
return errors
|
||||
}
|
||||
|
||||
// Check for collectors
|
||||
collectors, hasCollectors := spec["collectors"]
|
||||
_, hasHostCollectors := spec["hostCollectors"]
|
||||
|
||||
if !hasCollectors && !hasHostCollectors {
|
||||
errors = append(errors, LintError{
|
||||
Line: findLineNumber(content, "spec:"),
|
||||
Field: "spec.collectors",
|
||||
Message: "SupportBundle spec must contain 'collectors' or 'hostCollectors'",
|
||||
})
|
||||
} else {
|
||||
// Check if collectors list is empty
|
||||
if hasCollectors {
|
||||
if collectorsList, ok := collectors.([]interface{}); ok && len(collectorsList) == 0 {
|
||||
errors = append(errors, LintError{
|
||||
Line: findLineNumber(content, "collectors"),
|
||||
Field: "spec.collectors",
|
||||
Message: "Collectors list is empty",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
func checkCommonIssues(parsed map[string]interface{}, content string) []LintWarning {
|
||||
warnings := []LintWarning{}
|
||||
|
||||
// Check for missing docStrings in analyzers
|
||||
spec, ok := parsed["spec"].(map[string]interface{})
|
||||
if !ok {
|
||||
return warnings
|
||||
}
|
||||
|
||||
if analyzers, ok := spec["analyzers"].([]interface{}); ok {
|
||||
for i, analyzer := range analyzers {
|
||||
if analyzerMap, ok := analyzer.(map[string]interface{}); ok {
|
||||
if _, hasDocString := analyzerMap["docString"]; !hasDocString {
|
||||
warnings = append(warnings, LintWarning{
|
||||
Line: findAnalyzerLine(content, i),
|
||||
Field: fmt.Sprintf("spec.analyzers[%d].docString", i),
|
||||
Message: "Analyzer missing docString (recommended for v1beta3)",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return warnings
|
||||
}
|
||||
|
||||
func applyFixes(filePath, content string, result LintResult) (bool, error) {
|
||||
fixed := false
|
||||
newContent := content
|
||||
lines := strings.Split(newContent, "\n")
|
||||
|
||||
// Determine desired apiVersion for fixes
|
||||
hasTemplates := strings.Contains(content, "{{") && strings.Contains(content, "}}")
|
||||
hasDocStrings := strings.Contains(content, "docString:")
|
||||
desiredAPIVersion := constants.Troubleshootv1beta2Kind
|
||||
if hasTemplates || hasDocStrings {
|
||||
desiredAPIVersion = constants.Troubleshootv1beta3Kind
|
||||
}
|
||||
|
||||
// Sort errors by line number (descending) to avoid line number shifts when editing
|
||||
errorsByLine := make(map[int][]LintError)
|
||||
for _, err := range result.Errors {
|
||||
if err.Line > 0 {
|
||||
errorsByLine[err.Line] = append(errorsByLine[err.Line], err)
|
||||
}
|
||||
}
|
||||
|
||||
// Process errors line by line
|
||||
for lineNum, errs := range errorsByLine {
|
||||
if lineNum > len(lines) {
|
||||
continue
|
||||
}
|
||||
|
||||
line := lines[lineNum-1]
|
||||
originalLine := line
|
||||
|
||||
for _, err := range errs {
|
||||
// Fix 1: Add missing colon
|
||||
// YAML parsers often report the error on the line AFTER the actual problem
|
||||
if strings.Contains(err.Message, "could not find expected ':'") {
|
||||
// Check current line first
|
||||
if !strings.Contains(line, ":") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||
line = indent + trimmed + ":"
|
||||
fixed = true
|
||||
} else if lineNum > 1 {
|
||||
// Check previous line (where the colon is likely missing)
|
||||
prevLine := lines[lineNum-2]
|
||||
if !strings.Contains(prevLine, ":") && strings.TrimSpace(prevLine) != "" {
|
||||
trimmed := strings.TrimSpace(prevLine)
|
||||
indent := prevLine[:len(prevLine)-len(strings.TrimLeft(prevLine, " \t"))]
|
||||
lines[lineNum-2] = indent + trimmed + ":"
|
||||
fixed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fix 2: Add missing leading dot in template expressions
|
||||
if strings.Contains(err.Message, "Template expression may be missing leading dot:") {
|
||||
// Extract the expression from the error message
|
||||
re := regexp.MustCompile(`Template expression may be missing leading dot: \{\{ (.+?) \}\}`)
|
||||
matches := re.FindStringSubmatch(err.Message)
|
||||
if len(matches) > 1 {
|
||||
badExpr := matches[1]
|
||||
// Add the leading dot
|
||||
fixedExpr := "." + badExpr
|
||||
// Replace in the line
|
||||
line = strings.Replace(line, "{{ "+badExpr+" }}", "{{ "+fixedExpr+" }}", 1)
|
||||
line = strings.Replace(line, "{{"+badExpr+"}}", "{{"+fixedExpr+"}}", 1)
|
||||
line = strings.Replace(line, "{{- "+badExpr+" }}", "{{- "+fixedExpr+" }}", 1)
|
||||
line = strings.Replace(line, "{{- "+badExpr+" -}}", "{{- "+fixedExpr+" -}}", 1)
|
||||
fixed = true
|
||||
}
|
||||
}
|
||||
|
||||
// Fix 3: Replace invalid apiVersion value with desiredAPIVersion
|
||||
if strings.Contains(err.Message, "Invalid 'apiVersion' value") && err.Field == "apiVersion" {
|
||||
if strings.Contains(line, "apiVersion:") {
|
||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||
line = indent + "apiVersion: " + desiredAPIVersion
|
||||
fixed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the line if it changed
|
||||
if line != originalLine {
|
||||
lines[lineNum-1] = line
|
||||
}
|
||||
}
|
||||
|
||||
// Fix 4: Add missing required top-level fields
|
||||
for _, err := range result.Errors {
|
||||
if err.Field == "apiVersion" && strings.Contains(err.Message, "Missing or empty 'apiVersion'") {
|
||||
// Replace existing empty apiVersion line if present; otherwise prepend
|
||||
if avLine, avVal := findAPIVersionLineAndValue(newContent); avLine > 0 && strings.TrimSpace(avVal) == "" {
|
||||
line := lines[avLine-1]
|
||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||
lines[avLine-1] = indent + "apiVersion: " + desiredAPIVersion
|
||||
} else {
|
||||
lines = append([]string{"apiVersion: " + desiredAPIVersion}, lines...)
|
||||
}
|
||||
fixed = true
|
||||
} else if err.Field == "kind" && strings.Contains(err.Message, "Missing or empty 'kind'") {
|
||||
// Try to determine if it should be Preflight or SupportBundle based on filename
|
||||
kind := "Preflight"
|
||||
if strings.Contains(strings.ToLower(filePath), "bundle") {
|
||||
kind = "SupportBundle"
|
||||
}
|
||||
// Add kind after apiVersion
|
||||
insertIndex := 0
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, "apiVersion:") {
|
||||
insertIndex = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
newLines := make([]string, 0, len(lines)+1)
|
||||
newLines = append(newLines, lines[:insertIndex]...)
|
||||
newLines = append(newLines, "kind: "+kind)
|
||||
newLines = append(newLines, lines[insertIndex:]...)
|
||||
lines = newLines
|
||||
fixed = true
|
||||
} else if err.Field == "metadata" && strings.Contains(err.Message, "Missing 'metadata'") {
|
||||
// Add metadata section after kind
|
||||
insertIndex := 0
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, "kind:") {
|
||||
insertIndex = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
newLines := make([]string, 0, len(lines)+2)
|
||||
newLines = append(newLines, lines[:insertIndex]...)
|
||||
newLines = append(newLines, "metadata:")
|
||||
newLines = append(newLines, " name: my-spec")
|
||||
newLines = append(newLines, lines[insertIndex:]...)
|
||||
lines = newLines
|
||||
fixed = true
|
||||
}
|
||||
}
|
||||
|
||||
// Write fixed content back to file if changes were made
|
||||
if fixed {
|
||||
newContent = strings.Join(lines, "\n")
|
||||
if err := os.WriteFile(filePath, []byte(newContent), 0644); err != nil {
|
||||
return false, errors.Wrapf(err, "failed to write fixed content to %s", filePath)
|
||||
}
|
||||
}
|
||||
|
||||
return fixed, nil
|
||||
}
|
||||
|
||||
func findLineNumber(content, search string) int {
|
||||
lines := strings.Split(content, "\n")
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, search) {
|
||||
return i + 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// findAPIVersionLineAndValue locates the first line that declares apiVersion and returns its
|
||||
// 1-based line number and the trimmed value to the right of the colon. Returns (0, "") if not found.
|
||||
func findAPIVersionLineAndValue(content string) (int, string) {
|
||||
lines := strings.Split(content, "\n")
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "apiVersion:") {
|
||||
// extract value after the first colon
|
||||
parts := strings.SplitN(trimmed, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
value := strings.TrimSpace(parts[1])
|
||||
return i + 1, value
|
||||
}
|
||||
return i + 1, ""
|
||||
}
|
||||
}
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
func findAnalyzerLine(content string, index int) int {
|
||||
lines := strings.Split(content, "\n")
|
||||
analyzerCount := 0
|
||||
inAnalyzers := false
|
||||
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, "analyzers:") {
|
||||
inAnalyzers = true
|
||||
continue
|
||||
}
|
||||
if inAnalyzers && strings.HasPrefix(strings.TrimSpace(line), "- ") {
|
||||
if analyzerCount == index {
|
||||
return i + 1
|
||||
}
|
||||
analyzerCount++
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func extractLineFromError(err error) int {
|
||||
// Try to extract line number from YAML error message
|
||||
re := regexp.MustCompile(`line (\d+)`)
|
||||
matches := re.FindStringSubmatch(err.Error())
|
||||
if len(matches) > 1 {
|
||||
var line int
|
||||
fmt.Sscanf(matches[1], "%d", &line)
|
||||
return line
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// extractTemplateBetweenBraces extracts template expressions from a line
|
||||
func extractTemplateBetweenBraces(line string) []string {
|
||||
var expressions []string
|
||||
// Match {{ ... }} with optional whitespace trimming (-), including comments {{/* */}}
|
||||
re := regexp.MustCompile(`\{\{-?\s*(.+?)\s*-?\}\}`)
|
||||
matches := re.FindAllStringSubmatch(line, -1)
|
||||
for _, match := range matches {
|
||||
if len(match) > 1 {
|
||||
// Clean up the expression
|
||||
expr := match[1]
|
||||
// Remove */ at the end if it's part of a comment
|
||||
expr = strings.TrimSuffix(strings.TrimSpace(expr), "*/")
|
||||
expressions = append(expressions, expr)
|
||||
}
|
||||
}
|
||||
return expressions
|
||||
}
|
||||
|
||||
// isControlStructure checks if a template expression is a control structure
|
||||
func isControlStructure(expr string) bool {
|
||||
trimmed := strings.TrimSpace(expr)
|
||||
controlKeywords := []string{"if", "else", "end", "range", "with", "define", "template", "block", "include"}
|
||||
for _, keyword := range controlKeywords {
|
||||
if strings.HasPrefix(trimmed, keyword+" ") || trimmed == keyword {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// FormatResults formats lint results for output
|
||||
func FormatResults(results []LintResult, format string) string {
|
||||
if format == "json" {
|
||||
return formatJSON(results)
|
||||
}
|
||||
return formatText(results)
|
||||
}
|
||||
|
||||
func formatText(results []LintResult) string {
|
||||
var output strings.Builder
|
||||
totalErrors := 0
|
||||
totalWarnings := 0
|
||||
|
||||
for _, result := range results {
|
||||
if len(result.Errors) == 0 && len(result.Warnings) == 0 {
|
||||
output.WriteString(fmt.Sprintf("✓ %s: No issues found\n", result.FilePath))
|
||||
continue
|
||||
}
|
||||
|
||||
output.WriteString(fmt.Sprintf("\n%s:\n", result.FilePath))
|
||||
|
||||
for _, err := range result.Errors {
|
||||
output.WriteString(fmt.Sprintf(" ✗ Error (line %d): %s\n", err.Line, err.Message))
|
||||
if err.Field != "" {
|
||||
output.WriteString(fmt.Sprintf(" Field: %s\n", err.Field))
|
||||
}
|
||||
totalErrors++
|
||||
}
|
||||
|
||||
for _, warn := range result.Warnings {
|
||||
output.WriteString(fmt.Sprintf(" ⚠ Warning (line %d): %s\n", warn.Line, warn.Message))
|
||||
if warn.Field != "" {
|
||||
output.WriteString(fmt.Sprintf(" Field: %s\n", warn.Field))
|
||||
}
|
||||
totalWarnings++
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString(fmt.Sprintf("\nSummary: %d error(s), %d warning(s) across %d file(s)\n", totalErrors, totalWarnings, len(results)))
|
||||
|
||||
return output.String()
|
||||
}
|
||||
|
||||
func formatJSON(results []LintResult) string {
|
||||
// Simple JSON formatting without importing encoding/json
|
||||
var output strings.Builder
|
||||
output.WriteString("{\n")
|
||||
output.WriteString(" \"results\": [\n")
|
||||
|
||||
for i, result := range results {
|
||||
output.WriteString(" {\n")
|
||||
output.WriteString(fmt.Sprintf(" \"filePath\": %q,\n", result.FilePath))
|
||||
output.WriteString(" \"errors\": [\n")
|
||||
|
||||
for j, err := range result.Errors {
|
||||
output.WriteString(" {\n")
|
||||
output.WriteString(fmt.Sprintf(" \"line\": %d,\n", err.Line))
|
||||
output.WriteString(fmt.Sprintf(" \"column\": %d,\n", err.Column))
|
||||
output.WriteString(fmt.Sprintf(" \"message\": %q,\n", err.Message))
|
||||
output.WriteString(fmt.Sprintf(" \"field\": %q\n", err.Field))
|
||||
output.WriteString(" }")
|
||||
if j < len(result.Errors)-1 {
|
||||
output.WriteString(",")
|
||||
}
|
||||
output.WriteString("\n")
|
||||
}
|
||||
|
||||
output.WriteString(" ],\n")
|
||||
output.WriteString(" \"warnings\": [\n")
|
||||
|
||||
for j, warn := range result.Warnings {
|
||||
output.WriteString(" {\n")
|
||||
output.WriteString(fmt.Sprintf(" \"line\": %d,\n", warn.Line))
|
||||
output.WriteString(fmt.Sprintf(" \"column\": %d,\n", warn.Column))
|
||||
output.WriteString(fmt.Sprintf(" \"message\": %q,\n", warn.Message))
|
||||
output.WriteString(fmt.Sprintf(" \"field\": %q\n", warn.Field))
|
||||
output.WriteString(" }")
|
||||
if j < len(result.Warnings)-1 {
|
||||
output.WriteString(",")
|
||||
}
|
||||
output.WriteString("\n")
|
||||
}
|
||||
|
||||
output.WriteString(" ]\n")
|
||||
output.WriteString(" }")
|
||||
if i < len(results)-1 {
|
||||
output.WriteString(",")
|
||||
}
|
||||
output.WriteString("\n")
|
||||
}
|
||||
|
||||
output.WriteString(" ]\n")
|
||||
output.WriteString("}\n")
|
||||
|
||||
return output.String()
|
||||
}
|
||||
|
||||
// HasErrors returns true if any of the results contain errors
|
||||
func HasErrors(results []LintResult) bool {
|
||||
for _, result := range results {
|
||||
if len(result.Errors) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user