Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
255 changes: 255 additions & 0 deletions pkg/kubernetes/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,21 @@ import (
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"

v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/rand"
"k8s.io/client-go/tools/remotecommand"
"k8s.io/metrics/pkg/apis/metrics"
metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1"
"k8s.io/utils/ptr"

"github.com/containers/kubernetes-mcp-server/pkg/version"
)

func (k *Kubernetes) NodesLog(ctx context.Context, name string, query string, tailLines int64) (string, error) {
Expand Down Expand Up @@ -77,3 +88,247 @@ func (k *Kubernetes) NodesTop(ctx context.Context, options NodesTopOptions) (*me
}
return k.manager.accessControlClientSet.NodesMetricses(ctx, options.Name, options.ListOptions)
}

// NodeFilesOptions contains options for node file operations
type NodeFilesOptions struct {
NodeName string
Operation string // "put", "get", "list"
SourcePath string
DestPath string
Namespace string
Image string
Privileged bool
}

// NodesFiles handles file operations on a node filesystem by creating a privileged pod
func (k *Kubernetes) NodesFiles(ctx context.Context, opts NodeFilesOptions) (string, error) {
// Set defaults
if opts.Namespace == "" {
opts.Namespace = "default"
}
if opts.Image == "" {
opts.Image = "busybox"
}

// Create privileged pod for accessing node filesystem
podName := fmt.Sprintf("node-files-%s", rand.String(5))
pod := &v1.Pod{
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"},
ObjectMeta: metav1.ObjectMeta{
Name: podName,
Namespace: opts.Namespace,
Labels: map[string]string{
AppKubernetesName: podName,
AppKubernetesComponent: "node-files",
AppKubernetesManagedBy: version.BinaryName,
},
},
Spec: v1.PodSpec{
NodeName: opts.NodeName,
RestartPolicy: v1.RestartPolicyNever,
Containers: []v1.Container{{
Name: "node-files",
Image: opts.Image,
Command: []string{"/bin/sh", "-c", "sleep 3600"},
SecurityContext: &v1.SecurityContext{
Privileged: ptr.To(opts.Privileged),
},
VolumeMounts: []v1.VolumeMount{{
Name: "node-root",
MountPath: "/host",
}},
}},
Volumes: []v1.Volume{{
Name: "node-root",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{
Path: "/",
},
},
}},
},
}

// Create the pod
pods, err := k.manager.accessControlClientSet.Pods(opts.Namespace)
if err != nil {
return "", fmt.Errorf("failed to get pods client: %w", err)
}

createdPod, err := pods.Create(ctx, pod, metav1.CreateOptions{})
if err != nil {
return "", fmt.Errorf("failed to create pod: %w", err)
}

// Ensure pod is deleted after operation
defer func() {
deleteCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_ = pods.Delete(deleteCtx, podName, metav1.DeleteOptions{})
}()

// Wait for pod to be ready
if err := k.waitForPodReady(ctx, opts.Namespace, podName, 2*time.Minute); err != nil {
return "", fmt.Errorf("pod failed to become ready: %w", err)
}

// Perform the requested operation
var result string
var opErr error
switch opts.Operation {
case "put":
result, opErr = k.nodeFilesPut(ctx, opts.Namespace, podName, opts.SourcePath, opts.DestPath)
case "get":
result, opErr = k.nodeFilesGet(ctx, opts.Namespace, podName, opts.SourcePath, opts.DestPath)
case "list":
result, opErr = k.nodeFilesList(ctx, opts.Namespace, podName, opts.SourcePath)
default:
return "", fmt.Errorf("unknown operation: %s", opts.Operation)
}

_ = createdPod
return result, opErr
}

