Skip to content

Commit

Permalink
quadlet: Support systemd style dropin files
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
alexlarsson committed Nov 29, 2023
1 parent 8387d2d commit 8ee2622
Show file tree
Hide file tree
Showing 10 changed files with 114 additions and 2 deletions.
67 changes: 67 additions & 0 deletions cmd/quadlet/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions docs/source/markdown/podman-systemd.unit.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions pkg/systemd/parser/unitfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
Expand Down
8 changes: 8 additions & 0 deletions test/e2e/quadlet/merged-override.container
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions test/e2e/quadlet/merged-override.container.d/10-first.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[Container]
Environment=FIRST=value
4 changes: 4 additions & 0 deletions test/e2e/quadlet/merged-override.container.d/20-second.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[Container]
# Empty previous
Environment=
Environment=SECOND=othervalue
8 changes: 8 additions & 0 deletions test/e2e/quadlet/merged.container
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions test/e2e/quadlet/merged.container.d/10-first.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[Container]
Environment=FIRST=value
2 changes: 2 additions & 0 deletions test/e2e/quadlet/merged.container.d/20-second.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[Container]
Environment=SECOND=othervalue
12 changes: 12 additions & 0 deletions test/e2e/quadlet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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, ""),
Expand Down

0 comments on commit 8ee2622

Please sign in to comment.