summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 17:07:40 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 17:07:40 +0000
commit5041bb4e999ef46a4c40be24072a3638f545f2a0 (patch)
tree8aabc98fcc1c743edd4d41f186007e21c0b5e065
parentInitial commit. (diff)
downloadgolang-github-checkpoint-restore-checkpointctl-5041bb4e999ef46a4c40be24072a3638f545f2a0.tar.xz
golang-github-checkpoint-restore-checkpointctl-5041bb4e999ef46a4c40be24072a3638f545f2a0.zip
Adding upstream version 0.1.0+ds1.upstream/0.1.0+ds1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.github/dependabot.yml6
-rw-r--r--.github/workflows/coverage.yml15
-rw-r--r--.github/workflows/golangci-lint.yml14
-rw-r--r--.github/workflows/markdown-lint.yml20
-rw-r--r--.github/workflows/tests.yml17
-rw-r--r--.gitignore1
-rw-r--r--.golangci.yml10
-rw-r--r--LICENSE201
-rw-r--r--Makefile63
-rw-r--r--README.md105
-rw-r--r--checkpointctl.go77
-rw-r--r--checkpointctl_coverage_test.go40
-rw-r--r--checkpointctl_test.go11
-rw-r--r--container.go208
-rw-r--r--go.mod29
-rw-r--r--go.sum103
-rw-r--r--lib/metadata.go142
-rw-r--r--test/checkpointctl.bats156
-rw-r--r--test/config.dump2
-rw-r--r--test/spec.dump3
-rw-r--r--test/spec.dump.cri-o6
-rw-r--r--test/stats-dumpbin0 -> 54 bytes
22 files changed, 1229 insertions, 0 deletions
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..b4c78be
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: gomod
+ directory: /
+ schedule:
+ interval: monthly
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
new file mode 100644
index 0000000..1802da5
--- /dev/null
+++ b/.github/workflows/coverage.yml
@@ -0,0 +1,15 @@
+name: Run coverage
+
+on: [push, pull_request]
+
+jobs:
+ coverage:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Install tools
+ run: sudo apt-get install -qqy bats
+ - name: Run make coverage
+ run: make coverage
+ - name: Run make codecov
+ run: make codecov
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
new file mode 100644
index 0000000..6fb03c8
--- /dev/null
+++ b/.github/workflows/golangci-lint.yml
@@ -0,0 +1,14 @@
+name: golangci-lint
+
+on: [push, pull_request]
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: golangci-lint
+ uses: golangci/golangci-lint-action@v3
+ with:
+ version: latest
+ only-new-issues: true
diff --git a/.github/workflows/markdown-lint.yml b/.github/workflows/markdown-lint.yml
new file mode 100644
index 0000000..65f0c1c
--- /dev/null
+++ b/.github/workflows/markdown-lint.yml
@@ -0,0 +1,20 @@
+name: markdown-lint
+
+on:
+ push:
+ paths:
+ - README.md
+ pull_request:
+ paths:
+ - README.md
+jobs:
+ lint_markdown:
+ name: Run markdown linter
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Lint markdown
+ uses: DavidAnson/markdownlint-cli2-action@v9
+ with:
+ globs: |
+ README.md
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..740ba8f
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,17 @@
+name: Run Tests
+
+on: [push, pull_request]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Install tools
+ run: sudo apt-get install -qqy shellcheck bats
+ - name: Run make shellcheck
+ run: make shellcheck
+ - name: Run make all
+ run: make all
+ - name: Run make test
+ run: make test
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e038840
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+checkpointctl
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..9df8f61
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,10 @@
+---
+run:
+ concurrency: 6
+ deadline: 5m
+linters:
+ enable:
+ - errorlint
+ - unconvert
+ - gofumpt
+ - unparam
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8dada3e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..dfc3f02
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,63 @@
+SHELL = /bin/bash
+GO ?= go
+GOPATH := $(shell $(GO) env GOPATH)
+GOBIN := $(shell $(GO) env GOBIN)
+GO_SRC = $(shell find . -name \*.go)
+GO_BUILD = $(GO) build
+NAME = checkpointctl
+COVERAGE_PATH ?= $(shell pwd)/.coverage
+
+all: $(NAME)
+
+$(NAME): $(GO_SRC)
+ $(GO_BUILD) -buildmode=pie -o $@ -ldflags "-X main.name=$(NAME)"
+
+$(NAME).coverage: $(GO_SRC)
+ $(GO) test \
+ -covermode=count \
+ -coverpkg=./... \
+ -mod=vendor \
+ -tags coverage \
+ -buildmode=pie -c -o $@ \
+ -ldflags "-X main.name=$(NAME)"
+
+clean:
+ rm -f $(NAME) $(NAME).coverage $(COVERAGE_PATH)/*
+ if [ -d $(COVERAGE_PATH) ]; then rmdir $(COVERAGE_PATH); fi
+
+golang-lint:
+ golangci-lint run
+
+shellcheck:
+ shellcheck test/*bats
+
+lint: golang-lint shellcheck
+
+test: $(NAME)
+ bats test/*bats
+
+coverage: $(NAME).coverage
+ mkdir -p $(COVERAGE_PATH)
+ COVERAGE_PATH=$(COVERAGE_PATH) COVERAGE=1 bats test/*bats
+
+codecov:
+ curl -Os https://uploader.codecov.io/latest/linux/codecov
+ chmod +x codecov
+ ./codecov -f '.coverage/*'
+
+vendor:
+ go mod tidy
+ go mod vendor
+ go mod verify
+
+help:
+ @echo "Usage: make <target>"
+ @echo " * clean - remove artifacts"
+ @echo " * lint - verify the source code (shellcheck/golangci-lint)"
+ @echo " * golang-lint - run golang-lint"
+ @echo " * shellcheck - run shellecheck"
+ @echo " * vendor - update go.mod, go.sum and vendor directory"
+ @echo " * test - run tests"
+ @echo " * help - show help"
+
+.PHONY: clean lint golang-lint shellcheck vendor test help
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..bacc897
--- /dev/null
+++ b/README.md
@@ -0,0 +1,105 @@
+<!-- markdownlint-disable MD013 -->
+# checkpointctl -- Show information about checkpoint archives
+
+[![Run Tests](https://github.com/checkpoint-restore/checkpointctl/actions/workflows/tests.yml/badge.svg)](https://github.com/checkpoint-restore/checkpointctl/actions/workflows/tests.yml)
+
+Container engines like *Podman* and *CRI-O* have the ability to checkpoint a
+container. All data related to a checkpoint is collected in a checkpoint
+archive. With the help of this tool, `checkpointctl`, it is possible to display
+information about these checkpoint archives.
+
+Details on how to create checkpoints with the help of [CRIU][criu] can be found at:
+
+* [Forensic container checkpointing in Kubernetes][forensic]
+* [Podman checkpoint][podman]
+
+To display information about a checkpoint archive you can just use
+`checkpointctl show`:
+
+```console
+$ checkpointctl show /tmp/dump.tar
+
++-----------------+------------------------------------------+--------------+---------+----------------------+--------+------------+-------------------+
+| CONTAINER | IMAGE | ID | RUNTIME | CREATED | ENGINE | CHKPT SIZE | ROOT FS DIFF SIZE |
++-----------------+------------------------------------------+--------------+---------+----------------------+--------+------------+-------------------+
+| magical_murdock | quay.io/adrianreber/wildfly-hello:latest | f11d11844af0 | crun | 2023-02-28T09:43:52Z | Podman | 338.2 MiB | 177.0 KiB |
++-----------------+------------------------------------------+--------------+---------+----------------------+--------+------------+-------------------+
+```
+
+For a checkpoint archive created by Kubernetes with *CRI-O* the output would
+look like this:
+
+```console
+$ checkpointctl show /var/lib/kubelet/checkpoints/checkpoint-counters_default-counter-2023-02-13T16\:20\:09Z.tar
+
++-----------+------------------------------------+--------------+---------+--------------------------------+--------+------------+------------+
+| CONTAINER | IMAGE | ID | RUNTIME | CREATED | ENGINE | IP | CHKPT SIZE |
++-----------+------------------------------------+--------------+---------+--------------------------------+--------+------------+------------+
+| counter | quay.io/adrianreber/counter:latest | 7eb9680287f1 | runc | 2023-02-13T16:12:25.843774934Z | CRI-O | 10.88.0.24 | 8.5 MiB |
++-----------+------------------------------------+--------------+---------+--------------------------------+--------+------------+------------+
+```
+
+It is also possible to display additional checkpoint related information
+with the parameter `--print-stats`:
+
+```console
+$ checkpointctl show /tmp/dump.tar --print-stats
+
++-----------------+------------------------------------------+--------------+---------+----------------------+--------+------------+-------------------+
+| CONTAINER | IMAGE | ID | RUNTIME | CREATED | ENGINE | CHKPT SIZE | ROOT FS DIFF SIZE |
++-----------------+------------------------------------------+--------------+---------+----------------------+--------+------------+-------------------+
+| magical_murdock | quay.io/adrianreber/wildfly-hello:latest | f11d11844af0 | crun | 2023-02-28T09:43:52Z | Podman | 338.2 MiB | 177.0 KiB |
++-----------------+------------------------------------------+--------------+---------+----------------------+--------+------------+-------------------+
+CRIU dump statistics
++---------------+-------------+--------------+---------------+---------------+---------------+
+| FREEZING TIME | FROZEN TIME | MEMDUMP TIME | MEMWRITE TIME | PAGES SCANNED | PAGES WRITTEN |
++---------------+-------------+--------------+---------------+---------------+---------------+
+| 104450 us | 442148 us | 212281 us | 148292 us | 495649 | 86510 |
++---------------+-------------+--------------+---------------+---------------+---------------+
+```
+
+## How to contribute
+
+While bug fixes can first be identified via an "issue", that is not required.
+It's ok to just open up a PR with the fix, but make sure you include the same
+information you would have included in an issue - like how to reproduce it.
+
+PRs for new features should include some background on what use cases the
+new code is trying to address. When possible and when it makes sense, try to
+break-up larger PRs into smaller ones - it's easier to review smaller
+code changes. But only if those smaller ones make sense as stand-alone PRs.
+
+Regardless of the type of PR, all PRs should include:
+
+* well documented code changes;
+* additional testcases: ideally, they should fail w/o your code change applied;
+* documentation changes.
+
+Squash your commits into logical pieces of work that might want to be reviewed
+separate from the rest of the PRs. Ideally, each commit should implement a
+single idea, and the PR branch should pass the tests at every commit. GitHub
+makes it easy to review the cumulative effect of many commits; so, when in
+doubt, use smaller commits.
+
+PRs that fix issues should include a reference like `Closes #XXXX` in the
+commit message so that github will automatically close the referenced issue
+when the PR is merged.
+
+Contributors must assert that they are in compliance with the [Developer
+Certificate of Origin 1.1](http://developercertificate.org/). This is achieved
+by adding a "Signed-off-by" line containing the contributor's name and e-mail
+to every commit message. Your signature certifies that you wrote the patch or
+otherwise have the right to pass it on as an open-source patch.
+
+## License and copyright
+
+Unless mentioned otherwise in a specific file's header, all code in
+this project is released under the Apache 2.0 license.
+
+The author of a change remains the copyright holder of their code
+(no copyright assignment). The list of authors and contributors can be
+retrieved from the git commit history and in some cases, the file headers.
+
+[forensic]: https://kubernetes.io/blog/2022/12/05/forensic-container-checkpointing-alpha/
+[podman]: https://podman.io/getting-started/checkpoint
+[criu]: https://criu.org/
diff --git a/checkpointctl.go b/checkpointctl.go
new file mode 100644
index 0000000..b02fd81
--- /dev/null
+++ b/checkpointctl.go
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/containers/storage/pkg/archive"
+ "github.com/spf13/cobra"
+)
+
+var (
+ name string
+ printStats bool
+)
+
+func main() {
+ rootCommand := &cobra.Command{
+ Use: name,
+ Short: name + " is a tool to read and manipulate checkpoint archives",
+ Long: name + " is a tool to read and manipulate checkpoint archives as " +
+ "created by Podman, CRI-O and containerd",
+ SilenceUsage: true,
+ }
+
+ showCommand := setupShow()
+ rootCommand.AddCommand(showCommand)
+
+ if err := rootCommand.Execute(); err != nil {
+ os.Exit(1)
+ }
+}
+
+func setupShow() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "show",
+ Short: "Show information about available checkpoints",
+ RunE: show,
+ Args: cobra.MinimumNArgs(1),
+ }
+ flags := cmd.Flags()
+ flags.BoolVar(
+ &printStats,
+ "print-stats",
+ false,
+ "Print checkpointing statistics if available",
+ )
+
+ return cmd
+}
+
+func show(cmd *cobra.Command, args []string) error {
+ input := args[0]
+ tar, err := os.Stat(input)
+ if err != nil {
+ return err
+ }
+ if !tar.Mode().IsRegular() {
+ return fmt.Errorf("input %s not a regular file", input)
+ }
+ dir, err := os.MkdirTemp("", "checkpointctl")
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err := os.RemoveAll(dir); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ }
+ }()
+
+ if err := archive.UntarPath(input, dir); err != nil {
+ return fmt.Errorf("unpacking of checkpoint archive %s failed: %w", input, err)
+ }
+
+ return showContainerCheckpoint(dir)
+}
diff --git a/checkpointctl_coverage_test.go b/checkpointctl_coverage_test.go
new file mode 100644
index 0000000..a8398d8
--- /dev/null
+++ b/checkpointctl_coverage_test.go
@@ -0,0 +1,40 @@
+//go:build coverage
+// +build coverage
+
+package main
+
+import (
+ "os"
+ "os/signal"
+ "strings"
+ "testing"
+)
+
+// NOTE: do not use this in production. Binaries built with this file are
+// merely useful to collect coverage data.
+func TestCoverageMain(_ *testing.T) {
+ var args []string
+
+ for _, arg := range os.Args {
+ switch {
+ case strings.HasPrefix(arg, "COVERAGE"):
+ // Dummy argument to enable global flags.
+ case strings.HasPrefix(arg, "-test"):
+ // Make sure we don't pass `go test` specific flags to
+ // checkpointctl.
+ default:
+ args = append(args, arg)
+ }
+ }
+
+ signal.Reset()
+ os.Args = args
+ main() // "run" checkpointctl
+
+ // Make sure that std{err,out} write to /dev/null so we prevent the
+ // testing backend to print "PASS" along with the coverage. We really
+ // want the coverage to be set via the `-test.coverprofile=$path` flag.
+ null, _ := os.Open(os.DevNull)
+ os.Stdout = null
+ os.Stderr = null
+}
diff --git a/checkpointctl_test.go b/checkpointctl_test.go
new file mode 100644
index 0000000..e5bc078
--- /dev/null
+++ b/checkpointctl_test.go
@@ -0,0 +1,11 @@
+package main_test
+
+import (
+ "testing"
+)
+
+func TestMain(t *testing.T) {
+ // Do nothing. We just need dummy to make `ginkgo` happy. Without that,
+ // `ginkgo` would try to execute the _coverage_test.go _despite_ the
+ t.Parallel()
+}
diff --git a/container.go b/container.go
new file mode 100644
index 0000000..a11be11
--- /dev/null
+++ b/container.go
@@ -0,0 +1,208 @@
+// SPDX-License-Identifier: Apache-2.0
+
+// This file is used to handle container checkpoint archives
+
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+
+ metadata "github.com/checkpoint-restore/checkpointctl/lib"
+ "github.com/checkpoint-restore/go-criu/v6/crit"
+ "github.com/olekukonko/tablewriter"
+ spec "github.com/opencontainers/runtime-spec/specs-go"
+)
+
+type containerMetadata struct {
+ Name string `json:"name,omitempty"`
+ Attempt uint32 `json:"attempt,omitempty"`
+}
+
+type containerInfo struct {
+ Name string
+ IP string
+ MAC string
+ Created string
+ Engine string
+}
+
+func getPodmanInfo(containerConfig *metadata.ContainerConfig, _ *spec.Spec) *containerInfo {
+ return &containerInfo{
+ Name: containerConfig.Name,
+ Created: containerConfig.CreatedTime.Format(time.RFC3339),
+ Engine: "Podman",
+ }
+}
+
+func getContainerdInfo(containerdStatus *metadata.ContainerdStatus, specDump *spec.Spec) *containerInfo {
+ return &containerInfo{
+ Name: specDump.Annotations["io.kubernetes.cri.container-name"],
+ Created: time.Unix(0, containerdStatus.CreatedAt).Format(time.RFC3339),
+ Engine: "containerd",
+ }
+}
+
+func getCRIOInfo(_ *metadata.ContainerConfig, specDump *spec.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)
+ }
+
+ return &containerInfo{
+ IP: specDump.Annotations["io.kubernetes.cri-o.IP.0"],
+ Name: cm.Name,
+ Created: specDump.Annotations["io.kubernetes.cri-o.Created"],
+ Engine: "CRI-O",
+ }, nil
+}
+
+func showContainerCheckpoint(checkpointDirectory string) error {
+ var (
+ row []string
+ ci *containerInfo
+ )
+ containerConfig, _, err := metadata.ReadContainerCheckpointConfigDump(checkpointDirectory)
+ if err != nil {
+ return err
+ }
+ specDump, _, err := metadata.ReadContainerCheckpointSpecDump(checkpointDirectory)
+ if err != nil {
+ return err
+ }
+
+ switch m := specDump.Annotations["io.container.manager"]; m {
+ case "libpod":
+ ci = getPodmanInfo(containerConfig, specDump)
+ case "cri-o":
+ ci, err = getCRIOInfo(containerConfig, specDump)
+ default:
+ containerdStatus, _, _ := metadata.ReadContainerCheckpointStatusFile(checkpointDirectory)
+ if containerdStatus == nil {
+ return fmt.Errorf("unknown container manager found: %s", m)
+ }
+ ci = getContainerdInfo(containerdStatus, specDump)
+ }
+
+ if err != nil {
+ return fmt.Errorf("getting container checkpoint information failed: %w", err)
+ }
+
+ fmt.Printf("\nDisplaying container checkpoint data from %s\n\n", checkpointDirectory)
+
+ table := tablewriter.NewWriter(os.Stdout)
+ header := []string{
+ "Container",
+ "Image",
+ "ID",
+ "Runtime",
+ "Created",
+ "Engine",
+ }
+
+ row = append(row, ci.Name)
+ row = append(row, containerConfig.RootfsImageName)
+ if len(containerConfig.ID) > 12 {
+ row = append(row, containerConfig.ID[:12])
+ } else {
+ row = append(row, containerConfig.ID)
+ }
+
+ row = append(row, containerConfig.OCIRuntime)
+ row = append(row, ci.Created)
+
+ row = append(row, ci.Engine)
+ if ci.IP != "" {
+ header = append(header, "IP")
+ row = append(row, ci.IP)
+ }
+ if ci.MAC != "" {
+ header = append(header, "MAC")
+ row = append(row, ci.MAC)
+ }
+
+ size, err := getCheckpointSize(checkpointDirectory)
+ if err != nil {
+ return err
+ }
+
+ header = append(header, "CHKPT Size")
+ row = append(row, metadata.ByteToString(size))
+
+ // Display root fs diff size if available
+ fi, err := os.Lstat(filepath.Join(checkpointDirectory, metadata.RootFsDiffTar))
+ if err == nil {
+ if fi.Size() != 0 {
+ header = append(header, "Root Fs Diff Size")
+ row = append(row, metadata.ByteToString(fi.Size()))
+ }
+ }
+
+ table.SetAutoMergeCells(true)
+ table.SetRowLine(true)
+ table.SetHeader(header)
+ table.Append(row)
+ table.Render()
+
+ if !printStats {
+ return nil
+ }
+
+ cpDir, err := os.Open(checkpointDirectory)
+ if err != nil {
+ return err
+ }
+ defer cpDir.Close()
+
+ // Get dump statistics with crit
+ dumpStatistics, err := crit.GetDumpStats(cpDir.Name())
+ if err != nil {
+ return fmt.Errorf("unable to display checkpointing statistics: %w", err)
+ }
+
+ table = tablewriter.NewWriter(os.Stdout)
+ table.SetHeader([]string{
+ "Freezing Time",
+ "Frozen Time",
+ "Memdump Time",
+ "Memwrite Time",
+ "Pages Scanned",
+ "Pages Written",
+ })
+ table.Append([]string{
+ fmt.Sprintf("%d us", dumpStatistics.GetFreezingTime()),
+ fmt.Sprintf("%d us", dumpStatistics.GetFrozenTime()),
+ fmt.Sprintf("%d us", dumpStatistics.GetMemdumpTime()),
+ fmt.Sprintf("%d us", dumpStatistics.GetMemwriteTime()),
+ fmt.Sprintf("%d", dumpStatistics.GetPagesScanned()),
+ fmt.Sprintf("%d", dumpStatistics.GetPagesWritten()),
+ })
+ fmt.Println("CRIU dump statistics")
+ table.Render()
+
+ return nil
+}
+
+func dirSize(path string) (size int64, err error) {
+ err = filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if !info.IsDir() {
+ size += info.Size()
+ }
+
+ return err
+ })
+
+ return size, err
+}
+
+func getCheckpointSize(path string) (size int64, err error) {
+ dir := filepath.Join(path, metadata.CheckpointDirectory)
+
+ return dirSize(dir)
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..68ebf35
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,29 @@
+module github.com/checkpoint-restore/checkpointctl
+
+go 1.18
+
+require (
+ github.com/checkpoint-restore/go-criu/v6 v6.3.0
+ github.com/containers/storage v1.45.3
+ github.com/olekukonko/tablewriter v0.0.5
+ github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417
+ github.com/spf13/cobra v1.6.1
+)
+
+require (
+ github.com/docker/go-units v0.5.0 // indirect
+ github.com/inconshreveable/mousetrap v1.0.1 // indirect
+ github.com/klauspost/compress v1.15.14 // indirect
+ github.com/klauspost/pgzip v1.2.5 // indirect
+ github.com/mattn/go-runewidth v0.0.9 // indirect
+ github.com/moby/sys/mountinfo v0.6.2 // indirect
+ github.com/opencontainers/runc v1.1.4 // indirect
+ github.com/sirupsen/logrus v1.9.0 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
+ github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect
+ github.com/ulikunitz/xz v0.5.11 // indirect
+ golang.org/x/net v0.0.0-20210825183410-e898025ed96a // indirect
+ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
+ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
+ google.golang.org/protobuf v1.28.1 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..ab70b60
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,103 @@
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
+github.com/checkpoint-restore/go-criu/v6 v6.3.0 h1:mIdrSO2cPNWQY1truPg6uHLXyKHk3Z5Odx4wjKOASzA=
+github.com/checkpoint-restore/go-criu/v6 v6.3.0/go.mod h1:rrRTN/uSwY2X+BPRl/gkulo9gsKOSAeVp9/K2tv7xZI=
+github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=
+github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
+github.com/containers/storage v1.45.3 h1:GbtTvTtp3GW2/tcFg5VhgHXcYMwVn2KfZKiHjf9FAOM=
+github.com/containers/storage v1.45.3/go.mod h1:OdRUYHrq1HP6iAo79VxqtYuJzC5j4eA2I60jKOoCT7g=
+github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
+github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/klauspost/compress v1.15.14 h1:i7WCKDToww0wA+9qrUZ1xOjp218vfFo3nTU6UHp+gOc=
+github.com/klauspost/compress v1.15.14/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
+github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
+github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
+github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU=
+github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
+github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
+github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
+github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
+github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
+github.com/opencontainers/runc v1.1.4 h1:nRCz/8sKg6K6jgYAFLDlXzPeITBZJyX28DBVhWD+5dg=
+github.com/opencontainers/runc v1.1.4/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg=
+github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 h1:3snG66yBm59tKhhSPQrQ/0bCrv1LQbKt40LnUPiUxdc=
+github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
+github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
+github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
+github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
+github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
+github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
+github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
+github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
+github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
+golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210825183410-e898025ed96a h1:bRuuGXV8wwSdGTB+CtJf+FjgO1APK1CoO39T4BN/XBw=
+golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
+google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/lib/metadata.go b/lib/metadata.go
new file mode 100644
index 0000000..7339ac9
--- /dev/null
+++ b/lib/metadata.go
@@ -0,0 +1,142 @@
+// SPDX-License-Identifier: Apache-2.0
+
+package metadata
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+
+ spec "github.com/opencontainers/runtime-spec/specs-go"
+)
+
+const (
+ // container archive
+ ConfigDumpFile = "config.dump"
+ SpecDumpFile = "spec.dump"
+ NetworkStatusFile = "network.status"
+ CheckpointDirectory = "checkpoint"
+ CheckpointVolumesDirectory = "volumes"
+ DevShmCheckpointTar = "devshm-checkpoint.tar"
+ RootFsDiffTar = "rootfs-diff.tar"
+ DeletedFilesFile = "deleted.files"
+ DumpLogFile = "dump.log"
+ RestoreLogFile = "restore.log"
+ // pod archive
+ PodOptionsFile = "pod.options"
+ PodDumpFile = "pod.dump"
+ // containerd only
+ StatusFile = "status"
+)
+
+// This is a reduced copy of what Podman uses to store checkpoint metadata
+type ContainerConfig struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ RootfsImage string `json:"rootfsImage,omitempty"`
+ RootfsImageRef string `json:"rootfsImageRef,omitempty"`
+ RootfsImageName string `json:"rootfsImageName,omitempty"`
+ OCIRuntime string `json:"runtime,omitempty"`
+ CreatedTime time.Time `json:"createdTime"`
+ CheckpointedAt time.Time `json:"checkpointedTime"`
+ RestoredAt time.Time `json:"restoredTime"`
+ Restored bool `json:"restored"`
+}
+
+type ContainerdStatus struct {
+ CreatedAt int64
+ StartedAt int64
+ FinishedAt int64
+ ExitCode int32
+ Pid uint32
+ Reason string
+ Message string
+}
+
+// This structure is used by the KubernetesContainerCheckpointMetadata structure
+type KubernetesCheckpoint struct {
+ Archive string `json:"archive,omitempty"`
+ Size int64 `json:"size,omitempty"`
+ Timestamp int64 `json:"timestamp,omitempty"`
+}
+
+// This structure is the basis for Kubernetes to track how many checkpoints
+// for a certain container have been created.
+type KubernetesContainerCheckpointMetadata struct {
+ PodFullName string `json:"podFullName,omitempty"`
+ ContainerName string `json:"containerName,omitempty"`
+ TotalSize int64 `json:"totalSize,omitempty"`
+ Checkpoints []KubernetesCheckpoint `json:"checkpoints"`
+}
+
+func ReadContainerCheckpointSpecDump(checkpointDirectory string) (*spec.Spec, string, error) {
+ var specDump spec.Spec
+ specDumpFile, err := ReadJSONFile(&specDump, checkpointDirectory, SpecDumpFile)
+
+ return &specDump, specDumpFile, err
+}
+
+func ReadContainerCheckpointConfigDump(checkpointDirectory string) (*ContainerConfig, string, error) {
+ var containerConfig ContainerConfig
+ configDumpFile, err := ReadJSONFile(&containerConfig, checkpointDirectory, ConfigDumpFile)
+
+ return &containerConfig, configDumpFile, err
+}
+
+func ReadContainerCheckpointDeletedFiles(checkpointDirectory string) ([]string, string, error) {
+ var deletedFiles []string
+ deletedFilesFile, err := ReadJSONFile(&deletedFiles, checkpointDirectory, DeletedFilesFile)
+
+ return deletedFiles, deletedFilesFile, err
+}
+
+func ReadContainerCheckpointStatusFile(checkpointDirectory string) (*ContainerdStatus, string, error) {
+ var containerdStatus ContainerdStatus
+ statusFile, err := ReadJSONFile(&containerdStatus, checkpointDirectory, StatusFile)
+
+ return &containerdStatus, statusFile, err
+}
+
+// WriteJSONFile marshalls and writes the given data to a JSON file
+func WriteJSONFile(v interface{}, dir, file string) (string, error) {
+ fileJSON, err := json.MarshalIndent(v, "", " ")
+ if err != nil {
+ return "", fmt.Errorf("error marshalling JSON: %w", err)
+ }
+ file = filepath.Join(dir, file)
+ if err := os.WriteFile(file, fileJSON, 0o600); err != nil {
+ return "", err
+ }
+
+ return file, nil
+}
+
+func ReadJSONFile(v interface{}, dir, file string) (string, error) {
+ file = filepath.Join(dir, file)
+ content, err := os.ReadFile(file)
+ if err != nil {
+ return "", err
+ }
+ if err = json.Unmarshal(content, v); err != nil {
+ return "", fmt.Errorf("failed to unmarshal %s: %w", file, err)
+ }
+
+ return file, nil
+}
+
+func ByteToString(b int64) string {
+ const unit = 1024
+ if b < unit {
+ return fmt.Sprintf("%d B", b)
+ }
+ div, exp := int64(unit), 0
+ for n := b / unit; n >= unit; n /= unit {
+ div *= unit
+ exp++
+ }
+
+ return fmt.Sprintf("%.1f %ciB",
+ float64(b)/float64(div), "KMGTPE"[exp])
+}
diff --git a/test/checkpointctl.bats b/test/checkpointctl.bats
new file mode 100644
index 0000000..0e96b43
--- /dev/null
+++ b/test/checkpointctl.bats
@@ -0,0 +1,156 @@
+if [ -n "$COVERAGE" ]; then
+ CHECKPOINTCTL="./checkpointctl.coverage"
+ ARGS="-test.coverprofile=coverprofile.integration.$RANDOM -test.outputdir=${COVERAGE_PATH} COVERAGE"
+else
+ CHECKPOINTCTL="./checkpointctl"
+fi
+TEST_TMP_DIR1=""
+TEST_TMP_DIR2=""
+
+function checkpointctl() {
+ # shellcheck disable=SC2086
+ run $CHECKPOINTCTL $ARGS "$@"
+ echo "$output"
+}
+
+function setup() {
+ TEST_TMP_DIR1=$(mktemp -d)
+ TEST_TMP_DIR2=$(mktemp -d)
+}
+
+function teardown() {
+ [ "$TEST_TMP_DIR1" != "" ] && rm -rf "$TEST_TMP_DIR1"
+ [ "$TEST_TMP_DIR2" != "" ] && rm -rf "$TEST_TMP_DIR2"
+}
+
+@test "Run checkpointctl" {
+ checkpointctl
+ [ "$status" -eq 0 ]
+}
+
+@test "Run checkpointctl with wrong parameter" {
+ checkpointctl --wrong-parameter
+ [ "$status" -eq 1 ]
+ [ "$output" = "Error: unknown flag: --wrong-parameter" ]
+}
+
+@test "Run checkpointctl show with non existing directory" {
+ checkpointctl show /does-not-exist
+ [ "$status" -eq 1 ]
+ [[ ${lines[0]} = "Error: stat /does-not-exist: no such file or directory" ]]
+}
+
+@test "Run checkpointctl show with empty tar file" {
+ touch "$TEST_TMP_DIR1"/empty.tar
+ checkpointctl show "$TEST_TMP_DIR1"/empty.tar
+ [ "$status" -eq 1 ]
+ [[ ${lines[0]} == *"config.dump: no such file or directory"* ]]
+}
+
+@test "Run checkpointctl show with tar file with empty config.dump" {
+ touch "$TEST_TMP_DIR1"/config.dump
+ ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . )
+ checkpointctl show "$TEST_TMP_DIR2"/test.tar
+ [ "$status" -eq 1 ]
+ [[ ${lines[0]} == *"config.dump: unexpected end of JSON input" ]]
+}
+
+@test "Run checkpointctl show with tar file with valid config.dump and no spec.dump" {
+ cp test/config.dump "$TEST_TMP_DIR1"
+ ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . )
+ checkpointctl show "$TEST_TMP_DIR2"/test.tar
+ [ "$status" -eq 1 ]
+ [[ ${lines[0]} == *"spec.dump: no such file or directory" ]]
+}
+
+@test "Run checkpointctl show with tar file with valid config.dump and empty spec.dump" {
+ cp test/config.dump "$TEST_TMP_DIR1"
+ touch "$TEST_TMP_DIR1"/spec.dump
+ ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . )
+ checkpointctl show "$TEST_TMP_DIR2"/test.tar
+ [ "$status" -eq 1 ]
+ [[ ${lines[0]} == *"spec.dump: unexpected end of JSON input" ]]
+}
+
+@test "Run checkpointctl show with tar file with valid config.dump and valid spec.dump and no checkpoint directory" {
+ cp test/config.dump "$TEST_TMP_DIR1"
+ cp test/spec.dump "$TEST_TMP_DIR1"
+ ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . )
+ checkpointctl show "$TEST_TMP_DIR2"/test.tar
+ [ "$status" -eq 1 ]
+ [[ ${lines[1]} == *"checkpoint: no such file or directory" ]]
+}
+
+@test "Run checkpointctl show with tar file with valid config.dump and valid spec.dump and checkpoint directory" {
+ cp test/config.dump "$TEST_TMP_DIR1"
+ cp test/spec.dump "$TEST_TMP_DIR1"
+ mkdir "$TEST_TMP_DIR1"/checkpoint
+ ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . )
+ checkpointctl show "$TEST_TMP_DIR2"/test.tar
+ [ "$status" -eq 0 ]
+ [[ ${lines[4]} == *"Podman"* ]]
+}
+
+@test "Run checkpointctl show with tar file from containerd with valid config.dump and valid spec.dump and checkpoint directory" {
+ cp test/config.dump "$TEST_TMP_DIR1"
+ mkdir "$TEST_TMP_DIR1"/checkpoint
+ echo "{}" > "$TEST_TMP_DIR1"/status
+ echo "{}" > "$TEST_TMP_DIR1"/spec.dump
+ ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . )
+ checkpointctl show "$TEST_TMP_DIR2"/test.tar
+ [ "$status" -eq 0 ]
+ [[ ${lines[4]} == *"containerd"* ]]
+}
+
+@test "Run checkpointctl show with tar file and --print-stats and missing stats-dump" {
+ cp test/config.dump "$TEST_TMP_DIR1"
+ cp test/spec.dump "$TEST_TMP_DIR1"
+ mkdir "$TEST_TMP_DIR1"/checkpoint
+ ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . )
+ checkpointctl show "$TEST_TMP_DIR2"/test.tar --print-stats
+ [ "$status" -eq 1 ]
+ [[ ${lines[6]} == *"unable to display checkpointing statistics"* ]]
+}
+
+@test "Run checkpointctl show with tar file and --print-stats and invalid stats-dump" {
+ cp test/config.dump "$TEST_TMP_DIR1"
+ cp test/spec.dump "$TEST_TMP_DIR1"
+ cp test/spec.dump "$TEST_TMP_DIR1"/stats-dump
+ mkdir "$TEST_TMP_DIR1"/checkpoint
+ ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . )
+ checkpointctl show "$TEST_TMP_DIR2"/test.tar --print-stats
+ [ "$status" -eq 1 ]
+ [[ ${lines[6]} == *"Unknown magic"* ]]
+}
+
+@test "Run checkpointctl show with tar file and --print-stats and valid stats-dump" {
+ cp test/config.dump "$TEST_TMP_DIR1"
+ cp test/spec.dump "$TEST_TMP_DIR1"
+ cp test/stats-dump "$TEST_TMP_DIR1"
+ mkdir "$TEST_TMP_DIR1"/checkpoint
+ ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . )
+ checkpointctl show "$TEST_TMP_DIR2"/test.tar --print-stats
+ [ "$status" -eq 0 ]
+ [[ ${lines[6]} == *"CRIU dump statistics"* ]]
+ [[ ${lines[8]} == *"MEMWRITE TIME"* ]]
+ [[ ${lines[10]} == *"446571 us"* ]]
+}
+
+@test "Run checkpointctl show with tar file with valid config.dump and valid spec.dump (CRI-O) and no checkpoint directory" {
+ cp test/config.dump "$TEST_TMP_DIR1"
+ cp test/spec.dump.cri-o "$TEST_TMP_DIR1"/spec.dump
+ ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . )
+ checkpointctl show "$TEST_TMP_DIR2"/test.tar
+ [ "$status" -eq 1 ]
+ [[ ${lines[1]} == *"checkpoint: no such file or directory"* ]]
+}
+
+@test "Run checkpointctl show with tar file with valid config.dump and valid spec.dump (CRI-O) and checkpoint directory" {
+ cp test/config.dump "$TEST_TMP_DIR1"
+ cp test/spec.dump.cri-o "$TEST_TMP_DIR1"/spec.dump
+ mkdir "$TEST_TMP_DIR1"/checkpoint
+ ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . )
+ checkpointctl show "$TEST_TMP_DIR2"/test.tar
+ [ "$status" -eq 0 ]
+ [[ ${lines[4]} == *"CRI-O"* ]]
+}
diff --git a/test/config.dump b/test/config.dump
new file mode 100644
index 0000000..2c63c08
--- /dev/null
+++ b/test/config.dump
@@ -0,0 +1,2 @@
+{
+}
diff --git a/test/spec.dump b/test/spec.dump
new file mode 100644
index 0000000..237795b
--- /dev/null
+++ b/test/spec.dump
@@ -0,0 +1,3 @@
+{
+"annotations": {"io.container.manager": "libpod"}
+}
diff --git a/test/spec.dump.cri-o b/test/spec.dump.cri-o
new file mode 100644
index 0000000..17079ec
--- /dev/null
+++ b/test/spec.dump.cri-o
@@ -0,0 +1,6 @@
+{
+ "annotations": {
+ "io.container.manager": "cri-o",
+ "io.kubernetes.cri-o.Metadata" : "{}"
+ }
+}
diff --git a/test/stats-dump b/test/stats-dump
new file mode 100644
index 0000000..455fe98
--- /dev/null
+++ b/test/stats-dump
Binary files differ