// nodeFilesPut copies a file from local filesystem to node filesystem
func (k *Kubernetes) nodeFilesPut(ctx context.Context, namespace, podName, sourcePath, destPath string) (string, error) {
// Read local file content
content, err := os.ReadFile(sourcePath)
if err != nil {
return "", fmt.Errorf("failed to read source file: %w", err)
}

// Create destination directory if needed
destDir := filepath.Dir(destPath)
if destDir != "." && destDir != "/" {
mkdirCmd := []string{"/bin/sh", "-c", fmt.Sprintf("mkdir -p /host%s", destDir)}
if _, err := k.execInPod(ctx, namespace, podName, mkdirCmd); err != nil {
return "", fmt.Errorf("failed to create destination directory: %w", err)
}
}

// Write content using cat command
escapedContent := strings.ReplaceAll(string(content), "'", "'\\''")
writeCmd := []string{"/bin/sh", "-c", fmt.Sprintf("cat > /host%s << 'EOF'\n%s\nEOF", destPath, escapedContent)}

if _, err := k.execInPod(ctx, namespace, podName, writeCmd); err != nil {
return "", fmt.Errorf("failed to write file to node: %w", err)
}

return fmt.Sprintf("File successfully copied from %s to node:%s", sourcePath, destPath), nil
}

// nodeFilesGet copies a file from node filesystem to local filesystem
func (k *Kubernetes) nodeFilesGet(ctx context.Context, namespace, podName, sourcePath, destPath string) (string, error) {
// Read file content from node using cat
readCmd := []string{"/bin/sh", "-c", fmt.Sprintf("cat /host%s", sourcePath)}
content, err := k.execInPod(ctx, namespace, podName, readCmd)
if err != nil {
return "", fmt.Errorf("failed to read file from node: %w", err)
}

// Determine destination path
if destPath == "" {
destPath = filepath.Base(sourcePath)
}

// Create local destination directory if needed
destDir := filepath.Dir(destPath)
if destDir != "." && destDir != "" {
if err := os.MkdirAll(destDir, 0755); err != nil {
return "", fmt.Errorf("failed to create local directory: %w", err)
}
}

// Write to local file
if err := os.WriteFile(destPath, []byte(content), 0644); err != nil {
return "", fmt.Errorf("failed to write local file: %w", err)
}

return fmt.Sprintf("File successfully copied from node:%s to %s", sourcePath, destPath), nil
}

// nodeFilesList lists files in a directory on node filesystem
func (k *Kubernetes) nodeFilesList(ctx context.Context, namespace, podName, path string) (string, error) {
// List directory contents using ls
listCmd := []string{"/bin/sh", "-c", fmt.Sprintf("ls -la /host%s", path)}
output, err := k.execInPod(ctx, namespace, podName, listCmd)
if err != nil {
return "", fmt.Errorf("failed to list directory: %w", err)
}

return output, nil
}

// execInPod executes a command in the pod and returns the output
func (k *Kubernetes) execInPod(ctx context.Context, namespace, podName string, command []string) (string, error) {
podExecOptions := &v1.PodExecOptions{
Container: "node-files",
Command: command,
Stdout: true,
Stderr: true,
}

executor, err := k.manager.accessControlClientSet.PodsExec(namespace, podName, podExecOptions)
if err != nil {
return "", err
}

stdout := &strings.Builder{}
stderr := &strings.Builder{}

if err = executor.StreamWithContext(ctx, remotecommand.StreamOptions{
Stdout: stdout,
Stderr: stderr,
Tty: false,
}); err != nil {
if stderr.Len() > 0 {
return "", fmt.Errorf("exec error: %s: %w", stderr.String(), err)
}
return "", err
}

if stderr.Len() > 0 && stdout.Len() == 0 {
return stderr.String(), nil
}

return stdout.String(), nil
}

// waitForPodReady waits for a pod to be ready
func (k *Kubernetes) waitForPodReady(ctx context.Context, namespace, podName string, timeout time.Duration) error {
pods, err := k.manager.accessControlClientSet.Pods(namespace)
if err != nil {
return err
}

deadline := time.Now().Add(timeout)
for {
if time.Now().After(deadline) {
return fmt.Errorf("timeout waiting for pod to be ready")
}

pod, err := pods.Get(ctx, podName, metav1.GetOptions{})
if err != nil {
return err
}

// Check if pod is ready
if pod.Status.Phase == v1.PodRunning {
for _, condition := range pod.Status.Conditions {
if condition.Type == v1.PodReady && condition.Status == v1.ConditionTrue {
return nil
}
}
}

if pod.Status.Phase == v1.PodFailed {
return fmt.Errorf("pod failed")
}

time.Sleep(2 * time.Second)
}
}

// Ensure io package is used (if not already imported elsewhere)
var _ = io.Copy
Loading
Loading