podman
1287 строк · 33.4 Кб
1package specgenutil
2
3import (
4"encoding/json"
5"errors"
6"fmt"
7"os"
8"strconv"
9"strings"
10"time"
11
12"github.com/containers/common/pkg/config"
13"github.com/containers/image/v5/manifest"
14"github.com/containers/podman/v5/cmd/podman/parse"
15"github.com/containers/podman/v5/libpod/define"
16"github.com/containers/podman/v5/pkg/domain/entities"
17envLib "github.com/containers/podman/v5/pkg/env"
18"github.com/containers/podman/v5/pkg/namespaces"
19"github.com/containers/podman/v5/pkg/specgen"
20systemdDefine "github.com/containers/podman/v5/pkg/systemd/define"
21"github.com/containers/podman/v5/pkg/util"
22"github.com/docker/go-units"
23"github.com/opencontainers/runtime-spec/specs-go"
24"github.com/opencontainers/selinux/go-selinux"
25)
26
27const (
28rlimitPrefix = "rlimit_"
29)
30
31func getCPULimits(c *entities.ContainerCreateOptions) *specs.LinuxCPU {
32cpu := &specs.LinuxCPU{}
33hasLimits := false
34
35if c.CPUS > 0 {
36period, quota := util.CoresToPeriodAndQuota(c.CPUS)
37
38cpu.Period = &period
39cpu.Quota = "a
40hasLimits = true
41}
42if c.CPUShares > 0 {
43cpu.Shares = &c.CPUShares
44hasLimits = true
45}
46if c.CPUPeriod > 0 {
47cpu.Period = &c.CPUPeriod
48hasLimits = true
49}
50if c.CPUSetCPUs != "" {
51cpu.Cpus = c.CPUSetCPUs
52hasLimits = true
53}
54if c.CPUSetMems != "" {
55cpu.Mems = c.CPUSetMems
56hasLimits = true
57}
58if c.CPUQuota > 0 {
59cpu.Quota = &c.CPUQuota
60hasLimits = true
61}
62if c.CPURTPeriod > 0 {
63cpu.RealtimePeriod = &c.CPURTPeriod
64hasLimits = true
65}
66if c.CPURTRuntime > 0 {
67cpu.RealtimeRuntime = &c.CPURTRuntime
68hasLimits = true
69}
70
71if !hasLimits {
72return nil
73}
74return cpu
75}
76
77func getIOLimits(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions) (*specs.LinuxBlockIO, error) {
78var err error
79io := &specs.LinuxBlockIO{}
80if s.ResourceLimits == nil {
81s.ResourceLimits = &specs.LinuxResources{}
82}
83hasLimits := false
84if b := c.BlkIOWeight; len(b) > 0 {
85if s.ResourceLimits.BlockIO == nil {
86s.ResourceLimits.BlockIO = &specs.LinuxBlockIO{}
87}
88u, err := strconv.ParseUint(b, 10, 16)
89if err != nil {
90return nil, fmt.Errorf("invalid value for blkio-weight: %w", err)
91}
92nu := uint16(u)
93io.Weight = &nu
94s.ResourceLimits.BlockIO.Weight = &nu
95hasLimits = true
96}
97
98if len(c.BlkIOWeightDevice) > 0 {
99if s.WeightDevice, err = parseWeightDevices(c.BlkIOWeightDevice); err != nil {
100return nil, err
101}
102hasLimits = true
103}
104
105if bps := c.DeviceReadBPs; len(bps) > 0 {
106if s.ThrottleReadBpsDevice, err = parseThrottleBPSDevices(bps); err != nil {
107return nil, err
108}
109hasLimits = true
110}
111
112if bps := c.DeviceWriteBPs; len(bps) > 0 {
113if s.ThrottleWriteBpsDevice, err = parseThrottleBPSDevices(bps); err != nil {
114return nil, err
115}
116hasLimits = true
117}
118
119if iops := c.DeviceReadIOPs; len(iops) > 0 {
120if s.ThrottleReadIOPSDevice, err = parseThrottleIOPsDevices(iops); err != nil {
121return nil, err
122}
123hasLimits = true
124}
125
126if iops := c.DeviceWriteIOPs; len(iops) > 0 {
127if s.ThrottleWriteIOPSDevice, err = parseThrottleIOPsDevices(iops); err != nil {
128return nil, err
129}
130hasLimits = true
131}
132
133if !hasLimits {
134return nil, nil
135}
136return io, nil
137}
138
139func LimitToSwap(memory *specs.LinuxMemory, swap string, ml int64) {
140if ml > 0 {
141memory.Limit = &ml
142if swap == "" {
143limit := 2 * ml
144memory.Swap = &(limit)
145}
146}
147}
148
149func getMemoryLimits(c *entities.ContainerCreateOptions) (*specs.LinuxMemory, error) {
150var err error
151memory := &specs.LinuxMemory{}
152hasLimits := false
153if m := c.Memory; len(m) > 0 {
154ml, err := units.RAMInBytes(m)
155if err != nil {
156return nil, fmt.Errorf("invalid value for memory: %w", err)
157}
158LimitToSwap(memory, c.MemorySwap, ml)
159hasLimits = true
160}
161if m := c.MemoryReservation; len(m) > 0 {
162mr, err := units.RAMInBytes(m)
163if err != nil {
164return nil, fmt.Errorf("invalid value for memory: %w", err)
165}
166memory.Reservation = &mr
167hasLimits = true
168}
169if m := c.MemorySwap; len(m) > 0 {
170var ms int64
171// only set memory swap if it was set
172// -1 indicates unlimited
173if m != "-1" {
174ms, err = units.RAMInBytes(m)
175memory.Swap = &ms
176if err != nil {
177return nil, fmt.Errorf("invalid value for memory: %w", err)
178}
179hasLimits = true
180}
181}
182if c.MemorySwappiness >= 0 {
183swappiness := uint64(c.MemorySwappiness)
184memory.Swappiness = &swappiness
185hasLimits = true
186}
187if c.OOMKillDisable {
188memory.DisableOOMKiller = &c.OOMKillDisable
189hasLimits = true
190}
191if !hasLimits {
192return nil, nil
193}
194return memory, nil
195}
196
197func setNamespaces(rtc *config.Config, s *specgen.SpecGenerator, c *entities.ContainerCreateOptions) error {
198var err error
199
200if c.PID != "" {
201s.PidNS, err = specgen.ParseNamespace(c.PID)
202if err != nil {
203return err
204}
205}
206if c.IPC != "" {
207s.IpcNS, err = specgen.ParseIPCNamespace(c.IPC)
208if err != nil {
209return err
210}
211}
212if c.UTS != "" {
213s.UtsNS, err = specgen.ParseNamespace(c.UTS)
214if err != nil {
215return err
216}
217}
218if c.CgroupNS != "" {
219s.CgroupNS, err = specgen.ParseNamespace(c.CgroupNS)
220if err != nil {
221return err
222}
223}
224userns := c.UserNS
225if userns == "" && c.Pod == "" {
226if ns, ok := os.LookupEnv("PODMAN_USERNS"); ok {
227userns = ns
228} else {
229// TODO: This should be moved into pkg/specgen/generate so we don't use the client's containers.conf
230userns = rtc.Containers.UserNS
231}
232}
233// userns must be treated differently
234if userns != "" {
235s.UserNS, err = specgen.ParseUserNamespace(userns)
236if err != nil {
237return err
238}
239}
240if c.Net != nil {
241s.NetNS = c.Net.Network
242}
243
244if s.IDMappings == nil {
245userNS := namespaces.UsernsMode(s.UserNS.NSMode)
246tempIDMap, err := util.ParseIDMapping(namespaces.UsernsMode(userns), []string{}, []string{}, "", "")
247if err != nil {
248return err
249}
250s.IDMappings, err = util.ParseIDMapping(userNS, c.UIDMap, c.GIDMap, c.SubUIDName, c.SubGIDName)
251if err != nil {
252return err
253}
254if len(s.IDMappings.GIDMap) == 0 {
255s.IDMappings.AutoUserNsOpts.AdditionalGIDMappings = tempIDMap.AutoUserNsOpts.AdditionalGIDMappings
256if s.UserNS.NSMode == specgen.NamespaceMode("auto") {
257s.IDMappings.AutoUserNs = true
258}
259}
260if len(s.IDMappings.UIDMap) == 0 {
261s.IDMappings.AutoUserNsOpts.AdditionalUIDMappings = tempIDMap.AutoUserNsOpts.AdditionalUIDMappings
262if s.UserNS.NSMode == specgen.NamespaceMode("auto") {
263s.IDMappings.AutoUserNs = true
264}
265}
266if tempIDMap.AutoUserNsOpts.Size != 0 {
267s.IDMappings.AutoUserNsOpts.Size = tempIDMap.AutoUserNsOpts.Size
268}
269// If some mappings are specified, assume a private user namespace
270if userNS.IsDefaultValue() && (!s.IDMappings.HostUIDMapping || !s.IDMappings.HostGIDMapping) {
271s.UserNS.NSMode = specgen.Private
272} else {
273s.UserNS.NSMode = specgen.NamespaceMode(userNS)
274}
275}
276
277return nil
278}
279
280func GenRlimits(ulimits []string) ([]specs.POSIXRlimit, error) {
281rlimits := make([]specs.POSIXRlimit, 0, len(ulimits))
282// Rlimits/Ulimits
283for _, ulimit := range ulimits {
284if ulimit == "host" {
285rlimits = nil
286break
287}
288// `ulimitNameMapping` from go-units uses lowercase and names
289// without prefixes, e.g. `RLIMIT_NOFILE` should be converted to `nofile`.
290// https://github.com/containers/podman/issues/9803
291u := strings.TrimPrefix(strings.ToLower(ulimit), rlimitPrefix)
292ul, err := units.ParseUlimit(u)
293if err != nil {
294return nil, fmt.Errorf("ulimit option %q requires name=SOFT:HARD, failed to be parsed: %w", u, err)
295}
296rl := specs.POSIXRlimit{
297Type: ul.Name,
298Hard: uint64(ul.Hard),
299Soft: uint64(ul.Soft),
300}
301rlimits = append(rlimits, rl)
302}
303return rlimits, nil
304}
305
306func currentLabelOpts() ([]string, error) {
307label, err := selinux.CurrentLabel()
308if err != nil {
309return nil, err
310}
311if label == "" {
312return nil, nil
313}
314con, err := selinux.NewContext(label)
315if err != nil {
316return nil, err
317}
318return []string{
319fmt.Sprintf("label=user:%s", con["user"]),
320fmt.Sprintf("label=role:%s", con["role"]),
321}, nil
322}
323
324func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions, args []string) error {
325rtc, err := config.Default()
326if err != nil {
327return err
328}
329
330// TODO: This needs to move into pkg/specgen/generate so we aren't using containers.conf on the client.
331if rtc.Containers.EnableLabeledUsers {
332defSecurityOpts, err := currentLabelOpts()
333if err != nil {
334return err
335}
336
337c.SecurityOpt = append(defSecurityOpts, c.SecurityOpt...)
338}
339
340// validate flags as needed
341if err := validate(c); err != nil {
342return err
343}
344s.User = c.User
345var inputCommand []string
346if !c.IsInfra {
347if len(args) > 1 {
348inputCommand = args[1:]
349}
350}
351
352if len(c.HealthCmd) > 0 {
353if c.NoHealthCheck {
354return errors.New("cannot specify both --no-healthcheck and --health-cmd")
355}
356s.HealthConfig, err = makeHealthCheckFromCli(c.HealthCmd, c.HealthInterval, c.HealthRetries, c.HealthTimeout, c.HealthStartPeriod, false)
357if err != nil {
358return err
359}
360} else if c.NoHealthCheck {
361s.HealthConfig = &manifest.Schema2HealthConfig{
362Test: []string{"NONE"},
363}
364}
365
366onFailureAction, err := define.ParseHealthCheckOnFailureAction(c.HealthOnFailure)
367if err != nil {
368return err
369}
370s.HealthCheckOnFailureAction = onFailureAction
371
372if c.StartupHCCmd != "" {
373if c.NoHealthCheck {
374return errors.New("cannot specify both --no-healthcheck and --health-startup-cmd")
375}
376// The hardcoded "1s" will be discarded, as the startup
377// healthcheck does not have a period. So just hardcode
378// something that parses correctly.
379tmpHcConfig, err := makeHealthCheckFromCli(c.StartupHCCmd, c.StartupHCInterval, c.StartupHCRetries, c.StartupHCTimeout, "1s", true)
380if err != nil {
381return err
382}
383s.StartupHealthConfig = new(define.StartupHealthCheck)
384s.StartupHealthConfig.Test = tmpHcConfig.Test
385s.StartupHealthConfig.Interval = tmpHcConfig.Interval
386s.StartupHealthConfig.Timeout = tmpHcConfig.Timeout
387s.StartupHealthConfig.Retries = tmpHcConfig.Retries
388s.StartupHealthConfig.Successes = int(c.StartupHCSuccesses)
389}
390
391if err := setNamespaces(rtc, s, c); err != nil {
392return err
393}
394
395if s.Terminal == nil {
396s.Terminal = &c.TTY
397}
398
399if err := verifyExpose(c.Expose); err != nil {
400return err
401}
402// We are not handling the Expose flag yet.
403// s.PortsExpose = c.Expose
404if c.Net != nil {
405s.PortMappings = c.Net.PublishPorts
406}
407if s.PublishExposedPorts == nil {
408s.PublishExposedPorts = &c.PublishAll
409}
410
411if len(s.Pod) == 0 || len(c.Pod) > 0 {
412s.Pod = c.Pod
413}
414
415if len(c.PodIDFile) > 0 {
416if len(s.Pod) > 0 {
417return errors.New("cannot specify both --pod and --pod-id-file")
418}
419podID, err := ReadPodIDFile(c.PodIDFile)
420if err != nil {
421return err
422}
423s.Pod = podID
424}
425
426expose, err := CreateExpose(c.Expose)
427if err != nil {
428return err
429}
430
431if len(s.Expose) == 0 {
432s.Expose = expose
433}
434
435if sig := c.StopSignal; len(sig) > 0 {
436stopSignal, err := util.ParseSignal(sig)
437if err != nil {
438return err
439}
440s.StopSignal = &stopSignal
441}
442
443// ENVIRONMENT VARIABLES
444//
445// Precedence order (higher index wins):
446// 1) containers.conf (EnvHost, EnvHTTP, Env) 2) image data, 3 User EnvHost/EnvHTTP, 4) env-file, 5) env
447// containers.conf handled and image data handled on the server side
448// user specified EnvHost and EnvHTTP handled on Server Side relative to Server
449// env-file and env handled on client side
450var env map[string]string
451
452// First transform the os env into a map. We need it for the labels later in
453// any case.
454osEnv := envLib.Map(os.Environ())
455
456if s.EnvHost == nil {
457s.EnvHost = &c.EnvHost
458}
459
460if s.HTTPProxy == nil {
461s.HTTPProxy = &c.HTTPProxy
462}
463
464// env-file overrides any previous variables
465for _, f := range c.EnvFile {
466fileEnv, err := envLib.ParseFile(f)
467if err != nil {
468return err
469}
470// File env is overridden by env.
471env = envLib.Join(env, fileEnv)
472}
473
474parsedEnv, err := envLib.ParseSlice(c.Env)
475if err != nil {
476return err
477}
478
479if len(s.Env) == 0 {
480s.Env = envLib.Join(env, parsedEnv)
481}
482
483// LABEL VARIABLES
484labels, err := parse.GetAllLabels(c.LabelFile, c.Label)
485if err != nil {
486return fmt.Errorf("unable to process labels: %w", err)
487}
488
489if systemdUnit, exists := osEnv[systemdDefine.EnvVariable]; exists {
490labels[systemdDefine.EnvVariable] = systemdUnit
491}
492
493if len(s.Labels) == 0 {
494s.Labels = labels
495}
496
497// Intel RDT CAT
498if c.IntelRdtClosID != "" {
499s.IntelRdt = &specs.LinuxIntelRdt{}
500s.IntelRdt.ClosID = c.IntelRdtClosID
501}
502
503// ANNOTATIONS
504annotations := make(map[string]string)
505
506// Last, add user annotations
507for _, annotation := range c.Annotation {
508key, val, hasVal := strings.Cut(annotation, "=")
509if !hasVal {
510return errors.New("annotations must be formatted KEY=VALUE")
511}
512annotations[key] = val
513}
514if len(s.Annotations) == 0 {
515s.Annotations = annotations
516}
517
518if len(c.StorageOpts) > 0 {
519opts := make(map[string]string, len(c.StorageOpts))
520for _, opt := range c.StorageOpts {
521key, val, hasVal := strings.Cut(opt, "=")
522if !hasVal {
523return errors.New("storage-opt must be formatted KEY=VALUE")
524}
525opts[key] = val
526}
527s.StorageOpts = opts
528}
529if len(s.WorkDir) == 0 {
530s.WorkDir = c.Workdir
531}
532if c.Entrypoint != nil {
533entrypoint := []string{}
534// Check if entrypoint specified is json
535if err := json.Unmarshal([]byte(*c.Entrypoint), &entrypoint); err != nil {
536entrypoint = append(entrypoint, *c.Entrypoint)
537}
538s.Entrypoint = entrypoint
539}
540
541// Include the command used to create the container.
542
543if len(s.ContainerCreateCommand) == 0 {
544s.ContainerCreateCommand = os.Args
545}
546
547if len(inputCommand) > 0 {
548s.Command = inputCommand
549}
550
551// SHM Size
552if c.ShmSize != "" {
553val, err := units.RAMInBytes(c.ShmSize)
554
555if err != nil {
556return fmt.Errorf("unable to translate --shm-size: %w", err)
557}
558
559s.ShmSize = &val
560}
561
562// SHM Size Systemd
563if c.ShmSizeSystemd != "" {
564val, err := units.RAMInBytes(c.ShmSizeSystemd)
565if err != nil {
566return fmt.Errorf("unable to translate --shm-size-systemd: %w", err)
567}
568
569s.ShmSizeSystemd = &val
570}
571
572if c.Net != nil {
573s.Networks = c.Net.Networks
574}
575
576if c.Net != nil {
577s.HostAdd = c.Net.AddHosts
578s.UseImageResolvConf = &c.Net.UseImageResolvConf
579s.DNSServers = c.Net.DNSServers
580s.DNSSearch = c.Net.DNSSearch
581s.DNSOptions = c.Net.DNSOptions
582s.NetworkOptions = c.Net.NetworkOptions
583s.UseImageHosts = &c.Net.NoHosts
584}
585if len(s.HostUsers) == 0 || len(c.HostUsers) != 0 {
586s.HostUsers = c.HostUsers
587}
588if len(c.ImageVolume) != 0 {
589if len(s.ImageVolumeMode) == 0 {
590s.ImageVolumeMode = c.ImageVolume
591}
592}
593if s.ImageVolumeMode == define.TypeBind {
594s.ImageVolumeMode = "anonymous"
595}
596
597if len(s.Systemd) == 0 || len(c.Systemd) != 0 {
598s.Systemd = strings.ToLower(c.Systemd)
599}
600if len(s.SdNotifyMode) == 0 || len(c.SdNotifyMode) != 0 {
601s.SdNotifyMode = c.SdNotifyMode
602}
603if s.ResourceLimits == nil {
604s.ResourceLimits = &specs.LinuxResources{}
605}
606
607s.ResourceLimits, err = GetResources(s, c)
608if err != nil {
609return err
610}
611
612if s.LogConfiguration == nil {
613s.LogConfiguration = &specgen.LogConfig{}
614}
615
616if ld := c.LogDriver; len(ld) > 0 {
617s.LogConfiguration.Driver = ld
618}
619if len(s.CgroupParent) == 0 || len(c.CgroupParent) != 0 {
620s.CgroupParent = c.CgroupParent
621}
622if len(s.CgroupsMode) == 0 {
623s.CgroupsMode = c.CgroupsMode
624}
625
626if len(s.Groups) == 0 || len(c.GroupAdd) != 0 {
627s.Groups = c.GroupAdd
628}
629
630if len(s.Hostname) == 0 || len(c.Hostname) != 0 {
631s.Hostname = c.Hostname
632}
633sysctl := map[string]string{}
634if ctl := c.Sysctl; len(ctl) > 0 {
635sysctl, err = util.ValidateSysctls(ctl)
636if err != nil {
637return err
638}
639}
640if len(s.Sysctl) == 0 || len(c.Sysctl) != 0 {
641s.Sysctl = sysctl
642}
643
644if len(s.CapAdd) == 0 || len(c.CapAdd) != 0 {
645s.CapAdd = c.CapAdd
646}
647if len(s.CapDrop) == 0 || len(c.CapDrop) != 0 {
648s.CapDrop = c.CapDrop
649}
650if s.Privileged == nil {
651s.Privileged = &c.Privileged
652}
653if s.ReadOnlyFilesystem == nil {
654s.ReadOnlyFilesystem = &c.ReadOnly
655}
656if len(s.ConmonPidFile) == 0 || len(c.ConmonPIDFile) != 0 {
657s.ConmonPidFile = c.ConmonPIDFile
658}
659
660if len(s.DependencyContainers) == 0 || len(c.Requires) != 0 {
661s.DependencyContainers = c.Requires
662}
663
664// Only add ReadWrite tmpfs mounts iff the container is
665// being run ReadOnly and ReadWriteTmpFS is not disabled,
666// (user specifying --read-only-tmpfs=false.)
667localRWTmpfs := c.ReadOnly && c.ReadWriteTmpFS
668s.ReadWriteTmpfs = &localRWTmpfs
669
670// TODO convert to map?
671// check if key=value and convert
672sysmap := make(map[string]string)
673for _, ctl := range c.Sysctl {
674key, val, hasVal := strings.Cut(ctl, "=")
675if !hasVal {
676return fmt.Errorf("invalid sysctl value %q", ctl)
677}
678sysmap[key] = val
679}
680if len(s.Sysctl) == 0 || len(c.Sysctl) != 0 {
681s.Sysctl = sysmap
682}
683
684if c.CIDFile != "" {
685s.Annotations[define.InspectAnnotationCIDFile] = c.CIDFile
686}
687
688for _, opt := range c.SecurityOpt {
689// Docker deprecated the ":" syntax but still supports it,
690// so we need to as well
691var key, val string
692var hasVal bool
693if strings.Contains(opt, "=") {
694key, val, hasVal = strings.Cut(opt, "=")
695} else {
696key, val, hasVal = strings.Cut(opt, ":")
697}
698if !hasVal &&
699key != "no-new-privileges" {
700return fmt.Errorf("invalid --security-opt 1: %q", opt)
701}
702switch key {
703case "apparmor":
704s.ContainerSecurityConfig.ApparmorProfile = val
705s.Annotations[define.InspectAnnotationApparmor] = val
706case "label":
707if val == "nested" {
708localTrue := true
709s.ContainerSecurityConfig.LabelNested = &localTrue
710continue
711}
712// TODO selinux opts and label opts are the same thing
713s.ContainerSecurityConfig.SelinuxOpts = append(s.ContainerSecurityConfig.SelinuxOpts, val)
714s.Annotations[define.InspectAnnotationLabel] = strings.Join(s.ContainerSecurityConfig.SelinuxOpts, ",label=")
715case "mask":
716s.ContainerSecurityConfig.Mask = append(s.ContainerSecurityConfig.Mask, strings.Split(val, ":")...)
717case "proc-opts":
718s.ProcOpts = strings.Split(val, ",")
719case "seccomp":
720s.SeccompProfilePath = val
721s.Annotations[define.InspectAnnotationSeccomp] = val
722// this option is for docker compatibility, it is the same as unmask=ALL
723case "systempaths":
724if val == "unconfined" {
725s.ContainerSecurityConfig.Unmask = append(s.ContainerSecurityConfig.Unmask, []string{"ALL"}...)
726} else {
727return fmt.Errorf("invalid systempaths option %q, only `unconfined` is supported", val)
728}
729case "unmask":
730if hasVal {
731s.ContainerSecurityConfig.Unmask = append(s.ContainerSecurityConfig.Unmask, val)
732}
733case "no-new-privileges":
734noNewPrivileges := true
735if hasVal {
736noNewPrivileges, err = strconv.ParseBool(val)
737if err != nil {
738return fmt.Errorf("invalid --security-opt 2: %q", opt)
739}
740}
741s.ContainerSecurityConfig.NoNewPrivileges = &noNewPrivileges
742default:
743return fmt.Errorf("invalid --security-opt 2: %q", opt)
744}
745}
746
747if len(s.SeccompPolicy) == 0 || len(c.SeccompPolicy) != 0 {
748s.SeccompPolicy = c.SeccompPolicy
749}
750
751if len(s.VolumesFrom) == 0 || len(c.VolumesFrom) != 0 {
752s.VolumesFrom = c.VolumesFrom
753}
754
755// Only add read-only tmpfs mounts in case that we are read-only and the
756// read-only tmpfs flag has been set.
757mounts, volumes, overlayVolumes, imageVolumes, err := parseVolumes(rtc, c.Volume, c.Mount, c.TmpFS)
758if err != nil {
759return err
760}
761if len(s.Mounts) == 0 || len(c.Mount) != 0 {
762s.Mounts = mounts
763}
764if len(s.Volumes) == 0 || len(c.Volume) != 0 {
765s.Volumes = volumes
766}
767
768if s.LabelNested != nil && *s.LabelNested {
769// Need to unmask the SELinux file system
770s.Unmask = append(s.Unmask, "/sys/fs/selinux", "/proc")
771s.Mounts = append(s.Mounts, specs.Mount{
772Source: "/sys/fs/selinux",
773Destination: "/sys/fs/selinux",
774Type: define.TypeBind,
775})
776s.Annotations[define.RunOCIMountContextType] = "rootcontext"
777}
778// TODO make sure these work in clone
779if len(s.OverlayVolumes) == 0 {
780s.OverlayVolumes = overlayVolumes
781}
782if len(s.ImageVolumes) == 0 {
783s.ImageVolumes = imageVolumes
784}
785
786devices := c.Devices
787for _, gpu := range c.GPUs {
788devices = append(devices, "nvidia.com/gpu="+gpu)
789}
790
791for _, dev := range devices {
792s.Devices = append(s.Devices, specs.LinuxDevice{Path: dev})
793}
794
795for _, rule := range c.DeviceCgroupRule {
796dev, err := parseLinuxResourcesDeviceAccess(rule)
797if err != nil {
798return err
799}
800s.DeviceCgroupRule = append(s.DeviceCgroupRule, dev)
801}
802
803if s.Init == nil {
804s.Init = &c.Init
805}
806if len(s.InitPath) == 0 || len(c.InitPath) != 0 {
807s.InitPath = c.InitPath
808}
809if s.Stdin == nil {
810s.Stdin = &c.Interactive
811}
812// quiet
813// DeviceCgroupRules: c.StringSlice("device-cgroup-rule"),
814
815// Rlimits/Ulimits
816s.Rlimits, err = GenRlimits(c.Ulimit)
817if err != nil {
818return err
819}
820
821logOpts := make(map[string]string)
822for _, o := range c.LogOptions {
823key, val, hasVal := strings.Cut(o, "=")
824if !hasVal {
825return fmt.Errorf("invalid log option %q", o)
826}
827switch strings.ToLower(key) {
828case "driver":
829s.LogConfiguration.Driver = val
830case "path":
831s.LogConfiguration.Path = val
832case "max-size":
833logSize, err := units.FromHumanSize(val)
834if err != nil {
835return err
836}
837s.LogConfiguration.Size = logSize
838default:
839logOpts[key] = val
840}
841}
842if len(s.LogConfiguration.Options) == 0 || len(c.LogOptions) != 0 {
843s.LogConfiguration.Options = logOpts
844}
845if len(s.Name) == 0 || len(c.Name) != 0 {
846s.Name = c.Name
847}
848
849if c.PreserveFDs != 0 && c.PreserveFD != nil {
850return errors.New("cannot specify both --preserve-fds and --preserve-fd")
851}
852
853if s.PreserveFDs == 0 || c.PreserveFDs != 0 {
854s.PreserveFDs = c.PreserveFDs
855}
856if s.PreserveFD == nil || c.PreserveFD != nil {
857s.PreserveFD = c.PreserveFD
858}
859
860if s.OOMScoreAdj == nil || c.OOMScoreAdj != nil {
861s.OOMScoreAdj = c.OOMScoreAdj
862}
863if c.Restart != "" {
864policy, retries, err := util.ParseRestartPolicy(c.Restart)
865if err != nil {
866return err
867}
868s.RestartPolicy = policy
869s.RestartRetries = &retries
870}
871
872if len(s.Secrets) == 0 || len(c.Secrets) != 0 {
873s.Secrets, s.EnvSecrets, err = parseSecrets(c.Secrets)
874if err != nil {
875return err
876}
877}
878
879if c.Personality != "" {
880s.Personality = &specs.LinuxPersonality{}
881s.Personality.Domain = specs.LinuxPersonalityDomain(c.Personality)
882}
883
884if s.Remove == nil {
885s.Remove = &c.Rm
886}
887if s.StopTimeout == nil || c.StopTimeout != 0 {
888s.StopTimeout = &c.StopTimeout
889}
890if s.Timeout == 0 || c.Timeout != 0 {
891s.Timeout = c.Timeout
892}
893if len(s.Timezone) == 0 || len(c.Timezone) != 0 {
894s.Timezone = c.Timezone
895}
896if len(s.Umask) == 0 || len(c.Umask) != 0 {
897s.Umask = c.Umask
898}
899if len(s.PidFile) == 0 || len(c.PidFile) != 0 {
900s.PidFile = c.PidFile
901}
902if s.Volatile == nil {
903s.Volatile = &c.Rm
904}
905if len(s.EnvMerge) == 0 || len(c.EnvMerge) != 0 {
906s.EnvMerge = c.EnvMerge
907}
908if len(s.UnsetEnv) == 0 || len(c.UnsetEnv) != 0 {
909s.UnsetEnv = c.UnsetEnv
910}
911if s.UnsetEnvAll == nil {
912s.UnsetEnvAll = &c.UnsetEnvAll
913}
914if len(s.ChrootDirs) == 0 || len(c.ChrootDirs) != 0 {
915s.ChrootDirs = c.ChrootDirs
916}
917
918// Initcontainers
919if len(s.InitContainerType) == 0 || len(c.InitContainerType) != 0 {
920s.InitContainerType = c.InitContainerType
921}
922
923t := true
924if s.Passwd == nil {
925s.Passwd = &t
926}
927
928if len(s.PasswdEntry) == 0 || len(c.PasswdEntry) != 0 {
929s.PasswdEntry = c.PasswdEntry
930}
931
932if len(s.GroupEntry) == 0 || len(c.GroupEntry) != 0 {
933s.GroupEntry = c.GroupEntry
934}
935
936return nil
937}
938
939func makeHealthCheckFromCli(inCmd, interval string, retries uint, timeout, startPeriod string, isStartup bool) (*manifest.Schema2HealthConfig, error) {
940cmdArr := []string{}
941isArr := true
942err := json.Unmarshal([]byte(inCmd), &cmdArr) // array unmarshalling
943if err != nil {
944cmdArr = strings.SplitN(inCmd, " ", 2) // default for compat
945isArr = false
946}
947// Every healthcheck requires a command
948if len(cmdArr) == 0 {
949return nil, errors.New("must define a healthcheck command for all healthchecks")
950}
951
952var concat string
953if 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
954cmdArr = strings.Fields(inCmd)
955} else if strings.ToUpper(cmdArr[0]) != define.HealthConfigTestCmdShell { // this is for podman side of things, won't contain the keywords
956if isArr && len(cmdArr) > 1 { // an array of consecutive commands
957cmdArr = append([]string{define.HealthConfigTestCmd}, cmdArr...)
958} else { // one singular command
959if len(cmdArr) == 1 {
960concat = cmdArr[0]
961} else {
962concat = strings.Join(cmdArr[0:], " ")
963}
964cmdArr = append([]string{define.HealthConfigTestCmdShell}, concat)
965}
966}
967
968if strings.ToUpper(cmdArr[0]) == define.HealthConfigTestNone { // if specified to remove healtcheck
969cmdArr = []string{define.HealthConfigTestNone}
970}
971
972// healthcheck is by default an array, so we simply pass the user input
973hc := manifest.Schema2HealthConfig{
974Test: cmdArr,
975}
976
977if interval == "disable" {
978interval = "0"
979}
980intervalDuration, err := time.ParseDuration(interval)
981if err != nil {
982return nil, fmt.Errorf("invalid healthcheck-interval: %w", err)
983}
984
985hc.Interval = intervalDuration
986
987if retries < 1 && !isStartup {
988return nil, errors.New("healthcheck-retries must be greater than 0")
989}
990hc.Retries = int(retries)
991timeoutDuration, err := time.ParseDuration(timeout)
992if err != nil {
993return nil, fmt.Errorf("invalid healthcheck-timeout: %w", err)
994}
995if timeoutDuration < time.Duration(1) {
996return nil, errors.New("healthcheck-timeout must be at least 1 second")
997}
998hc.Timeout = timeoutDuration
999
1000startPeriodDuration, err := time.ParseDuration(startPeriod)
1001if err != nil {
1002return nil, fmt.Errorf("invalid healthcheck-start-period: %w", err)
1003}
1004if startPeriodDuration < time.Duration(0) {
1005return nil, errors.New("healthcheck-start-period must be 0 seconds or greater")
1006}
1007hc.StartPeriod = startPeriodDuration
1008
1009return &hc, nil
1010}
1011
1012func parseWeightDevices(weightDevs []string) (map[string]specs.LinuxWeightDevice, error) {
1013wd := make(map[string]specs.LinuxWeightDevice)
1014for _, dev := range weightDevs {
1015key, val, hasVal := strings.Cut(dev, ":")
1016if !hasVal {
1017return nil, fmt.Errorf("bad format: %s", dev)
1018}
1019if !strings.HasPrefix(key, "/dev/") {
1020return nil, fmt.Errorf("bad format for device path: %s", dev)
1021}
1022weight, err := strconv.ParseUint(val, 10, 0)
1023if err != nil {
1024return nil, fmt.Errorf("invalid weight for device: %s", dev)
1025}
1026if weight > 0 && (weight < 10 || weight > 1000) {
1027return nil, fmt.Errorf("invalid weight for device: %s", dev)
1028}
1029w := uint16(weight)
1030wd[key] = specs.LinuxWeightDevice{
1031Weight: &w,
1032LeafWeight: nil,
1033}
1034}
1035return wd, nil
1036}
1037
1038func parseThrottleBPSDevices(bpsDevices []string) (map[string]specs.LinuxThrottleDevice, error) {
1039td := make(map[string]specs.LinuxThrottleDevice)
1040for _, dev := range bpsDevices {
1041key, val, hasVal := strings.Cut(dev, ":")
1042if !hasVal {
1043return nil, fmt.Errorf("bad format: %s", dev)
1044}
1045if !strings.HasPrefix(key, "/dev/") {
1046return nil, fmt.Errorf("bad format for device path: %s", dev)
1047}
1048rate, err := units.RAMInBytes(val)
1049if err != nil {
1050return nil, fmt.Errorf("invalid rate for device: %s. The correct format is <device-path>:<number>[<unit>]. Number must be a positive integer. Unit is optional and can be kb, mb, or gb", dev)
1051}
1052if rate < 0 {
1053return nil, fmt.Errorf("invalid rate for device: %s. The correct format is <device-path>:<number>[<unit>]. Number must be a positive integer. Unit is optional and can be kb, mb, or gb", dev)
1054}
1055td[key] = specs.LinuxThrottleDevice{Rate: uint64(rate)}
1056}
1057return td, nil
1058}
1059
1060func parseThrottleIOPsDevices(iopsDevices []string) (map[string]specs.LinuxThrottleDevice, error) {
1061td := make(map[string]specs.LinuxThrottleDevice)
1062for _, dev := range iopsDevices {
1063key, val, hasVal := strings.Cut(dev, ":")
1064if !hasVal {
1065return nil, fmt.Errorf("bad format: %s", dev)
1066}
1067if !strings.HasPrefix(key, "/dev/") {
1068return nil, fmt.Errorf("bad format for device path: %s", dev)
1069}
1070rate, err := strconv.ParseUint(val, 10, 64)
1071if err != nil {
1072return nil, fmt.Errorf("invalid rate for device: %s. The correct format is <device-path>:<number>. Number must be a positive integer", dev)
1073}
1074td[key] = specs.LinuxThrottleDevice{Rate: rate}
1075}
1076return td, nil
1077}
1078
1079func parseSecrets(secrets []string) ([]specgen.Secret, map[string]string, error) {
1080secretParseError := errors.New("parsing secret")
1081var mount []specgen.Secret
1082envs := make(map[string]string)
1083for _, val := range secrets {
1084// mount only tells if user has set an option that can only be used with mount secret type
1085mountOnly := false
1086source := ""
1087secretType := ""
1088target := ""
1089var uid, gid uint32
1090// default mode 444 octal = 292 decimal
1091var mode uint32 = 292
1092split := strings.Split(val, ",")
1093
1094// --secret mysecret
1095if len(split) == 1 {
1096mountSecret := specgen.Secret{
1097Source: val,
1098Target: target,
1099UID: uid,
1100GID: gid,
1101Mode: mode,
1102}
1103mount = append(mount, mountSecret)
1104continue
1105}
1106// --secret mysecret,opt=opt
1107if !strings.Contains(split[0], "=") {
1108source = split[0]
1109split = split[1:]
1110}
1111
1112for _, val := range split {
1113name, value, hasValue := strings.Cut(val, "=")
1114if !hasValue {
1115return nil, nil, fmt.Errorf("option %s must be in form option=value: %w", val, secretParseError)
1116}
1117switch name {
1118case "source":
1119source = value
1120case "type":
1121if secretType != "" {
1122return nil, nil, fmt.Errorf("cannot set more than one secret type: %w", secretParseError)
1123}
1124if value != "mount" && value != "env" {
1125return nil, nil, fmt.Errorf("type %s is invalid: %w", value, secretParseError)
1126}
1127secretType = value
1128case "target":
1129target = value
1130case "mode":
1131mountOnly = true
1132mode64, err := strconv.ParseUint(value, 8, 32)
1133if err != nil {
1134return nil, nil, fmt.Errorf("mode %s invalid: %w", value, secretParseError)
1135}
1136mode = uint32(mode64)
1137case "uid", "UID":
1138mountOnly = true
1139uid64, err := strconv.ParseUint(value, 10, 32)
1140if err != nil {
1141return nil, nil, fmt.Errorf("UID %s invalid: %w", value, secretParseError)
1142}
1143uid = uint32(uid64)
1144case "gid", "GID":
1145mountOnly = true
1146gid64, err := strconv.ParseUint(value, 10, 32)
1147if err != nil {
1148return nil, nil, fmt.Errorf("GID %s invalid: %w", value, secretParseError)
1149}
1150gid = uint32(gid64)
1151
1152default:
1153return nil, nil, fmt.Errorf("option %s invalid: %w", val, secretParseError)
1154}
1155}
1156
1157if secretType == "" {
1158secretType = "mount"
1159}
1160if source == "" {
1161return nil, nil, fmt.Errorf("no source found %s: %w", val, secretParseError)
1162}
1163if secretType == "mount" {
1164mountSecret := specgen.Secret{
1165Source: source,
1166Target: target,
1167UID: uid,
1168GID: gid,
1169Mode: mode,
1170}
1171mount = append(mount, mountSecret)
1172}
1173if secretType == "env" {
1174if mountOnly {
1175return nil, nil, fmt.Errorf("UID, GID, Mode options cannot be set with secret type env: %w", secretParseError)
1176}
1177if target == "" {
1178target = source
1179}
1180envs[target] = source
1181}
1182}
1183return mount, envs, nil
1184}
1185
1186var cgroupDeviceType = map[string]bool{
1187"a": true, // all
1188"b": true, // block device
1189"c": true, // character device
1190}
1191
1192var cgroupDeviceAccess = map[string]bool{
1193"r": true, // read
1194"w": true, // write
1195"m": true, // mknod
1196}
1197
1198// parseLinuxResourcesDeviceAccess parses the raw string passed with the --device-access-add flag
1199func parseLinuxResourcesDeviceAccess(device string) (specs.LinuxDeviceCgroup, error) {
1200var devType, access string
1201var major, minor *int64
1202
1203value := strings.Split(device, " ")
1204if len(value) != 3 {
1205return specs.LinuxDeviceCgroup{}, fmt.Errorf("invalid device cgroup rule requires type, major:Minor, and access rules: %q", device)
1206}
1207
1208devType = value[0]
1209if !cgroupDeviceType[devType] {
1210return specs.LinuxDeviceCgroup{}, fmt.Errorf("invalid device type in device-access-add: %s", devType)
1211}
1212
1213majorNumber, minorNumber, hasMinor := strings.Cut(value[1], ":")
1214if majorNumber != "*" {
1215i, err := strconv.ParseUint(majorNumber, 10, 64)
1216if err != nil {
1217return specs.LinuxDeviceCgroup{}, err
1218}
1219m := int64(i)
1220major = &m
1221}
1222if hasMinor && minorNumber != "*" {
1223i, err := strconv.ParseUint(minorNumber, 10, 64)
1224if err != nil {
1225return specs.LinuxDeviceCgroup{}, err
1226}
1227m := int64(i)
1228minor = &m
1229}
1230access = value[2]
1231for _, c := range strings.Split(access, "") {
1232if !cgroupDeviceAccess[c] {
1233return specs.LinuxDeviceCgroup{}, fmt.Errorf("invalid device access in device-access-add: %s", c)
1234}
1235}
1236return specs.LinuxDeviceCgroup{
1237Allow: true,
1238Type: devType,
1239Major: major,
1240Minor: minor,
1241Access: access,
1242}, nil
1243}
1244
1245func GetResources(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions) (*specs.LinuxResources, error) {
1246var err error
1247if s.ResourceLimits.Memory == nil || (len(c.Memory) != 0 || len(c.MemoryReservation) != 0 || len(c.MemorySwap) != 0 || c.MemorySwappiness != 0) {
1248s.ResourceLimits.Memory, err = getMemoryLimits(c)
1249if err != nil {
1250return nil, err
1251}
1252}
1253if s.ResourceLimits.BlockIO == nil || (len(c.BlkIOWeight) != 0 || len(c.BlkIOWeightDevice) != 0 || len(c.DeviceReadBPs) != 0 || len(c.DeviceWriteBPs) != 0) {
1254s.ResourceLimits.BlockIO, err = getIOLimits(s, c)
1255if err != nil {
1256return nil, err
1257}
1258}
1259if c.PIDsLimit != nil {
1260pids := specs.LinuxPids{
1261Limit: *c.PIDsLimit,
1262}
1263
1264s.ResourceLimits.Pids = &pids
1265}
1266
1267if 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) {
1268s.ResourceLimits.CPU = getCPULimits(c)
1269}
1270
1271unifieds := make(map[string]string)
1272for _, unified := range c.CgroupConf {
1273key, val, hasVal := strings.Cut(unified, "=")
1274if !hasVal {
1275return nil, errors.New("--cgroup-conf must be formatted KEY=VALUE")
1276}
1277unifieds[key] = val
1278}
1279if len(unifieds) > 0 {
1280s.ResourceLimits.Unified = unifieds
1281}
1282
1283if s.ResourceLimits.CPU == nil && s.ResourceLimits.Pids == nil && s.ResourceLimits.BlockIO == nil && s.ResourceLimits.Memory == nil && s.ResourceLimits.Unified == nil {
1284s.ResourceLimits = nil
1285}
1286return s.ResourceLimits, nil
1287}
1288