diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a32ad160d6..c808aaeb77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,6 +99,7 @@ jobs: with: version: ${{ steps.gv.outputs.version }} install-only: true + skip-cache: true # cache causes flaky results https://github.com/podman-container-tools/podman/issues/28893 - name: Install pre-commit run: pipx install pre-commit diff --git a/docs/source/markdown/podman-quadlet-basic-usage.7.md b/docs/source/markdown/podman-quadlet-basic-usage.7.md index 2311a8f337..28c55e76e9 100644 --- a/docs/source/markdown/podman-quadlet-basic-usage.7.md +++ b/docs/source/markdown/podman-quadlet-basic-usage.7.md @@ -42,7 +42,7 @@ For rootful use: sudo cp hello.container /etc/containers/systemd/ ``` -## Step 3: Reload and enable the service +## Step 3: Reload and start the service For rootless use: ```bash @@ -53,9 +53,13 @@ systemctl --user start hello.service For rootful use: ```bash sudo systemctl daemon-reload -sudo systemctl enable --now hello.service +sudo systemctl start hello.service ``` +Note quadlet services cannot be enabled as they are a generated systemd unit, +see [podman-systemd.unit(5)](podman-systemd.unit.5.md#enabling-unit-files) for more information. + + ## Expected Output: For rootless, check logs using: diff --git a/hack/ci/ci.sh b/hack/ci/ci.sh index 789708d38e..ce079a5aec 100755 --- a/hack/ci/ci.sh +++ b/hack/ci/ci.sh @@ -6,7 +6,7 @@ SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) source "$SCRIPT_DIR/lib.sh" -AUTOMATION_RELEASE="20260520t200858z" +AUTOMATION_RELEASE="20260616t073924z" # TODO should be renovate managed LIMA_VM_NAME=podman-ci REPO_DIR="$SCRIPT_DIR/../.." diff --git a/hack/ci/pr-should-include-tests b/hack/ci/pr-should-include-tests index b78cac51bb..9cca62ba3b 100755 --- a/hack/ci/pr-should-include-tests +++ b/hack/ci/pr-should-include-tests @@ -23,25 +23,13 @@ fi # Nothing changed under test subdirectory. # -# This is OK if the only files being touched are "safe" ones. +# This is OK if no source code files were changed. +# Also we allow vendor updated without tests so exclude the vendor files +# and also the version files so release PRs do not need tests. filtered_changes=$(git diff --name-only $base $head | - grep -F -vx .cirrus.yml | - grep -F -vx .pre-commit-config.yaml | - grep -F -vx .gitignore | - grep -F -vx go.mod | - grep -F -vx go.sum | - grep -F -vx podman.spec.rpkg | - grep -F -vx .golangci.yml | - grep -F -vx winmake.ps1 | - grep -E -v '/*Makefile$' | - grep -E -v '^[^/]+\.md$' | - grep -E -v '^.github' | - grep -E -v '^contrib/' | - grep -E -v '^docs/' | - grep -E -v '^hack/' | - grep -E -v '^nix/' | - grep -E -v '^vendor/' | - grep -E -v '^version/') + grep -E -v '^(test/tools/)?vendor/' | + grep -E -v '^version/' | + grep -E -x '.*\.(go|c|h)') if [[ -z "$filtered_changes" ]]; then exit 0 fi diff --git a/pkg/machine/volume_systemd.go b/pkg/machine/volume_systemd.go index 28dcc08934..dda038948d 100644 --- a/pkg/machine/volume_systemd.go +++ b/pkg/machine/volume_systemd.go @@ -7,6 +7,38 @@ import ( "go.podman.io/podman/v6/pkg/systemd/parser" ) +// fcosDirSymlinks maps FCOS root-level symlinks to their real paths. +// FCOS symlinks several top-level dirs into /var for the read-only rootfs. +// systemd rejects a mount unit whose Where= traverses a symlink, so we +// resolve these before writing the ignition unit. +var fcosDirSymlinks = map[string]string{ + "/home": "/var/home", + "/mnt": "/var/mnt", + "/opt": "/var/opt", + "/root": "/var/roothome", + "/srv": "/var/srv", +} + +// canonicalizeFCOSMountTarget returns the canonical path for target by +// substituting any known FCOS root-level symlink prefix. +func canonicalizeFCOSMountTarget(target string) string { + // Guard check for empty strings or paths not starting with '/' + if len(target) < 2 || target[0] != '/' { + return target + } + + // Find the end of the first path component (e.g. "/home" in "/home/alice"). + end := 1 + for end < len(target) && target[end] != '/' { + end++ + } + + if real, ok := fcosDirSymlinks[target[:end]]; ok { + return real + target[end:] + } + return target +} + // GenerateSystemDFilesForVirtiofsMounts generates the systemd unit files needed // to mount virtiofs volumes inside a FCOS guest VM. It is shared between the // AppleHV, LibKrun, and QEMU providers. @@ -30,10 +62,13 @@ func GenerateSystemDFilesForVirtiofsMounts(mounts []VirtIoFs) ([]ignition.Unit, return nil, err } + // Use the canonical path so systemd accepts the Where= value. + // On FCOS /home is a symlink to var/home; systemd rejects non-canonical paths. + canonicalTarget := canonicalizeFCOSMountTarget(mnt.Target) virtiofsMount := ignition.Unit{ Enabled: ignition.BoolToPtr(true), - Name: fmt.Sprintf("%s.mount", parser.PathEscape(mnt.Target)), - Contents: ignition.StrToPtr(fmt.Sprintf(mountUnitFile, mnt.Tag, mnt.Target)), + Name: fmt.Sprintf("%s.mount", parser.PathEscape(canonicalTarget)), + Contents: ignition.StrToPtr(fmt.Sprintf(mountUnitFile, mnt.Tag, canonicalTarget)), } unitFiles = append(unitFiles, virtiofsMount) diff --git a/pkg/machine/volume_systemd_test.go b/pkg/machine/volume_systemd_test.go new file mode 100644 index 0000000000..7bda6f0487 --- /dev/null +++ b/pkg/machine/volume_systemd_test.go @@ -0,0 +1,72 @@ +package machine + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCanonicalizeFCOSMountTarget(t *testing.T) { + tests := []struct { + input string + expected string + }{ + // Known FCOS symlinks — must be rewritten + {"/home/alice", "/var/home/alice"}, + {"/home/alice/projects", "/var/home/alice/projects"}, + {"/mnt/data", "/var/mnt/data"}, + {"/opt/myapp", "/var/opt/myapp"}, + {"/root", "/var/roothome"}, + {"/root/.config", "/var/roothome/.config"}, + {"/srv/www", "/var/srv/www"}, + // Exact match on a symlinked dir itself + {"/home", "/var/home"}, + {"/mnt", "/var/mnt"}, + // Paths that do NOT start with a known symlink — unchanged + {"/var/home/alice", "/var/home/alice"}, + {"/tmp/foo", "/tmp/foo"}, + {"/data/work", "/data/work"}, + {"/work", "/work"}, + // Prefix collision guard: /homes should NOT match /home + {"/homes/alice", "/homes/alice"}, + {"/rootfs", "/rootfs"}, + } + + for _, tt := range tests { + got := canonicalizeFCOSMountTarget(tt.input) + assert.Equal(t, tt.expected, got, "input: %q", tt.input) + } +} + +func TestGenerateSystemDFilesForVirtiofsmountsCanonicalPath(t *testing.T) { + mounts := []VirtIoFs{ + NewVirtIoFsMount("/home/alice", "/home/alice", false), + NewVirtIoFsMount("/data", "/data", false), + } + + units, err := GenerateSystemDFilesForVirtiofsMounts(mounts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // First two units are the mount units; the rest are immutable-root helpers. + mountUnits := units[:2] + + cases := []struct { + wantName string + wantWhere string + }{ + // /home/alice must be rewritten to /var/home/alice + {"var-home-alice.mount", "/var/home/alice"}, + // /data has no FCOS symlink — stays as-is + {"data.mount", "/data"}, + } + + for i, c := range cases { + u := mountUnits[i] + assert.Equal(t, c.wantName, u.Name, "unit[%d].Name", i) + if assert.NotNil(t, u.Contents, "unit[%d].Contents", i) { + assert.Contains(t, *u.Contents, "Where="+c.wantWhere, "unit[%d] missing Where=", i) + } + } +} diff --git a/test/e2e/common_test.go b/test/e2e/common_test.go index 99e32382bc..2475958580 100644 --- a/test/e2e/common_test.go +++ b/test/e2e/common_test.go @@ -69,7 +69,7 @@ type PodmanTestIntegration struct { SignaturePolicyPath string CgroupManager string Host HostOS - TmpDir string + CliTmpDir string // value of podman --tmpdir } var ( @@ -363,11 +363,15 @@ func PodmanTestCreateUtil(tempDir string, target PodmanTestCreateUtilTarget) *Po } } + perTestTempDir := filepath.Join(tempDir, "ptemp") + err := os.Mkdir(perTestTempDir, 0o755) + Expect(err).ToNot(HaveOccurred()) + p := &PodmanTestIntegration{ PodmanTest: PodmanTest{ PodmanBinary: podmanBinary, RemotePodmanBinary: podmanRemoteBinary, - TempDir: tempDir, + TempDir: perTestTempDir, RemoteTest: target != PodmanTestCreateUtilTargetLocal, ImageCacheFS: storageFs, ImageCacheDir: ImageCacheDir, @@ -376,7 +380,7 @@ func PodmanTestCreateUtil(tempDir string, target PodmanTestCreateUtilTarget) *Po ConmonBinary: conmonBinary, QuadletBinary: quadletBinary, Root: root, - TmpDir: tempDir, + CliTmpDir: filepath.Join(tempDir, "clitmp"), NetworkConfigDir: networkConfigDir, OCIRuntime: ociRuntime, RunRoot: filepath.Join(tempDir, "runroot"), @@ -1417,7 +1421,7 @@ func (p *PodmanTestIntegration) makeOptions(args []string, options PodmanExecOpt "--conmon", p.ConmonBinary, "--network-config-dir", p.NetworkConfigDir, "--cgroup-manager", p.CgroupManager, - "--tmpdir", p.TmpDir, + "--tmpdir", p.CliTmpDir, "--events-backend", eventsType, ) @@ -1547,11 +1551,8 @@ func (s *PodmanSessionIntegration) jq(jqCommand string) (string, error) { } func (p *PodmanTestIntegration) buildImage(dockerfile, imageName string, layers string, label string, extraOptions []string) string { - buildDir := filepath.Join(p.TempDir, "build"+stringid.GenerateRandomID()) - err := os.Mkdir(buildDir, 0o755) - Expect(err).ToNot(HaveOccurred()) - dockerfilePath := filepath.Join(buildDir, "Dockerfile-"+stringid.GenerateRandomID()) - err = os.WriteFile(dockerfilePath, []byte(dockerfile), 0o644) + dockerfilePath := filepath.Join(p.TempDir, "Dockerfile-"+stringid.GenerateRandomID()) + err := os.WriteFile(dockerfilePath, []byte(dockerfile), 0o755) Expect(err).ToNot(HaveOccurred()) cmd := []string{"build", "--pull-never", "--layers=" + layers, "--file", dockerfilePath} if label != "" { @@ -1563,7 +1564,7 @@ func (p *PodmanTestIntegration) buildImage(dockerfile, imageName string, layers if len(extraOptions) > 0 { cmd = append(cmd, extraOptions...) } - cmd = append(cmd, buildDir) + 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())) diff --git a/test/e2e/container_create_volume_test.go b/test/e2e/container_create_volume_test.go index feaa083965..45fc033f1f 100644 --- a/test/e2e/container_create_volume_test.go +++ b/test/e2e/container_create_volume_test.go @@ -10,17 +10,12 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "go.podman.io/podman/v6/test/utils" - "go.podman.io/storage/pkg/stringid" ) func buildDataVolumeImage(pTest *PodmanTestIntegration, image, data, dest string) { - buildDir := filepath.Join(pTest.TempDir, "build"+stringid.GenerateRandomID()) - err := os.Mkdir(buildDir, 0o755) - Expect(err).ToNot(HaveOccurred()) - // Create a dummy file for data volume - dummyFile := filepath.Join(buildDir, data) - err = os.WriteFile(dummyFile, []byte(data), 0o644) + dummyFile := filepath.Join(pTest.TempDir, data) + err := os.WriteFile(dummyFile, []byte(data), 0o644) Expect(err).ToNot(HaveOccurred()) // Create a data volume container image but no CMD binary in it @@ -28,11 +23,7 @@ func buildDataVolumeImage(pTest *PodmanTestIntegration, image, data, dest string CMD doesnotexist.sh ADD %s %s/ VOLUME %s/`, data, dest, dest) - - containerFilePath := filepath.Join(buildDir, "Containerfile") - err = os.WriteFile(containerFilePath, []byte(containerFile), 0o644) - Expect(err).ToNot(HaveOccurred()) - pTest.PodmanExitCleanly("build", "--pull-never", "-q", "-t", image, "--layers=false", "--file", containerFilePath) + pTest.BuildImage(containerFile, image, "false") } func createContainersConfFile(pTest *PodmanTestIntegration) { diff --git a/test/e2e/libpod_suite_remote_test.go b/test/e2e/libpod_suite_remote_test.go index 70ce8bbc8f..107ab00df6 100644 --- a/test/e2e/libpod_suite_remote_test.go +++ b/test/e2e/libpod_suite_remote_test.go @@ -133,7 +133,7 @@ func getRemoteOptions(p *PodmanTestIntegration, args []string) []string { "--conmon", p.ConmonBinary, "--network-config-dir", networkDir, "--cgroup-manager", p.CgroupManager, - "--tmpdir", p.TmpDir, + "--tmpdir", p.CliTmpDir, "--events-backend", "file", } diff --git a/test/e2e/mount_rootless_test.go b/test/e2e/mount_rootless_test.go index 46b096965d..ad3d421356 100644 --- a/test/e2e/mount_rootless_test.go +++ b/test/e2e/mount_rootless_test.go @@ -38,12 +38,12 @@ var _ = Describe("Podman mount", func() { opts := podmanTest.PodmanMakeOptions([]string{"mount", cid}, PodmanExecOptions{}) args = append(args, opts...) - // container root file system location is podmanTest.TempDir/... - // because "--root podmanTest.TempDir/..." + // container root file system location is podmanTest.Root/... + // because "--root podmanTest.Root/..." session := podmanTest.Podman(args) session.WaitWithDefaultTimeout() Expect(session).Should(ExitCleanly()) - Expect(session.OutputToString()).To(ContainSubstring(podmanTest.TempDir)) + Expect(session.OutputToString()).To(ContainSubstring(podmanTest.Root)) }) It("podman image mount", func() { @@ -61,11 +61,11 @@ var _ = Describe("Podman mount", func() { opts := podmanTest.PodmanMakeOptions([]string{"image", "mount", CITEST_IMAGE}, PodmanExecOptions{}) args = append(args, opts...) - // image location is podmanTest.TempDir/... because "--root podmanTest.TempDir/..." + // image location is podmanTest.Root/... because "--root podmanTest.Root/..." session := podmanTest.Podman(args) session.WaitWithDefaultTimeout() Expect(session).Should(ExitCleanly()) - Expect(session.OutputToString()).To(ContainSubstring(podmanTest.TempDir)) + Expect(session.OutputToString()).To(ContainSubstring(podmanTest.Root)) // We have to unmount the image again otherwise we leak the tmpdir // as active mount points cannot be removed. diff --git a/test/e2e/play_kube_test.go b/test/e2e/play_kube_test.go index 92deee3174..ecda611ee9 100644 --- a/test/e2e/play_kube_test.go +++ b/test/e2e/play_kube_test.go @@ -5694,7 +5694,7 @@ spec: playKube := podmanTest.Podman([]string{"kube", "play", kubeYaml}) playKube.WaitWithDefaultTimeout() - Expect(playKube).Should(ExitWithError(125, fmt.Sprintf("securejoin.OpenInRoot testing/onlythis: openat2 %s/root/volumes/testvol/_data/testing/onlythis: no such file or directory", podmanTest.TempDir))) + Expect(playKube).Should(ExitWithError(125, fmt.Sprintf("securejoin.OpenInRoot testing/onlythis: openat2 %s/volumes/testvol/_data/testing/onlythis: no such file or directory", podmanTest.Root))) }) It("with unsafe hostPath subpaths", func() { diff --git a/test/e2e/pod_create_test.go b/test/e2e/pod_create_test.go index a7e3a72d71..9f74b16735 100644 --- a/test/e2e/pod_create_test.go +++ b/test/e2e/pod_create_test.go @@ -20,7 +20,6 @@ import ( "go.podman.io/common/pkg/sysinfo" "go.podman.io/podman/v6/pkg/util" . "go.podman.io/podman/v6/test/utils" - "go.podman.io/storage/pkg/stringid" ) var _ = Describe("Podman pod create", func() { @@ -176,12 +175,8 @@ var _ = Describe("Podman pod create", func() { Describe("podman create pod with --hosts-file", func() { BeforeEach(func() { - buildDir := filepath.Join(podmanTest.TempDir, "build"+stringid.GenerateRandomID()) - err := os.Mkdir(buildDir, 0o755) - Expect(err).ToNot(HaveOccurred()) - - imageHosts := filepath.Join(buildDir, "pause_hosts") - err = os.WriteFile(imageHosts, []byte("56.78.12.34 image.example.com"), 0o755) + imageHosts := filepath.Join(podmanTest.TempDir, "pause_hosts") + err := os.WriteFile(imageHosts, []byte("56.78.12.34 image.example.com"), 0o755) Expect(err).ToNot(HaveOccurred()) configHosts := filepath.Join(podmanTest.TempDir, "hosts") @@ -196,16 +191,11 @@ var _ = Describe("Podman pod create", func() { podmanTest.RestartRemoteService() } - containerfile := strings.Join([]string{ + dockerfile := strings.Join([]string{ `FROM ` + INFRA_IMAGE, `COPY pause_hosts /etc/hosts`, }, "\n") - - containerFilePath := filepath.Join(buildDir, "Containerfile") - err = os.WriteFile(containerFilePath, []byte(containerfile), 0o644) - Expect(err).ToNot(HaveOccurred()) - - podmanTest.PodmanExitCleanly("build", "-q", "-t", "foobar.com/hosts_test_pause:latest", "--layers=false", "--no-hosts", buildDir) + podmanTest.BuildImage(dockerfile, "foobar.com/hosts_test_pause:latest", "false", "--no-hosts") }) It("--hosts-file=path", func() { diff --git a/test/e2e/pull_chunked_test.go b/test/e2e/pull_chunked_test.go index 5723f32c1d..3be2573eb0 100644 --- a/test/e2e/pull_chunked_test.go +++ b/test/e2e/pull_chunked_test.go @@ -69,18 +69,11 @@ func pullChunkedTests() { // included in pull_test.go, must use a Ginkgo DSL at registryRef: pullChunkedRegistryPrefix + "chunked-normal", dirPath: filepath.Join(imageDir, "chunked-normal"), } - - buildDir := filepath.Join(podmanTest.TempDir, "build") - err := os.Mkdir(buildDir, 0o755) - Expect(err).ToNot(HaveOccurred()) chunkedNormalContentPath := "chunked-normal-image-content" - err = os.WriteFile(filepath.Join(buildDir, chunkedNormalContentPath), fmt.Appendf(nil, "content-%d", rand.Int64()), 0o600) + err := os.WriteFile(filepath.Join(podmanTest.TempDir, chunkedNormalContentPath), fmt.Appendf(nil, "content-%d", rand.Int64()), 0o600) Expect(err).NotTo(HaveOccurred()) chunkedNormalContainerFile := fmt.Sprintf("FROM scratch\nADD %s /content", chunkedNormalContentPath) - err = os.WriteFile(filepath.Join(buildDir, "Containerfile"), []byte(chunkedNormalContainerFile), 0o600) - Expect(err).NotTo(HaveOccurred()) - podmanTest.PodmanExitCleanly("build", "-q", "-t", chunkedNormal.localTag(), "--layers=true", buildDir) - + podmanTest.BuildImage(chunkedNormalContainerFile, chunkedNormal.localTag(), "true") podmanTest.PodmanExitCleanly("push", "-q", "--tls-verify=false", "--force-compression", "--compression-format=zstd:chunked", chunkedNormal.localTag(), chunkedNormal.registryRef) skopeo := SystemExec("skopeo", []string{"copy", "-q", "--preserve-digests", "--all", "--src-tls-verify=false", chunkedNormal.registryRef, "dir:" + chunkedNormal.dirPath}) skopeo.WaitWithDefaultTimeout() diff --git a/test/e2e/run_volume_test.go b/test/e2e/run_volume_test.go index 2a0465e825..eab77368ae 100644 --- a/test/e2e/run_volume_test.go +++ b/test/e2e/run_volume_test.go @@ -135,7 +135,7 @@ var _ = Describe("Podman run with volumes", func() { }) It("podman run with conflicting volumes errors", func() { - mountPath := filepath.Join(podmanTest.TmpDir, "secrets") + mountPath := filepath.Join(podmanTest.TempDir, "secrets") err := os.Mkdir(mountPath, 0o755) Expect(err).ToNot(HaveOccurred()) session := podmanTest.Podman([]string{"run", "-v", mountPath + ":" + dest, "-v", "/tmp" + ":" + dest, ALPINE, "ls"}) diff --git a/test/system/272-system-connection.bats b/test/system/272-system-connection.bats index e91e9d83ef..a68669493e 100644 --- a/test/system/272-system-connection.bats +++ b/test/system/272-system-connection.bats @@ -144,7 +144,7 @@ $c2[ ]\+tcp://localhost:54321[ ]\+true[ ]\+true" \ # Stop server. Use 'run' to avoid failing on nonzero exit status run kill $_SERVICE_PID - run wait $_SERVICE_PID + wait $_SERVICE_PID || true _SERVICE_PID= run_podman system connection rm fakeconnect @@ -205,7 +205,7 @@ $c2[ ]\+tcp://localhost:54321[ ]\+true[ ]\+true" \ # Stop server. Use 'run' to avoid failing on nonzero exit status run kill $_SERVICE_PID - run wait $_SERVICE_PID + wait $_SERVICE_PID || true _SERVICE_PID= run_podman system connection rm fakeconnect @@ -270,7 +270,7 @@ $c2[ ]\+tcp://localhost:54321[ ]\+true[ ]\+true" \ # Stop server. Use 'run' to avoid failing on nonzero exit status run kill $_SERVICE_PID - run wait $_SERVICE_PID + wait $_SERVICE_PID || true _SERVICE_PID= run_podman system connection rm fakeconnect diff --git a/test/utils/utils.go b/test/utils/utils.go index 5fb0be861a..edc46755f3 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -67,7 +67,7 @@ type PodmanTest struct { RemoteTLSClientKeyFile string RemoteTLSDetails string RemoteTest bool - TempDir string + TempDir string // TempDir is a unique per test directory. } // PodmanSession wraps the gexec.session so we can extend it