diff --git a/docs/source/markdown/podman-systemd.unit.5.md b/docs/source/markdown/podman-systemd.unit.5.md index 5ad2d33a31be..9f700f78b8e8 100644 --- a/docs/source/markdown/podman-systemd.unit.5.md +++ b/docs/source/markdown/podman-systemd.unit.5.md @@ -89,6 +89,7 @@ Valid options for `[Container]` are listed below: | Image=ubi8 | Image specification - ubi8 | | Label="YXZ" | --label "XYZ" | | LogDriver=journald | --log-driver journald | +| Mount=type=bind,source=/path/on/host,destination=/path/in/container | --mount type=bind,source=/path/on/host,destination=/path/in/container | | Network=host | --net host | | NoNewPrivileges=true | --security-opt no-new-privileges | | Rootfs=/var/lib/rootfs | --rootfs /var/lib/rootfs | @@ -217,6 +218,19 @@ Equivalent to the Podman `--log-driver` option. The default value is `passthrough`. +### `Mount=` + +Attach a filesystem mount to the container. +This is equivalent to the Podman `--mount` option, and +generally has the form `type=TYPE,TYPE-SPECIFIC-OPTION[,...]`. + +As a special case, for `type=volume` if `source` ends with `.volume`, a Podman named volume called +`systemd-$name` will be used as the source, and the generated systemd service will contain +a dependency on the `$name-volume.service`. Such a volume can be automatically be lazily +created by using a `$name.volume` quadlet file. + +This key can be listed multiple times. + ### `Network=` Specify a custom network for the container. This has the same format as the `--network` option diff --git a/pkg/systemd/quadlet/quadlet.go b/pkg/systemd/quadlet/quadlet.go index eb2bdfa289e0..13942af562d1 100644 --- a/pkg/systemd/quadlet/quadlet.go +++ b/pkg/systemd/quadlet/quadlet.go @@ -51,6 +51,7 @@ const ( KeyImage = "Image" KeyLabel = "Label" KeyLogDriver = "LogDriver" + KeyMount = "Mount" KeyNetwork = "Network" KeyNetworkDisableDNS = "DisableDNS" KeyNetworkDriver = "Driver" @@ -106,6 +107,7 @@ var ( KeyImage: true, KeyLabel: true, KeyLogDriver: true, + KeyMount: true, KeyNetwork: true, KeyNoNewPrivileges: true, KeyNotify: true, @@ -464,21 +466,7 @@ func ConvertContainer(container *parser.UnitFile, isUser bool) (*parser.UnitFile } if source != "" { - if source[0] == '/' { - // Absolute path - service.Add(UnitGroup, "RequiresMountsFor", source) - } else if strings.HasSuffix(source, ".volume") { - // the podman volume name is systemd-$name - volumeName := replaceExtension(source, "", "systemd-", "") - - // the systemd unit name is $name-volume.service - volumeServiceName := replaceExtension(source, ".service", "", "-volume") - - source = volumeName - - service.Add(UnitGroup, "Requires", volumeServiceName) - service.Add(UnitGroup, "After", volumeServiceName) - } + source = handleStorageSource(service, source) } podman.add("-v") @@ -530,6 +518,34 @@ func ConvertContainer(container *parser.UnitFile, isUser bool) (*parser.UnitFile podman.add("--secret", secret) } + mounts := container.LookupAllArgs(ContainerGroup, KeyMount) + for _, mount := range mounts { + params := strings.Split(mount, ",") + paramsMap := make(map[string]string, len(params)) + for _, param := range params { + kv := strings.Split(param, "=") + paramsMap[kv[0]] = kv[1] + } + if paramType, ok := paramsMap["type"]; ok { + if paramType == "volume" || paramType == "bind" { + if paramSource, ok := paramsMap["source"]; ok { + paramsMap["source"] = handleStorageSource(service, paramSource) + } else if paramSource, ok = paramsMap["src"]; ok { + paramsMap["src"] = handleStorageSource(service, paramSource) + } + } + } + paramsArray := make([]string, 0, len(params)) + paramsArray = append(paramsArray, fmt.Sprintf("%s=%s", "type", paramsMap["type"])) + for k, v := range paramsMap { + if k != "type" { + paramsArray = append(paramsArray, fmt.Sprintf("%s=%s", k, v)) + } + } + mountStr := strings.Join(paramsArray, ",") + podman.add("--mount", mountStr) + } + podmanArgs := container.LookupAllArgs(ContainerGroup, KeyPodmanArgs) podman.add(podmanArgs...) @@ -992,3 +1008,23 @@ func handleLogDriver(unitFile *parser.UnitFile, groupName string, podman *Podman } podman.add("--log-driver", logDriver) } + +func handleStorageSource(unitFile *parser.UnitFile, source string) string { + if source[0] == '/' { + // Absolute path + unitFile.Add(UnitGroup, "RequiresMountsFor", source) + } else if strings.HasSuffix(source, ".volume") { + // the podman volume name is systemd-$name + volumeName := replaceExtension(source, "", "systemd-", "") + + // the systemd unit name is $name-volume.service + volumeServiceName := replaceExtension(source, ".service", "", "-volume") + + source = volumeName + + unitFile.Add(UnitGroup, "Requires", volumeServiceName) + unitFile.Add(UnitGroup, "After", volumeServiceName) + } + + return source +} diff --git a/test/e2e/quadlet/mount.container b/test/e2e/quadlet/mount.container new file mode 100644 index 000000000000..097f27e07154 --- /dev/null +++ b/test/e2e/quadlet/mount.container @@ -0,0 +1,20 @@ +[Container] +Image=localhost/imagename +## assert-podman-args-key-val "--mount" "," "type=bind,source=/path/on/host,destination=/path/in/container" +Mount=type=bind,source=/path/on/host,destination=/path/in/container +## assert-podman-args-key-val "--mount" "," "type=bind,src=/path/on/host,dst=/path/in/container,relabel=shared" +Mount=type=bind,src=/path/on/host,dst=/path/in/container,relabel=shared +## assert-podman-args-key-val "--mount" "," "type=bind,src=/path/on/host,dst=/path/in/container,relabel=shared,U=true" +Mount=type=bind,src=/path/on/host,dst=/path/in/container,relabel=shared,U=true +## assert-podman-args-key-val "--mount" "," "type=volume,source=vol1,destination=/path/in/container,ro=true" +Mount=type=volume,source=vol1,destination=/path/in/container,ro=true +## assert-podman-args-key-val "--mount" "," "type=volume,source=systemd-vol2,destination=/path/in/container,ro=true" +## assert-key-is "Unit" "Requires" "vol2-volume.service" +## assert-key-is "Unit" "After" "vol2-volume.service" +Mount=type=volume,source=vol2.volume,destination=/path/in/container,ro=true +## assert-podman-args-key-val "--mount" "," "type=tmpfs,tmpfs-size=512M,destination=/path/in/container" +Mount=type=tmpfs,tmpfs-size=512M,destination=/path/in/container +## assert-podman-args-key-val "--mount" "," "type=image,source=fedora,destination=/fedora-image,rw=true" +Mount=type=image,source=fedora,destination=/fedora-image,rw=true +## assert-podman-args-key-val "--mount" "," "type=devpts,destination=/dev/pts" +Mount=type=devpts,destination=/dev/pts diff --git a/test/e2e/quadlet_test.go b/test/e2e/quadlet_test.go index 22beeac2ce28..7cf0359d6199 100644 --- a/test/e2e/quadlet_test.go +++ b/test/e2e/quadlet_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "reflect" "regexp" "strings" @@ -175,6 +176,44 @@ func (t *quadletTestcase) assertPodmanArgsRegex(args []string, unit *parser.Unit return findSublistRegex(podmanArgs, args) != -1 } +func keyValueStringToMap(keyValueString, seperator string) map[string]string { + keyValMap := make(map[string]string) + keyVarList := strings.Split(keyValueString, seperator) + for _, param := range keyVarList { + kv := strings.Split(param, "=") + keyValMap[kv[0]] = kv[1] + } + + return keyValMap +} + +func (t *quadletTestcase) assertPodmanArgsKeyVal(args []string, unit *parser.UnitFile, key string) bool { + podmanArgs, _ := unit.LookupLastArgs("Service", key) + + expectedKeyValMap := keyValueStringToMap(args[2], args[1]) + argKeyLocation := 0 + for { + subListLocation := findSublist(podmanArgs[argKeyLocation:], []string{args[0]}) + if subListLocation == -1 { + break + } + + argKeyLocation += subListLocation + actualKeyValMap := keyValueStringToMap(podmanArgs[argKeyLocation+1], args[1]) + if reflect.DeepEqual(expectedKeyValMap, actualKeyValMap) { + return true + } + + argKeyLocation += 2 + + if argKeyLocation > len(podmanArgs) { + break + } + } + + return false +} + func (t *quadletTestcase) assertPodmanFinalArgs(args []string, unit *parser.UnitFile, key string) bool { podmanArgs, _ := unit.LookupLastArgs("Service", key) if len(podmanArgs) < len(args) { @@ -199,6 +238,10 @@ func (t *quadletTestcase) assertStartPodmanArgsRegex(args []string, unit *parser return t.assertPodmanArgsRegex(args, unit, "ExecStart") } +func (t *quadletTestcase) assertStartPodmanArgsKeyVal(args []string, unit *parser.UnitFile) bool { + return t.assertPodmanArgsKeyVal(args, unit, "ExecStart") +} + func (t *quadletTestcase) assertStartPodmanFinalArgs(args []string, unit *parser.UnitFile) bool { return t.assertPodmanFinalArgs(args, unit, "ExecStart") } @@ -264,6 +307,8 @@ func (t *quadletTestcase) doAssert(check []string, unit *parser.UnitFile, sessio ok = t.assertStartPodmanArgs(args, unit) case "assert-podman-args-regex": ok = t.assertStartPodmanArgsRegex(args, unit) + case "assert-podman-args-key-val": + ok = t.assertStartPodmanArgsKeyVal(args, unit) case "assert-podman-final-args": ok = t.assertStartPodmanFinalArgs(args, unit) case "assert-podman-final-args-regex": @@ -484,6 +529,7 @@ var _ = Describe("quadlet system generator", func() { Entry("env-host-false.container", "env-host-false.container"), Entry("secrets.container", "secrets.container"), Entry("logdriver.container", "logdriver.container"), + Entry("mount.container", "mount.container"), Entry("basic.volume", "basic.volume"), Entry("label.volume", "label.volume"),