@@ -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 ) {
@@ -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\n EOF" , 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