Skip to content
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

Add support for list command to get the list of container checkpoints #115

Merged
merged 1 commit into from
Mar 3, 2024
Merged
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
2 changes: 2 additions & 0 deletions checkpointctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ func main() {

rootCommand.AddCommand(cmd.MemParse())

rootCommand.AddCommand(cmd.List())

rootCommand.Version = version

if err := rootCommand.Execute(); err != nil {
Expand Down
101 changes: 101 additions & 0 deletions cmd/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// SPDX-License-Identifier: Apache-2.0

// This file is used to show the list of container checkpoints

package cmd

import (
"fmt"
"log"
"os"
"path/filepath"
"time"

"github.com/checkpoint-restore/checkpointctl/internal"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
)

var (
defaultCheckpointPath = "/var/lib/kubelet/checkpoints/"
additionalCheckpointPaths []string
)

func List() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List checkpoints stored in the default and additional paths",
RunE: list,
}

flags := cmd.Flags()
flags.StringSliceVarP(
&additionalCheckpointPaths,
"paths",
"p",
[]string{},
"Specify additional paths to include in checkpoint listing",
)

return cmd
}

func list(cmd *cobra.Command, args []string) error {
allPaths := append([]string{defaultCheckpointPath}, additionalCheckpointPaths...)
showTable := false

table := tablewriter.NewWriter(os.Stdout)
header := []string{
"Namespace",
"Pod",
"Container",
"Engine",
"Time Checkpointed",
"Checkpoint Name",
}

table.SetHeader(header)
table.SetAutoMergeCells(false)
table.SetRowLine(true)

for _, checkpointPath := range allPaths {
files, err := filepath.Glob(filepath.Join(checkpointPath, "checkpoint-*"))
if err != nil {
return err
}

if len(files) == 0 {
continue
}

showTable = true
fmt.Printf("Listing checkpoints in path: %s\n", checkpointPath)

for _, file := range files {
chkptConfig, err := internal.ExtractConfigDump(file)
if err != nil {
log.Printf("Error extracting information from %s: %v\n", file, err)
continue
}

row := []string{
chkptConfig.Namespace,
chkptConfig.Pod,
chkptConfig.Container,
chkptConfig.ContainerManager,
chkptConfig.Timestamp.Format(time.RFC822),
filepath.Base(file),
}

table.Append(row)
}
}

if !showTable {
fmt.Println("No checkpoints found")
return nil
}

table.Render()
return nil
}
26 changes: 26 additions & 0 deletions docs/checkpointctl-list.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
= checkpointctl-list(1)
include::footer.adoc[]

== Name

*checkpointctl-list* - List checkpoints stored in the default and additional paths

Parthiba-Hazra marked this conversation as resolved.
Show resolved Hide resolved
== Synopsis

*checkpointctl list* [_OPTION_]... _FOLDER_...

== Options

*-h*, *--help*::
Show help for checkpointctl list

*-p*, *--path*::
Specify additional paths to include in checkpoint listing

== Default Path

The default path for checking checkpoints is `/var/lib/kubelet/checkpoints/`.

== See also

checkpointctl(1)
76 changes: 76 additions & 0 deletions internal/config_extractor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package internal

import (
"encoding/json"
"log"
"os"
"path/filepath"
"time"

metadata "github.com/checkpoint-restore/checkpointctl/lib"
)

type ChkptConfig struct {
Namespace string
Pod string
Container string
ContainerManager string
Timestamp time.Time
}

func ExtractConfigDump(checkpointPath string) (*ChkptConfig, error) {
tempDir, err := os.MkdirTemp("", "extracted-checkpoint")
if err != nil {
return nil, err
}
defer os.RemoveAll(tempDir)

filesToExtract := []string{"spec.dump", "config.dump"}
if err := UntarFiles(checkpointPath, tempDir, filesToExtract); err != nil {
log.Printf("Error extracting files from archive %s: %v\n", checkpointPath, err)
return nil, err
}

specDumpPath := filepath.Join(tempDir, "spec.dump")
specContent, err := os.ReadFile(specDumpPath)
if err != nil {
log.Printf("Error reading spec.dump file: %v\n", err)
return nil, err
}

configDumpPath := filepath.Join(tempDir, "config.dump")
configContent, err := os.ReadFile(configDumpPath)
if err != nil {
log.Printf("Error reading config.dump file: %v\n", err)
return nil, err
}

return extractConfigDumpContent(configContent, specContent)
}

