diff --git a/Makefile b/Makefile index 6863d2e..6925717 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,9 @@ LD_FLAGS = -s -w \ -X '$(PACKAGE)/pkg/version.BinaryName=$(BINARY_NAME)' COMMON_BUILD_ARGS = -ldflags "$(LD_FLAGS)" +GOLANGCI_LINT = $(shell pwd)/_output/tools/bin/golangci-lint +GOLANGCI_LINT_VERSION ?= v2.2.2 + # NPM version should not append the -dirty flag NPM_VERSION ?= $(shell echo $(shell git describe --tags --always) | sed 's/^v//') OSES = darwin linux windows @@ -97,3 +100,14 @@ format: ## Format the code .PHONY: tidy tidy: ## Tidy up the go modules go mod tidy + +.PHONY: golangci-lint +golangci-lint: ## Download and install golangci-lint if not already installed + @[ -f $(GOLANGCI_LINT) ] || { \ + set -e ;\ + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell dirname $(GOLANGCI_LINT)) $(GOLANGCI_LINT_VERSION) ;\ + } + +.PHONY: lint +lint: golangci-lint ## Lint the code + $(GOLANGCI_LINT) run --verbose --print-resources-usage diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go index 7fd1335..186b50d 100644 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -45,11 +45,14 @@ func (h *Helm) Install(ctx context.Context, chart string, values map[string]inte install.Timeout = 5 * time.Minute install.DryRun = false - chartRequested, err := install.ChartPathOptions.LocateChart(chart, cli.New()) + chartRequested, err := install.LocateChart(chart, cli.New()) if err != nil { return "", err } chartLoaded, err := loader.Load(chartRequested) + if err != nil { + return "", err + } installedRelease, err := install.RunWithContext(ctx, chartLoaded, values) if err != nil { diff --git a/pkg/kubernetes-mcp-server/cmd/root.go b/pkg/kubernetes-mcp-server/cmd/root.go index f39ab38..607379b 100644 --- a/pkg/kubernetes-mcp-server/cmd/root.go +++ b/pkg/kubernetes-mcp-server/cmd/root.go @@ -113,11 +113,11 @@ func NewMCPServer(streams genericiooptions.IOStreams) *cobra.Command { cmd.Flags().BoolVar(&o.ReadOnly, "read-only", o.ReadOnly, "If true, only tools annotated with readOnlyHint=true are exposed") cmd.Flags().BoolVar(&o.DisableDestructive, "disable-destructive", o.DisableDestructive, "If true, tools annotated with destructiveHint=true are disabled") cmd.Flags().BoolVar(&o.RequireOAuth, "require-oauth", o.RequireOAuth, "If true, requires OAuth authorization as defined in the Model Context Protocol (MCP) specification. This flag is ignored if transport type is stdio") - cmd.Flags().MarkHidden("require-oauth") + _ = cmd.Flags().MarkHidden("require-oauth") cmd.Flags().StringVar(&o.AuthorizationURL, "authorization-url", o.AuthorizationURL, "OAuth authorization server URL for protected resource endpoint. If not provided, the Kubernetes API server host will be used. Only valid if require-oauth is enabled.") - cmd.Flags().MarkHidden("authorization-url") + _ = cmd.Flags().MarkHidden("authorization-url") cmd.Flags().StringVar(&o.ServerURL, "server-url", o.ServerURL, "Server URL of this application. Optional. If set, this url will be served in protected resource metadata endpoint and tokens will be validated with this audience. If not set, expected audience is kubernetes-mcp-server. Only valid if require-oauth is enabled.") - cmd.Flags().MarkHidden("server-url") + _ = cmd.Flags().MarkHidden("server-url") return cmd } @@ -228,11 +228,11 @@ func (m *MCPServerOptions) Validate() error { func (m *MCPServerOptions) Run() error { profile := mcp.ProfileFromString(m.Profile) if profile == nil { - return fmt.Errorf("Invalid profile name: %s, valid names are: %s\n", m.Profile, strings.Join(mcp.ProfileNames, ", ")) + return fmt.Errorf("invalid profile name: %s, valid names are: %s", m.Profile, strings.Join(mcp.ProfileNames, ", ")) } listOutput := output.FromString(m.StaticConfig.ListOutput) if listOutput == nil { - return fmt.Errorf("Invalid output name: %s, valid names are: %s\n", m.StaticConfig.ListOutput, strings.Join(output.Names, ", ")) + return fmt.Errorf("invalid output name: %s, valid names are: %s", m.StaticConfig.ListOutput, strings.Join(output.Names, ", ")) } klog.V(1).Info("Starting kubernetes-mcp-server") klog.V(1).Infof(" - Config: %s", m.ConfigPath) @@ -261,7 +261,7 @@ func (m *MCPServerOptions) Run() error { StaticConfig: m.StaticConfig, }) if err != nil { - return fmt.Errorf("Failed to initialize MCP server: %w\n", err) + return fmt.Errorf("failed to initialize MCP server: %w", err) } defer mcpServer.Close() diff --git a/pkg/kubernetes/configuration.go b/pkg/kubernetes/configuration.go index eafff04..df88530 100644 --- a/pkg/kubernetes/configuration.go +++ b/pkg/kubernetes/configuration.go @@ -106,6 +106,7 @@ func (m *Manager) ConfigurationView(minify bool) (runtime.Object, error) { return nil, err } } + //nolint:staticcheck if err = clientcmdapi.FlattenConfig(&cfg); err != nil { // ignore error //return "", err diff --git a/pkg/kubernetes/impersonate_roundtripper.go b/pkg/kubernetes/impersonate_roundtripper.go index 362c9e9..a2c15bf 100644 --- a/pkg/kubernetes/impersonate_roundtripper.go +++ b/pkg/kubernetes/impersonate_roundtripper.go @@ -2,10 +2,12 @@ package kubernetes import "net/http" +// nolint:unused type impersonateRoundTripper struct { delegate http.RoundTripper } +// nolint:unused func (irt *impersonateRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { // TODO: Solution won't work with discoveryclient which uses context.TODO() instead of the passed-in context if v, ok := req.Context().Value(OAuthAuthorizationHeader).(string); ok { diff --git a/pkg/kubernetes/kubernetes.go b/pkg/kubernetes/kubernetes.go index 93199da..4de6eea 100644 --- a/pkg/kubernetes/kubernetes.go +++ b/pkg/kubernetes/kubernetes.go @@ -26,9 +26,11 @@ import ( _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" ) +type HeaderKey string + const ( - CustomAuthorizationHeader = "kubernetes-authorization" - OAuthAuthorizationHeader = "Authorization" + CustomAuthorizationHeader = HeaderKey("kubernetes-authorization") + OAuthAuthorizationHeader = HeaderKey("Authorization") CustomUserAgent = "kubernetes-mcp-server/bearer-token-auth" ) @@ -155,10 +157,10 @@ func (m *Manager) Derived(ctx context.Context) (*Kubernetes, error) { APIPath: m.cfg.APIPath, // Copy only server verification TLS settings (CA bundle and server name) TLSClientConfig: rest.TLSClientConfig{ - Insecure: m.cfg.TLSClientConfig.Insecure, - ServerName: m.cfg.TLSClientConfig.ServerName, - CAFile: m.cfg.TLSClientConfig.CAFile, - CAData: m.cfg.TLSClientConfig.CAData, + Insecure: m.cfg.Insecure, + ServerName: m.cfg.ServerName, + CAFile: m.cfg.CAFile, + CAData: m.cfg.CAData, }, BearerToken: strings.TrimPrefix(authorization, "Bearer "), // pass custom UserAgent to identify the client diff --git a/pkg/kubernetes/kubernetes_test.go b/pkg/kubernetes/kubernetes_test.go index 7b64c5f..d6e64c1 100644 --- a/pkg/kubernetes/kubernetes_test.go +++ b/pkg/kubernetes/kubernetes_test.go @@ -137,17 +137,17 @@ users: t.Errorf("expected Timeout %v, got %v", originalCfg.Timeout, derivedCfg.Timeout) } - if derivedCfg.TLSClientConfig.Insecure != originalCfg.TLSClientConfig.Insecure { - t.Errorf("expected TLS Insecure %v, got %v", originalCfg.TLSClientConfig.Insecure, derivedCfg.TLSClientConfig.Insecure) + if derivedCfg.Insecure != originalCfg.Insecure { + t.Errorf("expected TLS Insecure %v, got %v", originalCfg.Insecure, derivedCfg.Insecure) } - if derivedCfg.TLSClientConfig.ServerName != originalCfg.TLSClientConfig.ServerName { - t.Errorf("expected TLS ServerName %s, got %s", originalCfg.TLSClientConfig.ServerName, derivedCfg.TLSClientConfig.ServerName) + if derivedCfg.ServerName != originalCfg.ServerName { + t.Errorf("expected TLS ServerName %s, got %s", originalCfg.ServerName, derivedCfg.ServerName) } - if derivedCfg.TLSClientConfig.CAFile != originalCfg.TLSClientConfig.CAFile { - t.Errorf("expected TLS CAFile %s, got %s", originalCfg.TLSClientConfig.CAFile, derivedCfg.TLSClientConfig.CAFile) + if derivedCfg.CAFile != originalCfg.CAFile { + t.Errorf("expected TLS CAFile %s, got %s", originalCfg.CAFile, derivedCfg.CAFile) } - if string(derivedCfg.TLSClientConfig.CAData) != string(originalCfg.TLSClientConfig.CAData) { - t.Errorf("expected TLS CAData %s, got %s", string(originalCfg.TLSClientConfig.CAData), string(derivedCfg.TLSClientConfig.CAData)) + if string(derivedCfg.CAData) != string(originalCfg.CAData) { + t.Errorf("expected TLS CAData %s, got %s", string(originalCfg.CAData), string(derivedCfg.CAData)) } if derivedCfg.BearerToken != testBearerToken { @@ -160,17 +160,17 @@ users: // Verify that sensitive fields are NOT copied to prevent credential leakage // The derived config should only use the bearer token from the Authorization header // and not inherit any authentication credentials from the original kubeconfig - if derivedCfg.TLSClientConfig.CertFile != "" { - t.Errorf("expected TLS CertFile to be empty, got %s", derivedCfg.TLSClientConfig.CertFile) + if derivedCfg.CertFile != "" { + t.Errorf("expected TLS CertFile to be empty, got %s", derivedCfg.CertFile) } - if derivedCfg.TLSClientConfig.KeyFile != "" { - t.Errorf("expected TLS KeyFile to be empty, got %s", derivedCfg.TLSClientConfig.KeyFile) + if derivedCfg.KeyFile != "" { + t.Errorf("expected TLS KeyFile to be empty, got %s", derivedCfg.KeyFile) } - if len(derivedCfg.TLSClientConfig.CertData) != 0 { - t.Errorf("expected TLS CertData to be empty, got %v", derivedCfg.TLSClientConfig.CertData) + if len(derivedCfg.CertData) != 0 { + t.Errorf("expected TLS CertData to be empty, got %v", derivedCfg.CertData) } - if len(derivedCfg.TLSClientConfig.KeyData) != 0 { - t.Errorf("expected TLS KeyData to be empty, got %v", derivedCfg.TLSClientConfig.KeyData) + if len(derivedCfg.KeyData) != 0 { + t.Errorf("expected TLS KeyData to be empty, got %v", derivedCfg.KeyData) } if derivedCfg.Username != "" { diff --git a/pkg/mcp/common_test.go b/pkg/mcp/common_test.go index 340ed17..d1b0104 100644 --- a/pkg/mcp/common_test.go +++ b/pkg/mcp/common_test.go @@ -25,12 +25,9 @@ import ( apiextensionsv1spec "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" - "k8s.io/client-go/scale" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd/api" toolswatch "k8s.io/client-go/tools/watch" @@ -71,7 +68,7 @@ func TestMain(m *testing.M) { } envTestEnv.CheckCoherence() workflows.Use{}.Do(envTestEnv) - versionDir := envTestEnv.Platform.Platform.BaseName(*envTestEnv.Version.AsConcrete()) + versionDir := envTestEnv.Platform.BaseName(*envTestEnv.Version.AsConcrete()) envTest = &envtest.Environment{ BinaryAssetsDirectory: filepath.Join(envTestDir, "k8s", versionDir), } @@ -190,9 +187,9 @@ func (c *mcpContext) withKubeConfig(rc *rest.Config) *api.Config { fakeConfig.AuthInfos["additional-auth"] = api.NewAuthInfo() if rc != nil { fakeConfig.Clusters["fake"].Server = rc.Host - fakeConfig.Clusters["fake"].CertificateAuthorityData = rc.TLSClientConfig.CAData - fakeConfig.AuthInfos["fake"].ClientKeyData = rc.TLSClientConfig.KeyData - fakeConfig.AuthInfos["fake"].ClientCertificateData = rc.TLSClientConfig.CertData + fakeConfig.Clusters["fake"].CertificateAuthorityData = rc.CAData + fakeConfig.AuthInfos["fake"].ClientKeyData = rc.KeyData + fakeConfig.AuthInfos["fake"].ClientCertificateData = rc.CertData } fakeConfig.Contexts["fake-context"] = api.NewContext() fakeConfig.Contexts["fake-context"].Cluster = "fake" @@ -264,18 +261,6 @@ func (c *mcpContext) newKubernetesClient() *kubernetes.Clientset { return kubernetes.NewForConfigOrDie(envTestRestConfig) } -func (c *mcpContext) newRestClient(groupVersion *schema.GroupVersion) *rest.RESTClient { - config := *envTestRestConfig - config.GroupVersion = groupVersion - config.APIPath = "/api" - config.NegotiatedSerializer = serializer.NewCodecFactory(scale.NewScaleConverter().Scheme()).WithoutConversion() - rc, err := rest.RESTClientFor(&config) - if err != nil { - panic(err) - } - return rc -} - // newApiExtensionsClient creates a new ApiExtensions client with the envTest kubeconfig func (c *mcpContext) newApiExtensionsClient() *apiextensionsv1.ApiextensionsV1Client { return apiextensionsv1.NewForConfigOrDie(envTestRestConfig) @@ -286,6 +271,9 @@ func (c *mcpContext) crdApply(resource string) error { apiExtensionsV1Client := c.newApiExtensionsClient() var crd = &apiextensionsv1spec.CustomResourceDefinition{} err := json.Unmarshal([]byte(resource), crd) + if err != nil { + return fmt.Errorf("failed to create CRD %v", err) + } _, err = apiExtensionsV1Client.CustomResourceDefinitions().Create(c.ctx, crd, metav1.CreateOptions{}) if err != nil { return fmt.Errorf("failed to create CRD %v", err) diff --git a/pkg/mcp/configuration.go b/pkg/mcp/configuration.go index a8bbb80..38637c9 100644 --- a/pkg/mcp/configuration.go +++ b/pkg/mcp/configuration.go @@ -12,7 +12,7 @@ import ( func (s *Server) initConfiguration() []server.ServerTool { tools := []server.ServerTool{ - {mcp.NewTool("configuration_view", + {Tool: mcp.NewTool("configuration_view", mcp.WithDescription("Get the current Kubernetes configuration content as a kubeconfig YAML"), mcp.WithBoolean("minified", mcp.Description("Return a minified version of the configuration. "+ "If set to true, keeps only the current-context and the relevant pieces of the configuration for that context. "+ @@ -23,7 +23,7 @@ func (s *Server) initConfiguration() []server.ServerTool { mcp.WithReadOnlyHintAnnotation(true), mcp.WithDestructiveHintAnnotation(false), mcp.WithOpenWorldHintAnnotation(true), - ), s.configurationView}, + ), Handler: s.configurationView}, } return tools } diff --git a/pkg/mcp/events.go b/pkg/mcp/events.go index 39860f8..29b865f 100644 --- a/pkg/mcp/events.go +++ b/pkg/mcp/events.go @@ -12,7 +12,7 @@ import ( func (s *Server) initEvents() []server.ServerTool { return []server.ServerTool{ - {mcp.NewTool("events_list", + {Tool: mcp.NewTool("events_list", mcp.WithDescription("List all the Kubernetes events in the current cluster from all namespaces"), mcp.WithString("namespace", mcp.Description("Optional Namespace to retrieve the events from. If not provided, will list events from all namespaces")), @@ -21,7 +21,7 @@ func (s *Server) initEvents() []server.ServerTool { mcp.WithReadOnlyHintAnnotation(true), mcp.WithDestructiveHintAnnotation(false), mcp.WithOpenWorldHintAnnotation(true), - ), s.eventsList}, + ), Handler: s.eventsList}, } } diff --git a/pkg/mcp/helm.go b/pkg/mcp/helm.go index 94359e4..e265965 100644 --- a/pkg/mcp/helm.go +++ b/pkg/mcp/helm.go @@ -10,7 +10,7 @@ import ( func (s *Server) initHelm() []server.ServerTool { return []server.ServerTool{ - {mcp.NewTool("helm_install", + {Tool: mcp.NewTool("helm_install", mcp.WithDescription("Install a Helm chart in the current or provided namespace"), mcp.WithString("chart", mcp.Description("Chart reference to install (for example: stable/grafana, oci://ghcr.io/nginxinc/charts/nginx-ingress)"), mcp.Required()), mcp.WithObject("values", mcp.Description("Values to pass to the Helm chart (Optional)")), @@ -22,8 +22,8 @@ func (s *Server) initHelm() []server.ServerTool { mcp.WithDestructiveHintAnnotation(false), mcp.WithIdempotentHintAnnotation(false), // TODO: consider replacing implementation with equivalent to: helm upgrade --install mcp.WithOpenWorldHintAnnotation(true), - ), s.helmInstall}, - {mcp.NewTool("helm_list", + ), Handler: s.helmInstall}, + {Tool: mcp.NewTool("helm_list", mcp.WithDescription("List all the Helm releases in the current or provided namespace (or in all namespaces if specified)"), mcp.WithString("namespace", mcp.Description("Namespace to list Helm releases from (Optional, all namespaces if not provided)")), mcp.WithBoolean("all_namespaces", mcp.Description("If true, lists all Helm releases in all namespaces ignoring the namespace argument (Optional)")), @@ -32,8 +32,8 @@ func (s *Server) initHelm() []server.ServerTool { mcp.WithReadOnlyHintAnnotation(true), mcp.WithDestructiveHintAnnotation(false), mcp.WithOpenWorldHintAnnotation(true), - ), s.helmList}, - {mcp.NewTool("helm_uninstall", + ), Handler: s.helmList}, + {Tool: mcp.NewTool("helm_uninstall", mcp.WithDescription("Uninstall a Helm release in the current or provided namespace"), mcp.WithString("name", mcp.Description("Name of the Helm release to uninstall"), mcp.Required()), mcp.WithString("namespace", mcp.Description("Namespace to uninstall the Helm release from (Optional, current namespace if not provided)")), @@ -43,7 +43,7 @@ func (s *Server) initHelm() []server.ServerTool { mcp.WithDestructiveHintAnnotation(true), mcp.WithIdempotentHintAnnotation(true), mcp.WithOpenWorldHintAnnotation(true), - ), s.helmUninstall}, + ), Handler: s.helmUninstall}, } } diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index d3f1ca5..2c403c8 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -152,13 +152,13 @@ func NewTextResult(content string, err error) *mcp.CallToolResult { func contextFunc(ctx context.Context, r *http.Request) context.Context { // Get the standard Authorization header (OAuth compliant) - authHeader := r.Header.Get(internalk8s.OAuthAuthorizationHeader) + authHeader := r.Header.Get(string(internalk8s.OAuthAuthorizationHeader)) if authHeader != "" { return context.WithValue(ctx, internalk8s.OAuthAuthorizationHeader, authHeader) } // Fallback to custom header for backward compatibility - customAuthHeader := r.Header.Get(internalk8s.CustomAuthorizationHeader) + customAuthHeader := r.Header.Get(string(internalk8s.CustomAuthorizationHeader)) if customAuthHeader != "" { return context.WithValue(ctx, internalk8s.OAuthAuthorizationHeader, customAuthHeader) } diff --git a/pkg/mcp/mcp_test.go b/pkg/mcp/mcp_test.go index 72568ec..9b2c78e 100644 --- a/pkg/mcp/mcp_test.go +++ b/pkg/mcp/mcp_test.go @@ -28,13 +28,9 @@ func TestWatchKubeConfig(t *testing.T) { // When f, _ := os.OpenFile(filepath.Join(c.tempDir, "config"), os.O_APPEND|os.O_WRONLY, 0644) _, _ = f.WriteString("\n") - for { - if notification != nil { - break - } + for notification == nil { select { case <-withTimeout.Done(): - break default: time.Sleep(100 * time.Millisecond) } @@ -94,7 +90,7 @@ func TestSseHeaders(t *testing.T) { w.WriteHeader(404) })) testCaseWithContext(t, &mcpContext{before: before}, func(c *mcpContext) { - c.callTool("pods_list", map[string]interface{}{}) + _, _ = c.callTool("pods_list", map[string]interface{}{}) t.Run("DiscoveryClient propagates headers to Kube API", func(t *testing.T) { if len(pathHeaders) == 0 { t.Fatalf("No requests were made to Kube API") @@ -117,7 +113,7 @@ func TestSseHeaders(t *testing.T) { t.Fatalf("Overridden header Authorization not found in request to /api/v1/namespaces/default/pods") } }) - c.callTool("pods_delete", map[string]interface{}{"name": "a-pod-to-delete"}) + _, _ = c.callTool("pods_delete", map[string]interface{}{"name": "a-pod-to-delete"}) t.Run("kubernetes.Interface propagates headers to Kube API", func(t *testing.T) { if len(pathHeaders) == 0 { t.Fatalf("No requests were made to Kube API") diff --git a/pkg/mcp/pods.go b/pkg/mcp/pods.go index 0ae8c68..055dcb8 100644 --- a/pkg/mcp/pods.go +++ b/pkg/mcp/pods.go @@ -127,7 +127,7 @@ func (s *Server) podsListInAllNamespaces(ctx context.Context, ctr mcp.CallToolRe AsTable: s.configuration.ListOutput.AsTable(), } if labelSelector != nil { - resourceListOptions.ListOptions.LabelSelector = labelSelector.(string) + resourceListOptions.LabelSelector = labelSelector.(string) } derived, err := s.k.Derived(ctx) if err != nil { @@ -150,7 +150,7 @@ func (s *Server) podsListInNamespace(ctx context.Context, ctr mcp.CallToolReques } labelSelector := ctr.GetArguments()["labelSelector"] if labelSelector != nil { - resourceListOptions.ListOptions.LabelSelector = labelSelector.(string) + resourceListOptions.LabelSelector = labelSelector.(string) } derived, err := s.k.Derived(ctx) if err != nil { diff --git a/pkg/mcp/pods_test.go b/pkg/mcp/pods_test.go index 278cf6e..fa1519d 100644 --- a/pkg/mcp/pods_test.go +++ b/pkg/mcp/pods_test.go @@ -482,7 +482,7 @@ func TestPodsDelete(t *testing.T) { }) t.Run("pods_delete with name and nil namespace deletes Pod", func(t *testing.T) { p, pErr := kc.CoreV1().Pods("default").Get(c.ctx, "a-pod-to-delete", metav1.GetOptions{}) - if pErr == nil && p != nil && p.ObjectMeta.DeletionTimestamp == nil { + if pErr == nil && p != nil && p.DeletionTimestamp == nil { t.Errorf("Pod not deleted") return } @@ -512,7 +512,7 @@ func TestPodsDelete(t *testing.T) { }) t.Run("pods_delete with name and namespace deletes Pod", func(t *testing.T) { p, pErr := kc.CoreV1().Pods("ns-1").Get(c.ctx, "a-pod-to-delete-in-ns-1", metav1.GetOptions{}) - if pErr == nil && p != nil && p.ObjectMeta.DeletionTimestamp == nil { + if pErr == nil && p != nil && p.DeletionTimestamp == nil { t.Errorf("Pod not deleted") return } @@ -549,12 +549,12 @@ func TestPodsDelete(t *testing.T) { }) t.Run("pods_delete with managed pod deletes Pod and Service", func(t *testing.T) { p, pErr := kc.CoreV1().Pods("default").Get(c.ctx, "a-managed-pod-to-delete", metav1.GetOptions{}) - if pErr == nil && p != nil && p.ObjectMeta.DeletionTimestamp == nil { + if pErr == nil && p != nil && p.DeletionTimestamp == nil { t.Errorf("Pod not deleted") return } s, sErr := kc.CoreV1().Services("default").Get(c.ctx, "a-managed-service-to-delete", metav1.GetOptions{}) - if sErr == nil && s != nil && s.ObjectMeta.DeletionTimestamp == nil { + if sErr == nil && s != nil && s.DeletionTimestamp == nil { t.Errorf("Service not deleted") return } @@ -621,7 +621,7 @@ func TestPodsDeleteInOpenShift(t *testing.T) { }) t.Run("pods_delete with managed pod in OpenShift deletes Pod and Route", func(t *testing.T) { p, pErr := kc.CoreV1().Pods("default").Get(c.ctx, "a-managed-pod-to-delete-in-openshift", metav1.GetOptions{}) - if pErr == nil && p != nil && p.ObjectMeta.DeletionTimestamp == nil { + if pErr == nil && p != nil && p.DeletionTimestamp == nil { t.Errorf("Pod not deleted") return } diff --git a/pkg/mcp/pods_top_test.go b/pkg/mcp/pods_top_test.go index f45add0..4c14097 100644 --- a/pkg/mcp/pods_top_test.go +++ b/pkg/mcp/pods_top_test.go @@ -112,7 +112,7 @@ func TestPodsTopMetricsAvailable(t *testing.T) { if podsTopDefaults.IsError { t.Fatalf("call tool failed %s", textContent) } - expectedHeaders := regexp.MustCompile("(?m)^\\s*NAMESPACE\\s+POD\\s+NAME\\s+CPU\\(cores\\)\\s+MEMORY\\(bytes\\)\\s*$") + expectedHeaders := regexp.MustCompile(`(?m)^\s*NAMESPACE\s+POD\s+NAME\s+CPU\(cores\)\s+MEMORY\(bytes\)\s*$`) if !expectedHeaders.MatchString(textContent) { t.Errorf("Expected headers '%s' not found in output:\n%s", expectedHeaders.String(), textContent) } @@ -126,7 +126,7 @@ func TestPodsTopMetricsAvailable(t *testing.T) { t.Errorf("Expected row '%s' not found in output:\n%s", row, textContent) } } - expectedTotal := regexp.MustCompile("(?m)^\\s+600m\\s+900Mi\\s*$") + expectedTotal := regexp.MustCompile(`(?m)^\s+600m\s+900Mi\s*$`) if !expectedTotal.MatchString(textContent) { t.Errorf("Expected total row '%s' not found in output:\n%s", expectedTotal.String(), textContent) } @@ -148,7 +148,7 @@ func TestPodsTopMetricsAvailable(t *testing.T) { t.Errorf("Expected row '%s' not found in output:\n%s", row, textContent) } } - expectedTotal := regexp.MustCompile("(?m)^\\s+40m\\s+60Mi\\s*$") + expectedTotal := regexp.MustCompile(`(?m)^\s+40m\s+60Mi\s*$`) if !expectedTotal.MatchString(textContent) { t.Errorf("Expected total row '%s' not found in output:\n%s", expectedTotal.String(), textContent) } @@ -161,11 +161,11 @@ func TestPodsTopMetricsAvailable(t *testing.T) { t.Fatalf("call tool failed %v", err) } textContent := podsTopNamespace.Content[0].(mcp.TextContent).Text - expectedRow := regexp.MustCompile("ns-5\\s+pod-ns-5-1\\s+container-1\\s+10m\\s+20Mi") + expectedRow := regexp.MustCompile(`ns-5\s+pod-ns-5-1\s+container-1\s+10m\s+20Mi`) if !expectedRow.MatchString(textContent) { t.Errorf("Expected row '%s' not found in output:\n%s", expectedRow.String(), textContent) } - expectedTotal := regexp.MustCompile("(?m)^\\s+10m\\s+20Mi\\s*$") + expectedTotal := regexp.MustCompile(`(?m)^\s+10m\s+20Mi\s*$`) if !expectedTotal.MatchString(textContent) { t.Errorf("Expected total row '%s' not found in output:\n%s", expectedTotal.String(), textContent) } @@ -179,11 +179,11 @@ func TestPodsTopMetricsAvailable(t *testing.T) { t.Fatalf("call tool failed %v", err) } textContent := podsTopNamespaceName.Content[0].(mcp.TextContent).Text - expectedRow := regexp.MustCompile("ns-5\\s+pod-ns-5-5\\s+container-1\\s+13m\\s+37Mi") + expectedRow := regexp.MustCompile(`ns-5\s+pod-ns-5-5\s+container-1\s+13m\s+37Mi`) if !expectedRow.MatchString(textContent) { t.Errorf("Expected row '%s' not found in output:\n%s", expectedRow.String(), textContent) } - expectedTotal := regexp.MustCompile("(?m)^\\s+13m\\s+37Mi\\s*$") + expectedTotal := regexp.MustCompile(`(?m)^\s+13m\s+37Mi\s*$`) if !expectedTotal.MatchString(textContent) { t.Errorf("Expected total row '%s' not found in output:\n%s", expectedTotal.String(), textContent) } @@ -196,11 +196,11 @@ func TestPodsTopMetricsAvailable(t *testing.T) { t.Fatalf("call tool failed %v", err) } textContent := podsTopNamespaceLabelSelector.Content[0].(mcp.TextContent).Text - expectedRow := regexp.MustCompile("ns-5\\s+pod-ns-5-42\\s+container-1\\s+42m\\s+42Mi") + expectedRow := regexp.MustCompile(`ns-5\s+pod-ns-5-42\s+container-1\s+42m\s+42Mi`) if !expectedRow.MatchString(textContent) { t.Errorf("Expected row '%s' not found in output:\n%s", expectedRow.String(), textContent) } - expectedTotal := regexp.MustCompile("(?m)^\\s+42m\\s+42Mi\\s*$") + expectedTotal := regexp.MustCompile(`(?m)^\s+42m\s+42Mi\s*$`) if !expectedTotal.MatchString(textContent) { t.Errorf("Expected total row '%s' not found in output:\n%s", expectedTotal.String(), textContent) } diff --git a/pkg/mcp/resources.go b/pkg/mcp/resources.go index 82ddf61..d44452e 100644 --- a/pkg/mcp/resources.go +++ b/pkg/mcp/resources.go @@ -116,7 +116,7 @@ func (s *Server) resourcesList(ctx context.Context, ctr mcp.CallToolRequest) (*m if !ok { return NewTextResult("", fmt.Errorf("labelSelector is not a string")), nil } - resourceListOptions.ListOptions.LabelSelector = l + resourceListOptions.LabelSelector = l } gvk, err := parseGroupVersionKind(ctr.GetArguments()) if err != nil { diff --git a/pkg/mcp/resources_test.go b/pkg/mcp/resources_test.go index 39b43bb..1c05b2e 100644 --- a/pkg/mcp/resources_test.go +++ b/pkg/mcp/resources_test.go @@ -736,7 +736,7 @@ func TestResourcesDelete(t *testing.T) { }) t.Run("resources_delete with valid namespaced resource deletes Namespace", func(t *testing.T) { ns, err := client.CoreV1().Namespaces().Get(c.ctx, "ns-to-delete", metav1.GetOptions{}) - if err == nil && ns != nil && ns.ObjectMeta.DeletionTimestamp == nil { + if err == nil && ns != nil && ns.DeletionTimestamp == nil { t.Fatalf("Namespace not deleted") return }