diff --git a/buildroot b/buildroot index 32c57f4c..add67823 160000 --- a/buildroot +++ b/buildroot @@ -1 +1 @@ -Subproject commit 32c57f4cb8a1a4a59f811577f53efdaaf03875ce +Subproject commit add67823a7bea007732d60a2a79bf6b2a4e3cdc9 diff --git a/buildroot-external/patches/podman/0001-quadlet-support-systemd-style-dropin-files.patch b/buildroot-external/patches/podman/0001-quadlet-support-systemd-style-dropin-files.patch new file mode 100644 index 00000000..5ff4d9f6 --- /dev/null +++ b/buildroot-external/patches/podman/0001-quadlet-support-systemd-style-dropin-files.patch @@ -0,0 +1,252 @@ +From 8ee26220281bc5bbb6a2b72ab0cf5a2f120b1afa Mon Sep 17 00:00:00 2001 +From: Alexander Larsson +Date: Wed, 29 Nov 2023 10:57:42 +0100 +Subject: [PATCH] quadlet: Support systemd style dropin files + +For a source file like `foo.container`, look for drop in named +`foo.container.d/*.conf` and merged them into the main file. The +dropins are applied in alphabetical order, and files in earlier +diretories override later files with same name. + +This is similar to how systemd dropins work, see: +https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html + +Also adds some tests for these + +Signed-off-by: Alexander Larsson +--- + cmd/quadlet/main.go | 67 +++++++++++++++++++ + docs/source/markdown/podman-systemd.unit.5.md | 7 ++ + pkg/systemd/parser/unitfile.go | 4 +- + test/e2e/quadlet/merged-override.container | 8 +++ + .../merged-override.container.d/10-first.conf | 2 + + .../20-second.conf | 4 ++ + test/e2e/quadlet/merged.container | 8 +++ + .../quadlet/merged.container.d/10-first.conf | 2 + + .../quadlet/merged.container.d/20-second.conf | 2 + + test/e2e/quadlet_test.go | 12 ++++ + 10 files changed, 114 insertions(+), 2 deletions(-) + create mode 100644 test/e2e/quadlet/merged-override.container + create mode 100644 test/e2e/quadlet/merged-override.container.d/10-first.conf + create mode 100644 test/e2e/quadlet/merged-override.container.d/20-second.conf + create mode 100644 test/e2e/quadlet/merged.container + create mode 100644 test/e2e/quadlet/merged.container.d/10-first.conf + create mode 100644 test/e2e/quadlet/merged.container.d/20-second.conf + +diff --git a/cmd/quadlet/main.go b/cmd/quadlet/main.go +index b36997b32a73..9c032427b0a4 100644 +--- a/cmd/quadlet/main.go ++++ b/cmd/quadlet/main.go +@@ -242,6 +242,67 @@ func loadUnitsFromDir(sourcePath string) ([]*parser.UnitFile, error) { + return units, prevError + } + ++func loadUnitDropins(unit *parser.UnitFile, sourcePaths []string) error { ++ var prevError error ++ reportError := func(err error) { ++ if prevError != nil { ++ err = fmt.Errorf("%s\n%s", prevError, err) ++ } ++ prevError = err ++ } ++ ++ var dropinPaths = make(map[string]string) ++ for _, sourcePath := range sourcePaths { ++ dropinDir := path.Join(sourcePath, unit.Filename+".d") ++ ++ dropinFiles, err := os.ReadDir(dropinDir) ++ if err != nil { ++ if !errors.Is(err, os.ErrNotExist) { ++ reportError(fmt.Errorf("error reading directory %q, %w", dropinDir, err)) ++ } ++ ++ continue ++ } ++ ++ for _, dropinFile := range dropinFiles { ++ dropinName := dropinFile.Name() ++ if filepath.Ext(dropinName) != ".conf" { ++ continue // Only *.conf supported ++ } ++ ++ if _, ok := dropinPaths[dropinName]; ok { ++ continue // We already saw this name ++ } ++ ++ dropinPaths[dropinName] = path.Join(dropinDir, dropinName) ++ } ++ } ++ ++ dropinFiles := make([]string, len(dropinPaths)) ++ i := 0 ++ for k := range dropinPaths { ++ dropinFiles[i] = k ++ i++ ++ } ++ ++ // Merge in alpha-numerical order ++ sort.Strings(dropinFiles) ++ ++ for _, dropinFile := range dropinFiles { ++ dropinPath := dropinPaths[dropinFile] ++ ++ Debugf("Loading source drop-in file %s", dropinPath) ++ ++ if f, err := parser.ParseUnitFile(dropinPath); err != nil { ++ reportError(fmt.Errorf("error loading %q, %w", dropinPath, err)) ++ } else { ++ unit.Merge(f) ++ } ++ } ++ ++ return prevError ++} ++ + func generateServiceFile(service *parser.UnitFile) error { + Debugf("writing %q", service.Path) + +@@ -456,6 +517,12 @@ func process() error { + return prevError + } + ++ for _, unit := range units { ++ if err := loadUnitDropins(unit, sourcePaths); err != nil { ++ reportError(err) ++ } ++ } ++ + if !dryRunFlag { + err := os.MkdirAll(outputPath, os.ModePerm) + if err != nil { +diff --git a/docs/source/markdown/podman-systemd.unit.5.md b/docs/source/markdown/podman-systemd.unit.5.md +index 8101338a56b6..4969a84aca7b 100644 +--- a/docs/source/markdown/podman-systemd.unit.5.md ++++ b/docs/source/markdown/podman-systemd.unit.5.md +@@ -47,6 +47,13 @@ Each file type has a custom section (for example, `[Container]`) that is handled + other sections are passed on untouched, allowing the use of any normal systemd configuration options + like dependencies or cgroup limits. + ++The source files also support drop-ins in the same [way systemd does](https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html). ++For a given source file (say `foo.container`), the corresponding `.d`directory (in this ++case `foo.container.d`) will be scanned for files with a `.conf` extension that are merged into ++the base file in alphabetical order. The format of these drop-in files is the same as the base file. ++This is useful to alter or add configuration settings for a unit, without having to modify unit ++files. ++ + For rootless containers, when administrators place Quadlet files in the + /etc/containers/systemd/users directory, all users' sessions execute the + Quadlet when the login session begins. If the administrator places a Quadlet +diff --git a/pkg/systemd/parser/unitfile.go b/pkg/systemd/parser/unitfile.go +index 963909f9d876..732daa2be4fb 100644 +--- a/pkg/systemd/parser/unitfile.go ++++ b/pkg/systemd/parser/unitfile.go +@@ -182,7 +182,7 @@ func (f *UnitFile) ensureGroup(groupName string) *unitGroup { + return g + } + +-func (f *UnitFile) merge(source *UnitFile) { ++func (f *UnitFile) Merge(source *UnitFile) { + for _, srcGroup := range source.groups { + group := f.ensureGroup(srcGroup.name) + group.merge(srcGroup) +@@ -193,7 +193,7 @@ func (f *UnitFile) merge(source *UnitFile) { + func (f *UnitFile) Dup() *UnitFile { + copy := NewUnitFile() + +- copy.merge(f) ++ copy.Merge(f) + copy.Filename = f.Filename + return copy + } +diff --git a/test/e2e/quadlet/merged-override.container b/test/e2e/quadlet/merged-override.container +new file mode 100644 +index 000000000000..d93a53b340a9 +--- /dev/null ++++ b/test/e2e/quadlet/merged-override.container +@@ -0,0 +1,8 @@ ++## assert-podman-final-args localhost/imagename ++## !assert-podman-args --env "MAIN=mainvalue" ++## !assert-podman-args --env "FIRST=value" ++## assert-podman-args --env "SECOND=othervalue" ++ ++[Container] ++Image=localhost/imagename ++Environment=MAIN=mainvalue +diff --git a/test/e2e/quadlet/merged-override.container.d/10-first.conf b/test/e2e/quadlet/merged-override.container.d/10-first.conf +new file mode 100644 +index 000000000000..f6164d631e05 +--- /dev/null ++++ b/test/e2e/quadlet/merged-override.container.d/10-first.conf +@@ -0,0 +1,2 @@ ++[Container] ++Environment=FIRST=value +diff --git a/test/e2e/quadlet/merged-override.container.d/20-second.conf b/test/e2e/quadlet/merged-override.container.d/20-second.conf +new file mode 100644 +index 000000000000..5bfcdd44dcc8 +--- /dev/null ++++ b/test/e2e/quadlet/merged-override.container.d/20-second.conf +@@ -0,0 +1,4 @@ ++[Container] ++# Empty previous ++Environment= ++Environment=SECOND=othervalue +diff --git a/test/e2e/quadlet/merged.container b/test/e2e/quadlet/merged.container +new file mode 100644 +index 000000000000..3d19987fd0ca +--- /dev/null ++++ b/test/e2e/quadlet/merged.container +@@ -0,0 +1,8 @@ ++## assert-podman-final-args localhost/imagename ++## assert-podman-args --env "MAIN=mainvalue" ++## assert-podman-args --env "FIRST=value" ++## assert-podman-args --env "SECOND=othervalue" ++ ++[Container] ++Image=localhost/imagename ++Environment=MAIN=mainvalue +diff --git a/test/e2e/quadlet/merged.container.d/10-first.conf b/test/e2e/quadlet/merged.container.d/10-first.conf +new file mode 100644 +index 000000000000..f6164d631e05 +--- /dev/null ++++ b/test/e2e/quadlet/merged.container.d/10-first.conf +@@ -0,0 +1,2 @@ ++[Container] ++Environment=FIRST=value +diff --git a/test/e2e/quadlet/merged.container.d/20-second.conf b/test/e2e/quadlet/merged.container.d/20-second.conf +new file mode 100644 +index 000000000000..f1dcaa61fc93 +--- /dev/null ++++ b/test/e2e/quadlet/merged.container.d/20-second.conf +@@ -0,0 +1,2 @@ ++[Container] ++Environment=SECOND=othervalue +diff --git a/test/e2e/quadlet_test.go b/test/e2e/quadlet_test.go +index ad3061f4cdd2..c9c43d284971 100644 +--- a/test/e2e/quadlet_test.go ++++ b/test/e2e/quadlet_test.go +@@ -664,6 +664,16 @@ BOGUS=foo + err = os.WriteFile(filepath.Join(quadletDir, fileName), testcase.data, 0644) + Expect(err).ToNot(HaveOccurred()) + ++ // Also copy any extra snippets ++ dotdDir := filepath.Join("quadlet", fileName+".d") ++ if s, err := os.Stat(dotdDir); err == nil && s.IsDir() { ++ dotdDirDest := filepath.Join(quadletDir, fileName+".d") ++ err = os.Mkdir(dotdDirDest, os.ModePerm) ++ Expect(err).ToNot(HaveOccurred()) ++ err = CopyDirectory(dotdDir, dotdDirDest) ++ Expect(err).ToNot(HaveOccurred()) ++ } ++ + // Run quadlet to convert the file + session := podmanTest.Quadlet([]string{"--user", "--no-kmsg-log", generatedDir}, quadletDir) + session.WaitWithDefaultTimeout() +@@ -748,6 +758,8 @@ BOGUS=foo + Entry("workingdir.container", "workingdir.container", 0, ""), + Entry("Container - global args", "globalargs.container", 0, ""), + Entry("Container - Containers Conf Modules", "containersconfmodule.container", 0, ""), ++ Entry("merged.container", "merged.container", 0, ""), ++ Entry("merged-override.container", "merged-override.container", 0, ""), + + Entry("basic.volume", "basic.volume", 0, ""), + Entry("device-copy.volume", "device-copy.volume", 0, ""), diff --git a/buildroot-external/patches/podman/0002-quadlet-support-healthy-for-notify.patch b/buildroot-external/patches/podman/0002-quadlet-support-healthy-for-notify.patch new file mode 100644 index 00000000..26805af2 --- /dev/null +++ b/buildroot-external/patches/podman/0002-quadlet-support-healthy-for-notify.patch @@ -0,0 +1,78 @@ +From 6cb2f9b1225ade1248ed954e5e03fea9ff279730 Mon Sep 17 00:00:00 2001 +From: Alex Palaistras +Date: Sat, 18 Nov 2023 21:37:00 +0000 +Subject: [PATCH] quadlet: Support `healthy` for `Notify` directives + +This expands support for the (previously) boolean `Notify` directive, in +support of healthcheck determined SD-NOTIFY event emission, as +supported by Podman with the `--sdnotify=healthy` option. + +Closes: #18189 +Signed-off-by: Alex Palaistras +--- + docs/source/markdown/podman-systemd.unit.5.md | 4 ++++ + pkg/systemd/quadlet/quadlet.go | 9 ++++++--- + test/e2e/quadlet/notify-healthy.container | 5 +++++ + test/e2e/quadlet_test.go | 1 + + 4 files changed, 16 insertions(+), 3 deletions(-) + create mode 100644 test/e2e/quadlet/notify-healthy.container + +diff --git a/docs/source/markdown/podman-systemd.unit.5.md b/docs/source/markdown/podman-systemd.unit.5.md +index 79659ded24fa..01ca6293a0f7 100644 +--- a/docs/source/markdown/podman-systemd.unit.5.md ++++ b/docs/source/markdown/podman-systemd.unit.5.md +@@ -496,6 +496,10 @@ starts the child in the container. However, if the container application support + `Notify` to true passes the notification details to the container allowing it to notify + of startup on its own. + ++In addition, setting `Notify` to `healthy` will postpone startup notifications until such time as ++the container is marked healthy, as determined by Podman healthchecks. Note that this requires ++setting up a container healthcheck, see the `HealthCmd` option for more. ++ + ### `PidsLimit=` + + Tune the container's pids limit. +diff --git a/pkg/systemd/quadlet/quadlet.go b/pkg/systemd/quadlet/quadlet.go +index 26e1745b1e98..3328087900d6 100644 +--- a/pkg/systemd/quadlet/quadlet.go ++++ b/pkg/systemd/quadlet/quadlet.go +@@ -496,10 +496,13 @@ func ConvertContainer(container *parser.UnitFile, names map[string]string, isUse + if serviceType != "oneshot" { + // If we're not in oneshot mode always use some form of sd-notify, normally via conmon, + // but we also allow passing it to the container by setting Notify=yes +- notify := container.LookupBooleanWithDefault(ContainerGroup, KeyNotify, false) +- if notify { ++ notify, ok := container.Lookup(ContainerGroup, KeyNotify) ++ switch { ++ case ok && strings.EqualFold(notify, "healthy"): ++ podman.add("--sdnotify=healthy") ++ case container.LookupBooleanWithDefault(ContainerGroup, KeyNotify, false): + podman.add("--sdnotify=container") +- } else { ++ default: + podman.add("--sdnotify=conmon") + } + service.Setv(ServiceGroup, +diff --git a/test/e2e/quadlet/notify-healthy.container b/test/e2e/quadlet/notify-healthy.container +new file mode 100644 +index 000000000000..6dc3d8c09257 +--- /dev/null ++++ b/test/e2e/quadlet/notify-healthy.container +@@ -0,0 +1,5 @@ ++## assert-podman-args "--sdnotify=healthy" ++ ++[Container] ++Image=localhost/imagename ++Notify=healthy +diff --git a/test/e2e/quadlet_test.go b/test/e2e/quadlet_test.go +index 34de88293008..ad3061f4cdd2 100644 +--- a/test/e2e/quadlet_test.go ++++ b/test/e2e/quadlet_test.go +@@ -712,6 +712,7 @@ BOGUS=foo + Entry("network.quadlet.container", "network.quadlet.container", 0, ""), + Entry("noimage.container", "noimage.container", 1, "converting \"noimage.container\": no Image or Rootfs key specified"), + Entry("notify.container", "notify.container", 0, ""), ++ Entry("notify-healthy.container", "notify-healthy.container", 0, ""), + Entry("oneshot.container", "oneshot.container", 0, ""), + Entry("other-sections.container", "other-sections.container", 0, ""), + Entry("podmanargs.container", "podmanargs.container", 0, ""),