mirror of https://github.com/containers/podman.git
				
				
				
			
		
			
				
	
	
		
			1860 lines
		
	
	
		
			54 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			1860 lines
		
	
	
		
			54 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/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"
 | |
| 	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,
 | |
| 		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 := os.Stat(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
 | |
| 	}
 | |
| 
 | |
| 	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
 | |
| }
 |