diff options
33 files changed, 474 insertions, 29 deletions
diff --git a/.cirrus.yml b/.cirrus.yml index a11bbbe61..8b1036d7c 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -39,7 +39,7 @@ env: UBUNTU_NAME: "ubuntu" PRIOR_UBUNTU_NAME: "prior-ubuntu" - _BUILT_IMAGE_SUFFIX: "c5809900649447424" + _BUILT_IMAGE_SUFFIX: "c6110627968057344" FEDORA_CACHE_IMAGE_NAME: "${FEDORA_NAME}-${_BUILT_IMAGE_SUFFIX}" PRIOR_FEDORA_CACHE_IMAGE_NAME: "${PRIOR_FEDORA_NAME}-${_BUILT_IMAGE_SUFFIX}" UBUNTU_CACHE_IMAGE_NAME: "${UBUNTU_NAME}-${_BUILT_IMAGE_SUFFIX}" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 5e5a75713..8fd51b5e9 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -22,3 +22,4 @@ jobs: stale-pr-label: 'stale-pr' days-before-stale: 30 days-before-close: 365 + remove-stale-when-updated: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dc48b389e..ba321921c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,7 @@ that we follow. ## Topics * [Reporting Issues](#reporting-issues) +* [Working On Issues](#working-on-issues) * [Contributing to Podman](#contributing-to-podman) * [Continuous Integration](#continuous-integration) [![Build Status](https://api.cirrus-ci.com/github/containers/podman.svg)](https://cirrus-ci.com/github/containers/podman/master) * [Submitting Pull Requests](#submitting-pull-requests) @@ -28,6 +29,17 @@ The easier it is for us to reproduce it, the faster it'll be fixed! Please don't include any private/sensitive information in your issue! +## Working On Issues + +Once you have decided to contribute to Podman by working on an issue, check our +backlog of [open issues](https://github.com/containers/podman/issues) looking +for any that do not have an "In Progress" label attached to it. Often issues +will be assigned to someone, to be worked on at a later time. If you have the +time to work on the issue now add yourself as an assignee, and set the +"In Progress" label if you’re a member of the “Containers” GitHub organization. +If you can not set the label, just add a quick comment in the issue asking that +the “In Progress” label be set and a member will do so for you. + ## Contributing to Podman This section describes how to start a contribution to Podman. @@ -135,8 +135,13 @@ export PRINT_HELP_PYSCRIPT err_if_empty = $(if $(strip $($(1))),$(strip $($(1))),$(error Required variable $(1) value is undefined, whitespace, or empty)) .PHONY: help +ifneq (, ${PYTHON}) help: @$(PYTHON) -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) +else +help: + $(error python required for 'make help', executable not found) +endif .gopathok: ifeq ("$(wildcard $(GOPKGDIR))","") @@ -5,7 +5,7 @@ Podman (the POD MANager) is a tool for managing containers and images, volumes mounted into those containers, and pods made from groups of containers. Podman is based on libpod, a library for container lifecycle management that is also contained in this repository. The libpod library provides APIs for managing containers, pods, container images, and volumes. -* [Latest Version: 2.0.6](https://github.com/containers/podman/releases/latest) +* [Latest Version: 2.1.0](https://github.com/containers/podman/releases/latest) * Latest Remote client for Windows * Latest Remote client for MacOs * Latest Static Remote client for Linux diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index f3ac50a59..cabfafabb 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -15,6 +15,7 @@ - The `podman play kube` command now supports the Socket HostPath type ([#7112](https://github.com/containers/podman/issues/7112)). - The `podman play kube` command now supports read-only mounts. - The `podman play kube` command now supports setting labels on pods from Kubernetes metadata labels. +- The `podman play kube` command now supports setting container restart policy ([#7656](https://github.com/containers/podman/issues/7656)). - The `podman play kube` command now properly handles `HostAlias` entries. - The `podman generate kube` command now adds entries to `/etc/hosts` from `--host-add` generated YAML as `HostAlias` entries. - The `podman play kube` and `podman generate kube` commands now properly support `shareProcessNamespace` to share the PID namespace in pods. @@ -29,6 +30,9 @@ - A new global option has been added to Podman, `--runtime-flags`, which allows for setting flags to use when the OCI runtime is called. - The `podman manifest add` command now supports the `--cert-dir`, `--auth-file`, `--creds`, and `--tls-verify` options. +### Security +- This release resolves CVE-2020-14370, in which environment variables could be leaked between containers created using the Varlink API. + ### Changes - Podman will now retry pulling an image 3 times if a pull fails due to network errors. - The `podman exec` command would previously print error messages (e.g. `exec session exited with non-zero exit code -1`) when the command run exited with a non-0 exit code. It no longer does this. The `podman exec` command will still exit with the same exit code as the command run in the container did. @@ -72,8 +76,12 @@ - Fixed a bug where the `--infra-command` parameter to `podman pod create` was nonfunctional. - Fixed a bug where `podman auto-update` would fail for any container started with `--pull=always` ([#7407](https://github.com/containers/podman/issues/7407)). - Fixed a bug where the `podman wait` command would only accept a single argument. +- Fixed a bug where the parsing of the `--volumes-from` option to `podman run` and `podman create` was broken, making it impossible to use multiple mount options at the same time ([#7701](https://github.com/containers/podman/issues/7701)). +- Fixed a bug where the `podman exec` command would not join executed processes to the container's supplemental groups if the container was started with both the `--user` and `--group-add` options. +- Fixed a bug where the `--iidfile` option to `podman-remote build` was nonfunctional. ### API +- The Libpod API version has been bumped to v2.0.0 due to a breaking change in the Image List API. - Docker-compatible Volume Endpoints (Create, Inspect, List, Remove, Prune) are now available! - Added an endpoint for generating systemd unit files for containers. - The `last` parameter to the Libpod container list endpoint now has an alias, `limit` ([#6413](https://github.com/containers/podman/issues/6413)). @@ -96,6 +104,9 @@ - All non-hijacking responses to API requests should not include headers with the version of the server. - Fixed a bug where Libpod and Compat Events endpoints did not send response headers until the first event occurred ([#7263](https://github.com/containers/podman/issues/7263)). - Fixed a bug where the Build endpoints (Compat and Libpod) did not stream progress to the client. +- Fixed a bug where the Stats endpoints (Compat and Libpod) did not properly handle clients disconnecting. +- Fixed a bug where the Ignore parameter to the Libpod Stop endpoint was not performing properly. +- Fixed a bug where the Compat Logs endpoint for containers did not stream its output in the correct format ([#7196](https://github.com/containers/podman/issues/7196)). ### Misc - Updated Buildah to v1.16.1 diff --git a/changelog.txt b/changelog.txt index b98e91d63..0ec721996 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,32 @@ +- Changelog for v2.1.0 (2020-09-22): + * Update release notes for v2.1.0 Final Release + * Fix up attach tests for podman remote + * update stale bot + * [CI:DOCS] Add 'In Progress' note to CONTRIBUTING.md + * Restore 'id' stanza in pull results + * Fix podman image unmount to only report images unmounted + * libpod: bumps up rootless-cni-infra to 2 + * stats: log errors instead of sending 500 + * Fix incorrect parsing of create/run --volumes-from + * rootless-cni-infra: fix flakiness during bringing up lo interface + * Fix handling of podman-remote stop --ignore + * Refactor version handling in cmd tree + * Preserve groups in exec sessions in ctrs with --user + * Install bats as root + * Makefile: Fix broken libpodimage targets + * stats: detect closed client connection + * stats endpoint: write OK header once + * handle the play kube and generate kube for with restartPolicy + * fix the .Path and .Args when use the infra-command + * Update nix pin with `make nixpkgs` + * fix a typo of login.1.md + * Bump github.com/rootless-containers/rootlesskit from 0.10.0 to 0.10.1 + * enable --iidfile for podman-remote build + * update github.com/docker/docker and relevant deps + * Make Go builds more consistent + * dependabot-dance: new tool for managing revendor PRs + * WIP: Fix remote logs + - Changelog for v2.1.0-rc2 (2020-09-17) * Update release notes for Podman v2.1.0-RC2 * Fix play_kube_test deployment template diff --git a/cmd/podman/images/save.go b/cmd/podman/images/save.go index c57f61221..b164a2534 100644 --- a/cmd/podman/images/save.go +++ b/cmd/podman/images/save.go @@ -94,6 +94,7 @@ func save(cmd *cobra.Command, args []string) (finalErr error) { return errors.Errorf("--compress can only be set when --format is either 'oci-dir' or 'docker-dir'") } if len(saveOpts.Output) == 0 { + saveOpts.Quiet = true fi := os.Stdout if terminal.IsTerminal(int(fi.Fd())) { return errors.Errorf("refusing to save to terminal. Use -o flag or redirect") @@ -122,12 +123,6 @@ func save(cmd *cobra.Command, args []string) (finalErr error) { tags = args[1:] } - // Decide whether c/image's progress bars should use stderr or stdout. - // If the output is set of stdout, any log message there would corrupt - // the tarfile. - if saveOpts.Output == os.Stdout.Name() { - saveOpts.Quiet = true - } err := registry.ImageEngine().Save(context.Background(), args[0], tags, saveOpts) if err == nil { succeeded = true diff --git a/contrib/rootless-cni-infra/Containerfile b/contrib/rootless-cni-infra/Containerfile index 5be30ccc9..6bf70d644 100644 --- a/contrib/rootless-cni-infra/Containerfile +++ b/contrib/rootless-cni-infra/Containerfile @@ -34,4 +34,4 @@ COPY rootless-cni-infra /usr/local/bin ENV CNI_PATH=/opt/cni/bin CMD ["sleep", "infinity"] -ENV ROOTLESS_CNI_INFRA_VERSION=1 +ENV ROOTLESS_CNI_INFRA_VERSION=2 diff --git a/contrib/rootless-cni-infra/rootless-cni-infra b/contrib/rootless-cni-infra/rootless-cni-infra index f6622b23c..5cb43621d 100755 --- a/contrib/rootless-cni-infra/rootless-cni-infra +++ b/contrib/rootless-cni-infra/rootless-cni-infra @@ -4,6 +4,23 @@ set -eu ARG0="$0" BASE="/run/rootless-cni-infra" +wait_unshare_net() { + pid="$1" + # NOTE: busybox shell doesn't support the `for ((i=0; i < $MAX; i++)); do foo; done` statement + i=0 + while :; do + if [ "$(readlink /proc/self/ns/net)" != "$(readlink /proc/${pid}/ns/net)" ]; then + break + fi + sleep 0.1 + if [ $i -ge 10 ]; then + echo >&2 "/proc/${pid}/ns/net cannot be unshared" + exit 1 + fi + i=$((i + 1)) + done +} + # CLI subcommand: "alloc $CONTAINER_ID $NETWORK_NAME $POD_NAME" cmd_entrypoint_alloc() { if [ "$#" -ne 3 ]; then @@ -24,6 +41,7 @@ cmd_entrypoint_alloc() { else unshare -n sleep infinity & pid="$!" + wait_unshare_net "${pid}" echo "${pid}" >"${dir}/pid" nsenter -t "${pid}" -n ip link set lo up fi diff --git a/contrib/spec/podman.spec.in b/contrib/spec/podman.spec.in index 363aa60d7..2e266b59f 100644 --- a/contrib/spec/podman.spec.in +++ b/contrib/spec/podman.spec.in @@ -42,7 +42,7 @@ Epoch: 99 %else Epoch: 0 %endif -Version: 2.1.0 +Version: 2.2.0 Release: #COMMITDATE#.git%{shortcommit0}%{?dist} Summary: Manage Pods, Containers and Container Images License: ASL 2.0 diff --git a/libpod/define/info.go b/libpod/define/info.go index 47c53d067..f0e05801c 100644 --- a/libpod/define/info.go +++ b/libpod/define/info.go @@ -15,6 +15,7 @@ type Info struct { type HostInfo struct { Arch string `json:"arch"` BuildahVersion string `json:"buildahVersion"` + CgroupManager string `json:"cgroupManager"` CGroupsVersion string `json:"cgroupVersion"` Conmon *ConmonInfo `json:"conmon"` CPUs int `json:"cpus"` diff --git a/libpod/info.go b/libpod/info.go index 153000b6f..dd7a521c1 100644 --- a/libpod/info.go +++ b/libpod/info.go @@ -87,6 +87,7 @@ func (r *Runtime) hostInfo() (*define.HostInfo, error) { info := define.HostInfo{ Arch: runtime.GOARCH, BuildahVersion: buildah.Version, + CgroupManager: r.config.Engine.CgroupManager, Linkmode: linkmode.Linkmode(), CPUs: runtime.NumCPU(), Distribution: hostDistributionInfo, diff --git a/libpod/rootless_cni_linux.go b/libpod/rootless_cni_linux.go index 7feec6b44..2877191e5 100644 --- a/libpod/rootless_cni_linux.go +++ b/libpod/rootless_cni_linux.go @@ -25,7 +25,7 @@ import ( // Built from ../contrib/rootless-cni-infra. var rootlessCNIInfraImage = map[string]string{ - "amd64": "quay.io/libpod/rootless-cni-infra@sha256:8aa681c4c08dee3ec5d46ff592fddd0259a35626717006d6b77ee786b1d02967", // 1-amd64 + "amd64": "quay.io/libpod/rootless-cni-infra@sha256:e92c3a6367f8e554121b96d39af1f19f0f9ac5a32922b290112e13bc661d3a29", // 2-amd64 } const ( diff --git a/pkg/api/handlers/libpod/images_pull.go b/pkg/api/handlers/libpod/images_pull.go index 8a2f4f4cf..ad8d1f38e 100644 --- a/pkg/api/handlers/libpod/images_pull.go +++ b/pkg/api/handlers/libpod/images_pull.go @@ -178,10 +178,19 @@ loop: // break out of for/select infinite loop flush() case <-runCtx.Done(): if !failed { + // Send all image id's pulled in 'images' stanza report.Images = images if err := enc.Encode(report); err != nil { logrus.Warnf("Failed to json encode error %q", err.Error()) } + + report.Images = nil + // Pull last ID from list and publish in 'id' stanza. This maintains previous API contract + report.ID = images[len(images)-1] + if err := enc.Encode(report); err != nil { + logrus.Warnf("Failed to json encode error %q", err.Error()) + } + flush() } break loop // break out of for/select infinite loop diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index 0ccaa95bb..9e503dbb0 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -33,7 +33,7 @@ type LibpodImagesLoadReport struct { } type LibpodImagesPullReport struct { - ID string `json:"id"` + entities.ImagePullReport } // LibpodImagesRemoveReport is the return type for image removal via the rest diff --git a/pkg/api/server/handler_api.go b/pkg/api/server/handler_api.go index f2ce0301b..920811c51 100644 --- a/pkg/api/server/handler_api.go +++ b/pkg/api/server/handler_api.go @@ -34,15 +34,18 @@ func (s *APIServer) APIHandler(h http.HandlerFunc) http.HandlerFunc { } // TODO: Use r.ConnContext when ported to go 1.13 - c := context.WithValue(r.Context(), "decoder", s.Decoder) //nolint - c = context.WithValue(c, "runtime", s.Runtime) //nolint - c = context.WithValue(c, "shutdownFunc", s.Shutdown) //nolint - c = context.WithValue(c, "idletracker", s.idleTracker) //nolint + c := context.WithValue(r.Context(), "decoder", s.Decoder) // nolint + c = context.WithValue(c, "runtime", s.Runtime) // nolint + c = context.WithValue(c, "shutdownFunc", s.Shutdown) // nolint + c = context.WithValue(c, "idletracker", s.idleTracker) // nolint r = r.WithContext(c) - v := utils.APIVersion[utils.CompatTree][utils.CurrentAPIVersion] - w.Header().Set("API-Version", fmt.Sprintf("%d.%d", v.Major, v.Minor)) - w.Header().Set("Libpod-API-Version", utils.APIVersion[utils.LibpodTree][utils.CurrentAPIVersion].String()) + cv := utils.APIVersion[utils.CompatTree][utils.CurrentAPIVersion] + w.Header().Set("API-Version", fmt.Sprintf("%d.%d", cv.Major, cv.Minor)) + + lv := utils.APIVersion[utils.LibpodTree][utils.CurrentAPIVersion].String() + w.Header().Set("Libpod-API-Version", lv) + w.Header().Set("Server", "Libpod/"+lv+" ("+runtime.GOOS+")") h(w, r) } diff --git a/pkg/bindings/images/pull.go b/pkg/bindings/images/pull.go index 261a481a2..2bfbbb2ac 100644 --- a/pkg/bindings/images/pull.go +++ b/pkg/bindings/images/pull.go @@ -89,6 +89,7 @@ func Pull(ctx context.Context, rawImage string, options entities.ImagePullOption mErr = multierror.Append(mErr, errors.New(report.Error)) case len(report.Images) > 0: images = report.Images + case report.ID != "": default: return images, errors.New("failed to parse pull results stream, unexpected input") } diff --git a/pkg/bindings/test/images_test.go b/pkg/bindings/test/images_test.go index e0dd28d7a..681855293 100644 --- a/pkg/bindings/test/images_test.go +++ b/pkg/bindings/test/images_test.go @@ -360,19 +360,19 @@ var _ = Describe("Podman images", func() { rawImage := "docker.io/library/busybox:latest" pulledImages, err := images.Pull(bt.conn, rawImage, entities.ImagePullOptions{}) - Expect(err).To(BeNil()) + Expect(err).NotTo(HaveOccurred()) Expect(len(pulledImages)).To(Equal(1)) exists, err := images.Exists(bt.conn, rawImage) - Expect(err).To(BeNil()) + Expect(err).NotTo(HaveOccurred()) Expect(exists).To(BeTrue()) // Make sure the normalization AND the full-transport reference works. _, err = images.Pull(bt.conn, "docker://"+rawImage, entities.ImagePullOptions{}) - Expect(err).To(BeNil()) + Expect(err).NotTo(HaveOccurred()) // The v2 endpoint only supports the docker transport. Let's see if that's really true. _, err = images.Pull(bt.conn, "bogus-transport:bogus.com/image:reference", entities.ImagePullOptions{}) - Expect(err).To(Not(BeNil())) + Expect(err).To(HaveOccurred()) }) }) diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index d0b738934..cad6693fa 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -156,6 +156,8 @@ type ImagePullReport struct { Error string `json:"error,omitempty"` // Images contains the ID's of the images pulled Images []string `json:"images,omitempty"` + // ID contains image id (retained for backwards compatibility) + ID string `json:"id,omitempty"` } // ImagePushOptions are the arguments for pushing images. diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index cc62c3f27..25c0c184f 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -191,6 +191,15 @@ func (ir *ImageEngine) Unmount(ctx context.Context, nameOrIDs []string, options reports := []*entities.ImageUnmountReport{} for _, img := range images { report := entities.ImageUnmountReport{Id: img.ID()} + mounted, _, err := img.Mounted() + if err != nil { + // Errors will be caught in Unmount call below + // Default assumption to mounted + mounted = true + } + if !mounted { + continue + } if err := img.Unmount(options.Force); err != nil { if options.All && errors.Cause(err) == storage.ErrLayerNotMounted { logrus.Debugf("Error umounting image %s, storage.ErrLayerNotMounted", img.ID()) diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index 3e99b73b6..d0f90d900 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -389,6 +389,15 @@ func (ic *ContainerEngine) ContainerLogs(_ context.Context, nameOrIDs []string, } func (ic *ContainerEngine) ContainerAttach(ctx context.Context, nameOrID string, options entities.AttachOptions) error { + ctrs, err := getContainersByContext(ic.ClientCxt, false, false, []string{nameOrID}) + if err != nil { + return err + } + ctr := ctrs[0] + if ctr.State != define.ContainerStateRunning.String() { + return errors.Errorf("you can only attach to running containers") + } + return containers.Attach(ic.ClientCxt, nameOrID, &options.DetachKeys, nil, bindings.PTrue, options.Stdin, options.Stdout, options.Stderr, nil) } diff --git a/pkg/hooks/1.0.0/hook.go b/pkg/hooks/1.0.0/hook.go index 77fbab5aa..244e8800f 100644 --- a/pkg/hooks/1.0.0/hook.go +++ b/pkg/hooks/1.0.0/hook.go @@ -67,7 +67,14 @@ func (hook *Hook) Validate(extensionStages []string) (err error) { return errors.New("missing required property: stages") } - validStages := map[string]bool{"prestart": true, "poststart": true, "poststop": true} + validStages := map[string]bool{ + "createContainer": true, + "createRuntime": true, + "prestart": true, + "poststart": true, + "poststop": true, + "startContainer": true, + } for _, stage := range extensionStages { validStages[stage] = true } diff --git a/pkg/hooks/hooks.go b/pkg/hooks/hooks.go index 2a12eceac..6257529ab 100644 --- a/pkg/hooks/hooks.go +++ b/pkg/hooks/hooks.go @@ -120,12 +120,18 @@ func (m *Manager) Hooks(config *rspec.Spec, annotations map[string]string, hasBi extensionStageHooks[stage] = append(extensionStageHooks[stage], namedHook.hook.Hook) } else { switch stage { + case "createContainer": + config.Hooks.CreateContainer = append(config.Hooks.CreateContainer, namedHook.hook.Hook) + case "createRuntime": + config.Hooks.CreateRuntime = append(config.Hooks.CreateRuntime, namedHook.hook.Hook) case "prestart": config.Hooks.Prestart = append(config.Hooks.Prestart, namedHook.hook.Hook) case "poststart": config.Hooks.Poststart = append(config.Hooks.Poststart, namedHook.hook.Hook) case "poststop": config.Hooks.Poststop = append(config.Hooks.Poststop, namedHook.hook.Hook) + case "startContainer": + config.Hooks.StartContainer = append(config.Hooks.StartContainer, namedHook.hook.Hook) default: return extensionStageHooks, fmt.Errorf("hook %q: unknown stage %q", namedHook.name, stage) } diff --git a/test/apiv2/rest_api/test_rest_v2_0_0.py b/test/apiv2/rest_api/test_rest_v2_0_0.py new file mode 100644 index 000000000..3376f8402 --- /dev/null +++ b/test/apiv2/rest_api/test_rest_v2_0_0.py @@ -0,0 +1,246 @@ +import json +import os +import subprocess +import sys +import time +import unittest +from multiprocessing import Process + +import requests +from dateutil.parser import parse + +PODMAN_URL = "http://localhost:8080" + + +def _url(path): + return PODMAN_URL + "/v1.0.0/libpod" + path + + +def podman(): + binary = os.getenv("PODMAN_BINARY") + if binary is None: + binary = "bin/podman" + return binary + + +def ctnr(path): + r = requests.get(_url("/containers/json?all=true")) + try: + ctnrs = json.loads(r.text) + except Exception as e: + sys.stderr.write("Bad container response: {}/{}".format(r.text, e)) + raise e + return path.format(ctnrs[0]["Id"]) + + +def validateObjectFields(buffer): + objs = json.loads(buffer) + if not isinstance(objs, dict): + for o in objs: + _ = o["Id"] + else: + _ = objs["Id"] + return objs + + +class TestApi(unittest.TestCase): + podman = None + + def setUp(self): + super().setUp() + if TestApi.podman.poll() is not None: + sys.stderr.write(f"podman service returned {TestApi.podman.returncode}\n") + sys.exit(2) + requests.get( + _url("/images/create?fromSrc=docker.io%2Falpine%3Alatest")) + # calling out to podman is easier than the API for running a container + subprocess.run([podman(), "run", "alpine", "/bin/ls"], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + + @classmethod + def setUpClass(cls): + super().setUpClass() + + TestApi.podman = subprocess.Popen( + [ + podman(), "system", "service", "tcp:localhost:8080", + "--log-level=debug", "--time=0" + ], + shell=False, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + time.sleep(2) + + @classmethod + def tearDownClass(cls): + TestApi.podman.terminate() + stdout, stderr = TestApi.podman.communicate(timeout=0.5) + if stdout: + print("\nService Stdout:\n" + stdout.decode('utf-8')) + if stderr: + print("\nService Stderr:\n" + stderr.decode('utf-8')) + + if TestApi.podman.returncode > 0: + sys.stderr.write(f"podman exited with error code {TestApi.podman.returncode}\n") + sys.exit(2) + + return super().tearDownClass() + + def test_info(self): + r = requests.get(_url("/info")) + self.assertEqual(r.status_code, 200) + self.assertIsNotNone(r.content) + _ = json.loads(r.text) + + def test_events(self): + r = requests.get(_url("/events?stream=false")) + self.assertEqual(r.status_code, 200, r.text) + self.assertIsNotNone(r.content) + for line in r.text.splitlines(): + obj = json.loads(line) + # Actor.ID is uppercase for compatibility + _ = obj["Actor"]["ID"] + + def test_containers(self): + r = requests.get(_url("/containers/json"), timeout=5) + self.assertEqual(r.status_code, 200, r.text) + obj = json.loads(r.text) + self.assertEqual(len(obj), 0) + + def test_containers_all(self): + r = requests.get(_url("/containers/json?all=true")) + self.assertEqual(r.status_code, 200, r.text) + validateObjectFields(r.text) + + def test_inspect_container(self): + r = requests.get(_url(ctnr("/containers/{}/json"))) + self.assertEqual(r.status_code, 200, r.text) + obj = validateObjectFields(r.content) + _ = parse(obj["Created"]) + + def test_stats(self): + r = requests.get(_url(ctnr("/containers/{}/stats?stream=false"))) + self.assertIn(r.status_code, (200, 409), r.text) + if r.status_code == 200: + validateObjectFields(r.text) + + def test_delete_containers(self): + r = requests.delete(_url(ctnr("/containers/{}"))) + self.assertEqual(r.status_code, 204, r.text) + + def test_stop_containers(self): + r = requests.post(_url(ctnr("/containers/{}/start"))) + self.assertIn(r.status_code, (204, 304), r.text) + + r = requests.post(_url(ctnr("/containers/{}/stop"))) + self.assertIn(r.status_code, (204, 304), r.text) + + def test_start_containers(self): + r = requests.post(_url(ctnr("/containers/{}/stop"))) + self.assertIn(r.status_code, (204, 304), r.text) + + r = requests.post(_url(ctnr("/containers/{}/start"))) + self.assertIn(r.status_code, (204, 304), r.text) + + def test_restart_containers(self): + r = requests.post(_url(ctnr("/containers/{}/start"))) + self.assertIn(r.status_code, (204, 304), r.text) + + r = requests.post(_url(ctnr("/containers/{}/restart")), timeout=5) + self.assertEqual(r.status_code, 204, r.text) + + def test_resize(self): + r = requests.post(_url(ctnr("/containers/{}/resize?h=43&w=80"))) + self.assertIn(r.status_code, (200, 409), r.text) + if r.status_code == 200: + self.assertIsNone(r.text) + + def test_attach_containers(self): + r = requests.post(_url(ctnr("/containers/{}/attach")), timeout=5) + self.assertIn(r.status_code, (101, 500), r.text) + + def test_logs_containers(self): + r = requests.get(_url(ctnr("/containers/{}/logs?stdout=true"))) + self.assertEqual(r.status_code, 200, r.text) + + def test_post_create(self): + self.skipTest("TODO: create request body") + r = requests.post(_url("/containers/create?args=True")) + self.assertEqual(r.status_code, 200, r.text) + json.loads(r.text) + + def test_commit(self): + r = requests.post(_url(ctnr("/commit?container={}"))) + self.assertEqual(r.status_code, 200, r.text) + validateObjectFields(r.text) + + def test_images(self): + r = requests.get(_url("/images/json")) + self.assertEqual(r.status_code, 200, r.text) + validateObjectFields(r.content) + + def test_inspect_image(self): + r = requests.get(_url("/images/alpine/json")) + self.assertEqual(r.status_code, 200, r.text) + obj = validateObjectFields(r.content) + _ = parse(obj["Created"]) + + def test_delete_image(self): + r = requests.delete(_url("/images/alpine?force=true")) + self.assertEqual(r.status_code, 200, r.text) + json.loads(r.text) + + def test_pull(self): + r = requests.post(_url("/images/pull?reference=alpine"), timeout=15) + self.assertEqual(r.status_code, 200, r.status_code) + text = r.text + keys = { + "error": False, + "id": False, + "images": False, + "stream": False, + } + # Read and record stanza's from pull + for line in str.splitlines(text): + obj = json.loads(line) + key_list = list(obj.keys()) + for k in key_list: + keys[k] = True + + self.assertFalse(keys["error"], "Expected no errors") + self.assertTrue(keys["id"], "Expected to find id stanza") + self.assertTrue(keys["images"], "Expected to find images stanza") + self.assertTrue(keys["stream"], "Expected to find stream progress stanza's") + + def test_search(self): + # Had issues with this test hanging when repositories not happy + def do_search(): + r = requests.get(_url("/images/search?term=alpine"), timeout=5) + self.assertEqual(r.status_code, 200, r.text) + json.loads(r.text) + + search = Process(target=do_search) + search.start() + search.join(timeout=10) + self.assertFalse(search.is_alive(), "/images/search took too long") + + def test_ping(self): + r = requests.get(PODMAN_URL + "/_ping") + self.assertEqual(r.status_code, 200, r.text) + + r = requests.head(PODMAN_URL + "/_ping") + self.assertEqual(r.status_code, 200, r.text) + + r = requests.get(_url("/_ping")) + self.assertEqual(r.status_code, 200, r.text) + + r = requests.get(_url("/_ping")) + self.assertEqual(r.status_code, 200, r.text) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/e2e/attach_test.go b/test/e2e/attach_test.go index 7b18f71ac..8065f6298 100644 --- a/test/e2e/attach_test.go +++ b/test/e2e/attach_test.go @@ -40,7 +40,6 @@ var _ = Describe("Podman attach", func() { }) It("podman attach to non-running container", func() { - SkipIfRemote() session := podmanTest.Podman([]string{"create", "--name", "test1", "-d", "-i", ALPINE, "ls"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) @@ -51,8 +50,8 @@ var _ = Describe("Podman attach", func() { }) It("podman container attach to non-running container", func() { - SkipIfRemote() session := podmanTest.Podman([]string{"container", "create", "--name", "test1", "-d", "-i", ALPINE, "ls"}) + session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) @@ -87,7 +86,6 @@ var _ = Describe("Podman attach", func() { Expect(podmanTest.NumberOfContainersRunning()).To(Equal(1)) }) It("podman attach to the latest container", func() { - SkipIfRemote() session := podmanTest.Podman([]string{"run", "-d", "--name", "test1", ALPINE, "/bin/sh", "-c", "while true; do echo test1; sleep 1; done"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) @@ -96,7 +94,11 @@ var _ = Describe("Podman attach", func() { session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - results := podmanTest.Podman([]string{"attach", "-l"}) + cid := "-l" + if IsRemote() { + cid = "test2" + } + results := podmanTest.Podman([]string{"attach", cid}) time.Sleep(2 * time.Second) results.Signal(syscall.SIGTSTP) Expect(results.OutputToString()).To(ContainSubstring("test2")) diff --git a/test/e2e/libpod_suite_remote_test.go b/test/e2e/libpod_suite_remote_test.go index 874789b5e..e74d9bf7c 100644 --- a/test/e2e/libpod_suite_remote_test.go +++ b/test/e2e/libpod_suite_remote_test.go @@ -19,6 +19,10 @@ import ( "github.com/onsi/ginkgo" ) +func IsRemote() bool { + return true +} + func SkipIfRemote() { ginkgo.Skip("This function is not enabled for remote podman") } diff --git a/test/e2e/libpod_suite_test.go b/test/e2e/libpod_suite_test.go index bfd898108..0f33798b7 100644 --- a/test/e2e/libpod_suite_test.go +++ b/test/e2e/libpod_suite_test.go @@ -12,6 +12,10 @@ import ( . "github.com/onsi/ginkgo" ) +func IsRemote() bool { + return false +} + func SkipIfRemote() { } diff --git a/test/e2e/libpod_suite_varlink_test.go b/test/e2e/libpod_suite_varlink_test.go index 750c8cd09..0d7032429 100644 --- a/test/e2e/libpod_suite_varlink_test.go +++ b/test/e2e/libpod_suite_varlink_test.go @@ -19,6 +19,10 @@ import ( "github.com/onsi/ginkgo" ) +func IsRemote() bool { + return true +} + func SkipIfRemote() { ginkgo.Skip("This function is not enabled for remote podman") } diff --git a/test/e2e/mount_test.go b/test/e2e/mount_test.go index a2b448337..1fbb92b09 100644 --- a/test/e2e/mount_test.go +++ b/test/e2e/mount_test.go @@ -348,6 +348,25 @@ var _ = Describe("Podman mount", func() { Expect(umount.ExitCode()).To(Equal(0)) }) + It("podman umount --all", func() { + setup := podmanTest.PodmanNoCache([]string{"pull", fedoraMinimal}) + setup.WaitWithDefaultTimeout() + Expect(setup.ExitCode()).To(Equal(0)) + + setup = podmanTest.PodmanNoCache([]string{"pull", ALPINE}) + setup.WaitWithDefaultTimeout() + Expect(setup.ExitCode()).To(Equal(0)) + + mount := podmanTest.Podman([]string{"image", "mount", fedoraMinimal}) + mount.WaitWithDefaultTimeout() + Expect(mount.ExitCode()).To(Equal(0)) + + umount := podmanTest.Podman([]string{"image", "umount", "--all"}) + umount.WaitWithDefaultTimeout() + Expect(umount.ExitCode()).To(Equal(0)) + Expect(len(umount.OutputToStringArray())).To(Equal(1)) + }) + It("podman mount many", func() { setup := podmanTest.PodmanNoCache([]string{"pull", fedoraMinimal}) setup.WaitWithDefaultTimeout() @@ -402,6 +421,10 @@ var _ = Describe("Podman mount", func() { Expect(mount.ExitCode()).To(Equal(0)) Expect(mount.OutputToString()).To(Equal("")) + umount = podmanTest.PodmanNoCache([]string{"image", "umount", fedoraMinimal, ALPINE}) + umount.WaitWithDefaultTimeout() + Expect(umount.ExitCode()).To(Equal(0)) + mount1 = podmanTest.PodmanNoCache([]string{"image", "mount", "--all"}) mount1.WaitWithDefaultTimeout() Expect(mount1.ExitCode()).To(Equal(0)) diff --git a/test/system/005-info.bats b/test/system/005-info.bats index 3f1efd364..ef3e97af0 100644 --- a/test/system/005-info.bats +++ b/test/system/005-info.bats @@ -19,6 +19,8 @@ graphRoot: graphStatus: imageStore:\\\s\\\+number: 1 runRoot: +cgroupManager: +cgroupVersion: v " while read expect; do is "$output" ".*$expect" "output includes '$expect'" diff --git a/test/system/120-load.bats b/test/system/120-load.bats index d7aa16d95..8ea9b1c69 100644 --- a/test/system/120-load.bats +++ b/test/system/120-load.bats @@ -147,4 +147,45 @@ verify_iid_and_name() { "Diagnostic from 'podman load' without redirection or -i" } +@test "podman load - multi-image archive" { + img1="quay.io/libpod/testimage:00000000" + img2="quay.io/libpod/testimage:20200902" + archive=$PODMAN_TMPDIR/myimage-$(random_string 8).tar + + run_podman pull $img1 + run_podman pull $img2 + + run_podman save -m -o $archive $img1 $img2 + run_podman rmi -f $img1 $img2 + run_podman load -i $archive + + run_podman image exists $img1 + run_podman image exists $img2 + run_podman rmi -f $img1 $img2 +} + +@test "podman load - multi-image archive with redirect" { + img1="quay.io/libpod/testimage:00000000" + img2="quay.io/libpod/testimage:20200902" + archive=$PODMAN_TMPDIR/myimage-$(random_string 8).tar + + run_podman pull $img1 + run_podman pull $img2 + + # We can't use run_podman because that uses the BATS 'run' function + # which redirects stdout and stderr. Here we need to guarantee + # that podman's stdout is a pipe, not any other form of redirection + $PODMAN save -m $img1 $img2 | cat >$archive + if [ "$status" -ne 0 ]; then + die "Command failed: podman save ... | cat" + fi + + run_podman rmi -f $img1 $img2 + run_podman load -i $archive + + run_podman image exists $img1 + run_podman image exists $img2 + run_podman rmi -f $img1 $img2 +} + # vim: filetype=sh diff --git a/version/version.go b/version/version.go index df2e4f2ba..e6b1425ef 100644 --- a/version/version.go +++ b/version/version.go @@ -8,7 +8,7 @@ import ( // NOTE: remember to bump the version at the top // of the top-level README.md file when this is // bumped. -var Version = semver.MustParse("2.1.0-dev") +var Version = semver.MustParse("2.2.0-dev") // APIVersion is the version for the remote // client API. It is used to determine compatibility |