Skip to content

Commit 6f832ce

Browse files
committed
feat: add operations for kubeconfig
Signed-off-by: Matthias Wessendorf <mwessend@redhat.com>
1 parent c48f333 commit 6f832ce

7 files changed

Lines changed: 557 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: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ package extension
22

33
import (
44
"context"
5+
"fmt"
56

67
authorizationv1 "k8s.io/api/authorization/v1"
78
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
89
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
910
"k8s.io/apimachinery/pkg/runtime/schema"
1011
"k8s.io/client-go/dynamic"
1112
authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1"
13+
"k8s.io/client-go/tools/clientcmd"
1214
)
1315

1416
// ResourceClient abstracts Kubernetes resource operations for testability.
@@ -25,12 +27,22 @@ type ResourceClient interface {
2527

2628
// CheckAccess checks if a user can perform an action on a resource.
2729
CheckAccess(ctx context.Context, user, verb, resource, apiGroup, namespace, resourceName string) (bool, string, error)
30+
31+
// ListContexts returns all contexts from the kubeconfig
32+
ListContexts(ctx context.Context) ([]ContextInfo, error)
33+
34+
// GetCurrentContext returns the current context name from the kubeconfig
35+
GetCurrentContext(ctx context.Context) (string, error)
36+
37+
// ViewConfig returns the kubeconfig as YAML, optionally minified
38+
ViewConfig(ctx context.Context, minify bool) (string, error)
2839
}
2940

3041
// dynamicClientAdapter adapts the Kubernetes dynamic client to the ResourceClient interface.
3142
type dynamicClientAdapter struct {
32-
client dynamic.Interface
33-
authzClient authorizationv1client.AuthorizationV1Interface
43+
client dynamic.Interface
44+
authzClient authorizationv1client.AuthorizationV1Interface
45+
kubeconfigPath string
3446
}
3547

