mirror of
https://github.com/containers/podman.git
synced 2025-05-17 15:18:43 +08:00

Co-authored-by: Ygal Blum <ygal.blum@gmail.com> Signed-off-by: Jonas Berlin <xkr47@outerspace.dyndns.org>
1870 lines
55 KiB
Go
1870 lines
55 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 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"
|
|
XContainerGroup = "X-Container"
|
|
XKubeGroup = "X-Kube"
|
|
XNetworkGroup = "X-Network"
|
|
XPodGroup = "X-Pod"
|
|
XVolumeGroup = "X-Volume"
|
|
XImageGroup = "X-Image"
|
|
)
|
|
|
|
// Systemd Unit file keys
|
|
const (
|
|
ServiceKeyWorkingDirectory = "WorkingDirectory"
|
|
)
|
|
|
|
// All the supported quadlet keys
|
|
const (
|
|
KeyAddCapability = "AddCapability"
|
|
KeyAddDevice = "AddDevice"
|
|
KeyAllTags = "AllTags"
|
|
KeyAnnotation = "Annotation"
|
|
KeyArch = "Arch"
|
|
KeyAuthFile = "AuthFile"
|
|
KeyAutoUpdate = "AutoUpdate"
|
|
KeyCertDir = "CertDir"
|
|
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"
|
|
KeyGateway = "Gateway"
|
|
KeyGIDMap = "GIDMap"
|
|
KeyGlobalArgs = "GlobalArgs"
|
|
KeyGroup = "Group"
|
|
KeyGroupAdd = "GroupAdd"
|
|
KeyHealthCmd = "HealthCmd"
|
|
KeyHealthInterval = "HealthInterval"
|
|
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"
|
|
KeyMask = "Mask"
|
|
KeyMount = "Mount"
|
|
KeyNetwork = "Network"
|
|
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"
|
|
KeySetWorkingDirectory = "SetWorkingDirectory"
|
|
KeyShmSize = "ShmSize"
|
|
KeyStopTimeout = "StopTimeout"
|
|
KeySubGIDMap = "SubGIDMap"
|
|
KeySubnet = "Subnet"
|
|
KeySubUIDMap = "SubUIDMap"
|
|
KeySysctl = "Sysctl"
|
|
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 PodInfo struct {
|
|
ServiceName string
|
|
Containers []string
|
|
}
|
|
|
|
var (
|
|
validPortRange = regexp.Delayed(`\d+(-\d+)?(/udp|/tcp)?$`)
|
|
|
|
// Supported keys in "Container" group
|
|
supportedContainerKeys = map[string]bool{
|
|
KeyAddCapability: true,
|
|
KeyAddDevice: true,
|
|
KeyAnnotation: true,
|
|
KeyAutoUpdate: 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,
|
|
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,
|
|
KeyMask: true,
|
|
KeyMount: true,
|
|
KeyNetwork: 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,
|
|
KeyShmSize: 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,
|
|
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,
|
|
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,
|
|
KeyNetwork: true,
|
|
KeyPodmanArgs: true,
|
|
KeyPublishPort: true,
|
|
KeyRemapGid: true,
|
|
KeyRemapUid: true,
|
|
KeyRemapUidSize: true,
|
|
KeyRemapUsers: 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,
|
|
KeyTLSVerify: true,
|
|
KeyVariant: true,
|
|
}
|
|
|
|
supportedPodKeys = map[string]bool{
|
|
KeyContainersConfModule: true,
|
|
KeyGlobalArgs: true,
|
|
KeyNetwork: true,
|
|
KeyPodName: true,
|
|
KeyPodmanArgs: true,
|
|
KeyPublishPort: true,
|
|
KeyVolume: true,
|
|
}
|
|
)
|
|
|
|
func replaceExtension(name string, extension string, extraPrefix string, extraSuffix string) string {
|
|
baseName := name
|
|
|
|
dot := strings.LastIndexByte(name, '.')
|
|
if dot > 0 {
|
|
baseName = name[:dot]
|
|
}
|
|
|
|
return extraPrefix + baseName + extraSuffix + extension
|
|
}
|
|
|
|
func isPortRange(port string) bool {
|
|
return validPortRange.MatchString(port)
|
|
}
|
|
|
|
func checkForUnknownKeys(unit *parser.UnitFile, groupName string, supportedKeys map[string]bool) error {
|
|
keys := unit.ListKeys(groupName)
|
|
for _, key := range keys {
|
|
if !supportedKeys[key] {
|
|
return fmt.Errorf("unsupported key '%s' in group '%s' in %s", key, groupName, unit.Path)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func splitPorts(ports string) []string {
|
|
parts := make([]string, 0)
|
|
|
|
// IP address could have colons in it. For example: "[::]:8080:80/tcp, so we split carefully
|
|
start := 0
|
|
end := 0
|
|
for end < len(ports) {
|
|
switch ports[end] {
|
|
case '[':
|
|
end++
|
|
for end < len(ports) && ports[end] != ']' {
|
|
end++
|
|
}
|
|
if end < len(ports) {
|
|
end++ // Skip ]
|
|
}
|
|
case ':':
|
|
parts = append(parts, ports[start:end])
|
|
end++
|
|
start = end
|
|
default:
|
|
end++
|
|
}
|
|
}
|
|
|
|
parts = append(parts, ports[start:end])
|
|
return parts
|
|
}
|
|
|
|
func usernsOpts(kind string, opts []string) string {
|
|
var res strings.Builder
|
|
res.WriteString(kind)
|
|
if len(opts) > 0 {
|
|
res.WriteString(":")
|
|
}
|
|
for i, opt := range opts {
|
|
if i != 0 {
|
|
res.WriteString(",")
|
|
}
|
|
res.WriteString(opt)
|
|
}
|
|
return res.String()
|
|
}
|
|
|
|
// Convert a quadlet container file (unit file with a Container group) to a systemd
|
|
// service file (unit file with Service group) based on the options in the
|
|
// Container group.
|
|
// The original Container group is kept around as X-Container.
|
|
func ConvertContainer(container *parser.UnitFile, names map[string]string, isUser bool, podsInfoMap map[string]*PodInfo) (*parser.UnitFile, error) {
|
|
service := container.Dup()
|
|
service.Filename = replaceExtension(container.Filename, ".service", "", "")
|
|
|
|
if container.Path != "" {
|
|
service.Add(UnitGroup, "SourcePath", container.Path)
|
|
}
|
|
|
|
if err := checkForUnknownKeys(container, ContainerGroup, supportedContainerKeys); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Rename old Container group to x-Container so that systemd ignores it
|
|
service.RenameGroup(ContainerGroup, XContainerGroup)
|
|
|
|
// One image or rootfs must be specified for the container
|
|
image, _ := container.Lookup(ContainerGroup, KeyImage)
|
|
rootfs, _ := container.Lookup(ContainerGroup, KeyRootfs)
|
|
if len(image) == 0 && len(rootfs) == 0 {
|
|
return nil, fmt.Errorf("no Image or Rootfs key specified")
|
|
}
|
|
if len(image) > 0 && len(rootfs) > 0 {
|
|
return nil, fmt.Errorf("the Image And Rootfs keys conflict can not be specified together")
|
|
}
|
|
|
|
if len(image) > 0 {
|
|
var err error
|
|
if image, err = handleImageSource(image, service, names); 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)
|
|
|
|
// We delegate groups to the runtime
|
|
service.Add(ServiceGroup, "Delegate", "yes")
|
|
podman.add("--cgroups=split")
|
|
|
|
timezone, ok := container.Lookup(ContainerGroup, KeyTimezone)
|
|
if ok && len(timezone) > 0 {
|
|
podman.addf("--tz=%s", timezone)
|
|
}
|
|
|
|
addNetworks(container, ContainerGroup, service, names, podman)
|
|
|
|
// Run with a pid1 init to reap zombies by default (as most apps don't do that)
|
|
runInit, ok := container.LookupBoolean(ContainerGroup, KeyRunInit)
|
|
if ok {
|
|
podman.addBool("--init", runInit)
|
|
}
|
|
|
|
serviceType, ok := service.Lookup(ServiceGroup, "Type")
|
|
if ok && serviceType != "notify" && serviceType != "oneshot" {
|
|
return nil, fmt.Errorf("invalid service Type '%s'", serviceType)
|
|
}
|
|
|
|
if serviceType != "oneshot" {
|
|
// If we're not in oneshot mode always use some form of sd-notify, normally via conmon,
|
|
// but we also allow passing it to the container by setting Notify=yes
|
|
notify, 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, names, 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)
|
|
}
|
|
|
|
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, names)
|
|
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, podsInfoMap, podman); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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)
|
|
|
|
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) (*parser.UnitFile, string, error) {
|
|
service := network.Dup()
|
|
service.Filename = replaceExtension(network.Filename, ".service", "", "-network")
|
|
|
|
if err := checkForUnknownKeys(network, NetworkGroup, supportedNetworkKeys); err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
/* Rename old Network group to x-Network so that systemd ignores it */
|
|
service.RenameGroup(NetworkGroup, XNetworkGroup)
|
|
|
|
// 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 = replaceExtension(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")
|
|
|
|
return service, networkName, 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, names map[string]string) (*parser.UnitFile, string, error) {
|
|
service := volume.Dup()
|
|
service.Filename = replaceExtension(volume.Filename, ".service", "", "-volume")
|
|
|
|
if err := checkForUnknownKeys(volume, VolumeGroup, supportedVolumeKeys); err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
/* Rename old Volume group to x-Volume so that systemd ignores it */
|
|
service.RenameGroup(VolumeGroup, XVolumeGroup)
|
|
|
|
// 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 = replaceExtension(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, names)
|
|
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")
|
|
|
|
return service, volumeName, nil
|
|
}
|
|
|
|
func ConvertKube(kube *parser.UnitFile, names map[string]string, isUser bool) (*parser.UnitFile, error) {
|
|
service := kube.Dup()
|
|
service.Filename = replaceExtension(kube.Filename, ".service", "", "")
|
|
|
|
if kube.Path != "" {
|
|
service.Add(UnitGroup, "SourcePath", kube.Path)
|
|
}
|
|
|
|
if err := checkForUnknownKeys(kube, KubeGroup, supportedKubeKeys); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Rename old Kube group to x-Kube so that systemd ignores it
|
|
service.RenameGroup(KubeGroup, XKubeGroup)
|
|
|
|
yamlPath, ok := kube.Lookup(KubeGroup, KeyYaml)
|
|
if !ok || len(yamlPath) == 0 {
|
|
return nil, fmt.Errorf("no Yaml key specified")
|
|
}
|
|
|
|
yamlPath, err := getAbsolutePath(kube, yamlPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Only allow mixed or control-group, as nothing else works well
|
|
killMode, ok := service.Lookup(ServiceGroup, "KillMode")
|
|
if !ok || !(killMode == "mixed" || killMode == "control-group") {
|
|
if ok {
|
|
return nil, fmt.Errorf("invalid KillMode '%s'", killMode)
|
|
}
|
|
|
|
// We default to mixed instead of control-group, because it lets conmon do its thing
|
|
service.Set(ServiceGroup, "KillMode", "mixed")
|
|
}
|
|
|
|
// Set PODMAN_SYSTEMD_UNIT so that podman auto-update can restart the service.
|
|
service.Add(ServiceGroup, "Environment", "PODMAN_SYSTEMD_UNIT=%n")
|
|
|
|
// Need the containers filesystem mounted to start podman
|
|
service.Add(UnitGroup, "RequiresMountsFor", "%t/containers")
|
|
|
|
// 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)
|
|
|
|
if err := handleUserMappings(kube, KubeGroup, execStart, isUser, false); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
addNetworks(kube, KubeGroup, service, names, execStart)
|
|
|
|
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)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return service, nil
|
|
}
|
|
|
|
func ConvertImage(image *parser.UnitFile) (*parser.UnitFile, string, error) {
|
|
service := image.Dup()
|
|
service.Filename = replaceExtension(image.Filename, ".service", "", "-image")
|
|
|
|
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
|
|
}
|
|
|
|
return service, imageName, nil
|
|
}
|
|
|
|
func GetPodServiceName(podUnit *parser.UnitFile) string {
|
|
return replaceExtension(podUnit.Filename, "", "", "-pod")
|
|
}
|
|
|
|
func ConvertPod(podUnit *parser.UnitFile, name string, podsInfoMap map[string]*PodInfo, names map[string]string) (*parser.UnitFile, error) {
|
|
podInfo, ok := podsInfoMap[podUnit.Filename]
|
|
if !ok {
|
|
return nil, fmt.Errorf("internal error while processing pod %s", podUnit.Filename)
|
|
}
|
|
|
|
service := podUnit.Dup()
|
|
service.Filename = fmt.Sprintf("%s.service", podInfo.ServiceName)
|
|
|
|
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 = replaceExtension(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 podInfo.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 := handlePublishPorts(podUnit, PodGroup, execStartPre); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
addNetworks(podUnit, PodGroup, service, names, execStartPre)
|
|
|
|
if err := addVolumes(podUnit, service, PodGroup, names, execStartPre); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
execStartPre.addf("--name=%s", 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
|
|
}
|
|
|
|
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.addf("--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.addf("--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, names map[string]string, podman *PodmanCmdline) {
|
|
networks := quadletUnitFile.LookupAll(groupName, KeyNetwork)
|
|
for _, network := range networks {
|
|
if len(network) > 0 {
|
|
quadletNetworkName, options, found := strings.Cut(network, ":")
|
|
if strings.HasSuffix(quadletNetworkName, ".network") {
|
|
// the podman network name is systemd-$name if none is specified by the user.
|
|
networkName := names[quadletNetworkName]
|
|
if networkName == "" {
|
|
networkName = replaceExtension(quadletNetworkName, "", "systemd-", "")
|
|
}
|
|
|
|
// the systemd unit name is $name-network.service
|
|
networkServiceName := replaceExtension(quadletNetworkName, ".service", "", "-network")
|
|
|
|
serviceUnitFile.Add(UnitGroup, "Requires", networkServiceName)
|
|
serviceUnitFile.Add(UnitGroup, "After", networkServiceName)
|
|
|
|
if found {
|
|
network = fmt.Sprintf("%s:%s", networkName, options)
|
|
} else {
|
|
network = networkName
|
|
}
|
|
}
|
|
|
|
podman.addf("--network=%s", network)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 handleStorageSource(quadletUnitFile, serviceUnitFile *parser.UnitFile, source string, names map[string]string) (string, error) {
|
|
if source[0] == '.' {
|
|
var err error
|
|
source, err = getAbsolutePath(quadletUnitFile, source)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
if source[0] == '/' {
|
|
// Absolute path
|
|
serviceUnitFile.Add(UnitGroup, "RequiresMountsFor", source)
|
|
} else if strings.HasSuffix(source, ".volume") {
|
|
// the podman volume name is systemd-$name if none has been provided by the user.
|
|
volumeName := names[source]
|
|
if volumeName == "" {
|
|
volumeName = replaceExtension(source, "", "systemd-", "")
|
|
}
|
|
|
|
// the systemd unit name is $name-volume.service
|
|
volumeServiceName := replaceExtension(source, ".service", "", "-volume")
|
|
|
|
source = volumeName
|
|
|
|
serviceUnitFile.Add(UnitGroup, "Requires", volumeServiceName)
|
|
serviceUnitFile.Add(UnitGroup, "After", volumeServiceName)
|
|
}
|
|
|
|
return source, nil
|
|
}
|
|
|
|
func handleHealth(unitFile *parser.UnitFile, groupName string, podman *PodmanCmdline) {
|
|
keyArgMap := [][2]string{
|
|
{KeyHealthCmd, "cmd"},
|
|
{KeyHealthInterval, "interval"},
|
|
{KeyHealthOnFailure, "on-failure"},
|
|
{KeyHealthRetries, "retries"},
|
|
{KeyHealthStartPeriod, "start-period"},
|
|
{KeyHealthTimeout, "timeout"},
|
|
{KeyHealthStartupCmd, "startup-cmd"},
|
|
{KeyHealthStartupInterval, "startup-interval"},
|
|
{KeyHealthStartupRetries, "startup-retries"},
|
|
{KeyHealthStartupSuccess, "startup-success"},
|
|
{KeyHealthStartupTimeout, "startup-timeout"},
|
|
}
|
|
|
|
for _, keyArg := range keyArgMap {
|
|
val, found := unitFile.Lookup(groupName, keyArg[0])
|
|
if found && len(val) > 0 {
|
|
podman.addf("--health-%s", keyArg[1])
|
|
podman.addf("%s", val)
|
|
}
|
|
}
|
|
}
|
|
|
|
func handlePodmanArgs(unitFile *parser.UnitFile, groupName string, podman *PodmanCmdline) {
|
|
podmanArgs := unitFile.LookupAllArgs(groupName, KeyPodmanArgs)
|
|
if len(podmanArgs) > 0 {
|
|
podman.add(podmanArgs...)
|
|
}
|
|
}
|
|
|
|
func handleSetWorkingDirectory(kube, serviceUnitFile *parser.UnitFile) error {
|
|
// If WorkingDirectory is already set in the Service section do not change it
|
|
workingDir, ok := kube.Lookup(ServiceGroup, ServiceKeyWorkingDirectory)
|
|
if ok && len(workingDir) > 0 {
|
|
return nil
|
|
}
|
|
|
|
setWorkingDirectory, ok := kube.Lookup(KubeGroup, KeySetWorkingDirectory)
|
|
if !ok || len(setWorkingDirectory) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var relativeToFile string
|
|
switch strings.ToLower(setWorkingDirectory) {
|
|
case "yaml":
|
|
relativeToFile, ok = kube.Lookup(KubeGroup, KeyYaml)
|
|
if !ok {
|
|
return fmt.Errorf("no Yaml key specified")
|
|
}
|
|
case "unit":
|
|
relativeToFile = kube.Path
|
|
default:
|
|
return fmt.Errorf("unsupported value for %s: %s ", ServiceKeyWorkingDirectory, setWorkingDirectory)
|
|
}
|
|
|
|
fileInWorkingDir, err := getAbsolutePath(kube, relativeToFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
serviceUnitFile.Add(ServiceGroup, ServiceKeyWorkingDirectory, filepath.Dir(fileInWorkingDir))
|
|
|
|
return 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, names map[string]string) (string, error) {
|
|
if strings.HasSuffix(quadletImageName, ".image") {
|
|
// since there is no default name conversion, the actual image name must exist in the names map
|
|
imageName, ok := names[quadletImageName]
|
|
if !ok {
|
|
return "", fmt.Errorf("requested Quadlet image %s was not found", imageName)
|
|
}
|
|
|
|
// the systemd unit name is $name-image.service
|
|
imageServiceName := replaceExtension(quadletImageName, ".service", "", "-image")
|
|
|
|
serviceUnitFile.Add(UnitGroup, "Requires", imageServiceName)
|
|
serviceUnitFile.Add(UnitGroup, "After", imageServiceName)
|
|
|
|
quadletImageName = imageName
|
|
}
|
|
|
|
return quadletImageName, nil
|
|
}
|
|
|
|
func resolveContainerMountParams(containerUnitFile, serviceUnitFile *parser.UnitFile, mount string, names map[string]string) (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, names)
|
|
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, podsInfoMap map[string]*PodInfo, 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 := podsInfoMap[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 := fmt.Sprintf("%s.service", podInfo.ServiceName)
|
|
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, names map[string]string, 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, names)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
podman.add("-v")
|
|
if source == "" {
|
|
podman.add(dest)
|
|
} else {
|
|
podman.addf("%s:%s%s", source, dest, options)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|