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 <alexl@redhat.com>
This commit is contained in:
Alexander Larsson 2023-11-29 10:57:42 +01:00
parent 8387d2dfaa
commit 8ee2622028
10 changed files with 114 additions and 2 deletions

View File

@ -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 {

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -0,0 +1,2 @@
[Container]
Environment=FIRST=value

View File

@ -0,0 +1,4 @@
[Container]
# Empty previous
Environment=
Environment=SECOND=othervalue

View File

@ -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

View File

@ -0,0 +1,2 @@
[Container]
Environment=FIRST=value

View File

@ -0,0 +1,2 @@
[Container]
Environment=SECOND=othervalue

View File

@ -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, ""),