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 b36997b32a..9c032427b0 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 8101338a56..4969a84aca 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 963909f9d8..732daa2be4 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 0000000000..d93a53b340 --- /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 0000000000..f6164d631e --- /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 0000000000..5bfcdd44dc --- /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 0000000000..3d19987fd0 --- /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 0000000000..f6164d631e --- /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 0000000000..f1dcaa61fc --- /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 ad3061f4cd..c9c43d2849 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, ""),