package specgenutil import ( "encoding/json" "errors" "fmt" "os" "strconv" "strings" "time" "github.com/containers/common/pkg/config" "github.com/containers/image/v5/manifest" "github.com/containers/podman/v5/cmd/podman/parse" "github.com/containers/podman/v5/libpod/define" "github.com/containers/podman/v5/pkg/domain/entities" envLib "github.com/containers/podman/v5/pkg/env" "github.com/containers/podman/v5/pkg/namespaces" "github.com/containers/podman/v5/pkg/specgen" systemdDefine "github.com/containers/podman/v5/pkg/systemd/define" "github.com/containers/podman/v5/pkg/util" "github.com/docker/go-units" "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/selinux/go-selinux" ) const ( rlimitPrefix = "rlimit_" ) func getCPULimits(c *entities.ContainerCreateOptions) *specs.LinuxCPU { cpu := &specs.LinuxCPU{} hasLimits := false if c.CPUS > 0 { period, quota := util.CoresToPeriodAndQuota(c.CPUS) cpu.Period = &period cpu.Quota = "a hasLimits = true } if c.CPUShares > 0 { cpu.Shares = &c.CPUShares hasLimits = true } if c.CPUPeriod > 0 { cpu.Period = &c.CPUPeriod hasLimits = true } if c.CPUSetCPUs != "" { cpu.Cpus = c.CPUSetCPUs hasLimits = true } if c.CPUSetMems != "" { cpu.Mems = c.CPUSetMems hasLimits = true } if c.CPUQuota > 0 { cpu.Quota = &c.CPUQuota hasLimits = true } if c.CPURTPeriod > 0 { cpu.RealtimePeriod = &c.CPURTPeriod hasLimits = true } if c.CPURTRuntime > 0 { cpu.RealtimeRuntime = &c.CPURTRuntime hasLimits = true } if !hasLimits { return nil } return cpu } func getIOLimits(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions) (*specs.LinuxBlockIO, error) { var err error io := &specs.LinuxBlockIO{} if s.ResourceLimits == nil { s.ResourceLimits = &specs.LinuxResources{} } hasLimits := false if b := c.BlkIOWeight; len(b) > 0 { if s.ResourceLimits.BlockIO == nil { s.ResourceLimits.BlockIO = &specs.LinuxBlockIO{} } u, err := strconv.ParseUint(b, 10, 16) if err != nil { return nil, fmt.Errorf("invalid value for blkio-weight: %w", err) } nu := uint16(u) io.Weight = &nu s.ResourceLimits.BlockIO.Weight = &nu hasLimits = true } if len(c.BlkIOWeightDevice) > 0 { if s.WeightDevice, err = parseWeightDevices(c.BlkIOWeightDevice); err != nil { return nil, err } hasLimits = true } if bps := c.DeviceReadBPs; len(bps) > 0 { if s.ThrottleReadBpsDevice, err = parseThrottleBPSDevices(bps); err != nil { return nil, err } hasLimits = true } if bps := c.DeviceWriteBPs; len(bps) > 0 { if s.ThrottleWriteBpsDevice, err = parseThrottleBPSDevices(bps); err != nil { return nil, err } hasLimits = true } if iops := c.DeviceReadIOPs; len(iops) > 0 { if s.ThrottleReadIOPSDevice, err = parseThrottleIOPsDevices(iops); err != nil { return nil, err } hasLimits = true } if iops := c.DeviceWriteIOPs; len(iops) > 0 { if s.ThrottleWriteIOPSDevice, err = parseThrottleIOPsDevices(iops); err != nil { return nil, err } hasLimits = true } if !hasLimits { return nil, nil } return io, nil } func LimitToSwap(memory *specs.LinuxMemory, swap string, ml int64) { if ml > 0 { memory.Limit = &ml if swap == "" { limit := 2 * ml memory.Swap = &(limit) } } } func getMemoryLimits(c *entities.ContainerCreateOptions) (*specs.LinuxMemory, error) { var err error memory := &specs.LinuxMemory{} hasLimits := false if m := c.Memory; len(m) > 0 { ml, err := units.RAMInBytes(m) if err != nil { return nil, fmt.Errorf("invalid value for memory: %w", err) } LimitToSwap(memory, c.MemorySwap, ml) hasLimits = true } if m := c.MemoryReservation; len(m) > 0 { mr, err := units.RAMInBytes(m) if err != nil { return nil, fmt.Errorf("invalid value for memory: %w", err) } memory.Reservation = &mr hasLimits = true } if m := c.MemorySwap; len(m) > 0 { var ms int64 // only set memory swap if it was set // -1 indicates unlimited if m != "-1" { ms, err = units.RAMInBytes(m) memory.Swap = &ms if err != nil { return nil, fmt.Errorf("invalid value for memory: %w", err) } hasLimits = true } } if c.MemorySwappiness >= 0 { swappiness := uint64(c.MemorySwappiness) memory.Swappiness = &swappiness hasLimits = true } if c.OOMKillDisable { memory.DisableOOMKiller = &c.OOMKillDisable hasLimits = true } if !hasLimits { return nil, nil } return memory, nil } func setNamespaces(rtc *config.Config, s *specgen.SpecGenerator, c *entities.ContainerCreateOptions) error { var err error if c.PID != "" { s.PidNS, err = specgen.ParseNamespace(c.PID) if err != nil { return err } } if c.IPC != "" { s.IpcNS, err = specgen.ParseIPCNamespace(c.IPC) if err != nil { return err } } if c.UTS != "" { s.UtsNS, err = specgen.ParseNamespace(c.UTS) if err != nil { return err } } if c.CgroupNS != "" { s.CgroupNS, err = specgen.ParseNamespace(c.CgroupNS) if err != nil { return err } } userns := c.UserNS if userns == "" && c.Pod == "" { if ns, ok := os.LookupEnv("PODMAN_USERNS"); ok { userns = ns } else { // TODO: This should be moved into pkg/specgen/generate so we don't use the client's containers.conf userns = rtc.Containers.UserNS } } // userns must be treated differently if userns != "" { s.UserNS, err = specgen.ParseUserNamespace(userns) if err != nil { return err } } if c.Net != nil { s.NetNS = c.Net.Network } if s.IDMappings == nil { userNS := namespaces.UsernsMode(s.UserNS.NSMode) tempIDMap, err := util.ParseIDMapping(namespaces.UsernsMode(userns), []string{}, []string{}, "", "") if err != nil { return err } s.IDMappings, err = util.ParseIDMapping(userNS, c.UIDMap, c.GIDMap, c.SubUIDName, c.SubGIDName) if err != nil { return err } if len(s.IDMappings.GIDMap) == 0 { s.IDMappings.AutoUserNsOpts.AdditionalGIDMappings = tempIDMap.AutoUserNsOpts.AdditionalGIDMappings if s.UserNS.NSMode == specgen.NamespaceMode("auto") { s.IDMappings.AutoUserNs = true } } if len(s.IDMappings.UIDMap) == 0 { s.IDMappings.AutoUserNsOpts.AdditionalUIDMappings = tempIDMap.AutoUserNsOpts.AdditionalUIDMappings if s.UserNS.NSMode == specgen.NamespaceMode("auto") { s.IDMappings.AutoUserNs = true } } if tempIDMap.AutoUserNsOpts.Size != 0 { s.IDMappings.AutoUserNsOpts.Size = tempIDMap.AutoUserNsOpts.Size } // If some mappings are specified, assume a private user namespace if userNS.IsDefaultValue() && (!s.IDMappings.HostUIDMapping || !s.IDMappings.HostGIDMapping) { s.UserNS.NSMode = specgen.Private } else { s.UserNS.NSMode = specgen.NamespaceMode(userNS) } } return nil } func GenRlimits(ulimits []string) ([]specs.POSIXRlimit, error) { rlimits := make([]specs.POSIXRlimit, 0, len(ulimits)) // Rlimits/Ulimits for _, ulimit := range ulimits { if ulimit == "host" { rlimits = nil break } // `ulimitNameMapping` from go-units uses lowercase and names // without prefixes, e.g. `RLIMIT_NOFILE` should be converted to `nofile`. // https://github.com/containers/podman/issues/9803 u := strings.TrimPrefix(strings.ToLower(ulimit), rlimitPrefix) ul, err := units.ParseUlimit(u) if err != nil { return nil, fmt.Errorf("ulimit option %q requires name=SOFT:HARD, failed to be parsed: %w", u, err) } rl := specs.POSIXRlimit{ Type: ul.Name, Hard: uint64(ul.Hard), Soft: uint64(ul.Soft), } rlimits = append(rlimits, rl) } return rlimits, nil } func currentLabelOpts() ([]string, error) { label, err := selinux.CurrentLabel() if err != nil { return nil, err } if label == "" { return nil, nil } con, err := selinux.NewContext(label) if err != nil { return nil, err } return []string{ fmt.Sprintf("label=user:%s", con["user"]), fmt.Sprintf("label=role:%s", con["role"]), }, nil } func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions, args []string) error { rtc, err := config.Default() if err != nil { return err } // TODO: This needs to move into pkg/specgen/generate so we aren't using containers.conf on the client. if rtc.Containers.EnableLabeledUsers { defSecurityOpts, err := currentLabelOpts() if err != nil { return err } c.SecurityOpt = append(defSecurityOpts, c.SecurityOpt...) } // validate flags as needed if err := validate(c); err != nil { return err } s.User = c.User var inputCommand []string if !c.IsInfra { if len(args) > 1 { inputCommand = args[1:] } } if len(c.HealthCmd) > 0 { if c.NoHealthCheck { return errors.New("cannot specify both --no-healthcheck and --health-cmd") } s.HealthConfig, err = makeHealthCheckFromCli(c.HealthCmd, c.HealthInterval, c.HealthRetries, c.HealthTimeout, c.HealthStartPeriod, false) if err != nil { return err } } else if c.NoHealthCheck { s.HealthConfig = &manifest.Schema2HealthConfig{ Test: []string{"NONE"}, } } onFailureAction, err := define.ParseHealthCheckOnFailureAction(c.HealthOnFailure) if err != nil { return err } s.HealthCheckOnFailureAction = onFailureAction if c.StartupHCCmd != "" { if c.NoHealthCheck { return errors.New("cannot specify both --no-healthcheck and --health-startup-cmd") } // The hardcoded "1s" will be discarded, as the startup // healthcheck does not have a period. So just hardcode // something that parses correctly. tmpHcConfig, err := makeHealthCheckFromCli(c.StartupHCCmd, c.StartupHCInterval, c.StartupHCRetries, c.StartupHCTimeout, "1s", true) if err != nil { return err } s.StartupHealthConfig = new(define.StartupHealthCheck) s.StartupHealthConfig.Test = tmpHcConfig.Test s.StartupHealthConfig.Interval = tmpHcConfig.Interval s.StartupHealthConfig.Timeout = tmpHcConfig.Timeout s.StartupHealthConfig.Retries = tmpHcConfig.Retries s.StartupHealthConfig.Successes = int(c.StartupHCSuccesses) } if err := setNamespaces(rtc, s, c); err != nil { return err } if s.Terminal == nil { s.Terminal = &c.TTY } if err := verifyExpose(c.Expose); err != nil { return err } // We are not handling the Expose flag yet. // s.PortsExpose = c.Expose if c.Net != nil { s.PortMappings = c.Net.PublishPorts } if s.PublishExposedPorts == nil { s.PublishExposedPorts = &c.PublishAll } if len(s.Pod) == 0 || len(c.Pod) > 0 { s.Pod = c.Pod } if len(c.PodIDFile) > 0 { if len(s.Pod) > 0 { return errors.New("cannot specify both --pod and --pod-id-file") } podID, err := ReadPodIDFile(c.PodIDFile) if err != nil { return err } s.Pod = podID } expose, err := CreateExpose(c.Expose) if err != nil { return err } if len(s.Expose) == 0 { s.Expose = expose } if sig := c.StopSignal; len(sig) > 0 { stopSignal, err := util.ParseSignal(sig) if err != nil { return err } s.StopSignal = &stopSignal } // ENVIRONMENT VARIABLES // // Precedence order (higher index wins): // 1) containers.conf (EnvHost, EnvHTTP, Env) 2) image data, 3 User EnvHost/EnvHTTP, 4) env-file, 5) env // containers.conf handled and image data handled on the server side // user specified EnvHost and EnvHTTP handled on Server Side relative to Server // env-file and env handled on client side var env map[string]string // First transform the os env into a map. We need it for the labels later in // any case. osEnv := envLib.Map(os.Environ()) if s.EnvHost == nil { s.EnvHost = &c.EnvHost } if s.HTTPProxy == nil { s.HTTPProxy = &c.HTTPProxy } // env-file overrides any previous variables for _, f := range c.EnvFile { fileEnv, err := envLib.ParseFile(f) if err != nil { return err } // File env is overridden by env. env = envLib.Join(env, fileEnv) } parsedEnv, err := envLib.ParseSlice(c.Env) if err != nil { return err } if len(s.Env) == 0 { s.Env = envLib.Join(env, parsedEnv) } // LABEL VARIABLES labels, err := parse.GetAllLabels(c.LabelFile, c.Label) if err != nil { return fmt.Errorf("unable to process labels: %w", err) } if systemdUnit, exists := osEnv[systemdDefine.EnvVariable]; exists { labels[systemdDefine.EnvVariable] = systemdUnit } if len(s.Labels) == 0 { s.Labels = labels } // Intel RDT CAT if c.IntelRdtClosID != "" { s.IntelRdt = &specs.LinuxIntelRdt{} s.IntelRdt.ClosID = c.IntelRdtClosID } // ANNOTATIONS annotations := make(map[string]string) // Last, add user annotations for _, annotation := range c.Annotation { key, val, hasVal := strings.Cut(annotation, "=") if !hasVal { return errors.New("annotations must be formatted KEY=VALUE") } annotations[key] = val } if len(s.Annotations) == 0 { s.Annotations = annotations } if len(c.StorageOpts) > 0 { opts := make(map[string]string, len(c.StorageOpts)) for _, opt := range c.StorageOpts { key, val, hasVal := strings.Cut(opt, "=") if !hasVal { return errors.New("storage-opt must be formatted KEY=VALUE") } opts[key] = val } s.StorageOpts = opts } if len(s.WorkDir) == 0 { s.WorkDir = c.Workdir } if c.Entrypoint != nil { entrypoint := []string{} // Check if entrypoint specified is json if err := json.Unmarshal([]byte(*c.Entrypoint), &entrypoint); err != nil { entrypoint = append(entrypoint, *c.Entrypoint) } s.Entrypoint = entrypoint } // Include the command used to create the container. if len(s.ContainerCreateCommand) == 0 { s.ContainerCreateCommand = os.Args } if len(inputCommand) > 0 { s.Command = inputCommand } // SHM Size if c.ShmSize != "" { val, err := units.RAMInBytes(c.ShmSize) if err != nil { return fmt.Errorf("unable to translate --shm-size: %w", err) } s.ShmSize = &val } // SHM Size Systemd if c.ShmSizeSystemd != "" { val, err := units.RAMInBytes(c.ShmSizeSystemd) if err != nil { return fmt.Errorf("unable to translate --shm-size-systemd: %w", err) } s.ShmSizeSystemd = &val } if c.Net != nil { s.Networks = c.Net.Networks } if c.Net != nil { s.HostAdd = c.Net.AddHosts s.UseImageResolvConf = &c.Net.UseImageResolvConf s.DNSServers = c.Net.DNSServers s.DNSSearch = c.Net.DNSSearch s.DNSOptions = c.Net.DNSOptions s.NetworkOptions = c.Net.NetworkOptions s.UseImageHosts = &c.Net.NoHosts } if len(s.HostUsers) == 0 || len(c.HostUsers) != 0 { s.HostUsers = c.HostUsers } if len(c.ImageVolume) != 0 { if len(s.ImageVolumeMode) == 0 { s.ImageVolumeMode = c.ImageVolume } } if s.ImageVolumeMode == define.TypeBind { s.ImageVolumeMode = "anonymous" } if len(s.Systemd) == 0 || len(c.Systemd) != 0 { s.Systemd = strings.ToLower(c.Systemd) } if len(s.SdNotifyMode) == 0 || len(c.SdNotifyMode) != 0 { s.SdNotifyMode = c.SdNotifyMode } if s.ResourceLimits == nil { s.ResourceLimits = &specs.LinuxResources{} } s.ResourceLimits, err = GetResources(s, c) if err != nil { return err } if s.LogConfiguration == nil { s.LogConfiguration = &specgen.LogConfig{} } if ld := c.LogDriver; len(ld) > 0 { s.LogConfiguration.Driver = ld } if len(s.CgroupParent) == 0 || len(c.CgroupParent) != 0 { s.CgroupParent = c.CgroupParent } if len(s.CgroupsMode) == 0 { s.CgroupsMode = c.CgroupsMode } if len(s.Groups) == 0 || len(c.GroupAdd) != 0 { s.Groups = c.GroupAdd } if len(s.Hostname) == 0 || len(c.Hostname) != 0 { s.Hostname = c.Hostname } sysctl := map[string]string{} if ctl := c.Sysctl; len(ctl) > 0 { sysctl, err = util.ValidateSysctls(ctl) if err != nil { return err } } if len(s.Sysctl) == 0 || len(c.Sysctl) != 0 { s.Sysctl = sysctl } if len(s.CapAdd) == 0 || len(c.CapAdd) != 0 { s.CapAdd = c.CapAdd } if len(s.CapDrop) == 0 || len(c.CapDrop) != 0 { s.CapDrop = c.CapDrop } if s.Privileged == nil { s.Privileged = &c.Privileged } if s.ReadOnlyFilesystem == nil { s.ReadOnlyFilesystem = &c.ReadOnly } if len(s.ConmonPidFile) == 0 || len(c.ConmonPIDFile) != 0 { s.ConmonPidFile = c.ConmonPIDFile } if len(s.DependencyContainers) == 0 || len(c.Requires) != 0 { s.DependencyContainers = c.Requires } // Only add ReadWrite tmpfs mounts iff the container is // being run ReadOnly and ReadWriteTmpFS is not disabled, // (user specifying --read-only-tmpfs=false.) localRWTmpfs := c.ReadOnly && c.ReadWriteTmpFS s.ReadWriteTmpfs = &localRWTmpfs // TODO convert to map? // check if key=value and convert sysmap := make(map[string]string) for _, ctl := range c.Sysctl { key, val, hasVal := strings.Cut(ctl, "=") if !hasVal { return fmt.Errorf("invalid sysctl value %q", ctl) } sysmap[key] = val } if len(s.Sysctl) == 0 || len(c.Sysctl) != 0 { s.Sysctl = sysmap } if c.CIDFile != "" { s.Annotations[define.InspectAnnotationCIDFile] = c.CIDFile } for _, opt := range c.SecurityOpt { // Docker deprecated the ":" syntax but still supports it, // so we need to as well var key, val string var hasVal bool if strings.Contains(opt, "=") { key, val, hasVal = strings.Cut(opt, "=") } else { key, val, hasVal = strings.Cut(opt, ":") } if !hasVal && key != "no-new-privileges" { return fmt.Errorf("invalid --security-opt 1: %q", opt) } switch key { case "apparmor": s.ContainerSecurityConfig.ApparmorProfile = val s.Annotations[define.InspectAnnotationApparmor] = val case "label": if val == "nested" { localTrue := true s.ContainerSecurityConfig.LabelNested = &localTrue continue } // TODO selinux opts and label opts are the same thing s.ContainerSecurityConfig.SelinuxOpts = append(s.ContainerSecurityConfig.SelinuxOpts, val) s.Annotations[define.InspectAnnotationLabel] = strings.Join(s.ContainerSecurityConfig.SelinuxOpts, ",label=") case "mask": s.ContainerSecurityConfig.Mask = append(s.ContainerSecurityConfig.Mask, strings.Split(val, ":")...) case "proc-opts": s.ProcOpts = strings.Split(val, ",") case "seccomp": s.SeccompProfilePath = val s.Annotations[define.InspectAnnotationSeccomp] = val // this option is for docker compatibility, it is the same as unmask=ALL case "systempaths": if val == "unconfined" { s.ContainerSecurityConfig.Unmask = append(s.ContainerSecurityConfig.Unmask, []string{"ALL"}...) } else { return fmt.Errorf("invalid systempaths option %q, only `unconfined` is supported", val) } case "unmask": if hasVal { s.ContainerSecurityConfig.Unmask = append(s.ContainerSecurityConfig.Unmask, val) } case "no-new-privileges": noNewPrivileges := true if hasVal { noNewPrivileges, err = strconv.ParseBool(val) if err != nil { return fmt.Errorf("invalid --security-opt 2: %q", opt) } } s.ContainerSecurityConfig.NoNewPrivileges = &noNewPrivileges default: return fmt.Errorf("invalid --security-opt 2: %q", opt) } } if len(s.SeccompPolicy) == 0 || len(c.SeccompPolicy) != 0 { s.SeccompPolicy = c.SeccompPolicy } if len(s.VolumesFrom) == 0 || len(c.VolumesFrom) != 0 { s.VolumesFrom = c.VolumesFrom } // Only add read-only tmpfs mounts in case that we are read-only and the // read-only tmpfs flag has been set. mounts, volumes, overlayVolumes, imageVolumes, err := parseVolumes(rtc, c.Volume, c.Mount, c.TmpFS) if err != nil { return err } if len(s.Mounts) == 0 || len(c.Mount) != 0 { s.Mounts = mounts } if len(s.Volumes) == 0 || len(c.Volume) != 0 { s.Volumes = volumes } if s.LabelNested != nil && *s.LabelNested { // Need to unmask the SELinux file system s.Unmask = append(s.Unmask, "/sys/fs/selinux", "/proc") s.Mounts = append(s.Mounts, specs.Mount{ Source: "/sys/fs/selinux", Destination: "/sys/fs/selinux", Type: define.TypeBind, }) s.Annotations[define.RunOCIMountContextType] = "rootcontext" } // TODO make sure these work in clone if len(s.OverlayVolumes) == 0 { s.OverlayVolumes = overlayVolumes } if len(s.ImageVolumes) == 0 { s.ImageVolumes = imageVolumes } devices := c.Devices for _, gpu := range c.GPUs { devices = append(devices, "nvidia.com/gpu="+gpu) } for _, dev := range devices { s.Devices = append(s.Devices, specs.LinuxDevice{Path: dev}) } for _, rule := range c.DeviceCgroupRule { dev, err := parseLinuxResourcesDeviceAccess(rule) if err != nil { return err } s.DeviceCgroupRule = append(s.DeviceCgroupRule, dev) } if s.Init == nil { s.Init = &c.Init } if len(s.InitPath) == 0 || len(c.InitPath) != 0 { s.InitPath = c.InitPath } if s.Stdin == nil { s.Stdin = &c.Interactive } // quiet // DeviceCgroupRules: c.StringSlice("device-cgroup-rule"), // Rlimits/Ulimits s.Rlimits, err = GenRlimits(c.Ulimit) if err != nil { return err } logOpts := make(map[string]string) for _, o := range c.LogOptions { key, val, hasVal := strings.Cut(o, "=") if !hasVal { return fmt.Errorf("invalid log option %q", o) } switch strings.ToLower(key) { case "driver": s.LogConfiguration.Driver = val case "path": s.LogConfiguration.Path = val case "max-size": logSize, err := units.FromHumanSize(val) if err != nil { return err } s.LogConfiguration.Size = logSize default: logOpts[key] = val } } if len(s.LogConfiguration.Options) == 0 || len(c.LogOptions) != 0 { s.LogConfiguration.Options = logOpts } if len(s.Name) == 0 || len(c.Name) != 0 { s.Name = c.Name } if c.PreserveFDs != 0 && c.PreserveFD != nil { return errors.New("cannot specify both --preserve-fds and --preserve-fd") } if s.PreserveFDs == 0 || c.PreserveFDs != 0 { s.PreserveFDs = c.PreserveFDs } if s.PreserveFD == nil || c.PreserveFD != nil { s.PreserveFD = c.PreserveFD } if s.OOMScoreAdj == nil || c.OOMScoreAdj != nil { s.OOMScoreAdj = c.OOMScoreAdj } if c.Restart != "" { policy, retries, err := util.ParseRestartPolicy(c.Restart) if err != nil { return err } s.RestartPolicy = policy s.RestartRetries = &retries } if len(s.Secrets) == 0 || len(c.Secrets) != 0 { s.Secrets, s.EnvSecrets, err = parseSecrets(c.Secrets) if err != nil { return err } } if c.Personality != "" { s.Personality = &specs.LinuxPersonality{} s.Personality.Domain = specs.LinuxPersonalityDomain(c.Personality) } if s.Remove == nil { s.Remove = &c.Rm } if s.StopTimeout == nil || c.StopTimeout != 0 { s.StopTimeout = &c.StopTimeout } if s.Timeout == 0 || c.Timeout != 0 { s.Timeout = c.Timeout } if len(s.Timezone) == 0 || len(c.Timezone) != 0 { s.Timezone = c.Timezone } if len(s.Umask) == 0 || len(c.Umask) != 0 { s.Umask = c.Umask } if len(s.PidFile) == 0 || len(c.PidFile) != 0 { s.PidFile = c.PidFile } if s.Volatile == nil { s.Volatile = &c.Rm } if len(s.EnvMerge) == 0 || len(c.EnvMerge) != 0 { s.EnvMerge = c.EnvMerge } if len(s.UnsetEnv) == 0 || len(c.UnsetEnv) != 0 { s.UnsetEnv = c.UnsetEnv } if s.UnsetEnvAll == nil { s.UnsetEnvAll = &c.UnsetEnvAll } if len(s.ChrootDirs) == 0 || len(c.ChrootDirs) != 0 { s.ChrootDirs = c.ChrootDirs } // Initcontainers if len(s.InitContainerType) == 0 || len(c.InitContainerType) != 0 { s.InitContainerType = c.InitContainerType } t := true if s.Passwd == nil { s.Passwd = &t } if len(s.PasswdEntry) == 0 || len(c.PasswdEntry) != 0 { s.PasswdEntry = c.PasswdEntry } if len(s.GroupEntry) == 0 || len(c.GroupEntry) != 0 { s.GroupEntry = c.GroupEntry } return nil } func makeHealthCheckFromCli(inCmd, interval string, retries uint, timeout, startPeriod string, isStartup bool) (*manifest.Schema2HealthConfig, error) { cmdArr := []string{} isArr := true err := json.Unmarshal([]byte(inCmd), &cmdArr) // array unmarshalling if err != nil { cmdArr = strings.SplitN(inCmd, " ", 2) // default for compat isArr = false } // Every healthcheck requires a command if len(cmdArr) == 0 { return nil, errors.New("must define a healthcheck command for all healthchecks") } var concat string if strings.ToUpper(cmdArr[0]) == define.HealthConfigTestCmd || strings.ToUpper(cmdArr[0]) == define.HealthConfigTestNone { // this is for compat, we are already split properly for most compat cases cmdArr = strings.Fields(inCmd) } else if strings.ToUpper(cmdArr[0]) != define.HealthConfigTestCmdShell { // this is for podman side of things, won't contain the keywords if isArr && len(cmdArr) > 1 { // an array of consecutive commands cmdArr = append([]string{define.HealthConfigTestCmd}, cmdArr...) } else { // one singular command if len(cmdArr) == 1 { concat = cmdArr[0] } else { concat = strings.Join(cmdArr[0:], " ") } cmdArr = append([]string{define.HealthConfigTestCmdShell}, concat) } } if strings.ToUpper(cmdArr[0]) == define.HealthConfigTestNone { // if specified to remove healtcheck cmdArr = []string{define.HealthConfigTestNone} } // healthcheck is by default an array, so we simply pass the user input hc := manifest.Schema2HealthConfig{ Test: cmdArr, } if interval == "disable" { interval = "0" } intervalDuration, err := time.ParseDuration(interval) if err != nil { return nil, fmt.Errorf("invalid healthcheck-interval: %w", err) } hc.Interval = intervalDuration if retries < 1 && !isStartup { return nil, errors.New("healthcheck-retries must be greater than 0") } hc.Retries = int(retries) timeoutDuration, err := time.ParseDuration(timeout) if err != nil { return nil, fmt.Errorf("invalid healthcheck-timeout: %w", err) } if timeoutDuration < time.Duration(1) { return nil, errors.New("healthcheck-timeout must be at least 1 second") } hc.Timeout = timeoutDuration startPeriodDuration, err := time.ParseDuration(startPeriod) if err != nil { return nil, fmt.Errorf("invalid healthcheck-start-period: %w", err) } if startPeriodDuration < time.Duration(0) { return nil, errors.New("healthcheck-start-period must be 0 seconds or greater") } hc.StartPeriod = startPeriodDuration return &hc, nil } func parseWeightDevices(weightDevs []string) (map[string]specs.LinuxWeightDevice, error) { wd := make(map[string]specs.LinuxWeightDevice) for _, dev := range weightDevs { key, val, hasVal := strings.Cut(dev, ":") if !hasVal { return nil, fmt.Errorf("bad format: %s", dev) } if !strings.HasPrefix(key, "/dev/") { return nil, fmt.Errorf("bad format for device path: %s", dev) } weight, err := strconv.ParseUint(val, 10, 0) if err != nil { return nil, fmt.Errorf("invalid weight for device: %s", dev) } if weight > 0 && (weight < 10 || weight > 1000) { return nil, fmt.Errorf("invalid weight for device: %s", dev) } w := uint16(weight) wd[key] = specs.LinuxWeightDevice{ Weight: &w, LeafWeight: nil, } } return wd, nil } func parseThrottleBPSDevices(bpsDevices []string) (map[string]specs.LinuxThrottleDevice, error) { td := make(map[string]specs.LinuxThrottleDevice) for _, dev := range bpsDevices { key, val, hasVal := strings.Cut(dev, ":") if !hasVal { return nil, fmt.Errorf("bad format: %s", dev) } if !strings.HasPrefix(key, "/dev/") { return nil, fmt.Errorf("bad format for device path: %s", dev) } rate, err := units.RAMInBytes(val) if err != nil { return nil, fmt.Errorf("invalid rate for device: %s. The correct format is :[]. Number must be a positive integer. Unit is optional and can be kb, mb, or gb", dev) } if rate < 0 { return nil, fmt.Errorf("invalid rate for device: %s. The correct format is :[]. Number must be a positive integer. Unit is optional and can be kb, mb, or gb", dev) } td[key] = specs.LinuxThrottleDevice{Rate: uint64(rate)} } return td, nil } func parseThrottleIOPsDevices(iopsDevices []string) (map[string]specs.LinuxThrottleDevice, error) { td := make(map[string]specs.LinuxThrottleDevice) for _, dev := range iopsDevices { key, val, hasVal := strings.Cut(dev, ":") if !hasVal { return nil, fmt.Errorf("bad format: %s", dev) } if !strings.HasPrefix(key, "/dev/") { return nil, fmt.Errorf("bad format for device path: %s", dev) } rate, err := strconv.ParseUint(val, 10, 64) if err != nil { return nil, fmt.Errorf("invalid rate for device: %s. The correct format is :. Number must be a positive integer", dev) } td[key] = specs.LinuxThrottleDevice{Rate: rate} } return td, nil } func parseSecrets(secrets []string) ([]specgen.Secret, map[string]string, error) { secretParseError := errors.New("parsing secret") var mount []specgen.Secret envs := make(map[string]string) for _, val := range secrets { // mount only tells if user has set an option that can only be used with mount secret type mountOnly := false source := "" secretType := "" target := "" var uid, gid uint32 // default mode 444 octal = 292 decimal var mode uint32 = 292 split := strings.Split(val, ",") // --secret mysecret if len(split) == 1 { mountSecret := specgen.Secret{ Source: val, Target: target, UID: uid, GID: gid, Mode: mode, } mount = append(mount, mountSecret) continue } // --secret mysecret,opt=opt if !strings.Contains(split[0], "=") { source = split[0] split = split[1:] } for _, val := range split { name, value, hasValue := strings.Cut(val, "=") if !hasValue { return nil, nil, fmt.Errorf("option %s must be in form option=value: %w", val, secretParseError) } switch name { case "source": source = value case "type": if secretType != "" { return nil, nil, fmt.Errorf("cannot set more than one secret type: %w", secretParseError) } if value != "mount" && value != "env" { return nil, nil, fmt.Errorf("type %s is invalid: %w", value, secretParseError) } secretType = value case "target": target = value case "mode": mountOnly = true mode64, err := strconv.ParseUint(value, 8, 32) if err != nil { return nil, nil, fmt.Errorf("mode %s invalid: %w", value, secretParseError) } mode = uint32(mode64) case "uid", "UID": mountOnly = true uid64, err := strconv.ParseUint(value, 10, 32) if err != nil { return nil, nil, fmt.Errorf("UID %s invalid: %w", value, secretParseError) } uid = uint32(uid64) case "gid", "GID": mountOnly = true gid64, err := strconv.ParseUint(value, 10, 32) if err != nil { return nil, nil, fmt.Errorf("GID %s invalid: %w", value, secretParseError) } gid = uint32(gid64) default: return nil, nil, fmt.Errorf("option %s invalid: %w", val, secretParseError) } } if secretType == "" { secretType = "mount" } if source == "" { return nil, nil, fmt.Errorf("no source found %s: %w", val, secretParseError) } if secretType == "mount" { mountSecret := specgen.Secret{ Source: source, Target: target, UID: uid, GID: gid, Mode: mode, } mount = append(mount, mountSecret) } if secretType == "env" { if mountOnly { return nil, nil, fmt.Errorf("UID, GID, Mode options cannot be set with secret type env: %w", secretParseError) } if target == "" { target = source } envs[target] = source } } return mount, envs, nil } var cgroupDeviceType = map[string]bool{ "a": true, // all "b": true, // block device "c": true, // character device } var cgroupDeviceAccess = map[string]bool{ "r": true, // read "w": true, // write "m": true, // mknod } // parseLinuxResourcesDeviceAccess parses the raw string passed with the --device-access-add flag func parseLinuxResourcesDeviceAccess(device string) (specs.LinuxDeviceCgroup, error) { var devType, access string var major, minor *int64 value := strings.Split(device, " ") if len(value) != 3 { return specs.LinuxDeviceCgroup{}, fmt.Errorf("invalid device cgroup rule requires type, major:Minor, and access rules: %q", device) } devType = value[0] if !cgroupDeviceType[devType] { return specs.LinuxDeviceCgroup{}, fmt.Errorf("invalid device type in device-access-add: %s", devType) } majorNumber, minorNumber, hasMinor := strings.Cut(value[1], ":") if majorNumber != "*" { i, err := strconv.ParseUint(majorNumber, 10, 64) if err != nil { return specs.LinuxDeviceCgroup{}, err } m := int64(i) major = &m } if hasMinor && minorNumber != "*" { i, err := strconv.ParseUint(minorNumber, 10, 64) if err != nil { return specs.LinuxDeviceCgroup{}, err } m := int64(i) minor = &m } access = value[2] for _, c := range strings.Split(access, "") { if !cgroupDeviceAccess[c] { return specs.LinuxDeviceCgroup{}, fmt.Errorf("invalid device access in device-access-add: %s", c) } } return specs.LinuxDeviceCgroup{ Allow: true, Type: devType, Major: major, Minor: minor, Access: access, }, nil } func GetResources(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions) (*specs.LinuxResources, error) { var err error if s.ResourceLimits.Memory == nil || (len(c.Memory) != 0 || len(c.MemoryReservation) != 0 || len(c.MemorySwap) != 0 || c.MemorySwappiness != 0) { s.ResourceLimits.Memory, err = getMemoryLimits(c) if err != nil { return nil, err } } if s.ResourceLimits.BlockIO == nil || (len(c.BlkIOWeight) != 0 || len(c.BlkIOWeightDevice) != 0 || len(c.DeviceReadBPs) != 0 || len(c.DeviceWriteBPs) != 0) { s.ResourceLimits.BlockIO, err = getIOLimits(s, c) if err != nil { return nil, err } } if c.PIDsLimit != nil { pids := specs.LinuxPids{ Limit: *c.PIDsLimit, } s.ResourceLimits.Pids = &pids } if s.ResourceLimits.CPU == nil || (c.CPUPeriod != 0 || c.CPUQuota != 0 || c.CPURTPeriod != 0 || c.CPURTRuntime != 0 || c.CPUS != 0 || len(c.CPUSetCPUs) != 0 || len(c.CPUSetMems) != 0 || c.CPUShares != 0) { s.ResourceLimits.CPU = getCPULimits(c) } unifieds := make(map[string]string) for _, unified := range c.CgroupConf { key, val, hasVal := strings.Cut(unified, "=") if !hasVal { return nil, errors.New("--cgroup-conf must be formatted KEY=VALUE") } unifieds[key] = val } if len(unifieds) > 0 { s.ResourceLimits.Unified = unifieds } if s.ResourceLimits.CPU == nil && s.ResourceLimits.Pids == nil && s.ResourceLimits.BlockIO == nil && s.ResourceLimits.Memory == nil && s.ResourceLimits.Unified == nil { s.ResourceLimits = nil } return s.ResourceLimits, nil }