diff --git a/cmd/kpod/export.go b/cmd/kpod/export.go new file mode 100644 index 00000000..53eb5080 --- /dev/null +++ b/cmd/kpod/export.go @@ -0,0 +1,103 @@ +package main + +import ( + "io" + "os" + + "fmt" + + "github.com/containers/storage" + "github.com/containers/storage/pkg/archive" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +type exportOptions struct { + output string + container string +} + +var ( + exportFlags = []cli.Flag{ + cli.StringFlag{ + Name: "output, o", + Usage: "Write to a file, default is STDOUT", + Value: "/dev/stdout", + }, + } + exportDescription = "Exports container's filesystem contents as a tar archive" + + " and saves it on the local machine." + exportCommand = cli.Command{ + Name: "export", + Usage: "Export container's filesystem contents as a tar archive", + Description: exportDescription, + Flags: exportFlags, + Action: exportCmd, + ArgsUsage: "CONTAINER", + } +) + +// exportCmd saves a container to a tarball on disk +func exportCmd(c *cli.Context) error { + args := c.Args() + if len(args) == 0 { + return errors.Errorf("container id must be specified") + } + if len(args) > 1 { + return errors.Errorf("too many arguments given, need 1 at most.") + } + container := args[0] + + config, err := getConfig(c) + if err != nil { + return errors.Wrapf(err, "could not get config") + } + store, err := getStore(config) + if err != nil { + return err + } + + output := c.String("output") + if output == "/dev/stdout" { + file := os.Stdout + if logrus.IsTerminal(file) { + return errors.Errorf("refusing to export to terminal. Use -o flag or redirect") + } + } + + opts := exportOptions{ + output: output, + container: container, + } + + return exportContainer(store, opts) +} + +// exportContainer exports the contents of a container and saves it as +// a tarball on disk +func exportContainer(store storage.Store, opts exportOptions) error { + mountPoint, err := store.Mount(opts.container, "") + if err != nil { + return errors.Wrapf(err, "error finding container %q", opts.container) + } + defer func() { + if err := store.Unmount(opts.container); err != nil { + fmt.Printf("error unmounting container %q: %v\n", opts.container, err) + } + }() + + input, err := archive.Tar(mountPoint, archive.Uncompressed) + if err != nil { + return errors.Wrapf(err, "error reading container directory %q", opts.container) + } + + outFile, err := os.Create(opts.output) + if err != nil { + return errors.Wrapf(err, "error creating file %q", opts.output) + } + defer outFile.Close() + + _, err = io.Copy(outFile, input) + return err +} diff --git a/cmd/kpod/main.go b/cmd/kpod/main.go index 9b5d48b8..7e91bd3e 100644 --- a/cmd/kpod/main.go +++ b/cmd/kpod/main.go @@ -22,6 +22,7 @@ func main() { app.Version = Version app.Commands = []cli.Command{ + exportCommand, historyCommand, imagesCommand, infoCommand, diff --git a/completions/bash/kpod b/completions/bash/kpod index fee71121..9bb7e0c1 100644 --- a/completions/bash/kpod +++ b/completions/bash/kpod @@ -182,6 +182,15 @@ _kpod_save() { _complete_ "$options_with_args" "$boolean_options" } +_kpod_export() { + local options_with_args=" + --output -o + " + local boolean_options=" + " + _complete_ "$options_with_args" "$boolean_options" +} + _complete_() { local options_with_args=$1 local boolean_options="$2 -h --help" diff --git a/docs/kpod-export.1.md b/docs/kpod-export.1.md new file mode 100644 index 00000000..260cac6b --- /dev/null +++ b/docs/kpod-export.1.md @@ -0,0 +1,48 @@ +% kpod(1) kpod-export - Simple tool to export a container's filesystem as a tarball +% Urvashi Mohnani +# kpod-export "1" "July 2017" "kpod" + +## NAME +kpod-export - Export container's filesystem contents as a tar archive + +## SYNOPSIS +**kpod export** +**CONTAINER** +[**--help**|**-h**] + +## DESCRIPTION +**kpod export** exports the filesystem of a container and saves it as a tarball +on the local machine. **kpod export** writes to STDOUT by default and can be +redirected to a file using the **output flag**. + +**kpod [GLOBAL OPTIONS]** + +**kpod export [GLOBAL OPTIONS]** + +**kpod export [OPTIONS] CONTAINER [GLOBAL OPTIONS]** + +## OPTIONS + +**--output, -o** +Write to a file, default is STDOUT + +## GLOBAL OPTIONS + +**--help, -h** + Print usage statement + +## EXAMPLES + +``` +# kpod export -o redis-container.tar 883504668ec465463bc0fe7e63d53154ac3b696ea8d7b233748918664ea90e57 +``` + +``` +# kpod export > redis-container.tar 883504668ec465463bc0fe7e63d53154ac3b696ea8d7b233748918664ea90e57 +``` + +## SEE ALSO +kpod(1), kpod-import(1), crio(8), crio.conf(5) + +## HISTORY +August 2017, Originally compiled by Urvashi Mohnani diff --git a/test/kpod_export.bats b/test/kpod_export.bats new file mode 100644 index 00000000..808d39ff --- /dev/null +++ b/test/kpod_export.bats @@ -0,0 +1,35 @@ +#!/usr/bin/env bats + +load helpers + +IMAGE="redis:alpine" +ROOT="$TESTDIR/crio" +RUNROOT="$TESTDIR/crio-run" +KPOD_OPTIONS="--root $ROOT --runroot $RUNROOT ${STORAGE_OPTS}" + + +@test "kpod export output flag" { + start_crio + [ "$status" -eq 0 ] + run crioctl pod run --config "$TESTDATA"/sandbox_config.json + echo "$output" + [ "$status" -eq 0 ] + pod_id="$output" + run crioctl image pull "$IMAGE" + [ "$status" -eq 0 ] + run crioctl ctr create --config "$TESTDATA"/container_config.json --pod "$pod_id" + echo "$output" + [ "$status" -eq 0 ] + ctr_id="$output" + run ${KPOD_BINARY} ${KPOD_OPTIONS} export -o container.tar "$ctr_id" + echo "$output" + [ "$status" -eq 0 ] + cleanup_ctrs + [ "$status" -eq 0 ] + cleanup_pods + [ "$status" -eq 0 ] + stop_crio + [ "$status" -eq 0 ] + rm -f container.tar + [ "$status" -eq 0 ] +}