Compare commits

..

3 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
b92512aec0 Complete PR #1602 review - all tests passing
Co-authored-by: AlexsJones <1235925+AlexsJones@users.noreply.github.com>
2026-01-30 16:59:09 +00:00
copilot-swe-agent[bot]
52f7808a3a Apply PR #1602: improve ConfigMap usage detection for sidecar patterns
Co-authored-by: AlexsJones <1235925+AlexsJones@users.noreply.github.com>
2026-01-30 16:52:40 +00:00
copilot-swe-agent[bot]
1f5d9a8fe0 Initial plan 2026-01-30 16:47:59 +00:00
22 changed files with 513 additions and 550 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

View File

@@ -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.30"}
{".":"0.4.27"}

View File

@@ -1,52 +1,5 @@
# Changelog
## [0.4.30](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.29...v0.4.30) (2026-02-20)
### Bug Fixes
* validate namespace before running custom analyzers ([#1617](https://github.com/k8sgpt-ai/k8sgpt/issues/1617)) ([458aa9d](https://github.com/k8sgpt-ai/k8sgpt/commit/458aa9debac7590eb0855ffd12141b702e999a36))
## [0.4.29](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.28...v0.4.29) (2026-02-20)
### Features
* **serve:** add short flag and env var for metrics port ([#1616](https://github.com/k8sgpt-ai/k8sgpt/issues/1616)) ([4f63e97](https://github.com/k8sgpt-ai/k8sgpt/commit/4f63e9737c6a2306686bd3b6f37e81f210665949))
### Bug Fixes
* **deps:** update k8s.io/utils digest to b8788ab ([#1572](https://github.com/k8sgpt-ai/k8sgpt/issues/1572)) ([a56e478](https://github.com/k8sgpt-ai/k8sgpt/commit/a56e4788c3361a64df17175f163f33422a8fe606))
* use proper JSON marshaling for customrest prompt to handle special characters ([#1615](https://github.com/k8sgpt-ai/k8sgpt/issues/1615)) ([99911fb](https://github.com/k8sgpt-ai/k8sgpt/commit/99911fbb3ac8c950fd7ee1b3210f8a9c2a6b0ad7)), closes [#1556](https://github.com/k8sgpt-ai/k8sgpt/issues/1556)
### Refactoring
* improve MCP server handlers with better error handling and pagination ([#1613](https://github.com/k8sgpt-ai/k8sgpt/issues/1613)) ([abc4647](https://github.com/k8sgpt-ai/k8sgpt/commit/abc46474e372bcd27201f1a64372c04269acee13))
## [0.4.28](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.27...v0.4.28) (2026-02-15)
### Features
* add Groq as LLM provider ([#1600](https://github.com/k8sgpt-ai/k8sgpt/issues/1600)) ([867bce1](https://github.com/k8sgpt-ai/k8sgpt/commit/867bce1907f5dd3387128b72c694e98091d55554))
* multiple security fixes. Prometheus: v0.302.1 → v0.306.0 ([#1597](https://github.com/k8sgpt-ai/k8sgpt/issues/1597)) ([f5fb2a7](https://github.com/k8sgpt-ai/k8sgpt/commit/f5fb2a7e12e14fad8107940aeead5e60b064add1))
### Bug Fixes
* align CI Go versions with go.mod to ensure consistency ([#1611](https://github.com/k8sgpt-ai/k8sgpt/issues/1611)) ([1f2ff98](https://github.com/k8sgpt-ai/k8sgpt/commit/1f2ff988342b8ef2aa3e3263eb845c0ee09fe24c))
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#1550](https://github.com/k8sgpt-ai/k8sgpt/issues/1550)) ([7fe3bdb](https://github.com/k8sgpt-ai/k8sgpt/commit/7fe3bdbd952bc9a1975121de5f21ad31dc1f691d))
* use MaxCompletionTokens instead of deprecated MaxTokens for OpenAI ([#1604](https://github.com/k8sgpt-ai/k8sgpt/issues/1604)) ([c80b2e2](https://github.com/k8sgpt-ai/k8sgpt/commit/c80b2e2c346845336593ce515fe90fd501b1d0a7))
### Other
* **deps:** update actions/checkout digest to 93cb6ef ([#1592](https://github.com/k8sgpt-ai/k8sgpt/issues/1592)) ([40ffcbe](https://github.com/k8sgpt-ai/k8sgpt/commit/40ffcbec6b65e3a99e40be5f414a3f2c087bffbb))
* **deps:** update actions/setup-go digest to 40f1582 ([#1593](https://github.com/k8sgpt-ai/k8sgpt/issues/1593)) ([a303ffa](https://github.com/k8sgpt-ai/k8sgpt/commit/a303ffa21c7ede3dd9391185bc91fb3b4e8276b6))
* util tests ([#1594](https://github.com/k8sgpt-ai/k8sgpt/issues/1594)) ([21369c5](https://github.com/k8sgpt-ai/k8sgpt/commit/21369c5c0917fd2b6ae4173378b2e257e2b1de7b))
## [0.4.27](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.26...v0.4.27) (2025-12-18)

View File

@@ -63,7 +63,7 @@ brew install k8sgpt
<!---x-release-please-start-version-->
```
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_386.rpm
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.27/k8sgpt_386.rpm
```
<!---x-release-please-end-->
@@ -71,7 +71,7 @@ brew install k8sgpt
<!---x-release-please-start-version-->
```
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/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>
@@ -84,7 +84,7 @@ brew install k8sgpt
<!---x-release-please-start-version-->
```
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/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
```
@@ -95,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.30/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
```
@@ -110,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.30/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-->
@@ -119,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.30/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-->

View File

@@ -42,7 +42,7 @@ var (
mcpPort string
mcpHTTP bool
// filters can be injected into the server (repeatable flag)
filters []string
filters []string
)
var ServeCmd = &cobra.Command{
@@ -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")

2
go.mod
View File

@@ -282,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

4
go.sum
View File

@@ -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

@@ -20,11 +20,11 @@ type MockBedrockClient struct {
func (m *MockBedrockClient) GetInferenceProfile(ctx context.Context, params *bedrock.GetInferenceProfileInput, optFns ...func(*bedrock.Options)) (*bedrock.GetInferenceProfileOutput, error) {
args := m.Called(ctx, params)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*bedrock.GetInferenceProfileOutput), args.Error(1)
}
@@ -35,11 +35,11 @@ type MockBedrockRuntimeClient struct {
func (m *MockBedrockRuntimeClient) InvokeModel(ctx context.Context, params *bedrockruntime.InvokeModelInput, optFns ...func(*bedrockruntime.Options)) (*bedrockruntime.InvokeModelOutput, error) {
args := m.Called(ctx, params)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*bedrockruntime.InvokeModelOutput), args.Error(1)
}
@@ -59,21 +59,21 @@ func TestBedrockInferenceProfileARNWithMocks(t *testing.T) {
},
},
}
// Create a client with test models
client := &AmazonBedRockClient{models: testModels}
// Create mock clients
mockMgmtClient := new(MockBedrockClient)
mockRuntimeClient := new(MockBedrockRuntimeClient)
// Inject mock clients into the AmazonBedRockClient
client.mgmtClient = mockMgmtClient
client.client = mockRuntimeClient
// Test with a valid inference profile ARN
inferenceProfileARN := "arn:aws:bedrock:us-east-1:123456789012:inference-profile/my-profile"
// Setup mock response for GetInferenceProfile
mockMgmtClient.On("GetInferenceProfile", mock.Anything, &bedrock.GetInferenceProfileInput{
InferenceProfileIdentifier: aws.String("my-profile"),
@@ -84,20 +84,20 @@ func TestBedrockInferenceProfileARNWithMocks(t *testing.T) {
},
},
}, nil)
// Configure the client with the inference profile ARN
config := AIProvider{
Model: inferenceProfileARN,
ProviderRegion: "us-east-1",
}
// Test the Configure method with the inference profile ARN
err := client.Configure(&config)
// Verify that the configuration was successful
assert.NoError(t, err)
assert.Equal(t, inferenceProfileARN, client.model.Config.ModelName)
// Verify that the mock was called
mockMgmtClient.AssertExpectations(t)
}

View File

@@ -45,7 +45,7 @@ func (p *ViperConfigProvider) UnmarshalKey(key string, rawVal interface{}) error
// Default instances to be used
var (
DefaultClientFactory = &DefaultAIClientFactory{}
DefaultClientFactory = &DefaultAIClientFactory{}
DefaultConfigProvider = &ViperConfigProvider{}
)
@@ -84,4 +84,4 @@ func SetTestConfigProvider(provider ConfigProvider) {
func ResetTestImplementations() {
testAIClientFactory = nil
testConfigProvider = nil
}
}

View File

@@ -94,11 +94,11 @@ func (c *OpenAIClient) GetCompletion(ctx context.Context, prompt string) (string
Content: prompt,
},
},
Temperature: c.temperature,
Temperature: c.temperature,
MaxCompletionTokens: maxToken,
PresencePenalty: presencePenalty,
FrequencyPenalty: frequencyPenalty,
TopP: c.topP,
PresencePenalty: presencePenalty,
FrequencyPenalty: frequencyPenalty,
TopP: c.topP,
})
if err != nil {
return "", err

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

@@ -17,6 +17,7 @@ import (
"fmt"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -80,6 +81,17 @@ func (ConfigMapAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
for _, cm := range configMaps.Items {
var failures []common.Failure
// Check if ConfigMap is dynamically loaded by sidecars
if isKnownSidecarPattern(cm) {
usedConfigMaps[cm.Name] = true
continue
}
// Check if usage check should be skipped
if shouldSkipUsageCheck(cm) {
continue
}
// Check for unused ConfigMaps
if !usedConfigMaps[cm.Name] {
failures = append(failures, common.Failure{
@@ -123,3 +135,33 @@ func (ConfigMapAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
return results, nil
}
// isKnownSidecarPattern detects ConfigMaps that are dynamically loaded by sidecar containers
// These ConfigMaps are not directly referenced in Pod specs but are watched via Kubernetes API
func isKnownSidecarPattern(cm v1.ConfigMap) bool {
// Common sidecar patterns
knownLabels := []string{
"grafana_dashboard", // Grafana sidecar dashboard loader
"grafana_datasource", // Grafana sidecar datasource loader
"prometheus_rule", // Prometheus operator rule loader
"fluentd_config", // Fluentd config reloader
}
for _, label := range knownLabels {
if _, exists := cm.Labels[label]; exists {
return true
}
}
// User-defined marker for dynamically loaded ConfigMaps
if cm.Labels["k8sgpt.ai/dynamically-loaded"] == "true" {
return true
}
return false
}
// shouldSkipUsageCheck allows users to opt-out of usage checking
func shouldSkipUsageCheck(cm v1.ConfigMap) bool {
return cm.Annotations["k8sgpt.ai/skip-usage-check"] == "true"
}

View File

@@ -147,3 +147,134 @@ func TestConfigMapAnalyzer(t *testing.T) {
})
}
}
// TestConfigMapAnalyzer_SidecarPatterns tests known sidecar patterns and skip annotations
func TestConfigMapAnalyzer_SidecarPatterns(t *testing.T) {
tests := []struct {
name string
namespace string
configMaps []v1.ConfigMap
pods []v1.Pod
expectedErrors int
}{
{
name: "grafana dashboard configmap should not be flagged as unused",
namespace: "monitoring",
configMaps: []v1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{
Name: "grafana-dashboard",
Namespace: "monitoring",
Labels: map[string]string{
"grafana_dashboard": "1",
},
},
Data: map[string]string{
"dashboard.json": `{"title": "My Dashboard"}`,
},
},
},
pods: []v1.Pod{},
expectedErrors: 0,
},
{
name: "configmap with skip annotation should be ignored",
namespace: "default",
configMaps: []v1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{
Name: "ignored-cm",
Namespace: "default",
Annotations: map[string]string{
"k8sgpt.ai/skip-usage-check": "true",
},
},
Data: map[string]string{
"key": "value",
},
},
},
expectedErrors: 0,
},
{
name: "normal unused configmap should still be flagged",
namespace: "default",
configMaps: []v1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{
Name: "unused-cm",
Namespace: "default",
},
Data: map[string]string{
"key": "value",
},
},
},
expectedErrors: 1,
},
{
name: "prometheus rule configmap should not be flagged",
namespace: "monitoring",
configMaps: []v1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{
Name: "prometheus-rules",
Namespace: "monitoring",
Labels: map[string]string{
"prometheus_rule": "1",
},
},
Data: map[string]string{
"rules.yaml": "groups: []",
},
},
},
expectedErrors: 0,
},
{
name: "custom dynamically-loaded label should work",
namespace: "default",
configMaps: []v1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{
Name: "custom-sidecar-cm",
Namespace: "default",
Labels: map[string]string{
"k8sgpt.ai/dynamically-loaded": "true",
},
},
Data: map[string]string{
"config": "value",
},
},
},
expectedErrors: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := fake.NewSimpleClientset()
for _, cm := range tt.configMaps {
_, err := client.CoreV1().ConfigMaps(tt.namespace).Create(context.TODO(), &cm, metav1.CreateOptions{})
assert.NoError(t, err)
}
for _, pod := range tt.pods {
_, err := client.CoreV1().Pods(tt.namespace).Create(context.TODO(), &pod, metav1.CreateOptions{})
assert.NoError(t, err)
}
analyzer := ConfigMapAnalyzer{}
results, err := analyzer.Analyze(common.Analyzer{
Client: &kubernetes.Client{Client: client},
Context: context.TODO(),
Namespace: tt.namespace,
})
assert.NoError(t, err)
assert.Equal(t, tt.expectedErrors, len(results), "Expected %d errors but got %d", tt.expectedErrors, len(results))
})
}
}

View File

@@ -55,7 +55,7 @@ func (d DeploymentAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error)
for _, deployment := range deployments.Items {
var failures []common.Failure
if *deployment.Spec.Replicas != deployment.Status.ReadyReplicas {
if deployment.Status.Replicas > *deployment.Spec.Replicas {
if deployment.Status.Replicas > *deployment.Spec.Replicas {
doc := apiDoc.GetApiDocV2("spec.replicas")
failures = append(failures, common.Failure{
@@ -88,7 +88,7 @@ func (d DeploymentAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error)
Masked: util.MaskString(deployment.Name),
},
}})
}
}
}
if len(failures) > 0 {
preAnalysis[fmt.Sprintf("%s/%s", deployment.Namespace, deployment.Name)] = common.PreAnalysis{

View File

@@ -1,60 +1,60 @@
package cache
import (
"os"
"testing"
"os"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestNewReturnsExpectedCache(t *testing.T) {
require.IsType(t, &FileBasedCache{}, New("file"))
require.IsType(t, &AzureCache{}, New("azure"))
require.IsType(t, &GCSCache{}, New("gcs"))
require.IsType(t, &S3Cache{}, New("s3"))
require.IsType(t, &InterplexCache{}, New("interplex"))
// default fallback
require.IsType(t, &FileBasedCache{}, New("unknown"))
require.IsType(t, &FileBasedCache{}, New("file"))
require.IsType(t, &AzureCache{}, New("azure"))
require.IsType(t, &GCSCache{}, New("gcs"))
require.IsType(t, &S3Cache{}, New("s3"))
require.IsType(t, &InterplexCache{}, New("interplex"))
// default fallback
require.IsType(t, &FileBasedCache{}, New("unknown"))
}
func TestNewCacheProvider_InterplexAndInvalid(t *testing.T) {
// valid: interplex
cp, err := NewCacheProvider("interplex", "", "", "localhost:1", "", "", "", false)
require.NoError(t, err)
require.Equal(t, "interplex", cp.CurrentCacheType)
require.Equal(t, "localhost:1", cp.Interplex.ConnectionString)
// valid: interplex
cp, err := NewCacheProvider("interplex", "", "", "localhost:1", "", "", "", false)
require.NoError(t, err)
require.Equal(t, "interplex", cp.CurrentCacheType)
require.Equal(t, "localhost:1", cp.Interplex.ConnectionString)
// invalid type
_, err = NewCacheProvider("not-a-type", "", "", "", "", "", "", false)
require.Error(t, err)
// invalid type
_, err = NewCacheProvider("not-a-type", "", "", "", "", "", "", false)
require.Error(t, err)
}
func TestAddRemoveRemoteCacheAndGet(t *testing.T) {
// isolate viper with temp config file
tmpFile, err := os.CreateTemp("", "k8sgpt-cache-config-*.yaml")
require.NoError(t, err)
defer func() {
_ = os.Remove(tmpFile.Name())
}()
viper.Reset()
viper.SetConfigFile(tmpFile.Name())
// isolate viper with temp config file
tmpFile, err := os.CreateTemp("", "k8sgpt-cache-config-*.yaml")
require.NoError(t, err)
defer func() {
_ = os.Remove(tmpFile.Name())
}()
viper.Reset()
viper.SetConfigFile(tmpFile.Name())
// add interplex remote cache
cp := CacheProvider{}
cp.CurrentCacheType = "interplex"
cp.Interplex.ConnectionString = "localhost:1"
require.NoError(t, AddRemoteCache(cp))
// add interplex remote cache
cp := CacheProvider{}
cp.CurrentCacheType = "interplex"
cp.Interplex.ConnectionString = "localhost:1"
require.NoError(t, AddRemoteCache(cp))
// read back via GetCacheConfiguration
c, err := GetCacheConfiguration()
require.NoError(t, err)
require.IsType(t, &InterplexCache{}, c)
// read back via GetCacheConfiguration
c, err := GetCacheConfiguration()
require.NoError(t, err)
require.IsType(t, &InterplexCache{}, c)
// remove remote cache
require.NoError(t, RemoveRemoteCache())
// now default should be file-based
c2, err := GetCacheConfiguration()
require.NoError(t, err)
require.IsType(t, &FileBasedCache{}, c2)
}
// remove remote cache
require.NoError(t, RemoveRemoteCache())
// now default should be file-based
c2, err := GetCacheConfiguration()
require.NoError(t, err)
require.IsType(t, &FileBasedCache{}, c2)
}

View File

@@ -1,77 +1,77 @@
package cache
import (
"os"
"path/filepath"
"testing"
"os"
"path/filepath"
"testing"
"github.com/adrg/xdg"
"github.com/stretchr/testify/require"
"github.com/adrg/xdg"
"github.com/stretchr/testify/require"
)
// withTempCacheHome sets XDG_CACHE_HOME to a temp dir for test isolation.
func withTempCacheHome(t *testing.T) func() {
t.Helper()
tmp, err := os.MkdirTemp("", "k8sgpt-cache-test-*")
require.NoError(t, err)
old := os.Getenv("XDG_CACHE_HOME")
require.NoError(t, os.Setenv("XDG_CACHE_HOME", tmp))
return func() {
_ = os.Setenv("XDG_CACHE_HOME", old)
_ = os.RemoveAll(tmp)
}
t.Helper()
tmp, err := os.MkdirTemp("", "k8sgpt-cache-test-*")
require.NoError(t, err)
old := os.Getenv("XDG_CACHE_HOME")
require.NoError(t, os.Setenv("XDG_CACHE_HOME", tmp))
return func() {
_ = os.Setenv("XDG_CACHE_HOME", old)
_ = os.RemoveAll(tmp)
}
}
func TestFileBasedCache_BasicOps(t *testing.T) {
cleanup := withTempCacheHome(t)
defer cleanup()
cleanup := withTempCacheHome(t)
defer cleanup()
c := &FileBasedCache{}
// Configure should be a no-op
require.NoError(t, c.Configure(CacheProvider{}))
require.Equal(t, "file", c.GetName())
require.False(t, c.IsCacheDisabled())
c.DisableCache()
require.True(t, c.IsCacheDisabled())
c := &FileBasedCache{}
// Configure should be a no-op
require.NoError(t, c.Configure(CacheProvider{}))
require.Equal(t, "file", c.GetName())
require.False(t, c.IsCacheDisabled())
c.DisableCache()
require.True(t, c.IsCacheDisabled())
key := "testkey"
data := "hello"
key := "testkey"
data := "hello"
// Store
require.NoError(t, c.Store(key, data))
// Store
require.NoError(t, c.Store(key, data))
// Exists
require.True(t, c.Exists(key))
// Exists
require.True(t, c.Exists(key))
// Load
got, err := c.Load(key)
require.NoError(t, err)
require.Equal(t, data, got)
// Load
got, err := c.Load(key)
require.NoError(t, err)
require.Equal(t, data, got)
// List should include our key file
items, err := c.List()
require.NoError(t, err)
// ensure at least one item and that one matches our key
found := false
for _, it := range items {
if it.Name == key {
found = true
break
}
}
require.True(t, found)
// List should include our key file
items, err := c.List()
require.NoError(t, err)
// ensure at least one item and that one matches our key
found := false
for _, it := range items {
if it.Name == key {
found = true
break
}
}
require.True(t, found)
// Remove
require.NoError(t, c.Remove(key))
require.False(t, c.Exists(key))
// Remove
require.NoError(t, c.Remove(key))
require.False(t, c.Exists(key))
}
func TestFileBasedCache_PathShape(t *testing.T) {
cleanup := withTempCacheHome(t)
defer cleanup()
// Verify xdg.CacheFile path shape (directory and filename)
p, err := xdg.CacheFile(filepath.Join("k8sgpt", "abc"))
require.NoError(t, err)
require.Equal(t, "abc", filepath.Base(p))
require.Contains(t, p, "k8sgpt")
}
cleanup := withTempCacheHome(t)
defer cleanup()
// Verify xdg.CacheFile path shape (directory and filename)
p, err := xdg.CacheFile(filepath.Join("k8sgpt", "abc"))
require.NoError(t, err)
require.Equal(t, "abc", filepath.Base(p))
require.Contains(t, p, "k8sgpt")
}

View File

@@ -1,41 +1,41 @@
package custom
import (
"context"
"testing"
"context"
"testing"
schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
)
// mockAnalyzerClient implements rpc.CustomAnalyzerServiceClient for testing
type mockAnalyzerClient struct{
resp *schemav1.RunResponse
err error
type mockAnalyzerClient struct {
resp *schemav1.RunResponse
err error
}
func (m *mockAnalyzerClient) Run(ctx context.Context, in *schemav1.RunRequest, opts ...grpc.CallOption) (*schemav1.RunResponse, error) {
return m.resp, m.err
return m.resp, m.err
}
func TestClientRunMapsResponse(t *testing.T) {
// prepare fake response
resp := &schemav1.RunResponse{
Result: &schemav1.Result{
Name: "AnalyzerA",
Kind: "Pod",
Details: "details",
ParentObject: "Deployment/foo",
},
}
cli := &Client{analyzerClient: &mockAnalyzerClient{resp: resp}}
// prepare fake response
resp := &schemav1.RunResponse{
Result: &schemav1.Result{
Name: "AnalyzerA",
Kind: "Pod",
Details: "details",
ParentObject: "Deployment/foo",
},
}
cli := &Client{analyzerClient: &mockAnalyzerClient{resp: resp}}
got, err := cli.Run()
require.NoError(t, err)
require.Equal(t, "AnalyzerA", got.Name)
require.Equal(t, "Pod", got.Kind)
require.Equal(t, "details", got.Details)
require.Equal(t, "Deployment/foo", got.ParentObject)
require.Len(t, got.Error, 0)
}
got, err := cli.Run()
require.NoError(t, err)
require.Equal(t, "AnalyzerA", got.Name)
require.Equal(t, "Pod", got.Kind)
require.Equal(t, "details", got.Details)
require.Equal(t, "Deployment/foo", got.ParentObject)
require.Len(t, got.Error, 0)
}

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,12 +244,8 @@ 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
@@ -349,9 +261,7 @@ func (s *K8sGptMCPServer) handleListEvents(ctx context.Context, request mcp.Call
}
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

View File

@@ -307,4 +307,4 @@ func TestQuery_GetCompletionError(t *testing.T) {
mockAI.AssertExpectations(t)
mockFactory.AssertExpectations(t)
mockConfig.AssertExpectations(t)
}
}

View File

@@ -55,10 +55,10 @@ type Config struct {
QueryHandler *query.Handler
Logger *zap.Logger
// Filters can be injected into the server to limit analysis to specific analyzers
Filters []string
metricsServer *http.Server
listener net.Listener
EnableHttp bool
Filters []string
metricsServer *http.Server
listener net.Listener
EnableHttp bool
}
type Health struct {