Compare commits

..

4 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
6c7b7c4751 Address code review feedback
- Fixed analyzer name in README.md to use consistent 'CustomResource' name
- Improved TestCRDAnalyzer_NilClientConfig to be more explicit about expected behavior
- Added clear comments about what the test verifies

Co-authored-by: AlexsJones <1235925+AlexsJones@users.noreply.github.com>
2026-02-06 07:32:08 +00:00
copilot-swe-agent[bot]
03bd9a8387 Add CRD analyzer documentation and examples
- Created comprehensive documentation in docs/CRD_ANALYZER.md
- Added example configuration file in examples/crd_analyzer_config.yaml
- Updated README.md to list the new customResourceAnalyzer
- Documentation includes use cases for cert-manager, ArgoCD, Kafka, Prometheus
- Added troubleshooting section and best practices

Co-authored-by: AlexsJones <1235925+AlexsJones@users.noreply.github.com>
2026-02-06 07:27:34 +00:00
copilot-swe-agent[bot]
dfdcf9edd2 Add Generic CRD Analyzer implementation with tests
- Added CRDAnalyzerConfig types to pkg/common/types.go for configuration
- Implemented CRD analyzer in pkg/analyzer/crd.go with support for:
  - Discovery of all installed CRDs via apiextensions API
  - Generic health checks based on common patterns (.status.conditions, .status.phase, etc.)
  - Configurable per-CRD health checks via YAML config
  - Detection of stuck resources (deletionTimestamp with finalizers)
- Registered CRDAnalyzer in additionalAnalyzerMap
- Added comprehensive unit tests in pkg/analyzer/crd_test.go
- All tests passing

Co-authored-by: AlexsJones <1235925+AlexsJones@users.noreply.github.com>
2026-02-06 07:25:29 +00:00
copilot-swe-agent[bot]
2253625f40 Initial plan 2026-02-06 07:15:24 +00:00
25 changed files with 1357 additions and 970 deletions

View File

@@ -14,7 +14,7 @@ on:
- "**.md"
env:
GO_VERSION: "~1.24"
GO_VERSION: "~1.23"
IMAGE_NAME: "k8sgpt"
REGISTRY_IMAGE: ghcr.io/k8sgpt-ai/k8sgpt
@@ -40,7 +40,7 @@ jobs:
- name: Extract branch name
id: extract_branch
uses: keptn/gh-action-extract-branch-name@6ca4fe061da10c66b2d7341fd1fb12962ad911b2 # main
uses: keptn/gh-action-extract-branch-name@main
- name: Get current date and time
id: get_datetime

View File

@@ -41,7 +41,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # main
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
@@ -61,7 +61,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: '~1.24'
go-version: '1.22'
- name: Download Syft
uses: anchore/sbom-action/download-syft@55dc4ee22412511ee8c3142cbea40418e6cec693 # v0.17.8
- name: Run GoReleaser

View File

@@ -9,7 +9,7 @@ on:
- main
env:
GO_VERSION: "~1.24"
GO_VERSION: "~1.22"
jobs:
build:

View File

@@ -1 +1 @@
{".":"0.4.31"}
{".":"0.4.27"}

View File

