Allow removing implicit quadlet systemd dependencies

Quadlet inserts network-online.target Wants/After dependencies to ensure pulling works.
Those systemd statements cannot be subsequently reset.

In the cases where those dependencies are not wanted, we add a new
configuration item called `DefaultDependencies=` in a new section called
[Quadlet]. This section is shared between different unit types.

fixes #24193

Signed-off-by: Farya L. Maerten <me@ltow.me>
This commit is contained in:
Farya L. M 2024-10-07 20:52:41 +02:00 committed by Farya L. Maerten
parent 6b0ad8269c
commit bac655a6b1
7 changed files with 121 additions and 15 deletions

View File

@ -236,6 +236,14 @@ QUADLET_UNIT_DIRS=<Directory> /usr/lib/systemd/system-generators/podman-system-g
This will instruct Quadlet to look for units in this directory instead of the common ones and by This will instruct Quadlet to look for units in this directory instead of the common ones and by
that limit the output to only the units you are debugging. that limit the output to only the units you are debugging.
### Implicit network dependencies
In the case of Container, Image and Build units, Quadlet will add dependencies on the `network-online.target`
by adding `After=` and `Wants=` properties to the unit. This is to ensure that the network is reachable if
an image needs to be pulled.
This behavior can be disabled by adding `DefaultDependencies=false` in the `Quadlet` section.
## Container units [Container] ## Container units [Container]
Container units are named with a `.container` extension and contain a `[Container]` section describing Container units are named with a `.container` extension and contain a `[Container]` section describing
@ -1914,6 +1922,22 @@ Override the default architecture variant of the container image.
This is equivalent to the Podman `--variant` option. This is equivalent to the Podman `--variant` option.
## Quadlet section [Quadlet]
Some quadlet specific configuration is shared between different unit types. Those settings
can be configured in the `[Quadlet]` section.
Valid options for `[Quadlet]` are listed below:
| **[Quadlet] options** | **Description** |
|----------------------------|---------------------------------------------------|
| DefaultDependencies=false | Disable implicit network dependencies to the unit |
### `DefaultDependencies=`
Add Quadlet's default network dependencies to the unit (default is `true`).
When set to false, Quadlet will **not** add a dependency (After=, Wants=) to `network-online.target` to the generated unit.
## EXAMPLES ## EXAMPLES
Example `test.container`: Example `test.container`:

View File