func extractConfigDumpContent(configContent []byte, specContent []byte) (*ChkptConfig, error) {
var spec metadata.Spec
var config metadata.ContainerConfig

if err := json.Unmarshal(configContent, &config); err != nil {
return nil, err
}

if err := json.Unmarshal(specContent, &spec); err != nil {
return nil, err
}

namespace := spec.Annotations["io.kubernetes.pod.namespace"]
timestamp := config.CheckpointedAt
pod := spec.Annotations["io.kubernetes.pod.name"]
container := spec.Annotations["io.kubernetes.container.name"]
containerManager := spec.Annotations["io.container.manager"]

return &ChkptConfig{
Namespace: namespace,
Pod: pod,
Container: container,
ContainerManager: containerManager,
Timestamp: timestamp,
}, nil
}
33 changes: 33 additions & 0 deletions internal/config_extractor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package internal

import (
"os"
"testing"
"time"
)

func TestExtractConfigDumpContent(t *testing.T) {
configContent, err := os.ReadFile("../test/data/list_config_spec.dump/config.dump")
if err != nil {
t.Fatal(err)
}

specContent, err := os.ReadFile("../test/data/list_config_spec.dump/spec.dump")
if err != nil {
t.Fatal(err)
}

chkptConfig, err := extractConfigDumpContent(configContent, specContent)
if err != nil {
t.Fatalf("ExtractConfigDumpContent failed: %v", err)
}

expectedNamespace := "default"
expectedPod := "pod-name"
expectedContainer := "container-name"
expectedContainerManager := "cri-o"
expectedTimestamp := time.Date(2024, 1, 28, 0, 10, 45, 673538606, time.FixedZone("", 19800))
if chkptConfig.Namespace != expectedNamespace || chkptConfig.Pod != expectedPod || chkptConfig.Container != expectedContainer || !chkptConfig.Timestamp.Equal(expectedTimestamp) || chkptConfig.ContainerManager != expectedContainerManager {
t.Errorf("ExtractConfigDumpContent returned unexpected values: namespace=%s, pod=%s, container=%s, timestamp=%v", chkptConfig.Namespace, chkptConfig.Pod, chkptConfig.Container, chkptConfig.Timestamp)
}
}
4 changes: 4 additions & 0 deletions lib/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ type ContainerConfig struct {
Restored bool `json:"restored"`
}

type Spec struct {
Annotations map[string]string `json:"annotations,omitempty"`
}

