diff --git a/Makefile b/Makefile index 6863d2ec..6925717b 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 7fd1335a..186b50df 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 f39ab38b..607379b7 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 eafff040..df88530f 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 362c9e95..a2c15bf2 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 93199da4..4de6eeac 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 7b64c5f8..d6e64c18 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 340ed177..d1b0104f 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 a8bbb808..38637c92 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 39860f8f..29b865ff 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 94359e4b..e2659653 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 d3f1ca50..2c403c86 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 72568eca..9b2c78ef 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 0ae8c681..055dcb8e 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 278cf6e7..fa1519d5 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 f45add0d..4c140975 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 82ddf61a..d44452ea 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 39b43bb2..1c05b2e0 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 }