Files
podman/pkg/systemd/quadlet/quadlet.go
Jan Rodák de856dab99 Add --health-max-log-count, --health-max-log-size, --health-log-destination flags
These flags can affect the output of the HealtCheck log. Currently, when a container is configured with HealthCheck, the output from the HealthCheck command is only logged to the container status file, which is accessible via `podman inspect`.
It is also limited to the last five executions and the first 500 characters per execution.

This makes debugging past problems very difficult, since the only information available about the failure of the HealthCheck command is the generic `healthcheck service failed` record.

- The `--health-log-destination` flag sets the destination of the HealthCheck log.
  - `none`: (default behavior) `HealthCheckResults` are stored in overlay containers. (For example: `$runroot/healthcheck.log`)
  - `directory`: creates a log file named `<container-ID>-healthcheck.log` with JSON `HealthCheckResults` in the specified directory.
  - `events_logger`: The log will be written with logging mechanism set by events_loggeri. It also saves the log to a default directory, for performance on a system with a large number of logs.

- The `--health-max-log-count` flag sets the maximum number of attempts in the HealthCheck log file.
  - A value of `0` indicates an infinite number of attempts in the log file.
  - The default value is `5` attempts in the log file.
- The `--health-max-log-size` flag sets the maximum length of the log stored.
  - A value of `0` indicates an infinite log length.
  - The default value is `500` log characters.

Add --health-max-log-count flag

Signed-off-by: Jan Rodák <hony.com@seznam.cz>

Add --health-max-log-size flag

Signed-off-by: Jan Rodák <hony.com@seznam.cz>

Add --health-log-destination flag

Signed-off-by: Jan Rodák <hony.com@seznam.cz>
2024-09-25 14:01:35 +02:00

