spiegel_podman/pkg/systemd/quadlet/quadlet.go
Paul Holzinger 67e0fa8b89
quadlet: add default network dependencies to all units
There is no good reason for the special case, kube and pod units
definitely need it. Volume and network units maybe not but for
consistency we add it there as well. This makes the docs much easier to
write and understand for users as the behavior will not differ.

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
2024-10-18 14:01:22 +02:00

2195 lines
66 KiB
Go

package quadlet
import (
"bytes"
"encoding/csv"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/containers/podman/v5/pkg/specgenutilexternal"
"github.com/containers/podman/v5/pkg/systemd/parser"
"github.com/containers/storage/pkg/fileutils"
"github.com/containers/storage/pkg/regexp"
)
const (
// Fixme should use
// github.com/containers/podman/v5/libpod/define.AutoUpdateLabel
// but it is causing bloat
autoUpdateLabel = "io.containers.autoupdate"
// Directory for temporary Quadlet files (sysadmin owned)
UnitDirTemp = "/run/containers/systemd"
// 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"
PodGroup = "Pod"
ServiceGroup = "Service"
UnitGroup = "Unit"
VolumeGroup = "Volume"
ImageGroup = "Image"
BuildGroup = "Build"
QuadletGroup = "Quadlet"
XContainerGroup = "X-Container"
XKubeGroup = "X-Kube"
XNetworkGroup = "X-Network"
XPodGroup = "X-Pod"
XVolumeGroup = "X-Volume"
XImageGroup = "X-Image"
XBuildGroup = "X-Build"
XQuadletGroup = "X-Quadlet"
)
// Systemd Unit file keys
const (
ServiceKeyWorkingDirectory = "WorkingDirectory"
)
// All the supported quadlet keys
const (
KeyAddCapability = "AddCapability"
KeyAddDevice = "AddDevice"
KeyAddHost = "AddHost"
KeyAllTags = "AllTags"
KeyAnnotation = "Annotation"
KeyArch = "Arch"
KeyAuthFile = "AuthFile"
KeyAutoUpdate = "AutoUpdate"
KeyCertDir = "CertDir"
KeyCgroupsMode = "CgroupsMode"
KeyConfigMap = "ConfigMap"
KeyContainerName = "ContainerName"
KeyContainersConfModule = "ContainersConfModule"
KeyCopy = "Copy"
KeyCreds = "Creds"
KeyDecryptionKey = "DecryptionKey"
KeyDefaultDependencies = "DefaultDependencies"
KeyDevice = "Device"
KeyDisableDNS = "DisableDNS"
KeyDNS = "DNS"
KeyDNSOption = "DNSOption"
KeyDNSSearch = "DNSSearch"
KeyDriver = "Driver"
KeyDropCapability = "DropCapability"
KeyEntrypoint = "Entrypoint"
KeyEnvironment = "Environment"
KeyEnvironmentFile = "EnvironmentFile"
KeyEnvironmentHost = "EnvironmentHost"
KeyExec = "Exec"
KeyExitCodePropagation = "ExitCodePropagation"
KeyExposeHostPort = "ExposeHostPort"
KeyFile = "File"
KeyForceRM = "ForceRM"
KeyGateway = "Gateway"
KeyGIDMap = "GIDMap"
KeyGlobalArgs = "GlobalArgs"
KeyGroup = "Group"
KeyGroupAdd = "GroupAdd"
KeyHealthCmd = "HealthCmd"
KeyHealthInterval = "HealthInterval"
KeyHealthLogDestination = "HealthLogDestination"
KeyHealthMaxLogCount = "HealthMaxLogCount"
KeyHealthMaxLogSize = "HealthMaxLogSize"
KeyHealthOnFailure = "HealthOnFailure"
KeyHealthRetries = "HealthRetries"
KeyHealthStartPeriod = "HealthStartPeriod"
KeyHealthStartupCmd = "HealthStartupCmd"
KeyHealthStartupInterval = "HealthStartupInterval"
KeyHealthStartupRetries = "HealthStartupRetries"
KeyHealthStartupSuccess = "HealthStartupSuccess"
KeyHealthStartupTimeout = "HealthStartupTimeout"
KeyHealthTimeout = "HealthTimeout"
KeyHostName = "HostName"
KeyImage = "Image"
KeyImageTag = "ImageTag"
KeyInternal = "Internal"
KeyIP = "IP"
KeyIP6 = "IP6"
KeyIPAMDriver = "IPAMDriver"
KeyIPRange = "IPRange"
KeyIPv6 = "IPv6"
KeyKubeDownForce = "KubeDownForce"
KeyLabel = "Label"
KeyLogDriver = "LogDriver"
KeyLogOpt = "LogOpt"
KeyMask = "Mask"
KeyMount = "Mount"
KeyNetwork = "Network"
KeyNetworkAlias = "NetworkAlias"
KeyNetworkName = "NetworkName"
KeyNoNewPrivileges = "NoNewPrivileges"
KeyNotify = "Notify"
KeyOptions = "Options"
KeyOS = "OS"
KeyPidsLimit = "PidsLimit"
KeyPod = "Pod"
KeyPodmanArgs = "PodmanArgs"
KeyPodName = "PodName"
KeyPublishPort = "PublishPort"
KeyPull = "Pull"
KeyReadOnly = "ReadOnly"
KeyReadOnlyTmpfs = "ReadOnlyTmpfs"
KeyRemapGid = "RemapGid" //nolint:stylecheck // deprecated
KeyRemapUid = "RemapUid" //nolint:stylecheck // deprecated
KeyRemapUidSize = "RemapUidSize" //nolint:stylecheck // deprecated
KeyRemapUsers = "RemapUsers" // deprecated
KeyRootfs = "Rootfs"
KeyRunInit = "RunInit"
KeySeccompProfile = "SeccompProfile"
KeySecret = "Secret"
KeySecurityLabelDisable = "SecurityLabelDisable"
KeySecurityLabelFileType = "SecurityLabelFileType"
KeySecurityLabelLevel = "SecurityLabelLevel"
KeySecurityLabelNested = "SecurityLabelNested"
KeySecurityLabelType = "SecurityLabelType"
KeyServiceName = "ServiceName"
KeySetWorkingDirectory = "SetWorkingDirectory"
KeyShmSize = "ShmSize"
KeyStopSignal = "StopSignal"
KeyStopTimeout = "StopTimeout"
KeySubGIDMap = "SubGIDMap"
KeySubnet = "Subnet"
KeySubUIDMap = "SubUIDMap"
KeySysctl = "Sysctl"
KeyTarget = "Target"
KeyTimezone = "Timezone"
KeyTLSVerify = "TLSVerify"
KeyTmpfs = "Tmpfs"
KeyType = "Type"
KeyUIDMap = "UIDMap"
KeyUlimit = "Ulimit"
KeyUnmask = "Unmask"
KeyUser = "User"
KeyUserNS = "UserNS"
KeyVariant = "Variant"
KeyVolatileTmp = "VolatileTmp" // deprecated
KeyVolume = "Volume"
KeyVolumeName = "VolumeName"
KeyWorkingDir = "WorkingDir"
KeyYaml = "Yaml"
)
type UnitInfo struct {
// The name of the generated systemd service unit
ServiceName string
// The name of the podman resource created by the service
ResourceName string
// For .pod units
// List of containers in a pod
Containers []string
}
var (
URL = regexp.Delayed(`^((https?)|(git)://)|(github\.com/).+$`)
validPortRange = regexp.Delayed(`\d+(-\d+)?(/udp|/tcp)?$`)
// Supported keys in "Container" group
supportedContainerKeys = map[string]bool{
KeyAddCapability: true,
KeyAddDevice: true,
KeyAddHost: true,
KeyAnnotation: true,
KeyAutoUpdate: true,
KeyCgroupsMode: true,
KeyContainerName: true,
KeyContainersConfModule: true,
KeyDNS: true,
KeyDNSOption: true,
KeyDNSSearch: true,
KeyDropCapability: true,
KeyEnvironment: true,
KeyEnvironmentFile: true,
KeyEnvironmentHost: true,
KeyEntrypoint: true,
KeyExec: true,
KeyExposeHostPort: true,
KeyGIDMap: true,
KeyGlobalArgs: true,
KeyGroup: true,
KeyGroupAdd: true,
KeyHealthCmd: true,
KeyHealthInterval: true,
KeyHealthOnFailure: true,
KeyHealthLogDestination: true,
KeyHealthMaxLogCount: true,
KeyHealthMaxLogSize: true,
KeyHealthRetries: true,
KeyHealthStartPeriod: true,
KeyHealthStartupCmd: true,
KeyHealthStartupInterval: true,
KeyHealthStartupRetries: true,
KeyHealthStartupSuccess: true,
KeyHealthStartupTimeout: true,
KeyHealthTimeout: true,
KeyHostName: true,
KeyIP6: true,
KeyIP: true,
KeyImage: true,
KeyLabel: true,
KeyLogDriver: true,
KeyLogOpt: true,
KeyMask: true,
KeyMount: true,
KeyNetwork: true,
KeyNetworkAlias: true,
KeyNoNewPrivileges: true,
KeyNotify: true,
KeyPidsLimit: true,
KeyPod: true,
KeyPodmanArgs: true,
KeyPublishPort: true,
KeyPull: true,
KeyReadOnly: true,
KeyReadOnlyTmpfs: true,
KeyRemapGid: true,
KeyRemapUid: true,
KeyRemapUidSize: true,
KeyRemapUsers: true,
KeyRootfs: true,
KeyRunInit: true,
KeySeccompProfile: true,
KeySecret: true,
KeySecurityLabelDisable: true,
KeySecurityLabelFileType: true,
KeySecurityLabelLevel: true,
KeySecurityLabelNested: true,
KeySecurityLabelType: true,
KeyServiceName: true,
KeyShmSize: true,
KeyStopSignal: true,
KeyStopTimeout: true,
KeySubGIDMap: true,
KeySubUIDMap: true,
KeySysctl: true,
KeyTimezone: true,
KeyTmpfs: true,
KeyUIDMap: true,
KeyUlimit: true,
KeyUnmask: true,
KeyUser: true,
KeyUserNS: true,
KeyVolatileTmp: true,
KeyVolume: true,
KeyWorkingDir: true,
}
// Supported keys in "Volume" group
supportedVolumeKeys = map[string]bool{
KeyContainersConfModule: true,
KeyCopy: true,
KeyDevice: true,
KeyDriver: true,
KeyGlobalArgs: true,
KeyGroup: true,
KeyImage: true,
KeyLabel: true,
KeyOptions: true,
KeyPodmanArgs: true,
KeyServiceName: true,
KeyType: true,
KeyUser: true,
KeyVolumeName: true,
}
// Supported keys in "Network" group
supportedNetworkKeys = map[string]bool{
KeyLabel: true,
KeyDNS: true,
KeyContainersConfModule: true,
KeyGlobalArgs: true,
KeyDisableDNS: true,
KeyDriver: true,
KeyGateway: true,
KeyIPAMDriver: true,
KeyIPRange: true,
KeyIPv6: true,
KeyInternal: true,
KeyNetworkName: true,
KeyOptions: true,
KeyServiceName: true,
KeySubnet: true,
KeyPodmanArgs: true,
}
// Supported keys in "Kube" group
supportedKubeKeys = map[string]bool{
KeyAutoUpdate: true,
KeyConfigMap: true,
KeyContainersConfModule: true,
KeyExitCodePropagation: true,
KeyGlobalArgs: true,
KeyKubeDownForce: true,
KeyLogDriver: true,
KeyLogOpt: true,
KeyNetwork: true,
KeyPodmanArgs: true,
KeyPublishPort: true,
KeyRemapGid: true,
KeyRemapUid: true,
KeyRemapUidSize: true,
KeyRemapUsers: true,
KeyServiceName: true,
KeySetWorkingDirectory: true,
KeyUserNS: true,
KeyYaml: true,
}
// Supported keys in "Image" group
supportedImageKeys = map[string]bool{
KeyAllTags: true,
KeyArch: true,
KeyAuthFile: true,
KeyCertDir: true,
KeyContainersConfModule: true,
KeyCreds: true,
KeyDecryptionKey: true,
KeyGlobalArgs: true,
KeyImage: true,
KeyImageTag: true,
KeyOS: true,
KeyPodmanArgs: true,
KeyServiceName: true,
KeyTLSVerify: true,
KeyVariant: true,
}
// Supported keys in "Build" group
supportedBuildKeys = map[string]bool{
KeyAnnotation: true,
KeyArch: true,
KeyAuthFile: true,
KeyContainersConfModule: true,
KeyDNS: true,
KeyDNSOption: true,
KeyDNSSearch: true,
KeyEnvironment: true,
KeyFile: true,
KeyForceRM: true,
KeyGlobalArgs: true,
KeyGroupAdd: true,
KeyImageTag: true,
KeyLabel: true,
KeyNetwork: true,
KeyPodmanArgs: true,
KeyPull: true,
KeySecret: true,
KeyServiceName: true,
KeySetWorkingDirectory: true,
KeyTarget: true,
KeyTLSVerify: true,
KeyVariant: true,
KeyVolume: true,
}
supportedPodKeys = map[string]bool{
KeyAddHost: true,
KeyContainersConfModule: true,
KeyDNS: true,
KeyDNSOption: true,
KeyDNSSearch: true,
KeyGIDMap: true,
KeyGlobalArgs: true,
KeyIP: true,
KeyIP6: true,
KeyNetwork: true,
KeyNetworkAlias: true,
KeyPodName: true,
KeyPodmanArgs: true,
KeyPublishPort: true,
KeyRemapGid: true,
KeyRemapUid: true,
KeyRemapUidSize: true,
KeyRemapUsers: true,
KeyServiceName: true,
KeySubGIDMap: true,
KeySubUIDMap: true,
KeyUIDMap: true,
KeyUserNS: true,
KeyVolume: true,
}
// Supported keys in "Quadlet" group
supportedQuadletKeys = map[string]bool{
KeyDefaultDependencies: true,
}
)
func (u *UnitInfo) ServiceFileName() string {
return fmt.Sprintf("%s.service", u.ServiceName)
}
func removeExtension(name string, extraPrefix string, extraSuffix string) string {
baseName := name
dot := strings.LastIndexByte(name, '.')
if dot > 0 {
baseName = name[:dot]
}
return extraPrefix + baseName + extraSuffix
}
func isURL(urlCandidate string) bool {
return URL.MatchString(urlCandidate)
}
func isPortRange(port string) bool {
return validPortRange.MatchString(port)
}
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)
// 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, unitsInfoMap map[string]*UnitInfo) (*parser.UnitFile, error) {
unitInfo, ok := unitsInfoMap[container.Filename]
if !ok {
return nil, fmt.Errorf("internal error while processing container %s", container.Filename)
}
service := container.Dup()
service.Filename = unitInfo.ServiceFileName()
addDefaultDependencies(service, isUser)
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)
// 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)
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")
}
if len(image) > 0 {
var err error
if image, err = handleImageSource(image, service, unitsInfoMap); err != nil {
return nil, err
}
}
containerName, ok := container.Lookup(ContainerGroup, KeyContainerName)
if !ok || len(containerName) == 0 {
// By default, We want to name the container by the service name
if strings.Contains(container.Filename, "@") {
containerName = "systemd-%p_%i"
} else {
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.
serviceStopCmd := createBasePodmanCommand(container, ContainerGroup)
serviceStopCmd.add("rm", "-v", "-f", "-i", "--cidfile=%t/%N.cid")
service.AddCmdline(ServiceGroup, "ExecStop", serviceStopCmd.Args)
// 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`..
serviceStopCmd.Args[0] = fmt.Sprintf("-%s", serviceStopCmd.Args[0])
service.AddCmdline(ServiceGroup, "ExecStopPost", serviceStopCmd.Args)
podman := createBasePodmanCommand(container, ContainerGroup)
podman.add("run")
podman.add("--name", 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)
handleLogOpt(container, ContainerGroup, podman)
// We delegate groups to the runtime
service.Add(ServiceGroup, "Delegate", "yes")
if cgroupsMode, ok := container.Lookup(ContainerGroup, KeyCgroupsMode); ok && len(cgroupsMode) > 0 {
podman.add("--cgroups", cgroupsMode)
} else {
podman.add("--cgroups=split")
}
stringKeys := map[string]string{
KeyTimezone: "--tz",
KeyPidsLimit: "--pids-limit",
KeyShmSize: "--shm-size",
KeyEntrypoint: "--entrypoint",
KeyWorkingDir: "--workdir",
KeyIP: "--ip",
KeyIP6: "--ip6",
KeyHostName: "--hostname",
KeyStopSignal: "--stop-signal",
KeyStopTimeout: "--stop-timeout",
KeyPull: "--pull",
}
lookupAndAddString(container, ContainerGroup, stringKeys, podman)
allStringsKeys := map[string]string{
KeyNetworkAlias: "--network-alias",
KeyUlimit: "--ulimit",
KeyDNS: "--dns",
KeyDNSOption: "--dns-option",
KeyDNSSearch: "--dns-search",
KeyGroupAdd: "--group-add",
KeyAddHost: "--add-host",
KeyTmpfs: "--tmpfs",
}
lookupAndAddAllStrings(container, ContainerGroup, allStringsKeys, podman)
boolKeys := map[string]string{
KeyRunInit: "--init",
KeyEnvironmentHost: "--env-host",
KeyReadOnlyTmpfs: "--read-only-tmpfs",
}
lookupAndAddBoolean(container, ContainerGroup, boolKeys, podman)
if err := addNetworks(container, ContainerGroup, service, unitsInfoMap, podman); err != nil {
return nil, err
}
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, ok := container.Lookup(ContainerGroup, KeyNotify)
switch {
case ok && strings.EqualFold(notify, "healthy"):
podman.add("--sdnotify=healthy")
case container.LookupBooleanWithDefault(ContainerGroup, KeyNotify, false):
podman.add("--sdnotify=container")
default:
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")
}
securityLabelNested := container.LookupBooleanWithDefault(ContainerGroup, KeySecurityLabelNested, false)
if securityLabelNested {
podman.add("--security-opt", "label=nested")
}
securityLabelType, ok := container.Lookup(ContainerGroup, KeySecurityLabelType)
if ok && len(securityLabelType) > 0 {
podman.add("--security-opt", fmt.Sprintf("label=type:%s", securityLabelType))
}
securityLabelFileType, ok := container.Lookup(ContainerGroup, KeySecurityLabelFileType)
if ok && len(securityLabelFileType) > 0 {
podman.add("--security-opt", fmt.Sprintf("label=filetype:%s", securityLabelFileType))
}
securityLabelLevel, ok := container.Lookup(ContainerGroup, KeySecurityLabelLevel)
if ok && len(securityLabelLevel) > 0 {
podman.add("--security-opt", fmt.Sprintf("label=level:%s", securityLabelLevel))
}
devices := container.LookupAllStrv(ContainerGroup, KeyAddDevice)
for _, device := range devices {
if device[0] == '-' {
device = device[1:]
err := fileutils.Exists(strings.Split(device, ":")[0])
if errors.Is(err, os.ErrNotExist) {
continue
}
}
podman.add("--device", 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.add("--cap-drop", strings.ToLower(caps))
}
// But allow overrides with AddCapability
addCaps := container.LookupAllStrv(ContainerGroup, KeyAddCapability)
for _, caps := range addCaps {
podman.add("--cap-add", strings.ToLower(caps))
}
sysctl := container.LookupAllStrv(ContainerGroup, KeySysctl)
for _, sysctlItem := range sysctl {
podman.add("--sysctl", sysctlItem)
}
// This was not moved to the generic handling since readOnly is used also with volatileTmp
readOnly, ok := container.LookupBoolean(ContainerGroup, KeyReadOnly)
if ok {
podman.addBool("--read-only", readOnly)
}
volatileTmp := container.LookupBooleanWithDefault(ContainerGroup, KeyVolatileTmp, false)
if volatileTmp && !readOnly {
podman.add("--tmpfs", "/tmp:rw,size=512M,mode=1777")
}
if err := handleUser(container, ContainerGroup, podman); err != nil {
return nil, err
}
if err := handleUserMappings(container, ContainerGroup, podman, true); err != nil {
return nil, err
}
if err := addVolumes(container, service, ContainerGroup, unitsInfoMap, podman); err != nil {
return nil, err
}
update, ok := container.Lookup(ContainerGroup, KeyAutoUpdate)
if ok && len(update) > 0 {
podman.addLabels(map[string]string{
autoUpdateLabel: update,
})
}
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.add("--expose", exposedPort)
}
handlePublishPorts(container, ContainerGroup, podman)
podman.addEnv(podmanEnv)
labels := container.LookupAllKeyVal(ContainerGroup, KeyLabel)
podman.addLabels(labels)
annotations := container.LookupAllKeyVal(ContainerGroup, KeyAnnotation)
podman.addAnnotations(annotations)
masks := container.LookupAllArgs(ContainerGroup, KeyMask)
for _, mask := range masks {
podman.add("--security-opt", fmt.Sprintf("mask=%s", mask))
}
unmasks := container.LookupAllArgs(ContainerGroup, KeyUnmask)
for _, unmask := range unmasks {
podman.add("--security-opt", fmt.Sprintf("unmask=%s", unmask))
}
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)
}
secrets := container.LookupAllArgs(ContainerGroup, KeySecret)
for _, secret := range secrets {
podman.add("--secret", secret)
}
mounts := container.LookupAllArgs(ContainerGroup, KeyMount)
for _, mount := range mounts {
mountStr, err := resolveContainerMountParams(container, service, mount, unitsInfoMap)
if err != nil {
return nil, err
}
podman.add("--mount", mountStr)
}
handleHealth(container, ContainerGroup, podman)
if err := handlePod(container, service, ContainerGroup, unitsInfoMap, podman); err != nil {
return nil, err
}
handlePodmanArgs(container, ContainerGroup, podman)
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)
// XXX: only %N is handled.
// it is difficult to properly implement specifiers handling without consulting systemd.
resourceName := strings.ReplaceAll(containerName, "%N", unitInfo.ServiceName)
if !strings.Contains(resourceName, "%") {
unitInfo.ResourceName = resourceName
}
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.
// Also returns the canonical network name, either auto-generated or user-defined via the
// NetworkName key-value.
func ConvertNetwork(network *parser.UnitFile, name string, unitsInfoMap map[string]*UnitInfo, isUser bool) (*parser.UnitFile, error) {
unitInfo, ok := unitsInfoMap[network.Filename]
if !ok {
return nil, fmt.Errorf("internal error while processing network %s", network.Filename)
}
service := network.Dup()
service.Filename = unitInfo.ServiceFileName()
addDefaultDependencies(service, isUser)
if network.Path != "" {
service.Add(UnitGroup, "SourcePath", network.Path)
}
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)
// 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 {
networkName = removeExtension(name, "systemd-", "")
}
// Need the containers filesystem mounted to start podman
service.Add(UnitGroup, "RequiresMountsFor", "%t/containers")
podman := createBasePodmanCommand(network, NetworkGroup)
podman.add("network", "create", "--ignore")
boolKeys := map[string]string{
KeyDisableDNS: "--disable-dns",
KeyInternal: "--internal",
KeyIPv6: "--ipv6",
}
lookupAndAddBoolean(network, NetworkGroup, boolKeys, podman)
stringKeys := map[string]string{
KeyDriver: "--driver",
KeyIPAMDriver: "--ipam-driver",
}
lookupAndAddString(network, NetworkGroup, stringKeys, podman)
allStringKeys := map[string]string{
KeyDNS: "--dns",
}
lookupAndAddAllStrings(network, NetworkGroup, allStringKeys, podman)
subnets := network.LookupAll(NetworkGroup, KeySubnet)
gateways := network.LookupAll(NetworkGroup, KeyGateway)
ipRanges := network.LookupAll(NetworkGroup, KeyIPRange)
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.add("--subnet", subnets[i])
if len(gateways) > i {
podman.add("--gateway", gateways[i])
}
if len(ipRanges) > i {
podman.add("--ip-range", ipRanges[i])
}
}
} else if len(ipRanges) > 0 || len(gateways) > 0 {
return nil, fmt.Errorf("cannot set gateway or range without subnet")
}
networkOptions := network.LookupAllKeyVal(NetworkGroup, KeyOptions)
if len(networkOptions) > 0 {
podman.addKeys("--opt", networkOptions)
}
if labels := network.LookupAllKeyVal(NetworkGroup, KeyLabel); len(labels) > 0 {
podman.addLabels(labels)
}
handlePodmanArgs(network, NetworkGroup, podman)
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")
// Store the name of the created resource
unitInfo.ResourceName = networkName
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.
// Also returns the canonical volume name, either auto-generated or user-defined via the VolumeName
// key-value.
func ConvertVolume(volume *parser.UnitFile, name string, unitsInfoMap map[string]*UnitInfo, isUser bool) (*parser.UnitFile, error) {
unitInfo, ok := unitsInfoMap[volume.Filename]
if !ok {
return nil, fmt.Errorf("internal error while processing network %s", volume.Filename)
}
service := volume.Dup()
service.Filename = unitInfo.ServiceFileName()
addDefaultDependencies(service, isUser)
if volume.Path != "" {
service.Add(UnitGroup, "SourcePath", volume.Path)
}
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)
// 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 {
volumeName = removeExtension(name, "systemd-", "")
}
// Need the containers filesystem mounted to start podman
service.Add(UnitGroup, "RequiresMountsFor", "%t/containers")
labels := volume.LookupAllKeyVal(VolumeGroup, "Label")
podman := createBasePodmanCommand(volume, VolumeGroup)
podman.add("volume", "create", "--ignore")
driver, ok := volume.Lookup(VolumeGroup, KeyDriver)
if ok {
podman.add("--driver", driver)
}
var opts strings.Builder
if driver == "image" {
opts.WriteString("image=")
imageName, ok := volume.Lookup(VolumeGroup, KeyImage)
if !ok {
return nil, fmt.Errorf("the key %s is mandatory when using the image driver", KeyImage)
}
imageName, err := handleImageSource(imageName, service, unitsInfoMap)
if err != nil {
return nil, err
}
opts.WriteString(imageName)
} else {
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)
handlePodmanArgs(volume, VolumeGroup, podman)
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")
// Store the name of the created resource
unitInfo.ResourceName = volumeName
return service, nil
}
func ConvertKube(kube *parser.UnitFile, unitsInfoMap map[string]*UnitInfo, isUser bool) (*parser.UnitFile, error) {
unitInfo, ok := unitsInfoMap[kube.Filename]
if !ok {
return nil, fmt.Errorf("internal error while processing network %s", kube.Filename)
}
service := kube.Dup()
service.Filename = unitInfo.ServiceFileName()
addDefaultDependencies(service, isUser)
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)
// 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")
}
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")
// Allow users to set the Service Type to oneshot to allow resources only kube yaml
serviceType, ok := service.Lookup(ServiceGroup, "Type")
if ok && serviceType != "notify" && serviceType != "oneshot" {
return nil, fmt.Errorf("invalid service Type '%s'", serviceType)
}
if serviceType != "oneshot" {
service.Setv(ServiceGroup,
"Type", "notify",
"NotifyAccess", "all")
}
if !kube.HasKey(ServiceGroup, "SyslogIdentifier") {
service.Set(ServiceGroup, "SyslogIdentifier", "%N")
}
execStart := createBasePodmanCommand(kube, KubeGroup)
execStart.add("kube", "play")
execStart.add(
// Replace any previous container with the same name, not fail
"--replace",
// Use a service container
"--service-container=true",
)
if ecp, ok := kube.Lookup(KubeGroup, KeyExitCodePropagation); ok && len(ecp) > 0 {
execStart.addf("--service-exit-code-propagation=%s", ecp)
}
handleLogDriver(kube, KubeGroup, execStart)
handleLogOpt(kube, KubeGroup, execStart)
if err := handleUserMappings(kube, KubeGroup, execStart, false); err != nil {
return nil, err
}
if err := addNetworks(kube, KubeGroup, service, unitsInfoMap, execStart); err != nil {
return nil, err
}
updateMaps := kube.LookupAllStrv(KubeGroup, KeyAutoUpdate)
for _, update := range updateMaps {
annotation := fmt.Sprintf("--annotation=%s", autoUpdateLabel)
updateType := update
annoValue, typ, hasSlash := strings.Cut(update, "/")
if hasSlash {
annotation = annotation + "/" + annoValue
updateType = typ
}
execStart.addf("%s=%s", annotation, updateType)
}
configMaps := kube.LookupAllStrv(KubeGroup, KeyConfigMap)
for _, configMap := range configMaps {
configMapPath, err := getAbsolutePath(kube, configMap)
if err != nil {
return nil, err
}
execStart.add("--configmap", configMapPath)
}
handlePublishPorts(kube, KubeGroup, execStart)
handlePodmanArgs(kube, KubeGroup, execStart)
execStart.add(yamlPath)
service.AddCmdline(ServiceGroup, "ExecStart", execStart.Args)
// Use `ExecStopPost` to make sure cleanup happens even in case of
// errors; otherwise containers, pods, etc. would be left behind.
execStop := createBasePodmanCommand(kube, KubeGroup)
execStop.add("kube", "down")
if kubeDownForce, ok := kube.LookupBoolean(KubeGroup, KeyKubeDownForce); ok {
execStop.addBool("--force", kubeDownForce)
}
execStop.add(yamlPath)
service.AddCmdline(ServiceGroup, "ExecStopPost", execStop.Args)
_, err = handleSetWorkingDirectory(kube, service, KubeGroup)
if err != nil {
return nil, err
}
return service, nil
}
func ConvertImage(image *parser.UnitFile, unitsInfoMap map[string]*UnitInfo, isUser bool) (*parser.UnitFile, error) {
unitInfo, ok := unitsInfoMap[image.Filename]
if !ok {
return nil, fmt.Errorf("internal error while processing network %s", image.Filename)
}
service := image.Dup()
service.Filename = unitInfo.ServiceFileName()
addDefaultDependencies(service, isUser)
if image.Path != "" {
service.Add(UnitGroup, "SourcePath", image.Path)
}
if err := checkForUnknownKeys(image, ImageGroup, supportedImageKeys); err != nil {
return nil, err
}
imageName, ok := image.Lookup(ImageGroup, KeyImage)
if !ok || len(imageName) == 0 {
return nil, fmt.Errorf("no Image key specified")
}
/* 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")
podman := createBasePodmanCommand(image, ImageGroup)
podman.add("image", "pull")
stringKeys := map[string]string{
KeyArch: "--arch",
KeyAuthFile: "--authfile",
KeyCertDir: "--cert-dir",
KeyCreds: "--creds",
KeyDecryptionKey: "--decryption-key",
KeyOS: "--os",
KeyVariant: "--variant",
}
lookupAndAddString(image, ImageGroup, stringKeys, podman)
boolKeys := map[string]string{
KeyAllTags: "--all-tags",
KeyTLSVerify: "--tls-verify",
}
lookupAndAddBoolean(image, ImageGroup, boolKeys, podman)
handlePodmanArgs(image, ImageGroup, podman)
podman.add(imageName)
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")
if name, ok := image.Lookup(ImageGroup, KeyImageTag); ok && len(name) > 0 {
imageName = name
}
// Store the name of the created resource
unitInfo.ResourceName = imageName
return service, nil
}
func ConvertBuild(build *parser.UnitFile, unitsInfoMap map[string]*UnitInfo, isUser bool) (*parser.UnitFile, error) {
unitInfo, ok := unitsInfoMap[build.Filename]
if !ok {
return nil, fmt.Errorf("internal error while processing network %s", build.Filename)
}
// Fast fail is ResouceName is not set
if len(unitInfo.ResourceName) == 0 {
return nil, fmt.Errorf("no ImageTag key specified")
}
service := build.Dup()
service.Filename = unitInfo.ServiceFileName()
addDefaultDependencies(service, isUser)
/* 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")
if build.Path != "" {
service.Add(UnitGroup, "SourcePath", build.Path)
}
if err := checkForUnknownKeys(build, BuildGroup, supportedBuildKeys); err != nil {
return nil, err
}
podman := createBasePodmanCommand(build, BuildGroup)
podman.add("build")
stringKeys := map[string]string{
KeyArch: "--arch",
KeyAuthFile: "--authfile",
KeyPull: "--pull",
KeyTarget: "--target",
KeyVariant: "--variant",
}
lookupAndAddString(build, BuildGroup, stringKeys, podman)
boolKeys := map[string]string{
KeyTLSVerify: "--tls-verify",
KeyForceRM: "--force-rm",
}
lookupAndAddBoolean(build, BuildGroup, boolKeys, podman)
allStringKeys := map[string]string{
KeyDNS: "--dns",
KeyDNSOption: "--dns-option",
KeyDNSSearch: "--dns-search",
KeyGroupAdd: "--group-add",
KeyImageTag: "--tag",
}
lookupAndAddAllStrings(build, BuildGroup, allStringKeys, podman)
annotations := build.LookupAllKeyVal(BuildGroup, KeyAnnotation)
podman.addAnnotations(annotations)
podmanEnv := build.LookupAllKeyVal(BuildGroup, KeyEnvironment)
podman.addEnv(podmanEnv)
labels := build.LookupAllKeyVal(BuildGroup, KeyLabel)
podman.addLabels(labels)
if err := addNetworks(build, BuildGroup, service, unitsInfoMap, podman); err != nil {
return nil, err
}
secrets := build.LookupAllArgs(BuildGroup, KeySecret)
for _, secret := range secrets {
podman.add("--secret", secret)
}
if err := addVolumes(build, service, BuildGroup, unitsInfoMap, podman); err != nil {
return nil, err
}
// In order to build an image locally, we need either a File key pointing directly at a
// Containerfile, or we need a context or WorkingDirectory containing all required files.
// SetWorkingDirectory= can also be a path, a URL to either a Containerfile, a Git repo, or
// an archive.
context, err := handleSetWorkingDirectory(build, service, BuildGroup)
if err != nil {
return nil, err
}
workingDirectory, okWD := service.Lookup(ServiceGroup, ServiceKeyWorkingDirectory)
filePath, okFile := build.Lookup(BuildGroup, KeyFile)
if (!okWD || len(workingDirectory) == 0) && (!okFile || len(filePath) == 0) && len(context) == 0 {
return nil, fmt.Errorf("neither SetWorkingDirectory, nor File key specified")
}
if len(filePath) > 0 {
podman.add("--file", filePath)
}
handlePodmanArgs(build, BuildGroup, podman)
// Context or WorkingDirectory has to be last argument
if len(context) > 0 {
podman.add(context)
} else if !filepath.IsAbs(filePath) && !isURL(filePath) {
// Special handling for relative filePaths
if len(workingDirectory) == 0 {
return nil, fmt.Errorf("relative path in File key requires SetWorkingDirectory key to be set")
}
podman.add(workingDirectory)
}
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 GetBuiltImageName(buildUnit *parser.UnitFile) string {
imageTags := buildUnit.LookupAll(BuildGroup, KeyImageTag)
if len(imageTags) > 0 {
return imageTags[0]
}
return ""
}
func GetContainerServiceName(podUnit *parser.UnitFile) string {
return getServiceName(podUnit, ContainerGroup, "")
}
func GetKubeServiceName(podUnit *parser.UnitFile) string {
return getServiceName(podUnit, KubeGroup, "")
}
func GetVolumeServiceName(podUnit *parser.UnitFile) string {
return getServiceName(podUnit, VolumeGroup, "-volume")
}
func GetNetworkServiceName(podUnit *parser.UnitFile) string {
return getServiceName(podUnit, NetworkGroup, "-network")
}
func GetImageServiceName(podUnit *parser.UnitFile) string {
return getServiceName(podUnit, ImageGroup, "-image")
}
func GetBuildServiceName(podUnit *parser.UnitFile) string {
return getServiceName(podUnit, BuildGroup, "-build")
}
func GetPodServiceName(podUnit *parser.UnitFile) string {
return getServiceName(podUnit, PodGroup, "-pod")
}
func getServiceName(quadletUnitFile *parser.UnitFile, groupName string, defaultExtraSuffix string) string {
if serviceName, ok := quadletUnitFile.Lookup(groupName, KeyServiceName); ok {
return serviceName
}
return removeExtension(quadletUnitFile.Filename, "", defaultExtraSuffix)
}
func ConvertPod(podUnit *parser.UnitFile, name string, unitsInfoMap map[string]*UnitInfo, isUser bool) (*parser.UnitFile, error) {
unitInfo, ok := unitsInfoMap[podUnit.Filename]
if !ok {
return nil, fmt.Errorf("internal error while processing pod %s", podUnit.Filename)
}
service := podUnit.Dup()
service.Filename = unitInfo.ServiceFileName()
addDefaultDependencies(service, isUser)
if podUnit.Path != "" {
service.Add(UnitGroup, "SourcePath", podUnit.Path)
}
if err := checkForUnknownKeys(podUnit, PodGroup, supportedPodKeys); err != nil {
return nil, err
}
// Derive pod name from unit name (with added prefix), or use user-provided name.
podName, ok := podUnit.Lookup(PodGroup, KeyPodName)
if !ok || len(podName) == 0 {
podName = removeExtension(name, "systemd-", "")
}
/* 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")
for _, containerService := range unitInfo.Containers {
service.Add(UnitGroup, "Wants", containerService)
service.Add(UnitGroup, "Before", containerService)
}
if !podUnit.HasKey(ServiceGroup, "SyslogIdentifier") {
service.Set(ServiceGroup, "SyslogIdentifier", "%N")
}
execStart := createBasePodmanCommand(podUnit, PodGroup)
execStart.add("pod", "start", "--pod-id-file=%t/%N.pod-id")
service.AddCmdline(ServiceGroup, "ExecStart", execStart.Args)
execStop := createBasePodmanCommand(podUnit, PodGroup)
execStop.add("pod", "stop")
execStop.add(
"--pod-id-file=%t/%N.pod-id",
"--ignore",
"--time=10",
)
service.AddCmdline(ServiceGroup, "ExecStop", execStop.Args)
execStopPost := createBasePodmanCommand(podUnit, PodGroup)
execStopPost.add("pod", "rm")
execStopPost.add(
"--pod-id-file=%t/%N.pod-id",
"--ignore",
"--force",
)
service.AddCmdline(ServiceGroup, "ExecStopPost", execStopPost.Args)
execStartPre := createBasePodmanCommand(podUnit, PodGroup)
execStartPre.add("pod", "create")
execStartPre.add(
"--infra-conmon-pidfile=%t/%N.pid",
"--pod-id-file=%t/%N.pod-id",
"--exit-policy=stop",
"--replace",
)
if err := handleUserMappings(podUnit, PodGroup, execStartPre, true); err != nil {
return nil, err
}
handlePublishPorts(podUnit, PodGroup, execStartPre)
if err := addNetworks(podUnit, PodGroup, service, unitsInfoMap, execStartPre); err != nil {
return nil, err
}
stringsKeys := map[string]string{
KeyIP: "--ip",
KeyIP6: "--ip6",
}
lookupAndAddAllStrings(podUnit, PodGroup, stringsKeys, execStartPre)
allStringsKeys := map[string]string{
KeyNetworkAlias: "--network-alias",
KeyDNS: "--dns",
KeyDNSOption: "--dns-option",
KeyDNSSearch: "--dns-search",
KeyAddHost: "--add-host",
}
lookupAndAddAllStrings(podUnit, PodGroup, allStringsKeys, execStartPre)
if err := addVolumes(podUnit, service, PodGroup, unitsInfoMap, execStartPre); err != nil {
return nil, err
}
execStartPre.add("--infra-name", fmt.Sprintf("%s-infra", podName))
execStartPre.add("--name", podName)
handlePodmanArgs(podUnit, PodGroup, execStartPre)
service.AddCmdline(ServiceGroup, "ExecStartPre", execStartPre.Args)
service.Setv(ServiceGroup,
"Environment", "PODMAN_SYSTEMD_UNIT=%n",
"Type", "forking",
"Restart", "on-failure",
"PIDFile", "%t/%N.pid",
)
return service, nil
}
func handleUser(unitFile *parser.UnitFile, groupName string, podman *PodmanCmdline) error {
user, hasUser := unitFile.Lookup(groupName, KeyUser)
okUser := hasUser && len(user) > 0
group, hasGroup := unitFile.Lookup(groupName, KeyGroup)
okGroup := hasGroup && len(group) > 0
if !okUser {
if okGroup {
return fmt.Errorf("invalid Group set without User")
}
return nil
}
var userGroupStr string
if !okGroup {
userGroupStr = user
} else {
userGroupStr = fmt.Sprintf("%s:%s", user, group)
}
podman.add("--user", userGroupStr)
return nil
}
func handleUserMappings(unitFile *parser.UnitFile, groupName string, podman *PodmanCmdline, supportManual bool) error {
mappingsDefined := false
if userns, ok := unitFile.Lookup(groupName, KeyUserNS); ok && len(userns) > 0 {
podman.add("--userns", userns)
mappingsDefined = true
}
uidMaps := unitFile.LookupAllStrv(groupName, KeyUIDMap)
mappingsDefined = mappingsDefined || len(uidMaps) > 0
for _, uidMap := range uidMaps {
podman.add("--uidmap", uidMap)
}
gidMaps := unitFile.LookupAllStrv(groupName, KeyGIDMap)
mappingsDefined = mappingsDefined || len(gidMaps) > 0
for _, gidMap := range gidMaps {
podman.add("--gidmap", gidMap)
}
if subUIDMap, ok := unitFile.Lookup(groupName, KeySubUIDMap); ok && len(subUIDMap) > 0 {
podman.add("--subuidname", subUIDMap)
mappingsDefined = true
}
if subGIDMap, ok := unitFile.Lookup(groupName, KeySubGIDMap); ok && len(subGIDMap) > 0 {
podman.add("--subgidname", subGIDMap)
mappingsDefined = true
}
if mappingsDefined {
_, hasRemapUID := unitFile.Lookup(groupName, KeyRemapUid)
_, hasRemapGID := unitFile.Lookup(groupName, KeyRemapGid)
_, RemapUsers := unitFile.LookupLast(groupName, KeyRemapUsers)
if hasRemapUID || hasRemapGID || RemapUsers {
return fmt.Errorf("deprecated Remap keys are set along with explicit mapping keys")
}
return nil
}
return handleUserRemap(unitFile, groupName, podman, supportManual)
}
func handleUserRemap(unitFile *parser.UnitFile, groupName string, podman *PodmanCmdline, 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.add("--uidmap", uidMap)
}
for _, gidMap := range gidMaps {
podman.add("--gidmap", 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.add("--userns", usernsOpts("auto", autoOpts))
case "keep-id":
keepidOpts := make([]string, 0)
if len(uidMaps) > 0 {
if len(uidMaps) > 1 {
return fmt.Errorf("RemapUsers=keep-id supports only a single value for UID mapping")
}
keepidOpts = append(keepidOpts, "uid="+uidMaps[0])
}
if len(gidMaps) > 0 {
if len(gidMaps) > 1 {
return fmt.Errorf("RemapUsers=keep-id supports only a single value for GID mapping")
}
keepidOpts = append(keepidOpts, "gid="+gidMaps[0])
}
podman.add("--userns", usernsOpts("keep-id", keepidOpts))
default:
return fmt.Errorf("unsupported RemapUsers option '%s'", remapUsers)
}
return nil
}
func addNetworks(quadletUnitFile *parser.UnitFile, groupName string, serviceUnitFile *parser.UnitFile, unitsInfoMap map[string]*UnitInfo, podman *PodmanCmdline) error {
networks := quadletUnitFile.LookupAll(groupName, KeyNetwork)
for _, network := range networks {
if len(network) > 0 {
quadletNetworkName, options, found := strings.Cut(network, ":")
isNetworkUnit := strings.HasSuffix(quadletNetworkName, ".network")
isContainerUnit := strings.HasSuffix(quadletNetworkName, ".container")
if isNetworkUnit || isContainerUnit {
unitInfo, ok := unitsInfoMap[quadletNetworkName]
if !ok {
return fmt.Errorf("requested Quadlet unit %s was not found", quadletNetworkName)
}
// XXX: this is usually because a '@' in service name
if len(unitInfo.ResourceName) == 0 {
return fmt.Errorf("cannot get the resource name of %s", quadletNetworkName)
}
// the systemd unit name is $serviceName.service
serviceFileName := unitInfo.ServiceFileName()
serviceUnitFile.Add(UnitGroup, "Requires", serviceFileName)
serviceUnitFile.Add(UnitGroup, "After", serviceFileName)
if found {
if isContainerUnit {
return fmt.Errorf("extra options are not supported when joining another container's network")
}
network = fmt.Sprintf("%s:%s", unitInfo.ResourceName, options)
} else {
if isContainerUnit {
network = fmt.Sprintf("container:%s", unitInfo.ResourceName)
} else {
network = unitInfo.ResourceName
}
}
}
podman.add("--network", network)
}
}
return nil
}
// Systemd Specifiers start with % with the exception of %%
func startsWithSystemdSpecifier(filePath string) bool {
if len(filePath) == 0 || filePath[0] != '%' {
return false
}
if len(filePath) > 1 && filePath[1] == '%' {
return false
}
return true
}
func getAbsolutePath(quadletUnitFile *parser.UnitFile, filePath string) (string, error) {
// When the path starts with a Systemd specifier do not resolve what looks like a relative address
if !startsWithSystemdSpecifier(filePath) && !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) {
publishPorts := unitFile.LookupAll(groupName, KeyPublishPort)
for _, publishPort := range publishPorts {
podman.add("--publish", publishPort)
}
}
func handleLogDriver(unitFile *parser.UnitFile, groupName string, podman *PodmanCmdline) {
logDriver, found := unitFile.Lookup(groupName, KeyLogDriver)
if found {
podman.add("--log-driver", logDriver)
}
}
func handleLogOpt(unitFile *parser.UnitFile, groupName string, podman *PodmanCmdline) {
logOpts := unitFile.LookupAllStrv(groupName, KeyLogOpt)
for _, logOpt := range logOpts {
podman.add("--log-opt", logOpt)
}
}
func handleStorageSource(quadletUnitFile, serviceUnitFile *parser.UnitFile, source string, unitsInfoMap map[string]*UnitInfo) (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") {
volumeUnitInfo, ok := unitsInfoMap[source]
if !ok {
return "", fmt.Errorf("requested Quadlet image %s was not found", source)
}
// the systemd unit name is $serviceName.service
volumeServiceName := volumeUnitInfo.ServiceFileName()
serviceUnitFile.Add(UnitGroup, "Requires", volumeServiceName)
serviceUnitFile.Add(UnitGroup, "After", volumeServiceName)
source = volumeUnitInfo.ResourceName
}
return source, nil
}
func handleHealth(unitFile *parser.UnitFile, groupName string, podman *PodmanCmdline) {
keyArgMap := [][2]string{
{KeyHealthCmd, "cmd"},
{KeyHealthInterval, "interval"},
{KeyHealthOnFailure, "on-failure"},
{KeyHealthLogDestination, "log-destination"},
{KeyHealthMaxLogCount, "max-log-count"},
{KeyHealthMaxLogSize, "max-log-size"},
{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)
}
}
}
func handlePodmanArgs(unitFile *parser.UnitFile, groupName string, podman *PodmanCmdline) {
podmanArgs := unitFile.LookupAllArgs(groupName, KeyPodmanArgs)
if len(podmanArgs) > 0 {
podman.add(podmanArgs...)
}
}
func handleSetWorkingDirectory(quadletUnitFile, serviceUnitFile *parser.UnitFile, quadletGroup string) (string, error) {
setWorkingDirectory, ok := quadletUnitFile.Lookup(quadletGroup, KeySetWorkingDirectory)
if !ok || len(setWorkingDirectory) == 0 {
return "", nil
}
var relativeToFile string
var context string
switch strings.ToLower(setWorkingDirectory) {
case "yaml":
if quadletGroup != KubeGroup {
return "", fmt.Errorf("SetWorkingDirectory=%s is only supported in .kube files", setWorkingDirectory)
}
relativeToFile, ok = quadletUnitFile.Lookup(quadletGroup, KeyYaml)
if !ok {
return "", fmt.Errorf("no Yaml key specified")
}
case "file":
if quadletGroup != BuildGroup {
return "", fmt.Errorf("SetWorkingDirectory=%s is only supported in .build files", setWorkingDirectory)
}
relativeToFile, ok = quadletUnitFile.Lookup(quadletGroup, KeyFile)
if !ok {
return "", fmt.Errorf("no File key specified")
}
case "unit":
relativeToFile = quadletUnitFile.Path
default:
// Path / URL handling is for .build files only
if quadletGroup != BuildGroup {
return "", fmt.Errorf("unsupported value for %s: %s ", ServiceKeyWorkingDirectory, setWorkingDirectory)
}
// Any value other than the above cases will be returned as context
context = setWorkingDirectory
// If we have a relative path, set the WorkingDirectory to that of the
// quadletUnitFile
if !filepath.IsAbs(context) {
relativeToFile = quadletUnitFile.Path
}
}
if len(relativeToFile) > 0 && !isURL(context) {
// If WorkingDirectory is already set in the Service section do not change it
workingDir, ok := quadletUnitFile.Lookup(ServiceGroup, ServiceKeyWorkingDirectory)
if ok && len(workingDir) > 0 {
return "", nil
}
fileInWorkingDir, err := getAbsolutePath(quadletUnitFile, relativeToFile)
if err != nil {
return "", err
}
serviceUnitFile.Add(ServiceGroup, ServiceKeyWorkingDirectory, filepath.Dir(fileInWorkingDir))
}
return context, nil
}
func lookupAndAddString(unit *parser.UnitFile, group string, keys map[string]string, podman *PodmanCmdline) {
for key, flag := range keys {
if val, ok := unit.Lookup(group, key); ok && len(val) > 0 {
podman.add(flag, val)
}
}
}
func lookupAndAddAllStrings(unit *parser.UnitFile, group string, keys map[string]string, podman *PodmanCmdline) {
for key, flag := range keys {
values := unit.LookupAll(group, key)
for _, val := range values {
podman.add(flag, val)
}
}
}
func lookupAndAddBoolean(unit *parser.UnitFile, group string, keys map[string]string, podman *PodmanCmdline) {
for key, flag := range keys {
if val, ok := unit.LookupBoolean(group, key); ok {
podman.addBool(flag, val)
}
}
}
func handleImageSource(quadletImageName string, serviceUnitFile *parser.UnitFile, unitsInfoMap map[string]*UnitInfo) (string, error) {
for _, suffix := range []string{".build", ".image"} {
if strings.HasSuffix(quadletImageName, suffix) {
// since there is no default name conversion, the actual image name must exist in the names map
unitInfo, ok := unitsInfoMap[quadletImageName]
if !ok {
return "", fmt.Errorf("requested Quadlet image %s was not found", quadletImageName)
}
// the systemd unit name is $name-$suffix.service
imageServiceName := unitInfo.ServiceFileName()
serviceUnitFile.Add(UnitGroup, "Requires", imageServiceName)
serviceUnitFile.Add(UnitGroup, "After", imageServiceName)
quadletImageName = unitInfo.ResourceName
}
}
return quadletImageName, nil
}
func resolveContainerMountParams(containerUnitFile, serviceUnitFile *parser.UnitFile, mount string, unitsInfoMap map[string]*UnitInfo) (string, error) {
mountType, tokens, err := specgenutilexternal.FindMountType(mount)
if err != nil {
return "", err
}
// Source resolution is required only for these types of mounts
if !(mountType == "volume" || mountType == "bind" || mountType == "glob") {
return mount, nil
}
sourceIndex := -1
originalSource := ""
for i, token := range tokens {
key, val, hasVal := strings.Cut(token, "=")
if key == "source" || key == "src" {
if !hasVal {
return "", fmt.Errorf("source parameter does not include a value")
}
sourceIndex = i
originalSource = val
}
}
resolvedSource, err := handleStorageSource(containerUnitFile, serviceUnitFile, originalSource, unitsInfoMap)
if err != nil {
return "", err
}
tokens[sourceIndex] = fmt.Sprintf("source=%s", resolvedSource)
tokens = append([]string{fmt.Sprintf("type=%s", mountType)}, tokens...)
return convertToCSV(tokens)
}
func convertToCSV(s []string) (string, error) {
var buf bytes.Buffer
writer := csv.NewWriter(&buf)
err := writer.Write(s)
if err != nil {
return "", err
}
writer.Flush()
ret := buf.String()
if ret[len(ret)-1] == '\n' {
ret = ret[:len(ret)-1]
}
return ret, nil
}
func createBasePodmanCommand(unitFile *parser.UnitFile, groupName string) *PodmanCmdline {
podman := NewPodmanCmdline()
containersConfModules := unitFile.LookupAll(groupName, KeyContainersConfModule)
for _, containersConfModule := range containersConfModules {
podman.addf("--module=%s", containersConfModule)
}
globalArgs := unitFile.LookupAllArgs(groupName, KeyGlobalArgs)
if len(globalArgs) > 0 {
podman.add(globalArgs...)
}
return podman
}
func handlePod(quadletUnitFile, serviceUnitFile *parser.UnitFile, groupName string, unitsInfoMap map[string]*UnitInfo, podman *PodmanCmdline) error {
pod, ok := quadletUnitFile.Lookup(groupName, KeyPod)
if ok && len(pod) > 0 {
if !strings.HasSuffix(pod, ".pod") {
return fmt.Errorf("pod %s is not Quadlet based", pod)
}
podInfo, ok := unitsInfoMap[pod]
if !ok {
return fmt.Errorf("quadlet pod unit %s does not exist", pod)
}
podman.add("--pod-id-file", fmt.Sprintf("%%t/%s.pod-id", podInfo.ServiceName))
podServiceName := podInfo.ServiceFileName()
serviceUnitFile.Add(UnitGroup, "BindsTo", podServiceName)
serviceUnitFile.Add(UnitGroup, "After", podServiceName)
podInfo.Containers = append(podInfo.Containers, serviceUnitFile.Filename)
}
return nil
}
func addVolumes(quadletUnitFile, serviceUnitFile *parser.UnitFile, groupName string, unitsInfoMap map[string]*UnitInfo, podman *PodmanCmdline) error {
volumes := quadletUnitFile.LookupAll(groupName, 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(quadletUnitFile, serviceUnitFile, source, unitsInfoMap)
if err != nil {
return err
}
}
podman.add("-v")
if source == "" {
podman.add(dest)
} else {
podman.addf("%s:%s%s", source, dest, options)
}
}
return nil
}
func addDefaultDependencies(service *parser.UnitFile, isUser bool) {
// Add a dependency on network-online.target so the image pull container startup
// does not happen before network is ready.
// https://github.com/containers/podman/issues/21873
if service.LookupBooleanWithDefault(QuadletGroup, KeyDefaultDependencies, true) {
networkUnit := "network-online.target"
// network-online.target only exists as root and user session cannot wait for it
// https://github.com/systemd/systemd/issues/3312
// Given this is a bad problem with pasta which can fail to start or use the
// wrong interface if the network is not fully set up we need to work around
// that: https://github.com/containers/podman/issues/22197.
if isUser {
networkUnit = "podman-user-wait-network-online.service"
}
service.PrependUnitLine(UnitGroup, "After", networkUnit)
service.PrependUnitLine(UnitGroup, "Wants", networkUnit)
}
}