Skip to content

Commit 0a8609c

Browse files
authored
feat: add operations for kubeconfig (#15)
1 parent c48f333 commit 0a8609c

7 files changed

Lines changed: 640 additions & 7 deletions

File tree

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ This extension enables declarative Kubernetes interactions within mcpchecker tas
1111
| `kubernetes.authCanI` | Check if a user or service account can perform an action on a resource |
1212
| `kubernetes.create` | Create a Kubernetes resource |
1313
| `kubernetes.delete` | Delete a Kubernetes resource |
14+
| `kubernetes.getCurrentContext` | Get the current context from kubeconfig |
15+
| `kubernetes.listContexts` | List all contexts from kubeconfig |
16+
| `kubernetes.viewConfig` | View kubeconfig as YAML (optionally minified) |
1417
| `kubernetes.wait` | Wait for a condition on a resource (e.g., `Ready`, `Available`) |
1518

1619
## Configuration
@@ -132,6 +135,43 @@ Waits for a condition on a resource. Supports configurable timeout and expected
132135
timeout: 5m # optional, defaults to 60s
133136
```
134137

138+
### kubernetes.listContexts
139+
140+
Lists all contexts from the kubeconfig file, including which one is currently active.
141+
142+
```yaml
143+
- kubernetes.listContexts:
144+
# No parameters required
145+
```
146+
147+
**Outputs:**
148+
- `current`: Name of the current context
149+
- `count`: Number of contexts found
150+
151+
### kubernetes.getCurrentContext
152+
153+
Returns the current context name from the kubeconfig.
154+
155+
```yaml
156+
- kubernetes.getCurrentContext:
157+
# No parameters required
158+
```
159+
160+
**Outputs:**
161+
- `context`: Name of the current context
162+
163+
### kubernetes.viewConfig
164+
165+
Views the kubeconfig as YAML, optionally minified to show only the current context.
166+
167+
```yaml
168+
- kubernetes.viewConfig:
169+
minify: false # optional, defaults to false
170+
```
171+
172+
**Outputs:**
173+
- `config`: The kubeconfig content as YAML
174+
135175
## Contributing
136176

137177
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, project structure, and guidelines for adding new operations.

pkg/extension/client.go

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ package extension
22

33
import (
44
"context"
5+
"fmt"
6+
"sort"
57

68
authorizationv1 "k8s.io/api/authorization/v1"
79
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
810
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
911
"k8s.io/apimachinery/pkg/runtime/schema"
1012
"k8s.io/client-go/dynamic"
1113
authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1"
14+
"k8s.io/client-go/tools/clientcmd"
15+
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
1216
)
1317

1418
// ResourceClient abstracts Kubernetes resource operations for testability.
@@ -25,12 +29,25 @@ type ResourceClient interface {
2529

2630
// CheckAccess checks if a user can perform an action on a resource.
2731
CheckAccess(ctx context.Context, user, verb, resource, apiGroup, namespace, resourceName string) (bool, string, error)
32+
33+
// ListContexts returns all contexts from the kubeconfig sorted by name.
34+
// Each context includes its name, cluster, user, namespace, and whether it's the current context.
35+
ListContexts(ctx context.Context) ([]ContextInfo, error)
36+
37+
// GetCurrentContext returns the current context name from the kubeconfig.
38+
// Returns an error if the kubeconfig cannot be loaded.
39+
GetCurrentContext(ctx context.Context) (string, error)
40+
41+
// ViewConfig returns the kubeconfig as YAML.
42+
// When minify is true, only the current context and its dependencies are included.
43+
ViewConfig(ctx context.Context, minify bool) (string, error)
2844
}
2945

3046
// dynamicClientAdapter adapts the Kubernetes dynamic client to the ResourceClient interface.
3147
type dynamicClientAdapter struct {
32-
client dynamic.Interface
33-
authzClient authorizationv1client.AuthorizationV1Interface
48+
client dynamic.Interface
49+
authzClient authorizationv1client.AuthorizationV1Interface
50+
kubeconfigPath string
3451
}
3552

3653
func (a *dynamicClientAdapter) Create(ctx context.Context, gvr schema.GroupVersionResource, obj *unstructured.Unstructured, namespace string) (*unstructured.Unstructured, error) {
@@ -75,3 +92,99 @@ func (a *dynamicClientAdapter) CheckAccess(ctx context.Context, user, verb, reso
7592

7693
return result.Status.Allowed, result.Status.Reason, nil
7794
}
95+
96+
func (a *dynamicClientAdapter) ListContexts(ctx context.Context) ([]ContextInfo, error) {
97+
config, err := clientcmd.LoadFromFile(a.kubeconfigPath)
98+
if err != nil {
99+
return nil, fmt.Errorf("failed to load kubeconfig: %w", err)
100+
}
101+
102+
var contexts []ContextInfo
103+
for name, context := range config.Contexts {
104+
contexts = append(contexts, ContextInfo{
105+
Name: name,
106+
Cluster: context.Cluster,
107+
User: context.AuthInfo,
108+
Namespace: context.Namespace,
109+
IsCurrent: name == config.CurrentContext,
110+
})
111+
}
112+
113+
// Sort contexts by name for deterministic output
114+
sort.Slice(contexts, func(i, j int) bool {
115+
return contexts[i].Name < contexts[j].Name
116+
})
117+
118+
return contexts, nil
119+
}
120+
121+
func (a *dynamicClientAdapter) GetCurrentContext(ctx context.Context) (string, error) {
122+
config, err := clientcmd.LoadFromFile(a.kubeconfigPath)
123+
if err != nil {
124+
return "", fmt.Errorf("failed to load kubeconfig: %w", err)
125+
}
126+
127+
return config.CurrentContext, nil
128+
}
129+
130+
func (a *dynamicClientAdapter) ViewConfig(ctx context.Context, minify bool) (string, error) {
131+
// Load the full config
132+
rawConfig, err := clientcmd.LoadFromFile(a.kubeconfigPath)
133+
if err != nil {
134+
return "", fmt.Errorf("failed to load kubeconfig: %w", err)
135+
}
136+
137+
// Apply minification if requested
138+
if minify {
139+
// Get current context
140+
currentContext := rawConfig.CurrentContext
141+
if currentContext == "" {
142+
return "", fmt.Errorf("no current context set in kubeconfig")
143+
}
144+
145+
// Create minified config with only current context and its dependencies
146+
currentCtx, exists := rawConfig.Contexts[currentContext]
147+
if !exists {
148+
return "", fmt.Errorf("current context %q not found in kubeconfig", currentContext)
149+
}
150+
151+
minifiedConfig := clientcmdapi.NewConfig()
152+
minifiedConfig.CurrentContext = currentContext
153+
minifiedConfig.Contexts = map[string]*clientcmdapi.Context{
154+
currentContext: currentCtx,
155+
}
156+
157+
// Add the cluster referenced by current context
158+
if currentCtx.Cluster == "" {
159+
return "", fmt.Errorf("current context %q has no cluster", currentContext)
160+
}
161+
if cluster, exists := rawConfig.Clusters[currentCtx.Cluster]; exists {
162+
minifiedConfig.Clusters = map[string]*clientcmdapi.Cluster{
163+
currentCtx.Cluster: cluster,
164+
}
165+
} else {
166+
return "", fmt.Errorf("cluster %q not found in kubeconfig", currentCtx.Cluster)
167+
}
168+
169+
// Add the user referenced by current context (optional)
170+
if currentCtx.AuthInfo != "" {
171+
authInfo, exists := rawConfig.AuthInfos[currentCtx.AuthInfo]
172+
if !exists {
173+
return "", fmt.Errorf("user %q not found in kubeconfig", currentCtx.AuthInfo)
174+
}
175+
minifiedConfig.AuthInfos = map[string]*clientcmdapi.AuthInfo{
176+
currentCtx.AuthInfo: authInfo,
177+
}
178+
}
179+
180+
rawConfig = minifiedConfig
181+
}
182+
183+
// Convert to YAML
184+
yamlBytes, err := clientcmd.Write(*rawConfig)
185+
if err != nil {
186+
return "", fmt.Errorf("failed to marshal config to YAML: %w", err)
187+
}
188+
189+
return string(yamlBytes), nil
190+
}

pkg/extension/extension.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,11 @@ func (e *Extension) handleInitialize(config map[string]any) error {
8080
return fmt.Errorf("failed to create authorization client: %w", err)
8181
}
8282

83-
e.client = &dynamicClientAdapter{client: client, authzClient: authzClient}
83+
e.client = &dynamicClientAdapter{
84+
client: client,
85+
authzClient: authzClient,
86+
kubeconfigPath: kubeconfigPath,
87+
}
8488
return nil
8589
}
8690

pkg/extension/kubeconfig.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package extension
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/mcpchecker/mcpchecker/pkg/extension/sdk"
8+
)
9+
10+
// ContextInfo represents information about a Kubernetes context
11+
type ContextInfo struct {
12+
Name string `json:"name"`
13+
Cluster string `json:"cluster"`
14+
User string `json:"user"`
15+
Namespace string `json:"namespace,omitempty"`
16+
IsCurrent bool `json:"isCurrent"`
17+
}
18+
19+
// handleListContexts lists all contexts from the kubeconfig file.
20+
// Returns the current context name and total count.
21+
func (e *Extension) handleListContexts(ctx context.Context, req *sdk.OperationRequest) (*sdk.OperationResult, error) {
22+
if e.client == nil {
23+
return sdk.Failure(fmt.Errorf("kubernetes client not initialized")), nil
24+
}
25+
26+
e.LogInfo(ctx, "Listing kubeconfig contexts", nil)
27+
28+
contexts, err := e.client.ListContexts(ctx)
29+
if err != nil {
30+
e.LogError(ctx, "Failed to list contexts", map[string]any{
31+
"error": err.Error(),
32+
})
33+
return sdk.Failure(fmt.Errorf("failed to list contexts: %w", err)), nil
34+
}
35+
36+
if len(contexts) == 0 {
37+
return sdk.Failure(fmt.Errorf("no contexts found in kubeconfig")), nil
38+
}
39+
40+
// Find current context
41+
var currentContext string
42+
for _, c := range contexts {
43+
if c.IsCurrent {
44+
currentContext = c.Name
45+
break
46+
}
47+
}
48+
49+
e.LogInfo(ctx, "Contexts listed successfully", map[string]any{
50+
"count": len(contexts),
51+
"current": currentContext,
52+
})
53+
54+
return sdk.SuccessWithOutputs(
55+
fmt.Sprintf("Found %d context(s), current: %s", len(contexts), currentContext),
56+
map[string]string{
57+
"current": currentContext,
58+
"count": fmt.Sprintf("%d", len(contexts)),
59+
},
60+
), nil
61+
}
62+
63+
// handleGetCurrentContext returns the current context name from the kubeconfig.
64+
// Fails if no current context is set.
65+
func (e *Extension) handleGetCurrentContext(ctx context.Context, req *sdk.OperationRequest) (*sdk.OperationResult, error) {
66+
if e.client == nil {
67+
return sdk.Failure(fmt.Errorf("kubernetes client not initialized")), nil
68+
}
69+
70+
e.LogInfo(ctx, "Getting current kubeconfig context", nil)
71+
72+
currentContext, err := e.client.GetCurrentContext(ctx)
73+
if err != nil {
74+
e.LogError(ctx, "Failed to get current context", map[string]any{
75+
"error": err.Error(),
76+
})
77+
return sdk.Failure(fmt.Errorf("failed to get current context: %w", err)), nil
78+
}
79+
80+
if currentContext == "" {
81+
return sdk.Failure(fmt.Errorf("no current context set in kubeconfig")), nil
82+
}
83+
84+
e.LogInfo(ctx, "Current context retrieved", map[string]any{
85+
"context": currentContext,
86+
})
87+
88+
return sdk.SuccessWithOutputs(
89+
fmt.Sprintf("Current context: %s", currentContext),
90+
map[string]string{
91+
"context": currentContext,
92+
},
93+
), nil
94+
}
95+
96+
// handleViewConfig returns the kubeconfig as YAML.
97+
// When minify is true, returns only the current context and its dependencies.
98+
func (e *Extension) handleViewConfig(ctx context.Context, req *sdk.OperationRequest) (*sdk.OperationResult, error) {
99+
if e.client == nil {
100+
return sdk.Failure(fmt.Errorf("kubernetes client not initialized")), nil
101+
}
102+
103+
// Parse args
104+
args, ok := req.Args.(map[string]any)
105+
if !ok {
106+
args = make(map[string]any)
107+
}
108+
109+
minify := false
110+
if m, ok := args["minify"].(bool); ok {
111+
minify = m
112+
}
113+
114+
e.LogInfo(ctx, "Viewing kubeconfig", map[string]any{
115+
"minify": minify,
116+
})
117+
118+
configYAML, err := e.client.ViewConfig(ctx, minify)
119+
if err != nil {
120+
e.LogError(ctx, "Failed to view config", map[string]any{
121+
"error": err.Error(),
122+
})
123+
return sdk.Failure(fmt.Errorf("failed to view config: %w", err)), nil
124+
}
125+
126+
e.LogInfo(ctx, "Kubeconfig retrieved successfully", nil)
127+
128+
return sdk.SuccessWithOutputs(
129+
"Kubeconfig retrieved",
130+
map[string]string{
131+
"config": configYAML,
132+
},
133+
), nil
134+
}

0 commit comments

Comments
 (0)