@@ -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
1324func (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\n EOF" , 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