From e498c6526ba017195a814b12fa36e60952f42ae2 Mon Sep 17 00:00:00 2001 From: Ygal Blum Date: Wed, 16 Apr 2025 14:53:50 -0400 Subject: [PATCH] Quadlet - translate dependencies on other quadlet units Signed-off-by: Ygal Blum --- cmd/quadlet/main.go | 15 +-- docs/source/markdown/podman-systemd.unit.5.md | 16 ++++ pkg/systemd/quadlet/quadlet.go | 85 +++++++++++++++++ test/e2e/quadlet/dependent.build | 10 ++ test/e2e/quadlet/dependent.container | 9 ++ test/e2e/quadlet/dependent.error.container | 9 ++ test/e2e/quadlet/dependent.image | 9 ++ test/e2e/quadlet/dependent.kube | 9 ++ test/e2e/quadlet/dependent.network | 8 ++ test/e2e/quadlet/dependent.pod | 8 ++ test/e2e/quadlet/dependent.volume | 8 ++ test/e2e/quadlet_test.go | 94 +++++++++++++++++++ 12 files changed, 267 insertions(+), 13 deletions(-) create mode 100644 test/e2e/quadlet/dependent.build create mode 100644 test/e2e/quadlet/dependent.container create mode 100644 test/e2e/quadlet/dependent.error.container create mode 100644 test/e2e/quadlet/dependent.image create mode 100644 test/e2e/quadlet/dependent.kube create mode 100644 test/e2e/quadlet/dependent.network create mode 100644 test/e2e/quadlet/dependent.pod create mode 100644 test/e2e/quadlet/dependent.volume diff --git a/cmd/quadlet/main.go b/cmd/quadlet/main.go index ff113499da..ea2f6b46e7 100644 --- a/cmd/quadlet/main.go +++ b/cmd/quadlet/main.go @@ -43,17 +43,6 @@ var ( var ( void struct{} - // Key: Extension - // Value: Processing order for resource naming dependencies - supportedExtensions = map[string]int{ - ".container": 4, - ".volume": 2, - ".kube": 4, - ".network": 2, - ".image": 1, - ".build": 3, - ".pod": 5, - } ) // We log directly to /dev/kmsg, because that is the only way to get information out @@ -312,7 +301,7 @@ func getUserLevelFilter(resolvedUnitDirAdminUser string) func(string, bool) bool func isExtSupported(filename string) bool { ext := filepath.Ext(filename) - _, ok := supportedExtensions[ext] + _, ok := quadlet.SupportedExtensions[ext] return ok } @@ -714,7 +703,7 @@ func process() bool { sort.Slice(units, func(i, j int) bool { getOrder := func(i int) int { ext := filepath.Ext(units[i].Filename) - order, ok := supportedExtensions[ext] + order, ok := quadlet.SupportedExtensions[ext] if !ok { return 0 } diff --git a/docs/source/markdown/podman-systemd.unit.5.md b/docs/source/markdown/podman-systemd.unit.5.md index 1c35c997ea..9dc614e916 100644 --- a/docs/source/markdown/podman-systemd.unit.5.md +++ b/docs/source/markdown/podman-systemd.unit.5.md @@ -258,6 +258,22 @@ the `network-online.target` unit is active with `systemctl is-active network-onl This behavior can be disabled by adding `DefaultDependencies=false` in the `Quadlet` section. Note, the _systemd_ `[Unit]` section has an option with the same name but a different meaning. +### Dependency between Quadlet units + +Quadlet will automatically translate dependencies, specified in the keys +`Wants`, `Requires`, `Requisite`, `BindsTo`, `PartOf`, `Upholds`, `Conflicts`, `Before` and `After` +of the `[Unit]` section, between different Quadlet units. + +For example the `fedora.container` unit below specifies a dependency on the `basic.container` unit. +``` +[Unit] +After=basic.container +Requires=basic.container + +[Container] +Image=registry.fedoraproject.org/fedora:41 +``` + ## Container units [Container] Container units are named with a `.container` extension and contain a `[Container]` section describing diff --git a/pkg/systemd/quadlet/quadlet.go b/pkg/systemd/quadlet/quadlet.go index 2c12a2212a..964298f015 100644 --- a/pkg/systemd/quadlet/quadlet.go +++ b/pkg/systemd/quadlet/quadlet.go @@ -196,9 +196,33 @@ type UnitInfo struct { } var ( + // Key: Extension + // Value: Processing order for resource naming dependencies + SupportedExtensions = map[string]int{ + ".container": 4, + ".volume": 2, + ".kube": 4, + ".network": 2, + ".image": 1, + ".build": 3, + ".pod": 5, + } + URL = regexp.Delayed(`^((https?)|(git)://)|(github\.com/).+$`) validPortRange = regexp.Delayed(`\d+(-\d+)?(/udp|/tcp)?$`) + unitDependencyKeys = []string{ + "Wants", + "Requires", + "Requisite", + "BindsTo", + "PartOf", + "Upholds", + "Conflicts", + "Before", + "After", + } + // Supported keys in "Container" group supportedContainerKeys = map[string]bool{ KeyAddCapability: true, @@ -514,6 +538,10 @@ func ConvertContainer(container *parser.UnitFile, isUser bool, unitsInfoMap map[ service := container.Dup() service.Filename = unitInfo.ServiceFileName() + if err := translateUnitDependencies(service, unitsInfoMap); err != nil { + return nil, warnings, err + } + addDefaultDependencies(service, isUser) if container.Path != "" { @@ -923,6 +951,10 @@ func ConvertNetwork(network *parser.UnitFile, name string, unitsInfoMap map[stri service := network.Dup() service.Filename = unitInfo.ServiceFileName() + if err := translateUnitDependencies(service, unitsInfoMap); err != nil { + return nil, warnings, err + } + addDefaultDependencies(service, isUser) if network.Path != "" { @@ -1034,6 +1066,10 @@ func ConvertVolume(volume *parser.UnitFile, name string, unitsInfoMap map[string service := volume.Dup() service.Filename = unitInfo.ServiceFileName() + if err := translateUnitDependencies(service, unitsInfoMap); err != nil { + return nil, warnings, err + } + addDefaultDependencies(service, isUser) if volume.Path != "" { @@ -1173,6 +1209,10 @@ func ConvertKube(kube *parser.UnitFile, unitsInfoMap map[string]*UnitInfo, isUse service := kube.Dup() service.Filename = unitInfo.ServiceFileName() + if err := translateUnitDependencies(service, unitsInfoMap); err != nil { + return nil, err + } + addDefaultDependencies(service, isUser) if kube.Path != "" { @@ -1318,6 +1358,10 @@ func ConvertImage(image *parser.UnitFile, unitsInfoMap map[string]*UnitInfo, isU service := image.Dup() service.Filename = unitInfo.ServiceFileName() + if err := translateUnitDependencies(service, unitsInfoMap); err != nil { + return nil, err + } + addDefaultDependencies(service, isUser) if image.Path != "" { @@ -1399,6 +1443,10 @@ func ConvertBuild(build *parser.UnitFile, unitsInfoMap map[string]*UnitInfo, isU service := build.Dup() service.Filename = unitInfo.ServiceFileName() + if err := translateUnitDependencies(service, unitsInfoMap); err != nil { + return nil, warnings, err + } + addDefaultDependencies(service, isUser) /* Rename old Build group to X-Build so that systemd ignores it */ @@ -1568,6 +1616,10 @@ func ConvertPod(podUnit *parser.UnitFile, name string, unitsInfoMap map[string]* service := podUnit.Dup() service.Filename = unitInfo.ServiceFileName() + if err := translateUnitDependencies(service, unitsInfoMap); err != nil { + return nil, err + } + addDefaultDependencies(service, isUser) if podUnit.Path != "" { @@ -2260,3 +2312,36 @@ func handleExecReload(quadletUnitFile, serviceUnitFile *parser.UnitFile, groupNa return nil } + +func translateUnitDependencies(serviceUnitFile *parser.UnitFile, unitsInfoMap map[string]*UnitInfo) error { + for _, unitDependencyKey := range unitDependencyKeys { + deps := serviceUnitFile.LookupAllStrv(UnitGroup, unitDependencyKey) + if len(deps) == 0 { + continue + } + translatedDeps := make([]string, 0, len(deps)) + translated := false + for _, dep := range deps { + var translatedDep string + + ext := filepath.Ext(dep) + if _, ok := SupportedExtensions[ext]; ok { + unitInfo, ok := unitsInfoMap[dep] + if !ok { + return fmt.Errorf("unable to translate dependency for %s", dep) + } + translatedDep = unitInfo.ServiceFileName() + translated = true + } else { + translatedDep = dep + } + translatedDeps = append(translatedDeps, translatedDep) + } + if !translated { + continue + } + serviceUnitFile.Unset(UnitGroup, unitDependencyKey) + serviceUnitFile.Add(UnitGroup, unitDependencyKey, strings.Join(translatedDeps, " ")) + } + return nil +} diff --git a/test/e2e/quadlet/dependent.build b/test/e2e/quadlet/dependent.build new file mode 100644 index 0000000000..f28843a8eb --- /dev/null +++ b/test/e2e/quadlet/dependent.build @@ -0,0 +1,10 @@ +## assert-key-is "Unit" "Requires" "basic-build.service basic.service basic-image.service basic.service basic-network.service basic-pod.service basic-volume.service" +## assert-key-is-regex "Unit" "After" "network-online.target|podman-user-wait-network-online.service" "basic-build.service basic.service basic-image.service basic.service basic-network.service basic-pod.service basic-volume.service" + +[Unit] +After=basic.build basic.container basic.image basic.kube basic.network basic.pod basic.volume +Requires=basic.build basic.container basic.image basic.kube basic.network basic.pod basic.volume + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=unit diff --git a/test/e2e/quadlet/dependent.container b/test/e2e/quadlet/dependent.container new file mode 100644 index 0000000000..bd3a2c8edd --- /dev/null +++ b/test/e2e/quadlet/dependent.container @@ -0,0 +1,9 @@ +## assert-key-is "Unit" "Requires" "basic-build.service basic.service basic-image.service basic.service basic-network.service basic-pod.service basic-volume.service" +## assert-key-is-regex "Unit" "After" "network-online.target|podman-user-wait-network-online.service" "basic-build.service basic.service basic-image.service basic.service basic-network.service basic-pod.service basic-volume.service" + +[Unit] +After=basic.build basic.container basic.image basic.kube basic.network basic.pod basic.volume +Requires=basic.build basic.container basic.image basic.kube basic.network basic.pod basic.volume + +[Container] +Image=localhost/imagename diff --git a/test/e2e/quadlet/dependent.error.container b/test/e2e/quadlet/dependent.error.container new file mode 100644 index 0000000000..b745b4a406 --- /dev/null +++ b/test/e2e/quadlet/dependent.error.container @@ -0,0 +1,9 @@ +## assert-failed +## assert-stderr-contains "unable to translate dependency for basic.container" + +[Unit] +After=basic.container +Requires=basic.container + +[Container] +Image=localhost/imagename diff --git a/test/e2e/quadlet/dependent.image b/test/e2e/quadlet/dependent.image new file mode 100644 index 0000000000..4a85f43faf --- /dev/null +++ b/test/e2e/quadlet/dependent.image @@ -0,0 +1,9 @@ +## assert-key-is "Unit" "Requires" "basic-build.service basic.service basic-image.service basic.service basic-network.service basic-pod.service basic-volume.service" +## assert-key-is-regex "Unit" "After" "network-online.target|podman-user-wait-network-online.service" "basic-build.service basic.service basic-image.service basic.service basic-network.service basic-pod.service basic-volume.service" + +[Unit] +After=basic.build basic.container basic.image basic.kube basic.network basic.pod basic.volume +Requires=basic.build basic.container basic.image basic.kube basic.network basic.pod basic.volume + +[Image] +Image=localhost/imagename diff --git a/test/e2e/quadlet/dependent.kube b/test/e2e/quadlet/dependent.kube new file mode 100644 index 0000000000..a7cc380a83 --- /dev/null +++ b/test/e2e/quadlet/dependent.kube @@ -0,0 +1,9 @@ +## assert-key-is "Unit" "Requires" "basic-build.service basic.service basic-image.service basic.service basic-network.service basic-pod.service basic-volume.service" +## assert-key-is-regex "Unit" "After" "network-online.target|podman-user-wait-network-online.service" "basic-build.service basic.service basic-image.service basic.service basic-network.service basic-pod.service basic-volume.service" + +[Unit] +After=basic.build basic.container basic.image basic.kube basic.network basic.pod basic.volume +Requires=basic.build basic.container basic.image basic.kube basic.network basic.pod basic.volume + +[Kube] +Yaml=deployment.yml diff --git a/test/e2e/quadlet/dependent.network b/test/e2e/quadlet/dependent.network new file mode 100644 index 0000000000..a9dde1d090 --- /dev/null +++ b/test/e2e/quadlet/dependent.network @@ -0,0 +1,8 @@ +## assert-key-is "Unit" "Requires" "basic-build.service basic.service basic-image.service basic.service basic-network.service basic-pod.service basic-volume.service" +## assert-key-is-regex "Unit" "After" "network-online.target|podman-user-wait-network-online.service" "basic-build.service basic.service basic-image.service basic.service basic-network.service basic-pod.service basic-volume.service" + +[Unit] +After=basic.build basic.container basic.image basic.kube basic.network basic.pod basic.volume +Requires=basic.build basic.container basic.image basic.kube basic.network basic.pod basic.volume + +[Network] diff --git a/test/e2e/quadlet/dependent.pod b/test/e2e/quadlet/dependent.pod new file mode 100644 index 0000000000..4e0add8b18 --- /dev/null +++ b/test/e2e/quadlet/dependent.pod @@ -0,0 +1,8 @@ +## assert-key-is "Unit" "Requires" "basic-build.service basic.service basic-image.service basic.service basic-network.service basic-pod.service basic-volume.service" +## assert-key-is-regex "Unit" "After" "network-online.target|podman-user-wait-network-online.service" "basic-build.service basic.service basic-image.service basic.service basic-network.service basic-pod.service basic-volume.service" + +[Unit] +After=basic.build basic.container basic.image basic.kube basic.network basic.pod basic.volume +Requires=basic.build basic.container basic.image basic.kube basic.network basic.pod basic.volume + +[Pod] diff --git a/test/e2e/quadlet/dependent.volume b/test/e2e/quadlet/dependent.volume new file mode 100644 index 0000000000..889f3761ef --- /dev/null +++ b/test/e2e/quadlet/dependent.volume @@ -0,0 +1,8 @@ +## assert-key-is "Unit" "Requires" "basic-build.service basic.service basic-image.service basic.service basic-network.service basic-pod.service basic-volume.service" +## assert-key-is-regex "Unit" "After" "network-online.target|podman-user-wait-network-online.service" "basic-build.service basic.service basic-image.service basic.service basic-network.service basic-pod.service basic-volume.service" + +[Unit] +After=basic.build basic.container basic.image basic.kube basic.network basic.pod basic.volume +Requires=basic.build basic.container basic.image basic.kube basic.network basic.pod basic.volume + +[Volume] diff --git a/test/e2e/quadlet_test.go b/test/e2e/quadlet_test.go index a621b6e361..30ceee98df 100644 --- a/test/e2e/quadlet_test.go +++ b/test/e2e/quadlet_test.go @@ -1102,6 +1102,7 @@ BOGUS=foo Entry("subidmapping-with-remap.container", "subidmapping-with-remap.container", "converting \"subidmapping-with-remap.container\": deprecated Remap keys are set along with explicit mapping keys"), Entry("userns-with-remap.container", "userns-with-remap.container", "converting \"userns-with-remap.container\": deprecated Remap keys are set along with explicit mapping keys"), Entry("reloadboth.container", "reloadboth.container", "converting \"reloadboth.container\": ReloadCmd and ReloadSignal are mutually exclusive but both are set"), + Entry("dependent.error.container", "dependent.error.container", "converting \"dependent.error.container\": unable to translate dependency for basic.container"), Entry("image-no-image.volume", "image-no-image.volume", "converting \"image-no-image.volume\": the key Image is mandatory when using the image driver"), Entry("Volume - Quadlet image (.build) not found", "build-not-found.quadlet.volume", "converting \"build-not-found.quadlet.volume\": requested Quadlet image not-found.build was not found"), @@ -1157,26 +1158,119 @@ BOGUS=foo Entry("Container - Reuse another named container's network", "network.reuse.name.container", []string{"name.container"}), Entry("Container - Reuse another container's network", "a.network.reuse.container", []string{"basic.container"}), Entry("Container - Reuse another named container's network", "a.network.reuse.name.container", []string{"name.container"}), + Entry( + "Container - Dependency between quadlet units", + "dependent.container", + []string{ + "basic.build", + "basic.container", + "basic.image", + "basic.kube", + "basic.network", + "basic.pod", + "basic.volume", + }, + ), Entry("Volume - Quadlet image (.build)", "build.quadlet.volume", []string{"basic.build"}), Entry("Volume - Quadlet image (.image)", "image.quadlet.volume", []string{"basic.image"}), Entry("Volume - Quadlet image (.build) overriding service name", "build.quadlet.servicename.volume", []string{"service-name.build"}), Entry("Volume - Quadlet image (.image) overriding service name", "image.quadlet.servicename.volume", []string{"service-name.image"}), + Entry( + "Volume - Dependency between quadlet units", + "dependent.volume", + []string{ + "basic.build", + "basic.container", + "basic.image", + "basic.kube", + "basic.network", + "basic.pod", + "basic.volume", + }, + ), Entry("Kube - Quadlet Network", "network.quadlet.kube", []string{"basic.network"}), Entry("Kube - Quadlet Network overriding service name", "network.quadlet.servicename.kube", []string{"service-name.network"}), + Entry( + "Kube - Dependency between quadlet units", + "dependent.kube", + []string{ + "basic.build", + "basic.container", + "basic.image", + "basic.kube", + "basic.network", + "basic.pod", + "basic.volume", + }, + ), Entry("Build - Network Key quadlet", "network.quadlet.build", []string{"basic.network"}), Entry("Build - Volume Key", "volume.build", []string{"basic.volume"}), Entry("Build - Volume Key quadlet", "volume.quadlet.build", []string{"basic.volume"}), Entry("Build - Network Key quadlet overriding service name", "network.quadlet.servicename.build", []string{"service-name.network"}), Entry("Build - Volume Key quadlet overriding service name", "volume.quadlet.servicename.build", []string{"service-name.volume"}), + Entry( + "Build - Dependency between quadlet units", + "dependent.build", + []string{ + "basic.build", + "basic.container", + "basic.image", + "basic.kube", + "basic.network", + "basic.pod", + "basic.volume", + }, + ), Entry("Pod - Quadlet Network", "network.quadlet.pod", []string{"basic.network"}), Entry("Pod - Quadlet Volume", "volume.pod", []string{"basic.volume"}), Entry("Pod - Quadlet Network overriding service name", "network.servicename.quadlet.pod", []string{"service-name.network"}), Entry("Pod - Quadlet Volume overriding service name", "volume.servicename.pod", []string{"service-name.volume"}), Entry("Pod - Do not autostart a container with pod", "startwithpod.pod", []string{"startwithpod_no.container", "startwithpod_yes.container"}), + Entry( + "Pod - Dependency between quadlet units", + "dependent.pod", + []string{ + "basic.build", + "basic.container", + "basic.image", + "basic.kube", + "basic.network", + "basic.pod", + "basic.volume", + }, + ), + + Entry( + "Image - Dependency between quadlet units", + "dependent.image", + []string{ + "basic.build", + "basic.container", + "basic.image", + "basic.kube", + "basic.network", + "basic.pod", + "basic.volume", + }, + ), + + Entry( + "Network - Dependency between quadlet units", + "dependent.network", + []string{ + "basic.build", + "basic.container", + "basic.image", + "basic.kube", + "basic.network", + "basic.pod", + "basic.volume", + }, + ), ) })