diff options
Diffstat (limited to 'test')
25 files changed, 769 insertions, 64 deletions
diff --git a/test/apiv2/10-images.at b/test/apiv2/10-images.at index 037a4c01f..9e464dbc7 100644 --- a/test/apiv2/10-images.at +++ b/test/apiv2/10-images.at @@ -147,4 +147,39 @@ t GET "images/get?names=alpine&names=busybox" 200 '[POSIX tar archive]' img_cnt=$(tar xf "$WORKDIR/curl.result.out" manifest.json -O | jq "length") is "$img_cnt" 2 "number of images in tar archive" +# check build works when uploading container file as a tar, see issue #10660 +TMPD=$(mktemp -d podman-apiv2-test.build.XXXXXXXX) +function cleanBuildTest() { + podman rmi -a -f + rm -rf "${TMPD}" &> /dev/null +} +CONTAINERFILE_TAR="${TMPD}/containerfile.tar" +cat > $TMPD/containerfile << EOF +FROM quay.io/libpod/alpine_labels:latest +EOF +tar --format=posix -C $TMPD -cvf ${CONTAINERFILE_TAR} containerfile &> /dev/null + +curl -XPOST --data-binary @<(cat $CONTAINERFILE_TAR) \ + -H "content-type: application/x-tar" \ + --dump-header "${TMPD}/headers.txt" \ + -o "${TMPD}/response.txt" \ + "http://$HOST:$PORT/v1.40/libpod/build?dockerfile=containerfile" &> /dev/null + +BUILD_TEST_ERROR="" + +if ! grep -q '200 OK' "${TMPD}/headers.txt"; then + echo -e "${red}NOK: Image build from tar failed response was not 200 OK" + BUILD_TEST_ERROR="1" +fi + +if ! grep -q 'quay.io/libpod/alpine_labels' "${TMPD}/response.txt"; then + echo -e "${red}NOK: Image build from tar failed image name not in response" + BUILD_TEST_ERROR="1" +fi + +cleanBuildTest +if [[ "${BUILD_TEST_ERROR}" ]]; then + exit 1 +fi + # vim: filetype=sh diff --git a/test/apiv2/20-containers.at b/test/apiv2/20-containers.at index a81210855..ef51757c9 100644 --- a/test/apiv2/20-containers.at +++ b/test/apiv2/20-containers.at @@ -341,3 +341,12 @@ t GET containers/$cid/json 200 \ .HostConfig.NanoCpus=500000 t DELETE containers/$cid?v=true 204 + +# Test Compat Create with default network mode (#10569) +t POST containers/create Image=$IMAGE HostConfig='{"NetworkMode":"default"}' 201 \ + .Id~[0-9a-f]\\{64\\} +cid=$(jq -r '.Id' <<<"$output") +t GET containers/$cid/json 200 \ + .HostConfig.NetworkMode="bridge" + +t DELETE containers/$cid?v=true 204 diff --git a/test/apiv2/python/rest_api/fixtures/api_testcase.py b/test/apiv2/python/rest_api/fixtures/api_testcase.py index 8b771774b..155e93928 100644 --- a/test/apiv2/python/rest_api/fixtures/api_testcase.py +++ b/test/apiv2/python/rest_api/fixtures/api_testcase.py @@ -49,7 +49,7 @@ class APITestCase(unittest.TestCase): def setUp(self): super().setUp() - APITestCase.podman.run("run", "alpine", "/bin/ls", check=True) + APITestCase.podman.run("run", "-d", "alpine", "top", check=True) def tearDown(self) -> None: APITestCase.podman.run("pod", "rm", "--all", "--force", check=True) diff --git a/test/apiv2/python/rest_api/test_v2_0_0_container.py b/test/apiv2/python/rest_api/test_v2_0_0_container.py index f67013117..b4b3af2df 100644 --- a/test/apiv2/python/rest_api/test_v2_0_0_container.py +++ b/test/apiv2/python/rest_api/test_v2_0_0_container.py @@ -12,7 +12,7 @@ class ContainerTestCase(APITestCase): r = requests.get(self.uri("/containers/json"), timeout=5) self.assertEqual(r.status_code, 200, r.text) obj = r.json() - self.assertEqual(len(obj), 0) + self.assertEqual(len(obj), 1) def test_list_all(self): r = requests.get(self.uri("/containers/json?all=true")) @@ -36,7 +36,7 @@ class ContainerTestCase(APITestCase): self.assertId(r.content) def test_delete(self): - r = requests.delete(self.uri(self.resolve_container("/containers/{}"))) + r = requests.delete(self.uri(self.resolve_container("/containers/{}?force=true"))) self.assertEqual(r.status_code, 204, r.text) def test_stop(self): diff --git a/test/apiv2/python/rest_api/test_v2_0_0_image.py b/test/apiv2/python/rest_api/test_v2_0_0_image.py index 243b1d5f5..59dcea87f 100644 --- a/test/apiv2/python/rest_api/test_v2_0_0_image.py +++ b/test/apiv2/python/rest_api/test_v2_0_0_image.py @@ -87,6 +87,13 @@ class ImageTestCase(APITestCase): self.assertTrue(keys["images"], "Expected to find images stanza") self.assertTrue(keys["stream"], "Expected to find stream progress stanza's") + def test_create(self): + r = requests.post( + self.podman_url + "/v1.40/images/create?fromImage=alpine&platform=linux/amd64/v8", timeout=15) + self.assertEqual(r.status_code, 200, r.text) + r = requests.post(self.podman_url + "/v1.40/images/create?fromSrc=-&repo=fedora&message=testing123&platform=linux/amd64", timeout=15) + self.assertEqual(r.status_code, 200, r.text) + def test_search_compat(self): url = self.podman_url + "/v1.40/images/search" diff --git a/test/apiv2/python/rest_api/test_v2_0_0_network.py b/test/apiv2/python/rest_api/test_v2_0_0_network.py index 3888123fb..d606b9351 100644 --- a/test/apiv2/python/rest_api/test_v2_0_0_network.py +++ b/test/apiv2/python/rest_api/test_v2_0_0_network.py @@ -102,6 +102,33 @@ class NetworkTestCase(APITestCase): "TestNetwork", payload["NetworkSettings"]["Networks"]["TestNetwork"]["NetworkID"], ) + def test_inspect(self): + name = f"Network_{random.getrandbits(160):x}" + create = requests.post(self.podman_url + "/v1.40/networks/create", json={"Name": name}) + self.assertEqual(create.status_code, 201, create.text) + self.assertId(create.content) + + net = create.json() + self.assertIsInstance(net, dict) + self.assertNotEqual(net["Id"], name) + ident = net["Id"] + + ls = requests.get(self.podman_url + "/v1.40/networks") + self.assertEqual(ls.status_code, 200, ls.text) + + networks = ls.json() + self.assertIsInstance(networks, list) + + found = False + for net in networks: + if net["Name"] == name: + found = True + break + self.assertTrue(found, f"Network '{name}' not found") + + inspect = requests.get(self.podman_url + f"/v1.40/networks/{ident}?verbose=false&scope=local") + self.assertEqual(inspect.status_code, 200, inspect.text) + def test_crud(self): name = f"Network_{random.getrandbits(160):x}" diff --git a/test/compose/test-compose b/test/compose/test-compose index 981f78a79..70db6dd55 100755 --- a/test/compose/test-compose +++ b/test/compose/test-compose @@ -183,6 +183,8 @@ function test_port() { fi echo "# cat $WORKDIR/server.log:" cat $WORKDIR/server.log + echo "# cat $logfile:" + cat $logfile return fi diff --git a/test/e2e/build_test.go b/test/e2e/build_test.go index 6255690b1..abaacdd5e 100644 --- a/test/e2e/build_test.go +++ b/test/e2e/build_test.go @@ -604,4 +604,38 @@ RUN echo hello`, ALPINE) Expect(inspect.OutputToString()).To(Equal("windows")) }) + + It("podman build device test", func() { + if _, err := os.Lstat("/dev/fuse"); err != nil { + Skip(fmt.Sprintf("test requires stat /dev/fuse to work: %v", err)) + } + containerfile := fmt.Sprintf(`FROM %s +RUN ls /dev/fuse`, ALPINE) + containerfilePath := filepath.Join(podmanTest.TempDir, "Containerfile") + err := ioutil.WriteFile(containerfilePath, []byte(containerfile), 0755) + Expect(err).To(BeNil()) + session := podmanTest.Podman([]string{"build", "--pull-never", "-t", "test", "--file", containerfilePath, podmanTest.TempDir}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(125)) + + session = podmanTest.Podman([]string{"build", "--pull-never", "--device", "/dev/fuse", "-t", "test", "--file", containerfilePath, podmanTest.TempDir}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + }) + + It("podman build device rename test", func() { + SkipIfRootless("rootless builds do not currently support renaming devices") + containerfile := fmt.Sprintf(`FROM %s +RUN ls /dev/test1`, ALPINE) + containerfilePath := filepath.Join(podmanTest.TempDir, "Containerfile") + err := ioutil.WriteFile(containerfilePath, []byte(containerfile), 0755) + Expect(err).To(BeNil()) + session := podmanTest.Podman([]string{"build", "--pull-never", "-t", "test", "--file", containerfilePath, podmanTest.TempDir}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(125)) + + session = podmanTest.Podman([]string{"build", "--pull-never", "--device", "/dev/zero:/dev/test1", "-t", "test", "--file", containerfilePath, podmanTest.TempDir}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + }) }) diff --git a/test/e2e/checkpoint_test.go b/test/e2e/checkpoint_test.go index 9d0049910..70a1d09ed 100644 --- a/test/e2e/checkpoint_test.go +++ b/test/e2e/checkpoint_test.go @@ -425,6 +425,106 @@ var _ = Describe("Podman checkpoint", func() { // Remove exported checkpoint os.Remove(fileName) }) + // This test does the same steps which are necessary for migrating + // a container from one host to another + It("podman checkpoint container with export and different compression algorithms", func() { + localRunString := getRunString([]string{"--rm", ALPINE, "top"}) + session := podmanTest.Podman(localRunString) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(1)) + cid := session.OutputToString() + fileName := "/tmp/checkpoint-" + cid + ".tar" + + // Checkpoint with the default algorithm + result := podmanTest.Podman([]string{"container", "checkpoint", "-l", "-e", fileName}) + result.WaitWithDefaultTimeout() + + // As the container has been started with '--rm' it will be completely + // cleaned up after checkpointing. + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) + Expect(podmanTest.NumberOfContainers()).To(Equal(0)) + + // Restore container + result = podmanTest.Podman([]string{"container", "restore", "-i", fileName}) + result.WaitWithDefaultTimeout() + + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(1)) + Expect(podmanTest.GetContainerStatus()).To(ContainSubstring("Up")) + + // Checkpoint with the zstd algorithm + result = podmanTest.Podman([]string{"container", "checkpoint", "-l", "-e", fileName, "--compress", "zstd"}) + result.WaitWithDefaultTimeout() + + // As the container has been started with '--rm' it will be completely + // cleaned up after checkpointing. + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) + Expect(podmanTest.NumberOfContainers()).To(Equal(0)) + + // Restore container + result = podmanTest.Podman([]string{"container", "restore", "-i", fileName}) + result.WaitWithDefaultTimeout() + + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(1)) + Expect(podmanTest.GetContainerStatus()).To(ContainSubstring("Up")) + + // Checkpoint with the none algorithm + result = podmanTest.Podman([]string{"container", "checkpoint", "-l", "-e", fileName, "-c", "none"}) + result.WaitWithDefaultTimeout() + + // As the container has been started with '--rm' it will be completely + // cleaned up after checkpointing. + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) + Expect(podmanTest.NumberOfContainers()).To(Equal(0)) + + // Restore container + result = podmanTest.Podman([]string{"container", "restore", "-i", fileName}) + result.WaitWithDefaultTimeout() + + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(1)) + Expect(podmanTest.GetContainerStatus()).To(ContainSubstring("Up")) + + // Checkpoint with the gzip algorithm + result = podmanTest.Podman([]string{"container", "checkpoint", "-l", "-e", fileName, "-c", "gzip"}) + result.WaitWithDefaultTimeout() + + // As the container has been started with '--rm' it will be completely + // cleaned up after checkpointing. + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) + Expect(podmanTest.NumberOfContainers()).To(Equal(0)) + + // Restore container + result = podmanTest.Podman([]string{"container", "restore", "-i", fileName}) + result.WaitWithDefaultTimeout() + + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(1)) + Expect(podmanTest.GetContainerStatus()).To(ContainSubstring("Up")) + + // Checkpoint with the non-existing algorithm + result = podmanTest.Podman([]string{"container", "checkpoint", "-l", "-e", fileName, "-c", "non-existing"}) + result.WaitWithDefaultTimeout() + + Expect(result.ExitCode()).To(Equal(125)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(1)) + Expect(podmanTest.NumberOfContainers()).To(Equal(1)) + + result = podmanTest.Podman([]string{"rm", "-fa"}) + result.WaitWithDefaultTimeout() + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) + Expect(podmanTest.NumberOfContainers()).To(Equal(0)) + + // Remove exported checkpoint + os.Remove(fileName) + }) It("podman checkpoint and restore container with root file-system changes", func() { // Start the container @@ -822,4 +922,58 @@ var _ = Describe("Podman checkpoint", func() { os.Remove(checkpointFileName) os.Remove(preCheckpointFileName) }) + + It("podman checkpoint and restore container with different port mappings", func() { + localRunString := getRunString([]string{"-p", "1234:6379", "--rm", redis}) + session := podmanTest.Podman(localRunString) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + cid := session.OutputToString() + fileName := "/tmp/checkpoint-" + cid + ".tar.gz" + + // Open a network connection to the redis server via initial port mapping + conn, err := net.Dial("tcp", "localhost:1234") + if err != nil { + os.Exit(1) + } + conn.Close() + + // Checkpoint the container + result := podmanTest.Podman([]string{"container", "checkpoint", "-l", "-e", fileName}) + result.WaitWithDefaultTimeout() + + // As the container has been started with '--rm' it will be completely + // cleaned up after checkpointing. + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) + Expect(podmanTest.NumberOfContainers()).To(Equal(0)) + + // Restore container with different port mapping + result = podmanTest.Podman([]string{"container", "restore", "-p", "1235:6379", "-i", fileName}) + result.WaitWithDefaultTimeout() + + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(1)) + Expect(podmanTest.GetContainerStatus()).To(ContainSubstring("Up")) + + // Open a network connection to the redis server via initial port mapping + // This should fail + conn, err = net.Dial("tcp", "localhost:1234") + Expect(err.Error()).To(ContainSubstring("connection refused")) + // Open a network connection to the redis server via new port mapping + conn, err = net.Dial("tcp", "localhost:1235") + if err != nil { + os.Exit(1) + } + conn.Close() + + result = podmanTest.Podman([]string{"rm", "-fa"}) + result.WaitWithDefaultTimeout() + Expect(result.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) + Expect(podmanTest.NumberOfContainers()).To(Equal(0)) + + // Remove exported checkpoint + os.Remove(fileName) + }) }) diff --git a/test/e2e/common_test.go b/test/e2e/common_test.go index 7ffee961c..1aeeca4cb 100644 --- a/test/e2e/common_test.go +++ b/test/e2e/common_test.go @@ -451,19 +451,13 @@ func (p *PodmanTestIntegration) RunLsContainerInPod(name, pod string) (*PodmanSe // BuildImage uses podman build and buildah to build an image // called imageName based on a string dockerfile func (p *PodmanTestIntegration) BuildImage(dockerfile, imageName string, layers string) string { - dockerfilePath := filepath.Join(p.TempDir, "Dockerfile") - err := ioutil.WriteFile(dockerfilePath, []byte(dockerfile), 0755) - Expect(err).To(BeNil()) - cmd := []string{"build", "--pull-never", "--layers=" + layers, "--file", dockerfilePath} - if len(imageName) > 0 { - cmd = append(cmd, []string{"-t", imageName}...) - } - cmd = append(cmd, p.TempDir) - session := p.Podman(cmd) - session.Wait(240) - Expect(session).Should(Exit(0), fmt.Sprintf("BuildImage session output: %q", session.OutputToString())) - output := session.OutputToStringArray() - return output[len(output)-1] + return p.buildImage(dockerfile, imageName, layers, "") +} + +// BuildImageWithLabel uses podman build and buildah to build an image +// called imageName based on a string dockerfile, adds desired label to paramset +func (p *PodmanTestIntegration) BuildImageWithLabel(dockerfile, imageName string, layers string, label string) string { + return p.buildImage(dockerfile, imageName, layers, label) } // PodmanPID execs podman and returns its PID @@ -828,3 +822,22 @@ func (p *PodmanSessionIntegration) jq(jqCommand string) (string, error) { err := cmd.Run() return strings.TrimRight(out.String(), "\n"), err } + +func (p *PodmanTestIntegration) buildImage(dockerfile, imageName string, layers string, label string) string { + dockerfilePath := filepath.Join(p.TempDir, "Dockerfile") + err := ioutil.WriteFile(dockerfilePath, []byte(dockerfile), 0755) + Expect(err).To(BeNil()) + cmd := []string{"build", "--pull-never", "--layers=" + layers, "--file", dockerfilePath} + if label != "" { + cmd = append(cmd, "--label="+label) + } + if len(imageName) > 0 { + cmd = append(cmd, []string{"-t", imageName}...) + } + cmd = append(cmd, p.TempDir) + session := p.Podman(cmd) + session.Wait(240) + Expect(session).Should(Exit(0), fmt.Sprintf("BuildImage session output: %q", session.OutputToString())) + output := session.OutputToStringArray() + return output[len(output)-1] +} diff --git a/test/e2e/events_test.go b/test/e2e/events_test.go index 4dbbe9dd8..cc7c4d996 100644 --- a/test/e2e/events_test.go +++ b/test/e2e/events_test.go @@ -8,6 +8,7 @@ import ( "sync" "time" + "github.com/containers/podman/v3/libpod/events" . "github.com/containers/podman/v3/test/utils" "github.com/containers/storage/pkg/stringid" . "github.com/onsi/ginkgo" @@ -134,12 +135,10 @@ var _ = Describe("Podman events", func() { jsonArr := test.OutputToStringArray() Expect(test.OutputToStringArray()).ShouldNot(BeEmpty()) - eventsMap := make(map[string]string) - err := json.Unmarshal([]byte(jsonArr[0]), &eventsMap) + event := events.Event{} + err := json.Unmarshal([]byte(jsonArr[0]), &event) Expect(err).ToNot(HaveOccurred()) - Expect(eventsMap).To(HaveKey("Status")) - test = podmanTest.Podman([]string{"events", "--stream=false", "--format", "{{json.}}"}) test.WaitWithDefaultTimeout() Expect(test).To(Exit(0)) @@ -147,11 +146,9 @@ var _ = Describe("Podman events", func() { jsonArr = test.OutputToStringArray() Expect(test.OutputToStringArray()).ShouldNot(BeEmpty()) - eventsMap = make(map[string]string) - err = json.Unmarshal([]byte(jsonArr[0]), &eventsMap) + event = events.Event{} + err = json.Unmarshal([]byte(jsonArr[0]), &event) Expect(err).ToNot(HaveOccurred()) - - Expect(eventsMap).To(HaveKey("Status")) }) It("podman events --until future", func() { diff --git a/test/e2e/generate_systemd_test.go b/test/e2e/generate_systemd_test.go index 75d778f10..e03d6899e 100644 --- a/test/e2e/generate_systemd_test.go +++ b/test/e2e/generate_systemd_test.go @@ -215,7 +215,6 @@ var _ = Describe("Podman generate systemd", func() { // Grepping the output (in addition to unit tests) Expect(session.OutputToString()).To(ContainSubstring("# container-foo.service")) Expect(session.OutputToString()).To(ContainSubstring(" --replace ")) - Expect(session.OutputToString()).To(ContainSubstring(" stop --ignore --cidfile %t/container-foo.ctr-id -t 42")) if !IsRemote() { // The podman commands in the unit should contain the root flags if generate systemd --new is used Expect(session.OutputToString()).To(ContainSubstring(" --runroot")) @@ -234,7 +233,6 @@ var _ = Describe("Podman generate systemd", func() { // Grepping the output (in addition to unit tests) Expect(session.OutputToString()).To(ContainSubstring("# container-foo.service")) Expect(session.OutputToString()).To(ContainSubstring(" --replace ")) - Expect(session.OutputToString()).To(ContainSubstring(" stop --ignore --cidfile %t/container-foo.ctr-id -t 42")) }) It("podman generate systemd --new without explicit detaching param", func() { @@ -247,7 +245,7 @@ var _ = Describe("Podman generate systemd", func() { Expect(session.ExitCode()).To(Equal(0)) // Grepping the output (in addition to unit tests) - Expect(session.OutputToString()).To(ContainSubstring("--cgroups=no-conmon -d")) + Expect(session.OutputToString()).To(ContainSubstring(" -d ")) }) It("podman generate systemd --new with explicit detaching param in middle", func() { diff --git a/test/e2e/images_test.go b/test/e2e/images_test.go index f6321ec1c..b4ec7447e 100644 --- a/test/e2e/images_test.go +++ b/test/e2e/images_test.go @@ -425,4 +425,25 @@ LABEL "com.example.vendor"="Example Vendor" Expect(result.OutputToStringArray()).To(Not(Equal(result1.OutputToStringArray()))) }) + It("podman image prune --filter", func() { + dockerfile := `FROM quay.io/libpod/alpine:latest +RUN > file +` + dockerfile2 := `FROM quay.io/libpod/alpine:latest +RUN > file2 +` + podmanTest.BuildImageWithLabel(dockerfile, "foobar.com/workdir:latest", "false", "abc") + podmanTest.BuildImageWithLabel(dockerfile2, "foobar.com/workdir:latest", "false", "xyz") + // --force used to to avoid y/n question + result := podmanTest.Podman([]string{"image", "prune", "--filter", "label=abc", "--force"}) + result.WaitWithDefaultTimeout() + Expect(result).Should(Exit(0)) + Expect(len(result.OutputToStringArray())).To(Equal(1)) + + //check if really abc is removed + result = podmanTest.Podman([]string{"image", "list", "--filter", "label=abc"}) + Expect(len(result.OutputToStringArray())).To(Equal(0)) + + }) + }) diff --git a/test/e2e/logs_test.go b/test/e2e/logs_test.go index 4d9cbb48b..b576fa072 100644 --- a/test/e2e/logs_test.go +++ b/test/e2e/logs_test.go @@ -173,9 +173,9 @@ var _ = Describe("Podman logs", func() { }) It("streaming output: "+log, func() { - containerName := "logs-f-rm" + containerName := "logs-f" - logc := podmanTest.Podman([]string{"run", "--log-driver", log, "--rm", "--name", containerName, "-dt", ALPINE, "sh", "-c", "echo podman; sleep 1; echo podman"}) + logc := podmanTest.Podman([]string{"run", "--log-driver", log, "--name", containerName, "-dt", ALPINE, "sh", "-c", "echo podman-1; sleep 1; echo podman-2"}) logc.WaitWithDefaultTimeout() Expect(logc).To(Exit(0)) @@ -183,10 +183,8 @@ var _ = Describe("Podman logs", func() { results.WaitWithDefaultTimeout() Expect(results).To(Exit(0)) - // TODO: we should actually check for two podman lines, - // but as of 2020-06-17 there's a race condition in which - // 'logs -f' may not catch all output from a container - Expect(results.OutputToString()).To(ContainSubstring("podman")) + Expect(results.OutputToString()).To(ContainSubstring("podman-1")) + Expect(results.OutputToString()).To(ContainSubstring("podman-2")) // Container should now be terminatING or terminatED, but we // have no guarantee of which: 'logs -f' does not necessarily @@ -199,6 +197,10 @@ var _ = Describe("Podman logs", func() { } else { Expect(inspect.ErrorToString()).To(ContainSubstring("no such container")) } + + results = podmanTest.Podman([]string{"rm", "-f", containerName}) + results.WaitWithDefaultTimeout() + Expect(results).To(Exit(0)) }) It("follow output stopped container: "+log, func() { diff --git a/test/e2e/run_test.go b/test/e2e/run_test.go index 58538b689..cae1b5aad 100644 --- a/test/e2e/run_test.go +++ b/test/e2e/run_test.go @@ -904,6 +904,18 @@ USER bin`, BB) Expect(session.ExitCode()).To(Equal(100)) }) + It("podman run with named volume", func() { + session := podmanTest.Podman([]string{"run", "--rm", ALPINE, "stat", "-c", "%a %Y", "/var/tmp"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + perms := session.OutputToString() + + session = podmanTest.Podman([]string{"run", "--rm", "-v", "test:/var/tmp", ALPINE, "stat", "-c", "%a %Y", "/var/tmp"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(Equal(perms)) + }) + It("podman run with built-in volume image", func() { session := podmanTest.Podman([]string{"run", "--rm", redis, "ls"}) session.WaitWithDefaultTimeout() diff --git a/test/e2e/run_volume_test.go b/test/e2e/run_volume_test.go index 9b77aaef8..4be1b2009 100644 --- a/test/e2e/run_volume_test.go +++ b/test/e2e/run_volume_test.go @@ -668,4 +668,36 @@ USER testuser`, fedoraMinimal) Expect(strings.Contains(test2.OutputToString(), testString)).To(BeTrue()) }) + + It("podman volume with uid and gid works", func() { + volName := "testVol" + volCreate := podmanTest.Podman([]string{"volume", "create", "--opt", "o=uid=1000", volName}) + volCreate.WaitWithDefaultTimeout() + Expect(volCreate.ExitCode()).To(Equal(0)) + + volMount := podmanTest.Podman([]string{"run", "--rm", "-v", fmt.Sprintf("%s:/test", volName), ALPINE, "stat", "-c", "%u", "/test"}) + volMount.WaitWithDefaultTimeout() + Expect(volMount.ExitCode()).To(Equal(0)) + Expect(volMount.OutputToString()).To(Equal("1000")) + + volName = "testVol2" + volCreate = podmanTest.Podman([]string{"volume", "create", "--opt", "o=gid=1000", volName}) + volCreate.WaitWithDefaultTimeout() + Expect(volCreate.ExitCode()).To(Equal(0)) + + volMount = podmanTest.Podman([]string{"run", "--rm", "-v", fmt.Sprintf("%s:/test", volName), ALPINE, "stat", "-c", "%g", "/test"}) + volMount.WaitWithDefaultTimeout() + Expect(volMount.ExitCode()).To(Equal(0)) + Expect(volMount.OutputToString()).To(Equal("1000")) + + volName = "testVol3" + volCreate = podmanTest.Podman([]string{"volume", "create", "--opt", "o=uid=1000,gid=1000", volName}) + volCreate.WaitWithDefaultTimeout() + Expect(volCreate.ExitCode()).To(Equal(0)) + + volMount = podmanTest.Podman([]string{"run", "--rm", "-v", fmt.Sprintf("%s:/test", volName), ALPINE, "stat", "-c", "%u:%g", "/test"}) + volMount.WaitWithDefaultTimeout() + Expect(volMount.ExitCode()).To(Equal(0)) + Expect(volMount.OutputToString()).To(Equal("1000:1000")) + }) }) diff --git a/test/e2e/systemd_test.go b/test/e2e/systemd_test.go index b132750b0..8dc14d5f7 100644 --- a/test/e2e/systemd_test.go +++ b/test/e2e/systemd_test.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/containers/podman/v3/pkg/rootless" . "github.com/containers/podman/v3/test/utils" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -115,6 +116,12 @@ WantedBy=multi-user.target conData := result.InspectContainerToJSON() Expect(len(conData)).To(Equal(1)) Expect(conData[0].Config.SystemdMode).To(BeTrue()) + + if CGROUPSV2 || !rootless.IsRootless() { + stats := podmanTest.Podman([]string{"stats", "--no-stream", ctrName}) + stats.WaitWithDefaultTimeout() + Expect(stats.ExitCode()).To(Equal(0)) + } }) It("podman create container with systemd entrypoint triggers systemd mode", func() { diff --git a/test/system/045-start.bats b/test/system/045-start.bats index 3e0118dba..7e4bbde8d 100644 --- a/test/system/045-start.bats +++ b/test/system/045-start.bats @@ -59,4 +59,15 @@ load helpers is "$output" "Error: fakepolicy invalid restart policy" } +@test "podman start --all --filter" { + run_podman run -d $IMAGE /bin/true + cid_exited_0="$output" + run_podman run -d $IMAGE /bin/false + cid_exited_1="$output" + + run_podman wait $cid_exited_0 $cid_exited_1 + run_podman start --all --filter exited=0 + is "$output" "$cid_exited_0" +} + # vim: filetype=sh diff --git a/test/system/070-build.bats b/test/system/070-build.bats index 0f3f3fa7f..6843e28a5 100644 --- a/test/system/070-build.bats +++ b/test/system/070-build.bats @@ -29,6 +29,29 @@ EOF run_podman rmi -f build_test } +@test "podman build test -f -" { + rand_filename=$(random_string 20) + rand_content=$(random_string 50) + + tmpdir=$PODMAN_TMPDIR/build-test + mkdir -p $tmpdir + containerfile=$PODMAN_TMPDIR/Containerfile + cat >$containerfile <<EOF +FROM $IMAGE +RUN apk add nginx +RUN echo $rand_content > /$rand_filename +EOF + + # The 'apk' command can take a long time to fetch files; bump timeout + PODMAN_TIMEOUT=240 run_podman build -t build_test -f - --format=docker $tmpdir < $containerfile + is "$output" ".*STEP 4: COMMIT" "COMMIT seen in log" + + run_podman run --rm build_test cat /$rand_filename + is "$output" "$rand_content" "reading generated file in image" + + run_podman rmi -f build_test +} + @test "podman build - global runtime flags test" { skip_if_remote "--runtime-flag flag not supported for remote" @@ -794,6 +817,32 @@ EOF run_podman rmi -f build_test } +@test "podman build -f test " { + tmpdir=$PODMAN_TMPDIR/build-test + subdir=$tmpdir/subdir + mkdir -p $subdir + + containerfile1=$tmpdir/Containerfile1 + cat >$containerfile1 <<EOF +FROM scratch +copy . /tmp +EOF + containerfile2=$PODMAN_TMPDIR/Containerfile2 + cat >$containerfile2 <<EOF +FROM $IMAGE +EOF + run_podman build -t build_test -f Containerfile1 $tmpdir + run_podman 125 build -t build_test -f Containerfile2 $tmpdir + is "$output" ".*Containerfile2: no such file or directory" "Containerfile2 should not exist" + run_podman build -t build_test -f $containerfile1 $tmpdir + run_podman build -t build_test -f $containerfile2 $tmpdir + run_podman build -t build_test -f $containerfile1 + run_podman build -t build_test -f $containerfile2 + run_podman build -t build_test -f $containerfile1 -f $containerfile2 $tmpdir + is "$output" ".*$IMAGE" "Containerfile2 is also passed to server" + run_podman rmi -f build_test +} + function teardown() { # A timeout or other error in 'build' can leave behind stale images # that podman can't even see and which will cascade into subsequent diff --git a/test/system/090-events.bats b/test/system/090-events.bats index 52936d7a0..d889bd7f9 100644 --- a/test/system/090-events.bats +++ b/test/system/090-events.bats @@ -6,7 +6,6 @@ load helpers @test "events with a filter by label" { - skip_if_remote "FIXME: -remote does not include labels in event output" cname=test-$(random_string 30 | tr A-Z a-z) labelname=$(random_string 10) labelvalue=$(random_string 15) diff --git a/test/system/255-auto-update.bats b/test/system/255-auto-update.bats new file mode 100644 index 000000000..9bfb44791 --- /dev/null +++ b/test/system/255-auto-update.bats @@ -0,0 +1,274 @@ +#!/usr/bin/env bats -*- bats -*- +# +# Tests for automatically update images for containerized services +# + +load helpers + +UNIT_DIR="/usr/lib/systemd/system" +SNAME_FILE=$BATS_TMPDIR/services + +function setup() { + skip_if_remote "systemd tests are meaningless over remote" + skip_if_rootless + + basic_setup +} + +function teardown() { + while read line; do + if [[ "$line" =~ "podman-auto-update" ]]; then + echo "Stop timer: $line.timer" + systemctl stop $line.timer + systemctl disable $line.timer + else + systemctl stop $line + fi + rm -f $UNIT_DIR/$line.{service,timer} + done < $SNAME_FILE + + rm -f $SNAME_FILE + run_podman ? rmi quay.io/libpod/alpine:latest + run_podman ? rmi quay.io/libpod/alpine_nginx:latest + run_podman ? rmi quay.io/libpod/localtest:latest + basic_teardown +} + +# This functions is used for handle the basic step in auto-update related +# tests. Including following steps: +# 1. Generate a random container name and echo it to output. +# 2. Tag the fake image before test +# 3. Start a container with io.containers.autoupdate +# 4. Generate the service file from the container +# 5. Remove the origin container +# 6. Start the container from service +function generate_service() { + local target_img_basename=$1 + local autoupdate=$2 + + # Container name. Include the autoupdate type, to make debugging easier. + # IMPORTANT: variable 'cname' is passed (out of scope) up to caller! + cname=c_${autoupdate//\'/}_$(random_string) + target_img="quay.io/libpod/$target_img_basename:latest" + run_podman tag $IMAGE $target_img + if [[ -n "$autoupdate" ]]; then + label="--label io.containers.autoupdate=$autoupdate" + else + label="" + fi + run_podman run -d --name $cname $label $target_img top -d 120 + + run_podman generate systemd --new $cname + echo "$output" > "$UNIT_DIR/container-$cname.service" + echo "container-$cname" >> $SNAME_FILE + run_podman rm -f $cname + + systemctl daemon-reload + systemctl start container-$cname + systemctl status container-$cname + + # Original image ID. + # IMPORTANT: variable 'ori_image' is passed (out of scope) up to caller! + run_podman inspect --format "{{.Image}}" $cname + ori_image=$output +} + +function _wait_service_ready() { + local sname=$1 + + local timeout=6 + while [[ $timeout -gt 1 ]]; do + if systemctl -q is-active $sname; then + return + fi + sleep 1 + let timeout=$timeout-1 + done + + # Print serivce status as debug information before failed the case + systemctl status $sname + die "Timed out waiting for $sname to start" +} + +# Wait for container to update, as confirmed by its image ID changing +function _confirm_update() { + local cname=$1 + local old_iid=$2 + + # Image has already been pulled, so this shouldn't take too long + local timeout=5 + while [[ $timeout -gt 0 ]]; do + run_podman '?' inspect --format "{{.Image}}" $cname + if [[ $status != 0 ]]; then + if [[ $output =~ (no such object|does not exist in database): ]]; then + # this is ok, it just means the container is being restarted + : + else + die "podman inspect $cname failed unexpectedly" + fi + elif [[ $output != $old_iid ]]; then + return + fi + sleep 1 + done + + die "Timed out waiting for $cname to update; old IID=$old_iid" +} + +# This test can fail in dev. environment because of SELinux. +# quick fix: chcon -t container_runtime_exec_t ./bin/podman +@test "podman auto-update - label io.containers.autoupdate=image" { + generate_service alpine image + + _wait_service_ready container-$cname.service + run_podman auto-update + is "$output" "Trying to pull.*" "Image is updated." + _confirm_update $cname $ori_image +} + +@test "podman auto-update - label io.containers.autoupdate=disabled" { + generate_service alpine disabled + + _wait_service_ready container-$cname.service + run_podman auto-update + is "$output" "" "Image is not updated when autoupdate=disabled." + + run_podman inspect --format "{{.Image}}" $cname + is "$output" "$ori_image" "Image ID should not change" +} + +@test "podman auto-update - label io.containers.autoupdate=fakevalue" { + fakevalue=fake_$(random_string) + generate_service alpine $fakevalue + + _wait_service_ready container-$cname.service + run_podman 125 auto-update + is "$output" ".*invalid auto-update policy.*" "invalid policy setup" + + run_podman inspect --format "{{.Image}}" $cname + is "$output" "$ori_image" "Image ID should not change" +} + +@test "podman auto-update - label io.containers.autoupdate=local" { + generate_service localtest local + podman commit --change CMD=/bin/bash $cname quay.io/libpod/localtest:latest + + _wait_service_ready container-$cname.service + run_podman auto-update + _confirm_update $cname $ori_image +} + +@test "podman auto-update with multiple services" { + # Preserve original image ID, to confirm that it changes (or not) + run_podman inspect --format "{{.Id}}" $IMAGE + local img_id="$output" + + local cnames=() + local -A expect_update + local -A will_update=([image]=1 [registry]=1 [local]=1) + + local fakevalue=fake_$(random_string) + for auto_update in image registry "" disabled "''" $fakevalue local + do + local img_base="alpine" + if [[ $auto_update == "registry" ]]; then + img_base="alpine_nginx" + elif [[ $auto_update == "local" ]]; then + img_base="localtest" + fi + generate_service $img_base $auto_update + cnames+=($cname) + if [[ $auto_update == "local" ]]; then + local_cname=$cname + fi + + if [[ -n "$auto_update" && -n "${will_update[$auto_update]}" ]]; then + expect_update[$cname]=1 + fi + done + + # Only check the last service is started. Previous services should already actived. + _wait_service_ready container-$cname.service + run_podman commit --change CMD=/bin/bash $local_cname quay.io/libpod/localtest:latest + # Exit code is expected, due to invalid 'fakevalue' + run_podman 125 auto-update + update_log=$output + is "$update_log" ".*invalid auto-update policy.*" "invalid policy setup" + is "$update_log" ".*1 error occurred.*" "invalid policy setup" + + local n_updated=$(grep -c 'Trying to pull' <<<"$update_log") + is "$n_updated" "2" "Number of images updated from registry." + + for cname in "${!expect_update[@]}"; do + is "$update_log" ".*$cname.*" "container with auto-update policy image updated" + # Just because podman says it fetched, doesn't mean it actually updated + _confirm_update $cname $img_id + done + + # Final confirmation that all image IDs have/haven't changed + for cname in "${cnames[@]}"; do + run_podman inspect --format "{{.Image}}" $cname + if [[ -n "${expect_update[$cname]}" ]]; then + if [[ "$output" == "$img_id" ]]; then + die "$cname: image ID ($output) did not change" + fi + else + is "$output" "$img_id" "Image should not be changed." + fi + done +} + +@test "podman auto-update using systemd" { + generate_service alpine image + + cat >$UNIT_DIR/podman-auto-update-$cname.timer <<EOF +[Unit] +Description=Podman auto-update testing timer + +[Timer] +OnCalendar=*-*-* *:*:0/2 +Persistent=true + +[Install] +WantedBy=timers.target +EOF + cat >$UNIT_DIR/podman-auto-update-$cname.service <<EOF +[Unit] +Description=Podman auto-update testing service +Documentation=man:podman-auto-update(1) +Wants=network.target +After=network-online.target + +[Service] +Type=oneshot +ExecStart=/usr/bin/podman auto-update + +[Install] +WantedBy=multi-user.target default.target +EOF + + echo "podman-auto-update-$cname" >> $SNAME_FILE + systemctl enable --now podman-auto-update-$cname.timer + systemctl list-timers --all + + local expect='Finished Podman auto-update testing service' + local failed_start=failed + local count=0 + while [ $count -lt 120 ]; do + run journalctl -n 15 -u podman-auto-update-$cname.service + if [[ "$output" =~ $expect ]]; then + failed_start= + break + fi + ((count+=1)) + sleep 1 + done + + if [[ -n "$failed_start" ]]; then + die "Did not find expected string '$expect' in journalctl output for $cname" + fi + + _confirm_update $cname $ori_image +} + +# vim: filetype=sh diff --git a/test/system/410-selinux.bats b/test/system/410-selinux.bats index f8cee0e59..4ef9c8b30 100644 --- a/test/system/410-selinux.bats +++ b/test/system/410-selinux.bats @@ -183,7 +183,10 @@ function check_label() { # runc and crun emit different diagnostics runtime=$(podman_runtime) case "$runtime" in - crun) expect="\`/proc/thread-self/attr/exec\`: OCI runtime error: unable to assign security attribute" ;; + # crun 0.20.1 changes the error message + # from /proc/thread-self/attr/exec`: .* unable to assign + # to /proc/self/attr/keycreate`: .* unable to process + crun) expect="\`/proc/.*\`: OCI runtime error: unable to \(assign\|process\) security attribute" ;; runc) expect="OCI runtime error: .*: failed to set /proc/self/attr/keycreate on procfs" ;; *) skip "Unknown runtime '$runtime'";; esac diff --git a/test/system/450-interactive.bats b/test/system/450-interactive.bats index a9bf52ee8..a2db39492 100644 --- a/test/system/450-interactive.bats +++ b/test/system/450-interactive.bats @@ -56,8 +56,7 @@ function teardown() { stty rows $rows cols $cols <$PODMAN_TEST_PTY # ...and make sure stty under podman reads that. - # FIXME: 'sleep 1' is needed for podman-remote; without it, there's - run_podman run -it --name mystty $IMAGE sh -c 'sleep 1;stty size' <$PODMAN_TEST_PTY + run_podman run -it --name mystty $IMAGE stty size <$PODMAN_TEST_PTY is "$output" "$rows $cols" "stty under podman reads the correct dimensions" } diff --git a/test/system/build-testimage b/test/system/build-testimage index 3e5b982ce..eb5849b5e 100755 --- a/test/system/build-testimage +++ b/test/system/build-testimage @@ -61,8 +61,8 @@ chmod 755 pause # - check for updates @ https://hub.docker.com/_/alpine # busybox-extras provides httpd needed in 500-networking.bats cat >Containerfile <<EOF -ARG ARCH=please-override-arch -FROM docker.io/\${ARCH}/alpine:3.12.0 +ARG REPO=please-override-repo +FROM docker.io/\${REPO}/alpine:3.13.5 RUN apk add busybox-extras ADD testimage-id pause /home/podman/ LABEL created_by=$create_script @@ -74,17 +74,46 @@ EOF # --squash-all : needed by 'tree' test in 070-build.bats podman rmi -f testimage &> /dev/null || true +# There should always be a testimage tagged ':0000000<X>' (eight digits, +# zero-padded sequence ID) in the same location; this is used by tests +# which need to pull a non-locally-cached image. This image will rarely +# if ever need to change, nor in fact does it even have to be a copy of +# this testimage since all we use it for is 'true'. +# However, it does need to be multiarch :-( +zerotag_latest=$(skopeo list-tags docker://quay.io/libpod/testimage |\ + jq -r '.Tags[]' |\ + sort --version-sort |\ + grep '^000' |\ + tail -n 1) +zerotag_next=$(printf "%08d" $((zerotag_latest + 1))) + +# We don't always need to push the :00xx image, but build it anyway. +zeroimg=quay.io/libpod/testimage:${zerotag_next} +buildah manifest create $zeroimg + # We need to use buildah because (as of 2021-02-23) only buildah has --manifest # and because Dan says arch emulation is not currently working on podman # (no further details). # Arch emulation on Fedora requires the qemu-user-static package. -for arch in amd64 arm64v8 ppc64le s390x;do +for arch in amd64 arm64 ppc64le s390x;do + # docker.io repo is usually the same name as the desired arch; except + # for arm64, where podman needs to have the arch be 'arm64' but the + # image lives in 'arm64v8'. + repo=$arch + if [[ $repo = "arm64" ]]; then + repo="${repo}v8" + fi + ${BUILDAH} bud \ --arch=$arch \ - --build-arg ARCH=$arch \ + --build-arg REPO=$repo \ --manifest=testimage \ --squash \ . + + # The zero-tag image + ${BUILDAH} pull --arch $arch docker.io/$repo/busybox:1.33.1 + ${BUILDAH} manifest add $zeroimg docker.io/$repo/busybox:1.33.1 done # Clean up @@ -94,23 +123,13 @@ rm -rf $tmpdir # Tag image and push (all arches) to quay. remote_tag=quay.io/libpod/testimage:$YMD podman tag testimage ${remote_tag} -${BUILDAH} manifest push --all ${remote_tag} docker://${remote_tag} +cat <<EOF -# Side note: there should always be a testimage tagged ':0000000<X>' -# (eight digits, zero-padded sequence ID) in the same location; this is -# used by tests which need to pull a non-locally-cached image. This -# image will rarely if ever need to change, nor in fact does it even -# have to be a copy of this testimage since all we use it for is 'true'. -# However, it does need to be multiarch :-( -# -# As of 2021-02-24 it is simply busybox, because it is super small, -# but it's complicated because of multiarch: -# -# img=quay.io/libpod/testimage:0000000<current+1> -# buildah manifest create $img -# for arch in amd64 arm64v8 ppc64le s390x;do -# buildah pull --arch $arch docker.io/$arch/busybox:1.32.0 -# buildah manifest add $img docker.io/$arch/busybox:1.32.0 -# done -# buildah manifest push --all $img docker://$img -# +If you're happy with these images, run: + + ${BUILDAH} manifest push --all ${remote_tag} docker://${remote_tag} + ${BUILDAH} manifest push --all ${zeroimg} docker://${zeroimg} + +(You do not always need to push the :0000 image) + +EOF diff --git a/test/system/helpers.bash b/test/system/helpers.bash index e0c208f57..1859a2168 100644 --- a/test/system/helpers.bash +++ b/test/system/helpers.bash @@ -7,14 +7,15 @@ PODMAN=${PODMAN:-podman} PODMAN_TEST_IMAGE_REGISTRY=${PODMAN_TEST_IMAGE_REGISTRY:-"quay.io"} PODMAN_TEST_IMAGE_USER=${PODMAN_TEST_IMAGE_USER:-"libpod"} PODMAN_TEST_IMAGE_NAME=${PODMAN_TEST_IMAGE_NAME:-"testimage"} -PODMAN_TEST_IMAGE_TAG=${PODMAN_TEST_IMAGE_TAG:-"20210427"} +PODMAN_TEST_IMAGE_TAG=${PODMAN_TEST_IMAGE_TAG:-"20210610"} PODMAN_TEST_IMAGE_FQN="$PODMAN_TEST_IMAGE_REGISTRY/$PODMAN_TEST_IMAGE_USER/$PODMAN_TEST_IMAGE_NAME:$PODMAN_TEST_IMAGE_TAG" PODMAN_TEST_IMAGE_ID= # Remote image that we *DO NOT* fetch or keep by default; used for testing pull -# This changed from 0 to 1 on 2021-02-24 due to multiarch considerations; it -# should change only very rarely. -PODMAN_NONLOCAL_IMAGE_FQN="$PODMAN_TEST_IMAGE_REGISTRY/$PODMAN_TEST_IMAGE_USER/$PODMAN_TEST_IMAGE_NAME:00000002" +# This has changed in 2021, from 0 through 3, various iterations of getting +# multiarch to work. It should change only very rarely. +PODMAN_NONLOCAL_IMAGE_TAG=${PODMAN_NONLOCAL_IMAGE_TAG:-"00000003"} +PODMAN_NONLOCAL_IMAGE_FQN="$PODMAN_TEST_IMAGE_REGISTRY/$PODMAN_TEST_IMAGE_USER/$PODMAN_TEST_IMAGE_NAME:$PODMAN_NONLOCAL_IMAGE_TAG" # Because who wants to spell that out each time? IMAGE=$PODMAN_TEST_IMAGE_FQN |