Skip to content

Commit

Permalink
feat: Introduce support for the list command to retrieve container
Browse files Browse the repository at this point in the history
      checkpoints

- If applied, this commit will introduce support for the list command
  to retrieve container checkpoints, enhance user accessibility to
  container-related information.

Signed-off-by: Parthiba-Hazra <[email protected]>
  • Loading branch information
Parthiba-Hazra committed Feb 22, 2024
1 parent d132ca0 commit 23e2986
Show file tree
Hide file tree
Showing 8 changed files with 318 additions and 0 deletions.
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
99 changes: 99 additions & 0 deletions cmd/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// 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",
"Time Created",
"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 {
namespace, pod, container, timestamp, err := internal.ExtractConfigDump(file)
if err != nil {
log.Printf("Error extracting information from %s: %v\n", file, err)
continue
}

row := []string{
namespace,
pod,
container,
timestamp.Format(time.RFC822),
filepath.Base(file),
}

table.Append(row)
}
}

if showTable {
table.Render()
} else {
fmt.Println("No checkpoints found")
}

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

== 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)
55 changes: 55 additions & 0 deletions internal/config_extractor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package internal

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

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

func ExtractConfigDump(checkpointPath string) (namespace, pod, container string, timestamp time.Time, err error) {
tempDir, err := os.MkdirTemp("", "extracted-checkpoint")
if err != nil {
return "", "", "", time.Time{}, err
}
defer os.RemoveAll(tempDir)

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

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

return extractConfigDumpContent(content)
}

func extractConfigDumpContent(content []byte) (namespace, pod, container string, timestamp time.Time, err error) {
var c metadata.ContainerConfig
if err := json.Unmarshal(content, &c); err != nil {
return "", "", "", time.Time{}, err
}

parts := strings.Split(c.Name, "_")

if len(parts) >= 4 {
container = parts[1]
pod = parts[2]
namespace = parts[3]
} else {
return "", "", "", time.Time{}, fmt.Errorf("invalid name format")
}

return namespace, pod, container, c.CheckpointedAt, nil
}
51 changes: 51 additions & 0 deletions internal/config_extractor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package internal

import (
"os"
"path/filepath"
"testing"
"time"
)

func TestExtractConfigDump(t *testing.T) {
tempDir, err := os.MkdirTemp("", "test-extract-config-dump")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)

checkpointFile := filepath.Join(tempDir, "checkpoint-123.tar")
if err := CreateTarWithFile(checkpointFile, "../test/data/list_config.dump/config.dump"); err != nil {
t.Fatal(err)
}

namespace, pod, container, timestamp, err := ExtractConfigDump(checkpointFile)
if err != nil {
t.Fatalf("ExtractConfigDump failed: %v", err)
}

expectedNamespace := "default"
expectedPod := "deployment-name"
expectedContainer := "container-name"
expectedTimestamp := time.Date(2024, 1, 28, 0, 10, 45, 673538606, time.FixedZone("", 19800))
if namespace != expectedNamespace || pod != expectedPod || container != expectedContainer || !timestamp.Equal(expectedTimestamp) {
t.Errorf("ExtractConfigDump returned unexpected values: namespace=%s, pod=%s, container=%s, timestamp=%v", namespace, pod, container, timestamp)
}
}

func TestExtractConfigDumpContent(t *testing.T) {
content := []byte(`{"id":"6924be1bd90c23f10e2667102b0ee0f74f09bba78b6661871e733cb3b1737821","name":"k8s_container-name_deployment-name_default_6975ee47-6765-45dc-9a2b-1e38d51031f7_0","checkpointedTime":"2024-01-28T00:10:45.673538606+05:30"}`)

namespace, pod, container, timestamp, err := extractConfigDumpContent(content)
if err != nil {
t.Fatalf("ExtractConfigDumpContent failed: %v", err)
}

expectedNamespace := "default"
expectedPod := "deployment-name"
expectedContainer := "container-name"
expectedTimestamp := time.Date(2024, 1, 28, 0, 10, 45, 673538606, time.FixedZone("", 19800))
if namespace != expectedNamespace || pod != expectedPod || container != expectedContainer || !timestamp.Equal(expectedTimestamp) {
t.Errorf("ExtractConfigDumpContent returned unexpected values: namespace=%s, pod=%s, container=%s, timestamp=%v", namespace, pod, container, timestamp)
}
}
33 changes: 33 additions & 0 deletions internal/utils.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package internal

import (
"archive/tar"
"fmt"
"os"
"path/filepath"
"strings"
"time"

Expand Down Expand Up @@ -82,3 +84,34 @@ func CleanupTasks(tasks []Task) {
}
}
}

func CreateTarWithFile(tarPath string, filePath string) error {
file, err := os.Create(tarPath)
if err != nil {
return err
}
defer file.Close()
tarWriter := tar.NewWriter(file)
defer tarWriter.Close()

fileInfo, err := os.Stat(filePath)
if err != nil {
return err
}
header, err := tar.FileInfoHeader(fileInfo, "")
if err != nil {
return err
}
header.Name = filepath.Base(filePath)
if err := tarWriter.WriteHeader(header); err != nil {
return err
}
content, err := os.ReadFile(filePath)
if err != nil {
return err
}
if _, err := tarWriter.Write(content); err != nil {
return err
}
return nil
}
40 changes: 40 additions & 0 deletions test/checkpointctl.bats
Original file line number Diff line number Diff line change
Expand Up @@ -640,3 +640,43 @@ 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 config.dump file"* ]]
[[ "${lines[2]}" == *"Error extracting information"* ]]
}

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

@test "Run checkpointctl list with tar file with valid config.dump" {
cp data/list_config.dump/config.dump "$TEST_TMP_DIR1"
mkdir "$TEST_TMP_DIR1"/checkpoint
( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/checkpoint-valid-config.tar . )
checkpointctl list -p "$TEST_TMP_DIR2"
[ "$status" -eq 0 ]
[[ "${lines[4]}" == *"| default | deployment-name | container-name |"* ]]
[[ "${lines[4]}" == *"| checkpoint-valid-config.tar |"* ]]
}
12 changes: 12 additions & 0 deletions test/data/list_config.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
}

0 comments on commit 23e2986

Please sign in to comment.