Skip to content

Commit a1ae83f

Browse files
committed
refactor(api)!: replace internal Kubernetes references with api package interfaces
Signed-off-by: Marc Nuri <[email protected]>
1 parent 5623684 commit a1ae83f

Some content is hidden

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

54 files changed

+388
-311
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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package config
1+
package api
22

33
const (
44
ClusterProviderKubeConfig = "kubeconfig"

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+
ExtendedProvider
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+
ExtendedProvider
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"`
@@ -82,15 +82,15 @@ type StaticConfig struct {
8282
promptsMetadata toml.MetaData // Internal: metadata for prompts decoding
8383

8484
// Internal: parsed provider configs (not exposed to TOML package)
85-
parsedClusterProviderConfigs map[string]configapi.Extended
85+
parsedClusterProviderConfigs map[string]api.Extended
8686
// Internal: parsed toolset configs (not exposed to TOML package)
87-
parsedToolsetConfigs map[string]configapi.Extended
87+
parsedToolsetConfigs map[string]api.Extended
8888

8989
// Internal: the config.toml directory, to help resolve relative file paths
9090
configDirPath string
9191
}
9292

93-
var _ configapi.BaseConfig = (*StaticConfig)(nil)
93+
var _ api.BaseConfig = (*StaticConfig)(nil)
9494

9595
type ReadConfigOpt func(cfg *StaticConfig)
9696

@@ -306,21 +306,21 @@ func (c *StaticConfig) GetClusterProviderStrategy() string {
306306
return c.ClusterProviderStrategy
307307
}
308308

309-
func (c *StaticConfig) GetDeniedResources() []configapi.GroupVersionKind {
309+
func (c *StaticConfig) GetDeniedResources() []api.GroupVersionKind {
310310
return c.DeniedResources
311311
}
312312

313313
func (c *StaticConfig) GetKubeConfigPath() string {
314314
return c.KubeConfig
315315
}
316316

317-
func (c *StaticConfig) GetProviderConfig(strategy string) (configapi.Extended, bool) {
317+
func (c *StaticConfig) GetProviderConfig(strategy string) (api.Extended, bool) {
318318
cfg, ok := c.parsedClusterProviderConfigs[strategy]
319319

320320
return cfg, ok
321321
}
322322

323-
func (c *StaticConfig) GetToolsetConfig(name string) (configapi.Extended, bool) {
323+
func (c *StaticConfig) GetToolsetConfig(name string) (api.Extended, bool) {
324324
cfg, ok := c.parsedToolsetConfigs[name]
325325
return cfg, ok
326326
}

pkg/config/config_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"strings"
99
"testing"
1010

11-
configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config"
11+
"github.com/containers/kubernetes-mcp-server/pkg/api"
1212
"github.com/stretchr/testify/suite"
1313
)
1414

@@ -137,11 +137,11 @@ func (s *ConfigSuite) TestReadConfigValid() {
137137
s.Run("denied_resources", func() {
138138
s.Require().Lenf(config.DeniedResources, 2, "Expected 2 denied resources, got %d", len(config.DeniedResources))
139139
s.Run("contains apps/v1/Deployment", func() {
140-
s.Contains(config.DeniedResources, configapi.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
140+
s.Contains(config.DeniedResources, api.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
141141
"Expected denied resources to contain apps/v1/Deployment")
142142
})
143143
s.Run("contains rbac.authorization.k8s.io/v1/Role", func() {
144-
s.Contains(config.DeniedResources, configapi.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "Role"},
144+
s.Contains(config.DeniedResources, api.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "Role"},
145145
"Expected denied resources to contain rbac.authorization.k8s.io/v1/Role")
146146
})
147147
})
@@ -778,16 +778,16 @@ func (s *ConfigSuite) TestDropInWithDeniedResources() {
778778

779779
s.Run("drop-in replaces denied_resources array", func() {
780780
s.Len(config.DeniedResources, 2, "denied_resources should have 2 entries from drop-in")
781-
s.Contains(config.DeniedResources, configapi.GroupVersionKind{
781+
s.Contains(config.DeniedResources, api.GroupVersionKind{
782782
Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRole",
783783
})
784-
s.Contains(config.DeniedResources, configapi.GroupVersionKind{
784+
s.Contains(config.DeniedResources, api.GroupVersionKind{
785785
Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBinding",
786786
})
787787
})
788788

789789
s.Run("original denied_resources from main config are replaced", func() {
790-
s.NotContains(config.DeniedResources, configapi.GroupVersionKind{
790+
s.NotContains(config.DeniedResources, api.GroupVersionKind{
791791
Group: "apps", Version: "v1", Kind: "Deployment",
792792
}, "original entry should be replaced by drop-in")
793793
})

pkg/config/extended.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import (
55
"fmt"
66

77
"github.com/BurntSushi/toml"
8-
configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config"
8+
"github.com/containers/kubernetes-mcp-server/pkg/api"
99
)
1010

11-
type ExtendedConfigParser func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (configapi.Extended, error)
11+
type ExtendedConfigParser func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (api.Extended, error)
1212

1313
type extendedConfigRegistry struct {
1414
parsers map[string]ExtendedConfigParser
@@ -28,11 +28,11 @@ func (r *extendedConfigRegistry) register(name string, parser ExtendedConfigPars
2828
r.parsers[name] = parser
2929
}
3030

31-
func (r *extendedConfigRegistry) parse(ctx context.Context, metaData toml.MetaData, configs map[string]toml.Primitive) (map[string]configapi.Extended, error) {
31+
func (r *extendedConfigRegistry) parse(ctx context.Context, metaData toml.MetaData, configs map[string]toml.Primitive) (map[string]api.Extended, error) {
3232
if len(configs) == 0 {
33-
return make(map[string]configapi.Extended), nil
33+
return make(map[string]api.Extended), nil
3434
}
35-
parsedConfigs := make(map[string]configapi.Extended, len(configs))
35+
parsedConfigs := make(map[string]api.Extended, len(configs))
3636

3737
for name, primitive := range configs {
3838
parser, ok := r.parsers[name]

pkg/config/provider_config_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"testing"
99

1010
"github.com/BurntSushi/toml"
11-
configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config"
11+
"github.com/containers/kubernetes-mcp-server/pkg/api"
1212
"github.com/stretchr/testify/suite"
1313
)
1414

@@ -32,7 +32,7 @@ type ProviderConfigForTest struct {
3232
IntProp int `toml:"int_prop"`
3333
}
3434

35-
var _ configapi.Extended = (*ProviderConfigForTest)(nil)
35+
var _ api.Extended = (*ProviderConfigForTest)(nil)
3636

3737
func (p *ProviderConfigForTest) Validate() error {
3838
if p.StrProp == "force-error" {
@@ -41,7 +41,7 @@ func (p *ProviderConfigForTest) Validate() error {
4141
return nil
4242
}
4343

44-
func providerConfigForTestParser(_ context.Context, primitive toml.Primitive, md toml.MetaData) (configapi.Extended, error) {
44+
func providerConfigForTestParser(_ context.Context, primitive toml.Primitive, md toml.MetaData) (api.Extended, error) {
4545
var providerConfigForTest ProviderConfigForTest
4646
if err := md.PrimitiveDecode(primitive, &providerConfigForTest); err != nil {
4747
return nil, err
@@ -130,7 +130,7 @@ func (s *ProviderConfigSuite) TestReadConfigUnregisteredProviderConfig() {
130130
}
131131

132132
func (s *ProviderConfigSuite) TestReadConfigParserError() {
133-
RegisterProviderConfig("test", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (configapi.Extended, error) {
133+
RegisterProviderConfig("test", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (api.Extended, error) {
134134
return nil, errors.New("parser error forced by test")
135135
})
136136
invalidConfigPath := s.writeConfig(`
@@ -153,7 +153,7 @@ func (s *ProviderConfigSuite) TestReadConfigParserError() {
153153

154154
func (s *ProviderConfigSuite) TestConfigDirPathInContext() {
155155
var capturedDirPath string
156-
RegisterProviderConfig("test", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (configapi.Extended, error) {
156+
RegisterProviderConfig("test", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (api.Extended, error) {
157157
capturedDirPath = ConfigDirPathFromContext(ctx)
158158
var providerConfigForTest ProviderConfigForTest
159159
if err := md.PrimitiveDecode(primitive, &providerConfigForTest); err != nil {
@@ -329,7 +329,7 @@ func (s *ProviderConfigSuite) TestStandaloneConfigDirWithExtendedConfig() {
329329
func (s *ProviderConfigSuite) TestConfigDirPathInContextStandalone() {
330330
// Test that configDirPath is correctly set in context for standalone --config-dir
331331
var capturedDirPath string
332-
RegisterProviderConfig("test", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (configapi.Extended, error) {
332+
RegisterProviderConfig("test", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (api.Extended, error) {
333333
capturedDirPath = ConfigDirPathFromContext(ctx)
334334
var providerConfigForTest ProviderConfigForTest
335335
if err := md.PrimitiveDecode(primitive, &providerConfigForTest); err != nil {

0 commit comments

Comments
 (0)