diff options
-rw-r--r-- | .github/dependabot.yml | 6 | ||||
-rw-r--r-- | .github/workflows/coverage.yml | 15 | ||||
-rw-r--r-- | .github/workflows/golangci-lint.yml | 14 | ||||
-rw-r--r-- | .github/workflows/markdown-lint.yml | 20 | ||||
-rw-r--r-- | .github/workflows/tests.yml | 17 | ||||
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | .golangci.yml | 10 | ||||
-rw-r--r-- | LICENSE | 201 | ||||
-rw-r--r-- | Makefile | 63 | ||||
-rw-r--r-- | README.md | 105 | ||||
-rw-r--r-- | checkpointctl.go | 77 | ||||
-rw-r--r-- | checkpointctl_coverage_test.go | 40 | ||||
-rw-r--r-- | checkpointctl_test.go | 11 | ||||
-rw-r--r-- | container.go | 208 | ||||
-rw-r--r-- | go.mod | 29 | ||||
-rw-r--r-- | go.sum | 103 | ||||
-rw-r--r-- | lib/metadata.go | 142 | ||||
-rw-r--r-- | test/checkpointctl.bats | 156 | ||||
-rw-r--r-- | test/config.dump | 2 | ||||
-rw-r--r-- | test/spec.dump | 3 | ||||
-rw-r--r-- | test/spec.dump.cri-o | 6 | ||||
-rw-r--r-- | test/stats-dump | bin | 0 -> 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 @@ -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) +} @@ -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 +) @@ -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 Binary files differnew file mode 100644 index 0000000..455fe98 --- /dev/null +++ b/test/stats-dump |