From 7cb7ba861fd95e689a3a365ed3617b45bfbb63fe Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Mon, 9 Sep 2024 16:23:22 +0200 Subject: [PATCH 1/2] Debug CRIU issue in TF --- packit.yaml | 12 - test/check-application | 2902 +--------------------------------------- test/run | 2 +- 3 files changed, 7 insertions(+), 2909 deletions(-) diff --git a/packit.yaml b/packit.yaml index fd73bc8bb..be1afdae1 100644 --- a/packit.yaml +++ b/packit.yaml @@ -19,23 +19,11 @@ jobs: - job: copr_build trigger: pull_request targets: - - fedora-40 - - fedora-41 - - fedora-latest-aarch64 - - fedora-development - - centos-stream-9-x86_64 - - centos-stream-9-aarch64 - centos-stream-10 - job: tests trigger: pull_request targets: - - fedora-40 - - fedora-41 - - fedora-latest-aarch64 - - fedora-development - - centos-stream-9-x86_64 - - centos-stream-9-aarch64 - centos-stream-10 - job: copr_build diff --git a/test/check-application b/test/check-application index f6bdeacc9..c13320521 100755 --- a/test/check-application +++ b/test/check-application @@ -137,2910 +137,20 @@ class TestApplication(testlib.MachineCase): self.has_selinux = not any(img in m.image for img in ["arch", "debian", "ubuntu", "suse"]) self.has_cgroupsV2 = not m.image.startswith('rhel-8') - self.system_images_count = int(self.execute(True, "podman images -n | wc -l").strip()) - self.user_images_count = int(self.execute(False, "podman images -n | wc -l").strip()) - # allow console.error self.allow_browser_errors( ".*couldn't search registry \".*\": pinging container registry .*", ".*Error occurred while connecting console: cannot resize session: cannot resize container.*", ) - def tearDown(self): - if self.getError(): - # dump container logs for debugging - for auth in [False, True]: - print(f"----- {'system' if auth else 'user'} containers -----", file=sys.stderr) - self.execute(auth, "podman ps -a >&2") - self.execute(auth, 'for c in $(podman ps -aq); do echo "---- $c ----" >&2; podman logs $c >&2; done') - - super().tearDown() - - def getRestartPolicy(self, auth, container_name): - cmd = f"podman inspect --format '{{{{.HostConfig.RestartPolicy}}}}' {container_name}" - return self.execute(auth, cmd).strip() - - def waitNumImages(self, expected): - self.browser.wait_js_func("ph_count_check", "#containers-images table[aria-label='Images'] > tbody", expected) - - def waitNumContainers(self, expected, auth): - if auth and self.machine.ostree_image: - extra = 1 # cockpit/ws - else: - extra = 0 - - self.browser.wait_js_func("ph_count_check", "#containers-containers tbody", expected + extra) - - def performContainerAction(self, container, cmd): - b = self.browser - b.click(f"#containers-containers tbody tr:contains('{container}') .pf-v5-c-menu-toggle") - b.click(f"#containers-containers tbody tr:contains('{container}') button.pf-v5-c-menu__item:contains({cmd})") - - def getContainerAction(self, container, cmd): - return f"#containers-containers tbody tr:contains('{container}') button.pf-v5-c-menu__item:contains({cmd})" - - def toggleExpandedContainer(self, container): - b = self.browser - b.click(f"#containers-containers tbody tr:contains('{container}') .pf-v5-c-table__toggle button") - - def getContainerAttr(self, container, key, selector=""): - b = self.browser - return b.text(f"#containers-containers tbody tr:contains('{container}') > td[data-label={key}] {selector}") - - def execute(self, system, cmd): - if system: - return self.machine.execute(cmd) - else: - return self.admin_s.execute(cmd) - - def login(self, system=True): - # HACK: The first rootless call often gets stuck or fails - # In such case we have alert banner to start the service (or just empty state) - # A real user would just hit the button so lets do the same as this is always getting - # back to us and we waste too much time reporting to podman with mixed results. - # Examples: - # https://github.com/containers/podman/issues/8762 - # https://github.com/containers/podman/issues/9251 - # https://github.com/containers/podman/issues/6660 - - b = self.browser - - self.login_and_go("/podman", superuser=system) - b.wait_visible("#app") - - with self.browser.wait_timeout(30): - try: - b.wait_not_in_text("#containers-containers", "Loading") - b.wait_not_present("#overview div.pf-v5-c-alert") - except testlib.Error: - if system: - b.click("#overview div.pf-v5-c-alert .pf-v5-c-alert__action > button:contains(Start)") - b.wait_not_present("#overview div.pf-v5-c-alert") - else: - b.click("#app .pf-v5-c-empty-state button.pf-m-primary") - b.wait_not_present("#app .pf-v5-c-empty-state button") - - def waitPodRow(self, podName, present=False): - if present: - self.browser.wait_visible("#table-" + podName) - else: - self.browser.wait_not_present("#table-" + podName) - - def waitPodContainer(self, podName, containerList, system=True): - if len(containerList): - for container in containerList: - self.waitContainer(container["id"], system, name=container["name"], image=container["image"], - cmd=container["command"], state=container["state"], pod=podName) - else: - if self.browser.val("#containers-containers-filter") == "all": - self.browser.wait_in_text("#table-" + podName + " .pf-v5-c-empty-state", "No containers in this pod") - else: - self.browser.wait_in_text("#table-" + podName + " .pf-v5-c-empty-state", - "No running containers in this pod") - - def waitContainerRow(self, container, present=True): - b = self.browser - if present: - b.wait_visible(f'#containers-containers td[data-label="Container"]:contains("{container}")') - else: - b.wait_not_present(f'#containers-containers td[data-label="Container"]:contains("{container}")') - - def performPodAction(self, podName, podOwner, action): - b = self.browser - - b.click(f"#pod-{podName}-{podOwner}-action-toggle") - b.click(f"ul.pf-v5-c-menu__list li > button.pod-action-{action.lower()}") - b.wait_not_present("ul.pf-v5-c-menu__list") - - def getStartTime(self, container: str, *, auth: bool) -> str: - # don't format the raw time strings from the API, force json format - out = self.execute(auth, "podman inspect --format '{{json .State.StartedAt}}' " + container) - return out.strip().replace('"', '') - - def waitRestart(self, container: str, old_start: str, *, auth: bool) -> str: - for _ in range(10): - new_start = self.getStartTime(container, auth=auth) - if new_start > old_start: - return new_start - time.sleep(1) - else: - self.fail("Timed out waiting for StartedAt change") - return '' # quiesce mypy, not reached - - def setupRegistry(self): - m = self.machine - - self.execute(True, f""" - podman run -d -p 5000:5000 --name registry --stop-timeout 0 {IMG_REGISTRY} - podman run -d -p 6000:5000 --name registry_alt --stop-timeout 0 {IMG_REGISTRY} - """) - - # Add local insecure registry into registries conf - m.write("/etc/containers/registries.conf", REGISTRIES_CONF) - self.execute(True, "systemctl stop podman.service") - - def testPods(self): - b = self.browser - - self.login() - - self.filter_containers("running") - if not self.machine.ostree_image: - b.wait_in_text("#containers-containers", "No running containers") - - # Run a pods as system - self.machine.execute("podman pod create --infra=false --name pod-1") - - self.waitPodRow("pod-1", False) - self.filter_containers("all") - self.waitPodContainer("pod-1", []) - - def get_pod_cpu_usage(pod_name): - cpu = self.browser.text(f"#table-{pod_name}-title .pod-cpu") - self.assertIn('%', cpu) - return float(cpu[:-1]) - - def get_pod_memory(pod_name): - memory = self.browser.text(f"#table-{pod_name}-title .pod-memory") - try: - value, unit = memory.split(' ') - self.assertIn(unit, ["GB", "MB", "kB", "B"]) - return float(value) - except ValueError: - # no unit → only for 0 or not initialized yet - self.assertIn(memory, ["0", ""]) - return 0 - - run_cmd = f"podman run -d --pod pod-1 --name test-pod-1-system --stop-timeout 0 {IMG_ALPINE} sleep 100" - containerId = self.machine.execute(run_cmd).strip() - self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE, - "command": "sleep 100", "state": "Running", "id": containerId}]) - cpu = get_pod_cpu_usage("pod-1") - b.wait(lambda: get_pod_memory("pod-1") > 0) - - # Test that cpu usage increases - self.machine.execute("podman exec -di test-pod-1-system sh -c 'dd bs=1024 < /dev/urandom > /dev/null'") - b.wait(lambda: get_pod_cpu_usage("pod-1") > cpu) - - self.machine.execute("podman pod stop -t0 pod-1") # disable timeout, so test doesn't wait endlessly - self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE, - "command": "sleep 100", "state": NOT_RUNNING, "id": containerId}]) - self.filter_containers("running") - self.waitPodRow("pod-1", False) - - self.filter_containers("all") - b.set_input_text('#containers-filter', 'pod-1') - self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE, - "command": "sleep 100", "state": NOT_RUNNING, "id": containerId}]) - b.set_input_text('#containers-filter', 'test-pod-1-system') - self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE, - "command": "sleep 100", "state": NOT_RUNNING, "id": containerId}]) - # TODO add pixel test when this is again reachable - https://github.com/cockpit-project/bots/issues/2463 - - # Check Pod Actions - self.performPodAction("pod-1", "system", "Start") - self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE, - "command": "sleep 100", "state": "Running", "id": containerId}]) - - self.performPodAction("pod-1", "system", "Pause") - self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE, - "command": "sleep 100", "state": "Paused", "id": containerId}]) - - self.performPodAction("pod-1", "system", "Unpause") - self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE, - "command": "sleep 100", "state": "Running", "id": containerId}]) - - self.performPodAction("pod-1", "system", "Stop") - self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE, - "command": "sleep 100", "state": NOT_RUNNING, "id": containerId}]) - - self.machine.execute("podman pod start pod-1") - self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE, - "command": "sleep 100", "state": "Running", "id": containerId}]) - - old_start = self.getStartTime("test-pod-1-system", auth=True) - self.performPodAction("pod-1", "system", "Restart") - self.waitRestart("test-pod-1-system", old_start, auth=True) - - self.performPodAction("pod-1", "system", "Delete") - b.click(".pf-v5-c-modal-box button:contains(Delete)") - # Alert should be shown, that running pods need to be force deleted. - b.wait_in_text(".pf-v5-c-modal-box__body .pf-v5-c-list", "test-pod-1-system") - b.click(".pf-v5-c-modal-box button:contains('Force delete')") - self.waitPodRow("pod-1", False) - - # HACK: there is some race here which steals the focus from the filter input and selects the page text instead - for _ in range(3): - b.focus('#containers-filter') - time.sleep(1) - if b.eval_js('document.activeElement == document.querySelector("#containers-filter")'): - break - b.set_input_text('#containers-filter', '') - self.machine.execute("podman pod create --infra=false --name pod-2") - self.waitPodContainer("pod-2", []) - run_cmd = f"podman run -d --pod pod-2 --name test-pod-2-system --stop-timeout 0 {IMG_ALPINE} sleep 100" - containerId = self.machine.execute(run_cmd).strip() - self.waitPodContainer("pod-2", [{"name": "test-pod-2-system", "image": IMG_ALPINE, - "command": "sleep 100", "state": "Running", "id": containerId}]) - self.machine.execute("podman rm --force -t0 test-pod-2-system") - self.waitPodContainer("pod-2", []) - self.performPodAction("pod-2", "system", "Delete") - b.wait_not_in_text(".pf-v5-c-modal-box__body", "test-pod-2-system") - b.click(".pf-v5-c-modal-box button:contains('Delete')") - self.waitPodRow("pod-2", False) - - # Volumes / mounts - self.machine.execute("podman pod create -p 9999:9999 -v /tmp:/app --name pod-3") - self.machine.execute("podman pod start pod-3") - - self.waitPodContainer("pod-3", []) - # Verify 1 port mapping - b.wait_in_text("#table-pod-3-title .pod-details-ports-btn", "1") - b.click("#table-pod-3-title .pod-details-ports-btn") - b.wait_in_text(".pf-v5-c-popover__content", "0.0.0.0:9999 → 9999/tcp") - # Verify 1 mount - b.wait_in_text("#table-pod-3-title .pod-details-volumes-btn", "1") - b.click("#table-pod-3-title .pod-details-volumes-btn") - b.wait_in_text(".pf-v5-c-popover__content", "/tmp ↔ /app") - - def testBasicSystem(self): - self._testBasic(True) - - b = self.browser - - # Test dropping and gaining privileges - b.set_val("#containers-containers-owner", "all") - self.filter_containers("all") - self.execute(False, "podman pod create --infra=false --name pod_user") - self.execute(True, "podman pod create --infra=false --name pod_system") - - checkImage(b, IMG_REGISTRY, "system") - checkImage(b, IMG_REGISTRY, "admin") - b.wait_visible("#containers-containers .pod-name:contains('pod_user')") - b.wait_visible("#containers-containers .pod-name:contains('pod_system')") - b.wait_visible("#containers-containers .container-name:contains('a')") - b.wait_visible("#containers-containers .container-name:contains('b')") - - # Drop privileges - all system things should disappear - b.drop_superuser() - b.wait_not_present("#containers-containers .pod-name:contains('pod_system')") - b.wait_not_present("#containers-containers .container-name:contains('a')") - b.wait_visible("#containers-containers .pod-name:contains('pod_user')") - b.wait_visible("#containers-containers .container-name:contains('b')") - # Checking images is harder but if there would be more than one this would fail - b.wait_visible(f"#containers-images:contains('{IMG_REGISTRY}')") - - # Owner select should disappear - b.wait_not_present("#containers-containers-owner") - - # Also user selection in image download should not be visible - b.click("#image-actions-dropdown") - b.click("button:contains(Download new image)") - - b.wait_visible('div.pf-v5-c-modal-box header:contains("Search for an image")') - b.wait_visible("div.pf-v5-c-modal-box footer button:contains(Download):disabled") - b.wait_not_present("#as-user") - b.click(".pf-v5-c-modal-box button:contains('Cancel')") - b.wait_not_present('div.pf-v5-c-modal-box header:contains("Search for an image")') - - # Gain privileges - b.become_superuser(passwordless=self.machine.image == "rhel4edge") - - # We are notified that we can also start the system one - b.wait_in_text("#overview div.pf-v5-c-alert .pf-v5-c-alert__title", "System Podman service is also available") - b.click("#overview div.pf-v5-c-alert .pf-v5-c-alert__action > button:contains(Start)") - b.wait_not_present("#overview div.pf-v5-c-alert .pf-v5-c-alert__title") - - checkImage(b, IMG_REGISTRY, "system") - checkImage(b, IMG_REGISTRY, "admin") - b.wait_visible("#containers-containers .pod-name:contains('pod_user')") - b.wait_visible("#containers-containers .pod-name:contains('pod_system')") - b.wait_visible("#containers-containers .container-name:contains('a')") - b.wait_visible("#containers-containers .container-name:contains('b')") - - # Owner select should appear - b.wait_visible("#containers-containers-owner") - - # Also user selection in image download should be visible - b.click("#image-actions-dropdown") - b.click("button:contains(Download new image)") - b.wait_visible('div.pf-v5-c-modal-box header:contains("Search for an image")') - b.wait_visible("div.pf-v5-c-modal-box footer button:contains(Download):disabled") - b.wait_visible("#as-user") - b.click(".pf-v5-c-modal-box button:contains('Cancel')") - b.wait_not_present('div.pf-v5-c-modal-box header:contains("Search for an image")') - - # Check that when we filter only system stuff an then drop privileges that we show user stuff - b.set_val("#containers-containers-owner", "system") - b.wait_not_present("#containers-containers .pod-name:contains('pod_user')") - b.wait_not_present("#containers-containers .container-name:contains('b')") - # Checking images is harder but if there would be more than one this would fail - b.wait_visible(f"#containers-images:contains('{IMG_REGISTRY}')") - - b.drop_superuser() - b.wait_visible("#containers-containers .pod-name:contains('pod_user')") - b.wait_visible("#containers-containers .container-name:contains('b')") - # Checking images is harder but if there would be more than one this would fail - b.wait_visible(f"#containers-images:contains('{IMG_REGISTRY}')") - - # Check showing of entrypoint - b.click("#containers-containers-create-container-btn") - b.click("#create-image-image-select-typeahead") - b.click(f'button.pf-v5-c-select__menu-item:contains("{IMG_REGISTRY}")') - b.wait_val("#run-image-dialog-command", '/etc/docker/registry/config.yml') - b.wait_text("#run-image-dialog-entrypoint", '/entrypoint.sh') - - # Deleting image will cleanup both command and entrypoint - b.click("button.pf-v5-c-select__toggle-clear") - b.wait_val("#run-image-dialog-command", '') - b.wait_not_present("#run-image-dialog-entrypoint") - - # Edited command will not be cleared - b.click("#create-image-image-select-typeahead") - b.click(f'button.pf-v5-c-select__menu-item:contains("{IMG_REGISTRY}")') - b.wait_val("#run-image-dialog-command", '/etc/docker/registry/config.yml') - b.set_input_text("#run-image-dialog-command", '/etc/docker/registry/config.yaml') - b.click("button.pf-v5-c-select__toggle-clear") - b.wait_not_present("#run-image-dialog-entrypoint") - b.wait_val("#run-image-dialog-command", '/etc/docker/registry/config.yaml') - - # Setting a new image will still keep the old command and not prefill it - b.click("#create-image-image-select-typeahead") - b.click(f'button.pf-v5-c-select__menu-item:contains({IMG_ALPINE})') - b.wait_visible("#run-image-dialog-pull-latest-image") - b.wait_val("#run-image-dialog-command", '/etc/docker/registry/config.yaml') - - b.logout() - - if self.machine.ostree_image: - self.machine.execute("echo foobar | passwd --stdin root") - self.write_file("/etc/ssh/sshd_config.d/99-root-password.conf", "PermitRootLogin yes", - post_restore_action="systemctl try-restart sshd") - self.machine.execute("systemctl try-restart sshd") - - # Test that when root is logged in we don't present "user" and "system" - self.login_and_go("/podman", user="root", enable_root_login=True) - b.wait_visible("#app") - - # `User Service is also available` banner should not be present - b.wait_not_present("#overview div.pf-v5-c-alert") - # There should not be any duplicate images listed - # The "busybox" and "alpine" images have been deleted by _testBasic. - showImages(b) - self.waitNumImages(self.system_images_count - 2) - # There should not be 'owner' selector - b.wait_not_present("#containers-containers-owner") - - # Test the isSystem boolean for searching - # https://github.com/cockpit-project/cockpit-podman/pull/891 - b.click("#containers-containers-create-container-btn") - b.set_input_text("#create-image-image-select-typeahead", "registry") - b.wait_visible('button.pf-v5-c-select__menu-item:contains("registry")') - self.confirm_modal("Cancel") - - def testBasicUser(self): - self._testBasic(False) - - def _testBasic(self, auth): - b = self.browser - - def clickDeleteImage(image_sel): - b.click(f'{image_sel} .pf-v5-c-menu-toggle') - b.click(image_sel + " button.btn-delete") - - if not auth: - self.allow_browser_errors("Failed to start system podman.socket.*") - - expected_ws = "" - if auth and self.machine.ostree_image: - expected_ws += "ws" - - self.login(auth) - - # Check all containers - if auth: - checkImage(b, IMG_ALPINE, "system") - checkImage(b, IMG_BUSYBOX, "system") - checkImage(b, IMG_REGISTRY, "system") - - checkImage(b, IMG_ALPINE, "admin") - checkImage(b, IMG_BUSYBOX, "admin") - checkImage(b, IMG_REGISTRY, "admin") - - # Check order of images - text = b.text("#containers-images table") - if auth: - # all user images before all system images - self.assertRegex(text, ".*admin.*system.*") - self.assertNotRegex(text, ".*system.*admin.*") - else: - self.assertNotIn("system", text) - # images are sorted alphabetically - self.assertRegex(text, ".*/test-alpine.*/test-busybox.*/test-registry") - - # build a dummy image so that the timestamp is "today" (for predictable pixel tests) - # ensure that CMD neither comes first (podman rmi leaves that layer otherwise) - # nor last (then the topmost layer does not match the image ID) - IMG_HELLO_LATEST = "localhost/test-hello:latest" - self.machine.execute(f"""set -eu; D={self.vm_tmpdir}/hello; - mkdir $D - printf 'FROM scratch\\nCOPY test.txt /\\nCMD ["/run.sh"]\\nCOPY test.txt /test2.txt\\n' > $D/Containerfile - echo hello > $D/test.txt""") - self.execute(auth, f"podman build -t {IMG_HELLO_LATEST} {self.vm_tmpdir}/hello") - - # prepare image ids - much easier to pick a specific container - images = {} - for image in self.execute(auth, "podman images --noheading --no-trunc").strip().split("\n"): - # sha256: - items = image.split() - images[f"{items[0]}:{items[1]}"] = items[2].split(":")[-1] - - # show image listing toggle - hello_sel = f"#containers-images tbody tr[data-row-id=\"{images[IMG_HELLO_LATEST]}{auth}\"]".lower() - b.wait_visible(hello_sel) - b.click(hello_sel + " td.pf-v5-c-table__toggle button") - b.click(hello_sel + " .pf-v5-c-menu-toggle") - b.wait_visible(hello_sel + " button.btn-delete") - b.wait_in_text("#containers-images tbody.pf-m-expanded tr .image-details:first-child", "Command/run.sh") - # Show history - b.click("#containers-images tbody.pf-m-expanded .pf-v5-c-tabs__list li:nth-child(2) button") - first_row_sel = "#containers-images .pf-v5-c-table__expandable-row.pf-m-expanded tbody:first-of-type" - b.wait_in_text(f"{first_row_sel} td[data-label=\"ID\"]", - images[IMG_HELLO_LATEST][:12]) - created_sel = f"{first_row_sel} td[data-label=\"Created\"]" - b.wait_text(f"{created_sel}", "less than a minute ago") - # topmost (last) layer - created_sel = f"{first_row_sel} td[data-label=\"Created by\"]" - b.wait_in_text(f"{created_sel}", "COPY") - b.wait_in_text(f"{created_sel}", "in /test2.txt") - # initial (first) layer - last_row_sel = "#containers-images .pf-v5-c-table__expandable-row.pf-m-expanded tbody:last-of-type" - b.wait_in_text(f"{last_row_sel} td[data-label=\"Created by\"]", "COPY") - - self.execute(auth, f"podman rmi {IMG_HELLO_LATEST}") - b.wait_not_present(hello_sel) - - # make sure no running containers shown; on CoreOS there's the cockpit/ws container - self.filter_containers('running') - if auth and self.machine.ostree_image: - self.waitContainerRow("ws") - else: - b.wait_in_text("#containers-containers", "No running containers") - - if auth: - # Run two containers as system (first exits immediately) - self.execute(auth, f"podman run -d --name test-sh-system --stop-timeout 0 {IMG_ALPINE} sh") - self.execute(auth, f"podman run -d --name swamped-crate-system --stop-timeout 0 {IMG_BUSYBOX} sleep 1000") - - # Run two containers as admin (first exits immediately) - self.execute(False, f"podman run -d --name test-sh-user --stop-timeout 0 {IMG_ALPINE} sh") - self.execute(False, f"podman run -d --name swamped-crate-user --stop-timeout 0 {IMG_BUSYBOX} sleep 1000") - - # Test owner filtering - if auth: - self.waitNumImages(self.user_images_count + self.system_images_count) - self.waitNumContainers(2, True) - - def verify_system(): - self.waitNumImages(self.system_images_count) - b.wait_in_text("#containers-images", "system") - self.waitNumContainers(1, True) - b.wait_in_text("#containers-containers", "system") - - b.set_val("#containers-containers-owner", "system") - verify_system() - b.set_val("#containers-containers-owner", "all") - b.go("#/?owner=system") - verify_system() - - def verify_user(): - self.waitNumImages(self.user_images_count) - b.wait_in_text("#containers-images", "admin") - self.waitNumContainers(1, False) - b.wait_in_text("#containers-containers", "admin") - - b.set_val("#containers-containers-owner", "user") - verify_user() - b.set_val("#containers-containers-owner", "all") - b.go("#/?owner=user") - verify_user() - - b.set_val("#containers-containers-owner", "all") - self.waitNumImages(self.user_images_count + self.system_images_count) - self.waitNumContainers(2, True) - else: # No 'owner' selector when not privileged - b.wait_not_present("#containers-containers-owner") - - user_containers = {} - system_containers = {} - for container in self.execute(True, "podman ps --all --no-trunc").strip().split("\n")[1:]: - # - items = container.split() - system_containers[items[-1]] = items[0] - for container in self.execute(False, "podman ps --all --no-trunc").strip().split("\n")[1:]: - # - items = container.split() - user_containers[items[-1]] = items[0] - - # running busybox shown - if auth: - self.waitContainerRow("swamped-crate-system") - self.waitContainer(system_containers["swamped-crate-system"], True, name='swamped-crate-system', - image=IMG_BUSYBOX, cmd="sleep 1000", state='Running') - - self.waitContainerRow("swamped-crate-user") - self.waitContainer(user_containers["swamped-crate-user"], False, name='swamped-crate-user', - image=IMG_BUSYBOX, cmd="sleep 1000", state='Running') - - # exited alpine not shown - b.wait_not_in_text("#containers-containers", "alpine") - - # show all containers and check status - b.go("#/?container=all") - - # exited alpine under everything list - b.wait_visible("#containers-containers") - if auth: - self.waitContainer(system_containers["test-sh-system"], True, name='test-sh-system', image=IMG_ALPINE, - cmd='sh', state=NOT_RUNNING) - - self.waitContainer(user_containers["test-sh-user"], False, name='test-sh-user', image=IMG_ALPINE, - cmd='sh', state=NOT_RUNNING) - - self.performContainerAction("swamped-crate-user", "Delete") - self.confirm_modal("Cancel") - - if auth: - self.performContainerAction("swamped-crate-system", "Delete") - self.confirm_modal("Cancel") - - # Checked order of containers - expected = ["swamped-crate-user", "test-sh-user"] - if auth: - expected.extend(["swamped-crate-system", "test-sh-system"]) - expected.extend([expected_ws]) - b.wait_collected_text("#containers-containers .container-name", ''.join(sorted(expected))) - - # show running container - self.filter_containers('running') - if auth: - self.waitContainer(system_containers["swamped-crate-system"], True, name='swamped-crate-system', - image=IMG_BUSYBOX, cmd="sleep 1000", state='Running') - self.waitContainer(user_containers["swamped-crate-user"], False, name='swamped-crate-user', - image=IMG_BUSYBOX, cmd="sleep 1000", state='Running') - # check exited alpine not in running list - b.wait_not_in_text("#containers-containers", "alpine") - - # delete running container busybox using force delete - if auth: - self.performContainerAction("swamped-crate-system", "Delete") - self.confirm_modal("Force delete") - self.waitContainerRow("swamped-crate-system", False) - - self.filter_containers("all") - - self.performContainerAction("swamped-crate-user", "Delete") - self.confirm_modal("Force delete") - self.waitContainerRow("swamped-crate-user", False) - - self.waitContainerRow("test-sh-user") - self.performContainerAction("test-sh-user", "Delete") - self.confirm_modal("Delete") - b.wait_not_in_text("#containers-containers", "test-sh-user") - - if auth: - self.waitContainerRow("test-sh-system") - self.performContainerAction("test-sh-system", "Delete") - self.confirm_modal("Delete") - b.wait_not_in_text("#containers-containers", "test-sh-system") - - # delete image busybox that hasn't been used - # First try to just untag and then remove with more tags - self.execute(auth, f"podman tag {IMG_BUSYBOX} {IMG_BUSYBOX}:1") - self.execute(auth, f"podman tag {IMG_BUSYBOX} {IMG_BUSYBOX}:2") - self.execute(auth, f"podman tag {IMG_BUSYBOX} {IMG_BUSYBOX}:3") - self.execute(auth, f"podman tag {IMG_BUSYBOX} {IMG_BUSYBOX}:4") - - busybox_sel = f"#containers-images tbody tr[data-row-id=\"{images[IMG_BUSYBOX_LATEST]}{auth}\"]".lower() - b.click(busybox_sel + " td.pf-v5-c-table__toggle button") - - b.wait_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:1") - b.wait_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:2") - b.wait_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:3") - b.wait_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:4") - - clickDeleteImage(busybox_sel) - self.assertTrue(b.get_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX_LATEST}']")) - b.set_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX}:1']", True) - b.set_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX}:3']", True) - b.set_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX_LATEST}']", False) - self.confirm_modal("Delete") - b.wait_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX_LATEST}") - b.wait_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:2") - b.wait_not_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:1") - b.wait_not_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:3") - - clickDeleteImage(busybox_sel) - b.click("#delete-all") - self.assertTrue(b.get_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX_LATEST}']")) - self.assertTrue(b.get_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX}:2']")) - self.assertTrue(b.get_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX}:4']")) - self.confirm_modal("Delete") - self.confirm_modal("Force delete") - b.wait_not_present(busybox_sel) - - # Check that we correctly show networking information - # Rootless don't have this info - if auth: - self.execute(auth, f"podman run -dt --name net_check --stop-timeout 0 {IMG_ALPINE}") - self.toggleExpandedContainer("net_check") - b.wait_in_text(".pf-m-expanded .container-details-networking", - self.execute(auth, """ - podman inspect --format '{{.NetworkSettings.Gateway}}' net_check""").strip()) - b.wait_in_text(".pf-m-expanded .container-details-networking", - self.execute(auth, """ - podman inspect --format '{{.NetworkSettings.IPAddress}}' net_check""").strip()) - b.wait_in_text(".pf-m-expanded .container-details-networking", - self.execute(auth, """ - podman inspect --format '{{.NetworkSettings.MacAddress}}' net_check""").strip()) - self.execute(auth, "podman stop net_check") - b.wait(lambda: self.execute(True, "podman ps --all | grep -e net_check -e Exited")) - self.toggleExpandedContainer("net_check") - sha = self.execute(auth, "podman inspect --format '{{.Id}}' net_check").strip() - self.waitContainer(sha, auth, state='Exited') - - # delete image alpine that has been used by a container - self.execute(auth, f"podman run -d --name test-sh4 --stop-timeout 0 {IMG_ALPINE} sh") - # our pixel test expects both containers to be in state "Exited" - sha = self.execute(auth, "podman inspect --format '{{.Id}}' test-sh4").strip() - self.waitContainer(sha, auth, name="test-sh4", state='Exited') - if auth: - b.assert_pixels('#app', "overview", ignore=[".ignore-pixels"], skip_layouts=["rtl", "mobile"]) - alpine_sel = f"#containers-images tbody tr[data-row-id=\"{images[IMG_ALPINE_LATEST]}{auth}\"]".lower() - b.wait_visible(alpine_sel) - b.click(alpine_sel + " td.pf-v5-c-table__toggle button") - clickDeleteImage(alpine_sel) - self.confirm_modal("Delete") - self.confirm_modal("Force delete") - b.wait_not_present(alpine_sel) - - b.wait_collected_text("#containers-containers .container-name", expected_ws) - self.execute(auth, f"podman run -d --name c --stop-timeout 0 {IMG_REGISTRY} sh") - b.wait_collected_text("#containers-containers .container-name", "c" + expected_ws) - self.execute(auth, f"podman run -d --name a --stop-timeout 0 {IMG_REGISTRY} sh") - b.wait_collected_text("#containers-containers .container-name", "ac" + expected_ws) - - self.execute(False, f"podman run -d --name b --stop-timeout 0 {IMG_REGISTRY} sh") - if auth: - b.wait_collected_text("#containers-containers .container-name", "abc" + expected_ws) - self.execute(False, f"podman run -d --name doremi --stop-timeout 0 {IMG_REGISTRY} sh") - b.wait_collected_text("#containers-containers .container-name", "abcdoremi" + expected_ws) - b.wait(lambda: self.getContainerAttr("doremi", "State") in NOT_RUNNING) - else: - b.wait_collected_text("#containers-containers .container-name", "abc") - - # Test intermediate images - b.wait_not_present(".listing-action") - tmpdir = self.execute(auth, "mktemp -d").strip() - self.execute(auth, f"echo 'FROM {IMG_REGISTRY}\nRUN ls' > {tmpdir}/Dockerfile") - self.execute(auth, f"podman build {tmpdir}") - - b.wait_not_in_text("#containers-images", ":") - b.click(".listing-action button:contains('Show intermediate images')") - b.wait_in_text("#containers-images", ":") - b.wait_text("#containers-images tbody:last-child td[data-label=Created]", "less than a minute ago") - - b.click(".listing-action button:contains('Hide intermediate images')") - b.wait_not_in_text("#containers-images", ":") - - # Intermediate images are not shown in create container dialog - b.click("#containers-containers-create-container-btn") - b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")') - b.click("#create-image-image-select-typeahead") - b.wait_visible(f".pf-v5-c-select__menu-item:contains('{IMG_REGISTRY}')") - b.wait_not_present(".pf-v5-c-select__menu-item:contains('none')") - b.click(".pf-v5-c-modal-box .btn-cancel") - b.wait_not_present(".pf-v5-c-modal-box") - - # Delete intermediate images - intermediate_image_sel = "#containers-images tbody:last-child:contains(':')" - b.click(".listing-action button:contains('Show intermediate images')") - clickDeleteImage(intermediate_image_sel) - self.confirm_modal("Delete") - b.wait_not_present(intermediate_image_sel) - - # Create intermediate image and use it in a container - tmpdir = self.execute(auth, "mktemp -d").strip() - self.execute(auth, f"echo 'FROM {IMG_REGISTRY}\nRUN ls' > {tmpdir}/Dockerfile") - IMG_INTERMEDIATE = 'localhost/test-intermediate' - self.execute(auth, f"podman build -t {IMG_INTERMEDIATE} {tmpdir}") - b.click(f'#containers-images tbody tr:contains("{IMG_INTERMEDIATE}") .ct-container-create') - b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")') - b.click("#create-image-create-btn") - b.wait_not_present("div.pf-v5-c-modal-box") - self.waitContainerRow(IMG_INTERMEDIATE) - - # Integration tab should not crash with an intermediate image - self.toggleExpandedContainer(IMG_INTERMEDIATE) - b.click(".pf-m-expanded button:contains('Integration')") - - # Delete intermediate image which is in use - self.execute(auth, f"podman untag {IMG_INTERMEDIATE}") - clickDeleteImage(intermediate_image_sel) - self.confirm_modal("Delete") - self.confirm_modal("Force delete") - b.wait_not_in_text("#containers-images", ":") - b.wait_not_in_text("#containers-containers", IMG_INTERMEDIATE) - - def testCommitUser(self): - self._testCommit(False) - - def testCommitSystem(self): - self._testCommit(True) - - def _testCommit(self, auth): - b = self.browser - self.allow_browser_errors("Failed to commit container .* repository name must be lowercase") - - self.login(auth) - - # run a container (will exit immediately) and test the display of commit modal - self.execute(auth, f"podman run -d --name test-sh0 --stop-timeout 0 {IMG_ALPINE} sh -c 'ls -a'") - - self.filter_containers("all") - self.waitContainerRow("test-sh0") - self.toggleExpandedContainer("test-sh0") - - self.performContainerAction("test-sh0", "Commit") - b.wait_visible(".pf-v5-c-modal-box") - - b.wait_in_text(".pf-v5-c-modal-box__description", "state of the test-sh0 container") - - # Empty name yields warning - b.click("button:contains(Commit)") - b.wait_text("#commit-dialog-image-name-helper", "Image name is required") - b.wait_visible("button:contains(Commit):disabled") - b.wait_visible("button:contains('Force commit')") - # Warning should be cleaned when updating name - b.set_input_text("#commit-dialog-image-name", "foobar") - b.wait_not_present("button:contains('Force commit')") - b.wait_not_present("#commit-dialog-image-name-helper") - - # Existing name yields warning - b.set_input_text("#commit-dialog-image-name", IMG_ALPINE) - b.click("button:contains(Commit)") - b.wait_text("#commit-dialog-image-name-helper", "Image name is not unique") - b.wait_visible("button:contains(Commit):disabled") - b.wait_visible("button:contains('Force commit')") - # Warning should be cleaned when updating tag - b.set_input_text("#commit-dialog-image-tag", "foobar") - b.wait_not_present("button:contains('Force commit')") - b.wait_not_present("#commit-dialog-image-name-helper") - - # Check failing commit - b.set_input_text("#commit-dialog-image-name", "TEST") - b.click("button:contains(Commit)") - b.wait_in_text(".pf-v5-c-alert", "Failed to commit container test-sh0") - b.wait_in_text(".pf-v5-c-alert", "repository name must be lowercase") - - # Test cancel - self.confirm_modal("Cancel") - - # Force commit empty container - self.performContainerAction("test-sh0", "Commit") - b.wait_visible(".pf-v5-c-modal-box") - # We prefill command - b.wait_val("#commit-dialog-command", 'sh -c "ls -a"') - # Test docker format - b.set_checked("#commit-dialog-docker", True) - b.click("button:contains(Commit)") - self.confirm_modal("Force commit") - - # don't use waitNumImages() here, as we want to include anonymous images - def waitImageCount(expected): - if auth: - expected += self.system_images_count - - b.wait_in_text("#containers-images", f"{expected} images") - - waitImageCount(self.user_images_count + 1) - image_id = self.execute(auth, "podman images --sort created --format '{{.Id}}' | head -n 1").strip() - manifest_type = self.execute(auth, "podman inspect --format '{{.ManifestType}}' " + image_id).strip() - cmd = self.execute(auth, "podman inspect --format '{{.Config.Cmd}}' " + image_id).strip() - self.assertIn("docker.distribution.manifest", manifest_type) - self.assertEqual("[sh -c ls -a]", cmd) - - # Commit with name, tag, author and edited command - self.performContainerAction("test-sh0", "Commit") - b.wait_visible(".pf-v5-c-modal-box") - b.set_input_text("#commit-dialog-image-name", "newname") - b.set_input_text("#commit-dialog-image-tag", "24") - b.set_input_text("#commit-dialog-author", "MM") - b.set_input_text("#commit-dialog-command", "sh -c 'ps'") - - if auth: - b.assert_pixels(".pf-v5-c-modal-box", "commit", skip_layouts=["rtl"]) - - self.confirm_modal("Commit") - - waitImageCount(self.user_images_count + 2) - self.assertEqual(self.execute(auth, "podman inspect --format '{{.Author}}' newname:24").strip(), "MM") - self.assertEqual(self.execute(auth, "podman inspect --format '{{.Config.Cmd}}' newname:24").strip(), - "[sh -c ps]") - self.assertIn("vnd.oci.image.manifest", - self.execute(auth, "podman inspect --format '{{.ManifestType}}' newname:24").strip()) - - # Test commit of running container - self.execute(auth, f"podman run -d --name test-sh2 --stop-timeout 0 {IMG_BUSYBOX} sleep 1000") - self.performContainerAction("test-sh2", "Commit") - b.wait_visible(".pf-v5-c-modal-box") - b.set_input_text("#commit-dialog-image-name", "newname") - self.confirm_modal("Commit") - waitImageCount(self.user_images_count + 3) - self.assertEqual(self.execute(auth, - "podman inspect --format '{{.Config.Cmd}}' newname:latest").strip(), - "[sleep 1000]") - - # Test commit of running container with pause (also conflicting name through :latest) - # This only works on rootless with cgroupsv2 - if auth or self.has_cgroupsV2: - self.performContainerAction("test-sh2", "Commit") - b.wait_visible(".pf-v5-c-modal-box") - b.set_input_text("#commit-dialog-image-name", "newname") - b.set_checked("#commit-dialog-pause", True) - b.click("button:contains(Commit)") - self.confirm_modal("Force commit") - waitImageCount(self.user_images_count + 4) - - def testDownloadImage(self): - b = self.browser - execute = self.execute - - def prepare(): - # Create and start registry containers - self.execute(True, f"podman run -d -p 5000:5000 --name registry --stop-timeout 0 {IMG_REGISTRY}") - self.execute(True, f"podman run -d -p 6000:5000 --name registry_alt --stop-timeout 0 {IMG_REGISTRY}") - # Add local insecure registry into registries conf - self.machine.write("/etc/containers/registries.conf", REGISTRIES_CONF) - self.execute(True, "systemctl stop podman.service") - # Push busybox image to the local registries - self.execute(True, - f"podman tag {IMG_BUSYBOX} localhost:5000/my-busybox; podman push localhost:5000/my-busybox") - self.execute(True, - f"podman tag {IMG_BUSYBOX} localhost:6000/my-busybox; podman push localhost:6000/my-busybox") - # Untag busybox image which duplicates the image we are about to download - self.execute(True, f"podman rmi -f {IMG_BUSYBOX} localhost:5000/my-busybox localhost:6000/my-busybox") - self.execute(False, f"podman rmi -f {IMG_BUSYBOX}") - - class DownloadImageDialog(): - def __init__(self, test_obj, imageName, imageTag=None, user="system"): - self.imageName = imageName - self.imageTag = imageTag - self.user = user - self.imageSha = "" - self.assertTrue = test_obj.assertTrue - - def openDialog(self): - # Open get new image modal - b.click("#image-actions-dropdown") - b.click("button:contains(Download new image)") - b.wait_visible('div.pf-v5-c-modal-box header:contains("Search for an image")') - b.wait_visible("div.pf-v5-c-modal-box footer button:contains(Download):disabled") - - return self - - def fillDialog(self): - # Search for image specified with self.imageName and self.imageTag - b.click(f"#{self.user}") - b.set_val('#registry-select', "localhost:5000") - # HACK: Sometimes the value is not shown fully. FIXME - b.set_input_text("#search-image-dialog-name", self.imageName, value_check=False) - if self.imageTag: - b.set_input_text(".image-tag-entry input", self.imageTag) - - return self - - def selectImageAndDownload(self): - # Select and download the self.imageName image - b.click(f".pf-v5-c-data-list .image-name:contains({self.imageName})") - b.click("div.pf-v5-c-modal-box footer button:contains(Download)") - b.wait_not_present("div.pf-v5-c-modal-box") - - return self - - def expectDownloadErrorForNonExistingTag(self): - title = f"Danger alert:Failed to download image localhost:5000/{self.imageName}:{self.imageTag}" - b.wait_visible(f'h4.pf-v5-c-alert__title:contains("{title}")') - - return self - - def expectSearchErrorForNotExistingImage(self): - b.wait_visible(f".pf-v5-c-modal-box__body:contains(No results for {self.imageName})") - b.click(".pf-v5-c-modal-box button.btn-cancel") - b.wait_not_present(".pf-v5-c-modal-box__body") - - return self - - def expectDownloadSuccess(self): - # Confirm that the modal dialog is not open anymore - b.wait_not_present('div.pf-v5-c-modal-box') - # Confirm that the image got downloaded - checkImage(b, - f"localhost:5000/{self.imageName}:{self.imageTag or 'latest'}", - "system" if self.user == "system" else "admin") - - # Confirm that no error has happened - b.wait_not_present('h4.pf-v5-c-alert__title:contains("Failed to download image")') - - # Find out this image ID - container_name = f"localhost:5000/{self.imageName}:{self.imageTag or 'latest'}" - self.imageSha = execute(self.user == "system", - f"podman inspect --format '{{{{.Id}}}}' {container_name}").strip() - - return self - - def deleteImage(self, force=False, another=None): - imageTagSuffix = ":" + (self.imageTag or 'latest') - - # Select the image row - - # show image listing toggle - imageId = f"{self.imageSha}{'true' if self.user == 'system' else 'false'}" - sel = f"#containers-images tbody tr[data-row-id=\"{imageId}\"]" - b.wait_visible(sel) - b.click(sel + " td.pf-v5-c-table__toggle button") - - # Click the delete icon on the image row - b.click(sel + " .pf-v5-c-menu-toggle") - b.click(sel + ' button.btn-delete') - - if another: - b.click("#delete-all") - sel = f".pf-v5-c-check__input[aria-label='localhost:5000/{self.imageName}{imageTagSuffix}']" - self.assertTrue(b.get_checked(sel)) - self.assertTrue(b.get_checked(f".pf-v5-c-check__input[aria-label='{another}']")) - b.click("#delete-all") - b.wait_visible("#btn-img-delete:disabled") - - b.set_checked( - f".pf-v5-c-check__input[aria-label='localhost:5000/{self.imageName}{imageTagSuffix}']", True) - b.set_checked(f".pf-v5-c-check__input[aria-label='{another}']", True) - - # Confirm deletion in the delete dialog - b.click(".pf-v5-c-modal-box #btn-img-delete") - - if force: - # Confirm force delete - b.click(".pf-v5-c-modal-box button:contains('Force delete')") - - b.wait_not_present(sel) - - return self - - prepare() - - self.login() - - # Test registries - b.click("#image-actions-dropdown") - b.click("button:contains(Download new image)") - b.wait_visible('div.pf-v5-c-modal-box header:contains("Search for an image")') - # HACK: Sometimes the value is not shown fully. FIXME - b.set_input_text("#search-image-dialog-name", "my-busybox", value_check=False) - - b.wait_visible(".pf-v5-c-data-list .image-name:contains('localhost:5000/my-busybox')") - b.wait_visible(".pf-v5-c-data-list .image-name:contains('localhost:6000/my-busybox')") - b.assert_pixels(".podman-search", "download", skip_layouts=["rtl"]) - - b.set_val('#registry-select', "localhost:6000") - b.wait_not_present(".pf-v5-c-data-list .image-name:contains('localhost:5000/my-busybox')") - b.wait_visible(".pf-v5-c-data-list .image-name:contains('localhost:6000/my-busybox')") - b.click(".pf-v5-c-modal-box button:contains('Cancel')") - b.wait_not_present('div.pf-v5-c-modal-box') - - dialog0 = DownloadImageDialog(self, imageName='my-busybox', user="system") - dialog0.openDialog() \ - .fillDialog() \ - .selectImageAndDownload() \ - .expectDownloadSuccess() - dialog0.deleteImage() - - dialog1 = DownloadImageDialog(self, imageName='my-busybox', user="user") - dialog1.openDialog() \ - .fillDialog() \ - .selectImageAndDownload() \ - .expectDownloadSuccess() - # test recognition/deletion of multiple image tags - second_tag = "localhost/copybox:latest" - self.execute(False, f"podman tag localhost:5000/my-busybox {second_tag}") - # expand details - b.click("#containers-images tr:contains('my-busybox') td.pf-v5-c-table__toggle button") - b.wait_in_text("#containers-images tbody.pf-m-expanded tr .image-details", second_tag) - dialog1.deleteImage(True, another=second_tag) - - dialog = DownloadImageDialog(self, imageName='my-busybox', imageTag='latest', user="system") - dialog.openDialog() \ - .fillDialog() \ - .selectImageAndDownload() \ - .expectDownloadSuccess() \ - .deleteImage() - - dialog = DownloadImageDialog(self, imageName='foobar') - dialog.openDialog() \ - .fillDialog() \ - .expectSearchErrorForNotExistingImage() - - dialog = DownloadImageDialog(self, imageName='my-busybox', imageTag='foobar') - dialog.openDialog() \ - .fillDialog() \ - .selectImageAndDownload() \ - .expectDownloadErrorForNonExistingTag() - - def testPullImage(self): - b = self.browser - m = self.machine - - self.setupRegistry() - - def buildImage(auth, size, image_name): - temp_dir = self.machine.execute(f""" - set -eu - D=$(mktemp -d -p {self.vm_tmpdir}) - # Allow admin to read the created directory, mktemp by default creates it unreadable for other users - chmod 755 $D - truncate -s {size}M $D/test.img - printf 'FROM scratch\\nCOPY test.img /\\n' > $D/Containerfile - echo $D - """).strip() - self.execute(auth, f"podman build -t {image_name} {temp_dir}") - - def getImageSize(image_name): - return float(b.text(f"tr:contains('{image_name}') td[data-label='Disk space']").replace(' MB', '')) - - image1 = "localhost:5000/test-image:1" - - # build update and push it - buildImage(True, 50, image1) - self.execute(True, f"podman push {image1}; podman rmi {image1}") - - # build image which we try to pull - buildImage(True, 1, image1) - image1_sha = self.execute(True, f"podman inspect --format '{{{{.Id}}}}' {image1}").strip() - - self.login() - - image1_sel = f"#containers-images tbody tr[data-row-id=\"{image1_sha}true\"]".lower() - - b.click("#containers-images button.pf-v5-c-expandable-section__toggle") - b.click(f'{image1_sel} .pf-v5-c-menu-toggle') - b.click(image1_sel + " button.pf-v5-c-menu__item:contains('Pull')") - b.wait_in_text("div.download-in-progress", "Pulling") - b.wait_not_present(image1_sel) - - # assert that the new image is bigger, the initial image as 1MB, the - # new one 50MB. - b.wait(lambda: getImageSize(image1) > 40) - - image1_update_sha = self.execute(True, f"podman inspect --format '{{{{.Id}}}}' {image1}").strip() - self.assertNotEqual(image1_sha, image1_update_sha) - b.wait_visible(f"#containers-images tbody tr[data-row-id=\"{image1_update_sha}true\"]".lower()) - - # Try to pull multiple images, drop to superuser as its easier to - # remove all images and pulling all images will show an error if the - # registry is not available. - b.drop_superuser() - - image1 = "localhost:5000/test-image:1" - - # build update and push it - buildImage(False, 25, image1) - self.execute(False, f"podman push {image1}; podman rmi {image1}") - - # build image which we try to pull - buildImage(False, 1, image1) - image1_sha = self.execute(False, f"podman inspect --format '{{{{.Id}}}}' {image1}").strip() - - image2 = "localhost:5000/test-image:2" - - # build update and push it - buildImage(False, 50, image2) - self.execute(False, f"podman push {image2}; podman rmi {image2}") - - # build image which we try to pull - buildImage(False, 10, image2) - image2_sha = self.execute(False, f"podman inspect --format '{{{{.Id}}}}' {image2}").strip() - - self.execute(False, """ - podman rmi localhost/test-busybox:latest - podman rmi localhost/test-alpine:latest - - # Remove registry container - podman rmi localhost/test-registry:latest - """) - - # Ubuntu has an old podman which ships with a k8s image - if m.image == "ubuntu-2204": - self.execute(False, "podman rmi k8s.gcr.io/pause:3.5") - - b.wait_in_text("#containers-images", "2 images") - b.click("#image-actions-dropdown") - b.click("button:contains(Pull all images)") - b.wait_in_text("div.download-in-progress", "Pulling") - b.wait_not_present('h4.pf-v5-c-alert__title:contains("Failed to download image")') - b.wait_not_present("div.download-in-progress") - b.wait_not_present(f"#containers-images tbody tr[data-row-id=\"{image1_sha}false\"]".lower()) - b.wait_not_present(f"#containers-images tbody tr[data-row-id=\"{image2_sha}false\"]".lower()) - - # assert the image size changed - b.wait(lambda: getImageSize(image1) > 20) - b.wait(lambda: getImageSize(image2) > 40) - - def testLifecycleOperationsUser(self): - self._testLifecycleOperations(False) - - def testLifecycleOperationsSystem(self): - self._testLifecycleOperations(True) - - def _testLifecycleOperations(self, auth): - b = self.browser - - if not auth: - self.allow_browser_errors("Failed to start system podman.socket.*") - - self.login() - self.filter_containers('all') - - # run a container - self.execute(auth, f""" - podman run -d --name swamped-crate --stop-timeout 0 {IMG_BUSYBOX} sh -c 'echo 123; sleep infinity'; - podman stop swamped-crate""") - b.wait(lambda: self.execute(auth, "podman ps --all | grep -e swamped-crate -e Exited")) - - b.wait_visible("#containers-containers") - container_sha = self.execute(auth, "podman inspect --format '{{.Id}}' swamped-crate").strip() - self.waitContainer(container_sha, auth, name='swamped-crate', image=IMG_BUSYBOX, - state='Exited', owner="system" if auth else "admin") - b.click("#containers-containers tbody tr:contains('swamped-crate') .pf-v5-c-menu-toggle") - - if not auth: - # Checkpoint/restore is not supported on user containers yet - the related buttons should not be shown - # Check that the restore option is not present - b.wait_not_present(self.getContainerAction('swamped-crate', 'Restore')) - - # Health check is not set up - b.wait_not_present(self.getContainerAction('swamped-crate', 'Run health check')) - - b.click("#containers-containers tbody tr:contains('swamped-crate') .pf-v5-c-menu-toggle") - - # Start the container - self.performContainerAction(IMG_BUSYBOX, "Start") - - self.waitContainer(container_sha, auth, name='swamped-crate', image=IMG_BUSYBOX, - state='Running', owner="system" if auth else "admin") - - # run another container with explicit memory 400 MiB limit, with a process that uses 300 MB memory - self.execute( - auth, - f"podman run -d --name picket-fence --memory 400m --stop-timeout 0 {IMG_ALPINE} " - """awk 'BEGIN { y = sprintf("%300000000s",""); system("sleep infinity") }'""") - picket_fence_sha = self.execute(auth, "podman inspect --format '{{.Id}}' picket-fence").strip() - self.waitContainer(picket_fence_sha, auth, name="picket-fence", state="Running") - - def get_cpu_usage(sel): - cpu = self.getContainerAttr(sel, "CPU") - self.assertIn('%', cpu) - # If it not a number it will raise ValueError which is what we want to know - return float(cpu[:-1]) - - # Check we show usage - b.wait(lambda: self.getContainerAttr(IMG_BUSYBOX, "CPU") != "") - b.wait(lambda: self.getContainerAttr(IMG_BUSYBOX, "Memory") != "") - memory = self.getContainerAttr(IMG_BUSYBOX, "Memory") - if auth or self.has_cgroupsV2: - # Wait for CPU usage to settle before saving CPU usage for comparison later - b.wait(lambda: get_cpu_usage(IMG_BUSYBOX) < 25) - cpu = get_cpu_usage(IMG_BUSYBOX) - - # swamped-crate has no explicit --memory limit - m = re.match(r'([0-9]+)%([0-9.]+) kB$', memory) - self.assertTrue(m, memory) - mem_pct = int(m.group(1)) - mem_abs = float(m.group(2)) - self.assertGreaterEqual(mem_pct, 0) - self.assertGreaterEqual(mem_abs, 1.0) - # this container doesn't do anything, should have small memory usage - self.assertLessEqual(mem_pct, 5) - self.assertLessEqual(mem_abs, 300.0) - - # Test that the CPU value is updated dynamically - self.execute(auth, "podman exec -i swamped-crate sh -c 'dd bs=1024 < /dev/urandom > /dev/null &'") - b.wait(lambda: get_cpu_usage(IMG_BUSYBOX) > cpu) - self.execute(auth, "podman exec swamped-crate sh -c 'pkill dd'") - - # picket-fence has a memory limit - - # Wait for CPU usage to settle before checking the memory usage, on - # a busy host the awk script takes longer to run. - b.wait(lambda: get_cpu_usage(IMG_ALPINE) < 10) - memory = self.getContainerAttr(IMG_ALPINE, "Memory") - m = re.match(r'([0-9]+)%([0-9.]+) MB([0-9]+)% of ([0-9.]+) MB limit$', memory) - self.assertTrue(m, memory) - # wide range here, to work on custom testbeds - host_mem_pct = int(m.group(1)) - self.assertGreater(host_mem_pct, 2) - self.assertLess(host_mem_pct, 90) - # this is fairly well predictable - mem_abs = float(m.group(2)) - self.assertGreaterEqual(mem_abs, 300.0) - self.assertLess(mem_abs, 305.0) - limit_mem_pct = int(m.group(3)) - self.assertGreaterEqual(limit_mem_pct, 70) - self.assertLessEqual(limit_mem_pct, 75) - # 400 MiB limit - self.assertEqual(m.group(4), "419") - else: - # No support for CGroupsV2 - self.assertEqual(self.getContainerAttr(IMG_BUSYBOX, "CPU"), "n/a") - self.assertEqual(memory, "n/a") - - # Restart the container; there is no steady state change in the visible UI, so look for - # a changed data-started-at attribute - old_start = self.getStartTime("swamped-crate", auth=auth) - b.wait_in_text(f'#containers-containers tr[data-started-at="{old_start}"]', "swamped-crate") - self.performContainerAction(IMG_BUSYBOX, "Force restart") - new_start = self.waitRestart("swamped-crate", old_start, auth=auth) - b.wait_in_text(f'#containers-containers tr[data-started-at="{new_start}"]', "swamped-crate") - self.waitContainer(container_sha, auth, name='swamped-crate', image=IMG_BUSYBOX, state='Running') - - self.waitContainerRow(IMG_BUSYBOX) - if not auth: - # Check that the checkpoint option is not present for rootless - b.click(f"#containers-containers tbody tr:contains('{IMG_BUSYBOX}') .pf-v5-c-menu-toggle") - b.wait_visible(self.getContainerAction(IMG_BUSYBOX, 'Force stop')) - b.wait_not_present(self.getContainerAction(IMG_BUSYBOX, 'Checkpoint')) - b.click(f"#containers-containers tbody tr:contains('{IMG_BUSYBOX}') .pf-v5-c-menu-toggle") - # Stop the container - self.performContainerAction(IMG_BUSYBOX, "Force stop") - - self.waitContainer(container_sha, auth, name='swamped-crate', image=IMG_BUSYBOX) - b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in NOT_RUNNING) - b.wait(lambda: self.getContainerAttr("swamped-crate", "CPU") == "") - b.wait(lambda: self.getContainerAttr("swamped-crate", "Memory") == "") - - # Check that container details are not lost when the container is stopped - self.toggleExpandedContainer("swamped-crate") - b.click(".pf-m-expanded button:contains('Integration')") - b.wait_visible(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Environment variables")') - - # Check that console reconnects when container starts - b.click(".pf-m-expanded button:contains('Console')") - b.wait_text(".pf-m-expanded .pf-v5-c-empty-state", "Container is not running") - self.performContainerAction("swamped-crate", "Start") - b.wait_in_text(".pf-m-expanded .xterm-accessibility-tree", "/ # ") - b.focus(".pf-m-expanded .xterm-helper-textarea") - b.input_text("clear") - b.key("Enter") - b.wait_not_in_text(".pf-m-expanded .xterm-accessibility-tree", "clear") - b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(1)", "/ # ") - b.input_text('echo hello') - b.key("Enter") - b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(2)", "hello") - b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(3)", "/ # ") - self.performContainerAction("swamped-crate", "Stop") - b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(3)", "/ # disconnected ") - sha = self.execute(auth, "podman inspect --format '{{.Id}}' swamped-crate").strip() - self.waitContainer(sha, auth, name='swamped-crate', image=IMG_BUSYBOX, state=NOT_RUNNING) - self.performContainerAction("swamped-crate", "Start") - self.waitContainer(sha, auth, state='Running') - b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(1)", "/ # ") - b.wait_not_in_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(2)", "hello") - - # Check that logs reconnect when container starts - b.click(".pf-m-expanded button:contains('Logs')") - self.performContainerAction("swamped-crate", "Stop") - self.waitContainer(sha, auth, state=NOT_RUNNING) - b.wait_in_text(".pf-m-expanded .container-logs .xterm-accessibility-tree", "Streaming disconnected") - self.performContainerAction("swamped-crate", "Start") - b.wait_in_text(".pf-m-expanded .container-logs .xterm-accessibility-tree", "Streaming disconnected123") - - def testCheckpointRestore(self): + def testManifest(self): m = self.machine - b = self.browser - - self.login() - self.filter_containers('all') - - if not self.has_criu: - # On cgroupsv1 systems just check that we get expected error messages - - # Run a container - self.execute(True, f"podman run -dit --name swamped-crate --stop-timeout 0 {IMG_BUSYBOX} sh") - b.wait(lambda: self.execute(True, "podman ps --all | grep -e swamped-crate")) - - # Checkpoint the container - self.performContainerAction(IMG_BUSYBOX, "Checkpoint") - b.set_checked('.pf-v5-c-modal-box input#checkpoint-dialog-keep', True) - b.set_checked('.pf-v5-c-modal-box input#checkpoint-dialog-tcpEstablished', True) - b.click('.pf-v5-c-modal-box button:contains(Checkpoint)') - b.wait_not_present('.modal_dialog') - - def criu_alert(): - text = b.text(".pf-v5-c-alert.pf-m-danger > .pf-v5-c-alert__description").lower() - return "checkpoint/restore requires at least criu" in text or "failed to check for criu" in text - b.wait(criu_alert) - return - - # Run a container - mac_address = '92:d0:c6:0a:29:38' - self.execute(True, f""" - podman run -dit --mac-address {mac_address} --name swamped-crate --stop-timeout 0 {IMG_BUSYBOX} sh; - podman stop swamped-crate + m.execute(""" + podman run -d --tty --name testing localhost/test-busybox sh + podman --log-level=debug container checkpoint --keep --tcp-established testing || true; + cat /var/lib/containers/storage/overlay-containers/*/userdata/dump.log || true; + exit 1 """) - b.wait(lambda: self.execute(True, "podman ps --all | grep -e swamped-crate -e Exited")) - - # Check that the restore option is not present (i.e. start is a regular button) - b.click(f"#containers-containers tbody tr:contains('{IMG_BUSYBOX}') .pf-v5-c-menu-toggle") - b.wait_not_present(self.getContainerAction(IMG_BUSYBOX, 'Restore')) - b.click(f"#containers-containers tbody tr:contains('{IMG_BUSYBOX}') .pf-v5-c-menu-toggle") - - # Start the container - self.performContainerAction("swamped-crate", "Start") - b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in 'Running') - - self.toggleExpandedContainer("swamped-crate") - b.wait_visible(".pf-m-expanded button:contains('Details')") - b.wait_not_present(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Latest checkpoint")') - - # Checkpoint the container - self.performContainerAction("swamped-crate", "Checkpoint") - b.set_checked('.pf-v5-c-modal-box input#checkpoint-dialog-keep', True) - b.set_checked('.pf-v5-c-modal-box input#checkpoint-dialog-tcpEstablished', True) - b.click('.pf-v5-c-modal-box button:contains(Checkpoint)') - - with b.wait_timeout(300): - b.wait_not_present(".pf-v5-c-modal-box") - - if self.has_criu: - b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in NOT_RUNNING) - b.wait_text( - f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Latest checkpoint") + dd', - 'less than a minute ago' - ) - else: - # expect proper error message - b.wait_in_text(".pf-v5-c-alert.pf-m-danger", "Failed to checkpoint container swamped-crate") - b.wait(lambda: "checkpoint/restore requires at least criu" in - b.text(".pf-v5-c-alert.pf-m-danger > .pf-v5-c-alert__description").lower()) - return - - # Restore the container - self.waitContainerRow("swamped-crate") - self.performContainerAction("swamped-crate", "Restore") - b.set_checked('.pf-v5-c-modal-box input#restore-dialog-keep', True) - b.set_checked('.pf-v5-c-modal-box input#restore-dialog-tcpEstablished', True) - b.set_checked('.pf-v5-c-modal-box input#restore-dialog-ignoreStaticIP', True) - b.set_checked('.pf-v5-c-modal-box input#restore-dialog-ignoreStaticMAC', True) - b.click('.pf-v5-c-modal-box button:contains(Restore)') - b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in 'Running') - - # A new MAC address should have been generated - # Fixed in podman 4.4.0 https://github.com/containers/podman/issues/16666 - cmd = "podman inspect --format '{{.NetworkSettings.MacAddress}}' swamped-crate" - new_mac_address = self.execute(True, cmd).strip() - if podman_version(self) >= (4, 4, 0): - self.assertNotEqual(new_mac_address, mac_address) - else: - self.assertEqual(new_mac_address, mac_address) - - # Checkpoint the container without stopping - self.waitContainerRow("swamped-crate") - self.performContainerAction("swamped-crate", "Checkpoint") - b.set_checked('.pf-v5-c-modal-box input#checkpoint-dialog-leaveRunning', True) - b.click('.pf-v5-c-modal-box button:contains(Checkpoint)') - b.wait_not_present('.modal_dialog') - - # Stop the container - m.execute("podman stop swamped-crate") - b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in NOT_RUNNING) - - # Restore the container - self.performContainerAction("swamped-crate", "Restore") - b.click('.pf-v5-c-modal-box button:contains(Restore)') - b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in 'Running') - - def testNotRunning(self): - b = self.browser - - def disable_system(): - self.execute(True, "systemctl disable --now podman.socket; systemctl stop podman.service") - - def enable_system(): - self.execute(True, "systemctl enable --now podman.socket") - - def enable_user(): - self.execute(False, "systemctl --user enable --now podman.socket") - - def disable_user(): - self.execute(False, "systemctl --user disable --now podman.socket") - - def is_active_system(string): - b.wait(lambda: self.execute(True, "systemctl is-active podman.socket || true").strip() == string) - - def is_enabled_system(string): - b.wait(lambda: self.execute(True, "systemctl is-enabled podman.socket || true").strip() == string) - - def is_active_user(string): - b.wait(lambda: self.execute(False, "systemctl --user is-active podman.socket || true").strip() == string) - - def is_enabled_user(string): - b.wait(lambda: self.execute(False, "systemctl --user is-enabled podman.socket || true").strip() == string) - - disable_system() - disable_user() - self.login_and_go("/podman") - - # Troubleshoot action - b.click("#app .pf-v5-c-empty-state button.pf-m-link") - b.enter_page("/system/services") - # services page is too slow - with b.wait_timeout(60): - b.wait_in_text("#service-details", "podman.socket") - - # Start action, with enabling (by default) - b.go("/podman") - b.enter_page("/podman") - b.click("#app .pf-v5-c-empty-state button.pf-m-primary") - - b.wait_visible("#containers-containers") - b.wait_not_present("#overview div.pf-v5-c-alert.pf-m-info") - - is_active_system("active") - is_active_user("active") - is_enabled_system("enabled") - is_enabled_user("enabled") - - # Start action, without enabling - disable_system() - disable_user() - b.click("#app .pf-v5-c-empty-state input[type=checkbox]") - b.assert_pixels("#app .pf-v5-c-empty-state", "podman-service-disabled", skip_layouts=["medium", "mobile"]) - b.click("#app .pf-v5-c-empty-state button.pf-m-primary") - - b.wait_visible("#containers-containers") - is_enabled_system("disabled") - is_enabled_user("disabled") - is_active_system("active") - is_active_user("active") - - b.logout() - disable_system() - # HACK: Due to https://github.com/containers/podman/issues/7180, avoid - # user podman.service to time out; make sure to start it afresh - disable_user() - enable_user() - self.login_and_go("/podman") - b.wait_in_text("#overview div.pf-v5-c-alert .pf-v5-c-alert__title", "System Podman service is also available") - b.click("#overview div.pf-v5-c-alert .pf-v5-c-alert__action > button:contains(Start)") - b.wait_not_present("#overview div.pf-v5-c-alert") - is_active_system("active") - is_active_user("active") - is_enabled_user("enabled") - is_enabled_system("enabled") - - b.logout() - disable_user() - enable_system() - self.login_and_go("/podman") - b.wait_in_text("#overview div.pf-v5-c-alert .pf-v5-c-alert__title", "User Podman service is also available") - b.click("#overview div.pf-v5-c-alert .pf-v5-c-alert__action > button:contains(Start)") - b.wait_not_present("#overview div.pf-v5-c-alert") - is_active_system("active") - is_active_user("active") - is_enabled_user("enabled") - is_enabled_system("enabled") - - b.logout() - disable_user() - disable_system() - self.login_and_go("/podman", superuser=False) - b.click("#app .pf-v5-c-empty-state button.pf-m-primary") - b.wait_visible("#containers-containers") - b.wait_not_present("#overview div.pf-v5-c-alert") - - is_active_system("inactive") - is_active_user("active") - is_enabled_user("enabled") - is_enabled_system("disabled") - b.logout() - - # no Troubleshoot action without cockpit-system package - disable_system() - disable_user() - self.restore_dir("/usr/share/cockpit/systemd") - self.machine.execute("rm /usr/share/cockpit/systemd/manifest.json") - self.login_and_go("/podman") - b.wait_visible("#app .pf-v5-c-empty-state button.pf-m-primary") - self.assertFalse(b.is_present("#app .pf-v5-c-empty-state button.pf-m-link")) - # starting still works - b.click("#app .pf-v5-c-empty-state button.pf-m-primary") - b.wait_visible("#containers-containers") - - self.allow_restart_journal_messages() - self.allow_journal_messages(".*podman/podman.sock/.*: couldn't connect:.*") - self.allow_journal_messages(".*podman/podman.sock: .*Connection.*Error.*") - self.allow_journal_messages(".*podman/podman.sock/.*/events.*: received truncated HTTP response.*") - - def testCreateContainerSystem(self): - self._testCreateContainer(True) - - def testCreateContainerUser(self): - self._testCreateContainer(False) - - def _testCreateContainer(self, auth): - new_container = 'new-container' - self.execute(True, f"podman run -d --name {new_container} --stop-timeout 0 {IMG_BUSYBOX} touch /latest") - self.execute(True, f"podman commit {new_container} newimage") - new_image_sha = self.execute(True, "podman inspect --format '{{.Id}}' newimage").strip() - - self.setupRegistry() - - # Push busybox image to the local registries - self.execute(True, - f"podman tag {IMG_BUSYBOX} localhost:5000/my-busybox; podman push localhost:5000/my-busybox") - self.execute(True, - f"podman tag {IMG_BUSYBOX} localhost:6000/my-busybox; podman push localhost:6000/my-busybox") - # Untag busybox image which duplicates the image we are about to download - self.execute(True, f"podman rmi -f {IMG_BUSYBOX} localhost:5000/my-busybox localhost:6000/my-busybox") - - self.login(auth) - - b = self.browser - container_name = "busybox-downloaded" - - b.click("#containers-containers button.pf-v5-c-button.pf-m-primary") - b.set_input_text("#run-image-dialog-name", container_name) - - # Test invalid input - b.set_input_text("#create-image-image-select-typeahead", "|alpi*ne?\\") - b.wait_text("button.pf-v5-c-select__menu-item:not(.pf-m-disabled)", "localhost/test-alpine:latest") - - # No local results found - b.set_input_text("#create-image-image-select-typeahead", "notfound") - - b.click('button.pf-v5-c-toggle-group__button:contains("Local")') - b.wait_text("button.pf-v5-c-select__menu-item.pf-m-disabled", "No images found") - - # Local results found - b.set_input_text("#create-image-image-select-typeahead", "registry") - b.wait_text("button.pf-v5-c-select__menu-item", IMG_REGISTRY_LATEST) - if auth: - b.assert_pixels(".pf-v5-c-modal-box", "image-select", skip_layouts=["rtl"]) - - # Local registry - b.click('button.pf-v5-c-toggle-group__button:contains("localhost:5000")') - b.set_input_text("#create-image-image-select-typeahead", "my-busybox") - b.wait_text("button.pf-v5-c-select__menu-item:not(.pf-m-disabled)", "localhost:5000/my-busybox") - - # Select image - b.click('button.pf-v5-c-select__menu-item:contains("localhost:5000/my-busybox")') - - # Remote image, no pull latest image option - b.wait_not_present("#run-image-dialog-pull-latest-image") - - # Create Container, image is pulled and should end up being "running" - b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn') - sel = " span:not(.downloading)" - b.wait(lambda: self.getContainerAttr(container_name, "State", sel) in 'Running') - self.execute(auth, f"podman exec {container_name} test ! -e /latest") - - # Now that we have downloaded an image, verify that selecting download latest image - # downloads the latest image we now push to the registry. Note this image has a /latest file - # to differentiate it from the other local image. - self.execute(True, f"podman push {new_image_sha} localhost:5000/my-busybox") - self.execute(True, f"podman push {new_image_sha} localhost:6000/my-busybox") - self.execute(True, f"podman rmi {new_image_sha}") - - container_name = "busybox-latest" - - b.click("#containers-containers button.pf-v5-c-button.pf-m-primary") - b.set_input_text("#run-image-dialog-name", container_name) - - # Local registry - b.set_input_text("#create-image-image-select-typeahead", "no-container") - b.wait_text("button.pf-v5-c-select__menu-item.pf-m-disabled", "No images found") - # Search with full url - b.set_input_text("#create-image-image-select-typeahead", "localhost:5000/my-busybox") - b.click('button.pf-v5-c-toggle-group__button:contains("Local")') - - # Select image - b.click('button.pf-v5-c-select__menu-item:contains("localhost:5000/my-busybox")') - - # Pull the latest image - b.set_checked("#run-image-dialog-pull-latest-image", True) - - # Create Container, image is pulled and should end up being "running" - b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn') - sel = " span:not(.downloading)" - b.wait(lambda: self.getContainerAttr(container_name, "State", sel) in 'Running') - # Verify that the latest file exists - output = self.execute(auth, f"podman exec {container_name} ls -lh /latest").strip() - self.assertNotIn("No such file or directory", output) - - # Test creating a container with - if auth: - container_name = "busybox-download-admin" - b.click("#containers-containers button.pf-v5-c-button.pf-m-primary") - - # Start container as admin - b.click('#run-image-dialog-owner-user') - - # Create Container, image is pulled and should end up being "Running" - b.set_input_text("#run-image-dialog-name", container_name) - - b.set_input_text("#create-image-image-select-typeahead", IMG_BUSYBOX) - # we want to switch to "Local", but to make waiting for IMG_BUSYBOX race-free, we first need to - # switch to another view; different images have different registries, so don't assume their name, just - # that at least one more exists - b.click('.image-search-footer .pf-v5-c-toggle-group__item:nth-of-type(3) button') - b.wait_in_text(".pf-v5-c-select__menu-list", "No images found") - b.click('button.pf-v5-c-toggle-group__button:contains("Local")') - b.click(f'button.pf-v5-c-select__menu-item:contains("{IMG_BUSYBOX}")') - - b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn') - b.wait(lambda: self.getContainerAttr(container_name, "State", sel) in 'Running') - - def testRunImageSystem(self): - self._testRunImage(True) - - def testRunImageUser(self): - self._testRunImage(False) - - def _testRunImage(self, auth): - b = self.browser - m = self.machine - - # Just drop user images so we can use simpler selectors - if auth: - self.execute(False, "podman rmi --all") - - self.login(auth) - - b.click("#containers-images button.pf-v5-c-expandable-section__toggle") - - b.wait_in_text("#containers-images", IMG_BUSYBOX) - b.wait_in_text("#containers-images", IMG_ALPINE) - if auth: - b.wait_not_in_text("#containers-images", "admin") - - # Check command in alpine - b.wait_visible(f'#containers-images td[data-label="Image"]:contains("{IMG_ALPINE}")') - b.click(f'#containers-images tbody tr:contains("{IMG_ALPINE}") .ct-container-create') - b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")') - # depending on the precise container, this can be /bin/sh or /bin/ash - cmd = self.execute(auth, 'podman image inspect --format "{{.Config.Cmd}}" ' + IMG_ALPINE) - cmd = cmd.strip().replace('[', '').replace(']', '') - b.wait_attr("#run-image-dialog-command", "value", cmd) - b.click(".btn-cancel") - - # Open run image dialog - b.wait_visible(f'#containers-images td[data-label="Image"]:contains("{IMG_BUSYBOX}")') - b.click(f'#containers-images tbody tr:contains("{IMG_BUSYBOX}") .ct-container-create') - b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")') - - # Inspect and fill modal dialog - b.wait_val("#create-image-image-select-typeahead", IMG_BUSYBOX_LATEST) - - # Check that there is autogenerated name and then overwrite it - b.wait_not_val("#run-image-dialog-name", "") - b.set_input_text("#run-image-dialog-name", "busybox-with-tty") - - b.wait_visible("#run-image-dialog-command[value='sh']") - - # Check memory configuration - # Only works with CGroupsV2 - if auth or self.has_cgroupsV2: - b.set_checked("#run-image-dialog-memory-limit-checkbox", True) - b.wait_visible("#run-image-dialog-memory-limit-checkbox:checked") - b.wait_visible('#run-image-dialog-memory-limit input[value="512"]') - b.set_input_text("#run-image-dialog-memory-limit input[type=number]", "0.5") - b.set_val('#memory-unit-select', "GB") - - # CPU shares work only with system containers - if auth: - # Check that the checkbox is enabled when clicked on the field - b.wait_visible("#run-image-dialog-cpu-priority-checkbox:not(:checked)") - b.mouse('#run-image-cpu-priority button[aria-label="Decrease CPU shares"]', "click") - b.wait_visible("#run-image-dialog-cpu-priority-checkbox:checked") - b.set_checked("#run-image-dialog-cpu-priority-checkbox", False) - - b.set_checked("#run-image-dialog-cpu-priority-checkbox", True) - b.wait_visible("#run-image-dialog-cpu-priority-checkbox:checked") - b.wait_visible('#run-image-dialog-cpu-priority input[value="1024"]') - b.set_input_text("#run-image-dialog-cpu-priority input[type=number]", "512") - else: - b.wait_not_present("#run-image-dialog-cpu-priority-checkbox") - - # Enable tty - b.set_checked("#run-image-dialog-tty", True) - - # Set up command line - b.set_input_text('#run-image-dialog-command', - "sh -c 'for i in $(seq 20); do sleep 1; echo $i; done; sleep infinity'") - - if auth: - # Set restart policy to 3 retries - b.set_val("#run-image-dialog-restart-policy", "on-failure") - b.set_input_text('#run-image-dialog-restart-retries input', '3') - else: # no lingering enabled so it's disabled - b.wait_not_present("#run-image-dialog-restart-policy") - - # Switch to Integration tab - b.click("#pf-tab-1-create-image-dialog-tab-integration") - - # Configure published ports - b.click('.publish-port-form .btn-add') - b.set_input_text('#run-image-dialog-publish-0-host-port', '6000') - b.set_input_text('#run-image-dialog-publish-0-container-port', '5000') - b.click('.publish-port-form .btn-add') - b.set_input_text('#run-image-dialog-publish-1-ip-address', '127.0.0.1') - b.set_input_text('#run-image-dialog-publish-1-host-port', '6001') - b.set_input_text('#run-image-dialog-publish-1-container-port', '5001') - b.set_val('#run-image-dialog-publish-1-protocol', "udp") - b.click('.publish-port-form .btn-add') - b.set_input_text('#run-image-dialog-publish-2-ip-address', '7001') - b.set_input_text('#run-image-dialog-publish-2-host-port', '7001') - b.click('#run-image-dialog-publish-2-btn-close') - b.click('.publish-port-form .btn-add') - b.set_input_text('#run-image-dialog-publish-3-container-port', '8001') - b.click('.publish-port-form .btn-add') - b.set_input_text('#run-image-dialog-publish-4-ip-address', '127.0.0.2') - b.set_input_text('#run-image-dialog-publish-4-container-port', '9001') - # publish range of ports - b.click('.publish-port-form .btn-add') - b.set_input_text('#run-image-dialog-publish-5-container-port', '4000') - b.set_input_text('#run-image-dialog-publish-5-host-port', '4000') - b.click('.publish-port-form .btn-add') - b.set_input_text('#run-image-dialog-publish-6-container-port', '4001') - b.set_input_text('#run-image-dialog-publish-6-host-port', '4001') - b.click('.publish-port-form .btn-add') - b.set_input_text('#run-image-dialog-publish-7-container-port', '4002') - b.set_input_text('#run-image-dialog-publish-7-host-port', '4002') - b.click('.publish-port-form .btn-add') - b.set_input_text('#run-image-dialog-publish-8-container-port', '4003') - b.set_input_text('#run-image-dialog-publish-8-host-port', '4003') - - # Configure env - b.click('.env-form .btn-add') - b.set_input_text('#run-image-dialog-env-0-key', 'APPLE') - b.set_input_text('#run-image-dialog-env-0-value', 'ORANGE') - b.click('.env-form .btn-add') - b.set_input_text('#run-image-dialog-env-1-key', 'PEAR') - b.set_input_text('#run-image-dialog-env-1-value', 'BANANA') - b.click('.env-form .btn-add') - b.set_input_text('#run-image-dialog-env-2-key', 'MELON') - b.set_input_text('#run-image-dialog-env-2-value', 'GRAPE') - b.click('#run-image-dialog-env-2-btn-close') - b.click('.env-form .btn-add') - # Test inputting an key=var entry; that should auto-split on '=' - b.set_input_text('#run-image-dialog-env-3-value', "RHUBARB=STRAWBERRY", value_check=False) - b.wait_val("#run-image-dialog-env-3-key", "RHUBARB") - b.wait_val("#run-image-dialog-env-3-value", "STRAWBERRY") - - # with copy&paste you can also set multiple variables - b.click('.env-form .btn-add') - b.set_val('#run-image-dialog-env-4-value', - "DURIAN=LEMON TEST_URL=wss://cockpit/?start=1&stop=0") - # set_val does not trigger onChange so append a space. - b.set_input_text('#run-image-dialog-env-4-value', ' ', append=True, value_check=False) - - b.wait_val("#run-image-dialog-env-4-key", "DURIAN") - b.wait_val("#run-image-dialog-env-4-value", "LEMON") - b.wait_val("#run-image-dialog-env-5-key", "TEST_URL") - b.wait_val("#run-image-dialog-env-5-value", "wss://cockpit/?start=1&stop=0") - - b.click('.env-form .btn-add') - b.set_input_text('#run-image-dialog-env-6-key', 'HOSTNAME') - b.set_input_text('#run-image-dialog-env-6-value', 'busybox') - - # Test inputting a var with = in it doesn't reset key - b.click('.env-form .btn-add') - b.set_input_text('#run-image-dialog-env-7-key', 'TEST') - b.set_input_text('#run-image-dialog-env-7-value', 'REBASE=1') - - # Configure volumes - b.click('.volume-form .btn-add') - rodir, rwdir = m.execute("mktemp; mktemp").split('\n')[:2] - m.execute(f"chown admin:admin {rodir}") - m.execute(f"chown admin:admin {rwdir}") - b.set_checked("#run-image-dialog-volume-0-mode", False) - - if self.has_selinux: - b.set_val('#run-image-dialog-volume-0-selinux', "z") - else: - b.wait_not_present('#run-image-dialog-volume-0-selinux') - - b.set_file_autocomplete_val("#run-image-dialog-volume-0 .pf-v5-c-select", rodir) - b.set_input_text('#run-image-dialog-volume-0-container-path', '/tmp/ro') - ro_label = m.execute(f"ls -dZ {rodir}").split(" ")[0] - b.click('.volume-form .btn-add') - b.wait_visible('#run-image-dialog-volume-1') - b.click('#run-image-dialog-volume-1-btn-close') - b.wait_not_present('#run-image-dialog-volume-1') - b.click('.volume-form .btn-add') - - if auth: - b.assert_pixels(".pf-v5-c-modal-box", "integration", - ignore=["#run-image-dialog-volume-0 .pf-v5-c-select__toggle-typeahead"], - skip_layouts=["rtl"]) - - if self.has_selinux: - b.set_val('#run-image-dialog-volume-2-selinux', "Z") - else: - b.wait_not_present('#run-image-dialog-volume-2-selinux') - - b.set_file_autocomplete_val("#run-image-dialog-volume-2 .pf-v5-c-select", rwdir) - b.set_input_text('#run-image-dialog-volume-2-container-path', '/tmp/rw') - rw_label = m.execute(f"ls -dZ {rwdir}").split(" ")[0] - - b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn') - b.wait_not_present("div.pf-v5-c-modal-box") - self.waitContainerRow(IMG_BUSYBOX) - sha = self.execute(auth, "podman inspect --format '{{.Id}}' busybox-with-tty").strip() - self.waitContainer(sha, auth, name='busybox-with-tty', image=IMG_BUSYBOX, - cmd='sh -c "for i in $(seq 20); do sleep 1; echo $i; done; sleep infinity"', - state='Running', owner="system" if auth else "admin") - hasTTY = self.execute(auth, "podman inspect --format '{{.Config.Tty}}' busybox-with-tty").strip() - self.assertEqual(hasTTY, 'true') - # Only works with CGroupsV2 - if auth or self.has_cgroupsV2: - memory = self.execute(auth, "podman inspect --format '{{.HostConfig.Memory}}' busybox-with-tty").strip() - self.assertEqual(memory, '500000000') - - if auth: - cpuShares = self.execute(auth, - "podman inspect --format '{{.HostConfig.CpuShares}}' busybox-with-tty").strip() - self.assertEqual(cpuShares, '512') - - restartPolicy = self.getRestartPolicy(auth, "busybox-with-tty") - if auth: - self.assertEqual(restartPolicy, '{on-failure 3}') - else: - # No restart policy - # format changed in podman 5.1 (https://github.com/containers/podman/pull/22322) - self.assertIn(restartPolicy, ['{ 0}', '{no 0}']) - - b.wait(lambda: "3" in self.execute(auth, "podman logs busybox-with-tty")) - - self.toggleExpandedContainer(IMG_BUSYBOX) - - b.wait_text(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Created") + dd', - 'less than a minute ago') - - b.click(".pf-m-expanded button:contains('Integration')") - - b.wait_in_text(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Ports") + dd', - '0.0.0.0:6000 \u2192 5000/tcp') - b.wait_in_text(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Ports") + dd', - '127.0.0.1:6001 \u2192 5001/udp') - b.wait_in_text(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Ports") + dd', - '127.0.0.2:') - b.wait_in_text(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Ports") + dd', - ' \u2192 8001/tcp') - b.wait_in_text(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Ports") + dd', - '0.0.0.0:4000-4003 \u2192 4000-4003/tcp') - b.wait_not_in_text(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Ports") + dd', - '7001/tcp') - - ports = self.execute(auth, "podman inspect --format '{{.NetworkSettings.Ports}}' busybox-with-tty") - self.assertRegex(ports, r'5000/tcp:\[{(0.0.0.0)? 6000}\]') - self.assertRegex(ports, r'400[0-3]?/tcp:\[{(0.0.0.0)? 400[0-3]?}\]') - self.assertIn('5001/udp:[{127.0.0.1 6001}]', ports) - self.assertIn('8001/tcp:[{', ports) - self.assertIn('9001/tcp:[{127.0.0.2 ', ports) - self.assertNotIn('7001/tcp', ports) - - env_select = f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Environment variables") + dd' - b.wait_in_text(env_select, 'APPLE=ORANGE') - b.wait_in_text(env_select, 'PEAR=BANANA') - b.wait_in_text(env_select, 'RHUBARB=STRAWBERRY') - b.wait_in_text(env_select, 'DURIAN=LEMON') - b.wait_in_text(env_select, 'TEST_URL=wss://cockpit/?start=1&stop=0') - b.wait_in_text(env_select, 'HOSTNAME=busybox') - b.wait_in_text(env_select, 'TEST=REBASE=1') - # variables are present in env but are not displayed in the UI - b.wait_not_in_text(env_select, 'container=podman') - b.wait_not_in_text(env_select, 'TERM=xterm') - b.wait_not_in_text(env_select, 'HOME=/root') - b.wait_not_in_text(env_select, 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin') - - b.click(".container-integration button:contains('Show more')") - # previously hidden variables are now visible - b.wait_in_text(env_select, 'container=podman') - b.wait_in_text(env_select, 'TERM=xterm') - b.wait_in_text(env_select, 'HOME=/root') - b.wait_in_text(env_select, 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin') - - env = self.execute(auth, "podman exec busybox-with-tty env") - self.assertIn('APPLE=ORANGE', env) - self.assertIn('PEAR=BANANA', env) - self.assertIn('RHUBARB=STRAWBERRY', env) - self.assertIn('DURIAN=LEMON', env) - self.assertIn('HOSTNAME=busybox', env) - self.assertIn('TEST=REBASE=1', env) - self.assertIn('container=podman', env) - self.assertIn('TERM=xterm', env) - self.assertIn('HOME=/root', env) - self.assertIn('PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', env) - self.assertNotIn('MELON=GRAPE', env) - - vol_select = f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Volumes") + dd' - b.wait_in_text(vol_select, f"{rodir} \u2192 /tmp/ro") - b.wait_in_text(vol_select, f"{rwdir} \u2194 /tmp/rw") - - romnt = self.execute(auth, "podman exec busybox-with-tty cat /proc/self/mountinfo | grep /tmp/ro") - self.assertIn('ro', romnt) - self.assertIn(rodir[4:], romnt) - rwmnt = self.execute(auth, "podman exec busybox-with-tty cat /proc/self/mountinfo | grep /tmp/rw") - self.assertIn('rw', rwmnt) - self.assertIn(rwdir[4:], rwmnt) - - if self.has_selinux: - # rw was set to :Z so it should change, but not be shared - rw_label_new = m.execute(f"ls -dZ {rwdir}").split(" ")[0] - self.assertNotEqual(rw_label, rw_label_new) - self.assertRegex(rw_label_new, r"container_file_t:s0:c\d*,c\d*$") - - # ro was set to :z to it should change and be shared - ro_label_new = m.execute(f"ls -dZ {rodir}").split(" ")[0] - self.assertNotEqual(ro_label, ro_label_new) - self.assertRegex(ro_label_new, "container_file_t:s0$") - - def get_int(n): - try: - return int(n) - except ValueError: - return 0 - - b.wait_not_present("button:contains('Health check logs')") - b.click(".pf-m-expanded button:contains('Logs')") - b.wait_text(".pf-m-expanded .container-logs .xterm-accessibility-tree > div:nth-child(1)", "1") - - # firefox optimizes these out when not visible - b.eval_js(""" - document.querySelector('.pf-m-expanded .container-logs .xterm-accessibility-tree').scrollIntoView() - """) - b.wait_in_text(".pf-m-expanded .container-logs .xterm-accessibility-tree", "6") - - b.click(".pf-m-expanded button:contains('Console')") - b.wait(lambda: - get_int(b.text(".pf-m-expanded .container-terminal .xterm-accessibility-tree > div:nth-child(3)")) > 7) - - # Create another instance without port publishing - b.wait_visible(f'#containers-images td[data-label="Image"]:contains("{IMG_BUSYBOX}")') - self.toggleExpandedContainer(IMG_BUSYBOX) - b.click(f'#containers-images tbody tr:contains("{IMG_BUSYBOX}") .ct-container-create') - b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")') - - b.wait_val("#create-image-image-select-typeahead", IMG_BUSYBOX_LATEST) - b.set_input_text("#run-image-dialog-name", "busybox-without-publish") - - # Set up command line - b.set_input_text('#run-image-dialog-command', - "sh -c 'for i in $(seq 20); do echo $i; sleep 3; done; sleep infinity'") - - # Run without tty, console should be able to `exec` - b.set_checked("#run-image-dialog-tty", False) - - b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn') - b.wait_not_present("div.pf-v5-c-modal-box") - - self.waitContainerRow("busybox-without-publish") - self.toggleExpandedContainer("busybox-without-publish") - b.wait_not_present(""" - #containers-containers tbody tr:contains("busybox-without-publish") + tr dt:contains("Ports") - """) - - # Rootless only works with CGroupsV2 - if auth or self.has_cgroupsV2: - cpuShares = self.execute(auth, """ - podman inspect --format '{{.HostConfig.CpuShares}}' busybox-without-publish - """).strip() - # podman ≥ 1.8 translates 0 default into actual value - self.assertIn(cpuShares, ['0', '1024']) - - b.set_val("#containers-containers-filter", "all") - - b.click(".pf-m-expanded button:contains('Console')") - b.wait_in_text(".pf-m-expanded .xterm-accessibility-tree", "/ # ") - b.focus(".pf-m-expanded .xterm-helper-textarea") - b.input_text("clear") - b.key("Enter") - b.wait_not_in_text(".pf-m-expanded .xterm-accessibility-tree", "clear") - b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(1)", "/ # ") - b.input_text("echo hello") - b.key("Enter") - b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(2)", "hello") - b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(3)", "/ # ") - b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(1)", "/ # echo hello") - - b.go("#/?name=tty") - self.check_containers(["busybox-with-tty"], ["busybox-without-publish"]) - b.go("#/?name=busy") - self.check_containers(["busybox-with-tty", "busybox-without-publish"], []) - - b.set_input_text('#containers-filter', 'tty') - self.check_containers(["busybox-with-tty"], ["busybox-without-publish"]) - self.check_images([], [IMG_ALPINE, IMG_BUSYBOX, IMG_REGISTRY]) - b.set_input_text('#containers-filter', 'busy') - b.wait_js_cond('window.location.hash === "#/?name=busy"') - self.check_containers(["busybox-with-tty", "busybox-without-publish"], []) - self.check_images([IMG_BUSYBOX], [IMG_ALPINE, IMG_REGISTRY]) - b.set_input_text('#containers-filter', 'alpine') - b.wait_js_cond('window.location.hash === "#/?name=alpine"') - self.check_containers([], ["busybox-with-tty", "busybox-without-publish"]) - self.check_images([IMG_ALPINE], [IMG_BUSYBOX, IMG_REGISTRY]) - b.set_input_text('#containers-filter', '') - self.check_containers(["busybox-with-tty", "busybox-without-publish"], []) - self.check_images([IMG_ALPINE, IMG_BUSYBOX, IMG_REGISTRY], []) - b.wait_js_cond('window.location.hash === "#/"') - - self.filter_containers("running") - id_with_tty = self.execute(auth, "podman inspect --format '{{.Id}}' busybox-with-tty").strip() - - container_sel = f'#containers-images tbody tr:contains("{IMG_BUSYBOX}")' - b.click(f'{container_sel} td.pf-v5-c-table__toggle button') - # running container, just selects it, but leaves "Only running" alone - b.click(f"{container_sel} + tr div.ct-listing-panel-body dt:contains('Used by') + dd button:contains('busybox-with-tty')") # noqa: E501 - b.wait_js_cond('window.location.hash === "#' + id_with_tty + '"') - b.wait_val("#containers-containers-filter", "running") - # FIXME: expanding running container details does not actually work right now - # b.wait_in_text("#containers-containers tr.pf-m-expanded .container-details", "sleep infinity") - # stopped container, switches to showing all containers - - # Create a container without starting it - self.filter_containers("all") - container_name = "busybox-not-started" - b.wait_visible(f'#containers-images td[data-label="Image"]:contains("{IMG_BUSYBOX}")') - b.click(f'#containers-images tbody tr:contains("{IMG_BUSYBOX}") .ct-container-create') - b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")') - - b.wait_val("#create-image-image-select-typeahead", IMG_BUSYBOX_LATEST) - b.set_input_text("#run-image-dialog-name", container_name) - b.set_input_text("#run-image-dialog-command", "sh -c sleep infinity") - - b.click('.pf-v5-c-modal-box__footer #create-image-create-btn') - b.wait_not_present("div.pf-v5-c-modal-box") - - sha = self.execute(auth, "podman inspect --format '{{.Id}}' " + container_name).strip() - self.waitContainer(sha, auth, name=container_name, image=IMG_BUSYBOX, state=['Configured', 'Created']) - - self.filter_containers("running") - b.wait_not_in_text("#containers-containers", "busybox-not-started") - container_sel = f"#containers-images tbody tr:contains('{IMG_BUSYBOX}') + tr div.ct-listing-panel-body" - b.click(f"{container_sel} dt:contains('Used by') + dd button:contains('busybox-not-started')") - b.wait_js_cond(f"window.location.hash === '#{sha}'") - b.wait_val("#containers-containers-filter", "all") - b.wait_in_text("#containers-containers", "busybox-not-started") - # auto-expands container details - b.wait_in_text("#containers-containers tbody tr:contains('busybox-not-started') + tr", "sleep infinity") - - b.click(f'#containers-images tbody tr:contains("{IMG_ALPINE}") td.pf-v5-c-table__toggle button') - b.wait_in_text(f"#containers-images tbody tr:contains('{IMG_ALPINE}') td[data-label='Used by']", 'unused') - - b.set_input_text('#containers-filter', 'foobar') - b.wait_in_text('#containers-containers .pf-v5-c-empty-state', 'No containers that match the current filter') - b.wait_in_text('#containers-images .pf-v5-c-empty-state', 'No images that match the current filter') - b.set_input_text('#containers-filter', '') - - if not auth or not self.machine.ostree_image: # don't kill ws container - # Ubuntu 22.04 has old podman that does not know about --time - if m.image != 'ubuntu-2204': - # Remove all containers first as it is not possible to set --time 0 to rmi command - self.execute(auth, "podman rm --all --force --time 0") - self.execute(auth, "podman rmi -af") - b.wait_in_text('#containers-containers .pf-v5-c-empty-state', 'No containers') - b.set_val("#containers-containers-filter", "running") - b.wait_in_text('#containers-containers .pf-v5-c-empty-state', 'No running containers') - b.wait_in_text('#containers-images .pf-v5-c-empty-state', 'No images') - - def check_content(self, kind, present, not_present): - b = self.browser - for item in present: - b.wait_visible(f'#containers-{kind} tbody tr:first-child:contains({item})') - for item in not_present: - b.wait_not_present(f'#containers-{kind} tbody tr:first-child:contains({item})') - - def check_containers(self, present, not_present): - self.check_content("containers", present, not_present) - - def check_images(self, present, not_present): - self.check_content("images", present, not_present) - - def waitContainer(self, row_id, auth, name="", image="", cmd="", owner="", state=None, pod="no-pod"): - """Check the container with row_name has the expected values - "image" can be substring, "state" might be string or array of possible states, other are - checked for exact match. - """ - sel = "#containers-containers #table-" + pod + f" tbody tr[data-row-id=\"{row_id}{auth}\"]".lower() - b = self.browser - if name: - b.wait_text(sel + " .container-name", name) - if image: - b.wait_in_text(sel + " td[data-label=Container] small:nth-child(2)", image) - if cmd: - b.wait_text(sel + " td[data-label=Container] small:last-child", cmd) - if owner: - if owner == "system": - b.wait_text(sel + " td[data-label=Owner]", owner) - else: - b.wait_text(sel + " td[data-label=Owner]", "user: " + owner) - if state is not None: - if not isinstance(state, list): - state = [state] - b.wait(lambda: b.text(sel + " td[data-label=State]") in state) - - def filter_containers(self, value): - """Use dropdown menu in the header to filter containers""" - b = self.browser - b.set_val("#containers-containers-filter", value) - - def confirm_modal(self, text): - """Wait for the pop up window and click the button with text""" - b = self.browser - b.click(f".pf-v5-c-modal-box footer button:contains({text})") - b.wait_not_present(f".pf-v5-c-modal-box footer button:contains({text})") - - def testPruneUnusedImagesSystem(self): - self._testPruneUnusedImagesSystem(True) - - def testPruneUnusedImagesUser(self): - self._testPruneUnusedImagesSystem(False) - - @testlib.skipOstree("no root login available on ostree") - def testPruneUnusedImagesRoot(self): - self._testPruneUnusedImagesSystem(False, True) - - def _testPruneUnusedImagesSystem(self, auth, root=False): - b = self.browser - if root: - self.login_and_go("/podman", user="root", enable_root_login=True) - b.wait_visible("#app") - else: - self.login(auth) - - leftover_images = 1 - # cockpit-ws image - if self.machine.ostree_image and auth: - leftover_images += 1 - - # By default we have 3 unused images, start one. - self.execute(auth or root, f"podman run -d --name used_image --stop-timeout 0 {IMG_ALPINE} sh") - b.click("#image-actions-dropdown") - b.click("#prune-unused-images-button") - - if auth: - b.wait_js_func("ph_count_check", ".pf-v5-c-modal-box__body .pf-v5-c-list li", - (self.user_images_count + self.system_images_count) - leftover_images) - elif root: - b.wait_js_func("ph_count_check", ".pf-v5-c-modal-box__body .pf-v5-c-list li", - self.system_images_count - leftover_images) - else: - b.wait_js_func("ph_count_check", ".pf-v5-c-modal-box__body .pf-v5-c-list li", - self.user_images_count - leftover_images) - b.click(".pf-v5-c-modal-box button:contains(Prune)") - - # When being superuser, admin images are also removed - if auth: - self.waitNumImages(leftover_images) - checkImage(b, IMG_ALPINE, "system") - else: - self.waitNumImages(leftover_images) - # Two images removed, one in use kept - b.wait_not_present(f"#containers-images:contains('{IMG_BUSYBOX}')") - b.wait_not_present(f"#containers-images:contains('{IMG_REGISTRY}')") - b.wait_visible(f"#containers-images:contains('{IMG_ALPINE}')") - - # Prune button should now be disabled - b.click("#image-actions-dropdown") - b.wait_visible(".pf-m-disabled.pf-v5-c-menu__list-item:contains(Prune unused images)") - - def testPruneUnusedImagesSystemSelections(self): - """ Test the prune unused images selection options""" - b = self.browser - self.login(True) - - b.click("#image-actions-dropdown") - b.click("button:contains(Prune unused images)") - - # Deselect both - b.click("#deleteSystemImages") - b.click("#deleteUserImages") - b.wait_visible(".pf-v5-c-modal-box button:contains(Prune):disabled") - - # Admin / user images are selected - expected_images = self.user_images_count + self.system_images_count - if self.machine.ostree_image: - expected_images -= 1 - b.wait_js_func("ph_count_check", ".pf-v5-c-modal-box__body .pf-v5-c-list li", expected_images) - # Select user images - b.click("#deleteUserImages") - b.click(".pf-v5-c-modal-box button:contains(Prune)") - - # System images are left over - self.waitNumImages(self.system_images_count) - checkImage(b, IMG_ALPINE, "system") - checkImage(b, IMG_BUSYBOX, "system") - checkImage(b, IMG_REGISTRY, "system") - - # Pruning again, should delete all system images - b.click("#image-actions-dropdown") - b.click("button:contains(Prune unused images)") - b.wait_js_func("ph_count_check", ".pf-v5-c-modal-box__body .pf-v5-c-list li", - self.system_images_count - 1 if self.machine.ostree_image else self.system_images_count) - b.click(".pf-v5-c-modal-box button:contains(Prune)") - self.waitNumImages(1 if self.machine.ostree_image else 0) - - # Prune button should now be disabled - b.click("#image-actions-dropdown") - b.wait_visible(".pf-v5-c-menu__list-item.pf-m-disabled:contains(Prune unused images)") - - def testPruneUnusedContainersSystem(self): - self._testPruneUnusedContainersSystem(True) - - def testPruneUnusedContainersUser(self): - self._testPruneUnusedContainersSystem(False) - - def _testPruneUnusedContainersSystem(self, auth): - """Test the prune unused container image dialog""" - - b = self.browser - self.login(auth) - - # Create running and non-running containers - self.execute(auth, "podman pod create --name pod") - notrunninginpodId = self.execute(auth, f""" - podman run --name inpod --pod pod -tid {IMG_BUSYBOX} sh -c 'exit 1'""").strip() - runninginpodId = self.execute(auth, f""" - podman run --name inpodrunning --pod pod -tid {IMG_BUSYBOX} sh -c 'sleep infinity'""").strip() - - self.execute(auth, f"podman run --name notrunning -tid {IMG_BUSYBOX} sh -c 'exit 1'") - self.execute(auth, f"podman run --name containerrunning -tid {IMG_BUSYBOX} sh -c 'sleep infinity'") - - # Create containers for the opposite of what we are, admin or super admin - if auth: - self.execute(False, f"podman run --name adminnotrunning -tid {IMG_BUSYBOX} sh 'exit 1'") - b.wait(lambda: self.getContainerAttr("adminnotrunning", "State") in NOT_RUNNING) - self.execute(False, f"podman run --name adminrunning -tid {IMG_BUSYBOX} sh -c 'sleep infinity'") - b.wait(lambda: self.getContainerAttr("adminrunning", "State") == "Running") - - b.click("#containers-actions-dropdown") - b.click("button:contains(Prune unused containers)") - - if auth: - b.wait_in_text(".pf-v5-c-modal-box__body tbody:nth-of-type(1) td[data-label=Name]", "adminnotrunning") - b.wait_in_text(".pf-v5-c-modal-box__body tbody:nth-of-type(2) td[data-label=Name]", "notrunning") - else: - b.wait_in_text(".pf-v5-c-modal-box__body tbody td[data-label=Name]", "notrunning") - - b.click(".pf-v5-c-modal-box button:contains(Prune)") - b.wait_not_present(".pf-v5-c-modal-box__body") - - if auth: - self.waitContainerRow("notrunning", False) - self.waitContainerRow("adminnotrunning", False) - else: - self.waitContainerRow("notrunning", False) - - # Verify running containers still exists - self.waitContainerRow("containerrunning") - pods = [{"name": "inpod", "state": "Exited", "id": notrunninginpodId, - "image": IMG_BUSYBOX, "command": 'sh -c "exit 1"'}, - {"name": "inpodrunning", "state": "Running", "id": runninginpodId, - "image": IMG_BUSYBOX, "command": 'sh -c "sleep infinity"'}] - self.waitPodContainer("pod", pods, auth) - - def testCreateContainerValidation(self): - def validateField(groupSelector, value, errorMessage, resetValue=""): - b.wait_visible(".pf-v5-c-modal-box__footer #create-image-create-run-btn:not(:disabled)") - b.set_input_text(f"{groupSelector} input", value) - b.wait_in_text(f"{groupSelector} .pf-v5-c-helper-text__item-text", errorMessage) - b.wait_visible(".pf-v5-c-modal-box__footer #create-image-create-run-btn:disabled") - # Reset to acceptable value and verify the validation message is not present - b.set_input_text(f"{groupSelector} input", resetValue) - b.wait_not_present(f"{groupSelector} .pf-v5-c-helper-text__item-text") - b.wait_visible(".pf-v5-c-modal-box__footer #create-image-create-run-btn:not(:disabled)") - - # Test the validation errors - - # complaint about port conflict - self.allow_browser_errors("error: Container failed to be started:.*") - self.allow_browser_errors("No routable interface.*") - self.allow_browser_errors(".*ddress already in use.*5000.*") - b = self.browser - self.login(False) - container_name = 'portused' - - # Start a podman container which uses a port - self.execute(False, f"podman run -d -p 5000:5000 --name registry --stop-timeout 0 {IMG_REGISTRY}") - b.click("#containers-images button.pf-v5-c-expandable-section__toggle") - - b.wait_visible(f'#containers-images td[data-label="Image"]:contains("{IMG_BUSYBOX}")') - b.click(f'#containers-images tbody tr:contains("{IMG_BUSYBOX}") .ct-container-create') - b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")') - - validateField("#image-name-group", "registry", "Name already in use") - - # Switch to Integration tab - b.click("#pf-tab-1-create-image-dialog-tab-integration") - - # Test validation of port mapping - b.click('.publish-port-form .btn-add') - b.set_input_text("#run-image-dialog-publish-0-container-port-group input", "1") - validateField("#run-image-dialog-publish-0-ip-address-group", "abcd", "valid IP address") - validateField("#run-image-dialog-publish-0-host-port-group", "-1", "1 to 65535") - validateField("#run-image-dialog-publish-0-host-port-group", "99999", "1 to 65535") - validateField("#run-image-dialog-publish-0-container-port-group", "-1", "1 to 65535", resetValue="1") - validateField("#run-image-dialog-publish-0-container-port-group", "", "must not be empty", resetValue="1") - validateField("#run-image-dialog-publish-0-container-port-group", "99999", "1 to 65535", resetValue="1") - - # Test validation of volumes - b.click('.volume-form .btn-add') - b.set_input_text("#run-image-dialog-volume-0-container-path-group input", "/somepath") - validateField("#run-image-dialog-volume-0-container-path-group", "", "not be empty", resetValue="/somepath") - - # Test validation of environment variables - b.click('.env-form .btn-add') - b.set_input_text("#run-image-dialog-env-0-key-group input", "sometext") - validateField("#run-image-dialog-env-0-key-group", "", "must not be empty", resetValue="sometext") - - b.set_input_text("#run-image-dialog-name", container_name) - - # Port address is already in use - b.set_input_text('#run-image-dialog-publish-0-host-port', '5000') - b.set_input_text('#run-image-dialog-publish-0-container-port', '5000') - b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn') - # Can be "[aA]ddress already in use" or "Failed to bind any port" with newer passt - b.wait_visible(".pf-v5-c-alert") - self.assertRegex(b.text(".pf-v5-c-alert"), "[aA]ddress already in use|[fF]ailed to bind any port") - - # Changing the port should allow creation of container - b.set_input_text('#run-image-dialog-publish-0-host-port', '5001') - b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn') - b.wait_not_present("#run-image-dialog-name") - - self.waitContainerRow(container_name) - - # Test validation JavaScript errors when removing invalid environment entries - container_name = 'env-var-validation' - b.click(f'#containers-images tbody tr:contains("{IMG_BUSYBOX}") .ct-container-create') - b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")') - - b.set_input_text("#run-image-dialog-name", container_name) - b.click("#pf-tab-1-create-image-dialog-tab-integration") - - # Make sure our form validation does not crash when adding and removing invalid entries - b.click('.env-form .btn-add') - b.click('.env-form .btn-add') - b.click('.env-form .btn-add') - b.set_input_text("#run-image-dialog-env-1-key-group input", "something") - b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn') - - b.wait_in_text("#run-image-dialog-env-0-key-group .pf-v5-c-helper-text__item-text", "must not be empty") - b.wait_in_text("#run-image-dialog-env-2-key-group .pf-v5-c-helper-text__item-text", "must not be empty") - - # remove invalid entries - b.click('#run-image-dialog-env-0-btn-close') - b.click('#run-image-dialog-env-2-btn-close') - - b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn') - self.waitContainerRow(container_name) - - def _testHealthcheck(self, auth): - b = self.browser - - # Just drop user images so we can use simpler selectors - if auth: - self.execute(False, f"podman rmi {IMG_BUSYBOX}") - - self.login(auth) - - b.click("#containers-images button.pf-v5-c-expandable-section__toggle") - - b.wait_visible(f'#containers-images td[data-label="Image"]:contains("{IMG_BUSYBOX}")') - b.click(f'#containers-images tbody tr:contains("{IMG_BUSYBOX}") .ct-container-create') - b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")') - - b.set_input_text("#run-image-dialog-name", "healthy") - - b.click("#pf-tab-2-create-image-dialog-tab-healthcheck") - b.set_input_text('#run-image-dialog-healthcheck-command', 'true') - b.set_input_text('#run-image-healthcheck-interval input', '325') - b.set_input_text('#run-image-healthcheck-timeout input', '35') - b.set_input_text('#run-image-healthcheck-start-period input', '5') - b.click('#run-image-healthcheck-retries .pf-v5-c-input-group__item:nth-child(1) button') - b.wait_val("#run-image-healthcheck-retries input", 2) - if auth: - b.assert_pixels('.pf-v5-c-modal-box', "healthcheck-modal", skip_layouts=["rtl"]) - # Test that the healthcheck option is not available before podman 4.3 - if podman_version(self) < (4, 3, 0): - b.wait_not_present("#run-image-healthcheck-action") - b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn') - - self.waitContainerRow("healthy") - b.click("#containers-images button.pf-v5-c-expandable-section__toggle") - - healthy_sha = self.execute(auth, "podman inspect --format '{{.Id}}' healthy").strip() - self.waitContainer(healthy_sha, auth, state='RunningHealthy') - - self.toggleExpandedContainer("healthy") - b.click(".pf-m-expanded button:contains('Health check')") - - b.wait_in_text('#container-details-healthcheck dt:contains("Command") + dd', 'true') - b.wait_in_text('#container-details-healthcheck dt:contains("Interval") + dd', '325 seconds') - b.wait_in_text('#container-details-healthcheck dt:contains("Retries") + dd', '2') - b.wait_in_text('#container-details-healthcheck dt:contains("Timeout") + dd', '35 seconds') - b.wait_in_text('#container-details-healthcheck dt:contains("Start period") + dd', '5 seconds') - b.wait_not_present('#container-details-healthcheck dt:contains("Failing streak")') - if podman_version(self) >= (4, 3, 0): - b.wait_in_text('#container-details-healthcheck dt:contains("When unhealthy") + dd', 'No action') - - # format changed in podman 5.1, adds StartInterval (https://github.com/containers/podman/pull/22334) - self.assertIn(self.execute(auth, "podman inspect --format '{{.Config.Healthcheck}}' healthy").strip(), - ["{[true] 5s 5m25s 35s 2}", "{[true] 5s 0s 5m25s 35s 2}"]) - - # single successful health check - b.wait_in_text(".ct-listing-panel-body tbody tr", "Passed health run") - b.wait_visible(".ct-listing-panel-body tbody:nth-of-type(1) svg.green") - b.wait_not_present(".ct-listing-panel-body tbody:nth-of-type(2)") - - # Trigger run manually, adds one more healthy run - self.performContainerAction("healthy", "Run health check") - b.wait_visible(".ct-listing-panel-body tbody:nth-of-type(2) svg.green") - b.wait_not_present(".ct-listing-panel-body tbody:nth-of-type(3)") - - self.toggleExpandedContainer("healthy") - - self.execute(auth, f"podman run --name sick -dt --health-cmd false --health-interval 5s {IMG_BUSYBOX}") - self.waitContainerRow("sick") - unhealthy_sha = self.execute(auth, "podman inspect --format '{{.Id}}' sick").strip() - self.waitContainer(unhealthy_sha, auth, state='RunningUnhealthy') - # Unhealthy should be first - expected_ws = "" - if auth and self.machine.ostree_image: - expected_ws = "ws" - b.wait_collected_text("#containers-containers .container-name", "healthysick" + expected_ws) - - self.toggleExpandedContainer("sick") - b.click(".pf-m-expanded button:contains('Health check')") - b.wait_visible(".pf-m-expanded .ct-listing-panel-body tbody:nth-of-type(1)") - b.wait_visible(".pf-m-expanded .ct-listing-panel-body tbody:nth-of-type(4)") - b.wait_visible(".pf-m-expanded .ct-listing-panel-body tbody:nth-of-type(2) svg.red") - b.wait_visible('.pf-m-expanded #container-details-healthcheck dt:contains("Failing streak")') - failures = int(b.text('.pf-m-expanded #container-details-healthcheck dt:contains("Failing streak") + dd')) - self.assertGreater(failures, 3) - if auth: - b.wait_js_func("ph_count_check", ".pf-m-expanded table[aria-label=Logs] tbody tr", 5) - b.assert_pixels(".pf-m-expanded .pf-v5-c-table__expandable-row-content", - "healthcheck-details", - ignore=["thead", "#container-details-healthcheck dt:contains('Failing streak') + dd", - "td[data-label='Started at']"], - skip_layouts=["rtl"]) - - self.toggleExpandedContainer("sick") - b.click("#containers-images button.pf-v5-c-expandable-section__toggle") - - b.wait_visible('#containers-images td[data-label="Image"]:contains("busybox:latest")') - b.click('#containers-images tbody tr:contains("busybox:latest") .ct-container-create') - b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")') - - # Test the health check action, only supported in podman 4.3 and later. - # To test this we make a healthcheck which depends on a file, so when starting the - # container is healthy, after we remove the file the healthcheck should fail and our - # configured action should executed. - if podman_version(self) < (4, 3, 0): - return - - containername = "healthaction" - b.set_input_text("#run-image-dialog-name", containername) - b.set_input_text("#run-image-dialog-command", "/bin/sh -c 'echo 1 > /healthy && sleep infinity'") - - b.click("#pf-tab-2-create-image-dialog-tab-healthcheck") - b.set_input_text('#run-image-dialog-healthcheck-command', '/bin/test -f /healthy') - b.set_input_text('#run-image-healthcheck-interval input', '1') - b.set_input_text('#run-image-healthcheck-timeout input', '1') - b.click('#run-image-healthcheck-action-2') - b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn') - - self.waitContainerRow(containername) - self.toggleExpandedContainer(containername) - b.wait(lambda: self.getContainerAttr(containername, "State") == "RunningHealthy") - b.click(".pf-m-expanded button:contains('Health check')") - b.wait_in_text('.pf-m-expanded #container-details-healthcheck dt:contains("When unhealthy") + dd', - 'Force stop') - # Removing the file should kill the container - status = self.execute(auth, f"podman exec {containername} rm -f /healthy").strip() - b.wait(lambda: self.getContainerAttr(containername, - "State", "span:not(.ct-badge-container-unhealthy)") in NOT_RUNNING) - status = self.execute(auth, f"podman inspect --format '{{{{.State.Health.Status}}}}' {containername}").strip() - self.assertEqual(status, "unhealthy") - - def testHealthcheckSystem(self): - self._testHealthcheck(True) - - def testHealthcheckUser(self): - self._testHealthcheck(False) - - # Ubuntu 2204 lacks user systemd units - # https://github.com/containers/podman/commit/9312d458b4254b48e331d1ae40cb2f6d0fec9bd0 - @testlib.skipImage("podman-restart not available for user", "ubuntu-2204") - def testPodmanRestartEnabledUser(self): - self._testPodmanRestartEnabled(False) - - def testPodmanRestartEnabledSystem(self): - self._testPodmanRestartEnabled(True) - - def _testPodmanRestartEnabled(self, auth): - b = self.browser - if auth: - self.addCleanup(self.machine.execute, "systemctl disable podman-restart.service") - else: - self.restore_dir("/home/admin/.config") - self.machine.execute("loginctl enable-linger $(id -u admin)") - # HACK: verify that the file we watch exists - self.assertTrue(self.machine.execute(""" - if test -e /var/lib/systemd/linger/admin; then echo yes; fi - """).strip() != "") - self.addCleanup(self.machine.execute, "loginctl disable-linger $(id -u admin)") - - # Drop user images for easy selection - if auth: - self.execute(False, f"podman rmi {IMG_BUSYBOX}") - - self.login(auth) - b.click("#containers-images button.pf-v5-c-expandable-section__toggle") - - def create_container(name, policy=None): - b.wait_visible(f'#containers-images td[data-label="Image"]:contains("{IMG_BUSYBOX}")') - b.click(f'#containers-images tbody tr:contains("{IMG_BUSYBOX}") .ct-container-create') - b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")') - - b.set_input_text("#run-image-dialog-name", name) - if policy: - b.set_val("#run-image-dialog-restart-policy", "always") - b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn') - self.waitContainerRow(name) - - container_name = 'none' - create_container(container_name) - # format changed in podman 5.1 (https://github.com/containers/podman/pull/22322) - self.assertIn(self.getRestartPolicy(auth, container_name), ['{ 0}', '{no 0}']) - - container_name = 'restart' - create_container(container_name, 'always') - self.assertEqual(self.getRestartPolicy(auth, container_name), '{always 0}') - if auth: - podmanRestartEnabled = self.execute(True, "systemctl is-enabled podman-restart.service || true").strip() - else: - podmanRestartEnabled = self.execute(False, - "systemctl --user is-enabled podman-restart.service || true").strip() - self.assertEqual(podmanRestartEnabled, 'enabled') - - def _testCreateContainerInPod(self, auth): - b = self.browser - - container_name = 'containerinpod' - podname = "pod1" - self.execute(auth, f"podman pod create --infra=false --name={podname}") - - self.login(auth) - - self.filter_containers('all') - b.click(".create-container-in-pod") - - # the podname should be in the "Create container" header - self.assertIn(podname, b.text("#pf-modal-part-1")) - - b.set_input_text("#run-image-dialog-name", container_name) - b.set_input_text("#create-image-image-select-typeahead", IMG_BUSYBOX_LATEST) - # we want to switch to "Local", but to make waiting for IMG_BUSYBOX race-free, we first need to - # switch to another view; different images have different registries, so don't assume their name, just - # that at least one more exists - b.click('.image-search-footer .pf-v5-c-toggle-group__item:nth-of-type(3) button') - b.wait_in_text(".pf-v5-c-select__menu-list", "No images found") - b.click('button.pf-v5-c-toggle-group__button:contains("Local")') - b.click(f'button.pf-v5-c-select__menu-item:contains("{IMG_BUSYBOX_LATEST}")') - b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn') - b.wait_not_present("#run-image-dialog-name") - - container_sha = self.execute(auth, f"podman inspect --format '{{{{.Id}}}}' {container_name}").strip() - self.waitContainer(container_sha, auth, name=container_name, image=IMG_BUSYBOX, cmd='sh', - state='Running', owner="system" if auth else "admin", pod=podname) - - # Check that we correctly preselect owner - if auth: - self.execute(False, "podman pod create --infra=false --name=system_pod") - b.click("#table-system_pod .create-container-in-pod") - b.wait_visible("#run-image-dialog-owner-user:checked") - b.wait_visible("#run-image-dialog-owner-user:disabled") - b.wait_visible("#run-image-dialog-owner-system:disabled") - - def testCreateContainerInPodSystem(self): - self._testCreateContainerInPod(True) - - def testCreateContainerInPodUser(self): - self._testCreateContainerInPod(False) - - def testPauseResumeContainerSystem(self): - self._testPauseResumeContainer(True) - - def testPauseResumeContainerUser(self): - # rootless cgroupv1 containers do not support pausing - if not self.has_cgroupsV2: - return - self._testPauseResumeContainer(False) - - def _testPauseResumeContainer(self, auth): - b = self.browser - container_name = "pauseresume" - - self.execute(auth, f"podman run -dt --name {container_name} --stop-timeout 0 {IMG_ALPINE}") - self.login(auth) - - self.waitContainerRow(container_name) - self.toggleExpandedContainer(container_name) - b.wait_not_present(self.getContainerAction(container_name, 'Resume')) - self.performContainerAction(container_name, "Pause") - - # show all containers and check status - self.filter_containers('all') - - # Check that container details are not lost when the container is paused - b.click(".pf-m-expanded button:contains('Integration')") - b.wait_visible(f'#containers-containers tr:contains("{IMG_ALPINE}") dt:contains("Environment variables")') - - b.wait(lambda: self.getContainerAttr(container_name, "State") == "Paused") - b.wait_not_present(self.getContainerAction(container_name, 'Pause')) - self.performContainerAction(container_name, "Resume") - b.wait(lambda: self.getContainerAttr(container_name, "State") == "Running") - - def testRenameContainerSystem(self): - self._testRenameContainer(True) - - def testRenameContainerUser(self): - self._testRenameContainer(False) - - def _testRenameContainer(self, auth): - b = self.browser - container_name = "rename" - container_name_new = "rename-new" - - self.execute(auth, f"podman container create -t --name {container_name} {IMG_BUSYBOX}") - self.login(auth) - - self.filter_containers('all') - - self.waitContainerRow(container_name) - self.toggleExpandedContainer(container_name) - self.performContainerAction(container_name, "Rename") - - # the container name should be in the "Rename container" header - b.wait_in_text("#pf-modal-part-1", container_name) - b.set_input_text("#rename-dialog-container-name", "") - b.wait_in_text("#commit-dialog-image-name-helper", "Container name is required") - b.set_input_text("#rename-dialog-container-name", "banana???") - b.wait_in_text("#commit-dialog-image-name-helper", "Name can only contain letters, numbers") - - b.set_input_text("#rename-dialog-container-name", container_name_new) - b.click('#btn-rename-dialog-container') - b.wait_not_present("#rename-dialog-container-name") - - self.execute(auth, f"podman inspect --format '{{{{.Id}}}}' {container_name_new}").strip() - self.waitContainerRow(container_name_new) - - # rename using the enter key - self.toggleExpandedContainer(container_name_new) - self.performContainerAction(container_name_new, "Rename") - - container_name_new = "rename-new-enter" - b.set_input_text("#rename-dialog-container-name", "") - b.focus("#rename-dialog-container-name") - b.key("Enter") - b.wait_in_text("#commit-dialog-image-name-helper", "Container name is required") - b.set_input_text("#rename-dialog-container-name", container_name_new) - b.focus("#rename-dialog-container-name") - b.key("Enter") - b.wait_not_present("#rename-dialog-container-name") - - self.execute(auth, f"podman inspect --format '{{{{.Id}}}}' {container_name_new}").strip() - self.waitContainerRow(container_name_new) - - def testMultipleContainers(self): - self.login() - - # Create 31 containers - for i in range(31): - self.execute(True, f"podman run -dt --name container{i} --stop-timeout 0 {IMG_BUSYBOX}") - - self.waitContainerRow("container30") - - # Generic cleanup takes too long and timeouts, so remove these container manually one by one - for i in range(31): - self.execute(True, f"podman rm -f container{i}") - - def testSpecialContainers(self): - m = self.machine - b = self.browser - - toolbox_label = "com.github.containers.toolbox=true" - distrobox_label = "manager=distrobox" - - container_1_id = m.execute(f"podman run -d --name container_1 -l {toolbox_label} {IMG_BUSYBOX}").strip() - container_2_id = m.execute(f"podman run -d --name container_2 -l {distrobox_label} {IMG_BUSYBOX}").strip() - - self.login() - - self.waitContainerRow('container_1') - self.waitContainerRow('container_2') - - container_1_sel = f"#containers-containers tbody tr[data-row-id=\"{container_1_id}{'true'}\"]" - container_2_sel = f"#containers-containers tbody tr[data-row-id=\"{container_2_id}{'true'}\"]" - - b.wait_visible(container_1_sel + " .ct-badge-toolbox:contains('toolbox')") - b.wait_visible(container_2_sel + " .ct-badge-distrobox:contains('distrobox')") - - # this isn't *really* a Firefox bug, but the different timing just triggers this a lot - # https://github.com/cockpit-project/cockpit-podman/issues/1836 - @testlib.skipBrowser("pod dialog state management is broken", "firefox") - def testCreatePodSystem(self): - self._createPod(True) - - @testlib.skipBrowser("pod dialog state management is broken", "firefox") - def testCreatePodUser(self): - self._createPod(False) - - def _createPod(self, auth): - b = self.browser - m = self.machine - pod_name = "testpod1" - - self.login(auth) - - b.click("#containers-containers-create-pod-btn") - b.set_input_text("#create-pod-dialog-name", "") - b.wait_visible(".pf-v5-c-modal-box__footer #create-pod-create-btn:disabled") - b.wait_in_text("#pod-name-group .pf-v5-c-helper-text__item-text", "Invalid characters") - - b.set_input_text("#create-pod-dialog-name", pod_name) - b.wait_visible(".pf-v5-c-modal-box__footer #create-pod-create-btn:not(:disabled)") - - b.click('.publish-port-form .btn-add') - b.set_input_text("#create-pod-dialog-publish-0-container-port-group input", "-1") - b.click(".pf-v5-c-modal-box__footer #create-pod-create-btn") - b.wait_in_text("#create-pod-dialog-publish-0-container-port-group .pf-v5-c-helper-text__item-text", - "1 to 65535") - b.click("#create-pod-dialog-publish-0-btn-close") - - if auth: - b.wait_visible("#create-pod-dialog-owner-system:checked") - else: - b.wait_not_present("#create-pod-dialog-owner-system") - - # Ports - b.click('.publish-port-form .btn-add') - b.set_input_text('#create-pod-dialog-publish-1-host-port', '6000') - b.set_input_text('#create-pod-dialog-publish-1-container-port', '5000') - b.click('.publish-port-form .btn-add') - b.set_input_text('#create-pod-dialog-publish-2-ip-address', '127.0.0.1') - b.set_input_text('#create-pod-dialog-publish-2-host-port', '6001') - b.set_input_text('#create-pod-dialog-publish-2-container-port', '5001') - b.set_val('#create-pod-dialog-publish-2-protocol', "udp") - b.click('.publish-port-form .btn-add') - b.set_input_text('#create-pod-dialog-publish-3-ip-address', '127.0.0.2') - b.set_input_text('#create-pod-dialog-publish-3-container-port', '9001') - - # Volumes - if self.machine.image not in ["ubuntu-2204"]: - b.click('.volume-form .btn-add') - rodir, rwdir = m.execute("mktemp; mktemp").split('\n')[:2] - m.execute(f"chown admin:admin {rodir}") - m.execute(f"chown admin:admin {rwdir}") - - if self.has_selinux: - b.set_val('#create-pod-dialog-volume-0-selinux', "z") - else: - b.wait_not_present('#create-pod-dialog-volume-0-selinux') - - b.set_file_autocomplete_val("#create-pod-dialog-volume-0 .pf-v5-c-select", rodir) - b.set_input_text('#create-pod-dialog-volume-0-container-path', '/tmp/ro') - b.click('.volume-form .btn-add') - - b.set_file_autocomplete_val("#create-pod-dialog-volume-1 .pf-v5-c-select", rwdir) - b.set_input_text('#create-pod-dialog-volume-1-container-path', '/tmp/rw') - - b.click("#create-pod-create-btn") - b.set_val("#containers-containers-filter", "all") - self.waitPodContainer(pod_name, []) - - container_name = 'test-pod-1-system' if auth else 'test-pod-1' - cmd = f"podman run -d --pod {pod_name} --name {container_name} --stop-timeout 0 {IMG_ALPINE} sleep 500" - containerId = self.execute(auth, cmd).strip() - self.waitPodContainer(pod_name, - [{"name": container_name, "image": IMG_ALPINE, - "command": "sleep 500", "state": "Running", "id": containerId}], auth) - - self.toggleExpandedContainer(container_name) - b.click(".pf-m-expanded button:contains('Integration')") - if self.machine.image not in ["ubuntu-2204"]: - b.wait_in_text('#containers-containers tr:contains("alpine") dt:contains("Volumes") + dd', - f"{rodir} \u2194 /tmp/ro") - b.wait_in_text('#containers-containers tr:contains("alpine") dt:contains("Volumes") + dd', - f"{rwdir} \u2194 /tmp/rw") - - b.wait_in_text('#containers-containers tr:contains("alpine") dt:contains("Ports") + dd', - '0.0.0.0:6000 \u2192 5000/tcp') - b.wait_in_text('#containers-containers tr:contains("alpine") dt:contains("Ports") + dd', - '127.0.0.1:6001 \u2192 5001/udp') - b.wait_in_text('#containers-containers tr:contains("alpine") dt:contains("Ports") + dd', - ' \u2192 9001/tcp') - - # Create pod as admin - if auth: - pod_name = 'testpod2' - b.click("#containers-containers-create-pod-btn") - b.set_input_text("#create-pod-dialog-name", pod_name) - b.click("#create-pod-dialog-owner-user") - b.click("#create-pod-create-btn") - - b.set_val("#containers-containers-filter", "all") - self.waitPodContainer(pod_name, []) - - @testlib.skipImage("passthrough log driver not supported", "ubuntu-2204") - def testLogErrors(self): - b = self.browser - container_name = "logissue" - self.login() - - self.execute(False, - f"podman run --log-driver=passthrough --name {container_name} -d {IMG_ALPINE} false Date: Tue, 10 Sep 2024 13:38:46 +0200 Subject: [PATCH 2/2] show me the stuff --- test/check-application | 1 + 1 file changed, 1 insertion(+) diff --git a/test/check-application b/test/check-application index c13320521..2b8984563 100755 --- a/test/check-application +++ b/test/check-application @@ -148,6 +148,7 @@ class TestApplication(testlib.MachineCase): m.execute(""" podman run -d --tty --name testing localhost/test-busybox sh podman --log-level=debug container checkpoint --keep --tcp-established testing || true; + ls -lh /var/lib/containers/storage/overlay-containers/*/userdata; cat /var/lib/containers/storage/overlay-containers/*/userdata/dump.log || true; exit 1 """)