Some checks failed
particle-os CI / Test particle-os (push) Failing after 1s
particle-os CI / Integration Test (push) Has been skipped
particle-os CI / Security & Quality (push) Failing after 1s
Test particle-os Basic Functionality / test-basic (push) Failing after 1s
particle-os CI / Build and Release (push) Has been skipped
333 lines
9.8 KiB
Go
333 lines
9.8 KiB
Go
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
|
|
}
|