Quadlet - translate dependencies on other quadlet units

Signed-off-by: Ygal Blum <ygal.blum@gmail.com>
This commit is contained in:
Ygal Blum
2025-04-16 14:53:50 -04:00
parent 48423a615d
commit e498c6526b
12 changed files with 267 additions and 13 deletions

View File

@ -43,17 +43,6 @@ var (
var ( var (
void struct{} 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 // 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 { func isExtSupported(filename string) bool {
ext := filepath.Ext(filename) ext := filepath.Ext(filename)
_, ok := supportedExtensions[ext] _, ok := quadlet.SupportedExtensions[ext]
return ok return ok
} }
@ -714,7 +703,7 @@ func process() bool {
sort.Slice(units, func(i, j int) bool { sort.Slice(units, func(i, j int) bool {
getOrder := func(i int) int { getOrder := func(i int) int {
ext := filepath.Ext(units[i].Filename) ext := filepath.Ext(units[i].Filename)
order, ok := supportedExtensions[ext] order, ok := quadlet.SupportedExtensions[ext]
if !ok { if !ok {
return 0 return 0
} }

View File

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

View File

@ -196,9 +196,33 @@ type UnitInfo struct {
} }
var ( 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/).+$`) URL = regexp.Delayed(`^((https?)|(git)://)|(github\.com/).+$`)
validPortRange = regexp.Delayed(`\d+(-\d+)?(/udp|/tcp)?$`) validPortRange = regexp.Delayed(`\d+(-\d+)?(/udp|/tcp)?$`)
unitDependencyKeys = []string{
"Wants",
"Requires",
"Requisite",
"BindsTo",
"PartOf",
"Upholds",
"Conflicts",
"Before",
"After",
}
// Supported keys in "Container" group // Supported keys in "Container" group
supportedContainerKeys = map[string]bool{ supportedContainerKeys = map[string]bool{
KeyAddCapability: true, KeyAddCapability: true,
@ -514,6 +538,10 @@ func ConvertContainer(container *parser.UnitFile, isUser bool, unitsInfoMap map[
service := container.Dup() service := container.Dup()
service.Filename = unitInfo.ServiceFileName() service.Filename = unitInfo.ServiceFileName()
if err := translateUnitDependencies(service, unitsInfoMap); err != nil {
return nil, warnings, err
}
addDefaultDependencies(service, isUser) addDefaultDependencies(service, isUser)
if container.Path != "" { if container.Path != "" {
@ -923,6 +951,10 @@ func ConvertNetwork(network *parser.UnitFile, name string, unitsInfoMap map[stri
service := network.Dup() service := network.Dup()
service.Filename = unitInfo.ServiceFileName() service.Filename = unitInfo.ServiceFileName()
if err := translateUnitDependencies(service, unitsInfoMap); err != nil {
return nil, warnings, err
}
addDefaultDependencies(service, isUser) addDefaultDependencies(service, isUser)
if network.Path != "" { if network.Path != "" {
@ -1034,6 +1066,10 @@ func ConvertVolume(volume *parser.UnitFile, name string, unitsInfoMap map[string
service := volume.Dup() service := volume.Dup()
service.Filename = unitInfo.ServiceFileName() service.Filename = unitInfo.ServiceFileName()
if err := translateUnitDependencies(service, unitsInfoMap); err != nil {
return nil, warnings, err
}
addDefaultDependencies(service, isUser) addDefaultDependencies(service, isUser)
if volume.Path != "" { if volume.Path != "" {
@ -1173,6 +1209,10 @@ func ConvertKube(kube *parser.UnitFile, unitsInfoMap map[string]*UnitInfo, isUse
service := kube.Dup() service := kube.Dup()
service.Filename = unitInfo.ServiceFileName() service.Filename = unitInfo.ServiceFileName()
if err := translateUnitDependencies(service, unitsInfoMap); err != nil {
return nil, err
}
addDefaultDependencies(service, isUser) addDefaultDependencies(service, isUser)
if kube.Path != "" { if kube.Path != "" {
@ -1318,6 +1358,10 @@ func ConvertImage(image *parser.UnitFile, unitsInfoMap map[string]*UnitInfo, isU
service := image.Dup() service := image.Dup()
service.Filename = unitInfo.ServiceFileName() service.Filename = unitInfo.ServiceFileName()
if err := translateUnitDependencies(service, unitsInfoMap); err != nil {
return nil, err
}
addDefaultDependencies(service, isUser) addDefaultDependencies(service, isUser)
if image.Path != "" { if image.Path != "" {
@ -1399,6 +1443,10 @@ func ConvertBuild(build *parser.UnitFile, unitsInfoMap map[string]*UnitInfo, isU
service := build.Dup() service := build.Dup()
service.Filename = unitInfo.ServiceFileName() service.Filename = unitInfo.ServiceFileName()
if err := translateUnitDependencies(service, unitsInfoMap); err != nil {
return nil, warnings, err
}
addDefaultDependencies(service, isUser) addDefaultDependencies(service, isUser)
/* Rename old Build group to X-Build so that systemd ignores it */ /* 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 := podUnit.Dup()
service.Filename = unitInfo.ServiceFileName() service.Filename = unitInfo.ServiceFileName()
if err := translateUnitDependencies(service, unitsInfoMap); err != nil {
return nil, err
}
addDefaultDependencies(service, isUser) addDefaultDependencies(service, isUser)
if podUnit.Path != "" { if podUnit.Path != "" {
@ -2260,3 +2312,36 @@ func handleExecReload(quadletUnitFile, serviceUnitFile *parser.UnitFile, groupNa
return nil 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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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("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("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("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("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"), 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 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 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 - 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 (.build)", "build.quadlet.volume", []string{"basic.build"}),
Entry("Volume - Quadlet image (.image)", "image.quadlet.volume", []string{"basic.image"}), 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 (.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 - 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", "network.quadlet.kube", []string{"basic.network"}),
Entry("Kube - Quadlet Network overriding service name", "network.quadlet.servicename.kube", []string{"service-name.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 - Network Key quadlet", "network.quadlet.build", []string{"basic.network"}),
Entry("Build - Volume Key", "volume.build", []string{"basic.volume"}), Entry("Build - Volume Key", "volume.build", []string{"basic.volume"}),
Entry("Build - Volume Key quadlet", "volume.quadlet.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 - 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 - 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 Network", "network.quadlet.pod", []string{"basic.network"}),
Entry("Pod - Quadlet Volume", "volume.pod", []string{"basic.volume"}), 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 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 - 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 - 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",
},
),
) )
}) })