moby/builder/tarsum.go
Tibor Vass e0ef11a4c2 Abstract builder and implement server-side dockerfile builder
This patch creates interfaces in builder/ for building Docker images.
It is a first step in a series of patches to remove the daemon
dependency on builder and later allow a client-side Dockerfile builder
as well as potential builder plugins.

It is needed because we cannot remove the /build API endpoint, so we
need to keep the server-side Dockerfile builder, but we also want to
reuse the same Dockerfile parser and evaluator for both server-side and
client-side.

builder/dockerfile/ and api/server/builder.go contain implementations
of those interfaces as a refactoring of the current code.

Signed-off-by: Tibor Vass <tibor@docker.com>
2015-10-06 19:10:19 -04:00

165 lines
4.2 KiB
Go

package builder
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/chrootarchive"
"github.com/docker/docker/pkg/ioutils"
"github.com/docker/docker/pkg/symlink"
"github.com/docker/docker/pkg/tarsum"
)
type tarSumContext struct {
root string
sums tarsum.FileInfoSums
}
func (c *tarSumContext) Close() error {
return os.RemoveAll(c.root)
}
func convertPathError(err error, cleanpath string) error {
if err, ok := err.(*os.PathError); ok {
err.Path = cleanpath
return err
}
return err
}
func (c *tarSumContext) Open(path string) (io.ReadCloser, error) {
cleanpath, fullpath, err := c.normalize(path)
if err != nil {
return nil, err
}
r, err := os.Open(fullpath)
if err != nil {
return nil, convertPathError(err, cleanpath)
}
return r, nil
}
func (c *tarSumContext) Stat(path string) (fi FileInfo, err error) {
cleanpath, fullpath, err := c.normalize(path)
if err != nil {
return nil, err
}
st, err := os.Lstat(fullpath)
if err != nil {
return nil, convertPathError(err, cleanpath)
}
fi = PathFileInfo{st, fullpath}
// we set sum to path by default for the case where GetFile returns nil.
// The usual case is if cleanpath is empty.
sum := path
if tsInfo := c.sums.GetFile(cleanpath); tsInfo != nil {
sum = tsInfo.Sum()
}
fi = &HashedFileInfo{fi, sum}
return fi, nil
}
// MakeTarSumContext returns a build Context from a tar stream.
//
// It extracts the tar stream to a temporary folder that is deleted as soon as
// the Context is closed.
// As the extraction happens, a tarsum is calculated for every file, and the set of
// all those sums then becomes the source of truth for all operations on this Context.
//
// Closing tarStream has to be done by the caller.
func MakeTarSumContext(tarStream io.Reader) (ModifiableContext, error) {
root, err := ioutils.TempDir("", "docker-builder")
if err != nil {
return nil, err
}
tsc := &tarSumContext{root: root}
// Make sure we clean-up upon error. In the happy case the caller
// is expected to manage the clean-up
defer func() {
if err != nil {
tsc.Close()
}
}()
decompressedStream, err := archive.DecompressStream(tarStream)
if err != nil {
return nil, err
}
sum, err := tarsum.NewTarSum(decompressedStream, true, tarsum.Version1)
if err != nil {
return nil, err
}
if err := chrootarchive.Untar(sum, root, nil); err != nil {
return nil, err
}
tsc.sums = sum.GetSums()
return tsc, nil
}
func (c *tarSumContext) normalize(path string) (cleanpath, fullpath string, err error) {
cleanpath = filepath.Clean(string(os.PathSeparator) + path)[1:]
fullpath, err = symlink.FollowSymlinkInScope(filepath.Join(c.root, path), c.root)
if err != nil {
return "", "", fmt.Errorf("Forbidden path outside the build context: %s (%s)", path, fullpath)
}
_, err = os.Stat(fullpath)
if err != nil {
return "", "", convertPathError(err, path)
}
return
}
func (c *tarSumContext) Walk(root string, walkFn WalkFunc) error {
for _, tsInfo := range c.sums {
path := tsInfo.Name()
path, fullpath, err := c.normalize(path)
if err != nil {
return err
}
// Any file in the context that starts with the given path will be
// picked up and its hashcode used. However, we'll exclude the
// root dir itself. We do this for a coupel of reasons:
// 1 - ADD/COPY will not copy the dir itself, just its children
// so there's no reason to include it in the hash calc
// 2 - the metadata on the dir will change when any child file
// changes. This will lead to a miss in the cache check if that
// child file is in the .dockerignore list.
if rel, err := filepath.Rel(root, path); err != nil {
return err
} else if rel == "." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
continue
}
info, err := os.Lstat(fullpath)
if err != nil {
return convertPathError(err, path)
}
// TODO check context breakout?
fi := &HashedFileInfo{PathFileInfo{info, fullpath}, tsInfo.Sum()}
if err := walkFn(path, fi, nil); err != nil {
return err
}
}
return nil
}
func (c *tarSumContext) Remove(path string) error {
_, fullpath, err := c.normalize(path)
if err != nil {
return err
}
return os.RemoveAll(fullpath)
}