podman/pkg/util/utils.go
Daniel J Walsh 429b1f7685
Fix codespell errors
[NO NEW TESTS NEEDED]

Signed-off-by: Daniel J Walsh <dwalsh@redhat.com>
2022-05-25 05:56:29 -04:00

754 lines
22 KiB
Go

package util
import (
"encoding/json"
"fmt"
"io/fs"
"math"
"os"
"os/user"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/BurntSushi/toml"
"github.com/containers/common/pkg/config"
"github.com/containers/common/pkg/util"
"github.com/containers/image/v5/types"
"github.com/containers/podman/v4/pkg/errorhandling"
"github.com/containers/podman/v4/pkg/namespaces"
"github.com/containers/podman/v4/pkg/rootless"
"github.com/containers/podman/v4/pkg/signal"
"github.com/containers/storage/pkg/idtools"
stypes "github.com/containers/storage/types"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/term"
)
var containerConfig *config.Config
func init() {
var err error
containerConfig, err = config.Default()
if err != nil {
logrus.Error(err)
os.Exit(1)
}
}
// Helper function to determine the username/password passed
// in the creds string. It could be either or both.
func parseCreds(creds string) (string, string) {
if creds == "" {
return "", ""
}
up := strings.SplitN(creds, ":", 2)
if len(up) == 1 {
return up[0], ""
}
return up[0], up[1]
}
// ParseRegistryCreds takes a credentials string in the form USERNAME:PASSWORD
// and returns a DockerAuthConfig
func ParseRegistryCreds(creds string) (*types.DockerAuthConfig, error) {
username, password := parseCreds(creds)
if username == "" {
fmt.Print("Username: ")
fmt.Scanln(&username)
}
if password == "" {
fmt.Print("Password: ")
termPassword, err := term.ReadPassword(0)
if err != nil {
return nil, errors.Wrapf(err, "could not read password from terminal")
}
password = string(termPassword)
}
return &types.DockerAuthConfig{
Username: username,
Password: password,
}, nil
}
// StringInSlice is deprecated, use containers/common/pkg/util/StringInSlice
func StringInSlice(s string, sl []string) bool {
return util.StringInSlice(s, sl)
}
// StringMatchRegexSlice determines if a given string matches one of the given regexes, returns bool
func StringMatchRegexSlice(s string, re []string) bool {
for _, r := range re {
m, err := regexp.MatchString(r, s)
if err == nil && m {
return true
}
}
return false
}
// ImageConfig is a wrapper around the OCIv1 Image Configuration struct exported
// by containers/image, but containing additional fields that are not supported
// by OCIv1 (but are by Docker v2) - notably OnBuild.
type ImageConfig struct {
v1.ImageConfig
OnBuild []string
}
// GetImageConfig produces a v1.ImageConfig from the --change flag that is
// accepted by several Podman commands. It accepts a (limited subset) of
// Dockerfile instructions.
func GetImageConfig(changes []string) (ImageConfig, error) {
// Valid changes:
// USER
// EXPOSE
// ENV
// ENTRYPOINT
// CMD
// VOLUME
// WORKDIR
// LABEL
// STOPSIGNAL
// ONBUILD
config := ImageConfig{}
for _, change := range changes {
// First, let's assume proper Dockerfile format - space
// separator between instruction and value
split := strings.SplitN(change, " ", 2)
if len(split) != 2 {
split = strings.SplitN(change, "=", 2)
if len(split) != 2 {
return ImageConfig{}, errors.Errorf("invalid change %q - must be formatted as KEY VALUE", change)
}
}
outerKey := strings.ToUpper(strings.TrimSpace(split[0]))
value := strings.TrimSpace(split[1])
switch outerKey {
case "USER":
// Assume literal contents are the user.
if value == "" {
return ImageConfig{}, errors.Errorf("invalid change %q - must provide a value to USER", change)
}
config.User = value
case "EXPOSE":
// EXPOSE is either [portnum] or
// [portnum]/[proto]
// Protocol must be "tcp" or "udp"
splitPort := strings.Split(value, "/")
if len(splitPort) > 2 {
return ImageConfig{}, errors.Errorf("invalid change %q - EXPOSE port must be formatted as PORT[/PROTO]", change)
}
portNum, err := strconv.Atoi(splitPort[0])
if err != nil {
return ImageConfig{}, errors.Wrapf(err, "invalid change %q - EXPOSE port must be an integer", change)
}
if portNum > 65535 || portNum <= 0 {
return ImageConfig{}, errors.Errorf("invalid change %q - EXPOSE port must be a valid port number", change)
}
proto := "tcp"
if len(splitPort) > 1 {
testProto := strings.ToLower(splitPort[1])
switch testProto {
case "tcp", "udp":
proto = testProto
default:
return ImageConfig{}, errors.Errorf("invalid change %q - EXPOSE protocol must be TCP or UDP", change)
}
}
if config.ExposedPorts == nil {
config.ExposedPorts = make(map[string]struct{})
}
config.ExposedPorts[fmt.Sprintf("%d/%s", portNum, proto)] = struct{}{}
case "ENV":
// Format is either:
// ENV key=value
// ENV key=value key=value ...
// ENV key value
// Both keys and values can be surrounded by quotes to group them.
// For now: we only support key=value
// We will attempt to strip quotation marks if present.
var (
key, val string
)
splitEnv := strings.SplitN(value, "=", 2)
key = splitEnv[0]
// We do need a key
if key == "" {
return ImageConfig{}, errors.Errorf("invalid change %q - ENV must have at least one argument", change)
}
// Perfectly valid to not have a value
if len(splitEnv) == 2 {
val = splitEnv[1]
}
if strings.HasPrefix(key, `"`) && strings.HasSuffix(key, `"`) {
key = strings.TrimPrefix(strings.TrimSuffix(key, `"`), `"`)
}
if strings.HasPrefix(val, `"`) && strings.HasSuffix(val, `"`) {
val = strings.TrimPrefix(strings.TrimSuffix(val, `"`), `"`)
}
config.Env = append(config.Env, fmt.Sprintf("%s=%s", key, val))
case "ENTRYPOINT":
// Two valid forms.
// First, JSON array.
// Second, not a JSON array - we interpret this as an
// argument to `sh -c`, unless empty, in which case we
// just use a blank entrypoint.
testUnmarshal := []string{}
if err := json.Unmarshal([]byte(value), &testUnmarshal); err != nil {
// It ain't valid JSON, so assume it's an
// argument to sh -c if not empty.
if value != "" {
config.Entrypoint = []string{"/bin/sh", "-c", value}
} else {
config.Entrypoint = []string{}
}
} else {
// Valid JSON
config.Entrypoint = testUnmarshal
}
case "CMD":
// Same valid forms as entrypoint.
// However, where ENTRYPOINT assumes that 'ENTRYPOINT '
// means no entrypoint, CMD assumes it is 'sh -c' with
// no third argument.
testUnmarshal := []string{}
if err := json.Unmarshal([]byte(value), &testUnmarshal); err != nil {
// It ain't valid JSON, so assume it's an
// argument to sh -c.
// Only include volume if it's not ""
config.Cmd = []string{"/bin/sh", "-c"}
if value != "" {
config.Cmd = append(config.Cmd, value)
}
} else {
// Valid JSON
config.Cmd = testUnmarshal
}
case "VOLUME":
// Either a JSON array or a set of space-separated
// paths.
// Acts rather similar to ENTRYPOINT and CMD, but always
// appends rather than replacing, and no sh -c prepend.
testUnmarshal := []string{}
if err := json.Unmarshal([]byte(value), &testUnmarshal); err != nil {
// Not valid JSON, so split on spaces
testUnmarshal = strings.Split(value, " ")
}
if len(testUnmarshal) == 0 {
return ImageConfig{}, errors.Errorf("invalid change %q - must provide at least one argument to VOLUME", change)
}
for _, vol := range testUnmarshal {
if vol == "" {
return ImageConfig{}, errors.Errorf("invalid change %q - VOLUME paths must not be empty", change)
}
if config.Volumes == nil {
config.Volumes = make(map[string]struct{})
}
config.Volumes[vol] = struct{}{}
}
case "WORKDIR":
// This can be passed multiple times.
// Each successive invocation is treated as relative to
// the previous one - so WORKDIR /A, WORKDIR b,
// WORKDIR c results in /A/b/c
// Just need to check it's not empty...
if value == "" {
return ImageConfig{}, errors.Errorf("invalid change %q - must provide a non-empty WORKDIR", change)
}
config.WorkingDir = filepath.Join(config.WorkingDir, value)
case "LABEL":
// Same general idea as ENV, but we no longer allow " "
// as a separator.
// We didn't do that for ENV either, so nice and easy.
// Potentially problematic: LABEL might theoretically
// allow an = in the key? If people really do this, we
// may need to investigate more advanced parsing.
var (
key, val string
)
splitLabel := strings.SplitN(value, "=", 2)
// Unlike ENV, LABEL must have a value
if len(splitLabel) != 2 {
return ImageConfig{}, errors.Errorf("invalid change %q - LABEL must be formatted key=value", change)
}
key = splitLabel[0]
val = splitLabel[1]
if strings.HasPrefix(key, `"`) && strings.HasSuffix(key, `"`) {
key = strings.TrimPrefix(strings.TrimSuffix(key, `"`), `"`)
}
if strings.HasPrefix(val, `"`) && strings.HasSuffix(val, `"`) {
val = strings.TrimPrefix(strings.TrimSuffix(val, `"`), `"`)
}
// Check key after we strip quotations
if key == "" {
return ImageConfig{}, errors.Errorf("invalid change %q - LABEL must have a non-empty key", change)
}
if config.Labels == nil {
config.Labels = make(map[string]string)
}
config.Labels[key] = val
case "STOPSIGNAL":
// Check the provided signal for validity.
killSignal, err := ParseSignal(value)
if err != nil {
return ImageConfig{}, errors.Wrapf(err, "invalid change %q - KILLSIGNAL must be given a valid signal", change)
}
config.StopSignal = fmt.Sprintf("%d", killSignal)
case "ONBUILD":
// Onbuild always appends.
if value == "" {
return ImageConfig{}, errors.Errorf("invalid change %q - ONBUILD must be given an argument", change)
}
config.OnBuild = append(config.OnBuild, value)
default:
return ImageConfig{}, errors.Errorf("invalid change %q - invalid instruction %s", change, outerKey)
}
}
return config, nil
}
// ParseSignal parses and validates a signal name or number.
func ParseSignal(rawSignal string) (syscall.Signal, error) {
// Strip off leading dash, to allow -1 or -HUP
basename := strings.TrimPrefix(rawSignal, "-")
sig, err := signal.ParseSignal(basename)
if err != nil {
return -1, err
}
// 64 is SIGRTMAX; wish we could get this from a standard Go library
if sig < 1 || sig > 64 {
return -1, errors.Errorf("valid signals are 1 through 64")
}
return sig, nil
}
// GetKeepIDMapping returns the mappings and the user to use when keep-id is used
func GetKeepIDMapping() (*stypes.IDMappingOptions, int, int, error) {
if !rootless.IsRootless() {
return nil, -1, -1, errors.New("keep-id is only supported in rootless mode")
}
options := stypes.IDMappingOptions{
HostUIDMapping: false,
HostGIDMapping: false,
}
min := func(a, b int) int {
if a < b {
return a
}
return b
}
uid := rootless.GetRootlessUID()
gid := rootless.GetRootlessGID()
uids, gids, err := rootless.GetConfiguredMappings()
if err != nil {
return nil, -1, -1, errors.Wrapf(err, "cannot read mappings")
}
if len(uids) == 0 || len(gids) == 0 {
return nil, -1, -1, errors.Wrapf(err, "keep-id requires additional UIDs or GIDs defined in /etc/subuid and /etc/subgid to function correctly")
}
maxUID, maxGID := 0, 0
for _, u := range uids {
maxUID += u.Size
}
for _, g := range gids {
maxGID += g.Size
}
options.UIDMap, options.GIDMap = nil, nil
options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: 0, HostID: 1, Size: min(uid, maxUID)})
options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: uid, HostID: 0, Size: 1})
if maxUID > uid {
options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: uid + 1, HostID: uid + 1, Size: maxUID - uid})
}
options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: 0, HostID: 1, Size: min(gid, maxGID)})
options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: gid, HostID: 0, Size: 1})
if maxGID > gid {
options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: gid + 1, HostID: gid + 1, Size: maxGID - gid})
}
return &options, uid, gid, nil
}
// GetNoMapMapping returns the mappings and the user to use when nomap is used
func GetNoMapMapping() (*stypes.IDMappingOptions, int, int, error) {
if !rootless.IsRootless() {
return nil, -1, -1, errors.New("nomap is only supported in rootless mode")
}
options := stypes.IDMappingOptions{
HostUIDMapping: false,
HostGIDMapping: false,
}
uids, gids, err := rootless.GetConfiguredMappings()
if err != nil {
return nil, -1, -1, errors.Wrapf(err, "cannot read mappings")
}
if len(uids) == 0 || len(gids) == 0 {
return nil, -1, -1, errors.Wrapf(err, "nomap requires additional UIDs or GIDs defined in /etc/subuid and /etc/subgid to function correctly")
}
options.UIDMap, options.GIDMap = nil, nil
uid, gid := 0, 0
for _, u := range uids {
options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: uid, HostID: uid + 1, Size: u.Size})
uid += u.Size
}
for _, g := range gids {
options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: gid, HostID: gid + 1, Size: g.Size})
gid += g.Size
}
return &options, 0, 0, nil
}
// ParseIDMapping takes idmappings and subuid and subgid maps and returns a storage mapping
func ParseIDMapping(mode namespaces.UsernsMode, uidMapSlice, gidMapSlice []string, subUIDMap, subGIDMap string) (*stypes.IDMappingOptions, error) {
options := stypes.IDMappingOptions{
HostUIDMapping: true,
HostGIDMapping: true,
}
if mode.IsAuto() {
var err error
options.HostUIDMapping = false
options.HostGIDMapping = false
options.AutoUserNs = true
opts, err := mode.GetAutoOptions()
if err != nil {
return nil, err
}
options.AutoUserNsOpts = *opts
return &options, nil
}
if mode.IsKeepID() || mode.IsNoMap() {
options.HostUIDMapping = false
options.HostGIDMapping = false
return &options, nil
}
if subGIDMap == "" && subUIDMap != "" {
subGIDMap = subUIDMap
}
if subUIDMap == "" && subGIDMap != "" {
subUIDMap = subGIDMap
}
if len(gidMapSlice) == 0 && len(uidMapSlice) != 0 {
gidMapSlice = uidMapSlice
}
if len(uidMapSlice) == 0 && len(gidMapSlice) != 0 {
uidMapSlice = gidMapSlice
}
if subUIDMap != "" && subGIDMap != "" {
mappings, err := idtools.NewIDMappings(subUIDMap, subGIDMap)
if err != nil {
return nil, err
}
options.UIDMap = mappings.UIDs()
options.GIDMap = mappings.GIDs()
}
parsedUIDMap, err := idtools.ParseIDMap(uidMapSlice, "UID")
if err != nil {
return nil, err
}
parsedGIDMap, err := idtools.ParseIDMap(gidMapSlice, "GID")
if err != nil {
return nil, err
}
options.UIDMap = append(options.UIDMap, parsedUIDMap...)
options.GIDMap = append(options.GIDMap, parsedGIDMap...)
if len(options.UIDMap) > 0 {
options.HostUIDMapping = false
}
if len(options.GIDMap) > 0 {
options.HostGIDMapping = false
}
return &options, nil
}
var (
rootlessConfigHomeDirOnce sync.Once
rootlessConfigHomeDir string
rootlessRuntimeDirOnce sync.Once
rootlessRuntimeDir string
)
type tomlOptionsConfig struct {
MountProgram string `toml:"mount_program"`
}
type tomlConfig struct {
Storage struct {
Driver string `toml:"driver"`
RunRoot string `toml:"runroot"`
GraphRoot string `toml:"graphroot"`
Options struct{ tomlOptionsConfig } `toml:"options"`
} `toml:"storage"`
}
func getTomlStorage(storeOptions *stypes.StoreOptions) *tomlConfig {
config := new(tomlConfig)
config.Storage.Driver = storeOptions.GraphDriverName
config.Storage.RunRoot = storeOptions.RunRoot
config.Storage.GraphRoot = storeOptions.GraphRoot
for _, i := range storeOptions.GraphDriverOptions {
s := strings.SplitN(i, "=", 2)
if s[0] == "overlay.mount_program" && len(s) == 2 {
config.Storage.Options.MountProgram = s[1]
}
}
return config
}
// WriteStorageConfigFile writes the configuration to a file
func WriteStorageConfigFile(storageOpts *stypes.StoreOptions, storageConf string) error {
if err := os.MkdirAll(filepath.Dir(storageConf), 0755); err != nil {
return err
}
storageFile, err := os.OpenFile(storageConf, os.O_RDWR|os.O_TRUNC, 0600)
if err != nil {
return err
}
tomlConfiguration := getTomlStorage(storageOpts)
defer errorhandling.CloseQuiet(storageFile)
enc := toml.NewEncoder(storageFile)
if err := enc.Encode(tomlConfiguration); err != nil {
if err := os.Remove(storageConf); err != nil {
logrus.Error(err)
}
return err
}
return nil
}
// ParseInputTime takes the users input and to determine if it is valid and
// returns a time format and error. The input is compared to known time formats
// or a duration which implies no-duration
func ParseInputTime(inputTime string, since bool) (time.Time, error) {
timeFormats := []string{time.RFC3339Nano, time.RFC3339, "2006-01-02T15:04:05", "2006-01-02T15:04:05.999999999",
"2006-01-02Z07:00", "2006-01-02"}
// iterate the supported time formats
for _, tf := range timeFormats {
t, err := time.Parse(tf, inputTime)
if err == nil {
return t, nil
}
}
unixTimestamp, err := strconv.ParseFloat(inputTime, 64)
if err == nil {
iPart, fPart := math.Modf(unixTimestamp)
return time.Unix(int64(iPart), int64(fPart*1_000_000_000)).UTC(), nil
}
// input might be a duration
duration, err := time.ParseDuration(inputTime)
if err != nil {
return time.Time{}, errors.Errorf("unable to interpret time value")
}
if since {
return time.Now().Add(-duration), nil
}
return time.Now().Add(duration), nil
}
// OpenExclusiveFile opens a file for writing and ensure it doesn't already exist
func OpenExclusiveFile(path string) (*os.File, error) {
baseDir := filepath.Dir(path)
if baseDir != "" {
if _, err := os.Stat(baseDir); err != nil {
return nil, err
}
}
return os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
}
// ExitCode reads the error message when failing to executing container process
// and then returns 0 if no error, 126 if command does not exist, or 127 for
// all other errors
func ExitCode(err error) int {
if err == nil {
return 0
}
e := strings.ToLower(err.Error())
if strings.Contains(e, "file not found") ||
strings.Contains(e, "no such file or directory") {
return 127
}
return 126
}
// HomeDir returns the home directory for the current user.
func HomeDir() (string, error) {
home := os.Getenv("HOME")
if home == "" {
usr, err := user.LookupId(fmt.Sprintf("%d", rootless.GetRootlessUID()))
if err != nil {
return "", errors.Wrapf(err, "unable to resolve HOME directory")
}
home = usr.HomeDir
}
return home, nil
}
func Tmpdir() string {
tmpdir := os.Getenv("TMPDIR")
if tmpdir == "" {
tmpdir = "/var/tmp"
}
return tmpdir
}
// ValidateSysctls validates a list of sysctl and returns it.
func ValidateSysctls(strSlice []string) (map[string]string, error) {
sysctl := make(map[string]string)
validSysctlMap := map[string]bool{
"kernel.msgmax": true,
"kernel.msgmnb": true,
"kernel.msgmni": true,
"kernel.sem": true,
"kernel.shmall": true,
"kernel.shmmax": true,
"kernel.shmmni": true,
"kernel.shm_rmid_forced": true,
}
validSysctlPrefixes := []string{
"net.",
"fs.mqueue.",
}
for _, val := range strSlice {
foundMatch := false
arr := strings.Split(val, "=")
if len(arr) < 2 {
return nil, errors.Errorf("%s is invalid, sysctl values must be in the form of KEY=VALUE", val)
}
trimmed := fmt.Sprintf("%s=%s", strings.TrimSpace(arr[0]), strings.TrimSpace(arr[1]))
if trimmed != val {
return nil, errors.Errorf("'%s' is invalid, extra spaces found", val)
}
if validSysctlMap[arr[0]] {
sysctl[arr[0]] = arr[1]
continue
}
for _, prefix := range validSysctlPrefixes {
if strings.HasPrefix(arr[0], prefix) {
sysctl[arr[0]] = arr[1]
foundMatch = true
break
}
}
if !foundMatch {
return nil, errors.Errorf("sysctl '%s' is not allowed", arr[0])
}
}
return sysctl, nil
}
func DefaultContainerConfig() *config.Config {
return containerConfig
}
func CreateCidFile(cidfile string, id string) error {
cidFile, err := OpenExclusiveFile(cidfile)
if err != nil {
if os.IsExist(err) {
return errors.Errorf("container id file exists. Ensure another container is not using it or delete %s", cidfile)
}
return errors.Errorf("opening cidfile %s", cidfile)
}
if _, err = cidFile.WriteString(id); err != nil {
logrus.Error(err)
}
cidFile.Close()
return nil
}
// DefaultCPUPeriod is the default CPU period (100ms) in microseconds, which is
// the same default as Kubernetes.
const DefaultCPUPeriod uint64 = 100000
// CoresToPeriodAndQuota converts a fraction of cores to the equivalent
// Completely Fair Scheduler (CFS) parameters period and quota.
//
// Cores is a fraction of the CFS period that a container may use. Period and
// Quota are in microseconds.
func CoresToPeriodAndQuota(cores float64) (uint64, int64) {
return DefaultCPUPeriod, int64(cores * float64(DefaultCPUPeriod))
}
// PeriodAndQuotaToCores takes the CFS parameters period and quota and returns
// a fraction that represents the limit to the number of cores that can be
// utilized over the scheduling period.
//
// Cores is a fraction of the CFS period that a container may use. Period and
// Quota are in microseconds.
func PeriodAndQuotaToCores(period uint64, quota int64) float64 {
return float64(quota) / float64(period)
}
// IDtoolsToRuntimeSpec converts idtools ID mapping to the one of the runtime spec.
func IDtoolsToRuntimeSpec(idMaps []idtools.IDMap) (convertedIDMap []specs.LinuxIDMapping) {
for _, idmap := range idMaps {
tempIDMap := specs.LinuxIDMapping{
ContainerID: uint32(idmap.ContainerID),
HostID: uint32(idmap.HostID),
Size: uint32(idmap.Size),
}
convertedIDMap = append(convertedIDMap, tempIDMap)
}
return convertedIDMap
}
func LookupUser(name string) (*user.User, error) {
// Assume UID look up first, if it fails lookup by username
if u, err := user.LookupId(name); err == nil {
return u, nil
}
return user.Lookup(name)
}
// SizeOfPath determines the file usage of a given path. it was called volumeSize in v1
// and now is made to be generic and take a path instead of a libpod volume
func SizeOfPath(path string) (uint64, error) {
var size uint64
err := filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error {
if err == nil && !d.IsDir() {
info, err := d.Info()
if err != nil {
return err
}
size += uint64(info.Size())
}
return err
})
return size, err
}