mirror of
https://github.com/containers/podman.git
synced 2026-03-18 06:38:49 -04:00
Add a new "image" mount type to `--mount`. The source of the mount is the name or ID of an image. The destination is the path inside the container. Image mounts further support an optional `rw,readwrite` parameter which if set to "true" will yield the mount writable inside the container. Note that no changes are propagated to the image mount on the host (which in any case is read only). Mounts are overlay mounts. To support read-only overlay mounts, vendor a non-release version of Buildah. Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
1013 lines
30 KiB
Go
1013 lines
30 KiB
Go
package parse
|
|
|
|
// this package should contain functions that parse and validate
|
|
// user input and is shared either amongst buildah subcommands or
|
|
// would be useful to projects vendoring buildah
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/containers/buildah"
|
|
"github.com/containers/image/v5/types"
|
|
"github.com/containers/storage/pkg/idtools"
|
|
"github.com/containers/storage/pkg/unshare"
|
|
units "github.com/docker/go-units"
|
|
specs "github.com/opencontainers/runtime-spec/specs-go"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
)
|
|
|
|
const (
|
|
// SeccompDefaultPath defines the default seccomp path
|
|
SeccompDefaultPath = "/usr/share/containers/seccomp.json"
|
|
// SeccompOverridePath if this exists it overrides the default seccomp path
|
|
SeccompOverridePath = "/etc/crio/seccomp.json"
|
|
// TypeBind is the type for mounting host dir
|
|
TypeBind = "bind"
|
|
// TypeTmpfs is the type for mounting tmpfs
|
|
TypeTmpfs = "tmpfs"
|
|
)
|
|
|
|
var (
|
|
errBadMntOption = errors.Errorf("invalid mount option")
|
|
errDuplicateDest = errors.Errorf("duplicate mount destination")
|
|
optionArgError = errors.Errorf("must provide an argument for option")
|
|
noDestError = errors.Errorf("must set volume destination")
|
|
)
|
|
|
|
// CommonBuildOptions parses the build options from the bud cli
|
|
func CommonBuildOptions(c *cobra.Command) (*buildah.CommonBuildOptions, error) {
|
|
var (
|
|
memoryLimit int64
|
|
memorySwap int64
|
|
noDNS bool
|
|
err error
|
|
)
|
|
|
|
memVal, _ := c.Flags().GetString("memory")
|
|
if memVal != "" {
|
|
memoryLimit, err = units.RAMInBytes(memVal)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "invalid value for memory")
|
|
}
|
|
}
|
|
|
|
memSwapValue, _ := c.Flags().GetString("memory-swap")
|
|
if memSwapValue != "" {
|
|
memorySwap, err = units.RAMInBytes(memSwapValue)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "invalid value for memory-swap")
|
|
}
|
|
}
|
|
|
|
addHost, _ := c.Flags().GetStringSlice("add-host")
|
|
if len(addHost) > 0 {
|
|
for _, host := range addHost {
|
|
if err := validateExtraHost(host); err != nil {
|
|
return nil, errors.Wrapf(err, "invalid value for add-host")
|
|
}
|
|
}
|
|
}
|
|
|
|
noDNS = false
|
|
dnsServers := []string{}
|
|
if c.Flag("dns").Changed {
|
|
dnsServers, _ = c.Flags().GetStringSlice("dns")
|
|
for _, server := range dnsServers {
|
|
if strings.ToLower(server) == "none" {
|
|
noDNS = true
|
|
}
|
|
}
|
|
if noDNS && len(dnsServers) > 1 {
|
|
return nil, errors.Errorf("invalid --dns, --dns=none may not be used with any other --dns options")
|
|
}
|
|
}
|
|
|
|
dnsSearch := []string{}
|
|
if c.Flag("dns-search").Changed {
|
|
dnsSearch, _ = c.Flags().GetStringSlice("dns-search")
|
|
if noDNS && len(dnsSearch) > 0 {
|
|
return nil, errors.Errorf("invalid --dns-search, --dns-search may not be used with --dns=none")
|
|
}
|
|
}
|
|
|
|
dnsOptions := []string{}
|
|
if c.Flag("dns-option").Changed {
|
|
dnsOptions, _ = c.Flags().GetStringSlice("dns-option")
|
|
if noDNS && len(dnsOptions) > 0 {
|
|
return nil, errors.Errorf("invalid --dns-option, --dns-option may not be used with --dns=none")
|
|
}
|
|
}
|
|
|
|
if _, err := units.FromHumanSize(c.Flag("shm-size").Value.String()); err != nil {
|
|
return nil, errors.Wrapf(err, "invalid --shm-size")
|
|
}
|
|
volumes, _ := c.Flags().GetStringArray("volume")
|
|
if err := Volumes(volumes); err != nil {
|
|
return nil, err
|
|
}
|
|
cpuPeriod, _ := c.Flags().GetUint64("cpu-period")
|
|
cpuQuota, _ := c.Flags().GetInt64("cpu-quota")
|
|
cpuShares, _ := c.Flags().GetUint64("cpu-shares")
|
|
httpProxy, _ := c.Flags().GetBool("http-proxy")
|
|
|
|
ulimit := []string{}
|
|
if c.Flag("ulimit").Changed {
|
|
ulimit, _ = c.Flags().GetStringSlice("ulimit")
|
|
}
|
|
|
|
commonOpts := &buildah.CommonBuildOptions{
|
|
AddHost: addHost,
|
|
CPUPeriod: cpuPeriod,
|
|
CPUQuota: cpuQuota,
|
|
CPUSetCPUs: c.Flag("cpuset-cpus").Value.String(),
|
|
CPUSetMems: c.Flag("cpuset-mems").Value.String(),
|
|
CPUShares: cpuShares,
|
|
CgroupParent: c.Flag("cgroup-parent").Value.String(),
|
|
DNSOptions: dnsOptions,
|
|
DNSSearch: dnsSearch,
|
|
DNSServers: dnsServers,
|
|
HTTPProxy: httpProxy,
|
|
Memory: memoryLimit,
|
|
MemorySwap: memorySwap,
|
|
ShmSize: c.Flag("shm-size").Value.String(),
|
|
Ulimit: ulimit,
|
|
Volumes: volumes,
|
|
}
|
|
securityOpts, _ := c.Flags().GetStringArray("security-opt")
|
|
if err := parseSecurityOpts(securityOpts, commonOpts); err != nil {
|
|
return nil, err
|
|
}
|
|
return commonOpts, nil
|
|
}
|
|
|
|
func parseSecurityOpts(securityOpts []string, commonOpts *buildah.CommonBuildOptions) error {
|
|
for _, opt := range securityOpts {
|
|
if opt == "no-new-privileges" {
|
|
return errors.Errorf("no-new-privileges is not supported")
|
|
}
|
|
con := strings.SplitN(opt, "=", 2)
|
|
if len(con) != 2 {
|
|
return errors.Errorf("Invalid --security-opt name=value pair: %q", opt)
|
|
}
|
|
|
|
switch con[0] {
|
|
case "label":
|
|
commonOpts.LabelOpts = append(commonOpts.LabelOpts, con[1])
|
|
case "apparmor":
|
|
commonOpts.ApparmorProfile = con[1]
|
|
case "seccomp":
|
|
commonOpts.SeccompProfilePath = con[1]
|
|
default:
|
|
return errors.Errorf("Invalid --security-opt 2: %q", opt)
|
|
}
|
|
|
|
}
|
|
|
|
if commonOpts.SeccompProfilePath == "" {
|
|
if _, err := os.Stat(SeccompOverridePath); err == nil {
|
|
commonOpts.SeccompProfilePath = SeccompOverridePath
|
|
} else {
|
|
if !os.IsNotExist(err) {
|
|
return errors.Wrapf(err, "can't check if %q exists", SeccompOverridePath)
|
|
}
|
|
if _, err := os.Stat(SeccompDefaultPath); err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return errors.Wrapf(err, "can't check if %q exists", SeccompDefaultPath)
|
|
}
|
|
} else {
|
|
commonOpts.SeccompProfilePath = SeccompDefaultPath
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Volume parses the input of --volume
|
|
func Volume(volume string) (specs.Mount, error) {
|
|
mount := specs.Mount{}
|
|
arr := strings.SplitN(volume, ":", 3)
|
|
if len(arr) < 2 {
|
|
return mount, errors.Errorf("incorrect volume format %q, should be host-dir:ctr-dir[:option]", volume)
|
|
}
|
|
if err := validateVolumeMountHostDir(arr[0]); err != nil {
|
|
return mount, err
|
|
}
|
|
if err := ValidateVolumeCtrDir(arr[1]); err != nil {
|
|
return mount, err
|
|
}
|
|
mountOptions := ""
|
|
if len(arr) > 2 {
|
|
mountOptions = arr[2]
|
|
if _, err := ValidateVolumeOpts(strings.Split(arr[2], ",")); err != nil {
|
|
return mount, err
|
|
}
|
|
}
|
|
mountOpts := strings.Split(mountOptions, ",")
|
|
mount.Source = arr[0]
|
|
mount.Destination = arr[1]
|
|
mount.Type = "rbind"
|
|
mount.Options = mountOpts
|
|
return mount, nil
|
|
}
|
|
|
|
// Volumes validates the host and container paths passed in to the --volume flag
|
|
func Volumes(volumes []string) error {
|
|
if len(volumes) == 0 {
|
|
return nil
|
|
}
|
|
for _, volume := range volumes {
|
|
if _, err := Volume(volume); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getVolumeMounts(volumes []string) (map[string]specs.Mount, error) {
|
|
finalVolumeMounts := make(map[string]specs.Mount)
|
|
|
|
for _, volume := range volumes {
|
|
volumeMount, err := Volume(volume)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, ok := finalVolumeMounts[volumeMount.Destination]; ok {
|
|
return nil, errors.Wrapf(errDuplicateDest, volumeMount.Destination)
|
|
}
|
|
finalVolumeMounts[volumeMount.Destination] = volumeMount
|
|
}
|
|
return finalVolumeMounts, nil
|
|
}
|
|
|
|
// GetVolumes gets the volumes from --volume and --mount
|
|
func GetVolumes(volumes []string, mounts []string) ([]specs.Mount, error) {
|
|
unifiedMounts, err := getMounts(mounts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
volumeMounts, err := getVolumeMounts(volumes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for dest, mount := range volumeMounts {
|
|
if _, ok := unifiedMounts[dest]; ok {
|
|
return nil, errors.Wrapf(errDuplicateDest, dest)
|
|
}
|
|
unifiedMounts[dest] = mount
|
|
}
|
|
|
|
finalMounts := make([]specs.Mount, 0, len(unifiedMounts))
|
|
for _, mount := range unifiedMounts {
|
|
finalMounts = append(finalMounts, mount)
|
|
}
|
|
return finalMounts, nil
|
|
}
|
|
|
|
// getMounts takes user-provided input from the --mount flag and creates OCI
|
|
// spec mounts.
|
|
// buildah run --mount type=bind,src=/etc/resolv.conf,target=/etc/resolv.conf ...
|
|
// buildah run --mount type=tmpfs,target=/dev/shm ...
|
|
func getMounts(mounts []string) (map[string]specs.Mount, error) {
|
|
finalMounts := make(map[string]specs.Mount)
|
|
|
|
errInvalidSyntax := errors.Errorf("incorrect mount format: should be --mount type=<bind|tmpfs>,[src=<host-dir>,]target=<ctr-dir>[,options]")
|
|
|
|
// TODO(vrothberg): the manual parsing can be replaced with a regular expression
|
|
// to allow a more robust parsing of the mount format and to give
|
|
// precise errors regarding supported format versus supported options.
|
|
for _, mount := range mounts {
|
|
arr := strings.SplitN(mount, ",", 2)
|
|
if len(arr) < 2 {
|
|
return nil, errors.Wrapf(errInvalidSyntax, "%q", mount)
|
|
}
|
|
kv := strings.Split(arr[0], "=")
|
|
// TODO: type is not explicitly required in Docker.
|
|
// If not specified, it defaults to "volume".
|
|
if len(kv) != 2 || kv[0] != "type" {
|
|
return nil, errors.Wrapf(errInvalidSyntax, "%q", mount)
|
|
}
|
|
|
|
tokens := strings.Split(arr[1], ",")
|
|
switch kv[1] {
|
|
case TypeBind:
|
|
mount, err := GetBindMount(tokens)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, ok := finalMounts[mount.Destination]; ok {
|
|
return nil, errors.Wrapf(errDuplicateDest, mount.Destination)
|
|
}
|
|
finalMounts[mount.Destination] = mount
|
|
case TypeTmpfs:
|
|
mount, err := GetTmpfsMount(tokens)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, ok := finalMounts[mount.Destination]; ok {
|
|
return nil, errors.Wrapf(errDuplicateDest, mount.Destination)
|
|
}
|
|
finalMounts[mount.Destination] = mount
|
|
default:
|
|
return nil, errors.Errorf("invalid filesystem type %q", kv[1])
|
|
}
|
|
}
|
|
|
|
return finalMounts, nil
|
|
}
|
|
|
|
// GetBindMount parses a single bind mount entry from the --mount flag.
|
|
func GetBindMount(args []string) (specs.Mount, error) {
|
|
newMount := specs.Mount{
|
|
Type: TypeBind,
|
|
}
|
|
|
|
setSource := false
|
|
setDest := false
|
|
|
|
for _, val := range args {
|
|
kv := strings.SplitN(val, "=", 2)
|
|
switch kv[0] {
|
|
case "bind-nonrecursive":
|
|
newMount.Options = append(newMount.Options, "bind")
|
|
case "ro", "nosuid", "nodev", "noexec":
|
|
// TODO: detect duplication of these options.
|
|
// (Is this necessary?)
|
|
newMount.Options = append(newMount.Options, kv[0])
|
|
case "readonly":
|
|
// Alias for "ro"
|
|
newMount.Options = append(newMount.Options, "ro")
|
|
case "shared", "rshared", "private", "rprivate", "slave", "rslave", "Z", "z":
|
|
newMount.Options = append(newMount.Options, kv[0])
|
|
case "bind-propagation":
|
|
if len(kv) == 1 {
|
|
return newMount, errors.Wrapf(optionArgError, kv[0])
|
|
}
|
|
newMount.Options = append(newMount.Options, kv[1])
|
|
case "src", "source":
|
|
if len(kv) == 1 {
|
|
return newMount, errors.Wrapf(optionArgError, kv[0])
|
|
}
|
|
if err := ValidateVolumeHostDir(kv[1]); err != nil {
|
|
return newMount, err
|
|
}
|
|
newMount.Source = kv[1]
|
|
setSource = true
|
|
case "target", "dst", "destination":
|
|
if len(kv) == 1 {
|
|
return newMount, errors.Wrapf(optionArgError, kv[0])
|
|
}
|
|
if err := ValidateVolumeCtrDir(kv[1]); err != nil {
|
|
return newMount, err
|
|
}
|
|
newMount.Destination = kv[1]
|
|
setDest = true
|
|
case "consistency":
|
|
// Option for OS X only, has no meaning on other platforms
|
|
// and can thus be safely ignored.
|
|
// See also the handling of the equivalent "delegated" and "cached" in ValidateVolumeOpts
|
|
default:
|
|
return newMount, errors.Wrapf(errBadMntOption, kv[0])
|
|
}
|
|
}
|
|
|
|
if !setDest {
|
|
return newMount, noDestError
|
|
}
|
|
|
|
if !setSource {
|
|
newMount.Source = newMount.Destination
|
|
}
|
|
|
|
opts, err := ValidateVolumeOpts(newMount.Options)
|
|
if err != nil {
|
|
return newMount, err
|
|
}
|
|
newMount.Options = opts
|
|
|
|
return newMount, nil
|
|
}
|
|
|
|
// GetTmpfsMount parses a single tmpfs mount entry from the --mount flag
|
|
func GetTmpfsMount(args []string) (specs.Mount, error) {
|
|
newMount := specs.Mount{
|
|
Type: TypeTmpfs,
|
|
Source: TypeTmpfs,
|
|
}
|
|
|
|
setDest := false
|
|
|
|
for _, val := range args {
|
|
kv := strings.SplitN(val, "=", 2)
|
|
switch kv[0] {
|
|
case "ro", "nosuid", "nodev", "noexec":
|
|
newMount.Options = append(newMount.Options, kv[0])
|
|
case "readonly":
|
|
// Alias for "ro"
|
|
newMount.Options = append(newMount.Options, "ro")
|
|
case "tmpfs-mode":
|
|
if len(kv) == 1 {
|
|
return newMount, errors.Wrapf(optionArgError, kv[0])
|
|
}
|
|
newMount.Options = append(newMount.Options, fmt.Sprintf("mode=%s", kv[1]))
|
|
case "tmpfs-size":
|
|
if len(kv) == 1 {
|
|
return newMount, errors.Wrapf(optionArgError, kv[0])
|
|
}
|
|
newMount.Options = append(newMount.Options, fmt.Sprintf("size=%s", kv[1]))
|
|
case "src", "source":
|
|
return newMount, errors.Errorf("source is not supported with tmpfs mounts")
|
|
case "target", "dst", "destination":
|
|
if len(kv) == 1 {
|
|
return newMount, errors.Wrapf(optionArgError, kv[0])
|
|
}
|
|
if err := ValidateVolumeCtrDir(kv[1]); err != nil {
|
|
return newMount, err
|
|
}
|
|
newMount.Destination = kv[1]
|
|
setDest = true
|
|
default:
|
|
return newMount, errors.Wrapf(errBadMntOption, kv[0])
|
|
}
|
|
}
|
|
|
|
if !setDest {
|
|
return newMount, noDestError
|
|
}
|
|
|
|
return newMount, nil
|
|
}
|
|
|
|
// ValidateVolumeHostDir validates a volume mount's source directory
|
|
func ValidateVolumeHostDir(hostDir string) error {
|
|
if len(hostDir) == 0 {
|
|
return errors.Errorf("host directory cannot be empty")
|
|
}
|
|
if filepath.IsAbs(hostDir) {
|
|
if _, err := os.Stat(hostDir); err != nil {
|
|
return errors.Wrapf(err, "error checking path %q", hostDir)
|
|
}
|
|
}
|
|
// If hostDir is not an absolute path, that means the user wants to create a
|
|
// named volume. This will be done later on in the code.
|
|
return nil
|
|
}
|
|
|
|
// validates the host path of buildah --volume
|
|
func validateVolumeMountHostDir(hostDir string) error {
|
|
if !filepath.IsAbs(hostDir) {
|
|
return errors.Errorf("invalid host path, must be an absolute path %q", hostDir)
|
|
}
|
|
if _, err := os.Stat(hostDir); err != nil {
|
|
return errors.Wrapf(err, "error checking path %q", hostDir)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateVolumeCtrDir validates a volume mount's destination directory.
|
|
func ValidateVolumeCtrDir(ctrDir string) error {
|
|
if len(ctrDir) == 0 {
|
|
return errors.Errorf("container directory cannot be empty")
|
|
}
|
|
if !filepath.IsAbs(ctrDir) {
|
|
return errors.Errorf("invalid container path %q, must be an absolute path", ctrDir)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateVolumeOpts validates a volume's options
|
|
func ValidateVolumeOpts(options []string) ([]string, error) {
|
|
var foundRootPropagation, foundRWRO, foundLabelChange, bindType, foundExec, foundDev, foundSuid int
|
|
finalOpts := make([]string, 0, len(options))
|
|
for _, opt := range options {
|
|
switch opt {
|
|
case "noexec", "exec":
|
|
foundExec++
|
|
if foundExec > 1 {
|
|
return nil, errors.Errorf("invalid options %q, can only specify 1 'noexec' or 'exec' option", strings.Join(options, ", "))
|
|
}
|
|
case "nodev", "dev":
|
|
foundDev++
|
|
if foundDev > 1 {
|
|
return nil, errors.Errorf("invalid options %q, can only specify 1 'nodev' or 'dev' option", strings.Join(options, ", "))
|
|
}
|
|
case "nosuid", "suid":
|
|
foundSuid++
|
|
if foundSuid > 1 {
|
|
return nil, errors.Errorf("invalid options %q, can only specify 1 'nosuid' or 'suid' option", strings.Join(options, ", "))
|
|
}
|
|
case "rw", "ro":
|
|
foundRWRO++
|
|
if foundRWRO > 1 {
|
|
return nil, errors.Errorf("invalid options %q, can only specify 1 'rw' or 'ro' option", strings.Join(options, ", "))
|
|
}
|
|
case "z", "Z", "O":
|
|
foundLabelChange++
|
|
if foundLabelChange > 1 {
|
|
return nil, errors.Errorf("invalid options %q, can only specify 1 'z', 'Z', or 'O' option", strings.Join(options, ", "))
|
|
}
|
|
case "private", "rprivate", "shared", "rshared", "slave", "rslave", "unbindable", "runbindable":
|
|
foundRootPropagation++
|
|
if foundRootPropagation > 1 {
|
|
return nil, errors.Errorf("invalid options %q, can only specify 1 '[r]shared', '[r]private', '[r]slave' or '[r]unbindable' option", strings.Join(options, ", "))
|
|
}
|
|
case "bind", "rbind":
|
|
bindType++
|
|
if bindType > 1 {
|
|
return nil, errors.Errorf("invalid options %q, can only specify 1 '[r]bind' option", strings.Join(options, ", "))
|
|
}
|
|
case "cached", "delegated":
|
|
// The discarded ops are OS X specific volume options
|
|
// introduced in a recent Docker version.
|
|
// They have no meaning on Linux, so here we silently
|
|
// drop them. This matches Docker's behavior (the options
|
|
// are intended to be always safe to use, even not on OS
|
|
// X).
|
|
continue
|
|
default:
|
|
return nil, errors.Errorf("invalid option type %q", opt)
|
|
}
|
|
finalOpts = append(finalOpts, opt)
|
|
}
|
|
return finalOpts, nil
|
|
}
|
|
|
|
// validateExtraHost validates that the specified string is a valid extrahost and returns it.
|
|
// ExtraHost is in the form of name:ip where the ip has to be a valid ip (ipv4 or ipv6).
|
|
// for add-host flag
|
|
func validateExtraHost(val string) error {
|
|
// allow for IPv6 addresses in extra hosts by only splitting on first ":"
|
|
arr := strings.SplitN(val, ":", 2)
|
|
if len(arr) != 2 || len(arr[0]) == 0 {
|
|
return errors.Errorf("bad format for add-host: %q", val)
|
|
}
|
|
if _, err := validateIPAddress(arr[1]); err != nil {
|
|
return errors.Errorf("invalid IP address in add-host: %q", arr[1])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateIPAddress validates an Ip address.
|
|
// for dns, ip, and ip6 flags also
|
|
func validateIPAddress(val string) (string, error) {
|
|
var ip = net.ParseIP(strings.TrimSpace(val))
|
|
if ip != nil {
|
|
return ip.String(), nil
|
|
}
|
|
return "", errors.Errorf("%s is not an ip address", val)
|
|
}
|
|
|
|
// SystemContextFromOptions returns a SystemContext populated with values
|
|
// per the input parameters provided by the caller for the use in authentication.
|
|
func SystemContextFromOptions(c *cobra.Command) (*types.SystemContext, error) {
|
|
certDir, err := c.Flags().GetString("cert-dir")
|
|
if err != nil {
|
|
certDir = ""
|
|
}
|
|
ctx := &types.SystemContext{
|
|
DockerCertPath: certDir,
|
|
}
|
|
tlsVerify, err := c.Flags().GetBool("tls-verify")
|
|
if err == nil && c.Flag("tls-verify").Changed {
|
|
ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!tlsVerify)
|
|
ctx.OCIInsecureSkipTLSVerify = !tlsVerify
|
|
ctx.DockerDaemonInsecureSkipTLSVerify = !tlsVerify
|
|
}
|
|
creds, err := c.Flags().GetString("creds")
|
|
if err == nil && c.Flag("creds").Changed {
|
|
var err error
|
|
ctx.DockerAuthConfig, err = getDockerAuth(creds)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
sigPolicy, err := c.Flags().GetString("signature-policy")
|
|
if err == nil && c.Flag("signature-policy").Changed {
|
|
ctx.SignaturePolicyPath = sigPolicy
|
|
}
|
|
authfile, err := c.Flags().GetString("authfile")
|
|
if err == nil {
|
|
ctx.AuthFilePath = getAuthFile(authfile)
|
|
}
|
|
regConf, err := c.Flags().GetString("registries-conf")
|
|
if err == nil && c.Flag("registries-conf").Changed {
|
|
ctx.SystemRegistriesConfPath = regConf
|
|
}
|
|
regConfDir, err := c.Flags().GetString("registries-conf-dir")
|
|
if err == nil && c.Flag("registries-conf-dir").Changed {
|
|
ctx.RegistriesDirPath = regConfDir
|
|
}
|
|
ctx.DockerRegistryUserAgent = fmt.Sprintf("Buildah/%s", buildah.Version)
|
|
if os, err := c.Flags().GetString("override-os"); err == nil {
|
|
ctx.OSChoice = os
|
|
}
|
|
if arch, err := c.Flags().GetString("override-arch"); err == nil {
|
|
ctx.ArchitectureChoice = arch
|
|
}
|
|
ctx.BigFilesTemporaryDir = GetTempDir()
|
|
return ctx, nil
|
|
}
|
|
|
|
func getAuthFile(authfile string) string {
|
|
if authfile != "" {
|
|
return authfile
|
|
}
|
|
return os.Getenv("REGISTRY_AUTH_FILE")
|
|
}
|
|
|
|
// PlatformFromOptions parses the operating system (os) and architecture (arch)
|
|
// from the provided command line options.
|
|
func PlatformFromOptions(c *cobra.Command) (os, arch string, err error) {
|
|
os = runtime.GOOS
|
|
arch = runtime.GOARCH
|
|
|
|
if selectedOS, err := c.Flags().GetString("os"); err == nil && selectedOS != runtime.GOOS {
|
|
os = selectedOS
|
|
}
|
|
if selectedArch, err := c.Flags().GetString("arch"); err == nil && selectedArch != runtime.GOARCH {
|
|
arch = selectedArch
|
|
}
|
|
|
|
if pf, err := c.Flags().GetString("platform"); err == nil && pf != DefaultPlatform() {
|
|
selectedOS, selectedArch, err := parsePlatform(pf)
|
|
if err != nil {
|
|
return "", "", errors.Wrap(err, "unable to parse platform")
|
|
}
|
|
arch = selectedArch
|
|
os = selectedOS
|
|
}
|
|
|
|
return os, arch, nil
|
|
}
|
|
|
|
const platformSep = "/"
|
|
|
|
// DefaultPlatform returns the standard platform for the current system
|
|
func DefaultPlatform() string {
|
|
return runtime.GOOS + platformSep + runtime.GOARCH
|
|
}
|
|
|
|
func parsePlatform(platform string) (os, arch string, err error) {
|
|
split := strings.Split(platform, platformSep)
|
|
if len(split) != 2 {
|
|
return "", "", errors.Errorf("invalid platform syntax for %q (use OS/ARCH)", platform)
|
|
}
|
|
return split[0], split[1], nil
|
|
}
|
|
|
|
func parseCreds(creds string) (string, string) {
|
|
if creds == "" {
|
|
return "", ""
|
|
}
|
|
up := strings.SplitN(creds, ":", 2)
|
|
if len(up) == 1 {
|
|
return up[0], ""
|
|
}
|
|
if up[0] == "" {
|
|
return "", up[1]
|
|
}
|
|
return up[0], up[1]
|
|
}
|
|
|
|
func getDockerAuth(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
|
|
}
|
|
|
|
// IDMappingOptions parses the build options related to user namespaces and ID mapping.
|
|
func IDMappingOptions(c *cobra.Command, isolation buildah.Isolation) (usernsOptions buildah.NamespaceOptions, idmapOptions *buildah.IDMappingOptions, err error) {
|
|
user := c.Flag("userns-uid-map-user").Value.String()
|
|
group := c.Flag("userns-gid-map-group").Value.String()
|
|
// If only the user or group was specified, use the same value for the
|
|
// other, since we need both in order to initialize the maps using the
|
|
// names.
|
|
if user == "" && group != "" {
|
|
user = group
|
|
}
|
|
if group == "" && user != "" {
|
|
group = user
|
|
}
|
|
// Either start with empty maps or the name-based maps.
|
|
mappings := idtools.NewIDMappingsFromMaps(nil, nil)
|
|
if user != "" && group != "" {
|
|
submappings, err := idtools.NewIDMappings(user, group)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
mappings = submappings
|
|
}
|
|
globalOptions := c.PersistentFlags()
|
|
// We'll parse the UID and GID mapping options the same way.
|
|
buildIDMap := func(basemap []idtools.IDMap, option string) ([]specs.LinuxIDMapping, error) {
|
|
outmap := make([]specs.LinuxIDMapping, 0, len(basemap))
|
|
// Start with the name-based map entries.
|
|
for _, m := range basemap {
|
|
outmap = append(outmap, specs.LinuxIDMapping{
|
|
ContainerID: uint32(m.ContainerID),
|
|
HostID: uint32(m.HostID),
|
|
Size: uint32(m.Size),
|
|
})
|
|
}
|
|
// Parse the flag's value as one or more triples (if it's even
|
|
// been set), and append them.
|
|
var spec []string
|
|
if globalOptions.Lookup(option) != nil && globalOptions.Lookup(option).Changed {
|
|
spec, _ = globalOptions.GetStringSlice(option)
|
|
}
|
|
if c.Flag(option).Changed {
|
|
spec, _ = c.Flags().GetStringSlice(option)
|
|
}
|
|
idmap, err := parseIDMap(spec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, m := range idmap {
|
|
outmap = append(outmap, specs.LinuxIDMapping{
|
|
ContainerID: m[0],
|
|
HostID: m[1],
|
|
Size: m[2],
|
|
})
|
|
}
|
|
return outmap, nil
|
|
}
|
|
uidmap, err := buildIDMap(mappings.UIDs(), "userns-uid-map")
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
gidmap, err := buildIDMap(mappings.GIDs(), "userns-gid-map")
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
// If we only have one map or the other populated at this point, then
|
|
// use the same mapping for both, since we know that no user or group
|
|
// name was specified, but a specific mapping was for one or the other.
|
|
if len(uidmap) == 0 && len(gidmap) != 0 {
|
|
uidmap = gidmap
|
|
}
|
|
if len(gidmap) == 0 && len(uidmap) != 0 {
|
|
gidmap = uidmap
|
|
}
|
|
|
|
// By default, having mappings configured means we use a user
|
|
// namespace. Otherwise, we don't.
|
|
usernsOption := buildah.NamespaceOption{
|
|
Name: string(specs.UserNamespace),
|
|
Host: len(uidmap) == 0 && len(gidmap) == 0,
|
|
}
|
|
// If the user specifically requested that we either use or don't use
|
|
// user namespaces, override that default.
|
|
if c.Flag("userns").Changed {
|
|
how := c.Flag("userns").Value.String()
|
|
switch how {
|
|
case "", "container", "private":
|
|
usernsOption.Host = false
|
|
case "host":
|
|
usernsOption.Host = true
|
|
default:
|
|
how = strings.TrimPrefix(how, "ns:")
|
|
if _, err := os.Stat(how); err != nil {
|
|
return nil, nil, errors.Wrapf(err, "error checking for %s namespace at %q", string(specs.UserNamespace), how)
|
|
}
|
|
logrus.Debugf("setting %q namespace to %q", string(specs.UserNamespace), how)
|
|
usernsOption.Path = how
|
|
}
|
|
}
|
|
usernsOptions = buildah.NamespaceOptions{usernsOption}
|
|
|
|
usernetwork := c.Flags().Lookup("network")
|
|
if usernetwork != nil && !usernetwork.Changed {
|
|
usernsOptions = append(usernsOptions, buildah.NamespaceOption{
|
|
Name: string(specs.NetworkNamespace),
|
|
Host: usernsOption.Host,
|
|
})
|
|
}
|
|
// If the user requested that we use the host namespace, but also that
|
|
// we use mappings, that's not going to work.
|
|
if (len(uidmap) != 0 || len(gidmap) != 0) && usernsOption.Host {
|
|
return nil, nil, errors.Errorf("can not specify ID mappings while using host's user namespace")
|
|
}
|
|
return usernsOptions, &buildah.IDMappingOptions{
|
|
HostUIDMapping: usernsOption.Host,
|
|
HostGIDMapping: usernsOption.Host,
|
|
UIDMap: uidmap,
|
|
GIDMap: gidmap,
|
|
}, nil
|
|
}
|
|
|
|
func parseIDMap(spec []string) (m [][3]uint32, err error) {
|
|
for _, s := range spec {
|
|
args := strings.FieldsFunc(s, func(r rune) bool { return !unicode.IsDigit(r) })
|
|
if len(args)%3 != 0 {
|
|
return nil, errors.Errorf("mapping %q is not in the form containerid:hostid:size[,...]", s)
|
|
}
|
|
for len(args) >= 3 {
|
|
cid, err := strconv.ParseUint(args[0], 10, 32)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error parsing container ID %q from mapping %q as a number", args[0], s)
|
|
}
|
|
hostid, err := strconv.ParseUint(args[1], 10, 32)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error parsing host ID %q from mapping %q as a number", args[1], s)
|
|
}
|
|
size, err := strconv.ParseUint(args[2], 10, 32)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error parsing %q from mapping %q as a number", args[2], s)
|
|
}
|
|
m = append(m, [3]uint32{uint32(cid), uint32(hostid), uint32(size)})
|
|
args = args[3:]
|
|
}
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// NamespaceOptions parses the build options for all namespaces except for user namespace.
|
|
func NamespaceOptions(c *cobra.Command) (namespaceOptions buildah.NamespaceOptions, networkPolicy buildah.NetworkConfigurationPolicy, err error) {
|
|
options := make(buildah.NamespaceOptions, 0, 7)
|
|
policy := buildah.NetworkDefault
|
|
for _, what := range []string{string(specs.IPCNamespace), "network", string(specs.PIDNamespace), string(specs.UTSNamespace)} {
|
|
if c.Flags().Lookup(what) != nil && c.Flag(what).Changed {
|
|
how := c.Flag(what).Value.String()
|
|
switch what {
|
|
case "network":
|
|
what = string(specs.NetworkNamespace)
|
|
}
|
|
switch how {
|
|
case "", "container", "private":
|
|
logrus.Debugf("setting %q namespace to %q", what, "")
|
|
options.AddOrReplace(buildah.NamespaceOption{
|
|
Name: what,
|
|
})
|
|
case "host":
|
|
logrus.Debugf("setting %q namespace to host", what)
|
|
options.AddOrReplace(buildah.NamespaceOption{
|
|
Name: what,
|
|
Host: true,
|
|
})
|
|
default:
|
|
if what == string(specs.NetworkNamespace) {
|
|
if how == "none" {
|
|
options.AddOrReplace(buildah.NamespaceOption{
|
|
Name: what,
|
|
})
|
|
policy = buildah.NetworkDisabled
|
|
logrus.Debugf("setting network to disabled")
|
|
break
|
|
}
|
|
if !filepath.IsAbs(how) {
|
|
options.AddOrReplace(buildah.NamespaceOption{
|
|
Name: what,
|
|
Path: how,
|
|
})
|
|
policy = buildah.NetworkEnabled
|
|
logrus.Debugf("setting network configuration to %q", how)
|
|
break
|
|
}
|
|
}
|
|
how = strings.TrimPrefix(how, "ns:")
|
|
if _, err := os.Stat(how); err != nil {
|
|
return nil, buildah.NetworkDefault, errors.Wrapf(err, "error checking for %s namespace at %q", what, how)
|
|
}
|
|
logrus.Debugf("setting %q namespace to %q", what, how)
|
|
options.AddOrReplace(buildah.NamespaceOption{
|
|
Name: what,
|
|
Path: how,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return options, policy, nil
|
|
}
|
|
|
|
func defaultIsolation() (buildah.Isolation, error) {
|
|
isolation, isSet := os.LookupEnv("BUILDAH_ISOLATION")
|
|
if isSet {
|
|
switch strings.ToLower(isolation) {
|
|
case "oci":
|
|
return buildah.IsolationOCI, nil
|
|
case "rootless":
|
|
return buildah.IsolationOCIRootless, nil
|
|
case "chroot":
|
|
return buildah.IsolationChroot, nil
|
|
default:
|
|
return 0, errors.Errorf("unrecognized $BUILDAH_ISOLATION value %q", isolation)
|
|
}
|
|
}
|
|
if unshare.IsRootless() {
|
|
return buildah.IsolationOCIRootless, nil
|
|
}
|
|
return buildah.IsolationDefault, nil
|
|
}
|
|
|
|
// IsolationOption parses the --isolation flag.
|
|
func IsolationOption(isolation string) (buildah.Isolation, error) {
|
|
if isolation != "" {
|
|
switch strings.ToLower(isolation) {
|
|
case "oci":
|
|
return buildah.IsolationOCI, nil
|
|
case "rootless":
|
|
return buildah.IsolationOCIRootless, nil
|
|
case "chroot":
|
|
return buildah.IsolationChroot, nil
|
|
default:
|
|
return 0, errors.Errorf("unrecognized isolation type %q", isolation)
|
|
}
|
|
}
|
|
return defaultIsolation()
|
|
}
|
|
|
|
// Device parses device mapping string to a src, dest & permissions string
|
|
// Valid values for device looklike:
|
|
// '/dev/sdc"
|
|
// '/dev/sdc:/dev/xvdc"
|
|
// '/dev/sdc:/dev/xvdc:rwm"
|
|
// '/dev/sdc:rm"
|
|
func Device(device string) (string, string, string, error) {
|
|
src := ""
|
|
dst := ""
|
|
permissions := "rwm"
|
|
arr := strings.Split(device, ":")
|
|
switch len(arr) {
|
|
case 3:
|
|
if !isValidDeviceMode(arr[2]) {
|
|
return "", "", "", errors.Errorf("invalid device mode: %s", arr[2])
|
|
}
|
|
permissions = arr[2]
|
|
fallthrough
|
|
case 2:
|
|
if isValidDeviceMode(arr[1]) {
|
|
permissions = arr[1]
|
|
} else {
|
|
if len(arr[1]) == 0 || arr[1][0] != '/' {
|
|
return "", "", "", errors.Errorf("invalid device mode: %s", arr[1])
|
|
}
|
|
dst = arr[1]
|
|
}
|
|
fallthrough
|
|
case 1:
|
|
if len(arr[0]) > 0 {
|
|
src = arr[0]
|
|
break
|
|
}
|
|
fallthrough
|
|
default:
|
|
return "", "", "", errors.Errorf("invalid device specification: %s", device)
|
|
}
|
|
|
|
if dst == "" {
|
|
dst = src
|
|
}
|
|
return src, dst, permissions, nil
|
|
}
|
|
|
|
// isValidDeviceMode checks if the mode for device is valid or not.
|
|
// isValid mode is a composition of r (read), w (write), and m (mknod).
|
|
func isValidDeviceMode(mode string) bool {
|
|
var legalDeviceMode = map[rune]bool{
|
|
'r': true,
|
|
'w': true,
|
|
'm': true,
|
|
}
|
|
if mode == "" {
|
|
return false
|
|
}
|
|
for _, c := range mode {
|
|
if !legalDeviceMode[c] {
|
|
return false
|
|
}
|
|
legalDeviceMode[c] = false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func GetTempDir() string {
|
|
if tmpdir, ok := os.LookupEnv("TMPDIR"); ok {
|
|
return tmpdir
|
|
}
|
|
return "/var/tmp"
|
|
}
|