mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-02-14 10:19:54 +00:00
V1beta3 (#1873)
* Change workflow branch from 'main' to 'v1beta3' * Auto updater (#1849) * added auto updater * updated docs * commit to trigger actions * Auto-collectors: foundational discovery, image metadata, CLI integrat… (#1845) * Auto-collectors: foundational discovery, image metadata, CLI integration; reset PRD markers * Address PR review feedback - Implement missing namespace exclude patterns functionality - Fix image facts collector to use empty Data field instead of static string - Correct APIVersion to use troubleshoot.sh/v1beta2 consistently * Fix bug bot issues: API parsing, EOF error, and API group corrections - Fix RBAC API parsing errors in rbac_checker.go (getAPIGroup/getAPIVersion functions) - Fix FakeReader EOF error to use standard io.EOF instead of custom error - Fix incorrect API group from troubleshoot.sh to troubleshoot.replicated.com in run.go These changes address the issues identified by the bug bot and ensure proper interface compliance and consistent API group usage. * Fix multiple bug bot issues - Fix RBAC API parsing errors in rbac_checker.go (getAPIGroup/getAPIVersion functions) - Fix FakeReader EOF error to use standard io.EOF instead of custom error - Fix incorrect API group from troubleshoot.sh to troubleshoot.replicated.com in run.go - Fix image facts collector Data field to contain structured JSON instead of static strings These changes address all issues identified by the bug bot and ensure proper interface compliance, consistent API usage, and meaningful data fields. * Update auto_discovery.go * Fix TODO comments in Auto-collector section Fixed 3 of 4 TODOs as requested in PR review: 1. pkg/collect/images/registry_client.go (line 46): - Implement custom CA certificate loading - Add x509 import and certificate parsing logic - Enables image collection from private registries with custom CAs 2. cmd/troubleshoot/cli/diff.go (line 209): - Implement bundle file count functionality - Add tar/gzip imports and getFileCountFromBundle() function - Properly counts files in support bundle archives (.gz/.tgz) 3. cmd/troubleshoot/cli/run.go (line 338): - Replace TODO with clarifying comment about RemoteCollectors usage - Confirmed RemoteCollectors are still actively used in preflights The 4th TODO (diff.go line 196) is left as-is since it's explicitly marked as Phase 4 future work (Support Bundle Differencing implementation). Addresses PR review feedback about unimplemented TODO comments. --------- Co-authored-by: Benjamin Yang <benjaminyang@Benjamins-MacBook-Pro.local> * resetting make targets and github workflows to support v1beta3 releas… (#1853) * resetting make targets and github workflows to support v1beta3 release later * removing generate * remove * removing * removing * Support bundle diff (#1855) implemented support bundle diff command * Preflight docs and template subcommands (#1847) * Added docs and template subcommands with test files * uses helm templating preflight yaml files * merge doc requirements for multiple inputs * Helm aware rendering and markdown output * v1beta3 yaml structure better mirrors beta2 * Update sample-preflight-templated.yaml * Added docs and template subcommands with test files * uses helm templating preflight yaml files * merge doc requirements for multiple inputs * Helm aware rendering and markdown output * v1beta3 yaml structure better mirrors beta2 * Update sample-preflight-templated.yaml * Added/updated documentation on subcommands * Update docs.go * commit to trigger actions * Updated yaml spec (#1851) * v1beta3 spec can be read by preflight * added test files for ease of testing * updated v1beta3 guide doc and added tests * fixed not removing tmp files from v1beta3 processing * created v1beta2 to v1beta3 converter * Updated yaml spec (#1863) * v1beta3 spec can be read by preflight * added test files for ease of testing * v1beta3 renderer fixes * fixed gitignore issue * Auto support bundle upload (#1860) * basic auto uploading support bundles * added upload command * added default vendor endpoint * added auth system from replicated cli * fixed case sensitivity issue in YAML parsing * support bundle uploads for end customers * app slug flag and detection without licenseID * moved v1beta3 examples to proper directory * does not auto update for package managers (#1850) * V1beta3 cleanup (#1869) * moving some files around * more cleanup * removing more unused * update ci for v1beta3 (#1870) * fmt: * removing unused examples * add a v1beta3 fixture * removing coverage reporting * adding brew (#1872) * Fixing testing errors (#1871) fix: resolve failing unit tests and diff consistency in v1beta3 - Fix readLinesFromReader to return lines WITH newlines (like difflib.SplitLines) - Update test expectations to match correct function behavior with newlines - This ensures consistency between streaming and non-streaming diff paths - Fix timeout test by changing from 10ms to 500ms to eliminate flaky failures Fixes TestReadLinesFromReader and Test_loadSupportBundleSpecsFromURIs_TimeoutError Resolves diff output inconsistency between code paths * Fix/exec textanalyze path clean (#1865) * created roadmap and yaml claude agent * Update roadmap.md * Fix textAnalyze analyzer to auto-match exec collector nested paths - Auto-detect exec output files (*-stdout.txt, *-stderr.txt, *-errors.json) - Convert simple filenames to wildcard patterns automatically - Preserve existing wildcard patterns - Fixes 'No matching file' errors for exec + textAnalyze workflows --------- Co-authored-by: Noah Campbell <noah.edward.campbell@gmail.com> * bump goreleaser to v2 * remove collect binary and risc binary * remove this check * add debug logging * larger runner for release * dropping goreleaser * fix syntax * fix syntax * goreleaser * larger * prerelease auto and more * publish to directory: * some more goreleaser/homebrew stuffs * removing risc * bump example * Advanced analysis clean (#1868) * created roadmap and yaml claude agent * Update roadmap.md * feat: Clean advanced analysis implementation - core agents, engine, artifacts * Remove unrelated files - keep only advanced analysis implementation * fix: Fix goroutine leak in hosted agent rate limiter - Added stop channel and stopped flag to RateLimiter struct - Modified replenishTokens to listen for stop signal and exit cleanly - Added Stop() method to gracefully shutdown rate limiter - Added Stop() method to HostedAgent to cleanup rate limiter on shutdown Fixes cursor bot issue: Rate Limiter Goroutine Leak * fix: Fix analyzer config and model validation bugs Bug 1: Analyzer Config Missing File Path - Added filePath to DeploymentStatus analyzer config in convertAnalyzerToSpec - Sets namespace-specific path (cluster-resources/deployments/{namespace}.json) - Falls back to generic path (cluster-resources/deployments.json) if no namespace - Fixes LocalAgent.analyzeDeploymentStatus backward compatibility Bug 2: HealthCheck Fails Model Validation - Changed Ollama model validation from prefix match to exact match - Prevents false positives where llama2:13b would match request for llama2:7b - Ensures agent only reports healthy when exact model is available Both fixes address cursor bot reported issues and maintain backward compatibility. * fixing lint errors * fixing lint errors * adding CLI flags * fix: resolve linting errors for CI - Remove unnecessary nil check in host_kernel_configs.go (len() for nil slices is zero) - Remove unnecessary fmt.Sprintf() calls in ceph.go for static strings - Apply go fmt formatting fixes Fixes failing lint CI check * fix: resolve CI failures in build-test workflow and Ollama tests 1. Fix GitHub Actions workflow logic error: - Replace problematic contains() expression with explicit job result checks - Properly handle failure and cancelled states for each job - Prevents false positive failures in success summary job 2. Fix Ollama agent parseLLMResponse panics: - Add proper error handling for malformed JSON in LLM responses - Return error when JSON is found but invalid (instead of silent fallback) - Add error when no meaningful content can be parsed from response - Prevents nil pointer dereference in test assertions Fixes failing build-test/success and build-test/test CI checks * fix: resolve all CI failures and cursor bot issues 1. Fix disable-ollama flag logic bug: - Remove disable-ollama from advanced analysis trigger condition - Prevents unintended advanced analysis mode when no agents registered - Allows proper fallback to legacy analysis 2. Fix diff test consistency: - Update test expectations to match function behavior (lines with newlines) - Ensures consistency between streaming and non-streaming diff paths 3. Fix Ollama agent error handling: - Add proper error return for malformed JSON in LLM responses - Add meaningful content validation for markdown parsing - Prevents nil pointer panics in test assertions 4. Fix analysis engine mock agent: - Mock agent now processes and returns results for all provided analyzers - Fixes test expectation mismatch (expected 8 results, got 1) Resolves all failing CI checks: lint, test, and success workflow logic --------- Co-authored-by: Noah Campbell <noah.edward.campbell@gmail.com> * Auto-Collect (#1867) * Fix auto-collector missing files issue - Add KOTS-aware detection for diagnostic files - Replace silent RBAC filtering with user warnings - Enhance error file collection for troubleshooting - Achieve parity with traditional support bundles Resolves issue where auto-collector was missing: - KOTS diagnostic files (now 4 vs 3) - ConfigMaps (now 6 vs 6) - Maintains superior log collection (24 vs 0) Final result: [SUCCESS] comprehensive collection achieved * fixing bugbog * fix: resolve production readiness issues in auto-collect branch 1. Fix diff test expectations (lines should have newlines for difflib consistency) 2. Fix preflight tests to use existing v1beta3 example file 3. Fix autodiscovery test context parameter (function signature update) Resolves TestReadLinesFromReader and preflight v1beta3 test failures * fix: resolve autodiscovery tests and cursor bot image matching issues 1. Fix cursor bot image matching bug in isKotsadmImage: - Replace flawed prefix matching with proper image component detection - Handle private registries correctly (registry.company.com/kotsadm/kotsadm:v1.0.0) - Prevent false positives with proper delimiter checking - Add helper functions: containsImageComponent, splitImagePath, removeTagAndDigest 2. Fix autodiscovery test failures: - Add TestMode flag to DiscoveryOptions to control KOTS diagnostic collection - Tests use TestMode=true to get only foundational collectors (no KOTS diagnostics) - Preserves production behavior while enabling clean testing Resolves failing TestDiscoverer_DiscoverFoundational tests and cursor bot issues * Cron job clean (#1862) * created roadmap and yaml claude agent * Update roadmap.md * chore(deps): bump sigstore/cosign-installer from 3.9.2 to 3.10.0 (#1857) Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.9.2 to 3.10.0. - [Release notes](https://github.com/sigstore/cosign-installer/releases) - [Commits](https://github.com/sigstore/cosign-installer/compare/v3.9.2...v3.10.0) --- updated-dependencies: - dependency-name: sigstore/cosign-installer dependency-version: 3.10.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump the security group with 2 updates (#1858) Bumps the security group with 2 updates: [github.com/vmware-tanzu/velero](https://github.com/vmware-tanzu/velero) and [helm.sh/helm/v3](https://github.com/helm/helm). Updates `github.com/vmware-tanzu/velero` from 1.16.2 to 1.17.0 - [Release notes](https://github.com/vmware-tanzu/velero/releases) - [Changelog](https://github.com/vmware-tanzu/velero/blob/main/CHANGELOG.md) - [Commits](https://github.com/vmware-tanzu/velero/compare/v1.16.2...v1.17.0) Updates `helm.sh/helm/v3` from 3.18.6 to 3.19.0 - [Release notes](https://github.com/helm/helm/releases) - [Commits](https://github.com/helm/helm/compare/v3.18.6...v3.19.0) --- updated-dependencies: - dependency-name: github.com/vmware-tanzu/velero dependency-version: 1.17.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: security - dependency-name: helm.sh/helm/v3 dependency-version: 3.19.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: security ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump helm.sh/helm/v3 from 3.18.6 to 3.19.0 in /examples/sdk/helm-template in the security group (#1859) chore(deps): bump helm.sh/helm/v3 Bumps the security group in /examples/sdk/helm-template with 1 update: [helm.sh/helm/v3](https://github.com/helm/helm). Updates `helm.sh/helm/v3` from 3.18.6 to 3.19.0 - [Release notes](https://github.com/helm/helm/releases) - [Commits](https://github.com/helm/helm/compare/v3.18.6...v3.19.0) --- updated-dependencies: - dependency-name: helm.sh/helm/v3 dependency-version: 3.19.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: security ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Add cron job support bundle scheduler Complete implementation with K8s integration: - pkg/schedule/job.go: Job management and persistence - pkg/schedule/daemon.go: Real-time scheduler daemon - pkg/schedule/cli.go: CLI commands (create, list, delete, daemon) - pkg/schedule/schedule_test.go: Comprehensive unit tests - cmd/troubleshoot/cli/root.go: CLI integration * fixing bugbot * Fix all bugbot errors: auto-update stability, job cooldown timing, and daemon execution * Deleting Agent * removed unused flags * fixing auto-upload * fixing markdown files * namespace not required flag for auto collectors to work * loosened cron job validation * writes logs to logfile * fix: resolve autoFromEnv variable scoping issue for CI - Ensure autoFromEnv variable and its usage are in correct scope - Fix build errors: declared and not used / undefined variable - All functionality preserved and tested locally - Force add to override gitignore --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Noah Campbell <noah.edward.campbell@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat: clean tokenization system implementation (#1874) Core tokenization functionality with minimal file changes: ✅ Core Features: - Intelligent tokenization engine (tokenizer.go) - Context-aware secret classification (PASSWORD, APIKEY, DATABASE, etc.) - Cross-file correlation with deterministic HMAC-SHA256 tokens - Optional encrypted mapping for token→original value resolution ✅ Integration: - CLI flags: --tokenize, --redaction-map, --encrypt-redaction-map - Updated all redactor types: literal, single-line, multi-line, YAML - Support bundle integration with auto-upload compatibility - Backward compatibility: preserves ***HIDDEN*** when disabled ✅ Production Ready: - Only 11 essential files (vs 31 in original PR) - No excessive test files or documentation - Clean build, all functionality verified - Maintains existing redaction behavior by default Token format: ***TOKEN_<TYPE>_<HASH>*** (e.g., ***TOKEN_PASSWORD_A1B2C3***) * Removes silent failing (#1877) * preserves stdout and stderr from collectors * Delete eliminate-silent-failures.md * Update host_kernel_modules_test.go * added error logs when a collector fails to start * Update host_filesystem_performance_linux.go * fixed error saving logic inconsistency * Update collect.go * Improved error handling for support bundles and redactors for windows (#1878) * improved error handling and window locking * Delete all-windows-collectors.yaml * addressing bugbot concerns * Update host_tcpportstatus.go * Update redact.go * Add regression test suite to github actions * Update regression-test.yaml * Update regression-test.yaml * Update regression-test.yaml * create test/output directory * handle node-specific files and multiple report arguments * simplify comparison to detect code regressions only * handle empty structural_compare rules * removed v1beta3 branch from github workflow * Update Makefile * removed outdated actions * Update Makefile --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Noah Campbell <noah.edward.campbell@gmail.com> Co-authored-by: Benjamin Yang <82779168+bennyyang11@users.noreply.github.com> Co-authored-by: Benjamin Yang <benjaminyang@Benjamins-MacBook-Pro.local> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
163
.claude/agents/preflight-check-writer.md
Normal file
163
.claude/agents/preflight-check-writer.md
Normal file
@@ -0,0 +1,163 @@
|
||||
---
|
||||
name: preflight-v1beta3-writer
|
||||
description: MUST BE USED PROACTIVELY WHEN WRITING PREFLIGHT CHECKS.Writes Troubleshoot v1beta3 Preflight YAML templates with strict .Values templating,
|
||||
optional docStrings, and values-driven toggles. Uses repo examples for structure
|
||||
and analyzer coverage. Produces ready-to-run, templated specs and companion values.
|
||||
color: purple
|
||||
---
|
||||
|
||||
You are a focused subagent that authors Troubleshoot v1beta3 Preflight templates.
|
||||
|
||||
Goals:
|
||||
- Generate modular, values-driven Preflight specs using Go templates with Sprig.
|
||||
- Use strict `.Values.*` references (no implicit defaults inside templates).
|
||||
- Guard optional analyzers with `{{- if .Values.<feature>.enabled }}`.
|
||||
- Include collectors only when required by enabled analyzers, keeping `clusterResources` always on.
|
||||
- Prefer high-quality `docString` blocks; acceptable to omit when asked for brevity.
|
||||
- Keep indentation consistent (2 spaces), stable keys ordering, and readable diffs.
|
||||
|
||||
Reference files in this repository:
|
||||
- `v1beta3-all-analyzers.yaml` (comprehensive example template)
|
||||
- `docs/v1beta3-guide.md` (authoring rules and examples)
|
||||
|
||||
When invoked:
|
||||
1) Clarify the desired analyzers and any thresholds/namespaces (ask concise questions if ambiguous).
|
||||
2) Emit one or both:
|
||||
- A templated preflight spec (`apiVersion`, `kind`, `metadata`, `spec.collectors`, `spec.analyzers`).
|
||||
- A companion values snippet covering all `.Values.*` keys used.
|
||||
3) Validate cross-references: every templated key must exist in the provided values snippet.
|
||||
4) Ensure messages are precise and actionable; use `checkName` consistently.
|
||||
|
||||
Conventions to follow:
|
||||
- Header:
|
||||
- `apiVersion: troubleshoot.sh/v1beta3`
|
||||
- `kind: Preflight`
|
||||
- `metadata.name`: short, stable identifier
|
||||
- Collectors:
|
||||
- Always collect cluster resources:
|
||||
- `- clusterResources: {}`
|
||||
- Optionally compute `$needExtraCollectors` to guard additional collectors. Keep logic simple and readable.
|
||||
- Analyzers:
|
||||
- Each optional analyzer is gated with `{{- if .Values.<feature>.enabled }}`.
|
||||
- Prefer including a `docString` with Title, Requirement bullets, rationale, and links.
|
||||
- Use `checkName` for stable labels.
|
||||
- Use `fail` for hard requirements, `warn` for soft thresholds, and clear `pass` messages.
|
||||
|
||||
Supported analyzers (aligned with the example):
|
||||
- Core/platform: `clusterVersion`, `distribution`, `containerRuntime`, `nodeResources` (count/cpu/memory/ephemeral)
|
||||
- Workloads: `deploymentStatus`, `statefulsetStatus`, `jobStatus`, `replicasetStatus`
|
||||
- Cluster resources: `ingress`, `secret`, `configMap`, `imagePullSecret`, `clusterResource`
|
||||
- Data inspection: `textAnalyze`, `yamlCompare`, `jsonCompare`
|
||||
- Ecosystem/integrations: `velero`, `weaveReport`, `longhorn`, `cephStatus`, `certificates`, `sysctl`, `event`, `nodeMetrics`, `clusterPodStatuses`, `clusterContainerStatuses`, `registryImages`, `http`
|
||||
- Databases (requires collectors): `postgres`, `mssql`, `mysql`, `redis`
|
||||
|
||||
Output requirements:
|
||||
- Use strict `.Values` references (no `.Values.analyzers.*` paths) and ensure they match the values snippet.
|
||||
- Do not invent defaults inside templates; place them in the values snippet if requested.
|
||||
- Preserve 2-space indentation; avoid tabs; wrap long lines.
|
||||
- Where lists are templated, prefer clear `range` blocks.
|
||||
|
||||
Example skeleton (template):
|
||||
```yaml
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: Preflight
|
||||
metadata:
|
||||
name: {{ .Values.meta.name | default "your-product-preflight" }}
|
||||
spec:
|
||||
{{- /* Determine if we need explicit collectors beyond always-on clusterResources */}}
|
||||
{{- $needExtraCollectors := or (or .Values.databases.postgres.enabled .Values.http.enabled) .Values.registryImages.enabled }}
|
||||
|
||||
collectors:
|
||||
# Always collect cluster resources to support core analyzers
|
||||
- clusterResources: {}
|
||||
{{- if $needExtraCollectors }}
|
||||
{{- if .Values.databases.postgres.enabled }}
|
||||
- postgres:
|
||||
collectorName: '{{ .Values.databases.postgres.collectorName }}'
|
||||
uri: '{{ .Values.databases.postgres.uri }}'
|
||||
{{- end }}
|
||||
{{- if .Values.http.enabled }}
|
||||
- http:
|
||||
collectorName: '{{ .Values.http.collectorName }}'
|
||||
get:
|
||||
url: '{{ .Values.http.get.url }}'
|
||||
{{- end }}
|
||||
{{- if .Values.registryImages.enabled }}
|
||||
- registryImages:
|
||||
collectorName: '{{ .Values.registryImages.collectorName }}'
|
||||
namespace: '{{ .Values.registryImages.namespace }}'
|
||||
images:
|
||||
{{- range .Values.registryImages.images }}
|
||||
- '{{ . }}'
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
analyzers:
|
||||
{{- if .Values.clusterVersion.enabled }}
|
||||
- docString: |
|
||||
Title: Kubernetes Control Plane Requirements
|
||||
Requirement:
|
||||
- Version:
|
||||
- Minimum: {{ .Values.clusterVersion.minVersion }}
|
||||
- Recommended: {{ .Values.clusterVersion.recommendedVersion }}
|
||||
- Docs: https://kubernetes.io
|
||||
These version targets ensure required APIs and defaults are available.
|
||||
clusterVersion:
|
||||
checkName: Kubernetes version
|
||||
outcomes:
|
||||
- fail:
|
||||
when: '< {{ .Values.clusterVersion.minVersion }}'
|
||||
message: Requires at least Kubernetes {{ .Values.clusterVersion.minVersion }}.
|
||||
- warn:
|
||||
when: '< {{ .Values.clusterVersion.recommendedVersion }}'
|
||||
message: Recommended {{ .Values.clusterVersion.recommendedVersion }} or later.
|
||||
- pass:
|
||||
when: '>= {{ .Values.clusterVersion.recommendedVersion }}'
|
||||
message: Meets recommended and required Kubernetes versions.
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.storageClass.enabled }}
|
||||
- docString: |
|
||||
Title: Default StorageClass Requirements
|
||||
Requirement:
|
||||
- A StorageClass named "{{ .Values.storageClass.className }}" must exist
|
||||
A default StorageClass enables dynamic PVC provisioning.
|
||||
storageClass:
|
||||
checkName: Default StorageClass
|
||||
storageClassName: '{{ .Values.storageClass.className }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Default StorageClass not found
|
||||
- pass:
|
||||
message: Default StorageClass present
|
||||
{{- end }}
|
||||
```
|
||||
|
||||
Example values snippet:
|
||||
```yaml
|
||||
meta:
|
||||
name: your-product-preflight
|
||||
clusterVersion:
|
||||
enabled: true
|
||||
minVersion: "1.24.0"
|
||||
recommendedVersion: "1.28.0"
|
||||
storageClass:
|
||||
enabled: true
|
||||
className: "standard"
|
||||
databases:
|
||||
postgres:
|
||||
enabled: false
|
||||
http:
|
||||
enabled: false
|
||||
registryImages:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
Checklist before finishing:
|
||||
- All `.Values.*` references exist in the values snippet.
|
||||
- Optional analyzers are gated by `if .Values.<feature>.enabled`.
|
||||
- Collectors included only when required by enabled analyzers.
|
||||
- `checkName` set, outcomes messages are specific and actionable.
|
||||
- Indentation is consistent; templates render as valid YAML.
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
# Modified from Codespaces default container image: https://github.com/microsoft/vscode-dev-containers/blob/main/containers/codespaces-linux/history/1.6.3.md
|
||||
# - Remove PHP, Ruby, Dotnet, Java, powershell, rust dependencies
|
||||
# - Remove fish shell
|
||||
# - Remove Oryx
|
||||
# - Remove git-lfs
|
||||
# - Change shell to zsh
|
||||
#
|
||||
# TODO (dans): find a better way to pull in library script dynamically from vscode repo
|
||||
# TODO (dans): AWS CLI - make a common script in the dev-containers repo
|
||||
# TODO (dans): Gcloud CLI - make a common script in the dev-containers repo
|
||||
# TODO (dans): add gcommands alias
|
||||
# TODO (dans): terraform
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
FROM mcr.microsoft.com/oryx/build:vso-focal-20210902.1 as replicated
|
||||
|
||||
ARG USERNAME=codespace
|
||||
ARG USER_UID=1000
|
||||
ARG USER_GID=$USER_UID
|
||||
ARG HOMEDIR=/home/$USERNAME
|
||||
|
||||
ARG GO_VERSION="latest"
|
||||
|
||||
# Default to bash shell (other shells available at /usr/bin/fish and /usr/bin/zsh)
|
||||
ENV SHELL=/bin/bash \
|
||||
ORYX_ENV_TYPE=vsonline-present \
|
||||
NODE_ROOT="${HOMEDIR}/.nodejs" \
|
||||
PYTHON_ROOT="${HOMEDIR}/.python" \
|
||||
HUGO_ROOT="${HOMEDIR}/.hugo" \
|
||||
NVM_SYMLINK_CURRENT=true \
|
||||
NVM_DIR="/home/${USERNAME}/.nvm" \
|
||||
NVS_HOME="/home/${USERNAME}/.nvs" \
|
||||
NPM_GLOBAL="/home/${USERNAME}/.npm-global" \
|
||||
KREW_HOME="/home/${USERNAME}/.krew/bin" \
|
||||
PIPX_HOME="/usr/local/py-utils" \
|
||||
PIPX_BIN_DIR="/usr/local/py-utils/bin" \
|
||||
GOROOT="/usr/local/go" \
|
||||
GOPATH="/go"
|
||||
|
||||
ENV PATH="${PATH}:${KREW_HOME}:${NVM_DIR}/current/bin:${NPM_GLOBAL}/bin:${ORIGINAL_PATH}:${GOROOT}/bin:${GOPATH}/bin:${PIPX_BIN_DIR}:/opt/conda/condabin:${NODE_ROOT}/current/bin:${PYTHON_ROOT}/current/bin:${HUGO_ROOT}/current/bin:${ORYX_PATHS}"
|
||||
|
||||
COPY library-scripts/* first-run-notice.txt /tmp/scripts/
|
||||
COPY ./config/* /etc/replicated/
|
||||
COPY ./lifecycle-scripts/* /var/lib/replicated/scripts/
|
||||
|
||||
# Install needed utilities and setup non-root user. Use a separate RUN statement to add your own dependencies.
|
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
# Restore man command
|
||||
&& yes | unminimize 2>&1 \
|
||||
# Run common script and setup user
|
||||
&& bash /tmp/scripts/common-debian.sh "true" "${USERNAME}" "${USER_UID}" "${USER_GID}" "true" "true" "true" \
|
||||
&& bash /tmp/scripts/setup-user.sh "${USERNAME}" "${PATH}" \
|
||||
# Change owner of opt contents since Oryx can dynamically install and will run as "codespace"
|
||||
&& chown ${USERNAME} /opt/* \
|
||||
&& chsh -s /bin/bash ${USERNAME} \
|
||||
# Verify expected build and debug tools are present
|
||||
&& apt-get -y install build-essential cmake python3-dev \
|
||||
# Install tools and shells not in common script
|
||||
&& apt-get install -yq vim vim-doc xtail software-properties-common libsecret-1-dev \
|
||||
# Install additional tools (useful for 'puppeteer' project)
|
||||
&& apt-get install -y --no-install-recommends libnss3 libnspr4 libatk-bridge2.0-0 libatk1.0-0 libx11-6 libpangocairo-1.0-0 \
|
||||
libx11-xcb1 libcups2 libxcomposite1 libxdamage1 libxfixes3 libpango-1.0-0 libgbm1 libgtk-3-0 \
|
||||
&& bash /tmp/scripts/sshd-debian.sh \
|
||||
&& bash /tmp/scripts/github-debian.sh \
|
||||
&& bash /tmp/scripts/azcli-debian.sh \
|
||||
# Install Moby CLI and Engine
|
||||
&& /bin/bash /tmp/scripts/docker-debian.sh "true" "/var/run/docker-host.sock" "/var/run/docker.sock" "${USERNAME}" "true" \
|
||||
# && bash /tmp/scripts/docker-in-docker-debian.sh "true" "${USERNAME}" "true" \
|
||||
&& bash /tmp/scripts/kubectl-helm-debian.sh \
|
||||
# Build latest git from source
|
||||
&& bash /tmp/scripts/git-from-src-debian.sh "latest" \
|
||||
# Clean up
|
||||
&& apt-get autoremove -y && apt-get clean -y \
|
||||
# Move first run notice to right spot
|
||||
&& mkdir -p /usr/local/etc/vscode-dev-containers/ \
|
||||
&& mv -f /tmp/scripts/first-run-notice.txt /usr/local/etc/vscode-dev-containers/
|
||||
|
||||
# Install Python
|
||||
RUN bash /tmp/scripts/python-debian.sh "none" "/opt/python/latest" "${PIPX_HOME}" "${USERNAME}" "true" \
|
||||
&& apt-get clean -y
|
||||
|
||||
# Setup Node.js, install NVM and NVS
|
||||
RUN bash /tmp/scripts/node-debian.sh "${NVM_DIR}" "none" "${USERNAME}" \
|
||||
&& (cd ${NVM_DIR} && git remote get-url origin && echo $(git log -n 1 --pretty=format:%H -- .)) > ${NVM_DIR}/.git-remote-and-commit \
|
||||
# Install nvs (alternate cross-platform Node.js version-management tool)
|
||||
&& sudo -u ${USERNAME} git clone -c advice.detachedHead=false --depth 1 https://github.com/jasongin/nvs ${NVS_HOME} 2>&1 \
|
||||
&& (cd ${NVS_HOME} && git remote get-url origin && echo $(git log -n 1 --pretty=format:%H -- .)) > ${NVS_HOME}/.git-remote-and-commit \
|
||||
&& sudo -u ${USERNAME} bash ${NVS_HOME}/nvs.sh install \
|
||||
&& rm ${NVS_HOME}/cache/* \
|
||||
# Set npm global location
|
||||
&& sudo -u ${USERNAME} npm config set prefix ${NPM_GLOBAL} \
|
||||
&& npm config -g set prefix ${NPM_GLOBAL} \
|
||||
# Clean up
|
||||
&& rm -rf ${NVM_DIR}/.git ${NVS_HOME}/.git
|
||||
|
||||
# Install Go
|
||||
RUN bash /tmp/scripts/go-debian.sh "${GO_VERSION}" "${GOROOT}" "${GOPATH}" "${USERNAME}"
|
||||
|
||||
# Install Replicated Tools
|
||||
RUN bash /tmp/scripts/replicated-debian.sh \
|
||||
&& rm -rf /tmp/scripts \
|
||||
&& apt-get clean -y
|
||||
|
||||
# Userspace
|
||||
ENV SHELL=/bin/zsh
|
||||
USER ${USERNAME}
|
||||
COPY --chown=${USERNAME}:root library-scripts/replicated-userspace.sh /tmp/scripts/
|
||||
RUN bash /usr/local/share/docker-init.sh \
|
||||
&& bash /tmp/scripts/replicated-userspace.sh \
|
||||
&& rm -rf /tmp/scripts/scripts
|
||||
|
||||
# Fire Docker/Moby script if needed along with Oryx's benv
|
||||
ENTRYPOINT [ "/usr/local/share/docker-init.sh", "/usr/local/share/ssh-init.sh", "benv" ]
|
||||
CMD [ "sleep", "infinity" ]
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# Replicated KOTS Codespace Container
|
||||
|
||||
Most of the code here is borrowed from this [Microsoft repo of base images](https://github.com/microsoft/vscode-dev-containers), except for replicated specific things.
|
||||
|
||||
## Notes
|
||||
* k3d *DOES NOT* work with DinD. You have to use the docker with docker install instead.
|
||||
* Might be faster to install kubectl plugins on the `$PATH` in the `Dockerfile` instead of downloading them `onCreate.sh`.
|
||||
@@ -1,10 +0,0 @@
|
||||
apiVersion: k3d.io/v1alpha3
|
||||
kind: Simple
|
||||
name: replicated
|
||||
servers: 1
|
||||
image: rancher/k3s:v1.21.4-k3s1 # v1.21.3-k3s1 default is broken
|
||||
registries:
|
||||
create:
|
||||
name: k3d-replicated-registry.localhost
|
||||
host: "0.0.0.0"
|
||||
hostPort: "5000"
|
||||
@@ -1,63 +0,0 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.162.0/containers/javascript-node
|
||||
{
|
||||
"name": "Replicated Codeserver",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
"args": {
|
||||
"GO_VERSION": "1.17",
|
||||
}
|
||||
},
|
||||
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"terminal.integrated.shell.linux": "/usr/bin/zsh",
|
||||
"go.toolsManagement.checkForUpdates": "local",
|
||||
"go.useLanguageServer": true,
|
||||
"go.gopath": "/go",
|
||||
"go.goroot": "/usr/local/go",
|
||||
"python.pythonPath": "/opt/python/latest/bin/python",
|
||||
"python.linting.enabled": true,
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
|
||||
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
|
||||
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
|
||||
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
|
||||
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
|
||||
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
|
||||
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
|
||||
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
|
||||
"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint",
|
||||
"lldb.executable": "/usr/bin/lldb",
|
||||
"files.watcherExclude": {
|
||||
"**/target/**": true
|
||||
}
|
||||
},
|
||||
"remoteUser": "codespace",
|
||||
"overrideCommand": false,
|
||||
"runArgs": [
|
||||
"--privileged",
|
||||
"--init"
|
||||
],
|
||||
"mounts": [
|
||||
"source=/var/run/docker.sock,target=/var/run/docker-host.sock,type=bind",
|
||||
],
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"GitHub.vscode-pull-request-github",
|
||||
"golang.go",
|
||||
"github.copilot",
|
||||
"lizebang.bash-extension-pack",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "bash /var/lib/replicated/scripts/onCreate.sh",
|
||||
|
||||
// Use 'postStartCommand' to run commands after the container is created like starting minikube.
|
||||
"postStartCommand": "bash /var/lib/replicated/scripts/onStart.sh",
|
||||
|
||||
// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
// "remoteUser": "node"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
👋 Welcome to your Replicated Codespace!
|
||||
|
||||
There's a local Kubernetes cluster set up for you.
|
||||
|
||||
Drivers Manual:
|
||||
* `k` alias is available for `kubectl` with auto-completion for your pleasure
|
||||
* This is a `zsh` terminal with Oh My Zsh installed. Just thought you should know.
|
||||
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
#
|
||||
# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/azcli.md
|
||||
# Maintainer: The VS Code and Codespaces Teams
|
||||
#
|
||||
# Syntax: ./azcli-debian.sh
|
||||
|
||||
set -e
|
||||
|
||||
MICROSOFT_GPG_KEYS_URI="https://packages.microsoft.com/keys/microsoft.asc"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get central common setting
|
||||
get_common_setting() {
|
||||
if [ "${common_settings_file_loaded}" != "true" ]; then
|
||||
curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping."
|
||||
common_settings_file_loaded=true
|
||||
fi
|
||||
if [ -f "/tmp/vsdc-settings.env" ]; then
|
||||
local multi_line=""
|
||||
if [ "$2" = "true" ]; then multi_line="-z"; fi
|
||||
local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')"
|
||||
if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi
|
||||
fi
|
||||
echo "$1=${!1}"
|
||||
}
|
||||
|
||||
# Function to run apt-get if needed
|
||||
apt_get_update_if_needed()
|
||||
{
|
||||
if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then
|
||||
echo "Running apt-get update..."
|
||||
apt-get update
|
||||
else
|
||||
echo "Skipping apt-get update."
|
||||
fi
|
||||
}
|
||||
|
||||
# Checks if packages are installed and installs them if not
|
||||
check_packages() {
|
||||
if ! dpkg -s "$@" > /dev/null 2>&1; then
|
||||
apt_get_update_if_needed
|
||||
apt-get -y install --no-install-recommends "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install dependencies
|
||||
check_packages apt-transport-https curl ca-certificates lsb-release gnupg2
|
||||
|
||||
# Import key safely (new 'signed-by' method rather than deprecated apt-key approach) and install
|
||||
. /etc/os-release
|
||||
get_common_setting MICROSOFT_GPG_KEYS_URI
|
||||
curl -sSL ${MICROSOFT_GPG_KEYS_URI} | gpg --dearmor > /usr/share/keyrings/microsoft-archive-keyring.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] https://packages.microsoft.com/repos/azure-cli/ ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/azure-cli.list
|
||||
apt-get update
|
||||
apt-get install -y azure-cli
|
||||
echo "Done!"
|
||||
@@ -1,478 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
#
|
||||
# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/common.md
|
||||
# Maintainer: The VS Code and Codespaces Teams
|
||||
#
|
||||
# Syntax: ./common-debian.sh [install zsh flag] [username] [user UID] [user GID] [upgrade packages flag] [install Oh My Zsh! flag] [Add non-free packages]
|
||||
|
||||
set -e
|
||||
|
||||
INSTALL_ZSH=${1:-"true"}
|
||||
USERNAME=${2:-"automatic"}
|
||||
USER_UID=${3:-"automatic"}
|
||||
USER_GID=${4:-"automatic"}
|
||||
UPGRADE_PACKAGES=${5:-"true"}
|
||||
INSTALL_OH_MYS=${6:-"true"}
|
||||
ADD_NON_FREE_PACKAGES=${7:-"false"}
|
||||
SCRIPT_DIR="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)"
|
||||
MARKER_FILE="/usr/local/etc/vscode-dev-containers/common"
|
||||
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure that login shells get the correct path if the user updated the PATH using ENV.
|
||||
rm -f /etc/profile.d/00-restore-env.sh
|
||||
echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh
|
||||
chmod +x /etc/profile.d/00-restore-env.sh
|
||||
|
||||
# If in automatic mode, determine if a user already exists, if not use vscode
|
||||
if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then
|
||||
USERNAME=""
|
||||
POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)")
|
||||
for CURRENT_USER in ${POSSIBLE_USERS[@]}; do
|
||||
if id -u ${CURRENT_USER} > /dev/null 2>&1; then
|
||||
USERNAME=${CURRENT_USER}
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ "${USERNAME}" = "" ]; then
|
||||
USERNAME=vscode
|
||||
fi
|
||||
elif [ "${USERNAME}" = "none" ]; then
|
||||
USERNAME=root
|
||||
USER_UID=0
|
||||
USER_GID=0
|
||||
fi
|
||||
|
||||
# Load markers to see which steps have already run
|
||||
if [ -f "${MARKER_FILE}" ]; then
|
||||
echo "Marker file found:"
|
||||
cat "${MARKER_FILE}"
|
||||
source "${MARKER_FILE}"
|
||||
fi
|
||||
|
||||
# Ensure apt is in non-interactive to avoid prompts
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Function to call apt-get if needed
|
||||
apt_get_update_if_needed()
|
||||
{
|
||||
if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then
|
||||
echo "Running apt-get update..."
|
||||
apt-get update
|
||||
else
|
||||
echo "Skipping apt-get update."
|
||||
fi
|
||||
}
|
||||
|
||||
# Run install apt-utils to avoid debconf warning then verify presence of other common developer tools and dependencies
|
||||
if [ "${PACKAGES_ALREADY_INSTALLED}" != "true" ]; then
|
||||
|
||||
package_list="apt-utils \
|
||||
openssh-client \
|
||||
gnupg2 \
|
||||
iproute2 \
|
||||
procps \
|
||||
lsof \
|
||||
htop \
|
||||
net-tools \
|
||||
psmisc \
|
||||
curl \
|
||||
wget \
|
||||
rsync \
|
||||
ca-certificates \
|
||||
unzip \
|
||||
zip \
|
||||
nano \
|
||||
vim-tiny \
|
||||
less \
|
||||
jq \
|
||||
lsb-release \
|
||||
apt-transport-https \
|
||||
dialog \
|
||||
libc6 \
|
||||
libgcc1 \
|
||||
libkrb5-3 \
|
||||
libgssapi-krb5-2 \
|
||||
libicu[0-9][0-9] \
|
||||
liblttng-ust0 \
|
||||
libstdc++6 \
|
||||
zlib1g \
|
||||
locales \
|
||||
sudo \
|
||||
ncdu \
|
||||
man-db \
|
||||
strace \
|
||||
manpages \
|
||||
manpages-dev \
|
||||
init-system-helpers"
|
||||
|
||||
# Needed for adding manpages-posix and manpages-posix-dev which are non-free packages in Debian
|
||||
if [ "${ADD_NON_FREE_PACKAGES}" = "true" ]; then
|
||||
# Bring in variables from /etc/os-release like VERSION_CODENAME
|
||||
. /etc/os-release
|
||||
sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list
|
||||
sed -i -E "s/deb-src http:\/\/(deb|httredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list
|
||||
sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list
|
||||
sed -i -E "s/deb-src http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list
|
||||
sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list
|
||||
sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list
|
||||
sed -i "s/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list
|
||||
sed -i "s/deb-src http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list
|
||||
echo "Running apt-get update..."
|
||||
apt-get update
|
||||
package_list="${package_list} manpages-posix manpages-posix-dev"
|
||||
else
|
||||
apt_get_update_if_needed
|
||||
fi
|
||||
|
||||
# Install libssl1.1 if available
|
||||
if [[ ! -z $(apt-cache --names-only search ^libssl1.1$) ]]; then
|
||||
package_list="${package_list} libssl1.1"
|
||||
fi
|
||||
|
||||
# Install appropriate version of libssl1.0.x if available
|
||||
libssl_package=$(dpkg-query -f '${db:Status-Abbrev}\t${binary:Package}\n' -W 'libssl1\.0\.?' 2>&1 || echo '')
|
||||
if [ "$(echo "$LIlibssl_packageBSSL" | grep -o 'libssl1\.0\.[0-9]:' | uniq | sort | wc -l)" -eq 0 ]; then
|
||||
if [[ ! -z $(apt-cache --names-only search ^libssl1.0.2$) ]]; then
|
||||
# Debian 9
|
||||
package_list="${package_list} libssl1.0.2"
|
||||
elif [[ ! -z $(apt-cache --names-only search ^libssl1.0.0$) ]]; then
|
||||
# Ubuntu 18.04, 16.04, earlier
|
||||
package_list="${package_list} libssl1.0.0"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Packages to verify are installed: ${package_list}"
|
||||
apt-get -y install --no-install-recommends ${package_list} 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 )
|
||||
|
||||
# Install git if not already installed (may be more recent than distro version)
|
||||
if ! type git > /dev/null 2>&1; then
|
||||
apt-get -y install --no-install-recommends git
|
||||
fi
|
||||
|
||||
PACKAGES_ALREADY_INSTALLED="true"
|
||||
fi
|
||||
|
||||
# Get to latest versions of all packages
|
||||
if [ "${UPGRADE_PACKAGES}" = "true" ]; then
|
||||
apt_get_update_if_needed
|
||||
apt-get -y upgrade --no-install-recommends
|
||||
apt-get autoremove -y
|
||||
fi
|
||||
|
||||
# Ensure at least the en_US.UTF-8 UTF-8 locale is available.
|
||||
# Common need for both applications and things like the agnoster ZSH theme.
|
||||
if [ "${LOCALE_ALREADY_SET}" != "true" ] && ! grep -o -E '^\s*en_US.UTF-8\s+UTF-8' /etc/locale.gen > /dev/null; then
|
||||
echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen
|
||||
locale-gen
|
||||
LOCALE_ALREADY_SET="true"
|
||||
fi
|
||||
|
||||
# Create or update a non-root user to match UID/GID.
|
||||
if id -u ${USERNAME} > /dev/null 2>&1; then
|
||||
# User exists, update if needed
|
||||
if [ "${USER_GID}" != "automatic" ] && [ "$USER_GID" != "$(id -G $USERNAME)" ]; then
|
||||
groupmod --gid $USER_GID $USERNAME
|
||||
usermod --gid $USER_GID $USERNAME
|
||||
fi
|
||||
if [ "${USER_UID}" != "automatic" ] && [ "$USER_UID" != "$(id -u $USERNAME)" ]; then
|
||||
usermod --uid $USER_UID $USERNAME
|
||||
fi
|
||||
else
|
||||
# Create user
|
||||
if [ "${USER_GID}" = "automatic" ]; then
|
||||
groupadd $USERNAME
|
||||
else
|
||||
groupadd --gid $USER_GID $USERNAME
|
||||
fi
|
||||
if [ "${USER_UID}" = "automatic" ]; then
|
||||
useradd -s /bin/bash --gid $USERNAME -m $USERNAME
|
||||
else
|
||||
useradd -s /bin/bash --uid $USER_UID --gid $USERNAME -m $USERNAME
|
||||
fi
|
||||
fi
|
||||
|
||||
# Add add sudo support for non-root user
|
||||
if [ "${USERNAME}" != "root" ] && [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then
|
||||
echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME
|
||||
chmod 0440 /etc/sudoers.d/$USERNAME
|
||||
EXISTING_NON_ROOT_USER="${USERNAME}"
|
||||
fi
|
||||
|
||||
# ** Shell customization section **
|
||||
if [ "${USERNAME}" = "root" ]; then
|
||||
user_rc_path="/root"
|
||||
else
|
||||
user_rc_path="/home/${USERNAME}"
|
||||
fi
|
||||
|
||||
# Restore user .bashrc defaults from skeleton file if it doesn't exist or is empty
|
||||
if [ ! -f "${user_rc_path}/.bashrc" ] || [ ! -s "${user_rc_path}/.bashrc" ] ; then
|
||||
cp /etc/skel/.bashrc "${user_rc_path}/.bashrc"
|
||||
fi
|
||||
|
||||
# Restore user .profile defaults from skeleton file if it doesn't exist or is empty
|
||||
if [ ! -f "${user_rc_path}/.profile" ] || [ ! -s "${user_rc_path}/.profile" ] ; then
|
||||
cp /etc/skel/.profile "${user_rc_path}/.profile"
|
||||
fi
|
||||
|
||||
# .bashrc/.zshrc snippet
|
||||
rc_snippet="$(cat << 'EOF'
|
||||
|
||||
if [ -z "${USER}" ]; then export USER=$(whoami); fi
|
||||
if [[ "${PATH}" != *"$HOME/.local/bin"* ]]; then export PATH="${PATH}:$HOME/.local/bin"; fi
|
||||
|
||||
# Display optional first run image specific notice if configured and terminal is interactive
|
||||
if [ -t 1 ] && [[ "${TERM_PROGRAM}" = "vscode" || "${TERM_PROGRAM}" = "codespaces" ]] && [ ! -f "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed" ]; then
|
||||
if [ -f "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" ]; then
|
||||
cat "/usr/local/etc/vscode-dev-containers/first-run-notice.txt"
|
||||
elif [ -f "/workspaces/.codespaces/shared/first-run-notice.txt" ]; then
|
||||
cat "/workspaces/.codespaces/shared/first-run-notice.txt"
|
||||
fi
|
||||
mkdir -p "$HOME/.config/vscode-dev-containers"
|
||||
# Mark first run notice as displayed after 10s to avoid problems with fast terminal refreshes hiding it
|
||||
((sleep 10s; touch "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed") &)
|
||||
fi
|
||||
|
||||
# Set the default git editor if not already set
|
||||
if [ -z "$(git config --get core.editor)" ] && [ -z "${GIT_EDITOR}" ]; then
|
||||
if [ "${TERM_PROGRAM}" = "vscode" ]; then
|
||||
if [[ -n $(command -v code-insiders) && -z $(command -v code) ]]; then
|
||||
export GIT_EDITOR="code-insiders --wait"
|
||||
else
|
||||
export GIT_EDITOR="code --wait"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
EOF
|
||||
)"
|
||||
|
||||
# code shim, it fallbacks to code-insiders if code is not available
|
||||
cat << 'EOF' > /usr/local/bin/code
|
||||
#!/bin/sh
|
||||
|
||||
get_in_path_except_current() {
|
||||
which -a "$1" | grep -A1 "$0" | grep -v "$0"
|
||||
}
|
||||
|
||||
code="$(get_in_path_except_current code)"
|
||||
|
||||
if [ -n "$code" ]; then
|
||||
exec "$code" "$@"
|
||||
elif [ "$(command -v code-insiders)" ]; then
|
||||
exec code-insiders "$@"
|
||||
else
|
||||
echo "code or code-insiders is not installed" >&2
|
||||
exit 127
|
||||
fi
|
||||
EOF
|
||||
chmod +x /usr/local/bin/code
|
||||
|
||||
# systemctl shim - tells people to use 'service' if systemd is not running
|
||||
cat << 'EOF' > /usr/local/bin/systemctl
|
||||
#!/bin/sh
|
||||
set -e
|
||||
if [ -d "/run/systemd/system" ]; then
|
||||
exec /bin/systemctl/systemctl "$@"
|
||||
else
|
||||
echo '\n"systemd" is not running in this container due to its overhead.\nUse the "service" command to start services intead. e.g.: \n\nservice --status-all'
|
||||
fi
|
||||
EOF
|
||||
chmod +x /usr/local/bin/systemctl
|
||||
|
||||
# Codespaces bash and OMZ themes - partly inspired by https://github.com/ohmyzsh/ohmyzsh/blob/master/themes/robbyrussell.zsh-theme
|
||||
codespaces_bash="$(cat \
|
||||
<<'EOF'
|
||||
|
||||
# Codespaces bash prompt theme
|
||||
__bash_prompt() {
|
||||
local userpart='`export XIT=$? \
|
||||
&& [ ! -z "${GITHUB_USER}" ] && echo -n "\[\033[0;32m\]@${GITHUB_USER} " || echo -n "\[\033[0;32m\]\u " \
|
||||
&& [ "$XIT" -ne "0" ] && echo -n "\[\033[1;31m\]➜" || echo -n "\[\033[0m\]➜"`'
|
||||
local gitbranch='`\
|
||||
export BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null); \
|
||||
if [ "${BRANCH}" != "" ]; then \
|
||||
echo -n "\[\033[0;36m\](\[\033[1;31m\]${BRANCH}" \
|
||||
&& if git ls-files --error-unmatch -m --directory --no-empty-directory -o --exclude-standard ":/*" > /dev/null 2>&1; then \
|
||||
echo -n " \[\033[1;33m\]✗"; \
|
||||
fi \
|
||||
&& echo -n "\[\033[0;36m\]) "; \
|
||||
fi`'
|
||||
local lightblue='\[\033[1;34m\]'
|
||||
local removecolor='\[\033[0m\]'
|
||||
PS1="${userpart} ${lightblue}\w ${gitbranch}${removecolor}\$ "
|
||||
unset -f __bash_prompt
|
||||
}
|
||||
__bash_prompt
|
||||
|
||||
EOF
|
||||
)"
|
||||
|
||||
codespaces_zsh="$(cat \
|
||||
<<'EOF'
|
||||
# Codespaces zsh prompt theme
|
||||
__zsh_prompt() {
|
||||
local prompt_username
|
||||
if [ ! -z "${GITHUB_USER}" ]; then
|
||||
prompt_username="@${GITHUB_USER}"
|
||||
else
|
||||
prompt_username="%n"
|
||||
fi
|
||||
PROMPT="%{$fg[green]%}${prompt_username} %(?:%{$reset_color%}➜ :%{$fg_bold[red]%}➜ )" # User/exit code arrow
|
||||
PROMPT+='%{$fg_bold[blue]%}%(5~|%-1~/…/%3~|%4~)%{$reset_color%} ' # cwd
|
||||
PROMPT+='$(git_prompt_info)%{$fg[white]%}$ %{$reset_color%}' # Git status
|
||||
unset -f __zsh_prompt
|
||||
}
|
||||
ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg_bold[cyan]%}(%{$fg_bold[red]%}"
|
||||
ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%} "
|
||||
ZSH_THEME_GIT_PROMPT_DIRTY=" %{$fg_bold[yellow]%}✗%{$fg_bold[cyan]%})"
|
||||
ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg_bold[cyan]%})"
|
||||
__zsh_prompt
|
||||
|
||||
EOF
|
||||
)"
|
||||
|
||||
# Add notice that Oh My Bash! has been removed from images and how to provide information on how to install manually
|
||||
omb_readme="$(cat \
|
||||
<<'EOF'
|
||||
"Oh My Bash!" has been removed from this image in favor of a simple shell prompt. If you
|
||||
still wish to use it, remove "~/.oh-my-bash" and install it from: https://github.com/ohmybash/oh-my-bash
|
||||
You may also want to consider "Bash-it" as an alternative: https://github.com/bash-it/bash-it
|
||||
See here for infomation on adding it to your image or dotfiles: https://aka.ms/codespaces/omb-remove
|
||||
EOF
|
||||
)"
|
||||
omb_stub="$(cat \
|
||||
<<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
if [ -t 1 ]; then
|
||||
cat $HOME/.oh-my-bash/README.md
|
||||
fi
|
||||
EOF
|
||||
)"
|
||||
|
||||
# Add RC snippet and custom bash prompt
|
||||
if [ "${RC_SNIPPET_ALREADY_ADDED}" != "true" ]; then
|
||||
echo "${rc_snippet}" >> /etc/bash.bashrc
|
||||
echo "${codespaces_bash}" >> "${user_rc_path}/.bashrc"
|
||||
echo 'export PROMPT_DIRTRIM=4' >> "${user_rc_path}/.bashrc"
|
||||
if [ "${USERNAME}" != "root" ]; then
|
||||
echo "${codespaces_bash}" >> "/root/.bashrc"
|
||||
echo 'export PROMPT_DIRTRIM=4' >> "/root/.bashrc"
|
||||
fi
|
||||
chown ${USERNAME}:${USERNAME} "${user_rc_path}/.bashrc"
|
||||
RC_SNIPPET_ALREADY_ADDED="true"
|
||||
fi
|
||||
|
||||
# Add stub for Oh My Bash!
|
||||
if [ ! -d "${user_rc_path}/.oh-my-bash}" ] && [ "${INSTALL_OH_MYS}" = "true" ]; then
|
||||
mkdir -p "${user_rc_path}/.oh-my-bash" "/root/.oh-my-bash"
|
||||
echo "${omb_readme}" >> "${user_rc_path}/.oh-my-bash/README.md"
|
||||
echo "${omb_stub}" >> "${user_rc_path}/.oh-my-bash/oh-my-bash.sh"
|
||||
chmod +x "${user_rc_path}/.oh-my-bash/oh-my-bash.sh"
|
||||
if [ "${USERNAME}" != "root" ]; then
|
||||
echo "${omb_readme}" >> "/root/.oh-my-bash/README.md"
|
||||
echo "${omb_stub}" >> "/root/.oh-my-bash/oh-my-bash.sh"
|
||||
chmod +x "/root/.oh-my-bash/oh-my-bash.sh"
|
||||
fi
|
||||
chown -R "${USERNAME}:${USERNAME}" "${user_rc_path}/.oh-my-bash"
|
||||
fi
|
||||
|
||||
# Optionally install and configure zsh and Oh My Zsh!
|
||||
if [ "${INSTALL_ZSH}" = "true" ]; then
|
||||
if ! type zsh > /dev/null 2>&1; then
|
||||
apt_get_update_if_needed
|
||||
apt-get install -y zsh
|
||||
fi
|
||||
if [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then
|
||||
echo "${rc_snippet}" >> /etc/zsh/zshrc
|
||||
ZSH_ALREADY_INSTALLED="true"
|
||||
fi
|
||||
|
||||
# Adapted, simplified inline Oh My Zsh! install steps that adds, defaults to a codespaces theme.
|
||||
# See https://github.com/ohmyzsh/ohmyzsh/blob/master/tools/install.sh for official script.
|
||||
oh_my_install_dir="${user_rc_path}/.oh-my-zsh"
|
||||
if [ ! -d "${oh_my_install_dir}" ] && [ "${INSTALL_OH_MYS}" = "true" ]; then
|
||||
template_path="${oh_my_install_dir}/templates/zshrc.zsh-template"
|
||||
user_rc_file="${user_rc_path}/.zshrc"
|
||||
umask g-w,o-w
|
||||
mkdir -p ${oh_my_install_dir}
|
||||
git clone --depth=1 \
|
||||
-c core.eol=lf \
|
||||
-c core.autocrlf=false \
|
||||
-c fsck.zeroPaddedFilemode=ignore \
|
||||
-c fetch.fsck.zeroPaddedFilemode=ignore \
|
||||
-c receive.fsck.zeroPaddedFilemode=ignore \
|
||||
"https://github.com/ohmyzsh/ohmyzsh" "${oh_my_install_dir}" 2>&1
|
||||
echo -e "$(cat "${template_path}")\nDISABLE_AUTO_UPDATE=true\nDISABLE_UPDATE_PROMPT=true" > ${user_rc_file}
|
||||
sed -i -e 's/ZSH_THEME=.*/ZSH_THEME="codespaces"/g' ${user_rc_file}
|
||||
|
||||
mkdir -p ${oh_my_install_dir}/custom/themes
|
||||
echo "${codespaces_zsh}" > "${oh_my_install_dir}/custom/themes/codespaces.zsh-theme"
|
||||
# Shrink git while still enabling updates
|
||||
cd "${oh_my_install_dir}"
|
||||
git repack -a -d -f --depth=1 --window=1
|
||||
# Copy to non-root user if one is specified
|
||||
if [ "${USERNAME}" != "root" ]; then
|
||||
cp -rf "${user_rc_file}" "${oh_my_install_dir}" /root
|
||||
chown -R ${USERNAME}:${USERNAME} "${user_rc_path}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Persist image metadata info, script if meta.env found in same directory
|
||||
meta_info_script="$(cat << 'EOF'
|
||||
#!/bin/sh
|
||||
. /usr/local/etc/vscode-dev-containers/meta.env
|
||||
|
||||
# Minimal output
|
||||
if [ "$1" = "version" ] || [ "$1" = "image-version" ]; then
|
||||
echo "${VERSION}"
|
||||
exit 0
|
||||
elif [ "$1" = "release" ]; then
|
||||
echo "${GIT_REPOSITORY_RELEASE}"
|
||||
exit 0
|
||||
elif [ "$1" = "content" ] || [ "$1" = "content-url" ] || [ "$1" = "contents" ] || [ "$1" = "contents-url" ]; then
|
||||
echo "${CONTENTS_URL}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
#Full output
|
||||
echo
|
||||
echo "Development container image information"
|
||||
echo
|
||||
if [ ! -z "${VERSION}" ]; then echo "- Image version: ${VERSION}"; fi
|
||||
if [ ! -z "${DEFINITION_ID}" ]; then echo "- Definition ID: ${DEFINITION_ID}"; fi
|
||||
if [ ! -z "${VARIANT}" ]; then echo "- Variant: ${VARIANT}"; fi
|
||||
if [ ! -z "${GIT_REPOSITORY}" ]; then echo "- Source code repository: ${GIT_REPOSITORY}"; fi
|
||||
if [ ! -z "${GIT_REPOSITORY_RELEASE}" ]; then echo "- Source code release/branch: ${GIT_REPOSITORY_RELEASE}"; fi
|
||||
if [ ! -z "${BUILD_TIMESTAMP}" ]; then echo "- Timestamp: ${BUILD_TIMESTAMP}"; fi
|
||||
if [ ! -z "${CONTENTS_URL}" ]; then echo && echo "More info: ${CONTENTS_URL}"; fi
|
||||
echo
|
||||
EOF
|
||||
)"
|
||||
if [ -f "${SCRIPT_DIR}/meta.env" ]; then
|
||||
mkdir -p /usr/local/etc/vscode-dev-containers/
|
||||
cp -f "${SCRIPT_DIR}/meta.env" /usr/local/etc/vscode-dev-containers/meta.env
|
||||
echo "${meta_info_script}" > /usr/local/bin/devcontainer-info
|
||||
chmod +x /usr/local/bin/devcontainer-info
|
||||
fi
|
||||
|
||||
# Write marker file
|
||||
mkdir -p "$(dirname "${MARKER_FILE}")"
|
||||
echo -e "\
|
||||
PACKAGES_ALREADY_INSTALLED=${PACKAGES_ALREADY_INSTALLED}\n\
|
||||
LOCALE_ALREADY_SET=${LOCALE_ALREADY_SET}\n\
|
||||
EXISTING_NON_ROOT_USER=${EXISTING_NON_ROOT_USER}\n\
|
||||
RC_SNIPPET_ALREADY_ADDED=${RC_SNIPPET_ALREADY_ADDED}\n\
|
||||
ZSH_ALREADY_INSTALLED=${ZSH_ALREADY_INSTALLED}" > "${MARKER_FILE}"
|
||||
|
||||
echo "Done!"
|
||||
@@ -1,224 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
#
|
||||
# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/docker.md
|
||||
# Maintainer: The VS Code and Codespaces Teams
|
||||
#
|
||||
# Syntax: ./docker-debian.sh [enable non-root docker socket access flag] [source socket] [target socket] [non-root user] [use moby]
|
||||
|
||||
ENABLE_NONROOT_DOCKER=${1:-"true"}
|
||||
SOURCE_SOCKET=${2:-"/var/run/docker-host.sock"}
|
||||
TARGET_SOCKET=${3:-"/var/run/docker.sock"}
|
||||
USERNAME=${4:-"automatic"}
|
||||
USE_MOBY=${5:-"true"}
|
||||
MICROSOFT_GPG_KEYS_URI="https://packages.microsoft.com/keys/microsoft.asc"
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine the appropriate non-root user
|
||||
if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then
|
||||
USERNAME=""
|
||||
POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)")
|
||||
for CURRENT_USER in ${POSSIBLE_USERS[@]}; do
|
||||
if id -u ${CURRENT_USER} > /dev/null 2>&1; then
|
||||
USERNAME=${CURRENT_USER}
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ "${USERNAME}" = "" ]; then
|
||||
USERNAME=root
|
||||
fi
|
||||
elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then
|
||||
USERNAME=root
|
||||
fi
|
||||
|
||||
# Get central common setting
|
||||
get_common_setting() {
|
||||
if [ "${common_settings_file_loaded}" != "true" ]; then
|
||||
curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping."
|
||||
common_settings_file_loaded=true
|
||||
fi
|
||||
if [ -f "/tmp/vsdc-settings.env" ]; then
|
||||
local multi_line=""
|
||||
if [ "$2" = "true" ]; then multi_line="-z"; fi
|
||||
local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')"
|
||||
if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi
|
||||
fi
|
||||
echo "$1=${!1}"
|
||||
}
|
||||
|
||||
# Function to run apt-get if needed
|
||||
apt_get_update_if_needed()
|
||||
{
|
||||
if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then
|
||||
echo "Running apt-get update..."
|
||||
apt-get update
|
||||
else
|
||||
echo "Skipping apt-get update."
|
||||
fi
|
||||
}
|
||||
|
||||
# Checks if packages are installed and installs them if not
|
||||
check_packages() {
|
||||
if ! dpkg -s "$@" > /dev/null 2>&1; then
|
||||
apt_get_update_if_needed
|
||||
apt-get -y install --no-install-recommends "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Ensure apt is in non-interactive to avoid prompts
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install dependencies
|
||||
check_packages apt-transport-https curl ca-certificates gnupg2
|
||||
|
||||
# Install Docker / Moby CLI if not already installed
|
||||
if type docker > /dev/null 2>&1; then
|
||||
echo "Docker / Moby CLI already installed."
|
||||
else
|
||||
# Source /etc/os-release to get OS info
|
||||
. /etc/os-release
|
||||
if [ "${USE_MOBY}" = "true" ]; then
|
||||
# Import key safely (new 'signed-by' method rather than deprecated apt-key approach) and install
|
||||
get_common_setting MICROSOFT_GPG_KEYS_URI
|
||||
curl -sSL ${MICROSOFT_GPG_KEYS_URI} | gpg --dearmor > /usr/share/keyrings/microsoft-archive-keyring.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] https://packages.microsoft.com/repos/microsoft-${ID}-${VERSION_CODENAME}-prod ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/microsoft.list
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends moby-cli moby-buildx moby-compose
|
||||
else
|
||||
# Import key safely (new 'signed-by' method rather than deprecated apt-key approach) and install
|
||||
curl -fsSL https://download.docker.com/linux/${ID}/gpg | gpg --dearmor > /usr/share/keyrings/docker-archive-keyring.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/${ID} ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends docker-ce-cli
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install Docker Compose if not already installed and is on a supported architecture
|
||||
if type docker-compose > /dev/null 2>&1; then
|
||||
echo "Docker Compose already installed."
|
||||
else
|
||||
TARGET_COMPOSE_ARCH="$(uname -m)"
|
||||
if [ "${TARGET_COMPOSE_ARCH}" = "amd64" ]; then
|
||||
TARGET_COMPOSE_ARCH="x86_64"
|
||||
fi
|
||||
if [ "${TARGET_COMPOSE_ARCH}" != "x86_64" ]; then
|
||||
# Use pip to get a version that runns on this architecture
|
||||
if ! dpkg -s python3-minimal python3-pip libffi-dev python3-venv pipx > /dev/null 2>&1; then
|
||||
apt_get_update_if_needed
|
||||
apt-get -y install python3-minimal python3-pip libffi-dev python3-venv pipx
|
||||
fi
|
||||
export PIPX_HOME=/usr/local/pipx
|
||||
mkdir -p ${PIPX_HOME}
|
||||
export PIPX_BIN_DIR=/usr/local/bin
|
||||
export PIP_CACHE_DIR=/tmp/pip-tmp/cache
|
||||
pipx install --system-site-packages --pip-args '--no-cache-dir --force-reinstall' docker-compose
|
||||
rm -rf /tmp/pip-tmp
|
||||
else
|
||||
LATEST_COMPOSE_VERSION=$(basename "$(curl -fsSL -o /dev/null -w "%{url_effective}" https://github.com/docker/compose/releases/latest)")
|
||||
curl -fsSL "https://github.com/docker/compose/releases/download/${LATEST_COMPOSE_VERSION}/docker-compose-$(uname -s)-${TARGET_COMPOSE_ARCH}" -o /usr/local/bin/docker-compose
|
||||
chmod +x /usr/local/bin/docker-compose
|
||||
fi
|
||||
fi
|
||||
|
||||
# If init file already exists, exit
|
||||
if [ -f "/usr/local/share/docker-init.sh" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# By default, make the source and target sockets the same
|
||||
if [ "${SOURCE_SOCKET}" != "${TARGET_SOCKET}" ]; then
|
||||
touch "${SOURCE_SOCKET}"
|
||||
ln -s "${SOURCE_SOCKET}" "${TARGET_SOCKET}"
|
||||
fi
|
||||
|
||||
# Add a stub if not adding non-root user access, user is root
|
||||
if [ "${ENABLE_NONROOT_DOCKER}" = "false" ] || [ "${USERNAME}" = "root" ]; then
|
||||
echo '/usr/bin/env bash -c "\$@"' > /usr/local/share/docker-init.sh
|
||||
chmod +x /usr/local/share/docker-init.sh
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# If enabling non-root access and specified user is found, setup socat and add script
|
||||
chown -h "${USERNAME}":root "${TARGET_SOCKET}"
|
||||
if ! dpkg -s socat > /dev/null 2>&1; then
|
||||
apt_get_update_if_needed
|
||||
apt-get -y install socat
|
||||
fi
|
||||
tee /usr/local/share/docker-init.sh > /dev/null \
|
||||
<< EOF
|
||||
#!/usr/bin/env bash
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
|
||||
set -e
|
||||
|
||||
SOCAT_PATH_BASE=/tmp/vscr-docker-from-docker
|
||||
SOCAT_LOG=\${SOCAT_PATH_BASE}.log
|
||||
SOCAT_PID=\${SOCAT_PATH_BASE}.pid
|
||||
|
||||
# Wrapper function to only use sudo if not already root
|
||||
sudoIf()
|
||||
{
|
||||
if [ "\$(id -u)" -ne 0 ]; then
|
||||
sudo "\$@"
|
||||
else
|
||||
"\$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Log messages
|
||||
log()
|
||||
{
|
||||
echo -e "[\$(date)] \$@" | sudoIf tee -a \${SOCAT_LOG} > /dev/null
|
||||
}
|
||||
|
||||
echo -e "\n** \$(date) **" | sudoIf tee -a \${SOCAT_LOG} > /dev/null
|
||||
log "Ensuring ${USERNAME} has access to ${SOURCE_SOCKET} via ${TARGET_SOCKET}"
|
||||
|
||||
# If enabled, try to add a docker group with the right GID. If the group is root,
|
||||
# fall back on using socat to forward the docker socket to another unix socket so
|
||||
# that we can set permissions on it without affecting the host.
|
||||
if [ "${ENABLE_NONROOT_DOCKER}" = "true" ] && [ "${SOURCE_SOCKET}" != "${TARGET_SOCKET}" ] && [ "${USERNAME}" != "root" ] && [ "${USERNAME}" != "0" ]; then
|
||||
SOCKET_GID=\$(stat -c '%g' ${SOURCE_SOCKET})
|
||||
if [ "\${SOCKET_GID}" != "0" ]; then
|
||||
log "Adding user to group with GID \${SOCKET_GID}."
|
||||
if [ "\$(cat /etc/group | grep :\${SOCKET_GID}:)" = "" ]; then
|
||||
sudoIf groupadd --gid \${SOCKET_GID} docker-host
|
||||
fi
|
||||
# Add user to group if not already in it
|
||||
if [ "\$(id ${USERNAME} | grep -E "groups.*(=|,)\${SOCKET_GID}\(")" = "" ]; then
|
||||
sudoIf usermod -aG \${SOCKET_GID} ${USERNAME}
|
||||
fi
|
||||
else
|
||||
# Enable proxy if not already running
|
||||
if [ ! -f "\${SOCAT_PID}" ] || ! ps -p \$(cat \${SOCAT_PID}) > /dev/null; then
|
||||
log "Enabling socket proxy."
|
||||
log "Proxying ${SOURCE_SOCKET} to ${TARGET_SOCKET} for vscode"
|
||||
sudoIf rm -rf ${TARGET_SOCKET}
|
||||
(sudoIf socat UNIX-LISTEN:${TARGET_SOCKET},fork,mode=660,user=${USERNAME} UNIX-CONNECT:${SOURCE_SOCKET} 2>&1 | sudoIf tee -a \${SOCAT_LOG} > /dev/null & echo "\$!" | sudoIf tee \${SOCAT_PID} > /dev/null)
|
||||
else
|
||||
log "Socket proxy already running."
|
||||
fi
|
||||
fi
|
||||
log "Success"
|
||||
fi
|
||||
|
||||
# Execute whatever commands were passed in (if any). This allows us
|
||||
# to set this script to ENTRYPOINT while still executing the default CMD.
|
||||
set +e
|
||||
exec "\$@"
|
||||
EOF
|
||||
chmod +x /usr/local/share/docker-init.sh
|
||||
chown ${USERNAME}:root /usr/local/share/docker-init.sh
|
||||
echo "Done!"
|
||||
@@ -1,237 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
#
|
||||
# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/docker-in-docker.md
|
||||
# Maintainer: The VS Code and Codespaces Teams
|
||||
#
|
||||
# Syntax: ./docker-in-docker-debian.sh [enable non-root docker access flag] [non-root user] [use moby]
|
||||
|
||||
ENABLE_NONROOT_DOCKER=${1:-"true"}
|
||||
USERNAME=${2:-"automatic"}
|
||||
USE_MOBY=${3:-"true"}
|
||||
MICROSOFT_GPG_KEYS_URI="https://packages.microsoft.com/keys/microsoft.asc"
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine the appropriate non-root user
|
||||
if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then
|
||||
USERNAME=""
|
||||
POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)")
|
||||
for CURRENT_USER in ${POSSIBLE_USERS[@]}; do
|
||||
if id -u ${CURRENT_USER} > /dev/null 2>&1; then
|
||||
USERNAME=${CURRENT_USER}
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ "${USERNAME}" = "" ]; then
|
||||
USERNAME=root
|
||||
fi
|
||||
elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then
|
||||
USERNAME=root
|
||||
fi
|
||||
|
||||
# Get central common setting
|
||||
get_common_setting() {
|
||||
if [ "${common_settings_file_loaded}" != "true" ]; then
|
||||
curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping."
|
||||
common_settings_file_loaded=true
|
||||
fi
|
||||
if [ -f "/tmp/vsdc-settings.env" ]; then
|
||||
local multi_line=""
|
||||
if [ "$2" = "true" ]; then multi_line="-z"; fi
|
||||
local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')"
|
||||
if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi
|
||||
fi
|
||||
echo "$1=${!1}"
|
||||
}
|
||||
|
||||
# Function to run apt-get if needed
|
||||
apt_get_update_if_needed()
|
||||
{
|
||||
if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then
|
||||
echo "Running apt-get update..."
|
||||
apt-get update
|
||||
else
|
||||
echo "Skipping apt-get update."
|
||||
fi
|
||||
}
|
||||
|
||||
# Checks if packages are installed and installs them if not
|
||||
check_packages() {
|
||||
if ! dpkg -s "$@" > /dev/null 2>&1; then
|
||||
apt_get_update_if_needed
|
||||
apt-get -y install --no-install-recommends "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Ensure apt is in non-interactive to avoid prompts
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install dependencies
|
||||
check_packages apt-transport-https curl ca-certificates lxc pigz iptables gnupg2
|
||||
|
||||
# Swap to legacy iptables for compatibility
|
||||
if type iptables-legacy > /dev/null 2>&1; then
|
||||
update-alternatives --set iptables /usr/sbin/iptables-legacy
|
||||
update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
|
||||
fi
|
||||
|
||||
# Install Docker / Moby CLI if not already installed
|
||||
if type docker > /dev/null 2>&1 && type dockerd > /dev/null 2>&1; then
|
||||
echo "Docker / Moby CLI and Engine already installed."
|
||||
else
|
||||
# Source /etc/os-release to get OS info
|
||||
. /etc/os-release
|
||||
if [ "${USE_MOBY}" = "true" ]; then
|
||||
# Import key safely (new 'signed-by' method rather than deprecated apt-key approach) and install
|
||||
get_common_setting MICROSOFT_GPG_KEYS_URI
|
||||
curl -sSL ${MICROSOFT_GPG_KEYS_URI} | gpg --dearmor > /usr/share/keyrings/microsoft-archive-keyring.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] https://packages.microsoft.com/repos/microsoft-${ID}-${VERSION_CODENAME}-prod ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/microsoft.list
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends moby-cli moby-buildx moby-compose moby-engine
|
||||
else
|
||||
# Import key safely (new 'signed-by' method rather than deprecated apt-key approach) and install
|
||||
curl -fsSL https://download.docker.com/linux/${ID}/gpg | gpg --dearmor > /usr/share/keyrings/docker-archive-keyring.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/${ID} ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends docker-ce-cli docker-ce
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Finished installing docker / moby"
|
||||
|
||||
# Install Docker Compose if not already installed and is on a supported architecture
|
||||
if type docker-compose > /dev/null 2>&1; then
|
||||
echo "Docker Compose already installed."
|
||||
else
|
||||
TARGET_COMPOSE_ARCH="$(uname -m)"
|
||||
if [ "${TARGET_COMPOSE_ARCH}" = "amd64" ]; then
|
||||
TARGET_COMPOSE_ARCH="x86_64"
|
||||
fi
|
||||
if [ "${TARGET_COMPOSE_ARCH}" != "x86_64" ]; then
|
||||
# Use pip to get a version that runns on this architecture
|
||||
if ! dpkg -s python3-minimal python3-pip libffi-dev python3-venv > /dev/null 2>&1; then
|
||||
apt_get_update_if_needed
|
||||
apt-get -y install python3-minimal python3-pip libffi-dev python3-venv
|
||||
fi
|
||||
export PIPX_HOME=/usr/local/pipx
|
||||
mkdir -p ${PIPX_HOME}
|
||||
export PIPX_BIN_DIR=/usr/local/bin
|
||||
export PYTHONUSERBASE=/tmp/pip-tmp
|
||||
export PIP_CACHE_DIR=/tmp/pip-tmp/cache
|
||||
pipx_bin=pipx
|
||||
if ! type pipx > /dev/null 2>&1; then
|
||||
pip3 install --disable-pip-version-check --no-warn-script-location --no-cache-dir --user pipx
|
||||
pipx_bin=/tmp/pip-tmp/bin/pipx
|
||||
fi
|
||||
${pipx_bin} install --system-site-packages --pip-args '--no-cache-dir --force-reinstall' docker-compose
|
||||
rm -rf /tmp/pip-tmp
|
||||
else
|
||||
LATEST_COMPOSE_VERSION=$(basename "$(curl -fsSL -o /dev/null -w "%{url_effective}" https://github.com/docker/compose/releases/latest)")
|
||||
curl -fsSL "https://github.com/docker/compose/releases/download/${LATEST_COMPOSE_VERSION}/docker-compose-$(uname -s)-${TARGET_COMPOSE_ARCH}" -o /usr/local/bin/docker-compose
|
||||
chmod +x /usr/local/bin/docker-compose
|
||||
fi
|
||||
fi
|
||||
|
||||
# If init file already exists, exit
|
||||
if [ -f "/usr/local/share/docker-init.sh" ]; then
|
||||
echo "/usr/local/share/docker-init.sh already exists, so exiting."
|
||||
exit 0
|
||||
fi
|
||||
echo "docker-init doesnt exist..."
|
||||
|
||||
# Add user to the docker group
|
||||
if [ "${ENABLE_NONROOT_DOCKER}" = "true" ]; then
|
||||
if ! getent group docker > /dev/null 2>&1; then
|
||||
groupadd docker
|
||||
fi
|
||||
|
||||
usermod -aG docker ${USERNAME}
|
||||
fi
|
||||
|
||||
tee /usr/local/share/docker-init.sh > /dev/null \
|
||||
<< 'EOF'
|
||||
#!/usr/bin/env bash
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
|
||||
sudoIf()
|
||||
{
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
sudo "$@"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# explicitly remove dockerd and containerd PID file to ensure that it can start properly if it was stopped uncleanly
|
||||
# ie: docker kill <ID>
|
||||
sudoIf find /run /var/run -iname 'docker*.pid' -delete || :
|
||||
sudoIf find /run /var/run -iname 'container*.pid' -delete || :
|
||||
|
||||
set -e
|
||||
|
||||
## Dind wrapper script from docker team
|
||||
# Maintained: https://github.com/moby/moby/blob/master/hack/dind
|
||||
|
||||
export container=docker
|
||||
|
||||
if [ -d /sys/kernel/security ] && ! sudoIf mountpoint -q /sys/kernel/security; then
|
||||
sudoIf mount -t securityfs none /sys/kernel/security || {
|
||||
echo >&2 'Could not mount /sys/kernel/security.'
|
||||
echo >&2 'AppArmor detection and --privileged mode might break.'
|
||||
}
|
||||
fi
|
||||
|
||||
# Mount /tmp (conditionally)
|
||||
if ! sudoIf mountpoint -q /tmp; then
|
||||
sudoIf mount -t tmpfs none /tmp
|
||||
fi
|
||||
|
||||
# cgroup v2: enable nesting
|
||||
if [ -f /sys/fs/cgroup/cgroup.controllers ]; then
|
||||
# move the init process (PID 1) from the root group to the /init group,
|
||||
# otherwise writing subtree_control fails with EBUSY.
|
||||
sudoIf mkdir -p /sys/fs/cgroup/init
|
||||
sudoIf echo 1 > /sys/fs/cgroup/init/cgroup.procs
|
||||
# enable controllers
|
||||
sudoIf sed -e 's/ / +/g' -e 's/^/+/' < /sys/fs/cgroup/cgroup.controllers \
|
||||
> /sys/fs/cgroup/cgroup.subtree_control
|
||||
fi
|
||||
## Dind wrapper over.
|
||||
|
||||
# Handle DNS
|
||||
set +e
|
||||
cat /etc/resolv.conf | grep -i 'internal.cloudapp.net'
|
||||
if [ $? -eq 0 ]
|
||||
then
|
||||
echo "Setting dockerd Azure DNS."
|
||||
CUSTOMDNS="--dns 168.63.129.16"
|
||||
else
|
||||
echo "Not setting dockerd DNS manually."
|
||||
CUSTOMDNS=""
|
||||
fi
|
||||
set -e
|
||||
|
||||
# Start docker/moby engine
|
||||
( sudoIf dockerd $CUSTOMDNS > /tmp/dockerd.log 2>&1 ) &
|
||||
|
||||
set +e
|
||||
|
||||
# Execute whatever commands were passed in (if any). This allows us
|
||||
# to set this script to ENTRYPOINT while still executing the default CMD.
|
||||
exec "$@"
|
||||
EOF
|
||||
|
||||
chmod +x /usr/local/share/docker-init.sh
|
||||
chown ${USERNAME}:root /usr/local/share/docker-init.sh
|
||||
@@ -1,140 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
#
|
||||
# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/git-from-src.md
|
||||
# Maintainer: The VS Code and Codespaces Teams
|
||||
#
|
||||
# Syntax: ./git-from-src-debian.sh [version] [use PPA if available]
|
||||
|
||||
GIT_VERSION=${1:-"latest"}
|
||||
USE_PPA_IF_AVAILABLE=${2:-"false"}
|
||||
|
||||
GIT_CORE_PPA_ARCHIVE_GPG_KEY=E1DD270288B4E6030699E45FA1715D88E1DF1F24
|
||||
GPG_KEY_SERVERS="keyserver hkp://keyserver.ubuntu.com:80
|
||||
keyserver hkps://keys.openpgp.org
|
||||
keyserver hkp://keyserver.pgp.com"
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get central common setting
|
||||
get_common_setting() {
|
||||
if [ "${common_settings_file_loaded}" != "true" ]; then
|
||||
curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping."
|
||||
common_settings_file_loaded=true
|
||||
fi
|
||||
if [ -f "/tmp/vsdc-settings.env" ]; then
|
||||
local multi_line=""
|
||||
if [ "$2" = "true" ]; then multi_line="-z"; fi
|
||||
local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')"
|
||||
if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi
|
||||
fi
|
||||
echo "$1=${!1}"
|
||||
}
|
||||
|
||||
# Import the specified key in a variable name passed in as
|
||||
receive_gpg_keys() {
|
||||
get_common_setting $1
|
||||
local keys=${!1}
|
||||
get_common_setting GPG_KEY_SERVERS true
|
||||
local keyring_args=""
|
||||
if [ ! -z "$2" ]; then
|
||||
mkdir -p "$(dirname \"$2\")"
|
||||
keyring_args="--no-default-keyring --keyring $2"
|
||||
fi
|
||||
|
||||
# Use a temporary locaiton for gpg keys to avoid polluting image
|
||||
export GNUPGHOME="/tmp/tmp-gnupg"
|
||||
mkdir -p ${GNUPGHOME}
|
||||
chmod 700 ${GNUPGHOME}
|
||||
echo -e "disable-ipv6\n${GPG_KEY_SERVERS}" > ${GNUPGHOME}/dirmngr.conf
|
||||
# GPG key download sometimes fails for some reason and retrying fixes it.
|
||||
local retry_count=0
|
||||
local gpg_ok="false"
|
||||
set +e
|
||||
until [ "${gpg_ok}" = "true" ] || [ "${retry_count}" -eq "5" ];
|
||||
do
|
||||
echo "(*) Downloading GPG key..."
|
||||
( echo "${keys}" | xargs -n 1 gpg -q ${keyring_args} --recv-keys) 2>&1 && gpg_ok="true"
|
||||
if [ "${gpg_ok}" != "true" ]; then
|
||||
echo "(*) Failed getting key, retring in 10s..."
|
||||
(( retry_count++ ))
|
||||
sleep 10s
|
||||
fi
|
||||
done
|
||||
set -e
|
||||
if [ "${gpg_ok}" = "false" ]; then
|
||||
echo "(!) Failed to install rvm."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to run apt-get if needed
|
||||
apt_get_update_if_needed()
|
||||
{
|
||||
if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then
|
||||
echo "Running apt-get update..."
|
||||
apt-get update
|
||||
else
|
||||
echo "Skipping apt-get update."
|
||||
fi
|
||||
}
|
||||
|
||||
# Checks if packages are installed and installs them if not
|
||||
check_packages() {
|
||||
if ! dpkg -s "$@" > /dev/null 2>&1; then
|
||||
apt_get_update_if_needed
|
||||
apt-get -y install --no-install-recommends "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Source /etc/os-release to get OS info
|
||||
. /etc/os-release
|
||||
# If ubuntu, PPAs allowed, and latest - install from there
|
||||
if ([ "${GIT_VERSION}" = "latest" ] || [ "${GIT_VERSION}" = "lts" ] || [ "${GIT_VERSION}" = "current" ]) && [ "${ID}" = "ubuntu" ] && [ "${USE_PPA_IF_AVAILABLE}" = "true" ]; then
|
||||
echo "Using PPA to install latest git..."
|
||||
check_packages apt-transport-https curl ca-certificates gnupg2
|
||||
receive_gpg_keys GIT_CORE_PPA_ARCHIVE_GPG_KEY /usr/share/keyrings/gitcoreppa-archive-keyring.gpg
|
||||
echo -e "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/gitcoreppa-archive-keyring.gpg] http://ppa.launchpad.net/git-core/ppa/ubuntu ${VERSION_CODENAME} main\ndeb-src [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/gitcoreppa-archive-keyring.gpg] http://ppa.launchpad.net/git-core/ppa/ubuntu ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/git-core-ppa.list
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends git
|
||||
rm -rf "/tmp/tmp-gnupg"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Install required packages to build if missing
|
||||
check_packages build-essential curl ca-certificates tar gettext libssl-dev zlib1g-dev libcurl?-openssl-dev libexpat1-dev
|
||||
|
||||
# Partial version matching
|
||||
if [ "$(echo "${GIT_VERSION}" | grep -o '\.' | wc -l)" != "2" ]; then
|
||||
requested_version="${GIT_VERSION}"
|
||||
version_list="$(curl -sSL -H "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/git/git/tags" | grep -oP '"name":\s*"v\K[0-9]+\.[0-9]+\.[0-9]+"' | tr -d '"' | sort -rV )"
|
||||
if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "lts" ] || [ "${requested_version}" = "current" ]; then
|
||||
GIT_VERSION="$(echo "${version_list}" | head -n 1)"
|
||||
else
|
||||
set +e
|
||||
GIT_VERSION="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")"
|
||||
set -e
|
||||
fi
|
||||
if [ -z "${GIT_VERSION}" ] || ! echo "${version_list}" | grep "^${GIT_VERSION//./\\.}$" > /dev/null 2>&1; then
|
||||
echo "Invalid git version: ${requested_version}" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Downloading source for ${GIT_VERSION}..."
|
||||
curl -sL https://github.com/git/git/archive/v${GIT_VERSION}.tar.gz | tar -xzC /tmp 2>&1
|
||||
echo "Building..."
|
||||
cd /tmp/git-${GIT_VERSION}
|
||||
make -s prefix=/usr/local all && make -s prefix=/usr/local install 2>&1
|
||||
rm -rf /tmp/git-${GIT_VERSION}
|
||||
echo "Done!"
|
||||
@@ -1,188 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
#
|
||||
# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/github.md
|
||||
# Maintainer: The VS Code and Codespaces Teams
|
||||
#
|
||||
# Syntax: ./github-debian.sh [version]
|
||||
|
||||
CLI_VERSION=${1:-"latest"}
|
||||
|
||||
GITHUB_CLI_ARCHIVE_GPG_KEY=C99B11DEB97541F0
|
||||
GPG_KEY_SERVERS="keyserver hkp://keyserver.ubuntu.com:80
|
||||
keyserver hkps://keys.openpgp.org
|
||||
keyserver hkp://keyserver.pgp.com"
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get central common setting
|
||||
get_common_setting() {
|
||||
if [ "${common_settings_file_loaded}" != "true" ]; then
|
||||
curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping."
|
||||
common_settings_file_loaded=true
|
||||
fi
|
||||
if [ -f "/tmp/vsdc-settings.env" ]; then
|
||||
local multi_line=""
|
||||
if [ "$2" = "true" ]; then multi_line="-z"; fi
|
||||
local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')"
|
||||
if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi
|
||||
fi
|
||||
echo "$1=${!1}"
|
||||
}
|
||||
|
||||
# Import the specified key in a variable name passed in as
|
||||
receive_gpg_keys() {
|
||||
get_common_setting $1
|
||||
local keys=${!1}
|
||||
get_common_setting GPG_KEY_SERVERS true
|
||||
|
||||
# Use a temporary locaiton for gpg keys to avoid polluting image
|
||||
export GNUPGHOME="/tmp/tmp-gnupg"
|
||||
mkdir -p ${GNUPGHOME}
|
||||
chmod 700 ${GNUPGHOME}
|
||||
echo -e "disable-ipv6\n${GPG_KEY_SERVERS}" > ${GNUPGHOME}/dirmngr.conf
|
||||
# GPG key download sometimes fails for some reason and retrying fixes it.
|
||||
local retry_count=0
|
||||
local gpg_ok="false"
|
||||
set +e
|
||||
until [ "${gpg_ok}" = "true" ] || [ "${retry_count}" -eq "5" ];
|
||||
do
|
||||
echo "(*) Downloading GPG key..."
|
||||
( echo "${keys}" | xargs -n 1 gpg --recv-keys) 2>&1 && gpg_ok="true"
|
||||
if [ "${gpg_ok}" != "true" ]; then
|
||||
echo "(*) Failed getting key, retring in 10s..."
|
||||
(( retry_count++ ))
|
||||
sleep 10s
|
||||
fi
|
||||
done
|
||||
set -e
|
||||
if [ "${gpg_ok}" = "false" ]; then
|
||||
echo "(!) Failed to install rvm."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Figure out correct version of a three part version number is not passed
|
||||
find_version_from_git_tags() {
|
||||
local variable_name=$1
|
||||
local requested_version=${!variable_name}
|
||||
if [ "${requested_version}" = "none" ]; then return; fi
|
||||
local repository=$2
|
||||
local prefix=${3:-"tags/v"}
|
||||
local separator=${4:-"."}
|
||||
local last_part_optional=${5:-"false"}
|
||||
if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then
|
||||
local escaped_separator=${separator//./\\.}
|
||||
local last_part
|
||||
if [ "${last_part_optional}" = "true" ]; then
|
||||
last_part="(${escaped_separator}[0-9]+)?"
|
||||
else
|
||||
last_part="${escaped_separator}[0-9]+"
|
||||
fi
|
||||
local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$"
|
||||
local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)"
|
||||
if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then
|
||||
declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)"
|
||||
else
|
||||
set +e
|
||||
declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")"
|
||||
set -e
|
||||
fi
|
||||
fi
|
||||
if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then
|
||||
echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "${variable_name}=${!variable_name}"
|
||||
}
|
||||
|
||||
# Import the specified key in a variable name passed in as
|
||||
receive_gpg_keys() {
|
||||
get_common_setting $1
|
||||
local keys=${!1}
|
||||
get_common_setting GPG_KEY_SERVERS true
|
||||
local keyring_args=""
|
||||
if [ ! -z "$2" ]; then
|
||||
keyring_args="--no-default-keyring --keyring $2"
|
||||
fi
|
||||
|
||||
# Use a temporary locaiton for gpg keys to avoid polluting image
|
||||
export GNUPGHOME="/tmp/tmp-gnupg"
|
||||
mkdir -p ${GNUPGHOME}
|
||||
chmod 700 ${GNUPGHOME}
|
||||
echo -e "disable-ipv6\n${GPG_KEY_SERVERS}" > ${GNUPGHOME}/dirmngr.conf
|
||||
# GPG key download sometimes fails for some reason and retrying fixes it.
|
||||
local retry_count=0
|
||||
local gpg_ok="false"
|
||||
set +e
|
||||
until [ "${gpg_ok}" = "true" ] || [ "${retry_count}" -eq "5" ];
|
||||
do
|
||||
echo "(*) Downloading GPG key..."
|
||||
( echo "${keys}" | xargs -n 1 gpg -q ${keyring_args} --recv-keys) 2>&1 && gpg_ok="true"
|
||||
if [ "${gpg_ok}" != "true" ]; then
|
||||
echo "(*) Failed getting key, retring in 10s..."
|
||||
(( retry_count++ ))
|
||||
sleep 10s
|
||||
fi
|
||||
done
|
||||
set -e
|
||||
if [ "${gpg_ok}" = "false" ]; then
|
||||
echo "(!) Failed to install rvm."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to run apt-get if needed
|
||||
apt_get_update_if_needed()
|
||||
{
|
||||
if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then
|
||||
echo "Running apt-get update..."
|
||||
apt-get update
|
||||
else
|
||||
echo "Skipping apt-get update."
|
||||
fi
|
||||
}
|
||||
|
||||
# Checks if packages are installed and installs them if not
|
||||
check_packages() {
|
||||
if ! dpkg -s "$@" > /dev/null 2>&1; then
|
||||
apt_get_update_if_needed
|
||||
apt-get -y install --no-install-recommends "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install curl, apt-transport-https, curl, gpg, or dirmngr, git if missing
|
||||
check_packages curl ca-certificates apt-transport-https dirmngr gnupg2
|
||||
if ! type git > /dev/null 2>&1; then
|
||||
apt_get_update_if_needed
|
||||
apt-get -y install --no-install-recommends git
|
||||
fi
|
||||
|
||||
# Soft version matching
|
||||
if [ "${CLI_VERSION}" != "latest" ] && [ "${CLI_VERSION}" != "lts" ] && [ "${CLI_VERSION}" != "stable" ]; then
|
||||
find_version_from_git_tags CLI_VERSION "https://github.com/cli/cli"
|
||||
version_suffix="=${CLI_VERSION}"
|
||||
else
|
||||
version_suffix=""
|
||||
fi
|
||||
|
||||
# Install the GitHub CLI
|
||||
echo "Downloading github CLI..."
|
||||
# Import key safely (new method rather than deprecated apt-key approach) and install
|
||||
. /etc/os-release
|
||||
receive_gpg_keys GITHUB_CLI_ARCHIVE_GPG_KEY /usr/share/keyrings/githubcli-archive-keyring.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/github-cli.list
|
||||
apt-get update
|
||||
apt-get -y install "gh${version_suffix}"
|
||||
rm -rf "/tmp/gh/gnupg"
|
||||
echo "Done!"
|
||||
@@ -1,201 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
#
|
||||
# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/go.md
|
||||
# Maintainer: The VS Code and Codespaces Teams
|
||||
#
|
||||
# Syntax: ./go-debian.sh [Go version] [GOROOT] [GOPATH] [non-root user] [Add GOPATH, GOROOT to rc files flag] [Install tools flag]
|
||||
|
||||
TARGET_GO_VERSION=${1:-"latest"}
|
||||
TARGET_GOROOT=${2:-"/usr/local/go"}
|
||||
TARGET_GOPATH=${3:-"/go"}
|
||||
USERNAME=${4:-"automatic"}
|
||||
UPDATE_RC=${5:-"true"}
|
||||
INSTALL_GO_TOOLS=${6:-"true"}
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure that login shells get the correct path if the user updated the PATH using ENV.
|
||||
rm -f /etc/profile.d/00-restore-env.sh
|
||||
echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh
|
||||
chmod +x /etc/profile.d/00-restore-env.sh
|
||||
|
||||
# Determine the appropriate non-root user
|
||||
if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then
|
||||
USERNAME=""
|
||||
POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)")
|
||||
for CURRENT_USER in ${POSSIBLE_USERS[@]}; do
|
||||
if id -u ${CURRENT_USER} > /dev/null 2>&1; then
|
||||
USERNAME=${CURRENT_USER}
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ "${USERNAME}" = "" ]; then
|
||||
USERNAME=root
|
||||
fi
|
||||
elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then
|
||||
USERNAME=root
|
||||
fi
|
||||
|
||||
updaterc() {
|
||||
if [ "${UPDATE_RC}" = "true" ]; then
|
||||
echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..."
|
||||
echo -e "$1" >> /etc/bash.bashrc
|
||||
if [ -f "/etc/zsh/zshrc" ]; then
|
||||
echo -e "$1" >> /etc/zsh/zshrc
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Figure out correct version of a three part version number is not passed
|
||||
find_version_from_git_tags() {
|
||||
local variable_name=$1
|
||||
local requested_version=${!variable_name}
|
||||
if [ "${requested_version}" = "none" ]; then return; fi
|
||||
local repository=$2
|
||||
local prefix=${3:-"tags/v"}
|
||||
local separator=${4:-"."}
|
||||
local last_part_optional=${5:-"false"}
|
||||
if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then
|
||||
local escaped_separator=${separator//./\\.}
|
||||
local last_part
|
||||
if [ "${last_part_optional}" = "true" ]; then
|
||||
last_part="(${escaped_separator}[0-9]+)?"
|
||||
else
|
||||
last_part="${escaped_separator}[0-9]+"
|
||||
fi
|
||||
local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$"
|
||||
local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)"
|
||||
if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then
|
||||
declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)"
|
||||
else
|
||||
set +e
|
||||
declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")"
|
||||
set -e
|
||||
fi
|
||||
fi
|
||||
if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then
|
||||
echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "${variable_name}=${!variable_name}"
|
||||
}
|
||||
|
||||
# Function to run apt-get if needed
|
||||
apt_get_update_if_needed()
|
||||
{
|
||||
if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then
|
||||
echo "Running apt-get update..."
|
||||
apt-get update
|
||||
else
|
||||
echo "Skipping apt-get update."
|
||||
fi
|
||||
}
|
||||
|
||||
# Checks if packages are installed and installs them if not
|
||||
check_packages() {
|
||||
if ! dpkg -s "$@" > /dev/null 2>&1; then
|
||||
apt_get_update_if_needed
|
||||
apt-get -y install --no-install-recommends "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install curl, tar, git, other dependencies if missing
|
||||
check_packages curl ca-certificates tar g++ gcc libc6-dev make pkg-config
|
||||
if ! type git > /dev/null 2>&1; then
|
||||
apt_get_update_if_needed
|
||||
apt-get -y install --no-install-recommends git
|
||||
fi
|
||||
|
||||
# Get closest match for version number specified
|
||||
find_version_from_git_tags TARGET_GO_VERSION "https://go.googlesource.com/go" "tags/go" "." "true"
|
||||
|
||||
architecture="$(uname -m)"
|
||||
case $architecture in
|
||||
x86_64) architecture="amd64";;
|
||||
aarch64 | armv8*) architecture="arm64";;
|
||||
aarch32 | armv7* | armvhf*) architecture="armv6l";;
|
||||
i?86) architecture="386";;
|
||||
*) echo "(!) Architecture $architecture unsupported"; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Install Go
|
||||
GO_INSTALL_SCRIPT="$(cat <<EOF
|
||||
set -e
|
||||
echo "Downloading Go ${TARGET_GO_VERSION}..."
|
||||
curl -sSL -o /tmp/go.tar.gz "https://golang.org/dl/go${TARGET_GO_VERSION}.linux-${architecture}.tar.gz"
|
||||
echo "Extracting Go ${TARGET_GO_VERSION}..."
|
||||
tar -xzf /tmp/go.tar.gz -C "${TARGET_GOROOT}" --strip-components=1
|
||||
rm -f /tmp/go.tar.gz
|
||||
EOF
|
||||
)"
|
||||
if [ "${TARGET_GO_VERSION}" != "none" ] && ! type go > /dev/null 2>&1; then
|
||||
mkdir -p "${TARGET_GOROOT}" "${TARGET_GOPATH}"
|
||||
chown -R ${USERNAME} "${TARGET_GOROOT}" "${TARGET_GOPATH}"
|
||||
su ${USERNAME} -c "${GO_INSTALL_SCRIPT}"
|
||||
else
|
||||
echo "Go already installed. Skipping."
|
||||
fi
|
||||
|
||||
# Install Go tools that are isImportant && !replacedByGopls based on
|
||||
# https://github.com/golang/vscode-go/blob/0ff533d408e4eb8ea54ce84d6efa8b2524d62873/src/goToolsInformation.ts
|
||||
# Exception `dlv-dap` is a copy of github.com/go-delve/delve/cmd/dlv built from the master.
|
||||
GO_TOOLS="\
|
||||
golang.org/x/tools/gopls@latest \
|
||||
honnef.co/go/tools/cmd/staticcheck@latest \
|
||||
golang.org/x/lint/golint@latest \
|
||||
github.com/mgechev/revive@latest \
|
||||
github.com/uudashr/gopkgs/v2/cmd/gopkgs@latest \
|
||||
github.com/ramya-rao-a/go-outline@latest \
|
||||
github.com/go-delve/delve/cmd/dlv@latest \
|
||||
github.com/golangci/golangci-lint/cmd/golangci-lint@latest"
|
||||
if [ "${INSTALL_GO_TOOLS}" = "true" ]; then
|
||||
echo "Installing common Go tools..."
|
||||
export PATH=${TARGET_GOROOT}/bin:${PATH}
|
||||
mkdir -p /tmp/gotools /usr/local/etc/vscode-dev-containers ${TARGET_GOPATH}/bin
|
||||
cd /tmp/gotools
|
||||
export GOPATH=/tmp/gotools
|
||||
export GOCACHE=/tmp/gotools/cache
|
||||
|
||||
# Use go get for versions of go under 1.17
|
||||
go_install_command=install
|
||||
if [[ "1.16" > "$(go version | grep -oP 'go\K[0-9]+\.[0-9]+(\.[0-9]+)?')" ]]; then
|
||||
export GO111MODULE=on
|
||||
go_install_command=get
|
||||
echo "Go version < 1.17, using go get."
|
||||
fi
|
||||
|
||||
(echo "${GO_TOOLS}" | xargs -n 1 go ${go_install_command} -v )2>&1 | tee -a /usr/local/etc/vscode-dev-containers/go.log
|
||||
|
||||
# Move Go tools into path and clean up
|
||||
mv /tmp/gotools/bin/* ${TARGET_GOPATH}/bin/
|
||||
|
||||
# install dlv-dap (dlv@master)
|
||||
go ${go_install_command} -v github.com/go-delve/delve/cmd/dlv@master 2>&1 | tee -a /usr/local/etc/vscode-dev-containers/go.log
|
||||
mv /tmp/gotools/bin/dlv ${TARGET_GOPATH}/bin/dlv-dap
|
||||
|
||||
rm -rf /tmp/gotools
|
||||
chown -R ${USERNAME} "${TARGET_GOPATH}"
|
||||
fi
|
||||
|
||||
# Add GOPATH variable and bin directory into PATH in bashrc/zshrc files (unless disabled)
|
||||
updaterc "$(cat << EOF
|
||||
export GOPATH="${TARGET_GOPATH}"
|
||||
if [[ "\${PATH}" != *"\${GOPATH}/bin"* ]]; then export PATH="\${PATH}:\${GOPATH}/bin"; fi
|
||||
export GOROOT="${TARGET_GOROOT}"
|
||||
if [[ "\${PATH}" != *"\${GOROOT}/bin"* ]]; then export PATH="\${PATH}:\${GOROOT}/bin"; fi
|
||||
EOF
|
||||
)"
|
||||
|
||||
echo "Done!"
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# This is a replicated script.
|
||||
#
|
||||
# Syntax: ./k3s-debian.sh [k3s version] [k3s SHA256]
|
||||
|
||||
set -e
|
||||
|
||||
K3S_VERSION="${1:-"latest"}" # latest is also valid
|
||||
K3S_SHA256="${2:-"automatic"}"
|
||||
|
||||
GPG_KEY_SERVERS="keyserver hkp://keyserver.ubuntu.com:80
|
||||
keyserver hkps://keys.openpgp.org
|
||||
keyserver hkp://keyserver.pgp.com"
|
||||
|
||||
architecture="$(uname -m)"
|
||||
case $architecture in
|
||||
x86_64) architecture="amd64";;
|
||||
aarch64 | armv8*) architecture="arm64";;
|
||||
aarch32 | armv7* | armvhf*) architecture="armhf";;
|
||||
*) echo "(!) Architecture $architecture unsupported"; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Figure out correct version of a three part version number is not passed
|
||||
find_version_from_git_tags() {
|
||||
local variable_name=$1
|
||||
local requested_version=${!variable_name}
|
||||
if [ "${requested_version}" = "none" ]; then return; fi
|
||||
local repository=$2
|
||||
local prefix=${3:-"tags/v"}
|
||||
local separator=${4:-"."}
|
||||
local last_part_optional=${5:-"false"}
|
||||
if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then
|
||||
local escaped_separator=${separator//./\\.}
|
||||
local last_part
|
||||
if [ "${last_part_optional}" = "true" ]; then
|
||||
last_part="(${escaped_separator}[0-9ks\+]+)?"
|
||||
else
|
||||
last_part="${escaped_separator}[0-9ks\+]+"
|
||||
fi
|
||||
local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$"
|
||||
local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)"
|
||||
echo $version_list
|
||||
if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then
|
||||
declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)"
|
||||
else
|
||||
set +e
|
||||
declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s+]|$)")"
|
||||
set -e
|
||||
fi
|
||||
fi
|
||||
if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then
|
||||
echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "${variable_name}=${!variable_name}"
|
||||
}
|
||||
|
||||
# Install K3s, verify checksum
|
||||
if [ "${K3S_VERSION}" != "none" ]; then
|
||||
echo "Downloading k3s..."
|
||||
urlPrefix=
|
||||
if [ "${K3S_VERSION}" = "latest" ] || [ "${K3S_VERSION}" = "lts" ] || [ "${K3S_VERSION}" = "current" ] || [ "${K3S_VERSION}" = "stable" ]; then
|
||||
K3S_VERSION="latest"
|
||||
urlPrefix="https://github.com/k3s-io/k3s/releases/latest/download"
|
||||
else
|
||||
find_version_from_git_tags K3S_VERSION https://github.com/k3s-io/k3s
|
||||
if [ "${K3S_VERSION::1}" != "v" ]; then
|
||||
K3S_VERSION="v${K3S_VERSION}"
|
||||
fi
|
||||
urlPrefix="https://github.com/k3s-io/k3s/releases/download/${K3S_VERSION}"
|
||||
fi
|
||||
|
||||
# URL encode plus sign
|
||||
K3S_VERSION="$(echo $K3S_VERSION | sed --expression='s/+/%2B/g')"
|
||||
|
||||
# latest is also valid in the download URLs
|
||||
downloadUrl="${urlPrefix}/k3s${architecture}"
|
||||
if [ "${architecture}" = "amd64" ]; then
|
||||
downloadUrl="${urlPrefix}/k3s"
|
||||
fi
|
||||
|
||||
curl -sSL -o /usr/local/bin/k3s "${downloadUrl}"
|
||||
chmod 0755 /usr/local/bin/k3s
|
||||
|
||||
if [ "$K3S_SHA256" = "automatic" ]; then
|
||||
|
||||
shaUrl="${urlPrefix}/sha256sum-${architecture}.txt"
|
||||
if [ "${architecture}" = "armhf" ]; then
|
||||
shaUrl="${urlPrefix}/sha256sum-arm.txt"
|
||||
fi
|
||||
|
||||
# Manifest contains image hashes, but we only need the binary
|
||||
K3S_SHA256="$(curl -sSL $shaUrl | grep -P '(^|\s)\Kk3s(?=\s|$)' | cut -d ' ' -f1 )"
|
||||
fi
|
||||
echo $K3S_SHA256
|
||||
([ "${K3S_SHA256}" = "dev-mode" ] || (echo "${K3S_SHA256} */usr/local/bin/k3s" | sha256sum -c -))
|
||||
if ! type k3s > /dev/null 2>&1; then
|
||||
echo '(!) k3s installation failed!'
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "\nDone!"
|
||||
@@ -1,218 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
#
|
||||
# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/kubectl-helm.md
|
||||
# Maintainer: The VS Code and Codespaces Teams
|
||||
#
|
||||
# Syntax: ./kubectl-helm-debian.sh [kubectl verison] [Helm version] [minikube version] [kubectl SHA256] [Helm SHA256] [minikube SHA256]
|
||||
|
||||
set -e
|
||||
|
||||
KUBECTL_VERSION="${1:-"latest"}"
|
||||
HELM_VERSION="${2:-"latest"}"
|
||||
MINIKUBE_VERSION="${3:-"none"}" # latest is also valid
|
||||
KUBECTL_SHA256="${4:-"automatic"}"
|
||||
HELM_SHA256="${5:-"automatic"}"
|
||||
MINIKUBE_SHA256="${6:-"automatic"}"
|
||||
|
||||
HELM_GPG_KEYS_URI="https://raw.githubusercontent.com/helm/helm/main/KEYS"
|
||||
GPG_KEY_SERVERS="keyserver hkp://keyserver.ubuntu.com:80
|
||||
keyserver hkps://keys.openpgp.org
|
||||
keyserver hkp://keyserver.pgp.com"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get central common setting
|
||||
get_common_setting() {
|
||||
if [ "${common_settings_file_loaded}" != "true" ]; then
|
||||
curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping."
|
||||
common_settings_file_loaded=true
|
||||
fi
|
||||
if [ -f "/tmp/vsdc-settings.env" ]; then
|
||||
local multi_line=""
|
||||
if [ "$2" = "true" ]; then multi_line="-z"; fi
|
||||
local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')"
|
||||
if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi
|
||||
fi
|
||||
echo "$1=${!1}"
|
||||
}
|
||||
|
||||
# Figure out correct version of a three part version number is not passed
|
||||
find_version_from_git_tags() {
|
||||
local variable_name=$1
|
||||
local requested_version=${!variable_name}
|
||||
if [ "${requested_version}" = "none" ]; then return; fi
|
||||
local repository=$2
|
||||
local prefix=${3:-"tags/v"}
|
||||
local separator=${4:-"."}
|
||||
local last_part_optional=${5:-"false"}
|
||||
if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then
|
||||
local escaped_separator=${separator//./\\.}
|
||||
local last_part
|
||||
if [ "${last_part_optional}" = "true" ]; then
|
||||
last_part="(${escaped_separator}[0-9]+)?"
|
||||
else
|
||||
last_part="${escaped_separator}[0-9]+"
|
||||
fi
|
||||
local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$"
|
||||
local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)"
|
||||
if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then
|
||||
declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)"
|
||||
else
|
||||
set +e
|
||||
declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")"
|
||||
set -e
|
||||
fi
|
||||
fi
|
||||
if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then
|
||||
echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "${variable_name}=${!variable_name}"
|
||||
}
|
||||
|
||||
# Function to run apt-get if needed
|
||||
apt_get_update_if_needed()
|
||||
{
|
||||
if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then
|
||||
echo "Running apt-get update..."
|
||||
apt-get update
|
||||
else
|
||||
echo "Skipping apt-get update."
|
||||
fi
|
||||
}
|
||||
|
||||
# Checks if packages are installed and installs them if not
|
||||
check_packages() {
|
||||
if ! dpkg -s "$@" > /dev/null 2>&1; then
|
||||
apt_get_update_if_needed
|
||||
apt-get -y install --no-install-recommends "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Ensure apt is in non-interactive to avoid prompts
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install dependencies
|
||||
check_packages curl ca-certificates coreutils gnupg2 dirmngr bash-completion
|
||||
if ! type git > /dev/null 2>&1; then
|
||||
apt_get_update_if_needed
|
||||
apt-get -y install --no-install-recommends git
|
||||
fi
|
||||
|
||||
architecture="$(uname -m)"
|
||||
case $architecture in
|
||||
x86_64) architecture="amd64";;
|
||||
aarch64 | armv8*) architecture="arm64";;
|
||||
aarch32 | armv7* | armvhf*) architecture="arm";;
|
||||
i?86) architecture="386";;
|
||||
*) echo "(!) Architecture $architecture unsupported"; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Install the kubectl, verify checksum
|
||||
echo "Downloading kubectl..."
|
||||
if [ "${KUBECTL_VERSION}" = "latest" ] || [ "${KUBECTL_VERSION}" = "lts" ] || [ "${KUBECTL_VERSION}" = "current" ] || [ "${KUBECTL_VERSION}" = "stable" ]; then
|
||||
KUBECTL_VERSION="$(curl -sSL https://dl.k8s.io/release/stable.txt)"
|
||||
else
|
||||
find_version_from_git_tags KUBECTL_VERSION https://github.com/kubernetes/kubernetes
|
||||
fi
|
||||
if [ "${KUBECTL_VERSION::1}" != 'v' ]; then
|
||||
KUBECTL_VERSION="v${KUBECTL_VERSION}"
|
||||
fi
|
||||
curl -sSL -o /usr/local/bin/kubectl "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${architecture}/kubectl"
|
||||
chmod 0755 /usr/local/bin/kubectl
|
||||
if [ "$KUBECTL_SHA256" = "automatic" ]; then
|
||||
KUBECTL_SHA256="$(curl -sSL "https://dl.k8s.io/${KUBECTL_VERSION}/bin/linux/${architecture}/kubectl.sha256")"
|
||||
fi
|
||||
([ "${KUBECTL_SHA256}" = "dev-mode" ] || (echo "${KUBECTL_SHA256} */usr/local/bin/kubectl" | sha256sum -c -))
|
||||
if ! type kubectl > /dev/null 2>&1; then
|
||||
echo '(!) kubectl installation failed!'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# kubectl bash completion
|
||||
kubectl completion bash > /etc/bash_completion.d/kubectl
|
||||
|
||||
# kubectl zsh completion
|
||||
mkdir -p /home/${USERNAME}/.oh-my-zsh/completions
|
||||
kubectl completion zsh > /home/${USERNAME}/.oh-my-zsh/completions/_kubectl
|
||||
|
||||
# Install Helm, verify signature and checksum
|
||||
echo "Downloading Helm..."
|
||||
find_version_from_git_tags HELM_VERSION "https://github.com/helm/helm"
|
||||
if [ "${HELM_VERSION::1}" != 'v' ]; then
|
||||
HELM_VERSION="v${HELM_VERSION}"
|
||||
fi
|
||||
mkdir -p /tmp/helm
|
||||
helm_filename="helm-${HELM_VERSION}-linux-${architecture}.tar.gz"
|
||||
tmp_helm_filename="/tmp/helm/${helm_filename}"
|
||||
curl -sSL "https://get.helm.sh/${helm_filename}" -o "${tmp_helm_filename}"
|
||||
curl -sSL "https://github.com/helm/helm/releases/download/${HELM_VERSION}/${helm_filename}.asc" -o "${tmp_helm_filename}.asc"
|
||||
export GNUPGHOME="/tmp/helm/gnupg"
|
||||
mkdir -p "${GNUPGHOME}"
|
||||
chmod 700 ${GNUPGHOME}
|
||||
get_common_setting HELM_GPG_KEYS_URI
|
||||
get_common_setting GPG_KEY_SERVERS true
|
||||
curl -sSL "${HELM_GPG_KEYS_URI}" -o /tmp/helm/KEYS
|
||||
echo -e "disable-ipv6\n${GPG_KEY_SERVERS}" > ${GNUPGHOME}/dirmngr.conf
|
||||
gpg -q --import "/tmp/helm/KEYS"
|
||||
if ! gpg --verify "${tmp_helm_filename}.asc" > ${GNUPGHOME}/verify.log 2>&1; then
|
||||
echo "Verification failed!"
|
||||
cat /tmp/helm/gnupg/verify.log
|
||||
exit 1
|
||||
fi
|
||||
if [ "${HELM_SHA256}" = "automatic" ]; then
|
||||
curl -sSL "https://get.helm.sh/${helm_filename}.sha256" -o "${tmp_helm_filename}.sha256"
|
||||
curl -sSL "https://github.com/helm/helm/releases/download/${HELM_VERSION}/${helm_filename}.sha256.asc" -o "${tmp_helm_filename}.sha256.asc"
|
||||
if ! gpg --verify "${tmp_helm_filename}.sha256.asc" > /tmp/helm/gnupg/verify.log 2>&1; then
|
||||
echo "Verification failed!"
|
||||
cat /tmp/helm/gnupg/verify.log
|
||||
exit 1
|
||||
fi
|
||||
HELM_SHA256="$(cat "${tmp_helm_filename}.sha256")"
|
||||
fi
|
||||
([ "${HELM_SHA256}" = "dev-mode" ] || (echo "${HELM_SHA256} *${tmp_helm_filename}" | sha256sum -c -))
|
||||
tar xf "${tmp_helm_filename}" -C /tmp/helm
|
||||
mv -f "/tmp/helm/linux-${architecture}/helm" /usr/local/bin/
|
||||
chmod 0755 /usr/local/bin/helm
|
||||
rm -rf /tmp/helm
|
||||
if ! type helm > /dev/null 2>&1; then
|
||||
echo '(!) Helm installation failed!'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install Minikube, verify checksum
|
||||
if [ "${MINIKUBE_VERSION}" != "none" ]; then
|
||||
echo "Downloading minikube..."
|
||||
if [ "${MINIKUBE_VERSION}" = "latest" ] || [ "${MINIKUBE_VERSION}" = "lts" ] || [ "${MINIKUBE_VERSION}" = "current" ] || [ "${MINIKUBE_VERSION}" = "stable" ]; then
|
||||
MINIKUBE_VERSION="latest"
|
||||
else
|
||||
find_version_from_git_tags MINIKUBE_VERSION https://github.com/kubernetes/minikube
|
||||
if [ "${MINIKUBE_VERSION::1}" != "v" ]; then
|
||||
MINIKUBE_VERSION="v${MINIKUBE_VERSION}"
|
||||
fi
|
||||
fi
|
||||
# latest is also valid in the download URLs
|
||||
curl -sSL -o /usr/local/bin/minikube "https://storage.googleapis.com/minikube/releases/${MINIKUBE_VERSION}/minikube-linux-${architecture}"
|
||||
chmod 0755 /usr/local/bin/minikube
|
||||
if [ "$MINIKUBE_SHA256" = "automatic" ]; then
|
||||
MINIKUBE_SHA256="$(curl -sSL "https://storage.googleapis.com/minikube/releases/${MINIKUBE_VERSION}/minikube-linux-${architecture}.sha256")"
|
||||
fi
|
||||
([ "${MINIKUBE_SHA256}" = "dev-mode" ] || (echo "${MINIKUBE_SHA256} */usr/local/bin/minikube" | sha256sum -c -))
|
||||
if ! type minikube > /dev/null 2>&1; then
|
||||
echo '(!) minikube installation failed!'
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! type docker > /dev/null 2>&1; then
|
||||
echo -e '\n(*) Warning: The docker command was not found.\n\nYou can use one of the following scripts to install it:\n\nhttps://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/docker-in-docker.md\n\nor\n\nhttps://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/docker.md'
|
||||
fi
|
||||
|
||||
echo -e "\nDone!"
|
||||
@@ -1 +0,0 @@
|
||||
VERSION='dev'
|
||||
@@ -1,141 +0,0 @@
|
||||
#!/bin/bash
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
#
|
||||
# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/node.md
|
||||
# Maintainer: The VS Code and Codespaces Teams
|
||||
#
|
||||
# Syntax: ./node-debian.sh [directory to install nvm] [node version to install (use "none" to skip)] [non-root user] [Update rc files flag]
|
||||
|
||||
export NVM_DIR=${1:-"/usr/local/share/nvm"}
|
||||
export NODE_VERSION=${2:-"lts"}
|
||||
USERNAME=${3:-"automatic"}
|
||||
UPDATE_RC=${4:-"true"}
|
||||
export NVM_VERSION="0.38.0"
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure that login shells get the correct path if the user updated the PATH using ENV.
|
||||
rm -f /etc/profile.d/00-restore-env.sh
|
||||
echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh
|
||||
chmod +x /etc/profile.d/00-restore-env.sh
|
||||
|
||||
# Determine the appropriate non-root user
|
||||
if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then
|
||||
USERNAME=""
|
||||
POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)")
|
||||
for CURRENT_USER in ${POSSIBLE_USERS[@]}; do
|
||||
if id -u ${CURRENT_USER} > /dev/null 2>&1; then
|
||||
USERNAME=${CURRENT_USER}
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ "${USERNAME}" = "" ]; then
|
||||
USERNAME=root
|
||||
fi
|
||||
elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then
|
||||
USERNAME=root
|
||||
fi
|
||||
|
||||
updaterc() {
|
||||
if [ "${UPDATE_RC}" = "true" ]; then
|
||||
echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..."
|
||||
echo -e "$1" >> /etc/bash.bashrc
|
||||
if [ -f "/etc/zsh/zshrc" ]; then
|
||||
echo -e "$1" >> /etc/zsh/zshrc
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to run apt-get if needed
|
||||
apt_get_update_if_needed()
|
||||
{
|
||||
if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then
|
||||
echo "Running apt-get update..."
|
||||
apt-get update
|
||||
else
|
||||
echo "Skipping apt-get update."
|
||||
fi
|
||||
}
|
||||
|
||||
# Checks if packages are installed and installs them if not
|
||||
check_packages() {
|
||||
if ! dpkg -s "$@" > /dev/null 2>&1; then
|
||||
apt_get_update_if_needed
|
||||
apt-get -y install --no-install-recommends "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Ensure apt is in non-interactive to avoid prompts
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install dependencies
|
||||
check_packages apt-transport-https curl ca-certificates tar gnupg2
|
||||
|
||||
# Install yarn
|
||||
if type yarn > /dev/null 2>&1; then
|
||||
echo "Yarn already installed."
|
||||
else
|
||||
# Import key safely (new method rather than deprecated apt-key approach) and install
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor > /usr/share/keyrings/yarn-archive-keyring.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/yarn-archive-keyring.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends yarn
|
||||
fi
|
||||
|
||||
# Adjust node version if required
|
||||
if [ "${NODE_VERSION}" = "none" ]; then
|
||||
export NODE_VERSION=
|
||||
elif [ "${NODE_VERSION}" = "lts" ]; then
|
||||
export NODE_VERSION="lts/*"
|
||||
fi
|
||||
|
||||
# Install the specified node version if NVM directory already exists, then exit
|
||||
if [ -d "${NVM_DIR}" ]; then
|
||||
echo "NVM already installed."
|
||||
if [ "${NODE_VERSION}" != "" ]; then
|
||||
su ${USERNAME} -c ". $NVM_DIR/nvm.sh && nvm install ${NODE_VERSION} && nvm clear-cache"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create nvm group, nvm dir, and set sticky bit
|
||||
if ! cat /etc/group | grep -e "^nvm:" > /dev/null 2>&1; then
|
||||
groupadd -r nvm
|
||||
fi
|
||||
umask 0002
|
||||
usermod -a -G nvm ${USERNAME}
|
||||
mkdir -p ${NVM_DIR}
|
||||
chown :nvm ${NVM_DIR}
|
||||
chmod g+s ${NVM_DIR}
|
||||
su ${USERNAME} -c "$(cat << EOF
|
||||
set -e
|
||||
umask 0002
|
||||
# Do not update profile - we'll do this manually
|
||||
export PROFILE=/dev/null
|
||||
curl -so- https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh | bash
|
||||
source ${NVM_DIR}/nvm.sh
|
||||
if [ "${NODE_VERSION}" != "" ]; then
|
||||
nvm alias default ${NODE_VERSION}
|
||||
fi
|
||||
nvm clear-cache
|
||||
EOF
|
||||
)" 2>&1
|
||||
# Update rc files
|
||||
if [ "${UPDATE_RC}" = "true" ]; then
|
||||
updaterc "$(cat <<EOF
|
||||
export NVM_DIR="${NVM_DIR}"
|
||||
[ -s "\$NVM_DIR/nvm.sh" ] && . "\$NVM_DIR/nvm.sh"
|
||||
[ -s "\$NVM_DIR/bash_completion" ] && . "\$NVM_DIR/bash_completion"
|
||||
EOF
|
||||
)"
|
||||
fi
|
||||
|
||||
echo "Done!"
|
||||
@@ -1,307 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
#
|
||||
# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/python.md
|
||||
# Maintainer: The VS Code and Codespaces Teams
|
||||
#
|
||||
# Syntax: ./python-debian.sh [Python Version] [Python intall path] [PIPX_HOME] [non-root user] [Update rc files flag] [install tools]
|
||||
|
||||
PYTHON_VERSION=${1:-"latest"}
|
||||
PYTHON_INSTALL_PATH=${2:-"/usr/local/python"}
|
||||
export PIPX_HOME=${3:-"/usr/local/py-utils"}
|
||||
USERNAME=${4:-"automatic"}
|
||||
UPDATE_RC=${5:-"true"}
|
||||
INSTALL_PYTHON_TOOLS=${6:-"true"}
|
||||
USE_PPA_IF_AVAILABLE=${7:-"true"}
|
||||
|
||||
DEADSNAKES_PPA_ARCHIVE_GPG_KEY="F23C5A6CF475977595C89F51BA6932366A755776"
|
||||
PYTHON_SOURCE_GPG_KEYS="64E628F8D684696D B26995E310250568 2D347EA6AA65421D FB9921286F5E1540 3A5CA953F73C700D 04C367C218ADD4FF 0EDDC5F26A45C816 6AF053F07D9DC8D2 C9BE28DEE6DF025C 126EB563A74B06BF D9866941EA5BBD71 ED9D77D5"
|
||||
GPG_KEY_SERVERS="keyserver hkp://keyserver.ubuntu.com:80
|
||||
keyserver hkps://keys.openpgp.org
|
||||
keyserver hkp://keyserver.pgp.com"
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure that login shells get the correct path if the user updated the PATH using ENV.
|
||||
rm -f /etc/profile.d/00-restore-env.sh
|
||||
echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh
|
||||
chmod +x /etc/profile.d/00-restore-env.sh
|
||||
|
||||
# Determine the appropriate non-root user
|
||||
if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then
|
||||
USERNAME=""
|
||||
POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)")
|
||||
for CURRENT_USER in ${POSSIBLE_USERS[@]}; do
|
||||
if id -u ${CURRENT_USER} > /dev/null 2>&1; then
|
||||
USERNAME=${CURRENT_USER}
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ "${USERNAME}" = "" ]; then
|
||||
USERNAME=root
|
||||
fi
|
||||
elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then
|
||||
USERNAME=root
|
||||
fi
|
||||
|
||||
updaterc() {
|
||||
if [ "${UPDATE_RC}" = "true" ]; then
|
||||
echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..."
|
||||
echo -e "$1" >> /etc/bash.bashrc
|
||||
if [ -f "/etc/zsh/zshrc" ]; then
|
||||
echo -e "$1" >> /etc/zsh/zshrc
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Get central common setting
|
||||
get_common_setting() {
|
||||
if [ "${common_settings_file_loaded}" != "true" ]; then
|
||||
curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping."
|
||||
common_settings_file_loaded=true
|
||||
fi
|
||||
if [ -f "/tmp/vsdc-settings.env" ]; then
|
||||
local multi_line=""
|
||||
if [ "$2" = "true" ]; then multi_line="-z"; fi
|
||||
local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')"
|
||||
if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi
|
||||
fi
|
||||
echo "$1=${!1}"
|
||||
}
|
||||
|
||||
# Import the specified key in a variable name passed in as
|
||||
receive_gpg_keys() {
|
||||
get_common_setting $1
|
||||
local keys=${!1}
|
||||
get_common_setting GPG_KEY_SERVERS true
|
||||
local keyring_args=""
|
||||
if [ ! -z "$2" ]; then
|
||||
mkdir -p "$(dirname \"$2\")"
|
||||
keyring_args="--no-default-keyring --keyring $2"
|
||||
fi
|
||||
|
||||
# Use a temporary locaiton for gpg keys to avoid polluting image
|
||||
export GNUPGHOME="/tmp/tmp-gnupg"
|
||||
mkdir -p ${GNUPGHOME}
|
||||
chmod 700 ${GNUPGHOME}
|
||||
echo -e "disable-ipv6\n${GPG_KEY_SERVERS}" > ${GNUPGHOME}/dirmngr.conf
|
||||
# GPG key download sometimes fails for some reason and retrying fixes it.
|
||||
local retry_count=0
|
||||
local gpg_ok="false"
|
||||
set +e
|
||||
until [ "${gpg_ok}" = "true" ] || [ "${retry_count}" -eq "5" ];
|
||||
do
|
||||
echo "(*) Downloading GPG key..."
|
||||
( echo "${keys}" | xargs -n 1 gpg -q ${keyring_args} --recv-keys) 2>&1 && gpg_ok="true"
|
||||
if [ "${gpg_ok}" != "true" ]; then
|
||||
echo "(*) Failed getting key, retring in 10s..."
|
||||
(( retry_count++ ))
|
||||
sleep 10s
|
||||
fi
|
||||
done
|
||||
set -e
|
||||
if [ "${gpg_ok}" = "false" ]; then
|
||||
echo "(!) Failed to install rvm."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Figure out correct version of a three part version number is not passed
|
||||
find_version_from_git_tags() {
|
||||
local variable_name=$1
|
||||
local requested_version=${!variable_name}
|
||||
if [ "${requested_version}" = "none" ]; then return; fi
|
||||
local repository=$2
|
||||
local prefix=${3:-"tags/v"}
|
||||
local separator=${4:-"."}
|
||||
local last_part_optional=${5:-"false"}
|
||||
if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then
|
||||
local escaped_separator=${separator//./\\.}
|
||||
local last_part
|
||||
if [ "${last_part_optional}" = "true" ]; then
|
||||
last_part="(${escaped_separator}[0-9]+)?"
|
||||
else
|
||||
last_part="${escaped_separator}[0-9]+"
|
||||
fi
|
||||
local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$"
|
||||
local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)"
|
||||
if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then
|
||||
declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)"
|
||||
else
|
||||
set +e
|
||||
declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")"
|
||||
set -e
|
||||
fi
|
||||
fi
|
||||
if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then
|
||||
echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "${variable_name}=${!variable_name}"
|
||||
}
|
||||
|
||||
# Function to run apt-get if needed
|
||||
apt_get_update_if_needed()
|
||||
{
|
||||
if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then
|
||||
echo "Running apt-get update..."
|
||||
apt-get update
|
||||
else
|
||||
echo "Skipping apt-get update."
|
||||
fi
|
||||
}
|
||||
|
||||
# Checks if packages are installed and installs them if not
|
||||
check_packages() {
|
||||
if ! dpkg -s "$@" > /dev/null 2>&1; then
|
||||
apt_get_update_if_needed
|
||||
apt-get -y install --no-install-recommends "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
install_from_ppa() {
|
||||
local requested_version="python${PYTHON_VERSION}"
|
||||
echo "Using PPA to install Python..."
|
||||
check_packages apt-transport-https curl ca-certificates gnupg2
|
||||
receive_gpg_keys DEADSNAKES_PPA_ARCHIVE_GPG_KEY /usr/share/keyrings/deadsnakes-archive-keyring.gpg
|
||||
echo -e "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/deadsnakes-archive-keyring.gpg] http://ppa.launchpad.net/deadsnakes/ppa/ubuntu ${VERSION_CODENAME} main\ndeb-src [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/deadsnakes-archive-keyring.gpg] http://ppa.launchpad.net/deadsnakes/ppa/ubuntu ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/deadsnakes-ppa.list
|
||||
apt-get update
|
||||
if [ "${PYTHON_VERSION}" = "latest" ] || [ "${PYTHON_VERSION}" = "current" ] || [ "${PYTHON_VERSION}" = "lts" ]; then
|
||||
requested_version="$(apt-cache search '^python3\.[0-9]$' | grep -oE '^python3\.[0-9]' | sort -rV | head -n 1)"
|
||||
echo "Using ${requested_version} in place of ${PYTHON_VERSION}."
|
||||
fi
|
||||
apt-get -y install ${requested_version}
|
||||
rm -rf /tmp/tmp-gnupg
|
||||
exit 0
|
||||
}
|
||||
|
||||
install_from_source() {
|
||||
if [ -d "${PYTHON_INSTALL_PATH}" ]; then
|
||||
echo "Path ${PYTHON_INSTALL_PATH} already exists. Remove this existing path or select a different one."
|
||||
exit 1
|
||||
else
|
||||
echo "Building Python ${PYTHON_VERSION} from source..."
|
||||
# Install prereqs if missing
|
||||
check_packages curl ca-certificates tar make build-essential libssl-dev zlib1g-dev \
|
||||
wget libbz2-dev libreadline-dev libxml2-dev xz-utils tk-dev gnupg2 \
|
||||
libxmlsec1-dev libsqlite3-dev libffi-dev liblzma-dev llvm dirmngr
|
||||
if ! type git > /dev/null 2>&1; then
|
||||
apt_get_update_if_needed
|
||||
apt-get -y install --no-install-recommends git
|
||||
fi
|
||||
|
||||
# Find version using soft match
|
||||
find_version_from_git_tags PYTHON_VERSION "https://github.com/python/cpython"
|
||||
|
||||
# Download tgz of source
|
||||
mkdir -p /tmp/python-src "${PYTHON_INSTALL_PATH}"
|
||||
cd /tmp/python-src
|
||||
TGZ_FILENAME="Python-${PYTHON_VERSION}.tgz"
|
||||
TGZ_URL="https://www.python.org/ftp/python/${PYTHON_VERSION}/${TGZ_FILENAME}"
|
||||
echo "Downloading ${TGZ_FILENAME}..."
|
||||
curl -sSL -o "/tmp/python-src/${TGZ_FILENAME}" "${TGZ_URL}"
|
||||
|
||||
# Verify signature
|
||||
if [ "${SKIP_SIGNATURE_CHECK}" != "true" ]; then
|
||||
receive_gpg_keys PYTHON_SOURCE_GPG_KEYS
|
||||
echo "Downloading ${TGZ_FILENAME}.asc..."
|
||||
curl -sSL -o "/tmp/python-src/${TGZ_FILENAME}.asc" "${TGZ_URL}.asc"
|
||||
gpg --verify "${TGZ_FILENAME}.asc"
|
||||
fi
|
||||
|
||||
# Update min protocol for testing only - https://bugs.python.org/issue41561
|
||||
cp /etc/ssl/openssl.cnf /tmp/python-src/
|
||||
sed -i -E 's/MinProtocol[=\ ]+.*/MinProtocol = TLSv1.0/g' /tmp/python-src/openssl.cnf
|
||||
export OPENSSL_CONF=/tmp/python-src/openssl.cnf
|
||||
|
||||
# Untar and build
|
||||
tar -xzf "/tmp/python-src/${TGZ_FILENAME}" -C "/tmp/python-src" --strip-components=1
|
||||
./configure --prefix="${PYTHON_INSTALL_PATH}" --enable-optimizations --with-ensurepip=install
|
||||
make -j 8
|
||||
make install
|
||||
cd /tmp
|
||||
rm -rf /tmp/python-src ${GNUPGHOME} /tmp/vscdc-settings.env
|
||||
chown -R ${USERNAME} "${PYTHON_INSTALL_PATH}"
|
||||
ln -s ${PYTHON_INSTALL_PATH}/bin/python3 ${PYTHON_INSTALL_PATH}/bin/python
|
||||
ln -s ${PYTHON_INSTALL_PATH}/bin/pip3 ${PYTHON_INSTALL_PATH}/bin/pip
|
||||
ln -s ${PYTHON_INSTALL_PATH}/bin/idle3 ${PYTHON_INSTALL_PATH}/bin/idle
|
||||
ln -s ${PYTHON_INSTALL_PATH}/bin/pydoc3 ${PYTHON_INSTALL_PATH}/bin/pydoc
|
||||
ln -s ${PYTHON_INSTALL_PATH}/bin/python3-config ${PYTHON_INSTALL_PATH}/bin/python-config
|
||||
updaterc "export PATH=${PYTHON_INSTALL_PATH}/bin:\${PATH}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Ensure apt is in non-interactive to avoid prompts
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install python from source if needed
|
||||
if [ "${PYTHON_VERSION}" != "none" ]; then
|
||||
# Source /etc/os-release to get OS info
|
||||
. /etc/os-release
|
||||
# If ubuntu, PPAs allowed - install from there
|
||||
if [ "${ID}" = "ubuntu" ] && [ "${USE_PPA_IF_AVAILABLE}" = "true" ]; then
|
||||
install_from_ppa
|
||||
else
|
||||
install_from_source
|
||||
fi
|
||||
fi
|
||||
|
||||
# If not installing python tools, exit
|
||||
if [ "${INSTALL_PYTHON_TOOLS}" != "true" ]; then
|
||||
echo "Done!"
|
||||
exit 0;
|
||||
fi
|
||||
|
||||
DEFAULT_UTILS="\
|
||||
pylint \
|
||||
flake8 \
|
||||
autopep8 \
|
||||
black \
|
||||
yapf \
|
||||
mypy \
|
||||
pydocstyle \
|
||||
pycodestyle \
|
||||
bandit \
|
||||
pipenv \
|
||||
virtualenv"
|
||||
|
||||
export PIPX_BIN_DIR=${PIPX_HOME}/bin
|
||||
export PATH=${PYTHON_INSTALL_PATH}/bin:${PIPX_BIN_DIR}:${PATH}
|
||||
|
||||
# Update pip
|
||||
echo "Updating pip..."
|
||||
python3 -m pip install --no-cache-dir --upgrade pip
|
||||
|
||||
# Create pipx group, dir, and set sticky bit
|
||||
if ! cat /etc/group | grep -e "^pipx:" > /dev/null 2>&1; then
|
||||
groupadd -r pipx
|
||||
fi
|
||||
usermod -a -G pipx ${USERNAME}
|
||||
umask 0002
|
||||
mkdir -p ${PIPX_BIN_DIR}
|
||||
chown :pipx ${PIPX_HOME} ${PIPX_BIN_DIR}
|
||||
chmod g+s ${PIPX_HOME} ${PIPX_BIN_DIR}
|
||||
|
||||
# Install tools
|
||||
echo "Installing Python tools..."
|
||||
export PYTHONUSERBASE=/tmp/pip-tmp
|
||||
export PIP_CACHE_DIR=/tmp/pip-tmp/cache
|
||||
pip3 install --disable-pip-version-check --no-warn-script-location --no-cache-dir --user pipx
|
||||
/tmp/pip-tmp/bin/pipx install --pip-args=--no-cache-dir pipx
|
||||
echo "${DEFAULT_UTILS}" | xargs -n 1 /tmp/pip-tmp/bin/pipx install --system-site-packages --pip-args '--no-cache-dir --force-reinstall'
|
||||
rm -rf /tmp/pip-tmp
|
||||
|
||||
updaterc "$(cat << EOF
|
||||
export PIPX_HOME="${PIPX_HOME}"
|
||||
export PIPX_BIN_DIR="${PIPX_BIN_DIR}"
|
||||
if [[ "\${PATH}" != *"\${PIPX_BIN_DIR}"* ]]; then export PATH="\${PATH}:\${PIPX_BIN_DIR}"; fi
|
||||
EOF
|
||||
)"
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# k3d
|
||||
# v5 RC is needed to deterministically set the Registry port. Should be replaces with official release
|
||||
curl -s https://raw.githubusercontent.com/rancher/k3d/main/install.sh | TAG=v5.0.0-rc.4 bash
|
||||
|
||||
# kustomize
|
||||
pushd /tmp
|
||||
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
|
||||
popd
|
||||
sudo mv /tmp/kustomize /usr/local/bin/
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# install Krew
|
||||
# TODO (dans): ditch krew and just download the latest binaries on the path in Dockerfile
|
||||
(
|
||||
set -x; cd "$(mktemp -d)" &&
|
||||
OS="$(uname | tr '[:upper:]' '[:lower:]')" &&
|
||||
ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/')" &&
|
||||
curl -fsSLO "https://github.com/kubernetes-sigs/krew/releases/latest/download/krew.tar.gz" &&
|
||||
tar zxvf krew.tar.gz &&
|
||||
KREW=./krew-"${OS}_${ARCH}" &&
|
||||
"$KREW" install krew
|
||||
)
|
||||
|
||||
# install krew plugins
|
||||
kubectl krew install schemahero
|
||||
kubectl krew install support-bundle
|
||||
kubectl krew install preflights
|
||||
kubectl krew install view-secret
|
||||
|
||||
# Make the cache from master branch
|
||||
pushd /tmp
|
||||
git clone https://github.com/replicatedhq/troubleshoot.git
|
||||
pushd troubleshoot
|
||||
# TODO (dans): find a way to cache images on image build
|
||||
go mod download
|
||||
popd
|
||||
rm -rf kots
|
||||
popd
|
||||
|
||||
# Clone any extra repos here
|
||||
|
||||
# Autocomplete Kubernetes
|
||||
cat >> ~/.zshrc << EOF
|
||||
|
||||
source <(kubectl completion zsh)
|
||||
alias k=kubectl
|
||||
complete -F __start_kubectl k
|
||||
EOF
|
||||
|
||||
# Set Git Editor Preference
|
||||
cat >> ~/.zshrc << EOF
|
||||
|
||||
export VISUAL=vim
|
||||
export EDITOR="$VISUAL"
|
||||
EOF
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/bin/bash
|
||||
# modified from https://github.com/microsoft/vscode-dev-containers/blob/main/containers/codespaces-linux/.devcontainer/setup-user.sh
|
||||
# not part of the standard script library
|
||||
|
||||
USERNAME=${1:-codespace}
|
||||
SECURE_PATH_BASE=${2:-$PATH}
|
||||
|
||||
echo "Defaults secure_path=\"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/bin:${SECURE_PATH_BASE}\"" >> /etc/sudoers.d/securepath
|
||||
|
||||
# Add user to a Docker group
|
||||
sudo -u ${USERNAME} mkdir /home/${USERNAME}/.vsonline
|
||||
groupadd -g 800 docker
|
||||
usermod -a -G docker ${USERNAME}
|
||||
|
||||
# Create user's .local/bin
|
||||
sudo -u ${USERNAME} mkdir -p /home/${USERNAME}/.local/bin
|
||||
@@ -1,165 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
|
||||
#-------------------------------------------------------------------------------------------------------------
|
||||
#
|
||||
# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/sshd.md
|
||||
# Maintainer: The VS Code and Codespaces Teams
|
||||
#
|
||||
# Syntax: ./sshd-debian.sh [SSH Port (don't use 22)] [non-root user] [start sshd now flag] [new password for user] [fix environment flag]
|
||||
#
|
||||
# Note: You can change your user's password with "sudo passwd $(whoami)" (or just "passwd" if running as root).
|
||||
|
||||
SSHD_PORT=${1:-"2222"}
|
||||
USERNAME=${2:-"automatic"}
|
||||
START_SSHD=${3:-"false"}
|
||||
NEW_PASSWORD=${4:-"skip"}
|
||||
FIX_ENVIRONMENT=${5:-"true"}
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine the appropriate non-root user
|
||||
if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then
|
||||
USERNAME=""
|
||||
POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)")
|
||||
for CURRENT_USER in ${POSSIBLE_USERS[@]}; do
|
||||
if id -u ${CURRENT_USER} > /dev/null 2>&1; then
|
||||
USERNAME=${CURRENT_USER}
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ "${USERNAME}" = "" ]; then
|
||||
USERNAME=root
|
||||
fi
|
||||
elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then
|
||||
USERNAME=root
|
||||
fi
|
||||
|
||||
# Function to run apt-get if needed
|
||||
apt_get_update_if_needed()
|
||||
{
|
||||
if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then
|
||||
echo "Running apt-get update..."
|
||||
apt-get update
|
||||
else
|
||||
echo "Skipping apt-get update."
|
||||
fi
|
||||
}
|
||||
|
||||
# Checks if packages are installed and installs them if not
|
||||
check_packages() {
|
||||
if ! dpkg -s "$@" > /dev/null 2>&1; then
|
||||
apt_get_update_if_needed
|
||||
apt-get -y install --no-install-recommends "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Ensure apt is in non-interactive to avoid prompts
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install openssh-server openssh-client
|
||||
check_packages openssh-server openssh-client lsof
|
||||
|
||||
# Generate password if new password set to the word "random"
|
||||
if [ "${NEW_PASSWORD}" = "random" ]; then
|
||||
NEW_PASSWORD="$(openssl rand -hex 16)"
|
||||
EMIT_PASSWORD="true"
|
||||
elif [ "${NEW_PASSWORD}" != "skip" ]; then
|
||||
# If new password not set to skip, set it for the specified user
|
||||
echo "${USERNAME}:${NEW_PASSWORD}" | chpasswd
|
||||
fi
|
||||
|
||||
# Add user to ssh group
|
||||
if [ "${USERNAME}" != "root" ]; then
|
||||
usermod -aG ssh ${USERNAME}
|
||||
fi
|
||||
|
||||
# Setup sshd
|
||||
mkdir -p /var/run/sshd
|
||||
sed -i 's/session\s*required\s*pam_loginuid\.so/session optional pam_loginuid.so/g' /etc/pam.d/sshd
|
||||
sed -i 's/#*PermitRootLogin prohibit-password/PermitRootLogin yes/g' /etc/ssh/sshd_config
|
||||
sed -i -E "s/#*\s*Port\s+.+/Port ${SSHD_PORT}/g" /etc/ssh/sshd_config
|
||||
# Need to UsePAM so /etc/environment is processed
|
||||
sed -i -E "s/#?\s*UsePAM\s+.+/UsePAM yes/g" /etc/ssh/sshd_config
|
||||
|
||||
# Script to store variables that exist at the time the ENTRYPOINT is fired
|
||||
store_env_script="$(cat << 'EOF'
|
||||
# Wire in codespaces secret processing to zsh if present (since may have been added to image after script was run)
|
||||
if [ -f /etc/zsh/zlogin ] && ! grep '/etc/profile.d/00-restore-secrets.sh' /etc/zsh/zlogin > /dev/null 2>&1; then
|
||||
echo -e "if [ -f /etc/profile.d/00-restore-secrets.sh ]; then . /etc/profile.d/00-restore-secrets.sh; fi\n$(cat /etc/zsh/zlogin 2>/dev/null || echo '')" | sudoIf tee /etc/zsh/zlogin > /dev/null
|
||||
fi
|
||||
EOF
|
||||
)"
|
||||
|
||||
# Script to ensure login shells get the latest Codespaces secrets
|
||||
restore_secrets_script="$(cat << 'EOF'
|
||||
#!/bin/sh
|
||||
if [ "${CODESPACES}" != "true" ] || [ "${VSCDC_FIXED_SECRETS}" = "true" ] || [ ! -z "${GITHUB_CODESPACES_TOKEN}" ]; then
|
||||
# Not codespaces, already run, or secrets already in environment, so return
|
||||
return
|
||||
fi
|
||||
if [ -f /workspaces/.codespaces/shared/.env ]; then
|
||||
set -o allexport
|
||||
. /workspaces/.codespaces/shared/.env
|
||||
set +o allexport
|
||||
fi
|
||||
export VSCDC_FIXED_SECRETS=true
|
||||
EOF
|
||||
)"
|
||||
|
||||
# Write out a scripts that can be referenced as an ENTRYPOINT to auto-start sshd and fix login environments
|
||||
tee /usr/local/share/ssh-init.sh > /dev/null \
|
||||
<< 'EOF'
|
||||
#!/usr/bin/env bash
|
||||
# This script is intended to be run as root with a container that runs as root (even if you connect with a different user)
|
||||
# However, it supports running as a user other than root if passwordless sudo is configured for that same user.
|
||||
|
||||
set -e
|
||||
|
||||
sudoIf()
|
||||
{
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
sudo "$@"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
EOF
|
||||
if [ "${FIX_ENVIRONMENT}" = "true" ]; then
|
||||
echo "${store_env_script}" >> /usr/local/share/ssh-init.sh
|
||||
echo "${restore_secrets_script}" > /etc/profile.d/00-restore-secrets.sh
|
||||
chmod +x /etc/profile.d/00-restore-secrets.sh
|
||||
# Wire in zsh if present
|
||||
if type zsh > /dev/null 2>&1; then
|
||||
echo -e "if [ -f /etc/profile.d/00-restore-secrets.sh ]; then . /etc/profile.d/00-restore-secrets.sh; fi\n$(cat /etc/zsh/zlogin 2>/dev/null || echo '')" > /etc/zsh/zlogin
|
||||
fi
|
||||
fi
|
||||
tee -a /usr/local/share/ssh-init.sh > /dev/null \
|
||||
<< 'EOF'
|
||||
|
||||
# ** Start SSH server **
|
||||
sudoIf /etc/init.d/ssh start 2>&1 | sudoIf tee /tmp/sshd.log > /dev/null
|
||||
|
||||
set +e
|
||||
exec "$@"
|
||||
EOF
|
||||
chmod +x /usr/local/share/ssh-init.sh
|
||||
|
||||
# If we should start sshd now, do so
|
||||
if [ "${START_SSHD}" = "true" ]; then
|
||||
/usr/local/share/ssh-init.sh
|
||||
fi
|
||||
|
||||
# Output success details
|
||||
echo -e "Done!\n\n- Port: ${SSHD_PORT}\n- User: ${USERNAME}"
|
||||
if [ "${EMIT_PASSWORD}" = "true" ]; then
|
||||
echo "- Password: ${NEW_PASSWORD}"
|
||||
fi
|
||||
echo -e "\nForward port ${SSHD_PORT} to your local machine and run:\n\n ssh -p ${SSHD_PORT} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o GlobalKnownHostsFile=/dev/null ${USERNAME}@localhost\n"
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Setup the cluster
|
||||
k3d cluster create --config /etc/replicated/k3d-cluster.yaml --kubeconfig-update-default
|
||||
|
||||
# Clone any extra repos here
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Start the cluster here
|
||||
k3d cluster start replicated
|
||||
|
||||
21
.github/CODEOWNERS
vendored
21
.github/CODEOWNERS
vendored
@@ -1,21 +0,0 @@
|
||||
# Lines starting with '#' are comments.
|
||||
# Each line is a file pattern followed by one or more owners.
|
||||
|
||||
# More details are here: https://help.github.com/articles/about-codeowners/
|
||||
|
||||
# The '*' pattern is global owners.
|
||||
|
||||
# Order is important. The last matching pattern has the most precedence.
|
||||
# The folders are ordered as follows:
|
||||
|
||||
# In each subsection folders are ordered first by depth, then alphabetically.
|
||||
# This should make it easy to add new rules without breaking existing ones.
|
||||
|
||||
## RULES
|
||||
|
||||
* @replicatedhq/troubleshoot
|
||||
*.md @replicatedhq/cre
|
||||
go.mod
|
||||
go.sum
|
||||
/examples/sdk/helm-template/go.mod
|
||||
/examples/sdk/helm-template/go.sum
|
||||
39
.github/actions/setup-go/action.yml
vendored
Normal file
39
.github/actions/setup-go/action.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: 'Setup Go Environment'
|
||||
description: 'Setup Go with caching and common environment variables'
|
||||
inputs:
|
||||
go-version-file:
|
||||
description: 'Path to go.mod file'
|
||||
required: false
|
||||
default: 'go.mod'
|
||||
outputs:
|
||||
go-version:
|
||||
description: 'The Go version that was installed'
|
||||
value: ${{ steps.setup-go.outputs.go-version }}
|
||||
cache-hit:
|
||||
description: 'Whether the Go cache was hit'
|
||||
value: ${{ steps.setup-go.outputs.cache-hit }}
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Setup Go
|
||||
id: setup-go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: ${{ inputs.go-version-file }}
|
||||
cache: true
|
||||
|
||||
- name: Set Go environment variables
|
||||
shell: bash
|
||||
run: |
|
||||
echo "GOMAXPROCS=2" >> $GITHUB_ENV
|
||||
echo "GOCACHE=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||
echo "GOMODCACHE=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||
|
||||
- name: Print Go environment
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Go version: $(go version)"
|
||||
echo "GOOS: $(go env GOOS)"
|
||||
echo "GOARCH: $(go env GOARCH)"
|
||||
echo "Cache directory: $(go env GOCACHE)"
|
||||
echo "Module cache: $(go env GOMODCACHE)"
|
||||
97
.github/workflows/automated-prs-manager.yaml
vendored
97
.github/workflows/automated-prs-manager.yaml
vendored
@@ -1,97 +0,0 @@
|
||||
name: Automated PRs Manager
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 */6 * * *" # every 6 hours
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
list-prs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
prs: ${{ steps.list-prs.outputs.prs }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.REPLICATED_GH_PAT }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: List PRs
|
||||
id: list-prs
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# list prs that are less than 24h old and exclude prs from forks
|
||||
|
||||
dependabot_prs=$(
|
||||
gh pr list \
|
||||
--author 'dependabot[bot]' \
|
||||
--json url,createdAt,headRefName,headRepository,headRepositoryOwner \
|
||||
-q '.[] | select((.createdAt | fromdateiso8601 > now - 24*60*60) and .headRepositoryOwner.login == "replicatedhq" and .headRepository.name == "troubleshoot")'
|
||||
)
|
||||
|
||||
prs=$(echo "$dependabot_prs" | jq -sc '. | unique')
|
||||
echo "prs=$prs" >> "$GITHUB_OUTPUT"
|
||||
|
||||
process-prs:
|
||||
needs: list-prs
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.list-prs.outputs.prs != '[]'
|
||||
strategy:
|
||||
matrix:
|
||||
pr: ${{ fromJson(needs.list-prs.outputs.prs) }}
|
||||
fail-fast: false
|
||||
max-parallel: 1
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.REPLICATED_GH_PAT }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ matrix.pr.headRefName }}
|
||||
|
||||
- name: Process PR
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "Ensuring required labels..."
|
||||
gh pr edit "${{ matrix.pr.url }}" --add-label "type::security"
|
||||
|
||||
echo "Checking status of tests..."
|
||||
run_id=$(gh run list --branch "${{ matrix.pr.headRefName }}" --workflow build-test-deploy --limit 1 --json databaseId -q '.[0].databaseId')
|
||||
|
||||
# If there are still pending jobs, skip.
|
||||
|
||||
num_of_pending_jobs=$(gh run view "$run_id" --json jobs -q '.jobs[] | select(.conclusion == "") | .name' | wc -l)
|
||||
if [ "$num_of_pending_jobs" -gt 0 ]; then
|
||||
echo "There are still pending jobs. Skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# If all checks passed, approve and merge.
|
||||
if gh run view "$run_id" --json jobs -q '.jobs[] | select(.name == "validate-success") | .conclusion' | grep -q "success"; then
|
||||
if gh pr checks "${{ matrix.pr.url }}"; then
|
||||
echo "All tests passed. Approving and merging."
|
||||
echo -e "LGTM :thumbsup: \n\nThis PR was automatically approved and merged by the [automated-prs-manager](https://github.com/replicatedhq/troubleshoot/blob/main/.github/workflows/automated-prs-manager.yaml) GitHub action" > body.txt
|
||||
gh pr review --approve "${{ matrix.pr.url }}" --body-file body.txt
|
||||
sleep 10
|
||||
gh pr merge --auto --squash "${{ matrix.pr.url }}"
|
||||
exit 0
|
||||
else
|
||||
echo "Some checks did not pass. Skipping."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# If more than half of the jobs are successful, re-run the failed jobs.
|
||||
|
||||
num_of_jobs=$(gh run view "$run_id" --json jobs -q '.jobs[].name ' | wc -l)
|
||||
num_of_successful_jobs=$(gh run view "$run_id" --json jobs -q '.jobs[] | select(.conclusion == "success") | .name' | wc -l)
|
||||
|
||||
if [ "$num_of_successful_jobs" -gt $((num_of_jobs / 2)) ]; then
|
||||
echo "More than half of the jobs are successful. Re-running failed jobs."
|
||||
gh run rerun "$run_id" --failed
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Less than half of the jobs are successful. Skipping."
|
||||
37
.github/workflows/build-test-deploy.yaml
vendored
37
.github/workflows/build-test-deploy.yaml
vendored
@@ -50,17 +50,6 @@ jobs:
|
||||
# test-integration includes unit tests
|
||||
- run: make test-integration
|
||||
|
||||
ensure-schemas-are-generated:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- run: |
|
||||
make check-schemas
|
||||
|
||||
compile-preflight:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -92,12 +81,6 @@ jobs:
|
||||
- run: chmod +x bin/preflight
|
||||
- run: make preflight-e2e-test
|
||||
|
||||
run-examples:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- run: make run-examples
|
||||
|
||||
compile-supportbundle:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -148,19 +131,6 @@ jobs:
|
||||
- run: chmod +x bin/preflight
|
||||
- run: make support-bundle-e2e-go-test
|
||||
|
||||
compile-collect:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- run: make generate collect
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: collect
|
||||
path: bin/collect
|
||||
|
||||
goreleaser-test:
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/v') != true
|
||||
@@ -186,8 +156,8 @@ jobs:
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: "v0.183.0"
|
||||
args: build --rm-dist --snapshot --config deploy/.goreleaser.yaml --single-target
|
||||
version: "v2.12.3"
|
||||
args: build --clean --snapshot --config deploy/.goreleaser.yaml --single-target
|
||||
env:
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: ${{ matrix.goos }}
|
||||
@@ -252,12 +222,9 @@ jobs:
|
||||
needs:
|
||||
- tidy-check
|
||||
- test-integration
|
||||
- run-examples
|
||||
- compile-collect
|
||||
- validate-preflight-e2e
|
||||
- validate-supportbundle-e2e
|
||||
- validate-supportbundle-e2e-go
|
||||
- ensure-schemas-are-generated
|
||||
steps:
|
||||
- run: echo "All PR tests passed"
|
||||
|
||||
|
||||
163
.github/workflows/build-test.yaml
vendored
Normal file
163
.github/workflows/build-test.yaml
vendored
Normal file
@@ -0,0 +1,163 @@
|
||||
name: build-test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Detect changes to optimize test execution
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
go-files: ${{ steps.filter.outputs.go-files }}
|
||||
preflight: ${{ steps.filter.outputs.preflight }}
|
||||
support-bundle: ${{ steps.filter.outputs.support-bundle }}
|
||||
examples: ${{ steps.filter.outputs.examples }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
go-files:
|
||||
- '**/*.go'
|
||||
- 'go.{mod,sum}'
|
||||
- 'Makefile'
|
||||
preflight:
|
||||
- 'cmd/preflight/**'
|
||||
- 'pkg/preflight/**'
|
||||
support-bundle:
|
||||
- 'cmd/troubleshoot/**'
|
||||
- 'pkg/supportbundle/**'
|
||||
|
||||
# Lint
|
||||
lint:
|
||||
if: needs.changes.outputs.go-files == 'true'
|
||||
needs: changes
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: ./.github/actions/setup-go
|
||||
|
||||
- name: Check go mod tidy
|
||||
run: |
|
||||
go mod tidy
|
||||
git diff --exit-code go.mod go.sum || {
|
||||
echo "::error::Please run 'go mod tidy' and commit changes"
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Format and vet
|
||||
run: |
|
||||
make fmt
|
||||
git diff --exit-code || {
|
||||
echo "::error::Please run 'make fmt' and commit changes"
|
||||
exit 1
|
||||
}
|
||||
make vet
|
||||
|
||||
# Unit and integration tests
|
||||
test:
|
||||
if: needs.changes.outputs.go-files == 'true'
|
||||
needs: [changes, lint]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: ./.github/actions/setup-go
|
||||
|
||||
- name: Setup K3s
|
||||
uses: replicatedhq/action-k3s@main
|
||||
with:
|
||||
version: v1.31.2-k3s1
|
||||
|
||||
- name: Run tests
|
||||
run: make test-integration
|
||||
|
||||
# Build binaries
|
||||
build:
|
||||
if: needs.changes.outputs.go-files == 'true'
|
||||
needs: [changes, lint]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: ./.github/actions/setup-go
|
||||
- run: make build
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binaries
|
||||
path: bin/
|
||||
retention-days: 1
|
||||
|
||||
# E2E tests
|
||||
e2e:
|
||||
if: needs.changes.outputs.go-files == 'true' || github.event_name == 'push'
|
||||
needs: [changes, build]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: preflight
|
||||
target: preflight-e2e-test
|
||||
needs-k3s: true
|
||||
- name: support-bundle-shell
|
||||
target: support-bundle-e2e-test
|
||||
needs-k3s: true
|
||||
- name: support-bundle-go
|
||||
target: support-bundle-e2e-go-test
|
||||
needs-k3s: false
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup K3s
|
||||
if: matrix.needs-k3s
|
||||
uses: replicatedhq/action-k3s@main
|
||||
with:
|
||||
version: v1.31.2-k3s1
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: binaries
|
||||
path: bin/
|
||||
|
||||
- run: chmod +x bin/*
|
||||
- run: make ${{ matrix.target }}
|
||||
|
||||
# Success summary
|
||||
success:
|
||||
if: always()
|
||||
needs: [lint, test, build, e2e]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check results
|
||||
run: |
|
||||
# Check if any required jobs failed
|
||||
if [[ "${{ needs.lint.result }}" == "failure" ]] || \
|
||||
[[ "${{ needs.test.result }}" == "failure" ]] || \
|
||||
[[ "${{ needs.build.result }}" == "failure" ]] || \
|
||||
[[ "${{ needs.e2e.result }}" == "failure" ]]; then
|
||||
echo "::error::Some jobs failed or were cancelled"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if any required jobs were cancelled
|
||||
if [[ "${{ needs.lint.result }}" == "cancelled" ]] || \
|
||||
[[ "${{ needs.test.result }}" == "cancelled" ]] || \
|
||||
[[ "${{ needs.build.result }}" == "cancelled" ]] || \
|
||||
[[ "${{ needs.e2e.result }}" == "cancelled" ]]; then
|
||||
echo "::error::Some jobs failed or were cancelled"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ All tests passed!"
|
||||
27
.github/workflows/daily-scan.yaml
vendored
27
.github/workflows/daily-scan.yaml
vendored
@@ -1,27 +0,0 @@
|
||||
name: Scan vulnerabilities
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
scan_troubleshoot_files_systems:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Run Trivy vulnerability scanner in repo mode
|
||||
uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
scan-type: 'fs'
|
||||
ignore-unfixed: true
|
||||
format: 'sarif'
|
||||
output: 'trivy-results.sarif'
|
||||
severity: 'HIGH,CRITICAL'
|
||||
|
||||
- name: Upload Trivy scan results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
35
.github/workflows/license.yaml
vendored
35
.github/workflows/license.yaml
vendored
@@ -1,35 +0,0 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
TRIVY_VERSION: 0.44.1
|
||||
|
||||
name: License scan
|
||||
|
||||
jobs:
|
||||
license:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install trivy
|
||||
run: |
|
||||
wget https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.deb
|
||||
sudo dpkg -i trivy_${TRIVY_VERSION}_Linux-64bit.deb
|
||||
|
||||
- name: Create license report artifact
|
||||
run: trivy fs --scanners license --skip-dirs ".github" . | tee license-report.txt
|
||||
|
||||
- name: Upload license report artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: license-report
|
||||
path: license-report.txt
|
||||
|
||||
- name: Check for unknown licenses
|
||||
run: trivy fs --scanners license --skip-dirs ".github" --exit-code 1 --severity UNKNOWN . || echo "::warning::Unknown licenses found, please verify"
|
||||
|
||||
- name: Check for forbidden licenses and fail
|
||||
run: trivy fs --scanners license --skip-dirs ".github" --exit-code 1 --severity CRITICAL,HIGH .
|
||||
292
.github/workflows/regression-test.yaml
vendored
Normal file
292
.github/workflows/regression-test.yaml
vendored
Normal file
@@ -0,0 +1,292 @@
|
||||
name: Regression Test Suite
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, v1beta3]
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
update_baselines:
|
||||
description: 'Update baselines after run (use with caution)'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
regression-test:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 25
|
||||
|
||||
steps:
|
||||
# 1. SETUP
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for git describe to work
|
||||
|
||||
- name: Create output directory
|
||||
run: mkdir -p test/output
|
||||
|
||||
- name: Create k3s cluster
|
||||
id: create-cluster
|
||||
uses: replicatedhq/compatibility-actions/create-cluster@v1
|
||||
with:
|
||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||
kubernetes-distribution: k3s
|
||||
cluster-name: regression-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
ttl: 25m
|
||||
timeout-minutes: 5
|
||||
|
||||
- name: Configure kubeconfig
|
||||
run: |
|
||||
echo "${{ steps.create-cluster.outputs.cluster-kubeconfig }}" > $GITHUB_WORKSPACE/kubeconfig.yaml
|
||||
echo "KUBECONFIG=$GITHUB_WORKSPACE/kubeconfig.yaml" >> $GITHUB_ENV
|
||||
|
||||
- name: Verify cluster access
|
||||
run: kubectl get nodes -o wide
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
cache-dependency-path: go.sum
|
||||
|
||||
- name: Build binaries
|
||||
run: |
|
||||
echo "Building preflight and support-bundle binaries..."
|
||||
make bin/preflight bin/support-bundle
|
||||
./bin/preflight version
|
||||
./bin/support-bundle version
|
||||
|
||||
- name: Setup Python for comparison
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
pip install pyyaml deepdiff
|
||||
|
||||
# 2. EXECUTE SPECS (in parallel)
|
||||
- name: Run all specs in parallel
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "Running all 3 specs in parallel..."
|
||||
|
||||
# Run v1beta3 in background
|
||||
(
|
||||
echo "Starting preflight v1beta3..."
|
||||
./bin/preflight \
|
||||
examples/preflight/complex-v1beta3.yaml \
|
||||
--values examples/preflight/values-complex-full.yaml \
|
||||
--interactive=false \
|
||||
--format=json \
|
||||
--output=test/output/preflight-results-v1beta3.json 2>&1 | tee test/output/v1beta3.log || true
|
||||
|
||||
BUNDLE=$(ls -t preflightbundle-*.tar.gz 2>/dev/null | head -1)
|
||||
if [ -n "$BUNDLE" ]; then
|
||||
mv "$BUNDLE" test/output/preflight-v1beta3-bundle.tar.gz
|
||||
echo "✓ v1beta3 bundle saved"
|
||||
fi
|
||||
) &
|
||||
PID_V1BETA3=$!
|
||||
|
||||
# Run v1beta2 in background
|
||||
(
|
||||
echo "Starting preflight v1beta2..."
|
||||
./bin/preflight \
|
||||
examples/preflight/all-analyzers-v1beta2.yaml \
|
||||
--interactive=false \
|
||||
--format=json \
|
||||
--output=test/output/preflight-results-v1beta2.json 2>&1 | tee test/output/v1beta2.log || true
|
||||
|
||||
BUNDLE=$(ls -t preflightbundle-*.tar.gz 2>/dev/null | head -1)
|
||||
if [ -n "$BUNDLE" ]; then
|
||||
mv "$BUNDLE" test/output/preflight-v1beta2-bundle.tar.gz
|
||||
echo "✓ v1beta2 bundle saved"
|
||||
fi
|
||||
) &
|
||||
PID_V1BETA2=$!
|
||||
|
||||
# Run support bundle in background
|
||||
(
|
||||
echo "Starting support bundle..."
|
||||
./bin/support-bundle \
|
||||
examples/collect/host/all-kubernetes-collectors.yaml \
|
||||
--interactive=false \
|
||||
--output=test/output/supportbundle.tar.gz 2>&1 | tee test/output/supportbundle.log || true
|
||||
|
||||
if [ -f test/output/supportbundle.tar.gz ]; then
|
||||
echo "✓ Support bundle saved"
|
||||
fi
|
||||
) &
|
||||
PID_SUPPORTBUNDLE=$!
|
||||
|
||||
# Wait for all to complete
|
||||
echo "Waiting for all specs to complete..."
|
||||
wait $PID_V1BETA3
|
||||
wait $PID_V1BETA2
|
||||
wait $PID_SUPPORTBUNDLE
|
||||
|
||||
echo "All specs completed!"
|
||||
|
||||
# Verify bundles exist
|
||||
ls -lh test/output/*.tar.gz || echo "Warning: Some bundles may be missing"
|
||||
|
||||
# 3. COMPARE BUNDLES
|
||||
- name: Compare preflight v1beta3 bundle
|
||||
id: compare-v1beta3
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "Comparing v1beta3 preflight bundle against baseline..."
|
||||
if [ ! -f test/baselines/preflight-v1beta3/baseline.tar.gz ]; then
|
||||
echo "⚠ No baseline found for v1beta3 - skipping comparison"
|
||||
echo "baseline_missing=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
python3 scripts/compare_bundles.py \
|
||||
--baseline test/baselines/preflight-v1beta3/baseline.tar.gz \
|
||||
--current test/output/preflight-v1beta3-bundle.tar.gz \
|
||||
--rules scripts/compare_rules.yaml \
|
||||
--report test/output/diff-report-v1beta3.json \
|
||||
--spec-type preflight
|
||||
|
||||
- name: Compare preflight v1beta2 bundle
|
||||
id: compare-v1beta2
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "Comparing v1beta2 preflight bundle against baseline..."
|
||||
if [ ! -f test/baselines/preflight-v1beta2/baseline.tar.gz ]; then
|
||||
echo "⚠ No baseline found for v1beta2 - skipping comparison"
|
||||
echo "baseline_missing=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
python3 scripts/compare_bundles.py \
|
||||
--baseline test/baselines/preflight-v1beta2/baseline.tar.gz \
|
||||
--current test/output/preflight-v1beta2-bundle.tar.gz \
|
||||
--rules scripts/compare_rules.yaml \
|
||||
--report test/output/diff-report-v1beta2.json \
|
||||
--spec-type preflight
|
||||
|
||||
- name: Compare support bundle
|
||||
id: compare-supportbundle
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "Comparing support bundle against baseline..."
|
||||
if [ ! -f test/baselines/supportbundle/baseline.tar.gz ]; then
|
||||
echo "⚠ No baseline found for support bundle - skipping comparison"
|
||||
echo "baseline_missing=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
python3 scripts/compare_bundles.py \
|
||||
--baseline test/baselines/supportbundle/baseline.tar.gz \
|
||||
--current test/output/supportbundle.tar.gz \
|
||||
--rules scripts/compare_rules.yaml \
|
||||
--report test/output/diff-report-supportbundle.json \
|
||||
--spec-type supportbundle
|
||||
|
||||
# 4. REPORT RESULTS
|
||||
- name: Generate summary report
|
||||
if: always()
|
||||
run: |
|
||||
python3 scripts/generate_summary.py \
|
||||
--reports test/output/diff-report-*.json \
|
||||
--output-file $GITHUB_STEP_SUMMARY \
|
||||
--output-console
|
||||
|
||||
- name: Upload test artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: regression-test-results-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
test/output/*.tar.gz
|
||||
test/output/*.json
|
||||
retention-days: 30
|
||||
|
||||
- name: Check for regressions
|
||||
if: always()
|
||||
run: |
|
||||
echo "Checking comparison results..."
|
||||
|
||||
# Check if any comparisons failed
|
||||
FAILURES=0
|
||||
|
||||
if [ "${{ steps.compare-v1beta3.outcome }}" == "failure" ] && [ "${{ steps.compare-v1beta3.outputs.baseline_missing }}" != "true" ]; then
|
||||
echo "❌ v1beta3 comparison failed"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
|
||||
if [ "${{ steps.compare-v1beta2.outcome }}" == "failure" ] && [ "${{ steps.compare-v1beta2.outputs.baseline_missing }}" != "true" ]; then
|
||||
echo "❌ v1beta2 comparison failed"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
|
||||
if [ "${{ steps.compare-supportbundle.outcome }}" == "failure" ] && [ "${{ steps.compare-supportbundle.outputs.baseline_missing }}" != "true" ]; then
|
||||
echo "❌ Support bundle comparison failed"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
|
||||
if [ $FAILURES -gt 0 ]; then
|
||||
echo ""
|
||||
echo "❌ $FAILURES regression(s) detected!"
|
||||
echo "Review the comparison reports in the artifacts."
|
||||
exit 1
|
||||
else
|
||||
echo "✅ All comparisons passed or skipped (no baseline)"
|
||||
fi
|
||||
|
||||
# 5. UPDATE BASELINES (optional, manual trigger only)
|
||||
- name: Update baselines
|
||||
if: github.event.inputs.update_baselines == 'true' && github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
echo "Updating baselines with current bundles..."
|
||||
|
||||
# Copy new bundles as baselines
|
||||
if [ -f test/output/preflight-v1beta3-bundle.tar.gz ]; then
|
||||
mkdir -p test/baselines/preflight-v1beta3
|
||||
cp test/output/preflight-v1beta3-bundle.tar.gz test/baselines/preflight-v1beta3/baseline.tar.gz
|
||||
echo "✓ Updated v1beta3 baseline"
|
||||
fi
|
||||
|
||||
if [ -f test/output/preflight-v1beta2-bundle.tar.gz ]; then
|
||||
mkdir -p test/baselines/preflight-v1beta2
|
||||
cp test/output/preflight-v1beta2-bundle.tar.gz test/baselines/preflight-v1beta2/baseline.tar.gz
|
||||
echo "✓ Updated v1beta2 baseline"
|
||||
fi
|
||||
|
||||
if [ -f test/output/supportbundle.tar.gz ]; then
|
||||
mkdir -p test/baselines/supportbundle
|
||||
cp test/output/supportbundle.tar.gz test/baselines/supportbundle/baseline.tar.gz
|
||||
echo "✓ Updated support bundle baseline"
|
||||
fi
|
||||
|
||||
# Create metadata file
|
||||
cat > test/baselines/metadata.json <<EOF
|
||||
{
|
||||
"updated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"git_sha": "${{ github.sha }}",
|
||||
"k8s_version": "v1.28.3",
|
||||
"workflow_run": "${{ github.run_id }}"
|
||||
}
|
||||
EOF
|
||||
|
||||
# Commit and push
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add test/baselines/
|
||||
git commit -m "chore: update regression test baselines from run ${{ github.run_id }}"
|
||||
git push
|
||||
|
||||
# 6. CLEANUP
|
||||
- name: Remove cluster
|
||||
if: always()
|
||||
uses: replicatedhq/compatibility-actions/remove-cluster@v1
|
||||
continue-on-error: true
|
||||
with:
|
||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||
cluster-id: ${{ steps.create-cluster.outputs.cluster-id }}
|
||||
48
.github/workflows/release.yaml
vendored
Normal file
48
.github/workflows/release.yaml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: troubleshoot_release
|
||||
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'
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: "v2.12.3"
|
||||
args: release --clean --config deploy/.goreleaser.yaml
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
|
||||
|
||||
- name: Update new preflight version in krew-index
|
||||
if: ${{ !contains(github.ref_name, '-') }}
|
||||
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
|
||||
if: ${{ !contains(github.ref_name, '-') }}
|
||||
uses: rajatjindal/krew-release-bot@v0.0.47
|
||||
with:
|
||||
krew_template_file: deploy/krew/support-bundle.yaml
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -48,3 +48,11 @@ sbom/
|
||||
# Ignore generated support bundles
|
||||
*.tar.gz
|
||||
!testdata/supportbundle/*.tar.gz
|
||||
!test/baselines/**/baseline.tar.gz
|
||||
|
||||
# Ignore built binaries
|
||||
troubleshoot
|
||||
troubleshoot-test
|
||||
cmd/troubleshoot/troubleshoot
|
||||
cmd/*/troubleshoot
|
||||
support-bundle
|
||||
@@ -1,15 +0,0 @@
|
||||
# https://golangci-lint.run/usage/configuration/#config-file
|
||||
|
||||
run:
|
||||
allow-parallel-runners: true
|
||||
timeout: 10m
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- gofmt
|
||||
- gosec
|
||||
- govet
|
||||
disable:
|
||||
- errcheck
|
||||
@@ -1,128 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
oss@replicated.com.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
@@ -1,12 +1,10 @@
|
||||
# Contributing to Troubleshoot
|
||||
|
||||
Thank you for your interest in Troubleshoot, we welcome your participation. Please familiarize yourself with our [Code of Conduct](https://github.com/replicatedhq/troubleshoot/blob/main/CODE_OF_CONDUCT.md) prior to contributing. There are a number of ways to participate in Troubleshoot as outlined below:
|
||||
Thank you for your interest in Troubleshoot, we welcome your participation. There are a number of ways to participate in Troubleshoot as outlined below:
|
||||
|
||||
# Community
|
||||
|
||||
For discussions about developing Troubleshoot, there's an [#app-troubleshoot channel in Kubernetes Slack](https://kubernetes.slack.com/channels/app-troubleshoot), plus IRC using [Libera](ircs://irc.libera.chat:6697/#troubleshoot) (#troubleshoot).
|
||||
|
||||
There are [community meetings](https://calendar.google.com/calendar/u/0?cid=Y19mMGx1aGhiZGtscGllOGo5dWpicXMwNnN1a0Bncm91cC5jYWxlbmRhci5nb29nbGUuY29t) on a regular basis, with a shared calendar and [public notes](https://hackmd.io/yZbotEHdTg6TfRZBzb8Tcg)
|
||||
For discussions about developing Troubleshoot, there's an [#app-troubleshoot channel in Kubernetes Slack](https://kubernetes.slack.com/channels/app-troubleshoot).
|
||||
|
||||
## Issues
|
||||
|
||||
@@ -21,44 +19,15 @@ When implementing a new feature please review the [design principles](./docs/des
|
||||
|
||||
To get started we recommend:
|
||||
|
||||
1. Go (v1.20 or later)
|
||||
2. A Kubernetes cluster (we recommend <https://k3d.io/>. This requires Docker v20.10.5 or later)
|
||||
1. Go (v1.24 or later)
|
||||
2. For cluster-based collectors, you will need access to a Kubernetes cluster
|
||||
3. Fork and clone repo
|
||||
4. Run `make clean build` to generate binaries
|
||||
5. Run `make run-support-bundle` to generate a support bundle with the `sample-troubleshoot.yaml` in the root of the repo
|
||||
5. You can now run `./bin/preflight` and/or `./bin/support-bundle` to use the code you've been writing
|
||||
|
||||
> Note: recent versions of Go support easy cross-compilation. For example, to cross-compile a Linux binary from MacOS:
|
||||
> Note: to cross-compile a Linux binary from MacOS:
|
||||
> `GOOS=linux GOARCH=amd64 make clean build`
|
||||
|
||||
6. Install [golangci-lint] linter and run `make lint` to execute additional code linters.
|
||||
|
||||
### Build automatically on save with `watch`
|
||||
|
||||
1. Install `npm`
|
||||
2. Run `make watch` to build binaries automatically on saving. Note: you may still have to run `make schemas` if you've added API changes, like a new collector or analyzer type.
|
||||
|
||||
### Syncing to a test cluster with `watchrsync`
|
||||
|
||||
1. Install `npm`
|
||||
2. Export `REMOTES=<user>@<ip>` so that `watchrsync` knows where to sync.
|
||||
3. Maybe run `export GOOS=linux` and `export GOARCH=amd64` so that you build Linux binaries.
|
||||
4. run `make watchrsync` to build and sync binaries automatically on saving.
|
||||
|
||||
```
|
||||
ssh-add --apple-use-keychain ~/.ssh/google_compute_engine
|
||||
export REMOTES=ada@35.229.61.56
|
||||
export GOOS=linux
|
||||
export GOARCH=amd64
|
||||
make watchrsync
|
||||
# bin/watchrsync.js
|
||||
# make support-bundle
|
||||
# go build -tags "netgo containers_image_ostree_stub exclude_graphdriver_devicemapper exclude_graphdriver_btrfs containers_image_openpgp" -installsuffix netgo -ldflags " -s -w -X github.com/replicatedhq/troubleshoot/pkg/version.version=`git describe --tags --dirty` -X github.com/replicatedhq/troubleshoot/pkg/version.gitSHA=`git rev-parse HEAD` -X github.com/replicatedhq/troubleshoot/pkg/version.buildTime=`date -u +"%Y-%m-%dT%H:%M:%SZ"` " -o bin/support-bundle github.com/replicatedhq/troubleshoot/cmd/troubleshoot
|
||||
# rsync bin/support-bundle ada@35.229.61.56:
|
||||
# date
|
||||
# Tue May 16 14:14:13 EDT 2023
|
||||
# synced
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
To run the tests locally run the following:
|
||||
@@ -104,42 +73,4 @@ More on profiling please visit https://go.dev/doc/diagnostics#profiling
|
||||
|
||||
## Contribution workflow
|
||||
|
||||
This is a rough outline of how to prepare a contribution:
|
||||
|
||||
- Create a fork of this repo.
|
||||
- Create a topic branch from where you want to base your work (branched from `main` is a safe choice).
|
||||
- Make commits of logical units.
|
||||
- When your changes are ready to merge, squash your history to 1 commit.
|
||||
- For example, if you want to squash your last 3 commits and write a new commit message:
|
||||
|
||||
```
|
||||
git reset --soft HEAD~3 &&
|
||||
git commit
|
||||
```
|
||||
|
||||
- If you want to keep the previous commit messages and concatenate them all into a new commit, you can do something like this instead:
|
||||
|
||||
```
|
||||
git reset --soft HEAD~3 &&
|
||||
git commit --edit -m"$(git log --format=%B --reverse HEAD..HEAD@{1})"
|
||||
```
|
||||
|
||||
- Push your changes to a topic branch in your fork of the repository.
|
||||
- Submit a pull request to the original repository. It will be reviewed in a timely manner.
|
||||
|
||||
### Pull Requests
|
||||
|
||||
A pull request should address a single issue, feature or bug. For example, lets say you've written code that fixes two issues. That's great! However, you should submit two small pull requests, one for each issue as opposed to combining them into a single larger pull request. In general the size of the pull request should be kept small in order to make it easy for a reviewer to understand, and to minimize risks from integrating many changes at the same time. For example, if you are working on a large feature you should break it into several smaller PRs by implementing the feature as changes to several packages and submitting a separate pull request for each one. Squash commit history when preparing your PR so it merges as 1 commit.
|
||||
|
||||
Code submitted in pull requests must be properly documented, formatted and tested in order to be approved and merged. The following guidelines describe the things a reviewer will look for when they evaluate your pull request. Here's a tip. If your reviewer doesn't understand what the code is doing, they won't approve the pull request. Strive to make code clear and well documented. If possible, request a reviewer that has some context on the PR.
|
||||
|
||||
### Commit messages
|
||||
|
||||
Commit messages should follow the general guidelines:
|
||||
|
||||
- Breaking changes should be highlighted in the heading of the commit message.
|
||||
- Commits should be clear about their purpose (and a single commit per thing that changed)
|
||||
- Messages should be descriptive:
|
||||
- First line, 50 chars or less, as a heading/title that people can find
|
||||
- Then a paragraph explaining things
|
||||
- Consider a footer with links to which bugs they fix etc, bearing in mind that Github does some of this magic already
|
||||
We'd love to talk before you dig into a a large feature.
|
||||
|
||||
1695
Cron-Job-Support-Bundles-PRD.md
Normal file
1695
Cron-Job-Support-Bundles-PRD.md
Normal file
File diff suppressed because it is too large
Load Diff
4
Makefile
4
Makefile
@@ -86,7 +86,7 @@ rebuild: clean build
|
||||
# Build all binaries in parallel ( -j )
|
||||
build: tidy
|
||||
@echo "Build cli binaries"
|
||||
$(MAKE) -j bin/support-bundle bin/preflight bin/analyze bin/collect
|
||||
$(MAKE) -j bin/support-bundle bin/preflight
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
@@ -295,4 +295,4 @@ longhorn:
|
||||
find pkg/longhorn -type f | xargs sed -i "s/github.com\/longhorn\/longhorn-manager\/k8s\/pkg/github.com\/replicatedhq\/troubleshoot\/pkg\/longhorn/g"
|
||||
find pkg/longhorn -type f | xargs sed -i "s/github.com\/longhorn\/longhorn-manager\/types/github.com\/replicatedhq\/troubleshoot\/pkg\/longhorn\/types/g"
|
||||
find pkg/longhorn -type f | xargs sed -i "s/github.com\/longhorn\/longhorn-manager\/util/github.com\/replicatedhq\/troubleshoot\/pkg\/longhorn\/util/g"
|
||||
rm -rf longhorn-manager
|
||||
rm -rf longhorn-manager
|
||||
29
bin/watch.js
29
bin/watch.js
@@ -1,29 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const gri = require('gaze-run-interrupt');
|
||||
|
||||
const commands = [
|
||||
// {
|
||||
// command: 'rm',
|
||||
// args: binList,
|
||||
// },
|
||||
{
|
||||
command: 'make',
|
||||
args: ['build'],
|
||||
},
|
||||
];
|
||||
|
||||
commands.push({
|
||||
command: "date",
|
||||
args: [],
|
||||
});
|
||||
|
||||
commands.push({
|
||||
command: "echo",
|
||||
args: ["synced"],
|
||||
});
|
||||
|
||||
gri([
|
||||
'cmd/**/*.go',
|
||||
'pkg/**/*.go',
|
||||
], commands);
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const gri = require('gaze-run-interrupt');
|
||||
|
||||
if (!process.env.REMOTES) {
|
||||
console.log("Usage: `REMOTES='user@h1.1.1.1,user@1.1.1.2' ./watchrsync.js`");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.env.GOOS = 'linux';
|
||||
process.env.GOARCH = 'amd64';
|
||||
|
||||
const binList = [
|
||||
// 'bin/analyze',
|
||||
// 'bin/preflight',
|
||||
'bin/support-bundle',
|
||||
// 'bin/collect'
|
||||
]
|
||||
|
||||
const commands = [
|
||||
// {
|
||||
// command: 'rm',
|
||||
// args: binList,
|
||||
// },
|
||||
{
|
||||
command: 'make',
|
||||
args: ['build'],
|
||||
},
|
||||
];
|
||||
|
||||
process.env.REMOTES.split(",").forEach(function (remote) {
|
||||
commands.push({
|
||||
command: 'rsync',
|
||||
args: binList.concat(`${remote}:`),
|
||||
});
|
||||
});
|
||||
|
||||
commands.push({
|
||||
command: "date",
|
||||
args: [],
|
||||
});
|
||||
|
||||
commands.push({
|
||||
command: "echo",
|
||||
args: ["synced"],
|
||||
});
|
||||
|
||||
gri([
|
||||
'cmd/**/*.go',
|
||||
'pkg/**/*.go',
|
||||
], commands);
|
||||
@@ -1,6 +1,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -12,10 +13,26 @@ import (
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// validateArgs allows certain flags to run without requiring bundle arguments
|
||||
func validateArgs(cmd *cobra.Command, args []string) error {
|
||||
// Special flags that don't require bundle arguments
|
||||
if cmd.Flags().Changed("check-ollama") || cmd.Flags().Changed("setup-ollama") ||
|
||||
cmd.Flags().Changed("list-models") || cmd.Flags().Changed("pull-model") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// For all other cases, require at least 1 argument (the bundle path)
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("requires at least 1 arg(s), only received %d. Usage: analyze [bundle-path] or use --check-ollama/--setup-ollama", len(args))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RootCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "analyze [url]",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Args: validateArgs,
|
||||
Short: "Analyze a support bundle",
|
||||
Long: `Run a series of analyzers on a support bundle archive`,
|
||||
SilenceUsage: true,
|
||||
@@ -32,7 +49,13 @@ func RootCmd() *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
v := viper.GetViper()
|
||||
|
||||
return runAnalyzers(v, args[0])
|
||||
// Handle cases where no bundle argument is provided (for utility flags)
|
||||
var bundlePath string
|
||||
if len(args) > 0 {
|
||||
bundlePath = args[0]
|
||||
}
|
||||
|
||||
return runAnalyzers(v, bundlePath)
|
||||
},
|
||||
PostRun: func(cmd *cobra.Command, args []string) {
|
||||
if err := util.StopProfiling(); err != nil {
|
||||
@@ -48,6 +71,23 @@ func RootCmd() *cobra.Command {
|
||||
cmd.Flags().String("analyzers", "", "filename or url of the analyzers to use")
|
||||
cmd.Flags().Bool("debug", false, "enable debug logging")
|
||||
|
||||
// Advanced analysis flags
|
||||
cmd.Flags().Bool("advanced-analysis", false, "use advanced analysis engine with AI capabilities")
|
||||
cmd.Flags().StringSlice("agents", []string{"local"}, "analysis agents to use: local, hosted, ollama")
|
||||
cmd.Flags().Bool("enable-ollama", false, "enable Ollama AI-powered analysis")
|
||||
cmd.Flags().Bool("disable-ollama", false, "explicitly disable Ollama AI-powered analysis")
|
||||
cmd.Flags().String("ollama-endpoint", "http://localhost:11434", "Ollama server endpoint")
|
||||
cmd.Flags().String("ollama-model", "llama2:7b", "Ollama model to use for analysis")
|
||||
cmd.Flags().Bool("use-codellama", false, "use CodeLlama model for code-focused analysis")
|
||||
cmd.Flags().Bool("use-mistral", false, "use Mistral model for fast analysis")
|
||||
cmd.Flags().Bool("auto-pull-model", true, "automatically pull model if not available")
|
||||
cmd.Flags().Bool("list-models", false, "list all available/installed Ollama models and exit")
|
||||
cmd.Flags().Bool("pull-model", false, "pull the specified model and exit")
|
||||
cmd.Flags().Bool("setup-ollama", false, "automatically setup and configure Ollama")
|
||||
cmd.Flags().Bool("check-ollama", false, "check Ollama installation status and exit")
|
||||
cmd.Flags().Bool("include-remediation", true, "include remediation suggestions in analysis results")
|
||||
cmd.Flags().String("output-file", "", "save analysis results to file (e.g., --output-file results.json)")
|
||||
|
||||
viper.BindPFlags(cmd.Flags())
|
||||
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||||
|
||||
@@ -1,18 +1,408 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/internal/util"
|
||||
analyzer "github.com/replicatedhq/troubleshoot/pkg/analyze"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/analyze/agents/local"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/analyze/agents/ollama"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/spf13/viper"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
func runAnalyzers(v *viper.Viper, bundlePath string) error {
|
||||
// Handle Ollama-specific commands first (these don't require a bundle)
|
||||
if v.GetBool("setup-ollama") {
|
||||
return handleOllamaSetup(v)
|
||||
}
|
||||
|
||||
if v.GetBool("check-ollama") {
|
||||
return handleOllamaStatus(v)
|
||||
}
|
||||
|
||||
if v.GetBool("list-models") {
|
||||
return handleListModels(v)
|
||||
}
|
||||
|
||||
if v.GetBool("pull-model") {
|
||||
return handlePullModel(v)
|
||||
}
|
||||
|
||||
// For all other operations, we need a bundle path
|
||||
if bundlePath == "" {
|
||||
return errors.New("bundle path is required for analysis operations")
|
||||
}
|
||||
|
||||
// Check if advanced analysis is requested
|
||||
useAdvanced := v.GetBool("advanced-analysis") ||
|
||||
v.GetBool("enable-ollama") ||
|
||||
(len(v.GetStringSlice("agents")) > 1 ||
|
||||
(len(v.GetStringSlice("agents")) == 1 && v.GetStringSlice("agents")[0] != "local"))
|
||||
|
||||
if useAdvanced {
|
||||
return runAdvancedAnalysis(v, bundlePath)
|
||||
}
|
||||
|
||||
// Only fall back to legacy analysis if no advanced flags are used at all
|
||||
return runLegacyAnalysis(v, bundlePath)
|
||||
}
|
||||
|
||||
// handleOllamaSetup automatically sets up Ollama for the user
|
||||
func handleOllamaSetup(v *viper.Viper) error {
|
||||
fmt.Println("🚀 Ollama Setup Assistant")
|
||||
fmt.Println("=" + strings.Repeat("=", 50))
|
||||
|
||||
helper := analyzer.NewOllamaHelper()
|
||||
|
||||
// Check current status
|
||||
status := helper.GetHealthStatus()
|
||||
fmt.Print(status.String())
|
||||
|
||||
if !status.Installed {
|
||||
fmt.Println("\n🔧 Installing Ollama...")
|
||||
if err := helper.DownloadAndInstall(); err != nil {
|
||||
return errors.Wrap(err, "failed to install Ollama")
|
||||
}
|
||||
fmt.Println("✅ Ollama installed successfully!")
|
||||
}
|
||||
|
||||
if !status.Running {
|
||||
fmt.Println("\n🚀 Starting Ollama service...")
|
||||
if err := helper.StartService(); err != nil {
|
||||
return errors.Wrap(err, "failed to start Ollama service")
|
||||
}
|
||||
fmt.Println("✅ Ollama service started!")
|
||||
}
|
||||
|
||||
if len(status.Models) == 0 {
|
||||
fmt.Println("\n📚 Downloading recommended model...")
|
||||
helper.PrintModelRecommendations()
|
||||
|
||||
model := v.GetString("ollama-model")
|
||||
if model == "" {
|
||||
model = "llama2:7b"
|
||||
}
|
||||
|
||||
fmt.Printf("\n⬇️ Pulling model: %s (this may take several minutes)...\n", model)
|
||||
if err := helper.PullModel(model); err != nil {
|
||||
return errors.Wrapf(err, "failed to pull model %s", model)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\n🎉 Ollama setup complete!")
|
||||
fmt.Println("\n💡 Next steps:")
|
||||
fmt.Printf(" troubleshoot analyze --enable-ollama %s\n", filepath.Base(os.Args[len(os.Args)-1]))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleOllamaStatus shows current Ollama installation and service status
|
||||
func handleOllamaStatus(v *viper.Viper) error {
|
||||
helper := analyzer.NewOllamaHelper()
|
||||
status := helper.GetHealthStatus()
|
||||
|
||||
fmt.Println("🔍 Ollama Status Report")
|
||||
fmt.Println("=" + strings.Repeat("=", 50))
|
||||
fmt.Print(status.String())
|
||||
|
||||
if !status.Installed {
|
||||
fmt.Println("\n🔧 Setup Instructions:")
|
||||
fmt.Println(helper.GetInstallInstructions())
|
||||
return nil
|
||||
}
|
||||
|
||||
if !status.Running {
|
||||
fmt.Println("\n🚀 To start Ollama service:")
|
||||
fmt.Println(" ollama serve &")
|
||||
fmt.Println(" # or")
|
||||
fmt.Println(" troubleshoot analyze --setup-ollama")
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(status.Models) == 0 {
|
||||
fmt.Println("\n📚 No models installed. Recommended models:")
|
||||
helper.PrintModelRecommendations()
|
||||
} else {
|
||||
fmt.Println("\n✅ Ready for AI-powered analysis!")
|
||||
fmt.Printf(" troubleshoot analyze --enable-ollama your-bundle.tar.gz\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleListModels lists available and installed Ollama models
|
||||
func handleListModels(v *viper.Viper) error {
|
||||
helper := analyzer.NewOllamaHelper()
|
||||
status := helper.GetHealthStatus()
|
||||
|
||||
fmt.Println("🤖 Ollama Model Management")
|
||||
fmt.Println("=" + strings.Repeat("=", 50))
|
||||
|
||||
if !status.Installed {
|
||||
fmt.Println("❌ Ollama is not installed")
|
||||
fmt.Println("💡 Install with: troubleshoot analyze --setup-ollama")
|
||||
return nil
|
||||
}
|
||||
|
||||
if !status.Running {
|
||||
fmt.Println("⚠️ Ollama service is not running")
|
||||
fmt.Println("🚀 Start with: ollama serve &")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Show installed models
|
||||
fmt.Println("📚 Installed Models:")
|
||||
if len(status.Models) == 0 {
|
||||
fmt.Println(" No models installed")
|
||||
} else {
|
||||
for _, model := range status.Models {
|
||||
fmt.Printf(" ✅ %s\n", model)
|
||||
}
|
||||
}
|
||||
|
||||
// Show available models for download
|
||||
fmt.Println("\n🌐 Available Models:")
|
||||
helper.PrintModelRecommendations()
|
||||
|
||||
// Show usage examples
|
||||
fmt.Println("💡 Usage Examples:")
|
||||
fmt.Println(" # Use specific model:")
|
||||
fmt.Printf(" troubleshoot analyze --ollama-model llama2:13b bundle.tar.gz\n")
|
||||
fmt.Println(" # Use preset models:")
|
||||
fmt.Printf(" troubleshoot analyze --use-codellama bundle.tar.gz\n")
|
||||
fmt.Printf(" troubleshoot analyze --use-mistral bundle.tar.gz\n")
|
||||
fmt.Println(" # Pull a new model:")
|
||||
fmt.Printf(" troubleshoot analyze --ollama-model llama2:13b --pull-model\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handlePullModel pulls a specific model
|
||||
func handlePullModel(v *viper.Viper) error {
|
||||
helper := analyzer.NewOllamaHelper()
|
||||
status := helper.GetHealthStatus()
|
||||
|
||||
if !status.Installed {
|
||||
fmt.Println("❌ Ollama is not installed")
|
||||
fmt.Println("💡 Install with: troubleshoot analyze --setup-ollama")
|
||||
return errors.New("Ollama must be installed to pull models")
|
||||
}
|
||||
|
||||
if !status.Running {
|
||||
fmt.Println("❌ Ollama service is not running")
|
||||
fmt.Println("🚀 Start with: ollama serve &")
|
||||
return errors.New("Ollama service must be running to pull models")
|
||||
}
|
||||
|
||||
// Determine which model to pull
|
||||
model := determineOllamaModel(v)
|
||||
|
||||
fmt.Printf("📥 Pulling model: %s\n", model)
|
||||
fmt.Println("=" + strings.Repeat("=", 50))
|
||||
|
||||
if err := helper.PullModel(model); err != nil {
|
||||
return errors.Wrapf(err, "failed to pull model %s", model)
|
||||
}
|
||||
|
||||
fmt.Printf("\n✅ Model %s ready for analysis!\n", model)
|
||||
fmt.Println("\n💡 Test it with:")
|
||||
fmt.Printf(" troubleshoot analyze --ollama-model %s bundle.tar.gz\n", model)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// runAdvancedAnalysis uses the new analysis engine with agent support
|
||||
func runAdvancedAnalysis(v *viper.Viper, bundlePath string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create the analysis engine
|
||||
engine := analyzer.NewAnalysisEngine()
|
||||
|
||||
// Determine which agents to use
|
||||
agents := v.GetStringSlice("agents")
|
||||
|
||||
// Handle Ollama flags
|
||||
enableOllama := v.GetBool("enable-ollama")
|
||||
disableOllama := v.GetBool("disable-ollama")
|
||||
|
||||
if enableOllama && !disableOllama {
|
||||
// Add ollama to agents if not already present
|
||||
hasOllama := false
|
||||
for _, agent := range agents {
|
||||
if agent == "ollama" {
|
||||
hasOllama = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasOllama {
|
||||
agents = append(agents, "ollama")
|
||||
}
|
||||
}
|
||||
|
||||
if disableOllama {
|
||||
// Remove ollama from agents
|
||||
filteredAgents := []string{}
|
||||
for _, agent := range agents {
|
||||
if agent != "ollama" {
|
||||
filteredAgents = append(filteredAgents, agent)
|
||||
}
|
||||
}
|
||||
agents = filteredAgents
|
||||
}
|
||||
|
||||
// Register requested agents
|
||||
registeredAgents := []string{}
|
||||
for _, agentName := range agents {
|
||||
switch agentName {
|
||||
case "ollama":
|
||||
if err := registerOllamaAgent(engine, v); err != nil {
|
||||
return err
|
||||
}
|
||||
registeredAgents = append(registeredAgents, agentName)
|
||||
|
||||
case "local":
|
||||
opts := &local.LocalAgentOptions{}
|
||||
agent := local.NewLocalAgent(opts)
|
||||
if err := engine.RegisterAgent("local", agent); err != nil {
|
||||
return errors.Wrap(err, "failed to register local agent")
|
||||
}
|
||||
registeredAgents = append(registeredAgents, agentName)
|
||||
|
||||
default:
|
||||
klog.Warningf("Unknown agent type: %s", agentName)
|
||||
}
|
||||
}
|
||||
|
||||
if len(registeredAgents) == 0 {
|
||||
return errors.New("no analysis agents available - check your configuration")
|
||||
}
|
||||
|
||||
fmt.Printf("🔍 Using analysis agents: %s\n", strings.Join(registeredAgents, ", "))
|
||||
|
||||
// Load support bundle
|
||||
bundle, err := loadSupportBundle(bundlePath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to load support bundle")
|
||||
}
|
||||
|
||||
// Load analyzer specs if provided
|
||||
var customAnalyzers []*troubleshootv1beta2.Analyze
|
||||
if specPath := v.GetString("analyzers"); specPath != "" {
|
||||
customAnalyzers, err = loadAnalyzerSpecs(specPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to load analyzer specs")
|
||||
}
|
||||
}
|
||||
|
||||
// Configure analysis options
|
||||
opts := analyzer.AnalysisOptions{
|
||||
Agents: registeredAgents,
|
||||
IncludeRemediation: v.GetBool("include-remediation"),
|
||||
CustomAnalyzers: customAnalyzers,
|
||||
Timeout: 5 * time.Minute,
|
||||
Concurrency: 2,
|
||||
}
|
||||
|
||||
// Run analysis
|
||||
fmt.Printf("🚀 Starting advanced analysis of bundle: %s\n", bundlePath)
|
||||
result, err := engine.Analyze(ctx, bundle, opts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "analysis failed")
|
||||
}
|
||||
|
||||
// Display results
|
||||
return displayAdvancedResults(result, v.GetString("output"), v.GetString("output-file"))
|
||||
}
|
||||
|
||||
// registerOllamaAgent creates and registers an Ollama agent
|
||||
func registerOllamaAgent(engine analyzer.AnalysisEngine, v *viper.Viper) error {
|
||||
// Check if Ollama is available
|
||||
helper := analyzer.NewOllamaHelper()
|
||||
status := helper.GetHealthStatus()
|
||||
|
||||
if !status.Installed {
|
||||
return showOllamaSetupHelp("Ollama is not installed")
|
||||
}
|
||||
|
||||
if !status.Running {
|
||||
return showOllamaSetupHelp("Ollama service is not running")
|
||||
}
|
||||
|
||||
if len(status.Models) == 0 {
|
||||
return showOllamaSetupHelp("No Ollama models are installed")
|
||||
}
|
||||
|
||||
// Determine which model to use
|
||||
selectedModel := determineOllamaModel(v)
|
||||
|
||||
// Auto-pull model if requested and not available
|
||||
if v.GetBool("auto-pull-model") {
|
||||
if err := ensureModelAvailable(selectedModel); err != nil {
|
||||
return errors.Wrapf(err, "failed to ensure model %s is available", selectedModel)
|
||||
}
|
||||
}
|
||||
|
||||
// Create Ollama agent
|
||||
opts := &ollama.OllamaAgentOptions{
|
||||
Endpoint: v.GetString("ollama-endpoint"),
|
||||
Model: selectedModel,
|
||||
Timeout: 5 * time.Minute,
|
||||
MaxTokens: 2000,
|
||||
Temperature: 0.2,
|
||||
}
|
||||
|
||||
agent, err := ollama.NewOllamaAgent(opts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create Ollama agent")
|
||||
}
|
||||
|
||||
// Register with engine
|
||||
if err := engine.RegisterAgent("ollama", agent); err != nil {
|
||||
return errors.Wrap(err, "failed to register Ollama agent")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// showOllamaSetupHelp displays helpful setup instructions when Ollama is not available
|
||||
func showOllamaSetupHelp(reason string) error {
|
||||
fmt.Printf("❌ Ollama AI analysis not available: %s\n\n", reason)
|
||||
|
||||
helper := analyzer.NewOllamaHelper()
|
||||
fmt.Println("🔧 Quick Setup:")
|
||||
fmt.Println(" troubleshoot analyze --setup-ollama")
|
||||
fmt.Println()
|
||||
fmt.Println("📋 Manual Setup:")
|
||||
fmt.Println(" 1. Install: curl -fsSL https://ollama.ai/install.sh | sh")
|
||||
fmt.Println(" 2. Start service: ollama serve &")
|
||||
fmt.Println(" 3. Pull model: ollama pull llama2:7b")
|
||||
fmt.Println(" 4. Retry analysis with: --enable-ollama")
|
||||
fmt.Println()
|
||||
fmt.Println("💡 Check status: troubleshoot analyze --check-ollama")
|
||||
fmt.Println()
|
||||
fmt.Println(helper.GetInstallInstructions())
|
||||
|
||||
return errors.New("Ollama setup required for AI-powered analysis")
|
||||
}
|
||||
|
||||
// runLegacyAnalysis runs the original analysis logic for backward compatibility
|
||||
func runLegacyAnalysis(v *viper.Viper, bundlePath string) error {
|
||||
specPath := v.GetString("analyzers")
|
||||
|
||||
specContent := ""
|
||||
@@ -66,3 +456,302 @@ func runAnalyzers(v *viper.Viper, bundlePath string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadSupportBundle loads and parses a support bundle from file
|
||||
func loadSupportBundle(bundlePath string) (*analyzer.SupportBundle, error) {
|
||||
if _, err := os.Stat(bundlePath); os.IsNotExist(err) {
|
||||
return nil, errors.Errorf("support bundle not found: %s", bundlePath)
|
||||
}
|
||||
|
||||
klog.Infof("Loading support bundle: %s", bundlePath)
|
||||
|
||||
// Open the tar.gz file
|
||||
file, err := os.Open(bundlePath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to open support bundle")
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Create gzip reader
|
||||
gzipReader, err := gzip.NewReader(file)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create gzip reader")
|
||||
}
|
||||
defer gzipReader.Close()
|
||||
|
||||
// Create tar reader
|
||||
tarReader := tar.NewReader(gzipReader)
|
||||
|
||||
// Create bundle structure
|
||||
bundle := &analyzer.SupportBundle{
|
||||
Files: make(map[string][]byte),
|
||||
Metadata: &analyzer.SupportBundleMetadata{
|
||||
CreatedAt: time.Now(),
|
||||
Version: "1.0.0",
|
||||
GeneratedBy: "troubleshoot-cli",
|
||||
},
|
||||
}
|
||||
|
||||
// Extract all files from tar
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read tar entry")
|
||||
}
|
||||
|
||||
// Skip directories
|
||||
if header.Typeflag == tar.TypeDir {
|
||||
continue
|
||||
}
|
||||
|
||||
// Read file content
|
||||
content, err := io.ReadAll(tarReader)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to read file %s", header.Name)
|
||||
}
|
||||
|
||||
// Remove bundle directory prefix from file path for consistent access
|
||||
// e.g., "live-cluster-bundle/cluster-info/version.json" → "cluster-info/version.json"
|
||||
cleanPath := header.Name
|
||||
if parts := strings.SplitN(header.Name, "/", 2); len(parts) == 2 {
|
||||
cleanPath = parts[1]
|
||||
}
|
||||
|
||||
bundle.Files[cleanPath] = content
|
||||
klog.V(2).Infof("Loaded file: %s (%d bytes)", cleanPath, len(content))
|
||||
}
|
||||
|
||||
klog.Infof("Successfully loaded support bundle with %d files", len(bundle.Files))
|
||||
|
||||
return bundle, nil
|
||||
}
|
||||
|
||||
// loadAnalyzerSpecs loads analyzer specifications from file or URL
|
||||
func loadAnalyzerSpecs(specPath string) ([]*troubleshootv1beta2.Analyze, error) {
|
||||
klog.Infof("Loading analyzer specs from: %s", specPath)
|
||||
|
||||
// Read the analyzer spec file (same logic as runLegacyAnalysis)
|
||||
specContent := ""
|
||||
var err error
|
||||
if _, err = os.Stat(specPath); err == nil {
|
||||
b, err := os.ReadFile(specPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read analyzer spec file")
|
||||
}
|
||||
specContent = string(b)
|
||||
} else {
|
||||
if !util.IsURL(specPath) {
|
||||
return nil, errors.Errorf("analyzer spec %s is not a URL and was not found", specPath)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", specPath, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create HTTP request")
|
||||
}
|
||||
req.Header.Set("User-Agent", "Replicated_Analyzer/v1beta2")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to fetch analyzer spec")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read analyzer spec response")
|
||||
}
|
||||
specContent = string(body)
|
||||
}
|
||||
|
||||
// Parse the YAML/JSON into troubleshoot analyzer struct
|
||||
var analyzerSpec troubleshootv1beta2.Analyzer
|
||||
if err := yaml.Unmarshal([]byte(specContent), &analyzerSpec); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse analyzer spec")
|
||||
}
|
||||
|
||||
// Return the analyzer specs from the parsed document
|
||||
return analyzerSpec.Spec.Analyzers, nil
|
||||
}
|
||||
|
||||
// displayAdvancedResults formats and displays analysis results
|
||||
func displayAdvancedResults(result *analyzer.AnalysisResult, outputFormat, outputFile string) error {
|
||||
if result == nil {
|
||||
return errors.New("no analysis results to display")
|
||||
}
|
||||
|
||||
// Display summary
|
||||
fmt.Println("\n📊 Analysis Summary")
|
||||
fmt.Println("=" + strings.Repeat("=", 50))
|
||||
fmt.Printf("Total Analyzers: %d\n", result.Summary.TotalAnalyzers)
|
||||
fmt.Printf("✅ Pass: %d\n", result.Summary.PassCount)
|
||||
fmt.Printf("⚠️ Warn: %d\n", result.Summary.WarnCount)
|
||||
fmt.Printf("❌ Fail: %d\n", result.Summary.FailCount)
|
||||
fmt.Printf("🚫 Errors: %d\n", result.Summary.ErrorCount)
|
||||
fmt.Printf("⏱️ Duration: %s\n", result.Summary.Duration)
|
||||
fmt.Printf("🤖 Agents Used: %s\n", strings.Join(result.Summary.AgentsUsed, ", "))
|
||||
|
||||
if result.Summary.Confidence > 0 {
|
||||
fmt.Printf("🎯 Confidence: %.1f%%\n", result.Summary.Confidence*100)
|
||||
}
|
||||
|
||||
// Display results based on format
|
||||
switch outputFormat {
|
||||
case "json":
|
||||
jsonData, err := json.MarshalIndent(result, "", " ")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to marshal results to JSON")
|
||||
}
|
||||
fmt.Println("\n📄 Full Results (JSON):")
|
||||
fmt.Println(string(jsonData))
|
||||
|
||||
default:
|
||||
// Human-readable format
|
||||
fmt.Println("\n🔍 Analysis Results")
|
||||
fmt.Println("=" + strings.Repeat("=", 50))
|
||||
|
||||
for _, analyzerResult := range result.Results {
|
||||
status := "❓"
|
||||
if analyzerResult.IsPass {
|
||||
status = "✅"
|
||||
} else if analyzerResult.IsWarn {
|
||||
status = "⚠️"
|
||||
} else if analyzerResult.IsFail {
|
||||
status = "❌"
|
||||
}
|
||||
|
||||
fmt.Printf("\n%s %s", status, analyzerResult.Title)
|
||||
if analyzerResult.AgentName != "" {
|
||||
fmt.Printf(" [%s]", analyzerResult.AgentName)
|
||||
}
|
||||
if analyzerResult.Confidence > 0 {
|
||||
fmt.Printf(" (%.0f%% confidence)", analyzerResult.Confidence*100)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
if analyzerResult.Message != "" {
|
||||
fmt.Printf(" %s\n", analyzerResult.Message)
|
||||
}
|
||||
|
||||
if analyzerResult.Category != "" {
|
||||
fmt.Printf(" Category: %s\n", analyzerResult.Category)
|
||||
}
|
||||
|
||||
// Display insights if available
|
||||
if len(analyzerResult.Insights) > 0 {
|
||||
fmt.Println(" 💡 Insights:")
|
||||
for _, insight := range analyzerResult.Insights {
|
||||
fmt.Printf(" • %s\n", insight)
|
||||
}
|
||||
}
|
||||
|
||||
// Display remediation if available
|
||||
if analyzerResult.Remediation != nil {
|
||||
fmt.Printf(" 🔧 Remediation: %s\n", analyzerResult.Remediation.Description)
|
||||
if analyzerResult.Remediation.Command != "" {
|
||||
fmt.Printf(" 💻 Command: %s\n", analyzerResult.Remediation.Command)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Display overall remediation suggestions
|
||||
if len(result.Remediation) > 0 {
|
||||
fmt.Println("\n🔧 Recommended Actions")
|
||||
fmt.Println("=" + strings.Repeat("=", 50))
|
||||
for i, remedy := range result.Remediation {
|
||||
fmt.Printf("%d. %s\n", i+1, remedy.Description)
|
||||
if remedy.Command != "" {
|
||||
fmt.Printf(" Command: %s\n", remedy.Command)
|
||||
}
|
||||
if remedy.Documentation != "" {
|
||||
fmt.Printf(" Docs: %s\n", remedy.Documentation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Display errors if any
|
||||
if len(result.Errors) > 0 {
|
||||
fmt.Println("\n⚠️ Errors During Analysis")
|
||||
fmt.Println("=" + strings.Repeat("=", 30))
|
||||
for _, analysisError := range result.Errors {
|
||||
fmt.Printf("• [%s] %s: %s\n", analysisError.Agent, analysisError.Category, analysisError.Error)
|
||||
}
|
||||
}
|
||||
|
||||
// Display agent metadata
|
||||
if len(result.Metadata.Agents) > 0 {
|
||||
fmt.Println("\n🤖 Agent Performance")
|
||||
fmt.Println("=" + strings.Repeat("=", 40))
|
||||
for _, agent := range result.Metadata.Agents {
|
||||
fmt.Printf("• %s: %d results, %s duration", agent.Name, agent.ResultCount, agent.Duration)
|
||||
if agent.ErrorCount > 0 {
|
||||
fmt.Printf(" (%d errors)", agent.ErrorCount)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save results to file if requested
|
||||
if outputFile != "" {
|
||||
jsonData, err := json.MarshalIndent(result, "", " ")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to marshal results for file output")
|
||||
}
|
||||
|
||||
if err := os.WriteFile(outputFile, jsonData, 0644); err != nil {
|
||||
return errors.Wrapf(err, "failed to write results to %s", outputFile)
|
||||
}
|
||||
|
||||
fmt.Printf("\n💾 Analysis results saved to: %s\n", outputFile)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// determineOllamaModel selects the appropriate model based on flags
|
||||
func determineOllamaModel(v *viper.Viper) string {
|
||||
// Check for specific model flags first
|
||||
if v.GetBool("use-codellama") {
|
||||
return "codellama:7b"
|
||||
}
|
||||
if v.GetBool("use-mistral") {
|
||||
return "mistral:7b"
|
||||
}
|
||||
|
||||
// Fall back to explicit model specification or default
|
||||
return v.GetString("ollama-model")
|
||||
}
|
||||
|
||||
// ensureModelAvailable checks if model exists and pulls it if needed
|
||||
func ensureModelAvailable(model string) error {
|
||||
// Check if model is already available
|
||||
cmd := exec.Command("ollama", "list")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to check available models")
|
||||
}
|
||||
|
||||
// Parse model list to see if our model exists
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, model) {
|
||||
klog.Infof("Model %s is already available", model)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Model not found, pull it
|
||||
fmt.Printf("📚 Model %s not found, pulling automatically...\n", model)
|
||||
pullCmd := exec.Command("ollama", "pull", model)
|
||||
pullCmd.Stdout = os.Stdout
|
||||
pullCmd.Stderr = os.Stderr
|
||||
|
||||
if err := pullCmd.Run(); err != nil {
|
||||
return errors.Wrapf(err, "failed to pull model %s", model)
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Model %s pulled successfully!\n", model)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/replicatedhq/troubleshoot/cmd/analyze/cli"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cli.InitAndExecute()
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
func checkAndSetChroot(newroot string) error {
|
||||
return errors.New("chroot is only implimented in linux/darwin")
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/cmd/internal/util"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/logger"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
func RootCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "collect [url]",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Short: "Run a collector",
|
||||
Long: `Run a collector and output the results.`,
|
||||
SilenceUsage: true,
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
v := viper.GetViper()
|
||||
v.BindPFlags(cmd.Flags())
|
||||
|
||||
logger.SetupLogger(v)
|
||||
|
||||
if err := util.StartProfiling(); err != nil {
|
||||
klog.Errorf("Failed to start profiling: %v", err)
|
||||
}
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
v := viper.GetViper()
|
||||
|
||||
if err := checkAndSetChroot(v.GetString("chroot")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return runCollect(v, args[0])
|
||||
},
|
||||
PostRun: func(cmd *cobra.Command, args []string) {
|
||||
if err := util.StopProfiling(); err != nil {
|
||||
klog.Errorf("Failed to stop profiling: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
cmd.AddCommand(util.VersionCmd())
|
||||
|
||||
cmd.Flags().StringSlice("redactors", []string{}, "names of the additional redactors to use")
|
||||
cmd.Flags().Bool("redact", true, "enable/disable default redactions")
|
||||
cmd.Flags().String("format", "json", "output format, one of json or raw.")
|
||||
cmd.Flags().String("collector-image", "", "the full name of the collector image to use")
|
||||
cmd.Flags().String("collector-pull-policy", "", "the pull policy of the collector image")
|
||||
cmd.Flags().String("selector", "", "selector (label query) to filter remote collection nodes on.")
|
||||
cmd.Flags().Bool("collect-without-permissions", false, "always generate a support bundle, even if it some require additional permissions")
|
||||
cmd.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")
|
||||
cmd.Flags().MarkHidden("allow-insecure-connections")
|
||||
|
||||
viper.BindPFlags(cmd.Flags())
|
||||
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||||
|
||||
k8sutil.AddFlags(cmd.Flags())
|
||||
|
||||
// Initialize klog flags
|
||||
logger.InitKlogFlags(cmd)
|
||||
|
||||
// CPU and memory profiling flags
|
||||
util.AddProfilingFlags(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func InitAndExecute() {
|
||||
if err := RootCmd().Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
viper.SetEnvPrefix("TROUBLESHOOT")
|
||||
viper.AutomaticEnv()
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/internal/util"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/docrewrite"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/specs"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/supportbundle"
|
||||
"github.com/spf13/viper"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
func runCollect(v *viper.Viper, arg string) error {
|
||||
go func() {
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
signal.Notify(signalChan, os.Interrupt)
|
||||
<-signalChan
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
var collectorContent []byte
|
||||
var err error
|
||||
if strings.HasPrefix(arg, "secret/") {
|
||||
// format secret/namespace-name/secret-name
|
||||
pathParts := strings.Split(arg, "/")
|
||||
if len(pathParts) != 3 {
|
||||
return errors.Errorf("path %s must have 3 components", arg)
|
||||
}
|
||||
|
||||
spec, err := specs.LoadFromSecret(pathParts[1], pathParts[2], "collect-spec")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get spec from secret")
|
||||
}
|
||||
|
||||
collectorContent = spec
|
||||
} else if arg == "-" {
|
||||
b, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collectorContent = b
|
||||
} else if _, err = os.Stat(arg); err == nil {
|
||||
b, err := os.ReadFile(arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collectorContent = b
|
||||
} else {
|
||||
if !util.IsURL(arg) {
|
||||
return fmt.Errorf("%s is not a URL and was not found", arg)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", arg, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Replicated_Collect/v1beta2")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collectorContent = body
|
||||
}
|
||||
|
||||
collectorContent, err = docrewrite.ConvertToV1Beta2(collectorContent)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert to v1beta2")
|
||||
}
|
||||
|
||||
multidocs := strings.Split(string(collectorContent), "\n---\n")
|
||||
|
||||
decode := scheme.Codecs.UniversalDeserializer().Decode
|
||||
|
||||
redactors, err := supportbundle.GetRedactorsFromURIs(v.GetStringSlice("redactors"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get redactors")
|
||||
}
|
||||
|
||||
additionalRedactors := &troubleshootv1beta2.Redactor{
|
||||
Spec: troubleshootv1beta2.RedactorSpec{
|
||||
Redactors: redactors,
|
||||
},
|
||||
}
|
||||
|
||||
for i, additionalDoc := range multidocs {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
additionalDoc, err := docrewrite.ConvertToV1Beta2([]byte(additionalDoc))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert to v1beta2")
|
||||
}
|
||||
obj, _, err := decode(additionalDoc, nil, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to parse additional doc %d", i)
|
||||
}
|
||||
multidocRedactors, ok := obj.(*troubleshootv1beta2.Redactor)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
additionalRedactors.Spec.Redactors = append(additionalRedactors.Spec.Redactors, multidocRedactors.Spec.Redactors...)
|
||||
}
|
||||
|
||||
// make sure we don't block any senders
|
||||
progressCh := make(chan interface{})
|
||||
defer close(progressCh)
|
||||
go func() {
|
||||
for range progressCh {
|
||||
}
|
||||
}()
|
||||
|
||||
restConfig, err := k8sutil.GetRESTConfig()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert kube flags to rest config")
|
||||
}
|
||||
|
||||
labelSelector, err := labels.Parse(v.GetString("selector"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to parse selector")
|
||||
}
|
||||
|
||||
namespace := v.GetString("namespace")
|
||||
if namespace == "" {
|
||||
namespace = "default"
|
||||
}
|
||||
|
||||
timeout := v.GetDuration("request-timeout")
|
||||
if timeout == 0 {
|
||||
timeout = defaultTimeout
|
||||
}
|
||||
|
||||
createOpts := collect.CollectorRunOpts{
|
||||
CollectWithoutPermissions: v.GetBool("collect-without-permissions"),
|
||||
KubernetesRestConfig: restConfig,
|
||||
Image: v.GetString("collector-image"),
|
||||
PullPolicy: v.GetString("collector-pullpolicy"),
|
||||
LabelSelector: labelSelector.String(),
|
||||
Namespace: namespace,
|
||||
Timeout: timeout,
|
||||
ProgressChan: progressCh,
|
||||
}
|
||||
|
||||
// we only support HostCollector or RemoteCollector kinds.
|
||||
hostCollector, err := collect.ParseHostCollectorFromDoc([]byte(multidocs[0]))
|
||||
if err == nil {
|
||||
results, err := collect.CollectHost(hostCollector, additionalRedactors, createOpts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to collect from host")
|
||||
}
|
||||
return showHostStdoutResults(v.GetString("format"), hostCollector.Name, results)
|
||||
}
|
||||
|
||||
remoteCollector, err := collect.ParseRemoteCollectorFromDoc([]byte(multidocs[0]))
|
||||
if err == nil {
|
||||
results, err := collect.CollectRemote(remoteCollector, additionalRedactors, createOpts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to collect from remote host(s)")
|
||||
}
|
||||
return showRemoteStdoutResults(v.GetString("format"), remoteCollector.Name, results)
|
||||
}
|
||||
|
||||
return errors.New("failed to parse hostCollector or remoteCollector")
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
)
|
||||
|
||||
const (
|
||||
// FormatJSON is intended for CLI output.
|
||||
FormatJSON = "json"
|
||||
|
||||
// FormatRaw is intended for consumption by a remote collector. Output is a
|
||||
// string of quoted JSON.
|
||||
FormatRaw = "raw"
|
||||
)
|
||||
|
||||
func showHostStdoutResults(format string, collectName string, results *collect.HostCollectResult) error {
|
||||
switch format {
|
||||
case FormatJSON:
|
||||
return showHostStdoutResultsJSON(collectName, results.AllCollectedData)
|
||||
case FormatRaw:
|
||||
return showHostStdoutResultsRaw(collectName, results.AllCollectedData)
|
||||
default:
|
||||
return errors.Errorf("unknown output format: %q", format)
|
||||
}
|
||||
}
|
||||
|
||||
func showRemoteStdoutResults(format string, collectName string, results *collect.RemoteCollectResult) error {
|
||||
switch format {
|
||||
case FormatJSON:
|
||||
return showRemoteStdoutResultsJSON(collectName, results.AllCollectedData)
|
||||
case FormatRaw:
|
||||
return errors.Errorf("raw format not supported for remote collectors")
|
||||
default:
|
||||
return errors.Errorf("unknown output format: %q", format)
|
||||
}
|
||||
}
|
||||
|
||||
func showHostStdoutResultsJSON(collectName string, results map[string][]byte) error {
|
||||
output := make(map[string]interface{})
|
||||
for file, collectorResult := range results {
|
||||
var collectedItems map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(collectorResult), &collectedItems); err != nil {
|
||||
return errors.Wrap(err, "failed to marshal collector results")
|
||||
}
|
||||
output[file] = collectedItems
|
||||
}
|
||||
|
||||
formatted, err := json.MarshalIndent(output, "", " ")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert output to json")
|
||||
}
|
||||
|
||||
fmt.Print(string(formatted))
|
||||
return nil
|
||||
}
|
||||
|
||||
// showHostStdoutResultsRaw outputs the collector output as a string of quoted json.
|
||||
func showHostStdoutResultsRaw(collectName string, results map[string][]byte) error {
|
||||
strData := map[string]string{}
|
||||
for k, v := range results {
|
||||
strData[k] = string(v)
|
||||
}
|
||||
formatted, err := json.MarshalIndent(strData, "", " ")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert output to json")
|
||||
}
|
||||
fmt.Print(string(formatted))
|
||||
return nil
|
||||
}
|
||||
|
||||
func showRemoteStdoutResultsJSON(collectName string, results map[string][]byte) error {
|
||||
type CollectorResult map[string]interface{}
|
||||
type NodeResult map[string]CollectorResult
|
||||
|
||||
var output = make(map[string]NodeResult)
|
||||
|
||||
for node, result := range results {
|
||||
var nodeResult map[string]string
|
||||
if err := json.Unmarshal(result, &nodeResult); err != nil {
|
||||
return errors.Wrap(err, "failed to marshal node results")
|
||||
}
|
||||
nr := make(NodeResult)
|
||||
for file, collectorResult := range nodeResult {
|
||||
var collectedItems map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(collectorResult), &collectedItems); err != nil {
|
||||
return errors.Wrap(err, "failed to marshal collector results")
|
||||
}
|
||||
nr[file] = collectedItems
|
||||
}
|
||||
output[node] = nr
|
||||
}
|
||||
|
||||
formatted, err := json.MarshalIndent(output, "", " ")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert output to json")
|
||||
}
|
||||
fmt.Print(string(formatted))
|
||||
return nil
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/replicatedhq/troubleshoot/cmd/collect/cli"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cli.InitAndExecute()
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
preflightcli "github.com/replicatedhq/troubleshoot/cmd/preflight/cli"
|
||||
troubleshootcli "github.com/replicatedhq/troubleshoot/cmd/troubleshoot/cli"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/spf13/cobra/doc"
|
||||
)
|
||||
|
||||
func RootCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "docsgen",
|
||||
Short: "Generate markdown docs for the commands in this project",
|
||||
}
|
||||
preflight := preflightcli.RootCmd()
|
||||
troubleshoot := troubleshootcli.RootCmd()
|
||||
commands := []*cobra.Command{preflight, troubleshoot}
|
||||
|
||||
for _, command := range commands {
|
||||
err := doc.GenMarkdownTree(command, "./docs")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func InitAndExecute() {
|
||||
if err := RootCmd().Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/replicatedhq/troubleshoot/cmd/docsgen/cli"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cli.InitAndExecute()
|
||||
}
|
||||
132
cmd/preflight/cli/convert.go
Normal file
132
cmd/preflight/cli/convert.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/convert"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func ConvertCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "convert [input-file]",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Short: "Convert v1beta2 preflight specs to v1beta3 format",
|
||||
Long: `Convert v1beta2 preflight specs to v1beta3 format with templating and values.
|
||||
|
||||
This command converts a v1beta2 preflight spec to the new v1beta3 templated format. It will:
|
||||
- Update the apiVersion to troubleshoot.sh/v1beta3
|
||||
- Extract hardcoded values and create a values.yaml file
|
||||
- Add conditional templating ({{- if .Values.feature.enabled }})
|
||||
- Add placeholder docString comments for you to fill in
|
||||
- Template hardcoded values with {{ .Values.* }} expressions
|
||||
|
||||
The conversion will create two files:
|
||||
- [input-file]-v1beta3.yaml: The templated v1beta3 spec
|
||||
- [input-file]-values.yaml: The values file with extracted configuration
|
||||
|
||||
Example:
|
||||
preflight convert my-preflight.yaml
|
||||
|
||||
This creates:
|
||||
my-preflight-v1beta3.yaml
|
||||
my-preflight-values.yaml`,
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
viper.BindPFlags(cmd.Flags())
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
v := viper.GetViper()
|
||||
|
||||
inputFile := args[0]
|
||||
outputSpec := v.GetString("output-spec")
|
||||
outputValues := v.GetString("output-values")
|
||||
|
||||
// Generate default output filenames if not specified
|
||||
if outputSpec == "" {
|
||||
ext := filepath.Ext(inputFile)
|
||||
base := strings.TrimSuffix(inputFile, ext)
|
||||
outputSpec = base + "-v1beta3" + ext
|
||||
}
|
||||
|
||||
if outputValues == "" {
|
||||
ext := filepath.Ext(inputFile)
|
||||
base := strings.TrimSuffix(inputFile, ext)
|
||||
outputValues = base + "-values" + ext
|
||||
}
|
||||
|
||||
return runConvert(v, inputFile, outputSpec, outputValues)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().String("output-spec", "", "Output file for the templated v1beta3 spec (default: [input]-v1beta3.yaml)")
|
||||
cmd.Flags().String("output-values", "", "Output file for the values (default: [input]-values.yaml)")
|
||||
cmd.Flags().Bool("dry-run", false, "Preview the conversion without writing files")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runConvert(v *viper.Viper, inputFile, outputSpec, outputValues string) error {
|
||||
// Read input file
|
||||
inputData, err := ioutil.ReadFile(inputFile)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to read input file %s", inputFile)
|
||||
}
|
||||
|
||||
// Check if it's a valid v1beta2 preflight spec
|
||||
if !strings.Contains(string(inputData), "troubleshoot.sh/v1beta2") {
|
||||
return fmt.Errorf("input file does not appear to be a v1beta2 troubleshoot spec")
|
||||
}
|
||||
|
||||
if !strings.Contains(string(inputData), "kind: Preflight") {
|
||||
return fmt.Errorf("input file does not appear to be a Preflight spec")
|
||||
}
|
||||
|
||||
// Convert to v1beta3
|
||||
result, err := convert.ConvertToV1Beta3(inputData)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert spec")
|
||||
}
|
||||
|
||||
dryRun := v.GetBool("dry-run")
|
||||
|
||||
if dryRun {
|
||||
fmt.Println("=== Templated v1beta3 Spec ===")
|
||||
fmt.Println(result.TemplatedSpec)
|
||||
fmt.Println("\n=== Values File ===")
|
||||
fmt.Println(result.ValuesFile)
|
||||
fmt.Println("\n=== Conversion Summary ===")
|
||||
fmt.Printf("Would write templated spec to: %s\n", outputSpec)
|
||||
fmt.Printf("Would write values to: %s\n", outputValues)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write templated spec
|
||||
err = ioutil.WriteFile(outputSpec, []byte(result.TemplatedSpec), 0644)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to write templated spec to %s", outputSpec)
|
||||
}
|
||||
|
||||
// Write values file
|
||||
err = ioutil.WriteFile(outputValues, []byte(result.ValuesFile), 0644)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to write values to %s", outputValues)
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully converted %s to v1beta3 format:\n", inputFile)
|
||||
fmt.Printf(" Templated spec: %s\n", outputSpec)
|
||||
fmt.Printf(" Values file: %s\n", outputValues)
|
||||
fmt.Println("\nNext steps:")
|
||||
fmt.Println("1. Add docStrings with Title, Requirement, and rationale for each check")
|
||||
fmt.Println("2. Customize the values in the values file")
|
||||
fmt.Println("3. Test the conversion with:")
|
||||
fmt.Printf(" preflight template %s --values %s\n", outputSpec, outputValues)
|
||||
fmt.Println("4. Run the templated preflight:")
|
||||
fmt.Printf(" preflight run %s --values %s\n", outputSpec, outputValues)
|
||||
|
||||
return nil
|
||||
}
|
||||
387
cmd/preflight/cli/docs.go
Normal file
387
cmd/preflight/cli/docs.go
Normal file
@@ -0,0 +1,387 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/preflight"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"gopkg.in/yaml.v2"
|
||||
"helm.sh/helm/v3/pkg/strvals"
|
||||
)
|
||||
|
||||
func DocsCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "docs [preflight-file...]",
|
||||
Short: "Extract and display documentation from a preflight spec",
|
||||
Long: `Extract all docString fields from enabled requirements in one or more preflight YAML files.
|
||||
This command processes templated preflight specs, evaluates conditionals, and outputs
|
||||
only the documentation for requirements that would be included based on the provided values.
|
||||
|
||||
Examples:
|
||||
# Extract docs with default values
|
||||
preflight docs ml-platform-preflight.yaml
|
||||
|
||||
# Extract docs from multiple specs with values from files
|
||||
preflight docs spec1.yaml spec2.yaml --values base-values.yaml --values prod-values.yaml
|
||||
|
||||
# Extract docs with inline values
|
||||
preflight docs ml-platform-preflight.yaml --set jupyter.enabled=true --set monitoring.enabled=false
|
||||
|
||||
# Extract docs and save to file
|
||||
preflight docs ml-platform-preflight.yaml --output requirements.md`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
v := viper.GetViper()
|
||||
|
||||
templateFiles := args
|
||||
valuesFiles := v.GetStringSlice("values")
|
||||
outputFile := v.GetString("output")
|
||||
setValues := v.GetStringSlice("set")
|
||||
|
||||
return extractDocs(templateFiles, valuesFiles, setValues, outputFile)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringSlice("values", []string{}, "Path to YAML files containing template values (can be used multiple times)")
|
||||
cmd.Flags().StringSlice("set", []string{}, "Set template values on the command line (can be used multiple times)")
|
||||
cmd.Flags().StringP("output", "o", "", "Output file (default: stdout)")
|
||||
|
||||
// Bind flags to viper
|
||||
viper.BindPFlag("values", cmd.Flags().Lookup("values"))
|
||||
viper.BindPFlag("set", cmd.Flags().Lookup("set"))
|
||||
viper.BindPFlag("output", cmd.Flags().Lookup("output"))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// PreflightDoc supports both legacy (requirements) and beta3 (spec.analyzers)
|
||||
type PreflightDoc struct {
|
||||
APIVersion string `yaml:"apiVersion"`
|
||||
Kind string `yaml:"kind"`
|
||||
Metadata map[string]interface{} `yaml:"metadata"`
|
||||
Spec struct {
|
||||
Analyzers []map[string]interface{} `yaml:"analyzers"`
|
||||
} `yaml:"spec"`
|
||||
// Legacy (pre-beta3 drafts)
|
||||
Requirements []Requirement `yaml:"requirements"`
|
||||
}
|
||||
|
||||
type Requirement struct {
|
||||
Name string `yaml:"name"`
|
||||
DocString string `yaml:"docString"`
|
||||
Checks []map[string]interface{} `yaml:"checks,omitempty"`
|
||||
}
|
||||
|
||||
func extractDocs(templateFiles []string, valuesFiles []string, setValues []string, outputFile string) error {
|
||||
// Prepare the values map (merge all files, then apply sets)
|
||||
values := make(map[string]interface{})
|
||||
|
||||
for _, valuesFile := range valuesFiles {
|
||||
fileValues, err := loadValuesFile(valuesFile)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to load values file %s", valuesFile)
|
||||
}
|
||||
values = mergeMaps(values, fileValues)
|
||||
}
|
||||
|
||||
// Normalize maps for Helm set merging
|
||||
values = normalizeStringMaps(values)
|
||||
|
||||
for _, setValue := range setValues {
|
||||
if err := applySetValue(values, setValue); err != nil {
|
||||
return errors.Wrapf(err, "failed to apply set value: %s", setValue)
|
||||
}
|
||||
}
|
||||
|
||||
var combinedDocs strings.Builder
|
||||
|
||||
for _, templateFile := range templateFiles {
|
||||
templateContent, err := os.ReadFile(templateFile)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to read template file %s", templateFile)
|
||||
}
|
||||
|
||||
useHelm := shouldUseHelmEngine(string(templateContent))
|
||||
var rendered string
|
||||
if useHelm {
|
||||
// Seed default-false for referenced boolean values to avoid nil map errors
|
||||
preflight.SeedDefaultBooleans(string(templateContent), values)
|
||||
rendered, err = preflight.RenderWithHelmTemplate(string(templateContent), values)
|
||||
if err != nil {
|
||||
execValues := legacyContext(values)
|
||||
rendered, err = renderTemplate(string(templateContent), execValues)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to render template (helm fallback also failed)")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
execValues := legacyContext(values)
|
||||
rendered, err = renderTemplate(string(templateContent), execValues)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to render template")
|
||||
}
|
||||
}
|
||||
|
||||
docs, err := extractDocStrings(rendered)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to extract documentation")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(docs) != "" {
|
||||
if combinedDocs.Len() > 0 {
|
||||
combinedDocs.WriteString("\n\n")
|
||||
}
|
||||
combinedDocs.WriteString(docs)
|
||||
}
|
||||
}
|
||||
|
||||
if outputFile != "" {
|
||||
if err := os.WriteFile(outputFile, []byte(combinedDocs.String()), 0644); err != nil {
|
||||
return errors.Wrapf(err, "failed to write output file %s", outputFile)
|
||||
}
|
||||
fmt.Printf("Documentation extracted successfully to %s\n", outputFile)
|
||||
} else {
|
||||
fmt.Print(combinedDocs.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func shouldUseHelmEngine(content string) bool {
|
||||
return strings.Contains(content, ".Values")
|
||||
}
|
||||
|
||||
func legacyContext(values map[string]interface{}) map[string]interface{} {
|
||||
ctx := make(map[string]interface{}, len(values)+1)
|
||||
for k, v := range values {
|
||||
ctx[k] = v
|
||||
}
|
||||
ctx["Values"] = values
|
||||
return ctx
|
||||
}
|
||||
|
||||
func normalizeStringMaps(v interface{}) map[string]interface{} {
|
||||
// Avoid unsafe type assertion; normalizeMap may return non-map types.
|
||||
if v == nil {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
normalized := normalizeMap(v)
|
||||
if m, ok := normalized.(map[string]interface{}); ok {
|
||||
return m
|
||||
}
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
|
||||
func normalizeMap(v interface{}) interface{} {
|
||||
switch t := v.(type) {
|
||||
case map[string]interface{}:
|
||||
m := make(map[string]interface{}, len(t))
|
||||
for k, val := range t {
|
||||
m[k] = normalizeMap(val)
|
||||
}
|
||||
return m
|
||||
case map[interface{}]interface{}:
|
||||
m := make(map[string]interface{}, len(t))
|
||||
for k, val := range t {
|
||||
key := fmt.Sprintf("%v", k)
|
||||
m[key] = normalizeMap(val)
|
||||
}
|
||||
return m
|
||||
case []interface{}:
|
||||
a := make([]interface{}, len(t))
|
||||
for i, val := range t {
|
||||
a[i] = normalizeMap(val)
|
||||
}
|
||||
return a
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func extractDocStrings(yamlContent string) (string, error) {
|
||||
var preflightDoc PreflightDoc
|
||||
if err := yaml.Unmarshal([]byte(yamlContent), &preflightDoc); err != nil {
|
||||
return "", errors.Wrap(err, "failed to parse YAML")
|
||||
}
|
||||
|
||||
var docs strings.Builder
|
||||
first := true
|
||||
|
||||
// Prefer beta3 analyzers docStrings
|
||||
if len(preflightDoc.Spec.Analyzers) > 0 {
|
||||
for _, analyzer := range preflightDoc.Spec.Analyzers {
|
||||
if raw, ok := analyzer["docString"]; ok {
|
||||
text, _ := raw.(string)
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
if !first {
|
||||
docs.WriteString("\n\n")
|
||||
}
|
||||
first = false
|
||||
writeMarkdownSection(&docs, text, "")
|
||||
}
|
||||
}
|
||||
return docs.String(), nil
|
||||
}
|
||||
|
||||
// Fallback: legacy requirements with docString
|
||||
for _, req := range preflightDoc.Requirements {
|
||||
if strings.TrimSpace(req.DocString) == "" {
|
||||
continue
|
||||
}
|
||||
if !first {
|
||||
docs.WriteString("\n\n")
|
||||
}
|
||||
first = false
|
||||
writeMarkdownSection(&docs, req.DocString, req.Name)
|
||||
}
|
||||
|
||||
return docs.String(), nil
|
||||
}
|
||||
|
||||
// writeMarkdownSection prints a heading from Title: or name, then the rest
|
||||
func writeMarkdownSection(b *strings.Builder, docString string, fallbackName string) {
|
||||
lines := strings.Split(docString, "\n")
|
||||
title := strings.TrimSpace(fallbackName)
|
||||
contentStart := 0
|
||||
for i, line := range lines {
|
||||
trim := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trim, "Title:") {
|
||||
parts := strings.SplitN(trim, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
t := strings.TrimSpace(parts[1])
|
||||
if t != "" {
|
||||
title = t
|
||||
}
|
||||
}
|
||||
contentStart = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
if title != "" {
|
||||
b.WriteString("### ")
|
||||
b.WriteString(title)
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
remaining := strings.Join(lines[contentStart:], "\n")
|
||||
remaining = strings.TrimSpace(remaining)
|
||||
if remaining != "" {
|
||||
b.WriteString(remaining)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// loadValuesFile loads values from a YAML file
|
||||
func loadValuesFile(filename string) (map[string]interface{}, error) {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var values map[string]interface{}
|
||||
if err := yaml.Unmarshal(data, &values); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse values file as YAML")
|
||||
}
|
||||
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// applySetValue applies a single --set value to the values map (Helm semantics)
|
||||
func applySetValue(values map[string]interface{}, setValue string) error {
|
||||
if idx := strings.Index(setValue, "="); idx > 0 {
|
||||
key := setValue[:idx]
|
||||
val := setValue[idx+1:]
|
||||
if strings.HasPrefix(key, "Values.") {
|
||||
key = strings.TrimPrefix(key, "Values.")
|
||||
setValue = key + "=" + val
|
||||
}
|
||||
}
|
||||
if err := strvals.ParseInto(setValue, values); err != nil {
|
||||
return fmt.Errorf("parsing --set: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setNestedValue sets a value in a nested map structure
|
||||
func setNestedValue(m map[string]interface{}, keys []string, value interface{}) {
|
||||
if len(keys) == 0 {
|
||||
return
|
||||
}
|
||||
if len(keys) == 1 {
|
||||
m[keys[0]] = value
|
||||
return
|
||||
}
|
||||
if _, ok := m[keys[0]]; !ok {
|
||||
m[keys[0]] = make(map[string]interface{})
|
||||
}
|
||||
if nextMap, ok := m[keys[0]].(map[string]interface{}); ok {
|
||||
setNestedValue(nextMap, keys[1:], value)
|
||||
} else {
|
||||
m[keys[0]] = make(map[string]interface{})
|
||||
setNestedValue(m[keys[0]].(map[string]interface{}), keys[1:], value)
|
||||
}
|
||||
}
|
||||
|
||||
func mergeMaps(base, overlay map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
for k, v := range base {
|
||||
result[k] = v
|
||||
}
|
||||
for k, v := range overlay {
|
||||
if baseVal, exists := result[k]; exists {
|
||||
if baseMap, ok := baseVal.(map[string]interface{}); ok {
|
||||
if overlayMap, ok := v.(map[string]interface{}); ok {
|
||||
result[k] = mergeMaps(baseMap, overlayMap)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func renderTemplate(templateContent string, values map[string]interface{}) (string, error) {
|
||||
tmpl := template.New("preflight").Funcs(sprig.FuncMap())
|
||||
tmpl, err := tmpl.Parse(templateContent)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to parse template")
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, values); err != nil {
|
||||
return "", errors.Wrap(err, "failed to execute template")
|
||||
}
|
||||
result := cleanRenderedYAML(buf.String())
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func cleanRenderedYAML(content string) string {
|
||||
lines := strings.Split(content, "\n")
|
||||
var cleaned []string
|
||||
var lastWasEmpty bool
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimRight(line, " \t")
|
||||
if trimmed == "" {
|
||||
if !lastWasEmpty {
|
||||
cleaned = append(cleaned, "")
|
||||
lastWasEmpty = true
|
||||
}
|
||||
} else {
|
||||
cleaned = append(cleaned, trimmed)
|
||||
lastWasEmpty = false
|
||||
}
|
||||
}
|
||||
for len(cleaned) > 0 && cleaned[len(cleaned)-1] == "" {
|
||||
cleaned = cleaned[:len(cleaned)-1]
|
||||
}
|
||||
return strings.Join(cleaned, "\n") + "\n"
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/replicatedhq/troubleshoot/pkg/logger"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/preflight"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/types"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/updater"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"k8s.io/klog/v2"
|
||||
@@ -37,6 +38,25 @@ that a cluster meets the requirements to run an application.`,
|
||||
if err := util.StartProfiling(); err != nil {
|
||||
klog.Errorf("Failed to start profiling: %v", err)
|
||||
}
|
||||
|
||||
// Auto-update preflight unless disabled by flag or env
|
||||
envAuto := os.Getenv("PREFLIGHT_AUTO_UPDATE")
|
||||
autoFromEnv := true
|
||||
if envAuto != "" {
|
||||
if strings.EqualFold(envAuto, "0") || strings.EqualFold(envAuto, "false") {
|
||||
autoFromEnv = false
|
||||
}
|
||||
}
|
||||
if v.GetBool("auto-update") && autoFromEnv {
|
||||
exe, err := os.Executable()
|
||||
if err == nil {
|
||||
_ = updater.CheckAndUpdate(cmd.Context(), updater.Options{
|
||||
BinaryName: "preflight",
|
||||
CurrentPath: exe,
|
||||
Printf: func(f string, a ...interface{}) { fmt.Fprintf(os.Stderr, f, a...) },
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
v := viper.GetViper()
|
||||
@@ -66,12 +86,21 @@ that a cluster meets the requirements to run an application.`,
|
||||
|
||||
cmd.AddCommand(util.VersionCmd())
|
||||
cmd.AddCommand(OciFetchCmd())
|
||||
cmd.AddCommand(TemplateCmd())
|
||||
cmd.AddCommand(DocsCmd())
|
||||
cmd.AddCommand(ConvertCmd())
|
||||
|
||||
preflight.AddFlags(cmd.PersistentFlags())
|
||||
|
||||
// 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`")
|
||||
cmd.Flags().Bool("auto-update", true, "enable automatic binary self-update check and install")
|
||||
|
||||
// Template values for v1beta3 specs
|
||||
cmd.Flags().StringSlice("values", []string{}, "Path to YAML files containing template values for v1beta3 specs (can be used multiple times)")
|
||||
cmd.Flags().StringSlice("set", []string{}, "Set template values on the command line for v1beta3 specs (can be used multiple times)")
|
||||
|
||||
k8sutil.AddFlags(cmd.Flags())
|
||||
|
||||
|
||||
42
cmd/preflight/cli/template.go
Normal file
42
cmd/preflight/cli/template.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/replicatedhq/troubleshoot/pkg/preflight"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TemplateCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "template [template-file]",
|
||||
Short: "Render a templated preflight spec with values",
|
||||
Long: `Process a templated preflight YAML file, substituting variables and removing conditional sections based on provided values.
|
||||
|
||||
Examples:
|
||||
# Render template with default values
|
||||
preflight template sample-preflight-templated.yaml
|
||||
|
||||
# Render template with values from files
|
||||
preflight template sample-preflight-templated.yaml --values values-base.yaml --values values-prod.yaml
|
||||
|
||||
# Render template with inline values
|
||||
preflight template sample-preflight-templated.yaml --set postgres.enabled=true --set cluster.minNodes=5
|
||||
|
||||
# Render template and save to file
|
||||
preflight template sample-preflight-templated.yaml --output rendered.yaml`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
templateFile := args[0]
|
||||
valuesFiles, _ := cmd.Flags().GetStringSlice("values")
|
||||
outputFile, _ := cmd.Flags().GetString("output")
|
||||
setValues, _ := cmd.Flags().GetStringSlice("set")
|
||||
|
||||
return preflight.RunTemplate(templateFile, valuesFiles, setValues, outputFile)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringSlice("values", []string{}, "Path to YAML files containing template values (can be used multiple times)")
|
||||
cmd.Flags().StringSlice("set", []string{}, "Set template values on the command line (can be used multiple times)")
|
||||
cmd.Flags().StringP("output", "o", "", "Output file (default: stdout)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
extensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
extensionsscheme "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
)
|
||||
|
||||
func RootCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "schemagen",
|
||||
Short: "Generate openapischemas for the kinds in this project",
|
||||
SilenceUsage: true,
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
viper.BindPFlags(cmd.Flags())
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
v := viper.GetViper()
|
||||
|
||||
return generateSchemas(v)
|
||||
},
|
||||
}
|
||||
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
cmd.Flags().String("output-dir", "./schemas", "directory to save the schemas in")
|
||||
|
||||
viper.BindPFlags(cmd.Flags())
|
||||
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func InitAndExecute() {
|
||||
if err := RootCmd().Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
viper.SetEnvPrefix("TROUBLESHOOT")
|
||||
viper.AutomaticEnv()
|
||||
}
|
||||
|
||||
func generateSchemas(v *viper.Viper) error {
|
||||
// we generate schemas from the config/crds in the root of this project
|
||||
// those crds can be created from controller-gen or by running `make openapischema`
|
||||
|
||||
workdir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get workdir")
|
||||
}
|
||||
|
||||
files := []struct {
|
||||
inFilename string
|
||||
outFilename string
|
||||
}{
|
||||
{
|
||||
"troubleshoot.replicated.com_preflights.yaml",
|
||||
"preflight-troubleshoot-v1beta1.json",
|
||||
},
|
||||
{
|
||||
"troubleshoot.replicated.com_analyzers.yaml",
|
||||
"analyzer-troubleshoot-v1beta1.json",
|
||||
},
|
||||
{
|
||||
"troubleshoot.replicated.com_collectors.yaml",
|
||||
"collector-troubleshoot-v1beta1.json",
|
||||
},
|
||||
{
|
||||
"troubleshoot.replicated.com_redactors.yaml",
|
||||
"redactor-troubleshoot-v1beta1.json",
|
||||
},
|
||||
{
|
||||
"troubleshoot.replicated.com_supportbundles.yaml",
|
||||
"supportbundle-troubleshoot-v1beta1.json",
|
||||
},
|
||||
{
|
||||
"troubleshoot.sh_analyzers.yaml",
|
||||
"analyzer-troubleshoot-v1beta2.json",
|
||||
},
|
||||
{
|
||||
"troubleshoot.sh_collectors.yaml",
|
||||
"collector-troubleshoot-v1beta2.json",
|
||||
},
|
||||
{
|
||||
"troubleshoot.sh_preflights.yaml",
|
||||
"preflight-troubleshoot-v1beta2.json",
|
||||
},
|
||||
{
|
||||
"troubleshoot.sh_redactors.yaml",
|
||||
"redactor-troubleshoot-v1beta2.json",
|
||||
},
|
||||
{
|
||||
"troubleshoot.sh_supportbundles.yaml",
|
||||
"supportbundle-troubleshoot-v1beta2.json",
|
||||
},
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
contents, err := ioutil.ReadFile(filepath.Join(workdir, "config", "crds", file.inFilename))
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to read crd from %s", file.inFilename)
|
||||
}
|
||||
if err := generateSchemaFromCRD(contents, filepath.Join(workdir, v.GetString("output-dir"), file.outFilename)); err != nil {
|
||||
return errors.Wrapf(err, "failed to write crd schema to %s", file.outFilename)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateSchemaFromCRD(crd []byte, outfile string) error {
|
||||
extensionsscheme.AddToScheme(scheme.Scheme)
|
||||
decode := scheme.Codecs.UniversalDeserializer().Decode
|
||||
obj, _, err := decode(crd, nil, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to decode crd")
|
||||
}
|
||||
|
||||
customResourceDefinition := obj.(*extensionsv1.CustomResourceDefinition)
|
||||
|
||||
if len(customResourceDefinition.Spec.Versions) == 0 {
|
||||
return errors.New("no versions found for CRD")
|
||||
}
|
||||
|
||||
crdSchema := customResourceDefinition.Spec.Versions[0].Schema
|
||||
if crdSchema == nil {
|
||||
return errors.New("CRD has a nil schema")
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(crdSchema.OpenAPIV3Schema, "", " ")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to marshal json")
|
||||
}
|
||||
|
||||
_, err = os.Stat(outfile)
|
||||
if err == nil {
|
||||
if err := os.Remove(outfile); err != nil {
|
||||
return errors.Wrap(err, "failed to remove file")
|
||||
}
|
||||
}
|
||||
|
||||
d, _ := path.Split(outfile)
|
||||
_, err = os.Stat(d)
|
||||
if os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(d, 0755); err != nil {
|
||||
return errors.Wrap(err, "failed to mkdir")
|
||||
}
|
||||
}
|
||||
|
||||
// whoa now
|
||||
// working around the fact that controller-gen doesn't have tags to generate oneOf schemas, so this is hacky.
|
||||
// going to work to add an issue there to support and if they accept, this terrible thing can go away
|
||||
boolStringed := strings.ReplaceAll(string(b), `"type": "BoolString"`, `"oneOf": [{"type": "string"},{"type": "boolean"}]`)
|
||||
|
||||
err = ioutil.WriteFile(outfile, []byte(boolStringed), 0644)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to write file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/replicatedhq/troubleshoot/cmd/schemagen/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cli.InitAndExecute()
|
||||
}
|
||||
372
cmd/troubleshoot/cli/auto_discovery.go
Normal file
372
cmd/troubleshoot/cli/auto_discovery.go
Normal file
@@ -0,0 +1,372 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect/autodiscovery"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect/images"
|
||||
"github.com/spf13/viper"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// AutoDiscoveryConfig contains configuration for auto-discovery
|
||||
type AutoDiscoveryConfig struct {
|
||||
Enabled bool
|
||||
IncludeImages bool
|
||||
RBACCheck bool
|
||||
Profile string
|
||||
ExcludeNamespaces []string
|
||||
IncludeNamespaces []string
|
||||
IncludeSystemNamespaces bool
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// DiscoveryProfile defines different levels of auto-discovery
|
||||
type DiscoveryProfile struct {
|
||||
Name string
|
||||
Description string
|
||||
IncludeImages bool
|
||||
RBACCheck bool
|
||||
MaxDepth int
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// GetAutoDiscoveryConfig extracts auto-discovery configuration from viper
|
||||
func GetAutoDiscoveryConfig(v *viper.Viper) AutoDiscoveryConfig {
|
||||
return AutoDiscoveryConfig{
|
||||
Enabled: v.GetBool("auto"),
|
||||
IncludeImages: v.GetBool("include-images"),
|
||||
RBACCheck: v.GetBool("rbac-check"),
|
||||
Profile: v.GetString("discovery-profile"),
|
||||
ExcludeNamespaces: v.GetStringSlice("exclude-namespaces"),
|
||||
IncludeNamespaces: v.GetStringSlice("include-namespaces"),
|
||||
IncludeSystemNamespaces: v.GetBool("include-system-namespaces"),
|
||||
Timeout: 30 * time.Second, // Default timeout
|
||||
}
|
||||
}
|
||||
|
||||
// GetDiscoveryProfiles returns available discovery profiles
|
||||
func GetDiscoveryProfiles() map[string]DiscoveryProfile {
|
||||
return map[string]DiscoveryProfile{
|
||||
"minimal": {
|
||||
Name: "minimal",
|
||||
Description: "Minimal collection: cluster info, basic logs",
|
||||
IncludeImages: false,
|
||||
RBACCheck: true,
|
||||
MaxDepth: 1,
|
||||
Timeout: 15 * time.Second,
|
||||
},
|
||||
"standard": {
|
||||
Name: "standard",
|
||||
Description: "Standard collection: logs, configs, secrets, events",
|
||||
IncludeImages: false,
|
||||
RBACCheck: true,
|
||||
MaxDepth: 2,
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
"comprehensive": {
|
||||
Name: "comprehensive",
|
||||
Description: "Comprehensive collection: everything + image metadata",
|
||||
IncludeImages: true,
|
||||
RBACCheck: true,
|
||||
MaxDepth: 3,
|
||||
Timeout: 60 * time.Second,
|
||||
},
|
||||
"paranoid": {
|
||||
Name: "paranoid",
|
||||
Description: "Paranoid collection: maximum data with extended timeouts",
|
||||
IncludeImages: true,
|
||||
RBACCheck: true,
|
||||
MaxDepth: 5,
|
||||
Timeout: 120 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyAutoDiscovery applies auto-discovery to the support bundle spec
|
||||
func ApplyAutoDiscovery(ctx context.Context, client kubernetes.Interface, restConfig *rest.Config,
|
||||
mainBundle *troubleshootv1beta2.SupportBundle, config AutoDiscoveryConfig, namespace string) error {
|
||||
|
||||
if !config.Enabled {
|
||||
return nil // Auto-discovery not enabled
|
||||
}
|
||||
|
||||
klog.V(2).Infof("Applying auto-discovery with profile: %s", config.Profile)
|
||||
|
||||
// Get discovery profile
|
||||
profiles := GetDiscoveryProfiles()
|
||||
profile, exists := profiles[config.Profile]
|
||||
if !exists {
|
||||
klog.Warningf("Unknown discovery profile '%s', using 'standard'", config.Profile)
|
||||
profile = profiles["standard"]
|
||||
}
|
||||
|
||||
// Override profile settings with explicit flags
|
||||
if config.IncludeImages {
|
||||
profile.IncludeImages = true
|
||||
}
|
||||
if config.Timeout > 0 {
|
||||
profile.Timeout = config.Timeout
|
||||
}
|
||||
|
||||
// Create auto-discovery options
|
||||
discoveryOpts := autodiscovery.DiscoveryOptions{
|
||||
IncludeImages: profile.IncludeImages,
|
||||
RBACCheck: config.RBACCheck,
|
||||
MaxDepth: profile.MaxDepth,
|
||||
Timeout: profile.Timeout,
|
||||
}
|
||||
|
||||
// Handle namespace filtering
|
||||
if namespace != "" {
|
||||
discoveryOpts.Namespaces = []string{namespace}
|
||||
} else {
|
||||
// Use include/exclude patterns if specified
|
||||
if len(config.IncludeNamespaces) > 0 || len(config.ExcludeNamespaces) > 0 {
|
||||
// Create namespace scanner to resolve include/exclude patterns
|
||||
nsScanner := autodiscovery.NewNamespaceScanner(client)
|
||||
scanOpts := autodiscovery.ScanOptions{
|
||||
IncludePatterns: config.IncludeNamespaces,
|
||||
ExcludePatterns: config.ExcludeNamespaces,
|
||||
IncludeSystemNamespaces: config.IncludeSystemNamespaces,
|
||||
}
|
||||
|
||||
targetNamespaces, err := nsScanner.GetTargetNamespaces(ctx, nil, scanOpts)
|
||||
if err != nil {
|
||||
klog.Warningf("Failed to resolve namespace patterns, using all accessible namespaces: %v", err)
|
||||
// Continue with empty namespace list (all namespaces)
|
||||
} else {
|
||||
discoveryOpts.Namespaces = targetNamespaces
|
||||
klog.V(2).Infof("Resolved namespace patterns to %d namespaces: %v", len(targetNamespaces), targetNamespaces)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create autodiscovery instance
|
||||
discoverer, err := autodiscovery.NewDiscoverer(restConfig, client)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create auto-discoverer")
|
||||
}
|
||||
|
||||
// Check if we have existing YAML collectors (Path 2) or just auto-discovery (Path 1)
|
||||
hasYAMLCollectors := len(mainBundle.Spec.Collectors) > 0
|
||||
|
||||
var autoCollectors []autodiscovery.CollectorSpec
|
||||
|
||||
if hasYAMLCollectors {
|
||||
// Path 2: Augment existing YAML collectors with foundational collectors
|
||||
klog.V(2).Info("Auto-discovery: Augmenting YAML collectors with foundational collectors (Path 2)")
|
||||
|
||||
// Convert existing collectors to autodiscovery format
|
||||
yamlCollectors, err := convertToCollectorSpecs(mainBundle.Spec.Collectors)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert YAML collectors")
|
||||
}
|
||||
|
||||
discoveryOpts.AugmentMode = true
|
||||
autoCollectors, err = discoverer.AugmentWithFoundational(ctx, yamlCollectors, discoveryOpts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to augment with foundational collectors")
|
||||
}
|
||||
} else {
|
||||
// Path 1: Pure foundational discovery
|
||||
klog.V(2).Info("Auto-discovery: Collecting foundational data only (Path 1)")
|
||||
|
||||
discoveryOpts.FoundationalOnly = true
|
||||
autoCollectors, err = discoverer.DiscoverFoundational(ctx, discoveryOpts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to discover foundational collectors")
|
||||
}
|
||||
}
|
||||
|
||||
// Convert auto-discovered collectors back to troubleshoot specs
|
||||
troubleshootCollectors, err := convertToTroubleshootCollectors(autoCollectors)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert auto-discovered collectors")
|
||||
}
|
||||
|
||||
// Update the support bundle spec
|
||||
if hasYAMLCollectors {
|
||||
// Replace existing collectors with augmented set
|
||||
mainBundle.Spec.Collectors = troubleshootCollectors
|
||||
} else {
|
||||
// Set foundational collectors
|
||||
mainBundle.Spec.Collectors = troubleshootCollectors
|
||||
}
|
||||
|
||||
klog.V(2).Infof("Auto-discovery complete: %d collectors configured", len(troubleshootCollectors))
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertToCollectorSpecs converts troubleshootv1beta2.Collect to autodiscovery.CollectorSpec
|
||||
func convertToCollectorSpecs(collectors []*troubleshootv1beta2.Collect) ([]autodiscovery.CollectorSpec, error) {
|
||||
var specs []autodiscovery.CollectorSpec
|
||||
|
||||
for i, collect := range collectors {
|
||||
// Determine collector type and extract relevant information
|
||||
spec := autodiscovery.CollectorSpec{
|
||||
Priority: 100, // High priority for YAML specs
|
||||
Source: autodiscovery.SourceYAML,
|
||||
}
|
||||
|
||||
// Map troubleshoot collectors to autodiscovery types
|
||||
switch {
|
||||
case collect.Logs != nil:
|
||||
spec.Type = autodiscovery.CollectorTypeLogs
|
||||
spec.Name = fmt.Sprintf("yaml-logs-%d", i)
|
||||
spec.Namespace = collect.Logs.Namespace
|
||||
spec.Spec = collect.Logs
|
||||
case collect.ConfigMap != nil:
|
||||
spec.Type = autodiscovery.CollectorTypeConfigMaps
|
||||
spec.Name = fmt.Sprintf("yaml-configmap-%d", i)
|
||||
spec.Namespace = collect.ConfigMap.Namespace
|
||||
spec.Spec = collect.ConfigMap
|
||||
case collect.Secret != nil:
|
||||
spec.Type = autodiscovery.CollectorTypeSecrets
|
||||
spec.Name = fmt.Sprintf("yaml-secret-%d", i)
|
||||
spec.Namespace = collect.Secret.Namespace
|
||||
spec.Spec = collect.Secret
|
||||
case collect.ClusterInfo != nil:
|
||||
spec.Type = autodiscovery.CollectorTypeClusterInfo
|
||||
spec.Name = fmt.Sprintf("yaml-clusterinfo-%d", i)
|
||||
spec.Spec = collect.ClusterInfo
|
||||
case collect.ClusterResources != nil:
|
||||
spec.Type = autodiscovery.CollectorTypeClusterResources
|
||||
spec.Name = fmt.Sprintf("yaml-clusterresources-%d", i)
|
||||
spec.Spec = collect.ClusterResources
|
||||
default:
|
||||
// For other collector types, create a generic spec
|
||||
spec.Type = "other"
|
||||
spec.Name = fmt.Sprintf("yaml-other-%d", i)
|
||||
spec.Spec = collect
|
||||
}
|
||||
|
||||
specs = append(specs, spec)
|
||||
}
|
||||
|
||||
return specs, nil
|
||||
}
|
||||
|
||||
// convertToTroubleshootCollectors converts autodiscovery.CollectorSpec to troubleshootv1beta2.Collect
|
||||
func convertToTroubleshootCollectors(collectors []autodiscovery.CollectorSpec) ([]*troubleshootv1beta2.Collect, error) {
|
||||
var troubleshootCollectors []*troubleshootv1beta2.Collect
|
||||
|
||||
for _, spec := range collectors {
|
||||
collect, err := spec.ToTroubleshootCollect()
|
||||
if err != nil {
|
||||
klog.Warningf("Failed to convert collector spec %s: %v", spec.Name, err)
|
||||
continue
|
||||
}
|
||||
troubleshootCollectors = append(troubleshootCollectors, collect)
|
||||
}
|
||||
|
||||
return troubleshootCollectors, nil
|
||||
}
|
||||
|
||||
// ValidateAutoDiscoveryFlags validates auto-discovery flag combinations
|
||||
func ValidateAutoDiscoveryFlags(v *viper.Viper) error {
|
||||
// If include-images is used without auto, it's an error
|
||||
if v.GetBool("include-images") && !v.GetBool("auto") {
|
||||
return errors.New("--include-images flag requires --auto flag to be enabled")
|
||||
}
|
||||
|
||||
// Validate discovery profile
|
||||
profile := v.GetString("discovery-profile")
|
||||
profiles := GetDiscoveryProfiles()
|
||||
if _, exists := profiles[profile]; !exists {
|
||||
return fmt.Errorf("unknown discovery profile: %s. Available profiles: minimal, standard, comprehensive, paranoid", profile)
|
||||
}
|
||||
|
||||
// Validate namespace patterns
|
||||
includeNS := v.GetStringSlice("include-namespaces")
|
||||
excludeNS := v.GetStringSlice("exclude-namespaces")
|
||||
|
||||
if len(includeNS) > 0 && len(excludeNS) > 0 {
|
||||
klog.Warning("Both include-namespaces and exclude-namespaces specified. Include patterns take precedence")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShouldUseAutoDiscovery determines if auto-discovery should be used
|
||||
func ShouldUseAutoDiscovery(v *viper.Viper, args []string) bool {
|
||||
// Auto-discovery is enabled by the --auto flag
|
||||
autoEnabled := v.GetBool("auto")
|
||||
|
||||
if !autoEnabled {
|
||||
return false
|
||||
}
|
||||
|
||||
// Auto-discovery can be used with or without YAML specs
|
||||
return true
|
||||
}
|
||||
|
||||
// GetAutoDiscoveryMode returns the auto-discovery mode based on arguments
|
||||
func GetAutoDiscoveryMode(args []string, autoEnabled bool) string {
|
||||
if !autoEnabled {
|
||||
return "disabled"
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
return "foundational-only" // Path 1
|
||||
}
|
||||
|
||||
return "yaml-augmented" // Path 2
|
||||
}
|
||||
|
||||
// CreateImageCollectionOptions creates image collection options from CLI config
|
||||
func CreateImageCollectionOptions(config AutoDiscoveryConfig) images.CollectionOptions {
|
||||
options := images.GetDefaultCollectionOptions()
|
||||
|
||||
// Configure based on profile and flags
|
||||
profiles := GetDiscoveryProfiles()
|
||||
if profile, exists := profiles[config.Profile]; exists {
|
||||
options.Timeout = profile.Timeout
|
||||
options.IncludeConfig = profile.Name == "comprehensive" || profile.Name == "paranoid"
|
||||
options.IncludeLayers = profile.Name == "paranoid"
|
||||
}
|
||||
|
||||
// Override based on explicit flags
|
||||
if config.Timeout > 0 {
|
||||
options.Timeout = config.Timeout
|
||||
}
|
||||
|
||||
// For auto-discovery, always continue on error to maximize collection
|
||||
options.ContinueOnError = true
|
||||
options.EnableCache = true
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// PrintAutoDiscoveryInfo prints information about auto-discovery configuration
|
||||
func PrintAutoDiscoveryInfo(config AutoDiscoveryConfig, mode string) {
|
||||
if !config.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Auto-discovery enabled (mode: %s, profile: %s)\n", mode, config.Profile)
|
||||
|
||||
if config.IncludeImages {
|
||||
fmt.Println(" - Container image metadata collection enabled")
|
||||
}
|
||||
|
||||
if len(config.IncludeNamespaces) > 0 {
|
||||
fmt.Printf(" - Including namespaces: %v\n", config.IncludeNamespaces)
|
||||
}
|
||||
|
||||
if len(config.ExcludeNamespaces) > 0 {
|
||||
fmt.Printf(" - Excluding namespaces: %v\n", config.ExcludeNamespaces)
|
||||
}
|
||||
|
||||
if config.IncludeSystemNamespaces {
|
||||
fmt.Println(" - System namespaces included")
|
||||
}
|
||||
|
||||
fmt.Printf(" - RBAC checking: %t\n", config.RBACCheck)
|
||||
}
|
||||
389
cmd/troubleshoot/cli/auto_discovery_test.go
Normal file
389
cmd/troubleshoot/cli/auto_discovery_test.go
Normal file
@@ -0,0 +1,389 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect/autodiscovery"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect/images"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func TestGetAutoDiscoveryConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
viperSetup func(*viper.Viper)
|
||||
wantEnabled bool
|
||||
wantImages bool
|
||||
wantRBAC bool
|
||||
wantProfile string
|
||||
}{
|
||||
{
|
||||
name: "default config",
|
||||
viperSetup: func(v *viper.Viper) {
|
||||
// No flags set, should use defaults
|
||||
},
|
||||
wantEnabled: false,
|
||||
wantImages: false,
|
||||
wantRBAC: true, // Default is true
|
||||
wantProfile: "standard",
|
||||
},
|
||||
{
|
||||
name: "auto enabled",
|
||||
viperSetup: func(v *viper.Viper) {
|
||||
v.Set("auto", true)
|
||||
},
|
||||
wantEnabled: true,
|
||||
wantImages: false,
|
||||
wantRBAC: true,
|
||||
wantProfile: "standard",
|
||||
},
|
||||
{
|
||||
name: "auto with images",
|
||||
viperSetup: func(v *viper.Viper) {
|
||||
v.Set("auto", true)
|
||||
v.Set("include-images", true)
|
||||
},
|
||||
wantEnabled: true,
|
||||
wantImages: true,
|
||||
wantRBAC: true,
|
||||
wantProfile: "standard",
|
||||
},
|
||||
{
|
||||
name: "comprehensive profile",
|
||||
viperSetup: func(v *viper.Viper) {
|
||||
v.Set("auto", true)
|
||||
v.Set("discovery-profile", "comprehensive")
|
||||
},
|
||||
wantEnabled: true,
|
||||
wantImages: false,
|
||||
wantRBAC: true,
|
||||
wantProfile: "comprehensive",
|
||||
},
|
||||
{
|
||||
name: "rbac disabled",
|
||||
viperSetup: func(v *viper.Viper) {
|
||||
v.Set("auto", true)
|
||||
v.Set("rbac-check", false)
|
||||
},
|
||||
wantEnabled: true,
|
||||
wantImages: false,
|
||||
wantRBAC: false,
|
||||
wantProfile: "standard",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
v := viper.New()
|
||||
|
||||
// Set defaults
|
||||
v.SetDefault("rbac-check", true)
|
||||
v.SetDefault("discovery-profile", "standard")
|
||||
|
||||
// Apply test-specific setup
|
||||
tt.viperSetup(v)
|
||||
|
||||
config := GetAutoDiscoveryConfig(v)
|
||||
|
||||
if config.Enabled != tt.wantEnabled {
|
||||
t.Errorf("GetAutoDiscoveryConfig() enabled = %v, want %v", config.Enabled, tt.wantEnabled)
|
||||
}
|
||||
if config.IncludeImages != tt.wantImages {
|
||||
t.Errorf("GetAutoDiscoveryConfig() includeImages = %v, want %v", config.IncludeImages, tt.wantImages)
|
||||
}
|
||||
if config.RBACCheck != tt.wantRBAC {
|
||||
t.Errorf("GetAutoDiscoveryConfig() rbacCheck = %v, want %v", config.RBACCheck, tt.wantRBAC)
|
||||
}
|
||||
if config.Profile != tt.wantProfile {
|
||||
t.Errorf("GetAutoDiscoveryConfig() profile = %v, want %v", config.Profile, tt.wantProfile)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDiscoveryProfiles(t *testing.T) {
|
||||
profiles := GetDiscoveryProfiles()
|
||||
|
||||
requiredProfiles := []string{"minimal", "standard", "comprehensive", "paranoid"}
|
||||
for _, profileName := range requiredProfiles {
|
||||
if profile, exists := profiles[profileName]; !exists {
|
||||
t.Errorf("Missing required discovery profile: %s", profileName)
|
||||
} else {
|
||||
if profile.Name != profileName {
|
||||
t.Errorf("Profile %s has wrong name: %s", profileName, profile.Name)
|
||||
}
|
||||
if profile.Description == "" {
|
||||
t.Errorf("Profile %s missing description", profileName)
|
||||
}
|
||||
if profile.Timeout <= 0 {
|
||||
t.Errorf("Profile %s has invalid timeout: %v", profileName, profile.Timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check profile progression (more features as we go up)
|
||||
if profiles["comprehensive"].IncludeImages && !profiles["paranoid"].IncludeImages {
|
||||
t.Error("Paranoid profile should include at least everything comprehensive does")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAutoDiscoveryFlags(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
viperSetup func(*viper.Viper)
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid auto discovery",
|
||||
viperSetup: func(v *viper.Viper) {
|
||||
v.Set("auto", true)
|
||||
v.Set("include-images", true)
|
||||
v.Set("discovery-profile", "standard")
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "include-images without auto",
|
||||
viperSetup: func(v *viper.Viper) {
|
||||
v.Set("auto", false)
|
||||
v.Set("include-images", true)
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid discovery profile",
|
||||
viperSetup: func(v *viper.Viper) {
|
||||
v.Set("auto", true)
|
||||
v.Set("discovery-profile", "invalid-profile")
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no auto discovery",
|
||||
viperSetup: func(v *viper.Viper) {
|
||||
v.Set("auto", false)
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "both include and exclude namespaces",
|
||||
viperSetup: func(v *viper.Viper) {
|
||||
v.Set("auto", true)
|
||||
v.Set("include-namespaces", []string{"app1"})
|
||||
v.Set("exclude-namespaces", []string{"system"})
|
||||
},
|
||||
wantErr: false, // Should warn but not error
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
v := viper.New()
|
||||
|
||||
// Set defaults
|
||||
v.SetDefault("rbac-check", true)
|
||||
v.SetDefault("discovery-profile", "standard")
|
||||
|
||||
// Apply test setup
|
||||
tt.viperSetup(v)
|
||||
|
||||
err := ValidateAutoDiscoveryFlags(v)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateAutoDiscoveryFlags() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldUseAutoDiscovery(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
viperSetup func(*viper.Viper)
|
||||
args []string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "auto flag enabled",
|
||||
viperSetup: func(v *viper.Viper) {
|
||||
v.Set("auto", true)
|
||||
},
|
||||
args: []string{},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "auto flag disabled",
|
||||
viperSetup: func(v *viper.Viper) {
|
||||
v.Set("auto", false)
|
||||
},
|
||||
args: []string{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "auto with yaml args",
|
||||
viperSetup: func(v *viper.Viper) {
|
||||
v.Set("auto", true)
|
||||
},
|
||||
args: []string{"spec.yaml"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "no auto flag",
|
||||
viperSetup: func(v *viper.Viper) {
|
||||
// No auto flag set
|
||||
},
|
||||
args: []string{"spec.yaml"},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
v := viper.New()
|
||||
tt.viperSetup(v)
|
||||
|
||||
got := ShouldUseAutoDiscovery(v, tt.args)
|
||||
if got != tt.want {
|
||||
t.Errorf("ShouldUseAutoDiscovery() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAutoDiscoveryMode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
autoEnabled bool
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "foundational only",
|
||||
args: []string{},
|
||||
autoEnabled: true,
|
||||
want: "foundational-only",
|
||||
},
|
||||
{
|
||||
name: "yaml augmented",
|
||||
args: []string{"spec.yaml"},
|
||||
autoEnabled: true,
|
||||
want: "yaml-augmented",
|
||||
},
|
||||
{
|
||||
name: "disabled",
|
||||
args: []string{},
|
||||
autoEnabled: false,
|
||||
want: "disabled",
|
||||
},
|
||||
{
|
||||
name: "disabled with args",
|
||||
args: []string{"spec.yaml"},
|
||||
autoEnabled: false,
|
||||
want: "disabled",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := GetAutoDiscoveryMode(tt.args, tt.autoEnabled)
|
||||
if got != tt.want {
|
||||
t.Errorf("GetAutoDiscoveryMode() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateImageCollectionOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config AutoDiscoveryConfig
|
||||
checkFunc func(t *testing.T, opts images.CollectionOptions)
|
||||
}{
|
||||
{
|
||||
name: "standard profile",
|
||||
config: AutoDiscoveryConfig{
|
||||
Profile: "standard",
|
||||
},
|
||||
checkFunc: func(t *testing.T, opts images.CollectionOptions) {
|
||||
if opts.Timeout != 30*time.Second {
|
||||
t.Errorf("Expected standard profile timeout 30s, got %v", opts.Timeout)
|
||||
}
|
||||
if !opts.ContinueOnError {
|
||||
t.Error("Should continue on error for auto-discovery")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "comprehensive profile",
|
||||
config: AutoDiscoveryConfig{
|
||||
Profile: "comprehensive",
|
||||
},
|
||||
checkFunc: func(t *testing.T, opts images.CollectionOptions) {
|
||||
if opts.Timeout != 60*time.Second {
|
||||
t.Errorf("Expected comprehensive profile timeout 60s, got %v", opts.Timeout)
|
||||
}
|
||||
if !opts.IncludeConfig {
|
||||
t.Error("Comprehensive profile should include config")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paranoid profile",
|
||||
config: AutoDiscoveryConfig{
|
||||
Profile: "paranoid",
|
||||
},
|
||||
checkFunc: func(t *testing.T, opts images.CollectionOptions) {
|
||||
if opts.Timeout != 120*time.Second {
|
||||
t.Errorf("Expected paranoid profile timeout 120s, got %v", opts.Timeout)
|
||||
}
|
||||
if !opts.IncludeLayers {
|
||||
t.Error("Paranoid profile should include layers")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "custom timeout",
|
||||
config: AutoDiscoveryConfig{
|
||||
Profile: "standard",
|
||||
Timeout: 45 * time.Second,
|
||||
},
|
||||
checkFunc: func(t *testing.T, opts images.CollectionOptions) {
|
||||
if opts.Timeout != 45*time.Second {
|
||||
t.Errorf("Expected custom timeout 45s, got %v", opts.Timeout)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
opts := CreateImageCollectionOptions(tt.config)
|
||||
tt.checkFunc(t, opts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToCollectorSpecs(t *testing.T) {
|
||||
// This test would need actual troubleshootv1beta2.Collect instances
|
||||
// For now, test with nil input
|
||||
specs, err := convertToCollectorSpecs([]*troubleshootv1beta2.Collect{})
|
||||
if err != nil {
|
||||
t.Errorf("convertToCollectorSpecs() with empty input should not error: %v", err)
|
||||
}
|
||||
if len(specs) != 0 {
|
||||
t.Errorf("convertToCollectorSpecs() with empty input should return empty slice, got %d items", len(specs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToTroubleshootCollectors(t *testing.T) {
|
||||
// This test would need actual autodiscovery.CollectorSpec instances
|
||||
// For now, test with nil input
|
||||
collectors, err := convertToTroubleshootCollectors([]autodiscovery.CollectorSpec{})
|
||||
if err != nil {
|
||||
t.Errorf("convertToTroubleshootCollectors() with empty input should not error: %v", err)
|
||||
}
|
||||
if len(collectors) != 0 {
|
||||
t.Errorf("convertToTroubleshootCollectors() with empty input should return empty slice, got %d items", len(collectors))
|
||||
}
|
||||
}
|
||||
848
cmd/troubleshoot/cli/diff.go
Normal file
848
cmd/troubleshoot/cli/diff.go
Normal file
@@ -0,0 +1,848 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pmezard/go-difflib/difflib"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const maxInlineDiffBytes = 256 * 1024
|
||||
|
||||
// DiffResult represents the result of comparing two support bundles
|
||||
type DiffResult struct {
|
||||
Summary DiffSummary `json:"summary"`
|
||||
Changes []Change `json:"changes"`
|
||||
Metadata DiffMetadata `json:"metadata"`
|
||||
Significance string `json:"significance"`
|
||||
}
|
||||
|
||||
// DiffSummary provides high-level statistics about the diff
|
||||
type DiffSummary struct {
|
||||
TotalChanges int `json:"totalChanges"`
|
||||
FilesAdded int `json:"filesAdded"`
|
||||
FilesRemoved int `json:"filesRemoved"`
|
||||
FilesModified int `json:"filesModified"`
|
||||
HighImpactChanges int `json:"highImpactChanges"`
|
||||
}
|
||||
|
||||
// Change represents a single difference between bundles
|
||||
type Change struct {
|
||||
Type string `json:"type"` // added, removed, modified
|
||||
Category string `json:"category"` // resource, log, config, etc.
|
||||
Path string `json:"path"` // file path or resource path
|
||||
Impact string `json:"impact"` // high, medium, low, none
|
||||
Details map[string]interface{} `json:"details"` // change-specific details
|
||||
Remediation *RemediationStep `json:"remediation,omitempty"`
|
||||
}
|
||||
|
||||
// RemediationStep represents a suggested remediation action
|
||||
type RemediationStep struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Command string `json:"command,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// DiffMetadata contains metadata about the diff operation
|
||||
type DiffMetadata struct {
|
||||
OldBundle BundleMetadata `json:"oldBundle"`
|
||||
NewBundle BundleMetadata `json:"newBundle"`
|
||||
GeneratedAt string `json:"generatedAt"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// BundleMetadata contains metadata about a support bundle
|
||||
type BundleMetadata struct {
|
||||
Path string `json:"path"`
|
||||
Size int64 `json:"size"`
|
||||
CreatedAt string `json:"createdAt,omitempty"`
|
||||
NumFiles int `json:"numFiles"`
|
||||
}
|
||||
|
||||
func Diff() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "diff <old-bundle.(tar.gz|tgz)> <new-bundle.(tar.gz|tgz)>",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Short: "Compare two support bundles and identify changes",
|
||||
Long: `Compare two support bundles to identify changes over time.
|
||||
This command analyzes differences between two support bundle archives and generates
|
||||
a human-readable report showing what has changed, including:
|
||||
|
||||
- Added, removed, or modified files
|
||||
- Configuration changes
|
||||
- Log differences
|
||||
- Resource status changes
|
||||
- Performance metric changes
|
||||
|
||||
Use -o to write the report to a file; otherwise it prints to stdout.`,
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
viper.BindPFlags(cmd.Flags())
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
v := viper.GetViper()
|
||||
return runBundleDiff(v, args[0], args[1])
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringP("output", "o", "", "file path of where to save the diff report (default prints to stdout)")
|
||||
cmd.Flags().Int("max-diff-lines", 200, "maximum total lines to include in an inline diff for a single file")
|
||||
cmd.Flags().Int("max-diff-files", 50, "maximum number of files to include inline diffs for; additional modified files will omit inline diffs")
|
||||
cmd.Flags().Bool("include-log-diffs", false, "include inline diffs for log files as well")
|
||||
cmd.Flags().Int("diff-context", 3, "number of context lines to include around changes in unified diffs")
|
||||
cmd.Flags().Bool("hide-inline-diffs", false, "hide inline unified diffs in the report")
|
||||
cmd.Flags().String("format", "", "output format; set to 'json' to emit machine-readable JSON to stdout or -o")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runBundleDiff(v *viper.Viper, oldBundle, newBundle string) error {
|
||||
klog.V(2).Infof("Comparing support bundles: %s -> %s", oldBundle, newBundle)
|
||||
|
||||
// Validate input files
|
||||
if err := validateBundleFile(oldBundle); err != nil {
|
||||
return errors.Wrap(err, "invalid old bundle")
|
||||
}
|
||||
if err := validateBundleFile(newBundle); err != nil {
|
||||
return errors.Wrap(err, "invalid new bundle")
|
||||
}
|
||||
|
||||
// Perform the diff (placeholder implementation)
|
||||
diffResult, err := performBundleDiff(oldBundle, newBundle, v)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to compare bundles")
|
||||
}
|
||||
|
||||
// Output the results
|
||||
if err := outputDiffResult(diffResult, v); err != nil {
|
||||
return errors.Wrap(err, "failed to output diff results")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateBundleFile(bundlePath string) error {
|
||||
if bundlePath == "" {
|
||||
return errors.New("bundle path cannot be empty")
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
if _, err := os.Stat(bundlePath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("bundle file not found: %s", bundlePath)
|
||||
}
|
||||
|
||||
// Support .tar.gz and .tgz bundles
|
||||
lower := strings.ToLower(bundlePath)
|
||||
if !(strings.HasSuffix(lower, ".tar.gz") || strings.HasSuffix(lower, ".tgz")) {
|
||||
return fmt.Errorf("unsupported bundle format. Expected path to end with .tar.gz or .tgz")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func performBundleDiff(oldBundle, newBundle string, v *viper.Viper) (*DiffResult, error) {
|
||||
klog.V(2).Info("Performing bundle diff analysis (streaming)...")
|
||||
|
||||
// Stream inventories
|
||||
oldInv, err := buildInventoryFromTarGz(oldBundle)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to inventory old bundle")
|
||||
}
|
||||
newInv, err := buildInventoryFromTarGz(newBundle)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to inventory new bundle")
|
||||
}
|
||||
|
||||
var changes []Change
|
||||
inlineDiffsIncluded := 0
|
||||
maxDiffLines := v.GetInt("max-diff-lines")
|
||||
if maxDiffLines <= 0 {
|
||||
maxDiffLines = 200
|
||||
}
|
||||
maxDiffFiles := v.GetInt("max-diff-files")
|
||||
if maxDiffFiles <= 0 {
|
||||
maxDiffFiles = 50
|
||||
}
|
||||
includeLogDiffs := v.GetBool("include-log-diffs")
|
||||
diffContext := v.GetInt("diff-context")
|
||||
if diffContext <= 0 {
|
||||
diffContext = 3
|
||||
}
|
||||
|
||||
// Added files
|
||||
for p, nf := range newInv {
|
||||
if _, ok := oldInv[p]; !ok {
|
||||
ch := Change{
|
||||
Type: "added",
|
||||
Category: categorizePath(p),
|
||||
Path: "/" + p,
|
||||
Impact: estimateImpact("added", p),
|
||||
Details: map[string]interface{}{
|
||||
"size": nf.Size,
|
||||
},
|
||||
}
|
||||
if rem := suggestRemediation(ch.Type, p); rem != nil {
|
||||
ch.Remediation = rem
|
||||
}
|
||||
changes = append(changes, ch)
|
||||
}
|
||||
}
|
||||
|
||||
// Removed files
|
||||
for p, of := range oldInv {
|
||||
if _, ok := newInv[p]; !ok {
|
||||
ch := Change{
|
||||
Type: "removed",
|
||||
Category: categorizePath(p),
|
||||
Path: "/" + p,
|
||||
Impact: estimateImpact("removed", p),
|
||||
Details: map[string]interface{}{
|
||||
"size": of.Size,
|
||||
},
|
||||
}
|
||||
if rem := suggestRemediation(ch.Type, p); rem != nil {
|
||||
ch.Remediation = rem
|
||||
}
|
||||
changes = append(changes, ch)
|
||||
}
|
||||
}
|
||||
|
||||
// Modified files
|
||||
for p, of := range oldInv {
|
||||
if nf, ok := newInv[p]; ok {
|
||||
if of.Digest != nf.Digest {
|
||||
ch := Change{
|
||||
Type: "modified",
|
||||
Category: categorizePath(p),
|
||||
Path: "/" + p,
|
||||
Impact: estimateImpact("modified", p),
|
||||
Details: map[string]interface{}{},
|
||||
}
|
||||
if rem := suggestRemediation(ch.Type, p); rem != nil {
|
||||
ch.Remediation = rem
|
||||
}
|
||||
changes = append(changes, ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort changes deterministically: type, then path
|
||||
sort.Slice(changes, func(i, j int) bool {
|
||||
if changes[i].Type == changes[j].Type {
|
||||
return changes[i].Path < changes[j].Path
|
||||
}
|
||||
return changes[i].Type < changes[j].Type
|
||||
})
|
||||
|
||||
// Populate inline diffs lazily for the first N eligible modified files using streaming approach
|
||||
for i := range changes {
|
||||
if inlineDiffsIncluded >= maxDiffFiles {
|
||||
break
|
||||
}
|
||||
ch := &changes[i]
|
||||
if ch.Type != "modified" {
|
||||
continue
|
||||
}
|
||||
allowLogs := includeLogDiffs || ch.Category != "logs"
|
||||
if !allowLogs {
|
||||
continue
|
||||
}
|
||||
// Use streaming diff generation to avoid loading large files into memory
|
||||
if d := generateStreamingUnifiedDiff(oldBundle, newBundle, ch.Path, diffContext, maxDiffLines); d != "" {
|
||||
if ch.Details == nil {
|
||||
ch.Details = map[string]interface{}{}
|
||||
}
|
||||
ch.Details["diff"] = d
|
||||
inlineDiffsIncluded++
|
||||
}
|
||||
}
|
||||
|
||||
// Summaries
|
||||
summary := DiffSummary{}
|
||||
for _, c := range changes {
|
||||
switch c.Type {
|
||||
case "added":
|
||||
summary.FilesAdded++
|
||||
case "removed":
|
||||
summary.FilesRemoved++
|
||||
case "modified":
|
||||
summary.FilesModified++
|
||||
}
|
||||
if c.Impact == "high" {
|
||||
summary.HighImpactChanges++
|
||||
}
|
||||
}
|
||||
summary.TotalChanges = summary.FilesAdded + summary.FilesRemoved + summary.FilesModified
|
||||
|
||||
oldMeta := getBundleMetadataWithCount(oldBundle, len(oldInv))
|
||||
newMeta := getBundleMetadataWithCount(newBundle, len(newInv))
|
||||
|
||||
result := &DiffResult{
|
||||
Summary: summary,
|
||||
Changes: changes,
|
||||
Metadata: DiffMetadata{OldBundle: oldMeta, NewBundle: newMeta, GeneratedAt: time.Now().Format(time.RFC3339), Version: "v1"},
|
||||
Significance: computeOverallSignificance(changes),
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type inventoryFile struct {
|
||||
Size int64
|
||||
Digest string
|
||||
}
|
||||
|
||||
func buildInventoryFromTarGz(bundlePath string) (map[string]inventoryFile, error) {
|
||||
f, err := os.Open(bundlePath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to open bundle")
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
gz, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create gzip reader")
|
||||
}
|
||||
defer gz.Close()
|
||||
|
||||
tr := tar.NewReader(gz)
|
||||
inv := make(map[string]inventoryFile)
|
||||
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read tar entry")
|
||||
}
|
||||
if !hdr.FileInfo().Mode().IsRegular() {
|
||||
continue
|
||||
}
|
||||
|
||||
norm := normalizePath(hdr.Name)
|
||||
if norm == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
var copied int64
|
||||
buf := make([]byte, 32*1024)
|
||||
for copied < hdr.Size {
|
||||
toRead := int64(len(buf))
|
||||
if remain := hdr.Size - copied; remain < toRead {
|
||||
toRead = remain
|
||||
}
|
||||
n, rerr := io.ReadFull(tr, buf[:toRead])
|
||||
if n > 0 {
|
||||
_, _ = h.Write(buf[:n])
|
||||
copied += int64(n)
|
||||
}
|
||||
if rerr == io.EOF || rerr == io.ErrUnexpectedEOF {
|
||||
break
|
||||
}
|
||||
if rerr != nil {
|
||||
return nil, errors.Wrap(rerr, "failed to read file content")
|
||||
}
|
||||
}
|
||||
|
||||
digest := hex.EncodeToString(h.Sum(nil))
|
||||
inv[norm] = inventoryFile{Size: hdr.Size, Digest: digest}
|
||||
}
|
||||
|
||||
return inv, nil
|
||||
}
|
||||
|
||||
func normalizePath(name string) string {
|
||||
name = strings.TrimPrefix(name, "./")
|
||||
if name == "" {
|
||||
return name
|
||||
}
|
||||
i := strings.IndexByte(name, '/')
|
||||
if i < 0 {
|
||||
return name
|
||||
}
|
||||
first := name[:i]
|
||||
rest := name[i+1:]
|
||||
|
||||
// Known domain roots we do not strip
|
||||
domainRoots := map[string]bool{
|
||||
"cluster-resources": true,
|
||||
"all-logs": true,
|
||||
"cluster-info": true,
|
||||
"execution-data": true,
|
||||
}
|
||||
if domainRoots[first] {
|
||||
return name
|
||||
}
|
||||
// Strip only known container prefixes
|
||||
if first == "root" || strings.HasPrefix(strings.ToLower(first), "support-bundle") {
|
||||
return rest
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func isProbablyText(sample []byte) bool {
|
||||
if len(sample) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, b := range sample {
|
||||
if b == 0x00 {
|
||||
return false
|
||||
}
|
||||
if b < 0x09 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func normalizeNewlines(s string) string {
|
||||
return strings.ReplaceAll(s, "\r\n", "\n")
|
||||
}
|
||||
|
||||
// generateStreamingUnifiedDiff creates a unified diff by streaming files line-by-line to avoid loading large files into memory
|
||||
func generateStreamingUnifiedDiff(oldBundle, newBundle, path string, context, maxTotalLines int) string {
|
||||
oldReader, err := createTarFileReader(oldBundle, strings.TrimPrefix(path, "/"))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer oldReader.Close()
|
||||
|
||||
newReader, err := createTarFileReader(newBundle, strings.TrimPrefix(path, "/"))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer newReader.Close()
|
||||
|
||||
// Read files line by line
|
||||
oldLines, err := readLinesFromReader(oldReader, maxInlineDiffBytes)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
newLines, err := readLinesFromReader(newReader, maxInlineDiffBytes)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Generate diff using the existing difflib
|
||||
ud := difflib.UnifiedDiff{
|
||||
A: oldLines,
|
||||
B: newLines,
|
||||
FromFile: "old:" + path,
|
||||
ToFile: "new:" + path,
|
||||
Context: context,
|
||||
}
|
||||
s, err := difflib.GetUnifiedDiffString(ud)
|
||||
if err != nil || s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
lines := strings.Split(s, "\n")
|
||||
if maxTotalLines > 0 && len(lines) > maxTotalLines {
|
||||
lines = append(lines[:maxTotalLines], "... (diff truncated)")
|
||||
}
|
||||
if len(lines) > 0 && lines[len(lines)-1] == "" {
|
||||
lines = lines[:len(lines)-1]
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// readLinesFromReader reads lines from a reader up to maxBytes total, returning normalized lines
|
||||
func readLinesFromReader(reader io.Reader, maxBytes int) ([]string, error) {
|
||||
var lines []string
|
||||
var totalBytes int
|
||||
scanner := bufio.NewScanner(reader)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := normalizeNewlines(scanner.Text())
|
||||
lineWithNL := line + "\n"
|
||||
lineBytes := len(lineWithNL)
|
||||
|
||||
if totalBytes+lineBytes > maxBytes {
|
||||
lines = append(lines, "... (content truncated due to size)\n")
|
||||
break
|
||||
}
|
||||
|
||||
lines = append(lines, lineWithNL)
|
||||
totalBytes += lineBytes
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
// generateUnifiedDiff builds a unified diff with headers and context, then truncates to maxTotalLines
|
||||
func generateUnifiedDiff(a, b string, path string, context, maxTotalLines int) string {
|
||||
ud := difflib.UnifiedDiff{
|
||||
A: difflib.SplitLines(a),
|
||||
B: difflib.SplitLines(b),
|
||||
FromFile: "old:" + path,
|
||||
ToFile: "new:" + path,
|
||||
Context: context,
|
||||
}
|
||||
s, err := difflib.GetUnifiedDiffString(ud)
|
||||
if err != nil || s == "" {
|
||||
return ""
|
||||
}
|
||||
lines := strings.Split(s, "\n")
|
||||
if maxTotalLines > 0 && len(lines) > maxTotalLines {
|
||||
lines = append(lines[:maxTotalLines], "... (diff truncated)")
|
||||
}
|
||||
if len(lines) > 0 && lines[len(lines)-1] == "" {
|
||||
lines = lines[:len(lines)-1]
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func categorizePath(p string) string {
|
||||
if strings.HasPrefix(p, "cluster-resources/pods/logs/") || strings.Contains(p, "/logs/") || strings.HasPrefix(p, "all-logs/") || strings.HasPrefix(p, "logs/") {
|
||||
return "logs"
|
||||
}
|
||||
if strings.HasPrefix(p, "cluster-resources/") {
|
||||
rest := strings.TrimPrefix(p, "cluster-resources/")
|
||||
seg := rest
|
||||
if i := strings.IndexByte(rest, '/'); i >= 0 {
|
||||
seg = rest[:i]
|
||||
}
|
||||
if seg == "" {
|
||||
return "resources"
|
||||
}
|
||||
return "resources:" + seg
|
||||
}
|
||||
if strings.HasPrefix(p, "config/") || strings.HasSuffix(p, ".yaml") || strings.HasSuffix(p, ".yml") || strings.HasSuffix(p, ".json") {
|
||||
return "config"
|
||||
}
|
||||
return "files"
|
||||
}
|
||||
|
||||
// estimateImpact determines impact based on change type and path patterns
|
||||
func estimateImpact(changeType, p string) string {
|
||||
// High impact cases
|
||||
if strings.HasPrefix(p, "cluster-resources/custom-resource-definitions") {
|
||||
if changeType == "removed" || changeType == "modified" {
|
||||
return "high"
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(p, "cluster-resources/clusterrole") || strings.HasPrefix(p, "cluster-resources/clusterrolebindings") || strings.Contains(p, "/rolebindings/") {
|
||||
if changeType != "added" { // reductions or changes can break access
|
||||
return "high"
|
||||
}
|
||||
}
|
||||
if strings.Contains(p, "/secrets/") || strings.HasSuffix(p, "-secrets.json") {
|
||||
if changeType != "added" {
|
||||
return "high"
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(p, "cluster-resources/nodes") {
|
||||
if changeType != "added" { // node status changes can be severe
|
||||
return "high"
|
||||
}
|
||||
}
|
||||
if strings.Contains(p, "/network-policy/") || strings.HasSuffix(p, "/networkpolicies.json") {
|
||||
if changeType != "added" {
|
||||
return "high"
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(p, "cluster-resources/") && strings.Contains(p, "/kube-system") {
|
||||
if changeType != "added" {
|
||||
return "high"
|
||||
}
|
||||
}
|
||||
// Medium default for cluster resources
|
||||
if strings.HasPrefix(p, "cluster-resources/") {
|
||||
return "medium"
|
||||
}
|
||||
// Logs and others default low
|
||||
if strings.Contains(p, "/logs/") || strings.HasPrefix(p, "all-logs/") {
|
||||
return "low"
|
||||
}
|
||||
return "low"
|
||||
}
|
||||
|
||||
// suggestRemediation returns a basic remediation suggestion based on category and change
|
||||
func suggestRemediation(changeType, p string) *RemediationStep {
|
||||
// RBAC
|
||||
if strings.HasPrefix(p, "cluster-resources/clusterrole") || strings.HasPrefix(p, "cluster-resources/clusterrolebindings") || strings.Contains(p, "/rolebindings/") {
|
||||
return &RemediationStep{Description: "Validate RBAC permissions and recent changes", Command: "kubectl auth can-i --list"}
|
||||
}
|
||||
// CRDs
|
||||
if strings.HasPrefix(p, "cluster-resources/custom-resource-definitions") {
|
||||
return &RemediationStep{Description: "Check CRD presence and reconcile operator status", Command: "kubectl get crds"}
|
||||
}
|
||||
// Nodes
|
||||
if strings.HasPrefix(p, "cluster-resources/nodes") {
|
||||
return &RemediationStep{Description: "Inspect node conditions and recent events", Command: "kubectl describe nodes"}
|
||||
}
|
||||
// Network policy
|
||||
if strings.Contains(p, "/network-policy/") || strings.HasSuffix(p, "/networkpolicies.json") {
|
||||
return &RemediationStep{Description: "Validate connectivity and recent NetworkPolicy changes", Command: "kubectl get networkpolicy -A"}
|
||||
}
|
||||
// Secrets/Config
|
||||
if strings.Contains(p, "/secrets/") || strings.HasPrefix(p, "config/") {
|
||||
return &RemediationStep{Description: "Review configuration or secret changes for correctness"}
|
||||
}
|
||||
// Workloads
|
||||
if strings.Contains(p, "/deployments/") || strings.Contains(p, "/statefulsets/") || strings.Contains(p, "/daemonsets/") {
|
||||
return &RemediationStep{Description: "Check rollout and pod status", Command: "kubectl rollout status -n <ns> <kind>/<name>"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func computeOverallSignificance(changes []Change) string {
|
||||
high, medium := 0, 0
|
||||
for _, c := range changes {
|
||||
switch c.Impact {
|
||||
case "high":
|
||||
high++
|
||||
case "medium":
|
||||
medium++
|
||||
}
|
||||
}
|
||||
if high > 0 {
|
||||
return "high"
|
||||
}
|
||||
if medium > 0 {
|
||||
return "medium"
|
||||
}
|
||||
if len(changes) > 0 {
|
||||
return "low"
|
||||
}
|
||||
return "none"
|
||||
}
|
||||
|
||||
func getBundleMetadata(bundlePath string) BundleMetadata {
|
||||
metadata := BundleMetadata{
|
||||
Path: bundlePath,
|
||||
}
|
||||
|
||||
if stat, err := os.Stat(bundlePath); err == nil {
|
||||
metadata.Size = stat.Size()
|
||||
metadata.CreatedAt = stat.ModTime().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
// getBundleMetadataWithCount sets NumFiles directly to avoid re-reading the archive
|
||||
func getBundleMetadataWithCount(bundlePath string, numFiles int) BundleMetadata {
|
||||
md := getBundleMetadata(bundlePath)
|
||||
md.NumFiles = numFiles
|
||||
return md
|
||||
}
|
||||
|
||||
func outputDiffResult(result *DiffResult, v *viper.Viper) error {
|
||||
outputPath := v.GetString("output")
|
||||
showInlineDiffs := !v.GetBool("hide-inline-diffs")
|
||||
formatMode := strings.ToLower(v.GetString("format"))
|
||||
|
||||
var output []byte
|
||||
if formatMode == "json" {
|
||||
data, err := json.MarshalIndent(result, "", " ")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to marshal diff result to JSON")
|
||||
}
|
||||
output = data
|
||||
} else {
|
||||
output = []byte(generateTextDiffReport(result, showInlineDiffs))
|
||||
}
|
||||
|
||||
if outputPath != "" {
|
||||
// Write to file
|
||||
if err := os.WriteFile(outputPath, output, 0644); err != nil {
|
||||
return errors.Wrap(err, "failed to write diff output to file")
|
||||
}
|
||||
fmt.Printf("Diff report written to: %s\n", outputPath)
|
||||
} else {
|
||||
// Write to stdout
|
||||
fmt.Print(string(output))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateTextDiffReport(result *DiffResult, showInlineDiffs bool) string {
|
||||
var report strings.Builder
|
||||
|
||||
report.WriteString("Support Bundle Diff Report\n")
|
||||
report.WriteString("==========================\n\n")
|
||||
|
||||
report.WriteString(fmt.Sprintf("Generated: %s\n", result.Metadata.GeneratedAt))
|
||||
report.WriteString(fmt.Sprintf("Old Bundle: %s (%s)\n", result.Metadata.OldBundle.Path, formatSize(result.Metadata.OldBundle.Size)))
|
||||
report.WriteString(fmt.Sprintf("New Bundle: %s (%s)\n\n", result.Metadata.NewBundle.Path, formatSize(result.Metadata.NewBundle.Size)))
|
||||
|
||||
// Summary
|
||||
report.WriteString("Summary:\n")
|
||||
report.WriteString(fmt.Sprintf(" Total Changes: %d\n", result.Summary.TotalChanges))
|
||||
report.WriteString(fmt.Sprintf(" Files Added: %d\n", result.Summary.FilesAdded))
|
||||
report.WriteString(fmt.Sprintf(" Files Removed: %d\n", result.Summary.FilesRemoved))
|
||||
report.WriteString(fmt.Sprintf(" Files Modified: %d\n", result.Summary.FilesModified))
|
||||
report.WriteString(fmt.Sprintf(" High Impact Changes: %d\n", result.Summary.HighImpactChanges))
|
||||
report.WriteString(fmt.Sprintf(" Significance: %s\n\n", result.Significance))
|
||||
|
||||
if len(result.Changes) == 0 {
|
||||
report.WriteString("No changes detected between bundles.\n")
|
||||
} else {
|
||||
report.WriteString("Changes:\n")
|
||||
for i, change := range result.Changes {
|
||||
report.WriteString(fmt.Sprintf(" %d. [%s] %s (%s impact)\n",
|
||||
i+1, strings.ToUpper(change.Type), change.Path, change.Impact))
|
||||
if change.Remediation != nil {
|
||||
report.WriteString(fmt.Sprintf(" Remediation: %s\n", change.Remediation.Description))
|
||||
}
|
||||
if showInlineDiffs {
|
||||
if diffStr, ok := change.Details["diff"].(string); ok && diffStr != "" {
|
||||
report.WriteString(" Diff:\n")
|
||||
for _, line := range strings.Split(diffStr, "\n") {
|
||||
report.WriteString(" " + line + "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return report.String()
|
||||
}
|
||||
|
||||
func formatSize(bytes int64) string {
|
||||
const unit = 1024
|
||||
if bytes < unit {
|
||||
return fmt.Sprintf("%d B", bytes)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := bytes / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %ciB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
|
||||
// tarFileReader provides a streaming interface to read a specific file from a tar.gz archive
|
||||
type tarFileReader struct {
|
||||
file *os.File
|
||||
gz *gzip.Reader
|
||||
tr *tar.Reader
|
||||
found bool
|
||||
header *tar.Header
|
||||
}
|
||||
|
||||
// createTarFileReader creates a streaming reader for a specific file in a tar.gz archive
|
||||
func createTarFileReader(bundlePath, normalizedPath string) (*tarFileReader, error) {
|
||||
f, err := os.Open(bundlePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gz, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tr := tar.NewReader(gz)
|
||||
|
||||
// Find the target file
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
gz.Close()
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
if !hdr.FileInfo().Mode().IsRegular() {
|
||||
continue
|
||||
}
|
||||
if normalizePath(hdr.Name) == normalizedPath {
|
||||
// Check if file is probably text
|
||||
sample := make([]byte, 512)
|
||||
n, _ := io.ReadFull(tr, sample[:])
|
||||
if n > 0 && !isProbablyText(sample[:n]) {
|
||||
gz.Close()
|
||||
f.Close()
|
||||
return nil, fmt.Errorf("file is not text")
|
||||
}
|
||||
|
||||
// Reopen to start from beginning of file
|
||||
gz.Close()
|
||||
f.Close()
|
||||
|
||||
f, err = os.Open(bundlePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gz, err = gzip.NewReader(f)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
tr = tar.NewReader(gz)
|
||||
|
||||
// Find the file again
|
||||
for {
|
||||
hdr, err = tr.Next()
|
||||
if err == io.EOF {
|
||||
gz.Close()
|
||||
f.Close()
|
||||
return nil, fmt.Errorf("file not found on second pass")
|
||||
}
|
||||
if err != nil {
|
||||
gz.Close()
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
if normalizePath(hdr.Name) == normalizedPath {
|
||||
return &tarFileReader{
|
||||
file: f,
|
||||
gz: gz,
|
||||
tr: tr,
|
||||
found: true,
|
||||
header: hdr,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gz.Close()
|
||||
f.Close()
|
||||
return nil, fmt.Errorf("file not found: %s", normalizedPath)
|
||||
}
|
||||
|
||||
// Read implements io.Reader interface
|
||||
func (r *tarFileReader) Read(p []byte) (n int, err error) {
|
||||
if !r.found {
|
||||
return 0, io.EOF
|
||||
}
|
||||
return r.tr.Read(p)
|
||||
}
|
||||
|
||||
// Close closes the underlying file handles
|
||||
func (r *tarFileReader) Close() error {
|
||||
if r.gz != nil {
|
||||
r.gz.Close()
|
||||
}
|
||||
if r.file != nil {
|
||||
return r.file.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
552
cmd/troubleshoot/cli/diff_test.go
Normal file
552
cmd/troubleshoot/cli/diff_test.go
Normal file
@@ -0,0 +1,552 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func TestValidateBundleFile(t *testing.T) {
|
||||
// Create temporary test files
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create bundle files
|
||||
validBundle := filepath.Join(tempDir, "test-bundle.tar.gz")
|
||||
if err := os.WriteFile(validBundle, []byte("dummy content"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create test bundle: %v", err)
|
||||
}
|
||||
|
||||
validTgz := filepath.Join(tempDir, "test-bundle.tgz")
|
||||
if err := os.WriteFile(validTgz, []byte("dummy content"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create test tgz bundle: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
bundlePath string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid tar.gz bundle",
|
||||
bundlePath: validBundle,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid tgz bundle",
|
||||
bundlePath: validTgz,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty path",
|
||||
bundlePath: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "non-existent file",
|
||||
bundlePath: "/path/to/nonexistent.tar.gz",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid extension",
|
||||
bundlePath: filepath.Join(tempDir, "invalid.txt"),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// For invalid extension test, create the file
|
||||
if strings.HasSuffix(tt.bundlePath, "invalid.txt") {
|
||||
os.WriteFile(tt.bundlePath, []byte("content"), 0644)
|
||||
}
|
||||
|
||||
err := validateBundleFile(tt.bundlePath)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("validateBundleFile() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBundleMetadata(t *testing.T) {
|
||||
// Create a temporary test file
|
||||
tempDir := t.TempDir()
|
||||
testBundle := filepath.Join(tempDir, "test-bundle.tar.gz")
|
||||
testContent := []byte("test bundle content")
|
||||
|
||||
if err := os.WriteFile(testBundle, testContent, 0644); err != nil {
|
||||
t.Fatalf("Failed to create test bundle: %v", err)
|
||||
}
|
||||
|
||||
metadata := getBundleMetadata(testBundle)
|
||||
|
||||
if metadata.Path != testBundle {
|
||||
t.Errorf("getBundleMetadata() path = %v, want %v", metadata.Path, testBundle)
|
||||
}
|
||||
|
||||
if metadata.Size != int64(len(testContent)) {
|
||||
t.Errorf("getBundleMetadata() size = %v, want %v", metadata.Size, len(testContent))
|
||||
}
|
||||
|
||||
if metadata.CreatedAt == "" {
|
||||
t.Error("getBundleMetadata() createdAt should not be empty")
|
||||
}
|
||||
|
||||
// Validate timestamp format
|
||||
if _, err := time.Parse(time.RFC3339, metadata.CreatedAt); err != nil {
|
||||
t.Errorf("getBundleMetadata() createdAt has invalid format: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPerformBundleDiff(t *testing.T) {
|
||||
// Create temporary test bundles
|
||||
tempDir := t.TempDir()
|
||||
|
||||
oldBundle := filepath.Join(tempDir, "old-bundle.tar.gz")
|
||||
newBundle := filepath.Join(tempDir, "new-bundle.tar.gz")
|
||||
|
||||
if err := writeTarGz(oldBundle, map[string]string{
|
||||
"root/cluster-resources/version.txt": "v1\n",
|
||||
"root/logs/app.log": "line1\n",
|
||||
}); err != nil {
|
||||
t.Fatalf("Failed to create old bundle: %v", err)
|
||||
}
|
||||
|
||||
if err := writeTarGz(newBundle, map[string]string{
|
||||
"root/cluster-resources/version.txt": "v2\n",
|
||||
"root/logs/app.log": "line1\nline2\n",
|
||||
"root/added.txt": "new\n",
|
||||
}); err != nil {
|
||||
t.Fatalf("Failed to create new bundle: %v", err)
|
||||
}
|
||||
|
||||
v := viper.New()
|
||||
result, err := performBundleDiff(oldBundle, newBundle, v)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("performBundleDiff() error = %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("performBundleDiff() returned nil result")
|
||||
}
|
||||
|
||||
// Verify result structure
|
||||
if result.Metadata.Version != "v1" {
|
||||
t.Errorf("performBundleDiff() version = %v, want v1", result.Metadata.Version)
|
||||
}
|
||||
|
||||
if result.Metadata.OldBundle.Path != oldBundle {
|
||||
t.Errorf("performBundleDiff() old bundle path = %v, want %v", result.Metadata.OldBundle.Path, oldBundle)
|
||||
}
|
||||
|
||||
if result.Metadata.NewBundle.Path != newBundle {
|
||||
t.Errorf("performBundleDiff() new bundle path = %v, want %v", result.Metadata.NewBundle.Path, newBundle)
|
||||
}
|
||||
|
||||
if result.Metadata.GeneratedAt == "" {
|
||||
t.Error("performBundleDiff() generatedAt should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
// writeTarGz creates a gzipped tar file at tarPath with the provided files map.
|
||||
// Keys are entry names inside the archive, values are file contents.
|
||||
func writeTarGz(tarPath string, files map[string]string) error {
|
||||
f, err := os.Create(tarPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
gz := gzip.NewWriter(f)
|
||||
defer gz.Close()
|
||||
|
||||
tw := tar.NewWriter(gz)
|
||||
defer tw.Close()
|
||||
|
||||
for name, content := range files {
|
||||
data := []byte(content)
|
||||
hdr := &tar.Header{
|
||||
Name: name,
|
||||
Mode: 0644,
|
||||
Size: int64(len(data)),
|
||||
Typeflag: tar.TypeReg,
|
||||
}
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := bytes.NewReader(data).WriteTo(tw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestGenerateTextDiffReport(t *testing.T) {
|
||||
result := &DiffResult{
|
||||
Summary: DiffSummary{
|
||||
TotalChanges: 3,
|
||||
FilesAdded: 1,
|
||||
FilesRemoved: 1,
|
||||
FilesModified: 1,
|
||||
HighImpactChanges: 1,
|
||||
},
|
||||
Changes: []Change{
|
||||
{
|
||||
Type: "added",
|
||||
Category: "config",
|
||||
Path: "/new-config.yaml",
|
||||
Impact: "medium",
|
||||
Remediation: &RemediationStep{
|
||||
Description: "Review new configuration",
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "removed",
|
||||
Category: "resource",
|
||||
Path: "/old-deployment.yaml",
|
||||
Impact: "high",
|
||||
},
|
||||
},
|
||||
Metadata: DiffMetadata{
|
||||
OldBundle: BundleMetadata{
|
||||
Path: "/old/bundle.tar.gz",
|
||||
Size: 1024,
|
||||
},
|
||||
NewBundle: BundleMetadata{
|
||||
Path: "/new/bundle.tar.gz",
|
||||
Size: 2048,
|
||||
},
|
||||
GeneratedAt: "2023-01-01T00:00:00Z",
|
||||
},
|
||||
Significance: "high",
|
||||
}
|
||||
|
||||
report := generateTextDiffReport(result, true)
|
||||
|
||||
// Check that report contains expected elements
|
||||
expectedStrings := []string{
|
||||
"Support Bundle Diff Report",
|
||||
"Total Changes: 3",
|
||||
"Files Added: 1",
|
||||
"High Impact Changes: 1",
|
||||
"Significance: high",
|
||||
"/new-config.yaml",
|
||||
"/old-deployment.yaml",
|
||||
"Remediation: Review new configuration",
|
||||
}
|
||||
|
||||
for _, expected := range expectedStrings {
|
||||
if !strings.Contains(report, expected) {
|
||||
t.Errorf("generateTextDiffReport() missing expected string: %s", expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutputDiffResult_JSON(t *testing.T) {
|
||||
// Minimal result
|
||||
result := &DiffResult{
|
||||
Summary: DiffSummary{},
|
||||
Metadata: DiffMetadata{
|
||||
OldBundle: BundleMetadata{Path: "/old.tar.gz"},
|
||||
NewBundle: BundleMetadata{Path: "/new.tar.gz"},
|
||||
GeneratedAt: time.Now().Format(time.RFC3339),
|
||||
Version: "v1",
|
||||
},
|
||||
Changes: []Change{{Type: "modified", Category: "files", Path: "/a", Impact: "low"}},
|
||||
Significance: "low",
|
||||
}
|
||||
|
||||
v := viper.New()
|
||||
v.Set("format", "json")
|
||||
|
||||
// Write to a temp file via -o to exercise file write path
|
||||
tempDir := t.TempDir()
|
||||
outPath := filepath.Join(tempDir, "diff.json")
|
||||
v.Set("output", outPath)
|
||||
|
||||
if err := outputDiffResult(result, v); err != nil {
|
||||
t.Fatalf("outputDiffResult(json) error = %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(outPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read output: %v", err)
|
||||
}
|
||||
|
||||
// Basic JSON sanity checks
|
||||
s := string(data)
|
||||
if !strings.Contains(s, "\"summary\"") || !strings.Contains(s, "\"changes\"") {
|
||||
t.Fatalf("json output missing keys: %s", s)
|
||||
}
|
||||
if !strings.Contains(s, "\"path\": \"/a\"") {
|
||||
t.Fatalf("json output missing change path: %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateTextDiffReport_DiffVisibility(t *testing.T) {
|
||||
result := &DiffResult{
|
||||
Summary: DiffSummary{TotalChanges: 1, FilesModified: 1},
|
||||
Changes: []Change{
|
||||
{
|
||||
Type: "modified",
|
||||
Category: "files",
|
||||
Path: "/path.txt",
|
||||
Impact: "low",
|
||||
Details: map[string]interface{}{"diff": "--- old:/path.txt\n+++ new:/path.txt\n@@\n-a\n+b"},
|
||||
},
|
||||
},
|
||||
Metadata: DiffMetadata{GeneratedAt: time.Now().Format(time.RFC3339)},
|
||||
}
|
||||
|
||||
reportShown := generateTextDiffReport(result, true)
|
||||
if !strings.Contains(reportShown, "Diff:") || !strings.Contains(reportShown, "+ new:/path.txt") {
|
||||
t.Fatalf("expected diff to be shown when enabled; got: %s", reportShown)
|
||||
}
|
||||
|
||||
reportHidden := generateTextDiffReport(result, false)
|
||||
if strings.Contains(reportHidden, "Diff:") {
|
||||
t.Fatalf("expected diff to be hidden when disabled; got: %s", reportHidden)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCategorizePath(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
out string
|
||||
}{
|
||||
{"cluster-resources/pods/logs/ns/pod/container.log", "logs"},
|
||||
{"some/ns/logs/thing.log", "logs"},
|
||||
{"all-logs/ns/pod/container.log", "logs"},
|
||||
{"logs/app.log", "logs"},
|
||||
{"cluster-resources/configmaps/ns.json", "resources:configmaps"},
|
||||
{"cluster-resources/", "resources"},
|
||||
{"config/settings.yaml", "config"},
|
||||
{"random/file.json", "config"},
|
||||
{"random/file.txt", "files"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := categorizePath(c.in); got != c.out {
|
||||
t.Errorf("categorizePath(%q)=%q want %q", c.in, got, c.out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizePath(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
out string
|
||||
}{
|
||||
{"root/foo.txt", "foo.txt"},
|
||||
{"support-bundle-123/foo.txt", "foo.txt"},
|
||||
{"Support-Bundle-ABC/bar/baz.txt", "bar/baz.txt"},
|
||||
{"cluster-resources/pods/logs/whatever.log", "cluster-resources/pods/logs/whatever.log"},
|
||||
{"all-logs/whatever.log", "all-logs/whatever.log"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := normalizePath(c.in); got != c.out {
|
||||
t.Errorf("normalizePath(%q)=%q want %q", c.in, got, c.out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateUnifiedDiff_TruncationAndContext(t *testing.T) {
|
||||
old := "line1\nline2\nline3\nline4\nline5\n"
|
||||
newv := "line1\nline2-mod\nline3\nline4\nline5\n"
|
||||
// context=1 should include headers and minimal context; max lines small to force truncation
|
||||
diff := generateUnifiedDiff(old, newv, "/file.txt", 1, 5)
|
||||
if diff == "" {
|
||||
t.Fatal("expected non-empty diff")
|
||||
}
|
||||
if !strings.Contains(diff, "old:/file.txt") || !strings.Contains(diff, "new:/file.txt") {
|
||||
t.Errorf("diff missing headers: %s", diff)
|
||||
}
|
||||
if !strings.Contains(diff, "... (diff truncated)") {
|
||||
t.Errorf("expected truncated marker in diff: %s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatSize(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
bytes int64
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "bytes",
|
||||
bytes: 512,
|
||||
want: "512 B",
|
||||
},
|
||||
{
|
||||
name: "kilobytes",
|
||||
bytes: 1536, // 1.5 KB
|
||||
want: "1.5 KiB",
|
||||
},
|
||||
{
|
||||
name: "megabytes",
|
||||
bytes: 1572864, // 1.5 MB
|
||||
want: "1.5 MiB",
|
||||
},
|
||||
{
|
||||
name: "zero",
|
||||
bytes: 0,
|
||||
want: "0 B",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := formatSize(tt.bytes)
|
||||
if got != tt.want {
|
||||
t.Errorf("formatSize() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTarFileReader(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
bundlePath := filepath.Join(tempDir, "test-bundle.tar.gz")
|
||||
|
||||
// Create test bundle with text and binary files
|
||||
testFiles := map[string]string{
|
||||
"root/text-file.txt": "line1\nline2\nline3\n",
|
||||
"root/config.yaml": "key: value\nother: data\n",
|
||||
"root/binary-file.bin": string([]byte{0x00, 0x01, 0x02, 0xFF}), // Binary content
|
||||
}
|
||||
|
||||
if err := writeTarGz(bundlePath, testFiles); err != nil {
|
||||
t.Fatalf("Failed to create test bundle: %v", err)
|
||||
}
|
||||
|
||||
// Test reading existing text file
|
||||
reader, err := createTarFileReader(bundlePath, "text-file.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("createTarFileReader() error = %v", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
content := make([]byte, 100)
|
||||
n, err := reader.Read(content)
|
||||
if err != nil && err != io.EOF {
|
||||
t.Errorf("Read() error = %v", err)
|
||||
}
|
||||
|
||||
contentStr := string(content[:n])
|
||||
if !strings.Contains(contentStr, "line1") {
|
||||
t.Errorf("Expected file content not found, got: %s", contentStr)
|
||||
}
|
||||
|
||||
// Test reading non-existent file
|
||||
_, err = createTarFileReader(bundlePath, "non-existent.txt")
|
||||
if err == nil {
|
||||
t.Error("Expected error for non-existent file")
|
||||
}
|
||||
|
||||
// Test reading binary file (should fail)
|
||||
_, err = createTarFileReader(bundlePath, "binary-file.bin")
|
||||
if err == nil {
|
||||
t.Error("Expected error for binary file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateStreamingUnifiedDiff(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
oldBundle := filepath.Join(tempDir, "old-bundle.tar.gz")
|
||||
newBundle := filepath.Join(tempDir, "new-bundle.tar.gz")
|
||||
|
||||
// Create bundles with different versions of the same file
|
||||
if err := writeTarGz(oldBundle, map[string]string{
|
||||
"root/config.yaml": "version: 1.0\napp: test\nfeature: disabled\n",
|
||||
}); err != nil {
|
||||
t.Fatalf("Failed to create old bundle: %v", err)
|
||||
}
|
||||
|
||||
if err := writeTarGz(newBundle, map[string]string{
|
||||
"root/config.yaml": "version: 2.0\napp: test\nfeature: enabled\n",
|
||||
}); err != nil {
|
||||
t.Fatalf("Failed to create new bundle: %v", err)
|
||||
}
|
||||
|
||||
// Generate streaming diff
|
||||
diff := generateStreamingUnifiedDiff(oldBundle, newBundle, "/config.yaml", 3, 100)
|
||||
|
||||
// Verify diff content
|
||||
if diff == "" {
|
||||
t.Error("Expected non-empty diff")
|
||||
}
|
||||
|
||||
expectedStrings := []string{
|
||||
"old:/config.yaml",
|
||||
"new:/config.yaml",
|
||||
"-version: 1.0",
|
||||
"+version: 2.0",
|
||||
"-feature: disabled",
|
||||
"+feature: enabled",
|
||||
}
|
||||
|
||||
for _, expected := range expectedStrings {
|
||||
if !strings.Contains(diff, expected) {
|
||||
t.Errorf("Diff missing expected string '%s'. Got: %s", expected, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadLinesFromReader(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
maxBytes int
|
||||
wantLen int
|
||||
wantLast string
|
||||
}{
|
||||
{
|
||||
name: "small content",
|
||||
content: "line1\nline2\nline3\n",
|
||||
maxBytes: 1000,
|
||||
wantLen: 3,
|
||||
wantLast: "line3\n",
|
||||
},
|
||||
{
|
||||
name: "content exceeds limit",
|
||||
content: "line1\nline2\nline3\nline4\nline5\n",
|
||||
maxBytes: 15, // Only allows first 2 lines plus truncation marker
|
||||
wantLen: 3,
|
||||
wantLast: "... (content truncated due to size)\n",
|
||||
},
|
||||
{
|
||||
name: "empty content",
|
||||
content: "",
|
||||
maxBytes: 1000,
|
||||
wantLen: 0,
|
||||
wantLast: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reader := strings.NewReader(tt.content)
|
||||
lines, err := readLinesFromReader(reader, tt.maxBytes)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("readLinesFromReader() error = %v", err)
|
||||
}
|
||||
|
||||
if len(lines) != tt.wantLen {
|
||||
t.Errorf("readLinesFromReader() got %d lines, want %d", len(lines), tt.wantLen)
|
||||
}
|
||||
|
||||
if tt.wantLen > 0 && lines[len(lines)-1] != tt.wantLast {
|
||||
t.Errorf("readLinesFromReader() last line = %s, want %s", lines[len(lines)-1], tt.wantLast)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
239
cmd/troubleshoot/cli/discovery_config.go
Normal file
239
cmd/troubleshoot/cli/discovery_config.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// DiscoveryConfig represents the configuration for auto-discovery
|
||||
type DiscoveryConfig struct {
|
||||
Version string `json:"version" yaml:"version"`
|
||||
Profiles map[string]DiscoveryProfile `json:"profiles" yaml:"profiles"`
|
||||
Patterns DiscoveryPatterns `json:"patterns" yaml:"patterns"`
|
||||
}
|
||||
|
||||
// DiscoveryPatterns defines inclusion/exclusion patterns for discovery
|
||||
type DiscoveryPatterns struct {
|
||||
NamespacePatterns PatternConfig `json:"namespacePatterns" yaml:"namespacePatterns"`
|
||||
ResourceTypePatterns PatternConfig `json:"resourceTypePatterns" yaml:"resourceTypePatterns"`
|
||||
RegistryPatterns PatternConfig `json:"registryPatterns" yaml:"registryPatterns"`
|
||||
}
|
||||
|
||||
// PatternConfig defines include/exclude patterns
|
||||
type PatternConfig struct {
|
||||
Include []string `json:"include" yaml:"include"`
|
||||
Exclude []string `json:"exclude" yaml:"exclude"`
|
||||
}
|
||||
|
||||
// LoadDiscoveryConfig loads discovery configuration from file or returns defaults
|
||||
func LoadDiscoveryConfig(configPath string) (*DiscoveryConfig, error) {
|
||||
// If no config path specified, use built-in defaults
|
||||
if configPath == "" {
|
||||
return getDefaultDiscoveryConfig(), nil
|
||||
}
|
||||
|
||||
// Check if config file exists
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
klog.V(2).Infof("Discovery config file not found: %s, using defaults", configPath)
|
||||
return getDefaultDiscoveryConfig(), nil
|
||||
}
|
||||
|
||||
// Read config file
|
||||
data, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read discovery config file")
|
||||
}
|
||||
|
||||
// Parse config file (support JSON for now)
|
||||
var config DiscoveryConfig
|
||||
if err := json.Unmarshal(data, &config); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse discovery config")
|
||||
}
|
||||
|
||||
// Validate config
|
||||
if err := validateDiscoveryConfig(&config); err != nil {
|
||||
return nil, errors.Wrap(err, "invalid discovery config")
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// getDefaultDiscoveryConfig returns the built-in default configuration
|
||||
func getDefaultDiscoveryConfig() *DiscoveryConfig {
|
||||
return &DiscoveryConfig{
|
||||
Version: "v1",
|
||||
Profiles: GetDiscoveryProfiles(),
|
||||
Patterns: DiscoveryPatterns{
|
||||
NamespacePatterns: PatternConfig{
|
||||
Include: []string{"*"}, // Include all by default
|
||||
Exclude: []string{
|
||||
"kube-system",
|
||||
"kube-public",
|
||||
"kube-node-lease",
|
||||
"kubernetes-dashboard",
|
||||
"cattle-*",
|
||||
"rancher-*",
|
||||
},
|
||||
},
|
||||
ResourceTypePatterns: PatternConfig{
|
||||
Include: []string{
|
||||
"pods",
|
||||
"deployments",
|
||||
"services",
|
||||
"configmaps",
|
||||
"secrets",
|
||||
"events",
|
||||
},
|
||||
Exclude: []string{
|
||||
"*.tmp",
|
||||
"*.log", // Exclude raw log files in favor of structured logs
|
||||
},
|
||||
},
|
||||
RegistryPatterns: PatternConfig{
|
||||
Include: []string{"*"}, // Include all registries
|
||||
Exclude: []string{}, // No exclusions by default
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// validateDiscoveryConfig validates a discovery configuration
|
||||
func validateDiscoveryConfig(config *DiscoveryConfig) error {
|
||||
if config.Version == "" {
|
||||
config.Version = "v1" // Default version
|
||||
}
|
||||
|
||||
if config.Profiles == nil {
|
||||
return errors.New("profiles section is required")
|
||||
}
|
||||
|
||||
// Validate each profile
|
||||
requiredProfiles := []string{"minimal", "standard", "comprehensive"}
|
||||
for _, profileName := range requiredProfiles {
|
||||
if _, exists := config.Profiles[profileName]; !exists {
|
||||
return fmt.Errorf("required profile missing: %s", profileName)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyDiscoveryPatterns applies include/exclude patterns to a list
|
||||
func ApplyDiscoveryPatterns(items []string, patterns PatternConfig) ([]string, error) {
|
||||
if len(patterns.Include) == 0 && len(patterns.Exclude) == 0 {
|
||||
return items, nil // No patterns to apply
|
||||
}
|
||||
|
||||
var result []string
|
||||
|
||||
for _, item := range items {
|
||||
include := true
|
||||
|
||||
// Check exclude patterns first
|
||||
for _, excludePattern := range patterns.Exclude {
|
||||
if matched, err := matchPattern(item, excludePattern); err != nil {
|
||||
return nil, errors.Wrapf(err, "invalid exclude pattern: %s", excludePattern)
|
||||
} else if matched {
|
||||
include = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If not excluded, check include patterns
|
||||
if include && len(patterns.Include) > 0 {
|
||||
include = false // Default to exclude if include patterns exist
|
||||
for _, includePattern := range patterns.Include {
|
||||
if matched, err := matchPattern(item, includePattern); err != nil {
|
||||
return nil, errors.Wrapf(err, "invalid include pattern: %s", includePattern)
|
||||
} else if matched {
|
||||
include = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if include {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// matchPattern checks if an item matches a glob pattern
|
||||
func matchPattern(item, pattern string) (bool, error) {
|
||||
// Simple glob pattern matching
|
||||
if pattern == "*" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if pattern == item {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Handle basic wildcard patterns
|
||||
if strings.HasPrefix(pattern, "*") && strings.HasSuffix(pattern, "*") {
|
||||
// Pattern is "*substring*"
|
||||
substring := pattern[1 : len(pattern)-1]
|
||||
return strings.Contains(item, substring), nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(pattern, "*") {
|
||||
// Pattern is "*suffix"
|
||||
suffix := pattern[1:]
|
||||
return strings.HasSuffix(item, suffix), nil
|
||||
}
|
||||
|
||||
if strings.HasSuffix(pattern, "*") {
|
||||
// Pattern is "prefix*"
|
||||
prefix := pattern[:len(pattern)-1]
|
||||
return strings.HasPrefix(item, prefix), nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// SaveDiscoveryConfig saves discovery configuration to a file
|
||||
func SaveDiscoveryConfig(config *DiscoveryConfig, configPath string) error {
|
||||
// Create directory if it doesn't exist
|
||||
dir := filepath.Dir(configPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return errors.Wrap(err, "failed to create config directory")
|
||||
}
|
||||
|
||||
// Marshal to JSON
|
||||
data, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to marshal config")
|
||||
}
|
||||
|
||||
// Write to file
|
||||
if err := ioutil.WriteFile(configPath, data, 0644); err != nil {
|
||||
return errors.Wrap(err, "failed to write config file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDiscoveryConfigPath returns the default path for discovery configuration
|
||||
func GetDiscoveryConfigPath() string {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "./troubleshoot-discovery.json"
|
||||
}
|
||||
|
||||
return filepath.Join(homeDir, ".troubleshoot", "discovery.json")
|
||||
}
|
||||
|
||||
// CreateDefaultDiscoveryConfigFile creates a default discovery config file
|
||||
func CreateDefaultDiscoveryConfigFile(configPath string) error {
|
||||
config := getDefaultDiscoveryConfig()
|
||||
return SaveDiscoveryConfig(config, configPath)
|
||||
}
|
||||
422
cmd/troubleshoot/cli/discovery_config_test.go
Normal file
422
cmd/troubleshoot/cli/discovery_config_test.go
Normal file
@@ -0,0 +1,422 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadDiscoveryConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupConfig func(string) error
|
||||
configPath string
|
||||
wantErr bool
|
||||
checkFunc func(*testing.T, *DiscoveryConfig)
|
||||
}{
|
||||
{
|
||||
name: "no config path - use defaults",
|
||||
configPath: "",
|
||||
wantErr: false,
|
||||
checkFunc: func(t *testing.T, config *DiscoveryConfig) {
|
||||
if config.Version != "v1" {
|
||||
t.Errorf("Expected version v1, got %s", config.Version)
|
||||
}
|
||||
if len(config.Profiles) == 0 {
|
||||
t.Error("Default config should have profiles")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-existent config file - use defaults",
|
||||
configPath: "/non/existent/path.json",
|
||||
wantErr: false,
|
||||
checkFunc: func(t *testing.T, config *DiscoveryConfig) {
|
||||
if config.Version != "v1" {
|
||||
t.Errorf("Expected version v1, got %s", config.Version)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid config file",
|
||||
setupConfig: func(path string) error {
|
||||
configContent := `{
|
||||
"version": "v1",
|
||||
"profiles": {
|
||||
"minimal": {
|
||||
"name": "minimal",
|
||||
"description": "Minimal collection",
|
||||
"includeImages": false,
|
||||
"rbacCheck": true,
|
||||
"maxDepth": 1,
|
||||
"timeout": 15000000000
|
||||
},
|
||||
"standard": {
|
||||
"name": "standard",
|
||||
"description": "Standard collection",
|
||||
"includeImages": false,
|
||||
"rbacCheck": true,
|
||||
"maxDepth": 2,
|
||||
"timeout": 30000000000
|
||||
},
|
||||
"comprehensive": {
|
||||
"name": "comprehensive",
|
||||
"description": "Comprehensive collection",
|
||||
"includeImages": true,
|
||||
"rbacCheck": true,
|
||||
"maxDepth": 3,
|
||||
"timeout": 60000000000
|
||||
}
|
||||
},
|
||||
"patterns": {
|
||||
"namespacePatterns": {
|
||||
"include": ["app-*"],
|
||||
"exclude": ["kube-*"]
|
||||
}
|
||||
}
|
||||
}`
|
||||
return os.WriteFile(path, []byte(configContent), 0644)
|
||||
},
|
||||
wantErr: false,
|
||||
checkFunc: func(t *testing.T, config *DiscoveryConfig) {
|
||||
if config.Version != "v1" {
|
||||
t.Errorf("Expected version v1, got %s", config.Version)
|
||||
}
|
||||
if len(config.Profiles) == 0 {
|
||||
t.Error("Config should have profiles")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid json config",
|
||||
setupConfig: func(path string) error {
|
||||
return os.WriteFile(path, []byte(`{invalid json`), 0644)
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var configPath string
|
||||
if tt.configPath != "" {
|
||||
configPath = tt.configPath
|
||||
}
|
||||
|
||||
// Setup config file if needed
|
||||
if tt.setupConfig != nil {
|
||||
tempDir := t.TempDir()
|
||||
configPath = filepath.Join(tempDir, "config.json")
|
||||
if err := tt.setupConfig(configPath); err != nil {
|
||||
t.Fatalf("Failed to setup config: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
config, err := LoadDiscoveryConfig(configPath)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("LoadDiscoveryConfig() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
if !tt.wantErr && tt.checkFunc != nil {
|
||||
tt.checkFunc(t, config)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyDiscoveryPatterns(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
items []string
|
||||
patterns PatternConfig
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "no patterns",
|
||||
items: []string{"app1", "app2", "kube-system"},
|
||||
patterns: PatternConfig{
|
||||
Include: []string{},
|
||||
Exclude: []string{},
|
||||
},
|
||||
want: []string{"app1", "app2", "kube-system"},
|
||||
},
|
||||
{
|
||||
name: "exclude patterns only",
|
||||
items: []string{"app1", "app2", "kube-system", "kube-public"},
|
||||
patterns: PatternConfig{
|
||||
Include: []string{},
|
||||
Exclude: []string{"kube-*"},
|
||||
},
|
||||
want: []string{"app1", "app2"},
|
||||
},
|
||||
{
|
||||
name: "include patterns only",
|
||||
items: []string{"app1", "app2", "kube-system"},
|
||||
patterns: PatternConfig{
|
||||
Include: []string{"app*"},
|
||||
Exclude: []string{},
|
||||
},
|
||||
want: []string{"app1", "app2"},
|
||||
},
|
||||
{
|
||||
name: "include and exclude patterns",
|
||||
items: []string{"app1", "app2", "app-system", "kube-system"},
|
||||
patterns: PatternConfig{
|
||||
Include: []string{"app*"},
|
||||
Exclude: []string{"*system"},
|
||||
},
|
||||
want: []string{"app1", "app2"}, // app-system excluded, kube-system not included
|
||||
},
|
||||
{
|
||||
name: "exact match patterns",
|
||||
items: []string{"app1", "app2", "special"},
|
||||
patterns: PatternConfig{
|
||||
Include: []string{"special", "app1"},
|
||||
Exclude: []string{},
|
||||
},
|
||||
want: []string{"app1", "special"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ApplyDiscoveryPatterns(tt.items, tt.patterns)
|
||||
if err != nil {
|
||||
t.Errorf("ApplyDiscoveryPatterns() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(got) != len(tt.want) {
|
||||
t.Errorf("ApplyDiscoveryPatterns() length = %v, want %v", len(got), len(tt.want))
|
||||
return
|
||||
}
|
||||
|
||||
// Check that all expected items are present
|
||||
gotMap := make(map[string]bool)
|
||||
for _, item := range got {
|
||||
gotMap[item] = true
|
||||
}
|
||||
|
||||
for _, wantItem := range tt.want {
|
||||
if !gotMap[wantItem] {
|
||||
t.Errorf("ApplyDiscoveryPatterns() missing expected item: %s", wantItem)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchPattern(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
item string
|
||||
pattern string
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "exact match",
|
||||
item: "app1",
|
||||
pattern: "app1",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "wildcard all",
|
||||
item: "anything",
|
||||
pattern: "*",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "prefix wildcard",
|
||||
item: "app-namespace",
|
||||
pattern: "app*",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "suffix wildcard",
|
||||
item: "kube-system",
|
||||
pattern: "*system",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "substring wildcard",
|
||||
item: "my-app-namespace",
|
||||
pattern: "*app*",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "no match",
|
||||
item: "different",
|
||||
pattern: "app*",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "prefix no match",
|
||||
item: "other-app",
|
||||
pattern: "app*",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "suffix no match",
|
||||
item: "system-app",
|
||||
pattern: "*system",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := matchPattern(tt.item, tt.pattern)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("matchPattern() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("matchPattern() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveDiscoveryConfig(t *testing.T) {
|
||||
config := getDefaultDiscoveryConfig()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
configPath := filepath.Join(tempDir, "test-config.json")
|
||||
|
||||
err := SaveDiscoveryConfig(config, configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("SaveDiscoveryConfig() error = %v", err)
|
||||
}
|
||||
|
||||
// Verify file was created
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
t.Error("SaveDiscoveryConfig() did not create config file")
|
||||
}
|
||||
|
||||
// Verify we can load it back
|
||||
loadedConfig, err := LoadDiscoveryConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to reload saved config: %v", err)
|
||||
}
|
||||
|
||||
if loadedConfig.Version != config.Version {
|
||||
t.Errorf("Reloaded config version = %v, want %v", loadedConfig.Version, config.Version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDiscoveryConfigPath(t *testing.T) {
|
||||
path := GetDiscoveryConfigPath()
|
||||
|
||||
if path == "" {
|
||||
t.Error("GetDiscoveryConfigPath() should not return empty string")
|
||||
}
|
||||
|
||||
// Should end with expected filename
|
||||
expectedSuffix := "discovery.json"
|
||||
if !strings.HasSuffix(path, expectedSuffix) {
|
||||
t.Errorf("GetDiscoveryConfigPath() should end with %s, got %s", expectedSuffix, path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDiscoveryConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config *DiscoveryConfig
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid config",
|
||||
config: &DiscoveryConfig{
|
||||
Version: "v1",
|
||||
Profiles: GetDiscoveryProfiles(),
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "missing version gets default",
|
||||
config: &DiscoveryConfig{
|
||||
Profiles: GetDiscoveryProfiles(),
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "nil profiles",
|
||||
config: &DiscoveryConfig{
|
||||
Version: "v1",
|
||||
Profiles: nil,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "missing required profile",
|
||||
config: &DiscoveryConfig{
|
||||
Version: "v1",
|
||||
Profiles: map[string]DiscoveryProfile{
|
||||
"custom": {Name: "custom"},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateDiscoveryConfig(tt.config)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("validateDiscoveryConfig() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMatchPattern(b *testing.B) {
|
||||
testCases := []struct {
|
||||
item string
|
||||
pattern string
|
||||
}{
|
||||
{"app-namespace", "app*"},
|
||||
{"kube-system", "*system"},
|
||||
{"my-app-test", "*app*"},
|
||||
{"exact-match", "exact-match"},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, tc := range testCases {
|
||||
_, err := matchPattern(tc.item, tc.pattern)
|
||||
if err != nil {
|
||||
b.Fatalf("matchPattern failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkApplyDiscoveryPatterns(b *testing.B) {
|
||||
items := make([]string, 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
if i%3 == 0 {
|
||||
items[i] = fmt.Sprintf("app-%d", i)
|
||||
} else if i%3 == 1 {
|
||||
items[i] = fmt.Sprintf("kube-system-%d", i)
|
||||
} else {
|
||||
items[i] = fmt.Sprintf("other-%d", i)
|
||||
}
|
||||
}
|
||||
|
||||
patterns := PatternConfig{
|
||||
Include: []string{"app*", "other*"},
|
||||
Exclude: []string{"*system*"},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := ApplyDiscoveryPatterns(items, patterns)
|
||||
if err != nil {
|
||||
b.Fatalf("ApplyDiscoveryPatterns failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/replicatedhq/troubleshoot/internal/traces"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/logger"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/updater"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"k8s.io/klog/v2"
|
||||
@@ -40,6 +41,28 @@ If no arguments are provided, specs are automatically loaded from the cluster by
|
||||
if err := util.StartProfiling(); err != nil {
|
||||
klog.Errorf("Failed to start profiling: %v", err)
|
||||
}
|
||||
|
||||
// Auto-update support-bundle unless disabled by flag or env
|
||||
// Only run auto-update for the root support-bundle command, not subcommands
|
||||
if cmd.Name() == "support-bundle" && !cmd.HasParent() {
|
||||
envAuto := os.Getenv("TROUBLESHOOT_AUTO_UPDATE")
|
||||
autoFromEnv := true
|
||||
if envAuto != "" {
|
||||
if strings.EqualFold(envAuto, "0") || strings.EqualFold(envAuto, "false") {
|
||||
autoFromEnv = false
|
||||
}
|
||||
}
|
||||
if v.GetBool("auto-update") && autoFromEnv {
|
||||
exe, err := os.Executable()
|
||||
if err == nil {
|
||||
_ = updater.CheckAndUpdate(cmd.Context(), updater.Options{
|
||||
BinaryName: "support-bundle",
|
||||
CurrentPath: exe,
|
||||
Printf: func(f string, a ...interface{}) { fmt.Fprintf(os.Stderr, f, a...) },
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
v := viper.GetViper()
|
||||
@@ -82,10 +105,22 @@ If no arguments are provided, specs are automatically loaded from the cluster by
|
||||
|
||||
cmd.AddCommand(Analyze())
|
||||
cmd.AddCommand(Redact())
|
||||
cmd.AddCommand(Diff())
|
||||
cmd.AddCommand(Schedule())
|
||||
cmd.AddCommand(UploadCmd())
|
||||
cmd.AddCommand(util.VersionCmd())
|
||||
|
||||
cmd.Flags().StringSlice("redactors", []string{}, "names of the additional redactors to use")
|
||||
cmd.Flags().Bool("redact", true, "enable/disable default redactions")
|
||||
|
||||
// Tokenization flags
|
||||
cmd.Flags().Bool("tokenize", false, "enable intelligent tokenization instead of simple masking (replaces ***HIDDEN*** with ***TOKEN_TYPE_HASH***)")
|
||||
cmd.Flags().String("redaction-map", "", "generate redaction mapping file at specified path (enables token→original mapping for authorized access)")
|
||||
cmd.Flags().Bool("encrypt-redaction-map", false, "encrypt the redaction mapping file using AES-256 (requires --redaction-map)")
|
||||
cmd.Flags().String("token-prefix", "", "custom token prefix format (default: ***TOKEN_%s_%s***)")
|
||||
cmd.Flags().Bool("verify-tokenization", false, "validation mode: verify tokenization setup without collecting data")
|
||||
cmd.Flags().String("bundle-id", "", "custom bundle identifier for token correlation (auto-generated if not provided)")
|
||||
cmd.Flags().Bool("tokenization-stats", false, "include detailed tokenization statistics in output")
|
||||
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")
|
||||
@@ -95,6 +130,16 @@ If no arguments are provided, specs are automatically loaded from the cluster by
|
||||
cmd.Flags().StringP("output", "o", "", "specify the output file path for the support bundle")
|
||||
cmd.Flags().Bool("debug", false, "enable debug logging. This is equivalent to --v=0")
|
||||
cmd.Flags().Bool("dry-run", false, "print support bundle spec without collecting anything")
|
||||
cmd.Flags().Bool("auto-update", true, "enable automatic binary self-update check and install")
|
||||
|
||||
// Auto-discovery flags
|
||||
cmd.Flags().Bool("auto", false, "enable auto-discovery of foundational collectors. When used with YAML specs, adds foundational collectors to YAML collectors. When used alone, collects only foundational data")
|
||||
cmd.Flags().Bool("include-images", false, "include container image metadata collection when using auto-discovery")
|
||||
cmd.Flags().Bool("rbac-check", true, "enable RBAC permission checking for auto-discovered collectors")
|
||||
cmd.Flags().String("discovery-profile", "standard", "auto-discovery profile: minimal, standard, comprehensive, or paranoid")
|
||||
cmd.Flags().StringSlice("exclude-namespaces", []string{}, "namespaces to exclude from auto-discovery (supports glob patterns)")
|
||||
cmd.Flags().StringSlice("include-namespaces", []string{}, "namespaces to include in auto-discovery (supports glob patterns). If specified, only these namespaces will be included")
|
||||
cmd.Flags().Bool("include-system-namespaces", false, "include system namespaces (kube-system, etc.) in auto-discovery")
|
||||
|
||||
// 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")
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -27,6 +28,7 @@ import (
|
||||
"github.com/replicatedhq/troubleshoot/pkg/httputil"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/loader"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/redact"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/supportbundle"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/types"
|
||||
"github.com/spf13/viper"
|
||||
@@ -55,6 +57,30 @@ func runTroubleshoot(v *viper.Viper, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate auto-discovery flags
|
||||
if err := ValidateAutoDiscoveryFlags(v); err != nil {
|
||||
return errors.Wrap(err, "invalid auto-discovery configuration")
|
||||
}
|
||||
|
||||
// Validate tokenization flags
|
||||
if err := ValidateTokenizationFlags(v); err != nil {
|
||||
return errors.Wrap(err, "invalid tokenization configuration")
|
||||
}
|
||||
// Apply auto-discovery if enabled
|
||||
autoConfig := GetAutoDiscoveryConfig(v)
|
||||
if autoConfig.Enabled {
|
||||
mode := GetAutoDiscoveryMode(args, autoConfig.Enabled)
|
||||
if !v.GetBool("quiet") {
|
||||
PrintAutoDiscoveryInfo(autoConfig, mode)
|
||||
}
|
||||
|
||||
// Apply auto-discovery to the main bundle
|
||||
namespace := v.GetString("namespace")
|
||||
if err := ApplyAutoDiscovery(ctx, client, restConfig, mainBundle, autoConfig, namespace); err != nil {
|
||||
return errors.Wrap(err, "auto-discovery failed")
|
||||
}
|
||||
}
|
||||
|
||||
// For --dry-run, we want to print the yaml and exit
|
||||
if v.GetBool("dry-run") {
|
||||
k := loader.TroubleshootKinds{
|
||||
@@ -185,6 +211,15 @@ func runTroubleshoot(v *viper.Viper, args []string) error {
|
||||
Redact: v.GetBool("redact"),
|
||||
FromCLI: true,
|
||||
RunHostCollectorsInPod: mainBundle.Spec.RunHostCollectorsInPod,
|
||||
|
||||
// Phase 4: Tokenization options
|
||||
Tokenize: v.GetBool("tokenize"),
|
||||
RedactionMapPath: v.GetString("redaction-map"),
|
||||
EncryptRedactionMap: v.GetBool("encrypt-redaction-map"),
|
||||
TokenPrefix: v.GetString("token-prefix"),
|
||||
VerifyTokenization: v.GetBool("verify-tokenization"),
|
||||
BundleID: v.GetString("bundle-id"),
|
||||
TokenizationStats: v.GetBool("tokenization-stats"),
|
||||
}
|
||||
|
||||
nonInteractiveOutput := analysisOutput{}
|
||||
@@ -314,10 +349,12 @@ func loadSpecs(ctx context.Context, args []string, client kubernetes.Interface)
|
||||
}
|
||||
|
||||
// Check if we have any collectors to run in the troubleshoot specs
|
||||
// TODO: Do we use the RemoteCollectors anymore?
|
||||
// Skip this check if auto-discovery is enabled, as collectors will be added later
|
||||
// Note: RemoteCollectors are still actively used in preflights and host preflights
|
||||
if len(kinds.CollectorsV1Beta2) == 0 &&
|
||||
len(kinds.HostCollectorsV1Beta2) == 0 &&
|
||||
len(kinds.SupportBundlesV1Beta2) == 0 {
|
||||
len(kinds.SupportBundlesV1Beta2) == 0 &&
|
||||
!vp.GetBool("auto") {
|
||||
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"),
|
||||
@@ -337,6 +374,25 @@ func loadSpecs(ctx context.Context, args []string, client kubernetes.Interface)
|
||||
},
|
||||
}
|
||||
|
||||
// If auto-discovery is enabled and no support bundle specs were loaded,
|
||||
// create a minimal default support bundle spec for auto-discovery to work with
|
||||
if vp.GetBool("auto") && len(kinds.SupportBundlesV1Beta2) == 0 {
|
||||
defaultSupportBundle := troubleshootv1beta2.SupportBundle{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "troubleshoot.replicated.com/v1beta2",
|
||||
Kind: "SupportBundle",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "auto-discovery-default",
|
||||
},
|
||||
Spec: troubleshootv1beta2.SupportBundleSpec{
|
||||
Collectors: []*troubleshootv1beta2.Collect{}, // Empty collectors - will be populated by auto-discovery
|
||||
},
|
||||
}
|
||||
kinds.SupportBundlesV1Beta2 = append(kinds.SupportBundlesV1Beta2, defaultSupportBundle)
|
||||
klog.V(2).Info("Created default support bundle spec for auto-discovery")
|
||||
}
|
||||
|
||||
var enableRunHostCollectorsInPod bool
|
||||
|
||||
for _, sb := range kinds.SupportBundlesV1Beta2 {
|
||||
@@ -357,8 +413,9 @@ func loadSpecs(ctx context.Context, args []string, client kubernetes.Interface)
|
||||
mainBundle.Spec.HostCollectors = util.Append(mainBundle.Spec.HostCollectors, hc.Spec.Collectors)
|
||||
}
|
||||
|
||||
if !(len(mainBundle.Spec.HostCollectors) > 0 && len(mainBundle.Spec.Collectors) == 0) {
|
||||
// Always add default collectors unless we only have host collectors
|
||||
// Don't add default collectors if auto-discovery is enabled, as auto-discovery will add them
|
||||
if !(len(mainBundle.Spec.HostCollectors) > 0 && len(mainBundle.Spec.Collectors) == 0) && !vp.GetBool("auto") {
|
||||
// Always add default collectors unless we only have host collectors or auto-discovery is enabled
|
||||
// We need to add them here so when we --dry-run, these collectors
|
||||
// are included. supportbundle.runCollectors duplicates this bit.
|
||||
// We'll need to refactor it out later when its clearer what other
|
||||
@@ -375,7 +432,7 @@ func loadSpecs(ctx context.Context, args []string, client kubernetes.Interface)
|
||||
|
||||
additionalRedactors := &troubleshootv1beta2.Redactor{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "troubleshoot.sh/v1beta2",
|
||||
APIVersion: "troubleshoot.replicated.com/v1beta2",
|
||||
Kind: "Redactor",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -444,3 +501,106 @@ func (a *analysisOutput) FormattedAnalysisOutput() (outputJson string, err error
|
||||
}
|
||||
return string(formatted), nil
|
||||
}
|
||||
|
||||
// ValidateTokenizationFlags validates tokenization flag combinations
|
||||
func ValidateTokenizationFlags(v *viper.Viper) error {
|
||||
// Verify tokenization mode early (before collection starts)
|
||||
if v.GetBool("verify-tokenization") {
|
||||
if err := VerifyTokenizationSetup(v); err != nil {
|
||||
return errors.Wrap(err, "tokenization verification failed")
|
||||
}
|
||||
fmt.Println("✅ Tokenization verification passed")
|
||||
os.Exit(0) // Exit after verification
|
||||
}
|
||||
|
||||
// Encryption requires redaction map
|
||||
if v.GetBool("encrypt-redaction-map") && v.GetString("redaction-map") == "" {
|
||||
return errors.New("--encrypt-redaction-map requires --redaction-map to be specified")
|
||||
}
|
||||
|
||||
// Redaction map requires tokenization or redaction to be enabled
|
||||
if v.GetString("redaction-map") != "" {
|
||||
if !v.GetBool("tokenize") && !v.GetBool("redact") {
|
||||
return errors.New("--redaction-map requires either --tokenize or --redact to be enabled")
|
||||
}
|
||||
}
|
||||
|
||||
// Custom token prefix requires tokenization
|
||||
if v.GetString("token-prefix") != "" && !v.GetBool("tokenize") {
|
||||
return errors.New("--token-prefix requires --tokenize to be enabled")
|
||||
}
|
||||
|
||||
// Bundle ID requires tokenization
|
||||
if v.GetString("bundle-id") != "" && !v.GetBool("tokenize") {
|
||||
return errors.New("--bundle-id requires --tokenize to be enabled")
|
||||
}
|
||||
|
||||
// Tokenization stats requires tokenization
|
||||
if v.GetBool("tokenization-stats") && !v.GetBool("tokenize") {
|
||||
return errors.New("--tokenization-stats requires --tokenize to be enabled")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyTokenizationSetup verifies tokenization configuration without collecting data
|
||||
func VerifyTokenizationSetup(v *viper.Viper) error {
|
||||
fmt.Println("🔍 Verifying tokenization setup...")
|
||||
|
||||
// Test 1: Environment variable check
|
||||
if v.GetBool("tokenize") {
|
||||
os.Setenv("TROUBLESHOOT_TOKENIZATION", "true")
|
||||
defer os.Unsetenv("TROUBLESHOOT_TOKENIZATION")
|
||||
}
|
||||
|
||||
// Test 2: Tokenizer initialization
|
||||
redact.ResetGlobalTokenizer()
|
||||
tokenizer := redact.GetGlobalTokenizer()
|
||||
|
||||
if v.GetBool("tokenize") && !tokenizer.IsEnabled() {
|
||||
return errors.New("tokenizer is not enabled despite --tokenize flag")
|
||||
}
|
||||
|
||||
if !v.GetBool("tokenize") && tokenizer.IsEnabled() {
|
||||
return errors.New("tokenizer is enabled despite --tokenize flag being false")
|
||||
}
|
||||
|
||||
fmt.Printf(" ✅ Tokenizer state: %v\n", tokenizer.IsEnabled())
|
||||
|
||||
// Test 3: Token generation
|
||||
if tokenizer.IsEnabled() {
|
||||
testToken := tokenizer.TokenizeValue("test-secret", "verification")
|
||||
if !tokenizer.ValidateToken(testToken) {
|
||||
return errors.Errorf("generated test token is invalid: %s", testToken)
|
||||
}
|
||||
fmt.Printf(" ✅ Test token generated: %s\n", testToken)
|
||||
}
|
||||
|
||||
// Test 4: Custom token prefix validation
|
||||
if customPrefix := v.GetString("token-prefix"); customPrefix != "" {
|
||||
if !strings.Contains(customPrefix, "%s") {
|
||||
return errors.Errorf("custom token prefix must contain %%s placeholders: %s", customPrefix)
|
||||
}
|
||||
fmt.Printf(" ✅ Custom token prefix validated: %s\n", customPrefix)
|
||||
}
|
||||
|
||||
// Test 5: Redaction map path validation
|
||||
if mapPath := v.GetString("redaction-map"); mapPath != "" {
|
||||
// Check if directory exists
|
||||
dir := filepath.Dir(mapPath)
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
return errors.Errorf("redaction map directory does not exist: %s", dir)
|
||||
}
|
||||
fmt.Printf(" ✅ Redaction map path validated: %s\n", mapPath)
|
||||
|
||||
// Test file creation (and cleanup)
|
||||
testFile := mapPath + ".test"
|
||||
if err := os.WriteFile(testFile, []byte("test"), 0600); err != nil {
|
||||
return errors.Errorf("cannot create redaction map file: %v", err)
|
||||
}
|
||||
os.Remove(testFile)
|
||||
fmt.Printf(" ✅ File creation permissions verified\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -140,10 +140,11 @@ func Test_loadSupportBundleSpecsFromURIs_TimeoutError(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Set the timeout on the http client to 10ms
|
||||
// Set the timeout on the http client to 500ms
|
||||
// The server sleeps for 2 seconds, so this should still timeout
|
||||
// supportbundle.LoadSupportBundleSpec does not yet use the context
|
||||
before := httputil.GetHttpClient().Timeout
|
||||
httputil.GetHttpClient().Timeout = 10 * time.Millisecond
|
||||
httputil.GetHttpClient().Timeout = 500 * time.Millisecond
|
||||
defer func() {
|
||||
// Reinstate the original timeout. Its a global var so we need to reset it
|
||||
httputil.GetHttpClient().Timeout = before
|
||||
|
||||
11
cmd/troubleshoot/cli/schedule.go
Normal file
11
cmd/troubleshoot/cli/schedule.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/replicatedhq/troubleshoot/pkg/schedule"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Schedule returns the schedule command for managing scheduled support bundle jobs
|
||||
func Schedule() *cobra.Command {
|
||||
return schedule.CLI()
|
||||
}
|
||||
56
cmd/troubleshoot/cli/upload.go
Normal file
56
cmd/troubleshoot/cli/upload.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/supportbundle"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func UploadCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "upload [bundle-file]",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Short: "Upload a support bundle to replicated.app",
|
||||
Long: `Upload a support bundle to replicated.app for analysis and troubleshooting.
|
||||
|
||||
This command automatically extracts the license ID and app slug from the bundle if not provided.
|
||||
|
||||
Examples:
|
||||
# Auto-detect license and app from bundle
|
||||
support-bundle upload bundle.tar.gz
|
||||
|
||||
# Specify license ID explicitly
|
||||
support-bundle upload bundle.tar.gz --license-id YOUR_LICENSE_ID
|
||||
|
||||
# Specify both license and app
|
||||
support-bundle upload bundle.tar.gz --license-id YOUR_LICENSE_ID --app-slug my-app`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
v := viper.GetViper()
|
||||
bundlePath := args[0]
|
||||
|
||||
// Check if bundle file exists
|
||||
if _, err := os.Stat(bundlePath); os.IsNotExist(err) {
|
||||
return errors.Errorf("bundle file does not exist: %s", bundlePath)
|
||||
}
|
||||
|
||||
// Get upload parameters
|
||||
licenseID := v.GetString("license-id")
|
||||
appSlug := v.GetString("app-slug")
|
||||
|
||||
// Use auto-detection for uploads
|
||||
if err := supportbundle.UploadBundleAutoDetect(bundlePath, licenseID, appSlug); err != nil {
|
||||
return errors.Wrap(err, "upload failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().String("license-id", "", "license ID for authentication (auto-detected from bundle if not provided)")
|
||||
cmd.Flags().String("app-slug", "", "application slug (auto-detected from bundle if not provided)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -1,85 +1,64 @@
|
||||
version: 2
|
||||
project_name: troubleshoot
|
||||
|
||||
release:
|
||||
prerelease: auto
|
||||
|
||||
builds:
|
||||
- id: preflight
|
||||
# NOTE: if you add any additional goos/goarch values, ensure you update ../.github/workflows/build-test-deploy.yaml
|
||||
# specifically the matrix values for goreleaser-test
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- riscv64
|
||||
main: ./cmd/preflight/main.go
|
||||
env: [CGO_ENABLED=0]
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm, arm64]
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
main: cmd/preflight/main.go
|
||||
ldflags: -s -w
|
||||
-X github.com/replicatedhq/troubleshoot/pkg/version.version={{.Version}}
|
||||
-X github.com/replicatedhq/troubleshoot/pkg/version.gitSHA={{.Commit}}
|
||||
-X github.com/replicatedhq/troubleshoot/pkg/version.buildTime={{.Date}}
|
||||
-extldflags "-static"
|
||||
flags: -tags netgo -tags containers_image_ostree_stub -tags exclude_graphdriver_devicemapper -tags exclude_graphdriver_btrfs -tags containers_image_openpgp -installsuffix netgo
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X github.com/replicatedhq/troubleshoot/pkg/version.version={{ .Version }}
|
||||
- -X github.com/replicatedhq/troubleshoot/pkg/version.gitSHA={{ .Commit }}
|
||||
- -X github.com/replicatedhq/troubleshoot/pkg/version.buildTime={{ .Date }}
|
||||
- -extldflags "-static"
|
||||
flags:
|
||||
- -tags=netgo
|
||||
- -tags=containers_image_ostree_stub
|
||||
- -tags=exclude_graphdriver_devicemapper
|
||||
- -tags=exclude_graphdriver_btrfs
|
||||
- -tags=containers_image_openpgp
|
||||
- -installsuffix=netgo
|
||||
binary: preflight
|
||||
hooks: {}
|
||||
|
||||
- id: support-bundle
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- riscv64
|
||||
main: ./cmd/troubleshoot/main.go
|
||||
env: [CGO_ENABLED=0]
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm, arm64]
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
main: cmd/troubleshoot/main.go
|
||||
ldflags: -s -w
|
||||
-X github.com/replicatedhq/troubleshoot/pkg/version.version={{.Version}}
|
||||
-X github.com/replicatedhq/troubleshoot/pkg/version.gitSHA={{.Commit}}
|
||||
-X github.com/replicatedhq/troubleshoot/pkg/version.buildTime={{.Date}}
|
||||
-extldflags "-static"
|
||||
flags: -tags netgo -tags containers_image_ostree_stub -tags exclude_graphdriver_devicemapper -tags exclude_graphdriver_btrfs -tags containers_image_openpgp -installsuffix netgo
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X github.com/replicatedhq/troubleshoot/pkg/version.version={{ .Version }}
|
||||
- -X github.com/replicatedhq/troubleshoot/pkg/version.gitSHA={{ .Commit }}
|
||||
- -X github.com/replicatedhq/troubleshoot/pkg/version.buildTime={{ .Date }}
|
||||
- -extldflags "-static"
|
||||
flags:
|
||||
- -tags=netgo
|
||||
- -tags=containers_image_ostree_stub
|
||||
- -tags=exclude_graphdriver_devicemapper
|
||||
- -tags=exclude_graphdriver_btrfs
|
||||
- -tags=containers_image_openpgp
|
||||
- -installsuffix=netgo
|
||||
binary: support-bundle
|
||||
hooks: {}
|
||||
- id: collect
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- riscv64
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
main: cmd/collect/main.go
|
||||
ldflags: -s -w
|
||||
-X github.com/replicatedhq/troubleshoot/pkg/version.version={{.Version}}
|
||||
-X github.com/replicatedhq/troubleshoot/pkg/version.gitSHA={{.Commit}}
|
||||
-X github.com/replicatedhq/troubleshoot/pkg/version.buildTime={{.Date}}
|
||||
-extldflags "-static"
|
||||
flags: -tags netgo -tags containers_image_ostree_stub -tags exclude_graphdriver_devicemapper -tags exclude_graphdriver_btrfs -tags containers_image_openpgp -installsuffix netgo
|
||||
binary: collect
|
||||
hooks: {}
|
||||
|
||||
archives:
|
||||
- id: preflight
|
||||
builds:
|
||||
- preflight
|
||||
format: tar.gz
|
||||
ids: [preflight]
|
||||
formats: [tar.gz]
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
name_template: 'preflight_{{ .Os }}_{{ .Arch }}'
|
||||
formats: [zip]
|
||||
name_template: "preflight_{{ .Os }}_{{ .Arch }}"
|
||||
files:
|
||||
- licence*
|
||||
- LICENCE*
|
||||
@@ -89,17 +68,16 @@ archives:
|
||||
- README*
|
||||
- changelog*
|
||||
- CHANGELOG*
|
||||
- src: 'sbom/assets/*'
|
||||
- src: "sbom/assets/*"
|
||||
dst: .
|
||||
strip_parent: true # this is needed to make up for the way unzips work in krew v0.4.1
|
||||
strip_parent: true
|
||||
- id: support-bundle
|
||||
builds:
|
||||
- support-bundle
|
||||
format: tar.gz
|
||||
ids: [support-bundle]
|
||||
formats: [tar.gz]
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
name_template: 'support-bundle_{{ .Os }}_{{ .Arch }}'
|
||||
formats: [zip]
|
||||
name_template: "support-bundle_{{ .Os }}_{{ .Arch }}"
|
||||
files:
|
||||
- licence*
|
||||
- LICENCE*
|
||||
@@ -109,17 +87,14 @@ archives:
|
||||
- README*
|
||||
- changelog*
|
||||
- CHANGELOG*
|
||||
- src: 'sbom/assets/*'
|
||||
- src: "sbom/assets/*"
|
||||
dst: .
|
||||
strip_parent: true # this is needed to make up for the way unzips work in krew v0.4.1
|
||||
- id: collect
|
||||
builds:
|
||||
- collect
|
||||
format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
name_template: 'collect_{{ .Os }}_{{ .Arch }}'
|
||||
strip_parent: true
|
||||
|
||||
- id: preflight-universal
|
||||
ids: [preflight-universal]
|
||||
formats: [tar.gz]
|
||||
name_template: "preflight_{{ .Os }}_{{ .Arch }}"
|
||||
files:
|
||||
- licence*
|
||||
- LICENCE*
|
||||
@@ -129,9 +104,27 @@ archives:
|
||||
- README*
|
||||
- changelog*
|
||||
- CHANGELOG*
|
||||
- src: 'sbom/assets/*'
|
||||
- src: "sbom/assets/*"
|
||||
dst: .
|
||||
strip_parent: true # this is needed to make up for the way unzips work in krew v0.4.1
|
||||
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
|
||||
image_templates:
|
||||
@@ -142,7 +135,7 @@ dockers:
|
||||
ids:
|
||||
- support-bundle
|
||||
- preflight
|
||||
- collect
|
||||
skip_push: true
|
||||
- dockerfile: ./deploy/Dockerfile.troubleshoot
|
||||
image_templates:
|
||||
- "replicated/preflight:latest"
|
||||
@@ -152,4 +145,37 @@ dockers:
|
||||
ids:
|
||||
- support-bundle
|
||||
- preflight
|
||||
- collect
|
||||
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]
|
||||
homepage: https://docs.replicated.com/reference/preflight-overview/
|
||||
description: "A preflight checker and conformance test for Kubernetes clusters."
|
||||
repository:
|
||||
owner: replicatedhq
|
||||
name: homebrew-replicated
|
||||
branch: main
|
||||
directory: HomebrewFormula
|
||||
install: bin.install "preflight"
|
||||
- name: support-bundle
|
||||
ids: [support-bundle, support-bundle-universal]
|
||||
homepage: https://docs.replicated.com/reference/support-bundle-overview/
|
||||
description: "Collect and redact support bundles for Kubernetes clusters."
|
||||
repository:
|
||||
owner: replicatedhq
|
||||
name: homebrew-replicated
|
||||
branch: main
|
||||
directory: HomebrewFormula
|
||||
install: bin.install "support-bundle"
|
||||
|
||||
@@ -7,7 +7,6 @@ RUN apt-get -qq update \
|
||||
|
||||
COPY support-bundle /troubleshoot/support-bundle
|
||||
COPY preflight /troubleshoot/preflight
|
||||
COPY collect /troubleshoot/collect
|
||||
|
||||
ENV PATH="/troubleshoot:${PATH}"
|
||||
|
||||
|
||||
1427
docs/Person-2-PRD.md
Normal file
1427
docs/Person-2-PRD.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
## preflight
|
||||
## preflight
|
||||
|
||||
Run and retrieve preflight checks in a cluster
|
||||
|
||||
@@ -17,7 +17,8 @@ preflight [url] [flags]
|
||||
--as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace.
|
||||
--as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups.
|
||||
--as-uid string UID to impersonate for the operation.
|
||||
--cache-dir string Default cache directory (default "$HOME/.kube/cache")
|
||||
--auto-update enable automatic binary self-update check and install (default true)
|
||||
--cache-dir string Default cache directory (default "/Users/marccampbell/.kube/cache")
|
||||
--certificate-authority string Path to a cert file for the certificate authority
|
||||
--client-certificate string Path to a client certificate file for TLS
|
||||
--client-key string Path to a client key file for TLS
|
||||
@@ -52,7 +53,9 @@ preflight [url] [flags]
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [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
|
||||
* [preflight oci-fetch](preflight_oci-fetch.md) - Fetch a preflight from an OCI registry and print it to standard out
|
||||
* [preflight template](preflight_template.md) - Render a templated preflight spec with values
|
||||
* [preflight docs](preflight_docs.md) - Extract and display documentation from a preflight spec
|
||||
* [preflight version](preflight_version.md) - Print the current version and exit
|
||||
|
||||
###### Auto generated by spf13/cobra on 23-Aug-2024
|
||||
###### Auto generated by spf13/cobra on 15-Sep-2025
|
||||
|
||||
60
docs/preflight_docs.md
Normal file
60
docs/preflight_docs.md
Normal file
@@ -0,0 +1,60 @@
|
||||
## preflight docs
|
||||
|
||||
Extract and display documentation from a preflight spec
|
||||
|
||||
### Synopsis
|
||||
|
||||
Extract all `docString` fields from enabled analyzers in one or more preflight YAML files. Templating is evaluated first using the provided values, so only documentation for analyzers that are enabled is emitted. The output is Markdown.
|
||||
|
||||
```
|
||||
preflight docs [preflight-file...] [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
# Extract docs with defaults
|
||||
preflight docs ml-platform-preflight.yaml
|
||||
|
||||
# Multiple specs with values files (later values override earlier ones)
|
||||
preflight docs spec1.yaml spec2.yaml \
|
||||
--values values-base.yaml --values values-prod.yaml
|
||||
|
||||
# Inline overrides (Helm-style --set)
|
||||
preflight docs ml-platform-preflight.yaml \
|
||||
--set monitoring.enabled=true --set ingress.enabled=false
|
||||
|
||||
# Save to file
|
||||
preflight docs ml-platform-preflight.yaml -o requirements.md
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
--values stringArray Path to YAML files containing template values (can be used multiple times)
|
||||
--set stringArray Set template values on the command line (can be used multiple times)
|
||||
-o, --output string Output file (default: stdout)
|
||||
```
|
||||
|
||||
### Behavior
|
||||
|
||||
- Accepts one or more preflight specs; all are rendered, and their docStrings are concatenated in input order.
|
||||
- Values merge: deep-merged left-to-right across `--values` files. `--set` overrides win last.
|
||||
- Rendering engine:
|
||||
- If a spec references `.Values`, it is rendered with the Helm engine; otherwise Go text/template is used. A fallback to the legacy engine is applied for mixed templates.
|
||||
- Map normalization: values maps are normalized to `map[string]interface{}` before applying `--set` to avoid type errors.
|
||||
- Markdown formatting:
|
||||
- The first line starting with `Title:` in a `docString` becomes a Markdown heading.
|
||||
- If no `Title:` is present, the analyzer (or requirement) name is used.
|
||||
- Sections are separated by blank lines.
|
||||
|
||||
### v1beta3 docString extraction
|
||||
|
||||
- v1beta3 layout uses `spec.analyzers: [...]`.
|
||||
- Each analyzer may include a sibling `docString` string.
|
||||
- The docs command extracts `spec.analyzers[*].docString` after rendering.
|
||||
- Backward compatibility: legacy `requirements` blocks are still supported and extracted when present.
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [preflight](preflight.md) - Run and retrieve preflight checks in a cluster
|
||||
@@ -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 23-Aug-2024
|
||||
###### Auto generated by spf13/cobra on 15-Sep-2025
|
||||
|
||||
56
docs/preflight_template.md
Normal file
56
docs/preflight_template.md
Normal file
@@ -0,0 +1,56 @@
|
||||
## preflight template
|
||||
|
||||
Render a templated preflight spec with values
|
||||
|
||||
### Synopsis
|
||||
|
||||
Process a templated preflight YAML file, substituting variables and removing conditional sections based on provided values. Supports multiple values files and inline overrides. Outputs the fully-resolved YAML (no conditional logic remains).
|
||||
|
||||
```
|
||||
preflight template [template-file] [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
# Render with defaults only
|
||||
preflight template sample-preflight-templated.yaml
|
||||
|
||||
# Render with multiple values files (later files override earlier ones)
|
||||
preflight template sample-preflight-templated.yaml \
|
||||
--values values-base.yaml --values values-prod.yaml
|
||||
|
||||
# Inline overrides (Helm-style --set)
|
||||
preflight template sample-preflight-templated.yaml \
|
||||
--set kubernetes.minVersion=v1.24.0 --set storage.enabled=true
|
||||
|
||||
# Save to file
|
||||
preflight template sample-preflight-templated.yaml -o rendered.yaml
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
--values stringArray Path to YAML files containing template values (can be used multiple times)
|
||||
--set stringArray Set template values on the command line (can be used multiple times)
|
||||
-o, --output string Output file (default: stdout)
|
||||
```
|
||||
|
||||
### Behavior
|
||||
|
||||
- Values merge: deep-merged left-to-right across multiple `--values` files. `--set` overrides win last.
|
||||
- Rendering engine:
|
||||
- v1beta3 specs (Helm-style templates using `.Values.*`) are rendered with the Helm engine.
|
||||
- Legacy templates are rendered with Go text/template; mixed templates are supported.
|
||||
- Map normalization: values files are normalized to `map[string]interface{}` before applying `--set` (avoids type errors when merging Helm `strvals`).
|
||||
|
||||
### v1beta3 spec decisions
|
||||
|
||||
- Layout aligns with v1beta2: `spec.analyzers: [...]`.
|
||||
- Each analyzer accepts an optional `docString` used by `preflight docs`.
|
||||
- Templating style is Helm-oriented (`.Values.*`).
|
||||
- Modularity via conditional analyzers is supported, e.g. `{{- if .Values.ingress.enabled }}`.
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [preflight](preflight.md) - Run and retrieve preflight checks in a cluster
|
||||
@@ -37,4 +37,4 @@ preflight version [flags]
|
||||
|
||||
* [preflight](preflight.md) - Run and retrieve preflight checks in a cluster
|
||||
|
||||
###### Auto generated by spf13/cobra on 23-Aug-2024
|
||||
###### Auto generated by spf13/cobra on 15-Sep-2025
|
||||
|
||||
@@ -25,7 +25,9 @@ support-bundle [urls...] [flags]
|
||||
--as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace.
|
||||
--as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups.
|
||||
--as-uid string UID to impersonate for the operation.
|
||||
--cache-dir string Default cache directory (default "$HOME/.kube/cache")
|
||||
--auto enable auto-discovery of foundational collectors. When used with YAML specs, adds foundational collectors to YAML collectors. When used alone, collects only foundational data
|
||||
--auto-update enable automatic binary self-update check and install (default true)
|
||||
--cache-dir string Default cache directory (default "/Users/marccampbell/.kube/cache")
|
||||
--certificate-authority string Path to a cert file for the certificate authority
|
||||
--client-certificate string Path to a client certificate file for TLS
|
||||
--client-key string Path to a client key file for TLS
|
||||
@@ -35,16 +37,22 @@ support-bundle [urls...] [flags]
|
||||
--cpuprofile string File path to write cpu profiling data
|
||||
--debug enable debug logging. This is equivalent to --v=0
|
||||
--disable-compression If true, opt-out of response compression for all requests to the server
|
||||
--discovery-profile string auto-discovery profile: minimal, standard, comprehensive, or paranoid (default "standard")
|
||||
--dry-run print support bundle spec without collecting anything
|
||||
--exclude-namespaces strings namespaces to exclude from auto-discovery (supports glob patterns)
|
||||
-h, --help help for support-bundle
|
||||
--include-images include container image metadata collection when using auto-discovery
|
||||
--include-namespaces strings namespaces to include in auto-discovery (supports glob patterns). If specified, only these namespaces will be included
|
||||
--include-system-namespaces include system namespaces (kube-system, etc.) in auto-discovery
|
||||
--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. This is the default behavior if no spec is provided as an argument
|
||||
--load-cluster-specs enable/disable loading additional troubleshoot specs found within the cluster. Do not load by default unless no specs are provided in the cli args
|
||||
--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`
|
||||
-o, --output string specify the output file path for the support bundle
|
||||
--rbac-check enable RBAC permission checking for auto-discovered collectors (default true)
|
||||
--redact enable/disable default redactions (default true)
|
||||
--redactors strings names of the additional redactors to use
|
||||
--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")
|
||||
@@ -61,7 +69,9 @@ support-bundle [urls...] [flags]
|
||||
### SEE ALSO
|
||||
|
||||
* [support-bundle analyze](support-bundle_analyze.md) - analyze a support bundle
|
||||
* [support-bundle diff](support-bundle_diff.md) - Compare two support bundles and identify changes
|
||||
* [support-bundle redact](support-bundle_redact.md) - Redact information from a generated support bundle archive
|
||||
* [support-bundle diff](support-bundle_diff.md) - Compare two support bundles and identify changes
|
||||
* [support-bundle version](support-bundle_version.md) - Print the current version and exit
|
||||
|
||||
###### Auto generated by spf13/cobra on 23-Aug-2024
|
||||
###### Auto generated by spf13/cobra on 15-Sep-2025
|
||||
|
||||
@@ -30,4 +30,4 @@ support-bundle analyze [url] [flags]
|
||||
|
||||
* [support-bundle](support-bundle.md) - Generate a support bundle from a Kubernetes cluster or specified sources
|
||||
|
||||
###### Auto generated by spf13/cobra on 23-Aug-2024
|
||||
###### Auto generated by spf13/cobra on 15-Sep-2025
|
||||
|
||||
54
docs/support-bundle_diff.md
Normal file
54
docs/support-bundle_diff.md
Normal file
@@ -0,0 +1,54 @@
|
||||
## support-bundle diff
|
||||
|
||||
Compare two support bundles and identify changes
|
||||
|
||||
### Synopsis
|
||||
|
||||
Compare two support bundle archives to identify changes over time. The command outputs a human-readable report by default and can also emit machine-readable JSON.
|
||||
|
||||
```
|
||||
support-bundle diff <old-bundle.(tar.gz|tgz)> <new-bundle.(tar.gz|tgz)> [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
--diff-context int number of context lines to include around changes in unified diffs (default 3)
|
||||
-h, --help help for diff
|
||||
--hide-inline-diffs hide inline unified diffs in the report
|
||||
--include-log-diffs include inline diffs for log files as well
|
||||
--max-diff-files int maximum number of files to include inline diffs for; additional modified files will omit inline diffs (default 50)
|
||||
--max-diff-lines int maximum total lines to include in an inline diff for a single file (default 200)
|
||||
-o, --output string file path of where to save the diff report (default prints to stdout)
|
||||
--format string output format; set to 'json' to emit machine-readable JSON to stdout or -o
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- Only `.tar.gz` bundles are supported.
|
||||
- Inline diffs are generated for text files up to an internal size cap and for a limited number of files (configurable with `--max-diff-files`).
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
# Human-readable diff to stdout
|
||||
support-bundle diff old.tgz new.tgz
|
||||
|
||||
# JSON output to a file
|
||||
support-bundle diff old.tgz new.tgz --format=json -o diff.json
|
||||
|
||||
# Human-readable report with more context lines, written to a file
|
||||
support-bundle diff old.tgz new.tgz --diff-context=5 -o report.txt
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
--cpuprofile string File path to write cpu profiling data
|
||||
--memprofile string File path to write memory profiling data
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
* [support-bundle](support-bundle.md) - Generate a support bundle from a Kubernetes cluster or specified sources
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-Sep-2025
|
||||
@@ -39,4 +39,4 @@ support-bundle redact [urls...] [flags]
|
||||
|
||||
* [support-bundle](support-bundle.md) - Generate a support bundle from a Kubernetes cluster or specified sources
|
||||
|
||||
###### Auto generated by spf13/cobra on 23-Aug-2024
|
||||
###### Auto generated by spf13/cobra on 15-Sep-2025
|
||||
|
||||
@@ -27,4 +27,4 @@ support-bundle version [flags]
|
||||
|
||||
* [support-bundle](support-bundle.md) - Generate a support bundle from a Kubernetes cluster or specified sources
|
||||
|
||||
###### Auto generated by spf13/cobra on 23-Aug-2024
|
||||
###### Auto generated by spf13/cobra on 15-Sep-2025
|
||||
|
||||
474
docs/v1beta3-guide.md
Normal file
474
docs/v1beta3-guide.md
Normal file
@@ -0,0 +1,474 @@
|
||||
## Writing modular, templated Preflight specs (v1beta3 style)
|
||||
|
||||
This guide shows how to author preflight YAML specs in a modular, values-driven style like `v1beta3.yaml`. The goal is to keep checks self-documenting, easy to toggle on/off, and customizable via values files or inline `--set` flags.
|
||||
|
||||
|
||||
### Core structure
|
||||
|
||||
- **Header**
|
||||
- `apiVersion`: `troubleshoot.sh/v1beta3`
|
||||
- `kind`: `Preflight`
|
||||
- `metadata.name`: a short, stable identifier
|
||||
- **Spec**
|
||||
- `spec.analyzers`: list of checks (analyzers)
|
||||
- Each analyzer is optionally guarded by templating conditionals (e.g., `{{- if .Values.kubernetes.enabled }}`)
|
||||
- A `docString` accompanies each analyzer, describing the requirement, why it matters, and any links
|
||||
|
||||
|
||||
### Use templating and values
|
||||
|
||||
The examples use Go templates with the standard Sprig function set. Values can be supplied by files (`--values`) and/or inline overrides (`--set`), and accessed in templates via `.Values`.
|
||||
|
||||
- **Toggling sections**: wrap analyzer blocks in conditionals tied to values.
|
||||
```yaml
|
||||
{{- if .Values.storageClass.enabled }}
|
||||
- docString: |
|
||||
Title: Default StorageClass Requirements
|
||||
Requirement:
|
||||
- A StorageClass named "{{ .Values.storageClass.className }}" must exist
|
||||
Default StorageClass enables dynamic PVC provisioning without manual intervention.
|
||||
storageClass:
|
||||
checkName: Default StorageClass
|
||||
storageClassName: '{{ .Values.storageClass.className }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Default StorageClass not found
|
||||
- pass:
|
||||
message: Default StorageClass present
|
||||
{{- end }}
|
||||
```
|
||||
|
||||
- **Values**: template expressions directly use values from your values files.
|
||||
```yaml
|
||||
{{ .Values.clusterVersion.minVersion }}
|
||||
```
|
||||
|
||||
- **Nested conditionals**: further constrain checks (e.g., only when a specific CRD is required).
|
||||
```yaml
|
||||
{{- if .Values.crd.enabled }}
|
||||
- docString: |
|
||||
Title: Required CRD Presence
|
||||
Requirement:
|
||||
- CRD must exist: {{ .Values.crd.name }}
|
||||
The application depends on this CRD for controllers to reconcile desired state.
|
||||
customResourceDefinition:
|
||||
checkName: Required CRD
|
||||
customResourceDefinitionName: '{{ .Values.crd.name }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Required CRD not found
|
||||
- pass:
|
||||
message: Required CRD present
|
||||
{{- end }}
|
||||
```
|
||||
|
||||
|
||||
### Author high-quality docString blocks
|
||||
|
||||
Every analyzer should start with a `docString` so you can extract documentation automatically:
|
||||
|
||||
- **Title**: a concise name for the requirement
|
||||
- **Requirement**: bullet list of specific, testable criteria (e.g., versions, counts, names)
|
||||
- **Rationale**: 1–3 sentences explaining why the requirement exists and the impact if unmet
|
||||
- **Links**: include authoritative docs with stable URLs
|
||||
|
||||
Example:
|
||||
```yaml
|
||||
docString: |
|
||||
Title: Required CRDs and Ingress Capabilities
|
||||
Requirement:
|
||||
- Ingress Controller: Contour
|
||||
- CRD must be present:
|
||||
- Group: heptio.com
|
||||
- Kind: IngressRoute
|
||||
- Version: v1beta1 or later served version
|
||||
The ingress layer terminates TLS and routes external traffic to Services.
|
||||
Contour relies on the IngressRoute CRD to express host/path routing, TLS
|
||||
configuration, and policy. If the CRD is not installed and served by the
|
||||
API server, Contour cannot reconcile desired state, leaving routes
|
||||
unconfigured and traffic unreachable.
|
||||
```
|
||||
|
||||
|
||||
### Choose the right analyzer type and outcomes
|
||||
|
||||
Use the analyzer that matches the requirement, and enumerate `outcomes` with clear messages. Common analyzers in this style:
|
||||
|
||||
- **clusterVersion**: compare to min and recommended versions
|
||||
```yaml
|
||||
clusterVersion:
|
||||
checkName: Kubernetes version
|
||||
outcomes:
|
||||
- fail:
|
||||
when: '< {{ .Values.clusterVersion.minVersion }}'
|
||||
message: Requires at least Kubernetes {{ .Values.clusterVersion.minVersion }}.
|
||||
- warn:
|
||||
when: '< {{ .Values.clusterVersion.recommendedVersion }}'
|
||||
message: Recommended to use Kubernetes {{ .Values.clusterVersion.recommendedVersion }} or later.
|
||||
- pass:
|
||||
when: '>= {{ .Values.clusterVersion.recommendedVersion }}'
|
||||
message: Meets recommended and required Kubernetes versions.
|
||||
```
|
||||
|
||||
- **customResourceDefinition**: ensure a CRD exists
|
||||
```yaml
|
||||
customResourceDefinition:
|
||||
checkName: Required CRD
|
||||
customResourceDefinitionName: '{{ .Values.crd.name }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Required CRD not found
|
||||
- pass:
|
||||
message: Required CRD present
|
||||
```
|
||||
|
||||
- **containerRuntime**: verify container runtime
|
||||
```yaml
|
||||
containerRuntime:
|
||||
outcomes:
|
||||
- pass:
|
||||
when: '== containerd'
|
||||
message: containerd runtime detected
|
||||
- fail:
|
||||
message: Unsupported container runtime; containerd required
|
||||
```
|
||||
|
||||
- **storageClass**: check for a named StorageClass (often the default)
|
||||
```yaml
|
||||
storageClass:
|
||||
checkName: Default StorageClass
|
||||
storageClassName: '{{ .Values.analyzers.storageClass.className }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Default StorageClass not found
|
||||
- pass:
|
||||
message: Default StorageClass present
|
||||
```
|
||||
|
||||
- **distribution**: whitelist/blacklist distributions
|
||||
```yaml
|
||||
distribution:
|
||||
checkName: Supported distribution
|
||||
outcomes:
|
||||
{{- range $d := .Values.distribution.unsupported }}
|
||||
- fail:
|
||||
when: '== {{ $d }}'
|
||||
message: '{{ $d }} is not supported'
|
||||
{{- end }}
|
||||
{{- range $d := .Values.distribution.supported }}
|
||||
- pass:
|
||||
when: '== {{ $d }}'
|
||||
message: '{{ $d }} is a supported distribution'
|
||||
{{- end }}
|
||||
- warn:
|
||||
message: Unable to determine the distribution
|
||||
```
|
||||
|
||||
- **nodeResources**: aggregate across nodes; common patterns include count, CPU, memory, and ephemeral storage
|
||||
```yaml
|
||||
# Node count requirement
|
||||
nodeResources:
|
||||
checkName: Node count
|
||||
outcomes:
|
||||
- fail:
|
||||
when: 'count() < {{ .Values.nodeResources.count.min }}'
|
||||
message: Requires at least {{ .Values.nodeResources.count.min }} nodes
|
||||
- warn:
|
||||
when: 'count() < {{ .Values.nodeResources.count.recommended }}'
|
||||
message: Recommended at least {{ .Values.nodeResources.count.recommended }} nodes
|
||||
- pass:
|
||||
message: Cluster has sufficient nodes
|
||||
|
||||
# Cluster CPU total
|
||||
nodeResources:
|
||||
checkName: Cluster CPU total
|
||||
outcomes:
|
||||
- fail:
|
||||
when: 'sum(cpuCapacity) < {{ .Values.nodeResources.cpu.min }}'
|
||||
message: Requires at least {{ .Values.nodeResources.cpu.min }} cores
|
||||
- pass:
|
||||
message: Cluster CPU capacity meets requirement
|
||||
|
||||
# Per-node memory (Gi)
|
||||
nodeResources:
|
||||
checkName: Per-node memory
|
||||
outcomes:
|
||||
- fail:
|
||||
when: 'min(memoryCapacity) < {{ .Values.nodeResources.memory.minGi }}Gi'
|
||||
message: All nodes must have at least {{ .Values.nodeResources.memory.minGi }} GiB
|
||||
- warn:
|
||||
when: 'min(memoryCapacity) < {{ .Values.nodeResources.memory.recommendedGi }}Gi'
|
||||
message: Recommended {{ .Values.nodeResources.memory.recommendedGi }} GiB per node
|
||||
- pass:
|
||||
message: All nodes meet recommended memory
|
||||
|
||||
# Per-node ephemeral storage (Gi)
|
||||
nodeResources:
|
||||
checkName: Per-node ephemeral storage
|
||||
outcomes:
|
||||
- fail:
|
||||
when: 'min(ephemeralStorageCapacity) < {{ .Values.nodeResources.ephemeral.minGi }}Gi'
|
||||
message: All nodes must have at least {{ .Values.nodeResources.ephemeral.minGi }} GiB
|
||||
- warn:
|
||||
when: 'min(ephemeralStorageCapacity) < {{ .Values.nodeResources.ephemeral.recommendedGi }}Gi'
|
||||
message: Recommended {{ .Values.nodeResources.ephemeral.recommendedGi }} GiB per node
|
||||
- pass:
|
||||
message: All nodes meet recommended ephemeral storage
|
||||
```
|
||||
|
||||
- **deploymentStatus**: verify workload deployment status
|
||||
```yaml
|
||||
deploymentStatus:
|
||||
checkName: Deployment ready
|
||||
namespace: '{{ .Values.workloads.deployments.namespace }}'
|
||||
name: '{{ .Values.workloads.deployments.name }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
when: absent
|
||||
message: Deployment not found
|
||||
- fail:
|
||||
when: '< {{ .Values.workloads.deployments.minReady }}'
|
||||
message: Deployment has insufficient ready replicas
|
||||
- pass:
|
||||
when: '>= {{ .Values.workloads.deployments.minReady }}'
|
||||
message: Deployment has sufficient ready replicas
|
||||
```
|
||||
|
||||
- **postgres/mysql/redis**: database connectivity (requires collectors)
|
||||
```yaml
|
||||
# Collector section
|
||||
- postgres:
|
||||
collectorName: '{{ .Values.databases.postgres.collectorName }}'
|
||||
uri: '{{ .Values.databases.postgres.uri }}'
|
||||
|
||||
# Analyzer section
|
||||
postgres:
|
||||
checkName: Postgres checks
|
||||
collectorName: '{{ .Values.databases.postgres.collectorName }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Postgres checks failed
|
||||
- pass:
|
||||
message: Postgres checks passed
|
||||
```
|
||||
|
||||
- **textAnalyze/yamlCompare/jsonCompare**: analyze collected data
|
||||
```yaml
|
||||
textAnalyze:
|
||||
checkName: Text analyze
|
||||
collectorName: 'cluster-resources'
|
||||
fileName: '{{ .Values.textAnalyze.fileName }}'
|
||||
regex: '{{ .Values.textAnalyze.regex }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Pattern matched in files
|
||||
- pass:
|
||||
message: Pattern not found
|
||||
```
|
||||
|
||||
|
||||
### Design conventions for maintainability
|
||||
|
||||
- **Guard every optional analyzer** with a values toggle, so consumers can enable only what they need.
|
||||
- **Always include collectors section** when analyzers require them (databases, http, registryImages, etc.).
|
||||
- **Use `checkName`** to provide a stable, user-facing label for each check.
|
||||
- **Prefer `fail` for unmet hard requirements**, `warn` for soft requirements, and `pass` with a direct, affirmative message.
|
||||
- **Attach `uri`** to outcomes when helpful for remediation.
|
||||
- **Keep docString in sync** with the actual checks; avoid drift by templating values into both the docs and the analyzer.
|
||||
- **Ensure values files contain all required fields** since templates now directly use values without fallback defaults.
|
||||
|
||||
|
||||
### Values files: shape and examples
|
||||
|
||||
Provide a values schema that mirrors your toggles and thresholds. Example full and minimal values are included in this repository:
|
||||
|
||||
- `values-v1beta3-full.yaml` (all features enabled, opinionated defaults)
|
||||
- `values-v1beta3-minimal.yaml` (most features disabled, conservative thresholds)
|
||||
|
||||
Typical structure:
|
||||
```yaml
|
||||
clusterVersion:
|
||||
enabled: true
|
||||
minVersion: "1.24.0"
|
||||
recommendedVersion: "1.28.0"
|
||||
|
||||
storageClass:
|
||||
enabled: true
|
||||
className: "standard"
|
||||
|
||||
crd:
|
||||
enabled: true
|
||||
name: "samples.mycompany.com"
|
||||
|
||||
containerRuntime:
|
||||
enabled: true
|
||||
|
||||
distribution:
|
||||
enabled: true
|
||||
supported: ["eks", "gke", "aks", "kubeadm"]
|
||||
unsupported: []
|
||||
|
||||
nodeResources:
|
||||
count:
|
||||
enabled: true
|
||||
min: 1
|
||||
recommended: 3
|
||||
cpu:
|
||||
enabled: true
|
||||
min: "4"
|
||||
memory:
|
||||
enabled: true
|
||||
minGi: 8
|
||||
recommendedGi: 16
|
||||
ephemeral:
|
||||
enabled: true
|
||||
minGi: 20
|
||||
recommendedGi: 50
|
||||
|
||||
workloads:
|
||||
deployments:
|
||||
enabled: true
|
||||
namespace: "default"
|
||||
name: "example-deploy"
|
||||
minReady: 1
|
||||
|
||||
databases:
|
||||
postgres:
|
||||
enabled: true
|
||||
collectorName: "postgres"
|
||||
uri: "postgres://user:pass@postgres:5432/db?sslmode=disable"
|
||||
mysql:
|
||||
enabled: true
|
||||
collectorName: "mysql"
|
||||
uri: "mysql://user:pass@tcp(mysql:3306)/db"
|
||||
```
|
||||
|
||||
|
||||
### Render, run, and extract docs
|
||||
|
||||
You can render templates, run preflights with values, and extract requirement docs without running checks.
|
||||
|
||||
- **Render a templated preflight spec** to stdout or a file:
|
||||
```bash
|
||||
preflight template v1beta3.yaml \
|
||||
--values values-base.yaml \
|
||||
--values values-prod.yaml \
|
||||
--set storage.className=fast-local \
|
||||
-o rendered-preflight.yaml
|
||||
```
|
||||
|
||||
- **Run preflights with values** (values and sets also work with `preflight` root command):
|
||||
```bash
|
||||
preflight run rendered-preflight.yaml
|
||||
# or run directly against the template with values
|
||||
preflight run v1beta3.yaml --values values-prod.yaml --set cluster.minNodes=5
|
||||
```
|
||||
|
||||
- **Extract only documentation** from enabled analyzers in one or more templates:
|
||||
```bash
|
||||
preflight docs v1beta3.yaml other-spec.yaml \
|
||||
--values values-prod.yaml \
|
||||
--set kubernetes.enabled=true \
|
||||
-o REQUIREMENTS.md
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Multiple `--values` files are merged in order; later files win.
|
||||
- `--set` uses Helm-style semantics for nested keys and types, applied after files.
|
||||
|
||||
|
||||
### Authoring checklist
|
||||
|
||||
- Add `docString` with Title, Requirement bullets, rationale, and links.
|
||||
- Gate optional analyzers with `{{- if .Values.analyzers.<feature>.enabled }}`.
|
||||
- Parameterize thresholds and names with `.Values` expressions.
|
||||
- Ensure all required values are present in your values files since there are no fallback defaults.
|
||||
- Use precise, user-actionable `message` text for each outcome; add `uri` where helpful.
|
||||
- Prefer a minimal values file with everything disabled, and a full values file enabling most checks.
|
||||
- Test with `preflight template` (no values, minimal, full) and verify `preflight docs` output reads well.
|
||||
|
||||
|
||||
### Example skeleton to start a new spec
|
||||
|
||||
```yaml
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: Preflight
|
||||
metadata:
|
||||
name: your-product-preflight
|
||||
spec:
|
||||
{{- /* Determine if we need explicit collectors beyond always-on clusterResources */}}
|
||||
{{- $needExtraCollectors := or .Values.databases.postgres.enabled .Values.http.enabled }}
|
||||
|
||||
collectors:
|
||||
# Always collect cluster resources to support core analyzers
|
||||
- clusterResources: {}
|
||||
|
||||
{{- if .Values.databases.postgres.enabled }}
|
||||
- postgres:
|
||||
collectorName: '{{ .Values.databases.postgres.collectorName }}'
|
||||
uri: '{{ .Values.databases.postgres.uri }}'
|
||||
{{- end }}
|
||||
|
||||
analyzers:
|
||||
{{- if .Values.clusterVersion.enabled }}
|
||||
- docString: |
|
||||
Title: Kubernetes Control Plane Requirements
|
||||
Requirement:
|
||||
- Version:
|
||||
- Minimum: {{ .Values.clusterVersion.minVersion }}
|
||||
- Recommended: {{ .Values.clusterVersion.recommendedVersion }}
|
||||
- Docs: https://kubernetes.io
|
||||
These version targets ensure required APIs and defaults are available and patched.
|
||||
clusterVersion:
|
||||
checkName: Kubernetes version
|
||||
outcomes:
|
||||
- fail:
|
||||
when: '< {{ .Values.clusterVersion.minVersion }}'
|
||||
message: Requires at least Kubernetes {{ .Values.clusterVersion.minVersion }}.
|
||||
- warn:
|
||||
when: '< {{ .Values.clusterVersion.recommendedVersion }}'
|
||||
message: Recommended to use Kubernetes {{ .Values.clusterVersion.recommendedVersion }} or later.
|
||||
- pass:
|
||||
when: '>= {{ .Values.clusterVersion.recommendedVersion }}'
|
||||
message: Meets recommended and required Kubernetes versions.
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.storageClass.enabled }}
|
||||
- docString: |
|
||||
Title: Default StorageClass Requirements
|
||||
Requirement:
|
||||
- A StorageClass named "{{ .Values.storageClass.className }}" must exist
|
||||
A default StorageClass enables dynamic PVC provisioning without manual intervention.
|
||||
storageClass:
|
||||
checkName: Default StorageClass
|
||||
storageClassName: '{{ .Values.storageClass.className }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Default StorageClass not found
|
||||
- pass:
|
||||
message: Default StorageClass present
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.databases.postgres.enabled }}
|
||||
- docString: |
|
||||
Title: Postgres Connectivity
|
||||
Requirement:
|
||||
- Postgres checks collected by '{{ .Values.databases.postgres.collectorName }}' must pass
|
||||
postgres:
|
||||
checkName: Postgres checks
|
||||
collectorName: '{{ .Values.databases.postgres.collectorName }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Postgres checks failed
|
||||
- pass:
|
||||
message: Postgres checks passed
|
||||
{{- end }}
|
||||
```
|
||||
|
||||
|
||||
### References
|
||||
|
||||
- Example template in this repo: `v1beta3-all-analyzers.yaml`
|
||||
- Values example: `values-v1beta3-all-analyzers.yaml`
|
||||
|
||||
|
||||
111
examples/collect/host/all-collectors.yaml
Normal file
111
examples/collect/host/all-collectors.yaml
Normal file
@@ -0,0 +1,111 @@
|
||||
apiVersion: troubleshoot.sh/v1beta2
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: all-host-collectors
|
||||
spec:
|
||||
hostCollectors:
|
||||
# System Info Collectors
|
||||
- cpu: {}
|
||||
- memory: {}
|
||||
- time: {}
|
||||
- hostOS: {}
|
||||
- ipv4Interfaces: {}
|
||||
- blockDevices: {}
|
||||
- hostServices: {}
|
||||
|
||||
# Kernel Collectors
|
||||
- kernelModules: {}
|
||||
- kernelConfigs: {}
|
||||
- sysctl: {}
|
||||
- cgroups: {}
|
||||
|
||||
# System Packages
|
||||
- systemPackages: {}
|
||||
|
||||
# Journald Logs
|
||||
- journald:
|
||||
collectorName: journald-system
|
||||
system: true
|
||||
- journald:
|
||||
collectorName: journald-dmesg
|
||||
dmesg: true
|
||||
|
||||
# Disk Usage
|
||||
- diskUsage:
|
||||
collectorName: root
|
||||
path: /
|
||||
- diskUsage:
|
||||
collectorName: tmp
|
||||
path: /tmp
|
||||
|
||||
# Filesystem Performance (requires sudo)
|
||||
- filesystemPerformance:
|
||||
collectorName: filesystem-latency
|
||||
timeout: 1m
|
||||
directory: /var/tmp
|
||||
fileSize: 10Mi
|
||||
operationSizeBytes: 2300
|
||||
|
||||
# Certificate Collectors
|
||||
- certificate:
|
||||
collectorName: test-cert
|
||||
certificatePath: /etc/ssl/certs/ca-certificates.crt
|
||||
- certificatesCollection:
|
||||
collectorName: certs-collection
|
||||
paths:
|
||||
- /etc/ssl/certs
|
||||
|
||||
# Network Tests
|
||||
- tcpPortStatus:
|
||||
collectorName: ssh-port
|
||||
port: 22
|
||||
- udpPortStatus:
|
||||
collectorName: dns-port
|
||||
port: 53
|
||||
- tcpConnect:
|
||||
collectorName: localhost-ssh
|
||||
address: 127.0.0.1:22
|
||||
- tcpLoadBalancer:
|
||||
collectorName: lb-test
|
||||
address: 127.0.0.1
|
||||
port: 80
|
||||
- httpLoadBalancer:
|
||||
collectorName: http-lb-test
|
||||
address: 127.0.0.1
|
||||
port: 80
|
||||
path: /healthz
|
||||
- http:
|
||||
collectorName: google
|
||||
get:
|
||||
url: https://www.google.com
|
||||
- dns:
|
||||
collectorName: dns-google
|
||||
hostnames:
|
||||
- google.com
|
||||
- subnetAvailable:
|
||||
collectorName: subnet-check
|
||||
CIDRRangeAlloc: 10.0.0.0/16
|
||||
desiredCIDR: 24
|
||||
- networkNamespaceConnectivity:
|
||||
collectorName: netns-connectivity
|
||||
fromCIDR: 10.0.0.0/8
|
||||
toCIDR: 192.168.0.0/16
|
||||
port: 80
|
||||
|
||||
# Custom Commands
|
||||
- run:
|
||||
collectorName: uname
|
||||
command: "uname"
|
||||
args: ["-a"]
|
||||
- run:
|
||||
collectorName: df
|
||||
command: "df"
|
||||
args: ["-h"]
|
||||
|
||||
# Copy Files
|
||||
- copy:
|
||||
collectorName: hosts-file
|
||||
path: /etc/hosts
|
||||
- copy:
|
||||
collectorName: resolv-conf
|
||||
path: /etc/resolv.conf
|
||||
170
examples/collect/host/all-kubernetes-collectors.yaml
Normal file
170
examples/collect/host/all-kubernetes-collectors.yaml
Normal file
@@ -0,0 +1,170 @@
|
||||
apiVersion: troubleshoot.sh/v1beta2
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: all-kubernetes-collectors
|
||||
spec:
|
||||
collectors:
|
||||
# Cluster Info Collectors (2)
|
||||
- clusterInfo: {}
|
||||
- clusterResources: {}
|
||||
|
||||
# Metrics Collectors (2)
|
||||
- customMetrics:
|
||||
collectorName: custom-metrics
|
||||
metricRequests:
|
||||
- resourceMetricName: example-metric
|
||||
- nodeMetrics: {}
|
||||
|
||||
# ConfigMap and Secret Collectors (2)
|
||||
- configMap:
|
||||
collectorName: example-configmap
|
||||
name: example-configmap
|
||||
namespace: default
|
||||
includeValue: false
|
||||
- secret:
|
||||
collectorName: example-secret
|
||||
name: example-secret
|
||||
namespace: default
|
||||
includeValue: false
|
||||
|
||||
# Logs Collector (1)
|
||||
- logs:
|
||||
collectorName: example-logs
|
||||
selector:
|
||||
- app=example
|
||||
namespace: default
|
||||
limits:
|
||||
maxAge: 720h
|
||||
maxLines: 10000
|
||||
|
||||
# Pod Execution Collectors (4)
|
||||
- run:
|
||||
collectorName: run-example
|
||||
name: run-example
|
||||
namespace: default
|
||||
image: busybox:latest
|
||||
command: ["echo"]
|
||||
args: ["hello from run"]
|
||||
- runPod:
|
||||
collectorName: run-pod-example
|
||||
name: run-pod-example
|
||||
namespace: default
|
||||
podSpec:
|
||||
containers:
|
||||
- name: example
|
||||
image: busybox:latest
|
||||
command: ["echo", "hello from runPod"]
|
||||
- runDaemonSet:
|
||||
collectorName: run-daemonset-example
|
||||
name: run-daemonset-example
|
||||
namespace: default
|
||||
podSpec:
|
||||
containers:
|
||||
- name: example
|
||||
image: busybox:latest
|
||||
command: ["echo", "hello from runDaemonSet"]
|
||||
- exec:
|
||||
collectorName: exec-example
|
||||
name: exec-example
|
||||
selector:
|
||||
- app=example
|
||||
namespace: default
|
||||
command: ["echo"]
|
||||
args: ["hello from exec"]
|
||||
|
||||
# Data Collector (1)
|
||||
- data:
|
||||
collectorName: static-data
|
||||
name: static-data.txt
|
||||
data: "This is static data"
|
||||
|
||||
# Copy Collectors (2)
|
||||
- copy:
|
||||
collectorName: copy-example
|
||||
selector:
|
||||
- app=example
|
||||
namespace: default
|
||||
containerPath: /tmp
|
||||
- copyFromHost:
|
||||
collectorName: copy-from-host-example
|
||||
name: copy-from-host-example
|
||||
namespace: default
|
||||
image: busybox:latest
|
||||
hostPath: /tmp/example
|
||||
|
||||
# HTTP Collector (1)
|
||||
- http:
|
||||
collectorName: http-get-example
|
||||
get:
|
||||
url: https://www.google.com
|
||||
insecureSkipVerify: false
|
||||
|
||||
# Database Collectors (4)
|
||||
- postgres:
|
||||
collectorName: postgres-example
|
||||
uri: postgresql://user:password@localhost:5432/dbname
|
||||
- mysql:
|
||||
collectorName: mysql-example
|
||||
uri: user:password@tcp(localhost:3306)/dbname
|
||||
- mssql:
|
||||
collectorName: mssql-example
|
||||
uri: sqlserver://user:password@localhost:1433?database=dbname
|
||||
- redis:
|
||||
collectorName: redis-example
|
||||
uri: redis://localhost:6379
|
||||
|
||||
# Storage and System Collectors (3)
|
||||
- collectd:
|
||||
collectorName: collectd-example
|
||||
namespace: default
|
||||
image: busybox:latest
|
||||
hostPath: /var/lib/collectd
|
||||
- ceph:
|
||||
collectorName: ceph-example
|
||||
namespace: rook-ceph
|
||||
- longhorn:
|
||||
collectorName: longhorn-example
|
||||
namespace: longhorn-system
|
||||
|
||||
# Registry and Image Collector (1)
|
||||
- registryImages:
|
||||
collectorName: registry-images-example
|
||||
namespace: default
|
||||
images:
|
||||
- busybox:latest
|
||||
|
||||
# Sysctl Collector (1)
|
||||
- sysctl:
|
||||
collectorName: sysctl-example
|
||||
name: sysctl-example
|
||||
namespace: default
|
||||
image: busybox:latest
|
||||
|
||||
# Certificate Collector (1)
|
||||
- certificates:
|
||||
collectorName: certificates-example
|
||||
secrets:
|
||||
- name: tls-secret
|
||||
namespaces:
|
||||
- default
|
||||
|
||||
# Application-Specific Collectors (3)
|
||||
- helm:
|
||||
collectorName: helm-example
|
||||
namespace: default
|
||||
releaseName: example-release
|
||||
collectValues: false
|
||||
- goldpinger:
|
||||
collectorName: goldpinger-example
|
||||
namespace: default
|
||||
- sonobuoy:
|
||||
collectorName: sonobuoy-example
|
||||
namespace: sonobuoy
|
||||
|
||||
# DNS and Network Collectors (2)
|
||||
- dns:
|
||||
collectorName: dns-example
|
||||
timeout: 10s
|
||||
- etcd:
|
||||
collectorName: etcd-example
|
||||
image: quay.io/coreos/etcd:latest
|
||||
905
examples/collect/host/default.yaml
Normal file
905
examples/collect/host/default.yaml
Normal file
@@ -0,0 +1,905 @@
|
||||
# Spec to run when a kURL cluster is down and in-cluster specs can't be run
|
||||
apiVersion: troubleshoot.sh/v1beta2
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: default
|
||||
spec:
|
||||
uri: https://raw.githubusercontent.com/replicatedhq/troubleshoot-specs/main/host/default.yaml
|
||||
hostCollectors:
|
||||
# System Info Collectors
|
||||
- blockDevices: {}
|
||||
- cpu: {}
|
||||
- hostOS: {}
|
||||
- hostServices: {}
|
||||
- ipv4Interfaces: {}
|
||||
- memory: {}
|
||||
- time: {}
|
||||
- ipv4Interfaces: {}
|
||||
# Certificate Info for ETCD and K8s API
|
||||
- certificate:
|
||||
collectorName: k8s-api-keypair
|
||||
certificatePath: /etc/kubernetes/pki/apiserver.crt
|
||||
keyPath: /etc/kubernetes/pki/apiserver.key
|
||||
- certificate:
|
||||
collectorName: etcd-keypair
|
||||
certificatePath: /etc/kubernetes/pki/etcd/server.crt
|
||||
keyPath: /etc/kubernetes/pki/etcd/server.key
|
||||
# Disk usage for commonly used directories in kURL installs
|
||||
- diskUsage:
|
||||
collectorName: root
|
||||
path: /
|
||||
- diskUsage:
|
||||
collectorName: tmp
|
||||
path: /tmp
|
||||
- diskUsage:
|
||||
collectorName: var-lib-kubelet
|
||||
path: /var/lib/kubelet
|
||||
- diskUsage:
|
||||
collectorName: var-lib-docker
|
||||
path: /var/lib/docker
|
||||
- diskUsage:
|
||||
collectorName: var-lib-containerd
|
||||
path: /var/lib/containerd
|
||||
- diskUsage:
|
||||
collectorName: var-lib-rook
|
||||
path: /var/lib/rook
|
||||
- diskUsage:
|
||||
collectorName: opt-replicated
|
||||
path: /opt/replicated
|
||||
- diskUsage:
|
||||
collectorName: var-openebs
|
||||
path: /var/openebs
|
||||
- http:
|
||||
collectorName: curl-k8s-api-6443
|
||||
get:
|
||||
url: https://localhost:6443/healthz
|
||||
insecureSkipVerify: true
|
||||
# Run collectors for system information
|
||||
- run:
|
||||
collectorName: k8s-api-healthz-6443
|
||||
command: "curl"
|
||||
args: ["-k", "https://localhost:6443/healthz?verbose"]
|
||||
- run:
|
||||
collectorName: curl-etcd-health-2379
|
||||
command: "curl"
|
||||
args: ["-ki", "https://localhost:2379/health", "--cert", "/etc/kubernetes/pki/etcd/healthcheck-client.crt", "--key", "/etc/kubernetes/pki/etcd/healthcheck-client.key"]
|
||||
- run:
|
||||
collectorName: "free"
|
||||
command: "free"
|
||||
args: ["-m"]
|
||||
- run:
|
||||
collectorName: "top"
|
||||
command: "top"
|
||||
args: ["-b", "-n", "1"]
|
||||
- run:
|
||||
collectorName: "uptime"
|
||||
command: "uptime"
|
||||
args: []
|
||||
- run:
|
||||
collectorName: "uname"
|
||||
command: "uname"
|
||||
args: ["-a"]
|
||||
- run:
|
||||
collectorName: "df"
|
||||
command: "df"
|
||||
args: ["-h"]
|
||||
- run:
|
||||
collectorName: "iostat"
|
||||
command: "iostat"
|
||||
args: ["-x"]
|
||||
- run:
|
||||
collectorName: "pidstat-disk-io"
|
||||
command: "pidstat"
|
||||
args: ["d"]
|
||||
- run:
|
||||
collectorName: "iotop"
|
||||
command: "iotop"
|
||||
args: ["-n", "1", "-b"]
|
||||
# SELinux status
|
||||
- run:
|
||||
collectorName: "sestatus"
|
||||
command: "sestatus"
|
||||
args: []
|
||||
- run:
|
||||
collectorName: "apparmor-status"
|
||||
command: "apparmor_status"
|
||||
args: []
|
||||
- run:
|
||||
collectorName: "docker-info"
|
||||
command: "docker"
|
||||
args: ["info"]
|
||||
- run:
|
||||
collectorName: "crictl-info"
|
||||
command: "crictl"
|
||||
args: ["info"]
|
||||
- run:
|
||||
collectorName: "crictl-ps"
|
||||
command: "crictl"
|
||||
args: ["ps", "-a"]
|
||||
- run:
|
||||
collectorName: "docker-ps"
|
||||
command: "docker"
|
||||
args: ["ps", "-a"]
|
||||
- run:
|
||||
collectorName: "docker-system-df"
|
||||
command: "docker"
|
||||
args: ["system", "df", "-v"]
|
||||
- run:
|
||||
collectorName: "iptables"
|
||||
command: "iptables"
|
||||
args: ["-L", "-v"]
|
||||
- run:
|
||||
collectorName: "iptables-save"
|
||||
command: "iptables-save"
|
||||
- run:
|
||||
collectorName: "iptables-version"
|
||||
command: "iptables"
|
||||
args: ["-V"]
|
||||
- run:
|
||||
collectorName: "nftables-list"
|
||||
command: "nft"
|
||||
args: ["list", "table", "filter"]
|
||||
- run:
|
||||
collectorName: "ipvsadm"
|
||||
command: "ipvsadm"
|
||||
args: ["-l", "-n"]
|
||||
- run:
|
||||
collectorName: "lsblk"
|
||||
command: "lsblk"
|
||||
args: ["--fs"]
|
||||
- run:
|
||||
collectorName: "netstat-ports"
|
||||
command: "netstat"
|
||||
args: ["-t", "-u", "-l", "-p", "-n"]
|
||||
- run:
|
||||
collectorName: "netstat-route-table"
|
||||
command: "netstat"
|
||||
args: ["-r", "-n"]
|
||||
- run:
|
||||
collectorName: "resolvectl-status"
|
||||
command: "resolvectl"
|
||||
args: ["status"]
|
||||
- run:
|
||||
collectorName: "resolv-conf"
|
||||
command: "cat"
|
||||
args: ["/etc/resolv.conf"]
|
||||
- run:
|
||||
collectorName: "systemd-resolved-conf"
|
||||
command: "cat"
|
||||
args: ["/etc/systemd/resolved.conf"]
|
||||
- run:
|
||||
collectorName: "nsswitch-conf"
|
||||
command: "cat"
|
||||
args: ["/etc/nsswitch.conf"]
|
||||
- run:
|
||||
collectorName: "hosts"
|
||||
command: "cat"
|
||||
args: ["/etc/hosts"]
|
||||
- run:
|
||||
collectorName: "ip-interface-stats"
|
||||
command: "ip"
|
||||
args: ["-s", "link"]
|
||||
- run:
|
||||
collectorName: "ip-route-table"
|
||||
command: "ip"
|
||||
args: ["route"]
|
||||
- run:
|
||||
collectorName: "sysctl"
|
||||
command: "sysctl"
|
||||
args: ["-a"]
|
||||
# Static Manifests
|
||||
- run:
|
||||
collectorName: "manifest-etcd"
|
||||
command: "cat"
|
||||
args: ["/etc/kubernetes/manifests/etcd.yaml"]
|
||||
- run:
|
||||
collectorName: "manifest-kube-apiserver"
|
||||
command: "cat"
|
||||
args: ["/etc/kubernetes/manifests/kube-apiserver.yaml"]
|
||||
- run:
|
||||
collectorName: "manifest-kube-controller-manager"
|
||||
command: "cat"
|
||||
args: ["/etc/kubernetes/manifests/kube-controller-manager.yaml"]
|
||||
- run:
|
||||
collectorName: "manifest-kube-scheduler"
|
||||
command: "cat"
|
||||
args: ["/etc/kubernetes/manifests/kube-scheduler.yaml"]
|
||||
# Systemctl service statuses for CRI, Kubelet, and Firewall
|
||||
- run:
|
||||
collectorName: "systemctl-firewalld-status"
|
||||
command: "systemctl"
|
||||
args: ["status", "firewalld"]
|
||||
- run:
|
||||
collectorName: "systemctl-resolved-status"
|
||||
command: "systemctl"
|
||||
args: ["status", "systemd-resolved"]
|
||||
- run:
|
||||
collectorName: "systemctl-docker-status"
|
||||
command: "systemctl"
|
||||
args: ["status", "docker"]
|
||||
- run:
|
||||
collectorName: "systemctl-kubelet-status"
|
||||
command: "systemctl"
|
||||
args: ["status", "kubelet"]
|
||||
- run:
|
||||
collectorName: "systemctl-containerd-status"
|
||||
command: "systemctl"
|
||||
args: ["status", "containerd"]
|
||||
# Systemd Service Configurations for CRI, Kubelet
|
||||
- run:
|
||||
collectorName: "systemctl-cat-journald"
|
||||
command: "systemctl"
|
||||
args: ["cat", "systemd-journald"]
|
||||
- run:
|
||||
collectorName: "systemctl-cat-resolved"
|
||||
command: "systemctl"
|
||||
args: ["cat", "systemd-resolved"]
|
||||
- run:
|
||||
collectorName: "systemctl-cat-docker"
|
||||
command: "systemctl"
|
||||
args: ["cat", "docker"]
|
||||
- run:
|
||||
collectorName: "systemctl-cat-containerd"
|
||||
command: "systemctl"
|
||||
args: ["cat", "containerd"]
|
||||
- run:
|
||||
collectorName: "systemctl-cat-kubelet"
|
||||
command: "systemctl"
|
||||
args: ["cat", "kubelet"]
|
||||
# Logs for CRI, Kubelet, Kernel
|
||||
- run:
|
||||
collectorName: "journalctl-containerd"
|
||||
command: "journalctl"
|
||||
args: ["-u", "containerd", "--no-pager", "-S", "7 days ago"]
|
||||
- run:
|
||||
collectorName: "journalctl-kubelet"
|
||||
command: "journalctl"
|
||||
args: ["-u", "kubelet", "--no-pager", "-S", "7 days ago"]
|
||||
- run:
|
||||
collectorName: "journalctl-docker"
|
||||
command: "journalctl"
|
||||
args: ["-u", "docker", "--no-pager", "-S", "7 days ago"]
|
||||
- run:
|
||||
collectorName: "journalctl-dmesg"
|
||||
command: "journalctl"
|
||||
args: ["--dmesg", "--no-pager", "-S", "7 days ago"]
|
||||
- copy:
|
||||
collectorName: "syslog"
|
||||
path: /var/log/syslog
|
||||
- copy:
|
||||
collectorName: "audit-logs"
|
||||
path: /var/log/audit/audit.log
|
||||
- copy:
|
||||
collectorName: "syslog" # Copy the previous syslog file as well in case the current one is rotated
|
||||
path: /var/log/syslog.1
|
||||
# Docker logs for K8s Control Plane
|
||||
- run:
|
||||
collectorName: "docker-logs-apiserver"
|
||||
command: "sh"
|
||||
args: ["-c", "docker logs $(docker ps -a --filter label=io.kubernetes.container.name=kube-apiserver -q -l) 2>&1"]
|
||||
- run:
|
||||
collectorName: "docker-logs-kube-scheduler"
|
||||
command: "sh"
|
||||
args: ["-c", "docker logs $(docker ps -a --filter label=io.kubernetes.container.name=kube-scheduler -q -l) 2>&1"]
|
||||
- run:
|
||||
collectorName: "docker-logs-kube-controller-manager"
|
||||
command: "sh"
|
||||
args: ["-c", "docker logs $(docker ps -a --filter label=io.kubernetes.container.name=kube-controller-manager -q -l) 2>&1"]
|
||||
- run:
|
||||
collectorName: "docker-logs-etcd"
|
||||
command: "sh"
|
||||
args: ["-c", "docker logs $(docker ps -a --filter label=io.kubernetes.container.name=etcd -q -l) 2>&1"]
|
||||
# Docker logs for haproxy (Used by kURL's internal load balancing feature)
|
||||
- run:
|
||||
collectorName: "docker-logs-haproxy"
|
||||
command: "sh"
|
||||
args: ["-c", "docker logs $(docker ps -a --filter label=io.kubernetes.container.name=haproxy -q -l) 2>&1"]
|
||||
# Containerd logs for K8s Control Plane
|
||||
- run:
|
||||
collectorName: "crictl-logs-apiserver"
|
||||
command: "sh"
|
||||
args: ["-c", "crictl logs $(crictl ps -a --name apiserver -l --quiet) 2>&1"]
|
||||
- run:
|
||||
collectorName: "crictl-logs-apiserver-previous"
|
||||
command: "sh"
|
||||
args: ["-c", "crictl logs -p $(crictl ps -a --name apiserver -l --quiet) 2>&1"]
|
||||
- run:
|
||||
collectorName: "crictl-logs-etcd"
|
||||
command: "sh"
|
||||
args: ["-c", "crictl logs $(crictl ps -a --name etcd -l --quiet) 2>&1"]
|
||||
- run:
|
||||
collectorName: "crictl-logs-etcd-previous"
|
||||
command: "sh"
|
||||
args: ["-c", "crictl logs -p $(crictl ps -a --name etcd -l --quiet) 2>&1"]
|
||||
- run:
|
||||
collectorName: "crictl-logs-kube-controller-manager"
|
||||
command: "sh"
|
||||
args: ["-c", "crictl logs $(crictl ps -a --name kube-controller-manager -l --quiet) 2>&1"]
|
||||
- run:
|
||||
collectorName: "crictl-logs-kube-controller-manager-previous"
|
||||
command: "sh"
|
||||
args: ["-c", "crictl logs -p $(crictl ps -a --name kube-controller-manager -l --quiet) 2>&1"]
|
||||
- run:
|
||||
collectorName: "crictl-logs-kube-scheduler"
|
||||
command: "sh"
|
||||
args: ["-c", "crictl logs $(crictl ps -a --name kube-scheduler -l --quiet) 2>&1"]
|
||||
- run:
|
||||
collectorName: "crictl-logs-kube-scheduler-previous"
|
||||
command: "sh"
|
||||
args: ["-c", "crictl logs -p $(crictl ps -a --name kube-scheduler -l --quiet) 2>&1"]
|
||||
# Logs for kube-flannel
|
||||
- run:
|
||||
collectorName: "crictl-logs-kube-flannel"
|
||||
command: "sh"
|
||||
args: ["-c", "crictl logs $(crictl ps -a --name kube-flannel -l --quiet) 2>&1"]
|
||||
- run:
|
||||
collectorName: "crictl-logs-kube-flannel-previous"
|
||||
command: "sh"
|
||||
args: ["-c", "crictl logs -p $(crictl ps -a --name kube-flannel -l --quiet) 2>&1"]
|
||||
# Logs for kube-proxy
|
||||
- run:
|
||||
collectorName: "crictl-logs-kube-proxy"
|
||||
command: "sh"
|
||||
args: ["-c", "crictl logs $(crictl ps -a --name kube-proxy -l --quiet) 2>&1"]
|
||||
- run:
|
||||
collectorName: "crictl-logs-kube-proxy-previous"
|
||||
command: "sh"
|
||||
args: ["-c", "crictl logs -p $(crictl ps -a --name kube-proxy -l --quiet) 2>&1"]
|
||||
# Logs for haproxy (Used by kURL's internal load balancing feature)
|
||||
- run:
|
||||
collectorName: "crictl-logs-haproxy"
|
||||
command: "sh"
|
||||
args: ["-c", "crictl logs $(crictl ps -a --name haproxy -l --quiet) 2>&1"]
|
||||
- run:
|
||||
collectorName: "crictl-logs-haproxy-previous"
|
||||
command: "sh"
|
||||
args: ["-c", "crictl logs -p $(crictl ps -a --name haproxy -l --quiet) 2>&1"]
|
||||
# Logs from ekco (Used by kURL to rotate certs and other tasks)
|
||||
- run:
|
||||
collectorName: "crictl-logs-ekco"
|
||||
command: "sh"
|
||||
args: ["-c", "crictl logs $(crictl ps -a --name ekc-operator -l --quiet) 2>&1"]
|
||||
- run:
|
||||
collectorName: "crictl-logs-ekco-previous"
|
||||
command: "sh"
|
||||
args: ["-c", "crictl logs -p $(crictl ps -a --name ekc-operator -l --quiet) 2>&1"]
|
||||
# sysctl parameters
|
||||
- run:
|
||||
collectorName: "sysctl-all"
|
||||
command: "sh"
|
||||
args: ["-c", "sysctl --all 2>/dev/null"]
|
||||
# Gathering hostname info to help troubleshoot scenarios where the hostname mismatch
|
||||
- run:
|
||||
collectorName: "hostnames"
|
||||
command: "sh"
|
||||
args:
|
||||
- -c
|
||||
- |
|
||||
echo "hostname = $(hostname)"
|
||||
echo "/proc/sys/kernel/hostname = $(cat /proc/sys/kernel/hostname)"
|
||||
echo "uname -n = $(uname -n)"
|
||||
# Collect apiserver audit logs
|
||||
# Note: apiserver logs are owned by root so for this collector
|
||||
# to succeed it requires sudo privileges for the user
|
||||
- copy:
|
||||
collectorName: "apiserver-audit-logs"
|
||||
path: /var/log/apiserver/k8s-audit.log
|
||||
# Collect kURL installer logs
|
||||
- copy:
|
||||
collectorName: "kurl-logs"
|
||||
path: /var/log/kurl/*
|
||||
- run:
|
||||
collectorName: "kubeadm.conf"
|
||||
command: "cat"
|
||||
args: ["/opt/replicated/kubeadm.conf"]
|
||||
- run:
|
||||
collectorName: "kubeadm-init-raw.yaml"
|
||||
command: "cat"
|
||||
args: ["/opt/replicated/kubeadm-init-raw.yaml"]
|
||||
- run:
|
||||
collectorName: "kubeadm-flags.env"
|
||||
command: "cat"
|
||||
args: ["/var/lib/kubelet/kubeadm-flags.env"]
|
||||
- run:
|
||||
collectorName: "kurl-host-preflights"
|
||||
command: "tail"
|
||||
args: ["-n", "+1", "/var/lib/kurl/host-preflights/*"]
|
||||
- run:
|
||||
collectorName: "kubeadm-kustomize-patches"
|
||||
command: "sh"
|
||||
args: ["-c", "find /var/lib/kurl/kustomize -type f -exec tail -n +1 {} +;"]
|
||||
- run:
|
||||
collectorName: "tmp-kubeadm.conf"
|
||||
command: "cat"
|
||||
args: ["/var/lib/kubelet/tmp-kubeadm.conf"]
|
||||
- http:
|
||||
collectorName: curl-api-replicated-com
|
||||
get:
|
||||
url: https://api.replicated.com/healthz
|
||||
- http:
|
||||
collectorName: get-proxy-replicated-com
|
||||
get:
|
||||
url: https://proxy.replicated.com/
|
||||
- http:
|
||||
collectorName: curl-get-replicated-com
|
||||
get:
|
||||
url: https://get.replicated.com/healthz
|
||||
- http:
|
||||
collectorName: curl-registry-replicated-com
|
||||
get:
|
||||
url: https://registry.replicated.com/healthz
|
||||
- http:
|
||||
collectorName: curl-proxy-replicated-com
|
||||
get:
|
||||
url: https://proxy.replicated.com/healthz
|
||||
- http:
|
||||
collectorName: curl-k8s-kurl-sh
|
||||
get:
|
||||
url: https://k8s.kurl.sh/healthz
|
||||
- http:
|
||||
collectorName: curl-replicated-app
|
||||
get:
|
||||
url: https://replicated.app/healthz
|
||||
# System Info Collectors
|
||||
- run:
|
||||
collectorName: "du-root"
|
||||
command: "sh"
|
||||
args: ["-c", "du -Shax / --exclude /proc | sort -rh | head -20"]
|
||||
- run:
|
||||
collectorName: "mount"
|
||||
command: "mount"
|
||||
args: ["-l"]
|
||||
- run:
|
||||
collectorName: "vmstat"
|
||||
command: "vmstat"
|
||||
args: ["-w"]
|
||||
- run:
|
||||
collectorName: "ps-high-load"
|
||||
command: "sh"
|
||||
args: ["-c", "ps -eo s,user,cmd | grep ^[RD] | sort | uniq -c | sort -nbr | head -20"]
|
||||
- run:
|
||||
collectorName: "ps-detect-antivirus-and-security-tools"
|
||||
command: "sh"
|
||||
args: [-c, "ps -ef | grep -E 'clamav|sophos|esets_daemon|fsav|symantec|mfend|ds_agent|kav|bdagent|s1agent|falcon|illumio|xagt|wdavdaemon|mdatp' | grep -v grep"]
|
||||
- systemPackages:
|
||||
collectorName: security-tools-packages
|
||||
rhel:
|
||||
- sdcss-kmod
|
||||
- sdcss
|
||||
- sdcss-scripts
|
||||
- filesystemPerformance:
|
||||
collectorName: filesystem-latency-two-minute-benchmark
|
||||
timeout: 2m
|
||||
directory: /var/lib/etcd
|
||||
fileSize: 22Mi
|
||||
operationSizeBytes: 2300
|
||||
datasync: true
|
||||
enableBackgroundIOPS: true
|
||||
backgroundIOPSWarmupSeconds: 10
|
||||
backgroundWriteIOPS: 300
|
||||
backgroundWriteIOPSJobs: 6
|
||||
backgroundReadIOPS: 50
|
||||
backgroundReadIOPSJobs: 1
|
||||
- run:
|
||||
collectorName: "localhost-ips"
|
||||
command: "sh"
|
||||
args: ["-c", "host localhost"]
|
||||
- run:
|
||||
collectorName: "ip-address-stats"
|
||||
command: "ip"
|
||||
args: ["-s", "-s", "address"]
|
||||
- run:
|
||||
collectorName: "ethool-info"
|
||||
command: "sh"
|
||||
args:
|
||||
- -c
|
||||
- >
|
||||
interfaces=$(ls /sys/class/net);
|
||||
for iface in $interfaces; do
|
||||
echo "==============================================";
|
||||
echo "Interface: $iface";
|
||||
echo "==============================================";
|
||||
|
||||
echo
|
||||
echo "--- Basic Info ---"
|
||||
ethtool "$iface"
|
||||
|
||||
echo
|
||||
echo "--- Features (Offloads) ---"
|
||||
ethtool -k "$iface"
|
||||
|
||||
echo
|
||||
echo "--- Pause Parameters ---"
|
||||
ethtool -a "$iface"
|
||||
|
||||
echo
|
||||
echo "--- Ring Parameters ---"
|
||||
ethtool -g "$iface"
|
||||
|
||||
echo
|
||||
echo "--- Coalesce Settings ---"
|
||||
ethtool -c "$iface"
|
||||
|
||||
echo
|
||||
echo "--- Driver Info ---"
|
||||
ethtool -i "$iface"
|
||||
|
||||
echo
|
||||
echo
|
||||
done
|
||||
hostAnalyzers:
|
||||
- certificate:
|
||||
collectorName: k8s-api-keypair
|
||||
outcomes:
|
||||
- fail:
|
||||
when: "key-pair-missing"
|
||||
message: Certificate key pair not found in /etc/kubernetes/pki/apiserver.*
|
||||
- fail:
|
||||
when: "key-pair-switched"
|
||||
message: Cert and key pair are switched
|
||||
- fail:
|
||||
when: "key-pair-encrypted"
|
||||
message: Private key is encrypted
|
||||
- fail:
|
||||
when: "key-pair-mismatch"
|
||||
message: Cert and key do not match
|
||||
- fail:
|
||||
when: "key-pair-invalid"
|
||||
message: Certificate key pair is invalid
|
||||
- pass:
|
||||
when: "key-pair-valid"
|
||||
message: Certificate key pair is valid
|
||||
- certificate:
|
||||
collectorName: etcd-keypair
|
||||
outcomes:
|
||||
- fail:
|
||||
when: "key-pair-missing"
|
||||
message: Certificate key pair not found in /etc/kubernetes/pki/etcd/server.*
|
||||
- fail:
|
||||
when: "key-pair-switched"
|
||||
message: Cert and key pair are switched
|
||||
- fail:
|
||||
when: "key-pair-encrypted"
|
||||
message: Private key is encrypted
|
||||
- fail:
|
||||
when: "key-pair-mismatch"
|
||||
message: Cert and key do not match
|
||||
- fail:
|
||||
when: "key-pair-invalid"
|
||||
message: Certificate key pair is invalid
|
||||
- pass:
|
||||
when: "key-pair-valid"
|
||||
message: Certificate key pair is valid
|
||||
- cpu:
|
||||
checkName: "Number of CPUs"
|
||||
outcomes:
|
||||
- warn:
|
||||
when: "count < 4"
|
||||
message: At least 4 CPU cores are recommended for kURL https://kurl.sh/docs/install-with-kurl/system-requirements
|
||||
- pass:
|
||||
message: This server has at least 4 CPU cores
|
||||
- memory:
|
||||
checkName: "Amount of Memory"
|
||||
outcomes:
|
||||
- warn:
|
||||
when: "< 8G"
|
||||
message: At least 8G of memory is recommended for kURL https://kurl.sh/docs/install-with-kurl/system-requirements
|
||||
- pass:
|
||||
message: The system has at least 8G of memory
|
||||
- time:
|
||||
checkName: "ntp-status"
|
||||
outcomes:
|
||||
- fail:
|
||||
when: "ntp == unsynchronized+inactive"
|
||||
message: "System clock is not synchronized"
|
||||
- warn:
|
||||
when: "ntp == unsynchronized+active"
|
||||
message: System clock not yet synchronized
|
||||
- pass:
|
||||
when: "ntp == synchronized+active"
|
||||
message: "System clock is synchronized"
|
||||
- diskUsage:
|
||||
checkName: "root"
|
||||
collectorName: "root"
|
||||
outcomes:
|
||||
- fail:
|
||||
when: "total < 40Gi"
|
||||
message: The disk containing directory / has less than 40Gi of total space
|
||||
- warn:
|
||||
when: "used/total > 80%"
|
||||
message: The disk containing directory / is more than 80% full
|
||||
- warn:
|
||||
when: "available < 10Gi"
|
||||
message: The disk containing directory / has less than 10Gi of disk space available
|
||||
- pass:
|
||||
message: The disk containing directory / has sufficient space
|
||||
- diskUsage:
|
||||
checkName: "tmp"
|
||||
collectorName: "tmp"
|
||||
outcomes:
|
||||
- warn:
|
||||
when: "total < 8Gi"
|
||||
message: The disk containing directory /tmp has less than 8Gi of total space
|
||||
- warn:
|
||||
when: "used/total > 80%"
|
||||
message: The disk containing directory /tmp is more than 80% full
|
||||
- warn:
|
||||
when: "available < 2Gi"
|
||||
message: The disk containing directory /tmp has less than 2Gi of disk space available
|
||||
- pass:
|
||||
message: The disk containing directory /tmp has sufficient space
|
||||
- diskUsage:
|
||||
checkName: "var-lib-kubelet"
|
||||
collectorName: "var-lib-kubelet"
|
||||
outcomes:
|
||||
- warn:
|
||||
when: "used/total > 80%"
|
||||
message: The disk containing directory /var/lib/kubelet is more than 80% full
|
||||
- warn:
|
||||
when: "available < 10Gi"
|
||||
message: The disk containing directory /var/lib/kubelet has less than 10Gi of disk space available
|
||||
- pass:
|
||||
message: The disk containing directory /var/lib/kubelet has sufficient space
|
||||
- diskUsage:
|
||||
checkName: "var-lib-docker"
|
||||
collectorName: "var-lib-docker"
|
||||
outcomes:
|
||||
- warn:
|
||||
when: "used/total > 80%"
|
||||
message: The disk containing directory /var/lib/docker is more than 80% full
|
||||
- warn:
|
||||
when: "available < 10Gi"
|
||||
message: The disk containing directory /var/lib/docker has less than 10Gi of disk space available
|
||||
- pass:
|
||||
message: The disk containing directory /var/lib/docker has sufficient space
|
||||
- diskUsage:
|
||||
checkName: "var-lib-containerd"
|
||||
collectorName: "var-lib-containerd"
|
||||
outcomes:
|
||||
- warn:
|
||||
when: "used/total > 80%"
|
||||
message: The disk containing directory /var/lib/containerd is more than 80% full
|
||||
- warn:
|
||||
when: "available < 10Gi"
|
||||
message: The disk containing directory /var/lib/containerd has less than 10Gi of disk space available
|
||||
- pass:
|
||||
message: The disk containing directory /var/lib/containerd has sufficient space
|
||||
- diskUsage:
|
||||
checkName: "var-lib-rook"
|
||||
collectorName: "var-lib-rook"
|
||||
outcomes:
|
||||
- warn:
|
||||
when: "used/total > 80%"
|
||||
message: The disk containing directory /var/lib/rook is more than 80% full
|
||||
- warn:
|
||||
when: "available < 10Gi"
|
||||
message: The disk containing directory /var/lib/rook has less than 10Gi of disk space available
|
||||
- pass:
|
||||
message: The disk containing directory /var/lib/rook has sufficient space
|
||||
- diskUsage:
|
||||
checkName: "opt-replicated"
|
||||
collectorName: "opt-replicated"
|
||||
outcomes:
|
||||
- warn:
|
||||
when: "used/total > 80%"
|
||||
message: The disk containing directory /opt/replicated is more than 80% full
|
||||
- warn:
|
||||
when: "available < 10Gi"
|
||||
message: The disk containing directory /opt/replicated has less than 10Gi of disk space available
|
||||
- pass:
|
||||
message: The disk containing directory /opt/replicated has sufficient space
|
||||
- diskUsage:
|
||||
checkName: "var-openebs"
|
||||
collectorName: "var-openebs"
|
||||
outcomes:
|
||||
- warn:
|
||||
when: "used/total > 80%"
|
||||
message: The disk containing directory /var/openebs is more than 80% full
|
||||
- warn:
|
||||
when: "available < 10Gi"
|
||||
message: The disk containing directory /var/openebs has less than 10Gi of disk space available
|
||||
- pass:
|
||||
message: The disk containing directory /var/openebs has sufficient space
|
||||
- http:
|
||||
checkName: curl-k8s-api-6443
|
||||
collectorName: curl-k8s-api-6443
|
||||
outcomes:
|
||||
- warn:
|
||||
when: "error"
|
||||
message: Unable to curl https://localhost:6443/healthz. Please, run `curl -k https://localhost:6443/healthz` to check further information.
|
||||
- pass:
|
||||
when: "statusCode == 200"
|
||||
message: curl -k https://localhost:6443/healthz returned HTTP CODE response 200.
|
||||
- warn:
|
||||
message: "Unexpected response. HTTP CODE response is not 200. Please, run `curl -ki https://localhost:6443/healthz` to check further information."
|
||||
- http:
|
||||
checkName: curl-api-replicated-com
|
||||
collectorName: curl-api-replicated-com
|
||||
outcomes:
|
||||
- warn:
|
||||
when: "error"
|
||||
message: Error connecting to https://api.replicated.com/healthz
|
||||
- pass:
|
||||
when: "statusCode == 200"
|
||||
message: Connected to https://api.replicated.com/healthz
|
||||
- warn:
|
||||
message: "Unexpected response"
|
||||
- http:
|
||||
checkName: get-proxy-replicated-com
|
||||
collectorName: get-proxy-replicated-com
|
||||
outcomes:
|
||||
- warn:
|
||||
when: "error"
|
||||
message: Error connecting to https://proxy.replicated.com
|
||||
- pass:
|
||||
when: "statusCode == 401"
|
||||
message: Connected to https://proxy.replicated.com
|
||||
- warn:
|
||||
message: "Unexpected response"
|
||||
- http:
|
||||
checkName: curl-get-replicated-com
|
||||
collectorName: curl-get-replicated-com
|
||||
outcomes:
|
||||
- warn:
|
||||
when: "error"
|
||||
message: Error connecting to https://get.replicated.com/healthz
|
||||
- pass:
|
||||
when: "statusCode == 200"
|
||||
message: Connected to https://get.replicated.com/healthz
|
||||
- warn:
|
||||
message: "Unexpected response"
|
||||
- http:
|
||||
checkName: curl-registry-replicated-com
|
||||
collectorName: curl-registry-replicated-com
|
||||
outcomes:
|
||||
- warn:
|
||||
when: "error"
|
||||
message: Error connecting to https://registry.replicated.com/healthz
|
||||
- pass:
|
||||
when: "statusCode == 200"
|
||||
message: Connected to https://registry.replicated.com/healthz
|
||||
- warn:
|
||||
message: "Unexpected response"
|
||||
- http:
|
||||
checkName: curl-proxy-replicated-com
|
||||
collectorName: curl-proxy-replicated-com
|
||||
outcomes:
|
||||
- warn:
|
||||
when: "error"
|
||||
message: Error connecting to https://proxy.replicated.com/healthz
|
||||
- pass:
|
||||
when: "statusCode == 200"
|
||||
message: Connected to https://proxy.replicated.com/healthz
|
||||
- warn:
|
||||
message: "Unexpected response"
|
||||
- http:
|
||||
checkName: curl-k8s-kurl-sh
|
||||
collectorName: curl-k8s-kurl-sh
|
||||
outcomes:
|
||||
- warn:
|
||||
when: "error"
|
||||
message: Error connecting to https://k8s.kurl.sh/healthz
|
||||
- pass:
|
||||
when: "statusCode == 200"
|
||||
message: Connected to https://k8s.kurl.sh/healthz
|
||||
- warn:
|
||||
message: "Unexpected response"
|
||||
- http:
|
||||
checkName: curl-replicated-app
|
||||
collectorName: curl-replicated-app
|
||||
outcomes:
|
||||
- warn:
|
||||
when: "error"
|
||||
message: Error connecting to https://replicated.app/healthz
|
||||
- pass:
|
||||
when: "statusCode == 200"
|
||||
message: Connected to https://replicated.app/healthz
|
||||
- warn:
|
||||
message: "Unexpected response"
|
||||
- filesystemPerformance:
|
||||
collectorName: filesystem-latency-two-minute-benchmark
|
||||
outcomes:
|
||||
- pass:
|
||||
when: "p99 < 10ms"
|
||||
message: "Write latency is ok (p99 target < 10ms)"
|
||||
- warn:
|
||||
message: "Write latency is high. p99 target >= 10ms)"
|
||||
analyzers:
|
||||
- textAnalyze:
|
||||
checkName: Hostname Mismatch
|
||||
fileName: host-collectors/run-host/journalctl-kubelet.txt
|
||||
regex: ".*can only access node lease with the same name as the requesting node.*"
|
||||
outcomes:
|
||||
- fail:
|
||||
when: "true"
|
||||
message: "Possible hostname change. Verify that the current hostname matches what's expected by the k8s control plane"
|
||||
- pass:
|
||||
when: "false"
|
||||
message: "No signs of hostname changes found"
|
||||
- textAnalyze:
|
||||
checkName: "Check for CNI 'not ready' messages"
|
||||
fileName: host-collectors/run-host/journalctl-kubelet.txt
|
||||
regex: "Container runtime network not ready.*cni plugin not initialized"
|
||||
outcomes:
|
||||
- pass:
|
||||
when: "false"
|
||||
message: "CNI is initialized"
|
||||
- fail:
|
||||
when: "true"
|
||||
message: "CNI plugin not initialized: there may be a problem with the CNI configuration on the host, check /etc/cni/net.d/*.conflist against a known good configuration"
|
||||
- textAnalyze:
|
||||
checkName: Kubernetes API health check
|
||||
fileName: host-collectors/run-host/k8s-api-healthz-6443.txt
|
||||
regex: ".*healthz check passed*"
|
||||
outcomes:
|
||||
- fail:
|
||||
when: "false"
|
||||
message: "Kubernetes API health check did not pass. One or more components are not working."
|
||||
- pass:
|
||||
when: "true"
|
||||
message: "Kubernetes API health check passed"
|
||||
- textAnalyze:
|
||||
checkName: ETCD Kubernetes API health check
|
||||
fileName: host-collectors/run-host/k8s-api-healthz-6443.txt
|
||||
regex: ".*etcd ok*"
|
||||
outcomes:
|
||||
- fail:
|
||||
when: "false"
|
||||
message: "ETCD is unhealthy"
|
||||
- pass:
|
||||
when: "true"
|
||||
message: "ETCD healthz check using Kubernetes API is OK"
|
||||
- textAnalyze:
|
||||
checkName: ETCD API Health
|
||||
fileName: host-collectors/run-host/curl-etcd-health-2379.txt
|
||||
regex: ".*\"health\":\"true\"*"
|
||||
outcomes:
|
||||
- fail:
|
||||
when: "false"
|
||||
message: "ETCD status returned: unhealthy"
|
||||
- pass:
|
||||
when: "true"
|
||||
message: "ETCD status returned: healthy"
|
||||
- textAnalyze:
|
||||
checkName: Check if localhost resolves to 127.0.0.1
|
||||
fileName: host-collectors/run-host/localhost-ips.txt
|
||||
regex: 'localhost has address 127.0.0.1'
|
||||
outcomes:
|
||||
- fail:
|
||||
when: "false"
|
||||
message: "'localhost' does not resolve to 127.0.0.1 ip address"
|
||||
- pass:
|
||||
when: "true"
|
||||
message: "'localhost' resolves to 127.0.0.1 ip address"
|
||||
- textAnalyze:
|
||||
checkName: Check if SELinux is enabled
|
||||
fileName: host-collectors/run-host/sestatus.txt
|
||||
regex: '(?m)^Current mode:\s+enforcing'
|
||||
ignoreIfNoFiles: true
|
||||
outcomes:
|
||||
- fail:
|
||||
when: "true"
|
||||
message: "SELinux is enabled when it should be disabled for kubernetes to work properly"
|
||||
- pass:
|
||||
when: "false"
|
||||
message: "SELinux is disabled as expected"
|
||||
- textAnalyze:
|
||||
checkName: "Detect Threat Management and Network Security Tools"
|
||||
fileName: host-collectors/run-host/ps-detect-antivirus-and-security-tools.txt
|
||||
regex: '\b(clamav|sophos|esets_daemon|fsav|symantec|mfend|ds_agent|kav|bdagent|s1agent|falcon|illumio|xagt|wdavdaemon|mdatp)\b'
|
||||
ignoreIfNoFiles: true
|
||||
outcomes:
|
||||
- fail:
|
||||
when: "true"
|
||||
message: "Antivirus or Network Security tools detected. These tools can interfere with kubernetes operation."
|
||||
- pass:
|
||||
when: "false"
|
||||
message: "No Antivirus or Network Security tools detected."
|
||||
- systemPackages:
|
||||
collectorName: security-tools-packages
|
||||
outcomes:
|
||||
- fail:
|
||||
when: '{{ .IsInstalled }}'
|
||||
message: Package {{ .Name }} is installed. This tool can interfere with kubernetes operation.
|
||||
- pass:
|
||||
message: Package {{ .Name }} is not installed
|
||||
483
examples/preflight/all-analyzers-v1beta2.yaml
Normal file
483
examples/preflight/all-analyzers-v1beta2.yaml
Normal file
@@ -0,0 +1,483 @@
|
||||
apiVersion: troubleshoot.sh/v1beta2
|
||||
kind: Preflight
|
||||
metadata:
|
||||
name: all-analyzers-v1beta2
|
||||
spec:
|
||||
collectors:
|
||||
# Generic cluster resources (used by several analyzers like events)
|
||||
- clusterResources:
|
||||
collectorName: cluster-resources
|
||||
|
||||
# Text/YAML/JSON inputs for textAnalyze/yamlCompare/jsonCompare
|
||||
- data:
|
||||
name: config/replicas.txt
|
||||
data: "5"
|
||||
- data:
|
||||
name: files/example.yaml
|
||||
data: |
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: sample
|
||||
data:
|
||||
key: value
|
||||
- data:
|
||||
name: files/example.json
|
||||
data: '{"foo": {"bar": "baz"}}'
|
||||
|
||||
# Database connection collectors (postgres, mssql, mysql, redis)
|
||||
- postgres:
|
||||
collectorName: pg
|
||||
uri: postgresql://user:password@hostname:5432/defaultdb?sslmode=disable
|
||||
- mssql:
|
||||
collectorName: mssql
|
||||
uri: sqlserver://user:password@hostname:1433/master
|
||||
- mysql:
|
||||
collectorName: mysql
|
||||
uri: mysql://user:password@hostname:3306/defaultdb
|
||||
- redis:
|
||||
collectorName: redis
|
||||
uri: redis://:password@hostname:6379
|
||||
|
||||
# Registry images (used by registryImages analyzer)
|
||||
- registryImages:
|
||||
collectorName: registry-images
|
||||
namespace: default
|
||||
images:
|
||||
- nginx:1.25
|
||||
- alpine:3.19
|
||||
|
||||
# HTTP checks (used by http analyzer)
|
||||
- http:
|
||||
collectorName: http-check
|
||||
get:
|
||||
url: https://example.com/healthz
|
||||
timeout: 5s
|
||||
|
||||
# Node metrics (used by nodeMetrics analyzer)
|
||||
- nodeMetrics:
|
||||
collectorName: node-metrics
|
||||
|
||||
# Sysctl (used by sysctl analyzer)
|
||||
- sysctl:
|
||||
collectorName: sysctl
|
||||
namespace: default
|
||||
image: busybox
|
||||
|
||||
# Certificates (used by certificates analyzer)
|
||||
- certificates:
|
||||
collectorName: certs
|
||||
secrets:
|
||||
- namespaces: ["default"]
|
||||
configMaps:
|
||||
- namespaces: ["default"]
|
||||
|
||||
# Goldpinger (used by goldpinger analyzer)
|
||||
- goldpinger:
|
||||
collectorName: goldpinger
|
||||
namespace: default
|
||||
collectDelay: 10s
|
||||
|
||||
analyzers:
|
||||
# Kubernetes version
|
||||
- clusterVersion:
|
||||
checkName: Kubernetes version
|
||||
outcomes:
|
||||
- fail:
|
||||
when: "< 1.20.0"
|
||||
message: Requires at least Kubernetes 1.20.0
|
||||
- warn:
|
||||
when: "< 1.22.0"
|
||||
message: Recommended to use Kubernetes 1.22.0 or later
|
||||
- pass:
|
||||
when: ">= 1.22.0"
|
||||
message: Meets recommended and required versions
|
||||
|
||||
# StorageClass
|
||||
- storageClass:
|
||||
checkName: Default StorageClass
|
||||
storageClassName: "default"
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Default StorageClass not found
|
||||
- pass:
|
||||
message: Default StorageClass present
|
||||
|
||||
# CustomResourceDefinition
|
||||
- customResourceDefinition:
|
||||
checkName: Required CRD
|
||||
customResourceDefinitionName: widgets.example.com
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Required CRD not found
|
||||
- pass:
|
||||
message: Required CRD present
|
||||
|
||||
# Ingress
|
||||
- ingress:
|
||||
checkName: Ingress exists
|
||||
namespace: default
|
||||
ingressName: my-app-ingress
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Expected ingress not found
|
||||
- pass:
|
||||
message: Expected ingress present
|
||||
|
||||
# Secret
|
||||
- secret:
|
||||
checkName: Required secret
|
||||
namespace: default
|
||||
secretName: my-secret
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Required secret not found
|
||||
- pass:
|
||||
message: Required secret present
|
||||
|
||||
# ConfigMap
|
||||
- configMap:
|
||||
checkName: Required ConfigMap
|
||||
namespace: default
|
||||
configMapName: my-config
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Required ConfigMap not found
|
||||
- pass:
|
||||
message: Required ConfigMap present
|
||||
|
||||
# ImagePullSecret presence
|
||||
- imagePullSecret:
|
||||
checkName: Registry credentials
|
||||
registryName: quay.io
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Cannot pull from registry; credentials missing
|
||||
- pass:
|
||||
message: Found credentials for registry
|
||||
|
||||
# Deployment status
|
||||
- deploymentStatus:
|
||||
checkName: Deployment ready
|
||||
namespace: default
|
||||
name: my-deployment
|
||||
outcomes:
|
||||
- fail:
|
||||
when: absent
|
||||
message: Deployment not found
|
||||
- fail:
|
||||
when: "< 1"
|
||||
message: Deployment has insufficient ready replicas
|
||||
- pass:
|
||||
when: ">= 1"
|
||||
message: Deployment has sufficient ready replicas
|
||||
|
||||
# StatefulSet status
|
||||
- statefulsetStatus:
|
||||
checkName: StatefulSet ready
|
||||
namespace: default
|
||||
name: my-statefulset
|
||||
outcomes:
|
||||
- fail:
|
||||
when: absent
|
||||
message: StatefulSet not found
|
||||
- fail:
|
||||
when: "< 1"
|
||||
message: StatefulSet has insufficient ready replicas
|
||||
- pass:
|
||||
when: ">= 1"
|
||||
message: StatefulSet has sufficient ready replicas
|
||||
|
||||
# Job status
|
||||
- jobStatus:
|
||||
checkName: Job completed
|
||||
namespace: default
|
||||
name: my-job
|
||||
outcomes:
|
||||
- fail:
|
||||
when: absent
|
||||
message: Job not found
|
||||
- fail:
|
||||
when: "= 0"
|
||||
message: Job has no successful completions
|
||||
- pass:
|
||||
when: "> 0"
|
||||
message: Job completed successfully
|
||||
|
||||
# ReplicaSet status
|
||||
- replicasetStatus:
|
||||
checkName: ReplicaSet ready
|
||||
namespace: default
|
||||
name: my-replicaset
|
||||
outcomes:
|
||||
- fail:
|
||||
message: ReplicaSet is not ready
|
||||
- pass:
|
||||
when: ">= 1"
|
||||
message: ReplicaSet has sufficient ready replicas
|
||||
|
||||
# Cluster pod statuses
|
||||
- clusterPodStatuses:
|
||||
checkName: Pod statuses
|
||||
namespaces:
|
||||
- kube-system
|
||||
outcomes:
|
||||
- warn:
|
||||
message: Some pods are not ready
|
||||
- pass:
|
||||
message: All pods are ready
|
||||
|
||||
# Cluster container statuses (restarts)
|
||||
- clusterContainerStatuses:
|
||||
checkName: Container restarts
|
||||
namespaces:
|
||||
- kube-system
|
||||
restartCount: 3
|
||||
outcomes:
|
||||
- warn:
|
||||
message: One or more containers exceed restart threshold
|
||||
- pass:
|
||||
message: Container restarts are within thresholds
|
||||
|
||||
# Container runtime
|
||||
- containerRuntime:
|
||||
checkName: Runtime must be containerd
|
||||
outcomes:
|
||||
- pass:
|
||||
when: "== containerd"
|
||||
message: containerd runtime detected
|
||||
- fail:
|
||||
message: Unsupported container runtime; containerd required
|
||||
|
||||
# Distribution
|
||||
- distribution:
|
||||
checkName: Supported distribution
|
||||
outcomes:
|
||||
- fail:
|
||||
when: "== docker-desktop"
|
||||
message: Docker Desktop is not supported
|
||||
- pass:
|
||||
when: "== eks"
|
||||
message: EKS is supported
|
||||
- warn:
|
||||
message: Unable to determine the distribution
|
||||
|
||||
# Node resources - cluster size
|
||||
- nodeResources:
|
||||
checkName: Node count
|
||||
outcomes:
|
||||
- fail:
|
||||
when: "count() < 3"
|
||||
message: Requires at least 3 nodes
|
||||
- warn:
|
||||
when: "count() < 5"
|
||||
message: Recommended at least 5 nodes
|
||||
- pass:
|
||||
message: Cluster has sufficient nodes
|
||||
|
||||
# Node resources - per-node memory
|
||||
- nodeResources:
|
||||
checkName: Per-node memory
|
||||
outcomes:
|
||||
- fail:
|
||||
when: "min(memoryCapacity) < 8Gi"
|
||||
message: All nodes must have at least 8 GiB
|
||||
- pass:
|
||||
message: All nodes meet recommended memory
|
||||
|
||||
# Text analyze (regex on collected file)
|
||||
- textAnalyze:
|
||||
checkName: Text analyze
|
||||
fileName: config/replicas.txt
|
||||
regexGroups: '(?P<Replicas>\d+)'
|
||||
outcomes:
|
||||
- fail:
|
||||
when: "Replicas < 5"
|
||||
message: Not enough replicas
|
||||
- pass:
|
||||
message: Replica count is sufficient
|
||||
|
||||
# YAML compare
|
||||
- yamlCompare:
|
||||
checkName: YAML compare
|
||||
fileName: files/example.yaml
|
||||
path: data.key
|
||||
value: value
|
||||
outcomes:
|
||||
- fail:
|
||||
message: YAML value does not match expected
|
||||
- pass:
|
||||
message: YAML value matches expected
|
||||
|
||||
# JSON compare
|
||||
- jsonCompare:
|
||||
checkName: JSON compare
|
||||
fileName: files/example.json
|
||||
jsonPath: $.foo.bar
|
||||
value: baz
|
||||
outcomes:
|
||||
- fail:
|
||||
message: JSON value does not match expected
|
||||
- pass:
|
||||
message: JSON value matches expected
|
||||
|
||||
# Postgres
|
||||
- postgres:
|
||||
checkName: Postgres checks
|
||||
collectorName: pg
|
||||
outcomes:
|
||||
- fail:
|
||||
when: "connected == false"
|
||||
message: Cannot connect to postgres server
|
||||
- pass:
|
||||
message: Postgres connection checks out
|
||||
|
||||
# MSSQL
|
||||
- mssql:
|
||||
checkName: MSSQL checks
|
||||
collectorName: mssql
|
||||
outcomes:
|
||||
- fail:
|
||||
when: "connected == false"
|
||||
message: Cannot connect to SQL Server
|
||||
- pass:
|
||||
message: MSSQL connection checks out
|
||||
|
||||
# MySQL
|
||||
- mysql:
|
||||
checkName: MySQL checks
|
||||
collectorName: mysql
|
||||
outcomes:
|
||||
- fail:
|
||||
when: "connected == false"
|
||||
message: Cannot connect to MySQL server
|
||||
- pass:
|
||||
message: MySQL connection checks out
|
||||
|
||||
# Redis
|
||||
- redis:
|
||||
checkName: Redis checks
|
||||
collectorName: redis
|
||||
outcomes:
|
||||
- fail:
|
||||
when: "connected == false"
|
||||
message: Cannot connect to Redis server
|
||||
- pass:
|
||||
message: Redis connection checks out
|
||||
|
||||
# Ceph status
|
||||
- cephStatus:
|
||||
checkName: Ceph cluster health
|
||||
namespace: rook-ceph
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Ceph is not healthy
|
||||
- pass:
|
||||
message: Ceph is healthy
|
||||
|
||||
# Velero
|
||||
- velero:
|
||||
checkName: Velero installed
|
||||
|
||||
# Longhorn
|
||||
- longhorn:
|
||||
checkName: Longhorn health
|
||||
namespace: longhorn-system
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Longhorn is not healthy
|
||||
- pass:
|
||||
message: Longhorn is healthy
|
||||
|
||||
# Registry images availability
|
||||
- registryImages:
|
||||
checkName: Registry image availability
|
||||
collectorName: registry-images
|
||||
outcomes:
|
||||
- fail:
|
||||
message: One or more images are not available
|
||||
- pass:
|
||||
message: All images are available
|
||||
|
||||
# Weave report (expects weave report files to be present if collected)
|
||||
- weaveReport:
|
||||
checkName: Weave report
|
||||
reportFileGlob: kots/kurl/weave/kube-system/*/weave-report-stdout.txt
|
||||
|
||||
# Sysctl (cluster-level)
|
||||
- sysctl:
|
||||
checkName: Sysctl settings
|
||||
outcomes:
|
||||
- warn:
|
||||
message: One or more sysctl values do not meet recommendations
|
||||
- pass:
|
||||
message: Sysctl values meet recommendations
|
||||
|
||||
# Cluster resource YAML field compare
|
||||
- clusterResource:
|
||||
checkName: Cluster resource value
|
||||
kind: Namespace
|
||||
clusterScoped: true
|
||||
name: kube-system
|
||||
yamlPath: metadata.name
|
||||
expectedValue: kube-system
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Cluster resource field does not match expected value
|
||||
- pass:
|
||||
message: Cluster resource field matches expected value
|
||||
|
||||
# Certificates analyzer
|
||||
- certificates:
|
||||
checkName: Certificates validity
|
||||
outcomes:
|
||||
- warn:
|
||||
message: One or more certificates may be invalid or expiring soon
|
||||
- pass:
|
||||
message: Certificates are valid
|
||||
|
||||
# Goldpinger analyzer
|
||||
- goldpinger:
|
||||
checkName: Goldpinger report
|
||||
collectorName: goldpinger
|
||||
filePath: goldpinger/report.json
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Goldpinger indicates network issues
|
||||
- pass:
|
||||
message: Goldpinger indicates healthy networking
|
||||
|
||||
# Event analyzer (requires events in clusterResources)
|
||||
- event:
|
||||
checkName: Events
|
||||
collectorName: cluster-resources
|
||||
namespace: default
|
||||
reason: Failed
|
||||
regex: ".*"
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Critical events detected
|
||||
- pass:
|
||||
message: No critical events detected
|
||||
|
||||
# Node metrics analyzer
|
||||
- nodeMetrics:
|
||||
checkName: Node metrics thresholds
|
||||
collectorName: node-metrics
|
||||
outcomes:
|
||||
- warn:
|
||||
message: Node metrics exceed warning thresholds
|
||||
- pass:
|
||||
message: Node metrics within thresholds
|
||||
|
||||
# HTTP analyzer (cluster)
|
||||
- http:
|
||||
checkName: HTTP checks
|
||||
collectorName: http-check
|
||||
outcomes:
|
||||
- fail:
|
||||
message: One or more HTTP checks failed
|
||||
- pass:
|
||||
message: All HTTP checks passed
|
||||
|
||||
|
||||
905
examples/preflight/complex-v1beta3.yaml
Normal file
905
examples/preflight/complex-v1beta3.yaml
Normal file
@@ -0,0 +1,905 @@
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: Preflight
|
||||
metadata:
|
||||
name: all-analyzers
|
||||
spec:
|
||||
{{- /* Determine if we need explicit collectors beyond always-on clusterResources */}}
|
||||
{{- $needExtraCollectors := or (or (or .Values.databases.postgres.enabled .Values.databases.mssql.enabled) (or .Values.databases.mysql.enabled .Values.databases.redis.enabled)) (or (or (or .Values.registryImages.enabled .Values.http.enabled) (or .Values.nodeMetrics.enabled (or .Values.sysctl.enabled .Values.certificates.enabled))) (or (or .Values.goldpinger.enabled .Values.cephStatus.enabled) .Values.longhorn.enabled)) }}
|
||||
|
||||
collectors:
|
||||
# Always collect cluster resources to support core analyzers (deployments, secrets, pods, events, etc.)
|
||||
- clusterResources: {}
|
||||
|
||||
{{- if .Values.databases.postgres.enabled }}
|
||||
- postgres:
|
||||
collectorName: '{{ .Values.databases.postgres.collectorName }}'
|
||||
uri: '{{ .Values.databases.postgres.uri }}'
|
||||
{{- if .Values.databases.postgres.tls }}
|
||||
tls:
|
||||
skipVerify: {{ .Values.databases.postgres.tls.skipVerify | default false }}
|
||||
{{- if .Values.databases.postgres.tls.secret }}
|
||||
secret:
|
||||
name: '{{ .Values.databases.postgres.tls.secret.name }}'
|
||||
namespace: '{{ .Values.databases.postgres.tls.secret.namespace }}'
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.databases.mssql.enabled }}
|
||||
- mssql:
|
||||
collectorName: '{{ .Values.databases.mssql.collectorName }}'
|
||||
uri: '{{ .Values.databases.mssql.uri }}'
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.databases.mysql.enabled }}
|
||||
- mysql:
|
||||
collectorName: '{{ .Values.databases.mysql.collectorName }}'
|
||||
uri: '{{ .Values.databases.mysql.uri }}'
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.databases.redis.enabled }}
|
||||
- redis:
|
||||
collectorName: '{{ .Values.databases.redis.collectorName }}'
|
||||
uri: '{{ .Values.databases.redis.uri }}'
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.registryImages.enabled }}
|
||||
- registryImages:
|
||||
collectorName: '{{ .Values.registryImages.collectorName }}'
|
||||
namespace: '{{ .Values.registryImages.namespace }}'
|
||||
{{- if .Values.registryImages.imagePullSecret }}
|
||||
imagePullSecret:
|
||||
name: '{{ .Values.registryImages.imagePullSecret.name }}'
|
||||
{{- if .Values.registryImages.imagePullSecret.data }}
|
||||
data:
|
||||
{{- range $k, $v := .Values.registryImages.imagePullSecret.data }}
|
||||
{{ $k }}: '{{ $v }}'
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
images:
|
||||
{{- range .Values.registryImages.images }}
|
||||
- '{{ . }}'
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.http.enabled }}
|
||||
- http:
|
||||
collectorName: '{{ .Values.http.collectorName }}'
|
||||
{{- if .Values.http.get }}
|
||||
get:
|
||||
url: '{{ .Values.http.get.url }}'
|
||||
{{- if .Values.http.get.timeout }}
|
||||
timeout: '{{ .Values.http.get.timeout }}'
|
||||
{{- end }}
|
||||
{{- if .Values.http.get.insecureSkipVerify }}
|
||||
insecureSkipVerify: {{ .Values.http.get.insecureSkipVerify }}
|
||||
{{- end }}
|
||||
{{- if .Values.http.get.headers }}
|
||||
headers:
|
||||
{{- range $k, $v := .Values.http.get.headers }}
|
||||
{{ $k }}: '{{ $v }}'
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.http.post }}
|
||||
post:
|
||||
url: '{{ .Values.http.post.url }}'
|
||||
{{- if .Values.http.post.timeout }}
|
||||
timeout: '{{ .Values.http.post.timeout }}'
|
||||
{{- end }}
|
||||
{{- if .Values.http.post.insecureSkipVerify }}
|
||||
insecureSkipVerify: {{ .Values.http.post.insecureSkipVerify }}
|
||||
{{- end }}
|
||||
{{- if .Values.http.post.headers }}
|
||||
headers:
|
||||
{{- range $k, $v := .Values.http.post.headers }}
|
||||
{{ $k }}: '{{ $v }}'
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.http.post.body }}
|
||||
body: '{{ .Values.http.post.body }}'
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.nodeMetrics.enabled }}
|
||||
- nodeMetrics:
|
||||
collectorName: '{{ .Values.nodeMetrics.collectorName }}'
|
||||
{{- if .Values.nodeMetrics.nodeNames }}
|
||||
nodeNames:
|
||||
{{- range .Values.nodeMetrics.nodeNames }}
|
||||
- '{{ . }}'
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.nodeMetrics.selector }}
|
||||
selector:
|
||||
{{- range .Values.nodeMetrics.selector }}
|
||||
- '{{ . }}'
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.sysctl.enabled }}
|
||||
- sysctl:
|
||||
collectorName: 'sysctl'
|
||||
namespace: '{{ .Values.sysctl.namespace }}'
|
||||
image: '{{ .Values.sysctl.image }}'
|
||||
{{- if .Values.sysctl.imagePullPolicy }}
|
||||
imagePullPolicy: '{{ .Values.sysctl.imagePullPolicy }}'
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.certificates.enabled }}
|
||||
- certificates:
|
||||
collectorName: 'certs'
|
||||
{{- if .Values.certificates.secrets }}
|
||||
secrets:
|
||||
{{- range .Values.certificates.secrets }}
|
||||
- name: '{{ .name }}'
|
||||
namespaces:
|
||||
{{- range .namespaces }}
|
||||
- '{{ . }}'
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.certificates.configMaps }}
|
||||
configMaps:
|
||||
{{- range .Values.certificates.configMaps }}
|
||||
- name: '{{ .name }}'
|
||||
namespaces:
|
||||
{{- range .namespaces }}
|
||||
- '{{ . }}'
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.longhorn.enabled }}
|
||||
- longhorn:
|
||||
collectorName: 'longhorn'
|
||||
namespace: '{{ .Values.longhorn.namespace }}'
|
||||
{{- if .Values.longhorn.timeout }}
|
||||
timeout: '{{ .Values.longhorn.timeout }}'
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.cephStatus.enabled }}
|
||||
- ceph:
|
||||
collectorName: 'ceph'
|
||||
namespace: '{{ .Values.cephStatus.namespace }}'
|
||||
{{- if .Values.cephStatus.timeout }}
|
||||
timeout: '{{ .Values.cephStatus.timeout }}'
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.goldpinger.enabled }}
|
||||
- goldpinger:
|
||||
collectorName: '{{ .Values.goldpinger.collectorName }}'
|
||||
namespace: '{{ .Values.goldpinger.namespace }}'
|
||||
{{- if .Values.goldpinger.collectDelay }}
|
||||
collectDelay: '{{ .Values.goldpinger.collectDelay }}'
|
||||
{{- end }}
|
||||
{{- if .Values.goldpinger.podLaunch }}
|
||||
podLaunchOptions:
|
||||
{{- if .Values.goldpinger.podLaunch.namespace }}
|
||||
namespace: '{{ .Values.goldpinger.podLaunch.namespace }}'
|
||||
{{- end }}
|
||||
{{- if .Values.goldpinger.podLaunch.image }}
|
||||
image: '{{ .Values.goldpinger.podLaunch.image }}'
|
||||
{{- end }}
|
||||
{{- if .Values.goldpinger.podLaunch.imagePullSecret }}
|
||||
imagePullSecret:
|
||||
name: '{{ .Values.goldpinger.podLaunch.imagePullSecret.name }}'
|
||||
{{- end }}
|
||||
{{- if .Values.goldpinger.podLaunch.serviceAccountName }}
|
||||
serviceAccountName: '{{ .Values.goldpinger.podLaunch.serviceAccountName }}'
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
analyzers:
|
||||
{{- if .Values.clusterVersion.enabled }}
|
||||
- docString: |
|
||||
Title: Kubernetes Control Plane Requirements
|
||||
Requirement:
|
||||
- Version:
|
||||
- Minimum: {{ .Values.clusterVersion.minVersion }}
|
||||
- Recommended: {{ .Values.clusterVersion.recommendedVersion }}
|
||||
Running below the minimum can remove or alter required GA APIs and lacks critical CVE fixes. The recommended version aligns with CI coverage and provides safer upgrades and operational guidance.
|
||||
clusterVersion:
|
||||
checkName: Kubernetes version
|
||||
outcomes:
|
||||
- fail:
|
||||
when: '< {{ .Values.clusterVersion.minVersion }}'
|
||||
message: Requires at least Kubernetes {{ .Values.clusterVersion.minVersion }}.
|
||||
- warn:
|
||||
when: '< {{ .Values.clusterVersion.recommendedVersion }}'
|
||||
message: Recommended to use Kubernetes {{ .Values.clusterVersion.recommendedVersion }} or later.
|
||||
- pass:
|
||||
when: '>= {{ .Values.clusterVersion.recommendedVersion }}'
|
||||
message: Meets recommended and required Kubernetes versions.
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.storageClass.enabled }}
|
||||
- docString: |
|
||||
Title: Default StorageClass Requirements
|
||||
Requirement:
|
||||
- A StorageClass named "{{ .Values.storageClass.className }}" must exist
|
||||
A default StorageClass enables dynamic PVC provisioning without manual intervention. Missing or misnamed defaults cause PVCs to remain Pending and block workloads.
|
||||
storageClass:
|
||||
checkName: Default StorageClass
|
||||
storageClassName: '{{ .Values.storageClass.className }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Default StorageClass not found
|
||||
- pass:
|
||||
message: Default StorageClass present
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.crd.enabled }}
|
||||
- docString: |
|
||||
Title: Required CRD Presence
|
||||
Requirement:
|
||||
- CRD must exist: {{ .Values.crd.name }}
|
||||
Controllers depending on this CRD cannot reconcile without it, leading to missing resources and degraded functionality.
|
||||
customResourceDefinition:
|
||||
checkName: Required CRD
|
||||
customResourceDefinitionName: '{{ .Values.crd.name }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Required CRD not found
|
||||
- pass:
|
||||
message: Required CRD present
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.ingress.enabled }}
|
||||
- docString: |
|
||||
Title: Ingress Object Presence
|
||||
Requirement:
|
||||
- Ingress exists: {{ .Values.ingress.namespace }}/{{ .Values.ingress.name }}
|
||||
Ensures external routing is configured to reach the application. Missing ingress prevents user traffic from reaching services.
|
||||
ingress:
|
||||
checkName: Ingress exists
|
||||
namespace: '{{ .Values.ingress.namespace }}'
|
||||
ingressName: '{{ .Values.ingress.name }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Expected ingress not found
|
||||
- pass:
|
||||
message: Expected ingress present
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.secret.enabled }}
|
||||
- docString: |
|
||||
Title: Required Secret Presence
|
||||
Requirement:
|
||||
- Secret exists: {{ .Values.secret.namespace }}/{{ .Values.secret.name }}{{ if .Values.secret.key }} (key: {{ .Values.secret.key }}){{ end }}
|
||||
Secrets commonly provide credentials or TLS material. Absence blocks components from authenticating or decrypting traffic.
|
||||
secret:
|
||||
checkName: Required secret
|
||||
namespace: '{{ .Values.secret.namespace }}'
|
||||
secretName: '{{ .Values.secret.name }}'
|
||||
{{- if .Values.secret.key }}
|
||||
key: '{{ .Values.secret.key }}'
|
||||
{{- end }}
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Required secret not found
|
||||
- pass:
|
||||
message: Required secret present
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.configMap.enabled }}
|
||||
- docString: |
|
||||
Title: Required ConfigMap Presence
|
||||
Requirement:
|
||||
- ConfigMap exists: {{ .Values.configMap.namespace }}/{{ .Values.configMap.name }}{{ if .Values.configMap.key }} (key: {{ .Values.configMap.key }}){{ end }}
|
||||
Required for bootstrapping configuration. Missing keys lead to defaulting or startup failure.
|
||||
configMap:
|
||||
checkName: Required ConfigMap
|
||||
namespace: '{{ .Values.configMap.namespace }}'
|
||||
configMapName: '{{ .Values.configMap.name }}'
|
||||
{{- if .Values.configMap.key }}
|
||||
key: '{{ .Values.configMap.key }}'
|
||||
{{- end }}
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Required ConfigMap not found
|
||||
- pass:
|
||||
message: Required ConfigMap present
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.imagePullSecret.enabled }}
|
||||
- docString: |
|
||||
Title: Container Registry Credentials
|
||||
Requirement:
|
||||
- Credentials present for registry: {{ .Values.imagePullSecret.registry }}
|
||||
Ensures images can be pulled from private registries. Missing secrets cause ImagePullBackOff and prevent workloads from starting.
|
||||
imagePullSecret:
|
||||
checkName: Registry credentials
|
||||
registryName: '{{ .Values.imagePullSecret.registry }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Cannot pull from registry; credentials missing
|
||||
- pass:
|
||||
message: Found credentials for registry
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.workloads.deployments.enabled }}
|
||||
- docString: |
|
||||
Title: Deployment Ready
|
||||
Requirement:
|
||||
- Deployment ready: {{ .Values.workloads.deployments.namespace }}/{{ .Values.workloads.deployments.name }} (minReady: {{ .Values.workloads.deployments.minReady }})
|
||||
Validates rollout completed and enough replicas are Ready to serve traffic.
|
||||
deploymentStatus:
|
||||
checkName: Deployment ready
|
||||
namespace: '{{ .Values.workloads.deployments.namespace }}'
|
||||
name: '{{ .Values.workloads.deployments.name }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
when: absent
|
||||
message: Deployment not found
|
||||
- fail:
|
||||
when: '< {{ .Values.workloads.deployments.minReady }}'
|
||||
message: Deployment has insufficient ready replicas
|
||||
- pass:
|
||||
when: '>= {{ .Values.workloads.deployments.minReady }}'
|
||||
message: Deployment has sufficient ready replicas
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.workloads.statefulsets.enabled }}
|
||||
- docString: |
|
||||
Title: StatefulSet Ready
|
||||
Requirement:
|
||||
- StatefulSet ready: {{ .Values.workloads.statefulsets.namespace }}/{{ .Values.workloads.statefulsets.name }} (minReady: {{ .Values.workloads.statefulsets.minReady }})
|
||||
Confirms ordered, persistent workloads have reached readiness before proceeding.
|
||||
statefulsetStatus:
|
||||
checkName: StatefulSet ready
|
||||
namespace: '{{ .Values.workloads.statefulsets.namespace }}'
|
||||
name: '{{ .Values.workloads.statefulsets.name }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
when: absent
|
||||
message: StatefulSet not found
|
||||
- fail:
|
||||
when: '< {{ .Values.workloads.statefulsets.minReady }}'
|
||||
message: StatefulSet has insufficient ready replicas
|
||||
- pass:
|
||||
when: '>= {{ .Values.workloads.statefulsets.minReady }}'
|
||||
message: StatefulSet has sufficient ready replicas
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.workloads.jobs.enabled }}
|
||||
- docString: |
|
||||
Title: Job Completion
|
||||
Requirement:
|
||||
- Job completed: {{ .Values.workloads.jobs.namespace }}/{{ .Values.workloads.jobs.name }}
|
||||
Verifies one-off tasks have succeeded; failures indicate setup or migration problems.
|
||||
jobStatus:
|
||||
checkName: Job completed
|
||||
namespace: '{{ .Values.workloads.jobs.namespace }}'
|
||||
name: '{{ .Values.workloads.jobs.name }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
when: absent
|
||||
message: Job not found
|
||||
- fail:
|
||||
when: '= 0'
|
||||
message: Job has no successful completions
|
||||
- pass:
|
||||
when: '> 0'
|
||||
message: Job completed successfully
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.workloads.replicasets.enabled }}
|
||||
- docString: |
|
||||
Title: ReplicaSet Ready
|
||||
Requirement:
|
||||
- ReplicaSet ready: {{ .Values.workloads.replicasets.namespace }}/{{ .Values.workloads.replicasets.name }} (minReady: {{ .Values.workloads.replicasets.minReady }})
|
||||
Ensures underlying ReplicaSet has produced the required number of Ready pods for upstream controllers.
|
||||
replicasetStatus:
|
||||
checkName: ReplicaSet ready
|
||||
namespace: '{{ .Values.workloads.replicasets.namespace }}'
|
||||
name: '{{ .Values.workloads.replicasets.name }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: ReplicaSet is not ready
|
||||
- pass:
|
||||
when: '>= {{ .Values.workloads.replicasets.minReady }}'
|
||||
message: ReplicaSet has sufficient ready replicas
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.clusterPodStatuses.enabled }}
|
||||
- docString: |
|
||||
Title: Cluster Pod Readiness by Namespace
|
||||
Requirement:
|
||||
- Namespaces checked: {{ toYaml .Values.clusterPodStatuses.namespaces | nindent 10 }}
|
||||
Highlights unhealthy pods across critical namespaces to surface rollout or configuration issues.
|
||||
clusterPodStatuses:
|
||||
checkName: Pod statuses
|
||||
namespaces: {{ toYaml .Values.clusterPodStatuses.namespaces | nindent 8 }}
|
||||
outcomes:
|
||||
- warn:
|
||||
message: Some pods are not ready
|
||||
- pass:
|
||||
message: All pods are ready
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.clusterContainerStatuses.enabled }}
|
||||
- docString: |
|
||||
Title: Container Restart Thresholds
|
||||
Requirement:
|
||||
- Namespaces checked: {{ toYaml .Values.clusterContainerStatuses.namespaces | nindent 10 }}
|
||||
- Restart threshold: {{ .Values.clusterContainerStatuses.restartCount }}
|
||||
Elevated restart counts often indicate crash loops, resource pressure, or image/runtime issues.
|
||||
clusterContainerStatuses:
|
||||
checkName: Container restarts
|
||||
namespaces: {{ toYaml .Values.clusterContainerStatuses.namespaces | nindent 8 }}
|
||||
restartCount: {{ .Values.clusterContainerStatuses.restartCount }}
|
||||
outcomes:
|
||||
- warn:
|
||||
message: One or more containers exceed restart threshold
|
||||
- pass:
|
||||
message: Container restarts are within thresholds
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.containerRuntime.enabled }}
|
||||
- docString: |
|
||||
Title: Container Runtime Compatibility
|
||||
Requirement:
|
||||
- Runtime must be: containerd
|
||||
containerd with CRI provides stable semantics; other runtimes are unsupported and may break image, cgroup, and networking expectations.
|
||||
containerRuntime:
|
||||
checkName: Runtime must be containerd
|
||||
outcomes:
|
||||
- pass:
|
||||
when: '== containerd'
|
||||
message: containerd runtime detected
|
||||
- fail:
|
||||
message: Unsupported container runtime; containerd required
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.distribution.enabled }}
|
||||
- docString: |
|
||||
Title: Supported Kubernetes Distributions
|
||||
Requirement:
|
||||
- Unsupported: {{ toYaml .Values.distribution.unsupported | nindent 12 }}
|
||||
- Supported: {{ toYaml .Values.distribution.supported | nindent 12 }}
|
||||
Production-tier assumptions (RBAC, admission, networking, storage) are validated on supported distros. Unsupported environments commonly diverge and reduce reliability.
|
||||
distribution:
|
||||
checkName: Supported distribution
|
||||
outcomes:
|
||||
{{- range $d := .Values.distribution.unsupported }}
|
||||
- fail:
|
||||
when: '== {{ $d }}'
|
||||
message: '{{ $d }} is not supported'
|
||||
{{- end }}
|
||||
{{- range $d := .Values.distribution.supported }}
|
||||
- pass:
|
||||
when: '== {{ $d }}'
|
||||
message: '{{ $d }} is a supported distribution'
|
||||
{{- end }}
|
||||
- warn:
|
||||
message: Unable to determine the distribution
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.nodeResources.count.enabled }}
|
||||
- docString: |
|
||||
Title: Node Count Requirement
|
||||
Requirement:
|
||||
- Minimum nodes: {{ .Values.nodeResources.count.min }}
|
||||
- Recommended nodes: {{ .Values.nodeResources.count.recommended }}
|
||||
Ensures capacity and disruption tolerance for upgrades and failures; too few nodes yields scheduling pressure and risk during maintenance.
|
||||
nodeResources:
|
||||
checkName: Node count
|
||||
outcomes:
|
||||
- fail:
|
||||
when: 'count() < {{ .Values.nodeResources.count.min }}'
|
||||
message: Requires at least {{ .Values.nodeResources.count.min }} nodes
|
||||
- warn:
|
||||
when: 'count() < {{ .Values.nodeResources.count.recommended }}'
|
||||
message: Recommended at least {{ .Values.nodeResources.count.recommended }} nodes
|
||||
- pass:
|
||||
message: Cluster has sufficient nodes
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.nodeResources.cpu.enabled }}
|
||||
- docString: |
|
||||
Title: Cluster CPU Capacity
|
||||
Requirement:
|
||||
- Total vCPU minimum: {{ .Values.nodeResources.cpu.min }}
|
||||
Aggregate CPU must cover control plane, system daemons, and application workloads; insufficient CPU causes scheduling delays and degraded throughput.
|
||||
nodeResources:
|
||||
checkName: Cluster CPU total
|
||||
outcomes:
|
||||
- fail:
|
||||
when: 'sum(cpuCapacity) < {{ .Values.nodeResources.cpu.min }}'
|
||||
message: Requires at least {{ .Values.nodeResources.cpu.min }} cores
|
||||
- pass:
|
||||
message: Cluster CPU capacity meets requirement
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.nodeResources.memory.enabled }}
|
||||
- docString: |
|
||||
Title: Per-node Memory Requirement
|
||||
Requirement:
|
||||
- Minimum per-node: {{ .Values.nodeResources.memory.minGi }} GiB
|
||||
- Recommended per-node: {{ .Values.nodeResources.memory.recommendedGi }} GiB
|
||||
Memory headroom avoids OOMKills and evictions during spikes and upgrades; recommended capacity supports stable operations.
|
||||
nodeResources:
|
||||
checkName: Per-node memory
|
||||
outcomes:
|
||||
- fail:
|
||||
when: 'min(memoryCapacity) < {{ .Values.nodeResources.memory.minGi }}Gi'
|
||||
message: All nodes must have at least {{ .Values.nodeResources.memory.minGi }} GiB
|
||||
- warn:
|
||||
when: 'min(memoryCapacity) < {{ .Values.nodeResources.memory.recommendedGi }}Gi'
|
||||
message: Recommended {{ .Values.nodeResources.memory.recommendedGi }} GiB per node
|
||||
- pass:
|
||||
message: All nodes meet recommended memory
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.nodeResources.ephemeral.enabled }}
|
||||
- docString: |
|
||||
Title: Per-node Ephemeral Storage Requirement
|
||||
Requirement:
|
||||
- Minimum per-node: {{ .Values.nodeResources.ephemeral.minGi }} GiB
|
||||
- Recommended per-node: {{ .Values.nodeResources.ephemeral.recommendedGi }} GiB
|
||||
Ephemeral storage backs images, container filesystems, and logs; insufficient capacity triggers disk pressure and failed pulls.
|
||||
nodeResources:
|
||||
checkName: Per-node ephemeral storage
|
||||
outcomes:
|
||||
- fail:
|
||||
when: 'min(ephemeralStorageCapacity) < {{ .Values.nodeResources.ephemeral.minGi }}Gi'
|
||||
message: All nodes must have at least {{ .Values.nodeResources.ephemeral.minGi }} GiB
|
||||
- warn:
|
||||
when: 'min(ephemeralStorageCapacity) < {{ .Values.nodeResources.ephemeral.recommendedGi }}Gi'
|
||||
message: Recommended {{ .Values.nodeResources.ephemeral.recommendedGi }} GiB per node
|
||||
- pass:
|
||||
message: All nodes meet recommended ephemeral storage
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.textAnalyze.enabled }}
|
||||
- docString: |
|
||||
Title: Text Analyze Pattern Check
|
||||
Requirement:
|
||||
- File(s): {{ .Values.textAnalyze.fileName }}
|
||||
- Regex: {{ .Values.textAnalyze.regex }}
|
||||
Surfaces error patterns in collected logs or text files that indicate configuration or runtime issues.
|
||||
textAnalyze:
|
||||
checkName: Text analyze
|
||||
collectorName: 'cluster-resources'
|
||||
fileName: '{{ .Values.textAnalyze.fileName }}'
|
||||
regex: '{{ .Values.textAnalyze.regex }}'
|
||||
ignoreIfNoFiles: true
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Pattern matched in files
|
||||
- pass:
|
||||
message: Pattern not found
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.yamlCompare.enabled }}
|
||||
- docString: |
|
||||
Title: YAML Field Comparison
|
||||
Requirement:
|
||||
- File: {{ .Values.yamlCompare.fileName }}
|
||||
- Path: {{ .Values.yamlCompare.path }}
|
||||
- Expected: {{ .Values.yamlCompare.value }}
|
||||
Validates rendered object fields match required configuration to ensure correct behavior.
|
||||
yamlCompare:
|
||||
checkName: YAML compare
|
||||
collectorName: 'cluster-resources'
|
||||
fileName: '{{ .Values.yamlCompare.fileName }}'
|
||||
path: '{{ .Values.yamlCompare.path }}'
|
||||
value: '{{ .Values.yamlCompare.value }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: YAML value does not match expected
|
||||
- pass:
|
||||
message: YAML value matches expected
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.jsonCompare.enabled }}
|
||||
- docString: |
|
||||
Title: JSON Field Comparison
|
||||
Requirement:
|
||||
- File: {{ .Values.jsonCompare.fileName }}
|
||||
- JSONPath: {{ .Values.jsonCompare.jsonPath }}
|
||||
- Expected: {{ .Values.jsonCompare.value }}
|
||||
Ensures collected JSON metrics or resources match required values.
|
||||
jsonCompare:
|
||||
checkName: JSON compare
|
||||
collectorName: 'cluster-resources'
|
||||
fileName: '{{ .Values.jsonCompare.fileName }}'
|
||||
jsonPath: '{{ .Values.jsonCompare.jsonPath }}'
|
||||
value: '{{ .Values.jsonCompare.value }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: JSON value does not match expected
|
||||
- pass:
|
||||
message: JSON value matches expected
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.databases.postgres.enabled }}
|
||||
- docString: |
|
||||
Title: Postgres Connectivity and Health
|
||||
Requirement:
|
||||
- Collector: {{ .Values.databases.postgres.collectorName }}
|
||||
Validates database availability and credentials to avoid boot failures or runtime errors.
|
||||
postgres:
|
||||
checkName: Postgres checks
|
||||
collectorName: '{{ .Values.databases.postgres.collectorName }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Postgres checks failed
|
||||
- pass:
|
||||
message: Postgres checks passed
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.databases.mssql.enabled }}
|
||||
- docString: |
|
||||
Title: MSSQL Connectivity and Health
|
||||
Requirement:
|
||||
- Collector: {{ .Values.databases.mssql.collectorName }}
|
||||
Ensures connectivity and credentials to Microsoft SQL Server are valid prior to workload startup.
|
||||
mssql:
|
||||
checkName: MSSQL checks
|
||||
collectorName: '{{ .Values.databases.mssql.collectorName }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: MSSQL checks failed
|
||||
- pass:
|
||||
message: MSSQL checks passed
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.databases.mysql.enabled }}
|
||||
- docString: |
|
||||
Title: MySQL Connectivity and Health
|
||||
Requirement:
|
||||
- Collector: {{ .Values.databases.mysql.collectorName }}
|
||||
Verifies MySQL reachability and credentials to prevent configuration-time failures.
|
||||
mysql:
|
||||
checkName: MySQL checks
|
||||
collectorName: '{{ .Values.databases.mysql.collectorName }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: MySQL checks failed
|
||||
- pass:
|
||||
message: MySQL checks passed
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.databases.redis.enabled }}
|
||||
- docString: |
|
||||
Title: Redis Connectivity and Health
|
||||
Requirement:
|
||||
- Collector: {{ .Values.databases.redis.collectorName }}
|
||||
Validates cache availability; failures cause timeouts, degraded performance, or startup errors.
|
||||
redis:
|
||||
checkName: Redis checks
|
||||
collectorName: '{{ .Values.databases.redis.collectorName }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Redis checks failed
|
||||
- pass:
|
||||
message: Redis checks passed
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.cephStatus.enabled }}
|
||||
- docString: |
|
||||
Title: Ceph Cluster Health
|
||||
Requirement:
|
||||
- Namespace: {{ .Values.cephStatus.namespace }}
|
||||
Ensures Ceph reports healthy status before depending on it for storage operations.
|
||||
cephStatus:
|
||||
checkName: Ceph cluster health
|
||||
namespace: '{{ .Values.cephStatus.namespace }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Ceph is not healthy
|
||||
- pass:
|
||||
message: Ceph is healthy
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.velero.enabled }}
|
||||
- docString: |
|
||||
Title: Velero Installed
|
||||
Requirement:
|
||||
- Velero controllers installed and discoverable
|
||||
Backup/restore operations require Velero components to be present.
|
||||
velero:
|
||||
checkName: Velero installed
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.longhorn.enabled }}
|
||||
- docString: |
|
||||
Title: Longhorn Health
|
||||
Requirement:
|
||||
- Namespace: {{ .Values.longhorn.namespace }}
|
||||
Verifies Longhorn is healthy to ensure persistent volumes remain available and replicas are in sync.
|
||||
longhorn:
|
||||
checkName: Longhorn health
|
||||
namespace: '{{ .Values.longhorn.namespace }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Longhorn is not healthy
|
||||
- pass:
|
||||
message: Longhorn is healthy
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.registryImages.enabled }}
|
||||
- docString: |
|
||||
Title: Registry Image Availability
|
||||
Requirement:
|
||||
- Collector: {{ .Values.registryImages.collectorName }}
|
||||
- Images: {{ toYaml .Values.registryImages.images | nindent 12 }}
|
||||
Ensures required images are available and pullable with provided credentials.
|
||||
registryImages:
|
||||
checkName: Registry image availability
|
||||
collectorName: '{{ .Values.registryImages.collectorName }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: One or more images are not available
|
||||
- pass:
|
||||
message: All images are available
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.weaveReport.enabled }}
|
||||
- docString: |
|
||||
Title: Weave Net Report Presence
|
||||
Requirement:
|
||||
- Report files: {{ .Values.weaveReport.reportFileGlob }}
|
||||
Validates networking diagnostics are collected for analysis of connectivity issues.
|
||||
weaveReport:
|
||||
checkName: Weave report
|
||||
reportFileGlob: '{{ .Values.weaveReport.reportFileGlob }}'
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.sysctl.enabled }}
|
||||
- docString: |
|
||||
Title: Sysctl Settings Validation
|
||||
Requirement:
|
||||
- Namespace: {{ .Values.sysctl.namespace }}
|
||||
- Image: {{ .Values.sysctl.image }}
|
||||
Checks kernel parameter configuration that impacts networking, file descriptors, and memory behavior.
|
||||
sysctl:
|
||||
checkName: Sysctl settings
|
||||
outcomes:
|
||||
- warn:
|
||||
message: One or more sysctl values do not meet recommendations
|
||||
- pass:
|
||||
message: Sysctl values meet recommendations
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.clusterResource.enabled }}
|
||||
- docString: |
|
||||
Title: Cluster Resource Field Requirement
|
||||
Requirement:
|
||||
- Kind: {{ .Values.clusterResource.kind }}
|
||||
- Name: {{ .Values.clusterResource.name }}{{ if not .Values.clusterResource.clusterScoped }} (ns: {{ .Values.clusterResource.namespace }}){{ end }}
|
||||
- YAML path: {{ .Values.clusterResource.yamlPath }}{{ if .Values.clusterResource.expectedValue }} (expected: {{ .Values.clusterResource.expectedValue }}){{ end }}
|
||||
Ensures critical configuration on a Kubernetes object matches expected value to guarantee correct behavior.
|
||||
clusterResource:
|
||||
checkName: Cluster resource value
|
||||
kind: '{{ .Values.clusterResource.kind }}'
|
||||
clusterScoped: {{ .Values.clusterResource.clusterScoped }}
|
||||
{{- if not .Values.clusterResource.clusterScoped }}
|
||||
namespace: '{{ .Values.clusterResource.namespace }}'
|
||||
{{- end }}
|
||||
name: '{{ .Values.clusterResource.name }}'
|
||||
yamlPath: '{{ .Values.clusterResource.yamlPath }}'
|
||||
{{- if .Values.clusterResource.expectedValue }}
|
||||
expectedValue: '{{ .Values.clusterResource.expectedValue }}'
|
||||
{{- end }}
|
||||
{{- if .Values.clusterResource.regex }}
|
||||
regex: '{{ .Values.clusterResource.regex }}'
|
||||
{{- end }}
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Cluster resource field does not match expected value
|
||||
- pass:
|
||||
message: Cluster resource field matches expected value
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.certificates.enabled }}
|
||||
- docString: |
|
||||
Title: Certificates Validity and Expiry
|
||||
Requirement:
|
||||
- Check certificate material in referenced secrets/configmaps
|
||||
Identifies expired or soon-to-expire certificates that would break TLS handshakes.
|
||||
certificates:
|
||||
checkName: Certificates validity
|
||||
outcomes:
|
||||
- warn:
|
||||
message: One or more certificates may be invalid or expiring soon
|
||||
- pass:
|
||||
message: Certificates are valid
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.goldpinger.enabled }}
|
||||
- docString: |
|
||||
Title: Goldpinger Network Health
|
||||
Requirement:
|
||||
- Collector: {{ .Values.goldpinger.collectorName }}
|
||||
- Report path: {{ .Values.goldpinger.filePath }}
|
||||
Uses Goldpinger probes to detect DNS, network, and kube-proxy issues across the cluster.
|
||||
goldpinger:
|
||||
checkName: Goldpinger report
|
||||
collectorName: '{{ .Values.goldpinger.collectorName }}'
|
||||
filePath: '{{ .Values.goldpinger.filePath }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Goldpinger indicates network issues
|
||||
- pass:
|
||||
message: Goldpinger indicates healthy networking
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.event.enabled }}
|
||||
- docString: |
|
||||
Title: Kubernetes Events Scan
|
||||
Requirement:
|
||||
- Namespace: {{ .Values.event.namespace }}
|
||||
- Reason: {{ .Values.event.reason }}{{ if .Values.event.kind }} (kind: {{ .Values.event.kind }}){{ end }}{{ if .Values.event.regex }} (regex: {{ .Values.event.regex }}){{ end }}
|
||||
Surfaces critical events that often correlate with configuration issues, crash loops, or cluster instability.
|
||||
event:
|
||||
checkName: Events
|
||||
collectorName: '{{ .Values.event.collectorName }}'
|
||||
namespace: '{{ .Values.event.namespace }}'
|
||||
{{- if .Values.event.kind }}
|
||||
kind: '{{ .Values.event.kind }}'
|
||||
{{- end }}
|
||||
reason: '{{ .Values.event.reason }}'
|
||||
{{- if .Values.event.regex }}
|
||||
regex: '{{ .Values.event.regex }}'
|
||||
{{- end }}
|
||||
outcomes:
|
||||
- fail:
|
||||
when: 'true'
|
||||
message: Critical events detected
|
||||
- pass:
|
||||
when: 'false'
|
||||
message: No critical events detected
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.nodeMetrics.enabled }}
|
||||
- docString: |
|
||||
Title: Node Metrics Thresholds
|
||||
Requirement:
|
||||
- Filters: PVC nameRegex={{ .Values.nodeMetrics.filters.pvc.nameRegex }}{{ if .Values.nodeMetrics.filters.pvc.namespace }}, namespace={{ .Values.nodeMetrics.filters.pvc.namespace }}{{ end }}
|
||||
Evaluates node-level metrics to detect capacity pressure and performance bottlenecks.
|
||||
nodeMetrics:
|
||||
checkName: Node metrics thresholds
|
||||
collectorName: '{{ .Values.nodeMetrics.collectorName }}'
|
||||
{{- if .Values.nodeMetrics.filters.pvc.nameRegex }}
|
||||
filters:
|
||||
pvc:
|
||||
nameRegex: '{{ .Values.nodeMetrics.filters.pvc.nameRegex }}'
|
||||
{{- if .Values.nodeMetrics.filters.pvc.namespace }}
|
||||
namespace: '{{ .Values.nodeMetrics.filters.pvc.namespace }}'
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
outcomes:
|
||||
- warn:
|
||||
message: Node metrics exceed warning thresholds
|
||||
- pass:
|
||||
message: Node metrics within thresholds
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.http.enabled }}
|
||||
- docString: |
|
||||
Title: HTTP Endpoint Health Checks
|
||||
Requirement:
|
||||
- Collected results: {{ .Values.http.collectorName }}
|
||||
Validates availability of service HTTP endpoints used by the application.
|
||||
http:
|
||||
checkName: HTTP checks
|
||||
collectorName: '{{ .Values.http.collectorName }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: One or more HTTP checks failed
|
||||
- pass:
|
||||
message: All HTTP checks passed
|
||||
{{- end }}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
apiVersion: troubleshoot.sh/v1beta2
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: Preflight
|
||||
metadata:
|
||||
name: example
|
||||
@@ -17,6 +17,18 @@ spec:
|
||||
- pass:
|
||||
when: ">= 1.22.0"
|
||||
message: Your cluster meets the recommended and required versions of Kubernetes.
|
||||
docString: |
|
||||
Title: Kubernetes Control Plane Requirements
|
||||
Requirement:
|
||||
- Version:
|
||||
- Minimum: 1.20.0
|
||||
- Recommended: 1.22.0
|
||||
These version targets ensure that required APIs and default behaviors are
|
||||
available and patched. Moving below the minimum commonly removes GA APIs
|
||||
(e.g., apps/v1 workloads, storage and ingress v1 APIs), changes admission
|
||||
defaults, and lacks critical CVE fixes. Running at or above the recommended
|
||||
version matches what is exercised most extensively in CI and receives the
|
||||
best operational guidance for upgrades and incident response.
|
||||
- customResourceDefinition:
|
||||
checkName: Ingress
|
||||
customResourceDefinitionName: ingressroutes.contour.heptio.com
|
||||
@@ -25,6 +37,19 @@ spec:
|
||||
message: Contour ingress not found!
|
||||
- pass:
|
||||
message: Contour ingress found!
|
||||
docString: |
|
||||
Title: Required CRDs and Ingress Capabilities
|
||||
Requirement:
|
||||
- Ingress Controller: Contour
|
||||
- CRD must be present:
|
||||
- Group: heptio.com
|
||||
- Kind: IngressRoute
|
||||
- Version: v1beta1 or later served version
|
||||
The ingress layer terminates TLS and routes external traffic to Services.
|
||||
Contour relies on the IngressRoute CRD to express host/path routing, TLS
|
||||
configuration, and policy. If the CRD is not installed and served by the
|
||||
API server, Contour cannot reconcile desired state, leaving routes
|
||||
unconfigured and traffic unreachable.
|
||||
- containerRuntime:
|
||||
outcomes:
|
||||
- pass:
|
||||
@@ -32,6 +57,17 @@ spec:
|
||||
message: containerd container runtime was found.
|
||||
- fail:
|
||||
message: Did not find containerd container runtime.
|
||||
docString: |
|
||||
Title: Container Runtime Requirements
|
||||
Requirement:
|
||||
- Runtime: containerd (CRI)
|
||||
- Kubelet cgroup driver: systemd
|
||||
- CRI socket path: /run/containerd/containerd.sock
|
||||
containerd (via the CRI) is the supported runtime for predictable container
|
||||
lifecycle management. On modern distros (cgroup v2), kubelet and the OS must
|
||||
both use the systemd cgroup driver to avoid resource accounting mismatches
|
||||
that lead to unexpected OOMKills and throttling. The CRI socket path must
|
||||
match kubelet configuration so the node can start and manage pods.
|
||||
- storageClass:
|
||||
checkName: Required storage classes
|
||||
storageClassName: "default"
|
||||
@@ -40,6 +76,17 @@ spec:
|
||||
message: Could not find a storage class called default.
|
||||
- pass:
|
||||
message: All good on storage classes
|
||||
docString: |
|
||||
Title: Default Storage Class Requirements
|
||||
Requirement:
|
||||
- Storage Class: default
|
||||
- Provisioner: Must support dynamic provisioning
|
||||
- Access Modes: ReadWriteOnce minimum
|
||||
A default storage class enables automatic persistent volume provisioning
|
||||
for StatefulSets and PVC-backed workloads. Without it, pods requiring
|
||||
persistent storage will remain in Pending state, unable to schedule.
|
||||
The storage class must support at least ReadWriteOnce access mode for
|
||||
single-pod workloads like databases and file servers.
|
||||
- distribution:
|
||||
outcomes:
|
||||
- fail:
|
||||
@@ -80,6 +127,17 @@ spec:
|
||||
message: Kind is a supported distribution
|
||||
- warn:
|
||||
message: Unable to determine the distribution of Kubernetes
|
||||
docString: |
|
||||
Title: Supported Kubernetes Distributions
|
||||
Requirement:
|
||||
- Production distributions: EKS, GKE, AKS, KURL, RKE2, K3S, DigitalOcean, OKE
|
||||
- Development distributions: Kind (testing only)
|
||||
- Unsupported: Docker Desktop, Microk8s, Minikube
|
||||
This application requires production-grade Kubernetes distributions that
|
||||
provide enterprise features like proper networking, storage integration,
|
||||
and security policies. Development-focused distributions lack the stability,
|
||||
performance characteristics, and operational tooling needed for reliable
|
||||
application deployment and management.
|
||||
- nodeResources:
|
||||
checkName: Must have at least 3 nodes in the cluster, with 5 recommended
|
||||
outcomes:
|
||||
@@ -93,6 +151,17 @@ spec:
|
||||
uri: https://kurl.sh/docs/install-with-kurl/adding-nodes
|
||||
- pass:
|
||||
message: This cluster has enough nodes.
|
||||
docString: |
|
||||
Title: Cluster Node Count Requirements
|
||||
Requirement:
|
||||
- Minimum: 3 nodes
|
||||
- Recommended: 5 nodes
|
||||
- High Availability: Odd number for quorum
|
||||
A minimum of 3 nodes ensures basic high availability and allows for
|
||||
rolling updates without service interruption. The recommended 5 nodes
|
||||
provide better resource distribution, fault tolerance, and maintenance
|
||||
windows. Odd numbers are preferred for etcd quorum and leader election
|
||||
in distributed components.
|
||||
- nodeResources:
|
||||
checkName: Every node in the cluster must have at least 8 GB of memory, with 32 GB recommended
|
||||
outcomes:
|
||||
@@ -106,6 +175,17 @@ spec:
|
||||
uri: https://kurl.sh/docs/install-with-kurl/system-requirements
|
||||
- pass:
|
||||
message: All nodes have at least 32 GB of memory.
|
||||
docString: |
|
||||
Title: Node Memory Requirements
|
||||
Requirement:
|
||||
- Minimum: 8 GB per node
|
||||
- Recommended: 32 GB per node
|
||||
- Reserved: ~2 GB for system processes
|
||||
Each node requires sufficient memory for the kubelet, container runtime,
|
||||
system processes, and application workloads. The 8 GB minimum accounts
|
||||
for Kubernetes overhead and basic application needs. The 32 GB recommendation
|
||||
provides headroom for memory-intensive workloads, caching, and prevents
|
||||
OOMKills during traffic spikes or batch processing.
|
||||
- nodeResources:
|
||||
checkName: Total CPU Cores in the cluster is 4 or greater
|
||||
outcomes:
|
||||
@@ -115,6 +195,17 @@ spec:
|
||||
uri: https://kurl.sh/docs/install-with-kurl/system-requirements
|
||||
- pass:
|
||||
message: There are at least 4 cores in the cluster
|
||||
docString: |
|
||||
Title: Cluster CPU Requirements
|
||||
Requirement:
|
||||
- Minimum: 4 total CPU cores across all nodes
|
||||
- Distribution: At least 1 core per node recommended
|
||||
- Architecture: x86_64 or arm64
|
||||
The cluster needs sufficient CPU capacity for Kubernetes control plane
|
||||
components, system daemons, and application workloads. 4 cores minimum
|
||||
ensures basic functionality, but distribution across multiple nodes
|
||||
provides better scheduling flexibility and fault tolerance than
|
||||
concentrating all cores on a single node.
|
||||
- nodeResources:
|
||||
checkName: Every node in the cluster must have at least 40 GB of ephemeral storage, with 100 GB recommended
|
||||
outcomes:
|
||||
@@ -128,3 +219,14 @@ spec:
|
||||
uri: https://kurl.sh/docs/install-with-kurl/system-requirements
|
||||
- pass:
|
||||
message: All nodes have at least 100 GB of ephemeral storage.
|
||||
docString: |
|
||||
Title: Node Ephemeral Storage Requirements
|
||||
Requirement:
|
||||
- Minimum: 40 GB per node
|
||||
- Recommended: 100 GB per node
|
||||
- Usage: Container images, logs, temporary files
|
||||
Ephemeral storage houses container images, pod logs, and temporary
|
||||
files created by running containers. The 40 GB minimum covers basic
|
||||
Kubernetes components and small applications. The 100 GB recommendation
|
||||
accommodates larger container images, extensive logging, and temporary
|
||||
data processing without triggering evictions due to disk pressure.
|
||||
|
||||
244
examples/preflight/simple-v1beta3.yaml
Normal file
244
examples/preflight/simple-v1beta3.yaml
Normal file
@@ -0,0 +1,244 @@
|
||||
apiVersion: troubleshoot.sh/v1beta3
|
||||
kind: Preflight
|
||||
metadata:
|
||||
name: templated-from-v1beta2
|
||||
spec:
|
||||
analyzers:
|
||||
{{- if .Values.kubernetes.enabled }}
|
||||
- docString: |
|
||||
Title: Kubernetes Control Plane Requirements
|
||||
Requirement:
|
||||
- Version:
|
||||
- Minimum: {{ .Values.kubernetes.minVersion }}
|
||||
- Recommended: {{ .Values.kubernetes.recommendedVersion }}
|
||||
- Docs: https://kubernetes.io
|
||||
These version targets ensure that required APIs and default behaviors are
|
||||
available and patched. Moving below the minimum commonly removes GA APIs
|
||||
(e.g., apps/v1 workloads, storage and ingress v1 APIs), changes admission
|
||||
defaults, and lacks critical CVE fixes. Running at or above the recommended
|
||||
version matches what is exercised most extensively in CI and receives the
|
||||
best operational guidance for upgrades and incident response.
|
||||
clusterVersion:
|
||||
checkName: Kubernetes version
|
||||
outcomes:
|
||||
- fail:
|
||||
when: '< {{ .Values.kubernetes.minVersion }}'
|
||||
message: This application requires at least Kubernetes {{ .Values.kubernetes.minVersion }}, and recommends {{ .Values.kubernetes.recommendedVersion }}.
|
||||
uri: https://www.kubernetes.io
|
||||
- warn:
|
||||
when: '< {{ .Values.kubernetes.recommendedVersion }}'
|
||||
message: Your cluster meets the minimum version of Kubernetes, but we recommend you update to {{ .Values.kubernetes.recommendedVersion }} or later.
|
||||
uri: https://kubernetes.io
|
||||
- pass:
|
||||
when: '>= {{ .Values.kubernetes.recommendedVersion }}'
|
||||
message: Your cluster meets the recommended and required versions of Kubernetes.
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.enabled }}
|
||||
- docString: |
|
||||
Title: Required CRDs and Ingress Capabilities
|
||||
Requirement:
|
||||
- Ingress Controller: Contour
|
||||
- CRD must be present:
|
||||
- Group: heptio.com
|
||||
- Kind: IngressRoute
|
||||
- Version: v1beta1 or later served version
|
||||
The ingress layer terminates TLS and routes external traffic to Services.
|
||||
Contour relies on the IngressRoute CRD to express host/path routing, TLS
|
||||
configuration, and policy. If the CRD is not installed and served by the
|
||||
API server, Contour cannot reconcile desired state, leaving routes
|
||||
unconfigured and traffic unreachable.
|
||||
{{- if eq .Values.ingress.type "Contour" }}
|
||||
customResourceDefinition:
|
||||
checkName: Contour IngressRoute CRD
|
||||
customResourceDefinitionName: ingressroutes.contour.heptio.com
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Contour IngressRoute CRD not found; required for ingress routing
|
||||
- pass:
|
||||
message: Contour IngressRoute CRD present
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.runtime.enabled }}
|
||||
- docString: |
|
||||
Title: Container Runtime Requirements
|
||||
Requirement:
|
||||
- Runtime: containerd (CRI)
|
||||
- Kubelet cgroup driver: systemd
|
||||
- CRI socket path: /run/containerd/containerd.sock
|
||||
containerd (via the CRI) is the supported runtime for predictable container
|
||||
lifecycle management. On modern distros (cgroup v2), kubelet and the OS must
|
||||
both use the systemd cgroup driver to avoid resource accounting mismatches
|
||||
that lead to unexpected OOMKills and throttling. The CRI socket path must
|
||||
match kubelet configuration so the node can start and manage pods.
|
||||
containerRuntime:
|
||||
outcomes:
|
||||
- pass:
|
||||
when: '== containerd'
|
||||
message: containerd runtime detected
|
||||
- fail:
|
||||
message: Unsupported container runtime; containerd required
|
||||
{{- end }}
|
||||
{{- if .Values.storage.enabled }}
|
||||
- docString: |
|
||||
Title: Default StorageClass Requirements
|
||||
Requirement:
|
||||
- A StorageClass named "{{ .Values.storage.className }}" must exist (cluster default preferred)
|
||||
- AccessMode: ReadWriteOnce (RWO) required (RWX optional)
|
||||
- VolumeBindingMode: WaitForFirstConsumer preferred
|
||||
- allowVolumeExpansion: true recommended
|
||||
A default StorageClass enables dynamic PVC provisioning without manual
|
||||
intervention. RWO provides baseline persistence semantics for stateful pods.
|
||||
WaitForFirstConsumer defers binding until a pod is scheduled, improving
|
||||
topology-aware placement (zonal/az) and reducing unschedulable PVCs.
|
||||
AllowVolumeExpansion permits online growth during capacity pressure
|
||||
without disruptive migrations.
|
||||
storageClass:
|
||||
checkName: Default StorageClass
|
||||
storageClassName: '{{ .Values.storage.className }}'
|
||||
outcomes:
|
||||
- fail:
|
||||
message: Default StorageClass not found
|
||||
- pass:
|
||||
message: Default StorageClass present
|
||||
{{- end }}
|
||||
{{- if .Values.distribution.enabled }}
|
||||
- docString: |
|
||||
Title: Kubernetes Distribution Support
|
||||
Requirement:
|
||||
- Unsupported: docker-desktop, microk8s, minikube
|
||||
- Supported: eks, gke, aks, kurl, digitalocean, rke2, k3s, oke, kind
|
||||
Development or single-node environments are optimized for local testing and
|
||||
omit HA control-plane patterns, cloud integration, and production defaults.
|
||||
The supported distributions are validated for API compatibility, RBAC
|
||||
expectations, admission behavior, and default storage/networking this
|
||||
application depends on.
|
||||
distribution:
|
||||
outcomes:
|
||||
- fail:
|
||||
when: '== docker-desktop'
|
||||
message: The application does not support Docker Desktop Clusters
|
||||
- fail:
|
||||
when: '== microk8s'
|
||||
message: The application does not support Microk8s Clusters
|
||||
- fail:
|
||||
when: '== minikube'
|
||||
message: The application does not support Minikube Clusters
|
||||
- pass:
|
||||
when: '== eks'
|
||||
message: EKS is a supported distribution
|
||||
- pass:
|
||||
when: '== gke'
|
||||
message: GKE is a supported distribution
|
||||
- pass:
|
||||
when: '== aks'
|
||||
message: AKS is a supported distribution
|
||||
- pass:
|
||||
when: '== kurl'
|
||||
message: KURL is a supported distribution
|
||||
- pass:
|
||||
when: '== digitalocean'
|
||||
message: DigitalOcean is a supported distribution
|
||||
- pass:
|
||||
when: '== rke2'
|
||||
message: RKE2 is a supported distribution
|
||||
- pass:
|
||||
when: '== k3s'
|
||||
message: K3S is a supported distribution
|
||||
- pass:
|
||||
when: '== oke'
|
||||
message: OKE is a supported distribution
|
||||
- pass:
|
||||
when: '== kind'
|
||||
message: Kind is a supported distribution
|
||||
- warn:
|
||||
message: Unable to determine the distribution of Kubernetes
|
||||
{{- end }}
|
||||
{{- if .Values.nodeChecks.count.enabled }}
|
||||
- docString: |
|
||||
Title: Node count requirement
|
||||
Requirement:
|
||||
- Node count: Minimum {{ .Values.cluster.minNodes }} nodes, Recommended {{ .Values.cluster.recommendedNodes }} nodes
|
||||
Multiple worker nodes provide scheduling capacity, tolerance to disruptions,
|
||||
and safe rolling updates. Operating below the recommendation increases risk
|
||||
of unschedulable pods during maintenance or failures and reduces headroom
|
||||
for horizontal scaling.
|
||||
nodeResources:
|
||||
checkName: Node count
|
||||
outcomes:
|
||||
- fail:
|
||||
when: 'count() < {{ .Values.cluster.minNodes }}'
|
||||
message: This application requires at least {{ .Values.cluster.minNodes }} nodes.
|
||||
uri: https://kurl.sh/docs/install-with-kurl/adding-nodes
|
||||
- warn:
|
||||
when: 'count() < {{ .Values.cluster.recommendedNodes }}'
|
||||
message: This application recommends at least {{ .Values.cluster.recommendedNodes }} nodes.
|
||||
uri: https://kurl.sh/docs/install-with-kurl/adding-nodes
|
||||
- pass:
|
||||
message: This cluster has enough nodes.
|
||||
{{- end }}
|
||||
{{- if .Values.nodeChecks.cpu.enabled }}
|
||||
- docString: |
|
||||
Title: Cluster CPU requirement
|
||||
Requirement:
|
||||
- Total CPU: Minimum {{ .Values.cluster.minCPU }} vCPU
|
||||
Aggregate CPU must cover system daemons, controllers, and application pods.
|
||||
Insufficient CPU causes prolonged scheduling latency, readiness probe
|
||||
failures, and throughput collapse under load.
|
||||
nodeResources:
|
||||
checkName: Cluster CPU total
|
||||
outcomes:
|
||||
- fail:
|
||||
when: 'sum(cpuCapacity) < {{ .Values.cluster.minCPU }}'
|
||||
message: The cluster must contain at least {{ .Values.cluster.minCPU }} cores
|
||||
uri: https://kurl.sh/docs/install-with-kurl/system-requirements
|
||||
- pass:
|
||||
message: There are at least {{ .Values.cluster.minCPU }} cores in the cluster
|
||||
{{- end }}
|
||||
{{- if .Values.nodeChecks.memory.enabled }}
|
||||
- docString: |
|
||||
Title: Per-node memory requirement
|
||||
Requirement:
|
||||
- Per-node memory: Minimum {{ .Values.node.minMemoryGi }} GiB; Recommended {{ .Values.node.recommendedMemoryGi }} GiB
|
||||
Nodes must reserve memory for kubelet/system components and per-pod overhead.
|
||||
Below the minimum, pods will frequently be OOMKilled or evicted. The
|
||||
recommended capacity provides headroom for spikes, compactions, and
|
||||
upgrades without destabilizing workloads.
|
||||
nodeResources:
|
||||
checkName: Per-node memory requirement
|
||||
outcomes:
|
||||
- fail:
|
||||
when: 'min(memoryCapacity) < {{ .Values.node.minMemoryGi }}Gi'
|
||||
message: All nodes must have at least {{ .Values.node.minMemoryGi }} GiB of memory.
|
||||
uri: https://kurl.sh/docs/install-with-kurl/system-requirements
|
||||
- warn:
|
||||
when: 'min(memoryCapacity) < {{ .Values.node.recommendedMemoryGi }}Gi'
|
||||
message: All nodes are recommended to have at least {{ .Values.node.recommendedMemoryGi }} GiB of memory.
|
||||
uri: https://kurl.sh/docs/install-with-kurl/system-requirements
|
||||
- pass:
|
||||
message: All nodes have at least {{ .Values.node.recommendedMemoryGi }} GiB of memory.
|
||||
{{- end }}
|
||||
{{- if .Values.nodeChecks.ephemeral.enabled }}
|
||||
- docString: |
|
||||
Title: Per-node ephemeral storage requirement
|
||||
Requirement:
|
||||
- Per-node ephemeral storage: Minimum {{ .Values.node.minEphemeralGi }} GiB; Recommended {{ .Values.node.recommendedEphemeralGi }} GiB
|
||||
Ephemeral storage backs image layers, writable container filesystems, logs,
|
||||
and temporary data. When capacity is low, kubelet enters disk-pressure
|
||||
eviction and image pulls fail, causing pod restarts and data loss for
|
||||
transient files.
|
||||
nodeResources:
|
||||
checkName: Per-node ephemeral storage requirement
|
||||
outcomes:
|
||||
- fail:
|
||||
when: 'min(ephemeralStorageCapacity) < {{ .Values.node.minEphemeralGi }}Gi'
|
||||
message: All nodes must have at least {{ .Values.node.minEphemeralGi }} GiB of ephemeral storage.
|
||||
uri: https://kurl.sh/docs/install-with-kurl/system-requirements
|
||||
- warn:
|
||||
when: 'min(ephemeralStorageCapacity) < {{ .Values.node.recommendedEphemeralGi }}Gi'
|
||||
message: All nodes are recommended to have at least {{ .Values.node.recommendedEphemeralGi }} GiB of ephemeral storage.
|
||||
uri: https://kurl.sh/docs/install-with-kurl/system-requirements
|
||||
- pass:
|
||||
message: All nodes have at least {{ .Values.node.recommendedEphemeralGi }} GiB of ephemeral storage.
|
||||
{{- end }}
|
||||
|
||||
|
||||
229
examples/preflight/values-complex-full.yaml
Normal file
229
examples/preflight/values-complex-full.yaml
Normal file
@@ -0,0 +1,229 @@
|
||||
clusterVersion:
|
||||
enabled: true
|
||||
minVersion: "1.24.0"
|
||||
recommendedVersion: "1.28.0"
|
||||
|
||||
crd:
|
||||
enabled: true
|
||||
name: "samples.mycompany.com"
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
namespace: "default"
|
||||
name: "example"
|
||||
|
||||
secret:
|
||||
enabled: true
|
||||
namespace: "default"
|
||||
name: "my-secret"
|
||||
key: ""
|
||||
|
||||
configMap:
|
||||
enabled: true
|
||||
namespace: "kube-public"
|
||||
name: "cluster-info"
|
||||
key: ""
|
||||
|
||||
imagePullSecret:
|
||||
enabled: true
|
||||
registry: "registry.example.com"
|
||||
|
||||
workloads:
|
||||
deployments:
|
||||
enabled: true
|
||||
namespace: "default"
|
||||
name: "example-deploy"
|
||||
minReady: 1
|
||||
statefulsets:
|
||||
enabled: true
|
||||
namespace: "default"
|
||||
name: "example-sts"
|
||||
minReady: 1
|
||||
jobs:
|
||||
enabled: true
|
||||
namespace: "default"
|
||||
name: "example-job"
|
||||
replicasets:
|
||||
enabled: true
|
||||
namespace: "default"
|
||||
name: "example-rs"
|
||||
minReady: 1
|
||||
|
||||
clusterPodStatuses:
|
||||
enabled: true
|
||||
namespaces:
|
||||
- "default"
|
||||
- "kube-system"
|
||||
|
||||
clusterContainerStatuses:
|
||||
enabled: true
|
||||
namespaces:
|
||||
- "default"
|
||||
- "kube-system"
|
||||
restartCount: 3
|
||||
|
||||
containerRuntime:
|
||||
enabled: true
|
||||
|
||||
distribution:
|
||||
enabled: true
|
||||
supported: ["eks", "gke", "aks", "kubeadm"]
|
||||
unsupported: []
|
||||
|
||||
nodeResources:
|
||||
count:
|
||||
enabled: true
|
||||
min: 1
|
||||
recommended: 3
|
||||
cpu:
|
||||
enabled: true
|
||||
min: "4"
|
||||
memory:
|
||||
enabled: true
|
||||
minGi: 8
|
||||
recommendedGi: 16
|
||||
ephemeral:
|
||||
enabled: true
|
||||
minGi: 20
|
||||
recommendedGi: 50
|
||||
|
||||
textAnalyze:
|
||||
enabled: true
|
||||
fileName: "logs/*.log"
|
||||
regex: "error"
|
||||
|
||||
yamlCompare:
|
||||
enabled: true
|
||||
fileName: "kube-system/sample.yaml"
|
||||
path: "spec.replicas"
|
||||
value: "3"
|
||||
|
||||
jsonCompare:
|
||||
enabled: true
|
||||
fileName: "custom/sample.json"
|
||||
jsonPath: "$.items[0].status"
|
||||
value: "Running"
|
||||
|
||||
databases:
|
||||
postgres:
|
||||
enabled: true
|
||||
collectorName: "postgres"
|
||||
uri: "postgres://user:pass@postgres:5432/db?sslmode=disable"
|
||||
tls:
|
||||
skipVerify: true
|
||||
secret:
|
||||
name: ""
|
||||
namespace: ""
|
||||
mssql:
|
||||
enabled: true
|
||||
collectorName: "mssql"
|
||||
uri: "sqlserver://user:pass@mssql:1433?database=db"
|
||||
mysql:
|
||||
enabled: true
|
||||
collectorName: "mysql"
|
||||
uri: "mysql://user:pass@tcp(mysql:3306)/db"
|
||||
redis:
|
||||
enabled: true
|
||||
collectorName: "redis"
|
||||
uri: "redis://redis:6379"
|
||||
|
||||
cephStatus:
|
||||
enabled: true
|
||||
namespace: "rook-ceph"
|
||||
timeout: "30s"
|
||||
|
||||
velero:
|
||||
enabled: true
|
||||
|
||||
longhorn:
|
||||
enabled: true
|
||||
namespace: "longhorn-system"
|
||||
timeout: "30s"
|
||||
|
||||
registryImages:
|
||||
enabled: true
|
||||
collectorName: "images"
|
||||
namespace: "default"
|
||||
imagePullSecret:
|
||||
name: ""
|
||||
data: {}
|
||||
images:
|
||||
- "alpine:3.19"
|
||||
- "busybox:1.36"
|
||||
|
||||
http:
|
||||
enabled: true
|
||||
collectorName: "http"
|
||||
get:
|
||||
url: "https://example.com/healthz"
|
||||
timeout: "10s"
|
||||
insecureSkipVerify: true
|
||||
headers: {}
|
||||
post:
|
||||
url: ""
|
||||
timeout: ""
|
||||
insecureSkipVerify: true
|
||||
headers: {}
|
||||
body: ""
|
||||
|
||||
weaveReport:
|
||||
enabled: true
|
||||
reportFileGlob: "weave/*.json"
|
||||
|
||||
sysctl:
|
||||
enabled: true
|
||||
namespace: "default"
|
||||
image: "busybox:1.36"
|
||||
imagePullPolicy: "IfNotPresent"
|
||||
|
||||
clusterResource:
|
||||
enabled: true
|
||||
kind: "Deployment"
|
||||
clusterScoped: true
|
||||
namespace: "default"
|
||||
name: "example-deploy"
|
||||
yamlPath: "spec.replicas"
|
||||
expectedValue: "3"
|
||||
regex: ""
|
||||
|
||||
certificates:
|
||||
enabled: true
|
||||
secrets:
|
||||
- name: ""
|
||||
namespaces: []
|
||||
configMaps:
|
||||
- name: ""
|
||||
namespaces: []
|
||||
|
||||
goldpinger:
|
||||
enabled: true
|
||||
collectorName: "goldpinger"
|
||||
filePath: "goldpinger/check-all.json"
|
||||
namespace: "default"
|
||||
collectDelay: "30s"
|
||||
podLaunch:
|
||||
namespace: ""
|
||||
image: ""
|
||||
imagePullSecret:
|
||||
name: ""
|
||||
serviceAccountName: ""
|
||||
|
||||
event:
|
||||
enabled: true
|
||||
collectorName: "events"
|
||||
namespace: "default"
|
||||
kind: "Pod"
|
||||
reason: "Unhealthy"
|
||||
regex: ""
|
||||
|
||||
nodeMetrics:
|
||||
enabled: true
|
||||
collectorName: "node-metrics"
|
||||
filters:
|
||||
pvc:
|
||||
nameRegex: ""
|
||||
namespace: ""
|
||||
nodeNames: []
|
||||
selector: []
|
||||
|
||||
|
||||
4
examples/preflight/values-complex-small.yaml
Normal file
4
examples/preflight/values-complex-small.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
clusterVersion:
|
||||
enabled: true
|
||||
minVersion: "1.24.0"
|
||||
recommendedVersion: "1.28.0"
|
||||
66
examples/preflight/values-simple.yaml
Normal file
66
examples/preflight/values-simple.yaml
Normal file
@@ -0,0 +1,66 @@
|
||||
# Values for v1beta3-templated-from-v1beta2.yaml
|
||||
|
||||
kubernetes:
|
||||
enabled: true
|
||||
minVersion: "1.22.0"
|
||||
recommendedVersion: "1.29.0"
|
||||
|
||||
storage:
|
||||
enabled: true
|
||||
className: "default"
|
||||
|
||||
cluster:
|
||||
minNodes: 3
|
||||
recommendedNodes: 5
|
||||
minCPU: 4
|
||||
|
||||
node:
|
||||
minMemoryGi: 8
|
||||
recommendedMemoryGi: 32
|
||||
minEphemeralGi: 40
|
||||
recommendedEphemeralGi: 100
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
type: "Contour"
|
||||
contour:
|
||||
crdName: "ingressroutes.contour.heptio.com"
|
||||
crdGroup: "heptio.com"
|
||||
crdKind: "IngressRoute"
|
||||
crdVersion: "v1beta1 or later served version"
|
||||
|
||||
runtime:
|
||||
enabled: true
|
||||
name: "containerd"
|
||||
cgroupDriver: "systemd"
|
||||
criSocket: "/run/containerd/containerd.sock"
|
||||
|
||||
distribution:
|
||||
enabled: true
|
||||
unsupported:
|
||||
- docker-desktop
|
||||
- microk8s
|
||||
- minikube
|
||||
supported:
|
||||
- eks
|
||||
- gke
|
||||
- aks
|
||||
- kurl
|
||||
- digitalocean
|
||||
- rke2
|
||||
- k3s
|
||||
- oke
|
||||
- kind
|
||||
|
||||
nodeChecks:
|
||||
enabled: true
|
||||
count:
|
||||
enabled: true
|
||||
cpu:
|
||||
enabled: true
|
||||
memory:
|
||||
enabled: true
|
||||
ephemeral:
|
||||
enabled: true
|
||||
|
||||
|
||||
16
examples/preflight/values-v1beta3-1.yaml
Normal file
16
examples/preflight/values-v1beta3-1.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
# Minimal values for v1beta3-templated-from-v1beta2.yaml
|
||||
|
||||
kubernetes:
|
||||
enabled: true
|
||||
minVersion: "1.22.0"
|
||||
recommendedVersion: "1.29.0"
|
||||
|
||||
storage:
|
||||
enabled: true
|
||||
className: "default"
|
||||
|
||||
nodeChecks:
|
||||
cpu:
|
||||
enabled: false
|
||||
ephemeral:
|
||||
enabled: false
|
||||
10
examples/preflight/values-v1beta3-2.yaml
Normal file
10
examples/preflight/values-v1beta3-2.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
cluster:
|
||||
minNodes: 3
|
||||
recommendedNodes: 3
|
||||
minCPU: 4
|
||||
|
||||
node:
|
||||
minMemoryGi: 8
|
||||
recommendedMemoryGi: 16
|
||||
minEphemeralGi: 40
|
||||
recommendedEphemeralGi: 40
|
||||
26
examples/preflight/values-v1beta3-3.yaml
Normal file
26
examples/preflight/values-v1beta3-3.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
ingress:
|
||||
enabled: true
|
||||
type: "Contour"
|
||||
|
||||
runtime:
|
||||
enabled: true
|
||||
|
||||
distribution:
|
||||
enabled: true
|
||||
|
||||
nodeChecks:
|
||||
enabled: true
|
||||
count:
|
||||
enabled: true
|
||||
cpu:
|
||||
enabled: true
|
||||
memory:
|
||||
enabled: true
|
||||
ephemeral:
|
||||
enabled: true
|
||||
|
||||
|
||||
kubernetes:
|
||||
enabled: false
|
||||
minVersion: "1.22.0"
|
||||
recommendedVersion: "1.29.0"
|
||||
52
ffi/main.go
52
ffi/main.go
@@ -1,52 +0,0 @@
|
||||
package main
|
||||
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
analyzer "github.com/replicatedhq/troubleshoot/pkg/analyze"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/convert"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/logger"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
//export Analyze
|
||||
func Analyze(bundleURL string, analyzers string, outputFormat string, compatibility string) *C.char {
|
||||
logger.SetQuiet(true)
|
||||
|
||||
result, err := analyzer.DownloadAndAnalyze(bundleURL, analyzers)
|
||||
if err != nil {
|
||||
fmt.Printf("error downloading and analyzing: %s\n", err.Error())
|
||||
return C.CString("")
|
||||
}
|
||||
|
||||
var data interface{}
|
||||
switch compatibility {
|
||||
case "support-bundle":
|
||||
data = convert.FromAnalyzerResult(result)
|
||||
default:
|
||||
data = result
|
||||
}
|
||||
|
||||
var formatted []byte
|
||||
switch outputFormat {
|
||||
case "json":
|
||||
formatted, err = json.MarshalIndent(data, "", " ")
|
||||
case "", "yaml":
|
||||
formatted, err = yaml.Marshal(data)
|
||||
default:
|
||||
fmt.Printf("unknown output format: %s\n", outputFormat)
|
||||
return C.CString("")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("error formatting output: %#v\n", err)
|
||||
return C.CString("")
|
||||
}
|
||||
|
||||
return C.CString(string(formatted))
|
||||
}
|
||||
|
||||
func main() {}
|
||||
3
go.mod
3
go.mod
@@ -104,7 +104,6 @@ require (
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/containerd/typeurl/v2 v2.2.3 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/ebitengine/purego v0.9.0 // indirect
|
||||
@@ -249,7 +248,7 @@ require (
|
||||
github.com/opencontainers/selinux v1.12.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
|
||||
github.com/prometheus/client_golang v1.22.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user