diff --git a/internal/tools/update-readme/main.go b/internal/tools/update-readme/main.go index d400afe4b..9273d15a5 100644 --- a/internal/tools/update-readme/main.go +++ b/internal/tools/update-readme/main.go @@ -9,8 +9,8 @@ import ( "slices" "strings" + "github.com/containers/kubernetes-mcp-server/pkg/api" "github.com/containers/kubernetes-mcp-server/pkg/config" - internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" "github.com/containers/kubernetes-mcp-server/pkg/toolsets" _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/config" @@ -26,7 +26,7 @@ func (o *OpenShift) IsOpenShift(_ context.Context) bool { return true } -var _ internalk8s.Openshift = (*OpenShift)(nil) +var _ api.Openshift = (*OpenShift)(nil) func main() { // Snyk reports false positive unless we flow the args through filepath.Clean and filepath.Localize in this specific order diff --git a/pkg/api/config/config.go b/pkg/api/config.go similarity index 83% rename from pkg/api/config/config.go rename to pkg/api/config.go index ee20d312a..472102bef 100644 --- a/pkg/api/config/config.go +++ b/pkg/api/config.go @@ -1,4 +1,4 @@ -package config +package api const ( ClusterProviderKubeConfig = "kubeconfig" @@ -18,20 +18,20 @@ type ClusterProvider interface { GetKubeConfigPath() string } -// Extended is the interface that all configuration extensions must implement. +// ExtendedConfig is the interface that all configuration extensions must implement. // Each extended config manager registers a factory function to parse its config from TOML primitives -type Extended interface { +type ExtendedConfig interface { // Validate validates the extended configuration. Returns an error if the configuration is invalid. Validate() error } -type ExtendedProvider interface { +type ExtendedConfigProvider interface { // GetProviderConfig returns the extended configuration for the given provider strategy. // The boolean return value indicates whether the configuration was found. - GetProviderConfig(strategy string) (Extended, bool) + GetProviderConfig(strategy string) (ExtendedConfig, bool) // GetToolsetConfig returns the extended configuration for the given toolset name. // The boolean return value indicates whether the configuration was found. - GetToolsetConfig(name string) (Extended, bool) + GetToolsetConfig(name string) (ExtendedConfig, bool) } type GroupVersionKind struct { @@ -49,5 +49,5 @@ type BaseConfig interface { AuthProvider ClusterProvider DeniedResourcesProvider - ExtendedProvider + ExtendedConfigProvider } diff --git a/pkg/api/imports_test.go b/pkg/api/imports_test.go new file mode 100644 index 000000000..31d648a49 --- /dev/null +++ b/pkg/api/imports_test.go @@ -0,0 +1,58 @@ +package api + +import ( + "go/build" + "strings" + "testing" + + "github.com/stretchr/testify/suite" +) + +const modulePrefix = "github.com/containers/kubernetes-mcp-server/" + +// ImportsSuite verifies that pkg/api doesn't accidentally import internal packages +// that would create cyclic dependencies. +type ImportsSuite struct { + suite.Suite +} + +func (s *ImportsSuite) TestNoCyclicDependencies() { + // Whitelist of allowed internal packages that pkg/api can import. + // Any other internal import will cause the test to fail. + allowedInternalPackages := map[string]bool{ + "github.com/containers/kubernetes-mcp-server/pkg/output": true, + } + + s.Run("pkg/api only imports whitelisted internal packages", func() { + pkg, err := build.Import("github.com/containers/kubernetes-mcp-server/pkg/api", "", 0) + s.Require().NoError(err, "Failed to import pkg/api") + + for _, imp := range pkg.Imports { + // Skip external packages (not part of this module) + if !strings.HasPrefix(imp, modulePrefix) { + continue + } + + // Internal package - must be in whitelist + if !allowedInternalPackages[imp] { + s.Failf("Forbidden internal import detected", + "pkg/api imports %q which is not in the whitelist. "+ + "To prevent cyclic dependencies, pkg/api can only import: %v. "+ + "If this import is intentional, add it to allowedInternalPackages in this test.", + imp, keys(allowedInternalPackages)) + } + } + }) +} + +func keys(m map[string]bool) []string { + result := make([]string, 0, len(m)) + for k := range m { + result = append(result, k) + } + return result +} + +func TestImports(t *testing.T) { + suite.Run(t, new(ImportsSuite)) +} diff --git a/pkg/api/kubernetes.go b/pkg/api/kubernetes.go new file mode 100644 index 000000000..37def5fc3 --- /dev/null +++ b/pkg/api/kubernetes.go @@ -0,0 +1,129 @@ +package api + +import ( + "context" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/metrics/pkg/apis/metrics" + metricsv1beta1 "k8s.io/metrics/pkg/client/clientset/versioned/typed/metrics/v1beta1" +) + +// Openshift provides OpenShift-specific detection capabilities. +// This interface is used by toolsets to conditionally enable OpenShift-specific tools. +type Openshift interface { + IsOpenShift(context.Context) bool +} + +// ListOptions contains options for listing Kubernetes resources. +type ListOptions struct { + metav1.ListOptions + AsTable bool +} + +// PodsTopOptions contains options for getting pod metrics. +type PodsTopOptions struct { + metav1.ListOptions + AllNamespaces bool + Namespace string + Name string +} + +// NodesTopOptions contains options for getting node metrics. +type NodesTopOptions struct { + metav1.ListOptions + Name string +} + +type KubernetesClientSet interface { + genericclioptions.RESTClientGetter + kubernetes.Interface + // NamespaceOrDefault returns the provided namespace or the default configured namespace if empty + NamespaceOrDefault(namespace string) string + RESTConfig() *rest.Config + RESTMapper() meta.ResettableRESTMapper + DiscoveryClient() discovery.CachedDiscoveryInterface + DynamicClient() dynamic.Interface + MetricsV1beta1Client() *metricsv1beta1.MetricsV1beta1Client +} + +// KubernetesClient defines the interface for Kubernetes operations that tool and prompt handlers need. +// This interface abstracts the concrete Kubernetes implementation to allow for better decoupling +// and testability. The pkg/kubernetes.Kubernetes type implements this interface. +// +// For toolsets that need direct access to the Kubernetes clientset (e.g., for DynamicClient), +// they can type-assert to ClientsetProvider. +type KubernetesClient interface { + // AccessControlClientset provides access to the underlying Kubernetes clientset with access control enforced + AccessControlClientset() KubernetesClientSet + + // --- Resource Operations --- + + // ResourcesList lists resources of the specified GroupVersionKind + ResourcesList(ctx context.Context, gvk *schema.GroupVersionKind, namespace string, options ListOptions) (runtime.Unstructured, error) + // ResourcesGet retrieves a single resource by name + ResourcesGet(ctx context.Context, gvk *schema.GroupVersionKind, namespace, name string) (*unstructured.Unstructured, error) + // ResourcesCreateOrUpdate creates or updates resources from a YAML/JSON string + ResourcesCreateOrUpdate(ctx context.Context, resource string) ([]*unstructured.Unstructured, error) + // ResourcesDelete deletes a resource by name + ResourcesDelete(ctx context.Context, gvk *schema.GroupVersionKind, namespace, name string) error + // ResourcesScale gets or sets the scale of a resource + ResourcesScale(ctx context.Context, gvk *schema.GroupVersionKind, namespace, name string, desiredScale int64, shouldScale bool) (*unstructured.Unstructured, error) + + // --- Namespace Operations --- + + // NamespacesList lists all namespaces + NamespacesList(ctx context.Context, options ListOptions) (runtime.Unstructured, error) + // ProjectsList lists all OpenShift projects + ProjectsList(ctx context.Context, options ListOptions) (runtime.Unstructured, error) + + // --- Pod Operations --- + + // PodsListInAllNamespaces lists pods across all namespaces + PodsListInAllNamespaces(ctx context.Context, options ListOptions) (runtime.Unstructured, error) + // PodsListInNamespace lists pods in a specific namespace + PodsListInNamespace(ctx context.Context, namespace string, options ListOptions) (runtime.Unstructured, error) + // PodsGet retrieves a single pod by name + PodsGet(ctx context.Context, namespace, name string) (*unstructured.Unstructured, error) + // PodsDelete deletes a pod and its managed resources + PodsDelete(ctx context.Context, namespace, name string) (string, error) + // PodsLog retrieves logs from a pod container + PodsLog(ctx context.Context, namespace, name, container string, previous bool, tail int64) (string, error) + // PodsRun creates and runs a new pod with optional service and route + PodsRun(ctx context.Context, namespace, name, image string, port int32) ([]*unstructured.Unstructured, error) + // PodsTop retrieves pod metrics + PodsTop(ctx context.Context, options PodsTopOptions) (*metrics.PodMetricsList, error) + // PodsExec executes a command in a pod container + PodsExec(ctx context.Context, namespace, name, container string, command []string) (string, error) + + // --- Node Operations --- + + // NodesLog retrieves logs from a node + NodesLog(ctx context.Context, name string, query string, tailLines int64) (string, error) + // NodesStatsSummary retrieves stats summary from a node + NodesStatsSummary(ctx context.Context, name string) (string, error) + // NodesTop retrieves node metrics + NodesTop(ctx context.Context, options NodesTopOptions) (*metrics.NodeMetricsList, error) + + // --- Event Operations --- + + // EventsList lists events in a namespace + EventsList(ctx context.Context, namespace string) ([]map[string]any, error) + + // --- Configuration Operations --- + + // ConfigurationContextsList returns the list of available context names + ConfigurationContextsList() (map[string]string, error) + // ConfigurationContextsDefault returns the current context name + ConfigurationContextsDefault() (string, error) + // ConfigurationView returns the kubeconfig content + ConfigurationView(minify bool) (runtime.Object, error) +} diff --git a/pkg/api/prompts.go b/pkg/api/prompts.go index bc1d8b9f9..c2e9f11fc 100644 --- a/pkg/api/prompts.go +++ b/pkg/api/prompts.go @@ -1,10 +1,6 @@ package api -import ( - "context" - - internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" -) +import "context" // ServerPrompt represents a prompt that can be registered with the MCP server. // Prompts provide pre-defined workflow templates and guidance to AI assistants. @@ -88,7 +84,8 @@ func NewPromptCallResult(description string, messages []PromptMessage, err error // PromptHandlerParams contains the parameters passed to a prompt handler type PromptHandlerParams struct { context.Context - *internalk8s.Kubernetes + ExtendedConfigProvider + KubernetesClient PromptCallRequest } diff --git a/pkg/api/toolsets.go b/pkg/api/toolsets.go index c5960e3f0..3084f2954 100644 --- a/pkg/api/toolsets.go +++ b/pkg/api/toolsets.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" - internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" "github.com/containers/kubernetes-mcp-server/pkg/output" "github.com/google/jsonschema-go/jsonschema" ) @@ -43,7 +42,7 @@ type Toolset interface { // GetDescription returns a human-readable description of the toolset. // Will be used to generate documentation and help text. GetDescription() string - GetTools(o internalk8s.Openshift) []ServerTool + GetTools(o Openshift) []ServerTool } type ToolCallRequest interface { @@ -66,7 +65,8 @@ func NewToolCallResult(content string, err error) *ToolCallResult { type ToolHandlerParams struct { context.Context - *internalk8s.Kubernetes + ExtendedConfigProvider + KubernetesClient ToolCallRequest ListOutput output.Output } diff --git a/pkg/config/config.go b/pkg/config/config.go index 7a459a828..042b70514 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -10,7 +10,7 @@ import ( "strings" "github.com/BurntSushi/toml" - configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config" + "github.com/containers/kubernetes-mcp-server/pkg/api" "k8s.io/klog/v2" ) @@ -21,7 +21,7 @@ const ( // StaticConfig is the configuration for the server. // It allows to configure server specific settings and tools to be enabled or disabled. type StaticConfig struct { - DeniedResources []configapi.GroupVersionKind `toml:"denied_resources"` + DeniedResources []api.GroupVersionKind `toml:"denied_resources"` LogLevel int `toml:"log_level,omitzero"` Port string `toml:"port,omitempty"` @@ -82,15 +82,15 @@ type StaticConfig struct { promptsMetadata toml.MetaData // Internal: metadata for prompts decoding // Internal: parsed provider configs (not exposed to TOML package) - parsedClusterProviderConfigs map[string]configapi.Extended + parsedClusterProviderConfigs map[string]api.ExtendedConfig // Internal: parsed toolset configs (not exposed to TOML package) - parsedToolsetConfigs map[string]configapi.Extended + parsedToolsetConfigs map[string]api.ExtendedConfig // Internal: the config.toml directory, to help resolve relative file paths configDirPath string } -var _ configapi.BaseConfig = (*StaticConfig)(nil) +var _ api.BaseConfig = (*StaticConfig)(nil) type ReadConfigOpt func(cfg *StaticConfig) @@ -306,7 +306,7 @@ func (c *StaticConfig) GetClusterProviderStrategy() string { return c.ClusterProviderStrategy } -func (c *StaticConfig) GetDeniedResources() []configapi.GroupVersionKind { +func (c *StaticConfig) GetDeniedResources() []api.GroupVersionKind { return c.DeniedResources } @@ -314,13 +314,13 @@ func (c *StaticConfig) GetKubeConfigPath() string { return c.KubeConfig } -func (c *StaticConfig) GetProviderConfig(strategy string) (configapi.Extended, bool) { +func (c *StaticConfig) GetProviderConfig(strategy string) (api.ExtendedConfig, bool) { cfg, ok := c.parsedClusterProviderConfigs[strategy] return cfg, ok } -func (c *StaticConfig) GetToolsetConfig(name string) (configapi.Extended, bool) { +func (c *StaticConfig) GetToolsetConfig(name string) (api.ExtendedConfig, bool) { cfg, ok := c.parsedToolsetConfigs[name] return cfg, ok } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 547bd1685..4116f008f 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config" + "github.com/containers/kubernetes-mcp-server/pkg/api" "github.com/stretchr/testify/suite" ) @@ -137,11 +137,11 @@ func (s *ConfigSuite) TestReadConfigValid() { s.Run("denied_resources", func() { s.Require().Lenf(config.DeniedResources, 2, "Expected 2 denied resources, got %d", len(config.DeniedResources)) s.Run("contains apps/v1/Deployment", func() { - s.Contains(config.DeniedResources, configapi.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + s.Contains(config.DeniedResources, api.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, "Expected denied resources to contain apps/v1/Deployment") }) s.Run("contains rbac.authorization.k8s.io/v1/Role", func() { - s.Contains(config.DeniedResources, configapi.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "Role"}, + s.Contains(config.DeniedResources, api.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "Role"}, "Expected denied resources to contain rbac.authorization.k8s.io/v1/Role") }) }) @@ -778,16 +778,16 @@ func (s *ConfigSuite) TestDropInWithDeniedResources() { s.Run("drop-in replaces denied_resources array", func() { s.Len(config.DeniedResources, 2, "denied_resources should have 2 entries from drop-in") - s.Contains(config.DeniedResources, configapi.GroupVersionKind{ + s.Contains(config.DeniedResources, api.GroupVersionKind{ Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRole", }) - s.Contains(config.DeniedResources, configapi.GroupVersionKind{ + s.Contains(config.DeniedResources, api.GroupVersionKind{ Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBinding", }) }) s.Run("original denied_resources from main config are replaced", func() { - s.NotContains(config.DeniedResources, configapi.GroupVersionKind{ + s.NotContains(config.DeniedResources, api.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "Deployment", }, "original entry should be replaced by drop-in") }) diff --git a/pkg/config/extended.go b/pkg/config/extended.go index eb3e37eda..2234073ca 100644 --- a/pkg/config/extended.go +++ b/pkg/config/extended.go @@ -5,10 +5,10 @@ import ( "fmt" "github.com/BurntSushi/toml" - configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config" + "github.com/containers/kubernetes-mcp-server/pkg/api" ) -type ExtendedConfigParser func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (configapi.Extended, error) +type ExtendedConfigParser func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (api.ExtendedConfig, error) type extendedConfigRegistry struct { parsers map[string]ExtendedConfigParser @@ -28,11 +28,11 @@ func (r *extendedConfigRegistry) register(name string, parser ExtendedConfigPars r.parsers[name] = parser } -func (r *extendedConfigRegistry) parse(ctx context.Context, metaData toml.MetaData, configs map[string]toml.Primitive) (map[string]configapi.Extended, error) { +func (r *extendedConfigRegistry) parse(ctx context.Context, metaData toml.MetaData, configs map[string]toml.Primitive) (map[string]api.ExtendedConfig, error) { if len(configs) == 0 { - return make(map[string]configapi.Extended), nil + return make(map[string]api.ExtendedConfig), nil } - parsedConfigs := make(map[string]configapi.Extended, len(configs)) + parsedConfigs := make(map[string]api.ExtendedConfig, len(configs)) for name, primitive := range configs { parser, ok := r.parsers[name] diff --git a/pkg/config/provider_config_test.go b/pkg/config/provider_config_test.go index 28d511138..b45994b11 100644 --- a/pkg/config/provider_config_test.go +++ b/pkg/config/provider_config_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/BurntSushi/toml" - configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config" + "github.com/containers/kubernetes-mcp-server/pkg/api" "github.com/stretchr/testify/suite" ) @@ -32,7 +32,7 @@ type ProviderConfigForTest struct { IntProp int `toml:"int_prop"` } -var _ configapi.Extended = (*ProviderConfigForTest)(nil) +var _ api.ExtendedConfig = (*ProviderConfigForTest)(nil) func (p *ProviderConfigForTest) Validate() error { if p.StrProp == "force-error" { @@ -41,7 +41,7 @@ func (p *ProviderConfigForTest) Validate() error { return nil } -func providerConfigForTestParser(_ context.Context, primitive toml.Primitive, md toml.MetaData) (configapi.Extended, error) { +func providerConfigForTestParser(_ context.Context, primitive toml.Primitive, md toml.MetaData) (api.ExtendedConfig, error) { var providerConfigForTest ProviderConfigForTest if err := md.PrimitiveDecode(primitive, &providerConfigForTest); err != nil { return nil, err @@ -130,7 +130,7 @@ func (s *ProviderConfigSuite) TestReadConfigUnregisteredProviderConfig() { } func (s *ProviderConfigSuite) TestReadConfigParserError() { - RegisterProviderConfig("test", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (configapi.Extended, error) { + RegisterProviderConfig("test", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (api.ExtendedConfig, error) { return nil, errors.New("parser error forced by test") }) invalidConfigPath := s.writeConfig(` @@ -153,7 +153,7 @@ func (s *ProviderConfigSuite) TestReadConfigParserError() { func (s *ProviderConfigSuite) TestConfigDirPathInContext() { var capturedDirPath string - RegisterProviderConfig("test", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (configapi.Extended, error) { + RegisterProviderConfig("test", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (api.ExtendedConfig, error) { capturedDirPath = ConfigDirPathFromContext(ctx) var providerConfigForTest ProviderConfigForTest if err := md.PrimitiveDecode(primitive, &providerConfigForTest); err != nil { @@ -329,7 +329,7 @@ func (s *ProviderConfigSuite) TestStandaloneConfigDirWithExtendedConfig() { func (s *ProviderConfigSuite) TestConfigDirPathInContextStandalone() { // Test that configDirPath is correctly set in context for standalone --config-dir var capturedDirPath string - RegisterProviderConfig("test", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (configapi.Extended, error) { + RegisterProviderConfig("test", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (api.ExtendedConfig, error) { capturedDirPath = ConfigDirPathFromContext(ctx) var providerConfigForTest ProviderConfigForTest if err := md.PrimitiveDecode(primitive, &providerConfigForTest); err != nil { diff --git a/pkg/config/toolset_config_test.go b/pkg/config/toolset_config_test.go index b8045b064..36929fcd2 100644 --- a/pkg/config/toolset_config_test.go +++ b/pkg/config/toolset_config_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/BurntSushi/toml" - configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config" + "github.com/containers/kubernetes-mcp-server/pkg/api" "github.com/stretchr/testify/suite" ) @@ -32,7 +32,7 @@ type ToolsetConfigForTest struct { Timeout int `toml:"timeout"` } -var _ configapi.Extended = (*ToolsetConfigForTest)(nil) +var _ api.ExtendedConfig = (*ToolsetConfigForTest)(nil) func (t *ToolsetConfigForTest) Validate() error { if t.Endpoint == "force-error" { @@ -41,7 +41,7 @@ func (t *ToolsetConfigForTest) Validate() error { return nil } -func toolsetConfigForTestParser(_ context.Context, primitive toml.Primitive, md toml.MetaData) (configapi.Extended, error) { +func toolsetConfigForTestParser(_ context.Context, primitive toml.Primitive, md toml.MetaData) (api.ExtendedConfig, error) { var toolsetConfigForTest ToolsetConfigForTest if err := md.PrimitiveDecode(primitive, &toolsetConfigForTest); err != nil { return nil, err @@ -128,7 +128,7 @@ func (s *ToolsetConfigSuite) TestReadConfigUnregisteredToolsetConfig() { func (s *ToolsetConfigSuite) TestConfigDirPathInContext() { var capturedDirPath string - RegisterToolsetConfig("test-toolset", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (configapi.Extended, error) { + RegisterToolsetConfig("test-toolset", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (api.ExtendedConfig, error) { capturedDirPath = ConfigDirPathFromContext(ctx) var toolsetConfigForTest ToolsetConfigForTest if err := md.PrimitiveDecode(primitive, &toolsetConfigForTest); err != nil { @@ -300,7 +300,7 @@ func (s *ToolsetConfigSuite) TestStandaloneConfigDirWithExtendedConfig() { func (s *ToolsetConfigSuite) TestConfigDirPathInContextStandalone() { // Test that configDirPath is correctly set in context for standalone --config-dir var capturedDirPath string - RegisterToolsetConfig("test-toolset", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (configapi.Extended, error) { + RegisterToolsetConfig("test-toolset", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (api.ExtendedConfig, error) { capturedDirPath = ConfigDirPathFromContext(ctx) var toolsetConfigForTest ToolsetConfigForTest if err := md.PrimitiveDecode(primitive, &toolsetConfigForTest); err != nil { diff --git a/pkg/http/http_test.go b/pkg/http/http_test.go index 0b0f0de0a..0b7aa1020 100644 --- a/pkg/http/http_test.go +++ b/pkg/http/http_test.go @@ -19,6 +19,7 @@ import ( "time" "github.com/containers/kubernetes-mcp-server/internal/test" + "github.com/containers/kubernetes-mcp-server/pkg/api" "github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc/oidctest" "github.com/stretchr/testify/suite" @@ -26,7 +27,6 @@ import ( "k8s.io/klog/v2" "k8s.io/klog/v2/textlogger" - configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config" "github.com/containers/kubernetes-mcp-server/pkg/config" "github.com/containers/kubernetes-mcp-server/pkg/mcp" ) @@ -241,7 +241,7 @@ func TestHealthCheck(t *testing.T) { }) }) // Health exposed even when require Authorization - testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, ValidateToken: true, ClusterProviderStrategy: configapi.ClusterProviderKubeConfig}}, func(ctx *httpContext) { + testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, ValidateToken: true, ClusterProviderStrategy: api.ClusterProviderKubeConfig}}, func(ctx *httpContext) { resp, err := http.Get(fmt.Sprintf("http://%s/healthz", ctx.HttpAddress)) if err != nil { t.Fatalf("Failed to get health check endpoint with OAuth: %v", err) @@ -262,7 +262,7 @@ func TestWellKnownReverseProxy(t *testing.T) { ".well-known/openid-configuration", } // With No Authorization URL configured - testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, ValidateToken: true, ClusterProviderStrategy: configapi.ClusterProviderKubeConfig}}, func(ctx *httpContext) { + testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, ValidateToken: true, ClusterProviderStrategy: api.ClusterProviderKubeConfig}}, func(ctx *httpContext) { for _, path := range cases { resp, err := http.Get(fmt.Sprintf("http://%s/%s", ctx.HttpAddress, path)) t.Cleanup(func() { _ = resp.Body.Close() }) @@ -286,7 +286,7 @@ func TestWellKnownReverseProxy(t *testing.T) { AuthorizationURL: invalidPayloadServer.URL, RequireOAuth: true, ValidateToken: true, - ClusterProviderStrategy: configapi.ClusterProviderKubeConfig, + ClusterProviderStrategy: api.ClusterProviderKubeConfig, } testCaseWithContext(t, &httpContext{StaticConfig: invalidPayloadConfig}, func(ctx *httpContext) { for _, path := range cases { @@ -316,7 +316,7 @@ func TestWellKnownReverseProxy(t *testing.T) { AuthorizationURL: testServer.URL, RequireOAuth: true, ValidateToken: true, - ClusterProviderStrategy: configapi.ClusterProviderKubeConfig, + ClusterProviderStrategy: api.ClusterProviderKubeConfig, } testCaseWithContext(t, &httpContext{StaticConfig: staticConfig}, func(ctx *httpContext) { for _, path := range cases { @@ -366,7 +366,7 @@ func TestWellKnownHeaderPropagation(t *testing.T) { AuthorizationURL: testServer.URL, RequireOAuth: true, ValidateToken: true, - ClusterProviderStrategy: configapi.ClusterProviderKubeConfig, + ClusterProviderStrategy: api.ClusterProviderKubeConfig, } testCaseWithContext(t, &httpContext{StaticConfig: staticConfig}, func(ctx *httpContext) { for _, path := range cases { @@ -480,7 +480,7 @@ func TestWellKnownOverrides(t *testing.T) { AuthorizationURL: testServer.URL, RequireOAuth: true, ValidateToken: true, - ClusterProviderStrategy: configapi.ClusterProviderKubeConfig, + ClusterProviderStrategy: api.ClusterProviderKubeConfig, } // With Dynamic Client Registration disabled disableDynamicRegistrationConfig := baseConfig diff --git a/pkg/kiali/config.go b/pkg/kiali/config.go index b36899015..4f5cb4fa3 100644 --- a/pkg/kiali/config.go +++ b/pkg/kiali/config.go @@ -10,7 +10,7 @@ import ( "strings" "github.com/BurntSushi/toml" - configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config" + "github.com/containers/kubernetes-mcp-server/pkg/api" "github.com/containers/kubernetes-mcp-server/pkg/config" ) @@ -21,7 +21,7 @@ type Config struct { CertificateAuthority string `toml:"certificate_authority,omitempty"` } -var _ configapi.Extended = (*Config)(nil) +var _ api.ExtendedConfig = (*Config)(nil) func (c *Config) Validate() error { if c == nil { @@ -46,7 +46,7 @@ func (c *Config) Validate() error { return nil } -func kialiToolsetParser(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (configapi.Extended, error) { +func kialiToolsetParser(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (api.ExtendedConfig, error) { var cfg Config if err := md.PrimitiveDecode(primitive, &cfg); err != nil { return nil, err diff --git a/pkg/kiali/kiali.go b/pkg/kiali/kiali.go index 66c23fe82..592f6c2ae 100644 --- a/pkg/kiali/kiali.go +++ b/pkg/kiali/kiali.go @@ -11,7 +11,7 @@ import ( "os" "strings" - "github.com/containers/kubernetes-mcp-server/pkg/api/config" + "github.com/containers/kubernetes-mcp-server/pkg/api" "k8s.io/client-go/rest" "k8s.io/klog/v2" ) @@ -24,7 +24,7 @@ type Kiali struct { } // NewKiali creates a new Kiali instance -func NewKiali(configProvider config.ExtendedProvider, kubernetes *rest.Config) *Kiali { +func NewKiali(configProvider api.ExtendedConfigProvider, kubernetes *rest.Config) *Kiali { kiali := &Kiali{bearerToken: kubernetes.BearerToken} if cfg, ok := configProvider.GetToolsetConfig("kiali"); ok { if kc, ok := cfg.(*Config); ok && kc != nil { diff --git a/pkg/kubernetes-mcp-server/cmd/root.go b/pkg/kubernetes-mcp-server/cmd/root.go index e6c50b8ea..183495648 100644 --- a/pkg/kubernetes-mcp-server/cmd/root.go +++ b/pkg/kubernetes-mcp-server/cmd/root.go @@ -24,7 +24,7 @@ import ( "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" - configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config" + "github.com/containers/kubernetes-mcp-server/pkg/api" "github.com/containers/kubernetes-mcp-server/pkg/config" internalhttp "github.com/containers/kubernetes-mcp-server/pkg/http" "github.com/containers/kubernetes-mcp-server/pkg/mcp" @@ -224,7 +224,7 @@ func (m *MCPServerOptions) loadFlags(cmd *cobra.Command) { m.StaticConfig.CertificateAuthority = m.CertificateAuthority } if cmd.Flag(flagDisableMultiCluster).Changed && m.DisableMultiCluster { - m.StaticConfig.ClusterProviderStrategy = configapi.ClusterProviderDisabled + m.StaticConfig.ClusterProviderStrategy = api.ClusterProviderDisabled } } diff --git a/pkg/kubernetes/accesscontrol_client_set.go b/pkg/kubernetes/accesscontrol_client_set.go index c27fbf983..c9667a806 100644 --- a/pkg/kubernetes/accesscontrol_client_set.go +++ b/pkg/kubernetes/accesscontrol_client_set.go @@ -4,15 +4,12 @@ import ( "fmt" "net/http" - configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config" + "github.com/containers/kubernetes-mcp-server/pkg/api" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/client-go/discovery" "k8s.io/client-go/discovery/cached/memory" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" - authenticationv1 "k8s.io/client-go/kubernetes/typed/authentication/v1" - authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1" - corev1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/rest" "k8s.io/client-go/restmapper" "k8s.io/client-go/tools/clientcmd" @@ -24,52 +21,58 @@ import ( // apiVersion and kinds are checked for allowed access type AccessControlClientset struct { kubernetes.Interface - config configapi.BaseConfig + config api.BaseConfig clientCmdConfig clientcmd.ClientConfig - cfg *rest.Config + restConfig *rest.Config restMapper meta.ResettableRESTMapper discoveryClient discovery.CachedDiscoveryInterface dynamicClient dynamic.Interface metricsV1beta1 *metricsv1beta1.MetricsV1beta1Client } -func NewAccessControlClientset(config configapi.BaseConfig, clientCmdConfig clientcmd.ClientConfig, restConfig *rest.Config) (*AccessControlClientset, error) { +var _ api.KubernetesClientSet = (*AccessControlClientset)(nil) + +func NewAccessControlClientset(config api.BaseConfig, clientCmdConfig clientcmd.ClientConfig, restConfig *rest.Config) (*AccessControlClientset, error) { acc := &AccessControlClientset{ config: config, clientCmdConfig: clientCmdConfig, - cfg: rest.CopyConfig(restConfig), + restConfig: rest.CopyConfig(restConfig), } - if acc.cfg.UserAgent == "" { - acc.cfg.UserAgent = rest.DefaultKubernetesUserAgent() + if acc.restConfig.UserAgent == "" { + acc.restConfig.UserAgent = rest.DefaultKubernetesUserAgent() } - acc.cfg.Wrap(func(original http.RoundTripper) http.RoundTripper { + acc.restConfig.Wrap(func(original http.RoundTripper) http.RoundTripper { return &AccessControlRoundTripper{ delegate: original, deniedResourcesProvider: config, restMapper: acc.restMapper, } }) - discoveryClient, err := discovery.NewDiscoveryClientForConfig(acc.cfg) + discoveryClient, err := discovery.NewDiscoveryClientForConfig(acc.restConfig) if err != nil { return nil, fmt.Errorf("failed to create discovery client: %v", err) } acc.discoveryClient = memory.NewMemCacheClient(discoveryClient) acc.restMapper = restmapper.NewDeferredDiscoveryRESTMapper(acc.discoveryClient) - acc.Interface, err = kubernetes.NewForConfig(acc.cfg) + acc.Interface, err = kubernetes.NewForConfig(acc.restConfig) if err != nil { return nil, err } - acc.dynamicClient, err = dynamic.NewForConfig(acc.cfg) + acc.dynamicClient, err = dynamic.NewForConfig(acc.restConfig) if err != nil { return nil, err } - acc.metricsV1beta1, err = metricsv1beta1.NewForConfig(acc.cfg) + acc.metricsV1beta1, err = metricsv1beta1.NewForConfig(acc.restConfig) if err != nil { return nil, err } return acc, nil } +func (a *AccessControlClientset) RESTConfig() *rest.Config { + return a.restConfig +} + func (a *AccessControlClientset) RESTMapper() meta.ResettableRESTMapper { return a.restMapper } @@ -86,34 +89,31 @@ func (a *AccessControlClientset) MetricsV1beta1Client() *metricsv1beta1.MetricsV return a.metricsV1beta1 } -// Nodes returns NodeInterface -// Deprecated: use CoreV1().Nodes() directly -func (a *AccessControlClientset) Nodes() (corev1.NodeInterface, error) { - return a.CoreV1().Nodes(), nil +func (a *AccessControlClientset) configuredNamespace() string { + if ns, _, nsErr := a.ToRawKubeConfigLoader().Namespace(); nsErr == nil { + return ns + } + return "" } -// Pods returns PodInterface -// Deprecated: use CoreV1().Pods(namespace) directly -func (a *AccessControlClientset) Pods(namespace string) (corev1.PodInterface, error) { - return a.CoreV1().Pods(namespace), nil +func (a *AccessControlClientset) NamespaceOrDefault(namespace string) string { + if namespace == "" { + return a.configuredNamespace() + } + return namespace } -// Services returns ServiceInterface -// Deprecated: use CoreV1().Services(namespace) directly -func (a *AccessControlClientset) Services(namespace string) (corev1.ServiceInterface, error) { - return a.CoreV1().Services(namespace), nil +func (a *AccessControlClientset) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) { + return a.DiscoveryClient(), nil } -// SelfSubjectAccessReviews returns SelfSubjectAccessReviewInterface -// Deprecated: use AuthorizationV1().SelfSubjectAccessReviews() directly -func (a *AccessControlClientset) SelfSubjectAccessReviews() (authorizationv1.SelfSubjectAccessReviewInterface, error) { - return a.AuthorizationV1().SelfSubjectAccessReviews(), nil +func (a *AccessControlClientset) ToRESTMapper() (meta.RESTMapper, error) { + return a.RESTMapper(), nil } -// TokenReview returns TokenReviewInterface -// Deprecated: use AuthenticationV1().TokenReviews() directly -func (a *AccessControlClientset) TokenReview() (authenticationv1.TokenReviewInterface, error) { - return a.AuthenticationV1().TokenReviews(), nil +// ToRESTConfig returns the rest.Config object (genericclioptions.RESTClientGetter) +func (a *AccessControlClientset) ToRESTConfig() (*rest.Config, error) { + return a.RESTConfig(), nil } // ToRawKubeConfigLoader returns the clientcmd.ClientConfig object (genericclioptions.RESTClientGetter) diff --git a/pkg/kubernetes/accesscontrol_round_tripper.go b/pkg/kubernetes/accesscontrol_round_tripper.go index 887a94f13..1e4c365e9 100644 --- a/pkg/kubernetes/accesscontrol_round_tripper.go +++ b/pkg/kubernetes/accesscontrol_round_tripper.go @@ -5,14 +5,14 @@ import ( "net/http" "strings" - configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config" + "github.com/containers/kubernetes-mcp-server/pkg/api" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime/schema" ) type AccessControlRoundTripper struct { delegate http.RoundTripper - deniedResourcesProvider configapi.DeniedResourcesProvider + deniedResourcesProvider api.DeniedResourcesProvider restMapper meta.RESTMapper } diff --git a/pkg/kubernetes/configuration.go b/pkg/kubernetes/configuration.go index 93566a809..ff22e53e6 100644 --- a/pkg/kubernetes/configuration.go +++ b/pkg/kubernetes/configuration.go @@ -1,7 +1,7 @@ package kubernetes import ( - configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config" + "github.com/containers/kubernetes-mcp-server/pkg/api" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/rest" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" @@ -22,7 +22,7 @@ var InClusterConfig = func() (*rest.Config, error) { return inClusterConfig, err } -func IsInCluster(cfg configapi.ClusterProvider) bool { +func IsInCluster(cfg api.ClusterProvider) bool { // Even if running in-cluster, if a kubeconfig is provided, we consider it as out-of-cluster if cfg != nil && cfg.GetKubeConfigPath() != "" { return false @@ -31,17 +31,10 @@ func IsInCluster(cfg configapi.ClusterProvider) bool { return err == nil && restConfig != nil } -func (k *Kubernetes) NamespaceOrDefault(namespace string) string { - if namespace == "" { - return k.configuredNamespace() - } - return namespace -} - // ConfigurationContextsDefault returns the current context name // TODO: Should be moved to the Provider level ? func (k *Kubernetes) ConfigurationContextsDefault() (string, error) { - cfg, err := k.ToRawKubeConfigLoader().RawConfig() + cfg, err := k.AccessControlClientset().ToRawKubeConfigLoader().RawConfig() if err != nil { return "", err } @@ -51,7 +44,7 @@ func (k *Kubernetes) ConfigurationContextsDefault() (string, error) { // ConfigurationContextsList returns the list of available context names // TODO: Should be moved to the Provider level ? func (k *Kubernetes) ConfigurationContextsList() (map[string]string, error) { - cfg, err := k.ToRawKubeConfigLoader().RawConfig() + cfg, err := k.AccessControlClientset().ToRawKubeConfigLoader().RawConfig() if err != nil { return nil, err } @@ -74,7 +67,7 @@ func (k *Kubernetes) ConfigurationContextsList() (map[string]string, error) { func (k *Kubernetes) ConfigurationView(minify bool) (runtime.Object, error) { var cfg clientcmdapi.Config var err error - if cfg, err = k.ToRawKubeConfigLoader().RawConfig(); err != nil { + if cfg, err = k.AccessControlClientset().ToRawKubeConfigLoader().RawConfig(); err != nil { return nil, err } if minify { diff --git a/pkg/kubernetes/events.go b/pkg/kubernetes/events.go index e40720a35..ae53e6995 100644 --- a/pkg/kubernetes/events.go +++ b/pkg/kubernetes/events.go @@ -2,18 +2,20 @@ package kubernetes import ( "context" + "strings" + + "github.com/containers/kubernetes-mcp-server/pkg/api" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "strings" ) func (k *Kubernetes) EventsList(ctx context.Context, namespace string) ([]map[string]any, error) { var eventMap []map[string]any raw, err := k.ResourcesList(ctx, &schema.GroupVersionKind{ Group: "", Version: "v1", Kind: "Event", - }, namespace, ResourceListOptions{}) + }, namespace, api.ListOptions{}) if err != nil { return eventMap, err } diff --git a/pkg/kubernetes/kubernetes.go b/pkg/kubernetes/kubernetes.go index e594cf072..a13a1a8b4 100644 --- a/pkg/kubernetes/kubernetes.go +++ b/pkg/kubernetes/kubernetes.go @@ -1,16 +1,10 @@ package kubernetes import ( - "k8s.io/apimachinery/pkg/api/meta" + "github.com/containers/kubernetes-mcp-server/pkg/api" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/discovery" "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" - - "github.com/containers/kubernetes-mcp-server/pkg/helm" - "github.com/containers/kubernetes-mcp-server/pkg/kiali" ) type HeaderKey string @@ -31,45 +25,10 @@ type Kubernetes struct { accessControlClientSet *AccessControlClientset } -var _ helm.Kubernetes = (*Kubernetes)(nil) +var _ api.KubernetesClient = (*Kubernetes)(nil) // AccessControlClientset returns the access-controlled clientset // This ensures that any denied resources configured in the system are properly enforced -func (k *Kubernetes) AccessControlClientset() *AccessControlClientset { +func (k *Kubernetes) AccessControlClientset() api.KubernetesClientSet { return k.accessControlClientSet } - -func (k *Kubernetes) NewHelm() *helm.Helm { - // This is a derived Kubernetes, so it already has the Helm initialized - return helm.NewHelm(k) -} - -// NewKiali returns a Kiali client initialized with the same StaticConfig and bearer token -// as the underlying derived Kubernetes manager. -func (k *Kubernetes) NewKiali() *kiali.Kiali { - return kiali.NewKiali(k.AccessControlClientset().config, k.AccessControlClientset().cfg) -} - -func (k *Kubernetes) configuredNamespace() string { - if ns, _, nsErr := k.AccessControlClientset().ToRawKubeConfigLoader().Namespace(); nsErr == nil { - return ns - } - return "" -} - -func (k *Kubernetes) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) { - return k.AccessControlClientset().DiscoveryClient(), nil -} - -func (k *Kubernetes) ToRESTMapper() (meta.RESTMapper, error) { - return k.AccessControlClientset().RESTMapper(), nil -} - -// ToRESTConfig returns the rest.Config object (genericclioptions.RESTClientGetter) -func (k *Kubernetes) ToRESTConfig() (*rest.Config, error) { - return k.AccessControlClientset().cfg, nil -} - -func (k *Kubernetes) ToRawKubeConfigLoader() clientcmd.ClientConfig { - return k.AccessControlClientset().ToRawKubeConfigLoader() -} diff --git a/pkg/kubernetes/kubernetes_derived_test.go b/pkg/kubernetes/kubernetes_derived_test.go index f75e65993..5ac84e059 100644 --- a/pkg/kubernetes/kubernetes_derived_test.go +++ b/pkg/kubernetes/kubernetes_derived_test.go @@ -76,14 +76,14 @@ users: derived, err := testManager.Derived(ctx) s.Require().NoErrorf(err, "failed to create derived kubernetes: %v", err) - s.NotEqual(derived.AccessControlClientset(), testManager.accessControlClientset, "expected new derived clientset, got original clientset") - s.Equal(derived.AccessControlClientset().config, testStaticConfig, "config not properly wired to derived clientset") + s.NotEqual(derived.accessControlClientSet, testManager.accessControlClientset, "expected new derived clientset, got original clientset") + s.Equal(derived.accessControlClientSet.config, testStaticConfig, "config not properly wired to derived clientset") s.Run("RestConfig is correctly copied and sensitive fields are omitted", func() { - derivedCfg := derived.AccessControlClientset().cfg + derivedCfg := derived.AccessControlClientset().RESTConfig() s.Require().NotNil(derivedCfg, "derived config is nil") - originalCfg := testManager.accessControlClientset.cfg + originalCfg := testManager.accessControlClientset.RESTConfig() s.Equalf(originalCfg.Host, derivedCfg.Host, "expected Host %s, got %s", originalCfg.Host, derivedCfg.Host) s.Equalf(originalCfg.APIPath, derivedCfg.APIPath, "expected APIPath %s, got %s", originalCfg.APIPath, derivedCfg.APIPath) s.Equalf(originalCfg.QPS, derivedCfg.QPS, "expected QPS %f, got %f", originalCfg.QPS, derivedCfg.QPS) @@ -130,7 +130,7 @@ users: s.Run("derived kubernetes has initialized clients", func() { // Verify that the derived kubernetes has proper clients initialized s.NotNilf(derived.AccessControlClientset(), "expected accessControlClientSet to be initialized") - s.Equalf(testStaticConfig, derived.AccessControlClientset().config, "config not properly wired to derived clientset") + s.Equalf(testStaticConfig, derived.accessControlClientSet.config, "config not properly wired to derived clientset") s.NotNilf(derived.AccessControlClientset().RESTMapper(), "expected accessControlRESTMapper to be initialized") s.NotNilf(derived.AccessControlClientset().DiscoveryClient(), "expected discoveryClient to be initialized") s.NotNilf(derived.AccessControlClientset().DynamicClient(), "expected dynamicClient to be initialized") @@ -154,7 +154,7 @@ users: badConfig := test.Must(config.ReadToml([]byte(` kubeconfig = "` + strings.ReplaceAll(badKubeconfigPath, `\`, `\\`) + `" `))) - badManager, _ := NewManager(badConfig, testManager.accessControlClientset.cfg, testManager.accessControlClientset.clientCmdConfig) + badManager, _ := NewManager(badConfig, testManager.accessControlClientset.RESTConfig(), testManager.accessControlClientset.ToRawKubeConfigLoader()) // Replace the clientCmdConfig with one that will fail pathOptions := clientcmd.NewDefaultPathOptions() pathOptions.LoadingRules.ExplicitPath = badKubeconfigPath @@ -189,7 +189,7 @@ users: s.Require().NoErrorf(err, "failed to create test manager: %v", err) // Now create a bad manager with RequireOAuth=true - badManager, _ := NewManager(testStaticConfig, testManager.accessControlClientset.cfg, testManager.accessControlClientset.clientCmdConfig) + badManager, _ := NewManager(testStaticConfig, testManager.accessControlClientset.RESTConfig(), testManager.accessControlClientset.ToRawKubeConfigLoader()) // Replace the clientCmdConfig with one that will fail pathOptions := clientcmd.NewDefaultPathOptions() pathOptions.LoadingRules.ExplicitPath = badKubeconfigPath @@ -217,7 +217,7 @@ users: // Corrupt the rest config to make NewAccessControlClientset fail // Setting an invalid Host URL should cause client creation to fail - testManager.accessControlClientset.cfg.Host = "://invalid-url" + testManager.accessControlClientset.RESTConfig().Host = "://invalid-url" ctx := context.WithValue(s.T().Context(), HeaderKey("Authorization"), "Bearer aiTana-julIA") derived, err := testManager.Derived(ctx) @@ -237,7 +237,7 @@ users: s.Require().NoErrorf(err, "failed to create test manager: %v", err) // Corrupt the rest config to make NewAccessControlClientset fail - testManager.accessControlClientset.cfg.Host = "://invalid-url" + testManager.accessControlClientset.RESTConfig().Host = "://invalid-url" ctx := context.WithValue(s.T().Context(), HeaderKey("Authorization"), "Bearer aiTana-julIA") derived, err := testManager.Derived(ctx) @@ -282,10 +282,10 @@ users: derived, err := testManager.Derived(ctx) s.Require().NoErrorf(err, "failed to create derived kubernetes: %v", err) - s.NotEqual(derived.AccessControlClientset(), testManager.accessControlClientset, "expected new derived clientset, got original clientset") - s.Equal(derived.AccessControlClientset().config, testStaticConfig, "config not properly wired to derived clientset") + s.NotEqual(derived.accessControlClientSet, testManager.accessControlClientset, "expected new derived clientset, got original clientset") + s.Equal(derived.accessControlClientSet.config, testStaticConfig, "config not properly wired to derived clientset") - derivedCfg := derived.AccessControlClientset().cfg + derivedCfg := derived.accessControlClientSet.RESTConfig() s.Require().NotNil(derivedCfg, "derived config is nil") s.Equalf("aiTana-julIA", derivedCfg.BearerToken, "expected BearerToken %s, got %s", "aiTana-julIA", derivedCfg.BearerToken) diff --git a/pkg/kubernetes/manager.go b/pkg/kubernetes/manager.go index bd75fac21..e44beda64 100644 --- a/pkg/kubernetes/manager.go +++ b/pkg/kubernetes/manager.go @@ -8,7 +8,7 @@ import ( "strconv" "strings" - configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config" + "github.com/containers/kubernetes-mcp-server/pkg/api" authenticationv1api "k8s.io/api/authentication/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/rest" @@ -20,17 +20,17 @@ import ( type Manager struct { accessControlClientset *AccessControlClientset - config configapi.BaseConfig + config api.BaseConfig } -var _ Openshift = (*Manager)(nil) +var _ api.Openshift = (*Manager)(nil) var ( ErrorKubeconfigInClusterNotAllowed = errors.New("kubeconfig manager cannot be used in in-cluster deployments") ErrorInClusterNotInCluster = errors.New("in-cluster manager cannot be used outside of a cluster") ) -func NewKubeconfigManager(config configapi.BaseConfig, kubeconfigContext string) (*Manager, error) { +func NewKubeconfigManager(config api.BaseConfig, kubeconfigContext string) (*Manager, error) { if IsInCluster(config) { return nil, ErrorKubeconfigInClusterNotAllowed } @@ -54,7 +54,7 @@ func NewKubeconfigManager(config configapi.BaseConfig, kubeconfigContext string) return NewManager(config, restConfig, clientCmdConfig) } -func NewInClusterManager(config configapi.BaseConfig) (*Manager, error) { +func NewInClusterManager(config api.BaseConfig) (*Manager, error) { if config.GetKubeConfigPath() != "" { return nil, fmt.Errorf("kubeconfig file %s cannot be used with the in-cluster deployments: %v", config.GetKubeConfigPath(), ErrorKubeconfigInClusterNotAllowed) } @@ -86,7 +86,7 @@ func NewInClusterManager(config configapi.BaseConfig) (*Manager, error) { return NewManager(config, restConfig, clientcmd.NewDefaultClientConfig(*clientCmdConfig, nil)) } -func NewManager(config configapi.BaseConfig, restConfig *rest.Config, clientCmdConfig clientcmd.ClientConfig) (*Manager, error) { +func NewManager(config api.BaseConfig, restConfig *rest.Config, clientCmdConfig clientcmd.ClientConfig) (*Manager, error) { if config == nil { return nil, errors.New("config cannot be nil") } @@ -105,7 +105,7 @@ func NewManager(config configapi.BaseConfig, restConfig *rest.Config, clientCmdC } var err error // TODO: Won't work because not all client-go clients use the shared context (e.g. discovery client uses context.TODO()) - //k8s.cfg.Wrap(func(original http.RoundTripper) http.RoundTripper { + //k8s.restConfig.Wrap(func(original http.RoundTripper) http.RoundTripper { // return &impersonateRoundTripper{original} //}) k8s.accessControlClientset, err = NewAccessControlClientset(k8s.config, clientCmdConfig, restConfig) @@ -153,22 +153,22 @@ func (m *Manager) Derived(ctx context.Context) (*Kubernetes, error) { } klog.V(5).Infof("%s header found (Bearer), using provided bearer token", OAuthAuthorizationHeader) derivedCfg := &rest.Config{ - Host: m.accessControlClientset.cfg.Host, - APIPath: m.accessControlClientset.cfg.APIPath, - WrapTransport: m.accessControlClientset.cfg.WrapTransport, + Host: m.accessControlClientset.RESTConfig().Host, + APIPath: m.accessControlClientset.RESTConfig().APIPath, + WrapTransport: m.accessControlClientset.RESTConfig().WrapTransport, // Copy only server verification TLS settings (CA bundle and server name) TLSClientConfig: rest.TLSClientConfig{ - Insecure: m.accessControlClientset.cfg.Insecure, - ServerName: m.accessControlClientset.cfg.ServerName, - CAFile: m.accessControlClientset.cfg.CAFile, - CAData: m.accessControlClientset.cfg.CAData, + Insecure: m.accessControlClientset.RESTConfig().Insecure, + ServerName: m.accessControlClientset.RESTConfig().ServerName, + CAFile: m.accessControlClientset.RESTConfig().CAFile, + CAData: m.accessControlClientset.RESTConfig().CAData, }, BearerToken: strings.TrimPrefix(authorization, "Bearer "), // pass custom UserAgent to identify the client UserAgent: CustomUserAgent, - QPS: m.accessControlClientset.cfg.QPS, - Burst: m.accessControlClientset.cfg.Burst, - Timeout: m.accessControlClientset.cfg.Timeout, + QPS: m.accessControlClientset.RESTConfig().QPS, + Burst: m.accessControlClientset.RESTConfig().Burst, + Timeout: m.accessControlClientset.RESTConfig().Timeout, Impersonate: rest.ImpersonationConfig{}, } clientCmdApiConfig, err := m.accessControlClientset.clientCmdConfig.RawConfig() diff --git a/pkg/kubernetes/manager_test.go b/pkg/kubernetes/manager_test.go index aeed934ed..965f7908f 100644 --- a/pkg/kubernetes/manager_test.go +++ b/pkg/kubernetes/manager_test.go @@ -50,7 +50,7 @@ func (s *ManagerTestSuite) TestNewInClusterManager() { s.Equal("in-cluster", rawConfig.CurrentContext, "expected current context to be 'in-cluster'") }) s.Run("sets default user-agent", func() { - s.Contains(manager.accessControlClientset.cfg.UserAgent, "("+runtime.GOOS+"/"+runtime.GOARCH+")") + s.Contains(manager.accessControlClientset.RESTConfig().UserAgent, "("+runtime.GOOS+"/"+runtime.GOARCH+")") }) }) s.Run("with explicit kubeconfig", func() { @@ -99,10 +99,10 @@ func (s *ManagerTestSuite) TestNewKubeconfigManager() { s.Contains(manager.accessControlClientset.ToRawKubeConfigLoader().ConfigAccess().GetLoadingPrecedence(), kubeconfig, "expected kubeconfig path to match") }) s.Run("sets default user-agent", func() { - s.Contains(manager.accessControlClientset.cfg.UserAgent, "("+runtime.GOOS+"/"+runtime.GOARCH+")") + s.Contains(manager.accessControlClientset.RESTConfig().UserAgent, "("+runtime.GOOS+"/"+runtime.GOARCH+")") }) s.Run("rest config host points to mock server", func() { - s.Equal(s.mockServer.Config().Host, manager.accessControlClientset.cfg.Host, "expected rest config host to match mock server") + s.Equal(s.mockServer.Config().Host, manager.accessControlClientset.RESTConfig().Host, "expected rest config host to match mock server") }) }) s.Run("with valid kubeconfig in env and explicit kubeconfig in config", func() { @@ -125,7 +125,7 @@ func (s *ManagerTestSuite) TestNewKubeconfigManager() { s.Contains(manager.accessControlClientset.ToRawKubeConfigLoader().ConfigAccess().GetLoadingPrecedence(), kubeconfigExplicit, "expected kubeconfig path to match explicit") }) s.Run("rest config host points to mock server", func() { - s.Equal(s.mockServer.Config().Host, manager.accessControlClientset.cfg.Host, "expected rest config host to match mock server") + s.Equal(s.mockServer.Config().Host, manager.accessControlClientset.RESTConfig().Host, "expected rest config host to match mock server") }) }) s.Run("with valid kubeconfig in env and explicit kubeconfig context (valid)", func() { @@ -150,7 +150,7 @@ func (s *ManagerTestSuite) TestNewKubeconfigManager() { s.Contains(manager.accessControlClientset.ToRawKubeConfigLoader().ConfigAccess().GetLoadingPrecedence(), kubeconfigFile, "expected kubeconfig path to match") }) s.Run("rest config host points to mock server", func() { - s.Equal(s.mockServer.Config().Host, manager.accessControlClientset.cfg.Host, "expected rest config host to match mock server") + s.Equal(s.mockServer.Config().Host, manager.accessControlClientset.RESTConfig().Host, "expected rest config host to match mock server") }) }) s.Run("with valid kubeconfig in env and explicit kubeconfig context (invalid)", func() { diff --git a/pkg/kubernetes/namespaces.go b/pkg/kubernetes/namespaces.go index 8c191c1ec..07d856f79 100644 --- a/pkg/kubernetes/namespaces.go +++ b/pkg/kubernetes/namespaces.go @@ -2,17 +2,19 @@ package kubernetes import ( "context" + + "github.com/containers/kubernetes-mcp-server/pkg/api" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) -func (k *Kubernetes) NamespacesList(ctx context.Context, options ResourceListOptions) (runtime.Unstructured, error) { +func (k *Kubernetes) NamespacesList(ctx context.Context, options api.ListOptions) (runtime.Unstructured, error) { return k.ResourcesList(ctx, &schema.GroupVersionKind{ Group: "", Version: "v1", Kind: "Namespace", }, "", options) } -func (k *Kubernetes) ProjectsList(ctx context.Context, options ResourceListOptions) (runtime.Unstructured, error) { +func (k *Kubernetes) ProjectsList(ctx context.Context, options api.ListOptions) (runtime.Unstructured, error) { return k.ResourcesList(ctx, &schema.GroupVersionKind{ Group: "project.openshift.io", Version: "v1", Kind: "Project", }, "", options) diff --git a/pkg/kubernetes/nodes.go b/pkg/kubernetes/nodes.go index 152f84cf6..a0aad0075 100644 --- a/pkg/kubernetes/nodes.go +++ b/pkg/kubernetes/nodes.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" + "github.com/containers/kubernetes-mcp-server/pkg/api" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/metrics/pkg/apis/metrics" metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1" @@ -69,12 +70,7 @@ func (k *Kubernetes) NodesStatsSummary(ctx context.Context, name string) (string return string(rawData), nil } -type NodesTopOptions struct { - metav1.ListOptions - Name string -} - -func (k *Kubernetes) NodesTop(ctx context.Context, options NodesTopOptions) (*metrics.NodeMetricsList, error) { +func (k *Kubernetes) NodesTop(ctx context.Context, options api.NodesTopOptions) (*metrics.NodeMetricsList, error) { // TODO, maybe move to mcp Tools setup and omit in case metrics aren't available in the target cluster if !k.supportsGroupVersion(metrics.GroupName + "/" + metricsv1beta1api.SchemeGroupVersion.Version) { return nil, errors.New("metrics API is not available") diff --git a/pkg/kubernetes/openshift.go b/pkg/kubernetes/openshift.go index 0df78e54f..f0481a09d 100644 --- a/pkg/kubernetes/openshift.go +++ b/pkg/kubernetes/openshift.go @@ -6,10 +6,6 @@ import ( "github.com/containers/kubernetes-mcp-server/pkg/openshift" ) -type Openshift interface { - IsOpenShift(context.Context) bool -} - func (m *Manager) IsOpenShift(ctx context.Context) bool { // This method should be fast and not block (it's called at startup) k, err := m.Derived(ctx) diff --git a/pkg/kubernetes/pods.go b/pkg/kubernetes/pods.go index f36f1beee..c21ce4298 100644 --- a/pkg/kubernetes/pods.go +++ b/pkg/kubernetes/pods.go @@ -20,26 +20,20 @@ import ( metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1" "k8s.io/utils/ptr" + "github.com/containers/kubernetes-mcp-server/pkg/api" "github.com/containers/kubernetes-mcp-server/pkg/version" ) // DefaultTailLines is the default number of lines to retrieve from the end of the logs const DefaultTailLines = int64(100) -type PodsTopOptions struct { - metav1.ListOptions - AllNamespaces bool - Namespace string - Name string -} - -func (k *Kubernetes) PodsListInAllNamespaces(ctx context.Context, options ResourceListOptions) (runtime.Unstructured, error) { +func (k *Kubernetes) PodsListInAllNamespaces(ctx context.Context, options api.ListOptions) (runtime.Unstructured, error) { return k.ResourcesList(ctx, &schema.GroupVersionKind{ Group: "", Version: "v1", Kind: "Pod", }, "", options) } -func (k *Kubernetes) PodsListInNamespace(ctx context.Context, namespace string, options ResourceListOptions) (runtime.Unstructured, error) { +func (k *Kubernetes) PodsListInNamespace(ctx context.Context, namespace string, options api.ListOptions) (runtime.Unstructured, error) { return k.ResourcesList(ctx, &schema.GroupVersionKind{ Group: "", Version: "v1", Kind: "Pod", }, namespace, options) @@ -48,11 +42,11 @@ func (k *Kubernetes) PodsListInNamespace(ctx context.Context, namespace string, func (k *Kubernetes) PodsGet(ctx context.Context, namespace, name string) (*unstructured.Unstructured, error) { return k.ResourcesGet(ctx, &schema.GroupVersionKind{ Group: "", Version: "v1", Kind: "Pod", - }, k.NamespaceOrDefault(namespace), name) + }, k.AccessControlClientset().NamespaceOrDefault(namespace), name) } func (k *Kubernetes) PodsDelete(ctx context.Context, namespace, name string) (string, error) { - namespace = k.NamespaceOrDefault(namespace) + namespace = k.AccessControlClientset().NamespaceOrDefault(namespace) pod, err := k.ResourcesGet(ctx, &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, namespace, name) if err != nil { return "", err @@ -95,7 +89,7 @@ func (k *Kubernetes) PodsDelete(ctx context.Context, namespace, name string) (st } func (k *Kubernetes) PodsLog(ctx context.Context, namespace, name, container string, previous bool, tail int64) (string, error) { - pods := k.AccessControlClientset().CoreV1().Pods(k.NamespaceOrDefault(namespace)) + pods := k.AccessControlClientset().CoreV1().Pods(k.AccessControlClientset().NamespaceOrDefault(namespace)) logOptions := &v1.PodLogOptions{ Container: container, @@ -136,7 +130,7 @@ func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string, var resources []any pod := &v1.Pod{ TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"}, - ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: k.NamespaceOrDefault(namespace), Labels: labels}, + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: k.AccessControlClientset().NamespaceOrDefault(namespace), Labels: labels}, Spec: v1.PodSpec{Containers: []v1.Container{{ Name: name, Image: image, @@ -148,7 +142,7 @@ func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string, pod.Spec.Containers[0].Ports = []v1.ContainerPort{{ContainerPort: port}} resources = append(resources, &v1.Service{ TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Service"}, - ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: k.NamespaceOrDefault(namespace), Labels: labels}, + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: k.AccessControlClientset().NamespaceOrDefault(namespace), Labels: labels}, Spec: v1.ServiceSpec{ Selector: labels, Type: v1.ServiceTypeClusterIP, @@ -163,7 +157,7 @@ func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string, "kind": "Route", "metadata": map[string]interface{}{ "name": name, - "namespace": k.NamespaceOrDefault(namespace), + "namespace": k.AccessControlClientset().NamespaceOrDefault(namespace), "labels": labels, }, "spec": map[string]interface{}{ @@ -202,7 +196,7 @@ func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string, return k.resourcesCreateOrUpdate(ctx, toCreate) } -func (k *Kubernetes) PodsTop(ctx context.Context, options PodsTopOptions) (*metrics.PodMetricsList, error) { +func (k *Kubernetes) PodsTop(ctx context.Context, options api.PodsTopOptions) (*metrics.PodMetricsList, error) { // TODO, maybe move to mcp Tools setup and omit in case metrics aren't available in the target cluster if !k.supportsGroupVersion(metrics.GroupName + "/" + metricsv1beta1api.SchemeGroupVersion.Version) { return nil, errors.New("metrics API is not available") @@ -211,7 +205,7 @@ func (k *Kubernetes) PodsTop(ctx context.Context, options PodsTopOptions) (*metr if options.AllNamespaces && namespace == "" { namespace = "" } else { - namespace = k.NamespaceOrDefault(namespace) + namespace = k.AccessControlClientset().NamespaceOrDefault(namespace) } var err error versionedMetrics := &metricsv1beta1api.PodMetricsList{} @@ -232,7 +226,7 @@ func (k *Kubernetes) PodsTop(ctx context.Context, options PodsTopOptions) (*metr } func (k *Kubernetes) PodsExec(ctx context.Context, namespace, name, container string, command []string) (string, error) { - namespace = k.NamespaceOrDefault(namespace) + namespace = k.AccessControlClientset().NamespaceOrDefault(namespace) pods := k.AccessControlClientset().CoreV1().Pods(namespace) pod, err := pods.Get(ctx, name, metav1.GetOptions{}) if err != nil { @@ -260,11 +254,15 @@ func (k *Kubernetes) PodsExec(ctx context.Context, namespace, name, container st Name(name). SubResource("exec") execRequest.VersionedParams(podExecOptions, ParameterCodec) - spdyExec, err := remotecommand.NewSPDYExecutor(k.AccessControlClientset().cfg, "POST", execRequest.URL()) + restConfig, err := k.AccessControlClientset().ToRESTConfig() + if err != nil { + return "", err + } + spdyExec, err := remotecommand.NewSPDYExecutor(restConfig, "POST", execRequest.URL()) if err != nil { return "", err } - webSocketExec, err := remotecommand.NewWebSocketExecutor(k.AccessControlClientset().cfg, "GET", execRequest.URL().String()) + webSocketExec, err := remotecommand.NewWebSocketExecutor(restConfig, "GET", execRequest.URL().String()) if err != nil { return "", err } diff --git a/pkg/kubernetes/provider.go b/pkg/kubernetes/provider.go index ee7cc2d56..2ccc58868 100644 --- a/pkg/kubernetes/provider.go +++ b/pkg/kubernetes/provider.go @@ -3,7 +3,7 @@ package kubernetes import ( "context" - configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config" + "github.com/containers/kubernetes-mcp-server/pkg/api" ) // McpReload is a function type that defines a callback for reloading MCP toolsets (including tools, prompts, or other configurations) @@ -15,7 +15,7 @@ type Provider interface { // extending this interface might not be a good idea anymore. // For the kubecontext case, a user might be targeting both an OpenShift flavored cluster and a vanilla Kubernetes cluster. // See: https://github.com/containers/kubernetes-mcp-server/pull/372#discussion_r2421592315 - Openshift + api.Openshift TokenVerifier GetTargets(ctx context.Context) ([]string, error) GetDerivedKubernetes(ctx context.Context, target string) (*Kubernetes, error) @@ -26,7 +26,7 @@ type Provider interface { Close() } -func NewProvider(cfg configapi.BaseConfig) (Provider, error) { +func NewProvider(cfg api.BaseConfig) (Provider, error) { strategy := resolveStrategy(cfg) factory, err := getProviderFactory(strategy) @@ -37,18 +37,18 @@ func NewProvider(cfg configapi.BaseConfig) (Provider, error) { return factory(cfg) } -func resolveStrategy(cfg configapi.BaseConfig) string { +func resolveStrategy(cfg api.BaseConfig) string { if cfg.GetClusterProviderStrategy() != "" { return cfg.GetClusterProviderStrategy() } if cfg.GetKubeConfigPath() != "" { - return configapi.ClusterProviderKubeConfig + return api.ClusterProviderKubeConfig } if _, inClusterConfigErr := InClusterConfig(); inClusterConfigErr == nil { - return configapi.ClusterProviderInCluster + return api.ClusterProviderInCluster } - return configapi.ClusterProviderKubeConfig + return api.ClusterProviderKubeConfig } diff --git a/pkg/kubernetes/provider_kubeconfig.go b/pkg/kubernetes/provider_kubeconfig.go index 45332e352..202accf6c 100644 --- a/pkg/kubernetes/provider_kubeconfig.go +++ b/pkg/kubernetes/provider_kubeconfig.go @@ -6,7 +6,7 @@ import ( "fmt" "reflect" - configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config" + "github.com/containers/kubernetes-mcp-server/pkg/api" "github.com/containers/kubernetes-mcp-server/pkg/kubernetes/watcher" authenticationv1api "k8s.io/api/authentication/v1" ) @@ -19,7 +19,7 @@ const KubeConfigTargetParameterName = "context" // Kubernetes clusters using different contexts from a kubeconfig file. // It lazily initializes managers for each context as they are requested. type kubeConfigClusterProvider struct { - config configapi.BaseConfig + config api.BaseConfig defaultContext string managers map[string]*Manager kubeconfigWatcher *watcher.Kubeconfig @@ -29,14 +29,14 @@ type kubeConfigClusterProvider struct { var _ Provider = &kubeConfigClusterProvider{} func init() { - RegisterProvider(configapi.ClusterProviderKubeConfig, newKubeConfigClusterProvider) + RegisterProvider(api.ClusterProviderKubeConfig, newKubeConfigClusterProvider) } // newKubeConfigClusterProvider creates a provider that manages multiple clusters // via kubeconfig contexts. // Internally, it leverages a KubeconfigManager for each context, initializing them // lazily when requested. -func newKubeConfigClusterProvider(cfg configapi.BaseConfig) (Provider, error) { +func newKubeConfigClusterProvider(cfg api.BaseConfig) (Provider, error) { ret := &kubeConfigClusterProvider{config: cfg} if err := ret.reset(); err != nil { return nil, err diff --git a/pkg/kubernetes/provider_registry.go b/pkg/kubernetes/provider_registry.go index 9dc84666f..9560ab330 100644 --- a/pkg/kubernetes/provider_registry.go +++ b/pkg/kubernetes/provider_registry.go @@ -4,13 +4,13 @@ import ( "fmt" "sort" - configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config" + "github.com/containers/kubernetes-mcp-server/pkg/api" ) // ProviderFactory creates a new Provider instance for a given strategy. // Implementations should validate that the Manager is compatible with their strategy // (e.g., kubeconfig provider should reject in-cluster managers). -type ProviderFactory func(cfg configapi.BaseConfig) (Provider, error) +type ProviderFactory func(cfg api.BaseConfig) (Provider, error) var providerFactories = make(map[string]ProviderFactory) diff --git a/pkg/kubernetes/provider_registry_test.go b/pkg/kubernetes/provider_registry_test.go index 590a2d5cc..7fc3dc583 100644 --- a/pkg/kubernetes/provider_registry_test.go +++ b/pkg/kubernetes/provider_registry_test.go @@ -3,7 +3,7 @@ package kubernetes import ( "testing" - configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config" + "github.com/containers/kubernetes-mcp-server/pkg/api" "github.com/stretchr/testify/suite" ) @@ -13,18 +13,18 @@ type ProviderRegistryTestSuite struct { func (s *ProviderRegistryTestSuite) TestRegisterProvider() { s.Run("With no pre-existing provider, registers the provider", func() { - RegisterProvider("test-strategy", func(cfg configapi.BaseConfig) (Provider, error) { + RegisterProvider("test-strategy", func(cfg api.BaseConfig) (Provider, error) { return nil, nil }) _, exists := providerFactories["test-strategy"] s.True(exists, "Provider should be registered") }) s.Run("With pre-existing provider, panics", func() { - RegisterProvider("test-pre-existent", func(cfg configapi.BaseConfig) (Provider, error) { + RegisterProvider("test-pre-existent", func(cfg api.BaseConfig) (Provider, error) { return nil, nil }) s.Panics(func() { - RegisterProvider("test-pre-existent", func(cfg configapi.BaseConfig) (Provider, error) { + RegisterProvider("test-pre-existent", func(cfg api.BaseConfig) (Provider, error) { return nil, nil }) }, "Registering a provider with an existing strategy should panic") @@ -39,10 +39,10 @@ func (s *ProviderRegistryTestSuite) TestGetRegisteredStrategies() { }) s.Run("With multiple registered providers, returns sorted list", func() { providerFactories = make(map[string]ProviderFactory) - RegisterProvider("foo-strategy", func(cfg configapi.BaseConfig) (Provider, error) { + RegisterProvider("foo-strategy", func(cfg api.BaseConfig) (Provider, error) { return nil, nil }) - RegisterProvider("bar-strategy", func(cfg configapi.BaseConfig) (Provider, error) { + RegisterProvider("bar-strategy", func(cfg api.BaseConfig) (Provider, error) { return nil, nil }) strategies := GetRegisteredStrategies() diff --git a/pkg/kubernetes/provider_single.go b/pkg/kubernetes/provider_single.go index dbdacbe0e..5a35b35b4 100644 --- a/pkg/kubernetes/provider_single.go +++ b/pkg/kubernetes/provider_single.go @@ -6,7 +6,7 @@ import ( "fmt" "reflect" - configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config" + "github.com/containers/kubernetes-mcp-server/pkg/api" "github.com/containers/kubernetes-mcp-server/pkg/kubernetes/watcher" authenticationv1api "k8s.io/api/authentication/v1" ) @@ -15,7 +15,7 @@ import ( // Kubernetes cluster. Used for in-cluster deployments or when multi-cluster // support is disabled. type singleClusterProvider struct { - config configapi.BaseConfig + config api.BaseConfig strategy string manager *Manager kubeconfigWatcher *watcher.Kubeconfig @@ -25,15 +25,15 @@ type singleClusterProvider struct { var _ Provider = &singleClusterProvider{} func init() { - RegisterProvider(configapi.ClusterProviderInCluster, newSingleClusterProvider(configapi.ClusterProviderInCluster)) - RegisterProvider(configapi.ClusterProviderDisabled, newSingleClusterProvider(configapi.ClusterProviderDisabled)) + RegisterProvider(api.ClusterProviderInCluster, newSingleClusterProvider(api.ClusterProviderInCluster)) + RegisterProvider(api.ClusterProviderDisabled, newSingleClusterProvider(api.ClusterProviderDisabled)) } // newSingleClusterProvider creates a provider that manages a single cluster. // When used within a cluster or with an 'in-cluster' strategy, it uses an InClusterManager. // Otherwise, it uses a KubeconfigManager. func newSingleClusterProvider(strategy string) ProviderFactory { - return func(cfg configapi.BaseConfig) (Provider, error) { + return func(cfg api.BaseConfig) (Provider, error) { ret := &singleClusterProvider{ config: cfg, strategy: strategy, @@ -46,13 +46,13 @@ func newSingleClusterProvider(strategy string) ProviderFactory { } func (p *singleClusterProvider) reset() error { - if p.config != nil && p.config.GetKubeConfigPath() != "" && p.strategy == configapi.ClusterProviderInCluster { + if p.config != nil && p.config.GetKubeConfigPath() != "" && p.strategy == api.ClusterProviderInCluster { return fmt.Errorf("kubeconfig file %s cannot be used with the in-cluster ClusterProviderStrategy", p.config.GetKubeConfigPath()) } var err error - if p.strategy == configapi.ClusterProviderInCluster || IsInCluster(p.config) { + if p.strategy == api.ClusterProviderInCluster || IsInCluster(p.config) { p.manager, err = NewInClusterManager(p.config) } else { p.manager, err = NewKubeconfigManager(p.config, "") diff --git a/pkg/kubernetes/provider_watch_test.go b/pkg/kubernetes/provider_watch_test.go index c57d3d497..70d082bab 100644 --- a/pkg/kubernetes/provider_watch_test.go +++ b/pkg/kubernetes/provider_watch_test.go @@ -8,7 +8,7 @@ import ( "time" "github.com/containers/kubernetes-mcp-server/internal/test" - configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config" + "github.com/containers/kubernetes-mcp-server/pkg/api" "github.com/containers/kubernetes-mcp-server/pkg/config" "github.com/stretchr/testify/suite" "k8s.io/client-go/tools/clientcmd" @@ -52,7 +52,7 @@ func (s *ProviderWatchTargetsTestSuite) TestClusterStateChanges() { testCases := []func() (Provider, error){ func() (Provider, error) { return newKubeConfigClusterProvider(s.staticConfig) }, func() (Provider, error) { - return newSingleClusterProvider(configapi.ClusterProviderDisabled)(s.staticConfig) + return newSingleClusterProvider(api.ClusterProviderDisabled)(s.staticConfig) }, } for _, tc := range testCases { @@ -117,7 +117,7 @@ func (s *ProviderWatchTargetsTestSuite) TestKubeConfigClusterProvider() { } func (s *ProviderWatchTargetsTestSuite) TestSingleClusterProvider() { - provider, err := newSingleClusterProvider(configapi.ClusterProviderDisabled)(s.staticConfig) + provider, err := newSingleClusterProvider(api.ClusterProviderDisabled)(s.staticConfig) s.Require().NoError(err, "Expected no error from provider creation") callback, waitForCallback := CallbackWaiter() diff --git a/pkg/kubernetes/resources.go b/pkg/kubernetes/resources.go index 55091f9c5..2b7208244 100644 --- a/pkg/kubernetes/resources.go +++ b/pkg/kubernetes/resources.go @@ -9,6 +9,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/dynamic" + "github.com/containers/kubernetes-mcp-server/pkg/api" "github.com/containers/kubernetes-mcp-server/pkg/version" authv1 "k8s.io/api/authorization/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -25,12 +26,7 @@ const ( AppKubernetesPartOf = "app.kubernetes.io/part-of" ) -type ResourceListOptions struct { - metav1.ListOptions - AsTable bool -} - -func (k *Kubernetes) ResourcesList(ctx context.Context, gvk *schema.GroupVersionKind, namespace string, options ResourceListOptions) (runtime.Unstructured, error) { +func (k *Kubernetes) ResourcesList(ctx context.Context, gvk *schema.GroupVersionKind, namespace string, options api.ListOptions) (runtime.Unstructured, error) { gvr, err := k.resourceFor(gvk) if err != nil { return nil, err @@ -39,7 +35,7 @@ func (k *Kubernetes) ResourcesList(ctx context.Context, gvk *schema.GroupVersion // Check if operation is allowed for all namespaces (applicable for namespaced resources) isNamespaced, _ := k.isNamespaced(gvk) if isNamespaced && !k.canIUse(ctx, gvr, namespace, "list") && namespace == "" { - namespace = k.configuredNamespace() + namespace = k.AccessControlClientset().NamespaceOrDefault("") } if options.AsTable { return k.resourcesListAsTable(ctx, gvk, gvr, namespace, options) @@ -55,7 +51,7 @@ func (k *Kubernetes) ResourcesGet(ctx context.Context, gvk *schema.GroupVersionK // If it's a namespaced resource and namespace wasn't provided, try to use the default configured one if namespaced, nsErr := k.isNamespaced(gvk); nsErr == nil && namespaced { - namespace = k.NamespaceOrDefault(namespace) + namespace = k.AccessControlClientset().NamespaceOrDefault(namespace) } return k.AccessControlClientset().DynamicClient().Resource(*gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) } @@ -82,7 +78,7 @@ func (k *Kubernetes) ResourcesDelete(ctx context.Context, gvk *schema.GroupVersi // If it's a namespaced resource and namespace wasn't provided, try to use the default configured one if namespaced, nsErr := k.isNamespaced(gvk); nsErr == nil && namespaced { - namespace = k.NamespaceOrDefault(namespace) + namespace = k.AccessControlClientset().NamespaceOrDefault(namespace) } return k.AccessControlClientset().DynamicClient().Resource(*gvr).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{}) } @@ -106,7 +102,7 @@ func (k *Kubernetes) ResourcesScale( AccessControlClientset(). DynamicClient(). Resource(*gvr). - Namespace(k.NamespaceOrDefault(namespace)) + Namespace(k.AccessControlClientset().NamespaceOrDefault(namespace)) } else { resourceClient = k. AccessControlClientset().DynamicClient().Resource(*gvr) @@ -134,7 +130,7 @@ func (k *Kubernetes) ResourcesScale( // resourcesListAsTable retrieves a list of resources in a table format. // It's almost identical to the dynamic.DynamicClient implementation, but it uses a specific Accept header to request the table format. // dynamic.DynamicClient does not provide a way to set the HTTP header (TODO: create an issue to request this feature) -func (k *Kubernetes) resourcesListAsTable(ctx context.Context, gvk *schema.GroupVersionKind, gvr *schema.GroupVersionResource, namespace string, options ResourceListOptions) (runtime.Unstructured, error) { +func (k *Kubernetes) resourcesListAsTable(ctx context.Context, gvk *schema.GroupVersionKind, gvr *schema.GroupVersionResource, namespace string, options api.ListOptions) (runtime.Unstructured, error) { var url []string if len(gvr.Group) == 0 { url = append(url, "api") @@ -189,7 +185,7 @@ func (k *Kubernetes) resourcesCreateOrUpdate(ctx context.Context, resources []*u namespace := obj.GetNamespace() // If it's a namespaced resource and namespace wasn't provided, try to use the default configured one if namespaced, nsErr := k.isNamespaced(&gvk); nsErr == nil && namespaced { - namespace = k.NamespaceOrDefault(namespace) + namespace = k.AccessControlClientset().NamespaceOrDefault(namespace) } resources[i], rErr = k.AccessControlClientset().DynamicClient().Resource(*gvr).Namespace(namespace).Apply(ctx, obj.GetName(), obj, metav1.ApplyOptions{ FieldManager: version.BinaryName, diff --git a/pkg/mcp/gosdk.go b/pkg/mcp/gosdk.go index 2437b085b..a6e8cb85c 100644 --- a/pkg/mcp/gosdk.go +++ b/pkg/mcp/gosdk.go @@ -56,10 +56,11 @@ func ServerToolToGoSdkTool(s *Server, tool api.ServerTool) (*mcp.Tool, mcp.ToolH } result, err := tool.Handler(api.ToolHandlerParams{ - Context: ctx, - Kubernetes: k, - ToolCallRequest: toolCallRequest, - ListOutput: s.configuration.ListOutput(), + Context: ctx, + ExtendedConfigProvider: s.configuration, + KubernetesClient: k, + ToolCallRequest: toolCallRequest, + ListOutput: s.configuration.ListOutput(), }) if err != nil { return nil, err diff --git a/pkg/mcp/prompts_gosdk.go b/pkg/mcp/prompts_gosdk.go index a153ed24f..e2f3e7c9d 100644 --- a/pkg/mcp/prompts_gosdk.go +++ b/pkg/mcp/prompts_gosdk.go @@ -56,9 +56,10 @@ func ServerPromptToGoSdkPrompt(s *Server, serverPrompt api.ServerPrompt) (*mcp.P } params := api.PromptHandlerParams{ - Context: ctx, - Kubernetes: k8s, - PromptCallRequest: &promptCallRequestAdapter{request: request}, + Context: ctx, + ExtendedConfigProvider: s.configuration, + KubernetesClient: k8s, + PromptCallRequest: &promptCallRequestAdapter{request: request}, } result, err := serverPrompt.Handler(params) diff --git a/pkg/toolsets/config/toolset.go b/pkg/toolsets/config/toolset.go index 5d641fe56..d995b26c8 100644 --- a/pkg/toolsets/config/toolset.go +++ b/pkg/toolsets/config/toolset.go @@ -4,7 +4,6 @@ import ( "slices" "github.com/containers/kubernetes-mcp-server/pkg/api" - internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" "github.com/containers/kubernetes-mcp-server/pkg/toolsets" ) @@ -20,7 +19,7 @@ func (t *Toolset) GetDescription() string { return "View and manage the current local Kubernetes configuration (kubeconfig)" } -func (t *Toolset) GetTools(_ internalk8s.Openshift) []api.ServerTool { +func (t *Toolset) GetTools(_ api.Openshift) []api.ServerTool { return slices.Concat( initConfiguration(), ) diff --git a/pkg/toolsets/core/namespaces.go b/pkg/toolsets/core/namespaces.go index 2f2ee8fca..9e32912bc 100644 --- a/pkg/toolsets/core/namespaces.go +++ b/pkg/toolsets/core/namespaces.go @@ -8,10 +8,9 @@ import ( "k8s.io/utils/ptr" "github.com/containers/kubernetes-mcp-server/pkg/api" - internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" ) -func initNamespaces(o internalk8s.Openshift) []api.ServerTool { +func initNamespaces(o api.Openshift) []api.ServerTool { ret := make([]api.ServerTool, 0) ret = append(ret, api.ServerTool{ Tool: api.Tool{ @@ -49,7 +48,7 @@ func initNamespaces(o internalk8s.Openshift) []api.ServerTool { } func namespacesList(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - ret, err := params.NamespacesList(params, internalk8s.ResourceListOptions{AsTable: params.ListOutput.AsTable()}) + ret, err := params.NamespacesList(params, api.ListOptions{AsTable: params.ListOutput.AsTable()}) if err != nil { return api.NewToolCallResult("", fmt.Errorf("failed to list namespaces: %v", err)), nil } @@ -57,7 +56,7 @@ func namespacesList(params api.ToolHandlerParams) (*api.ToolCallResult, error) { } func projectsList(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - ret, err := params.ProjectsList(params, internalk8s.ResourceListOptions{AsTable: params.ListOutput.AsTable()}) + ret, err := params.ProjectsList(params, api.ListOptions{AsTable: params.ListOutput.AsTable()}) if err != nil { return api.NewToolCallResult("", fmt.Errorf("failed to list projects: %v", err)), nil } diff --git a/pkg/toolsets/core/nodes.go b/pkg/toolsets/core/nodes.go index 1d070a8f3..5d845a3e5 100644 --- a/pkg/toolsets/core/nodes.go +++ b/pkg/toolsets/core/nodes.go @@ -136,7 +136,7 @@ func nodesStatsSummary(params api.ToolHandlerParams) (*api.ToolCallResult, error } func nodesTop(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - nodesTopOptions := kubernetes.NodesTopOptions{} + nodesTopOptions := api.NodesTopOptions{} if v, ok := params.GetArguments()["name"].(string); ok { nodesTopOptions.Name = v } @@ -150,12 +150,13 @@ func nodesTop(params api.ToolHandlerParams) (*api.ToolCallResult, error) { } // Get the list of nodes to extract their allocatable resources - nodes, err := params.AccessControlClientset().Nodes() - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("failed to get nodes client: %v", err)), nil + // Type assert to concrete type to access AccessControlClientset + k8s, ok := params.KubernetesClient.(*kubernetes.Kubernetes) + if !ok { + return api.NewToolCallResult("", fmt.Errorf("kubernetes client type assertion failed")), nil } - nodeList, err := nodes.List(params, metav1.ListOptions{ + nodeList, err := k8s.AccessControlClientset().CoreV1().Nodes().List(params, metav1.ListOptions{ LabelSelector: nodesTopOptions.LabelSelector, }) if err != nil { diff --git a/pkg/toolsets/core/pods.go b/pkg/toolsets/core/pods.go index f19886866..aa15d5f16 100644 --- a/pkg/toolsets/core/pods.go +++ b/pkg/toolsets/core/pods.go @@ -251,7 +251,7 @@ func initPods() []api.ServerTool { func podsListInAllNamespaces(params api.ToolHandlerParams) (*api.ToolCallResult, error) { labelSelector := params.GetArguments()["labelSelector"] - resourceListOptions := kubernetes.ResourceListOptions{ + resourceListOptions := api.ListOptions{ AsTable: params.ListOutput.AsTable(), } if labelSelector != nil { @@ -269,7 +269,7 @@ func podsListInNamespace(params api.ToolHandlerParams) (*api.ToolCallResult, err if ns == nil { return api.NewToolCallResult("", errors.New("failed to list pods in namespace, missing argument namespace")), nil } - resourceListOptions := kubernetes.ResourceListOptions{ + resourceListOptions := api.ListOptions{ AsTable: params.ListOutput.AsTable(), } labelSelector := params.GetArguments()["labelSelector"] @@ -316,7 +316,7 @@ func podsDelete(params api.ToolHandlerParams) (*api.ToolCallResult, error) { } func podsTop(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - podsTopOptions := kubernetes.PodsTopOptions{AllNamespaces: true} + podsTopOptions := api.PodsTopOptions{AllNamespaces: true} if v, ok := params.GetArguments()["namespace"].(string); ok { podsTopOptions.Namespace = v } diff --git a/pkg/toolsets/core/resources.go b/pkg/toolsets/core/resources.go index 33b92bfa4..8a953cbb3 100644 --- a/pkg/toolsets/core/resources.go +++ b/pkg/toolsets/core/resources.go @@ -10,11 +10,10 @@ import ( "k8s.io/utils/ptr" "github.com/containers/kubernetes-mcp-server/pkg/api" - internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" "github.com/containers/kubernetes-mcp-server/pkg/output" ) -func initResources(o internalk8s.Openshift) []api.ServerTool { +func initResources(o api.Openshift) []api.ServerTool { commonApiVersion := "v1 Pod, v1 Service, v1 Node, apps/v1 Deployment, networking.k8s.io/v1 Ingress" if o.IsOpenShift(context.Background()) { commonApiVersion += ", route.openshift.io/v1 Route" @@ -183,7 +182,7 @@ func resourcesList(params api.ToolHandlerParams) (*api.ToolCallResult, error) { namespace = "" } labelSelector := params.GetArguments()["labelSelector"] - resourceListOptions := internalk8s.ResourceListOptions{ + resourceListOptions := api.ListOptions{ AsTable: params.ListOutput.AsTable(), } @@ -316,7 +315,7 @@ func resourcesScale(params api.ToolHandlerParams) (*api.ToolCallResult, error) { return api.NewToolCallResult("", fmt.Errorf("namespace is not a string")), nil } - ns = params.NamespaceOrDefault(ns) + ns = params.AccessControlClientset().NamespaceOrDefault(ns) n, ok := name.(string) if !ok { diff --git a/pkg/toolsets/core/toolset.go b/pkg/toolsets/core/toolset.go index dfd61f428..5a1a8e888 100644 --- a/pkg/toolsets/core/toolset.go +++ b/pkg/toolsets/core/toolset.go @@ -4,7 +4,6 @@ import ( "slices" "github.com/containers/kubernetes-mcp-server/pkg/api" - internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" "github.com/containers/kubernetes-mcp-server/pkg/toolsets" ) @@ -20,7 +19,7 @@ func (t *Toolset) GetDescription() string { return "Most common tools for Kubernetes management (Pods, Generic Resources, Events, etc.)" } -func (t *Toolset) GetTools(o internalk8s.Openshift) []api.ServerTool { +func (t *Toolset) GetTools(o api.Openshift) []api.ServerTool { return slices.Concat( initEvents(), initNamespaces(o), diff --git a/pkg/toolsets/helm/helm.go b/pkg/toolsets/helm/helm.go index 646941f19..635402b26 100644 --- a/pkg/toolsets/helm/helm.go +++ b/pkg/toolsets/helm/helm.go @@ -3,6 +3,7 @@ package helm import ( "fmt" + "github.com/containers/kubernetes-mcp-server/pkg/helm" "github.com/google/jsonschema-go/jsonschema" "k8s.io/utils/ptr" @@ -112,7 +113,7 @@ func helmInstall(params api.ToolHandlerParams) (*api.ToolCallResult, error) { if v, ok := params.GetArguments()["namespace"].(string); ok { namespace = v } - ret, err := params.NewHelm().Install(params, chart, values, name, namespace) + ret, err := helm.NewHelm(params.KubernetesClient.AccessControlClientset()).Install(params, chart, values, name, namespace) if err != nil { return api.NewToolCallResult("", fmt.Errorf("failed to install helm chart '%s': %w", chart, err)), nil } @@ -128,7 +129,7 @@ func helmList(params api.ToolHandlerParams) (*api.ToolCallResult, error) { if v, ok := params.GetArguments()["namespace"].(string); ok { namespace = v } - ret, err := params.NewHelm().List(namespace, allNamespaces) + ret, err := helm.NewHelm(params.KubernetesClient.AccessControlClientset()).List(namespace, allNamespaces) if err != nil { return api.NewToolCallResult("", fmt.Errorf("failed to list helm releases in namespace '%s': %w", namespace, err)), nil } @@ -145,7 +146,7 @@ func helmUninstall(params api.ToolHandlerParams) (*api.ToolCallResult, error) { if v, ok := params.GetArguments()["namespace"].(string); ok { namespace = v } - ret, err := params.NewHelm().Uninstall(name, namespace) + ret, err := helm.NewHelm(params.KubernetesClient.AccessControlClientset()).Uninstall(name, namespace) if err != nil { return api.NewToolCallResult("", fmt.Errorf("failed to uninstall helm chart '%s': %w", name, err)), nil } diff --git a/pkg/toolsets/helm/toolset.go b/pkg/toolsets/helm/toolset.go index dbe75c1ed..36a0ba28c 100644 --- a/pkg/toolsets/helm/toolset.go +++ b/pkg/toolsets/helm/toolset.go @@ -4,7 +4,6 @@ import ( "slices" "github.com/containers/kubernetes-mcp-server/pkg/api" - internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" "github.com/containers/kubernetes-mcp-server/pkg/toolsets" ) @@ -20,7 +19,7 @@ func (t *Toolset) GetDescription() string { return "Tools for managing Helm charts and releases" } -func (t *Toolset) GetTools(_ internalk8s.Openshift) []api.ServerTool { +func (t *Toolset) GetTools(_ api.Openshift) []api.ServerTool { return slices.Concat( initHelm(), ) diff --git a/pkg/toolsets/kiali/tools/get_mesh_graph.go b/pkg/toolsets/kiali/tools/get_mesh_graph.go index e1c1ecdb1..2cb77f7b6 100644 --- a/pkg/toolsets/kiali/tools/get_mesh_graph.go +++ b/pkg/toolsets/kiali/tools/get_mesh_graph.go @@ -99,8 +99,8 @@ func getMeshGraphHandler(params api.ToolHandlerParams) (*api.ToolCallResult, err if err := setQueryParam(params, queryParams, "graphType", kialiclient.DefaultGraphType); err != nil { return api.NewToolCallResult("", err), nil } - k := params.NewKiali() - content, err := k.GetMeshGraph(params.Context, namespaces, queryParams) + kiali := kialiclient.NewKiali(params, params.AccessControlClientset().RESTConfig()) + content, err := kiali.GetMeshGraph(params.Context, namespaces, queryParams) if err != nil { return api.NewToolCallResult("", fmt.Errorf("failed to retrieve mesh graph: %v", err)), nil } diff --git a/pkg/toolsets/kiali/tools/get_metrics.go b/pkg/toolsets/kiali/tools/get_metrics.go index d129be0a7..09db32dcb 100644 --- a/pkg/toolsets/kiali/tools/get_metrics.go +++ b/pkg/toolsets/kiali/tools/get_metrics.go @@ -160,9 +160,8 @@ func resourceMetricsHandler(params api.ToolHandlerParams) (*api.ToolCallResult, queryParams["byLabels"] = byLabels } - k := params.NewKiali() - - content, err := ops.metricsFunc(params.Context, k, namespace, resourceName, queryParams) + kiali := kialiclient.NewKiali(params, params.AccessControlClientset().RESTConfig()) + content, err := ops.metricsFunc(params.Context, kiali, namespace, resourceName, queryParams) if err != nil { return api.NewToolCallResult("", fmt.Errorf("failed to get %s metrics: %v", ops.singularName, err)), nil } diff --git a/pkg/toolsets/kiali/tools/get_resource_details.go b/pkg/toolsets/kiali/tools/get_resource_details.go index e1ca726d1..95b8c305e 100644 --- a/pkg/toolsets/kiali/tools/get_resource_details.go +++ b/pkg/toolsets/kiali/tools/get_resource_details.go @@ -92,7 +92,7 @@ func resourceDetailsHandler(params api.ToolHandlerParams) (*api.ToolCallResult, return api.NewToolCallResult("", fmt.Errorf("resource_type is required")), nil } - k := params.NewKiali() + kiali := kialiclient.NewKiali(params, params.AccessControlClientset().RESTConfig()) ops, ok := listDetailsOpsMap[resourceType] if !ok { @@ -104,7 +104,7 @@ func resourceDetailsHandler(params api.ToolHandlerParams) (*api.ToolCallResult, if count := len(strings.Split(namespaces, ",")); count != 1 { return api.NewToolCallResult("", fmt.Errorf("exactly one namespace must be provided for %s details", ops.singularName)), nil } - content, err := ops.detailsFunc(params.Context, k, namespaces, resourceName) + content, err := ops.detailsFunc(params.Context, kiali, namespaces, resourceName) if err != nil { return api.NewToolCallResult("", fmt.Errorf("failed to get %s details: %v", ops.singularName, err)), nil } @@ -112,7 +112,7 @@ func resourceDetailsHandler(params api.ToolHandlerParams) (*api.ToolCallResult, } // Otherwise, list resources (supports multiple namespaces) - content, err := ops.listFunc(params.Context, k, namespaces) + content, err := ops.listFunc(params.Context, kiali, namespaces) if err != nil { return api.NewToolCallResult("", fmt.Errorf("failed to list %ss: %v", ops.singularName, err)), nil } diff --git a/pkg/toolsets/kiali/tools/get_traces.go b/pkg/toolsets/kiali/tools/get_traces.go index 9621a6ba3..cea598264 100644 --- a/pkg/toolsets/kiali/tools/get_traces.go +++ b/pkg/toolsets/kiali/tools/get_traces.go @@ -112,12 +112,12 @@ func InitGetTraces() []api.ServerTool { } func TracesHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - k := params.NewKiali() + kiali := kialiclient.NewKiali(params, params.AccessControlClientset().RESTConfig()) // Check if traceId is provided - if so, get trace details directly if traceIdVal, ok := params.GetArguments()["traceId"].(string); ok && traceIdVal != "" { traceId := strings.TrimSpace(traceIdVal) - content, err := k.TraceDetails(params.Context, traceId) + content, err := kiali.TraceDetails(params.Context, traceId) if err != nil { return api.NewToolCallResult("", fmt.Errorf("failed to get trace details: %v", err)), nil } @@ -193,7 +193,7 @@ func TracesHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { if clusterName, ok := params.GetArguments()["clusterName"].(string); ok && clusterName != "" { queryParams["clusterName"] = clusterName } - content, err := ops.tracesFunc(params.Context, k, namespace, resourceName, queryParams) + content, err := ops.tracesFunc(params.Context, kiali, namespace, resourceName, queryParams) if err != nil { return api.NewToolCallResult("", fmt.Errorf("failed to get %s traces: %v", ops.singularName, err)), nil } diff --git a/pkg/toolsets/kiali/tools/logs.go b/pkg/toolsets/kiali/tools/logs.go index e5313107d..9e914e48e 100644 --- a/pkg/toolsets/kiali/tools/logs.go +++ b/pkg/toolsets/kiali/tools/logs.go @@ -3,6 +3,7 @@ package tools import ( "fmt" + kialiclient "github.com/containers/kubernetes-mcp-server/pkg/kiali" "github.com/google/jsonschema-go/jsonschema" "k8s.io/utils/ptr" @@ -93,8 +94,8 @@ func workloadLogsHandler(params api.ToolHandlerParams) (*api.ToolCallResult, err // WorkloadLogs handles container auto-detection internally, so we can pass empty string // if container is not specified - k := params.NewKiali() - logs, err := k.WorkloadLogs(params.Context, namespace, workload, container, duration, maxLines) + kiali := kialiclient.NewKiali(params, params.AccessControlClientset().RESTConfig()) + logs, err := kiali.WorkloadLogs(params.Context, namespace, workload, container, duration, maxLines) if err != nil { return api.NewToolCallResult("", fmt.Errorf("failed to get workload logs: %v", err)), nil } diff --git a/pkg/toolsets/kiali/tools/manage_istio_config.go b/pkg/toolsets/kiali/tools/manage_istio_config.go index 2e959dda3..5f571d860 100644 --- a/pkg/toolsets/kiali/tools/manage_istio_config.go +++ b/pkg/toolsets/kiali/tools/manage_istio_config.go @@ -3,6 +3,7 @@ package tools import ( "fmt" + kialiclient "github.com/containers/kubernetes-mcp-server/pkg/kiali" "github.com/google/jsonschema-go/jsonschema" "k8s.io/utils/ptr" @@ -74,8 +75,8 @@ func istioConfigHandler(params api.ToolHandlerParams) (*api.ToolCallResult, erro if err := validateIstioConfigInput(action, namespace, group, version, kind, name, jsonData); err != nil { return api.NewToolCallResult("", err), nil } - k := params.NewKiali() - content, err := k.IstioConfig(params.Context, action, namespace, group, version, kind, name, jsonData) + kiali := kialiclient.NewKiali(params, params.AccessControlClientset().RESTConfig()) + content, err := kiali.IstioConfig(params.Context, action, namespace, group, version, kind, name, jsonData) if err != nil { return api.NewToolCallResult("", fmt.Errorf("failed to retrieve Istio configuration: %v", err)), nil } diff --git a/pkg/toolsets/kiali/toolset.go b/pkg/toolsets/kiali/toolset.go index 8855c151e..70db0e8c3 100644 --- a/pkg/toolsets/kiali/toolset.go +++ b/pkg/toolsets/kiali/toolset.go @@ -4,7 +4,6 @@ import ( "slices" "github.com/containers/kubernetes-mcp-server/pkg/api" - internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" "github.com/containers/kubernetes-mcp-server/pkg/toolsets" "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kiali/internal/defaults" kialiTools "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kiali/tools" @@ -22,7 +21,7 @@ func (t *Toolset) GetDescription() string { return defaults.ToolsetDescription() } -func (t *Toolset) GetTools(_ internalk8s.Openshift) []api.ServerTool { +func (t *Toolset) GetTools(_ api.Openshift) []api.ServerTool { return slices.Concat( kialiTools.InitGetMeshGraph(), kialiTools.InitManageIstioConfig(), diff --git a/pkg/toolsets/kubevirt/toolset.go b/pkg/toolsets/kubevirt/toolset.go index 33a860a4d..221f82543 100644 --- a/pkg/toolsets/kubevirt/toolset.go +++ b/pkg/toolsets/kubevirt/toolset.go @@ -4,7 +4,6 @@ import ( "slices" "github.com/containers/kubernetes-mcp-server/pkg/api" - internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" "github.com/containers/kubernetes-mcp-server/pkg/toolsets" vm_create "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/create" vm_lifecycle "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/lifecycle" @@ -22,7 +21,7 @@ func (t *Toolset) GetDescription() string { return "KubeVirt virtual machine management tools" } -func (t *Toolset) GetTools(o internalk8s.Openshift) []api.ServerTool { +func (t *Toolset) GetTools(_ api.Openshift) []api.ServerTool { return slices.Concat( vm_create.Tools(), vm_lifecycle.Tools(), diff --git a/pkg/toolsets/kubevirt/vm/create/tool.go b/pkg/toolsets/kubevirt/vm/create/tool.go index a7b4f27cd..d2b264f26 100644 --- a/pkg/toolsets/kubevirt/vm/create/tool.go +++ b/pkg/toolsets/kubevirt/vm/create/tool.go @@ -7,6 +7,7 @@ import ( "text/template" "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" "github.com/containers/kubernetes-mcp-server/pkg/kubevirt" "github.com/containers/kubernetes-mcp-server/pkg/output" "github.com/google/jsonschema-go/jsonschema" @@ -103,7 +104,12 @@ func create(params api.ToolHandlerParams) (*api.ToolCallResult, error) { return api.NewToolCallResult("", err), nil } - dynamicClient := params.AccessControlClientset().DynamicClient() + // Type assert to concrete type to access AccessControlClientset + k8s, ok := params.KubernetesClient.(*kubernetes.Kubernetes) + if !ok { + return api.NewToolCallResult("", fmt.Errorf("kubernetes client type assertion failed")), nil + } + dynamicClient := k8s.AccessControlClientset().DynamicClient() // Search for available DataSources dataSources := kubevirt.SearchDataSources(params.Context, dynamicClient) diff --git a/pkg/toolsets/kubevirt/vm/lifecycle/tool.go b/pkg/toolsets/kubevirt/vm/lifecycle/tool.go index fdf2b0587..f699270e8 100644 --- a/pkg/toolsets/kubevirt/vm/lifecycle/tool.go +++ b/pkg/toolsets/kubevirt/vm/lifecycle/tool.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" "github.com/containers/kubernetes-mcp-server/pkg/kubevirt" "github.com/containers/kubernetes-mcp-server/pkg/output" "github.com/google/jsonschema-go/jsonschema" @@ -75,7 +76,12 @@ func lifecycle(params api.ToolHandlerParams) (*api.ToolCallResult, error) { return api.NewToolCallResult("", err), nil } - dynamicClient := params.AccessControlClientset().DynamicClient() + // Type assert to concrete type to access AccessControlClientset + k8s, ok := params.KubernetesClient.(*kubernetes.Kubernetes) + if !ok { + return api.NewToolCallResult("", fmt.Errorf("kubernetes client type assertion failed")), nil + } + dynamicClient := k8s.AccessControlClientset().DynamicClient() var vm *unstructured.Unstructured var message string diff --git a/pkg/toolsets/toolsets_test.go b/pkg/toolsets/toolsets_test.go index 05af11a8f..971964d75 100644 --- a/pkg/toolsets/toolsets_test.go +++ b/pkg/toolsets/toolsets_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/containers/kubernetes-mcp-server/pkg/api" - "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" "github.com/stretchr/testify/suite" ) @@ -33,7 +32,7 @@ func (t *TestToolset) GetName() string { return t.name } func (t *TestToolset) GetDescription() string { return t.description } -func (t *TestToolset) GetTools(_ kubernetes.Openshift) []api.ServerTool { return nil } +func (t *TestToolset) GetTools(_ api.Openshift) []api.ServerTool { return nil } var _ api.Toolset = (*TestToolset)(nil)