Skip to content

Commit 7c58fc1

Browse files
committed
imagebuildah: use a longer-lived overlay over the build context
Mount a read-write overlay directory over the build context directory to restore the ability to use it as a covert cache of sorts during the lifetime of each platform's build, but in a way that still ensures that we don't modify the real build context directory. N.B.: builds where FROM in one stage referenced a relative path which had been written to a bind-mounted default build context directory by an earlier stage broke when we started making those bind mounts into overlays to prevent/discard modifications to that directory, and while this extends the lifetime of that overlay so that it's consistent throughout the build, those relative path names are still going to point to the wrong location. Since we need to determine SELinux labeling before mounting the overlay, go ahead and calculate the labels to use before creating the first builder, and remove the logic that had whichever stage thought it was the first one set them in its parent object for use by other stages, in what was probably a racey way. Signed-off-by: Nalin Dahyabhai <[email protected]>
1 parent 2250a3f commit 7c58fc1

File tree

11 files changed

+171
-21
lines changed

11 files changed

+171
-21
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: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -618,9 +618,14 @@ func (s *stageExecutor) performCopy(excludes []string, copies ...imagebuilder.Co
618618
}
619619

620620
// Returns a map of StageName/ImageName:internal.StageMountDetails for the
621-
// items in the passed-in mounts list which include a "from=" value.
621+
// items in the passed-in mounts list which include a "from=" value. The ""
622+
// key in the returned map corresponds to the default build context.
622623
func (s *stageExecutor) runStageMountPoints(mountList []string) (map[string]internal.StageMountDetails, error) {
623624
stageMountPoints := make(map[string]internal.StageMountDetails)
625+
stageMountPoints[""] = internal.StageMountDetails{
626+
MountPoint: s.executor.contextDir,
627+
IsWritesDiscardedOverlay: s.executor.contextDirWritesAreDiscarded,
628+
}
624629
for _, flag := range mountList {
625630
if strings.Contains(flag, "from") {
626631
tokens := strings.Split(flag, ",")
@@ -1025,16 +1030,6 @@ func (s *stageExecutor) prepare(ctx context.Context, from string, initializeIBCo
10251030
return nil, fmt.Errorf("creating build container: %w", err)
10261031
}
10271032

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-
10381033
if initializeIBConfig {
10391034
volumes := map[string]struct{}{}
10401035
for _, v := range builder.Volumes() {

internal/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ type StageMountDetails struct {
2222
IsImage bool // true if the mountpoint is an image's rootfs
2323
IsAdditionalBuildContext bool // true if the mountpoint is an additional build context
2424
MountPoint string // mountpoint of the stage or image's root directory or path of the additional build context
25+
IsWritesDiscardedOverlay bool // this is an overlay that discards writes
2526
}

internal/volumes/volumes.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ func GetBindMount(sys *types.SystemContext, args []string, contextDir string, st
141141
setDest := ""
142142
bindNonRecursive := false
143143
fromWhere := ""
144+
skipOverlay := false
144145

145146
for _, val := range args {
146147
argName, argValue, hasArgValue := strings.Cut(val, "=")
@@ -248,6 +249,7 @@ func GetBindMount(sys *types.SystemContext, args []string, contextDir string, st
248249
if additionalMountPoints != nil {
249250
if val, ok := additionalMountPoints[fromWhere]; ok {
250251
mountPoint = val.MountPoint
252+
skipOverlay = val.IsWritesDiscardedOverlay
251253
}
252254
}
253255
// if mountPoint of image was not found in additionalMap
@@ -273,6 +275,14 @@ func GetBindMount(sys *types.SystemContext, args []string, contextDir string, st
273275
}()
274276
}
275277
contextDir = mountPoint
278+
} else {
279+
// special case an additional mount point for "" as shorthand for "preferred location of the default build context"
280+
if additionalMountPoints != nil {
281+
if val, ok := additionalMountPoints[""]; ok {
282+
contextDir = val.MountPoint
283+
skipOverlay = val.IsWritesDiscardedOverlay
284+
}
285+
}
276286
}
277287

278288
// buildkit parity: default bind option must be `rbind`
@@ -328,7 +338,7 @@ func GetBindMount(sys *types.SystemContext, args []string, contextDir string, st
328338
}
329339

330340
overlayDir := ""
331-
if mountedImage != "" || mountIsReadWrite(newMount) {
341+
if !skipOverlay && (mountedImage != "" || mountIsReadWrite(newMount)) {
332342
if newMount, overlayDir, err = convertToOverlay(newMount, store, mountLabel, tmpDir, 0, 0); err != nil {
333343
return newMount, "", "", "", err
334344
}

pkg/cli/build.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,10 @@ func GenBuildOptions(c *cobra.Command, inputArgs []string, iopts BuildOptions) (
112112
if c.Flag("build-context").Changed {
113113
for _, contextString := range iopts.BuildContext {
114114
av := strings.SplitN(contextString, "=", 2)
115-
if len(av) > 1 {
115+
// the key should be non-empty: we use "" as internal
116+
// shorthand for the default build context when there's
117+
// an overlay mounted over it
118+
if len(av) > 1 && av[0] != "" {
116119
parseAdditionalBuildContext, err := parse.GetAdditionalBuildContext(av[1])
117120
if err != nil {
118121
return options, nil, nil, fmt.Errorf("while parsing additional build context: %w", err)

pkg/parse/parse.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,12 @@ const (
6060
BuildahCacheDir = "buildah-cache"
6161
)
6262

63-
var errInvalidSecretSyntax = errors.New("incorrect secret flag format: should be --secret id=foo,src=bar[,env=ENV][,type=file|env]")
63+
var (
64+
errInvalidSecretSyntax = errors.New("incorrect secret flag format: should be --secret id=foo,src=bar[,env=ENV][,type=file|env]")
65+
errInvalidBuildContextPathname = errors.New(`invalid build context path ""`)
66+
errInvalidBuildContextImage = errors.New(`invalid build context image name ""`)
67+
errInvalidBuildContextURL = errors.New(`invalid build context image URL ""`)
68+
)
6469

6570
// RepoNamesToNamedReferences parse the raw string to Named reference
6671
func RepoNamesToNamedReferences(destList []string) ([]reference.Named, error) {
@@ -244,21 +249,39 @@ func CommonBuildOptionsFromFlagSet(flags *pflag.FlagSet, findFlagFunc func(name
244249
return commonOpts, nil
245250
}
246251

247-
// GetAdditionalBuildContext consumes raw string and returns parsed AdditionalBuildContext
252+
// GetAdditionalBuildContext consumes a raw string and returns a parsed
253+
// AdditionalBuildContext describing the build context.
248254
func GetAdditionalBuildContext(value string) (define.AdditionalBuildContext, error) {
255+
if value == "" {
256+
// reject empty values (filesystem paths?), because elsewhere we use an
257+
// empty string as an internal nickname for the default build context
258+
return define.AdditionalBuildContext{}, errInvalidBuildContextPathname
259+
}
249260
ret := define.AdditionalBuildContext{IsURL: false, IsImage: false, Value: value}
250261
if strings.HasPrefix(value, "docker-image://") {
251262
ret.IsImage = true
252263
ret.Value = strings.TrimPrefix(value, "docker-image://")
264+
if ret.Value == "" {
265+
return define.AdditionalBuildContext{}, errInvalidBuildContextImage
266+
}
253267
} else if strings.HasPrefix(value, "container-image://") {
254268
ret.IsImage = true
255269
ret.Value = strings.TrimPrefix(value, "container-image://")
270+
if ret.Value == "" {
271+
return define.AdditionalBuildContext{}, errInvalidBuildContextImage
272+
}
256273
} else if strings.HasPrefix(value, "docker://") {
257274
ret.IsImage = true
258275
ret.Value = strings.TrimPrefix(value, "docker://")
276+
if ret.Value == "" {
277+
return define.AdditionalBuildContext{}, errInvalidBuildContextImage
278+
}
259279
} else if strings.HasPrefix(value, "http://") || strings.HasPrefix(value, "https://") {
260280
ret.IsImage = false
261281
ret.IsURL = true
282+
if strings.TrimPrefix(ret.Value, "http://") == "" || strings.TrimPrefix(ret.Value, "https://") == "" {
283+
return define.AdditionalBuildContext{}, errInvalidBuildContextURL
284+
}
262285
} else {
263286
path, err := filepath.Abs(value)
264287
if err != nil {

run.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,9 @@ type RunOptions struct {
168168
SSHSources map[string]*sshagent.Source `json:"-"`
169169
// RunMounts are unparsed mounts to be added for this run
170170
RunMounts []string
171-
// Map of stages and container mountpoint if any from stage executor
171+
// Map of already-mounted stages, images, and container mountpoints
172+
// which entries in `RunMounts` might be referring to. If a value for
173+
// the "" key is present, it points to the context directory.
172174
StageMountPoints map[string]internal.StageMountDetails
173175
// IDs of mounted images to be unmounted before returning
174176
// Deprecated: before 1.39, these images would not be consistently

0 commit comments

Comments
 (0)