diff --git a/docs/source/markdown/podman-systemd.unit.5.md b/docs/source/markdown/podman-systemd.unit.5.md index 1413f73010..a204d62004 100644 --- a/docs/source/markdown/podman-systemd.unit.5.md +++ b/docs/source/markdown/podman-systemd.unit.5.md @@ -236,6 +236,14 @@ QUADLET_UNIT_DIRS= /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 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 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. +## 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 Example `test.container`: diff --git a/hack/xref-quadlet-docs b/hack/xref-quadlet-docs index a85d403ff8..022f2c4898 100755 --- a/hack/xref-quadlet-docs +++ b/hack/xref-quadlet-docs @@ -171,7 +171,7 @@ sub crossref_doc { chomp $line; # 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; $new_unit eq $2 or warn "$ME: $path:$.: inconsistent block names in '$line'\n"; @@ -227,7 +227,7 @@ sub crossref_doc { } 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; $documented{$key}++; diff --git a/pkg/systemd/quadlet/quadlet.go b/pkg/systemd/quadlet/quadlet.go index c8709ea85c..4c9ca689df 100644 --- a/pkg/systemd/quadlet/quadlet.go +++ b/pkg/systemd/quadlet/quadlet.go @@ -38,6 +38,7 @@ const ( VolumeGroup = "Volume" ImageGroup = "Image" BuildGroup = "Build" + QuadletGroup = "Quadlet" XContainerGroup = "X-Container" XKubeGroup = "X-Kube" XNetworkGroup = "X-Network" @@ -45,6 +46,7 @@ const ( XVolumeGroup = "X-Volume" XImageGroup = "X-Image" XBuildGroup = "X-Build" + XQuadletGroup = "X-Quadlet" ) // Systemd Unit file keys @@ -70,6 +72,7 @@ const ( KeyCopy = "Copy" KeyCreds = "Creds" KeyDecryptionKey = "DecryptionKey" + KeyDefaultDependencies = "DefaultDependencies" KeyDevice = "Device" KeyDisableDNS = "DisableDNS" KeyDNS = "DNS" @@ -414,6 +417,11 @@ var ( KeyUserNS: true, KeyVolume: true, } + + // Supported keys in "Quadlet" group + supportedQuadletKeys = map[string]bool{ + KeyDefaultDependencies: true, + } ) func (u *UnitInfo) ServiceFileName() string { @@ -439,16 +447,26 @@ func isPortRange(port string) bool { 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) for _, key := range keys { if !supportedKeys[key] { return fmt.Errorf("unsupported key '%s' in group '%s' in %s", key, groupName, unit.Path) } } + 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 { 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 // before network is ready // /~https://github.com/containers/podman/issues/21873 - // Prepend the lines, so the user-provided values - // override the default ones. - service.PrependUnitLine(UnitGroup, "After", "network-online.target") - service.PrependUnitLine(UnitGroup, "Wants", "network-online.target") + if service.LookupBooleanWithDefault(QuadletGroup, KeyDefaultDependencies, true) { + service.PrependUnitLine(UnitGroup, "After", "network-online.target") + service.PrependUnitLine(UnitGroup, "Wants", "network-online.target") + } if 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 service.RenameGroup(ContainerGroup, XContainerGroup) + // Rename common quadlet group + service.RenameGroup(QuadletGroup, XQuadletGroup) + // One image or rootfs must be specified for the container image, _ := container.Lookup(ContainerGroup, KeyImage) 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 */ 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. networkName, ok := network.Lookup(NetworkGroup, KeyNetworkName) 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 */ 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. volumeName, ok := volume.Lookup(VolumeGroup, KeyVolumeName) 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 service.RenameGroup(KubeGroup, XKubeGroup) + // Rename common quadlet group + service.RenameGroup(QuadletGroup, XQuadletGroup) + yamlPath, ok := kube.Lookup(KubeGroup, KeyYaml) if !ok || len(yamlPath) == 0 { 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 // before network is ready // /~https://github.com/containers/podman/issues/21873 - // Prepend the lines, so the user-provided values - // override the default ones. - service.PrependUnitLine(UnitGroup, "After", "network-online.target") - service.PrependUnitLine(UnitGroup, "Wants", "network-online.target") + if service.LookupBooleanWithDefault(QuadletGroup, KeyDefaultDependencies, true) { + service.PrependUnitLine(UnitGroup, "After", "network-online.target") + service.PrependUnitLine(UnitGroup, "Wants", "network-online.target") + } if 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 */ service.RenameGroup(ImageGroup, XImageGroup) + // Rename common quadlet group + service.RenameGroup(QuadletGroup, XQuadletGroup) + // Need the containers filesystem mounted to start podman 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 // before network is ready // /~https://github.com/containers/podman/issues/21873 - // Prepend the lines, so the user-provided values - // override the default ones. - service.PrependUnitLine(UnitGroup, "After", "network-online.target") - service.PrependUnitLine(UnitGroup, "Wants", "network-online.target") + if service.LookupBooleanWithDefault(QuadletGroup, KeyDefaultDependencies, true) { + service.PrependUnitLine(UnitGroup, "After", "network-online.target") + service.PrependUnitLine(UnitGroup, "Wants", "network-online.target") + } /* Rename old Build group to X-Build so that systemd ignores it */ service.RenameGroup(BuildGroup, XBuildGroup) + // Rename common quadlet group + service.RenameGroup(QuadletGroup, XQuadletGroup) + // Need the containers filesystem mounted to start podman 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 */ service.RenameGroup(PodGroup, XPodGroup) + // Rename common quadlet group + service.RenameGroup(QuadletGroup, XQuadletGroup) + // Need the containers filesystem mounted to start podman service.Add(UnitGroup, "RequiresMountsFor", "%t/containers") diff --git a/test/e2e/quadlet/no_deps.build b/test/e2e/quadlet/no_deps.build new file mode 100644 index 0000000000..d756843d42 --- /dev/null +++ b/test/e2e/quadlet/no_deps.build @@ -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 diff --git a/test/e2e/quadlet/no_deps.container b/test/e2e/quadlet/no_deps.container new file mode 100644 index 0000000000..568e0efdae --- /dev/null +++ b/test/e2e/quadlet/no_deps.container @@ -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 diff --git a/test/e2e/quadlet/no_deps.image b/test/e2e/quadlet/no_deps.image new file mode 100644 index 0000000000..b00dedd072 --- /dev/null +++ b/test/e2e/quadlet/no_deps.image @@ -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 diff --git a/test/e2e/quadlet_test.go b/test/e2e/quadlet_test.go index 51897716ed..2f2f251d2b 100644 --- a/test/e2e/quadlet_test.go +++ b/test/e2e/quadlet_test.go @@ -170,6 +170,15 @@ func (t *quadletTestcase) assertKeyIs(args []string, unit *parser.UnitFile) bool 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 { Expect(len(args)).To(BeNumerically(">=", 3)) group := args[0] @@ -501,6 +510,8 @@ func (t *quadletTestcase) doAssert(check []string, unit *parser.UnitFile, sessio ok = t.assertStdErrContains(args, session) case "assert-key-is": ok = t.assertKeyIs(args, unit) + case "assert-key-is-empty": + ok = t.assertKeyIsEmpty(args, unit) case "assert-key-is-regex": ok = t.assertKeyIsRegex(args, unit) case "assert-key-contains": @@ -899,6 +910,7 @@ BOGUS=foo Entry("Unit After Override", "unit-after-override.container"), Entry("NetworkAlias", "network-alias.container"), Entry("CgroupMode", "cgroups-mode.container"), + Entry("Container - No Default Dependencies", "no_deps.container"), Entry("basic.volume", "basic.volume"), Entry("device-copy.volume", "device-copy.volume"), @@ -967,6 +979,7 @@ BOGUS=foo Entry("Image - global args", "globalargs.image"), Entry("Image - Containers Conf Modules", "containersconfmodule.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 - Annotation Key", "annotation.build"), @@ -1000,6 +1013,7 @@ BOGUS=foo Entry("Build - Target Key", "target.build"), Entry("Build - TLSVerify Key", "tls-verify.build"), Entry("Build - Variant Key", "variant.build"), + Entry("Build - No Default Dependencies", "no_deps.build"), Entry("Pod - Basic", "basic.pod"), Entry("Pod - DNS", "dns.pod"),