Skip to content

Display network information of checkpoints created with Podman #162

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
6 changes: 5 additions & 1 deletion internal/config_extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
102 changes: 58 additions & 44 deletions internal/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would like to return the error here (possible wrapped with some context)

defer os.RemoveAll(tmpDir)

// Extract network.status file
err = UntarFiles(task.CheckpointFilePath, tmpDir, []string{metadata.NetworkStatusFile})
if err == nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

networkStatusFile := filepath.Join(tmpDir, metadata.NetworkStatusFile)
ip, mac, err := getPodmanNetworkInfo(networkStatusFile)
if err == nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

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),
Expand All @@ -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)
Expand All @@ -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)
Comment on lines +111 to +114
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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)
return nil, fmt.Errorf("config.dump: %w", err)

I don't think we need the special case since we are wrapping with context.

}
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)
Comment on lines +118 to +124
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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)
return nil, fmt.Errorf("spec.dump: %w", err)

I don't think we need the special case since we are wrapping with context.

}

info.containerInfo, err = getContainerInfo(info.specDump, info.configDump)
info.containerInfo, err = getContainerInfo(info.specDump, info.configDump, task)
if err != nil {
return nil, err
}
Expand All @@ -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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Root FS Diff Size",
"Root FS Diff",

Would it make sense to drop the "size" suffix, since the field ought to make it clear that we are talking about the 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: "))
}
Comment on lines +158 to +160
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems unnecessary

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)
Expand All @@ -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)
}
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion internal/oci_image_build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
45 changes: 45 additions & 0 deletions internal/podman_network.go
Original file line number Diff line number Diff line change
@@ -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
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a newline at the end of the file :)

69 changes: 69 additions & 0 deletions internal/podman_network_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a newline at the end of the file :)

Loading