mirror of
https://github.com/containers/podman.git
synced 2025-05-21 00:56:36 +08:00

We now use the golang error wrapping format specifier `%w` instead of the deprecated github.com/pkg/errors package. [NO NEW TESTS NEEDED] Signed-off-by: Sascha Grunert <sgrunert@redhat.com>
317 lines
9.2 KiB
Go
317 lines
9.2 KiB
Go
package specgenutil
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"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 := ioutil.ReadFile(path)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error 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("error 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("error 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
|
|
}
|