2330 lines
69 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"
XContainerGroup = "X-Container"
XKubeGroup = "X-Kube"
XNetworkGroup = "X-Network"
XPodGroup = "X-Pod"
XVolumeGroup = "X-Volume"
XImageGroup = "X-Image"
XBuildGroup = "X-Build"
)
// 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"
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,
}
)
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 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, 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()
// Add a dependency on network-online.target so the image pull does not happen
// before network is ready
// https://github.com/containers/podman/issues/21873
// Prepend the lines, so the user-provided values
// override the default ones.
service.PrependUnitLine(UnitGroup, "After", "network-online.target")
service.PrependUnitLine(UnitGroup, "Wants", "network-online.target")
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")
}
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.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)
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.addf("--cgroups=%s", cgroupsMode)
} else {
podman.add("--cgroups=split")
}
timezone, ok := container.Lookup(ContainerGroup, KeyTimezone)
if ok && len(timezone) > 0 {
podman.addf("--tz=%s", timezone)
}
if err := addNetworks(container, ContainerGroup, service, unitsInfoMap, podman); err != nil {
return nil, err
}
networkAliases := container.LookupAll(ContainerGroup, KeyNetworkAlias)
for _, networkAlias := range networkAliases {
podman.add("--network-alias", networkAlias)
}
// 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, 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")
}
pidsLimit, ok := container.Lookup(ContainerGroup, KeyPidsLimit)
if ok && len(pidsLimit) > 0 {
podman.add("--pids-limit", pidsLimit)
}
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))
}
ulimits := container.LookupAll(ContainerGroup, KeyUlimit)
for _, ulimit := range ulimits {
podman.add("--ulimit", ulimit)
}
// But allow overrides with AddCapability
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.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))
}
dns := container.LookupAll(ContainerGroup, KeyDNS)
for _, ipAddr := range dns {
podman.addf("--dns=%s", ipAddr)
}
dnsOptions := container.LookupAll(ContainerGroup, KeyDNSOption)
for _, dnsOption := range dnsOptions {
podman.addf("--dns-option=%s", dnsOption)
}
dnsSearches := container.LookupAll(ContainerGroup, KeyDNSSearch)
for _, dnsSearch := range dnsSearches {
podman.addf("--dns-search=%s", dnsSearch)
}
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))
}
shmSize, hasShmSize := container.Lookup(ContainerGroup, KeyShmSize)
if hasShmSize {
podman.addf("--shm-size=%s", shmSize)
}
entrypoint, hasEntrypoint := container.Lookup(ContainerGroup, KeyEntrypoint)
if hasEntrypoint {
podman.addf("--entrypoint=%s", entrypoint)
}
sysctl := container.LookupAllStrv(ContainerGroup, KeySysctl)
for _, sysctlItem := range sysctl {
podman.addf("--sysctl=%s", sysctlItem)
}
readOnly, ok := container.LookupBoolean(ContainerGroup, KeyReadOnly)
if ok {
podman.addBool("--read-only", readOnly)
}
if readOnlyTmpfs, ok := container.LookupBoolean(ContainerGroup, KeyReadOnlyTmpfs); ok {
podman.addBool("--read-only-tmpfs", readOnlyTmpfs)
}
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 workdir, exists := container.Lookup(ContainerGroup, KeyWorkingDir); exists {
podman.addf("-w=%s", workdir)
}
if err := handleUserMappings(container, ContainerGroup, podman, isUser, true); err != nil {
return nil, err
}
groupsAdd := container.LookupAll(ContainerGroup, KeyGroupAdd)
for _, groupAdd := range groupsAdd {
if len(groupAdd) > 0 {
podman.addf("--group-add=%s", groupAdd)
}
}
tmpfsValues := container.LookupAll(ContainerGroup, KeyTmpfs)
for _, tmpfs := range tmpfsValues {
if strings.Count(tmpfs, ":") > 1 {
return nil, fmt.Errorf("invalid tmpfs format '%s'", tmpfs)
}
podman.add("--tmpfs", tmpfs)
}
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.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)
}
addHosts := container.LookupAll(ContainerGroup, KeyAddHost)
for _, addHost := range addHosts {
podman.addf("--add-host=%s", addHost)
}
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)
}
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 {
mountStr, err := resolveContainerMountParams(container, service, mount, unitsInfoMap)
if err != nil {
return nil, err
}
podman.add("--mount", mountStr)
}
handleHealth(container, ContainerGroup, podman)
if hostname, ok := container.Lookup(ContainerGroup, KeyHostName); ok {
podman.add("--hostname", hostname)
}
pull, ok := container.Lookup(ContainerGroup, KeyPull)
if ok && len(pull) > 0 {
podman.add("--pull", pull)
}
if err := handlePod(container, service, ContainerGroup, unitsInfoMap, podman); err != nil {
return nil, err
}
if stopSignal, ok := container.Lookup(ContainerGroup, KeyStopSignal); ok && len(stopSignal) > 0 {
podman.add("--stop-signal", stopSignal)
}
if stopTimeout, ok := container.Lookup(ContainerGroup, KeyStopTimeout); ok && len(stopTimeout) > 0 {
podman.add("--stop-timeout", stopTimeout)
}
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) (*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()
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)
// 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")
if disableDNS := network.LookupBooleanWithDefault(NetworkGroup, KeyDisableDNS, false); disableDNS {
podman.add("--disable-dns")
}
dns := network.LookupAll(NetworkGroup, KeyDNS)
for _, ipAddr := range dns {
podman.addf("--dns=%s", ipAddr)
}
driver, ok := network.Lookup(NetworkGroup, KeyDriver)
if ok && len(driver) > 0 {
podman.addf("--driver=%s", driver)
}
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.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, KeyInternal, false); internal {
podman.add("--internal")
}
if ipamDriver, ok := network.Lookup(NetworkGroup, KeyIPAMDriver); ok && len(ipamDriver) > 0 {
podman.addf("--ipam-driver=%s", ipamDriver)
}
if ipv6 := network.LookupBooleanWithDefault(NetworkGroup, KeyIPv6, false); ipv6 {
podman.add("--ipv6")
}
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) (*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()
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)
// 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.addf("--driver=%s", 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()
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")
// 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, isUser, 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)
}
if err := handlePublishPorts(kube, KubeGroup, execStart); err != nil {
return nil, err
}
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) (*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()
// Add a dependency on network-online.target so the image pull does not happen
// before network is ready
// https://github.com/containers/podman/issues/21873
// Prepend the lines, so the user-provided values
// override the default ones.
service.PrependUnitLine(UnitGroup, "After", "network-online.target")
service.PrependUnitLine(UnitGroup, "Wants", "network-online.target")
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)
// 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",
}
boolKeys := map[string]string{
KeyAllTags: "--all-tags",
KeyTLSVerify: "--tls-verify",
}
for key, flag := range stringKeys {
lookupAndAddString(image, ImageGroup, key, flag, podman)
}
for key, flag := range boolKeys {
lookupAndAddBoolean(image, ImageGroup, key, flag, 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) (*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()
// Add a dependency on network-online.target so the image pull does not happen
// before network is ready
// https://github.com/containers/podman/issues/21873
// Prepend the lines, so the user-provided values
// override the default ones.
service.PrependUnitLine(UnitGroup, "After", "network-online.target")
service.PrependUnitLine(UnitGroup, "Wants", "network-online.target")
/* Rename old Build group to X-Build so that systemd ignores it */
service.RenameGroup(BuildGroup, XBuildGroup)
// 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",
}
boolKeys := map[string]string{
KeyTLSVerify: "--tls-verify",
KeyForceRM: "--force-rm",
}
for key, flag := range stringKeys {
lookupAndAddString(build, BuildGroup, key, flag, podman)
}
for key, flag := range boolKeys {
lookupAndAddBoolean(build, BuildGroup, key, flag, podman)
}
annotations := build.LookupAllKeyVal(BuildGroup, KeyAnnotation)
podman.addAnnotations(annotations)
dns := build.LookupAll(BuildGroup, KeyDNS)
for _, ipAddr := range dns {
podman.addf("--dns=%s", ipAddr)
}
dnsOptions := build.LookupAll(BuildGroup, KeyDNSOption)
for _, dnsOption := range dnsOptions {
podman.addf("--dns-option=%s", dnsOption)
}
dnsSearches := build.LookupAll(BuildGroup, KeyDNSSearch)
for _, dnsSearch := range dnsSearches {
podman.addf("--dns-search=%s", dnsSearch)
}
podmanEnv := build.LookupAllKeyVal(BuildGroup, KeyEnvironment)
podman.addEnv(podmanEnv)
groupsAdd := build.LookupAll(BuildGroup, KeyGroupAdd)
for _, groupAdd := range groupsAdd {
if len(groupAdd) > 0 {
podman.addf("--group-add=%s", groupAdd)
}
}
labels := build.LookupAllKeyVal(BuildGroup, KeyLabel)
podman.addLabels(labels)
imageTags := build.LookupAll(BuildGroup, KeyImageTag)
for _, imageTag := range imageTags {
podman.addf("--tag=%s", imageTag)
}
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.addf("--file=%s", 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()
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)
// 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, isUser, true); err != nil {
return nil, err
}
if err := handlePublishPorts(podUnit, PodGroup, execStartPre); err != nil {
return nil, err
}
if err := addNetworks(podUnit, PodGroup, service, unitsInfoMap, execStartPre); err != nil {
return nil, err
}
networkAliases := podUnit.LookupAll(PodGroup, KeyNetworkAlias)
for _, networkAlias := range networkAliases {
execStartPre.add("--network-alias", networkAlias)
}
if err := addVolumes(podUnit, service, PodGroup, unitsInfoMap, execStartPre); err != nil {
return nil, err
}
execStartPre.addf("--infra-name=%s-infra", podName)
execStartPre.addf("--name=%s", podName)
dns := podUnit.LookupAll(PodGroup, KeyDNS)
for _, ipAddr := range dns {
execStartPre.addf("--dns=%s", ipAddr)
}
dnsOptions := podUnit.LookupAll(PodGroup, KeyDNSOption)
for _, dnsOption := range dnsOptions {
execStartPre.addf("--dns-option=%s", dnsOption)
}
dnsSearches := podUnit.LookupAll(PodGroup, KeyDNSSearch)
for _, dnsSearch := range dnsSearches {
execStartPre.addf("--dns-search=%s", dnsSearch)
}
ip, ok := podUnit.Lookup(PodGroup, KeyIP)
if ok && len(ip) > 0 {
execStartPre.addf("--ip=%s", ip)
}
ip6, ok := podUnit.Lookup(PodGroup, KeyIP6)
if ok && len(ip6) > 0 {
execStartPre.addf("--ip6=%s", ip6)
}
addHosts := podUnit.LookupAll(PodGroup, KeyAddHost)
for _, addHost := range addHosts {
execStartPre.addf("--add-host=%s", addHost)
}
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
}
if !okGroup {
podman.add("--user", user)
return nil
}
podman.addf("--user=%s:%s", user, group)
return nil
}
func handleUserMappings(unitFile *parser.UnitFile, groupName string, podman *PodmanCmdline, isUser, 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.addf("--uidmap=%s", uidMap)
}
gidMaps := unitFile.LookupAllStrv(groupName, KeyGIDMap)
mappingsDefined = mappingsDefined || len(gidMaps) > 0
for _, gidMap := range gidMaps {
podman.addf("--gidmap=%s", 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, isUser, supportManual)
}
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.add("--userns=" + usernsOpts("auto", autoOpts))
case "keep-id":
if !isUser {
return fmt.Errorf("RemapUsers=keep-id is unsupported for system units")
}
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.addf("--network=%s", 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) 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 {
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, key, flag string, podman *PodmanCmdline) {
val, ok := unit.Lookup(group, key)
if ok && len(val) > 0 {
podman.addf("%s=%s", flag, val)
}
}
func lookupAndAddBoolean(unit *parser.UnitFile, group, key, flag string, podman *PodmanCmdline) {
val, ok := unit.LookupBoolean(group, key)
if 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
}