mirror of
https://github.com/containers/podman.git
synced 2025-06-04 21:55:24 +08:00

This will add --layers support. Also add missing information in man pages on podman build features. Signed-off-by: Daniel J Walsh <dwalsh@redhat.com> Closes: #938 Approved by: umohnani8
238 lines
7.9 KiB
Go
238 lines
7.9 KiB
Go
package buildah
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/containers/storage/pkg/archive"
|
|
"github.com/containers/storage/pkg/idtools"
|
|
"github.com/opencontainers/runtime-spec/specs-go"
|
|
"github.com/pkg/errors"
|
|
"github.com/projectatomic/libpod/pkg/chrootuser"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// AddAndCopyOptions holds options for add and copy commands.
|
|
type AddAndCopyOptions struct {
|
|
// Chown is a spec for the user who should be given ownership over the
|
|
// newly-added content, potentially overriding permissions which would
|
|
// otherwise match those of local files and directories being copied.
|
|
Chown string
|
|
// All of the data being copied will pass through Hasher, if set.
|
|
// If the sources are URLs or files, their contents will be passed to
|
|
// Hasher.
|
|
// If the sources include directory trees, Hasher will be passed
|
|
// tar-format archives of the directory trees.
|
|
Hasher io.Writer
|
|
}
|
|
|
|
// addURL copies the contents of the source URL to the destination. This is
|
|
// its own function so that deferred closes happen after we're done pulling
|
|
// down each item of potentially many.
|
|
func addURL(destination, srcurl string, owner idtools.IDPair, hasher io.Writer) error {
|
|
logrus.Debugf("saving %q to %q", srcurl, destination)
|
|
resp, err := http.Get(srcurl)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error getting %q", srcurl)
|
|
}
|
|
defer resp.Body.Close()
|
|
f, err := os.Create(destination)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error creating %q", destination)
|
|
}
|
|
if err = f.Chown(owner.UID, owner.GID); err != nil {
|
|
return errors.Wrapf(err, "error setting owner of %q", destination)
|
|
}
|
|
if last := resp.Header.Get("Last-Modified"); last != "" {
|
|
if mtime, err2 := time.Parse(time.RFC1123, last); err2 != nil {
|
|
logrus.Debugf("error parsing Last-Modified time %q: %v", last, err2)
|
|
} else {
|
|
defer func() {
|
|
if err3 := os.Chtimes(destination, time.Now(), mtime); err3 != nil {
|
|
logrus.Debugf("error setting mtime to Last-Modified time %q: %v", last, err3)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
defer f.Close()
|
|
bodyReader := io.Reader(resp.Body)
|
|
if hasher != nil {
|
|
bodyReader = io.TeeReader(bodyReader, hasher)
|
|
}
|
|
n, err := io.Copy(f, bodyReader)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error reading contents for %q", destination)
|
|
}
|
|
if resp.ContentLength >= 0 && n != resp.ContentLength {
|
|
return errors.Errorf("error reading contents for %q: wrong length (%d != %d)", destination, n, resp.ContentLength)
|
|
}
|
|
if err := f.Chmod(0600); err != nil {
|
|
return errors.Wrapf(err, "error setting permissions on %q", destination)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Add copies the contents of the specified sources into the container's root
|
|
// filesystem, optionally extracting contents of local files that look like
|
|
// non-empty archives.
|
|
func (b *Builder) Add(destination string, extract bool, options AddAndCopyOptions, source ...string) error {
|
|
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)
|
|
}
|
|
}()
|
|
// Find out which user (and group) the destination should belong to.
|
|
user, err := b.user(mountPoint, options.Chown)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
containerOwner := idtools.IDPair{UID: int(user.UID), GID: int(user.GID)}
|
|
hostUID, hostGID, err := getHostIDs(b.IDMappingOptions.UIDMap, b.IDMappingOptions.GIDMap, user.UID, user.GID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hostOwner := idtools.IDPair{UID: int(hostUID), GID: int(hostGID)}
|
|
dest := mountPoint
|
|
if destination != "" && filepath.IsAbs(destination) {
|
|
dest = filepath.Join(dest, destination)
|
|
} else {
|
|
if err = idtools.MkdirAllAndChownNew(filepath.Join(dest, b.WorkDir()), 0755, hostOwner); err != nil {
|
|
return err
|
|
}
|
|
dest = filepath.Join(dest, b.WorkDir(), destination)
|
|
}
|
|
// If the destination was explicitly marked as a directory by ending it
|
|
// with a '/', create it so that we can be sure that it's a directory,
|
|
// and any files we're copying will be placed in the directory.
|
|
if len(destination) > 0 && destination[len(destination)-1] == os.PathSeparator {
|
|
if err = idtools.MkdirAllAndChownNew(dest, 0755, hostOwner); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// Make sure the destination's parent directory is usable.
|
|
if destpfi, err2 := os.Stat(filepath.Dir(dest)); err2 == nil && !destpfi.IsDir() {
|
|
return errors.Errorf("%q already exists, but is not a subdirectory)", filepath.Dir(dest))
|
|
}
|
|
// Now look at the destination itself.
|
|
destfi, err := os.Stat(dest)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return errors.Wrapf(err, "couldn't determine what %q is", dest)
|
|
}
|
|
destfi = nil
|
|
}
|
|
if len(source) > 1 && (destfi == nil || !destfi.IsDir()) {
|
|
return errors.Errorf("destination %q is not a directory", dest)
|
|
}
|
|
copyFileWithTar := b.copyFileWithTar(&containerOwner, options.Hasher)
|
|
copyWithTar := b.copyWithTar(&containerOwner, options.Hasher)
|
|
untarPath := b.untarPath(nil, options.Hasher)
|
|
for _, src := range source {
|
|
if strings.HasPrefix(src, "http://") || strings.HasPrefix(src, "https://") {
|
|
// We assume that source is a file, and we're copying
|
|
// it to the destination. If the destination is
|
|
// already a directory, create a file inside of it.
|
|
// Otherwise, the destination is the file to which
|
|
// we'll save the contents.
|
|
url, err := url.Parse(src)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error parsing URL %q", src)
|
|
}
|
|
d := dest
|
|
if destfi != nil && destfi.IsDir() {
|
|
d = filepath.Join(dest, path.Base(url.Path))
|
|
}
|
|
if err := addURL(d, src, hostOwner, options.Hasher); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
|
|
glob, err := filepath.Glob(src)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "invalid glob %q", src)
|
|
}
|
|
if len(glob) == 0 {
|
|
return errors.Wrapf(syscall.ENOENT, "no files found matching %q", src)
|
|
}
|
|
for _, gsrc := range glob {
|
|
srcfi, err := os.Stat(gsrc)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error reading %q", gsrc)
|
|
}
|
|
if srcfi.IsDir() {
|
|
// The source is a directory, so copy the contents of
|
|
// the source directory into the target directory. Try
|
|
// to create it first, so that if there's a problem,
|
|
// we'll discover why that won't work.
|
|
if err = idtools.MkdirAllAndChownNew(dest, 0755, hostOwner); err != nil {
|
|
return err
|
|
}
|
|
logrus.Debugf("copying %q to %q", gsrc+string(os.PathSeparator)+"*", dest+string(os.PathSeparator)+"*")
|
|
if err := copyWithTar(gsrc, dest); err != nil {
|
|
return errors.Wrapf(err, "error copying %q to %q", gsrc, dest)
|
|
}
|
|
continue
|
|
}
|
|
if !extract || !archive.IsArchivePath(gsrc) {
|
|
// This source is a file, and either it's not an
|
|
// archive, or we don't care whether or not it's an
|
|
// archive.
|
|
d := dest
|
|
if destfi != nil && destfi.IsDir() {
|
|
d = filepath.Join(dest, filepath.Base(gsrc))
|
|
}
|
|
// Copy the file, preserving attributes.
|
|
logrus.Debugf("copying %q to %q", gsrc, d)
|
|
if err := copyFileWithTar(gsrc, d); err != nil {
|
|
return errors.Wrapf(err, "error copying %q to %q", gsrc, d)
|
|
}
|
|
continue
|
|
}
|
|
// We're extracting an archive into the destination directory.
|
|
logrus.Debugf("extracting contents of %q into %q", gsrc, dest)
|
|
if err := untarPath(gsrc, dest); err != nil {
|
|
return errors.Wrapf(err, "error extracting %q into %q", gsrc, dest)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// user returns the user (and group) information which the destination should belong to.
|
|
func (b *Builder) user(mountPoint string, userspec string) (specs.User, error) {
|
|
if userspec == "" {
|
|
userspec = b.User()
|
|
}
|
|
|
|
uid, gid, err := chrootuser.GetUser(mountPoint, userspec)
|
|
u := specs.User{
|
|
UID: uid,
|
|
GID: gid,
|
|
Username: userspec,
|
|
}
|
|
if !strings.Contains(userspec, ":") {
|
|
groups, err2 := chrootuser.GetAdditionalGroupsForUser(mountPoint, uint64(u.UID))
|
|
if err2 != nil {
|
|
if errors.Cause(err2) != chrootuser.ErrNoSuchUser && err == nil {
|
|
err = err2
|
|
}
|
|
} else {
|
|
u.AdditionalGids = groups
|
|
}
|
|
|
|
}
|
|
return u, err
|
|
}
|