Files
Sascha Grunert a46f798831 pkg: switch to golang native error wrapping
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>
2022-07-08 08:54:47 +02:00

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
}