mirror of
https://github.com/k8sgpt-ai/k8sgpt.git
synced 2026-03-18 19:17:25 +00:00
Compare commits
13 Commits
copilot/ad
...
renovate/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4afa6ed068 | ||
|
|
2276b12b0f | ||
|
|
fd5bba6ab3 | ||
|
|
19a172e575 | ||
|
|
36de157e21 | ||
|
|
458aa9deba | ||
|
|
285c1353d5 | ||
|
|
4f63e9737c | ||
|
|
a56e4788c3 | ||
|
|
99911fbb3a | ||
|
|
abc46474e3 | ||
|
|
1a8f1d47a4 | ||
|
|
1f2ff98834 |
2
.github/workflows/build_container.yaml
vendored
2
.github/workflows/build_container.yaml
vendored
@@ -14,7 +14,7 @@ on:
|
||||
- "**.md"
|
||||
|
||||
env:
|
||||
GO_VERSION: "~1.23"
|
||||
GO_VERSION: "~1.24"
|
||||
IMAGE_NAME: "k8sgpt"
|
||||
REGISTRY_IMAGE: ghcr.io/k8sgpt-ai/k8sgpt
|
||||
|
||||
|
||||
2
.github/workflows/golangci_lint.yaml
vendored
2
.github/workflows/golangci_lint.yaml
vendored
@@ -14,5 +14,5 @@ jobs:
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8
|
||||
with:
|
||||
version: v2.1.0
|
||||
version: v2.11.3
|
||||
only-new-issues: true
|
||||
6
.github/workflows/release.yaml
vendored
6
.github/workflows/release.yaml
vendored
@@ -61,9 +61,9 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
go-version: '~1.26.0'
|
||||
- name: Download Syft
|
||||
uses: anchore/sbom-action/download-syft@55dc4ee22412511ee8c3142cbea40418e6cec693 # v0.17.8
|
||||
uses: anchore/sbom-action/download-syft@57aae528053a48a3f6235f2d9461b05fbcb7366d # v0.23.1
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6
|
||||
with:
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
cache-to: type=gha,scope=${{ github.ref_name }}-${{ env.IMAGE_TAG }}
|
||||
|
||||
- name: Generate SBOM
|
||||
uses: anchore/sbom-action@55dc4ee22412511ee8c3142cbea40418e6cec693 # v0.17.8
|
||||
uses: anchore/sbom-action@57aae528053a48a3f6235f2d9461b05fbcb7366d # v0.23.1
|
||||
with:
|
||||
image: ${{ env.IMAGE_TAG }}
|
||||
artifact-name: sbom-${{ env.IMAGE_NAME }}
|
||||
|
||||
2
.github/workflows/semantic_pr.yaml
vendored
2
.github/workflows/semantic_pr.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
pull-requests: read # Needed for reading prs
|
||||
steps:
|
||||
- name: Validate Pull Request
|
||||
uses: amannn/action-semantic-pull-request@fdd4d3ddf614fbcd8c29e4b106d3bbe0cb2c605d # v6.0.1
|
||||
uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
||||
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
@@ -9,7 +9,7 @@ on:
|
||||
- main
|
||||
|
||||
env:
|
||||
GO_VERSION: "~1.22"
|
||||
GO_VERSION: "~1.24"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -1 +1 @@
|
||||
{".":"0.4.27"}
|
||||
{".":"0.4.30"}
|
||||
47
CHANGELOG.md
47
CHANGELOG.md
@@ -1,5 +1,52 @@
|
||||
# Changelog
|
||||
|
||||
## [0.4.30](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.29...v0.4.30) (2026-02-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* validate namespace before running custom analyzers ([#1617](https://github.com/k8sgpt-ai/k8sgpt/issues/1617)) ([458aa9d](https://github.com/k8sgpt-ai/k8sgpt/commit/458aa9debac7590eb0855ffd12141b702e999a36))
|
||||
|
||||
## [0.4.29](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.28...v0.4.29) (2026-02-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **serve:** add short flag and env var for metrics port ([#1616](https://github.com/k8sgpt-ai/k8sgpt/issues/1616)) ([4f63e97](https://github.com/k8sgpt-ai/k8sgpt/commit/4f63e9737c6a2306686bd3b6f37e81f210665949))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update k8s.io/utils digest to b8788ab ([#1572](https://github.com/k8sgpt-ai/k8sgpt/issues/1572)) ([a56e478](https://github.com/k8sgpt-ai/k8sgpt/commit/a56e4788c3361a64df17175f163f33422a8fe606))
|
||||
* use proper JSON marshaling for customrest prompt to handle special characters ([#1615](https://github.com/k8sgpt-ai/k8sgpt/issues/1615)) ([99911fb](https://github.com/k8sgpt-ai/k8sgpt/commit/99911fbb3ac8c950fd7ee1b3210f8a9c2a6b0ad7)), closes [#1556](https://github.com/k8sgpt-ai/k8sgpt/issues/1556)
|
||||
|
||||
|
||||
### Refactoring
|
||||
|
||||
* improve MCP server handlers with better error handling and pagination ([#1613](https://github.com/k8sgpt-ai/k8sgpt/issues/1613)) ([abc4647](https://github.com/k8sgpt-ai/k8sgpt/commit/abc46474e372bcd27201f1a64372c04269acee13))
|
||||
|
||||
## [0.4.28](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.27...v0.4.28) (2026-02-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add Groq as LLM provider ([#1600](https://github.com/k8sgpt-ai/k8sgpt/issues/1600)) ([867bce1](https://github.com/k8sgpt-ai/k8sgpt/commit/867bce1907f5dd3387128b72c694e98091d55554))
|
||||
* multiple security fixes. Prometheus: v0.302.1 → v0.306.0 ([#1597](https://github.com/k8sgpt-ai/k8sgpt/issues/1597)) ([f5fb2a7](https://github.com/k8sgpt-ai/k8sgpt/commit/f5fb2a7e12e14fad8107940aeead5e60b064add1))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* align CI Go versions with go.mod to ensure consistency ([#1611](https://github.com/k8sgpt-ai/k8sgpt/issues/1611)) ([1f2ff98](https://github.com/k8sgpt-ai/k8sgpt/commit/1f2ff988342b8ef2aa3e3263eb845c0ee09fe24c))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#1550](https://github.com/k8sgpt-ai/k8sgpt/issues/1550)) ([7fe3bdb](https://github.com/k8sgpt-ai/k8sgpt/commit/7fe3bdbd952bc9a1975121de5f21ad31dc1f691d))
|
||||
* use MaxCompletionTokens instead of deprecated MaxTokens for OpenAI ([#1604](https://github.com/k8sgpt-ai/k8sgpt/issues/1604)) ([c80b2e2](https://github.com/k8sgpt-ai/k8sgpt/commit/c80b2e2c346845336593ce515fe90fd501b1d0a7))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update actions/checkout digest to 93cb6ef ([#1592](https://github.com/k8sgpt-ai/k8sgpt/issues/1592)) ([40ffcbe](https://github.com/k8sgpt-ai/k8sgpt/commit/40ffcbec6b65e3a99e40be5f414a3f2c087bffbb))
|
||||
* **deps:** update actions/setup-go digest to 40f1582 ([#1593](https://github.com/k8sgpt-ai/k8sgpt/issues/1593)) ([a303ffa](https://github.com/k8sgpt-ai/k8sgpt/commit/a303ffa21c7ede3dd9391185bc91fb3b4e8276b6))
|
||||
* util tests ([#1594](https://github.com/k8sgpt-ai/k8sgpt/issues/1594)) ([21369c5](https://github.com/k8sgpt-ai/k8sgpt/commit/21369c5c0917fd2b6ae4173378b2e257e2b1de7b))
|
||||
|
||||
## [0.4.27](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.26...v0.4.27) (2025-12-18)
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
We're happy that you want to contribute to this project. Please read the sections to make the process as smooth as possible.
|
||||
|
||||
## Requirements
|
||||
- Golang `1.23`
|
||||
- Golang `1.24+`
|
||||
- An OpenAI API key
|
||||
* OpenAI API keys can be obtained from [OpenAI](https://platform.openai.com/account/api-keys)
|
||||
* You can set the API key for k8sgpt using `./k8sgpt auth key`
|
||||
|
||||
17
README.md
17
README.md
@@ -21,6 +21,10 @@ It has SRE experience codified into its analyzers and helps to pull out the most
|
||||
|
||||
_Out of the box integration with OpenAI, Azure, Cohere, Amazon Bedrock, Google Gemini and local models._
|
||||
|
||||
|
||||
> **Sister project:** Check out [sympozium](https://github.com/AlexsJones/sympozium/) for managing agents in Kubernetes.
|
||||
|
||||
|
||||
<a href="https://www.producthunt.com/posts/k8sgpt?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-k8sgpt" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=389489&theme=light" alt="K8sGPT - K8sGPT gives Kubernetes Superpowers to everyone | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://hellogithub.com/repository/9dfe44c18dfb4d6fa0181baf8b2cf2e1" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=9dfe44c18dfb4d6fa0181baf8b2cf2e1&claim_uid=gqG4wmzkMrP0eFy" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
|
||||
@@ -63,7 +67,7 @@ brew install k8sgpt
|
||||
<!---x-release-please-start-version-->
|
||||
|
||||
```
|
||||
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.27/k8sgpt_386.rpm
|
||||
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_386.rpm
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
|
||||
@@ -71,7 +75,7 @@ brew install k8sgpt
|
||||
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.27/k8sgpt_amd64.rpm
|
||||
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_amd64.rpm
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
</details>
|
||||
@@ -84,7 +88,7 @@ brew install k8sgpt
|
||||
<!---x-release-please-start-version-->
|
||||
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.27/k8sgpt_386.deb
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_386.deb
|
||||
sudo dpkg -i k8sgpt_386.deb
|
||||
```
|
||||
|
||||
@@ -95,7 +99,7 @@ sudo dpkg -i k8sgpt_386.deb
|
||||
<!---x-release-please-start-version-->
|
||||
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.27/k8sgpt_amd64.deb
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_amd64.deb
|
||||
sudo dpkg -i k8sgpt_amd64.deb
|
||||
```
|
||||
|
||||
@@ -110,7 +114,7 @@ sudo dpkg -i k8sgpt_amd64.deb
|
||||
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.27/k8sgpt_386.apk
|
||||
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_386.apk
|
||||
apk add --allow-untrusted k8sgpt_386.apk
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
@@ -119,7 +123,7 @@ sudo dpkg -i k8sgpt_amd64.deb
|
||||
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.27/k8sgpt_amd64.apk
|
||||
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_amd64.apk
|
||||
apk add --allow-untrusted k8sgpt_amd64.apk
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
@@ -279,7 +283,6 @@ you will be able to write your own analyzers.
|
||||
- [x] OperatorGroup
|
||||
- [x] InstallPlan
|
||||
- [x] Subscription
|
||||
- [x] **CustomResource** - Generic analyzer for any CRD (cert-manager, ArgoCD, Kafka, etc.) [Documentation](docs/CRD_ANALYZER.md)
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -203,6 +203,11 @@ var ServeCmd = &cobra.Command{
|
||||
}()
|
||||
}
|
||||
|
||||
// Allow metrics port to be overridden by environment variable
|
||||
if envMetricsPort := os.Getenv("K8SGPT_METRICS_PORT"); envMetricsPort != "" && !cmd.Flags().Changed("metrics-port") {
|
||||
metricsPort = envMetricsPort
|
||||
}
|
||||
|
||||
server := k8sgptserver.Config{
|
||||
Backend: aiProvider.Name,
|
||||
Port: port,
|
||||
@@ -234,7 +239,7 @@ var ServeCmd = &cobra.Command{
|
||||
func init() {
|
||||
// add flag for backend
|
||||
ServeCmd.Flags().StringVarP(&port, "port", "p", "8080", "Port to run the server on")
|
||||
ServeCmd.Flags().StringVarP(&metricsPort, "metrics-port", "", "8081", "Port to run the metrics-server on")
|
||||
ServeCmd.Flags().StringVarP(&metricsPort, "metrics-port", "m", "8081", "Port to run the metrics-server on (env: K8SGPT_METRICS_PORT)")
|
||||
ServeCmd.Flags().StringVarP(&backend, "backend", "b", "openai", "Backend AI provider")
|
||||
ServeCmd.Flags().BoolVarP(&enableHttp, "http", "", false, "Enable REST/http using gppc-gateway")
|
||||
ServeCmd.Flags().BoolVarP(&enableMCP, "mcp", "", false, "Enable Mission Control Protocol server")
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
# Generic CRD Analyzer Configuration Examples
|
||||
|
||||
The Generic CRD Analyzer enables K8sGPT to automatically analyze custom resources from any installed CRD in your Kubernetes cluster. This provides observability for operator-managed resources like cert-manager, ArgoCD, Kafka, and more.
|
||||
|
||||
## Basic Configuration
|
||||
|
||||
The CRD analyzer is configured via the K8sGPT configuration file (typically `~/.config/k8sgpt/k8sgpt.yaml`). Here's a minimal example:
|
||||
|
||||
```yaml
|
||||
crd_analyzer:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
With this basic configuration, the analyzer will:
|
||||
- Discover all CRDs installed in your cluster
|
||||
- Apply generic health checks based on common Kubernetes patterns
|
||||
- Report issues with resources that have unhealthy status conditions
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Complete Example
|
||||
|
||||
```yaml
|
||||
crd_analyzer:
|
||||
enabled: true
|
||||
include:
|
||||
- name: certificates.cert-manager.io
|
||||
statusPath: ".status.conditions"
|
||||
readyCondition:
|
||||
type: "Ready"
|
||||
expectedStatus: "True"
|
||||
|
||||
- name: applications.argoproj.io
|
||||
statusPath: ".status.health.status"
|
||||
expectedValue: "Healthy"
|
||||
|
||||
- name: kafkas.kafka.strimzi.io
|
||||
readyCondition:
|
||||
type: "Ready"
|
||||
expectedStatus: "True"
|
||||
|
||||
exclude:
|
||||
- name: kafkatopics.kafka.strimzi.io
|
||||
- name: servicemonitors.monitoring.coreos.com
|
||||
```
|
||||
|
||||
### Configuration Fields
|
||||
|
||||
#### `enabled` (boolean)
|
||||
- **Default**: `false`
|
||||
- **Description**: Master switch to enable/disable the CRD analyzer
|
||||
- **Example**: `enabled: true`
|
||||
|
||||
#### `include` (array)
|
||||
- **Description**: List of CRDs with custom health check configurations
|
||||
- **Fields**:
|
||||
- `name` (string, required): The full CRD name (e.g., `certificates.cert-manager.io`)
|
||||
- `statusPath` (string, optional): JSONPath to the status field to check (e.g., `.status.health.status`)
|
||||
- `readyCondition` (object, optional): Configuration for checking a Ready-style condition
|
||||
- `type` (string): The condition type to check (e.g., `"Ready"`)
|
||||
- `expectedStatus` (string): Expected status value (e.g., `"True"`)
|
||||
- `expectedValue` (string, optional): Expected value at the statusPath (requires `statusPath`)
|
||||
|
||||
#### `exclude` (array)
|
||||
- **Description**: List of CRDs to skip during analysis
|
||||
- **Fields**:
|
||||
- `name` (string): The full CRD name to exclude
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. cert-manager Certificate Analysis
|
||||
|
||||
Detect certificates that are not ready or have issuance failures:
|
||||
|
||||
```yaml
|
||||
crd_analyzer:
|
||||
enabled: true
|
||||
include:
|
||||
- name: certificates.cert-manager.io
|
||||
readyCondition:
|
||||
type: "Ready"
|
||||
expectedStatus: "True"
|
||||
```
|
||||
|
||||
**Detected Issues:**
|
||||
- Certificates with `Ready=False`
|
||||
- Certificate renewal failures
|
||||
- Invalid certificate configurations
|
||||
|
||||
### 2. ArgoCD Application Health
|
||||
|
||||
Monitor ArgoCD application sync and health status:
|
||||
|
||||
```yaml
|
||||
crd_analyzer:
|
||||
enabled: true
|
||||
include:
|
||||
- name: applications.argoproj.io
|
||||
statusPath: ".status.health.status"
|
||||
expectedValue: "Healthy"
|
||||
```
|
||||
|
||||
**Detected Issues:**
|
||||
- Applications in `Degraded` state
|
||||
- Sync failures
|
||||
- Missing resources
|
||||
|
||||
### 3. Kafka Operator Resources
|
||||
|
||||
Check Kafka cluster health with Strimzi operator:
|
||||
|
||||
```yaml
|
||||
crd_analyzer:
|
||||
enabled: true
|
||||
include:
|
||||
- name: kafkas.kafka.strimzi.io
|
||||
readyCondition:
|
||||
type: "Ready"
|
||||
expectedStatus: "True"
|
||||
exclude:
|
||||
- name: kafkatopics.kafka.strimzi.io # Exclude topics to reduce noise
|
||||
```
|
||||
|
||||
**Detected Issues:**
|
||||
- Kafka clusters not ready
|
||||
- Broker failures
|
||||
- Configuration issues
|
||||
|
||||
### 4. Prometheus Operator
|
||||
|
||||
Monitor Prometheus instances:
|
||||
|
||||
```yaml
|
||||
crd_analyzer:
|
||||
enabled: true
|
||||
include:
|
||||
- name: prometheuses.monitoring.coreos.com
|
||||
readyCondition:
|
||||
type: "Available"
|
||||
expectedStatus: "True"
|
||||
```
|
||||
|
||||
**Detected Issues:**
|
||||
- Prometheus instances not available
|
||||
- Configuration reload failures
|
||||
- Storage issues
|
||||
|
||||
## Generic Health Checks
|
||||
|
||||
When a CRD is not explicitly configured in the `include` list, the analyzer applies generic health checks:
|
||||
|
||||
### Supported Patterns
|
||||
|
||||
1. **status.conditions** - Standard Kubernetes conditions
|
||||
- Flags `Ready` conditions with status != `"True"`
|
||||
- Flags any condition type containing "failed" with status = `"True"`
|
||||
|
||||
2. **status.phase** - Phase-based resources
|
||||
- Flags resources with phase = `"Failed"` or `"Error"`
|
||||
|
||||
3. **status.health.status** - ArgoCD-style health
|
||||
- Flags resources with health status != `"Healthy"` (except `"Unknown"`)
|
||||
|
||||
4. **status.state** - State-based resources
|
||||
- Flags resources with state = `"Failed"` or `"Error"`
|
||||
|
||||
5. **Deletion with Finalizers** - Stuck resources
|
||||
- Flags resources with `deletionTimestamp` set but still having finalizers
|
||||
|
||||
## Running the Analyzer
|
||||
|
||||
### Enable in Configuration
|
||||
|
||||
Add the CRD analyzer to your active filters:
|
||||
|
||||
```bash
|
||||
# Add CustomResource filter
|
||||
k8sgpt filters add CustomResource
|
||||
|
||||
# List active filters to verify
|
||||
k8sgpt filters list
|
||||
```
|
||||
|
||||
### Run Analysis
|
||||
|
||||
```bash
|
||||
# Basic analysis
|
||||
k8sgpt analyze --explain
|
||||
|
||||
# With specific filter
|
||||
k8sgpt analyze --explain --filter=CustomResource
|
||||
|
||||
# In a specific namespace
|
||||
k8sgpt analyze --explain --filter=CustomResource --namespace=production
|
||||
```
|
||||
|
||||
### Example Output
|
||||
|
||||
```
|
||||
AI Provider: openai
|
||||
|
||||
0: CustomResource/Certificate(default/example-cert)
|
||||
- Error: Condition Ready is False (reason: Failed): Certificate issuance failed
|
||||
- Details: The certificate 'example-cert' in namespace 'default' failed to issue.
|
||||
The Let's Encrypt challenge validation failed due to DNS propagation issues.
|
||||
Recommendation: Check DNS records and retry certificate issuance.
|
||||
|
||||
1: CustomResource/Application(argocd/my-app)
|
||||
- Error: Health status is Degraded
|
||||
- Details: The ArgoCD application 'my-app' is in a Degraded state.
|
||||
This typically indicates that deployed resources are not healthy.
|
||||
Recommendation: Check application logs and pod status.
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Start with Generic Checks
|
||||
Begin with just `enabled: true` to see what issues are detected across all CRDs.
|
||||
|
||||
### 2. Add Specific Configurations Gradually
|
||||
Add custom configurations for critical CRDs that need specialized health checks.
|
||||
|
||||
### 3. Use Exclusions to Reduce Noise
|
||||
Exclude CRDs that generate false positives or are less critical.
|
||||
|
||||
### 4. Combine with Other Analyzers
|
||||
Use the CRD analyzer alongside built-in analyzers for comprehensive cluster observability.
|
||||
|
||||
### 5. Monitor Performance
|
||||
If you have many CRDs, the analysis may take longer. Use exclusions to optimize.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Analyzer Not Running
|
||||
- Verify `enabled: true` is set in configuration
|
||||
- Check that `CustomResource` is in active filters: `k8sgpt filters list`
|
||||
- Ensure configuration file is in the correct location
|
||||
|
||||
### No Issues Detected
|
||||
- Verify CRDs are actually installed: `kubectl get crds`
|
||||
- Check if custom resources exist: `kubectl get <crd-name> --all-namespaces`
|
||||
- Review generic health check patterns - your CRDs may use different status fields
|
||||
|
||||
### Too Many False Positives
|
||||
- Add specific configurations for problematic CRDs in the `include` section
|
||||
- Use the `exclude` list to skip noisy CRDs
|
||||
- Review the status patterns your CRDs use and configure accordingly
|
||||
|
||||
### Configuration Not Applied
|
||||
- Restart K8sGPT after configuration changes
|
||||
- Verify YAML syntax is correct
|
||||
- Check K8sGPT logs for configuration parsing errors
|
||||
@@ -1,45 +0,0 @@
|
||||
# Example K8sGPT Configuration with CRD Analyzer
|
||||
# Place this file at ~/.config/k8sgpt/k8sgpt.yaml
|
||||
|
||||
# CRD Analyzer Configuration
|
||||
crd_analyzer:
|
||||
enabled: true
|
||||
|
||||
# Specific CRD configurations with custom health checks
|
||||
include:
|
||||
# cert-manager certificates
|
||||
- name: certificates.cert-manager.io
|
||||
readyCondition:
|
||||
type: "Ready"
|
||||
expectedStatus: "True"
|
||||
|
||||
# ArgoCD applications
|
||||
- name: applications.argoproj.io
|
||||
statusPath: ".status.health.status"
|
||||
expectedValue: "Healthy"
|
||||
|
||||
# Strimzi Kafka clusters
|
||||
- name: kafkas.kafka.strimzi.io
|
||||
readyCondition:
|
||||
type: "Ready"
|
||||
expectedStatus: "True"
|
||||
|
||||
# Prometheus instances
|
||||
- name: prometheuses.monitoring.coreos.com
|
||||
readyCondition:
|
||||
type: "Available"
|
||||
expectedStatus: "True"
|
||||
|
||||
# CRDs to skip during analysis
|
||||
exclude:
|
||||
- name: kafkatopics.kafka.strimzi.io
|
||||
- name: servicemonitors.monitoring.coreos.com
|
||||
- name: podmonitors.monitoring.coreos.com
|
||||
- name: prometheusrules.monitoring.coreos.com
|
||||
|
||||
# Other K8sGPT configuration...
|
||||
# ai:
|
||||
# providers:
|
||||
# - name: openai
|
||||
# model: gpt-4
|
||||
# # ... other AI config
|
||||
352
go.mod
352
go.mod
@@ -1,174 +1,190 @@
|
||||
module github.com/k8sgpt-ai/k8sgpt
|
||||
|
||||
go 1.24.1
|
||||
go 1.25.0
|
||||
|
||||
toolchain go1.24.11
|
||||
toolchain go1.26.1
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/kedacore/keda/v2 v2.16.0
|
||||
github.com/magiconair/properties v1.8.9
|
||||
github.com/mittwald/go-helm-client v0.12.14
|
||||
github.com/ollama/ollama v0.13.4
|
||||
github.com/sashabaranov/go-openai v1.36.0
|
||||
github.com/schollz/progressbar/v3 v3.17.1
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/viper v1.19.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/term v0.33.0
|
||||
helm.sh/helm/v3 v3.17.4
|
||||
k8s.io/api v0.32.3
|
||||
k8s.io/apimachinery v0.32.3
|
||||
k8s.io/client-go v0.32.3
|
||||
k8s.io/kubectl v0.32.2 // indirect
|
||||
|
||||
github.com/magiconair/properties v1.8.10
|
||||
github.com/mittwald/go-helm-client v0.12.19
|
||||
github.com/ollama/ollama v0.18.1
|
||||
github.com/sashabaranov/go-openai v1.41.2
|
||||
github.com/schollz/progressbar/v3 v3.19.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
golang.org/x/term v0.41.0
|
||||
helm.sh/helm/v3 v3.19.0
|
||||
k8s.io/api v0.35.2
|
||||
k8s.io/apimachinery v0.35.2
|
||||
k8s.io/client-go v0.35.2
|
||||
k8s.io/kubectl v0.34.0 // indirect
|
||||
)
|
||||
|
||||
require github.com/adrg/xdg v0.5.3
|
||||
|
||||
require (
|
||||
buf.build/gen/go/interplex-ai/schemas/grpc/go v1.5.1-20241117203254-a91193b62179.1
|
||||
buf.build/gen/go/interplex-ai/schemas/protocolbuffers/go v1.35.2-20241117203254-a91193b62179.1
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 v2.24.0-20241118152629-1379a5a1889d.1
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.5.1-20241118152629-1379a5a1889d.1
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.35.2-20241118152629-1379a5a1889d.1
|
||||
cloud.google.com/go/storage v1.50.0
|
||||
cloud.google.com/go/vertexai v0.13.2
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0
|
||||
github.com/IBM/watsonx-go v1.0.1
|
||||
github.com/agiledragon/gomonkey/v2 v2.13.0
|
||||
github.com/aws/aws-sdk-go v1.55.7
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.14
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrock v1.33.0
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.30.0
|
||||
github.com/aws/smithy-go v1.22.2
|
||||
github.com/cohere-ai/cohere-go/v2 v2.12.2
|
||||
buf.build/gen/go/interplex-ai/schemas/grpc/go v1.6.1-20241117203254-a91193b62179.1
|
||||
buf.build/gen/go/interplex-ai/schemas/protocolbuffers/go v1.36.11-20241117203254-a91193b62179.1
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 v2.28.0-20241118152629-1379a5a1889d.1
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.6.1-20241118152629-1379a5a1889d.1
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.36.11-20241118152629-1379a5a1889d.1
|
||||
cloud.google.com/go/storage v1.61.3
|
||||
cloud.google.com/go/vertexai v0.17.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4
|
||||
github.com/IBM/watsonx-go v1.0.2
|
||||
github.com/agiledragon/gomonkey/v2 v2.14.0
|
||||
github.com/aws/aws-sdk-go v1.55.8
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.4
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.12
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrock v1.57.0
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.2
|
||||
github.com/aws/smithy-go v1.24.2
|
||||
github.com/cohere-ai/cohere-go/v2 v2.16.2
|
||||
github.com/go-logr/zapr v1.3.0
|
||||
github.com/google/generative-ai-go v0.19.0
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3
|
||||
github.com/google/generative-ai-go v0.20.1
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0
|
||||
github.com/hupe1980/go-huggingface v0.0.15
|
||||
github.com/kyverno/policy-reporter-kyverno-plugin v1.6.4
|
||||
github.com/mark3labs/mcp-go v0.36.0
|
||||
github.com/mark3labs/mcp-go v0.45.0
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/oracle/oci-go-sdk/v65 v65.79.0
|
||||
github.com/prometheus/prometheus v0.306.0
|
||||
github.com/pterm/pterm v0.12.80
|
||||
google.golang.org/api v0.239.0
|
||||
github.com/oracle/oci-go-sdk/v65 v65.109.2
|
||||
github.com/prometheus/prometheus v0.310.0
|
||||
github.com/pterm/pterm v0.12.83
|
||||
google.golang.org/api v0.272.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
sigs.k8s.io/controller-runtime v0.19.3
|
||||
sigs.k8s.io/gateway-api v1.2.1
|
||||
sigs.k8s.io/controller-runtime v0.23.3
|
||||
sigs.k8s.io/gateway-api v1.5.1
|
||||
)
|
||||
|
||||
require (
|
||||
atomicgo.dev/cursor v0.2.0 // indirect
|
||||
atomicgo.dev/keyboard v0.2.9 // indirect
|
||||
atomicgo.dev/schedule v0.1.0 // indirect
|
||||
cel.dev/expr v0.23.0 // indirect
|
||||
cloud.google.com/go v0.120.0 // indirect
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
cloud.google.com/go/ai v0.8.0 // indirect
|
||||
cloud.google.com/go/aiplatform v1.85.0 // indirect
|
||||
cloud.google.com/go/auth v0.16.2 // indirect
|
||||
cloud.google.com/go/aiplatform v1.120.0 // indirect
|
||||
cloud.google.com/go/auth v0.18.2 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.7.0 // indirect
|
||||
cloud.google.com/go/iam v1.5.2 // indirect
|
||||
cloud.google.com/go/longrunning v0.6.7 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.2 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
cloud.google.com/go/iam v1.5.3 // indirect
|
||||
cloud.google.com/go/longrunning v0.8.0 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.3 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0 // indirect
|
||||
github.com/Microsoft/hcsshim v0.12.4 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect
|
||||
github.com/containerd/console v1.0.4 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
|
||||
github.com/containerd/console v1.0.5 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/creack/pty v1.1.21 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
github.com/expr-lang/expr v1.17.7 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/fileutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/loading v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/mangling v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/netutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
|
||||
github.com/gookit/color v1.5.4 // indirect
|
||||
github.com/gorilla/websocket v1.5.1 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.18.0 // indirect
|
||||
github.com/gookit/color v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jpillora/backoff v1.0.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||
github.com/moby/sys/mountinfo v0.7.1 // indirect
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/prometheus/sigv4 v0.2.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.6.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/prometheus/client_golang/exp v0.0.0-20260108101519-fb0838f53562 // indirect
|
||||
github.com/prometheus/otlptranslator v1.0.0 // indirect
|
||||
github.com/prometheus/sigv4 v0.4.1 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||
github.com/segmentio/fasthash v1.0.3 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/sony/gobreaker v0.5.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
github.com/zeebo/errs v1.4.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
google.golang.org/genai v1.50.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
knative.dev/pkg v0.0.0-20241026180704-25f6002b00f3 // indirect
|
||||
oras.land/oras-go/v2 v2.6.0 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/MakeNowJust/heredoc v1.0.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
@@ -176,124 +192,102 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chai2010/gettext-go v1.0.3 // indirect
|
||||
github.com/containerd/containerd v1.7.29 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.3.6 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/docker/cli v26.1.4+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker v28.3.0+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.8.2 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
|
||||
github.com/evanphx/json-patch v5.9.0+incompatible // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
|
||||
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-errors/errors v1.5.1 // indirect
|
||||
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.4 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.4 // indirect
|
||||
github.com/go-openapi/swag v0.25.4 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/gnostic v0.7.0
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/gnostic v0.7.1
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/gosuri/uitable v0.0.4 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jmoiron/sqlx v1.4.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mailru/easyjson v0.9.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.20 // indirect
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/locker v1.0.1 // indirect
|
||||
github.com/moby/spdystream v0.5.0 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.23.0-rc.1
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.65.1-0.20250703115700-7f8b2a0d32d3 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/rubenv/sql-migrate v1.7.1 // indirect
|
||||
github.com/rubenv/sql-migrate v1.8.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
go.opentelemetry.io/otel v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
|
||||
golang.org/x/net v0.42.0
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
google.golang.org/grpc v1.73.0
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
go.uber.org/zap v1.27.1
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/net v0.52.0
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
google.golang.org/grpc v1.79.3
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.32.2
|
||||
k8s.io/apiserver v0.32.2 // indirect
|
||||
k8s.io/cli-runtime v0.32.2 // indirect
|
||||
k8s.io/component-base v0.32.2 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.35.2
|
||||
k8s.io/apiserver v0.35.2 // indirect
|
||||
k8s.io/cli-runtime v0.34.0 // indirect
|
||||
k8s.io/component-base v0.35.2 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
|
||||
oras.land/oras-go v1.2.5 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.18.0 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.20.1 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
// v1.2.0 is taken from github.com/open-policy-agent/opa v0.42.0
|
||||
// v1.2.0 incompatible with github.com/docker/docker v23.0.0-rc.1+incompatible
|
||||
//replace oras.land/oras-go => oras.land/oras-go v1.2.4
|
||||
replace github.com/docker/docker => github.com/docker/docker v28.0.4+incompatible
|
||||
replace github.com/docker/docker => github.com/docker/docker v28.5.2+incompatible
|
||||
|
||||
replace dario.cat/mergo => github.com/imdario/mergo v1.0.1
|
||||
replace dario.cat/mergo => github.com/imdario/mergo v1.0.2
|
||||
|
||||
@@ -16,6 +16,7 @@ package analysis
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
@@ -34,6 +35,7 @@ import (
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
"github.com/schollz/progressbar/v3"
|
||||
"github.com/spf13/viper"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type Analysis struct {
|
||||
@@ -226,6 +228,15 @@ func (a *Analysis) CustomAnalyzersAreAvailable() bool {
|
||||
}
|
||||
|
||||
func (a *Analysis) RunCustomAnalysis() {
|
||||
// Validate namespace if specified, consistent with built-in filter behavior
|
||||
if a.Namespace != "" && a.Client != nil {
|
||||
_, err := a.Client.Client.CoreV1().Namespaces().Get(a.Context, a.Namespace, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
a.Errors = append(a.Errors, fmt.Sprintf("namespace %q not found: %s", a.Namespace, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var customAnalyzers []custom.CustomAnalyzer
|
||||
if err := viper.UnmarshalKey("custom_analyzers", &customAnalyzers); err != nil {
|
||||
a.Errors = append(a.Errors, err.Error())
|
||||
@@ -526,7 +537,22 @@ func (a *Analysis) getAIResultForSanitizedFailures(texts []string, promptTmpl st
|
||||
// Process template.
|
||||
prompt := fmt.Sprintf(strings.TrimSpace(promptTmpl), a.Language, inputKey)
|
||||
if a.AIClient.GetName() == ai.CustomRestClientName {
|
||||
prompt = fmt.Sprintf(ai.PromptMap["raw"], a.Language, inputKey, prompt)
|
||||
// Use proper JSON marshaling to handle special characters in error messages
|
||||
// This fixes issues with quotes, newlines, and other special chars in inputKey
|
||||
customRestPrompt := struct {
|
||||
Language string `json:"language"`
|
||||
Message string `json:"message"`
|
||||
Prompt string `json:"prompt"`
|
||||
}{
|
||||
Language: a.Language,
|
||||
Message: inputKey,
|
||||
Prompt: prompt,
|
||||
}
|
||||
promptBytes, err := json.Marshal(customRestPrompt)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal customrest prompt: %w", err)
|
||||
}
|
||||
prompt = string(promptBytes)
|
||||
}
|
||||
response, err := a.AIClient.GetCompletion(a.Context, prompt)
|
||||
if err != nil {
|
||||
|
||||
@@ -64,7 +64,6 @@ var additionalAnalyzerMap = map[string]common.IAnalyzer{
|
||||
"InstallPlan": InstallPlanAnalyzer{},
|
||||
"CatalogSource": CatalogSourceAnalyzer{},
|
||||
"OperatorGroup": OperatorGroupAnalyzer{},
|
||||
"CustomResource": CRDAnalyzer{},
|
||||
}
|
||||
|
||||
func ListFilters() ([]string, []string, []string) {
|
||||
|
||||
@@ -1,330 +0,0 @@
|
||||
/*
|
||||
Copyright 2023 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/spf13/viper"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type CRDAnalyzer struct{}
|
||||
|
||||
func (CRDAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
// Load CRD analyzer configuration
|
||||
var config common.CRDAnalyzerConfig
|
||||
if err := viper.UnmarshalKey("crd_analyzer", &config); err != nil {
|
||||
// If no config or error, disable the analyzer
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if !config.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create apiextensions client to discover CRDs
|
||||
apiExtClient, err := apiextensionsclientset.NewForConfig(a.Client.GetConfig())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create apiextensions client: %w", err)
|
||||
}
|
||||
|
||||
// List all CRDs in the cluster
|
||||
crdList, err := apiExtClient.ApiextensionsV1().CustomResourceDefinitions().List(a.Context, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list CRDs: %w", err)
|
||||
}
|
||||
|
||||
var results []common.Result
|
||||
|
||||
// Process each CRD
|
||||
for _, crd := range crdList.Items {
|
||||
// Check if CRD should be excluded
|
||||
if shouldExcludeCRD(crd.Name, config.Exclude) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the CRD configuration (if specified)
|
||||
crdConfig := getCRDConfig(crd.Name, config.Include)
|
||||
|
||||
// Analyze resources for this CRD
|
||||
crdResults, err := analyzeCRDResources(a, crd, crdConfig)
|
||||
if err != nil {
|
||||
// Log error but continue with other CRDs
|
||||
continue
|
||||
}
|
||||
|
||||
results = append(results, crdResults...)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// shouldExcludeCRD checks if a CRD should be excluded from analysis
|
||||
func shouldExcludeCRD(crdName string, excludeList []common.CRDExcludeConfig) bool {
|
||||
for _, exclude := range excludeList {
|
||||
if exclude.Name == crdName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getCRDConfig returns the configuration for a specific CRD if it exists
|
||||
func getCRDConfig(crdName string, includeList []common.CRDIncludeConfig) *common.CRDIncludeConfig {
|
||||
for _, include := range includeList {
|
||||
if include.Name == crdName {
|
||||
return &include
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// analyzeCRDResources analyzes all instances of a CRD
|
||||
func analyzeCRDResources(a common.Analyzer, crd apiextensionsv1.CustomResourceDefinition, config *common.CRDIncludeConfig) ([]common.Result, error) {
|
||||
if a.Client.GetDynamicClient() == nil {
|
||||
return nil, fmt.Errorf("dynamic client is nil")
|
||||
}
|
||||
|
||||
// Get the preferred version (typically the storage version)
|
||||
var version string
|
||||
for _, v := range crd.Spec.Versions {
|
||||
if v.Storage {
|
||||
version = v.Name
|
||||
break
|
||||
}
|
||||
}
|
||||
if version == "" && len(crd.Spec.Versions) > 0 {
|
||||
version = crd.Spec.Versions[0].Name
|
||||
}
|
||||
|
||||
// Construct GVR
|
||||
gvr := schema.GroupVersionResource{
|
||||
Group: crd.Spec.Group,
|
||||
Version: version,
|
||||
Resource: crd.Spec.Names.Plural,
|
||||
}
|
||||
|
||||
// List resources
|
||||
var list *unstructured.UnstructuredList
|
||||
var err error
|
||||
if crd.Spec.Scope == apiextensionsv1.NamespaceScoped {
|
||||
if a.Namespace != "" {
|
||||
list, err = a.Client.GetDynamicClient().Resource(gvr).Namespace(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector})
|
||||
} else {
|
||||
list, err = a.Client.GetDynamicClient().Resource(gvr).Namespace(metav1.NamespaceAll).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector})
|
||||
}
|
||||
} else {
|
||||
// Cluster-scoped
|
||||
list, err = a.Client.GetDynamicClient().Resource(gvr).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var results []common.Result
|
||||
|
||||
// Analyze each resource instance
|
||||
for _, item := range list.Items {
|
||||
failures := analyzeResource(item, crd, config)
|
||||
if len(failures) > 0 {
|
||||
resourceName := item.GetName()
|
||||
if item.GetNamespace() != "" {
|
||||
resourceName = item.GetNamespace() + "/" + resourceName
|
||||
}
|
||||
|
||||
results = append(results, common.Result{
|
||||
Kind: crd.Spec.Names.Kind,
|
||||
Name: resourceName,
|
||||
Error: failures,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// analyzeResource analyzes a single CR instance for issues
|
||||
func analyzeResource(item unstructured.Unstructured, crd apiextensionsv1.CustomResourceDefinition, config *common.CRDIncludeConfig) []common.Failure {
|
||||
var failures []common.Failure
|
||||
|
||||
// Check for deletion with finalizers (resource stuck in deletion)
|
||||
if item.GetDeletionTimestamp() != nil && len(item.GetFinalizers()) > 0 {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Resource is being deleted but has finalizers: %v", item.GetFinalizers()),
|
||||
})
|
||||
}
|
||||
|
||||
// If custom config is provided, use it
|
||||
if config != nil {
|
||||
configFailures := analyzeWithConfig(item, config)
|
||||
failures = append(failures, configFailures...)
|
||||
return failures
|
||||
}
|
||||
|
||||
// Otherwise, use generic health checks based on common patterns
|
||||
genericFailures := analyzeGenericHealth(item)
|
||||
failures = append(failures, genericFailures...)
|
||||
|
||||
return failures
|
||||
}
|
||||
|
||||
// analyzeWithConfig analyzes a resource using custom configuration
|
||||
func analyzeWithConfig(item unstructured.Unstructured, config *common.CRDIncludeConfig) []common.Failure {
|
||||
var failures []common.Failure
|
||||
|
||||
// Check ReadyCondition if specified
|
||||
if config.ReadyCondition != nil {
|
||||
conditions, found, err := unstructured.NestedSlice(item.Object, "status", "conditions")
|
||||
if !found || err != nil {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: "Expected status.conditions not found",
|
||||
})
|
||||
return failures
|
||||
}
|
||||
|
||||
ready := false
|
||||
var conditionMessages []string
|
||||
for _, cond := range conditions {
|
||||
condMap, ok := cond.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
condType, _, _ := unstructured.NestedString(condMap, "type")
|
||||
status, _, _ := unstructured.NestedString(condMap, "status")
|
||||
message, _, _ := unstructured.NestedString(condMap, "message")
|
||||
|
||||
if condType == config.ReadyCondition.Type {
|
||||
if status == config.ReadyCondition.ExpectedStatus {
|
||||
ready = true
|
||||
} else {
|
||||
conditionMessages = append(conditionMessages, fmt.Sprintf("%s=%s: %s", condType, status, message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !ready {
|
||||
msg := fmt.Sprintf("Ready condition not met: expected %s=%s", config.ReadyCondition.Type, config.ReadyCondition.ExpectedStatus)
|
||||
if len(conditionMessages) > 0 {
|
||||
msg += "; " + strings.Join(conditionMessages, "; ")
|
||||
}
|
||||
failures = append(failures, common.Failure{
|
||||
Text: msg,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Check ExpectedValue if specified and StatusPath provided
|
||||
if config.ExpectedValue != "" && config.StatusPath != "" {
|
||||
pathParts := strings.Split(config.StatusPath, ".")
|
||||
// Remove leading dot if present
|
||||
if len(pathParts) > 0 && pathParts[0] == "" {
|
||||
pathParts = pathParts[1:]
|
||||
}
|
||||
|
||||
actualValue, found, err := unstructured.NestedString(item.Object, pathParts...)
|
||||
if !found || err != nil {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Expected field %s not found", config.StatusPath),
|
||||
})
|
||||
} else if actualValue != config.ExpectedValue {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Field %s has value '%s', expected '%s'", config.StatusPath, actualValue, config.ExpectedValue),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return failures
|
||||
}
|
||||
|
||||
// analyzeGenericHealth applies generic health checks based on common Kubernetes patterns
|
||||
func analyzeGenericHealth(item unstructured.Unstructured) []common.Failure {
|
||||
var failures []common.Failure
|
||||
|
||||
// Check for status.conditions (common pattern)
|
||||
conditions, found, err := unstructured.NestedSlice(item.Object, "status", "conditions")
|
||||
if found && err == nil && len(conditions) > 0 {
|
||||
for _, cond := range conditions {
|
||||
condMap, ok := cond.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
condType, _, _ := unstructured.NestedString(condMap, "type")
|
||||
status, _, _ := unstructured.NestedString(condMap, "status")
|
||||
reason, _, _ := unstructured.NestedString(condMap, "reason")
|
||||
message, _, _ := unstructured.NestedString(condMap, "message")
|
||||
|
||||
// Check for common failure patterns
|
||||
if condType == "Ready" && status != "True" {
|
||||
msg := fmt.Sprintf("Condition Ready is %s", status)
|
||||
if reason != "" {
|
||||
msg += fmt.Sprintf(" (reason: %s)", reason)
|
||||
}
|
||||
if message != "" {
|
||||
msg += fmt.Sprintf(": %s", message)
|
||||
}
|
||||
failures = append(failures, common.Failure{Text: msg})
|
||||
} else if strings.Contains(strings.ToLower(condType), "failed") && status == "True" {
|
||||
msg := fmt.Sprintf("Condition %s is True", condType)
|
||||
if message != "" {
|
||||
msg += fmt.Sprintf(": %s", message)
|
||||
}
|
||||
failures = append(failures, common.Failure{Text: msg})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for status.phase (common pattern)
|
||||
phase, found, _ := unstructured.NestedString(item.Object, "status", "phase")
|
||||
if found && phase != "" {
|
||||
lowerPhase := strings.ToLower(phase)
|
||||
if lowerPhase == "failed" || lowerPhase == "error" {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Resource phase is %s", phase),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Check for status.health.status (ArgoCD pattern)
|
||||
healthStatus, found, _ := unstructured.NestedString(item.Object, "status", "health", "status")
|
||||
if found && healthStatus != "" {
|
||||
if healthStatus != "Healthy" && healthStatus != "Unknown" {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Health status is %s", healthStatus),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Check for status.state (common pattern)
|
||||
state, found, _ := unstructured.NestedString(item.Object, "status", "state")
|
||||
if found && state != "" {
|
||||
lowerState := strings.ToLower(state)
|
||||
if lowerState == "failed" || lowerState == "error" {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Resource state is %s", state),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return failures
|
||||
}
|
||||
@@ -1,410 +0,0 @@
|
||||
/*
|
||||
Copyright 2023 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/spf13/viper"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// TestCRDAnalyzer_Disabled tests that analyzer returns nil when disabled
|
||||
func TestCRDAnalyzer_Disabled(t *testing.T) {
|
||||
viper.Reset()
|
||||
viper.Set("crd_analyzer", map[string]interface{}{
|
||||
"enabled": false,
|
||||
})
|
||||
|
||||
a := common.Analyzer{
|
||||
Context: context.TODO(),
|
||||
Client: &kubernetes.Client{},
|
||||
}
|
||||
|
||||
res, err := (CRDAnalyzer{}).Analyze(a)
|
||||
if err != nil {
|
||||
t.Fatalf("Analyze error: %v", err)
|
||||
}
|
||||
if res != nil {
|
||||
t.Fatalf("expected nil result when disabled, got %d results", len(res))
|
||||
}
|
||||
}
|
||||
|
||||
// TestCRDAnalyzer_NoConfig tests that analyzer returns nil when no config exists
|
||||
func TestCRDAnalyzer_NoConfig(t *testing.T) {
|
||||
viper.Reset()
|
||||
|
||||
a := common.Analyzer{
|
||||
Context: context.TODO(),
|
||||
Client: &kubernetes.Client{},
|
||||
}
|
||||
|
||||
res, err := (CRDAnalyzer{}).Analyze(a)
|
||||
if err != nil {
|
||||
t.Fatalf("Analyze error: %v", err)
|
||||
}
|
||||
if res != nil {
|
||||
t.Fatalf("expected nil result when no config, got %d results", len(res))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAnalyzeGenericHealth_ReadyConditionFalse tests detection of Ready=False condition
|
||||
func TestAnalyzeGenericHealth_ReadyConditionFalse(t *testing.T) {
|
||||
item := unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "cert-manager.io/v1",
|
||||
"kind": "Certificate",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "example-cert",
|
||||
"namespace": "default",
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"conditions": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "Ready",
|
||||
"status": "False",
|
||||
"reason": "Failed",
|
||||
"message": "Certificate issuance failed",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
failures := analyzeGenericHealth(item)
|
||||
if len(failures) != 1 {
|
||||
t.Fatalf("expected 1 failure, got %d", len(failures))
|
||||
}
|
||||
if !strings.Contains(failures[0].Text, "Ready is False") {
|
||||
t.Errorf("expected 'Ready is False' in failure text, got: %s", failures[0].Text)
|
||||
}
|
||||
if !strings.Contains(failures[0].Text, "Failed") {
|
||||
t.Errorf("expected 'Failed' reason in failure text, got: %s", failures[0].Text)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAnalyzeGenericHealth_FailedPhase tests detection of Failed phase
|
||||
func TestAnalyzeGenericHealth_FailedPhase(t *testing.T) {
|
||||
item := unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "example.io/v1",
|
||||
"kind": "CustomJob",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "failed-job",
|
||||
"namespace": "default",
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"phase": "Failed",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
failures := analyzeGenericHealth(item)
|
||||
if len(failures) != 1 {
|
||||
t.Fatalf("expected 1 failure, got %d", len(failures))
|
||||
}
|
||||
if !strings.Contains(failures[0].Text, "phase is Failed") {
|
||||
t.Errorf("expected 'phase is Failed' in failure text, got: %s", failures[0].Text)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAnalyzeGenericHealth_UnhealthyHealthStatus tests ArgoCD-style health status
|
||||
func TestAnalyzeGenericHealth_UnhealthyHealthStatus(t *testing.T) {
|
||||
item := unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "argoproj.io/v1alpha1",
|
||||
"kind": "Application",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "my-app",
|
||||
"namespace": "argocd",
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"health": map[string]interface{}{
|
||||
"status": "Degraded",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
failures := analyzeGenericHealth(item)
|
||||
if len(failures) != 1 {
|
||||
t.Fatalf("expected 1 failure, got %d", len(failures))
|
||||
}
|
||||
if !strings.Contains(failures[0].Text, "Health status is Degraded") {
|
||||
t.Errorf("expected 'Health status is Degraded' in failure text, got: %s", failures[0].Text)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAnalyzeGenericHealth_HealthyResource tests that healthy resources are not flagged
|
||||
func TestAnalyzeGenericHealth_HealthyResource(t *testing.T) {
|
||||
item := unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "cert-manager.io/v1",
|
||||
"kind": "Certificate",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "healthy-cert",
|
||||
"namespace": "default",
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"conditions": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "Ready",
|
||||
"status": "True",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
failures := analyzeGenericHealth(item)
|
||||
if len(failures) != 0 {
|
||||
t.Fatalf("expected 0 failures for healthy resource, got %d", len(failures))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAnalyzeResource_DeletionWithFinalizers tests detection of stuck deletion
|
||||
func TestAnalyzeResource_DeletionWithFinalizers(t *testing.T) {
|
||||
deletionTimestamp := metav1.Now()
|
||||
item := unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "example.io/v1",
|
||||
"kind": "CustomResource",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "stuck-resource",
|
||||
"namespace": "default",
|
||||
"deletionTimestamp": deletionTimestamp.Format("2006-01-02T15:04:05Z"),
|
||||
"finalizers": []interface{}{"example.io/finalizer"},
|
||||
},
|
||||
},
|
||||
}
|
||||
item.SetDeletionTimestamp(&deletionTimestamp)
|
||||
item.SetFinalizers([]string{"example.io/finalizer"})
|
||||
|
||||
crd := apiextensionsv1.CustomResourceDefinition{}
|
||||
failures := analyzeResource(item, crd, nil)
|
||||
|
||||
if len(failures) != 1 {
|
||||
t.Fatalf("expected 1 failure for stuck deletion, got %d", len(failures))
|
||||
}
|
||||
if !strings.Contains(failures[0].Text, "being deleted") {
|
||||
t.Errorf("expected 'being deleted' in failure text, got: %s", failures[0].Text)
|
||||
}
|
||||
if !strings.Contains(failures[0].Text, "finalizers") {
|
||||
t.Errorf("expected 'finalizers' in failure text, got: %s", failures[0].Text)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAnalyzeWithConfig_ReadyConditionCheck tests custom ready condition checking
|
||||
func TestAnalyzeWithConfig_ReadyConditionCheck(t *testing.T) {
|
||||
item := unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "cert-manager.io/v1",
|
||||
"kind": "Certificate",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test-cert",
|
||||
"namespace": "default",
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"conditions": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "Ready",
|
||||
"status": "False",
|
||||
"message": "Certificate not issued",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
config := &common.CRDIncludeConfig{
|
||||
ReadyCondition: &common.CRDReadyCondition{
|
||||
Type: "Ready",
|
||||
ExpectedStatus: "True",
|
||||
},
|
||||
}
|
||||
|
||||
failures := analyzeWithConfig(item, config)
|
||||
if len(failures) != 1 {
|
||||
t.Fatalf("expected 1 failure, got %d", len(failures))
|
||||
}
|
||||
if !strings.Contains(failures[0].Text, "Ready condition not met") {
|
||||
t.Errorf("expected 'Ready condition not met' in failure text, got: %s", failures[0].Text)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAnalyzeWithConfig_ExpectedValueCheck tests custom status path value checking
|
||||
func TestAnalyzeWithConfig_ExpectedValueCheck(t *testing.T) {
|
||||
item := unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "argoproj.io/v1alpha1",
|
||||
"kind": "Application",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "my-app",
|
||||
"namespace": "argocd",
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"health": map[string]interface{}{
|
||||
"status": "Degraded",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
config := &common.CRDIncludeConfig{
|
||||
StatusPath: "status.health.status",
|
||||
ExpectedValue: "Healthy",
|
||||
}
|
||||
|
||||
failures := analyzeWithConfig(item, config)
|
||||
if len(failures) != 1 {
|
||||
t.Fatalf("expected 1 failure, got %d", len(failures))
|
||||
}
|
||||
if !strings.Contains(failures[0].Text, "Degraded") {
|
||||
t.Errorf("expected 'Degraded' in failure text, got: %s", failures[0].Text)
|
||||
}
|
||||
if !strings.Contains(failures[0].Text, "expected 'Healthy'") {
|
||||
t.Errorf("expected 'expected Healthy' in failure text, got: %s", failures[0].Text)
|
||||
}
|
||||
}
|
||||
|
||||
// TestShouldExcludeCRD tests exclusion logic
|
||||
func TestShouldExcludeCRD(t *testing.T) {
|
||||
excludeList := []common.CRDExcludeConfig{
|
||||
{Name: "kafkatopics.kafka.strimzi.io"},
|
||||
{Name: "prometheuses.monitoring.coreos.com"},
|
||||
}
|
||||
|
||||
if !shouldExcludeCRD("kafkatopics.kafka.strimzi.io", excludeList) {
|
||||
t.Error("expected kafkatopics to be excluded")
|
||||
}
|
||||
|
||||
if shouldExcludeCRD("certificates.cert-manager.io", excludeList) {
|
||||
t.Error("expected certificates not to be excluded")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetCRDConfig tests configuration retrieval
|
||||
func TestGetCRDConfig(t *testing.T) {
|
||||
includeList := []common.CRDIncludeConfig{
|
||||
{
|
||||
Name: "certificates.cert-manager.io",
|
||||
StatusPath: "status.conditions",
|
||||
ReadyCondition: &common.CRDReadyCondition{
|
||||
Type: "Ready",
|
||||
ExpectedStatus: "True",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
config := getCRDConfig("certificates.cert-manager.io", includeList)
|
||||
if config == nil {
|
||||
t.Fatal("expected config to be found")
|
||||
}
|
||||
if config.StatusPath != "status.conditions" {
|
||||
t.Errorf("expected StatusPath 'status.conditions', got %s", config.StatusPath)
|
||||
}
|
||||
|
||||
config = getCRDConfig("nonexistent.crd.io", includeList)
|
||||
if config != nil {
|
||||
t.Error("expected nil config for non-existent CRD")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAnalyzeGenericHealth_MultipleConditionTypes tests handling multiple condition types
|
||||
func TestAnalyzeGenericHealth_MultipleConditionTypes(t *testing.T) {
|
||||
item := unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "example.io/v1",
|
||||
"kind": "CustomResource",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "multi-cond",
|
||||
"namespace": "default",
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"conditions": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "Available",
|
||||
"status": "True",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"type": "Ready",
|
||||
"status": "False",
|
||||
"reason": "Pending",
|
||||
"message": "Waiting for dependencies",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
failures := analyzeGenericHealth(item)
|
||||
if len(failures) != 1 {
|
||||
t.Fatalf("expected 1 failure (Ready=False), got %d", len(failures))
|
||||
}
|
||||
if !strings.Contains(failures[0].Text, "Ready is False") {
|
||||
t.Errorf("expected 'Ready is False' in failure text, got: %s", failures[0].Text)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAnalyzeGenericHealth_NoStatusFields tests resource without any status fields
|
||||
func TestAnalyzeGenericHealth_NoStatusFields(t *testing.T) {
|
||||
item := unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "example.io/v1",
|
||||
"kind": "CustomResource",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "no-status",
|
||||
"namespace": "default",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
failures := analyzeGenericHealth(item)
|
||||
if len(failures) != 0 {
|
||||
t.Fatalf("expected 0 failures for resource without status, got %d", len(failures))
|
||||
}
|
||||
}
|
||||
|
||||
// TestCRDAnalyzer_NilClientConfig tests that the analyzer handles errors gracefully
|
||||
func TestCRDAnalyzer_NilClientConfig(t *testing.T) {
|
||||
viper.Reset()
|
||||
viper.Set("crd_analyzer", map[string]interface{}{
|
||||
"enabled": true,
|
||||
})
|
||||
|
||||
// Create a client with a config that will cause an error when trying to create apiextensions client
|
||||
a := common.Analyzer{
|
||||
Context: context.TODO(),
|
||||
Client: &kubernetes.Client{Config: &rest.Config{}},
|
||||
}
|
||||
|
||||
// The analyzer should handle the error gracefully without panicking
|
||||
results, err := (CRDAnalyzer{}).Analyze(a)
|
||||
|
||||
// We expect either an error or no results, but no panic
|
||||
if err != nil {
|
||||
// Error is expected in this case - that's fine
|
||||
if results != nil {
|
||||
t.Errorf("Expected nil results when error occurs, got %v", results)
|
||||
}
|
||||
}
|
||||
// The important thing is that we didn't panic
|
||||
}
|
||||
@@ -97,32 +97,6 @@ type Sensitive struct {
|
||||
Masked string
|
||||
}
|
||||
|
||||
// CRDAnalyzerConfig defines the configuration for the generic CRD analyzer
|
||||
type CRDAnalyzerConfig struct {
|
||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||
Include []CRDIncludeConfig `yaml:"include" json:"include"`
|
||||
Exclude []CRDExcludeConfig `yaml:"exclude" json:"exclude"`
|
||||
}
|
||||
|
||||
// CRDIncludeConfig defines configuration for a specific CRD to analyze
|
||||
type CRDIncludeConfig struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
StatusPath string `yaml:"statusPath" json:"statusPath"`
|
||||
ReadyCondition *CRDReadyCondition `yaml:"readyCondition" json:"readyCondition"`
|
||||
ExpectedValue string `yaml:"expectedValue" json:"expectedValue"`
|
||||
}
|
||||
|
||||
// CRDReadyCondition defines the expected ready condition
|
||||
type CRDReadyCondition struct {
|
||||
Type string `yaml:"type" json:"type"`
|
||||
ExpectedStatus string `yaml:"expectedStatus" json:"expectedStatus"`
|
||||
}
|
||||
|
||||
// CRDExcludeConfig defines a CRD to exclude from analysis
|
||||
type CRDExcludeConfig struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
}
|
||||
|
||||
type (
|
||||
SourceType string
|
||||
AvailabilityMode string
|
||||
|
||||
@@ -29,148 +29,247 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultListLimit is the default maximum number of resources to return
|
||||
DefaultListLimit = 100
|
||||
// MaxListLimit is the maximum allowed limit for list operations
|
||||
MaxListLimit = 1000
|
||||
)
|
||||
|
||||
// resourceLister defines a function that lists Kubernetes resources
|
||||
type resourceLister func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error)
|
||||
|
||||
// resourceGetter defines a function that gets a single Kubernetes resource
|
||||
type resourceGetter func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error)
|
||||
|
||||
// resourceRegistry maps resource types to their list and get functions
|
||||
var resourceRegistry = map[string]struct {
|
||||
list resourceLister
|
||||
get resourceGetter
|
||||
}{
|
||||
"pod": {
|
||||
list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) {
|
||||
return client.Client.CoreV1().Pods(namespace).List(ctx, opts)
|
||||
},
|
||||
get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) {
|
||||
return client.Client.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
},
|
||||
},
|
||||
"deployment": {
|
||||
list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) {
|
||||
return client.Client.AppsV1().Deployments(namespace).List(ctx, opts)
|
||||
},
|
||||
get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) {
|
||||
return client.Client.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
},
|
||||
},
|
||||
"service": {
|
||||
list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) {
|
||||
return client.Client.CoreV1().Services(namespace).List(ctx, opts)
|
||||
},
|
||||
get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) {
|
||||
return client.Client.CoreV1().Services(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
},
|
||||
},
|
||||
"node": {
|
||||
list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) {
|
||||
return client.Client.CoreV1().Nodes().List(ctx, opts)
|
||||
},
|
||||
get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) {
|
||||
return client.Client.CoreV1().Nodes().Get(ctx, name, metav1.GetOptions{})
|
||||
},
|
||||
},
|
||||
"job": {
|
||||
list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) {
|
||||
return client.Client.BatchV1().Jobs(namespace).List(ctx, opts)
|
||||
},
|
||||
get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) {
|
||||
return client.Client.BatchV1().Jobs(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
},
|
||||
},
|
||||
"cronjob": {
|
||||
list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) {
|
||||
return client.Client.BatchV1().CronJobs(namespace).List(ctx, opts)
|
||||
},
|
||||
get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) {
|
||||
return client.Client.BatchV1().CronJobs(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
},
|
||||
},
|
||||
"statefulset": {
|
||||
list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) {
|
||||
return client.Client.AppsV1().StatefulSets(namespace).List(ctx, opts)
|
||||
},
|
||||
get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) {
|
||||
return client.Client.AppsV1().StatefulSets(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
},
|
||||
},
|
||||
"daemonset": {
|
||||
list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) {
|
||||
return client.Client.AppsV1().DaemonSets(namespace).List(ctx, opts)
|
||||
},
|
||||
get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) {
|
||||
return client.Client.AppsV1().DaemonSets(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
},
|
||||
},
|
||||
"replicaset": {
|
||||
list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) {
|
||||
return client.Client.AppsV1().ReplicaSets(namespace).List(ctx, opts)
|
||||
},
|
||||
get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) {
|
||||
return client.Client.AppsV1().ReplicaSets(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
},
|
||||
},
|
||||
"configmap": {
|
||||
list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) {
|
||||
return client.Client.CoreV1().ConfigMaps(namespace).List(ctx, opts)
|
||||
},
|
||||
get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) {
|
||||
return client.Client.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
},
|
||||
},
|
||||
"secret": {
|
||||
list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) {
|
||||
return client.Client.CoreV1().Secrets(namespace).List(ctx, opts)
|
||||
},
|
||||
get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) {
|
||||
return client.Client.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
},
|
||||
},
|
||||
"ingress": {
|
||||
list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) {
|
||||
return client.Client.NetworkingV1().Ingresses(namespace).List(ctx, opts)
|
||||
},
|
||||
get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) {
|
||||
return client.Client.NetworkingV1().Ingresses(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
},
|
||||
},
|
||||
"persistentvolumeclaim": {
|
||||
list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) {
|
||||
return client.Client.CoreV1().PersistentVolumeClaims(namespace).List(ctx, opts)
|
||||
},
|
||||
get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) {
|
||||
return client.Client.CoreV1().PersistentVolumeClaims(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
},
|
||||
},
|
||||
"persistentvolume": {
|
||||
list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) {
|
||||
return client.Client.CoreV1().PersistentVolumes().List(ctx, opts)
|
||||
},
|
||||
get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) {
|
||||
return client.Client.CoreV1().PersistentVolumes().Get(ctx, name, metav1.GetOptions{})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Resource type aliases for convenience
|
||||
var resourceTypeAliases = map[string]string{
|
||||
"pods": "pod",
|
||||
"deployments": "deployment",
|
||||
"services": "service",
|
||||
"svc": "service",
|
||||
"nodes": "node",
|
||||
"jobs": "job",
|
||||
"cronjobs": "cronjob",
|
||||
"statefulsets": "statefulset",
|
||||
"sts": "statefulset",
|
||||
"daemonsets": "daemonset",
|
||||
"ds": "daemonset",
|
||||
"replicasets": "replicaset",
|
||||
"rs": "replicaset",
|
||||
"configmaps": "configmap",
|
||||
"cm": "configmap",
|
||||
"secrets": "secret",
|
||||
"ingresses": "ingress",
|
||||
"ing": "ingress",
|
||||
"persistentvolumeclaims": "persistentvolumeclaim",
|
||||
"pvc": "persistentvolumeclaim",
|
||||
"persistentvolumes": "persistentvolume",
|
||||
"pv": "persistentvolume",
|
||||
}
|
||||
|
||||
// normalizeResourceType converts resource type variants to canonical form
|
||||
func normalizeResourceType(resourceType string) (string, error) {
|
||||
normalized := strings.ToLower(resourceType)
|
||||
|
||||
// Check if it's an alias
|
||||
if canonical, ok := resourceTypeAliases[normalized]; ok {
|
||||
normalized = canonical
|
||||
}
|
||||
|
||||
// Check if it's a known resource type
|
||||
if _, ok := resourceRegistry[normalized]; !ok {
|
||||
return "", fmt.Errorf("unsupported resource type: %s", resourceType)
|
||||
}
|
||||
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
// marshalJSON marshals data to JSON with proper error handling
|
||||
func marshalJSON(data interface{}) (string, error) {
|
||||
jsonData, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal JSON: %w", err)
|
||||
}
|
||||
return string(jsonData), nil
|
||||
}
|
||||
|
||||
// handleListResources lists Kubernetes resources of a specific type
|
||||
func (s *K8sGptMCPServer) handleListResources(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
var req struct {
|
||||
ResourceType string `json:"resourceType"`
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
LabelSelector string `json:"labelSelector,omitempty"`
|
||||
Limit int64 `json:"limit,omitempty"`
|
||||
}
|
||||
if err := request.BindArguments(&req); err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to parse request arguments: %v", err), nil
|
||||
}
|
||||
|
||||
if req.ResourceType == "" {
|
||||
return mcp.NewToolResultErrorf("resourceType is required"), nil
|
||||
}
|
||||
|
||||
// Normalize and validate resource type
|
||||
resourceType, err := normalizeResourceType(req.ResourceType)
|
||||
if err != nil {
|
||||
supportedTypes := make([]string, 0, len(resourceRegistry))
|
||||
for key := range resourceRegistry {
|
||||
supportedTypes = append(supportedTypes, key)
|
||||
}
|
||||
return mcp.NewToolResultErrorf("%v. Supported types: %v", err, supportedTypes), nil
|
||||
}
|
||||
|
||||
// Set default and validate limit
|
||||
if req.Limit == 0 {
|
||||
req.Limit = DefaultListLimit
|
||||
} else if req.Limit > MaxListLimit {
|
||||
req.Limit = MaxListLimit
|
||||
}
|
||||
|
||||
client, err := kubernetes.NewClient("", "")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to create Kubernetes client: %v", err), nil
|
||||
}
|
||||
|
||||
listOptions := metav1.ListOptions{}
|
||||
if req.LabelSelector != "" {
|
||||
listOptions.LabelSelector = req.LabelSelector
|
||||
listOptions := metav1.ListOptions{
|
||||
LabelSelector: req.LabelSelector,
|
||||
Limit: req.Limit,
|
||||
}
|
||||
|
||||
var result string
|
||||
resourceType := strings.ToLower(req.ResourceType)
|
||||
|
||||
switch resourceType {
|
||||
case "pod", "pods":
|
||||
pods, err := client.Client.CoreV1().Pods(req.Namespace).List(ctx, listOptions)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to list pods: %v", err), nil
|
||||
}
|
||||
data, _ := json.MarshalIndent(pods.Items, "", " ")
|
||||
result = string(data)
|
||||
|
||||
case "deployment", "deployments":
|
||||
deps, err := client.Client.AppsV1().Deployments(req.Namespace).List(ctx, listOptions)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to list deployments: %v", err), nil
|
||||
}
|
||||
data, _ := json.MarshalIndent(deps.Items, "", " ")
|
||||
result = string(data)
|
||||
|
||||
case "service", "services", "svc":
|
||||
svcs, err := client.Client.CoreV1().Services(req.Namespace).List(ctx, listOptions)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to list services: %v", err), nil
|
||||
}
|
||||
data, _ := json.MarshalIndent(svcs.Items, "", " ")
|
||||
result = string(data)
|
||||
|
||||
case "node", "nodes":
|
||||
nodes, err := client.Client.CoreV1().Nodes().List(ctx, listOptions)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to list nodes: %v", err), nil
|
||||
}
|
||||
data, _ := json.MarshalIndent(nodes.Items, "", " ")
|
||||
result = string(data)
|
||||
|
||||
case "job", "jobs":
|
||||
jobs, err := client.Client.BatchV1().Jobs(req.Namespace).List(ctx, listOptions)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to list jobs: %v", err), nil
|
||||
}
|
||||
data, _ := json.MarshalIndent(jobs.Items, "", " ")
|
||||
result = string(data)
|
||||
|
||||
case "cronjob", "cronjobs":
|
||||
cronjobs, err := client.Client.BatchV1().CronJobs(req.Namespace).List(ctx, listOptions)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to list cronjobs: %v", err), nil
|
||||
}
|
||||
data, _ := json.MarshalIndent(cronjobs.Items, "", " ")
|
||||
result = string(data)
|
||||
|
||||
case "statefulset", "statefulsets", "sts":
|
||||
sts, err := client.Client.AppsV1().StatefulSets(req.Namespace).List(ctx, listOptions)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to list statefulsets: %v", err), nil
|
||||
}
|
||||
data, _ := json.MarshalIndent(sts.Items, "", " ")
|
||||
result = string(data)
|
||||
|
||||
case "daemonset", "daemonsets", "ds":
|
||||
ds, err := client.Client.AppsV1().DaemonSets(req.Namespace).List(ctx, listOptions)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to list daemonsets: %v", err), nil
|
||||
}
|
||||
data, _ := json.MarshalIndent(ds.Items, "", " ")
|
||||
result = string(data)
|
||||
|
||||
case "replicaset", "replicasets", "rs":
|
||||
rs, err := client.Client.AppsV1().ReplicaSets(req.Namespace).List(ctx, listOptions)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to list replicasets: %v", err), nil
|
||||
}
|
||||
data, _ := json.MarshalIndent(rs.Items, "", " ")
|
||||
result = string(data)
|
||||
|
||||
case "configmap", "configmaps", "cm":
|
||||
cms, err := client.Client.CoreV1().ConfigMaps(req.Namespace).List(ctx, listOptions)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to list configmaps: %v", err), nil
|
||||
}
|
||||
data, _ := json.MarshalIndent(cms.Items, "", " ")
|
||||
result = string(data)
|
||||
|
||||
case "secret", "secrets":
|
||||
secrets, err := client.Client.CoreV1().Secrets(req.Namespace).List(ctx, listOptions)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to list secrets: %v", err), nil
|
||||
}
|
||||
data, _ := json.MarshalIndent(secrets.Items, "", " ")
|
||||
result = string(data)
|
||||
|
||||
case "ingress", "ingresses", "ing":
|
||||
ingresses, err := client.Client.NetworkingV1().Ingresses(req.Namespace).List(ctx, listOptions)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to list ingresses: %v", err), nil
|
||||
}
|
||||
data, _ := json.MarshalIndent(ingresses.Items, "", " ")
|
||||
result = string(data)
|
||||
|
||||
case "persistentvolumeclaim", "persistentvolumeclaims", "pvc":
|
||||
pvcs, err := client.Client.CoreV1().PersistentVolumeClaims(req.Namespace).List(ctx, listOptions)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to list PVCs: %v", err), nil
|
||||
}
|
||||
data, _ := json.MarshalIndent(pvcs.Items, "", " ")
|
||||
result = string(data)
|
||||
|
||||
case "persistentvolume", "persistentvolumes", "pv":
|
||||
pvs, err := client.Client.CoreV1().PersistentVolumes().List(ctx, listOptions)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to list PVs: %v", err), nil
|
||||
}
|
||||
data, _ := json.MarshalIndent(pvs.Items, "", " ")
|
||||
result = string(data)
|
||||
|
||||
default:
|
||||
return mcp.NewToolResultErrorf("Unsupported resource type: %s. Supported types: pods, deployments, services, nodes, jobs, cronjobs, statefulsets, daemonsets, replicasets, configmaps, secrets, ingresses, pvc, pv", resourceType), nil
|
||||
// Get the list function from registry
|
||||
listFunc := resourceRegistry[resourceType].list
|
||||
result, err := listFunc(ctx, client, req.Namespace, listOptions)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to list %s: %v", resourceType, err), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(result), nil
|
||||
// Extract items from the result (all list types have an Items field)
|
||||
resultJSON, err := marshalJSON(result)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to serialize result: %v", err), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(resultJSON), nil
|
||||
}
|
||||
|
||||
// handleGetResource gets detailed information about a specific resource
|
||||
@@ -184,52 +283,37 @@ func (s *K8sGptMCPServer) handleGetResource(ctx context.Context, request mcp.Cal
|
||||
return mcp.NewToolResultErrorf("Failed to parse request arguments: %v", err), nil
|
||||
}
|
||||
|
||||
if req.ResourceType == "" {
|
||||
return mcp.NewToolResultErrorf("resourceType is required"), nil
|
||||
}
|
||||
if req.Name == "" {
|
||||
return mcp.NewToolResultErrorf("name is required"), nil
|
||||
}
|
||||
|
||||
// Normalize and validate resource type
|
||||
resourceType, err := normalizeResourceType(req.ResourceType)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("%v", err), nil
|
||||
}
|
||||
|
||||
client, err := kubernetes.NewClient("", "")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to create Kubernetes client: %v", err), nil
|
||||
}
|
||||
|
||||
var result string
|
||||
resourceType := strings.ToLower(req.ResourceType)
|
||||
|
||||
switch resourceType {
|
||||
case "pod", "pods":
|
||||
pod, err := client.Client.CoreV1().Pods(req.Namespace).Get(ctx, req.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to get pod: %v", err), nil
|
||||
}
|
||||
data, _ := json.MarshalIndent(pod, "", " ")
|
||||
result = string(data)
|
||||
|
||||
case "deployment", "deployments":
|
||||
dep, err := client.Client.AppsV1().Deployments(req.Namespace).Get(ctx, req.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to get deployment: %v", err), nil
|
||||
}
|
||||
data, _ := json.MarshalIndent(dep, "", " ")
|
||||
result = string(data)
|
||||
|
||||
case "service", "services", "svc":
|
||||
svc, err := client.Client.CoreV1().Services(req.Namespace).Get(ctx, req.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to get service: %v", err), nil
|
||||
}
|
||||
data, _ := json.MarshalIndent(svc, "", " ")
|
||||
result = string(data)
|
||||
|
||||
case "node", "nodes":
|
||||
node, err := client.Client.CoreV1().Nodes().Get(ctx, req.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to get node: %v", err), nil
|
||||
}
|
||||
data, _ := json.MarshalIndent(node, "", " ")
|
||||
result = string(data)
|
||||
|
||||
default:
|
||||
return mcp.NewToolResultErrorf("Unsupported resource type: %s", resourceType), nil
|
||||
// Get the get function from registry
|
||||
getFunc := resourceRegistry[resourceType].get
|
||||
result, err := getFunc(ctx, client, req.Namespace, req.Name)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to get %s '%s': %v", resourceType, req.Name, err), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(result), nil
|
||||
resultJSON, err := marshalJSON(result)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to serialize result: %v", err), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(resultJSON), nil
|
||||
}
|
||||
|
||||
// handleListNamespaces lists all namespaces in the cluster
|
||||
@@ -244,24 +328,30 @@ func (s *K8sGptMCPServer) handleListNamespaces(ctx context.Context, request mcp.
|
||||
return mcp.NewToolResultErrorf("Failed to list namespaces: %v", err), nil
|
||||
}
|
||||
|
||||
data, _ := json.MarshalIndent(namespaces.Items, "", " ")
|
||||
return mcp.NewToolResultText(string(data)), nil
|
||||
resultJSON, err := marshalJSON(namespaces.Items)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to serialize result: %v", err), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(resultJSON), nil
|
||||
}
|
||||
|
||||
// handleListEvents lists Kubernetes events
|
||||
func (s *K8sGptMCPServer) handleListEvents(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
var req struct {
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
InvolvedObjectName string `json:"involvedObjectName,omitempty"`
|
||||
InvolvedObjectKind string `json:"involvedObjectKind,omitempty"`
|
||||
Limit int64 `json:"limit,omitempty"`
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
InvolvedObjectName string `json:"involvedObjectName,omitempty"`
|
||||
InvolvedObjectKind string `json:"involvedObjectKind,omitempty"`
|
||||
Limit int64 `json:"limit,omitempty"`
|
||||
}
|
||||
if err := request.BindArguments(&req); err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to parse request arguments: %v", err), nil
|
||||
}
|
||||
|
||||
if req.Limit == 0 {
|
||||
req.Limit = 100
|
||||
req.Limit = DefaultListLimit
|
||||
} else if req.Limit > MaxListLimit {
|
||||
req.Limit = MaxListLimit
|
||||
}
|
||||
|
||||
client, err := kubernetes.NewClient("", "")
|
||||
@@ -290,8 +380,12 @@ func (s *K8sGptMCPServer) handleListEvents(ctx context.Context, request mcp.Call
|
||||
filteredEvents = append(filteredEvents, event)
|
||||
}
|
||||
|
||||
data, _ := json.MarshalIndent(filteredEvents, "", " ")
|
||||
return mcp.NewToolResultText(string(data)), nil
|
||||
resultJSON, err := marshalJSON(filteredEvents)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to serialize result: %v", err), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(resultJSON), nil
|
||||
}
|
||||
|
||||
// handleGetLogs retrieves logs from a pod container
|
||||
@@ -308,6 +402,13 @@ func (s *K8sGptMCPServer) handleGetLogs(ctx context.Context, request mcp.CallToo
|
||||
return mcp.NewToolResultErrorf("Failed to parse request arguments: %v", err), nil
|
||||
}
|
||||
|
||||
if req.PodName == "" {
|
||||
return mcp.NewToolResultErrorf("podName is required"), nil
|
||||
}
|
||||
if req.Namespace == "" {
|
||||
return mcp.NewToolResultErrorf("namespace is required"), nil
|
||||
}
|
||||
|
||||
if req.TailLines == 0 {
|
||||
req.TailLines = 100
|
||||
}
|
||||
@@ -356,8 +457,12 @@ func (s *K8sGptMCPServer) handleListFilters(ctx context.Context, request mcp.Cal
|
||||
"activeFilters": active,
|
||||
}
|
||||
|
||||
data, _ := json.MarshalIndent(result, "", " ")
|
||||
return mcp.NewToolResultText(string(data)), nil
|
||||
resultJSON, err := marshalJSON(result)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to serialize result: %v", err), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(resultJSON), nil
|
||||
}
|
||||
|
||||
// handleAddFilters adds filters to enable specific analyzers
|
||||
@@ -369,10 +474,17 @@ func (s *K8sGptMCPServer) handleAddFilters(ctx context.Context, request mcp.Call
|
||||
return mcp.NewToolResultErrorf("Failed to parse request arguments: %v", err), nil
|
||||
}
|
||||
|
||||
if len(req.Filters) == 0 {
|
||||
return mcp.NewToolResultErrorf("filters array is required and cannot be empty"), nil
|
||||
}
|
||||
|
||||
activeFilters := viper.GetStringSlice("active_filters")
|
||||
addedFilters := []string{}
|
||||
|
||||
for _, filter := range req.Filters {
|
||||
if !contains(activeFilters, filter) {
|
||||
activeFilters = append(activeFilters, filter)
|
||||
addedFilters = append(addedFilters, filter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,7 +493,11 @@ func (s *K8sGptMCPServer) handleAddFilters(ctx context.Context, request mcp.Call
|
||||
return mcp.NewToolResultErrorf("Failed to save configuration: %v", err), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Successfully added filters: %v", req.Filters)), nil
|
||||
if len(addedFilters) == 0 {
|
||||
return mcp.NewToolResultText("All specified filters were already active"), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Successfully added filters: %v", addedFilters)), nil
|
||||
}
|
||||
|
||||
// handleRemoveFilters removes filters to disable specific analyzers
|
||||
@@ -393,11 +509,19 @@ func (s *K8sGptMCPServer) handleRemoveFilters(ctx context.Context, request mcp.C
|
||||
return mcp.NewToolResultErrorf("Failed to parse request arguments: %v", err), nil
|
||||
}
|
||||
|
||||
if len(req.Filters) == 0 {
|
||||
return mcp.NewToolResultErrorf("filters array is required and cannot be empty"), nil
|
||||
}
|
||||
|
||||
activeFilters := viper.GetStringSlice("active_filters")
|
||||
newFilters := []string{}
|
||||
removedFilters := []string{}
|
||||
|
||||
for _, filter := range activeFilters {
|
||||
if !contains(req.Filters, filter) {
|
||||
newFilters = append(newFilters, filter)
|
||||
} else {
|
||||
removedFilters = append(removedFilters, filter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,7 +530,11 @@ func (s *K8sGptMCPServer) handleRemoveFilters(ctx context.Context, request mcp.C
|
||||
return mcp.NewToolResultErrorf("Failed to save configuration: %v", err), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Successfully removed filters: %v", req.Filters)), nil
|
||||
if len(removedFilters) == 0 {
|
||||
return mcp.NewToolResultText("None of the specified filters were active"), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Successfully removed filters: %v", removedFilters)), nil
|
||||
}
|
||||
|
||||
// handleListIntegrations lists available integrations
|
||||
@@ -423,8 +551,12 @@ func (s *K8sGptMCPServer) handleListIntegrations(ctx context.Context, request mc
|
||||
})
|
||||
}
|
||||
|
||||
data, _ := json.MarshalIndent(result, "", " ")
|
||||
return mcp.NewToolResultText(string(data)), nil
|
||||
resultJSON, err := marshalJSON(result)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorf("Failed to serialize result: %v", err), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(resultJSON), nil
|
||||
}
|
||||
|
||||
// contains checks if a string slice contains a specific string
|
||||
|
||||
Reference in New Issue
Block a user