Skip to content

Commit cdff82e

Browse files
Support to put,get and list files on k8s node
1 parent 09c8b23 commit cdff82e

File tree

8 files changed

+948
-8
lines changed

8 files changed

+948
-8
lines changed

pkg/kubernetes/nodes.go

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@ package kubernetes
33
import (
44
"context"
55
"fmt"
6+
"io"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
"time"
11+
12+
v1 "k8s.io/api/core/v1"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"k8s.io/apimachinery/pkg/util/rand"
15+
"k8s.io/client-go/tools/remotecommand"
16+
"k8s.io/utils/ptr"
17+
18+
"github.com/containers/kubernetes-mcp-server/pkg/version"
619
)
720

821
func (k *Kubernetes) NodesLog(ctx context.Context, name string, query string, tailLines int64) (string, error) {
@@ -59,3 +72,247 @@ func (k *Kubernetes) NodesStatsSummary(ctx context.Context, name string) (string
5972

6073
return string(rawData), nil
6174
}
75+
76+
// NodeFilesOptions contains options for node file operations
77+
type NodeFilesOptions struct {
78+
NodeName string
79+
Operation string // "put", "get", "list"
80+
SourcePath string
81+
DestPath string
82+
Namespace string
83+
Image string
84+
Privileged bool
85+
}
86+
87+
// NodesFiles handles file operations on a node filesystem by creating a privileged pod
88+
func (k *Kubernetes) NodesFiles(ctx context.Context, opts NodeFilesOptions) (string, error) {
89+
// Set defaults
90+
if opts.Namespace == "" {
91+
opts.Namespace = "default"
92+
}
93+
if opts.Image == "" {
94+
opts.Image = "busybox"
95+
}
96+
97+
// Create privileged pod for accessing node filesystem
98+
podName := fmt.Sprintf("node-files-%s", rand.String(5))
99+
pod := &v1.Pod{
100+
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"},
101+
ObjectMeta: metav1.ObjectMeta{
102+
Name: podName,
103+
Namespace: opts.Namespace,
104+
Labels: map[string]string{
105+
AppKubernetesName: podName,
106+
AppKubernetesComponent: "node-files",
107+
AppKubernetesManagedBy: version.BinaryName,
108+
},
109+
},
110+
Spec: v1.PodSpec{
111+
NodeName: opts.NodeName,
112+
RestartPolicy: v1.RestartPolicyNever,
113+
Containers: []v1.Container{{
114+
Name: "node-files",
115+
Image: opts.Image,
116+
Command: []string{"/bin/sh", "-c", "sleep 3600"},
117+
SecurityContext: &v1.SecurityContext{
118+
Privileged: ptr.To(opts.Privileged),
119+
},
120+
VolumeMounts: []v1.VolumeMount{{
121+
Name: "node-root",
122+
MountPath: "/host",
123+
}},
124+
}},
125+
Volumes: []v1.Volume{{
126+
Name: "node-root",
127+
VolumeSource: v1.VolumeSource{
128+
HostPath: &v1.HostPathVolumeSource{
129+
Path: "/",
130+
},
131+
},
132+
}},
133+
},
134+
}
135+
136+
// Create the pod
137+
pods, err := k.manager.accessControlClientSet.Pods(opts.Namespace)
138+
if err != nil {
139+
return "", fmt.Errorf("failed to get pods client: %w", err)
140+
}
141+
142+
createdPod, err := pods.Create(ctx, pod, metav1.CreateOptions{})
143+
if err != nil {
144+
return "", fmt.Errorf("failed to create pod: %w", err)
145+
}
146+
147+
// Ensure pod is deleted after operation
148+
defer func() {
149+
deleteCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
150+
defer cancel()
151+
_ = pods.Delete(deleteCtx, podName, metav1.DeleteOptions{})
152+
}()
153+
154+
// Wait for pod to be ready
155+
if err := k.waitForPodReady(ctx, opts.Namespace, podName, 2*time.Minute); err != nil {
156+
return "", fmt.Errorf("pod failed to become ready: %w", err)
157+
}
158+
159+
// Perform the requested operation
160+
var result string
161+
var opErr error
162+
switch opts.Operation {
163+
case "put":
164+
result, opErr = k.nodeFilesPut(ctx, opts.Namespace, podName, opts.SourcePath, opts.DestPath)
165+
case "get":
166+
result, opErr = k.nodeFilesGet(ctx, opts.Namespace, podName, opts.SourcePath, opts.DestPath)
167+
case "list":
168+
result, opErr = k.nodeFilesList(ctx, opts.Namespace, podName, opts.SourcePath)
169+
default:
170+
return "", fmt.Errorf("unknown operation: %s", opts.Operation)
171+
}
172+
173+
_ = createdPod
174+
return result, opErr
175+
}
176+
177+
// nodeFilesPut copies a file from local filesystem to node filesystem
178+
func (k *Kubernetes) nodeFilesPut(ctx context.Context, namespace, podName, sourcePath, destPath string) (string, error) {
179+
// Read local file content
180+
content, err := os.ReadFile(sourcePath)
181+
if err != nil {
182+
return "", fmt.Errorf("failed to read source file: %w", err)
183+
}
184+
185+
// Create destination directory if needed
186+
destDir := filepath.Dir(destPath)
187+
if destDir != "." && destDir != "/" {
188+
mkdirCmd := []string{"/bin/sh", "-c", fmt.Sprintf("mkdir -p /host%s", destDir)}
189+
if _, err := k.execInPod(ctx, namespace, podName, mkdirCmd); err != nil {
190+
return "", fmt.Errorf("failed to create destination directory: %w", err)
191+
}
192+
}
193+
194+
// Write content using cat command
195+
escapedContent := strings.ReplaceAll(string(content), "'", "'\\''")
196+
writeCmd := []string{"/bin/sh", "-c", fmt.Sprintf("cat > /host%s << 'EOF'\n%s\nEOF", destPath, escapedContent)}
197+
198+
if _, err := k.execInPod(ctx, namespace, podName, writeCmd); err != nil {
199+
return "", fmt.Errorf("failed to write file to node: %w", err)
200+
}
201+
202+
return fmt.Sprintf("File successfully copied from %s to node:%s", sourcePath, destPath), nil
203+
}
204+
205+
// nodeFilesGet copies a file from node filesystem to local filesystem
206+
func (k *Kubernetes) nodeFilesGet(ctx context.Context, namespace, podName, sourcePath, destPath string) (string, error) {
207+
// Read file content from node using cat
208+
readCmd := []string{"/bin/sh", "-c", fmt.Sprintf("cat /host%s", sourcePath)}
209+
content, err := k.execInPod(ctx, namespace, podName, readCmd)
210+
if err != nil {
211+
return "", fmt.Errorf("failed to read file from node: %w", err)
212+
}
213+
214+
// Determine destination path
215+
if destPath == "" {
216+
destPath = filepath.Base(sourcePath)
217+
}
218+
219+
// Create local destination directory if needed
220+
destDir := filepath.Dir(destPath)
221+
if destDir != "." && destDir != "" {
222+
if err := os.MkdirAll(destDir, 0755); err != nil {
223+
return "", fmt.Errorf("failed to create local directory: %w", err)
224+
}
225+
}
226+
227+
// Write to local file
228+
if err := os.WriteFile(destPath, []byte(content), 0644); err != nil {
229+
return "", fmt.Errorf("failed to write local file: %w", err)
230+
}
231+
232+
return fmt.Sprintf("File successfully copied from node:%s to %s", sourcePath, destPath), nil
233+
}
234+
235+
// nodeFilesList lists files in a directory on node filesystem
236+
func (k *Kubernetes) nodeFilesList(ctx context.Context, namespace, podName, path string) (string, error) {
237+
// List directory contents using ls
238+
listCmd := []string{"/bin/sh", "-c", fmt.Sprintf("ls -la /host%s", path)}
239+
output, err := k.execInPod(ctx, namespace, podName, listCmd)
240+
if err != nil {
241+
return "", fmt.Errorf("failed to list directory: %w", err)
242+
}
243+
244+
return output, nil
245+
}
246+
247+
// execInPod executes a command in the pod and returns the output
248+
func (k *Kubernetes) execInPod(ctx context.Context, namespace, podName string, command []string) (string, error) {
249+
podExecOptions := &v1.PodExecOptions{
250+
Container: "node-files",
251+
Command: command,
252+
Stdout: true,
253+
Stderr: true,
254+
}
255+
256+
executor, err := k.manager.accessControlClientSet.PodsExec(namespace, podName, podExecOptions)
257+
if err != nil {
258+
return "", err
259+
}
260+
261+
stdout := &strings.Builder{}
262+
stderr := &strings.Builder{}
263+
264+
if err = executor.StreamWithContext(ctx, remotecommand.StreamOptions{
265+
Stdout: stdout,
266+
Stderr: stderr,
267+
Tty: false,
268+
}); err != nil {
269+
if stderr.Len() > 0 {
270+
return "", fmt.Errorf("exec error: %s: %w", stderr.String(), err)
271+
}
272+
return "", err
273+
}
274+
275+
if stderr.Len() > 0 && stdout.Len() == 0 {
276+
return stderr.String(), nil
277+
}
278+
279+
return stdout.String(), nil
280+
}
281+
282+
// waitForPodReady waits for a pod to be ready
283+
func (k *Kubernetes) waitForPodReady(ctx context.Context, namespace, podName string, timeout time.Duration) error {
284+
pods, err := k.manager.accessControlClientSet.Pods(namespace)
285+
if err != nil {
286+
return err
287+
}
288+
289+
deadline := time.Now().Add(timeout)
290+
for {
291+
if time.Now().After(deadline) {
292+
return fmt.Errorf("timeout waiting for pod to be ready")
293+
}
294+
295+
pod, err := pods.Get(ctx, podName, metav1.GetOptions{})
296+
if err != nil {
297+
return err
298+
}
299+
300+
// Check if pod is ready
301+
if pod.Status.Phase == v1.PodRunning {
302+
for _, condition := range pod.Status.Conditions {
303+
if condition.Type == v1.PodReady && condition.Status == v1.ConditionTrue {
304+
return nil
305+
}
306+
}
307+
}
308+
309+
if pod.Status.Phase == v1.PodFailed {
310+
return fmt.Errorf("pod failed")
311+
}
312+
313+
time.Sleep(2 * time.Second)
314+
}
315+
}
316+
317+
// Ensure io package is used (if not already imported elsewhere)
318+
var _ = io.Copy

0 commit comments

Comments
 (0)