Files
podman/vendor/github.com/containers/buildah/pkg/parse/parse.go
Valentin Rothberg 65a618886e new "image" mount type
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>
2020-10-29 15:06:22 +01:00

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"
}