mirror of
https://github.com/containers/podman.git
synced 2025-05-25 11:06:18 +08:00

iFix builtin volumes to work with podman volume Currently builtin volumes are not recored in podman volumes when they are created automatically. This patch fixes this. Remove container volumes when requested Currently the --volume option on podman remove does nothing. This will implement the changes needed to remove the volumes if the user requests it. When removing a volume make sure that no container uses the volume. Signed-off-by: Daniel J Walsh dwalsh@redhat.com Signed-off-by: Daniel J Walsh <dwalsh@redhat.com>
366 lines
10 KiB
Go
366 lines
10 KiB
Go
package util
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
"github.com/containers/image/types"
|
|
"github.com/containers/libpod/pkg/rootless"
|
|
"github.com/containers/storage"
|
|
"github.com/containers/storage/pkg/idtools"
|
|
"github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
)
|
|
|
|
// 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 := terminal.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 determines if a string is in a string slice, returns bool
|
|
func StringInSlice(s string, sl []string) bool {
|
|
for _, i := range sl {
|
|
if i == s {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetImageConfig converts the --change flag values in the format "CMD=/bin/bash USER=example"
|
|
// to a type v1.ImageConfig
|
|
func GetImageConfig(changes []string) (v1.ImageConfig, error) {
|
|
// USER=value | EXPOSE=value | ENV=value | ENTRYPOINT=value |
|
|
// CMD=value | VOLUME=value | WORKDIR=value | LABEL=key=value | STOPSIGNAL=value
|
|
|
|
var (
|
|
user string
|
|
env []string
|
|
entrypoint []string
|
|
cmd []string
|
|
workingDir string
|
|
stopSignal string
|
|
)
|
|
|
|
exposedPorts := make(map[string]struct{})
|
|
volumes := make(map[string]struct{})
|
|
labels := make(map[string]string)
|
|
|
|
for _, ch := range changes {
|
|
pair := strings.Split(ch, "=")
|
|
if len(pair) == 1 {
|
|
return v1.ImageConfig{}, errors.Errorf("no value given for instruction %q", ch)
|
|
}
|
|
switch pair[0] {
|
|
case "USER":
|
|
user = pair[1]
|
|
case "EXPOSE":
|
|
var st struct{}
|
|
exposedPorts[pair[1]] = st
|
|
case "ENV":
|
|
env = append(env, pair[1])
|
|
case "ENTRYPOINT":
|
|
entrypoint = append(entrypoint, pair[1])
|
|
case "CMD":
|
|
cmd = append(cmd, pair[1])
|
|
case "VOLUME":
|
|
var st struct{}
|
|
volumes[pair[1]] = st
|
|
case "WORKDIR":
|
|
workingDir = pair[1]
|
|
case "LABEL":
|
|
if len(pair) == 3 {
|
|
labels[pair[1]] = pair[2]
|
|
} else {
|
|
labels[pair[1]] = ""
|
|
}
|
|
case "STOPSIGNAL":
|
|
stopSignal = pair[1]
|
|
}
|
|
}
|
|
|
|
return v1.ImageConfig{
|
|
User: user,
|
|
ExposedPorts: exposedPorts,
|
|
Env: env,
|
|
Entrypoint: entrypoint,
|
|
Cmd: cmd,
|
|
Volumes: volumes,
|
|
WorkingDir: workingDir,
|
|
Labels: labels,
|
|
StopSignal: stopSignal,
|
|
}, nil
|
|
}
|
|
|
|
// ParseIDMapping takes idmappings and subuid and subgid maps and returns a storage mapping
|
|
func ParseIDMapping(UIDMapSlice, GIDMapSlice []string, subUIDMap, subGIDMap string) (*storage.IDMappingOptions, error) {
|
|
options := storage.IDMappingOptions{
|
|
HostUIDMapping: true,
|
|
HostGIDMapping: true,
|
|
}
|
|
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 len(UIDMapSlice) == 0 && subUIDMap == "" && os.Getuid() != 0 {
|
|
UIDMapSlice = []string{fmt.Sprintf("0:%d:1", os.Getuid())}
|
|
}
|
|
if len(GIDMapSlice) == 0 && subGIDMap == "" && os.Getuid() != 0 {
|
|
GIDMapSlice = []string{fmt.Sprintf("0:%d:1", os.Getgid())}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// GetRootlessRuntimeDir returns the runtime directory when running as non root
|
|
func GetRootlessRuntimeDir() (string, error) {
|
|
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
|
|
uid := fmt.Sprintf("%d", rootless.GetRootlessUID())
|
|
if runtimeDir == "" {
|
|
tmpDir := filepath.Join("/run", "user", uid)
|
|
os.MkdirAll(tmpDir, 0700)
|
|
st, err := os.Stat(tmpDir)
|
|
if err == nil && int(st.Sys().(*syscall.Stat_t).Uid) == os.Getuid() && st.Mode().Perm() == 0700 {
|
|
runtimeDir = tmpDir
|
|
}
|
|
}
|
|
if runtimeDir == "" {
|
|
tmpDir := filepath.Join(os.TempDir(), "user", uid)
|
|
os.MkdirAll(tmpDir, 0700)
|
|
st, err := os.Stat(tmpDir)
|
|
if err == nil && int(st.Sys().(*syscall.Stat_t).Uid) == os.Getuid() && st.Mode().Perm() == 0700 {
|
|
runtimeDir = tmpDir
|
|
}
|
|
}
|
|
if runtimeDir == "" {
|
|
home := os.Getenv("HOME")
|
|
if home == "" {
|
|
return "", fmt.Errorf("neither XDG_RUNTIME_DIR nor HOME was set non-empty")
|
|
}
|
|
resolvedHome, err := filepath.EvalSymlinks(home)
|
|
if err != nil {
|
|
return "", errors.Wrapf(err, "cannot resolve %s", home)
|
|
}
|
|
runtimeDir = filepath.Join(resolvedHome, "rundir")
|
|
}
|
|
return runtimeDir, nil
|
|
}
|
|
|
|
// GetRootlessDirInfo returns the parent path of where the storage for containers and
|
|
// volumes will be in rootless mode
|
|
func GetRootlessDirInfo() (string, string, error) {
|
|
rootlessRuntime, err := GetRootlessRuntimeDir()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
dataDir := os.Getenv("XDG_DATA_HOME")
|
|
if dataDir == "" {
|
|
home := os.Getenv("HOME")
|
|
if home == "" {
|
|
return "", "", fmt.Errorf("neither XDG_DATA_HOME nor HOME was set non-empty")
|
|
}
|
|
// runc doesn't like symlinks in the rootfs path, and at least
|
|
// on CoreOS /home is a symlink to /var/home, so resolve any symlink.
|
|
resolvedHome, err := filepath.EvalSymlinks(home)
|
|
if err != nil {
|
|
return "", "", errors.Wrapf(err, "cannot resolve %s", home)
|
|
}
|
|
dataDir = filepath.Join(resolvedHome, ".local", "share")
|
|
}
|
|
return dataDir, rootlessRuntime, nil
|
|
}
|
|
|
|
// GetRootlessStorageOpts returns the storage opts for containers running as non root
|
|
func GetRootlessStorageOpts() (storage.StoreOptions, error) {
|
|
var opts storage.StoreOptions
|
|
|
|
dataDir, rootlessRuntime, err := GetRootlessDirInfo()
|
|
if err != nil {
|
|
return opts, err
|
|
}
|
|
opts.RunRoot = rootlessRuntime
|
|
opts.GraphRoot = filepath.Join(dataDir, "containers", "storage")
|
|
if path, err := exec.LookPath("fuse-overlayfs"); err == nil {
|
|
opts.GraphDriverName = "overlay"
|
|
opts.GraphDriverOptions = []string{fmt.Sprintf("overlay.mount_program=%s", path)}
|
|
} else {
|
|
opts.GraphDriverName = "vfs"
|
|
}
|
|
return opts, nil
|
|
}
|
|
|
|
// GetRootlessVolumePath returns where all the name volumes will be created in rootless mode
|
|
func GetRootlessVolumePath() (string, error) {
|
|
dataDir, _, err := GetRootlessDirInfo()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(dataDir, "containers", "storage", "volumes"), nil
|
|
}
|
|
|
|
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 *storage.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.Split(i, "=")
|
|
if s[0] == "overlay.mount_program" {
|
|
config.Storage.Options.MountProgram = s[1]
|
|
}
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
// GetDefaultStoreOptions returns the storage ops for containers and the volume path
|
|
// for the volume API
|
|
// It also returns the path where all named volumes will be created using the volume API
|
|
func GetDefaultStoreOptions() (storage.StoreOptions, string, error) {
|
|
var (
|
|
defaultRootlessRunRoot string
|
|
defaultRootlessGraphRoot string
|
|
err error
|
|
)
|
|
storageOpts := storage.DefaultStoreOptions
|
|
volumePath := filepath.Join(storageOpts.GraphRoot, "volumes")
|
|
if rootless.IsRootless() {
|
|
storageOpts, err = GetRootlessStorageOpts()
|
|
if err != nil {
|
|
return storageOpts, volumePath, err
|
|
}
|
|
volumePath, err = GetRootlessVolumePath()
|
|
if err != nil {
|
|
return storageOpts, volumePath, err
|
|
}
|
|
}
|
|
|
|
storageConf := StorageConfigFile()
|
|
if _, err = os.Stat(storageConf); err == nil {
|
|
defaultRootlessRunRoot = storageOpts.RunRoot
|
|
defaultRootlessGraphRoot = storageOpts.GraphRoot
|
|
storageOpts = storage.StoreOptions{}
|
|
storage.ReloadConfigurationFile(storageConf, &storageOpts)
|
|
}
|
|
|
|
if rootless.IsRootless() {
|
|
if os.IsNotExist(err) {
|
|
os.MkdirAll(filepath.Dir(storageConf), 0755)
|
|
file, err := os.OpenFile(storageConf, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
|
|
if err != nil {
|
|
return storageOpts, volumePath, errors.Wrapf(err, "cannot open %s", storageConf)
|
|
}
|
|
|
|
tomlConfiguration := getTomlStorage(&storageOpts)
|
|
defer file.Close()
|
|
enc := toml.NewEncoder(file)
|
|
if err := enc.Encode(tomlConfiguration); err != nil {
|
|
os.Remove(storageConf)
|
|
}
|
|
} else if err == nil {
|
|
// If the file did not specify a graphroot or runroot,
|
|
// set sane defaults so we don't try and use root-owned
|
|
// directories
|
|
if storageOpts.RunRoot == "" {
|
|
storageOpts.RunRoot = defaultRootlessRunRoot
|
|
}
|
|
if storageOpts.GraphRoot == "" {
|
|
storageOpts.GraphRoot = defaultRootlessGraphRoot
|
|
}
|
|
}
|
|
}
|
|
return storageOpts, volumePath, nil
|
|
}
|
|
|
|
// StorageConfigFile returns the path to the storage config file used
|
|
func StorageConfigFile() string {
|
|
if rootless.IsRootless() {
|
|
return filepath.Join(os.Getenv("HOME"), ".config/containers/storage.conf")
|
|
}
|
|
return storage.DefaultConfigFile
|
|
}
|