github.com/containers/libpod@v1.9.4-0.20220419124438-4284fd425507/cmd/podmanV2/common/specgen.go (about) 1 package common 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "os" 7 "path/filepath" 8 "strconv" 9 "strings" 10 "time" 11 12 "github.com/containers/image/v5/manifest" 13 "github.com/containers/libpod/cmd/podmanV2/parse" 14 "github.com/containers/libpod/libpod" 15 ann "github.com/containers/libpod/pkg/annotations" 16 envLib "github.com/containers/libpod/pkg/env" 17 ns "github.com/containers/libpod/pkg/namespaces" 18 "github.com/containers/libpod/pkg/specgen" 19 systemdGen "github.com/containers/libpod/pkg/systemd/generate" 20 "github.com/containers/libpod/pkg/util" 21 "github.com/docker/go-units" 22 "github.com/opencontainers/runtime-spec/specs-go" 23 "github.com/pkg/errors" 24 ) 25 26 func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string) error { 27 var ( 28 err error 29 //namespaces map[string]string 30 ) 31 32 // validate flags as needed 33 if err := c.validate(); err != nil { 34 return nil 35 } 36 37 inputCommand := args[1:] 38 if len(c.HealthCmd) > 0 { 39 s.HealthConfig, err = makeHealthCheckFromCli(c.HealthCmd, c.HealthInterval, c.HealthRetries, c.HealthTimeout, c.HealthStartPeriod) 40 if err != nil { 41 return err 42 } 43 } 44 45 s.IDMappings, err = util.ParseIDMapping(ns.UsernsMode(c.UserNS), c.UIDMap, c.GIDMap, c.SubUIDName, c.SubGIDName) 46 if err != nil { 47 return err 48 } 49 if m := c.Memory; len(m) > 0 { 50 ml, err := units.RAMInBytes(m) 51 if err != nil { 52 return errors.Wrapf(err, "invalid value for memory") 53 } 54 s.ResourceLimits.Memory.Limit = &ml 55 } 56 if m := c.MemoryReservation; len(m) > 0 { 57 mr, err := units.RAMInBytes(m) 58 if err != nil { 59 return errors.Wrapf(err, "invalid value for memory") 60 } 61 s.ResourceLimits.Memory.Reservation = &mr 62 } 63 if m := c.MemorySwap; len(m) > 0 { 64 var ms int64 65 if m == "-1" { 66 ms = int64(-1) 67 s.ResourceLimits.Memory.Swap = &ms 68 } else { 69 ms, err = units.RAMInBytes(m) 70 if err != nil { 71 return errors.Wrapf(err, "invalid value for memory") 72 } 73 } 74 s.ResourceLimits.Memory.Swap = &ms 75 } 76 if m := c.KernelMemory; len(m) > 0 { 77 mk, err := units.RAMInBytes(m) 78 if err != nil { 79 return errors.Wrapf(err, "invalid value for kernel-memory") 80 } 81 s.ResourceLimits.Memory.Kernel = &mk 82 } 83 if b := c.BlkIOWeight; len(b) > 0 { 84 u, err := strconv.ParseUint(b, 10, 16) 85 if err != nil { 86 return errors.Wrapf(err, "invalid value for blkio-weight") 87 } 88 nu := uint16(u) 89 s.ResourceLimits.BlockIO.Weight = &nu 90 } 91 92 s.Terminal = c.TTY 93 ep, err := ExposedPorts(c.Expose, c.Net.PublishPorts, c.PublishAll, nil) 94 if err != nil { 95 return err 96 } 97 s.PortMappings = ep 98 s.Pod = c.Pod 99 100 //s.CgroupNS = specgen.Namespace{ 101 // NSMode: , 102 // Value: "", 103 //} 104 105 //s.UserNS = specgen.Namespace{} 106 107 // Kernel Namespaces 108 // TODO Fix handling of namespace from pod 109 // Instead of integrating here, should be done in libpod 110 // However, that also involves setting up security opts 111 // when the pod's namespace is integrated 112 //namespaces = map[string]string{ 113 // "cgroup": c.CGroupsNS, 114 // "pid": c.PID, 115 // //"net": c.Net.Network.Value, // TODO need help here 116 // "ipc": c.IPC, 117 // "user": c.User, 118 // "uts": c.UTS, 119 //} 120 // 121 //if len(c.PID) > 0 { 122 // split := strings.SplitN(c.PID, ":", 2) 123 // // need a way to do thsi 124 // specgen.Namespace{ 125 // NSMode: split[0], 126 // } 127 // //Value: split1 if len allows 128 //} 129 // TODO this is going to have be done after things like pod creation are done because 130 // pod creation changes these values. 131 //pidMode := ns.PidMode(namespaces["pid"]) 132 //usernsMode := ns.UsernsMode(namespaces["user"]) 133 //utsMode := ns.UTSMode(namespaces["uts"]) 134 //cgroupMode := ns.CgroupMode(namespaces["cgroup"]) 135 //ipcMode := ns.IpcMode(namespaces["ipc"]) 136 //// Make sure if network is set to container namespace, port binding is not also being asked for 137 //netMode := ns.NetworkMode(namespaces["net"]) 138 //if netMode.IsContainer() { 139 // if len(portBindings) > 0 { 140 // return nil, errors.Errorf("cannot set port bindings on an existing container network namespace") 141 // } 142 //} 143 144 // TODO Remove when done with namespaces for realz 145 // Setting a default for IPC to get this working 146 s.IpcNS = specgen.Namespace{ 147 NSMode: specgen.Private, 148 Value: "", 149 } 150 151 // TODO this is going to have to be done the libpod/server end of things 152 // USER 153 //user := c.String("user") 154 //if user == "" { 155 // switch { 156 // case usernsMode.IsKeepID(): 157 // user = fmt.Sprintf("%d:%d", rootless.GetRootlessUID(), rootless.GetRootlessGID()) 158 // case data == nil: 159 // user = "0" 160 // default: 161 // user = data.Config.User 162 // } 163 //} 164 165 // STOP SIGNAL 166 signalString := "TERM" 167 if sig := c.StopSignal; len(sig) > 0 { 168 signalString = sig 169 } 170 stopSignal, err := util.ParseSignal(signalString) 171 if err != nil { 172 return err 173 } 174 s.StopSignal = &stopSignal 175 176 // ENVIRONMENT VARIABLES 177 // 178 // Precedence order (higher index wins): 179 // 1) env-host, 2) image data, 3) env-file, 4) env 180 env := map[string]string{ 181 "container": "podman", 182 } 183 184 // First transform the os env into a map. We need it for the labels later in 185 // any case. 186 osEnv, err := envLib.ParseSlice(os.Environ()) 187 if err != nil { 188 return errors.Wrap(err, "error parsing host environment variables") 189 } 190 191 if c.EnvHost { 192 env = envLib.Join(env, osEnv) 193 } 194 // env-file overrides any previous variables 195 for _, f := range c.EnvFile { 196 fileEnv, err := envLib.ParseFile(f) 197 if err != nil { 198 return err 199 } 200 // File env is overridden by env. 201 env = envLib.Join(env, fileEnv) 202 } 203 204 // env overrides any previous variables 205 if cmdLineEnv := c.env; len(cmdLineEnv) > 0 { 206 parsedEnv, err := envLib.ParseSlice(cmdLineEnv) 207 if err != nil { 208 return err 209 } 210 env = envLib.Join(env, parsedEnv) 211 } 212 s.Env = env 213 214 // LABEL VARIABLES 215 labels, err := parse.GetAllLabels(c.LabelFile, c.Label) 216 if err != nil { 217 return errors.Wrapf(err, "unable to process labels") 218 } 219 220 if systemdUnit, exists := osEnv[systemdGen.EnvVariable]; exists { 221 labels[systemdGen.EnvVariable] = systemdUnit 222 } 223 224 s.Labels = labels 225 226 // ANNOTATIONS 227 annotations := make(map[string]string) 228 229 // First, add our default annotations 230 annotations[ann.TTY] = "false" 231 if c.TTY { 232 annotations[ann.TTY] = "true" 233 } 234 235 // Last, add user annotations 236 for _, annotation := range c.Annotation { 237 splitAnnotation := strings.SplitN(annotation, "=", 2) 238 if len(splitAnnotation) < 2 { 239 return errors.Errorf("Annotations must be formatted KEY=VALUE") 240 } 241 annotations[splitAnnotation[0]] = splitAnnotation[1] 242 } 243 s.Annotations = annotations 244 245 workDir := "/" 246 if wd := c.Workdir; len(wd) > 0 { 247 workDir = wd 248 } 249 s.WorkDir = workDir 250 entrypoint := []string{} 251 userCommand := []string{} 252 if ep := c.Entrypoint; len(ep) > 0 { 253 // Check if entrypoint specified is json 254 if err := json.Unmarshal([]byte(c.Entrypoint), &entrypoint); err != nil { 255 entrypoint = append(entrypoint, ep) 256 } 257 } 258 259 var command []string 260 261 // Build the command 262 // If we have an entry point, it goes first 263 if len(entrypoint) > 0 { 264 command = entrypoint 265 } 266 if len(inputCommand) > 0 { 267 // User command overrides data CMD 268 command = append(command, inputCommand...) 269 userCommand = append(userCommand, inputCommand...) 270 } 271 272 if len(inputCommand) > 0 { 273 s.Command = userCommand 274 } else { 275 s.Command = command 276 } 277 278 // SHM Size 279 shmSize, err := units.FromHumanSize(c.ShmSize) 280 if err != nil { 281 return errors.Wrapf(err, "unable to translate --shm-size") 282 } 283 s.ShmSize = &shmSize 284 s.HostAdd = c.Net.AddHosts 285 s.DNSServer = c.Net.DNSServers 286 s.DNSSearch = c.Net.DNSSearch 287 s.DNSOption = c.Net.DNSOptions 288 289 // deferred, must be added on libpod side 290 //var ImageVolumes map[string]struct{} 291 //if data != nil && c.String("image-volume") != "ignore" { 292 // ImageVolumes = data.Config.Volumes 293 //} 294 295 s.ImageVolumeMode = c.ImageVolume 296 systemd := c.SystemdD == "always" 297 if !systemd && command != nil { 298 x, err := strconv.ParseBool(c.SystemdD) 299 if err != nil { 300 return errors.Wrapf(err, "cannot parse bool %s", c.SystemdD) 301 } 302 if x && (command[0] == "/usr/sbin/init" || command[0] == "/sbin/init" || (filepath.Base(command[0]) == "systemd")) { 303 systemd = true 304 } 305 } 306 if systemd { 307 if s.StopSignal == nil { 308 stopSignal, err = util.ParseSignal("RTMIN+3") 309 if err != nil { 310 return errors.Wrapf(err, "error parsing systemd signal") 311 } 312 s.StopSignal = &stopSignal 313 } 314 } 315 swappiness := uint64(c.MemorySwappiness) 316 if s.ResourceLimits == nil { 317 s.ResourceLimits = &specs.LinuxResources{} 318 } 319 if s.ResourceLimits.Memory == nil { 320 s.ResourceLimits.Memory = &specs.LinuxMemory{} 321 } 322 s.ResourceLimits.Memory.Swappiness = &swappiness 323 324 if s.LogConfiguration == nil { 325 s.LogConfiguration = &specgen.LogConfig{} 326 } 327 s.LogConfiguration.Driver = libpod.KubernetesLogging 328 if ld := c.LogDriver; len(ld) > 0 { 329 s.LogConfiguration.Driver = ld 330 } 331 if s.ResourceLimits.Pids == nil { 332 s.ResourceLimits.Pids = &specs.LinuxPids{} 333 } 334 s.ResourceLimits.Pids.Limit = c.PIDsLimit 335 if c.CGroups == "disabled" && c.PIDsLimit > 0 { 336 s.ResourceLimits.Pids.Limit = -1 337 } 338 // TODO WTF 339 //cgroup := &cc.CgroupConfig{ 340 // Cgroups: c.String("cgroups"), 341 // Cgroupns: c.String("cgroupns"), 342 // CgroupParent: c.String("cgroup-parent"), 343 // CgroupMode: cgroupMode, 344 //} 345 // 346 //userns := &cc.UserConfig{ 347 // GroupAdd: c.StringSlice("group-add"), 348 // IDMappings: idmappings, 349 // UsernsMode: usernsMode, 350 // User: user, 351 //} 352 // 353 //uts := &cc.UtsConfig{ 354 // UtsMode: utsMode, 355 // NoHosts: c.Bool("no-hosts"), 356 // HostAdd: c.StringSlice("add-host"), 357 // Hostname: c.String("hostname"), 358 //} 359 360 sysctl := map[string]string{} 361 if ctl := c.Sysctl; len(ctl) > 0 { 362 sysctl, err = util.ValidateSysctls(ctl) 363 if err != nil { 364 return err 365 } 366 } 367 s.Sysctl = sysctl 368 369 s.CapAdd = c.CapAdd 370 s.CapDrop = c.CapDrop 371 s.Privileged = c.Privileged 372 s.ReadOnlyFilesystem = c.ReadOnly 373 374 // TODO 375 // ouitside of specgen and oci though 376 // defaults to true, check spec/storage 377 //s.readon = c.ReadOnlyTmpFS 378 // TODO convert to map? 379 // check if key=value and convert 380 sysmap := make(map[string]string) 381 for _, ctl := range c.Sysctl { 382 splitCtl := strings.SplitN(ctl, "=", 2) 383 if len(splitCtl) < 2 { 384 return errors.Errorf("invalid sysctl value %q", ctl) 385 } 386 sysmap[splitCtl[0]] = splitCtl[1] 387 } 388 s.Sysctl = sysmap 389 390 for _, opt := range c.SecurityOpt { 391 if opt == "no-new-privileges" { 392 s.ContainerSecurityConfig.NoNewPrivileges = true 393 } else { 394 con := strings.SplitN(opt, "=", 2) 395 if len(con) != 2 { 396 return fmt.Errorf("invalid --security-opt 1: %q", opt) 397 } 398 399 switch con[0] { 400 case "label": 401 // TODO selinux opts and label opts are the same thing 402 s.ContainerSecurityConfig.SelinuxOpts = append(s.ContainerSecurityConfig.SelinuxOpts, con[1]) 403 case "apparmor": 404 s.ContainerSecurityConfig.ApparmorProfile = con[1] 405 case "seccomp": 406 s.SeccompProfilePath = con[1] 407 default: 408 return fmt.Errorf("invalid --security-opt 2: %q", opt) 409 } 410 } 411 } 412 413 // TODO any idea why this was done 414 // storage.go from spec/ 415 // grab it 416 //volumes := rtc.Containers.Volumes 417 // TODO conflict on populate? 418 //if v := c.Volume; len(v)> 0 { 419 // s.Volumes = append(volumes, c.StringSlice("volume")...) 420 //} 421 //s.volu 422 423 //s.Mounts = c.Mount 424 s.VolumesFrom = c.VolumesFrom 425 426 // TODO any idea why this was done 427 //devices := rtc.Containers.Devices 428 // TODO conflict on populate? 429 // 430 //if c.Changed("device") { 431 // devices = append(devices, c.StringSlice("device")...) 432 //} 433 434 // TODO things i cannot find in spec 435 // we dont think these are in the spec 436 // init - initbinary 437 // initpath 438 s.Stdin = c.Interactive 439 // quiet 440 //DeviceCgroupRules: c.StringSlice("device-cgroup-rule"), 441 442 if bps := c.DeviceReadBPs; len(bps) > 0 { 443 if s.ThrottleReadBpsDevice, err = parseThrottleBPSDevices(bps); err != nil { 444 return err 445 } 446 } 447 448 if bps := c.DeviceWriteBPs; len(bps) > 0 { 449 if s.ThrottleWriteBpsDevice, err = parseThrottleBPSDevices(bps); err != nil { 450 return err 451 } 452 } 453 454 if iops := c.DeviceReadIOPs; len(iops) > 0 { 455 if s.ThrottleReadIOPSDevice, err = parseThrottleIOPsDevices(iops); err != nil { 456 return err 457 } 458 } 459 460 if iops := c.DeviceWriteIOPs; len(iops) > 0 { 461 if s.ThrottleWriteIOPSDevice, err = parseThrottleIOPsDevices(iops); err != nil { 462 return err 463 } 464 } 465 466 s.ResourceLimits.Memory.DisableOOMKiller = &c.OOMKillDisable 467 468 // Rlimits/Ulimits 469 for _, u := range c.Ulimit { 470 if u == "host" { 471 s.Rlimits = nil 472 break 473 } 474 ul, err := units.ParseUlimit(u) 475 if err != nil { 476 return errors.Wrapf(err, "ulimit option %q requires name=SOFT:HARD, failed to be parsed", u) 477 } 478 rl := specs.POSIXRlimit{ 479 Type: ul.Name, 480 Hard: uint64(ul.Hard), 481 Soft: uint64(ul.Soft), 482 } 483 s.Rlimits = append(s.Rlimits, rl) 484 } 485 486 //Tmpfs: c.StringArray("tmpfs"), 487 488 // TODO how to handle this? 489 //Syslog: c.Bool("syslog"), 490 491 logOpts := make(map[string]string) 492 for _, o := range c.LogOptions { 493 split := strings.SplitN(o, "=", 2) 494 if len(split) < 2 { 495 return errors.Errorf("invalid log option %q", o) 496 } 497 logOpts[split[0]] = split[1] 498 } 499 s.LogConfiguration.Options = logOpts 500 s.Name = c.Name 501 502 if err := parseWeightDevices(c.BlkIOWeightDevice, s); err != nil { 503 return err 504 } 505 506 if s.ResourceLimits.CPU == nil { 507 s.ResourceLimits.CPU = &specs.LinuxCPU{} 508 } 509 s.ResourceLimits.CPU.Shares = &c.CPUShares 510 s.ResourceLimits.CPU.Period = &c.CPUPeriod 511 512 // TODO research these 513 //s.ResourceLimits.CPU.Cpus = c.CPUS 514 //s.ResourceLimits.CPU.Cpus = c.CPUSetCPUs 515 516 //s.ResourceLimits.CPU. = c.CPUSetCPUs 517 s.ResourceLimits.CPU.Mems = c.CPUSetMems 518 s.ResourceLimits.CPU.Quota = &c.CPUQuota 519 s.ResourceLimits.CPU.RealtimePeriod = &c.CPURTPeriod 520 s.ResourceLimits.CPU.RealtimeRuntime = &c.CPURTRuntime 521 s.OOMScoreAdj = &c.OOMScoreAdj 522 s.RestartPolicy = c.Restart 523 s.Remove = c.Rm 524 s.StopTimeout = &c.StopTimeout 525 526 // TODO where should we do this? 527 //func verifyContainerResources(config *cc.CreateConfig, update bool) ([]string, error) { 528 return nil 529 } 530 531 func makeHealthCheckFromCli(inCmd, interval string, retries uint, timeout, startPeriod string) (*manifest.Schema2HealthConfig, error) { 532 // Every healthcheck requires a command 533 if len(inCmd) == 0 { 534 return nil, errors.New("Must define a healthcheck command for all healthchecks") 535 } 536 537 // first try to parse option value as JSON array of strings... 538 cmd := []string{} 539 err := json.Unmarshal([]byte(inCmd), &cmd) 540 if err != nil { 541 // ...otherwise pass it to "/bin/sh -c" inside the container 542 cmd = []string{"CMD-SHELL", inCmd} 543 } 544 hc := manifest.Schema2HealthConfig{ 545 Test: cmd, 546 } 547 548 if interval == "disable" { 549 interval = "0" 550 } 551 intervalDuration, err := time.ParseDuration(interval) 552 if err != nil { 553 return nil, errors.Wrapf(err, "invalid healthcheck-interval %s ", interval) 554 } 555 556 hc.Interval = intervalDuration 557 558 if retries < 1 { 559 return nil, errors.New("healthcheck-retries must be greater than 0.") 560 } 561 hc.Retries = int(retries) 562 timeoutDuration, err := time.ParseDuration(timeout) 563 if err != nil { 564 return nil, errors.Wrapf(err, "invalid healthcheck-timeout %s", timeout) 565 } 566 if timeoutDuration < time.Duration(1) { 567 return nil, errors.New("healthcheck-timeout must be at least 1 second") 568 } 569 hc.Timeout = timeoutDuration 570 571 startPeriodDuration, err := time.ParseDuration(startPeriod) 572 if err != nil { 573 return nil, errors.Wrapf(err, "invalid healthcheck-start-period %s", startPeriod) 574 } 575 if startPeriodDuration < time.Duration(0) { 576 return nil, errors.New("healthcheck-start-period must be 0 seconds or greater") 577 } 578 hc.StartPeriod = startPeriodDuration 579 580 return &hc, nil 581 } 582 583 func parseWeightDevices(weightDevs []string, s *specgen.SpecGenerator) error { 584 for _, val := range weightDevs { 585 split := strings.SplitN(val, ":", 2) 586 if len(split) != 2 { 587 return fmt.Errorf("bad format: %s", val) 588 } 589 if !strings.HasPrefix(split[0], "/dev/") { 590 return fmt.Errorf("bad format for device path: %s", val) 591 } 592 weight, err := strconv.ParseUint(split[1], 10, 0) 593 if err != nil { 594 return fmt.Errorf("invalid weight for device: %s", val) 595 } 596 if weight > 0 && (weight < 10 || weight > 1000) { 597 return fmt.Errorf("invalid weight for device: %s", val) 598 } 599 w := uint16(weight) 600 s.WeightDevice[split[0]] = specs.LinuxWeightDevice{ 601 Weight: &w, 602 LeafWeight: nil, 603 } 604 } 605 return nil 606 } 607 608 func parseThrottleBPSDevices(bpsDevices []string) (map[string]specs.LinuxThrottleDevice, error) { 609 td := make(map[string]specs.LinuxThrottleDevice) 610 for _, val := range bpsDevices { 611 split := strings.SplitN(val, ":", 2) 612 if len(split) != 2 { 613 return nil, fmt.Errorf("bad format: %s", val) 614 } 615 if !strings.HasPrefix(split[0], "/dev/") { 616 return nil, fmt.Errorf("bad format for device path: %s", val) 617 } 618 rate, err := units.RAMInBytes(split[1]) 619 if err != nil { 620 return 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", val) 621 } 622 if rate < 0 { 623 return 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", val) 624 } 625 td[split[0]] = specs.LinuxThrottleDevice{Rate: uint64(rate)} 626 } 627 return td, nil 628 } 629 630 func parseThrottleIOPsDevices(iopsDevices []string) (map[string]specs.LinuxThrottleDevice, error) { 631 td := make(map[string]specs.LinuxThrottleDevice) 632 for _, val := range iopsDevices { 633 split := strings.SplitN(val, ":", 2) 634 if len(split) != 2 { 635 return nil, fmt.Errorf("bad format: %s", val) 636 } 637 if !strings.HasPrefix(split[0], "/dev/") { 638 return nil, fmt.Errorf("bad format for device path: %s", val) 639 } 640 rate, err := strconv.ParseUint(split[1], 10, 64) 641 if err != nil { 642 return nil, fmt.Errorf("invalid rate for device: %s. The correct format is <device-path>:<number>. Number must be a positive integer", val) 643 } 644 td[split[0]] = specs.LinuxThrottleDevice{Rate: rate} 645 } 646 return td, nil 647 }