@@ -1,69 +1,5 @@
# Changelog
## [0.4.31](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.30...v0.4.31) (2026-03-24)
### Features
* support amazonbedrock converse api ([#1627](https://github.com/k8sgpt-ai/k8sgpt/issues/1627)) ([fc6a83d](https://github.com/k8sgpt-ai/k8sgpt/commit/fc6a83d063e69293f4e3aa18bd887740401c8fe0))
### Other
* updated readme ([#1620](https://github.com/k8sgpt-ai/k8sgpt/issues/1620)) ([fd5bba6](https://github.com/k8sgpt-ai/k8sgpt/commit/fd5bba6ab3ad7a81ef982f1980ac9c9de23bc46c))
### Docs
* align Go version with go.mod toolchain ([#1609](https://github.com/k8sgpt-ai/k8sgpt/issues/1609)) ([19a172e](https://github.com/k8sgpt-ai/k8sgpt/commit/19a172e575ffba6cd89330479033731426358342))
## [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)

View File

@@ -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.24+`
- Golang `1.23`
- 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`

View File

@@ -21,10 +21,6 @@ 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&#0032;gives&#0032;Kubernetes&#0032;Superpowers&#0032;to&#0032;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="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
@@ -67,7 +63,7 @@ brew install k8sgpt
<!---x-release-please-start-version-->
```
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.31/k8sgpt_386.rpm
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.27/k8sgpt_386.rpm
```
<!---x-release-please-end-->
@@ -75,7 +71,7 @@ brew install k8sgpt
<!---x-release-please-start-version-->
```
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.31/k8sgpt_amd64.rpm
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.27/k8sgpt_amd64.rpm
```
<!---x-release-please-end-->
</details>
@@ -88,7 +84,7 @@ brew install k8sgpt
<!---x-release-please-start-version-->
```
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.31/k8sgpt_386.deb
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.27/k8sgpt_386.deb
sudo dpkg -i k8sgpt_386.deb
```
@@ -99,7 +95,7 @@ sudo dpkg -i k8sgpt_386.deb
<!---x-release-please-start-version-->
```
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.31/k8sgpt_amd64.deb
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.27/k8sgpt_amd64.deb
sudo dpkg -i k8sgpt_amd64.deb
```
@@ -114,7 +110,7 @@ sudo dpkg -i k8sgpt_amd64.deb
<!---x-release-please-start-version-->
```
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.31/k8sgpt_386.apk
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.27/k8sgpt_386.apk
apk add --allow-untrusted k8sgpt_386.apk
```
<!---x-release-please-end-->
@@ -123,7 +119,7 @@ sudo dpkg -i k8sgpt_amd64.deb
<!---x-release-please-start-version-->
```
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.31/k8sgpt_amd64.apk
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.27/k8sgpt_amd64.apk
apk add --allow-untrusted k8sgpt_amd64.apk
```
<!---x-release-please-end-->
@@ -283,6 +279,7 @@ 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
@@ -500,21 +497,6 @@ k8sgpt auth default -p azureopenai
Default provider set to azureopenai
```
_Using Amazon Bedrock Converse with inference profiles_
_System Inference Profile_
```
k8sgpt auth add --backend amazonbedrockconverse --providerRegion us-east-1 --model arn:aws:bedrock:us-east-1:123456789012:inference-profile/my-inference-profile
```
_Application Inference Profile_
```
k8sgpt auth add --backend amazonbedrockconverse --providerRegion us-east-1 --model arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/2uzp4s0w39t6
```
_Using Amazon Bedrock with inference profiles_
_System Inference Profile_

View File

@@ -24,9 +24,6 @@ K8sGPT supports a variety of AI/LLM providers (backends). Some providers have a
### Cohere
- **Model:** User-configurable (any model supported by Cohere)
### Amazon Bedrock Converse
- **Model:** User-configurable (any model supported by [Amazon Bedrock Converse](https://docs.aws.amazon.com/bedrock/latest/userguide/models-api-compatibility.html))
### Amazon Bedrock
- **Supported Models:**
- anthropic.claude-sonnet-4-20250514-v1:0
@@ -83,4 +80,4 @@ K8sGPT supports a variety of AI/LLM providers (backends). Some providers have a
---
For more details on configuring each provider and model, refer to the official K8sGPT documentation and the provider's own documentation.
For more details on configuring each provider and model, refer to the official K8sGPT documentation and the provider's own documentation.

View File

@@ -48,9 +48,6 @@ var addCmd = &cobra.Command{
if strings.ToLower(backend) == "amazonbedrock" {
_ = cmd.MarkFlagRequired("providerRegion")
}
if strings.ToLower(backend) == "amazonbedrockconverse" {
_ = cmd.MarkFlagRequired("providerRegion")
}
if strings.ToLower(backend) == "ibmwatsonxai" {
_ = cmd.MarkFlagRequired("providerId")
}
@@ -143,7 +140,6 @@ var addCmd = &cobra.Command{
TopP: topP,
TopK: topK,
MaxTokens: maxTokens,
StopSequences: stopSequences,
OrganizationId: organizationId,
}
@@ -177,14 +173,12 @@ func init() {
addCmd.Flags().Int32VarP(&topK, "topk", "c", 50, "Sampling Cutoff: Set a threshold (1-100) to restrict the sampling process to the top K most probable words at each step. Higher values lead to greater variability, lower values increases predictability.")
// max tokens
addCmd.Flags().IntVarP(&maxTokens, "maxtokens", "l", 2048, "Specify a maximum output length. Adjust (1-...) to control text length. Higher values produce longer output, lower values limit length")
// stop sequences
addCmd.Flags().StringSliceVarP(&stopSequences, "stopsequences", "s", []string{}, "Stop Sequences: Define specific tokens or phrases that signal the model to stop generating text.")
// add flag for temperature
addCmd.Flags().Float32VarP(&temperature, "temperature", "t", 0.7, "The sampling temperature, value ranges between 0 ( output be more deterministic) and 1 (more random)")
// add flag for azure open ai engine/deployment name
addCmd.Flags().StringVarP(&engine, "engine", "e", "", "Azure AI deployment name (only for azureopenai backend)")
//add flag for amazonbedrock region name
addCmd.Flags().StringVarP(&providerRegion, "providerRegion", "r", "", "Provider Region name (only for amazonbedrock, amazonbedrockconverse, googlevertexai backend)")
addCmd.Flags().StringVarP(&providerRegion, "providerRegion", "r", "", "Provider Region name (only for amazonbedrock, googlevertexai backend)")
//add flag for vertexAI/WatsonxAI Project ID
addCmd.Flags().StringVarP(&providerId, "providerId", "i", "", "Provider specific ID for e.g. project (only for googlevertexai/ibmwatsonxai backend)")
//add flag for OCI Compartment ID

View File

@@ -32,7 +32,6 @@ var (
topP float32
topK int32
maxTokens int
stopSequences []string
organizationId string
)

View File

@@ -203,11 +203,6 @@ 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,
@@ -239,7 +234,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", "m", "8081", "Port to run the metrics-server on (env: K8SGPT_METRICS_PORT)")
ServeCmd.Flags().StringVarP(&metricsPort, "metrics-port", "", "8081", "Port to run the metrics-server on")
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")

252
docs/CRD_ANALYZER.md Normal file
View File

@@ -0,0 +1,252 @@
# 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

View File

@@ -0,0 +1,45 @@
# 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

59
go.mod
View File

@@ -14,8 +14,8 @@ require (
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.11.1
golang.org/x/term v0.38.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
@@ -65,13 +65,13 @@ 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.25.1 // indirect
cel.dev/expr v0.23.0 // indirect
cloud.google.com/go v0.120.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/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // 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
@@ -79,7 +79,7 @@ require (
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.30.0 // 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
@@ -98,7 +98,7 @@ require (
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-20251210132809-ee656c7534f5 // indirect
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect
github.com/containerd/console v1.0.4 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/log v0.1.0 // indirect
@@ -106,13 +106,13 @@ require (
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.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // 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/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.1.3 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // 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
@@ -139,23 +139,24 @@ require (
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.6.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.5.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
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // 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.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.39.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
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-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // 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
gopkg.in/yaml.v3 v3.0.1 // indirect
knative.dev/pkg v0.0.0-20241026180704-25f6002b00f3 // indirect
@@ -189,7 +190,7 @@ require (
github.com/fsnotify/fsnotify v1.8.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.3 // indirect
github.com/go-logr/logr v1.4.2 // 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
@@ -259,20 +260,20 @@ require (
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.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
golang.org/x/net v0.48.0
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // 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.79.3
google.golang.org/protobuf v1.36.10 // indirect
google.golang.org/grpc v1.73.0
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
k8s.io/apiextensions-apiserver v0.32.2
@@ -281,7 +282,7 @@ require (
k8s.io/component-base v0.32.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-20260210185600-b8788abfbbc2
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

136
go.sum
View File

@@ -16,8 +16,8 @@ buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.5.1-20241118152629-1379a5a1889d.1 h
buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.5.1-20241118152629-1379a5a1889d.1/go.mod h1:rlbkTkVN2P3aNR0U/7N5d9/uvNW8/dzHwtJDfPzh2vc=
buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.35.2-20241118152629-1379a5a1889d.1 h1:Z+fW0kWryP6LdjP+z+d1/WT4tObrq890aye4aPIh6hM=
buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.35.2-20241118152629-1379a5a1889d.1/go.mod h1:dqopmdpTDT6p9kPTxVCgR8WDnNb1SjZjwzaNj/kRbps=
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cel.dev/expr v0.23.0 h1:wUb94w6OYQS4uXraxo9U+wUAs9jT47Xvl4iPgAwM2ss=
cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@@ -205,8 +205,8 @@ cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZ
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY=
cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck=
cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w=
@@ -674,8 +674,8 @@ github.com/Code-Hex/go-generics-cache v1.5.1 h1:6vhZGc5M7Y/YD8cIUcY8kcuQLB4cHR7U
github.com/Code-Hex/go-generics-cache v1.5.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 h1:5IT7xOdq17MtcdtL/vtl6mGfzhaq4m4vpollPRmlsBQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0/go.mod h1:ZV4VOm0/eHR06JLrXWe09068dHpr3TRpY9Uo7T+anuA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.50.0 h1:nNMpRpnkWDAaqcpxMJvxa/Ud98gjbYwayJY4/9bdjiU=
@@ -817,8 +817,8 @@ github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k=
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cohere-ai/cohere-go/v2 v2.12.2 h1:8WJqqcCe3q6TB1CdhgzJOgRO2ouno8xcYcOoeWtI8Pk=
github.com/cohere-ai/cohere-go/v2 v2.12.2/go.mod h1:MuiJkCxlR18BDV2qQPbz2Yb/OCVphT1y6nD2zYaKeR0=
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
@@ -887,17 +887,17 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls=
github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
@@ -934,16 +934,16 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
@@ -1399,8 +1399,8 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmiUq4=
github.com/rubenv/sql-migrate v1.7.1/go.mod h1:Ob2Psprc0/3ggbM6wCzyYVFFuc6FyZrb2AS+ezLDFb4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@@ -1447,8 +1447,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
github.com/stackitcloud/stackit-sdk-go/core v0.17.2 h1:jPyn+i8rkp2hM80+hOg0B/1EVRbMt778Tr5RWyK1m2E=
github.com/stackitcloud/stackit-sdk-go/core v0.17.2/go.mod h1:8KIw3czdNJ9sdil9QQimxjR6vHjeINFrRv0iZ67wfn0=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
@@ -1469,8 +1469,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs=
@@ -1507,6 +1507,8 @@ github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPS
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY=
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
@@ -1517,26 +1519,26 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA=
go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
@@ -1557,8 +1559,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1617,8 +1619,8 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1677,8 +1679,8 @@ golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1707,8 +1709,8 @@ golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri
golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec=
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1725,8 +1727,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1811,8 +1813,8 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1821,8 +1823,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1839,8 +1841,8 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1912,8 +1914,8 @@ golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1928,8 +1930,6 @@ gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJ
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=
@@ -2137,12 +2137,12 @@ google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRx
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.mod h1:ts19tUU+Z0ZShN1y3aPyq2+O3d5FUNNgT6FtOzmrNn8=
google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -2182,8 +2182,8 @@ google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCD
google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww=
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -2202,8 +2202,8 @@ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -2260,8 +2260,8 @@ k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJ
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
k8s.io/kubectl v0.32.2 h1:TAkag6+XfSBgkqK9I7ZvwtF0WVtUAvK8ZqTt+5zi1Us=
k8s.io/kubectl v0.32.2/go.mod h1:+h/NQFSPxiDZYX/WZaWw9fwYezGLISP0ud8nQKg+3g8=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
knative.dev/pkg v0.0.0-20241026180704-25f6002b00f3 h1:uUSDGlOIkdPT4svjlhi+JEnP2Ufw7AM/F5QDYiEL02U=
knative.dev/pkg v0.0.0-20241026180704-25f6002b00f3/go.mod h1:FeMbTLlxQqSASwlRCrYEOsZ0OKUgSj52qxhECwYCJsw=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=

View File

@@ -1,161 +0,0 @@
package ai
import (
"context"
"errors"
"fmt"
"github.com/aws/aws-sdk-go-v2/aws"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
"github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types"
"os"
"strings"
)
const amazonBedrockConverseClientName = "amazonbedrockconverse"
type bedrockConverseAPI interface {
Converse(ctx context.Context, input *bedrockruntime.ConverseInput, optFns ...func(*bedrockruntime.Options)) (*bedrockruntime.ConverseOutput, error)
}
type AmazonBedrockConverseClient struct {
nopCloser
client bedrockConverseAPI
model string
temperature float32
topP float32
maxTokens int
stopSequences []string
}
func getRegion(region string) string {
if os.Getenv("AWS_DEFAULT_REGION") != "" {
region = os.Getenv("AWS_DEFAULT_REGION")
}
// Return the supplied provider region if not overridden by environment variable
return region
}
func (a *AmazonBedrockConverseClient) getModelFromString(model string) (string, error) {
if model == "" {
return "", errors.New("model name cannot be empty")
}
model = strings.TrimSpace(model)
return model, nil
}
func processError(err error, modelId string) error {
errMsg := err.Error()
if strings.Contains(errMsg, "no such host") {
return fmt.Errorf(`the bedrock service is not available in the selected region.
please double-check the service availability for your region at
https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/`)
} else if strings.Contains(errMsg, "Could not resolve the foundation model") {
return fmt.Errorf(`could not resolve the foundation model from model identifier: \"%s\".
please verify that the requested model exists and is accessible
within the specified region`, modelId)
} else {
return fmt.Errorf("could not invoke model: \"%s\". here is why: %s", modelId, err)
}
}
func (a *AmazonBedrockConverseClient) Configure(config IAIConfig) error {
modelInput := config.GetModel()
var region = getRegion(config.GetProviderRegion())
// Only create AWS clients if they haven't been injected (for testing)
if a.client == nil {
cfg, err := awsconfig.LoadDefaultConfig(context.Background(),
awsconfig.WithRegion(region),
)
if err != nil {
if strings.Contains(err.Error(), "InvalidAccessKeyId") || strings.Contains(err.Error(), "SignatureDoesNotMatch") || strings.Contains(err.Error(), "NoCredentialProviders") {
return fmt.Errorf("aws credentials are invalid or missing. Please check your environment variables or aws config. details: %v", err)
}
return fmt.Errorf("failed to load aws config for region %s: %w", region, err)
}
a.client = bedrockruntime.NewFromConfig(cfg)
}
foundModel, err := a.getModelFromString(modelInput)
if err != nil {
return fmt.Errorf("failed to find model configuration for %s: %w", modelInput, err)
}
a.model = foundModel
// Set common configuration parameters
a.temperature = config.GetTemperature()
a.topP = config.GetTopP()
a.maxTokens = config.GetMaxTokens()
a.stopSequences = config.GetStopSequences()
return nil
}
func extractTextFromConverseOutput(output types.ConverseOutput, modelId string) (string, error) {
if output == nil {
return "", fmt.Errorf("empty response from model: %s", modelId)
}
msg, ok := output.(*types.ConverseOutputMemberMessage)
if !ok {
return "", fmt.Errorf("unexpected response type from model: %s", modelId)
}
if len(msg.Value.Content) == 0 {
return "", fmt.Errorf("no content returned from model: %s", modelId)
}
var builder strings.Builder
for _, block := range msg.Value.Content {
if textBlock, ok := block.(*types.ContentBlockMemberText); ok && textBlock != nil {
builder.WriteString(textBlock.Value)
}
}
if builder.Len() == 0 {
return "", fmt.Errorf("no text content returned from model: %s", modelId)
}
return builder.String(), nil
}
func (a *AmazonBedrockConverseClient) GetCompletion(ctx context.Context, prompt string) (string, error) {
var content = types.ContentBlockMemberText{
Value: prompt,
}
var message = types.Message{
Content: []types.ContentBlock{&content},
Role: "user",
}
var converseInput = bedrockruntime.ConverseInput{
ModelId: aws.String(a.model),
Messages: []types.Message{message},
InferenceConfig: &types.InferenceConfiguration{
Temperature: aws.Float32(a.temperature),
TopP: aws.Float32(a.topP),
MaxTokens: aws.Int32(int32(a.maxTokens)),
StopSequences: a.stopSequences,
},
}
response, err := a.client.Converse(ctx, &converseInput)
if err != nil {
return "", processError(err, a.model)
}
text, err := extractTextFromConverseOutput(response.Output, a.model)
if err != nil {
return "", err
}
return text, nil
}
func (a *AmazonBedrockConverseClient) GetName() string {
return amazonBedrockConverseClientName
}

View File

@@ -1,250 +0,0 @@
package ai
import (
"context"
"errors"
"github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
"github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types"
"github.com/stretchr/testify/assert"
"testing"
)
// ---- Mock Wrapper ----
type mockConverseClient struct {
converseFunc func(ctx context.Context, input *bedrockruntime.ConverseInput) (*bedrockruntime.ConverseOutput, error)
}
func (m *mockConverseClient) Converse(ctx context.Context, input *bedrockruntime.ConverseInput, _ ...func(*bedrockruntime.Options)) (*bedrockruntime.ConverseOutput, error) {
return m.converseFunc(ctx, input)
}
// ---- Tests ----
func TestGetCompletion_Success(t *testing.T) {
mock := &mockConverseClient{
converseFunc: func(ctx context.Context, input *bedrockruntime.ConverseInput) (*bedrockruntime.ConverseOutput, error) {
return &bedrockruntime.ConverseOutput{
Output: &types.ConverseOutputMemberMessage{
Value: types.Message{
Content: []types.ContentBlock{
&types.ContentBlockMemberText{
Value: "mock response",
},
},
},
},
}, nil
},
}
client := &AmazonBedrockConverseClient{
client: mock,
model: "test-model",
}
result, err := client.GetCompletion(context.Background(), "hello")
assert.NoError(t, err)
assert.Equal(t, "mock response", result)
}
func TestGetCompletion_Error(t *testing.T) {
mock := &mockConverseClient{
converseFunc: func(ctx context.Context, input *bedrockruntime.ConverseInput) (*bedrockruntime.ConverseOutput, error) {
return nil, errors.New("some error")
},
}
client := &AmazonBedrockConverseClient{
client: mock,
model: "test-model",
}
_, err := client.GetCompletion(context.Background(), "hello")
assert.Error(t, err)
}
func TestConfigure_WithInjectedClient(t *testing.T) {
mock := &mockConverseClient{}
cfg := &AIProvider{
Model: "test-model",
ProviderRegion: "us-west-2",
Temperature: 0.5,
TopP: 0.9,
MaxTokens: 100,
StopSequences: []string{"stop"},
}
client := &AmazonBedrockConverseClient{
client: mock,
}
err := client.Configure(cfg)
assert.NoError(t, err)
assert.Equal(t, "test-model", client.model)
assert.Equal(t, float32(0.5), client.temperature)
assert.Equal(t, float32(0.9), client.topP)
assert.Equal(t, 100, client.maxTokens)
assert.Equal(t, []string{"stop"}, client.stopSequences)
}
func TestConfigure_InvalidModel(t *testing.T) {
mock := &mockConverseClient{}
cfg := &AIProvider{
Model: "",
}
client := &AmazonBedrockConverseClient{
client: mock,
}
err := client.Configure(cfg)
assert.Error(t, err)
assert.Contains(t, err.Error(), "model name cannot be empty")
}
func TestGetRegion(t *testing.T) {
t.Run("uses provided region when env not set", func(t *testing.T) {
t.Setenv("AWS_DEFAULT_REGION", "")
result := getRegion("us-west-2")
assert.Equal(t, "us-west-2", result)
})
t.Run("env overrides provided region", func(t *testing.T) {
t.Setenv("AWS_DEFAULT_REGION", "us-east-1")
result := getRegion("us-west-2")
assert.Equal(t, "us-east-1", result)
})
}
func TestProcessError(t *testing.T) {
tests := []struct {
name string
err error
modelId string
contains string
}{
{
name: "no such host",
err: errors.New("dial tcp: no such host"),
modelId: "test-model",
contains: "bedrock service is not available",
},
{
name: "model not found",
err: errors.New("Could not resolve the foundation model"),
modelId: "test-model",
contains: "could not resolve the foundation model",
},
{
name: "generic error",
err: errors.New("something else"),
modelId: "test-model",
contains: "could not invoke model",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := processError(tt.err, tt.modelId)
assert.Contains(t, result.Error(), tt.contains)
})
}
}
func TestExtractTextFromConverseOutput(t *testing.T) {
tests := []struct {
name string
output types.ConverseOutput
expectError bool
expected string
}{
{
name: "nil output",
output: nil,
expectError: true,
},
{
name: "empty content",
output: &types.ConverseOutputMemberMessage{
Value: types.Message{
Content: []types.ContentBlock{},
},
},
expectError: true,
},
{
name: "single text block",
output: &types.ConverseOutputMemberMessage{
Value: types.Message{
Content: []types.ContentBlock{
&types.ContentBlockMemberText{Value: "hello"},
},
},
},
expected: "hello",
},
{
name: "multiple text blocks",
output: &types.ConverseOutputMemberMessage{
Value: types.Message{
Content: []types.ContentBlock{
&types.ContentBlockMemberText{Value: "hello "},
&types.ContentBlockMemberText{Value: "world"},
},
},
},
expected: "hello world",
},
{
name: "mixed content blocks",
output: &types.ConverseOutputMemberMessage{
Value: types.Message{
Content: []types.ContentBlock{
&types.ContentBlockMemberText{Value: "hello"},
// simulate non-text block
&types.ContentBlockMemberImage{},
&types.ContentBlockMemberText{Value: " world"},
},
},
},
expected: "hello world",
},
{
name: "no text blocks",
output: &types.ConverseOutputMemberMessage{
Value: types.Message{
Content: []types.ContentBlock{
&types.ContentBlockMemberImage{},
},
},
},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := extractTextFromConverseOutput(tt.output, "test-model")
if tt.expectError {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.expected, result)
})
}
}
func TestGetName(t *testing.T) {
client := &AmazonBedrockConverseClient{}
assert.Equal(t, "amazonbedrockconverse", client.GetName())
}

View File

@@ -27,7 +27,6 @@ var (
&NoOpAIClient{},
&CohereClient{},
&AmazonBedRockClient{},
&AmazonBedrockConverseClient{},
&SageMakerAIClient{},
&GoogleGenAIClient{},
&HuggingfaceClient{},
@@ -44,7 +43,6 @@ var (
azureAIClientName,
cohereAIClientName,
amazonbedrockAIClientName,
amazonBedrockConverseClientName,
amazonsagemakerAIClientName,
googleAIClientName,
noopAIClientName,
@@ -87,7 +85,6 @@ type IAIConfig interface {
GetTopP() float32
GetTopK() int32
GetMaxTokens() int
GetStopSequences() []string
GetProviderId() string
GetCompartmentId() string
GetOrganizationId() string
@@ -125,7 +122,6 @@ type AIProvider struct {
TopP float32 `mapstructure:"topp" yaml:"topp,omitempty"`
TopK int32 `mapstructure:"topk" yaml:"topk,omitempty"`
MaxTokens int `mapstructure:"maxtokens" yaml:"maxtokens,omitempty"`
StopSequences []string `mapstructure:"stopsequences" yaml:"stopsequences,omitempty"`
OrganizationId string `mapstructure:"organizationid" yaml:"organizationid,omitempty"`
CustomHeaders []http.Header `mapstructure:"customHeaders"`
}
@@ -154,10 +150,6 @@ func (p *AIProvider) GetMaxTokens() int {
return p.MaxTokens
}
func (p *AIProvider) GetStopSequences() []string {
return p.StopSequences
}
func (p *AIProvider) GetPassword() string {
return p.Password
}
@@ -193,7 +185,7 @@ func (p *AIProvider) GetCustomHeaders() []http.Header {
return p.CustomHeaders
}
var passwordlessProviders = []string{"localai", "ollama", "amazonsagemaker", "amazonbedrock", "amazonbedrockconverse", "googlevertexai", "oci", "customrest"}
var passwordlessProviders = []string{"localai", "ollama", "amazonsagemaker", "amazonbedrock", "googlevertexai", "oci", "customrest"}
func NeedPassword(backend string) bool {
for _, b := range passwordlessProviders {

View File

@@ -61,10 +61,6 @@ func (m *mockConfig) GetMaxTokens() int {
return 0
}
func (m *mockConfig) GetStopSequences() []string {
return []string{"", "", "", ""}
}
func (m *mockConfig) GetEndpointName() string {
return ""
}

View File

@@ -16,7 +16,6 @@ package analysis
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"reflect"
@@ -35,7 +34,6 @@ 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 {
@@ -228,15 +226,6 @@ 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())
@@ -537,22 +526,7 @@ 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 {
// 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)
prompt = fmt.Sprintf(ai.PromptMap["raw"], a.Language, inputKey, prompt)
}
response, err := a.AIClient.GetCompletion(a.Context, prompt)
if err != nil {

View File

@@ -64,6 +64,7 @@ var additionalAnalyzerMap = map[string]common.IAnalyzer{
"InstallPlan": InstallPlanAnalyzer{},
"CatalogSource": CatalogSourceAnalyzer{},
"OperatorGroup": OperatorGroupAnalyzer{},
"CustomResource": CRDAnalyzer{},
}
func ListFilters() ([]string, []string, []string) {

330
pkg/analyzer/crd.go Normal file
View File

@@ -0,0 +1,330 @@
/*
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
}

410
pkg/analyzer/crd_test.go Normal file
View File

@@ -0,0 +1,410 @@
/*
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
}

View File

@@ -97,6 +97,32 @@ 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

View File

@@ -29,247 +29,148 @@ 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{
LabelSelector: req.LabelSelector,
Limit: req.Limit,
listOptions := metav1.ListOptions{}
if req.LabelSelector != "" {
listOptions.LabelSelector = req.LabelSelector
}
// 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
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
}
// 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
return mcp.NewToolResultText(result), nil
}
// handleGetResource gets detailed information about a specific resource
@@ -283,37 +184,52 @@ 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
}
// 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
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
}
resultJSON, err := marshalJSON(result)
if err != nil {
return mcp.NewToolResultErrorf("Failed to serialize result: %v", err), nil
}
return mcp.NewToolResultText(resultJSON), nil
return mcp.NewToolResultText(result), nil
}
// handleListNamespaces lists all namespaces in the cluster
@@ -328,30 +244,24 @@ func (s *K8sGptMCPServer) handleListNamespaces(ctx context.Context, request mcp.
return mcp.NewToolResultErrorf("Failed to list namespaces: %v", err), nil
}
resultJSON, err := marshalJSON(namespaces.Items)
if err != nil {
return mcp.NewToolResultErrorf("Failed to serialize result: %v", err), nil
}
return mcp.NewToolResultText(resultJSON), nil
data, _ := json.MarshalIndent(namespaces.Items, "", " ")
return mcp.NewToolResultText(string(data)), 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 = DefaultListLimit
} else if req.Limit > MaxListLimit {
req.Limit = MaxListLimit
req.Limit = 100
}
client, err := kubernetes.NewClient("", "")
@@ -380,12 +290,8 @@ func (s *K8sGptMCPServer) handleListEvents(ctx context.Context, request mcp.Call
filteredEvents = append(filteredEvents, event)
}
resultJSON, err := marshalJSON(filteredEvents)
if err != nil {
return mcp.NewToolResultErrorf("Failed to serialize result: %v", err), nil
}
return mcp.NewToolResultText(resultJSON), nil
data, _ := json.MarshalIndent(filteredEvents, "", " ")
return mcp.NewToolResultText(string(data)), nil
}
// handleGetLogs retrieves logs from a pod container
@@ -402,13 +308,6 @@ 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
}
@@ -457,12 +356,8 @@ func (s *K8sGptMCPServer) handleListFilters(ctx context.Context, request mcp.Cal
"activeFilters": active,
}
resultJSON, err := marshalJSON(result)
if err != nil {
return mcp.NewToolResultErrorf("Failed to serialize result: %v", err), nil
}
return mcp.NewToolResultText(resultJSON), nil
data, _ := json.MarshalIndent(result, "", " ")
return mcp.NewToolResultText(string(data)), nil
}
// handleAddFilters adds filters to enable specific analyzers
@@ -474,17 +369,10 @@ 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)
}
}
@@ -493,11 +381,7 @@ func (s *K8sGptMCPServer) handleAddFilters(ctx context.Context, request mcp.Call
return mcp.NewToolResultErrorf("Failed to save configuration: %v", err), 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
return mcp.NewToolResultText(fmt.Sprintf("Successfully added filters: %v", req.Filters)), nil
}
// handleRemoveFilters removes filters to disable specific analyzers
@@ -509,19 +393,11 @@ 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)
}
}
@@ -530,11 +406,7 @@ func (s *K8sGptMCPServer) handleRemoveFilters(ctx context.Context, request mcp.C
return mcp.NewToolResultErrorf("Failed to save configuration: %v", err), 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
return mcp.NewToolResultText(fmt.Sprintf("Successfully removed filters: %v", req.Filters)), nil
}
// handleListIntegrations lists available integrations
@@ -551,12 +423,8 @@ func (s *K8sGptMCPServer) handleListIntegrations(ctx context.Context, request mc
})
}
resultJSON, err := marshalJSON(result)
if err != nil {
return mcp.NewToolResultErrorf("Failed to serialize result: %v", err), nil
}
return mcp.NewToolResultText(resultJSON), nil
data, _ := json.MarshalIndent(result, "", " ")
return mcp.NewToolResultText(string(data)), nil
}
// contains checks if a string slice contains a specific string