Skip to content

Commit 8b38d37

Browse files
Support to put,get and list files on k8s node
Signed-off-by: Neeraj Krishna Gopalakrishna <[email protected]>
1 parent a356eb8 commit 8b38d37

File tree

8 files changed

+945
-8
lines changed

8 files changed

+945
-8
lines changed

pkg/kubernetes/nodes.go

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,21 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"io"
8+
"os"
9+
"path/filepath"
10+
"strings"
11+
"time"
712

13+
v1 "k8s.io/api/core/v1"
814
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15+
"k8s.io/apimachinery/pkg/util/rand"
16+
"k8s.io/client-go/tools/remotecommand"
917
"k8s.io/metrics/pkg/apis/metrics"
1018
metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1"
19+
"k8s.io/utils/ptr"
20+
21+
"github.com/containers/kubernetes-mcp-server/pkg/version"
1122
)
1223

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

0 commit comments

Comments
 (0)