Files
podman/pkg/systemd/quadlet/quadlet.go
Ygal Blum 5382997e01 Quadlet - add support for relative path in Volume key in .container file
If the volume source starts with . resolve the path relative to the
location of the unit file

Update the test code to allow verification of regex for the value in key
value arguments
Add the usage of relative paths to the volume and mount test cases
Update the man page

Signed-off-by: Ygal Blum <ygal.blum@gmail.com>
2023-03-20 18:54:00 +02:00

1108 lines
32 KiB
Go

package quadlet
import (
"fmt"
"path/filepath"
"strings"
"github.com/containers/podman/v4/pkg/systemd/parser"
"github.com/containers/storage/pkg/regexp"
)
const (
// Directory for global Quadlet files (sysadmin owned)
UnitDirAdmin = "/etc/containers/systemd"
// Directory for global Quadlet files (distro owned)
UnitDirDistro = "/usr/share/containers/systemd"
// Names of commonly used systemd/quadlet group names
ContainerGroup = "Container"
InstallGroup = "Install"
KubeGroup = "Kube"
NetworkGroup = "Network"
ServiceGroup = "Service"
UnitGroup = "Unit"
VolumeGroup = "Volume"
XContainerGroup = "X-Container"
XKubeGroup = "X-Kube"
XNetworkGroup = "X-Network"
XVolumeGroup = "X-Volume"
// Use passthough as the default log driver to output to Journal
defaultLogDriver = "passthrough"
)
// All the supported quadlet keys
const (
KeyAddCapability = "AddCapability"
KeyAddDevice = "AddDevice"
KeyAnnotation = "Annotation"
KeyConfigMap = "ConfigMap"
KeyContainerName = "ContainerName"
KeyCopy = "Copy"
KeyDevice = "Device"
KeyDropCapability = "DropCapability"
KeyEnvironment = "Environment"
KeyEnvironmentFile = "EnvironmentFile"
KeyEnvironmentHost = "EnvironmentHost"
KeyExec = "Exec"
KeyExposeHostPort = "ExposeHostPort"
KeyGroup = "Group"
KeyHealthCmd = "HealthCmd"
KeyHealthInterval = "HealthInterval"
KeyHealthOnFailure = "HealthOnFailure"
KeyHealthRetries = "HealthRetries"
KeyHealthStartPeriod = "HealthStartPeriod"
KeyHealthStartupCmd = "HealthStartupCmd"
KeyHealthStartupInterval = "HealthStartupInterval"
KeyHealthStartupRetries = "HealthStartupRetries"
KeyHealthStartupSuccess = "HealthStartupSuccess"
KeyHealthStartupTimeout = "HealthStartupTimeout"
KeyHealthTimeout = "HealthTimeout"
KeyImage = "Image"
KeyIP = "IP"
KeyIP6 = "IP6"
KeyLabel = "Label"
KeyLogDriver = "LogDriver"
KeyMount = "Mount"
KeyNetwork = "Network"
KeyNetworkDisableDNS = "DisableDNS"
KeyNetworkDriver = "Driver"
KeyNetworkGateway = "Gateway"
KeyNetworkIPAMDriver = "IPAMDriver"
KeyNetworkIPRange = "IPRange"
KeyNetworkIPv6 = "IPv6"
KeyNetworkInternal = "Internal"
KeyNetworkOptions = "Options"
KeyNetworkSubnet = "Subnet"
KeyNoNewPrivileges = "NoNewPrivileges"
KeyNotify = "Notify"
KeyOptions = "Options"
KeyPodmanArgs = "PodmanArgs"
KeyPublishPort = "PublishPort"
KeyReadOnly = "ReadOnly"
KeyRemapGID = "RemapGid"
KeyRemapUID = "RemapUid"
KeyRemapUIDSize = "RemapUidSize"
KeyRemapUsers = "RemapUsers"
KeyRootfs = "Rootfs"
KeyRunInit = "RunInit"
KeySeccompProfile = "SeccompProfile"
KeySecurityLabelDisable = "SecurityLabelDisable"
KeySecurityLabelFileType = "SecurityLabelFileType"
KeySecurityLabelLevel = "SecurityLabelLevel"
KeySecurityLabelType = "SecurityLabelType"
KeySecret = "Secret"
KeyTimezone = "Timezone"
KeyType = "Type"
KeyUser = "User"
KeyVolatileTmp = "VolatileTmp"
KeyVolume = "Volume"
KeyYaml = "Yaml"
)
var (
validPortRange = regexp.Delayed(`\d+(-\d+)?(/udp|/tcp)?$`)
// Supported keys in "Container" group
supportedContainerKeys = map[string]bool{
KeyAddCapability: true,
KeyAddDevice: true,
KeyAnnotation: true,
KeyContainerName: true,
KeyDropCapability: true,
KeyEnvironment: true,
KeyEnvironmentFile: true,
KeyEnvironmentHost: true,
KeyExec: true,
KeyExposeHostPort: true,
KeyGroup: true,
KeyHealthCmd: true,
KeyHealthInterval: true,
KeyHealthOnFailure: true,
KeyHealthRetries: true,
KeyHealthStartPeriod: true,
KeyHealthStartupCmd: true,
KeyHealthStartupInterval: true,
KeyHealthStartupRetries: true,
KeyHealthStartupSuccess: true,
KeyHealthStartupTimeout: true,
KeyHealthTimeout: true,
KeyImage: true,
KeyIP: true,
KeyIP6: true,
KeyLabel: true,
KeyLogDriver: true,
KeyMount: true,
KeyNetwork: true,
KeyNoNewPrivileges: true,
KeyNotify: true,
KeyPodmanArgs: true,
KeyPublishPort: true,
KeyReadOnly: true,
KeyRemapGID: true,
KeyRemapUID: true,
KeyRemapUIDSize: true,
KeyRemapUsers: true,
KeyRootfs: true,
KeyRunInit: true,
KeySeccompProfile: true,
KeySecurityLabelDisable: true,
KeySecurityLabelFileType: true,
KeySecurityLabelLevel: true,
KeySecurityLabelType: true,
KeySecret: true,
KeyTimezone: true,
KeyUser: true,
KeyVolatileTmp: true,
KeyVolume: true,
}
// Supported keys in "Volume" group
supportedVolumeKeys = map[string]bool{
KeyCopy: true,
KeyDevice: true,
KeyGroup: true,
KeyLabel: true,
KeyOptions: true,
KeyType: true,
KeyUser: true,
}
// Supported keys in "Network" group
supportedNetworkKeys = map[string]bool{
KeyLabel: true,
KeyNetworkDisableDNS: true,
KeyNetworkDriver: true,
KeyNetworkGateway: true,
KeyNetworkIPAMDriver: true,
KeyNetworkIPRange: true,
KeyNetworkIPv6: true,
KeyNetworkInternal: true,
KeyNetworkOptions: true,
KeyNetworkSubnet: true,
}
// Supported keys in "Kube" group
supportedKubeKeys = map[string]bool{
KeyConfigMap: true,
KeyLogDriver: true,
KeyNetwork: true,
KeyPublishPort: true,
KeyRemapGID: true,
KeyRemapUID: true,
KeyRemapUIDSize: true,
KeyRemapUsers: true,
KeyYaml: true,
}
)
func replaceExtension(name string, extension string, extraPrefix string, extraSuffix string) string {
baseName := name
dot := strings.LastIndexByte(name, '.')
if dot > 0 {
baseName = name[:dot]
}
return extraPrefix + baseName + extraSuffix + extension
}
func isPortRange(port string) bool {
return validPortRange.MatchString(port)
}
func checkForUnknownKeys(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 splitPorts(ports string) []string {
parts := make([]string, 0)
// IP address could have colons in it. For example: "[::]:8080:80/tcp, so we split carefully
start := 0
end := 0
for end < len(ports) {
switch ports[end] {
case '[':
end++
for end < len(ports) && ports[end] != ']' {
end++
}
if end < len(ports) {
end++ // Skip ]
}
case ':':
parts = append(parts, ports[start:end])
end++
start = end
default:
end++
}
}
parts = append(parts, ports[start:end])
return parts
}
func usernsOpts(kind string, opts []string) string {
var res strings.Builder
res.WriteString(kind)
if len(opts) > 0 {
res.WriteString(":")
}
for i, opt := range opts {
if i != 0 {
res.WriteString(",")
}
res.WriteString(opt)
}
return res.String()
}
// Convert a quadlet container file (unit file with a Container group) to a systemd
// service file (unit file with Service group) based on the options in the
// Container group.
// The original Container group is kept around as X-Container.
func ConvertContainer(container *parser.UnitFile, isUser bool) (*parser.UnitFile, error) {
service := container.Dup()
service.Filename = replaceExtension(container.Filename, ".service", "", "")
if container.Path != "" {
service.Add(UnitGroup, "SourcePath", container.Path)
}
if err := checkForUnknownKeys(container, ContainerGroup, supportedContainerKeys); err != nil {
return nil, err
}
// Rename old Container group to x-Container so that systemd ignores it
service.RenameGroup(ContainerGroup, XContainerGroup)
// One image or rootfs must be specified for the container
image, _ := container.Lookup(ContainerGroup, KeyImage)
rootfs, _ := container.Lookup(ContainerGroup, KeyRootfs)
if len(image) == 0 && len(rootfs) == 0 {
return nil, fmt.Errorf("no Image or Rootfs key specified")
}
if len(image) > 0 && len(rootfs) > 0 {
return nil, fmt.Errorf("the Image And Rootfs keys conflict can not be specified together")
}
containerName, ok := container.Lookup(ContainerGroup, KeyContainerName)
if !ok || len(containerName) == 0 {
// By default, We want to name the container by the service name
containerName = "systemd-%N"
}
// Set PODMAN_SYSTEMD_UNIT so that podman auto-update can restart the service.
service.Add(ServiceGroup, "Environment", "PODMAN_SYSTEMD_UNIT=%n")
// Only allow mixed or control-group, as nothing else works well
killMode, ok := service.Lookup(ServiceGroup, "KillMode")
if !ok || !(killMode == "mixed" || killMode == "control-group") {
if ok {
return nil, fmt.Errorf("invalid KillMode '%s'", killMode)
}
// We default to mixed instead of control-group, because it lets conmon do its thing
service.Set(ServiceGroup, "KillMode", "mixed")
}
// Read env early so we can override it below
podmanEnv := container.LookupAllKeyVal(ContainerGroup, KeyEnvironment)
// Need the containers filesystem mounted to start podman
service.Add(UnitGroup, "RequiresMountsFor", "%t/containers")
// If conmon exited uncleanly it may not have removed the container, so
// force it, -i makes it ignore non-existing files.
service.Add(ServiceGroup, "ExecStop", podmanBinary()+" rm -f -i --cidfile=%t/%N.cid")
// The ExecStopPost is needed when the main PID (i.e., conmon) gets killed.
// In that case, ExecStop is not executed but *Post only. If both are
// fired in sequence, *Post will exit when detecting that the --cidfile
// has already been removed by the previous `rm`..
service.Add(ServiceGroup, "ExecStopPost", "-"+podmanBinary()+" rm -f -i --cidfile=%t/%N.cid")
podman := NewPodmanCmdline("run")
podman.addf("--name=%s", containerName)
podman.add(
// We store the container id so we can clean it up in case of failure
"--cidfile=%t/%N.cid",
// And replace any previous container with the same name, not fail
"--replace",
// On clean shutdown, remove container
"--rm",
)
handleLogDriver(container, ContainerGroup, podman)
// We delegate groups to the runtime
service.Add(ServiceGroup, "Delegate", "yes")
podman.add("--cgroups=split")
timezone, ok := container.Lookup(ContainerGroup, KeyTimezone)
if ok && len(timezone) > 0 {
podman.addf("--tz=%s", timezone)
}
addNetworks(container, ContainerGroup, service, podman)
// Run with a pid1 init to reap zombies by default (as most apps don't do that)
runInit, ok := container.LookupBoolean(ContainerGroup, KeyRunInit)
if ok {
podman.addBool("--init", runInit)
}
serviceType, ok := service.Lookup(ServiceGroup, "Type")
if ok && serviceType != "notify" && serviceType != "oneshot" {
return nil, fmt.Errorf("invalid service Type '%s'", serviceType)
}
if serviceType != "oneshot" {
// If we're not in oneshot mode always use some form of sd-notify, normally via conmon,
// but we also allow passing it to the container by setting Notify=yes
notify := container.LookupBooleanWithDefault(ContainerGroup, KeyNotify, false)
if notify {
podman.add("--sdnotify=container")
} else {
podman.add("--sdnotify=conmon")
}
service.Setv(ServiceGroup,
"Type", "notify",
"NotifyAccess", "all")
// Detach from container, we don't need the podman process to hang around
podman.add("-d")
}
if !container.HasKey(ServiceGroup, "SyslogIdentifier") {
service.Set(ServiceGroup, "SyslogIdentifier", "%N")
}
// Default to no higher level privileges or caps
noNewPrivileges := container.LookupBooleanWithDefault(ContainerGroup, KeyNoNewPrivileges, false)
if noNewPrivileges {
podman.add("--security-opt=no-new-privileges")
}
securityLabelDisable := container.LookupBooleanWithDefault(ContainerGroup, KeySecurityLabelDisable, false)
if securityLabelDisable {
podman.add("--security-opt", "label:disable")
}
securityLabelType, _ := container.Lookup(ContainerGroup, KeySecurityLabelType)
if len(securityLabelType) > 0 {
podman.add("--security-opt", fmt.Sprintf("label=type:%s", securityLabelType))
}
securityLabelFileType, _ := container.Lookup(ContainerGroup, KeySecurityLabelFileType)
if len(securityLabelFileType) > 0 {
podman.add("--security-opt", fmt.Sprintf("label=filetype:%s", securityLabelFileType))
}
securityLabelLevel, _ := container.Lookup(ContainerGroup, KeySecurityLabelLevel)
if len(securityLabelLevel) > 0 {
podman.add("--security-opt", fmt.Sprintf("label=level:%s", securityLabelLevel))
}
// But allow overrides with AddCapability
devices := container.LookupAllStrv(ContainerGroup, KeyAddDevice)
for _, device := range devices {
podman.addf("--device=%s", device)
}
// Default to no higher level privileges or caps
seccompProfile, hasSeccompProfile := container.Lookup(ContainerGroup, KeySeccompProfile)
if hasSeccompProfile {
podman.add("--security-opt", fmt.Sprintf("seccomp=%s", seccompProfile))
}
dropCaps := container.LookupAllStrv(ContainerGroup, KeyDropCapability)
for _, caps := range dropCaps {
podman.addf("--cap-drop=%s", strings.ToLower(caps))
}
// But allow overrides with AddCapability
addCaps := container.LookupAllStrv(ContainerGroup, KeyAddCapability)
for _, caps := range addCaps {
podman.addf("--cap-add=%s", strings.ToLower(caps))
}
readOnly, ok := container.LookupBoolean(ContainerGroup, KeyReadOnly)
if ok {
podman.addBool("--read-only", readOnly)
}
volatileTmp := container.LookupBooleanWithDefault(ContainerGroup, KeyVolatileTmp, false)
if volatileTmp {
/* Read only mode already has a tmpfs by default */
if !readOnly {
podman.add("--tmpfs", "/tmp:rw,size=512M,mode=1777")
}
} else if readOnly {
/* !volatileTmp, disable the default tmpfs from --read-only */
podman.add("--read-only-tmpfs=false")
}
hasUser := container.HasKey(ContainerGroup, KeyUser)
hasGroup := container.HasKey(ContainerGroup, KeyGroup)
if hasUser || hasGroup {
uid := container.LookupUint32(ContainerGroup, KeyUser, 0)
gid := container.LookupUint32(ContainerGroup, KeyGroup, 0)
podman.add("--user")
if hasGroup {
podman.addf("%d:%d", uid, gid)
} else {
podman.addf("%d", uid)
}
}
if err := handleUserRemap(container, ContainerGroup, podman, isUser, true); err != nil {
return nil, err
}
volumes := container.LookupAll(ContainerGroup, KeyVolume)
for _, volume := range volumes {
parts := strings.SplitN(volume, ":", 3)
source := ""
var dest string
options := ""
if len(parts) >= 2 {
source = parts[0]
dest = parts[1]
} else {
dest = parts[0]
}
if len(parts) >= 3 {
options = ":" + parts[2]
}
if source != "" {
var err error
source, err = handleStorageSource(container, service, source)
if err != nil {
return nil, err
}
}
podman.add("-v")
if source == "" {
podman.add(dest)
} else {
podman.addf("%s:%s%s", source, dest, options)
}
}
exposedPorts := container.LookupAll(ContainerGroup, KeyExposeHostPort)
for _, exposedPort := range exposedPorts {
exposedPort = strings.TrimSpace(exposedPort) // Allow whitespace after
if !isPortRange(exposedPort) {
return nil, fmt.Errorf("invalid port format '%s'", exposedPort)
}
podman.addf("--expose=%s", exposedPort)
}
if err := handlePublishPorts(container, ContainerGroup, podman); err != nil {
return nil, err
}
podman.addEnv(podmanEnv)
ip, ok := container.Lookup(ContainerGroup, KeyIP)
if ok && len(ip) > 0 {
podman.add("--ip", ip)
}
ip6, ok := container.Lookup(ContainerGroup, KeyIP6)
if ok && len(ip6) > 0 {
podman.add("--ip6", ip6)
}
labels := container.LookupAllKeyVal(ContainerGroup, KeyLabel)
podman.addLabels(labels)
annotations := container.LookupAllKeyVal(ContainerGroup, KeyAnnotation)
podman.addAnnotations(annotations)
envFiles := container.LookupAllArgs(ContainerGroup, KeyEnvironmentFile)
for _, envFile := range envFiles {
filePath, err := getAbsolutePath(container, envFile)
if err != nil {
return nil, err
}
podman.add("--env-file", filePath)
}
if envHost, ok := container.LookupBoolean(ContainerGroup, KeyEnvironmentHost); ok {
podman.addBool("--env-host", envHost)
}
secrets := container.LookupAllArgs(ContainerGroup, KeySecret)
for _, secret := range secrets {
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" {
var err error
if paramSource, ok := paramsMap["source"]; ok {
paramsMap["source"], err = handleStorageSource(container, service, paramSource)
} else if paramSource, ok = paramsMap["src"]; ok {
paramsMap["src"], err = handleStorageSource(container, service, paramSource)
}
if err != nil {
return nil, err
}
}
}
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)
}
handleHealth(container, ContainerGroup, podman)
podmanArgs := container.LookupAllArgs(ContainerGroup, KeyPodmanArgs)
podman.add(podmanArgs...)
if len(image) > 0 {
podman.add(image)
} else {
podman.add("--rootfs", rootfs)
}
execArgs, ok := container.LookupLastArgs(ContainerGroup, KeyExec)
if ok {
podman.add(execArgs...)
}
service.AddCmdline(ServiceGroup, "ExecStart", podman.Args)
return service, nil
}
// Convert a quadlet network file (unit file with a Network group) to a systemd
// service file (unit file with Service group) based on the options in the
// Network group.
// The original Network group is kept around as X-Network.
func ConvertNetwork(network *parser.UnitFile, name string) (*parser.UnitFile, error) {
service := network.Dup()
service.Filename = replaceExtension(network.Filename, ".service", "", "-network")
if err := checkForUnknownKeys(network, NetworkGroup, supportedNetworkKeys); err != nil {
return nil, err
}
/* Rename old Network group to x-Network so that systemd ignores it */
service.RenameGroup(NetworkGroup, XNetworkGroup)
networkName := replaceExtension(name, "", "systemd-", "")
// Need the containers filesystem mounted to start podman
service.Add(UnitGroup, "RequiresMountsFor", "%t/containers")
podman := NewPodmanCmdline("network", "create", "--ignore")
if disableDNS := network.LookupBooleanWithDefault(NetworkGroup, KeyNetworkDisableDNS, false); disableDNS {
podman.add("--disable-dns")
}
driver, ok := network.Lookup(NetworkGroup, KeyNetworkDriver)
if ok && len(driver) > 0 {
podman.addf("--driver=%s", driver)
}
subnets := network.LookupAll(NetworkGroup, KeyNetworkSubnet)
gateways := network.LookupAll(NetworkGroup, KeyNetworkGateway)
ipRanges := network.LookupAll(NetworkGroup, KeyNetworkIPRange)
if len(subnets) > 0 {
if len(gateways) > len(subnets) {
return nil, fmt.Errorf("cannot set more gateways than subnets")
}
if len(ipRanges) > len(subnets) {
return nil, fmt.Errorf("cannot set more ranges than subnets")
}
for i := range subnets {
podman.addf("--subnet=%s", subnets[i])
if len(gateways) > i {
podman.addf("--gateway=%s", gateways[i])
}
if len(ipRanges) > i {
podman.addf("--ip-range=%s", ipRanges[i])
}
}
} else if len(ipRanges) > 0 || len(gateways) > 0 {
return nil, fmt.Errorf("cannot set gateway or range without subnet")
}
if internal := network.LookupBooleanWithDefault(NetworkGroup, KeyNetworkInternal, false); internal {
podman.add("--internal")
}
if ipamDriver, ok := network.Lookup(NetworkGroup, KeyNetworkIPAMDriver); ok && len(ipamDriver) > 0 {
podman.addf("--ipam-driver=%s", ipamDriver)
}
if ipv6 := network.LookupBooleanWithDefault(NetworkGroup, KeyNetworkIPv6, false); ipv6 {
podman.add("--ipv6")
}
networkOptions := network.LookupAllKeyVal(NetworkGroup, KeyNetworkOptions)
if len(networkOptions) > 0 {
podman.addKeys("--opt", networkOptions)
}
if labels := network.LookupAllKeyVal(NetworkGroup, KeyLabel); len(labels) > 0 {
podman.addLabels(labels)
}
podman.add(networkName)
service.AddCmdline(ServiceGroup, "ExecStart", podman.Args)
service.Setv(ServiceGroup,
"Type", "oneshot",
"RemainAfterExit", "yes",
// The default syslog identifier is the exec basename (podman) which isn't very useful here
"SyslogIdentifier", "%N")
return service, nil
}
// Convert a quadlet volume file (unit file with a Volume group) to a systemd
// service file (unit file with Service group) based on the options in the
// Volume group.
// The original Volume group is kept around as X-Volume.
func ConvertVolume(volume *parser.UnitFile, name string) (*parser.UnitFile, error) {
service := volume.Dup()
service.Filename = replaceExtension(volume.Filename, ".service", "", "-volume")
if err := checkForUnknownKeys(volume, VolumeGroup, supportedVolumeKeys); err != nil {
return nil, err
}
/* Rename old Volume group to x-Volume so that systemd ignores it */
service.RenameGroup(VolumeGroup, XVolumeGroup)
volumeName := replaceExtension(name, "", "systemd-", "")
// Need the containers filesystem mounted to start podman
service.Add(UnitGroup, "RequiresMountsFor", "%t/containers")
labels := volume.LookupAllKeyVal(VolumeGroup, "Label")
podman := NewPodmanCmdline("volume", "create", "--ignore")
var opts strings.Builder
opts.WriteString("o=")
if volume.HasKey(VolumeGroup, "User") {
uid := volume.LookupUint32(VolumeGroup, "User", 0)
if opts.Len() > 2 {
opts.WriteString(",")
}
opts.WriteString(fmt.Sprintf("uid=%d", uid))
}
if volume.HasKey(VolumeGroup, "Group") {
gid := volume.LookupUint32(VolumeGroup, "Group", 0)
if opts.Len() > 2 {
opts.WriteString(",")
}
opts.WriteString(fmt.Sprintf("gid=%d", gid))
}
copy, ok := volume.LookupBoolean(VolumeGroup, KeyCopy)
if ok {
if copy {
podman.add("--opt", "copy")
} else {
podman.add("--opt", "nocopy")
}
}
devValid := false
dev, ok := volume.Lookup(VolumeGroup, KeyDevice)
if ok && len(dev) != 0 {
podman.add("--opt", fmt.Sprintf("device=%s", dev))
devValid = true
}
devType, ok := volume.Lookup(VolumeGroup, KeyType)
if ok && len(devType) != 0 {
if devValid {
podman.add("--opt", fmt.Sprintf("type=%s", devType))
} else {
return nil, fmt.Errorf("key Type can't be used without Device")
}
}
mountOpts, ok := volume.Lookup(VolumeGroup, KeyOptions)
if ok && len(mountOpts) != 0 {
if devValid {
if opts.Len() > 2 {
opts.WriteString(",")
}
opts.WriteString(mountOpts)
} else {
return nil, fmt.Errorf("key Options can't be used without Device")
}
}
if opts.Len() > 2 {
podman.add("--opt", opts.String())
}
podman.addLabels(labels)
podman.add(volumeName)
service.AddCmdline(ServiceGroup, "ExecStart", podman.Args)
service.Setv(ServiceGroup,
"Type", "oneshot",
"RemainAfterExit", "yes",
// The default syslog identifier is the exec basename (podman) which isn't very useful here
"SyslogIdentifier", "%N")
return service, nil
}
func ConvertKube(kube *parser.UnitFile, isUser bool) (*parser.UnitFile, error) {
service := kube.Dup()
service.Filename = replaceExtension(kube.Filename, ".service", "", "")
if kube.Path != "" {
service.Add(UnitGroup, "SourcePath", kube.Path)
}
if err := checkForUnknownKeys(kube, KubeGroup, supportedKubeKeys); err != nil {
return nil, err
}
// Rename old Kube group to x-Kube so that systemd ignores it
service.RenameGroup(KubeGroup, XKubeGroup)
yamlPath, ok := kube.Lookup(KubeGroup, KeyYaml)
if !ok || len(yamlPath) == 0 {
return nil, fmt.Errorf("no Yaml key specified")
}
yamlPath, err := getAbsolutePath(kube, yamlPath)
if err != nil {
return nil, err
}
// Only allow mixed or control-group, as nothing else works well
killMode, ok := service.Lookup(ServiceGroup, "KillMode")
if !ok || !(killMode == "mixed" || killMode == "control-group") {
if ok {
return nil, fmt.Errorf("invalid KillMode '%s'", killMode)
}
// We default to mixed instead of control-group, because it lets conmon do its thing
service.Set(ServiceGroup, "KillMode", "mixed")
}
// Set PODMAN_SYSTEMD_UNIT so that podman auto-update can restart the service.
service.Add(ServiceGroup, "Environment", "PODMAN_SYSTEMD_UNIT=%n")
// Need the containers filesystem mounted to start podman
service.Add(UnitGroup, "RequiresMountsFor", "%t/containers")
service.Setv(ServiceGroup,
"Type", "notify",
"NotifyAccess", "all")
if !kube.HasKey(ServiceGroup, "SyslogIdentifier") {
service.Set(ServiceGroup, "SyslogIdentifier", "%N")
}
execStart := NewPodmanCmdline("kube", "play")
execStart.add(
// Replace any previous container with the same name, not fail
"--replace",
// Use a service container
"--service-container=true",
)
handleLogDriver(kube, KubeGroup, execStart)
if err := handleUserRemap(kube, KubeGroup, execStart, isUser, false); err != nil {
return nil, err
}
addNetworks(kube, KubeGroup, service, execStart)
configMaps := kube.LookupAllStrv(KubeGroup, KeyConfigMap)
for _, configMap := range configMaps {
configMapPath, err := getAbsolutePath(kube, configMap)
if err != nil {
return nil, err
}
execStart.add("--configmap", configMapPath)
}
if err := handlePublishPorts(kube, KubeGroup, execStart); err != nil {
return nil, err
}
execStart.add(yamlPath)
service.AddCmdline(ServiceGroup, "ExecStart", execStart.Args)
execStop := NewPodmanCmdline("kube", "down")
execStop.add(yamlPath)
service.AddCmdline(ServiceGroup, "ExecStop", execStop.Args)
return service, nil
}
func handleUserRemap(unitFile *parser.UnitFile, groupName string, podman *PodmanCmdline, isUser, supportManual bool) error {
uidMaps := unitFile.LookupAllStrv(groupName, KeyRemapUID)
gidMaps := unitFile.LookupAllStrv(groupName, KeyRemapGID)
remapUsers, _ := unitFile.LookupLast(groupName, KeyRemapUsers)
switch remapUsers {
case "":
if len(uidMaps) > 0 {
return fmt.Errorf("UidMap set without RemapUsers")
}
if len(gidMaps) > 0 {
return fmt.Errorf("GidMap set without RemapUsers")
}
case "manual":
if supportManual {
for _, uidMap := range uidMaps {
podman.addf("--uidmap=%s", uidMap)
}
for _, gidMap := range gidMaps {
podman.addf("--gidmap=%s", gidMap)
}
} else {
return fmt.Errorf("RemapUsers=manual is not supported")
}
case "auto":
autoOpts := make([]string, 0)
for _, uidMap := range uidMaps {
autoOpts = append(autoOpts, "uidmapping="+uidMap)
}
for _, gidMap := range gidMaps {
autoOpts = append(autoOpts, "gidmapping="+gidMap)
}
uidSize := unitFile.LookupUint32(groupName, KeyRemapUIDSize, 0)
if uidSize > 0 {
autoOpts = append(autoOpts, fmt.Sprintf("size=%v", uidSize))
}
podman.addf("--userns=" + usernsOpts("auto", autoOpts))
case "keep-id":
if !isUser {
return fmt.Errorf("RemapUsers=keep-id is unsupported for system units")
}
podman.addf("--userns=keep-id")
default:
return fmt.Errorf("unsupported RemapUsers option '%s'", remapUsers)
}
return nil
}
func addNetworks(quadletUnitFile *parser.UnitFile, groupName string, serviceUnitFile *parser.UnitFile, podman *PodmanCmdline) {
networks := quadletUnitFile.LookupAll(groupName, KeyNetwork)
for _, network := range networks {
if len(network) > 0 {
quadletNetworkName, options, found := strings.Cut(network, ":")
if strings.HasSuffix(quadletNetworkName, ".network") {
// the podman network name is systemd-$name
networkName := replaceExtension(quadletNetworkName, "", "systemd-", "")
// the systemd unit name is $name-network.service
networkServiceName := replaceExtension(quadletNetworkName, ".service", "", "-network")
serviceUnitFile.Add(UnitGroup, "Requires", networkServiceName)
serviceUnitFile.Add(UnitGroup, "After", networkServiceName)
if found {
network = fmt.Sprintf("%s:%s", networkName, options)
} else {
network = networkName
}
}
podman.addf("--network=%s", network)
}
}
}
func getAbsolutePath(quadletUnitFile *parser.UnitFile, filePath string) (string, error) {
if !filepath.IsAbs(filePath) {
if len(quadletUnitFile.Path) > 0 {
filePath = filepath.Join(filepath.Dir(quadletUnitFile.Path), filePath)
} else {
var err error
filePath, err = filepath.Abs(filePath)
if err != nil {
return "", err
}
}
}
return filePath, nil
}
func handlePublishPorts(unitFile *parser.UnitFile, groupName string, podman *PodmanCmdline) error {
publishPorts := unitFile.LookupAll(groupName, KeyPublishPort)
for _, publishPort := range publishPorts {
publishPort = strings.TrimSpace(publishPort) // Allow whitespace after
// IP address could have colons in it. For example: "[::]:8080:80/tcp, so use custom splitter
parts := splitPorts(publishPort)
var containerPort string
ip := ""
hostPort := ""
// format (from podman run):
// ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort
//
// ip could be IPv6 with minimum of these chars "[::]"
// containerPort can have a suffix of "/tcp" or "/udp"
//
switch len(parts) {
case 1:
containerPort = parts[0]
case 2:
hostPort = parts[0]
containerPort = parts[1]
case 3:
ip = parts[0]
hostPort = parts[1]
containerPort = parts[2]
default:
return fmt.Errorf("invalid published port '%s'", publishPort)
}
if ip == "0.0.0.0" {
ip = ""
}
if len(hostPort) > 0 && !isPortRange(hostPort) {
return fmt.Errorf("invalid port format '%s'", hostPort)
}
if len(containerPort) > 0 && !isPortRange(containerPort) {
return fmt.Errorf("invalid port format '%s'", containerPort)
}
podman.add("--publish")
switch {
case len(ip) > 0 && len(hostPort) > 0:
podman.addf("%s:%s:%s", ip, hostPort, containerPort)
case len(ip) > 0:
podman.addf("%s::%s", ip, containerPort)
case len(hostPort) > 0:
podman.addf("%s:%s", hostPort, containerPort)
default:
podman.addf("%s", containerPort)
}
}
return nil
}
func handleLogDriver(unitFile *parser.UnitFile, groupName string, podman *PodmanCmdline) {
logDriver, found := unitFile.Lookup(groupName, KeyLogDriver)
if !found {
logDriver = defaultLogDriver
}
podman.add("--log-driver", logDriver)
}
func handleStorageSource(quadletUnitFile, serviceUnitFile *parser.UnitFile, source string) (string, error) {
if source[0] == '.' {
var err error
source, err = getAbsolutePath(quadletUnitFile, source)
if err != nil {
return "", err
}
}
if source[0] == '/' {
// Absolute path
serviceUnitFile.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
serviceUnitFile.Add(UnitGroup, "Requires", volumeServiceName)
serviceUnitFile.Add(UnitGroup, "After", volumeServiceName)
}
return source, nil
}
func handleHealth(unitFile *parser.UnitFile, groupName string, podman *PodmanCmdline) {
keyArgMap := [][2]string{
{KeyHealthCmd, "cmd"},
{KeyHealthInterval, "interval"},
{KeyHealthOnFailure, "on-failure"},
{KeyHealthRetries, "retries"},
{KeyHealthStartPeriod, "start-period"},
{KeyHealthTimeout, "timeout"},
{KeyHealthStartupCmd, "startup-cmd"},
{KeyHealthStartupInterval, "startup-interval"},
{KeyHealthStartupRetries, "startup-retries"},
{KeyHealthStartupSuccess, "startup-success"},
{KeyHealthStartupTimeout, "startup-timeout"},
}
for _, keyArg := range keyArgMap {
val, found := unitFile.Lookup(groupName, keyArg[0])
if found && len(val) > 0 {
podman.addf("--health-%s", keyArg[1])
podman.addf("%s", val)
}
}
}