@ -171,7 +171,7 @@ sub crossref_doc {
chomp $line; chomp $line;
# New section, with its own '| table |' and '### Keyword blocks' # New section, with its own '| table |' and '### Keyword blocks'
if ($line =~ /^##\s+(\S+)\s+units\s+\[(\S+)\]/) { if ($line =~ /^##\s+(\S+)\s+(?:units|section)\s+\[(\S+)\]/) {
my $new_unit = $1; my $new_unit = $1;
$new_unit eq $2 $new_unit eq $2
or warn "$ME: $path:$.: inconsistent block names in '$line'\n"; or warn "$ME: $path:$.: inconsistent block names in '$line'\n";
@ -227,7 +227,7 @@ sub crossref_doc {
} }
grep { $_ eq $key } @found_in_table grep { $_ eq $key } @found_in_table
or warn "$ME: $path:$.: key '$key' is not listed in table for unit '$unit'\n"; or warn "$ME: $path:$.: key '$key' is not listed in table for unit/section '$unit'\n";
push @described, $key; push @described, $key;
$documented{$key}++; $documented{$key}++;

View File

@ -38,6 +38,7 @@ const (
VolumeGroup = "Volume" VolumeGroup = "Volume"
ImageGroup = "Image" ImageGroup = "Image"
BuildGroup = "Build" BuildGroup = "Build"
QuadletGroup = "Quadlet"
XContainerGroup = "X-Container" XContainerGroup = "X-Container"
XKubeGroup = "X-Kube" XKubeGroup = "X-Kube"
XNetworkGroup = "X-Network" XNetworkGroup = "X-Network"
@ -45,6 +46,7 @@ const (
XVolumeGroup = "X-Volume" XVolumeGroup = "X-Volume"
XImageGroup = "X-Image" XImageGroup = "X-Image"
XBuildGroup = "X-Build" XBuildGroup = "X-Build"
XQuadletGroup = "X-Quadlet"
) )
// Systemd Unit file keys // Systemd Unit file keys
@ -70,6 +72,7 @@ const (
KeyCopy = "Copy" KeyCopy = "Copy"
KeyCreds = "Creds" KeyCreds = "Creds"
KeyDecryptionKey = "DecryptionKey" KeyDecryptionKey = "DecryptionKey"
KeyDefaultDependencies = "DefaultDependencies"
KeyDevice = "Device" KeyDevice = "Device"
KeyDisableDNS = "DisableDNS" KeyDisableDNS = "DisableDNS"
KeyDNS = "DNS" KeyDNS = "DNS"
@ -414,6 +417,11 @@ var (
KeyUserNS: true, KeyUserNS: true,
KeyVolume: true, KeyVolume: true,
} }
// Supported keys in "Quadlet" group
supportedQuadletKeys = map[string]bool{
KeyDefaultDependencies: true,
}
) )
func (u *UnitInfo) ServiceFileName() string { func (u *UnitInfo) ServiceFileName() string {
@ -439,16 +447,26 @@ func isPortRange(port string) bool {
return validPortRange.MatchString(port) return validPortRange.MatchString(port)
} }
func checkForUnknownKeys(unit *parser.UnitFile, groupName string, supportedKeys map[string]bool) error { func checkForUnknownKeysInSpecificGroup(unit *parser.UnitFile, groupName string, supportedKeys map[string]bool) error {
keys := unit.ListKeys(groupName) keys := unit.ListKeys(groupName)
for _, key := range keys { for _, key := range keys {
if !supportedKeys[key] { if !supportedKeys[key] {
return fmt.Errorf("unsupported key '%s' in group '%s' in %s", key, groupName, unit.Path) return fmt.Errorf("unsupported key '%s' in group '%s' in %s", key, groupName, unit.Path)
} }
} }
return nil return nil
} }
func checkForUnknownKeys(unit *parser.UnitFile, groupName string, supportedKeys map[string]bool) error {
err := checkForUnknownKeysInSpecificGroup(unit, groupName, supportedKeys)
if err == nil {
return checkForUnknownKeysInSpecificGroup(unit, QuadletGroup, supportedQuadletKeys)
}
return err
}
func splitPorts(ports string) []string { func splitPorts(ports string) []string {
parts := make([]string, 0) parts := make([]string, 0)
@ -509,10 +527,10 @@ func ConvertContainer(container *parser.UnitFile, isUser bool, unitsInfoMap map[
// Add a dependency on network-online.target so the image pull does not happen // Add a dependency on network-online.target so the image pull does not happen
// before network is ready // before network is ready
// https://github.com/containers/podman/issues/21873 // https://github.com/containers/podman/issues/21873
// Prepend the lines, so the user-provided values if service.LookupBooleanWithDefault(QuadletGroup, KeyDefaultDependencies, true) {
// override the default ones.
service.PrependUnitLine(UnitGroup, "After", "network-online.target") service.PrependUnitLine(UnitGroup, "After", "network-online.target")
service.PrependUnitLine(UnitGroup, "Wants", "network-online.target") service.PrependUnitLine(UnitGroup, "Wants", "network-online.target")
}
if container.Path != "" { if container.Path != "" {
service.Add(UnitGroup, "SourcePath", container.Path) service.Add(UnitGroup, "SourcePath", container.Path)
@ -525,6 +543,9 @@ func ConvertContainer(container *parser.UnitFile, isUser bool, unitsInfoMap map[
// Rename old Container group to x-Container so that systemd ignores it // Rename old Container group to x-Container so that systemd ignores it
service.RenameGroup(ContainerGroup, XContainerGroup) service.RenameGroup(ContainerGroup, XContainerGroup)
// Rename common quadlet group
service.RenameGroup(QuadletGroup, XQuadletGroup)
// One image or rootfs must be specified for the container // One image or rootfs must be specified for the container
image, _ := container.Lookup(ContainerGroup, KeyImage) image, _ := container.Lookup(ContainerGroup, KeyImage)
rootfs, _ := container.Lookup(ContainerGroup, KeyRootfs) rootfs, _ := container.Lookup(ContainerGroup, KeyRootfs)
@ -887,6 +908,9 @@ func ConvertNetwork(network *parser.UnitFile, name string, unitsInfoMap map[stri
/* Rename old Network group to x-Network so that systemd ignores it */ /* Rename old Network group to x-Network so that systemd ignores it */
service.RenameGroup(NetworkGroup, XNetworkGroup) service.RenameGroup(NetworkGroup, XNetworkGroup)
// Rename common quadlet group
service.RenameGroup(QuadletGroup, XQuadletGroup)
// Derive network name from unit name (with added prefix), or use user-provided name. // Derive network name from unit name (with added prefix), or use user-provided name.
networkName, ok := network.Lookup(NetworkGroup, KeyNetworkName) networkName, ok := network.Lookup(NetworkGroup, KeyNetworkName)
if !ok || len(networkName) == 0 { if !ok || len(networkName) == 0 {
@ -994,6 +1018,9 @@ func ConvertVolume(volume *parser.UnitFile, name string, unitsInfoMap map[string
/* Rename old Volume group to x-Volume so that systemd ignores it */ /* Rename old Volume group to x-Volume so that systemd ignores it */
service.RenameGroup(VolumeGroup, XVolumeGroup) service.RenameGroup(VolumeGroup, XVolumeGroup)
// Rename common quadlet group
service.RenameGroup(QuadletGroup, XQuadletGroup)
// Derive volume name from unit name (with added prefix), or use user-provided name. // Derive volume name from unit name (with added prefix), or use user-provided name.
volumeName, ok := volume.Lookup(VolumeGroup, KeyVolumeName) volumeName, ok := volume.Lookup(VolumeGroup, KeyVolumeName)
if !ok || len(volumeName) == 0 { if !ok || len(volumeName) == 0 {
@ -1132,6 +1159,9 @@ func ConvertKube(kube *parser.UnitFile, unitsInfoMap map[string]*UnitInfo, isUse
// Rename old Kube group to x-Kube so that systemd ignores it // Rename old Kube group to x-Kube so that systemd ignores it
service.RenameGroup(KubeGroup, XKubeGroup) service.RenameGroup(KubeGroup, XKubeGroup)
// Rename common quadlet group
service.RenameGroup(QuadletGroup, XQuadletGroup)
yamlPath, ok := kube.Lookup(KubeGroup, KeyYaml) yamlPath, ok := kube.Lookup(KubeGroup, KeyYaml)
if !ok || len(yamlPath) == 0 { if !ok || len(yamlPath) == 0 {
return nil, fmt.Errorf("no Yaml key specified") return nil, fmt.Errorf("no Yaml key specified")
@ -1264,10 +1294,10 @@ func ConvertImage(image *parser.UnitFile, unitsInfoMap map[string]*UnitInfo) (*p
// Add a dependency on network-online.target so the image pull does not happen // Add a dependency on network-online.target so the image pull does not happen
// before network is ready // before network is ready
// https://github.com/containers/podman/issues/21873 // https://github.com/containers/podman/issues/21873
// Prepend the lines, so the user-provided values if service.LookupBooleanWithDefault(QuadletGroup, KeyDefaultDependencies, true) {
// override the default ones.
service.PrependUnitLine(UnitGroup, "After", "network-online.target") service.PrependUnitLine(UnitGroup, "After", "network-online.target")
service.PrependUnitLine(UnitGroup, "Wants", "network-online.target") service.PrependUnitLine(UnitGroup, "Wants", "network-online.target")
}
if image.Path != "" { if image.Path != "" {
service.Add(UnitGroup, "SourcePath", image.Path) service.Add(UnitGroup, "SourcePath", image.Path)
@ -1285,6 +1315,9 @@ func ConvertImage(image *parser.UnitFile, unitsInfoMap map[string]*UnitInfo) (*p
/* Rename old Network group to x-Network so that systemd ignores it */ /* Rename old Network group to x-Network so that systemd ignores it */
service.RenameGroup(ImageGroup, XImageGroup) service.RenameGroup(ImageGroup, XImageGroup)
// Rename common quadlet group
service.RenameGroup(QuadletGroup, XQuadletGroup)
// Need the containers filesystem mounted to start podman // Need the containers filesystem mounted to start podman
service.Add(UnitGroup, "RequiresMountsFor", "%t/containers") service.Add(UnitGroup, "RequiresMountsFor", "%t/containers")
@ -1349,14 +1382,17 @@ func ConvertBuild(build *parser.UnitFile, unitsInfoMap map[string]*UnitInfo) (*p
// Add a dependency on network-online.target so the image pull does not happen // Add a dependency on network-online.target so the image pull does not happen
// before network is ready // before network is ready
// https://github.com/containers/podman/issues/21873 // https://github.com/containers/podman/issues/21873
// Prepend the lines, so the user-provided values if service.LookupBooleanWithDefault(QuadletGroup, KeyDefaultDependencies, true) {
// override the default ones.
service.PrependUnitLine(UnitGroup, "After", "network-online.target") service.PrependUnitLine(UnitGroup, "After", "network-online.target")
service.PrependUnitLine(UnitGroup, "Wants", "network-online.target") service.PrependUnitLine(UnitGroup, "Wants", "network-online.target")
}
/* Rename old Build group to X-Build so that systemd ignores it */ /* Rename old Build group to X-Build so that systemd ignores it */
service.RenameGroup(BuildGroup, XBuildGroup) service.RenameGroup(BuildGroup, XBuildGroup)
// Rename common quadlet group
service.RenameGroup(QuadletGroup, XQuadletGroup)
// Need the containers filesystem mounted to start podman // Need the containers filesystem mounted to start podman
service.Add(UnitGroup, "RequiresMountsFor", "%t/containers") service.Add(UnitGroup, "RequiresMountsFor", "%t/containers")
@ -1531,6 +1567,9 @@ func ConvertPod(podUnit *parser.UnitFile, name string, unitsInfoMap map[string]*
/* Rename old Pod group to x-Pod so that systemd ignores it */ /* Rename old Pod group to x-Pod so that systemd ignores it */
service.RenameGroup(PodGroup, XPodGroup) service.RenameGroup(PodGroup, XPodGroup)
// Rename common quadlet group
service.RenameGroup(QuadletGroup, XQuadletGroup)
// Need the containers filesystem mounted to start podman // Need the containers filesystem mounted to start podman
service.Add(UnitGroup, "RequiresMountsFor", "%t/containers") service.Add(UnitGroup, "RequiresMountsFor", "%t/containers")

View File

@ -0,0 +1,11 @@
## assert-key-is-empty "Unit" "Wants"
## assert-key-is-empty "Unit" "After"
## assert-key-is-empty "Unit" "Before"
[Quadlet]
DefaultDependencies=no
[Build]
ImageTag=localhost/imagename
File=Containerfile
SetWorkingDirectory=dir

View File

@ -0,0 +1,9 @@
## assert-key-is-empty "Unit" "Wants"
## assert-key-is-empty "Unit" "After"
## assert-key-is-empty "Unit" "Before"
[Quadlet]
DefaultDependencies=no
[Container]
Image=localhost/imagename

View File

@ -0,0 +1,9 @@
## assert-key-is-empty "Unit" "Wants"
## assert-key-is-empty "Unit" "After"
## assert-key-is-empty "Unit" "Before"
[Quadlet]
DefaultDependencies=no
[Image]
Image=localhost/imagename

View File

@ -170,6 +170,15 @@ func (t *quadletTestcase) assertKeyIs(args []string, unit *parser.UnitFile) bool
return true return true
} }
func (t *quadletTestcase) assertKeyIsEmpty(args []string, unit *parser.UnitFile) bool {
Expect(args).To(HaveLen(2))
group := args[0]
key := args[1]
realValues := unit.LookupAll(group, key)
return len(realValues) == 0
}
func (t *quadletTestcase) assertKeyIsRegex(args []string, unit *parser.UnitFile) bool { func (t *quadletTestcase) assertKeyIsRegex(args []string, unit *parser.UnitFile) bool {
Expect(len(args)).To(BeNumerically(">=", 3)) Expect(len(args)).To(BeNumerically(">=", 3))
group := args[0] group := args[0]
@ -501,6 +510,8 @@ func (t *quadletTestcase) doAssert(check []string, unit *parser.UnitFile, sessio
ok = t.assertStdErrContains(args, session) ok = t.assertStdErrContains(args, session)
case "assert-key-is": case "assert-key-is":
ok = t.assertKeyIs(args, unit) ok = t.assertKeyIs(args, unit)
case "assert-key-is-empty":
ok = t.assertKeyIsEmpty(args, unit)
case "assert-key-is-regex": case "assert-key-is-regex":
ok = t.assertKeyIsRegex(args, unit) ok = t.assertKeyIsRegex(args, unit)
case "assert-key-contains": case "assert-key-contains":
@ -899,6 +910,7 @@ BOGUS=foo
Entry("Unit After Override", "unit-after-override.container"), Entry("Unit After Override", "unit-after-override.container"),
Entry("NetworkAlias", "network-alias.container"), Entry("NetworkAlias", "network-alias.container"),
Entry("CgroupMode", "cgroups-mode.container"), Entry("CgroupMode", "cgroups-mode.container"),
Entry("Container - No Default Dependencies", "no_deps.container"),
Entry("basic.volume", "basic.volume"), Entry("basic.volume", "basic.volume"),
Entry("device-copy.volume", "device-copy.volume"), Entry("device-copy.volume", "device-copy.volume"),
@ -967,6 +979,7 @@ BOGUS=foo
Entry("Image - global args", "globalargs.image"), Entry("Image - global args", "globalargs.image"),
Entry("Image - Containers Conf Modules", "containersconfmodule.image"), Entry("Image - Containers Conf Modules", "containersconfmodule.image"),
Entry("Image - Unit After Override", "unit-after-override.image"), Entry("Image - Unit After Override", "unit-after-override.image"),
Entry("Image - No Default Dependencies", "no_deps.image"),
Entry("Build - Basic", "basic.build"), Entry("Build - Basic", "basic.build"),
Entry("Build - Annotation Key", "annotation.build"), Entry("Build - Annotation Key", "annotation.build"),
@ -1000,6 +1013,7 @@ BOGUS=foo
Entry("Build - Target Key", "target.build"), Entry("Build - Target Key", "target.build"),
Entry("Build - TLSVerify Key", "tls-verify.build"), Entry("Build - TLSVerify Key", "tls-verify.build"),
Entry("Build - Variant Key", "variant.build"), Entry("Build - Variant Key", "variant.build"),
Entry("Build - No Default Dependencies", "no_deps.build"),
Entry("Pod - Basic", "basic.pod"), Entry("Pod - Basic", "basic.pod"),
Entry("Pod - DNS", "dns.pod"), Entry("Pod - DNS", "dns.pod"),