diff options
43 files changed, 974 insertions, 95 deletions
@@ -1,6 +1,6 @@ GO ?= go DESTDIR ?= / -EPOCH_TEST_COMMIT ?= 8161802f7df857e0850f842261079c83290f9891 +EPOCH_TEST_COMMIT ?= 1f31892a9fd8573d4b25274b208e6b9f860cdf81 HEAD ?= HEAD CHANGELOG_BASE ?= HEAD~ CHANGELOG_TARGET ?= HEAD diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 5eb85d0bc..e36757edc 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,36 @@ # Release Notes +## 1.4.0 +### Features +- The `podman checkpoint` and `podman restore` commands can now be used to migrate containers between Podman installations on different systems ([#1618](https://github.com/containers/libpod/issues/1618)) +- The `podman cp` command now supports a `pause` flag to pause containers while copying into them +- The remote client now supports a configuration file for pre-configuring connections to remote Podman installations + +### Bugfixes +- Fixed CVE-2019-10152 - The `podman cp` command improperly dereferenced symlinks in host context +- Fixed a bug where `podman commit` could improperly set environment variables that contained `=` characters ([#3132](https://github.com/containers/libpod/issues/3132)) +- Fixed a bug where rootless Podman would sometimes fail to start containers with forwarded ports ([#2942](https://github.com/containers/libpod/issues/2942)) +- Fixed a bug where `podman version` on the remote client could segfault ([#3145](https://github.com/containers/libpod/issues/3145)) +- Fixed a bug where `podman container runlabel` would use `/proc/self/exe` instead of the path of the Podman command when printing the command being executed +- Fixed a bug where filtering images by label did not work ([#3163](https://github.com/containers/libpod/issues/3163)) +- Fixed a bug where specifying a bing mount or tmpfs mount over an image volume would cause a container to be unable to start ([#3174](https://github.com/containers/libpod/issues/3174)) +- Fixed a bug where `podman generate kube` did not work with containers with named volumes +- Fixed a bug where rootless Podman would receive `permission denied` errors accessing `conmon.pid` ([#3187](https://github.com/containers/libpod/issues/3187)) +- Fixed a bug where `podman cp` with a folder specified as target would replace the folder, as opposed to copying into it ([#3184](https://github.com/containers/libpod/issues/3184)) +- Fixed a bug where rootless Podman commands could double-unlock a lock, causing a crash ([#3207](https://github.com/containers/libpod/issues/3207)) +- Fixed a bug where Podman incorrectly set `tmpcopyup` on `/dev/` mounts, causing errors when using the Kata containers runtime ([#3229](https://github.com/containers/libpod/issues/3229)) +- Fixed a bug where `podman exec` would fail on older kernels ([#2968](https://github.com/containers/libpod/issues/2968)) + +### Misc +- The `podman commit` command is now usable with the Podman remote client +- The `--signature-policy` flag (used with several image-related commands) has been deprecated +- The `podman unshare` command now defines two environment variables in the spawned shell: `CONTAINERS_RUNROOT` and `CONTAINERS_GRAPHROOT`, pointing to temporary and permanent storage for rootless containers +- Updated vendored containers/storage and containers/image libraries with numerous bugfixes +- Updated vendored Buildah to v1.8.3 +- Podman now requires [Conmon v0.2.0](https://github.com/containers/conmon/releases/tag/v0.2.0) +- The `podman cp` command is now aliased as `podman container cp` +- Rootless Podman will now default `init_path` using root Podman's configuration files (`/etc/containers/libpod.conf` and `/usr/share/containers/libpod.conf`) if not overridden in the rootless configuration + ## 1.3.1 ### Features - The `podman cp` command can now read input redirected to `STDIN`, and output to `STDOUT` instead of a file, using `-` instead of an argument. diff --git a/changelog.txt b/changelog.txt index ec0a62f26..ece82e15b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,137 @@ +- Changelog for v1.4.0 (2019-06-07) + * Update release notes for v1.4.0 + * Update release notes for v1.4.0 + * Disable a very badly flaking healthcheck test + * rootless: skip NS_GET_PARENT on old kernels + * Cirrus: Track VM Image calling GCE project + * remove -c for podman remote global options + * Vendor Buildah v1.8.3 + * Cirrus: Disable testing on F28 (EOL) + * migration: add possibility to restore a container with a new name + * Inherit rootless init_path from system libpod.conf + * Also download container images during restore + * Include container migration into tutorial + * Add man-pages for container migration + * Added bash completion for container migration + * Add test case for container migration + * Added support to migrate containers + * Added helper functions for container migration + * Fix restore options help text and comments + * fix timing issues with some tests + * pkg/varlinkapi/virtwriter/virtwriter.go: simplify func Reader + * rootless: block signals on re-exec + * cirrus: minor cleanup and refactoring + * manpage: podman-tool table: un-confuse version and varlink + * Create Dockerfiles for podmanimage + * rootless: use TEMP_FAILURE_RETRY macro + * rootless: fix return type + * rootless: make sure the buffer is NUL terminated + * split rootless local and remote testing + * Fix podman cp test by reordering operations + * Small fix to readme to force tests to run + * Do not set tmpcopyup on /dev + * do not run remote tests inside container + * podman remote-client commit + * Fix podman cp tests + * podman-remote.conf enablement + * Error when trying to copy into a running rootless ctr + * rootless: skip check fo /etc/containers/registries.conf + * We can't pause rootless containers during cp + * Fix bug in e2e tests for podman cp + * Tolerate non-running containers in paused cp + * Add test to ensure symlinks are resolved in ctr scope + * Add --pause to podman cp manpage and bash completions + * Pause containers while copying into them + * Use securejoin to merge paths in `podman cp` + * use imagecaches for local tests + * add dns flags to docs + * add missing container cp command + * Podman logs man page shouldn't include timestamps + * Fix the varlink upgraded calls + * hack: support setting local region/zone + * document missing container update command + * Add --follow to journald ctr logging + * Address comments + * Implement podman logs with log-driver journald + * bump go-systemd version + * Added --log-driver and journald logging + * Update completions and docs to use k8s file as log driver + * bump conmon to v0.2.0 + * runtime: unlock the alive lock only once + * rootless: make JoinUserAndMountNS private + * Revert "rootless: change default path for conmon.pid" + * rootless: enable loginctl linger + * rootless: new function to join existing conmon processes + * rootless: block signals for pause + * Update install.md ostree Debian dependencies. + * fix bug dest path of copying tar + * podman: honor env variable PODMAN_USERNS + * userns: add new option --userns=keep-id + * warn when --security-opt and --privileged + * baseline tests: apparmor with --privileged + * rootless: store also the original GID in the host + * Fix a potential flake in the tests for podman cp + * cirrus: update images w/ zip pkg + * Cirrus: Add zip package to images + * rootless: fix top huser and hgroup + * vendor: update psgo to v1.3.0 + * apparmor: don't load/set profile in privileged mode + * hack: ignore from all VCS files when tarballing + * hack: shrink xfer tarball size + * hack: Display IP address of VM from script + * document nullable types + * Add test cases for login and logout + * Remove unused return statement in kube volume code + * Fix play kube when a pod is specified + * Fix a 'generate kube' bug on ctrs with named volumes + * Add test for image volume conflict with user volume + * Cirrus: Fix missing CRIO_COMMIT -> CONMON_COMMIT + * When superceding mounts, check for opposite types + * make remote resize channel buffered + * Cirrus: workaround root expand failure + * Cirrus: Stub in F30 support + * Cirrus: fixups based on review feedback + * Cirrus: Overhaul/Simplify env. var setup + * Cirrus: Run tests on test-built cache-images + * Cirrus: Support testing of VM cache-image changes + * Cirrus: Remove "too new" runc hack + * libpod: prefer WaitForFile to polling + * Remove conmon from fedora install instructions + * rootless: force resources to be nil on cgroup v1 + * Fixup Flags + * Minor fix filtering images by label + * container: move channel close to its writer + * util: fix race condition in WaitForFile + * Update vendor of buildah and containers/images + * Add Jhon Honce (@jwhonce on github) to OWNERS + * Don't set apparmor if --priviliged + * docs/libpod.conf.5: Add "have" to "higher precedence" typo + * Output name of process on runlabel command + * Minor fix splitting env vars in podman-commit + * Fixup conmon documentation + * troubleshooting.md: add note about updating subuid/subgid + * system: migrate stops the pause process + * rootless: join namespace immediately when possible + * rootless: use a pause process + * migrate: not create a new namespace + * install.remote should be separate for install.bin + * Cirrus: Confirm networking is working + * Use containers/conmon + * Fix a typo in release notes, and bump README version + * s|kubernetes-sigs/cri-o|cri-o/cri-o|g + * Bump github.com/containers/storage to v1.12.7 + * remote: version: fix nil dereference + * Bump gitvalidation epoch + * Bump to v1.3.2-dev + * Add connection information to podman-remote info + * unshare: define CONTAINERS_GRAPHROOT and CONTAINERS_RUNROOT + * Touchup run man page + * unshare: use rootless from libpod + * Replace root-based rootless tests + * rootless: default --cgroup-manager=systemd in unified mode + * create: skip resources validation with cgroup v2 + * rootless, spec: allow resources with cgroup v2 + - Changelog for v1.3.1 (2019-05-16) * More release notes * Add unshare to podman diff --git a/cmd/podman/checkpoint.go b/cmd/podman/checkpoint.go index 234d683bb..86bc8b973 100644 --- a/cmd/podman/checkpoint.go +++ b/cmd/podman/checkpoint.go @@ -46,6 +46,7 @@ func init() { flags.BoolVar(&checkpointCommand.TcpEstablished, "tcp-established", false, "Checkpoint a container with established TCP connections") flags.BoolVarP(&checkpointCommand.All, "all", "a", false, "Checkpoint all running containers") flags.BoolVarP(&checkpointCommand.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + flags.StringVarP(&checkpointCommand.Export, "export", "e", "", "Export the checkpoint image to a tar.gz") markFlagHiddenForRemoteClient("latest", flags) } @@ -64,6 +65,7 @@ func checkpointCmd(c *cliconfig.CheckpointValues) error { Keep: c.Keep, KeepRunning: c.LeaveRunning, TCPEstablished: c.TcpEstablished, + TargetFile: c.Export, } return runtime.Checkpoint(c, options) } diff --git a/cmd/podman/cliconfig/config.go b/cmd/podman/cliconfig/config.go index 61ea26cf7..b8b1648b8 100644 --- a/cmd/podman/cliconfig/config.go +++ b/cmd/podman/cliconfig/config.go @@ -91,6 +91,7 @@ type CheckpointValues struct { TcpEstablished bool All bool Latest bool + Export string } type CommitValues struct { @@ -428,6 +429,8 @@ type RestoreValues struct { Keep bool Latest bool TcpEstablished bool + Import string + Name string } type RmValues struct { diff --git a/cmd/podman/cp.go b/cmd/podman/cp.go index 2e2ca272a..7679ebcf1 100644 --- a/cmd/podman/cp.go +++ b/cmd/podman/cp.go @@ -51,7 +51,7 @@ func init() { cpCommand.Command = _cpCommand flags := cpCommand.Flags() flags.BoolVar(&cpCommand.Extract, "extract", false, "Extract the tar file into the destination directory.") - flags.BoolVar(&cpCommand.Pause, "pause", true, "Pause the container while copying") + flags.BoolVar(&cpCommand.Pause, "pause", false, "Pause the container while copying") cpCommand.SetHelpTemplate(HelpTemplate()) cpCommand.SetUsageTemplate(UsageTemplate()) rootCmd.AddCommand(cpCommand.Command) diff --git a/cmd/podman/restore.go b/cmd/podman/restore.go index 8cfd5ca0d..9c77d4a5e 100644 --- a/cmd/podman/restore.go +++ b/cmd/podman/restore.go @@ -24,10 +24,10 @@ var ( restoreCommand.InputArgs = args restoreCommand.GlobalFlags = MainGlobalOpts restoreCommand.Remote = remoteclient - return restoreCmd(&restoreCommand) + return restoreCmd(&restoreCommand, cmd) }, Args: func(cmd *cobra.Command, args []string) error { - return checkAllAndLatest(cmd, args, false) + return checkAllAndLatest(cmd, args, true) }, Example: `podman container restore ctrID podman container restore --latest @@ -43,13 +43,14 @@ func init() { flags.BoolVarP(&restoreCommand.All, "all", "a", false, "Restore all checkpointed containers") flags.BoolVarP(&restoreCommand.Keep, "keep", "k", false, "Keep all temporary checkpoint files") flags.BoolVarP(&restoreCommand.Latest, "latest", "l", false, "Act on the latest container podman is aware of") - // TODO: add ContainerStateCheckpointed - flags.BoolVar(&restoreCommand.TcpEstablished, "tcp-established", false, "Checkpoint a container with established TCP connections") + flags.BoolVar(&restoreCommand.TcpEstablished, "tcp-established", false, "Restore a container with established TCP connections") + flags.StringVarP(&restoreCommand.Import, "import", "i", "", "Restore from exported checkpoint archive (tar.gz)") + flags.StringVarP(&restoreCommand.Name, "name", "n", "", "Specify new name for container restored from exported checkpoint (only works with --import)") markFlagHiddenForRemoteClient("latest", flags) } -func restoreCmd(c *cliconfig.RestoreValues) error { +func restoreCmd(c *cliconfig.RestoreValues, cmd *cobra.Command) error { if rootless.IsRootless() { return errors.New("restoring a container requires root") } @@ -63,6 +64,20 @@ func restoreCmd(c *cliconfig.RestoreValues) error { options := libpod.ContainerCheckpointOptions{ Keep: c.Keep, TCPEstablished: c.TcpEstablished, + TargetFile: c.Import, + Name: c.Name, } - return runtime.Restore(c, options) + + if c.Import == "" && c.Name != "" { + return errors.Errorf("--name can only used with --import") + } + + if c.Name != "" && c.TcpEstablished { + return errors.Errorf("--tcp-established cannot be used with --name") + } + + if (c.Import != "") && (c.All || c.Latest) { + return errors.Errorf("Cannot use --import and --all or --latest at the same time") + } + return runtime.Restore(getContext(), c, options) } diff --git a/completions/bash/podman b/completions/bash/podman index 49c8c0e52..efb8a6a9b 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -742,6 +742,10 @@ _podman_container_attach() { } _podman_container_checkpoint() { + local options_with_args=" + -e + --export + " local boolean_options=" -a --all @@ -755,9 +759,15 @@ _podman_container_checkpoint() { --leave-running --tcp-established " + case "$prev" in + -e|--export) + _filedir + return + ;; + esac case "$cur" in -*) - COMPREPLY=($(compgen -W "$boolean_options" -- "$cur")) + COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; *) __podman_complete_containers_running @@ -844,6 +854,12 @@ _podman_container_restart() { } _podman_container_restore() { + local options_with_args=" + -i + --import + -n + --name + " local boolean_options=" -a --all @@ -855,9 +871,15 @@ _podman_container_restore() { --latest --tcp-established " + case "$prev" in + -i|--import) + _filedir + return + ;; + esac case "$cur" in -*) - COMPREPLY=($(compgen -W "$boolean_options" -- "$cur")) + COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; *) __podman_complete_containers_created diff --git a/contrib/imgts/entrypoint.sh b/contrib/imgts/entrypoint.sh index 65a76d8e4..610e1f3b6 100755 --- a/contrib/imgts/entrypoint.sh +++ b/contrib/imgts/entrypoint.sh @@ -32,6 +32,7 @@ ARGS="--update-labels=last-used=$(date +%s)" # optional [[ -z "$BUILDID" ]] || ARGS="$ARGS --update-labels=build-id=$BUILDID" [[ -z "$REPOREF" ]] || ARGS="$ARGS --update-labels=repo-ref=$REPOREF" +[[ -z "$GCPPROJECT" ]] || ARGS="$ARGS --update-labels=project=$GCPPROJECT" gcloud config set account "$GCPNAME" gcloud config set project "$GCPPROJECT" diff --git a/contrib/spec/podman.spec.in b/contrib/spec/podman.spec.in index a1c11a5a6..8c2ccd4b0 100644 --- a/contrib/spec/podman.spec.in +++ b/contrib/spec/podman.spec.in @@ -39,7 +39,7 @@ %global shortcommit_conmon %(c=%{commit_conmon}; echo ${c:0:7}) Name: podman -Version: 1.3.2 +Version: 1.4.1 Release: #COMMITDATE#.git%{shortcommit0}%{?dist} Summary: Manage Pods, Containers and Container Images License: ASL 2.0 diff --git a/docs/podman-build.1.md b/docs/podman-build.1.md index e2769c2a9..1b86992d9 100644 --- a/docs/podman-build.1.md +++ b/docs/podman-build.1.md @@ -529,7 +529,7 @@ Only the current container can use a private volume. `Overlay Volume Mounts` - The `:O` flag tells Buildah to mount the directory from the host as a temporary storage using the Overlay file system. The `RUN` command containers are allowed to modify contents within the mountpoint and are stored in the container storage in a separate directory. In Ovelay FS terms the source directory will be the lower, and the container storage directory will be the upper. Modifications to the mount point are destroyed when the `RUN` command finishes executing, similar to a tmpfs mount point. + The `:O` flag tells Buildah to mount the directory from the host as a temporary storage using the Overlay file system. The `RUN` command containers are allowed to modify contents within the mountpoint and are stored in the container storage in a separate directory. In Overlay FS terms the source directory will be the lower, and the container storage directory will be the upper. Modifications to the mount point are destroyed when the `RUN` command finishes executing, similar to a tmpfs mount point. Any subsequent execution of `RUN` commands sees the original source directory content, any changes from previous RUN commands no longer exists. diff --git a/docs/podman-container-checkpoint.1.md b/docs/podman-container-checkpoint.1.md index 79dc12261..afccdf59a 100644 --- a/docs/podman-container-checkpoint.1.md +++ b/docs/podman-container-checkpoint.1.md @@ -38,6 +38,12 @@ image contains established TCP connections, this options is required during restore. Defaults to not checkpointing containers with established TCP connections. +**--export, -e** + +Export the checkpoint to a tar.gz file. The exported checkpoint can be used +to import the container on another system and thus enabling container live +migration. + ## EXAMPLE podman container checkpoint mywebserver diff --git a/docs/podman-container-restore.1.md b/docs/podman-container-restore.1.md index e41f7c1d8..5efc280fe 100644 --- a/docs/podman-container-restore.1.md +++ b/docs/podman-container-restore.1.md @@ -42,6 +42,24 @@ If the checkpoint image does not contain established TCP connections this option is ignored. Defaults to not restoring containers with established TCP connections. +**--import, -i** + +Import a checkpoint tar.gz file, which was exported by Podman. This can be used +to import a checkpointed container from another host. It is not necessary to specify +a container when restoring from an exported checkpoint. + +**--name, -n** + +This is only available in combination with **--import, -i**. If a container is restored +from a checkpoint tar.gz file it is possible to rename it with **--name, -n**. This +way it is possible to restore a container from a checkpoint multiple times with different +names. + +If the **--name, -n** option is used, Podman will not attempt to assign the same IP +address to the container it was using before checkpointing as each IP address can only +be used once and the restored container will have another IP address. This also means +that **--name, -n** cannot be used in combination with **--tcp-established**. + ## EXAMPLE podman container restore mywebserver diff --git a/docs/podman-cp.1.md b/docs/podman-cp.1.md index 76fe57a9e..ee218d66a 100644 --- a/docs/podman-cp.1.md +++ b/docs/podman-cp.1.md @@ -63,7 +63,7 @@ Extract the tar file into the destination directory. If the destination director **--pause** -Pause the container while copying into it to avoid potential security issues around symlinks. Defaults to *true*. +Pause the container while copying into it to avoid potential security issues around symlinks. Defaults to *false*. ## ALTERNATIVES diff --git a/docs/tutorials/podman_tutorial.md b/docs/tutorials/podman_tutorial.md index 032b7c851..8b29264db 100644 --- a/docs/tutorials/podman_tutorial.md +++ b/docs/tutorials/podman_tutorial.md @@ -96,6 +96,28 @@ After being restored, the container will answer requests again as it did before curl http://<IP_address>:8080 ``` +### Migrate the container +To live migrate a container from one host to another the container is checkpointed on the source +system of the migration, transferred to the destination system and then restored on the destination +system. When transferring the checkpoint, it is possible to specify an output-file. + +On the source system: +```console +sudo podman container checkpoint <container_id> -e /tmp/checkpoint.tar.gz +scp /tmp/checkpoint.tar.gz <destination_system>:/tmp +``` + +On the destination system: +```console +sudo podman container restore -i /tmp/checkpoint.tar.gz +``` + +After being restored, the container will answer requests again as it did before checkpointing. This +time the container will continue to run on the destination system. +```console +curl http://<IP_address>:8080 +``` + ### Stopping the container To stop the httpd container: ```console diff --git a/libpod/container_api.go b/libpod/container_api.go index eff5bfe5f..c27cb85ea 100644 --- a/libpod/container_api.go +++ b/libpod/container_api.go @@ -815,11 +815,27 @@ type ContainerCheckpointOptions struct { // TCPEstablished tells the API to checkpoint a container // even if it contains established TCP connections TCPEstablished bool + // Export tells the API to write the checkpoint image to + // the filename set in TargetFile + // Import tells the API to read the checkpoint image from + // the filename set in TargetFile + TargetFile string + // Name tells the API that during restore from an exported + // checkpoint archive a new name should be used for the + // restored container + Name string } // Checkpoint checkpoints a container func (c *Container) Checkpoint(ctx context.Context, options ContainerCheckpointOptions) error { logrus.Debugf("Trying to checkpoint container %s", c.ID()) + + if options.TargetFile != "" { + if err := c.prepareCheckpointExport(); err != nil { + return err + } + } + if !c.batched { c.lock.Lock() defer c.lock.Unlock() diff --git a/libpod/container_internal.go b/libpod/container_internal.go index 5f8dd1c72..c0b5e4302 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -21,6 +21,7 @@ import ( "github.com/containers/storage/pkg/archive" "github.com/containers/storage/pkg/mount" spec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/runtime-tools/generate" "github.com/opencontainers/selinux/go-selinux/label" opentracing "github.com/opentracing/opentracing-go" "github.com/pkg/errors" @@ -1345,7 +1346,7 @@ func (c *Container) appendStringToRundir(destFile, output string) (string, error return filepath.Join(c.state.RunDir, destFile), nil } -// Save OCI spec to disk, replacing any existing specs for the container +// saveSpec saves the OCI spec to disk, replacing any existing specs for the container func (c *Container) saveSpec(spec *spec.Spec) error { // If the OCI spec already exists, we need to replace it // Cannot guarantee some things, e.g. network namespaces, have the same @@ -1501,3 +1502,40 @@ func (c *Container) checkReadyForRemoval() error { return nil } + +// writeJSONFile marshalls and writes the given data to a JSON file +// in the bundle path +func (c *Container) writeJSONFile(v interface{}, file string) (err error) { + fileJSON, err := json.MarshalIndent(v, "", " ") + if err != nil { + return errors.Wrapf(err, "error writing JSON to %s for container %s", file, c.ID()) + } + file = filepath.Join(c.bundlePath(), file) + if err := ioutil.WriteFile(file, fileJSON, 0644); err != nil { + return err + } + + return nil +} + +// prepareCheckpointExport writes the config and spec to +// JSON files for later export +func (c *Container) prepareCheckpointExport() (err error) { + // save live config + if err := c.writeJSONFile(c.Config(), "config.dump"); err != nil { + return err + } + + // save spec + jsonPath := filepath.Join(c.bundlePath(), "config.json") + g, err := generate.NewFromFile(jsonPath) + if err != nil { + logrus.Debugf("generating spec for container %q failed with %v", c.ID(), err) + return err + } + if err := c.writeJSONFile(g.Spec(), "spec.dump"); err != nil { + return err + } + + return nil +} diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index f25f76092..4acc77afa 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -5,6 +5,7 @@ package libpod import ( "context" "fmt" + "io" "io/ioutil" "net" "os" @@ -25,6 +26,7 @@ import ( "github.com/containers/libpod/pkg/lookup" "github.com/containers/libpod/pkg/resolvconf" "github.com/containers/libpod/pkg/rootless" + "github.com/containers/storage/pkg/archive" securejoin "github.com/cyphar/filepath-securejoin" "github.com/opencontainers/runc/libcontainer/user" spec "github.com/opencontainers/runtime-spec/specs-go" @@ -496,6 +498,45 @@ func (c *Container) addNamespaceContainer(g *generate.Generator, ns LinuxNS, ctr return nil } +func (c *Container) exportCheckpoint(dest string) (err error) { + if (len(c.config.NamedVolumes) > 0) || (len(c.Dependencies()) > 0) { + return errors.Errorf("Cannot export checkpoints of containers with named volumes or dependencies") + } + logrus.Debugf("Exporting checkpoint image of container %q to %q", c.ID(), dest) + input, err := archive.TarWithOptions(c.bundlePath(), &archive.TarOptions{ + Compression: archive.Gzip, + IncludeSourceDir: true, + IncludeFiles: []string{ + "checkpoint", + "artifacts", + "ctr.log", + "config.dump", + "spec.dump", + "network.status"}, + }) + + if err != nil { + return errors.Wrapf(err, "error reading checkpoint directory %q", c.ID()) + } + + outFile, err := os.Create(dest) + if err != nil { + return errors.Wrapf(err, "error creating checkpoint export file %q", dest) + } + defer outFile.Close() + + if err := os.Chmod(dest, 0600); err != nil { + return errors.Wrapf(err, "cannot chmod %q", dest) + } + + _, err = io.Copy(outFile, input) + if err != nil { + return err + } + + return nil +} + func (c *Container) checkpointRestoreSupported() (err error) { if !criu.CheckForCriu() { return errors.Errorf("Checkpoint/Restore requires at least CRIU %d", criu.MinCriuVersion) @@ -549,6 +590,12 @@ func (c *Container) checkpoint(ctx context.Context, options ContainerCheckpointO return err } + if options.TargetFile != "" { + if err = c.exportCheckpoint(options.TargetFile); err != nil { + return err + } + } + logrus.Debugf("Checkpointed container %s", c.ID()) if !options.KeepRunning { @@ -561,15 +608,50 @@ func (c *Container) checkpoint(ctx context.Context, options ContainerCheckpointO } if !options.Keep { - // Remove log file - os.Remove(filepath.Join(c.bundlePath(), "dump.log")) - // Remove statistic file - os.Remove(filepath.Join(c.bundlePath(), "stats-dump")) + cleanup := []string{ + "dump.log", + "stats-dump", + "config.dump", + "spec.dump", + } + for _, delete := range cleanup { + file := filepath.Join(c.bundlePath(), delete) + os.Remove(file) + } } return c.save() } +func (c *Container) importCheckpoint(input string) (err error) { + archiveFile, err := os.Open(input) + if err != nil { + return errors.Wrapf(err, "Failed to open checkpoint archive %s for import", input) + } + + defer archiveFile.Close() + options := &archive.TarOptions{ + ExcludePatterns: []string{ + // config.dump and spec.dump are only required + // container creation + "config.dump", + "spec.dump", + }, + } + err = archive.Untar(archiveFile, c.bundlePath(), options) + if err != nil { + return errors.Wrapf(err, "Unpacking of checkpoint archive %s failed", input) + } + + // Make sure the newly created config.json exists on disk + g := generate.NewFromSpec(c.config.Spec) + if err = c.saveSpec(g.Spec()); err != nil { + return errors.Wrap(err, "Saving imported container specification for restore failed") + } + + return nil +} + func (c *Container) restore(ctx context.Context, options ContainerCheckpointOptions) (err error) { if err := c.checkpointRestoreSupported(); err != nil { @@ -580,6 +662,12 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti return errors.Wrapf(ErrCtrStateInvalid, "container %s is running or paused, cannot restore", c.ID()) } + if options.TargetFile != "" { + if err = c.importCheckpoint(options.TargetFile); err != nil { + return err + } + } + // Let's try to stat() CRIU's inventory file. If it does not exist, it makes // no sense to try a restore. This is a minimal check if a checkpoint exist. if _, err := os.Stat(filepath.Join(c.CheckpointPath(), "inventory.img")); os.IsNotExist(err) { @@ -593,7 +681,13 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti // Read network configuration from checkpoint // Currently only one interface with one IP is supported. networkStatusFile, err := os.Open(filepath.Join(c.bundlePath(), "network.status")) - if err == nil { + // If the restored container should get a new name, the IP address of + // the container will not be restored. This assumes that if a new name is + // specified, the container is restored multiple times. + // TODO: This implicit restoring with or without IP depending on an + // unrelated restore parameter (--name) does not seem like the + // best solution. + if err == nil && options.Name == "" { // The file with the network.status does exist. Let's restore the // container with the same IP address as during checkpointing. defer networkStatusFile.Close() @@ -637,23 +731,44 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti return err } + // Restoring from an import means that we are doing migration + if options.TargetFile != "" { + g.SetRootPath(c.state.Mountpoint) + } + // We want to have the same network namespace as before. if c.config.CreateNetNS { g.AddOrReplaceLinuxNamespace(spec.NetworkNamespace, c.state.NetNS.Path()) } - // Save the OCI spec to disk - if err := c.saveSpec(g.Spec()); err != nil { + if err := c.makeBindMounts(); err != nil { return err } - if err := c.makeBindMounts(); err != nil { - return err + if options.TargetFile != "" { + for dstPath, srcPath := range c.state.BindMounts { + newMount := spec.Mount{ + Type: "bind", + Source: srcPath, + Destination: dstPath, + Options: []string{"bind", "private"}, + } + if c.IsReadOnly() && dstPath != "/dev/shm" { + newMount.Options = append(newMount.Options, "ro", "nosuid", "noexec", "nodev") + } + if !MountExists(g.Mounts(), dstPath) { + g.AddMount(newMount) + } + } } // Cleanup for a working restore. c.removeConmonFiles() + // Save the OCI spec to disk + if err := c.saveSpec(g.Spec()); err != nil { + return err + } if err := c.runtime.ociRuntime.createContainer(c, c.config.CgroupParent, &options); err != nil { return err } diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go index 0c8d3edab..cf1f5701d 100644 --- a/libpod/runtime_ctr.go +++ b/libpod/runtime_ctr.go @@ -14,6 +14,7 @@ import ( "github.com/containers/storage" "github.com/containers/storage/pkg/stringid" spec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/runtime-tools/generate" opentracing "github.com/opentracing/opentracing-go" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -34,7 +35,7 @@ type CtrCreateOption func(*Container) error // A true return will include the container, a false return will exclude it. type ContainerFilter func(*Container) bool -// NewContainer creates a new container from a given OCI config +// NewContainer creates a new container from a given OCI config. func (r *Runtime) NewContainer(ctx context.Context, rSpec *spec.Spec, options ...CtrCreateOption) (c *Container, err error) { r.lock.Lock() defer r.lock.Unlock() @@ -44,20 +45,46 @@ func (r *Runtime) NewContainer(ctx context.Context, rSpec *spec.Spec, options .. return r.newContainer(ctx, rSpec, options...) } -func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options ...CtrCreateOption) (c *Container, err error) { - span, _ := opentracing.StartSpanFromContext(ctx, "newContainer") - span.SetTag("type", "runtime") - defer span.Finish() +// RestoreContainer re-creates a container from an imported checkpoint +func (r *Runtime) RestoreContainer(ctx context.Context, rSpec *spec.Spec, config *ContainerConfig) (c *Container, err error) { + r.lock.Lock() + defer r.lock.Unlock() + if !r.valid { + return nil, ErrRuntimeStopped + } + ctr, err := r.initContainerVariables(rSpec, config) + if err != nil { + return nil, errors.Wrapf(err, "error initializing container variables") + } + return r.setupContainer(ctx, ctr, true) +} + +func (r *Runtime) initContainerVariables(rSpec *spec.Spec, config *ContainerConfig) (c *Container, err error) { if rSpec == nil { return nil, errors.Wrapf(ErrInvalidArg, "must provide a valid runtime spec to create container") } - ctr := new(Container) ctr.config = new(ContainerConfig) ctr.state = new(ContainerState) - ctr.config.ID = stringid.GenerateNonCryptoID() + if config == nil { + ctr.config.ID = stringid.GenerateNonCryptoID() + ctr.config.ShmSize = DefaultShmSize + } else { + // This is a restore from an imported checkpoint + if err := JSONDeepCopy(config, ctr.config); err != nil { + return nil, errors.Wrapf(err, "error copying container config for restore") + } + // If the ID is empty a new name for the restored container was requested + if ctr.config.ID == "" { + ctr.config.ID = stringid.GenerateNonCryptoID() + // Fixup ExitCommand with new ID + ctr.config.ExitCommand[len(ctr.config.ExitCommand)-1] = ctr.config.ID + } + // Reset the log path to point to the default + ctr.config.LogPath = "" + } ctr.config.Spec = new(spec.Spec) if err := JSONDeepCopy(rSpec, ctr.config.Spec); err != nil { @@ -65,8 +92,6 @@ func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options .. } ctr.config.CreatedTime = time.Now() - ctr.config.ShmSize = DefaultShmSize - ctr.state.BindMounts = make(map[string]string) ctr.config.StopTimeout = CtrRemoveTimeout @@ -80,12 +105,29 @@ func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options .. } ctr.runtime = r + + return ctr, nil +} + +func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options ...CtrCreateOption) (c *Container, err error) { + span, _ := opentracing.StartSpanFromContext(ctx, "newContainer") + span.SetTag("type", "runtime") + defer span.Finish() + + ctr, err := r.initContainerVariables(rSpec, nil) + if err != nil { + return nil, errors.Wrapf(err, "error initializing container variables") + } + for _, option := range options { if err := option(ctr); err != nil { return nil, errors.Wrapf(err, "error running container create option") } } + return r.setupContainer(ctx, ctr, false) +} +func (r *Runtime) setupContainer(ctx context.Context, ctr *Container, restore bool) (c *Container, err error) { // Allocate a lock for the container lock, err := r.lockManager.AllocateLock() if err != nil { @@ -154,6 +196,19 @@ func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options .. return nil, errors.Wrapf(ErrInvalidArg, "unsupported CGroup manager: %s - cannot validate cgroup parent", r.config.CgroupManager) } + if restore { + // Remove information about bind mount + // for new container from imported checkpoint + g := generate.Generator{Config: ctr.config.Spec} + g.RemoveMount("/dev/shm") + ctr.config.ShmDir = "" + g.RemoveMount("/etc/resolv.conf") + g.RemoveMount("/etc/hostname") + g.RemoveMount("/etc/hosts") + g.RemoveMount("/run/.containerenv") + g.RemoveMount("/run/secrets") + } + // Set up storage for the container if err := ctr.setupStorage(ctx); err != nil { return nil, err diff --git a/pkg/adapter/checkpoint_restore.go b/pkg/adapter/checkpoint_restore.go new file mode 100644 index 000000000..97ba5ecf7 --- /dev/null +++ b/pkg/adapter/checkpoint_restore.go @@ -0,0 +1,145 @@ +// +build !remoteclient + +package adapter + +import ( + "context" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/image" + "github.com/containers/storage/pkg/archive" + jsoniter "github.com/json-iterator/go" + spec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" + "io" + "io/ioutil" + "os" + "path/filepath" +) + +// Prefixing the checkpoint/restore related functions with 'cr' + +// crImportFromJSON imports the JSON files stored in the exported +// checkpoint tarball +func crImportFromJSON(filePath string, v interface{}) error { + jsonFile, err := os.Open(filePath) + if err != nil { + return errors.Wrapf(err, "Failed to open container definition %s for restore", filePath) + } + defer jsonFile.Close() + + content, err := ioutil.ReadAll(jsonFile) + if err != nil { + return errors.Wrapf(err, "Failed to read container definition %s for restore", filePath) + } + json := jsoniter.ConfigCompatibleWithStandardLibrary + if err = json.Unmarshal([]byte(content), v); err != nil { + return errors.Wrapf(err, "Failed to unmarshal container definition %s for restore", filePath) + } + + return nil +} + +// crImportCheckpoint it the function which imports the information +// from checkpoint tarball and re-creates the container from that information +func crImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, input string, name string) ([]*libpod.Container, error) { + // First get the container definition from the + // tarball to a temporary directory + archiveFile, err := os.Open(input) + if err != nil { + return nil, errors.Wrapf(err, "Failed to open checkpoint archive %s for import", input) + } + defer archiveFile.Close() + options := &archive.TarOptions{ + // Here we only need the files config.dump and spec.dump + ExcludePatterns: []string{ + "checkpoint", + "artifacts", + "ctr.log", + "network.status", + }, + } + dir, err := ioutil.TempDir("", "checkpoint") + if err != nil { + return nil, err + } + defer os.RemoveAll(dir) + err = archive.Untar(archiveFile, dir, options) + if err != nil { + return nil, errors.Wrapf(err, "Unpacking of checkpoint archive %s failed", input) + } + + // Load spec.dump from temporary directory + spec := new(spec.Spec) + if err := crImportFromJSON(filepath.Join(dir, "spec.dump"), spec); err != nil { + return nil, err + } + + // Load config.dump from temporary directory + config := new(libpod.ContainerConfig) + if err = crImportFromJSON(filepath.Join(dir, "config.dump"), config); err != nil { + return nil, err + } + + // This should not happen as checkpoints with these options are not exported. + if (len(config.Dependencies) > 0) || (len(config.NamedVolumes) > 0) { + return nil, errors.Errorf("Cannot import checkpoints of containers with named volumes or dependencies") + } + + ctrID := config.ID + newName := false + + // Check if the restored container gets a new name + if name != "" { + config.ID = "" + config.Name = name + newName = true + } + + ctrName := config.Name + + // The code to load the images is copied from create.go + var writer io.Writer + // In create.go this only set if '--quiet' does not exist. + writer = os.Stderr + rtc, err := runtime.GetConfig() + if err != nil { + return nil, err + } + + _, err = runtime.ImageRuntime().New(ctx, config.RootfsImageName, rtc.SignaturePolicyPath, "", writer, nil, image.SigningOptions{}, false, nil) + if err != nil { + return nil, err + } + + // Now create a new container from the just loaded information + container, err := runtime.RestoreContainer(ctx, spec, config) + if err != nil { + return nil, err + } + + var containers []*libpod.Container + if container == nil { + return nil, nil + } + + containerConfig := container.Config() + if containerConfig.Name != ctrName { + return nil, errors.Errorf("Name of restored container (%s) does not match requested name (%s)", containerConfig.Name, ctrName) + } + + if newName == false { + // Only check ID for a restore with the same name. + // Using -n to request a new name for the restored container, will also create a new ID + if containerConfig.ID != ctrID { + return nil, errors.Errorf("ID of restored container (%s) does not match requested ID (%s)", containerConfig.ID, ctrID) + } + } + + // Check if the ExitCommand points to the correct container ID + if containerConfig.ExitCommand[len(containerConfig.ExitCommand)-1] != containerConfig.ID { + return nil, errors.Errorf("'ExitCommandID' uses ID %s instead of container ID %s", containerConfig.ExitCommand[len(containerConfig.ExitCommand)-1], containerConfig.ID) + } + + containers = append(containers, container) + return containers, nil +} diff --git a/pkg/adapter/containers.go b/pkg/adapter/containers.go index 34ee70d3d..29297fbd5 100644 --- a/pkg/adapter/containers.go +++ b/pkg/adapter/containers.go @@ -526,7 +526,7 @@ func (r *LocalRuntime) Checkpoint(c *cliconfig.CheckpointValues, options libpod. } // Restore one or more containers -func (r *LocalRuntime) Restore(c *cliconfig.RestoreValues, options libpod.ContainerCheckpointOptions) error { +func (r *LocalRuntime) Restore(ctx context.Context, c *cliconfig.RestoreValues, options libpod.ContainerCheckpointOptions) error { var ( containers []*libpod.Container err, lastError error @@ -538,7 +538,9 @@ func (r *LocalRuntime) Restore(c *cliconfig.RestoreValues, options libpod.Contai return state == libpod.ContainerStateExited }) - if c.All { + if c.Import != "" { + containers, err = crImportCheckpoint(ctx, r.Runtime, c.Import, c.Name) + } else if c.All { containers, err = r.GetContainers(filterFuncs...) } else { containers, err = shortcuts.GetContainersByContext(false, c.Latest, c.InputArgs, r.Runtime) diff --git a/pkg/adapter/containers_remote.go b/pkg/adapter/containers_remote.go index bc6a9cfcd..776fcbb70 100644 --- a/pkg/adapter/containers_remote.go +++ b/pkg/adapter/containers_remote.go @@ -664,6 +664,10 @@ func (r *LocalRuntime) Attach(ctx context.Context, c *cliconfig.AttachValues) er // Checkpoint one or more containers func (r *LocalRuntime) Checkpoint(c *cliconfig.CheckpointValues, options libpod.ContainerCheckpointOptions) error { + if c.Export != "" { + return errors.New("the remote client does not support exporting checkpoints") + } + var lastError error ids, err := iopodman.GetContainersByContext().Call(r.Conn, c.All, c.Latest, c.InputArgs) if err != nil { @@ -699,7 +703,11 @@ func (r *LocalRuntime) Checkpoint(c *cliconfig.CheckpointValues, options libpod. } // Restore one or more containers -func (r *LocalRuntime) Restore(c *cliconfig.RestoreValues, options libpod.ContainerCheckpointOptions) error { +func (r *LocalRuntime) Restore(ctx context.Context, c *cliconfig.RestoreValues, options libpod.ContainerCheckpointOptions) error { + if c.Import != "" { + return errors.New("the remote client does not support importing checkpoints") + } + var lastError error ids, err := iopodman.GetContainersByContext().Call(r.Conn, c.All, c.Latest, c.InputArgs) if err != nil { diff --git a/pkg/rootless/rootless_linux.go b/pkg/rootless/rootless_linux.go index d302b1777..0cac50fc0 100644 --- a/pkg/rootless/rootless_linux.go +++ b/pkg/rootless/rootless_linux.go @@ -169,6 +169,9 @@ func getUserNSFirstChild(fd uintptr) (*os.File, error) { for { nextFd, err := getParentUserNs(fd) if err != nil { + if err == syscall.ENOTTY { + return os.NewFile(fd, "userns child"), nil + } return nil, errors.Wrapf(err, "cannot get parent user namespace") } diff --git a/test/e2e/checkpoint_test.go b/test/e2e/checkpoint_test.go index 95ec21433..d452a062b 100644 --- a/test/e2e/checkpoint_test.go +++ b/test/e2e/checkpoint_test.go @@ -347,4 +347,49 @@ var _ = Describe("Podman checkpoint", func() { Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) }) + // This test does the same steps which are necessary for migrating + // a container from one host to another + It("podman checkpoint container with export (migration)", func() { + // CRIU does not work with seccomp correctly on RHEL7 + session := podmanTest.Podman([]string{"run", "-it", "--security-opt", "seccomp=unconfined", "-d", ALPINE, "top"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(1)) + + result := podmanTest.Podman([]string{"container", "checkpoint", "-l", "-e", "/tmp/checkpoint.tar.gz"}) + result.WaitWithDefaultTimeout() + + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) + Expect(podmanTest.GetContainerStatus()).To(ContainSubstring("Exited")) + + // Remove all containers to simulate migration + result = podmanTest.Podman([]string{"rm", "-fa"}) + result.WaitWithDefaultTimeout() + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) + + result = podmanTest.Podman([]string{"container", "restore", "-i", "/tmp/checkpoint.tar.gz"}) + result.WaitWithDefaultTimeout() + + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(1)) + Expect(podmanTest.GetContainerStatus()).To(ContainSubstring("Up")) + + // Restore container a second time with different name + result = podmanTest.Podman([]string{"container", "restore", "-i", "/tmp/checkpoint.tar.gz", "-n", "restore_again"}) + result.WaitWithDefaultTimeout() + + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(2)) + Expect(podmanTest.GetContainerStatus()).To(ContainSubstring("Up")) + + result = podmanTest.Podman([]string{"rm", "-fa"}) + result.WaitWithDefaultTimeout() + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) + + // Remove exported checkpoint + os.Remove("/tmp/checkpoint.tar.gz") + }) }) diff --git a/vendor.conf b/vendor.conf index cfd410889..a33a52786 100644 --- a/vendor.conf +++ b/vendor.conf @@ -19,7 +19,7 @@ github.com/containers/image 2c0349c99af7d90694b3faa0e9bde404d407b145 github.com/vbauerster/mpb v3.3.4 github.com/mattn/go-isatty v0.0.4 github.com/VividCortex/ewma v1.1.1 -github.com/containers/storage v1.12.7 +github.com/containers/storage 9b10041d7b2ef767ce9c42b5862b6c51eeb82214 github.com/containers/psgo v1.3.0 github.com/coreos/go-systemd v17 github.com/coreos/pkg v4 diff --git a/vendor/github.com/containers/storage/containers.go b/vendor/github.com/containers/storage/containers.go index bbac78b60..e69552361 100644 --- a/vendor/github.com/containers/storage/containers.go +++ b/vendor/github.com/containers/storage/containers.go @@ -572,6 +572,10 @@ func (r *containerStore) Lock() { r.lockfile.Lock() } +func (r *containerStore) RecursiveLock() { + r.lockfile.RecursiveLock() +} + func (r *containerStore) RLock() { r.lockfile.RLock() } diff --git a/vendor/github.com/containers/storage/drivers/aufs/aufs.go b/vendor/github.com/containers/storage/drivers/aufs/aufs.go index e821bc0c5..353d1707a 100644 --- a/vendor/github.com/containers/storage/drivers/aufs/aufs.go +++ b/vendor/github.com/containers/storage/drivers/aufs/aufs.go @@ -255,6 +255,9 @@ func (a *Driver) AdditionalImageStores() []string { // CreateFromTemplate creates a layer with the same contents and parent as another layer. func (a *Driver) CreateFromTemplate(id, template string, templateIDMappings *idtools.IDMappings, parent string, parentIDMappings *idtools.IDMappings, opts *graphdriver.CreateOpts, readWrite bool) error { + if opts == nil { + opts = &graphdriver.CreateOpts{} + } return graphdriver.NaiveCreateFromTemplate(a, id, template, templateIDMappings, parent, parentIDMappings, opts, readWrite) } diff --git a/vendor/github.com/containers/storage/drivers/chown.go b/vendor/github.com/containers/storage/drivers/chown.go index 4d4011ee0..f2f1ec386 100644 --- a/vendor/github.com/containers/storage/drivers/chown.go +++ b/vendor/github.com/containers/storage/drivers/chown.go @@ -55,6 +55,9 @@ func chownByMapsMain() { if err != nil { return fmt.Errorf("error walking to %q: %v", path, err) } + if path == "." { + return nil + } return platformLChown(path, info, toHost, toContainer) } if err := filepath.Walk(".", chown); err != nil { diff --git a/vendor/github.com/containers/storage/images.go b/vendor/github.com/containers/storage/images.go index 38b5a3ef3..6f487504a 100644 --- a/vendor/github.com/containers/storage/images.go +++ b/vendor/github.com/containers/storage/images.go @@ -82,6 +82,9 @@ type Image struct { // is set before using it. Created time.Time `json:"created,omitempty"` + // ReadOnly is true if this image resides in a read-only layer store. + ReadOnly bool `json:"-"` + Flags map[string]interface{} `json:"flags,omitempty"` } @@ -159,6 +162,7 @@ func copyImage(i *Image) *Image { BigDataSizes: copyStringInt64Map(i.BigDataSizes), BigDataDigests: copyStringDigestMap(i.BigDataDigests), Created: i.Created, + ReadOnly: i.ReadOnly, Flags: copyStringInterfaceMap(i.Flags), } } @@ -269,6 +273,7 @@ func (r *imageStore) Load() error { list := digests[digest] digests[digest] = append(list, image) } + image.ReadOnly = !r.IsReadWrite() } } if shouldSave && (!r.IsReadWrite() || !r.Locked()) { @@ -739,6 +744,10 @@ func (r *imageStore) Lock() { r.lockfile.Lock() } +func (r *imageStore) RecursiveLock() { + r.lockfile.RecursiveLock() +} + func (r *imageStore) RLock() { r.lockfile.RLock() } diff --git a/vendor/github.com/containers/storage/layers.go b/vendor/github.com/containers/storage/layers.go index a35dd476b..fb79238cd 100644 --- a/vendor/github.com/containers/storage/layers.go +++ b/vendor/github.com/containers/storage/layers.go @@ -103,6 +103,9 @@ type Layer struct { // for use inside of a user namespace where UID mapping is being used. UIDMap []idtools.IDMap `json:"uidmap,omitempty"` GIDMap []idtools.IDMap `json:"gidmap,omitempty"` + + // ReadOnly is true if this layer resides in a read-only layer store. + ReadOnly bool `json:"-"` } type layerMountPoint struct { @@ -259,6 +262,7 @@ func copyLayer(l *Layer) *Layer { UncompressedDigest: l.UncompressedDigest, UncompressedSize: l.UncompressedSize, CompressionType: l.CompressionType, + ReadOnly: l.ReadOnly, Flags: copyStringInterfaceMap(l.Flags), UIDMap: copyIDMap(l.UIDMap), GIDMap: copyIDMap(l.GIDMap), @@ -318,6 +322,7 @@ func (r *layerStore) Load() error { if layer.MountLabel != "" { label.ReserveLabel(layer.MountLabel) } + layer.ReadOnly = !r.IsReadWrite() } err = nil } @@ -1304,6 +1309,10 @@ func (r *layerStore) Lock() { r.lockfile.Lock() } +func (r *layerStore) RecursiveLock() { + r.lockfile.RecursiveLock() +} + func (r *layerStore) RLock() { r.lockfile.RLock() } diff --git a/vendor/github.com/containers/storage/layers_ffjson.go b/vendor/github.com/containers/storage/layers_ffjson.go index 09b5d0f33..125b5d8c9 100644 --- a/vendor/github.com/containers/storage/layers_ffjson.go +++ b/vendor/github.com/containers/storage/layers_ffjson.go @@ -1,5 +1,5 @@ // Code generated by ffjson <https://github.com/pquerna/ffjson>. DO NOT EDIT. -// source: ./layers.go +// source: layers.go package storage diff --git a/vendor/github.com/containers/storage/lockfile.go b/vendor/github.com/containers/storage/lockfile.go index ed8753337..c4f1b5549 100644 --- a/vendor/github.com/containers/storage/lockfile.go +++ b/vendor/github.com/containers/storage/lockfile.go @@ -15,6 +15,10 @@ type Locker interface { // Acquire a writer lock. Lock() + // Acquire a writer lock recursively, allowing for recursive acquisitions + // within the same process space. + RecursiveLock() + // Unlock the lock. Unlock() diff --git a/vendor/github.com/containers/storage/lockfile_linux.go b/vendor/github.com/containers/storage/lockfile_linux.go deleted file mode 100644 index 903387c66..000000000 --- a/vendor/github.com/containers/storage/lockfile_linux.go +++ /dev/null @@ -1,20 +0,0 @@ -// +build linux solaris - -package storage - -import ( - "time" - - "golang.org/x/sys/unix" -) - -// TouchedSince indicates if the lock file has been touched since the specified time -func (l *lockfile) TouchedSince(when time.Time) bool { - st := unix.Stat_t{} - err := unix.Fstat(int(l.fd), &st) - if err != nil { - return true - } - touched := time.Unix(st.Mtim.Unix()) - return when.Before(touched) -} diff --git a/vendor/github.com/containers/storage/lockfile_otherunix.go b/vendor/github.com/containers/storage/lockfile_otherunix.go deleted file mode 100644 index 041d54c05..000000000 --- a/vendor/github.com/containers/storage/lockfile_otherunix.go +++ /dev/null @@ -1,19 +0,0 @@ -// +build darwin freebsd - -package storage - -import ( - "time" - - "golang.org/x/sys/unix" -) - -func (l *lockfile) TouchedSince(when time.Time) bool { - st := unix.Stat_t{} - err := unix.Fstat(int(l.fd), &st) - if err != nil { - return true - } - touched := time.Unix(st.Mtimespec.Unix()) - return when.Before(touched) -} diff --git a/vendor/github.com/containers/storage/lockfile_unix.go b/vendor/github.com/containers/storage/lockfile_unix.go index 8e0f22cb5..00215e928 100644 --- a/vendor/github.com/containers/storage/lockfile_unix.go +++ b/vendor/github.com/containers/storage/lockfile_unix.go @@ -9,6 +9,7 @@ import ( "time" "github.com/containers/storage/pkg/stringid" + "github.com/containers/storage/pkg/system" "github.com/pkg/errors" "golang.org/x/sys/unix" ) @@ -25,6 +26,7 @@ type lockfile struct { locktype int16 locked bool ro bool + recursive bool } // openLock opens the file at path and returns the corresponding file @@ -75,7 +77,7 @@ func createLockerForPath(path string, ro bool) (Locker, error) { // lock locks the lockfile via FCTNL(2) based on the specified type and // command. -func (l *lockfile) lock(l_type int16) { +func (l *lockfile) lock(l_type int16, recursive bool) { lk := unix.Flock_t{ Type: l_type, Whence: int16(os.SEEK_SET), @@ -86,7 +88,13 @@ func (l *lockfile) lock(l_type int16) { case unix.F_RDLCK: l.rwMutex.RLock() case unix.F_WRLCK: - l.rwMutex.Lock() + if recursive { + // NOTE: that's okay as recursive is only set in RecursiveLock(), so + // there's no need to protect against hypothetical RDLCK cases. + l.rwMutex.RLock() + } else { + l.rwMutex.Lock() + } default: panic(fmt.Sprintf("attempted to acquire a file lock of unrecognized type %d", l_type)) } @@ -110,6 +118,7 @@ func (l *lockfile) lock(l_type int16) { } l.locktype = l_type l.locked = true + l.recursive = recursive l.counter++ } @@ -119,13 +128,24 @@ func (l *lockfile) Lock() { if l.ro { l.RLock() } else { - l.lock(unix.F_WRLCK) + l.lock(unix.F_WRLCK, false) + } +} + +// RecursiveLock locks the lockfile as a writer but allows for recursive +// acquisitions within the same process space. Note that RLock() will be called +// if it's a lockTypReader lock. +func (l *lockfile) RecursiveLock() { + if l.ro { + l.RLock() + } else { + l.lock(unix.F_WRLCK, true) } } // LockRead locks the lockfile as a reader. func (l *lockfile) RLock() { - l.lock(unix.F_RDLCK) + l.lock(unix.F_RDLCK, false) } // Unlock unlocks the lockfile. @@ -161,7 +181,7 @@ func (l *lockfile) Unlock() { // Close the file descriptor on the last unlock. unix.Close(int(l.fd)) } - if l.locktype == unix.F_RDLCK { + if l.locktype == unix.F_RDLCK || l.recursive { l.rwMutex.RUnlock() } else { l.rwMutex.Unlock() @@ -232,3 +252,14 @@ func (l *lockfile) Modified() (bool, error) { func (l *lockfile) IsReadWrite() bool { return !l.ro } + +// TouchedSince indicates if the lock file has been touched since the specified time +func (l *lockfile) TouchedSince(when time.Time) bool { + st, err := system.Fstat(int(l.fd)) + if err != nil { + return true + } + mtim := st.Mtim() + touched := time.Unix(mtim.Unix()) + return when.Before(touched) +} diff --git a/vendor/github.com/containers/storage/lockfile_windows.go b/vendor/github.com/containers/storage/lockfile_windows.go index c02069495..caf7c184a 100644 --- a/vendor/github.com/containers/storage/lockfile_windows.go +++ b/vendor/github.com/containers/storage/lockfile_windows.go @@ -36,6 +36,12 @@ func (l *lockfile) Lock() { l.locked = true } +func (l *lockfile) RecursiveLock() { + // We don't support Windows but a recursive writer-lock in one process-space + // is really a writer lock, so just panic. + panic("not supported") +} + func (l *lockfile) RLock() { l.mu.Lock() l.locked = true diff --git a/vendor/github.com/containers/storage/pkg/chrootarchive/archive.go b/vendor/github.com/containers/storage/pkg/chrootarchive/archive.go index a36ff1cb1..33ba6a128 100644 --- a/vendor/github.com/containers/storage/pkg/chrootarchive/archive.go +++ b/vendor/github.com/containers/storage/pkg/chrootarchive/archive.go @@ -1,7 +1,7 @@ package chrootarchive import ( - "archive/tar" + stdtar "archive/tar" "fmt" "io" "io/ioutil" @@ -34,18 +34,34 @@ func NewArchiverWithChown(tarIDMappings *idtools.IDMappings, chownOpts *idtools. // The archive may be compressed with one of the following algorithms: // identity (uncompressed), gzip, bzip2, xz. func Untar(tarArchive io.Reader, dest string, options *archive.TarOptions) error { - return untarHandler(tarArchive, dest, options, true) + return untarHandler(tarArchive, dest, options, true, dest) +} + +// UntarWithRoot is the same as `Untar`, but allows you to pass in a root directory +// The root directory is the directory that will be chrooted to. +// `dest` must be a path within `root`, if it is not an error will be returned. +// +// `root` should set to a directory which is not controlled by any potentially +// malicious process. +// +// This should be used to prevent a potential attacker from manipulating `dest` +// such that it would provide access to files outside of `dest` through things +// like symlinks. Normally `ResolveSymlinksInScope` would handle this, however +// sanitizing symlinks in this manner is inherrently racey: +// ref: CVE-2018-15664 +func UntarWithRoot(tarArchive io.Reader, dest string, options *archive.TarOptions, root string) error { + return untarHandler(tarArchive, dest, options, true, root) } // UntarUncompressed reads a stream of bytes from `archive`, parses it as a tar archive, // and unpacks it into the directory at `dest`. // The archive must be an uncompressed stream. func UntarUncompressed(tarArchive io.Reader, dest string, options *archive.TarOptions) error { - return untarHandler(tarArchive, dest, options, false) + return untarHandler(tarArchive, dest, options, false, dest) } // Handler for teasing out the automatic decompression -func untarHandler(tarArchive io.Reader, dest string, options *archive.TarOptions, decompress bool) error { +func untarHandler(tarArchive io.Reader, dest string, options *archive.TarOptions, decompress bool, root string) error { if tarArchive == nil { return fmt.Errorf("Empty archive") } @@ -77,7 +93,15 @@ func untarHandler(tarArchive io.Reader, dest string, options *archive.TarOptions r = decompressedArchive } - return invokeUnpack(r, dest, options) + return invokeUnpack(r, dest, options, root) +} + +// Tar tars the requested path while chrooted to the specified root. +func Tar(srcPath string, options *archive.TarOptions, root string) (io.ReadCloser, error) { + if options == nil { + options = &archive.TarOptions{} + } + return invokePack(srcPath, options, root) } // CopyFileWithTarAndChown returns a function which copies a single file from outside @@ -99,7 +123,7 @@ func CopyFileWithTarAndChown(chownOpts *idtools.IDPair, hasher io.Writer, uidmap var hashWorker sync.WaitGroup hashWorker.Add(1) go func() { - t := tar.NewReader(contentReader) + t := stdtar.NewReader(contentReader) _, err := t.Next() if err != nil { hashError = err diff --git a/vendor/github.com/containers/storage/pkg/chrootarchive/archive_unix.go b/vendor/github.com/containers/storage/pkg/chrootarchive/archive_unix.go index e04ed787c..ca9fb10d7 100644 --- a/vendor/github.com/containers/storage/pkg/chrootarchive/archive_unix.go +++ b/vendor/github.com/containers/storage/pkg/chrootarchive/archive_unix.go @@ -10,10 +10,13 @@ import ( "io" "io/ioutil" "os" + "path/filepath" "runtime" + "strings" "github.com/containers/storage/pkg/archive" "github.com/containers/storage/pkg/reexec" + "github.com/pkg/errors" ) // untar is the entry-point for storage-untar on re-exec. This is not used on @@ -23,18 +26,28 @@ func untar() { runtime.LockOSThread() flag.Parse() - var options *archive.TarOptions + var options archive.TarOptions //read the options from the pipe "ExtraFiles" if err := json.NewDecoder(os.NewFile(3, "options")).Decode(&options); err != nil { fatal(err) } - if err := chroot(flag.Arg(0)); err != nil { + dst := flag.Arg(0) + var root string + if len(flag.Args()) > 1 { + root = flag.Arg(1) + } + + if root == "" { + root = dst + } + + if err := chroot(root); err != nil { fatal(err) } - if err := archive.Unpack(os.Stdin, "/", options); err != nil { + if err := archive.Unpack(os.Stdin, dst, &options); err != nil { fatal(err) } // fully consume stdin in case it is zero padded @@ -45,7 +58,10 @@ func untar() { os.Exit(0) } -func invokeUnpack(decompressedArchive io.Reader, dest string, options *archive.TarOptions) error { +func invokeUnpack(decompressedArchive io.Reader, dest string, options *archive.TarOptions, root string) error { + if root == "" { + return errors.New("must specify a root to chroot to") + } // We can't pass a potentially large exclude list directly via cmd line // because we easily overrun the kernel's max argument/environment size @@ -57,7 +73,21 @@ func invokeUnpack(decompressedArchive io.Reader, dest string, options *archive.T return fmt.Errorf("Untar pipe failure: %v", err) } - cmd := reexec.Command("storage-untar", dest) + if root != "" { + relDest, err := filepath.Rel(root, dest) + if err != nil { + return err + } + if relDest == "." { + relDest = "/" + } + if relDest[0] != '/' { + relDest = "/" + relDest + } + dest = relDest + } + + cmd := reexec.Command("storage-untar", dest, root) cmd.Stdin = decompressedArchive cmd.ExtraFiles = append(cmd.ExtraFiles, r) @@ -68,6 +98,7 @@ func invokeUnpack(decompressedArchive io.Reader, dest string, options *archive.T if err := cmd.Start(); err != nil { return fmt.Errorf("Untar error on re-exec cmd: %v", err) } + //write the options to the pipe for the untar exec to read if err := json.NewEncoder(w).Encode(options); err != nil { return fmt.Errorf("Untar json encode to pipe failed: %v", err) @@ -84,3 +115,92 @@ func invokeUnpack(decompressedArchive io.Reader, dest string, options *archive.T } return nil } + +func tar() { + runtime.LockOSThread() + flag.Parse() + + src := flag.Arg(0) + var root string + if len(flag.Args()) > 1 { + root = flag.Arg(1) + } + + if root == "" { + root = src + } + + if err := realChroot(root); err != nil { + fatal(err) + } + + var options archive.TarOptions + if err := json.NewDecoder(os.Stdin).Decode(&options); err != nil { + fatal(err) + } + + rdr, err := archive.TarWithOptions(src, &options) + if err != nil { + fatal(err) + } + defer rdr.Close() + + if _, err := io.Copy(os.Stdout, rdr); err != nil { + fatal(err) + } + + os.Exit(0) +} + +func invokePack(srcPath string, options *archive.TarOptions, root string) (io.ReadCloser, error) { + if root == "" { + return nil, errors.New("root path must not be empty") + } + + relSrc, err := filepath.Rel(root, srcPath) + if err != nil { + return nil, err + } + if relSrc == "." { + relSrc = "/" + } + if relSrc[0] != '/' { + relSrc = "/" + relSrc + } + + // make sure we didn't trim a trailing slash with the call to `Rel` + if strings.HasSuffix(srcPath, "/") && !strings.HasSuffix(relSrc, "/") { + relSrc += "/" + } + + cmd := reexec.Command("storage-tar", relSrc, root) + + errBuff := bytes.NewBuffer(nil) + cmd.Stderr = errBuff + + tarR, tarW := io.Pipe() + cmd.Stdout = tarW + + stdin, err := cmd.StdinPipe() + if err != nil { + return nil, errors.Wrap(err, "error getting options pipe for tar process") + } + + if err := cmd.Start(); err != nil { + return nil, errors.Wrap(err, "tar error on re-exec cmd") + } + + go func() { + err := cmd.Wait() + err = errors.Wrapf(err, "error processing tar file: %s", errBuff) + tarW.CloseWithError(err) + }() + + if err := json.NewEncoder(stdin).Encode(options); err != nil { + stdin.Close() + return nil, errors.Wrap(err, "tar json encode to pipe failed") + } + stdin.Close() + + return tarR, nil +} diff --git a/vendor/github.com/containers/storage/pkg/chrootarchive/archive_windows.go b/vendor/github.com/containers/storage/pkg/chrootarchive/archive_windows.go index 93fde4220..8a5c680b1 100644 --- a/vendor/github.com/containers/storage/pkg/chrootarchive/archive_windows.go +++ b/vendor/github.com/containers/storage/pkg/chrootarchive/archive_windows.go @@ -14,9 +14,16 @@ func chroot(path string) error { func invokeUnpack(decompressedArchive io.ReadCloser, dest string, - options *archive.TarOptions) error { + options *archive.TarOptions, root string) error { // Windows is different to Linux here because Windows does not support // chroot. Hence there is no point sandboxing a chrooted process to // do the unpack. We call inline instead within the daemon process. return archive.Unpack(decompressedArchive, longpath.AddPrefix(dest), options) } + +func invokePack(srcPath string, options *archive.TarOptions, root string) (io.ReadCloser, error) { + // Windows is different to Linux here because Windows does not support + // chroot. Hence there is no point sandboxing a chrooted process to + // do the pack. We call inline instead within the daemon process. + return archive.TarWithOptions(srcPath, options) +} diff --git a/vendor/github.com/containers/storage/pkg/chrootarchive/chroot_unix.go b/vendor/github.com/containers/storage/pkg/chrootarchive/chroot_unix.go index f9b5dece8..83278ee50 100644 --- a/vendor/github.com/containers/storage/pkg/chrootarchive/chroot_unix.go +++ b/vendor/github.com/containers/storage/pkg/chrootarchive/chroot_unix.go @@ -4,9 +4,13 @@ package chrootarchive import "golang.org/x/sys/unix" -func chroot(path string) error { +func realChroot(path string) error { if err := unix.Chroot(path); err != nil { return err } return unix.Chdir("/") } + +func chroot(path string) error { + return realChroot(path) +} diff --git a/vendor/github.com/containers/storage/pkg/chrootarchive/init_unix.go b/vendor/github.com/containers/storage/pkg/chrootarchive/init_unix.go index 21cd87992..ea08135e4 100644 --- a/vendor/github.com/containers/storage/pkg/chrootarchive/init_unix.go +++ b/vendor/github.com/containers/storage/pkg/chrootarchive/init_unix.go @@ -14,6 +14,7 @@ import ( func init() { reexec.Register("storage-applyLayer", applyLayer) reexec.Register("storage-untar", untar) + reexec.Register("storage-tar", tar) } func fatal(err error) { diff --git a/vendor/github.com/containers/storage/pkg/system/stat_unix.go b/vendor/github.com/containers/storage/pkg/system/stat_unix.go index 91c7d121c..f9a1b4877 100644 --- a/vendor/github.com/containers/storage/pkg/system/stat_unix.go +++ b/vendor/github.com/containers/storage/pkg/system/stat_unix.go @@ -58,3 +58,15 @@ func Stat(path string) (*StatT, error) { } return fromStatT(s) } + +// Fstat takes an open file descriptor and returns +// a system.StatT type pertaining to that file. +// +// Throws an error if the file descriptor is invalid +func Fstat(fd int) (*StatT, error) { + s := &syscall.Stat_t{} + if err := syscall.Fstat(fd, s); err != nil { + return nil, err + } + return fromStatT(s) +} diff --git a/version/version.go b/version/version.go index a917931b7..c3917c016 100644 --- a/version/version.go +++ b/version/version.go @@ -4,7 +4,7 @@ package version // NOTE: remember to bump the version at the top // of the top-level README.md file when this is // bumped. -const Version = "1.3.2-dev" +const Version = "1.4.1-dev" // RemoteAPIVersion is the version for the remote // client API. It is used to determine compatibility |