Skip to content

Commit 1fa3023

Browse files
Merge pull request containers#5975 from nalind/overlay-build-context
`buildah build`: use the same overlay for the context directory for the whole build
2 parents b639ccf + f57a5bc commit 1fa3023

File tree

15 files changed

+1037
-43
lines changed

15 files changed

+1037
-43
lines changed

imagebuildah/build.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,12 @@ func BuildDockerfiles(ctx context.Context, store storage.Store, options define.B
284284
}
285285

286286
builds.Go(func() error {
287+
contextDirectory, processLabel, mountLabel, usingContextOverlay, cleanupOverlay, err := platformSetupContextDirectoryOverlay(store, &options)
288+
if err != nil {
289+
return fmt.Errorf("mounting an overlay over build context directory: %w", err)
290+
}
291+
defer cleanupOverlay()
292+
platformOptions.ContextDirectory = contextDirectory
287293
loggerPerPlatform := logger
288294
if platformOptions.LogFile != "" && platformOptions.LogSplitByPlatform {
289295
logFile := platformOptions.LogFile + "_" + platformOptions.OS + "_" + platformOptions.Architecture
@@ -302,7 +308,7 @@ func BuildDockerfiles(ctx context.Context, store storage.Store, options define.B
302308
platformOptions.ReportWriter = reporter
303309
platformOptions.Err = stderr
304310
}
305-
thisID, thisRef, err := buildDockerfilesOnce(ctx, store, loggerPerPlatform, logPrefix, platformOptions, paths, files)
311+
thisID, thisRef, err := buildDockerfilesOnce(ctx, store, loggerPerPlatform, logPrefix, platformOptions, paths, files, processLabel, mountLabel, usingContextOverlay)
306312
if err != nil {
307313
if errorContext := strings.TrimSpace(logPrefix); errorContext != "" {
308314
return fmt.Errorf("%s: %w", errorContext, err)
@@ -413,7 +419,7 @@ func BuildDockerfiles(ctx context.Context, store storage.Store, options define.B
413419
return id, ref, nil
414420
}
415421

416-
func buildDockerfilesOnce(ctx context.Context, store storage.Store, logger *logrus.Logger, logPrefix string, options define.BuildOptions, containerFiles []string, dockerfilecontents [][]byte) (string, reference.Canonical, error) {
422+
func buildDockerfilesOnce(ctx context.Context, store storage.Store, logger *logrus.Logger, logPrefix string, options define.BuildOptions, containerFiles []string, dockerfilecontents [][]byte, processLabel, mountLabel string, usingContextOverlay bool) (string, reference.Canonical, error) {
417423
mainNode, err := imagebuilder.ParseDockerfile(bytes.NewReader(dockerfilecontents[0]))
418424
if err != nil {
419425
return "", nil, fmt.Errorf("parsing main Dockerfile: %s: %w", containerFiles[0], err)
@@ -454,7 +460,7 @@ func buildDockerfilesOnce(ctx context.Context, store storage.Store, logger *logr
454460
mainNode.Children = append(mainNode.Children, additionalNode.Children...)
455461
}
456462

457-
exec, err := newExecutor(logger, logPrefix, store, options, mainNode, containerFiles)
463+
exec, err := newExecutor(logger, logPrefix, store, options, mainNode, containerFiles, processLabel, mountLabel, usingContextOverlay)
458464
if err != nil {
459465
return "", nil, fmt.Errorf("creating build executor: %w", err)
460466
}

imagebuildah/build_linux.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package imagebuildah
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io/fs"
7+
"os"
8+
"path/filepath"
9+
"slices"
10+
11+
"github.com/containers/buildah/define"
12+
"github.com/containers/buildah/internal/tmpdir"
13+
"github.com/containers/buildah/pkg/overlay"
14+
"github.com/opencontainers/selinux/go-selinux/label"
15+
"github.com/sirupsen/logrus"
16+
"go.podman.io/storage"
17+
"golang.org/x/sys/unix"
18+
)
19+
20+
// platformSetupContextDirectoryOverlay() sets up an overlay _over_ the build
21+
// context directory, and sorts out labeling. Returns the location which
22+
// should be used as the default build context; the process label and mount
23+
// label for the build, if any; a boolean value that indicates whether we did,
24+
// in fact, mount an overlay; and a cleanup function which should be called
25+
// when the location is no longer needed (on success). Returned errors should
26+
// be treated as fatal.
27+
func platformSetupContextDirectoryOverlay(store storage.Store, options *define.BuildOptions) (string, string, string, bool, func(), error) {
28+
var succeeded bool
29+
var tmpDir, contentDir string
30+
cleanup := func() {
31+
if contentDir != "" {
32+
if err := overlay.CleanupContent(tmpDir); err != nil {
33+
logrus.Debugf("cleaning up overlay scaffolding for build context directory: %v", err)
34+
}
35+
}
36+
if tmpDir != "" {
37+
if err := os.Remove(tmpDir); err != nil && !errors.Is(err, fs.ErrNotExist) {
38+
logrus.Debugf("removing should-be-empty temporary directory %q: %v", tmpDir, err)
39+
}
40+
}
41+
}
42+
defer func() {
43+
if !succeeded {
44+
cleanup()
45+
}
46+
}()
47+
// double-check that the context directory location is an absolute path
48+
contextDirectoryAbsolute, err := filepath.Abs(options.ContextDirectory)
49+
if err != nil {
50+
return "", "", "", false, nil, fmt.Errorf("determining absolute path of %q: %w", options.ContextDirectory, err)
51+
}
52+
var st unix.Stat_t
53+
if err := unix.Stat(contextDirectoryAbsolute, &st); err != nil {
54+
return "", "", "", false, nil, fmt.Errorf("checking ownership of %q: %w", options.ContextDirectory, err)
55+
}
56+
// figure out the labeling situation
57+
processLabel, mountLabel, err := label.InitLabels(options.CommonBuildOpts.LabelOpts)
58+
if err != nil {
59+
return "", "", "", false, nil, err
60+
}
61+
// create a temporary directory
62+
tmpDir, err = os.MkdirTemp(tmpdir.GetTempDir(), "buildah-context-")
63+
if err != nil {
64+
return "", "", "", false, nil, fmt.Errorf("creating temporary directory: %w", err)
65+
}
66+
// create the scaffolding for an overlay mount under it
67+
contentDir, err = overlay.TempDir(tmpDir, 0, 0)
68+
if err != nil {
69+
return "", "", "", false, nil, fmt.Errorf("creating overlay scaffolding for build context directory: %w", err)
70+
}
71+
// mount an overlay that uses it as a lower
72+
overlayOptions := overlay.Options{
73+
GraphOpts: slices.Clone(store.GraphOptions()),
74+
ForceMount: true,
75+
MountLabel: mountLabel,
76+
}
77+
targetDir := filepath.Join(contentDir, "target")
78+
contextDirMountSpec, err := overlay.MountWithOptions(contentDir, contextDirectoryAbsolute, targetDir, &overlayOptions)
79+
if err != nil {
80+
return "", "", "", false, nil, fmt.Errorf("creating overlay scaffolding for build context directory: %w", err)
81+
}
82+
// going forward, pretend that the merged directory is the actual context directory
83+
logrus.Debugf("mounted an overlay at %q over %q", contextDirMountSpec.Source, contextDirectoryAbsolute)
84+
succeeded = true
85+
return contextDirMountSpec.Source, processLabel, mountLabel, true, cleanup, nil
86+
}

imagebuildah/build_notlinux.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//go:build !linux
2+
3+
package imagebuildah
4+
5+
import (
6+
"github.com/containers/buildah/define"
7+
"go.podman.io/storage"
8+
)
9+
10+
// platformSetupContextDirectoryOverlay() should set up an overlay _over_ the
11+
// build context directory, and sort out labeling. Should return the location
12+
// which should be used as the default build context; the process label and
13+
// mount label for the build, if any; a boolean value that indicates whether we
14+
// did, in fact, mount an overlay; and a cleanup function which should be
15+
// called when the location is no longer needed (on success). Returned errors
16+
// should be treated as fatal.
17+
// TODO: currenty a no-op on this platform.
18+
func platformSetupContextDirectoryOverlay(store storage.Store, options *define.BuildOptions) (string, string, string, bool, func(), error) {
19+
return options.ContextDirectory, "", "", false, func() {}, nil
20+
}

imagebuildah/executor.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ type executor struct {
7373
stages map[string]*stageExecutor
7474
store storage.Store
7575
contextDir string
76+
contextDirWritesAreDiscarded bool
7677
pullPolicy define.PullPolicy
7778
registry string
7879
ignoreUnrecognizedInstructions bool
@@ -187,7 +188,7 @@ type imageTypeAndHistoryAndDiffIDs struct {
187188
}
188189

189190
// newExecutor creates a new instance of the imagebuilder.Executor interface.
190-
func newExecutor(logger *logrus.Logger, logPrefix string, store storage.Store, options define.BuildOptions, mainNode *parser.Node, containerFiles []string) (*executor, error) {
191+
func newExecutor(logger *logrus.Logger, logPrefix string, store storage.Store, options define.BuildOptions, mainNode *parser.Node, containerFiles []string, processLabel, mountLabel string, contextWritesDiscarded bool) (*executor, error) {
191192
defaultContainerConfig, err := config.Default()
192193
if err != nil {
193194
return nil, fmt.Errorf("failed to get container config: %w", err)
@@ -257,6 +258,7 @@ func newExecutor(logger *logrus.Logger, logPrefix string, store storage.Store, o
257258
stages: make(map[string]*stageExecutor),
258259
store: store,
259260
contextDir: options.ContextDirectory,
261+
contextDirWritesAreDiscarded: contextWritesDiscarded,
260262
excludes: excludes,
261263
groupAdd: options.GroupAdd,
262264
ignoreFile: options.IgnoreFile,
@@ -294,6 +296,8 @@ func newExecutor(logger *logrus.Logger, logPrefix string, store storage.Store, o
294296
squash: options.Squash,
295297
labels: slices.Clone(options.Labels),
296298
layerLabels: slices.Clone(options.LayerLabels),
299+
processLabel: processLabel,
300+
mountLabel: mountLabel,
297301
annotations: slices.Clone(options.Annotations),
298302
layers: options.Layers,
299303
noHostname: options.CommonBuildOpts.NoHostname,

imagebuildah/stage_executor.go

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
buildahdocker "github.com/containers/buildah/docker"
2121
"github.com/containers/buildah/internal"
2222
"github.com/containers/buildah/internal/metadata"
23+
"github.com/containers/buildah/internal/sanitize"
2324
"github.com/containers/buildah/internal/tmpdir"
2425
internalUtil "github.com/containers/buildah/internal/util"
2526
"github.com/containers/buildah/pkg/parse"
@@ -453,7 +454,7 @@ func (s *stageExecutor) performCopy(excludes []string, copies ...imagebuilder.Co
453454
copy.Src = copySources
454455
}
455456

456-
if len(copy.From) > 0 && len(copy.Files) == 0 {
457+
if copy.From != "" && len(copy.Files) == 0 {
457458
// If from has an argument within it, resolve it to its
458459
// value. Otherwise just return the value found.
459460
from, fromErr := imagebuilder.ProcessWord(copy.From, s.stage.Builder.Arguments())
@@ -535,7 +536,8 @@ func (s *stageExecutor) performCopy(excludes []string, copies ...imagebuilder.Co
535536
}
536537
contextDir = mountPoint
537538
}
538-
// Original behaviour of buildah still stays true for COPY irrespective of additional context.
539+
// With --from set, the content being copied isn't coming from the default
540+
// build context directory, so we're not expected to force everything to 0:0
539541
preserveOwnership = true
540542
copyExcludes = excludes
541543
} else {
@@ -618,9 +620,14 @@ func (s *stageExecutor) performCopy(excludes []string, copies ...imagebuilder.Co
618620
}
619621

620622
// Returns a map of StageName/ImageName:internal.StageMountDetails for the
621-
// items in the passed-in mounts list which include a "from=" value.
623+
// items in the passed-in mounts list which include a "from=" value. The ""
624+
// key in the returned map corresponds to the default build context.
622625
func (s *stageExecutor) runStageMountPoints(mountList []string) (map[string]internal.StageMountDetails, error) {
623626
stageMountPoints := make(map[string]internal.StageMountDetails)
627+
stageMountPoints[""] = internal.StageMountDetails{
628+
MountPoint: s.executor.contextDir,
629+
IsWritesDiscardedOverlay: s.executor.contextDirWritesAreDiscarded,
630+
}
624631
for _, flag := range mountList {
625632
if strings.Contains(flag, "from") {
626633
tokens := strings.Split(flag, ",")
@@ -650,7 +657,7 @@ func (s *stageExecutor) runStageMountPoints(mountList []string) (map[string]inte
650657
if additionalBuildContext.IsImage {
651658
mountPoint, err := s.getImageRootfs(s.ctx, additionalBuildContext.Value)
652659
if err != nil {
653-
return nil, fmt.Errorf("%s from=%s: image found with that name", flag, from)
660+
return nil, fmt.Errorf("%s from=%s: image not found with that name", flag, from)
654661
}
655662
// The `from` in stageMountPoints should point
656663
// to `mountPoint` replaced from additional
@@ -932,6 +939,29 @@ func (s *stageExecutor) UnrecognizedInstruction(step *imagebuilder.Step) error {
932939
return errors.New(err)
933940
}
934941

942+
// sanitizeFrom limits which image names (with or without transport prefixes)
943+
// we'll accept. For those it accepts which refer to filesystem objects, where
944+
// relative path names are evaluated relative to "contextDir", it will create a
945+
// copy of the original image, under "tmpdir", which contains no symbolic
946+
// links, and return either the original image reference or a reference to a
947+
// sanitized copy which should be used instead.
948+
func (s *stageExecutor) sanitizeFrom(from, tmpdir string) (newFrom string, err error) {
949+
transportName, restOfImageName, maybeHasTransportName := strings.Cut(from, ":")
950+
if !maybeHasTransportName || transports.Get(transportName) == nil {
951+
if _, err = reference.ParseNormalizedNamed(from); err == nil {
952+
// this is a normal-looking image-in-a-registry-or-named-in-storage name
953+
return from, nil
954+
}
955+
if img, err := s.executor.store.Image(from); img != nil && err == nil {
956+
// this is an image ID
957+
return from, nil
958+
}
959+
return "", fmt.Errorf("parsing image name %q: %w", from, err)
960+
}
961+
// TODO: drop this part and just return an error... someday
962+
return sanitize.ImageName(transportName, restOfImageName, s.executor.contextDir, tmpdir)
963+
}
964+
935965
// prepare creates a working container based on the specified image, or if one
936966
// isn't specified, the first argument passed to the first FROM instruction we
937967
// can find in the stage's parsed tree.
@@ -948,6 +978,10 @@ func (s *stageExecutor) prepare(ctx context.Context, from string, initializeIBCo
948978
}
949979
from = base
950980
}
981+
sanitizedFrom, err := s.sanitizeFrom(from, tmpdir.GetTempDir())
982+
if err != nil {
983+
return nil, fmt.Errorf("invalid base image specification %q: %w", from, err)
984+
}
951985
displayFrom := from
952986
if ib.Platform != "" {
953987
displayFrom = "--platform=" + ib.Platform + " " + displayFrom
@@ -987,7 +1021,7 @@ func (s *stageExecutor) prepare(ctx context.Context, from string, initializeIBCo
9871021

9881022
builderOptions := buildah.BuilderOptions{
9891023
Args: ib.Args,
990-
FromImage: from,
1024+
FromImage: sanitizedFrom,
9911025
GroupAdd: s.executor.groupAdd,
9921026
PullPolicy: pullPolicy,
9931027
ContainerSuffix: s.executor.containerSuffix,
@@ -1025,16 +1059,6 @@ func (s *stageExecutor) prepare(ctx context.Context, from string, initializeIBCo
10251059
return nil, fmt.Errorf("creating build container: %w", err)
10261060
}
10271061

1028-
// If executor's ProcessLabel and MountLabel is empty means this is the first stage
1029-
// Make sure we share first stage's ProcessLabel and MountLabel with all other subsequent stages
1030-
// Doing this will ensure and one stage in same build can mount another stage even if `selinux`
1031-
// is enabled.
1032-
1033-
if s.executor.mountLabel == "" && s.executor.processLabel == "" {
1034-
s.executor.mountLabel = builder.MountLabel
1035-
s.executor.processLabel = builder.ProcessLabel
1036-
}
1037-
10381062
if initializeIBConfig {
10391063
volumes := map[string]struct{}{}
10401064
for _, v := range builder.Volumes() {

0 commit comments

Comments
 (0)