diff options
43 files changed, 2197 insertions, 139 deletions
diff --git a/.cirrus.yml b/.cirrus.yml new file mode 100644 index 000000000..bc471cb84 --- /dev/null +++ b/.cirrus.yml @@ -0,0 +1,119 @@ +--- + +# Only github users with write-access can define or use encrypted variables +# This credential represents a service account with access to manage both VMs +# and storage. +gcp_credentials: ENCRYPTED[885c6e4297dd8d6f67593c42b810353af0c505a7a670e2c6fd830c56e86bbb2debcc3c18f942d0d46ab36b63521061d4] + +# Default VM to use for testing, unless values overriden by specific tasks (below) +gce_instance: + image_project: "libpod-218412" + zone: "us-central1-a" # Required by Cirrus for the time being + cpu: 2 + memory: "4Gb" + disk: 40 + +# Main collection of env. varss to set for all scripts. All others +# are cooked in by $SCRIPT_BASE/setup_environment.sh +env: + CNI_COMMIT: "7480240de9749f9a0a5c8614b17f1f03e0c06ab9" + CRIO_COMMIT: "662dbb31b5d4f5ed54511a47cde7190c61c28677" + RUNC_COMMIT: "ad0f5255060d36872be04de22f8731f38ef2d7b1" + # File to update in home-dir with task-specific env. var values + ENVLIB: ".bash_profile" + # Overrides default location (/tmp/cirrus) for repo clone + CIRRUS_WORKING_DIR: "/go/src/github.com/containers/libpod" + # Required so $ENVLIB gets loaded + CIRRUS_SHELL: "/bin/bash" + # Save a little typing (path relative to $CIRRUS_WORKING_DIR) + SCRIPT_BASE: "./contrib/cirrus" + PACKER_BASE: "./contrib/cirrus/packer" + +# Every *_task runs in parallel in separate VMs. The name prefix only for reference +# in WebUI, and will be followed by matrix details. This task does all the +# per-pr unit/integration testing. +full_vm_testing_task: + + gce_instance: + # Generate multiple 'test' tasks, covering all possible + # 'matrix' combinations. All run in parallel. + matrix: + # Images are generated separetly, from build_images_task (below) + image_name: "ubuntu-1804-bionic-v20180911-libpod-5763563410948096" + # TODO: Make these work (also build_images_task below) + #image_name: "rhel-server-ec2-7-5-165-1-libpod-5358668723781632" + #image_name: "centos-7-v20180911-libpod-5358668723781632" + #image_name: "fedora-cloud-base-28-1-1-7-libpod-5358668723781632" + + timeout_in: 120m + + # Every *_script runs in sequence, for each task. The name prefix is for + # WebUI reference. The values may be strings... + setup_environment_script: $SCRIPT_BASE/setup_environment.sh + + # ...or lists of strings + verify_source_script: + - whoami # root! + - $SCRIPT_BASE/verify_source.sh + + unit_test_script: $SCRIPT_BASE/unit_test.sh + + integration_test_script: $SCRIPT_BASE/integration_test.sh + + +# This task build new images for future PR testing, but only after a PR merge. +# These images save needing to install/setup the same environment to test every +# PR. The 'active' image for testing is selected by the 'image_name' items in +# task above. Currently this requires manually updating them, but this could +# be automated (see comment at end). + +build_vm_images_task: + # Only produce new images after a PR merge + only_if: $CIRRUS_BRANCH == 'master' + + # Require tests to pass first. + depends_on: + - test # i.e. 'test_task' + + env: + # CSV of packer builder names to enable (see $PACKER_BASE/libpod_images.json) + PACKER_BUILDS: "ubuntu-18" + # TODO: Make these work (also full_vm_testing_task above) + # PACKER_BUILDS: "rhel-7,centos-7,fedora-28,ubuntu-18" + # Command to register a RHEL VM + RHSM_COMMAND: ENCRYPTED[fec01433222af1ed0b8e40e89e7d18f6ee2fa9f49a1e721dc72f7eed3c740661215d1bd05cb54ac66a1a62116b92bdce] + # Additional environment variables needed to build GCE images, within a GCE VM + SERVICE_ACCOUNT: ENCRYPTED[02e03838b1156eb9516c7cc1e888e287910759842275f3c7bc2b4d56075cc6740e29ffa0ab71ebdbbd079673361dd8c9] + GCE_SSH_USERNAME: ENCRYPTED[a19a4ec62423e3e0fe4e7d1a5c9f11eda8fde321b9047ab5ed5590c2b1d7a2d12091c2be1531f949eae927059c2ae531] + GCP_PROJECT_ID: ENCRYPTED[77cb2d392bbc8d17412547d7d91f8d190089bf6e6b96eab9927994bbff6ab2c691ba0329ac7a650ba6182fbbab9fb68d] + # Existing base values to use, output images get epoc stamped names + PACKER_VER: "1.3.1" + # low-level base VM image name inputs to packer + CENTOS_BASE_IMAGE: "centos-7-v20180911" + RHEL_BASE_IMAGE: "rhel-server-ec2-7-5-165-1" + FEDORA_BASE_IMAGE: "fedora-cloud-base-28-1-1-7" + UBUNTU_BASE_IMAGE: "ubuntu-1804-bionic-v20180911" + + gce_instance: + image_name: "image-builder-image" # Simply CentOS 7 + packer dependencies + # Additional permissions for building GCE images, within a GCE VM + scopes: + - compute + - devstorage.full_control + # Doesn't need many local resources to run + cpu: 2 + memory: "2Gb" + disk: 20 + environment_script: $SCRIPT_BASE/setup_environment.sh + build_vm_images_script: $SCRIPT_BASE/build_vm_images.sh + + # TODO,Continuous Delivery: Automaticly open a libpod PR after using 'sed' to replace + # the image_names with the new (just build) images. That will + # cause a new round of testing to happen (via the PR) using + # the new images. When all is good, the PR may be manually + # merged so all PR testing uses the new images. The script + # names (below) describe their purpose in this workflow. + # deploy_images_script: + # - clone_podman_release_branch.sh + # - modify_cirrus_yaml_image_names.sh + # - commit_and_create_upstream_pr.sh @@ -6,6 +6,18 @@ export PATH=$HOME/gopath/bin:$PATH:$GOPATH/bin export GOSRC=$GOPATH/src/github.com/containers/libpod DIST=${DIST:=""} +CONTAINER_RUNTIME=${DIST:=""} + +source /etc/os-release + +INTEGRATION_TEST_ENVS="" + +# For all distributions not Fedora, we need to skip USERNS tests +# for now. +if [ "${ID}" != "fedora" ] || [ "${CONTAINER_RUNTIME}" != "" ]; then + INTEGRATION_TEST_ENVS="SKIP_USERNS=1" +fi + pwd # -i install @@ -121,11 +133,11 @@ fi # Run integration tests if [ $integrationtest -eq 1 ]; then make TAGS="${TAGS}" test-binaries - SKIP_USERNS=1 make varlink_generate GOPATH=/go + make varlink_generate GOPATH=/go if [ $runpython -eq 1 ]; then - SKIP_USERNS=1 make clientintegration GOPATH=/go + make clientintegration GOPATH=/go fi - SKIP_USERNS=1 make ginkgo GOPATH=/go + make ginkgo GOPATH=/go $INTEGRATION_TEST_ENVS fi diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 86744f728..000000000 --- a/.travis.yml +++ /dev/null @@ -1,39 +0,0 @@ -language: go - -sudo: required -dist: trusty - -services: - - docker - -before_install: - - if [ "${TRAVIS_OS_NAME}" = linux ]; then sudo apt-get -qq update; fi - - if [ "${TRAVIS_OS_NAME}" = linux ]; then sudo apt-get -qq install btrfs-tools libdevmapper-dev libgpgme11-dev libapparmor-dev; fi - - if [ "${TRAVIS_OS_NAME}" = linux ]; then sudo apt-get -qq install autoconf automake bison e2fslibs-dev libfuse-dev libtool liblzma-dev gettext; fi - - if [ "${TRAVIS_OS_NAME}" = linux ]; then sudo apt-get -qq install python3-setuptools python3-dateutil python3-psutil; fi - - if [ "${TRAVIS_OS_NAME}" = linux ]; then sudo make install.libseccomp.sudo; fi - -install: - - make install.tools - -before_script: - - export PATH=$HOME/gopath/bin:$PATH - - export LD_LIBRARY_PATH=/usr/local/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}} - -env: - global: - - TRAVIS=1 - -jobs: - include: - - stage: Build and Verify - script: - - make testunit - go: 1.10.x - - stage: Integration Test - script: - - make integration - go: 1.9.x - -notifications: - irc: "chat.freenode.net#podman" diff --git a/Dockerfile b/Dockerfile index 749c5edb9..2c43cb046 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,8 @@ RUN apt-get update && apt-get install -y \ libaio-dev \ libcap-dev \ libfuse-dev \ + libnet-dev \ + libnl-3-dev \ libostree-dev \ libprotobuf-dev \ libprotobuf-c0-dev \ @@ -110,6 +112,16 @@ RUN set -x \ && go get -u github.com/mailru/easyjson/... \ && install -D -m 755 "$GOPATH"/bin/easyjson /usr/bin/ +# Install criu +ENV CRIU_COMMIT 584cbe4643c3fc7dc901ff08bf923ca0fe7326f9 +RUN set -x \ + && cd /tmp \ + && git clone https://github.com/checkpoint-restore/criu.git \ + && cd criu \ + && make \ + && install -D -m 755 criu/criu /usr/sbin/ \ + && rm -rf /tmp/criu + # Install cni config #RUN make install.cni RUN mkdir -p /etc/cni/net.d/ @@ -80,8 +80,8 @@ Information about contributing to this project. Buildah and Podman are two complementary Open-source projects that are available on most Linux platforms and both projects reside at [GitHub.com](https://github.com) -with Buildah [here](https://github.com/containers/buildah) and -Podman [here](https://github.com/containers/libpod). Both Buildah and Podman are +with [Buildah](https://buildah.io) [(GitHub)](https://github.com/containers/buildah) and +[Podman](https://podman.io) [(GitHub)](https://github.com/containers/libpod). Both Buildah and Podman are command line tools that work on OCI images and containers. The two projects differentiate in their specialization. diff --git a/cmd/podman/checkpoint.go b/cmd/podman/checkpoint.go new file mode 100644 index 000000000..cbbbcd740 --- /dev/null +++ b/cmd/podman/checkpoint.go @@ -0,0 +1,73 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/containers/libpod/cmd/podman/libpodruntime" + "github.com/containers/libpod/pkg/rootless" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +var ( + checkpointDescription = ` + podman container checkpoint + + Checkpoints one or more running containers. The container name or ID can be used. +` + checkpointFlags = []cli.Flag{ + cli.BoolFlag{ + Name: "keep, k", + Usage: "keep all temporary checkpoint files", + }, + } + checkpointCommand = cli.Command{ + Name: "checkpoint", + Usage: "Checkpoints one or more containers", + Description: checkpointDescription, + Flags: checkpointFlags, + Action: checkpointCmd, + ArgsUsage: "CONTAINER-NAME [CONTAINER-NAME ...]", + } +) + +func checkpointCmd(c *cli.Context) error { + if rootless.IsRootless() { + return errors.New("checkpointing a container requires root") + } + + runtime, err := libpodruntime.GetRuntime(c) + if err != nil { + return errors.Wrapf(err, "could not get runtime") + } + defer runtime.Shutdown(false) + + keep := c.Bool("keep") + args := c.Args() + if len(args) < 1 { + return errors.Errorf("you must provide at least one container name or id") + } + + var lastError error + for _, arg := range args { + ctr, err := runtime.LookupContainer(arg) + if err != nil { + if lastError != nil { + fmt.Fprintln(os.Stderr, lastError) + } + lastError = errors.Wrapf(err, "error looking up container %q", arg) + continue + } + if err = ctr.Checkpoint(context.TODO(), keep); err != nil { + if lastError != nil { + fmt.Fprintln(os.Stderr, lastError) + } + lastError = errors.Wrapf(err, "failed to checkpoint container %v", ctr.ID()) + } else { + fmt.Println(ctr.ID()) + } + } + return lastError +} diff --git a/cmd/podman/container.go b/cmd/podman/container.go index 82c1c824d..ff634278f 100644 --- a/cmd/podman/container.go +++ b/cmd/podman/container.go @@ -7,6 +7,7 @@ import ( var ( subCommands = []cli.Command{ attachCommand, + checkpointCommand, cleanupCommand, commitCommand, createCommand, @@ -23,6 +24,7 @@ var ( // pruneCommand, refreshCommand, restartCommand, + restoreCommand, rmCommand, runCommand, runlabelCommand, diff --git a/cmd/podman/restore.go b/cmd/podman/restore.go new file mode 100644 index 000000000..43ef87ca2 --- /dev/null +++ b/cmd/podman/restore.go @@ -0,0 +1,73 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/containers/libpod/cmd/podman/libpodruntime" + "github.com/containers/libpod/pkg/rootless" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +var ( + restoreDescription = ` + podman container restore + + Restores a container from a checkpoint. The container name or ID can be used. +` + restoreFlags = []cli.Flag{ + cli.BoolFlag{ + Name: "keep, k", + Usage: "keep all temporary checkpoint files", + }, + } + restoreCommand = cli.Command{ + Name: "restore", + Usage: "Restores one or more containers from a checkpoint", + Description: restoreDescription, + Flags: restoreFlags, + Action: restoreCmd, + ArgsUsage: "CONTAINER-NAME [CONTAINER-NAME ...]", + } +) + +func restoreCmd(c *cli.Context) error { + if rootless.IsRootless() { + return errors.New("restoring a container requires root") + } + + runtime, err := libpodruntime.GetRuntime(c) + if err != nil { + return errors.Wrapf(err, "could not get runtime") + } + defer runtime.Shutdown(false) + + keep := c.Bool("keep") + args := c.Args() + if len(args) < 1 { + return errors.Errorf("you must provide at least one container name or id") + } + + var lastError error + for _, arg := range args { + ctr, err := runtime.LookupContainer(arg) + if err != nil { + if lastError != nil { + fmt.Fprintln(os.Stderr, lastError) + } + lastError = errors.Wrapf(err, "error looking up container %q", arg) + continue + } + if err = ctr.Restore(context.TODO(), keep); err != nil { + if lastError != nil { + fmt.Fprintln(os.Stderr, lastError) + } + lastError = errors.Wrapf(err, "failed to restore container %v", ctr.ID()) + } else { + fmt.Println(ctr.ID()) + } + } + return lastError +} diff --git a/cmd/podman/runlabel.go b/cmd/podman/runlabel.go index c5dd98ee6..34e6b9093 100644 --- a/cmd/podman/runlabel.go +++ b/cmd/podman/runlabel.go @@ -94,6 +94,14 @@ func runlabelCmd(c *cli.Context) error { newImage *image.Image ) + // Evil images could trick into recursively executing the runlabel + // command. Avoid this by setting the "PODMAN_RUNLABEL_NESTED" env + // variable when executing a label first. + nested := os.Getenv("PODMAN_RUNLABEL_NESTED") + if nested == "1" { + return fmt.Errorf("nested runlabel calls: runlabels cannot execute the runlabel command") + } + opts := make(map[string]string) runtime, err := libpodruntime.GetRuntime(c) if err != nil { @@ -177,6 +185,7 @@ func runlabelCmd(c *cli.Context) error { cmd := shared.GenerateCommand(runLabel, imageName, c.String("name")) env := shared.GenerateRunEnvironment(c.String("name"), imageName, opts) + env = append(env, "PODMAN_RUNLABEL_NESTED=1") if !c.Bool("quiet") { fmt.Printf("Command: %s\n", strings.Join(cmd, " ")) diff --git a/cmd/podman/shared/funcs.go b/cmd/podman/shared/funcs.go index 5c401634c..21e7fe10d 100644 --- a/cmd/podman/shared/funcs.go +++ b/cmd/podman/shared/funcs.go @@ -15,9 +15,8 @@ func GenerateCommand(command, imageName, name string) []string { name = imageName } cmd := strings.Split(command, " ") - // Replace the first position of cmd with podman whether - // it is docker, /usr/bin/docker, or podman - newCommand = append(newCommand, "podman") + // Replace the first element of cmd with "/proc/self/exe" + newCommand = append(newCommand, "/proc/self/exe") for _, arg := range cmd[1:] { var newArg string switch arg { diff --git a/cmd/podman/shared/funcs_test.go b/cmd/podman/shared/funcs_test.go index 3d0ac005f..612be480b 100644 --- a/cmd/podman/shared/funcs_test.go +++ b/cmd/podman/shared/funcs_test.go @@ -15,35 +15,35 @@ var ( func TestGenerateCommand(t *testing.T) { inputCommand := "docker run -it --name NAME -e NAME=NAME -e IMAGE=IMAGE IMAGE echo install" - correctCommand := "podman run -it --name bar -e NAME=bar -e IMAGE=foo foo echo install" + correctCommand := "/proc/self/exe run -it --name bar -e NAME=bar -e IMAGE=foo foo echo install" newCommand := GenerateCommand(inputCommand, "foo", "bar") assert.Equal(t, correctCommand, strings.Join(newCommand, " ")) } func TestGenerateCommandPath(t *testing.T) { inputCommand := "/usr/bin/docker run -it --name NAME -e NAME=NAME -e IMAGE=IMAGE IMAGE echo install" - correctCommand := "podman run -it --name bar -e NAME=bar -e IMAGE=foo foo echo install" + correctCommand := "/proc/self/exe run -it --name bar -e NAME=bar -e IMAGE=foo foo echo install" newCommand := GenerateCommand(inputCommand, "foo", "bar") assert.Equal(t, correctCommand, strings.Join(newCommand, " ")) } func TestGenerateCommandNoSetName(t *testing.T) { inputCommand := "docker run -it --name NAME -e NAME=NAME -e IMAGE=IMAGE IMAGE echo install" - correctCommand := "podman run -it --name foo -e NAME=foo -e IMAGE=foo foo echo install" + correctCommand := "/proc/self/exe run -it --name foo -e NAME=foo -e IMAGE=foo foo echo install" newCommand := GenerateCommand(inputCommand, "foo", "") assert.Equal(t, correctCommand, strings.Join(newCommand, " ")) } func TestGenerateCommandNoName(t *testing.T) { inputCommand := "docker run -it -e IMAGE=IMAGE IMAGE echo install" - correctCommand := "podman run -it -e IMAGE=foo foo echo install" + correctCommand := "/proc/self/exe run -it -e IMAGE=foo foo echo install" newCommand := GenerateCommand(inputCommand, "foo", "") assert.Equal(t, correctCommand, strings.Join(newCommand, " ")) } func TestGenerateCommandAlreadyPodman(t *testing.T) { inputCommand := "podman run -it --name NAME -e NAME=NAME -e IMAGE=IMAGE IMAGE echo install" - correctCommand := "podman run -it --name bar -e NAME=bar -e IMAGE=foo foo echo install" + correctCommand := "/proc/self/exe run -it --name bar -e NAME=bar -e IMAGE=foo foo echo install" newCommand := GenerateCommand(inputCommand, "foo", "bar") assert.Equal(t, correctCommand, strings.Join(newCommand, " ")) } diff --git a/completions/bash/podman b/completions/bash/podman index f63bf4469..604a25f5d 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -87,6 +87,10 @@ __podman_complete_containers_all() { __podman_complete_containers "$@" --all } +__podman_complete_containers_created() { + __podman_complete_containers "$@" --all --filter status=created +} + __podman_complete_containers_running() { __podman_complete_containers "$@" --filter status=running } @@ -710,6 +714,24 @@ _podman_container_attach() { _podman_attach } +_podman_container_checkpoint() { + local options_with_args=" + --help -h + " + local boolean_options=" + --keep + -k + " + case "$cur" in + -*) + COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) + ;; + *) + __podman_complete_containers_running + ;; + esac +} + _podman_container_commit() { _podman_commit } @@ -770,6 +792,24 @@ _podman_container_restart() { _podman_restart } +_podman_container_restore() { + local options_with_args=" + --help -h + " + local boolean_options=" + --keep + -k + " + case "$cur" in + -*) + COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) + ;; + *) + __podman_complete_containers_created + ;; + esac +} + _podman_container_rm() { _podman_rm } @@ -817,6 +857,7 @@ _podman_container() { " subcommands=" attach + checkpoint commit create diff @@ -831,6 +872,7 @@ _podman_container() { port refresh restart + restore rm run start diff --git a/contrib/cirrus/build_vm_images.sh b/contrib/cirrus/build_vm_images.sh new file mode 100755 index 000000000..8538ee910 --- /dev/null +++ b/contrib/cirrus/build_vm_images.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +set -e +source $(dirname $0)/lib.sh + +req_env_var " +CNI_COMMIT $CNI_COMMIT +CRIO_COMMIT $CRIO_COMMIT +RUNC_COMMIT $RUNC_COMMIT +PACKER_BUILDS $PACKER_BUILDS +CENTOS_BASE_IMAGE $CENTOS_BASE_IMAGE +UBUNTU_BASE_IMAGE $UBUNTU_BASE_IMAGE +FEDORA_BASE_IMAGE $FEDORA_BASE_IMAGE +RHEL_BASE_IMAGE $RHEL_BASE_IMAGE +RHSM_COMMAND $RHSM_COMMAND +CIRRUS_BUILD_ID $CIRRUS_BUILD_ID +SERVICE_ACCOUNT $SERVICE_ACCOUNT +GCE_SSH_USERNAME $GCE_SSH_USERNAME +GCP_PROJECT_ID $GCP_PROJECT_ID +PACKER_VER $PACKER_VER +SCRIPT_BASE $SCRIPT_BASE +PACKER_BASE $PACKER_BASE +" + +# TODO: Skip building images if $CIRRUS_BRANCH =~ "master" and +# commit message of $CIRRUS_CHANGE_IN_REPO contains a magic word +# produced by 'commit_and_create_upstream_pr.sh' script (see .cirrus.yml) + +show_env_vars + +# Everything here is running on the 'image-builder-image' GCE image +# Assume basic dependencies are all met, but there could be a newer version +# of the packer binary +PACKER_FILENAME="packer_${PACKER_VER}_linux_amd64.zip" +mkdir -p "$HOME/packer" +cd "$HOME/packer" +# image_builder_image has packer pre-installed, check if same version requested +if ! [[ -r "$PACKER_FILENAME" ]] +then + curl -L -O https://releases.hashicorp.com/packer/$PACKER_VER/$PACKER_FILENAME + curl -L https://releases.hashicorp.com/packer/${PACKER_VER}/packer_${PACKER_VER}_SHA256SUMS | \ + grep 'linux_amd64' > ./sha256sums + sha256sum --check ./sha256sums + unzip -o $PACKER_FILENAME + ./packer --help &> /dev/null # verify exit(0) +fi + +set -x + +cd "$GOSRC" +# N/B: /usr/sbin/packer is a DIFFERENT tool, and will exit 0 given the args below :( +TEMPLATE="./$PACKER_BASE/libpod_images.json" + +$HOME/packer/packer inspect "$TEMPLATE" + +#$HOME/packer/packer build -machine-readable "-only=$PACKER_BUILDS" "$TEMPLATE" | tee /tmp/packer_log.csv +$HOME/packer/packer build "-only=$PACKER_BUILDS" "$TEMPLATE" + +# TODO: Report back to PR names of built images diff --git a/contrib/cirrus/integration_test.sh b/contrib/cirrus/integration_test.sh new file mode 100755 index 000000000..226053724 --- /dev/null +++ b/contrib/cirrus/integration_test.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +set -e +source $(dirname $0)/lib.sh + +req_env_var " +GOSRC $GOSRC +OS_RELEASE_ID $OS_RELEASE_ID +OS_RELEASE_VER $OS_RELEASE_VER +" + +show_env_vars + +set -x +cd "$GOSRC" +case "${OS_RELEASE_ID}-${OS_RELEASE_VER}" in + ubuntu-18) + make install PREFIX=/usr ETCDIR=/etc "BUILDTAGS=$BUILDTAGS" + make test-binaries "BUILDTAGS=$BUILDTAGS" + SKIP_USERNS=1 make localintegration "BUILDTAGS=$BUILDTAGS" + ;; + fedora-28) ;& # Continue to the next item + centos-7) ;& + rhel-7) + stub 'integration testing not working on $OS_RELEASE_ID' + ;; + *) bad_os_id_ver ;; +esac diff --git a/contrib/cirrus/lib.sh b/contrib/cirrus/lib.sh new file mode 100644 index 000000000..e69f1e040 --- /dev/null +++ b/contrib/cirrus/lib.sh @@ -0,0 +1,258 @@ + + +# Library of common, shared utility functions. This file is intended +# to be sourced by other scripts, not called directly. + +# Under some contexts these values are not set, make sure they are. +USER="$(whoami)" +HOME="$(getent passwd $USER | cut -d : -f 6)" +if ! [[ "$PATH" =~ "/usr/local/bin" ]] +then + export PATH="$PATH:/usr/local/bin" +fi + +# In ci/testing environment, ensure variables are always loaded +if [[ -r "$HOME/$ENVLIB" ]] && [[ -n "$CI" ]] +then + # Make sure this is always loaded + source "$HOME/$ENVLIB" +fi + +# Pass in a line delimited list of, space delimited name/value pairs +# exit non-zero with helpful error message if any value is empty +req_env_var() { + echo "$1" | while read NAME VALUE + do + if [[ -n "$NAME" ]] && [[ -z "$VALUE" ]] + then + echo "Required env. var. \$$NAME is not set" + exit 9 + fi + done +} + +# Some env. vars may contain secrets. Display values for known "safe" +# and useful variables. +# ref: https://cirrus-ci.org/guide/writing-tasks/#environment-variables +show_env_vars() { + echo " +BUILDTAGS $BUILDTAGS +CI $CI +CIRRUS_CI $CIRRUS_CI +CI_NODE_INDEX $CI_NODE_INDEX +CI_NODE_TOTAL $CI_NODE_TOTAL +CONTINUOUS_INTEGRATION $CONTINUOUS_INTEGRATION +CIRRUS_BASE_BRANCH $CIRRUS_BASE_BRANCH +CIRRUS_BASE_SHA $CIRRUS_BASE_SHA +CIRRUS_BRANCH $CIRRUS_BRANCH +CIRRUS_BUILD_ID $CIRRUS_BUILD_ID +CIRRUS_CHANGE_IN_REPO $CIRRUS_CHANGE_IN_REPO +CIRRUS_CHANGE_MESSAGE $CIRRUS_CHANGE_MESSAGE +CIRRUS_CLONE_DEPTH $CIRRUS_CLONE_DEPTH +CIRRUS_DEFAULT_BRANCH $CIRRUS_DEFAULT_BRANCH +CIRRUS_PR $CIRRUS_PR +CIRRUS_TAG $CIRRUS_TAG +CIRRUS_OS $CIRRUS_OS +OS $OS +CIRRUS_TASK_NAME $CIRRUS_TASK_NAME +CIRRUS_TASK_ID $CIRRUS_TASK_ID +CIRRUS_REPO_NAME $CIRRUS_REPO_NAME +CIRRUS_REPO_OWNER $CIRRUS_REPO_OWNER +CIRRUS_REPO_FULL_NAME $CIRRUS_REPO_FULL_NAME +CIRRUS_REPO_CLONE_URL $CIRRUS_REPO_CLONE_URL +CIRRUS_SHELL $CIRRUS_SHELL +CIRRUS_USER_COLLABORATOR $CIRRUS_USER_COLLABORATOR +CIRRUS_USER_PERMISSION $CIRRUS_USER_PERMISSION +CIRRUS_WORKING_DIR $CIRRUS_WORKING_DIR +CIRRUS_HTTP_CACHE_HOST $CIRRUS_HTTP_CACHE_HOST +$(go env) + " | while read NAME VALUE + do + [[ -z "$NAME" ]] || echo "export $NAME=\"$VALUE\"" + done +} + +# Return a GCE image-name compatible string representation of distribution name +os_release_id() { + eval "$(egrep -m 1 '^ID=' /etc/os-release | tr -d \' | tr -d \")" + echo "$ID" +} + +# Return a GCE image-name compatible string representation of distribution major version +os_release_ver() { + eval "$(egrep -m 1 '^VERSION_ID=' /etc/os-release | tr -d \' | tr -d \")" + echo "$VERSION_ID" | cut -d '.' -f 1 +} + +bad_os_id_ver() { + echo "Unknown/Unsupported distro. $OS_RELEASE_ID and/or version $OS_RELEASE_VER for $ARGS" + exit 42 +} + +stub() { + echo "STUB: Pretending to do $1" +} + +# Run sudo in directory with GOPATH set +cdsudo() { + DIR="$1" + shift + CMD="cd $DIR && $@" + sudo --preserve-env=GOPATH --non-interactive bash -c "$CMD" +} + + +# Helper/wrapper script to only show stderr/stdout on non-zero exit +install_ooe() { + req_env_var "SCRIPT_BASE $SCRIPT_BASE" + echo "Installing script to mask stdout/stderr unless non-zero exit." + sudo install -D -m 755 "/tmp/libpod/$SCRIPT_BASE/ooe.sh" /usr/local/bin/ooe.sh +} + +# Grab a newer version of git from software collections +# https://www.softwarecollections.org/en/ +# and use it with a wrapper +install_scl_git() { + echo "Installing SoftwareCollections updated 'git' version." + ooe.sh sudo yum -y install rh-git29 + cat << "EOF" | sudo tee /usr/bin/git +#!/bin/bash + +scl enable rh-git29 -- git $@ +EOF + sudo chmod 755 /usr/bin/git +} + +install_cni_plugins() { + echo "Installing CNI Plugins from commit $CNI_COMMIT" + req_env_var " + GOPATH $GOPATH + CNI_COMMIT $CNI_COMMIT + " + DEST="$GOPATH/src/github.com/containernetworking/plugins" + rm -rf "$DEST" + ooe.sh git clone "https://github.com/containernetworking/plugins.git" "$DEST" + cd "$DEST" + ooe.sh git checkout -q "$CNI_COMMIT" + ooe.sh ./build.sh + sudo mkdir -p /usr/libexec/cni + sudo cp bin/* /usr/libexec/cni +} + +install_runc(){ + OS_RELEASE_ID=$(os_release_id) + echo "Installing RunC from commit $RUNC_COMMIT" + echo "Platform is $OS_RELEASE_ID" + req_env_var " + GOPATH $GOPATH + RUNC_COMMIT $RUNC_COMMIT + OS_RELEASE_ID $OS_RELEASE_ID + " + if [[ "$OS_RELEASE_ID" =~ "ubuntu" ]]; then + echo "Running make install.libseccomp.sudo for ubuntu" + if ! [[ -d "/tmp/libpod" ]] + then + echo "Expecting a copy of libpod repository in /tmp/libpod" + exit 5 + fi + mkdir -p "$GOPATH/src/github.com/containers/" + # Symlinks don't work with Go + cp -a /tmp/libpod "$GOPATH/src/github.com/containers/" + cd "$GOPATH/src/github.com/containers/libpod" + ooe.sh sudo make install.libseccomp.sudo + fi + DEST="$GOPATH/src/github.com/opencontainers/runc" + rm -rf "$DEST" + ooe.sh git clone https://github.com/opencontainers/runc.git "$DEST" + cd "$DEST" + ooe.sh git fetch origin --tags + ooe.sh git checkout -q "$RUNC_COMMIT" + ooe.sh make static BUILDTAGS="seccomp selinux" + sudo install -m 755 runc /usr/bin/runc +} + +install_buildah() { + echo "Installing buildah from latest upstream master" + req_env_var "GOPATH $GOPATH" + DEST="$GOPATH/src/github.com/containers/buildah" + rm -rf "$DEST" + ooe.sh git clone https://github.com/containers/buildah "$DEST" + cd "$DEST" + ooe.sh make + ooe.sh sudo make install +} + +# Requires $GOPATH and $CRIO_COMMIT to be set +install_conmon(){ + echo "Installing conmon from commit $CRIO_COMMIT" + req_env_var " + GOPATH $GOPATH + CRIO_COMMIT $CRIO_COMMIT + " + DEST="$GOPATH/src/github.com/kubernetes-sigs/cri-o.git" + rm -rf "$DEST" + ooe.sh git clone https://github.com/kubernetes-sigs/cri-o.git "$DEST" + cd "$DEST" + ooe.sh git fetch origin --tags + ooe.sh git checkout -q "$CRIO_COMMIT" + ooe.sh make + sudo install -D -m 755 bin/conmon /usr/libexec/podman/conmon +} + +# Runs in testing VM, not image building +install_testing_dependencies() { + echo "Installing ginkgo, gomega, and easyjson into \$GOPATH=$GOPATH" + req_env_var " + GOPATH $GOPATH + GOSRC $GOSRC + " + cd "$GOSRC" + ooe.sh go get -u github.com/onsi/ginkgo/ginkgo + ooe.sh install -D -m 755 "$GOPATH"/bin/ginkgo /usr/bin/ + ooe.sh go get github.com/onsi/gomega/... + ooe.sh go get -u github.com/mailru/easyjson/... + sudo install -D -m 755 "$GOPATH"/bin/easyjson /usr/bin/ +} + +install_packer_copied_files(){ + # Install cni config, policy and registry config + sudo install -D -m 755 /tmp/libpod/cni/87-podman-bridge.conflist \ + /etc/cni/net.d/87-podman-bridge.conflist + sudo install -D -m 755 /tmp/libpod/test/policy.json \ + /etc/containers/policy.json + sudo install -D -m 755 /tmp/libpod/test/redhat_sigstore.yaml \ + /etc/containers/registries.d/registry.access.redhat.com.yaml +} + +install_varlink(){ + echo "Installing varlink from the cheese-factory" + ooe.sh sudo -H pip3 install varlink +} + +_finalize(){ + echo "Removing leftover giblets from cloud-init" + cd / + sudo rm -rf /var/lib/cloud + sudo rm -rf /root/.ssh/* + sudo rm -rf /home/* +} + +rh_finalize(){ + # Allow root ssh-logins + if [[ -r /etc/cloud/cloud.cfg ]] + then + sudo sed -re 's/^disable_root:.*/disable_root: 0/g' -i /etc/cloud/cloud.cfg + fi + echo "Resetting to fresh-state for usage as cloud-image." + sudo $(type -P dnf || type -P yum) clean all + sudo rm -rf /var/cache/{yum,dnf} + sudo rm -f /etc/udev/rules.d/*-persistent-*.rules + sudo touch /.unconfigured # force firstboot to run + _finalize +} + +ubuntu_finalize(){ + echo "Resetting to fresh-state for usage as cloud-image." + sudo rm -rf /var/cache/apt + _finalize +} diff --git a/contrib/cirrus/ooe.sh b/contrib/cirrus/ooe.sh new file mode 100755 index 000000000..d79e574b2 --- /dev/null +++ b/contrib/cirrus/ooe.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# This script executes a command while logging all output to a temporary +# file. If the command exits non-zero, then all output is sent to the console, +# before returning the exit code. If the script itself fails, the exit code 121 +# is returned. + +set -eo pipefail + +SCRIPT_PATH="$0" + +badusage() { + echo "Incorrect usage: $(basename $SCRIPT_PATH) <command> [options]" > /dev/stderr + echo "ERROR: $1" + exit 121 +} + +COMMAND="$@" +[[ -n "$COMMAND" ]] || badusage "No command specified" + +OUTPUT_TMPFILE="$(mktemp -p '' $(basename $0)_output_XXXX)" +output_on_error() { + RET=$? + set +e + if [[ "$RET" -ne "0" ]] + then + echo "---------------------------" + cat "$OUTPUT_TMPFILE" + echo "[$(date --iso-8601=second)] <exit $RET> $COMMAND" + fi + rm -f "$OUTPUT_TMPFILE" +} +trap "output_on_error" EXIT + +"$@" 2>&1 | while IFS='' read LINE # Preserve leading/trailing whitespace +do + # Every stdout and (copied) stderr line + echo "[$(date --iso-8601=second)] $LINE" +done >> "$OUTPUT_TMPFILE" diff --git a/contrib/cirrus/packer/README.md b/contrib/cirrus/packer/README.md new file mode 100644 index 000000000..8ff6947e9 --- /dev/null +++ b/contrib/cirrus/packer/README.md @@ -0,0 +1,2 @@ +These are definitions and scripts consumed by packer to produce the +various distribution images used for CI testing. diff --git a/contrib/cirrus/packer/centos_setup.sh b/contrib/cirrus/packer/centos_setup.sh new file mode 100644 index 000000000..2253d7b35 --- /dev/null +++ b/contrib/cirrus/packer/centos_setup.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +# This script is called by packer on the subject CentOS VM, to setup the podman +# build/test environment. It's not intended to be used outside of this context. + +set -e + +# Load in library (copied by packer, before this script was run) +source /tmp/libpod/$SCRIPT_BASE/lib.sh + +req_env_var " +SCRIPT_BASE $SCRIPT_BASE +CNI_COMMIT $CNI_COMMIT +CRIO_COMMIT $CRIO_COMMIT +" + +install_ooe + +export GOPATH="$(mktemp -d)" +trap "sudo rm -rf $GOPATH" EXIT + +ooe.sh sudo yum -y update + +ooe.sh sudo yum -y install centos-release-scl epel-release + +ooe.sh sudo yum -y install \ + atomic-registries \ + btrfs-progs-devel \ + bzip2 \ + device-mapper-devel \ + findutils \ + glib2-devel \ + glibc-static \ + gnupg \ + golang \ + golang-github-cpuguy83-go-md2man \ + golang-github-cpuguy83-go-md2man \ + gpgme-devel \ + iptables \ + libassuan-devel \ + libseccomp-devel \ + libselinux-devel \ + lsof \ + make \ + nmap-ncat \ + ostree-devel \ + python \ + python3-dateutil \ + python3-psutil \ + python3-pytoml \ + runc \ + skopeo-containers \ + unzip \ + which \ + xz + +install_scl_git + +install_cni_plugins + +install_buildah + +install_conmon + +install_packer_copied_files + +rh_finalize + +echo "SUCCESS!" diff --git a/contrib/cirrus/packer/fedora_setup.sh b/contrib/cirrus/packer/fedora_setup.sh new file mode 100644 index 000000000..53709fbdd --- /dev/null +++ b/contrib/cirrus/packer/fedora_setup.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +# This script is called by packer on the subject fedora VM, to setup the podman +# build/test environment. It's not intended to be used outside of this context. + +set -e + +# Load in library (copied by packer, before this script was run) +source /tmp/libpod/$SCRIPT_BASE/lib.sh + +req_env_var " +SCRIPT_BASE $SCRIPT_BASE +CNI_COMMIT $CNI_COMMIT +CRIO_COMMIT $CRIO_COMMIT +RUNC_COMMIT $RUNC_COMMIT +" + +install_ooe + +export GOPATH="$(mktemp -d)" +trap "sudo rm -rf $GOPATH" EXIT + +# breaks networking on f28/29 in GCE +# ooe.sh sudo dnf update -y + +ooe.sh sudo dnf install -y \ + atomic-registries \ + btrfs-progs-devel \ + bzip2 \ + conmon \ + device-mapper-devel \ + findutils \ + git \ + glib2-devel \ + glibc-static \ + gnupg \ + golang \ + golang-github-cpuguy83-go-md2man \ + golang-github-cpuguy83-go-md2man \ + gpgme-devel \ + iptables \ + libassuan-devel \ + libseccomp-devel \ + libselinux-devel \ + lsof \ + make \ + nmap-ncat \ + ostree-devel \ + procps-ng \ + python \ + python3-dateutil \ + python3-psutil \ + python3-pytoml \ + runc \ + skopeo-containers \ + slirp4netns \ + which\ + xz + +install_varlink + +install_cni_plugins + +install_buildah + +install_conmon + +install_packer_copied_files + +rh_finalize # N/B: Halts system! + +echo "SUCCESS!" diff --git a/contrib/cirrus/packer/libpod_images.json b/contrib/cirrus/packer/libpod_images.json new file mode 100644 index 000000000..82a41ca25 --- /dev/null +++ b/contrib/cirrus/packer/libpod_images.json @@ -0,0 +1,124 @@ +{ + "variables": { + "CNI_COMMIT": "{{env `CNI_COMMIT`}}", + "CRIO_COMMIT": "{{env `CRIO_COMMIT`}}", + "RUNC_COMMIT": "{{env `RUNC_COMMIT`}}", + + "CENTOS_BASE_IMAGE": "{{env `CENTOS_BASE_IMAGE`}}" , + "UBUNTU_BASE_IMAGE": "{{env `UBUNTU_BASE_IMAGE`}}", + "FEDORA_BASE_IMAGE": "{{env `FEDORA_BASE_IMAGE`}}", + "RHEL_BASE_IMAGE": "{{env `RHEL_BASE_IMAGE`}}", + + "GOSRC": "{{env `GOSRC`}}", + "PACKER_BASE": "{{env `PACKER_BASE`}}", + "SCRIPT_BASE": "{{env `SCRIPT_BASE`}}", + + "SERVICE_ACCOUNT": "{{env `SERVICE_ACCOUNT`}}", + "GCP_PROJECT_ID": "{{env `GCP_PROJECT_ID`}}", + "CIRRUS_BUILD_ID": "{{env `CIRRUS_BUILD_ID`}}", + "GCE_SSH_USERNAME": "{{env `GCE_SSH_USERNAME`}}", + "RHSM_COMMAND": "{{env `RHSM_COMMAND`}}" + }, + "sensitive-variables": [ + "GCP_PROJECT_ID", "SERVICE_ACCOUNT", "GCE_SSH_USERNAME", "RHSM_COMMAND" + ], + "builders": [ + { + "name": "rhel-7", + "type": "googlecompute", + "project_id": "{{user `GCP_PROJECT_ID`}}", + "zone": "us-central1-a", + "source_image": "{{user `RHEL_BASE_IMAGE`}}", + "image_name": "{{user `RHEL_BASE_IMAGE`}}-libpod-{{user `CIRRUS_BUILD_ID`}}", + "image_family": "{{user `RHEL_BASE_IMAGE`}}-libpod", + "service_account_email": "{{user `SERVICE_ACCOUNT`}}", + "communicator": "ssh", + "ssh_username": "ec2-user", + "ssh_pty": "true" + },{ + "name": "centos-7", + "type": "googlecompute", + "project_id": "{{user `GCP_PROJECT_ID`}}", + "zone": "us-central1-a", + "source_image": "{{user `CENTOS_BASE_IMAGE`}}", + "image_name": "{{user `CENTOS_BASE_IMAGE`}}-libpod-{{user `CIRRUS_BUILD_ID`}}", + "image_family": "{{user `CENTOS_BASE_IMAGE`}}-libpod", + "service_account_email": "{{user `SERVICE_ACCOUNT`}}", + "communicator": "ssh", + "ssh_username": "{{user `GCE_SSH_USERNAME`}}", + "ssh_pty": "true" + },{ + "name": "fedora-28", + "type": "googlecompute", + "project_id": "{{user `GCP_PROJECT_ID`}}", + "zone": "us-central1-a", + "source_image": "{{user `FEDORA_BASE_IMAGE`}}", + "image_name": "{{user `FEDORA_BASE_IMAGE`}}-libpod-{{user `CIRRUS_BUILD_ID`}}", + "image_family": "{{user `FEDORA_BASE_IMAGE`}}-libpod", + "service_account_email": "{{user `SERVICE_ACCOUNT`}}", + "communicator": "ssh", + "ssh_username": "fedora", + "ssh_pty": "true" + },{ + "name": "ubuntu-18", + "type": "googlecompute", + "project_id": "{{user `GCP_PROJECT_ID`}}", + "zone": "us-central1-a", + "source_image": "{{user `UBUNTU_BASE_IMAGE`}}", + "image_name": "{{user `UBUNTU_BASE_IMAGE`}}-libpod-{{user `CIRRUS_BUILD_ID`}}", + "image_family": "{{user `UBUNTU_BASE_IMAGE`}}-libpod", + "service_account_email": "{{user `SERVICE_ACCOUNT`}}", + "communicator": "ssh", + "ssh_username": "{{user `GCE_SSH_USERNAME`}}", + "ssh_pty": "true" + } + ], + "provisioners": [ + { + "type": "file", + "source": "{{user `GOSRC`}}", + "destination": "/tmp/libpod" + },{ + "type": "shell", + "only": ["rhel-7"], + "script": "{{user `GOSRC`}}/{{user `PACKER_BASE`}}/rhel_setup.sh", + "environment_vars": [ + "SCRIPT_BASE={{user `SCRIPT_BASE`}}", + "CNI_COMMIT={{user `CNI_COMMIT`}}", + "CRIO_COMMIT={{user `CRIO_COMMIT`}}", + "RUNC_COMMIT={{user `RUNC_COMMIT`}}", + "RHSM_COMMAND={{user `RHSM_COMMAND`}}" + ] + },{ + "type": "shell", + "only": ["centos-7"], + "script": "{{user `GOSRC`}}/{{user `PACKER_BASE`}}/centos_setup.sh", + "environment_vars": [ + "SCRIPT_BASE={{user `SCRIPT_BASE`}}", + "CNI_COMMIT={{user `CNI_COMMIT`}}", + "CRIO_COMMIT={{user `CRIO_COMMIT`}}", + "RUNC_COMMIT={{user `RUNC_COMMIT`}}" + ] + },{ + "type": "shell", + "only": ["fedora-28"], + "script": "{{user `GOSRC`}}/{{user `PACKER_BASE`}}/fedora_setup.sh", + "environment_vars": [ + "SCRIPT_BASE={{user `SCRIPT_BASE`}}", + "CNI_COMMIT={{user `CNI_COMMIT`}}", + "CRIO_COMMIT={{user `CRIO_COMMIT`}}", + "RUNC_COMMIT={{user `RUNC_COMMIT`}}" + ] + },{ + "type": "shell", + "only": ["ubuntu-18"], + "script": "{{user `GOSRC`}}/{{user `PACKER_BASE`}}/ubuntu_setup.sh", + "environment_vars": [ + "SCRIPT_BASE={{user `SCRIPT_BASE`}}", + "CNI_COMMIT={{user `CNI_COMMIT`}}", + "CRIO_COMMIT={{user `CRIO_COMMIT`}}", + "RUNC_COMMIT={{user `RUNC_COMMIT`}}" + ] + } + ] +} diff --git a/contrib/cirrus/packer/rhel_setup.sh b/contrib/cirrus/packer/rhel_setup.sh new file mode 100644 index 000000000..b776a0d97 --- /dev/null +++ b/contrib/cirrus/packer/rhel_setup.sh @@ -0,0 +1,111 @@ +#!/bin/bash + +# This script is called by packer on the subject CentOS VM, to setup the podman +# build/test environment. It's not intended to be used outside of this context. + +set -e + +# Load in library (copied by packer, before this script was run) +source /tmp/libpod/$SCRIPT_BASE/lib.sh + +req_env_var " +SCRIPT_BASE $SCRIPT_BASE +CNI_COMMIT $CNI_COMMIT +CRIO_COMMIT $CRIO_COMMIT +RHSM_COMMAND $RHSM_COMMAND +" + +install_ooe + +export GOPATH="$(mktemp -d)" +export RHSMCMD="$(mktemp)" + +exit_handler() { + set +ex + cd / + sudo rm -rf "$RHSMCMD" + sudo rm -rf "$GOPATH" + sudo subscription-manager remove --all + sudo subscription-manager unregister + sudo subscription-manager clean +} +trap "exit_handler" EXIT + +# Avoid logging sensitive details +echo "$RHSM_COMMAND" > "$RHSMCMD" +ooe.sh sudo bash "$RHSMCMD" +sudo rm -rf "$RHSMCMD" + +ooe.sh sudo yum -y erase "rh-amazon-rhui-client*" +ooe.sh sudo subscription-manager repos "--disable=*" +ooe.sh sudo subscription-manager repos \ + --enable=rhel-7-server-rpms \ + --enable=rhel-7-server-optional-rpms \ + --enable=rhel-7-server-extras-rpms \ + --enable=rhel-server-rhscl-7-rpms + +ooe.sh sudo yum -y update + +# Frequently needed +ooe.sh sudo yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm + +# Required for google to manage ssh keys +sudo tee -a /etc/yum.repos.d/google-cloud-sdk.repo << EOM +[google-cloud-compute] +name=google-cloud-compute +baseurl=https://packages.cloud.google.com/yum/repos/google-cloud-compute-el7-x86_64 +enabled=1 +gpgcheck=1 +repo_gpgcheck=1 +gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg + https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg +EOM + +ooe.sh sudo yum -y install \ + atomic-registries \ + btrfs-progs-devel \ + bzip2 \ + device-mapper-devel \ + findutils \ + glib2-devel \ + glibc-static \ + gnupg \ + golang \ + golang-github-cpuguy83-go-md2man \ + golang-github-cpuguy83-go-md2man \ + google-compute-engine \ + google-compute-engine-oslogin \ + gpgme-devel \ + iptables \ + libassuan-devel \ + libseccomp-devel \ + libselinux-devel \ + lsof \ + make \ + nmap-ncat \ + ostree-devel \ + python \ + python34-dateutil \ + python34-psutil \ + python34-pytoml \ + runc \ + skopeo-containers \ + unzip \ + which \ + xz + +install_scl_git + +install_cni_plugins + +install_buildah + +install_conmon + +install_packer_copied_files + +exit_handler # release subscription! + +rh_finalize + +echo "SUCCESS!" diff --git a/contrib/cirrus/packer/ubuntu_setup.sh b/contrib/cirrus/packer/ubuntu_setup.sh new file mode 100644 index 000000000..96b3a573f --- /dev/null +++ b/contrib/cirrus/packer/ubuntu_setup.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +# This script is called by packer on the subject Ubuntu VM, to setup the podman +# build/test environment. It's not intended to be used outside of this context. + +set -e + +# Load in library (copied by packer, before this script was run) +source /tmp/libpod/$SCRIPT_BASE/lib.sh + +req_env_var " +SCRIPT_BASE $SCRIPT_BASE +CNI_COMMIT $CNI_COMMIT +CRIO_COMMIT $CRIO_COMMIT +RUNC_COMMIT $RUNC_COMMIT +" + +install_ooe + +export GOPATH="$(mktemp -d)" +trap "sudo rm -rf $GOPATH" EXIT + +ooe.sh sudo apt-get -qq update +ooe.sh sudo apt-get -qq update # sometimes it needs to get it twice :S +ooe.sh sudo apt-get -qq upgrade +ooe.sh sudo apt-get -qq install --no-install-recommends \ + apparmor \ + autoconf \ + automake \ + bison \ + btrfs-tools \ + build-essential \ + curl \ + e2fslibs-dev \ + gawk \ + gettext \ + golang \ + go-md2man \ + iptables \ + libaio-dev \ + libapparmor-dev \ + libcap-dev \ + libdevmapper-dev \ + libdevmapper1.02.1 \ + libfuse-dev \ + libglib2.0-dev \ + libgpgme11-dev \ + liblzma-dev \ + libostree-dev \ + libprotobuf-c0-dev \ + libprotobuf-dev \ + libtool \ + libtool \ + libudev-dev \ + lsof \ + netcat \ + pkg-config \ + protobuf-c-compiler \ + protobuf-compiler \ + python-minimal \ + python3-dateutil \ + python3-pip \ + python3-psutil \ + python3-pytoml \ + python3-setuptools \ + socat \ + unzip \ + xz-utils + +echo "Fixing Ubuntu kernel not enabling swap accounting by default" +SEDCMD='s/^GRUB_CMDLINE_LINUX="(.*)"/GRUB_CMDLINE_LINUX="\1 cgroup_enable=memory swapaccount=1"/g' +ooe.sh sudo sed -re "$SEDCMD" -i /etc/default/grub.d/* +ooe.sh sudo sed -re "$SEDCMD" -i /etc/default/grub +ooe.sh sudo update-grub + +install_runc + +install_conmon + +install_cni_plugins + +install_buildah + +install_packer_copied_files + +install_varlink + +sudo curl https://raw.githubusercontent.com/projectatomic/registries/master/registries.fedora\ + -o /etc/containers/registries.conf + +ubuntu_finalize + +echo "SUCCESS!" diff --git a/contrib/cirrus/setup_environment.sh b/contrib/cirrus/setup_environment.sh new file mode 100755 index 000000000..2302f0e15 --- /dev/null +++ b/contrib/cirrus/setup_environment.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +set -e +source $(dirname $0)/lib.sh + +req_env_var " +CI $CI +USER $USER +HOME $HOME +ENVLIB $ENVLIB +SCRIPT_BASE $SCRIPT_BASE +CIRRUS_BUILD_ID $CIRRUS_BUILD_ID" + +[[ "$SHELL" =~ "bash" ]] || chsh -s /bin/bash + +cd "$CIRRUS_WORKING_DIR" # for clarity of initial conditions + +# Verify basic dependencies +for depbin in go rsync unzip sha256sum curl make +do + if ! type -P "$depbin" &> /dev/null + then + echo "ERROR: $depbin binary not found in $PATH" + exit 2 + fi +done + +# Setup env. vars common to all tasks/scripts/platforms and +# ensure they return for every following script execution. +MARK="# Added by $0, manual changes will be lost." +touch "$HOME/$ENVLIB" +if ! grep -q "$MARK" "$HOME/$ENVLIB" +then + cp "$HOME/$ENVLIB" "$HOME/${ENVLIB}_original" + # N/B: Single-quote items evaluated every time, double-quotes only once (right now). + for envstr in \ + "$MARK" \ + "export HEAD=\"$CIRRUS_CHANGE_IN_REPO\"" \ + "export TRAVIS=\"1\"" \ + "export GOSRC=\"$CIRRUS_WORKING_DIR\"" \ + "export OS_RELEASE_ID=\"$(os_release_id)\"" \ + "export OS_RELEASE_VER=\"$(os_release_ver)\"" \ + "export OS_REL_VER=\"${OS_RELEASE_ID}-${OS_RELEASE_VER}\"" \ + "export GOPATH=\"/go\"" \ + 'export PATH="$HOME/bin:$GOPATH/bin:/usr/local/bin:$PATH"' \ + 'export LD_LIBRARY_PATH="/usr/local/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"' + do + # Make permanent in later shells, and set in current shell + X=$(echo "$envstr" | tee -a "$HOME/$ENVLIB") && eval "$X" && echo "$X" + done + + # Some setup needs to vary between distros + case "${OS_RELEASE_ID}-${OS_RELEASE_VER}" in + ubuntu-18) + envstr='export BUILDTAGS="seccomp $($GOSRC/hack/btrfs_tag.sh) $($GOSRC/hack/btrfs_installed_tag.sh) $($GOSRC/hack/ostree_tag.sh) varlink exclude_graphdriver_devicemapper"' + ;; + fedora-28) ;& # Continue to the next item + centos-7) ;& + rhel-7) + envstr='unset BUILDTAGS' # Use default from Makefile + ;; + *) bad_os_id_ver ;; + esac + X=$(echo "$envstr" | tee -a "$HOME/$ENVLIB") && eval "$X" && echo "$X" + + # Do the same for golang env. vars + go env | while read envline + do + X=$(echo "export $envline" | tee -a "$HOME/$ENVLIB") && eval "$X" && echo "$X" + done + + cd "${GOSRC}/" + source "$SCRIPT_BASE/lib.sh" + + # Only testing-VMs need deps installed + [[ -n "$PACKER_BUILDS" ]] || install_testing_dependencies # must exist in $GOPATH +fi diff --git a/contrib/cirrus/unit_test.sh b/contrib/cirrus/unit_test.sh new file mode 100755 index 000000000..cacc23045 --- /dev/null +++ b/contrib/cirrus/unit_test.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +set -e +source $(dirname $0)/lib.sh + +req_env_var " +GOSRC $GOSRC +OS_RELEASE_ID $OS_RELEASE_ID +OS_RELEASE_VER $OS_RELEASE_VER +" + +show_env_vars + +set -x +cd "$GOSRC" +case "${OS_RELEASE_ID}-${OS_RELEASE_VER}" in + ubuntu-18) + make localunit "BUILDTAGS=$BUILDTAGS" + make "BUILDTAGS=$BUILDTAGS" + ;; + fedora-28) + make localunit + make + ;; + centos-7) ;& # Continue to the next item + rhel-7) + stub 'unit testing not working on $OS_RELEASE_ID' + ;; + *) bad_os_id_ver ;; +esac diff --git a/contrib/cirrus/verify_source.sh b/contrib/cirrus/verify_source.sh new file mode 100755 index 000000000..860bafc00 --- /dev/null +++ b/contrib/cirrus/verify_source.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +set -e +source $(dirname $0)/lib.sh + +req_env_var " +OS_RELEASE_ID $OS_RELEASE_ID +OS_RELEASE_VER $OS_RELEASE_VER +" + +show_env_vars + +set -x +cd "$GOSRC" + +case "${OS_RELEASE_ID}-${OS_RELEASE_VER}" in + ubuntu-18) + make install.tools "BUILDTAGS=$BUILDTAGS" + make validate "BUILDTAGS=$BUILDTAGS" + # make lint "BUILDTAGS=$BUILDTAGS" + ;; + fedora-28) ;& + centos-7) ;& + rhel-7) + make install.tools + make validate + # make lint + ;; + *) bad_os_id_ver ;; +esac @@ -1,4 +1,4 @@ #!/bin/sh [ -f /etc/containers/nodocker ] || \ echo "Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg." >&2 -exec /usr/bin/podman $@ +exec /usr/bin/podman "$@" diff --git a/docs/podman-container-checkpoint.1.md b/docs/podman-container-checkpoint.1.md new file mode 100644 index 000000000..4906e0e12 --- /dev/null +++ b/docs/podman-container-checkpoint.1.md @@ -0,0 +1,30 @@ +% podman-container-checkpoint(1) + +## NAME +podman\-container\-checkpoint - Checkpoints one or more running containers + +## SYNOPSIS +**podman container checkpoint** [*options*] *container* ... + +## DESCRIPTION +Checkpoints all the processes in one or more containers. You may use container IDs or names as input. + +## OPTIONS +**-k**, **--keep** + +Keep all temporary log and statistics files created by CRIU during checkpointing. These files +are not deleted if checkpointing fails for further debugging. If checkpointing succeeds these +files are theoretically not needed, but if these files are needed Podman can keep the files +for further analysis. + +## EXAMPLE + +podman container checkpoint mywebserver + +podman container checkpoint 860a4b23 + +## SEE ALSO +podman(1), podman-container-restore(1) + +## HISTORY +September 2018, Originally compiled by Adrian Reber <areber@redhat.com> diff --git a/docs/podman-container-restore.1.md b/docs/podman-container-restore.1.md new file mode 100644 index 000000000..6360bccb0 --- /dev/null +++ b/docs/podman-container-restore.1.md @@ -0,0 +1,37 @@ +% podman-container-restore(1) + +## NAME +podman\-container\-restore - Restores one or more running containers + +## SYNOPSIS +**podman container restore** [*options*] *container* ... + +## DESCRIPTION +Restores a container from a checkpoint. You may use container IDs or names as input. + +## OPTIONS +**-k**, **--keep** + +Keep all temporary log and statistics files created by CRIU during +checkpointing as well as restoring. These files are not deleted if restoring +fails for further debugging. If restoring succeeds these files are +theoretically not needed, but if these files are needed Podman can keep the +files for further analysis. This includes the checkpoint directory with all +files created during checkpointing. The size required by the checkpoint +directory is roughly the same as the amount of memory required by the +processes in the checkpointed container. + +Without the **-k**, **--keep** option the checkpoint will be consumed and cannot be used +again. + +## EXAMPLE + +podman container restore mywebserver + +podman container restore 860a4b23 + +## SEE ALSO +podman(1), podman-container-checkpoint(1) + +## HISTORY +September 2018, Originally compiled by Adrian Reber <areber@redhat.com> diff --git a/docs/podman-container.1.md b/docs/podman-container.1.md index bbc325823..eac3343d5 100644 --- a/docs/podman-container.1.md +++ b/docs/podman-container.1.md @@ -14,6 +14,7 @@ The container command allows you to manage containers | Command | Man Page | Description | | ------- | --------------------------------------------------- | ---------------------------------------------------------------------------- | | attach | [podman-attach(1)](podman-attach.1.md) | Attach to a running container. | +| checkpoint | [podman-container-checkpoint(1)](podman-container-checkpoint.1.md) | Checkpoints one or more containers. | | cleanup | [podman-container-cleanup(1)](podman-container-cleanup.1.md) | Cleanup containers network and mountpoints. | | commit | [podman-commit(1)](podman-commit.1.md) | Create new image based on the changed container. | | create | [podman-create(1)](podman-create.1.md) | Create a new container. | @@ -29,6 +30,7 @@ The container command allows you to manage containers | port | [podman-port(1)](podman-port.1.md) | List port mappings for the container. | | refresh | [podman-refresh(1)](podman-container-refresh.1.md) | Refresh the state of all containers | | restart | [podman-restart(1)](podman-restart.1.md) | Restart one or more containers. | +| restore | [podman-container-restore(1)](podman-container-restore.1.md) | Restores one or more containers from a checkpoint. | | rm | [podman-rm(1)](podman-rm.1.md) | Remove one or more containers. | | run | [podman-run(1)](podman-run.1.md) | Run a command in a container. | | start | [podman-start(1)](podman-start.1.md) | Starts one or more containers. | diff --git a/docs/tutorials/podman_tutorial.md b/docs/tutorials/podman_tutorial.md index a866b8eed..152d65a59 100644 --- a/docs/tutorials/podman_tutorial.md +++ b/docs/tutorials/podman_tutorial.md @@ -157,6 +157,28 @@ $ sudo podman top <container_id> 101 31889 31873 0 09:21 ? 00:00:00 nginx: worker process ``` +### Checkpointing the container +Checkpointing a container stops the container while writing the state of all processes in the container to disk. +With this a container can later be restored and continue running at exactly the same point in time as the +checkpoint. This capability requires CRIU 3.11 or later installed on the system. +To checkpoint the container use: +```console +$ sudo podman container checkpoint <container_id> +``` + +### Restoring the container +Restoring a container is only possible for a previously checkpointed container. The restored container will +continue to run at exactly the same point in time it was checkpointed. +To restore the container use: +```console +$ sudo podman container restore <container_id> +``` + +After being restored, the container will answer requests again as it did before checkpointing. +```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 192ccd347..93becb80d 100644 --- a/libpod/container_api.go +++ b/libpod/container_api.go @@ -832,3 +832,33 @@ func (c *Container) Refresh(ctx context.Context) error { return nil } + +// Checkpoint checkpoints a container +func (c *Container) Checkpoint(ctx context.Context, keep bool) error { + logrus.Debugf("Trying to checkpoint container %s", c) + if !c.batched { + c.lock.Lock() + defer c.lock.Unlock() + + if err := c.syncContainer(); err != nil { + return err + } + } + + return c.checkpoint(ctx, keep) +} + +// Restore restores a container +func (c *Container) Restore(ctx context.Context, keep bool) (err error) { + logrus.Debugf("Trying to restore container %s", c) + if !c.batched { + c.lock.Lock() + defer c.lock.Unlock() + + if err := c.syncContainer(); err != nil { + return err + } + } + + return c.restore(ctx, keep) +} diff --git a/libpod/container_easyjson.go b/libpod/container_easyjson.go index 2d0481f3b..916118aec 100644 --- a/libpod/container_easyjson.go +++ b/libpod/container_easyjson.go @@ -1,3 +1,5 @@ +// +build seccomp ostree selinux varlink exclude_graphdriver_devicemapper + // Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. package libpod diff --git a/libpod/container_internal.go b/libpod/container_internal.go index 033426817..77bba9e85 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -15,9 +15,9 @@ import ( "github.com/containers/libpod/pkg/chrootuser" "github.com/containers/libpod/pkg/hooks" "github.com/containers/libpod/pkg/hooks/exec" + "github.com/containers/libpod/pkg/resolvconf" "github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/secrets" - "github.com/containers/libpod/pkg/util" "github.com/containers/storage" "github.com/containers/storage/pkg/archive" "github.com/containers/storage/pkg/chrootarchive" @@ -129,6 +129,11 @@ func (c *Container) ControlSocketPath() string { return filepath.Join(c.bundlePath(), "ctl") } +// CheckpointPath returns the path to the directory containing the checkpoint +func (c *Container) CheckpointPath() string { + return filepath.Join(c.bundlePath(), "checkpoint") +} + // AttachSocketPath retrieves the path of the container's attach socket func (c *Container) AttachSocketPath() string { return filepath.Join(c.runtime.ociRuntime.socketsDir, c.ID(), "attach") @@ -523,7 +528,7 @@ func (c *Container) init(ctx context.Context) error { } // With the spec complete, do an OCI create - if err := c.runtime.ociRuntime.createContainer(c, c.config.CgroupParent); err != nil { + if err := c.runtime.ociRuntime.createContainer(c, c.config.CgroupParent, false); err != nil { return err } @@ -1012,12 +1017,6 @@ func (c *Container) writeStringToRundir(destFile, output string) (string, error) return filepath.Join(c.state.DestinationRunDir, destFile), nil } -type resolvConf struct { - nameServers []string - searchDomains []string - options []string -} - // generateResolvConf generates a containers resolv.conf func (c *Container) generateResolvConf() (string, error) { // Determine the endpoint for resolv.conf in case it is a symlink @@ -1025,86 +1024,56 @@ func (c *Container) generateResolvConf() (string, error) { if err != nil { return "", err } - orig, err := ioutil.ReadFile(resolvPath) + + contents, err := ioutil.ReadFile(resolvPath) if err != nil { return "", errors.Wrapf(err, "unable to read %s", resolvPath) } - if len(c.config.DNSServer) == 0 && len(c.config.DNSSearch) == 0 && len(c.config.DNSOption) == 0 { - return c.writeStringToRundir("resolv.conf", fmt.Sprintf("%s", orig)) - } - // Read and organize the hosts /etc/resolv.conf - resolv := createResolv(string(orig[:])) - - // Populate the resolv struct with user's dns search domains - if len(c.config.DNSSearch) > 0 { - resolv.searchDomains = nil - // The . character means the user doesnt want any search domains in the container - if !util.StringInSlice(".", c.config.DNSSearch) { - resolv.searchDomains = append(resolv.searchDomains, c.Config().DNSSearch...) - } + // Process the file to remove localhost nameservers + // TODO: set ipv6 enable bool more sanely + resolv, err := resolvconf.FilterResolvDNS(contents, true) + if err != nil { + return "", errors.Wrapf(err, "error parsing host resolv.conf") } - // Populate the resolv struct with user's dns servers + // Make a new resolv.conf + nameservers := resolvconf.GetNameservers(resolv.Content) if len(c.config.DNSServer) > 0 { - resolv.nameServers = nil - for _, i := range c.config.DNSServer { - resolv.nameServers = append(resolv.nameServers, i.String()) + // We store DNS servers as net.IP, so need to convert to string + nameservers = []string{} + for _, server := range c.config.DNSServer { + nameservers = append(nameservers, server.String()) } } - // Populate the resolve struct with the users dns options + search := resolvconf.GetSearchDomains(resolv.Content) + if len(c.config.DNSSearch) > 0 { + search = c.config.DNSSearch + } + + options := resolvconf.GetOptions(resolv.Content) if len(c.config.DNSOption) > 0 { - resolv.options = nil - resolv.options = append(resolv.options, c.Config().DNSOption...) + options = c.config.DNSOption } - return c.writeStringToRundir("resolv.conf", resolv.ToString()) -} -// createResolv creates a resolv struct from an input string -func createResolv(input string) resolvConf { - var resolv resolvConf - for _, line := range strings.Split(input, "\n") { - if strings.HasPrefix(line, "search") { - fields := strings.Fields(line) - if len(fields) < 2 { - logrus.Debugf("invalid resolv.conf line %s", line) - continue - } - resolv.searchDomains = append(resolv.searchDomains, fields[1:]...) - } else if strings.HasPrefix(line, "nameserver") { - fields := strings.Fields(line) - if len(fields) < 2 { - logrus.Debugf("invalid resolv.conf line %s", line) - continue - } - resolv.nameServers = append(resolv.nameServers, fields[1]) - } else if strings.HasPrefix(line, "options") { - fields := strings.Fields(line) - if len(fields) < 2 { - logrus.Debugf("invalid resolv.conf line %s", line) - continue - } - resolv.options = append(resolv.options, fields[1:]...) - } + destPath := filepath.Join(c.state.RunDir, "resolv.conf") + + if err := os.Remove(destPath); err != nil && !os.IsNotExist(err) { + return "", errors.Wrapf(err, "error removing resolv.conf for container %s", c.ID()) } - return resolv -} -//ToString returns a resolv struct in the form of a resolv.conf -func (r resolvConf) ToString() string { - var result string - // Populate the output string with search domains - result += fmt.Sprintf("search %s\n", strings.Join(r.searchDomains, " ")) - // Populate the output string with name servers - for _, i := range r.nameServers { - result += fmt.Sprintf("nameserver %s\n", i) + // Build resolv.conf + if _, err = resolvconf.Build(destPath, nameservers, search, options); err != nil { + return "", errors.Wrapf(err, "error building resolv.conf for container %s") } - // Populate the output string with dns options - for _, i := range r.options { - result += fmt.Sprintf("options %s\n", i) + + // Relabel resolv.conf for the container + if err := label.Relabel(destPath, c.config.MountLabel, false); err != nil { + return "", err } - return result + + return filepath.Join(c.state.DestinationRunDir, "resolv.conf"), nil } // generateHosts creates a containers hosts file diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index b77beaf64..0353124dd 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -4,12 +4,18 @@ package libpod import ( "context" + "encoding/json" "fmt" + "io/ioutil" + "net" + "os" "path" + "path/filepath" "strings" "syscall" "time" + cnitypes "github.com/containernetworking/cni/pkg/types/current" crioAnnotations "github.com/containers/libpod/pkg/annotations" "github.com/containers/libpod/pkg/chrootuser" "github.com/containers/libpod/pkg/rootless" @@ -307,3 +313,155 @@ func (c *Container) addNamespaceContainer(g *generate.Generator, ns LinuxNS, ctr return nil } + +func (c *Container) checkpoint(ctx context.Context, keep bool) (err error) { + + if c.state.State != ContainerStateRunning { + return errors.Wrapf(ErrCtrStateInvalid, "%q is not running, cannot checkpoint", c.state.State) + } + if err := c.runtime.ociRuntime.checkpointContainer(c); err != nil { + return err + } + + // Save network.status. This is needed to restore the container with + // the same IP. Currently limited to one IP address in a container + // with one interface. + formatJSON, err := json.MarshalIndent(c.state.NetworkStatus, "", " ") + if err != nil { + return err + } + if err := ioutil.WriteFile(filepath.Join(c.bundlePath(), "network.status"), formatJSON, 0644); err != nil { + return err + } + + logrus.Debugf("Checkpointed container %s", c.ID()) + + c.state.State = ContainerStateStopped + + // Cleanup Storage and Network + if err := c.cleanup(ctx); err != nil { + return err + } + + if !keep { + // Remove log file + os.Remove(filepath.Join(c.bundlePath(), "dump.log")) + // Remove statistic file + os.Remove(filepath.Join(c.bundlePath(), "stats-dump")) + } + + return c.save() +} + +func (c *Container) restore(ctx context.Context, keep bool) (err error) { + + if (c.state.State != ContainerStateConfigured) && (c.state.State != ContainerStateExited) { + return errors.Wrapf(ErrCtrStateInvalid, "container %s is running or paused, cannot restore", c.ID()) + } + + // 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) { + return errors.Wrapf(err, "A complete checkpoint for this container cannot be found, cannot restore") + } + + // 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 { + // The file with the network.status does exist. Let's restore the + // container with the same IP address as during checkpointing. + defer networkStatusFile.Close() + var networkStatus []*cnitypes.Result + networkJSON, err := ioutil.ReadAll(networkStatusFile) + if err != nil { + return err + } + json.Unmarshal(networkJSON, &networkStatus) + // Take the first IP address + var IP net.IP + if len(networkStatus) > 0 { + if len(networkStatus[0].IPs) > 0 { + IP = networkStatus[0].IPs[0].Address.IP + } + } + if IP != nil { + env := fmt.Sprintf("IP=%s", IP) + // Tell CNI which IP address we want. + os.Setenv("CNI_ARGS", env) + logrus.Debugf("Restoring container with %s", env) + } + } + + if err := c.prepare(); err != nil { + return err + } + defer func() { + if err != nil { + if err2 := c.cleanup(ctx); err2 != nil { + logrus.Errorf("error cleaning up container %s: %v", c.ID(), err2) + } + } + }() + + // TODO: use existing way to request static IPs, once it is merged in ocicni + // https://github.com/cri-o/ocicni/pull/23/ + + // CNI_ARGS was used to request a certain IP address. Unconditionally remove it. + os.Unsetenv("CNI_ARGS") + + // Read config + jsonPath := filepath.Join(c.bundlePath(), "config.json") + logrus.Debugf("generate.NewFromFile at %v", jsonPath) + g, err := generate.NewFromFile(jsonPath) + if err != nil { + logrus.Debugf("generate.NewFromFile failed with %v", err) + return err + } + + // 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 { + return err + } + + if err := c.makeBindMounts(); err != nil { + return err + } + + // Cleanup for a working restore. + c.removeConmonFiles() + + if err := c.runtime.ociRuntime.createContainer(c, c.config.CgroupParent, true); err != nil { + return err + } + + logrus.Debugf("Restored container %s", c.ID()) + + c.state.State = ContainerStateRunning + + if !keep { + // Delete all checkpoint related files. At this point, in theory, all files + // should exist. Still ignoring errors for now as the container should be + // restored and running. Not erroring out just because some cleanup operation + // failed. Starting with the checkpoint directory + err = os.RemoveAll(c.CheckpointPath()) + if err != nil { + logrus.Debugf("Non-fatal: removal of checkpoint directory (%s) failed: %v", c.CheckpointPath(), err) + } + cleanup := [...]string{"restore.log", "dump.log", "stats-dump", "stats-restore", "network.status"} + for _, delete := range cleanup { + file := filepath.Join(c.bundlePath(), delete) + err = os.Remove(file) + if err != nil { + logrus.Debugf("Non-fatal: removal of checkpoint file (%s) failed: %v", file, err) + } + } + } + + return c.save() +} diff --git a/libpod/container_internal_unsupported.go b/libpod/container_internal_unsupported.go index 45b54efab..eed0449a9 100644 --- a/libpod/container_internal_unsupported.go +++ b/libpod/container_internal_unsupported.go @@ -27,3 +27,11 @@ func (c *Container) cleanupNetwork() error { func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { return nil, ErrNotImplemented } + +func (c *Container) checkpoint(ctx context.Context, keep bool) error { + return ErrNotImplemented +} + +func (c *Container) restore(ctx context.Context, keep bool) error { + return ErrNotImplemented +} diff --git a/libpod/oci.go b/libpod/oci.go index e5db06540..f6d320017 100644 --- a/libpod/oci.go +++ b/libpod/oci.go @@ -227,7 +227,7 @@ func bindPorts(ports []ocicni.PortMapping) ([]*os.File, error) { return files, nil } -func (r *OCIRuntime) createOCIContainer(ctr *Container, cgroupParent string) (err error) { +func (r *OCIRuntime) createOCIContainer(ctr *Container, cgroupParent string, restoreContainer bool) (err error) { var stderrBuf bytes.Buffer runtimeDir, err := GetRootlessRuntimeDir() @@ -289,6 +289,10 @@ func (r *OCIRuntime) createOCIContainer(ctr *Container, cgroupParent string) (er args = append(args, "--syslog") } + if restoreContainer { + args = append(args, "--restore", ctr.CheckpointPath()) + } + logrus.WithFields(logrus.Fields{ "args": args, }).Debugf("running conmon: %s", r.conmonPath) @@ -452,9 +456,20 @@ func (r *OCIRuntime) updateContainerStatus(ctr *Container) error { cmd := exec.Command(r.path, "state", ctr.ID()) cmd.Env = append(cmd.Env, fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)) - - out, err := cmd.CombinedOutput() + outPipe, err := cmd.StdoutPipe() + if err != nil { + return errors.Wrapf(err, "getting stdout pipe") + } + errPipe, err := cmd.StderrPipe() if err != nil { + return errors.Wrapf(err, "getting stderr pipe") + } + + if err := cmd.Start(); err != nil { + out, err2 := ioutil.ReadAll(errPipe) + if err2 != nil { + return errors.Wrapf(err, "error getting container %s state", ctr.ID()) + } if strings.Contains(string(out), "does not exist") { ctr.removeConmonFiles() ctr.state.State = ContainerStateExited @@ -462,6 +477,12 @@ func (r *OCIRuntime) updateContainerStatus(ctr *Container) error { } return errors.Wrapf(err, "error getting container %s state. stderr/out: %s", ctr.ID(), out) } + + errPipe.Close() + out, err := ioutil.ReadAll(outPipe) + if err != nil { + return errors.Wrapf(err, "error reading stdout: %s", ctr.ID()) + } if err := json.NewDecoder(bytes.NewBuffer(out)).Decode(state); err != nil { return errors.Wrapf(err, "error decoding container status for container %s", ctr.ID()) } @@ -535,7 +556,12 @@ func (r *OCIRuntime) updateContainerStatus(ctr *Container) error { // Sets time the container was started, but does not save it. func (r *OCIRuntime) startContainer(ctr *Container) error { // TODO: streams should probably *not* be our STDIN/OUT/ERR - redirect to buffers? - if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, "start", ctr.ID()); err != nil { + runtimeDir, err := GetRootlessRuntimeDir() + if err != nil { + return err + } + env := []string{fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)} + if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, "start", ctr.ID()); err != nil { return err } @@ -547,7 +573,12 @@ func (r *OCIRuntime) startContainer(ctr *Container) error { // killContainer sends the given signal to the given container func (r *OCIRuntime) killContainer(ctr *Container, signal uint) error { logrus.Debugf("Sending signal %d to container %s", signal, ctr.ID()) - if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, "kill", ctr.ID(), fmt.Sprintf("%d", signal)); err != nil { + runtimeDir, err := GetRootlessRuntimeDir() + if err != nil { + return err + } + env := []string{fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)} + if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, "kill", ctr.ID(), fmt.Sprintf("%d", signal)); err != nil { return errors.Wrapf(err, "error sending signal to container %s", ctr.ID()) } @@ -605,7 +636,12 @@ func (r *OCIRuntime) stopContainer(ctr *Container, timeout uint) error { args = []string{"kill", "--all", ctr.ID(), "KILL"} } - if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, args...); err != nil { + runtimeDir, err := GetRootlessRuntimeDir() + if err != nil { + return err + } + env := []string{fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)} + if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, args...); err != nil { // Again, check if the container is gone. If it is, exit cleanly. err := unix.Kill(ctr.state.PID, 0) if err == unix.ESRCH { @@ -631,12 +667,22 @@ func (r *OCIRuntime) deleteContainer(ctr *Container) error { // pauseContainer pauses the given container func (r *OCIRuntime) pauseContainer(ctr *Container) error { - return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, "pause", ctr.ID()) + runtimeDir, err := GetRootlessRuntimeDir() + if err != nil { + return err + } + env := []string{fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)} + return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, "pause", ctr.ID()) } // unpauseContainer unpauses the given container func (r *OCIRuntime) unpauseContainer(ctr *Container) error { - return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, "resume", ctr.ID()) + runtimeDir, err := GetRootlessRuntimeDir() + if err != nil { + return err + } + env := []string{fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)} + return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, "resume", ctr.ID()) } // execContainer executes a command in a running container @@ -734,13 +780,18 @@ func (r *OCIRuntime) execStopContainer(ctr *Container, timeout uint) error { if len(execSessions) == 0 { return nil } + runtimeDir, err := GetRootlessRuntimeDir() + if err != nil { + return err + } + env := []string{fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)} // If timeout is 0, just use SIGKILL if timeout > 0 { // Stop using SIGTERM by default // Use SIGSTOP after a timeout logrus.Debugf("Killing all processes in container %s with SIGTERM", ctr.ID()) - if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, "kill", "--all", ctr.ID(), "TERM"); err != nil { + if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, "kill", "--all", ctr.ID(), "TERM"); err != nil { return errors.Wrapf(err, "error sending SIGTERM to container %s processes", ctr.ID()) } @@ -755,7 +806,7 @@ func (r *OCIRuntime) execStopContainer(ctr *Container, timeout uint) error { // Send SIGKILL logrus.Debugf("Killing all processes in container %s with SIGKILL", ctr.ID()) - if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, "kill", "--all", ctr.ID(), "KILL"); err != nil { + if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, "kill", "--all", ctr.ID(), "KILL"); err != nil { return errors.Wrapf(err, "error sending SIGKILL to container %s processes", ctr.ID()) } @@ -766,3 +817,15 @@ func (r *OCIRuntime) execStopContainer(ctr *Container, timeout uint) error { return nil } + +// checkpointContainer checkpoints the given container +func (r *OCIRuntime) checkpointContainer(ctr *Container) error { + // imagePath is used by CRIU to store the actual checkpoint files + imagePath := ctr.CheckpointPath() + // workPath will be used to store dump.log and stats-dump + workPath := ctr.bundlePath() + logrus.Debugf("Writing checkpoint to %s", imagePath) + logrus.Debugf("Writing checkpoint logs to %s", workPath) + return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, "checkpoint", + "--image-path", imagePath, "--work-path", workPath, ctr.ID()) +} diff --git a/libpod/oci_linux.go b/libpod/oci_linux.go index 210ba57d1..0447670b3 100644 --- a/libpod/oci_linux.go +++ b/libpod/oci_linux.go @@ -63,10 +63,10 @@ func newPipe() (parent *os.File, child *os.File, err error) { // CreateContainer creates a container in the OCI runtime // TODO terminal support for container // Presently just ignoring conmon opts related to it -func (r *OCIRuntime) createContainer(ctr *Container, cgroupParent string) (err error) { +func (r *OCIRuntime) createContainer(ctr *Container, cgroupParent string, restoreContainer bool) (err error) { if ctr.state.UserNSRoot == "" { // no need of an intermediate mount ns - return r.createOCIContainer(ctr, cgroupParent) + return r.createOCIContainer(ctr, cgroupParent, restoreContainer) } var wg sync.WaitGroup wg.Add(1) @@ -103,7 +103,7 @@ func (r *OCIRuntime) createContainer(ctr *Container, cgroupParent string) (err e if err != nil { return } - err = r.createOCIContainer(ctr, cgroupParent) + err = r.createOCIContainer(ctr, cgroupParent, restoreContainer) }() wg.Wait() diff --git a/libpod/oci_unsupported.go b/libpod/oci_unsupported.go index 8cb4994d3..b133eb402 100644 --- a/libpod/oci_unsupported.go +++ b/libpod/oci_unsupported.go @@ -15,7 +15,7 @@ func newPipe() (parent *os.File, child *os.File, err error) { return nil, nil, ErrNotImplemented } -func (r *OCIRuntime) createContainer(ctr *Container, cgroupParent string) (err error) { +func (r *OCIRuntime) createContainer(ctr *Container, cgroupParent string, restoreContainer bool) (err error) { return ErrNotImplemented } diff --git a/pkg/resolvconf/dns/resolvconf.go b/pkg/resolvconf/dns/resolvconf.go new file mode 100644 index 000000000..cb4bd1033 --- /dev/null +++ b/pkg/resolvconf/dns/resolvconf.go @@ -0,0 +1,28 @@ +// Originally from github.com/docker/libnetwork/resolvconf/dns + +package dns + +import ( + "regexp" +) + +// IPLocalhost is a regex pattern for IPv4 or IPv6 loopback range. +const IPLocalhost = `((127\.([0-9]{1,3}\.){2}[0-9]{1,3})|(::1)$)` + +// IPv4Localhost is a regex pattern for IPv4 localhost address range. +const IPv4Localhost = `(127\.([0-9]{1,3}\.){2}[0-9]{1,3})` + +var localhostIPRegexp = regexp.MustCompile(IPLocalhost) +var localhostIPv4Regexp = regexp.MustCompile(IPv4Localhost) + +// IsLocalhost returns true if ip matches the localhost IP regular expression. +// Used for determining if nameserver settings are being passed which are +// localhost addresses +func IsLocalhost(ip string) bool { + return localhostIPRegexp.MatchString(ip) +} + +// IsIPv4Localhost returns true if ip matches the IPv4 localhost regular expression. +func IsIPv4Localhost(ip string) bool { + return localhostIPv4Regexp.MatchString(ip) +} diff --git a/pkg/resolvconf/resolvconf.go b/pkg/resolvconf/resolvconf.go new file mode 100644 index 000000000..fccd60093 --- /dev/null +++ b/pkg/resolvconf/resolvconf.go @@ -0,0 +1,242 @@ +// Package resolvconf provides utility code to query and update DNS configuration in /etc/resolv.conf. +// Originally from github.com/docker/libnetwork/resolvconf. +package resolvconf + +import ( + "bytes" + "io/ioutil" + "regexp" + "strings" + "sync" + + "github.com/containers/libpod/pkg/resolvconf/dns" + "github.com/docker/docker/pkg/ioutils" + "github.com/sirupsen/logrus" +) + +const ( + // DefaultResolvConf points to the default file used for dns configuration on a linux machine + DefaultResolvConf = "/etc/resolv.conf" +) + +var ( + // Note: the default IPv4 & IPv6 resolvers are set to Google's Public DNS + defaultIPv4Dns = []string{"nameserver 8.8.8.8", "nameserver 8.8.4.4"} + defaultIPv6Dns = []string{"nameserver 2001:4860:4860::8888", "nameserver 2001:4860:4860::8844"} + ipv4NumBlock = `(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)` + ipv4Address = `(` + ipv4NumBlock + `\.){3}` + ipv4NumBlock + // This is not an IPv6 address verifier as it will accept a super-set of IPv6, and also + // will *not match* IPv4-Embedded IPv6 Addresses (RFC6052), but that and other variants + // -- e.g. other link-local types -- either won't work in containers or are unnecessary. + // For readability and sufficiency for Docker purposes this seemed more reasonable than a + // 1000+ character regexp with exact and complete IPv6 validation + ipv6Address = `([0-9A-Fa-f]{0,4}:){2,7}([0-9A-Fa-f]{0,4})(%\w+)?` + + localhostNSRegexp = regexp.MustCompile(`(?m)^nameserver\s+` + dns.IPLocalhost + `\s*\n*`) + nsIPv6Regexp = regexp.MustCompile(`(?m)^nameserver\s+` + ipv6Address + `\s*\n*`) + nsRegexp = regexp.MustCompile(`^\s*nameserver\s*((` + ipv4Address + `)|(` + ipv6Address + `))\s*$`) + searchRegexp = regexp.MustCompile(`^\s*search\s*(([^\s]+\s*)*)$`) + optionsRegexp = regexp.MustCompile(`^\s*options\s*(([^\s]+\s*)*)$`) +) + +var lastModified struct { + sync.Mutex + sha256 string + contents []byte +} + +// File contains the resolv.conf content and its hash +type File struct { + Content []byte + Hash string +} + +// Get returns the contents of /etc/resolv.conf and its hash +func Get() (*File, error) { + return GetSpecific(DefaultResolvConf) +} + +// GetSpecific returns the contents of the user specified resolv.conf file and its hash +func GetSpecific(path string) (*File, error) { + resolv, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + hash, err := ioutils.HashData(bytes.NewReader(resolv)) + if err != nil { + return nil, err + } + return &File{Content: resolv, Hash: hash}, nil +} + +// GetIfChanged retrieves the host /etc/resolv.conf file, checks against the last hash +// and, if modified since last check, returns the bytes and new hash. +// This feature is used by the resolv.conf updater for containers +func GetIfChanged() (*File, error) { + lastModified.Lock() + defer lastModified.Unlock() + + resolv, err := ioutil.ReadFile("/etc/resolv.conf") + if err != nil { + return nil, err + } + newHash, err := ioutils.HashData(bytes.NewReader(resolv)) + if err != nil { + return nil, err + } + if lastModified.sha256 != newHash { + lastModified.sha256 = newHash + lastModified.contents = resolv + return &File{Content: resolv, Hash: newHash}, nil + } + // nothing changed, so return no data + return nil, nil +} + +// GetLastModified retrieves the last used contents and hash of the host resolv.conf. +// Used by containers updating on restart +func GetLastModified() *File { + lastModified.Lock() + defer lastModified.Unlock() + + return &File{Content: lastModified.contents, Hash: lastModified.sha256} +} + +// FilterResolvDNS cleans up the config in resolvConf. It has two main jobs: +// 1. It looks for localhost (127.*|::1) entries in the provided +// resolv.conf, removing local nameserver entries, and, if the resulting +// cleaned config has no defined nameservers left, adds default DNS entries +// 2. Given the caller provides the enable/disable state of IPv6, the filter +// code will remove all IPv6 nameservers if it is not enabled for containers +// +func FilterResolvDNS(resolvConf []byte, ipv6Enabled bool) (*File, error) { + cleanedResolvConf := localhostNSRegexp.ReplaceAll(resolvConf, []byte{}) + // if IPv6 is not enabled, also clean out any IPv6 address nameserver + if !ipv6Enabled { + cleanedResolvConf = nsIPv6Regexp.ReplaceAll(cleanedResolvConf, []byte{}) + } + // if the resulting resolvConf has no more nameservers defined, add appropriate + // default DNS servers for IPv4 and (optionally) IPv6 + if len(GetNameservers(cleanedResolvConf)) == 0 { + logrus.Infof("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers: %v", defaultIPv4Dns) + dns := defaultIPv4Dns + if ipv6Enabled { + logrus.Infof("IPv6 enabled; Adding default IPv6 external servers: %v", defaultIPv6Dns) + dns = append(dns, defaultIPv6Dns...) + } + cleanedResolvConf = append(cleanedResolvConf, []byte("\n"+strings.Join(dns, "\n"))...) + } + hash, err := ioutils.HashData(bytes.NewReader(cleanedResolvConf)) + if err != nil { + return nil, err + } + return &File{Content: cleanedResolvConf, Hash: hash}, nil +} + +// getLines parses input into lines and strips away comments. +func getLines(input []byte, commentMarker []byte) [][]byte { + lines := bytes.Split(input, []byte("\n")) + var output [][]byte + for _, currentLine := range lines { + var commentIndex = bytes.Index(currentLine, commentMarker) + if commentIndex == -1 { + output = append(output, currentLine) + } else { + output = append(output, currentLine[:commentIndex]) + } + } + return output +} + +// GetNameservers returns nameservers (if any) listed in /etc/resolv.conf +func GetNameservers(resolvConf []byte) []string { + nameservers := []string{} + for _, line := range getLines(resolvConf, []byte("#")) { + ns := nsRegexp.FindSubmatch(line) + if len(ns) > 0 { + nameservers = append(nameservers, string(ns[1])) + } + } + return nameservers +} + +// GetNameserversAsCIDR returns nameservers (if any) listed in +// /etc/resolv.conf as CIDR blocks (e.g., "1.2.3.4/32") +// This function's output is intended for net.ParseCIDR +func GetNameserversAsCIDR(resolvConf []byte) []string { + nameservers := []string{} + for _, nameserver := range GetNameservers(resolvConf) { + var address string + // If IPv6, strip zone if present + if strings.Contains(nameserver, ":") { + address = strings.Split(nameserver, "%")[0] + "/128" + } else { + address = nameserver + "/32" + } + nameservers = append(nameservers, address) + } + return nameservers +} + +// GetSearchDomains returns search domains (if any) listed in /etc/resolv.conf +// If more than one search line is encountered, only the contents of the last +// one is returned. +func GetSearchDomains(resolvConf []byte) []string { + domains := []string{} + for _, line := range getLines(resolvConf, []byte("#")) { + match := searchRegexp.FindSubmatch(line) + if match == nil { + continue + } + domains = strings.Fields(string(match[1])) + } + return domains +} + +// GetOptions returns options (if any) listed in /etc/resolv.conf +// If more than one options line is encountered, only the contents of the last +// one is returned. +func GetOptions(resolvConf []byte) []string { + options := []string{} + for _, line := range getLines(resolvConf, []byte("#")) { + match := optionsRegexp.FindSubmatch(line) + if match == nil { + continue + } + options = strings.Fields(string(match[1])) + } + return options +} + +// Build writes a configuration file to path containing a "nameserver" entry +// for every element in dns, a "search" entry for every element in +// dnsSearch, and an "options" entry for every element in dnsOptions. +func Build(path string, dns, dnsSearch, dnsOptions []string) (*File, error) { + content := bytes.NewBuffer(nil) + if len(dnsSearch) > 0 { + if searchString := strings.Join(dnsSearch, " "); strings.Trim(searchString, " ") != "." { + if _, err := content.WriteString("search " + searchString + "\n"); err != nil { + return nil, err + } + } + } + for _, dns := range dns { + if _, err := content.WriteString("nameserver " + dns + "\n"); err != nil { + return nil, err + } + } + if len(dnsOptions) > 0 { + if optsString := strings.Join(dnsOptions, " "); strings.Trim(optsString, " ") != "" { + if _, err := content.WriteString("options " + optsString + "\n"); err != nil { + return nil, err + } + } + } + + hash, err := ioutils.HashData(bytes.NewReader(content.Bytes())) + if err != nil { + return nil, err + } + + return &File{Content: content.Bytes(), Hash: hash}, ioutil.WriteFile(path, content.Bytes(), 0644) +} diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go index 1ad55fc8c..4c855d659 100644 --- a/pkg/spec/spec.go +++ b/pkg/spec/spec.go @@ -417,7 +417,7 @@ func setupSystemd(config *CreateConfig, g *generate.Generator) error { return err } options := []string{"rw", "rprivate", "noexec", "nosuid", "nodev"} - for _, dest := range []string{"/run", "/run/lock", "/sys/fs/cgroup/systemd"} { + for _, dest := range []string{"/run", "/run/lock"} { if libpod.MountExists(mounts, dest) { continue } @@ -441,6 +441,13 @@ func setupSystemd(config *CreateConfig, g *generate.Generator) error { } g.AddMount(tmpfsMnt) } + tmpfsMnt := spec.Mount{ + Destination: "/sys/fs/cgroup/systemd", + Type: "tmpfs", + Source: "tmpfs", + Options: append(options, "size=65536k"), + } + g.AddMount(tmpfsMnt) return nil } diff --git a/test/e2e/checkpoint_test.go b/test/e2e/checkpoint_test.go new file mode 100644 index 000000000..6c5d891a0 --- /dev/null +++ b/test/e2e/checkpoint_test.go @@ -0,0 +1,129 @@ +package integration + +import ( + "fmt" + "os" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Podman checkpoint", func() { + var ( + tempdir string + err error + podmanTest PodmanTest + ) + + BeforeEach(func() { + tempdir, err = CreateTempDirInTempDir() + if err != nil { + os.Exit(1) + } + podmanTest = PodmanCreate(tempdir) + podmanTest.RestoreAllArtifacts() + // At least CRIU 3.11 is needed + skip, err := podmanTest.isCriuAtLeast(31100) + if err != nil || skip { + Skip("CRIU missing or too old.") + } + }) + + AfterEach(func() { + podmanTest.Cleanup() + f := CurrentGinkgoTestDescription() + timedResult := fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds()) + GinkgoWriter.Write([]byte(timedResult)) + }) + + It("podman checkpoint bogus container", func() { + session := podmanTest.Podman([]string{"container", "checkpoint", "foobar"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Not(Equal(0))) + }) + + It("podman restore bogus container", func() { + session := podmanTest.Podman([]string{"container", "restore", "foobar"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Not(Equal(0))) + }) + + It("podman checkpoint a running container by id", 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)) + cid := session.OutputToString() + + result := podmanTest.Podman([]string{"container", "checkpoint", cid}) + result.WaitWithDefaultTimeout() + + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) + Expect(podmanTest.GetContainerStatus()).To(ContainSubstring("Exited")) + + result = podmanTest.Podman([]string{"container", "restore", cid}) + result.WaitWithDefaultTimeout() + + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(1)) + Expect(podmanTest.GetContainerStatus()).To(ContainSubstring("Up")) + }) + + It("podman checkpoint a running container by name", func() { + session := podmanTest.Podman([]string{"run", "-it", "--security-opt", "seccomp=unconfined", "--name", "test_name", "-d", ALPINE, "top"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + result := podmanTest.Podman([]string{"container", "checkpoint", "test_name"}) + result.WaitWithDefaultTimeout() + + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) + Expect(podmanTest.GetContainerStatus()).To(ContainSubstring("Exited")) + + result = podmanTest.Podman([]string{"container", "restore", "test_name"}) + result.WaitWithDefaultTimeout() + + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(1)) + Expect(podmanTest.GetContainerStatus()).To(ContainSubstring("Up")) + }) + + It("podman pause a checkpointed container by id", func() { + session := podmanTest.Podman([]string{"run", "-it", "--security-opt", "seccomp=unconfined", "-d", ALPINE, "top"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + cid := session.OutputToString() + + result := podmanTest.Podman([]string{"container", "checkpoint", cid}) + result.WaitWithDefaultTimeout() + + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) + Expect(podmanTest.GetContainerStatus()).To(ContainSubstring("Exited")) + + result = podmanTest.Podman([]string{"pause", cid}) + result.WaitWithDefaultTimeout() + + Expect(result.ExitCode()).To(Equal(125)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) + Expect(podmanTest.GetContainerStatus()).To(ContainSubstring("Exited")) + + result = podmanTest.Podman([]string{"container", "restore", cid}) + result.WaitWithDefaultTimeout() + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(1)) + + result = podmanTest.Podman([]string{"rm", cid}) + result.WaitWithDefaultTimeout() + Expect(result.ExitCode()).To(Equal(125)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(1)) + + result = podmanTest.Podman([]string{"rm", "-f", cid}) + result.WaitWithDefaultTimeout() + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) + + }) +}) diff --git a/test/e2e/libpod_suite_test.go b/test/e2e/libpod_suite_test.go index d521632d7..a032b0e88 100644 --- a/test/e2e/libpod_suite_test.go +++ b/test/e2e/libpod_suite_test.go @@ -2,6 +2,7 @@ package integration import ( "bufio" + "bytes" "context" "encoding/json" "fmt" @@ -64,6 +65,7 @@ type PodmanTest struct { TempDir string CgroupManager string Host HostOS + CriuBinary string } // HostOS is a simple struct for the test os @@ -164,6 +166,7 @@ func PodmanCreate(tempDir string) PodmanTest { runCBinary = "/usr/bin/runc" } + criuBinary := "/usr/sbin/criu" CNIConfigDir := "/etc/cni/net.d" p := PodmanTest{ @@ -179,6 +182,7 @@ func PodmanCreate(tempDir string) PodmanTest { TempDir: tempDir, CgroupManager: cgroupManager, Host: host, + CriuBinary: criuBinary, } // Setup registries.conf ENV variable @@ -678,6 +682,39 @@ func (p *PodmanTest) setRegistriesConfigEnv(b []byte) { ioutil.WriteFile(outfile, b, 0644) } +func (p *PodmanTest) isCriuAtLeast(version int) (bool, error) { + cmd := exec.Command(p.CriuBinary, "-V") + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + return false, err + } + + var x int + var y int + var z int + + fmt.Sscanf(out.String(), "Version: %d.%d.%d", &x, &y, &z) + + if strings.Contains(out.String(), "GitID") { + // If CRIU is built from git it contains a git ID. + // If that is the case, increase minor by one as this + // could mean we are running a development version. + y = y + 1 + } + + parsed_version := x*10000 + y*100 + z + + fmt.Println(parsed_version) + + if parsed_version >= version { + return false, nil + } else { + return true, nil + } +} + func resetRegistriesConfigEnv() { os.Setenv("REGISTRIES_CONFIG_PATH", "") } |