type ContainerdStatus struct {
CreatedAt int64
StartedAt int64
Expand Down
57 changes: 57 additions & 0 deletions test/checkpointctl.bats
Original file line number Diff line number Diff line change
Expand Up @@ -640,3 +640,60 @@ function teardown() {
run bash -c "$CHECKPOINTCTL inspect $TEST_TMP_DIR2/test.tar --format=json --sockets | test_socket_src_port"
[ "$status" -eq 0 ]
}

@test "Run checkpointctl list with empty directory" {
mkdir "$TEST_TMP_DIR1"/empty
checkpointctl list -p "$TEST_TMP_DIR1"/empty/
[ "$status" -eq 0 ]
[[ ${lines[0]} == *"No checkpoints found"* ]]
}

@test "Run checkpointctl list with non existing directory" {
checkpointctl list -p /does-not-exist
[ "$status" -eq 0 ]
[[ ${lines[0]} == *"No checkpoints found"* ]]
}

@test "Run checkpointctl list with empty tar file" {
touch "$TEST_TMP_DIR1"/checkpoint-nginx-empty.tar
checkpointctl list -p "$TEST_TMP_DIR1"
[ "$status" -eq 0 ]
[[ "${lines[1]}" == *"Error reading spec.dump file"* ]]
[[ "${lines[2]}" == *"Error extracting information"* ]]
}

@test "Run checkpointctl list with tar file with valid spec.dump and empty config.dump" {
touch "$TEST_TMP_DIR1"/config.dump
cp data/list_config_spec.dump/spec.dump "$TEST_TMP_DIR1"
mkdir "$TEST_TMP_DIR1"/checkpoint
( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/checkpoint-config.tar . )
checkpointctl list -p "$TEST_TMP_DIR2"
[ "$status" -eq 0 ]
[[ "${lines[1]}" == *"Error extracting information from $TEST_TMP_DIR2/checkpoint-config.tar: unexpected end of JSON input"* ]]
}

@test "Run checkpointctl list with tar file with valid config.dump and empty spec.dump" {
touch "$TEST_TMP_DIR1"/spec.dump
cp data/list_config_spec.dump/config.dump "$TEST_TMP_DIR1"
mkdir "$TEST_TMP_DIR1"/checkpoint
( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/checkpoint-config.tar . )
checkpointctl list -p "$TEST_TMP_DIR2"
[ "$status" -eq 0 ]
[[ ${lines[1]} == *"Error extracting information from $TEST_TMP_DIR2/checkpoint-config.tar: unexpected end of JSON input" ]]
}

@test "Run checkpointctl list with tar file with valid config.dump and spec.dump" {
cp data/list_config_spec.dump/config.dump "$TEST_TMP_DIR1"
cp data/list_config_spec.dump/spec.dump "$TEST_TMP_DIR1"
mkdir "$TEST_TMP_DIR1"/checkpoint
( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/checkpoint-valid-config.tar . )
jq '.["annotations"]["io.kubernetes.pod.name"] = "modified-pod-name"' "$TEST_TMP_DIR1"/spec.dump > "$TEST_TMP_DIR1"/spec_modified.dump
mv "$TEST_TMP_DIR1"/spec_modified.dump "$TEST_TMP_DIR1"/spec.dump
( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/checkpoint-valid-config-modified.tar . )
checkpointctl list -p "$TEST_TMP_DIR2"
[ "$status" -eq 0 ]
[[ "${lines[4]}" == *"| default | modified-pod-name | container-name | cri-o |"* ]]
[[ "${lines[4]}" == *"| checkpoint-valid-config-modified.tar |"* ]]
[[ "${lines[6]}" == *"| default | pod-name | container-name | cri-o |"* ]]
[[ "${lines[6]}" == *"| checkpoint-valid-config.tar |"* ]]
}
12 changes: 12 additions & 0 deletions test/data/list_config_spec.dump/config.dump
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"id": "6924be1bd90c23f10e2667102b0ee0f74f09bba78b6661871e733cb3b1737821",
"name": "k8s_container-name_deployment-name_default_6975ee47-6765-45dc-9a2b-1e38d51031f7_0",
"rootfsImage": "docker.io/library/nginx@sha256:161ef4b1bf7effb350a2a9625cb2b59f69d54ec6059a8a155a1438d0439c593c",
"rootfsImageRef": "a8758716bb6aa4d90071160d27028fe4eaee7ce8166221a97d30440c8eac2be6",
"rootfsImageName": "docker.io/library/nginx:latest",
"runtime": "runc",
"createdTime": "2024-01-27T14:45:26.083444055Z",
"checkpointedTime": "2024-01-28T00:10:45.673538606+05:30",
"restoredTime": "0001-01-01T00:00:00Z",
"restored": false
}
9 changes: 9 additions & 0 deletions test/data/list_config_spec.dump/spec.dump
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"annotations": {
"io.container.manager": "cri-o",
"io.kubernetes.container.hash": "1511917a",
"io.kubernetes.container.name": "container-name",
"io.kubernetes.pod.name": "pod-name",
"io.kubernetes.pod.namespace": "default"
}
}
Loading