3648
func (a *dynamicClientAdapter) Create(ctx context.Context, gvr schema.GroupVersionResource, obj *unstructured.Unstructured, namespace string) (*unstructured.Unstructured, error) {
@@ -75,3 +87,70 @@ func (a *dynamicClientAdapter) CheckAccess(ctx context.Context, user, verb, reso
7587

7688
return result.Status.Allowed, result.Status.Reason, nil
7789
}
90+
91+
func (a *dynamicClientAdapter) ListContexts(ctx context.Context) ([]ContextInfo, error) {
92+
config, err := clientcmd.LoadFromFile(a.kubeconfigPath)
93+
if err != nil {
94+
return nil, fmt.Errorf("failed to load kubeconfig: %w", err)
95+
}
96+
97+
var contexts []ContextInfo
98+
for name, context := range config.Contexts {
99+
contexts = append(contexts, ContextInfo{
100+
Name: name,
101+
Cluster: context.Cluster,
102+
User: context.AuthInfo,
103+
Namespace: context.Namespace,
104+
IsCurrent: name == config.CurrentContext,
105+
})
106+
}
107+
108+
return contexts, nil
109+
}
110+
111+
func (a *dynamicClientAdapter) GetCurrentContext(ctx context.Context) (string, error) {
112+
config, err := clientcmd.LoadFromFile(a.kubeconfigPath)
113+
if err != nil {
114+
return "", fmt.Errorf("failed to load kubeconfig: %w", err)
115+
}
116+
117+
return config.CurrentContext, nil
118+
}
119+
120+
func (a *dynamicClientAdapter) ViewConfig(ctx context.Context, minify bool) (string, error) {
121+
var config *clientcmd.DirectClientConfig
122+
123+
if minify {
124+
// Load with minify - only current context
125+
loadingRules := &clientcmd.ClientConfigLoadingRules{ExplicitPath: a.kubeconfigPath}
126+
configOverrides := &clientcmd.ConfigOverrides{}
127+
config = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides).(*clientcmd.DirectClientConfig)
128+
129+
rawConfig, err := config.RawConfig()
130+
if err != nil {
131+
return "", fmt.Errorf("failed to load minified config: %w", err)
132+
}
133+
134+
// Convert to YAML
135+
yamlBytes, err := clientcmd.Write(rawConfig)
136+
if err != nil {
137+
return "", fmt.Errorf("failed to marshal config to YAML: %w", err)
138+
}
139+
140+
return string(yamlBytes), nil
141+
}
142+
143+
// Load full config
144+
rawConfig, err := clientcmd.LoadFromFile(a.kubeconfigPath)
145+
if err != nil {
146+
return "", fmt.Errorf("failed to load kubeconfig: %w", err)
147+
}
148+
149+
// Convert to YAML
150+
yamlBytes, err := clientcmd.Write(*rawConfig)
151+
if err != nil {
152+
return "", fmt.Errorf("failed to marshal config to YAML: %w", err)
153+
}
154+
155+
return string(yamlBytes), nil
156+
}

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+
func (e *Extension) handleListContexts(ctx context.Context, req *sdk.OperationRequest) (*sdk.OperationResult, error) {
20+
if e.client == nil {
21+
return sdk.Failure(fmt.Errorf("kubernetes client not initialized")), nil
22+
}
23+
24+
e.LogInfo(ctx, "Listing kubeconfig contexts", nil)
25+
26+
contexts, err := e.client.ListContexts(ctx)
27+
if err != nil {
28+
e.LogError(ctx, "Failed to list contexts", map[string]any{
29+
"error": err.Error(),
30+
})
31+
return sdk.Failure(fmt.Errorf("failed to list contexts: %w", err)), nil
32+
}
33+
34+
if len(contexts) == 0 {
35+
return sdk.Failure(fmt.Errorf("no contexts found in kubeconfig")), nil
36+
}
37+
38+
// Find current context
39+
var currentContext string
40+
for _, ctx := range contexts {
41+
if ctx.IsCurrent {
42+
currentContext = ctx.Name
43+
break
44+
}
45+
}
46+
47+
e.LogInfo(ctx, "Contexts listed successfully", map[string]any{
48+
"count": len(contexts),
49+
"current": currentContext,
50+
})
51+
52+
// Convert contexts to output strings
53+
contextNames := make([]string, 0, len(contexts))
54+
for _, ctx := range contexts {
55+
contextNames = append(contextNames, ctx.Name)
56+
}
57+
58+
return sdk.SuccessWithOutputs(
59+
fmt.Sprintf("Found %d context(s), current: %s", len(contexts), currentContext),
60+
map[string]string{
61+
"current": currentContext,
62+
"count": fmt.Sprintf("%d", len(contexts)),
63+
},
64+
), nil
65+
}
66+
67+
func (e *Extension) handleGetCurrentContext(ctx context.Context, req *sdk.OperationRequest) (*sdk.OperationResult, error) {
68+
if e.client == nil {
69+
return sdk.Failure(fmt.Errorf("kubernetes client not initialized")), nil
70+
}
71+
72+
e.LogInfo(ctx, "Getting current kubeconfig context", nil)
73+
74+
currentContext, err := e.client.GetCurrentContext(ctx)
75+
if err != nil {
76+
e.LogError(ctx, "Failed to get current context", map[string]any{
77+
"error": err.Error(),
78+
})
79+
return sdk.Failure(fmt.Errorf("failed to get current context: %w", err)), nil
80+
}
81+
82+
if currentContext == "" {
83+
return sdk.Failure(fmt.Errorf("no current context set in kubeconfig")), nil
84+
}
85+
86+
e.LogInfo(ctx, "Current context retrieved", map[string]any{
87+
"context": currentContext,
88+
})
89+
90+
return sdk.SuccessWithOutputs(
91+
fmt.Sprintf("Current context: %s", currentContext),
92+
map[string]string{
93+
"context": currentContext,
94+
},
95+
), nil
96+
}
97+
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)