mirror of
https://github.com/containers/skopeo.git
synced 2026-07-14 13:48:32 +00:00
Just have to refuse to use previous created containers when doing a run. Signed-off-by: Daniel J Walsh <dwalsh@redhat.com> Closes: #500 Approved by: rhatdan
474 lines
13 KiB
Go
474 lines
13 KiB
Go
package buildah
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/containers/storage/pkg/ioutils"
|
|
"github.com/docker/docker/profiles/seccomp"
|
|
units "github.com/docker/go-units"
|
|
digest "github.com/opencontainers/go-digest"
|
|
"github.com/opencontainers/runtime-spec/specs-go"
|
|
"github.com/opencontainers/runtime-tools/generate"
|
|
"github.com/opencontainers/selinux/go-selinux/label"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
)
|
|
|
|
const (
|
|
// DefaultWorkingDir is used if none was specified.
|
|
DefaultWorkingDir = "/"
|
|
// DefaultRuntime is the default command to use to run the container.
|
|
DefaultRuntime = "runc"
|
|
)
|
|
|
|
const (
|
|
// DefaultTerminal indicates that this Run invocation should be
|
|
// connected to a pseudoterminal if we're connected to a terminal.
|
|
DefaultTerminal = iota
|
|
// WithoutTerminal indicates that this Run invocation should NOT be
|
|
// connected to a pseudoterminal.
|
|
WithoutTerminal
|
|
// WithTerminal indicates that this Run invocation should be connected
|
|
// to a pseudoterminal.
|
|
WithTerminal
|
|
)
|
|
|
|
// RunOptions can be used to alter how a command is run in the container.
|
|
type RunOptions struct {
|
|
// Hostname is the hostname we set for the running container.
|
|
Hostname string
|
|
// Runtime is the name of the command to run. It should accept the same arguments that runc does.
|
|
Runtime string
|
|
// Args adds global arguments for the runtime.
|
|
Args []string
|
|
// Mounts are additional mount points which we want to provide.
|
|
Mounts []specs.Mount
|
|
// Env is additional environment variables to set.
|
|
Env []string
|
|
// User is the user as whom to run the command.
|
|
User string
|
|
// WorkingDir is an override for the working directory.
|
|
WorkingDir string
|
|
// Cmd is an override for the configured default command.
|
|
Cmd []string
|
|
// Entrypoint is an override for the configured entry point.
|
|
Entrypoint []string
|
|
// NetworkDisabled puts the container into its own network namespace.
|
|
NetworkDisabled bool
|
|
// Terminal provides a way to specify whether or not the command should
|
|
// be run with a pseudoterminal. By default (DefaultTerminal), a
|
|
// terminal is used if os.Stdout is connected to a terminal, but that
|
|
// decision can be overridden by specifying either WithTerminal or
|
|
// WithoutTerminal.
|
|
Terminal int
|
|
// Quiet tells the run to turn off output to stdout.
|
|
Quiet bool
|
|
}
|
|
|
|
func addRlimits(ulimit []string, g *generate.Generator) error {
|
|
var (
|
|
ul *units.Ulimit
|
|
err error
|
|
)
|
|
|
|
for _, u := range ulimit {
|
|
if ul, err = units.ParseUlimit(u); err != nil {
|
|
return errors.Wrapf(err, "ulimit option %q requires name=SOFT:HARD, failed to be parsed", u)
|
|
}
|
|
|
|
g.AddProcessRlimits("RLIMIT_"+strings.ToUpper(ul.Name), uint64(ul.Hard), uint64(ul.Soft))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func addHostsToFile(hosts []string) error {
|
|
if len(hosts) == 0 {
|
|
return nil
|
|
}
|
|
|
|
file, err := os.OpenFile("/etc/hosts", os.O_APPEND|os.O_WRONLY, os.ModeAppend)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
w := bufio.NewWriter(file)
|
|
for _, host := range hosts {
|
|
fmt.Fprintln(w, host)
|
|
}
|
|
return w.Flush()
|
|
}
|
|
|
|
func addCommonOptsToSpec(commonOpts *CommonBuildOptions, g *generate.Generator) error {
|
|
// RESOURCES - CPU
|
|
if commonOpts.CPUPeriod != 0 {
|
|
g.SetLinuxResourcesCPUPeriod(commonOpts.CPUPeriod)
|
|
}
|
|
if commonOpts.CPUQuota != 0 {
|
|
g.SetLinuxResourcesCPUQuota(commonOpts.CPUQuota)
|
|
}
|
|
if commonOpts.CPUShares != 0 {
|
|
g.SetLinuxResourcesCPUShares(commonOpts.CPUShares)
|
|
}
|
|
if commonOpts.CPUSetCPUs != "" {
|
|
g.SetLinuxResourcesCPUCpus(commonOpts.CPUSetCPUs)
|
|
}
|
|
if commonOpts.CPUSetMems != "" {
|
|
g.SetLinuxResourcesCPUMems(commonOpts.CPUSetMems)
|
|
}
|
|
|
|
// RESOURCES - MEMORY
|
|
if commonOpts.Memory != 0 {
|
|
g.SetLinuxResourcesMemoryLimit(commonOpts.Memory)
|
|
}
|
|
if commonOpts.MemorySwap != 0 {
|
|
g.SetLinuxResourcesMemorySwap(commonOpts.MemorySwap)
|
|
}
|
|
|
|
if commonOpts.CgroupParent != "" {
|
|
g.SetLinuxCgroupsPath(commonOpts.CgroupParent)
|
|
}
|
|
|
|
if err := addRlimits(commonOpts.Ulimit, g); err != nil {
|
|
return err
|
|
}
|
|
if err := addHostsToFile(commonOpts.AddHost); err != nil {
|
|
return err
|
|
}
|
|
|
|
logrus.Debugln("Resources:", commonOpts)
|
|
return nil
|
|
}
|
|
|
|
func (b *Builder) setupMounts(mountPoint string, spec *specs.Spec, optionMounts []specs.Mount, bindFiles, builtinVolumes, volumeMounts []string, shmSize string) error {
|
|
// The passed-in mounts matter the most to us.
|
|
mounts := make([]specs.Mount, len(optionMounts))
|
|
copy(mounts, optionMounts)
|
|
haveMount := func(destination string) bool {
|
|
for _, mount := range mounts {
|
|
if mount.Destination == destination {
|
|
// Already have something to mount there.
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
// Add mounts from the generated list, unless they conflict.
|
|
for _, specMount := range spec.Mounts {
|
|
if specMount.Destination == "/dev/shm" {
|
|
specMount.Options = []string{"nosuid", "noexec", "nodev", "mode=1777", "size=" + shmSize}
|
|
}
|
|
if haveMount(specMount.Destination) {
|
|
// Already have something to mount there, so skip this one.
|
|
continue
|
|
}
|
|
mounts = append(mounts, specMount)
|
|
}
|
|
// Add bind mounts for important files, unless they conflict.
|
|
for _, boundFile := range bindFiles {
|
|
if haveMount(boundFile) {
|
|
// Already have something to mount there, so skip this one.
|
|
continue
|
|
}
|
|
mounts = append(mounts, specs.Mount{
|
|
Source: boundFile,
|
|
Destination: boundFile,
|
|
Type: "bind",
|
|
Options: []string{"rbind", "ro"},
|
|
})
|
|
}
|
|
|
|
cdir, err := b.store.ContainerDirectory(b.ContainerID)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error determining work directory for container %q", b.ContainerID)
|
|
}
|
|
|
|
// Add secrets mounts
|
|
mountsFiles := []string{OverrideMountsFile, b.DefaultMountsFilePath}
|
|
for _, file := range mountsFiles {
|
|
secretMounts, err := secretMounts(file, b.MountLabel, cdir)
|
|
if err != nil {
|
|
logrus.Warn("error mounting secrets, skipping...")
|
|
continue
|
|
}
|
|
for _, mount := range secretMounts {
|
|
if haveMount(mount.Destination) {
|
|
continue
|
|
}
|
|
mounts = append(mounts, mount)
|
|
}
|
|
}
|
|
// Add temporary copies of the contents of volume locations at the
|
|
// volume locations, unless we already have something there.
|
|
for _, volume := range builtinVolumes {
|
|
if haveMount(volume) {
|
|
// Already mounting something there, no need to bother.
|
|
continue
|
|
}
|
|
subdir := digest.Canonical.FromString(volume).Hex()
|
|
volumePath := filepath.Join(cdir, "buildah-volumes", subdir)
|
|
// If we need to, initialize the volume path's initial contents.
|
|
if _, err = os.Stat(volumePath); os.IsNotExist(err) {
|
|
if err = os.MkdirAll(volumePath, 0755); err != nil {
|
|
return errors.Wrapf(err, "error creating directory %q for volume %q in container %q", volumePath, volume, b.ContainerID)
|
|
}
|
|
if err = label.Relabel(volumePath, b.MountLabel, false); err != nil {
|
|
return errors.Wrapf(err, "error relabeling directory %q for volume %q in container %q", volumePath, volume, b.ContainerID)
|
|
}
|
|
srcPath := filepath.Join(mountPoint, volume)
|
|
if err = copyWithTar(srcPath, volumePath); err != nil && !os.IsNotExist(err) {
|
|
return errors.Wrapf(err, "error populating directory %q for volume %q in container %q using contents of %q", volumePath, volume, b.ContainerID, srcPath)
|
|
}
|
|
|
|
}
|
|
// Add the bind mount.
|
|
mounts = append(mounts, specs.Mount{
|
|
Source: volumePath,
|
|
Destination: volume,
|
|
Type: "bind",
|
|
Options: []string{"bind"},
|
|
})
|
|
}
|
|
// Bind mount volumes given by the user at execution
|
|
var options []string
|
|
for _, i := range volumeMounts {
|
|
spliti := strings.Split(i, ":")
|
|
if len(spliti) > 2 {
|
|
options = strings.Split(spliti[2], ",")
|
|
}
|
|
if haveMount(spliti[1]) {
|
|
continue
|
|
}
|
|
options = append(options, "rbind")
|
|
var foundrw, foundro, foundz, foundZ bool
|
|
var rootProp string
|
|
for _, opt := range options {
|
|
switch opt {
|
|
case "rw":
|
|
foundrw = true
|
|
case "ro":
|
|
foundro = true
|
|
case "z":
|
|
foundz = true
|
|
case "Z":
|
|
foundZ = true
|
|
case "private", "rprivate", "slave", "rslave", "shared", "rshared":
|
|
rootProp = opt
|
|
}
|
|
}
|
|
if !foundrw && !foundro {
|
|
options = append(options, "rw")
|
|
}
|
|
if foundz {
|
|
if err := label.Relabel(spliti[0], spec.Linux.MountLabel, true); err != nil {
|
|
return errors.Wrapf(err, "relabel failed %q", spliti[0])
|
|
}
|
|
}
|
|
if foundZ {
|
|
if err := label.Relabel(spliti[0], spec.Linux.MountLabel, false); err != nil {
|
|
return errors.Wrapf(err, "relabel failed %q", spliti[0])
|
|
}
|
|
}
|
|
if rootProp == "" {
|
|
options = append(options, "private")
|
|
}
|
|
|
|
mounts = append(mounts, specs.Mount{
|
|
Destination: spliti[1],
|
|
Type: "bind",
|
|
Source: spliti[0],
|
|
Options: options,
|
|
})
|
|
}
|
|
// Set the list in the spec.
|
|
spec.Mounts = mounts
|
|
return nil
|
|
}
|
|
|
|
// Run runs the specified command in the container's root filesystem.
|
|
func (b *Builder) Run(command []string, options RunOptions) error {
|
|
var user specs.User
|
|
path, err := ioutil.TempDir(os.TempDir(), Package)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
logrus.Debugf("using %q to hold bundle data", path)
|
|
defer func() {
|
|
if err2 := os.RemoveAll(path); err2 != nil {
|
|
logrus.Errorf("error removing %q: %v", path, err2)
|
|
}
|
|
}()
|
|
g := generate.New()
|
|
|
|
for _, envSpec := range append(b.Env(), options.Env...) {
|
|
env := strings.SplitN(envSpec, "=", 2)
|
|
if len(env) > 1 {
|
|
g.AddProcessEnv(env[0], env[1])
|
|
}
|
|
}
|
|
|
|
if b.CommonBuildOpts == nil {
|
|
return errors.Errorf("Invalid format on container you must recreate the container")
|
|
}
|
|
|
|
if err := addCommonOptsToSpec(b.CommonBuildOpts, &g); err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(command) > 0 {
|
|
g.SetProcessArgs(command)
|
|
} else {
|
|
cmd := b.Cmd()
|
|
if len(options.Cmd) > 0 {
|
|
cmd = options.Cmd
|
|
}
|
|
ep := b.Entrypoint()
|
|
if len(options.Entrypoint) > 0 {
|
|
ep = options.Entrypoint
|
|
}
|
|
g.SetProcessArgs(append(ep, cmd...))
|
|
}
|
|
if options.WorkingDir != "" {
|
|
g.SetProcessCwd(options.WorkingDir)
|
|
} else if b.WorkDir() != "" {
|
|
g.SetProcessCwd(b.WorkDir())
|
|
}
|
|
if options.Hostname != "" {
|
|
g.SetHostname(options.Hostname)
|
|
} else if b.Hostname() != "" {
|
|
g.SetHostname(b.Hostname())
|
|
}
|
|
g.SetProcessSelinuxLabel(b.ProcessLabel)
|
|
g.SetLinuxMountLabel(b.MountLabel)
|
|
mountPoint, err := b.Mount(b.MountLabel)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if err2 := b.Unmount(); err2 != nil {
|
|
logrus.Errorf("error unmounting container: %v", err2)
|
|
}
|
|
}()
|
|
for _, mp := range []string{
|
|
"/proc/kcore",
|
|
"/proc/latency_stats",
|
|
"/proc/timer_list",
|
|
"/proc/timer_stats",
|
|
"/proc/sched_debug",
|
|
"/proc/scsi",
|
|
"/sys/firmware",
|
|
} {
|
|
g.AddLinuxMaskedPaths(mp)
|
|
}
|
|
|
|
for _, rp := range []string{
|
|
"/proc/asound",
|
|
"/proc/bus",
|
|
"/proc/fs",
|
|
"/proc/irq",
|
|
"/proc/sys",
|
|
"/proc/sysrq-trigger",
|
|
} {
|
|
g.AddLinuxReadonlyPaths(rp)
|
|
}
|
|
g.SetRootPath(mountPoint)
|
|
switch options.Terminal {
|
|
case DefaultTerminal:
|
|
g.SetProcessTerminal(terminal.IsTerminal(int(os.Stdout.Fd())))
|
|
case WithTerminal:
|
|
g.SetProcessTerminal(true)
|
|
case WithoutTerminal:
|
|
g.SetProcessTerminal(false)
|
|
}
|
|
if !options.NetworkDisabled {
|
|
if err = g.RemoveLinuxNamespace("network"); err != nil {
|
|
return errors.Wrapf(err, "error removing network namespace for run")
|
|
}
|
|
}
|
|
user, err = b.user(mountPoint, options.User)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
g.SetProcessUID(user.UID)
|
|
g.SetProcessGID(user.GID)
|
|
spec := g.Spec()
|
|
if spec.Process.Cwd == "" {
|
|
spec.Process.Cwd = DefaultWorkingDir
|
|
}
|
|
if err = os.MkdirAll(filepath.Join(mountPoint, spec.Process.Cwd), 0755); err != nil {
|
|
return errors.Wrapf(err, "error ensuring working directory %q exists", spec.Process.Cwd)
|
|
}
|
|
|
|
//Security Opts
|
|
g.SetProcessApparmorProfile(b.CommonBuildOpts.ApparmorProfile)
|
|
|
|
// HANDLE SECCOMP
|
|
if b.CommonBuildOpts.SeccompProfilePath != "unconfined" {
|
|
if b.CommonBuildOpts.SeccompProfilePath != "" {
|
|
seccompProfile, err := ioutil.ReadFile(b.CommonBuildOpts.SeccompProfilePath)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "opening seccomp profile (%s) failed", b.CommonBuildOpts.SeccompProfilePath)
|
|
}
|
|
seccompConfig, err := seccomp.LoadProfile(string(seccompProfile), spec)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "loading seccomp profile (%s) failed", b.CommonBuildOpts.SeccompProfilePath)
|
|
}
|
|
spec.Linux.Seccomp = seccompConfig
|
|
} else {
|
|
seccompConfig, err := seccomp.GetDefaultProfile(spec)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "loading seccomp profile (%s) failed", b.CommonBuildOpts.SeccompProfilePath)
|
|
}
|
|
spec.Linux.Seccomp = seccompConfig
|
|
}
|
|
}
|
|
|
|
cgroupMnt := specs.Mount{
|
|
Destination: "/sys/fs/cgroup",
|
|
Type: "cgroup",
|
|
Source: "cgroup",
|
|
Options: []string{"nosuid", "noexec", "nodev", "relatime", "ro"},
|
|
}
|
|
g.AddMount(cgroupMnt)
|
|
|
|
bindFiles := []string{"/etc/hosts", "/etc/resolv.conf"}
|
|
err = b.setupMounts(mountPoint, spec, options.Mounts, bindFiles, b.Volumes(), b.CommonBuildOpts.Volumes, b.CommonBuildOpts.ShmSize)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error resolving mountpoints for container")
|
|
}
|
|
specbytes, err := json.Marshal(spec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = ioutils.AtomicWriteFile(filepath.Join(path, "config.json"), specbytes, 0600)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error storing runtime configuration")
|
|
}
|
|
logrus.Debugf("config = %v", string(specbytes))
|
|
runtime := options.Runtime
|
|
if runtime == "" {
|
|
runtime = DefaultRuntime
|
|
}
|
|
args := append(options.Args, "run", "-b", path, Package+"-"+b.ContainerID)
|
|
cmd := exec.Command(runtime, args...)
|
|
cmd.Dir = mountPoint
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
if options.Quiet {
|
|
cmd.Stdout = nil
|
|
}
|
|
cmd.Stderr = os.Stderr
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
logrus.Debugf("error running runc %v: %v", spec.Process.Args, err)
|
|
}
|
|
return err
|
|
}
|