package particle_os import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "time" "github.com/sirupsen/logrus" ) // ContainerInfo represents information about a container image type ContainerInfo struct { ID string `json:"id"` Digest string `json:"digest"` Size int64 `json:"size"` Created string `json:"created"` Labels map[string]string `json:"labels"` Arch string `json:"arch"` OS string `json:"os"` Variant string `json:"variant"` Layers []string `json:"layers"` } // ContainerProcessor handles container image operations type ContainerProcessor struct { logger *logrus.Logger workDir string usePodman bool } // NewContainerProcessor creates a new container processor func NewContainerProcessor(workDir string, logLevel logrus.Level) *ContainerProcessor { // Check if podman is available, otherwise use docker usePodman := true if _, err := exec.LookPath("podman"); err != nil { usePodman = false } logger := logrus.New() logger.SetLevel(logLevel) return &ContainerProcessor{ logger: logger, workDir: workDir, usePodman: usePodman, } } // ExtractContainer extracts a container image to the filesystem func (cp *ContainerProcessor) ExtractContainer(imageRef, targetDir string) error { cp.logger.Infof("Extracting container: %s to %s", imageRef, targetDir) // Create target directory (clean up if it exists) cp.logger.Infof("Cleaning up existing target directory: %s", targetDir) cleanupCmd := exec.Command("sudo", "rm", "-rf", targetDir) if err := cleanupCmd.Run(); err != nil { cp.logger.Warnf("Failed to remove existing target directory: %v", err) } if err := os.MkdirAll(targetDir, 0755); err != nil { return fmt.Errorf("failed to create target directory: %w", err) } // Pull the image first if err := cp.pullImage(imageRef); err != nil { return fmt.Errorf("failed to pull image: %w", err) } // Extract the image if err := cp.extractImage(imageRef, targetDir); err != nil { return fmt.Errorf("failed to extract image: %w", err) } cp.logger.Info("Container extraction completed successfully") return nil } // GetContainerInfo gets information about a container image func (cp *ContainerProcessor) GetContainerInfo(imageRef string) (*ContainerInfo, error) { cp.logger.Infof("Getting container info for: %s", imageRef) // Pull the image first if err := cp.pullImage(imageRef); err != nil { return nil, fmt.Errorf("failed to pull image: %w", err) } // Inspect the image info, err := cp.inspectImage(imageRef) if err != nil { return nil, fmt.Errorf("failed to inspect image: %w", err) } return info, nil } // pullImage pulls a container image func (cp *ContainerProcessor) pullImage(imageRef string) error { cp.logger.Infof("Pulling image: %s", imageRef) var cmd *exec.Cmd if cp.usePodman { cmd = exec.Command("podman", "pull", imageRef) } else { cmd = exec.Command("docker", "pull", imageRef) } output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("failed to pull image: %s, output: %s", err, string(output)) } cp.logger.Info("Image pulled successfully") return nil } // extractImage extracts a container image to the filesystem func (cp *ContainerProcessor) extractImage(imageRef, targetDir string) error { cp.logger.Infof("Extracting image: %s to %s", imageRef, targetDir) // Create a temporary container with shorter name containerName := fmt.Sprintf("po-%d", time.Now().Unix()) cp.logger.Infof("Using container name: %s", containerName) // Clean up any existing container with the same name cp.logger.Infof("Cleaning up any existing container: %s", containerName) var cleanupCmd *exec.Cmd if cp.usePodman { cleanupCmd = exec.Command("podman", "rm", containerName) } else { cleanupCmd = exec.Command("docker", "rm", containerName) } cleanupCmd.Run() // Ignore errors for cleanup var createCmd *exec.Cmd if cp.usePodman { createCmd = exec.Command("podman", "create", "--name", containerName, imageRef) } else { createCmd = exec.Command("docker", "create", "--name", containerName, imageRef) } cp.logger.Infof("Creating container with command: %v", createCmd.Args) createOutput, err := createCmd.CombinedOutput() if err != nil { cp.logger.Errorf("Container creation failed: %v, output: %s", err, string(createOutput)) return fmt.Errorf("failed to create container: %w, output: %s", err, string(createOutput)) } cp.logger.Infof("Container created successfully: %s", containerName) // Extract the container filesystem var extractCmd *exec.Cmd if cp.usePodman { extractCmd = exec.Command("podman", "export", containerName) } else { extractCmd = exec.Command("docker", "export", containerName) } cp.logger.Infof("Exporting container with command: %v", extractCmd.Args) // Create tar file tarFile := filepath.Join(cp.workDir, "container.tar") cp.logger.Infof("Creating tar file: %s", tarFile) // Use a pipe to avoid Stdout conflicts extractCmd.Stdout = nil // Reset Stdout extractOutput, err := extractCmd.Output() if err != nil { cp.logger.Errorf("Container export failed: %v", err) return fmt.Errorf("failed to export container: %w", err) } // Write the output to the tar file if err := os.WriteFile(tarFile, extractOutput, 0644); err != nil { cp.logger.Errorf("Failed to write tar file: %v", err) return fmt.Errorf("failed to write tar file: %w", err) } cp.logger.Info("Container export completed successfully") // Extract tar to target directory cp.logger.Infof("Extracting tar file %s to %s", tarFile, targetDir) untarCmd := exec.Command("tar", "-xf", tarFile, "-C", targetDir) cp.logger.Infof("Running tar command: %v", untarCmd.Args) untarOutput, err := untarCmd.CombinedOutput() if err != nil { cp.logger.Errorf("Tar extraction failed: %v, output: %s", err, string(untarOutput)) return fmt.Errorf("failed to extract tar: %w, output: %s", err, string(untarOutput)) } cp.logger.Info("Tar extraction completed successfully") // Fix ownership to root:root for chroot operations cp.logger.Info("Fixing ownership for chroot operations") chownCmd := exec.Command("sudo", "chown", "-R", "root:root", targetDir) chownOutput, err := chownCmd.CombinedOutput() if err != nil { cp.logger.Warnf("Failed to fix ownership: %v, output: %s", err, string(chownOutput)) } else { cp.logger.Info("Ownership fixed successfully") } // Fix permissions for /tmp and other critical directories cp.logger.Info("Fixing permissions for critical directories") chmodCmd := exec.Command("sudo", "chmod", "-R", "1777", filepath.Join(targetDir, "tmp")) if err := chmodCmd.Run(); err != nil { cp.logger.Warnf("Failed to fix tmp permissions: %v", err) } chmodCmd = exec.Command("sudo", "chmod", "-R", "1777", filepath.Join(targetDir, "var", "tmp")) if err := chmodCmd.Run(); err != nil { cp.logger.Warnf("Failed to fix var/tmp permissions: %v", err) } // Clean up var rmCmd *exec.Cmd if cp.usePodman { rmCmd = exec.Command("podman", "rm", containerName) } else { rmCmd = exec.Command("docker", "rm", containerName) } rmOutput, err := rmCmd.CombinedOutput() if err != nil { cp.logger.Warnf("Failed to cleanup container: %v, output: %s", err, string(rmOutput)) } else { cp.logger.Info("Container cleanup completed") } // Remove tar file if err := os.Remove(tarFile); err != nil { cp.logger.Warnf("Failed to remove tar file: %v", err) } else { cp.logger.Info("Tar file cleanup completed") } cp.logger.Info("Image extraction completed") return nil } // inspectImage inspects a container image and returns its information func (cp *ContainerProcessor) inspectImage(imageRef string) (*ContainerInfo, error) { cp.logger.Infof("Inspecting image: %s", imageRef) var cmd *exec.Cmd if cp.usePodman { cmd = exec.Command("podman", "inspect", imageRef) } else { cmd = exec.Command("docker", "inspect", imageRef) } output, err := cmd.Output() if err != nil { return nil, fmt.Errorf("failed to inspect image: %w", err) } // Parse the inspect output (it's an array, so we take the first element) var inspectResults []map[string]interface{} if err := json.Unmarshal(output, &inspectResults); err != nil { return nil, fmt.Errorf("failed to parse inspect output: %w", err) } if len(inspectResults) == 0 { return nil, fmt.Errorf("no inspect results found") } inspect := inspectResults[0] // Extract relevant information info := &ContainerInfo{ ID: getString(inspect, "Id"), Digest: getString(inspect, "Digest"), Created: getString(inspect, "Created"), Labels: make(map[string]string), } // Get size if size, ok := inspect["Size"].(float64); ok { info.Size = int64(size) } // Get architecture and OS if config, ok := inspect["Architecture"].(string); ok { info.Arch = config } if os, ok := inspect["Os"].(string); ok { info.OS = os } // Get labels if labels, ok := inspect["Labels"].(map[string]interface{}); ok { for k, v := range labels { if str, ok := v.(string); ok { info.Labels[k] = str } } } // Get layers if layers, ok := inspect["Layers"].([]interface{}); ok { for _, layer := range layers { if layerStr, ok := layer.(string); ok { info.Layers = append(info.Layers, layerStr) } } } return info, nil } // getString safely extracts a string value from a map func getString(m map[string]interface{}, key string) string { if val, ok := m[key].(string); ok { return val } return "" } // GetContainerRuntime returns the container runtime being used func (cp *ContainerProcessor) GetContainerRuntime() string { if cp.usePodman { return "podman" } return "docker" } // IsAvailable checks if the required container runtime is available func (cp *ContainerProcessor) IsAvailable() bool { if cp.usePodman { _, err := exec.LookPath("podman") return err == nil } _, err := exec.LookPath("docker") return err == nil }