package specgenutil import ( "errors" "fmt" "net" "os" "strconv" "strings" "github.com/containers/common/libnetwork/types" "github.com/containers/common/pkg/config" storageTypes "github.com/containers/storage/types" "github.com/sirupsen/logrus" ) // ReadPodIDFile reads the specified file and returns its content (i.e., first // line). func ReadPodIDFile(path string) (string, error) { content, err := os.ReadFile(path) if err != nil { return "", fmt.Errorf("reading pod ID file: %w", err) } return strings.Split(string(content), "\n")[0], nil } // ReadPodIDFiles reads the specified files and returns their content (i.e., // first line). func ReadPodIDFiles(files []string) ([]string, error) { ids := []string{} for _, file := range files { id, err := ReadPodIDFile(file) if err != nil { return nil, err } ids = append(ids, id) } return ids, nil } // CreateExpose parses user-provided exposed port definitions and converts them // into SpecGen format. // TODO: The SpecGen format should really handle ranges more sanely - we could // be massively inflating what is sent over the wire with a large range. func CreateExpose(expose []string) (map[uint16]string, error) { toReturn := make(map[uint16]string) for _, e := range expose { // Check for protocol proto := "tcp" splitProto := strings.Split(e, "/") if len(splitProto) > 2 { return nil, errors.New("invalid expose format - protocol can only be specified once") } else if len(splitProto) == 2 { proto = splitProto[1] } // Check for a range start, len, err := parseAndValidateRange(splitProto[0]) if err != nil { return nil, err } var index uint16 for index = 0; index < len; index++ { portNum := start + index protocols, ok := toReturn[portNum] if !ok { toReturn[portNum] = proto } else { newProto := strings.Join(append(strings.Split(protocols, ","), strings.Split(proto, ",")...), ",") toReturn[portNum] = newProto } } } return toReturn, nil } // CreatePortBindings iterates ports mappings into SpecGen format. func CreatePortBindings(ports []string) ([]types.PortMapping, error) { // --publish is formatted as follows: // [[hostip:]hostport[-endPort]:]containerport[-endPort][/protocol] toReturn := make([]types.PortMapping, 0, len(ports)) for _, p := range ports { var ( ctrPort string proto, hostIP, hostPort *string ) splitProto := strings.Split(p, "/") switch len(splitProto) { case 1: // No protocol was provided case 2: proto = &(splitProto[1]) default: return nil, errors.New("invalid port format - protocol can only be specified once") } remainder := splitProto[0] haveV6 := false // Check for an IPv6 address in brackets splitV6 := strings.Split(remainder, "]") switch len(splitV6) { case 1: // Do nothing, proceed as before case 2: // We potentially have an IPv6 address haveV6 = true if !strings.HasPrefix(splitV6[0], "[") { return nil, errors.New("invalid port format - IPv6 addresses must be enclosed by []") } if !strings.HasPrefix(splitV6[1], ":") { return nil, errors.New("invalid port format - IPv6 address must be followed by a colon (':')") } ipNoPrefix := strings.TrimPrefix(splitV6[0], "[") hostIP = &ipNoPrefix remainder = strings.TrimPrefix(splitV6[1], ":") default: return nil, errors.New("invalid port format - at most one IPv6 address can be specified in a --publish") } splitPort := strings.Split(remainder, ":") switch len(splitPort) { case 1: if haveV6 { return nil, errors.New("invalid port format - must provide host and destination port if specifying an IP") } ctrPort = splitPort[0] case 2: hostPort = &(splitPort[0]) ctrPort = splitPort[1] case 3: if haveV6 { return nil, errors.New("invalid port format - when v6 address specified, must be [ipv6]:hostPort:ctrPort") } hostIP = &(splitPort[0]) hostPort = &(splitPort[1]) ctrPort = splitPort[2] default: return nil, errors.New("invalid port format - format is [[hostIP:]hostPort:]containerPort") } newPort, err := parseSplitPort(hostIP, hostPort, ctrPort, proto) if err != nil { return nil, err } toReturn = append(toReturn, newPort) } return toReturn, nil } // parseSplitPort parses individual components of the --publish flag to produce // a single port mapping in SpecGen format. func parseSplitPort(hostIP, hostPort *string, ctrPort string, protocol *string) (types.PortMapping, error) { newPort := types.PortMapping{} if ctrPort == "" { return newPort, errors.New("must provide a non-empty container port to publish") } ctrStart, ctrLen, err := parseAndValidateRange(ctrPort) if err != nil { return newPort, fmt.Errorf("parsing container port: %w", err) } newPort.ContainerPort = ctrStart newPort.Range = ctrLen if protocol != nil { if *protocol == "" { return newPort, errors.New("must provide a non-empty protocol to publish") } newPort.Protocol = *protocol } if hostIP != nil { if *hostIP == "" { return newPort, errors.New("must provide a non-empty container host IP to publish") } else if *hostIP != "0.0.0.0" { // If hostIP is 0.0.0.0, leave it unset - CNI treats // 0.0.0.0 and empty differently, Docker does not. testIP := net.ParseIP(*hostIP) if testIP == nil { return newPort, fmt.Errorf("cannot parse %q as an IP address", *hostIP) } newPort.HostIP = testIP.String() } } if hostPort != nil { if *hostPort == "" { // Set 0 as a placeholder. The server side of Specgen // will find a random, open, unused port to use. newPort.HostPort = 0 } else { hostStart, hostLen, err := parseAndValidateRange(*hostPort) if err != nil { return newPort, fmt.Errorf("parsing host port: %w", err) } if hostLen != ctrLen { return newPort, fmt.Errorf("host and container port ranges have different lengths: %d vs %d", hostLen, ctrLen) } newPort.HostPort = hostStart } } hport := newPort.HostPort logrus.Debugf("Adding port mapping from %d to %d length %d protocol %q", hport, newPort.ContainerPort, newPort.Range, newPort.Protocol) return newPort, nil } // Parse and validate a port range. // Returns start port, length of range, error. func parseAndValidateRange(portRange string) (uint16, uint16, error) { splitRange := strings.Split(portRange, "-") if len(splitRange) > 2 { return 0, 0, errors.New("invalid port format - port ranges are formatted as startPort-stopPort") } if splitRange[0] == "" { return 0, 0, errors.New("port numbers cannot be negative") } startPort, err := parseAndValidatePort(splitRange[0]) if err != nil { return 0, 0, err } var rangeLen uint16 = 1 if len(splitRange) == 2 { if splitRange[1] == "" { return 0, 0, errors.New("must provide ending number for port range") } endPort, err := parseAndValidatePort(splitRange[1]) if err != nil { return 0, 0, err } if endPort <= startPort { return 0, 0, fmt.Errorf("the end port of a range must be higher than the start port - %d is not higher than %d", endPort, startPort) } // Our range is the total number of ports // involved, so we need to add 1 (8080:8081 is // 2 ports, for example, not 1) rangeLen = endPort - startPort + 1 } return startPort, rangeLen, nil } // Turn a single string into a valid U16 port. func parseAndValidatePort(port string) (uint16, error) { num, err := strconv.Atoi(port) if err != nil { return 0, fmt.Errorf("invalid port number: %w", err) } if num < 1 || num > 65535 { return 0, fmt.Errorf("port numbers must be between 1 and 65535 (inclusive), got %d", num) } return uint16(num), nil } func CreateExitCommandArgs(storageConfig storageTypes.StoreOptions, config *config.Config, syslog, rm, exec bool) ([]string, error) { // We need a cleanup process for containers in the current model. // But we can't assume that the caller is Podman - it could be another // user of the API. // As such, provide a way to specify a path to Podman, so we can // still invoke a cleanup process. podmanPath, err := os.Executable() if err != nil { return nil, err } command := []string{podmanPath, "--root", storageConfig.GraphRoot, "--runroot", storageConfig.RunRoot, "--log-level", logrus.GetLevel().String(), "--cgroup-manager", config.Engine.CgroupManager, "--tmpdir", config.Engine.TmpDir, "--network-config-dir", config.Network.NetworkConfigDir, "--network-backend", config.Network.NetworkBackend, "--volumepath", config.Engine.VolumePath, } if config.Engine.OCIRuntime != "" { command = append(command, []string{"--runtime", config.Engine.OCIRuntime}...) } if storageConfig.GraphDriverName != "" { command = append(command, []string{"--storage-driver", storageConfig.GraphDriverName}...) } for _, opt := range storageConfig.GraphDriverOptions { command = append(command, []string{"--storage-opt", opt}...) } if config.Engine.EventsLogger != "" { command = append(command, []string{"--events-backend", config.Engine.EventsLogger}...) } if syslog { command = append(command, "--syslog") } command = append(command, []string{"container", "cleanup"}...) if rm { command = append(command, "--rm") } // This has to be absolutely last, to ensure that the exec session ID // will be added after it by Libpod. if exec { command = append(command, "--exec") } return command, nil }