summaryrefslogtreecommitdiff
path: root/builder/remotecontext/archive.go
blob: bfe937b9aa508c936181dff1d36334e19b375546 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package remotecontext // import "github.com/docker/docker/builder/remotecontext"

import (
	"io"
	"os"
	"path/filepath"

	"github.com/docker/docker/builder"
	"github.com/docker/docker/pkg/archive"
	"github.com/docker/docker/pkg/chrootarchive"
	"github.com/docker/docker/pkg/containerfs"
	"github.com/docker/docker/pkg/longpath"
	"github.com/docker/docker/pkg/tarsum"
	"github.com/pkg/errors"
)

type archiveContext struct {
	root string
	sums tarsum.FileInfoSums
}

func (c *archiveContext) 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
}

type modifiableContext interface {
	builder.Source
	// Remove deletes the entry specified by `path`.
	// It is usual for directory entries to delete all its subentries.
	Remove(path string) error
}

// FromArchive returns a build source 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 FromArchive(tarStream io.Reader) (builder.Source, error) {
	root, err := longpath.MkdirTemp("", "docker-builder")
	if err != nil {
		return nil, err
	}

	// Assume local file system. Since it's coming from a tar file.
	tsc := &archiveContext{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
	}

	err = chrootarchive.Untar(sum, root, nil)
	if err != nil {
		return nil, err
	}

	tsc.sums = sum.GetSums()
	return tsc, nil
}

func (c *archiveContext) Root() string {
	return c.root
}

func (c *archiveContext) Remove(path string) error {
	_, fullpath, err := normalize(path, c.root)
	if err != nil {
		return err
	}
	return os.RemoveAll(fullpath)
}

func (c *archiveContext) Hash(path string) (string, error) {
	cleanpath, fullpath, err := normalize(path, c.root)
	if err != nil {
		return "", err
	}

	rel, err := filepath.Rel(c.root, fullpath)
	if err != nil {
		return "", convertPathError(err, cleanpath)
	}

	// Use the checksum of the followed path(not the possible symlink) because
	// this is the file that is actually copied.
	if tsInfo := c.sums.GetFile(filepath.ToSlash(rel)); tsInfo != nil {
		return tsInfo.Sum(), nil
	}
	// We set sum to path by default for the case where GetFile returns nil.
	// The usual case is if relative path is empty.
	return path, nil // backwards compat TODO: see if really needed
}

func normalize(path string, root string) (cleanPath, fullPath string, err error) {
	cleanPath = filepath.Clean(string(filepath.Separator) + path)[1:]
	fullPath, err = containerfs.ResolveScopedPath(root, path)
	if err != nil {
		return "", "", errors.Wrapf(err, "forbidden path outside the build context: %s (%s)", path, cleanPath)
	}
	return
}