Skip to content

Commit 738f88a

Browse files
Support to put,get and list files on k8s node
Signed-off-by: Neeraj Krishna Gopalakrishna <ngopalak@redhat.com>
1 parent a301b0f commit 738f88a

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

0 commit comments

Comments
 (0)