Skip to content

Commit 4f0ed3e

Browse files
authored
refactor(api)!: replace internal Kubernetes references with api package interfaces (#578)
Signed-off-by: Marc Nuri <[email protected]>
1 parent 70882a3 commit 4f0ed3e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+454
-319
lines changed

internal/tools/update-readme/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import (
99
"slices"
1010
"strings"
1111

12+
"github.com/containers/kubernetes-mcp-server/pkg/api"
1213
"github.com/containers/kubernetes-mcp-server/pkg/config"
13-
internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
1414
"github.com/containers/kubernetes-mcp-server/pkg/toolsets"
1515

1616
_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/config"
@@ -26,7 +26,7 @@ func (o *OpenShift) IsOpenShift(_ context.Context) bool {
2626
return true
2727
}
2828

29-
var _ internalk8s.Openshift = (*OpenShift)(nil)
29+
var _ api.Openshift = (*OpenShift)(nil)
3030

3131
func main() {
3232
// Snyk reports false positive unless we flow the args through filepath.Clean and filepath.Localize in this specific order
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package config
1+
package api
22

33
const (
44
ClusterProviderKubeConfig = "kubeconfig"
@@ -18,20 +18,20 @@ type ClusterProvider interface {
1818
GetKubeConfigPath() string
1919
}
2020

21-
// Extended is the interface that all configuration extensions must implement.
21+
// ExtendedConfig is the interface that all configuration extensions must implement.
2222
// Each extended config manager registers a factory function to parse its config from TOML primitives
23-
type Extended interface {
23+
type ExtendedConfig interface {
2424
// Validate validates the extended configuration. Returns an error if the configuration is invalid.
2525
Validate() error
2626
}
2727

28-
type ExtendedProvider interface {
28+
type ExtendedConfigProvider interface {
2929
// GetProviderConfig returns the extended configuration for the given provider strategy.
3030
// The boolean return value indicates whether the configuration was found.
31-
GetProviderConfig(strategy string) (Extended, bool)
31+
GetProviderConfig(strategy string) (ExtendedConfig, bool)
3232
// GetToolsetConfig returns the extended configuration for the given toolset name.
3333
// The boolean return value indicates whether the configuration was found.
34-
GetToolsetConfig(name string) (Extended, bool)
34+
GetToolsetConfig(name string) (ExtendedConfig, bool)
3535
}
3636

3737
type GroupVersionKind struct {
@@ -49,5 +49,5 @@ type BaseConfig interface {
4949
AuthProvider
5050
ClusterProvider
5151
DeniedResourcesProvider
52-
ExtendedProvider
52+
ExtendedConfigProvider
5353
}

pkg/api/imports_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package api
2+
3+
import (
4+
"go/build"
5+
"strings"
6+
"testing"
7+
8+
"github.com/stretchr/testify/suite"
9+
)
10+
11+
const modulePrefix = "github.com/containers/kubernetes-mcp-server/"
12+
13+
// ImportsSuite verifies that pkg/api doesn't accidentally import internal packages
14+
// that would create cyclic dependencies.
15+
type ImportsSuite struct {
16+
suite.Suite
17+
}
18+
19+
func (s *ImportsSuite) TestNoCyclicDependencies() {
20+
// Whitelist of allowed internal packages that pkg/api can import.
21+
// Any other internal import will cause the test to fail.
22+
allowedInternalPackages := map[string]bool{
23+
"github.com/containers/kubernetes-mcp-server/pkg/output": true,
24+
}
25+
26+
s.Run("pkg/api only imports whitelisted internal packages", func() {
27+
pkg, err := build.Import("github.com/containers/kubernetes-mcp-server/pkg/api", "", 0)
28+
s.Require().NoError(err, "Failed to import pkg/api")
29+
30+
for _, imp := range pkg.Imports {
31+
// Skip external packages (not part of this module)
32+
if !strings.HasPrefix(imp, modulePrefix) {
33+
continue
34+
}
35+
36+
// Internal package - must be in whitelist
37+
if !allowedInternalPackages[imp] {
38+
s.Failf("Forbidden internal import detected",
39+
"pkg/api imports %q which is not in the whitelist. "+
40+
"To prevent cyclic dependencies, pkg/api can only import: %v. "+
41+
"If this import is intentional, add it to allowedInternalPackages in this test.",
42+
imp, keys(allowedInternalPackages))
43+
}
44+
}
45+
})
46+
}
47+
48+
func keys(m map[string]bool) []string {
49+
result := make([]string, 0, len(m))
50+
for k := range m {
51+
result = append(result, k)
52+
}
53+
return result
54+
}
55+
56+
func TestImports(t *testing.T) {
57+
suite.Run(t, new(ImportsSuite))
58+
}

pkg/api/kubernetes.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package api
2+
3+
import (
4+
"context"
5+
6+
"k8s.io/apimachinery/pkg/api/meta"
7+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
9+
"k8s.io/apimachinery/pkg/runtime"
10+
"k8s.io/apimachinery/pkg/runtime/schema"
11+
"k8s.io/cli-runtime/pkg/genericclioptions"
12+
"k8s.io/client-go/discovery"
13+
"k8s.io/client-go/dynamic"
14+
"k8s.io/client-go/kubernetes"
15+
"k8s.io/client-go/rest"
16+
"k8s.io/metrics/pkg/apis/metrics"
17+
metricsv1beta1 "k8s.io/metrics/pkg/client/clientset/versioned/typed/metrics/v1beta1"
18+
)
19+
20+
// Openshift provides OpenShift-specific detection capabilities.
21+
// This interface is used by toolsets to conditionally enable OpenShift-specific tools.
22+
type Openshift interface {
23+
IsOpenShift(context.Context) bool
24+
}
25+
26+
// ListOptions contains options for listing Kubernetes resources.
27+
type ListOptions struct {
28+
metav1.ListOptions
29+
AsTable bool
30+
}
31+
32+
// PodsTopOptions contains options for getting pod metrics.
33+
type PodsTopOptions struct {
34+
metav1.ListOptions
35+
AllNamespaces bool
36+
Namespace string
37+
Name string
38+
}
39+
40+
// NodesTopOptions contains options for getting node metrics.
41+
type NodesTopOptions struct {
42+
metav1.ListOptions
43+
Name string
44+
}
45+
46+
type KubernetesClientSet interface {
47+
genericclioptions.RESTClientGetter
48+
kubernetes.Interface
49+
// NamespaceOrDefault returns the provided namespace or the default configured namespace if empty
50+
NamespaceOrDefault(namespace string) string
51+
RESTConfig() *rest.Config
52+
RESTMapper() meta.ResettableRESTMapper
53+
DiscoveryClient() discovery.CachedDiscoveryInterface
54+
DynamicClient() dynamic.Interface
55+
MetricsV1beta1Client() *metricsv1beta1.MetricsV1beta1Client
56+
}
57+
58+
// KubernetesClient defines the interface for Kubernetes operations that tool and prompt handlers need.
59+
// This interface abstracts the concrete Kubernetes implementation to allow for better decoupling
60+
// and testability. The pkg/kubernetes.Kubernetes type implements this interface.
61+
//
62+
// For toolsets that need direct access to the Kubernetes clientset (e.g., for DynamicClient),
63+
// they can type-assert to ClientsetProvider.
64+
type KubernetesClient interface {
65+
// AccessControlClientset provides access to the underlying Kubernetes clientset with access control enforced
66+
AccessControlClientset() KubernetesClientSet
67+
68+
// --- Resource Operations ---
69+
70+
// ResourcesList lists resources of the specified GroupVersionKind
71+
ResourcesList(ctx context.Context, gvk *schema.GroupVersionKind, namespace string, options ListOptions) (runtime.Unstructured, error)
72+
// ResourcesGet retrieves a single resource by name
73+
ResourcesGet(ctx context.Context, gvk *schema.GroupVersionKind, namespace, name string) (*unstructured.Unstructured, error)
74+
// ResourcesCreateOrUpdate creates or updates resources from a YAML/JSON string
75+
ResourcesCreateOrUpdate(ctx context.Context, resource string) ([]*unstructured.Unstructured, error)
76+
// ResourcesDelete deletes a resource by name
77+
ResourcesDelete(ctx context.Context, gvk *schema.GroupVersionKind, namespace, name string) error
78+
// ResourcesScale gets or sets the scale of a resource
79+
ResourcesScale(ctx context.Context, gvk *schema.GroupVersionKind, namespace, name string, desiredScale int64, shouldScale bool) (*unstructured.Unstructured, error)
80+
81+
// --- Namespace Operations ---
82+
83+
// NamespacesList lists all namespaces
84+
NamespacesList(ctx context.Context, options ListOptions) (runtime.Unstructured, error)
85+
// ProjectsList lists all OpenShift projects
86+
ProjectsList(ctx context.Context, options ListOptions) (runtime.Unstructured, error)
87+
88+
// --- Pod Operations ---
89+
90+
// PodsListInAllNamespaces lists pods across all namespaces
91+
PodsListInAllNamespaces(ctx context.Context, options ListOptions) (runtime.Unstructured, error)
92+
// PodsListInNamespace lists pods in a specific namespace
93+
PodsListInNamespace(ctx context.Context, namespace string, options ListOptions) (runtime.Unstructured, error)
94+
// PodsGet retrieves a single pod by name
95+
PodsGet(ctx context.Context, namespace, name string) (*unstructured.Unstructured, error)
96+
// PodsDelete deletes a pod and its managed resources
97+
PodsDelete(ctx context.Context, namespace, name string) (string, error)
98+
// PodsLog retrieves logs from a pod container
99+
PodsLog(ctx context.Context, namespace, name, container string, previous bool, tail int64) (string, error)
100+
// PodsRun creates and runs a new pod with optional service and route
101+
PodsRun(ctx context.Context, namespace, name, image string, port int32) ([]*unstructured.Unstructured, error)
102+
// PodsTop retrieves pod metrics
103+
PodsTop(ctx context.Context, options PodsTopOptions) (*metrics.PodMetricsList, error)
104+
// PodsExec executes a command in a pod container
105+
PodsExec(ctx context.Context, namespace, name, container string, command []string) (string, error)
106+
107+
// --- Node Operations ---
108+
109+
// NodesLog retrieves logs from a node
110+
NodesLog(ctx context.Context, name string, query string, tailLines int64) (string, error)
111+
// NodesStatsSummary retrieves stats summary from a node
112+
NodesStatsSummary(ctx context.Context, name string) (string, error)
113+
// NodesTop retrieves node metrics
114+
NodesTop(ctx context.Context, options NodesTopOptions) (*metrics.NodeMetricsList, error)
115+
116+
// --- Event Operations ---
117+
118+
// EventsList lists events in a namespace
119+
EventsList(ctx context.Context, namespace string) ([]map[string]any, error)
120+
121+
// --- Configuration Operations ---
122+
123+
// ConfigurationContextsList returns the list of available context names
124+
ConfigurationContextsList() (map[string]string, error)
125+
// ConfigurationContextsDefault returns the current context name
126+
ConfigurationContextsDefault() (string, error)
127+
// ConfigurationView returns the kubeconfig content
128+
ConfigurationView(minify bool) (runtime.Object, error)
129+
}

pkg/api/prompts.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
package api
22

3-
import (
4-
"context"
5-
6-
internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
7-
)
3+
import "context"
84

95
// ServerPrompt represents a prompt that can be registered with the MCP server.
106
// Prompts provide pre-defined workflow templates and guidance to AI assistants.
@@ -88,7 +84,8 @@ func NewPromptCallResult(description string, messages []PromptMessage, err error
8884
// PromptHandlerParams contains the parameters passed to a prompt handler
8985
type PromptHandlerParams struct {
9086
context.Context
91-
*internalk8s.Kubernetes
87+
ExtendedConfigProvider
88+
KubernetesClient
9289
PromptCallRequest
9390
}
9491

pkg/api/toolsets.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"encoding/json"
66

7-
internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
87
"github.com/containers/kubernetes-mcp-server/pkg/output"
98
"github.com/google/jsonschema-go/jsonschema"
109
)
@@ -43,7 +42,7 @@ type Toolset interface {
4342
// GetDescription returns a human-readable description of the toolset.
4443
// Will be used to generate documentation and help text.
4544
GetDescription() string
46-
GetTools(o internalk8s.Openshift) []ServerTool
45+
GetTools(o Openshift) []ServerTool
4746
}
4847

4948
type ToolCallRequest interface {
@@ -66,7 +65,8 @@ func NewToolCallResult(content string, err error) *ToolCallResult {
6665

6766
type ToolHandlerParams struct {
6867
context.Context
69-
*internalk8s.Kubernetes
68+
ExtendedConfigProvider
69+
KubernetesClient
7070
ToolCallRequest
7171
ListOutput output.Output
7272
}

pkg/config/config.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"strings"
1111

1212
"github.com/BurntSushi/toml"
13-
configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config"
13+
"github.com/containers/kubernetes-mcp-server/pkg/api"
1414
"k8s.io/klog/v2"
1515
)
1616

@@ -21,7 +21,7 @@ const (
2121
// StaticConfig is the configuration for the server.
2222
// It allows to configure server specific settings and tools to be enabled or disabled.
2323
type StaticConfig struct {
24-
DeniedResources []configapi.GroupVersionKind `toml:"denied_resources"`
24+
DeniedResources []api.GroupVersionKind `toml:"denied_resources"`
2525

2626
LogLevel int `toml:"log_level,omitzero"`
2727
Port string `toml:"port,omitempty"`
@@ -86,15 +86,15 @@ type StaticConfig struct {
8686
ServerInstructions string `toml:"server_instructions,omitempty"`
8787

8888
// Internal: parsed provider configs (not exposed to TOML package)
89-
parsedClusterProviderConfigs map[string]configapi.Extended
89+
parsedClusterProviderConfigs map[string]api.ExtendedConfig
9090
// Internal: parsed toolset configs (not exposed to TOML package)
91-
parsedToolsetConfigs map[string]configapi.Extended
91+
parsedToolsetConfigs map[string]api.ExtendedConfig
9292

9393
// Internal: the config.toml directory, to help resolve relative file paths
9494
configDirPath string
9595
}
9696

97-
var _ configapi.BaseConfig = (*StaticConfig)(nil)
97+
var _ api.BaseConfig = (*StaticConfig)(nil)
9898

9999
type ReadConfigOpt func(cfg *StaticConfig)
100100

@@ -310,21 +310,21 @@ func (c *StaticConfig) GetClusterProviderStrategy() string {
310310
return c.ClusterProviderStrategy
311311
}
312312

313-
func (c *StaticConfig) GetDeniedResources() []configapi.GroupVersionKind {
313+
func (c *StaticConfig) GetDeniedResources() []api.GroupVersionKind {
314314
return c.DeniedResources
315315
}
316316

317317
func (c *StaticConfig) GetKubeConfigPath() string {
318318
return c.KubeConfig
319319
}
320320

321-
func (c *StaticConfig) GetProviderConfig(strategy string) (configapi.Extended, bool) {
321+
func (c *StaticConfig) GetProviderConfig(strategy string) (api.ExtendedConfig, bool) {
322322
cfg, ok := c.parsedClusterProviderConfigs[strategy]
323323

324324
return cfg, ok
325325
}
326326

327-
func (c *StaticConfig) GetToolsetConfig(name string) (configapi.Extended, bool) {
327+
func (c *StaticConfig) GetToolsetConfig(name string) (api.ExtendedConfig, bool) {
328328
cfg, ok := c.parsedToolsetConfigs[name]
329329
return cfg, ok
330330
}

0 commit comments

Comments
 (0)