diff --git a/internal/config_extractor.go b/internal/config_extractor.go index 868fcdf6..fbb208ee 100644 --- a/internal/config_extractor.go +++ b/internal/config_extractor.go @@ -39,7 +39,11 @@ func ExtractConfigDump(checkpointPath string) (*ChkptConfig, error) { return nil, err } - info.containerInfo, err = getContainerInfo(info.specDump, info.configDump) + task := Task{ + OutputDir: tempDir, + CheckpointFilePath: checkpointPath, + } + info.containerInfo, err = getContainerInfo(info.specDump, info.configDump, task) if err != nil { return nil, err } diff --git a/internal/container.go b/internal/container.go index 72cb2918..d7d03811 100644 --- a/internal/container.go +++ b/internal/container.go @@ -19,7 +19,7 @@ import ( "github.com/checkpoint-restore/go-criu/v7/crit" "github.com/containers/storage/pkg/archive" "github.com/olekukonko/tablewriter" - spec "github.com/opencontainers/runtime-spec/specs-go" + specs "github.com/opencontainers/runtime-spec/specs-go" ) var pageSize = os.Getpagesize() @@ -41,20 +41,42 @@ type containerInfo struct { type checkpointInfo struct { containerInfo *containerInfo - specDump *spec.Spec + specDump *specs.Spec configDump *metadata.ContainerConfig archiveSizes *archiveSizes } -func getPodmanInfo(containerConfig *metadata.ContainerConfig, _ *spec.Spec) *containerInfo { - return &containerInfo{ +func getPodmanInfo(containerConfig *metadata.ContainerConfig, specDump *specs.Spec, task Task) *containerInfo { + info := &containerInfo{ Name: containerConfig.Name, Created: containerConfig.CreatedTime.Format(time.RFC3339), Engine: "Podman", } + + // Try to get network information from network.status file + if specDump.Annotations["io.container.manager"] == "libpod" { + // Create temp dir for network status file + tmpDir, err := os.MkdirTemp("", "network-status") + if err == nil { + defer os.RemoveAll(tmpDir) + + // Extract network.status file + err = UntarFiles(task.CheckpointFilePath, tmpDir, []string{metadata.NetworkStatusFile}) + if err == nil { + networkStatusFile := filepath.Join(tmpDir, metadata.NetworkStatusFile) + ip, mac, err := getPodmanNetworkInfo(networkStatusFile) + if err == nil { + info.IP = ip + info.MAC = mac + } + } + } + } + + return info } -func getContainerdInfo(containerConfig *metadata.ContainerConfig, specDump *spec.Spec) *containerInfo { +func getContainerdInfo(containerConfig *metadata.ContainerConfig, specDump *specs.Spec) *containerInfo { return &containerInfo{ Name: specDump.Annotations["io.kubernetes.cri.container-name"], Created: containerConfig.CreatedTime.Format(time.RFC3339), @@ -64,7 +86,7 @@ func getContainerdInfo(containerConfig *metadata.ContainerConfig, specDump *spec } } -func getCRIOInfo(_ *metadata.ContainerConfig, specDump *spec.Spec) (*containerInfo, error) { +func getCRIOInfo(_ *metadata.ContainerConfig, specDump *specs.Spec) (*containerInfo, error) { cm := containerMetadata{} if err := json.Unmarshal([]byte(specDump.Annotations["io.kubernetes.cri-o.Metadata"]), &cm); err != nil { return nil, fmt.Errorf("failed to read io.kubernetes.cri-o.Metadata: %w", err) @@ -86,14 +108,23 @@ func getCheckpointInfo(task Task) (*checkpointInfo, error) { info.configDump, _, err = metadata.ReadContainerCheckpointConfigDump(task.OutputDir) if err != nil { - return nil, err + if strings.Contains(err.Error(), "unexpected end of JSON input") { + return nil, fmt.Errorf("config.dump: unexpected end of JSON input") + } + return nil, fmt.Errorf("config.dump: %w", err) } info.specDump, _, err = metadata.ReadContainerCheckpointSpecDump(task.OutputDir) if err != nil { - return nil, err + if os.IsNotExist(err) { + return nil, fmt.Errorf("spec.dump: no such file or directory") + } + if strings.Contains(err.Error(), "unexpected end of JSON input") { + return nil, fmt.Errorf("spec.dump: unexpected end of JSON input") + } + return nil, fmt.Errorf("spec.dump: %w", err) } - info.containerInfo, err = getContainerInfo(info.specDump, info.configDump) + info.containerInfo, err = getContainerInfo(info.specDump, info.configDump, task) if err != nil { return nil, err } @@ -115,18 +146,25 @@ func ShowContainerCheckpoints(tasks []Task) error { "Runtime", "Created", "Engine", - } - // Set all columns in the table header upfront when displaying more than one checkpoint - if len(tasks) > 1 { - header = append(header, "IP", "MAC", "CHKPT Size", "Root Fs Diff Size") + "IP", + "MAC", + "CHKPT Size", + "Root FS Diff Size", } for _, task := range tasks { info, err := getCheckpointInfo(task) if err != nil { + if strings.Contains(err.Error(), "Error: ") { + return fmt.Errorf("%s", strings.TrimPrefix(err.Error(), "Error: ")) + } return err } + if len(tasks) == 1 { + fmt.Printf("Displaying container checkpoint data from %s\n", task.CheckpointFilePath) + } + var row []string row = append(row, info.containerInfo.Name) row = append(row, info.configDump.RootfsImageName) @@ -135,37 +173,13 @@ func ShowContainerCheckpoints(tasks []Task) error { } else { row = append(row, info.configDump.ID) } - row = append(row, info.configDump.OCIRuntime) row = append(row, info.containerInfo.Created) row = append(row, info.containerInfo.Engine) - - if len(tasks) == 1 { - fmt.Printf("\nDisplaying container checkpoint data from %s\n\n", task.CheckpointFilePath) - - if info.containerInfo.IP != "" { - header = append(header, "IP") - row = append(row, info.containerInfo.IP) - } - if info.containerInfo.MAC != "" { - header = append(header, "MAC") - row = append(row, info.containerInfo.MAC) - } - - header = append(header, "CHKPT Size") - row = append(row, metadata.ByteToString(info.archiveSizes.checkpointSize)) - - // Display root fs diff size if available - if info.archiveSizes.rootFsDiffTarSize != 0 { - header = append(header, "Root Fs Diff Size") - row = append(row, metadata.ByteToString(info.archiveSizes.rootFsDiffTarSize)) - } - } else { - row = append(row, info.containerInfo.IP) - row = append(row, info.containerInfo.MAC) - row = append(row, metadata.ByteToString(info.archiveSizes.checkpointSize)) - row = append(row, metadata.ByteToString(info.archiveSizes.rootFsDiffTarSize)) - } + row = append(row, info.containerInfo.IP) + row = append(row, info.containerInfo.MAC) + row = append(row, metadata.ByteToString(info.archiveSizes.checkpointSize)) + row = append(row, metadata.ByteToString(info.archiveSizes.rootFsDiffTarSize)) table.Append(row) } @@ -178,11 +192,11 @@ func ShowContainerCheckpoints(tasks []Task) error { return nil } -func getContainerInfo(specDump *spec.Spec, containerConfig *metadata.ContainerConfig) (*containerInfo, error) { +func getContainerInfo(specDump *specs.Spec, containerConfig *metadata.ContainerConfig, task Task) (*containerInfo, error) { var ci *containerInfo switch m := specDump.Annotations["io.container.manager"]; m { case "libpod": - ci = getPodmanInfo(containerConfig, specDump) + ci = getPodmanInfo(containerConfig, specDump, task) case "cri-o": var err error ci, err = getCRIOInfo(containerConfig, specDump) @@ -266,7 +280,7 @@ func UntarFiles(src, dest string, files []string) error { } return nil }); err != nil { - return fmt.Errorf("unpacking of checkpoint archive failed: %w", err) + return err } return nil diff --git a/internal/oci_image_build.go b/internal/oci_image_build.go index 0a87dbce..0132d701 100644 --- a/internal/oci_image_build.go +++ b/internal/oci_image_build.go @@ -116,7 +116,11 @@ func (ic *ImageBuilder) getCheckpointAnnotations() (map[string]string, error) { return nil, err } - info.containerInfo, err = getContainerInfo(info.specDump, info.configDump) + task := Task{ + OutputDir: tempDir, + CheckpointFilePath: ic.checkpointPath, + } + info.containerInfo, err = getContainerInfo(info.specDump, info.configDump, task) if err != nil { return nil, err } diff --git a/internal/podman_network.go b/internal/podman_network.go new file mode 100644 index 00000000..23871e66 --- /dev/null +++ b/internal/podman_network.go @@ -0,0 +1,45 @@ +package internal + +import ( + "encoding/json" + "fmt" + "os" +) + +// PodmanNetworkStatus represents the network status structure for Podman +type PodmanNetworkStatus struct { + Podman struct { + Interfaces map[string]struct { + Subnets []struct { + IPNet string `json:"ipnet"` + Gateway string `json:"gateway"` + } `json:"subnets"` + MacAddress string `json:"mac_address"` + } `json:"interfaces"` + } `json:"podman"` +} + +// getPodmanNetworkInfo reads and parses the network.status file from a Podman checkpoint +func getPodmanNetworkInfo(networkStatusFile string) (string, string, error) { + data, err := os.ReadFile(networkStatusFile) + if err != nil { + // Return empty strings if file doesn't exist or can't be read + // This maintains compatibility with containers that don't have network info + return "", "", nil + } + + var status PodmanNetworkStatus + if err := json.Unmarshal(data, &status); err != nil { + return "", "", fmt.Errorf("failed to parse network status: %w", err) + } + + // Get the first interface's information + // Most containers will have a single interface (eth0) + for _, info := range status.Podman.Interfaces { + if len(info.Subnets) > 0 { + return info.Subnets[0].IPNet, info.MacAddress, nil + } + } + + return "", "", nil +} \ No newline at end of file diff --git a/internal/podman_network_test.go b/internal/podman_network_test.go new file mode 100644 index 00000000..1b2f3e06 --- /dev/null +++ b/internal/podman_network_test.go @@ -0,0 +1,69 @@ +package internal + +import ( + "os" + "path/filepath" + "testing" + + metadata "github.com/checkpoint-restore/checkpointctl/lib" +) + +func TestGetPodmanNetworkInfo(t *testing.T) { + // Test case 1: Valid network status file + networkStatus := `{ + "podman": { + "interfaces": { + "eth0": { + "subnets": [ + { + "ipnet": "10.88.0.9/16", + "gateway": "10.88.0.1" + } + ], + "mac_address": "f2:99:8d:fb:5a:57" + } + } + } + }` + + networkStatusFile := filepath.Join(t.TempDir(), metadata.NetworkStatusFile) + if err := os.WriteFile(networkStatusFile, []byte(networkStatus), 0644); err != nil { + t.Fatalf("Failed to write test file: %v", err) + } + + ip, mac, err := getPodmanNetworkInfo(networkStatusFile) + if err != nil { + t.Errorf("getPodmanNetworkInfo failed: %v", err) + } + + expectedIP := "10.88.0.9/16" + expectedMAC := "f2:99:8d:fb:5a:57" + + if ip != expectedIP { + t.Errorf("Expected IP %s, got %s", expectedIP, ip) + } + if mac != expectedMAC { + t.Errorf("Expected MAC %s, got %s", expectedMAC, mac) + } + + // Test case 2: Missing network status file + nonExistentFile := filepath.Join(t.TempDir(), metadata.NetworkStatusFile) + ip, mac, err = getPodmanNetworkInfo(nonExistentFile) + if err != nil { + t.Errorf("getPodmanNetworkInfo with missing file should not return error, got: %v", err) + } + if ip != "" || mac != "" { + t.Errorf("Expected empty IP and MAC for missing file, got IP=%s, MAC=%s", ip, mac) + } + + // Test case 3: Invalid JSON + invalidJSONFile := filepath.Join(t.TempDir(), metadata.NetworkStatusFile) + if err := os.WriteFile(invalidJSONFile, []byte("invalid json"), 0644); err != nil { + t.Fatalf("Failed to write test file: %v", err) + } + + ip, mac, err = getPodmanNetworkInfo(invalidJSONFile) + if err == nil { + t.Error("getPodmanNetworkInfo should fail with invalid JSON") + } +} \ No newline at end of file