diff options
-rw-r--r-- | Makefile | 1 | ||||
-rw-r--r-- | OWNERS | 2 | ||||
-rw-r--r-- | docs/source/markdown/containers-mounts.conf.5.md | 2 | ||||
-rw-r--r-- | docs/tutorials/README.md | 2 | ||||
-rw-r--r-- | nix/default.nix | 2 | ||||
-rw-r--r-- | pkg/specgen/generate/config_linux.go | 5 | ||||
-rw-r--r-- | pkg/specgen/generate/security.go | 10 | ||||
-rw-r--r-- | test/apiv2/rest_api/__init__.py | 132 | ||||
-rw-r--r-- | test/apiv2/rest_api/test_rest_v2_0_0.py | 82 | ||||
-rw-r--r-- | test/apiv2/rest_api/v1_test_rest_v1_0_0.py (renamed from test/apiv2/rest_api/test_rest_v1_0_0.py) | 52 | ||||
-rw-r--r-- | test/e2e/common_test.go | 8 | ||||
-rw-r--r-- | test/e2e/play_kube_test.go | 2 | ||||
-rw-r--r-- | test/e2e/run_test.go | 33 | ||||
-rw-r--r-- | test/e2e/toolbox_test.go | 15 |
14 files changed, 273 insertions, 75 deletions
@@ -357,6 +357,7 @@ remotesystem: .PHONY: localapiv2 localapiv2: env PODMAN=./bin/podman ./test/apiv2/test-apiv2 + env PODMAN=./bin/podman ${PYTHON} -m unittest discover -v ./test/apiv2/rest_api/ .PHONY: remoteapiv2 remoteapiv2: @@ -7,6 +7,7 @@ approvers: - rhatdan - TomSweeneyRedHat - vrothberg + - umohnani8 reviewers: - baude - edsantiago @@ -18,3 +19,4 @@ reviewers: - vrothberg - ashley-cui - QiWang19 + - umohnani8 diff --git a/docs/source/markdown/containers-mounts.conf.5.md b/docs/source/markdown/containers-mounts.conf.5.md index 130c1c523..74492c831 100644 --- a/docs/source/markdown/containers-mounts.conf.5.md +++ b/docs/source/markdown/containers-mounts.conf.5.md @@ -10,7 +10,7 @@ The mounts.conf file specifies volume mount directories that are automatically m The format of the mounts.conf is the volume format `/SRC:/DEST`, one mount per line. For example, a mounts.conf with the line `/usr/share/secrets:/run/secrets` would cause the contents of the `/usr/share/secrets` directory on the host to be mounted on the `/run/secrets` directory inside the container. Setting mountpoints allows containers to use the files of the host, for instance, to use the host's subscription to some enterprise Linux distribution. ## FILES -Some distributions may provide a `/usr/share/containers/mounts.conf` file to provide default mounts, but users can create a `/etc/containers/mounts.conf`, to specify their own special volumes to mount in the container. +Some distributions may provide a `/usr/share/containers/mounts.conf` file to provide default mounts, but users can create a `/etc/containers/mounts.conf`, to specify their own special volumes to mount in the container. When Podman runs in rootless mode, the file `$HOME/.config/containers/mounts.conf` will override the default if it exists. ## HISTORY Aug 2018, Originally compiled by Valentin Rothberg <vrothberg@suse.com> diff --git a/docs/tutorials/README.md b/docs/tutorials/README.md index 7f7b4853d..246d235ee 100644 --- a/docs/tutorials/README.md +++ b/docs/tutorials/README.md @@ -12,7 +12,7 @@ Learn how to setup Podman and perform some basic commands with the utility. The steps required to setup rootless Podman are enumerated. -**[Setup Mac/Windows](mac_win_client.md) +**[Setup Mac/Windows](mac_win_client.md)** Special setup for running the Podman remote client on a Mac or Windows PC and connecting to Podman running on a Linux VM are documented. diff --git a/nix/default.nix b/nix/default.nix index cc8786ce0..a1a8c5287 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -44,7 +44,7 @@ let export CFLAGS='-static' export LDFLAGS='-s -w -static-libgcc -static' export EXTRA_LDFLAGS='-s -w -linkmode external -extldflags "-static -lm"' - export BUILDTAGS='static netgo exclude_graphdriver_btrfs exclude_graphdriver_devicemapper seccomp apparmor selinux' + export BUILDTAGS='static netgo osusergo exclude_graphdriver_btrfs exclude_graphdriver_devicemapper seccomp apparmor selinux' ''; buildPhase = '' patchShebangs . diff --git a/pkg/specgen/generate/config_linux.go b/pkg/specgen/generate/config_linux.go index fcb7641d2..2d40dba8f 100644 --- a/pkg/specgen/generate/config_linux.go +++ b/pkg/specgen/generate/config_linux.go @@ -350,3 +350,8 @@ func deviceFromPath(path string) (*spec.LinuxDevice, error) { Minor: int64(unix.Minor(devNumber)), }, nil } + +func supportAmbientCapabilities() bool { + err := unix.Prctl(unix.PR_CAP_AMBIENT, unix.PR_CAP_AMBIENT_IS_SET, 0, 0, 0) + return err == nil +} diff --git a/pkg/specgen/generate/security.go b/pkg/specgen/generate/security.go index d17cd4a9a..dee140282 100644 --- a/pkg/specgen/generate/security.go +++ b/pkg/specgen/generate/security.go @@ -135,7 +135,9 @@ func securityConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator, configSpec.Process.Capabilities.Bounding = caplist configSpec.Process.Capabilities.Inheritable = caplist - if s.User == "" || s.User == "root" || s.User == "0" { + user := strings.Split(s.User, ":")[0] + + if (user == "" && s.UserNS.NSMode != specgen.KeepID) || user == "root" || user == "0" { configSpec.Process.Capabilities.Effective = caplist configSpec.Process.Capabilities.Permitted = caplist } else { @@ -145,6 +147,12 @@ func securityConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator, } configSpec.Process.Capabilities.Effective = userCaps configSpec.Process.Capabilities.Permitted = userCaps + + // Ambient capabilities were added to Linux 4.3. Set ambient + // capabilities only when the kernel supports them. + if supportAmbientCapabilities() { + configSpec.Process.Capabilities.Ambient = userCaps + } } g.SetProcessNoNewPrivileges(s.NoNewPrivileges) diff --git a/test/apiv2/rest_api/__init__.py b/test/apiv2/rest_api/__init__.py index e69de29bb..5f0777d58 100644 --- a/test/apiv2/rest_api/__init__.py +++ b/test/apiv2/rest_api/__init__.py @@ -0,0 +1,132 @@ +import configparser +import json +import os +import shutil +import subprocess +import sys +import tempfile + + +class Podman(object): + """ + Instances hold the configuration and setup for running podman commands + """ + + def __init__(self): + """Initialize a Podman instance with global options""" + binary = os.getenv("PODMAN", "bin/podman") + self.cmd = [binary, "--storage-driver=vfs"] + + cgroupfs = os.getenv("CGROUP_MANAGER", "cgroupfs") + self.cmd.append(f"--cgroup-manager={cgroupfs}") + + if os.getenv("DEBUG"): + self.cmd.append("--log-level=debug") + + self.anchor_directory = tempfile.mkdtemp(prefix="podman_restapi_") + self.cmd.append("--root=" + os.path.join(self.anchor_directory, "crio")) + self.cmd.append("--runroot=" + os.path.join(self.anchor_directory, "crio-run")) + + os.environ["REGISTRIES_CONFIG_PATH"] = os.path.join( + self.anchor_directory, "registry.conf" + ) + p = configparser.ConfigParser() + p.read_dict( + { + "registries.search": {"registries": "['docker.io']"}, + "registries.insecure": {"registries": "[]"}, + "registries.block": {"registries": "[]"}, + } + ) + with open(os.environ["REGISTRIES_CONFIG_PATH"], "w") as w: + p.write(w) + + os.environ["CNI_CONFIG_PATH"] = os.path.join( + self.anchor_directory, "cni", "net.d" + ) + os.makedirs(os.environ["CNI_CONFIG_PATH"], exist_ok=True) + self.cmd.append("--cni-config-dir=" + os.environ["CNI_CONFIG_PATH"]) + cni_cfg = os.path.join( + os.environ["CNI_CONFIG_PATH"], "87-podman-bridge.conflist" + ) + # json decoded and encoded to ensure legal json + buf = json.loads( + """ + { + "cniVersion": "0.3.0", + "name": "podman", + "plugins": [{ + "type": "bridge", + "bridge": "cni0", + "isGateway": true, + "ipMasq": true, + "ipam": { + "type": "host-local", + "subnet": "10.88.0.0/16", + "routes": [{ + "dst": "0.0.0.0/0" + }] + } + }, + { + "type": "portmap", + "capabilities": { + "portMappings": true + } + } + ] + } + """ + ) + with open(cni_cfg, "w") as w: + json.dump(buf, w) + + def open(self, command, *args, **kwargs): + """Podman initialized instance to run a given command + + :param self: Podman instance + :param command: podman sub-command to run + :param args: arguments and options for command + :param kwargs: See subprocess.Popen() for shell keyword + :return: subprocess.Popen() instance configured to run podman instance + """ + cmd = self.cmd.copy() + cmd.append(command) + cmd.extend(args) + + shell = kwargs.get("shell", False) + + return subprocess.Popen( + cmd, + shell=shell, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + def run(self, command, *args, **kwargs): + """Podman initialized instance to run a given command + + :param self: Podman instance + :param command: podman sub-command to run + :param args: arguments and options for command + :param kwargs: See subprocess.Popen() for shell and check keywords + :return: subprocess.Popen() instance configured to run podman instance + """ + cmd = self.cmd.copy() + cmd.append(command) + cmd.extend(args) + + check = kwargs.get("check", False) + shell = kwargs.get("shell", False) + + return subprocess.run( + cmd, + shell=shell, + check=check, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + def tear_down(self): + shutil.rmtree(self.anchor_directory, ignore_errors=True) diff --git a/test/apiv2/rest_api/test_rest_v2_0_0.py b/test/apiv2/rest_api/test_rest_v2_0_0.py index 3376f8402..5dfd1fc02 100644 --- a/test/apiv2/rest_api/test_rest_v2_0_0.py +++ b/test/apiv2/rest_api/test_rest_v2_0_0.py @@ -1,5 +1,4 @@ import json -import os import subprocess import sys import time @@ -9,27 +8,25 @@ from multiprocessing import Process import requests from dateutil.parser import parse +from test.apiv2.rest_api import Podman + 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 + return PODMAN_URL + "/v2.0.0/libpod" + path def ctnr(path): - r = requests.get(_url("/containers/json?all=true")) try: + r = requests.get(_url("/containers/json?all=true")) ctnrs = json.loads(r.text) except Exception as e: - sys.stderr.write("Bad container response: {}/{}".format(r.text, e)) - raise e + msg = f"Bad container response: {e}" + if r is not None: + msg = msg + " " + r.text + sys.stderr.write(msg + "\n") + raise return path.format(ctnrs[0]["Id"]) @@ -44,50 +41,50 @@ def validateObjectFields(buffer): class TestApi(unittest.TestCase): - podman = None + podman = None # initialized podman configuration for tests + service = None # podman service instance 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) + + try: + TestApi.podman.run("run", "alpine", "/bin/ls", check=True) + except subprocess.CalledProcessError as e: + if e.stdout: + sys.stdout.write("\nRun Stdout:\n" + e.stdout.decode("utf-8")) + if e.stderr: + sys.stderr.write("\nRun Stderr:\n" + e.stderr.decode("utf-8")) + raise @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, + TestApi.podman = Podman() + TestApi.service = TestApi.podman.open( + "system", "service", "tcp:localhost:8080", "--log-level=debug", "--time=0" ) + # give the service some time to be ready... time.sleep(2) + returncode = TestApi.service.poll() + if returncode is not None: + raise subprocess.CalledProcessError(returncode, "podman system service") + + r = requests.post(_url("/images/pull?reference=docker.io%2Falpine%3Alatest")) + if r.status_code != 200: + raise subprocess.CalledProcessError( + r.status_code, f"podman images pull docker.io/alpine:latest {r.text}" + ) + @classmethod def tearDownClass(cls): - TestApi.podman.terminate() - stdout, stderr = TestApi.podman.communicate(timeout=0.5) + TestApi.service.terminate() + stdout, stderr = TestApi.service.communicate(timeout=0.5) if stdout: - print("\nService Stdout:\n" + stdout.decode('utf-8')) + sys.stdout.write("\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) - + sys.stderr.write("\nService Stderr:\n" + stderr.decode("utf-8")) return super().tearDownClass() def test_info(self): @@ -160,6 +157,7 @@ class TestApi(unittest.TestCase): self.assertIsNone(r.text) def test_attach_containers(self): + self.skipTest("FIXME: Test timeouts") r = requests.post(_url(ctnr("/containers/{}/attach")), timeout=5) self.assertIn(r.status_code, (101, 500), r.text) @@ -242,5 +240,5 @@ class TestApi(unittest.TestCase): self.assertEqual(r.status_code, 200, r.text) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/apiv2/rest_api/test_rest_v1_0_0.py b/test/apiv2/rest_api/v1_test_rest_v1_0_0.py index 2e574e015..acd6273ef 100644 --- a/test/apiv2/rest_api/test_rest_v1_0_0.py +++ b/test/apiv2/rest_api/v1_test_rest_v1_0_0.py @@ -43,16 +43,16 @@ class TestApi(unittest.TestCase): def setUp(self): super().setUp() if TestApi.podman.poll() is not None: - sys.stderr.write("podman service returned {}", - TestApi.podman.returncode) + sys.stderr.write("podman service returned {}", TestApi.podman.returncode) sys.exit(2) - requests.get( - _url("/images/create?fromSrc=docker.io%2Falpine%3Alatest")) + 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) + subprocess.run( + [podman(), "run", "alpine", "/bin/ls"], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) @classmethod def setUpClass(cls): @@ -60,8 +60,12 @@ class TestApi(unittest.TestCase): TestApi.podman = subprocess.Popen( [ - podman(), "system", "service", "tcp:localhost:8080", - "--log-level=debug", "--time=0" + podman(), + "system", + "service", + "tcp:localhost:8080", + "--log-level=debug", + "--time=0", ], shell=False, stdin=subprocess.DEVNULL, @@ -75,13 +79,14 @@ class TestApi(unittest.TestCase): TestApi.podman.terminate() stdout, stderr = TestApi.podman.communicate(timeout=0.5) if stdout: - print("\nService Stdout:\n" + stdout.decode('utf-8')) + print("\nService Stdout:\n" + stdout.decode("utf-8")) if stderr: - print("\nService Stderr:\n" + stderr.decode('utf-8')) + print("\nService Stderr:\n" + stderr.decode("utf-8")) if TestApi.podman.returncode > 0: - sys.stderr.write("podman exited with error code {}\n".format( - TestApi.podman.returncode)) + sys.stderr.write( + "podman exited with error code {}\n".format(TestApi.podman.returncode) + ) sys.exit(2) return super().tearDownClass() @@ -222,13 +227,14 @@ class TestApi(unittest.TestCase): def validateObjectFields(self, buffer): - objs = json.loads(buffer) - if not isinstance(objs, dict): - for o in objs: - _ = o["Id"] - else: - _ = objs["Id"] - return objs - -if __name__ == '__main__': + objs = json.loads(buffer) + if not isinstance(objs, dict): + for o in objs: + _ = o["Id"] + else: + _ = objs["Id"] + return objs + + +if __name__ == "__main__": unittest.main() diff --git a/test/e2e/common_test.go b/test/e2e/common_test.go index 3814d161d..678b2c882 100644 --- a/test/e2e/common_test.go +++ b/test/e2e/common_test.go @@ -613,12 +613,10 @@ func SkipIfRootlessCgroupsV1(reason string) { } } -func SkipIfUnprevilegedCPULimits() { +func SkipIfUnprivilegedCPULimits() { info := GetHostDistributionInfo() - if isRootless() && - info.Distribution == "fedora" && - (info.Version == "31" || info.Version == "32") { - ginkgo.Skip("Rootless Fedora doesn't have permission to set CPU limits before version 33") + if isRootless() && info.Distribution == "fedora" { + ginkgo.Skip("Rootless Fedora doesn't have permission to set CPU limits") } } diff --git a/test/e2e/play_kube_test.go b/test/e2e/play_kube_test.go index 6dcfa9bd8..1d683e987 100644 --- a/test/e2e/play_kube_test.go +++ b/test/e2e/play_kube_test.go @@ -1406,7 +1406,7 @@ spec: It("podman play kube allows setting resource limits", func() { SkipIfContainerized("Resource limits require a running systemd") SkipIfRootlessCgroupsV1("Limits require root or cgroups v2") - SkipIfUnprevilegedCPULimits() + SkipIfUnprivilegedCPULimits() podmanTest.CgroupManager = "systemd" var ( diff --git a/test/e2e/run_test.go b/test/e2e/run_test.go index e6bba9f67..deb4419af 100644 --- a/test/e2e/run_test.go +++ b/test/e2e/run_test.go @@ -315,6 +315,39 @@ var _ = Describe("Podman run", func() { session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) Expect(session.OutputToString()).To(ContainSubstring("00000000a80425fb")) + + session = podmanTest.Podman([]string{"run", "--user=1000:1000", "--cap-add=DAC_OVERRIDE", "--rm", ALPINE, "grep", "CapAmb", "/proc/self/status"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(ContainSubstring("0000000000000002")) + + session = podmanTest.Podman([]string{"run", "--user=1000:1000", "--rm", ALPINE, "grep", "CapAmb", "/proc/self/status"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(ContainSubstring("0000000000000000")) + + session = podmanTest.Podman([]string{"run", "--user=0", "--cap-add=DAC_OVERRIDE", "--rm", ALPINE, "grep", "CapAmb", "/proc/self/status"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(ContainSubstring("0000000000000000")) + + session = podmanTest.Podman([]string{"run", "--user=0:0", "--cap-add=DAC_OVERRIDE", "--rm", ALPINE, "grep", "CapAmb", "/proc/self/status"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(ContainSubstring("0000000000000000")) + + if os.Geteuid() > 0 { + if os.Getenv("SKIP_USERNS") != "" { + Skip("Skip userns tests.") + } + if _, err := os.Stat("/proc/self/uid_map"); err != nil { + Skip("User namespaces not supported.") + } + session = podmanTest.Podman([]string{"run", "--userns=keep-id", "--cap-add=DAC_OVERRIDE", "--rm", ALPINE, "grep", "CapAmb", "/proc/self/status"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(ContainSubstring("0000000000000002")) + } }) It("podman run user capabilities test with image", func() { diff --git a/test/e2e/toolbox_test.go b/test/e2e/toolbox_test.go index fbff8d19e..822159fc2 100644 --- a/test/e2e/toolbox_test.go +++ b/test/e2e/toolbox_test.go @@ -30,10 +30,12 @@ import ( "os" "os/exec" "os/user" + "path" "strconv" "strings" "syscall" + "github.com/containers/podman/v2/pkg/rootless" . "github.com/containers/podman/v2/test/utils" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -371,10 +373,23 @@ var _ = Describe("Toolbox-specific testing", func() { currentUser, err := user.Current() Expect(err).To(BeNil()) + session = podmanTest.Podman([]string{"run", "-v", fmt.Sprintf("%s:%s", currentUser.HomeDir, currentUser.HomeDir), "--userns=keep-id", fedoraToolbox, "sh", "-c", "echo $HOME"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) Expect(session.OutputToString()).To(ContainSubstring(currentUser.HomeDir)) + + if rootless.IsRootless() { + location := path.Dir(currentUser.HomeDir) + volumeArg := fmt.Sprintf("%s:%s", location, location) + session = podmanTest.Podman([]string{"run", + "--userns=keep-id", + "--volume", volumeArg, + fedoraToolbox, "sh", "-c", "echo $HOME"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(ContainSubstring(currentUser.HomeDir)) + } }) }) |