@@ -3,6 +3,19 @@ package kubernetes
33import (
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
821func (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\n EOF" , 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