mirror of
https://github.com/podman-container-tools/podman.git
synced 2026-09-09 17:17:53 +00:00
The rework of namespace handling for rootless CNI broke this, as CNI networks were being computed incorrectly. Fix handling of CNI networks for the Compat Create REST API for containers, and add a test so we don't regress again. Fixes #10569 Signed-off-by: Matthew Heon <mheon@redhat.com>
601 lines
17 KiB
Go
601 lines
17 KiB
Go
package common
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/containers/common/pkg/config"
|
|
"github.com/containers/podman/v3/cmd/podman/registry"
|
|
"github.com/containers/podman/v3/pkg/api/handlers"
|
|
"github.com/containers/podman/v3/pkg/cgroups"
|
|
"github.com/containers/podman/v3/pkg/domain/entities"
|
|
"github.com/containers/podman/v3/pkg/rootless"
|
|
"github.com/containers/podman/v3/pkg/specgen"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
type ContainerCLIOpts struct {
|
|
Annotation []string
|
|
Attach []string
|
|
Authfile string
|
|
BlkIOWeight string
|
|
BlkIOWeightDevice []string
|
|
CapAdd []string
|
|
CapDrop []string
|
|
CgroupNS string
|
|
CGroupsMode string
|
|
CGroupParent string
|
|
CIDFile string
|
|
ConmonPIDFile string
|
|
CPUPeriod uint64
|
|
CPUQuota int64
|
|
CPURTPeriod uint64
|
|
CPURTRuntime int64
|
|
CPUShares uint64
|
|
CPUS float64
|
|
CPUSetCPUs string
|
|
CPUSetMems string
|
|
Devices []string
|
|
DeviceCGroupRule []string
|
|
DeviceReadBPs []string
|
|
DeviceReadIOPs []string
|
|
DeviceWriteBPs []string
|
|
DeviceWriteIOPs []string
|
|
Entrypoint *string
|
|
Env []string
|
|
EnvHost bool
|
|
EnvFile []string
|
|
Expose []string
|
|
GIDMap []string
|
|
GroupAdd []string
|
|
HealthCmd string
|
|
HealthInterval string
|
|
HealthRetries uint
|
|
HealthStartPeriod string
|
|
HealthTimeout string
|
|
Hostname string
|
|
HTTPProxy bool
|
|
ImageVolume string
|
|
Init bool
|
|
InitPath string
|
|
Interactive bool
|
|
IPC string
|
|
KernelMemory string
|
|
Label []string
|
|
LabelFile []string
|
|
LogDriver string
|
|
LogOptions []string
|
|
Memory string
|
|
MemoryReservation string
|
|
MemorySwap string
|
|
MemorySwappiness int64
|
|
Name string
|
|
NoHealthCheck bool
|
|
OOMKillDisable bool
|
|
OOMScoreAdj int
|
|
Arch string
|
|
OS string
|
|
Variant string
|
|
PID string
|
|
PIDsLimit *int64
|
|
Platform string
|
|
Pod string
|
|
PodIDFile string
|
|
PreserveFDs uint
|
|
Privileged bool
|
|
PublishAll bool
|
|
Pull string
|
|
Quiet bool
|
|
ReadOnly bool
|
|
ReadOnlyTmpFS bool
|
|
Restart string
|
|
Replace bool
|
|
Requires []string
|
|
Rm bool
|
|
RootFS bool
|
|
Secrets []string
|
|
SecurityOpt []string
|
|
SdNotifyMode string
|
|
ShmSize string
|
|
SignaturePolicy string
|
|
StopSignal string
|
|
StopTimeout uint
|
|
StorageOpt []string
|
|
SubUIDName string
|
|
SubGIDName string
|
|
Sysctl []string
|
|
Systemd string
|
|
Timeout uint
|
|
TmpFS []string
|
|
TTY bool
|
|
Timezone string
|
|
Umask string
|
|
UIDMap []string
|
|
Ulimit []string
|
|
User string
|
|
UserNS string
|
|
UTS string
|
|
Mount []string
|
|
Volume []string
|
|
VolumesFrom []string
|
|
Workdir string
|
|
SeccompPolicy string
|
|
PidFile string
|
|
|
|
Net *entities.NetOptions
|
|
|
|
CgroupConf []string
|
|
}
|
|
|
|
func stringMaptoArray(m map[string]string) []string {
|
|
a := make([]string, 0, len(m))
|
|
for k, v := range m {
|
|
a = append(a, fmt.Sprintf("%s=%s", k, v))
|
|
}
|
|
return a
|
|
}
|
|
|
|
// ContainerCreateToContainerCLIOpts converts a compat input struct to cliopts so it can be converted to
|
|
// a specgen spec.
|
|
func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, rtc *config.Config) (*ContainerCLIOpts, []string, error) {
|
|
var (
|
|
capAdd []string
|
|
cappDrop []string
|
|
entrypoint *string
|
|
init bool
|
|
specPorts []specgen.PortMapping
|
|
)
|
|
|
|
if cc.HostConfig.Init != nil {
|
|
init = *cc.HostConfig.Init
|
|
}
|
|
|
|
// Iterate devices and convert back to string
|
|
devices := make([]string, 0, len(cc.HostConfig.Devices))
|
|
for _, dev := range cc.HostConfig.Devices {
|
|
devices = append(devices, fmt.Sprintf("%s:%s:%s", dev.PathOnHost, dev.PathInContainer, dev.CgroupPermissions))
|
|
}
|
|
|
|
// iterate blkreaddevicebps
|
|
readBps := make([]string, 0, len(cc.HostConfig.BlkioDeviceReadBps))
|
|
for _, dev := range cc.HostConfig.BlkioDeviceReadBps {
|
|
readBps = append(readBps, dev.String())
|
|
}
|
|
|
|
// iterate blkreaddeviceiops
|
|
readIops := make([]string, 0, len(cc.HostConfig.BlkioDeviceReadIOps))
|
|
for _, dev := range cc.HostConfig.BlkioDeviceReadIOps {
|
|
readIops = append(readIops, dev.String())
|
|
}
|
|
|
|
// iterate blkwritedevicebps
|
|
writeBps := make([]string, 0, len(cc.HostConfig.BlkioDeviceWriteBps))
|
|
for _, dev := range cc.HostConfig.BlkioDeviceWriteBps {
|
|
writeBps = append(writeBps, dev.String())
|
|
}
|
|
|
|
// iterate blkwritedeviceiops
|
|
writeIops := make([]string, 0, len(cc.HostConfig.BlkioDeviceWriteIOps))
|
|
for _, dev := range cc.HostConfig.BlkioDeviceWriteIOps {
|
|
writeIops = append(writeIops, dev.String())
|
|
}
|
|
|
|
// entrypoint
|
|
// can be a string or slice. if it is a slice, we need to
|
|
// marshall it to json; otherwise it should just be the string
|
|
// value
|
|
if len(cc.Config.Entrypoint) > 0 {
|
|
entrypoint = &cc.Config.Entrypoint[0]
|
|
if len(cc.Config.Entrypoint) > 1 {
|
|
b, err := json.Marshal(cc.Config.Entrypoint)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
var jsonString = string(b)
|
|
entrypoint = &jsonString
|
|
}
|
|
}
|
|
|
|
// expose ports
|
|
expose := make([]string, 0, len(cc.Config.ExposedPorts))
|
|
for p := range cc.Config.ExposedPorts {
|
|
expose = append(expose, fmt.Sprintf("%s/%s", p.Port(), p.Proto()))
|
|
}
|
|
|
|
// mounts type=tmpfs/bind,source=,dest=,opt=val
|
|
// TODO options
|
|
mounts := make([]string, 0, len(cc.HostConfig.Mounts))
|
|
for _, m := range cc.HostConfig.Mounts {
|
|
mount := fmt.Sprintf("type=%s", m.Type)
|
|
if len(m.Source) > 0 {
|
|
mount += fmt.Sprintf(",source=%s", m.Source)
|
|
}
|
|
if len(m.Target) > 0 {
|
|
mount += fmt.Sprintf(",dst=%s", m.Target)
|
|
}
|
|
mounts = append(mounts, mount)
|
|
}
|
|
|
|
// dns
|
|
dns := make([]net.IP, 0, len(cc.HostConfig.DNS))
|
|
for _, d := range cc.HostConfig.DNS {
|
|
dns = append(dns, net.ParseIP(d))
|
|
}
|
|
|
|
// publish
|
|
for port, pbs := range cc.HostConfig.PortBindings {
|
|
for _, pb := range pbs {
|
|
var hostport int
|
|
var err error
|
|
if pb.HostPort != "" {
|
|
hostport, err = strconv.Atoi(pb.HostPort)
|
|
}
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
tmpPort := specgen.PortMapping{
|
|
HostIP: pb.HostIP,
|
|
ContainerPort: uint16(port.Int()),
|
|
HostPort: uint16(hostport),
|
|
Range: 0,
|
|
Protocol: port.Proto(),
|
|
}
|
|
specPorts = append(specPorts, tmpPort)
|
|
}
|
|
}
|
|
|
|
// netMode
|
|
nsmode, networks, err := specgen.ParseNetworkNamespace(string(cc.HostConfig.NetworkMode), true)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
var netOpts map[string][]string
|
|
parts := strings.SplitN(string(cc.HostConfig.NetworkMode), ":", 2)
|
|
if len(parts) > 1 {
|
|
netOpts = make(map[string][]string)
|
|
netOpts[parts[0]] = strings.Split(parts[1], ",")
|
|
}
|
|
|
|
// network
|
|
// Note: we cannot emulate compat exactly here. we only allow specifics of networks to be
|
|
// defined when there is only one network.
|
|
netInfo := entities.NetOptions{
|
|
AddHosts: cc.HostConfig.ExtraHosts,
|
|
DNSOptions: cc.HostConfig.DNSOptions,
|
|
DNSSearch: cc.HostConfig.DNSSearch,
|
|
DNSServers: dns,
|
|
Network: nsmode,
|
|
PublishPorts: specPorts,
|
|
NetworkOptions: netOpts,
|
|
}
|
|
|
|
// network names
|
|
switch {
|
|
case len(cc.NetworkingConfig.EndpointsConfig) > 0:
|
|
var aliases []string
|
|
|
|
endpointsConfig := cc.NetworkingConfig.EndpointsConfig
|
|
cniNetworks := make([]string, 0, len(endpointsConfig))
|
|
for netName, endpoint := range endpointsConfig {
|
|
cniNetworks = append(cniNetworks, netName)
|
|
|
|
if endpoint == nil {
|
|
continue
|
|
}
|
|
if len(endpoint.Aliases) > 0 {
|
|
aliases = append(aliases, endpoint.Aliases...)
|
|
}
|
|
}
|
|
|
|
// static IP and MAC
|
|
if len(endpointsConfig) == 1 {
|
|
for _, ep := range endpointsConfig {
|
|
if ep == nil {
|
|
continue
|
|
}
|
|
// if IP address is provided
|
|
if len(ep.IPAddress) > 0 {
|
|
staticIP := net.ParseIP(ep.IPAddress)
|
|
netInfo.StaticIP = &staticIP
|
|
}
|
|
// if IPAMConfig.IPv4Address is provided
|
|
if ep.IPAMConfig != nil && ep.IPAMConfig.IPv4Address != "" {
|
|
staticIP := net.ParseIP(ep.IPAMConfig.IPv4Address)
|
|
netInfo.StaticIP = &staticIP
|
|
}
|
|
// If MAC address is provided
|
|
if len(ep.MacAddress) > 0 {
|
|
staticMac, err := net.ParseMAC(ep.MacAddress)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
netInfo.StaticMAC = &staticMac
|
|
}
|
|
break
|
|
}
|
|
}
|
|
netInfo.Aliases = aliases
|
|
netInfo.CNINetworks = cniNetworks
|
|
case len(cc.HostConfig.NetworkMode) > 0:
|
|
netInfo.CNINetworks = networks
|
|
}
|
|
|
|
parsedTmp := make([]string, 0, len(cc.HostConfig.Tmpfs))
|
|
for path, options := range cc.HostConfig.Tmpfs {
|
|
finalString := path
|
|
if options != "" {
|
|
finalString += ":" + options
|
|
}
|
|
parsedTmp = append(parsedTmp, finalString)
|
|
}
|
|
|
|
// Note: several options here are marked as "don't need". this is based
|
|
// on speculation by Matt and I. We think that these come into play later
|
|
// like with start. We believe this is just a difference in podman/compat
|
|
cliOpts := ContainerCLIOpts{
|
|
// Attach: nil, // don't need?
|
|
Authfile: "",
|
|
CapAdd: append(capAdd, cc.HostConfig.CapAdd...),
|
|
CapDrop: append(cappDrop, cc.HostConfig.CapDrop...),
|
|
CGroupParent: cc.HostConfig.CgroupParent,
|
|
CIDFile: cc.HostConfig.ContainerIDFile,
|
|
CPUPeriod: uint64(cc.HostConfig.CPUPeriod),
|
|
CPUQuota: cc.HostConfig.CPUQuota,
|
|
CPURTPeriod: uint64(cc.HostConfig.CPURealtimePeriod),
|
|
CPURTRuntime: cc.HostConfig.CPURealtimeRuntime,
|
|
CPUShares: uint64(cc.HostConfig.CPUShares),
|
|
// CPUS: 0, // don't need?
|
|
CPUSetCPUs: cc.HostConfig.CpusetCpus,
|
|
CPUSetMems: cc.HostConfig.CpusetMems,
|
|
// Detach: false, // don't need
|
|
// DetachKeys: "", // don't need
|
|
Devices: devices,
|
|
DeviceCGroupRule: nil,
|
|
DeviceReadBPs: readBps,
|
|
DeviceReadIOPs: readIops,
|
|
DeviceWriteBPs: writeBps,
|
|
DeviceWriteIOPs: writeIops,
|
|
Entrypoint: entrypoint,
|
|
Env: cc.Config.Env,
|
|
Expose: expose,
|
|
GroupAdd: cc.HostConfig.GroupAdd,
|
|
Hostname: cc.Config.Hostname,
|
|
ImageVolume: "bind",
|
|
Init: init,
|
|
Interactive: cc.Config.OpenStdin,
|
|
IPC: string(cc.HostConfig.IpcMode),
|
|
Label: stringMaptoArray(cc.Config.Labels),
|
|
LogDriver: cc.HostConfig.LogConfig.Type,
|
|
LogOptions: stringMaptoArray(cc.HostConfig.LogConfig.Config),
|
|
Name: cc.Name,
|
|
OOMScoreAdj: cc.HostConfig.OomScoreAdj,
|
|
Arch: "",
|
|
OS: "",
|
|
Variant: "",
|
|
PID: string(cc.HostConfig.PidMode),
|
|
PIDsLimit: cc.HostConfig.PidsLimit,
|
|
Privileged: cc.HostConfig.Privileged,
|
|
PublishAll: cc.HostConfig.PublishAllPorts,
|
|
Quiet: false,
|
|
ReadOnly: cc.HostConfig.ReadonlyRootfs,
|
|
ReadOnlyTmpFS: true, // podman default
|
|
Rm: cc.HostConfig.AutoRemove,
|
|
SecurityOpt: cc.HostConfig.SecurityOpt,
|
|
StopSignal: cc.Config.StopSignal,
|
|
StorageOpt: stringMaptoArray(cc.HostConfig.StorageOpt),
|
|
Sysctl: stringMaptoArray(cc.HostConfig.Sysctls),
|
|
Systemd: "true", // podman default
|
|
TmpFS: parsedTmp,
|
|
TTY: cc.Config.Tty,
|
|
User: cc.Config.User,
|
|
UserNS: string(cc.HostConfig.UsernsMode),
|
|
UTS: string(cc.HostConfig.UTSMode),
|
|
Mount: mounts,
|
|
VolumesFrom: cc.HostConfig.VolumesFrom,
|
|
Workdir: cc.Config.WorkingDir,
|
|
Net: &netInfo,
|
|
}
|
|
if !rootless.IsRootless() {
|
|
var ulimits []string
|
|
if len(cc.HostConfig.Ulimits) > 0 {
|
|
for _, ul := range cc.HostConfig.Ulimits {
|
|
ulimits = append(ulimits, ul.String())
|
|
}
|
|
cliOpts.Ulimit = ulimits
|
|
}
|
|
}
|
|
if cc.HostConfig.Resources.NanoCPUs > 0 {
|
|
if cliOpts.CPUPeriod != 0 || cliOpts.CPUQuota != 0 {
|
|
return nil, nil, errors.Errorf("NanoCpus conflicts with CpuPeriod and CpuQuota")
|
|
}
|
|
cliOpts.CPUPeriod = 100000
|
|
cliOpts.CPUQuota = cc.HostConfig.Resources.NanoCPUs / 10000
|
|
}
|
|
|
|
// volumes
|
|
volSources := make(map[string]bool)
|
|
volDestinations := make(map[string]bool)
|
|
for _, vol := range cc.HostConfig.Binds {
|
|
cliOpts.Volume = append(cliOpts.Volume, vol)
|
|
// Extract the destination so we don't add duplicate mounts in
|
|
// the volumes phase.
|
|
splitVol := strings.SplitN(vol, ":", 3)
|
|
switch len(splitVol) {
|
|
case 1:
|
|
volDestinations[vol] = true
|
|
default:
|
|
volSources[splitVol[0]] = true
|
|
volDestinations[splitVol[1]] = true
|
|
}
|
|
}
|
|
// Anonymous volumes are added differently from other volumes, in their
|
|
// own special field, for reasons known only to Docker. Still use the
|
|
// format of `-v` so we can just append them in there.
|
|
// Unfortunately, these may be duplicates of existing mounts in Binds.
|
|
// So... We need to catch that.
|
|
for vol := range cc.Volumes {
|
|
if _, ok := volDestinations[filepath.Clean(vol)]; ok {
|
|
continue
|
|
}
|
|
cliOpts.Volume = append(cliOpts.Volume, vol)
|
|
}
|
|
// Make mount points for compat volumes
|
|
for vol := range volSources {
|
|
// This might be a named volume.
|
|
// Assume it is if it's not an absolute path.
|
|
if !filepath.IsAbs(vol) {
|
|
continue
|
|
}
|
|
// If volume already exists, there is nothing to do
|
|
if _, err := os.Stat(vol); err == nil {
|
|
continue
|
|
}
|
|
if err := os.MkdirAll(vol, 0755); err != nil {
|
|
if !os.IsExist(err) {
|
|
return nil, nil, errors.Wrapf(err, "error making volume mountpoint for volume %s", vol)
|
|
}
|
|
}
|
|
}
|
|
if len(cc.HostConfig.BlkioWeightDevice) > 0 {
|
|
devices := make([]string, 0, len(cc.HostConfig.BlkioWeightDevice))
|
|
for _, d := range cc.HostConfig.BlkioWeightDevice {
|
|
devices = append(devices, d.String())
|
|
}
|
|
cliOpts.BlkIOWeightDevice = devices
|
|
}
|
|
if cc.HostConfig.BlkioWeight > 0 {
|
|
cliOpts.BlkIOWeight = strconv.Itoa(int(cc.HostConfig.BlkioWeight))
|
|
}
|
|
|
|
if cc.HostConfig.Memory > 0 {
|
|
cliOpts.Memory = strconv.Itoa(int(cc.HostConfig.Memory))
|
|
}
|
|
|
|
if cc.HostConfig.MemoryReservation > 0 {
|
|
cliOpts.MemoryReservation = strconv.Itoa(int(cc.HostConfig.MemoryReservation))
|
|
}
|
|
|
|
cgroupsv2, err := cgroups.IsCgroup2UnifiedMode()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if cc.HostConfig.MemorySwap > 0 && (!rootless.IsRootless() || (rootless.IsRootless() && cgroupsv2)) {
|
|
cliOpts.MemorySwap = strconv.Itoa(int(cc.HostConfig.MemorySwap))
|
|
}
|
|
|
|
if cc.Config.StopTimeout != nil {
|
|
cliOpts.StopTimeout = uint(*cc.Config.StopTimeout)
|
|
}
|
|
|
|
if cc.HostConfig.ShmSize > 0 {
|
|
cliOpts.ShmSize = strconv.Itoa(int(cc.HostConfig.ShmSize))
|
|
}
|
|
|
|
if cc.HostConfig.KernelMemory > 0 {
|
|
cliOpts.KernelMemory = strconv.Itoa(int(cc.HostConfig.KernelMemory))
|
|
}
|
|
if len(cc.HostConfig.RestartPolicy.Name) > 0 {
|
|
policy := cc.HostConfig.RestartPolicy.Name
|
|
// only add restart count on failure
|
|
if cc.HostConfig.RestartPolicy.IsOnFailure() {
|
|
policy += fmt.Sprintf(":%d", cc.HostConfig.RestartPolicy.MaximumRetryCount)
|
|
}
|
|
cliOpts.Restart = policy
|
|
}
|
|
|
|
if cc.HostConfig.MemorySwappiness != nil && (!rootless.IsRootless() || rootless.IsRootless() && cgroupsv2 && rtc.Engine.CgroupManager == "systemd") {
|
|
cliOpts.MemorySwappiness = *cc.HostConfig.MemorySwappiness
|
|
} else {
|
|
cliOpts.MemorySwappiness = -1
|
|
}
|
|
if cc.HostConfig.OomKillDisable != nil {
|
|
cliOpts.OOMKillDisable = *cc.HostConfig.OomKillDisable
|
|
}
|
|
if cc.Config.Healthcheck != nil {
|
|
cliOpts.HealthCmd = strings.Join(cc.Config.Healthcheck.Test, " ")
|
|
cliOpts.HealthInterval = cc.Config.Healthcheck.Interval.String()
|
|
cliOpts.HealthRetries = uint(cc.Config.Healthcheck.Retries)
|
|
cliOpts.HealthStartPeriod = cc.Config.Healthcheck.StartPeriod.String()
|
|
cliOpts.HealthTimeout = cc.Config.Healthcheck.Timeout.String()
|
|
}
|
|
|
|
// specgen assumes the image name is arg[0]
|
|
cmd := []string{cc.Config.Image}
|
|
cmd = append(cmd, cc.Config.Cmd...)
|
|
return &cliOpts, cmd, nil
|
|
}
|
|
|
|
func ulimits() []string {
|
|
if !registry.IsRemote() {
|
|
return containerConfig.Ulimits()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cgroupConfig() string {
|
|
if !registry.IsRemote() {
|
|
return containerConfig.Cgroups()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func devices() []string {
|
|
if !registry.IsRemote() {
|
|
return containerConfig.Devices()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func env() []string {
|
|
if !registry.IsRemote() {
|
|
return containerConfig.Env()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func initPath() string {
|
|
if !registry.IsRemote() {
|
|
return containerConfig.InitPath()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func pidsLimit() int64 {
|
|
if !registry.IsRemote() {
|
|
return containerConfig.PidsLimit()
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func policy() string {
|
|
if !registry.IsRemote() {
|
|
return containerConfig.Engine.PullPolicy
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func shmSize() string {
|
|
if !registry.IsRemote() {
|
|
return containerConfig.ShmSize()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func volumes() []string {
|
|
if !registry.IsRemote() {
|
|
return containerConfig.Volumes()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func logDriver() string {
|
|
if !registry.IsRemote() {
|
|
return containerConfig.Containers.LogDriver
|
|
}
|
|
return ""
|
|
}
|