mirror of
https://github.com/containers/podman.git
synced 2025-05-23 01:57:56 +08:00

Truncate the container and pod ID files instead of throwing an error. The main motivation is to prevent redundant work when starting systemd units. Throwing an error when the file already exists is not preventing races or file corruptions, so let's leave that to the user which in almost all cases are generated (and tested) systemd units. Signed-off-by: Valentin Rothberg <vrothberg@redhat.com>
757 lines
22 KiB
Go
757 lines
22 KiB
Go
package util
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"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/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, fmt.Errorf("could not read password from terminal: %w", err)
|
|
}
|
|
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{}, fmt.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{}, fmt.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{}, fmt.Errorf("invalid change %q - EXPOSE port must be formatted as PORT[/PROTO]", change)
|
|
}
|
|
portNum, err := strconv.Atoi(splitPort[0])
|
|
if err != nil {
|
|
return ImageConfig{}, fmt.Errorf("invalid change %q - EXPOSE port must be an integer: %w", change, err)
|
|
}
|
|
if portNum > 65535 || portNum <= 0 {
|
|
return ImageConfig{}, fmt.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{}, fmt.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{}, fmt.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{}, fmt.Errorf("invalid change %q - must provide at least one argument to VOLUME", change)
|
|
}
|
|
for _, vol := range testUnmarshal {
|
|
if vol == "" {
|
|
return ImageConfig{}, fmt.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{}, fmt.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{}, fmt.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{}, fmt.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{}, fmt.Errorf("invalid change %q - KILLSIGNAL must be given a valid signal: %w", change, err)
|
|
}
|
|
config.StopSignal = fmt.Sprintf("%d", killSignal)
|
|
case "ONBUILD":
|
|
// Onbuild always appends.
|
|
if value == "" {
|
|
return ImageConfig{}, fmt.Errorf("invalid change %q - ONBUILD must be given an argument", change)
|
|
}
|
|
config.OnBuild = append(config.OnBuild, value)
|
|
default:
|
|
return ImageConfig{}, fmt.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.New("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(opts *namespaces.KeepIDUserNsOptions) (*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()
|
|
if opts.UID != nil {
|
|
uid = int(*opts.UID)
|
|
}
|
|
if opts.GID != nil {
|
|
gid = int(*opts.GID)
|
|
}
|
|
|
|
uids, gids, err := rootless.GetConfiguredMappings()
|
|
if err != nil {
|
|
return nil, -1, -1, fmt.Errorf("cannot read mappings: %w", err)
|
|
}
|
|
if len(uids) == 0 || len(gids) == 0 {
|
|
return nil, -1, -1, fmt.Errorf("keep-id requires additional UIDs or GIDs defined in /etc/subuid and /etc/subgid to function correctly: %w", err)
|
|
}
|
|
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, fmt.Errorf("cannot read mappings: %w", err)
|
|
}
|
|
if len(uids) == 0 || len(gids) == 0 {
|
|
return nil, -1, -1, fmt.Errorf("nomap requires additional UIDs or GIDs defined in /etc/subuid and /etc/subgid to function correctly: %w", err)
|
|
}
|
|
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.New("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 "", fmt.Errorf("unable to resolve HOME directory: %w", err)
|
|
}
|
|
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, fmt.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, fmt.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, fmt.Errorf("sysctl '%s' is not allowed", arr[0])
|
|
}
|
|
}
|
|
return sysctl, nil
|
|
}
|
|
|
|
func DefaultContainerConfig() *config.Config {
|
|
return containerConfig
|
|
}
|
|
|
|
func CreateIDFile(path string, id string) error {
|
|
idFile, err := os.Create(path)
|
|
if err != nil {
|
|
return fmt.Errorf("creating idfile: %w", err)
|
|
}
|
|
defer idFile.Close()
|
|
if _, err = idFile.WriteString(id); err != nil {
|
|
return fmt.Errorf("writing idfile: %w", err)
|
|
}
|
|
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
|
|
}
|