Skip to content

Commit 0bc20d2

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 the 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 f6ab6d5 commit 0bc20d2

File tree

11 files changed

+174
-21
lines changed

11 files changed

+174
-21
lines changed

imagebuildah/build.go

+12-3
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,15 @@ func BuildDockerfiles(ctx context.Context, store storage.Store, options define.B
204204
options.AdditionalBuildContexts = make(map[string]*define.AdditionalBuildContext)
205205
}
206206

207+
contextDirectory, processLabel, mountLabel, usingContextOverlay, cleanupOverlay, err := platformSetupContextDirectoryOverlay(store, &options)
208+
if err != nil {
209+
return "", nil, fmt.Errorf("mounting an overlay over build context directory: %w", err)
210+
}
211+
defer cleanupOverlay()
212+
if contextDirectory != "" {
213+
options.ContextDirectory = contextDirectory
214+
}
215+
207216
if len(options.Platforms) == 0 {
208217
options.Platforms = append(options.Platforms, struct{ OS, Arch, Variant string }{
209218
OS: options.SystemContext.OSChoice,
@@ -278,7 +287,7 @@ func BuildDockerfiles(ctx context.Context, store storage.Store, options define.B
278287
platformOptions.ReportWriter = reporter
279288
platformOptions.Err = stderr
280289
}
281-
thisID, thisRef, err := buildDockerfilesOnce(ctx, store, loggerPerPlatform, logPrefix, platformOptions, paths, files)
290+
thisID, thisRef, err := buildDockerfilesOnce(ctx, store, loggerPerPlatform, logPrefix, platformOptions, paths, files, processLabel, mountLabel, usingContextOverlay)
282291
if err != nil {
283292
if errorContext := strings.TrimSpace(logPrefix); errorContext != "" {
284293
return fmt.Errorf("%s: %w", errorContext, err)
@@ -389,7 +398,7 @@ func BuildDockerfiles(ctx context.Context, store storage.Store, options define.B
389398
return id, ref, nil
390399
}
391400

392-
func buildDockerfilesOnce(ctx context.Context, store storage.Store, logger *logrus.Logger, logPrefix string, options define.BuildOptions, containerFiles []string, dockerfilecontents [][]byte) (string, reference.Canonical, error) {
401+
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) {
393402
mainNode, err := imagebuilder.ParseDockerfile(bytes.NewReader(dockerfilecontents[0]))
394403
if err != nil {
395404
return "", nil, fmt.Errorf("parsing main Dockerfile: %s: %w", containerFiles[0], err)
@@ -441,7 +450,7 @@ func buildDockerfilesOnce(ctx context.Context, store storage.Store, logger *logr
441450
mainNode.Children = append(mainNode.Children, additionalNode.Children...)
442451
}
443452

444-
exec, err := newExecutor(logger, logPrefix, store, options, mainNode, containerFiles)
453+
exec, err := newExecutor(logger, logPrefix, store, options, mainNode, containerFiles, processLabel, mountLabel, usingContextOverlay)
445454
if err != nil {
446455
return "", nil, fmt.Errorf("creating build executor: %w", err)
447456
}

imagebuildah/build_linux.go

+86
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/containers/storage"
15+
"github.com/opencontainers/selinux/go-selinux/label"
16+
"github.com/sirupsen/logrus"
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

+20
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+
"github.com/containers/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

+5-1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ type Executor struct {
6969
stages map[string]*StageExecutor
7070
store storage.Store
7171
contextDir string
72+
contextDirWritesAreDiscarded bool
7273
pullPolicy define.PullPolicy
7374
registry string
7475
ignoreUnrecognizedInstructions bool
@@ -173,7 +174,7 @@ type imageTypeAndHistoryAndDiffIDs struct {
173174
}
174175

175176
// newExecutor creates a new instance of the imagebuilder.Executor interface.
176-
func newExecutor(logger *logrus.Logger, logPrefix string, store storage.Store, options define.BuildOptions, mainNode *parser.Node, containerFiles []string) (*Executor, error) {
177+
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) {
177178
defaultContainerConfig, err := config.Default()
178179
if err != nil {
179180
return nil, fmt.Errorf("failed to get container config: %w", err)
@@ -238,6 +239,7 @@ func newExecutor(logger *logrus.Logger, logPrefix string, store storage.Store, o
238239
stages: make(map[string]*StageExecutor),
239240
store: store,
240241
contextDir: options.ContextDirectory,
242+
contextDirWritesAreDiscarded: contextWritesDiscarded,
241243
excludes: excludes,
242244
groupAdd: options.GroupAdd,
243245
ignoreFile: options.IgnoreFile,
@@ -273,6 +275,8 @@ func newExecutor(logger *logrus.Logger, logPrefix string, store storage.Store, o
273275
squash: options.Squash,
274276
labels: slices.Clone(options.Labels),
275277
layerLabels: slices.Clone(options.LayerLabels),
278+
processLabel: processLabel,
279+
mountLabel: mountLabel,
276280
annotations: slices.Clone(options.Annotations),
277281
layers: options.Layers,
278282
noHostname: options.CommonBuildOpts.NoHostname,

imagebuildah/stage_executor.go

+6-11
Original file line numberDiff line numberDiff line change
@@ -606,9 +606,14 @@ func (s *StageExecutor) performCopy(excludes []string, copies ...imagebuilder.Co
606606
}
607607

608608
// Returns a map of StageName/ImageName:internal.StageMountDetails for the
609-
// items in the passed-in mounts list which include a "from=" value.
609+
// items in the passed-in mounts list which include a "from=" value. The ""
610+
// key in the returned map corresponds to the default build context.
610611
func (s *StageExecutor) runStageMountPoints(mountList []string) (map[string]internal.StageMountDetails, error) {
611612
stageMountPoints := make(map[string]internal.StageMountDetails)
613+
stageMountPoints[""] = internal.StageMountDetails{
614+
MountPoint: s.executor.contextDir,
615+
IsWritesDiscardedOverlay: s.executor.contextDirWritesAreDiscarded,
616+
}
612617
for _, flag := range mountList {
613618
if strings.Contains(flag, "from") {
614619
tokens := strings.Split(flag, ",")
@@ -1014,16 +1019,6 @@ func (s *StageExecutor) prepare(ctx context.Context, from string, initializeIBCo
10141019
return nil, fmt.Errorf("creating build container: %w", err)
10151020
}
10161021

1017-
// If executor's ProcessLabel and MountLabel is empty means this is the first stage
1018-
// Make sure we share first stage's ProcessLabel and MountLabel with all other subsequent stages
1019-
// Doing this will ensure and one stage in same build can mount another stage even if `selinux`
1020-
// is enabled.
1021-
1022-
if s.executor.mountLabel == "" && s.executor.processLabel == "" {
1023-
s.executor.mountLabel = builder.MountLabel
1024-
s.executor.processLabel = builder.ProcessLabel
1025-
}
1026-
10271022
if initializeIBConfig {
10281023
volumes := map[string]struct{}{}
10291024
for _, v := range builder.Volumes() {

internal/types.go

+1
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ type StageMountDetails struct {
1717
IsImage bool // true if the mountpoint is an image's rootfs
1818
IsAdditionalBuildContext bool // true if the mountpoint is an additional build context
1919
MountPoint string // mountpoint of the stage or image's root directory or path of the additional build context
20+
IsWritesDiscardedOverlay bool // this is an overlay that discards writes
2021
}

internal/volumes/volumes.go

+11-1
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

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

pkg/parse/parse.go

+25-2
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,12 @@ const (
5757
BuildahCacheDir = "buildah-cache"
5858
)
5959

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

6267
// RepoNamesToNamedReferences parse the raw string to Named reference
6368
func RepoNamesToNamedReferences(destList []string) ([]reference.Named, error) {
@@ -206,21 +211,39 @@ func CommonBuildOptionsFromFlagSet(flags *pflag.FlagSet, findFlagFunc func(name
206211
return commonOpts, nil
207212
}
208213

209-
// GetAdditionalBuildContext consumes raw string and returns parsed AdditionalBuildContext
214+
// GetAdditionalBuildContext consumes a raw string and returns a parsed
215+
// AdditionalBuildContext describing the build context.
210216
func GetAdditionalBuildContext(value string) (define.AdditionalBuildContext, error) {
217+
if value == "" {
218+
// reject empty values (filesystem paths?), because elsewhere we use an
219+
// empty string as an internal nickname for the default build context
220+
return define.AdditionalBuildContext{}, errInvalidBuildContextPathname
221+
}
211222
ret := define.AdditionalBuildContext{IsURL: false, IsImage: false, Value: value}
212223
if strings.HasPrefix(value, "docker-image://") {
213224
ret.IsImage = true
214225
ret.Value = strings.TrimPrefix(value, "docker-image://")
226+
if ret.Value == "" {
227+
return define.AdditionalBuildContext{}, errInvalidBuildContextImage
228+
}
215229
} else if strings.HasPrefix(value, "container-image://") {
216230
ret.IsImage = true
217231
ret.Value = strings.TrimPrefix(value, "container-image://")
232+
if ret.Value == "" {
233+
return define.AdditionalBuildContext{}, errInvalidBuildContextImage
234+
}
218235
} else if strings.HasPrefix(value, "docker://") {
219236
ret.IsImage = true
220237
ret.Value = strings.TrimPrefix(value, "docker://")
238+
if ret.Value == "" {
239+
return define.AdditionalBuildContext{}, errInvalidBuildContextImage
240+
}
221241
} else if strings.HasPrefix(value, "http://") || strings.HasPrefix(value, "https://") {
222242
ret.IsImage = false
223243
ret.IsURL = true
244+
if strings.TrimPrefix(ret.Value, "http://") == "" || strings.TrimPrefix(ret.Value, "https://") == "" {
245+
return define.AdditionalBuildContext{}, errInvalidBuildContextURL
246+
}
224247
} else {
225248
path, err := filepath.Abs(value)
226249
if err != nil {

run.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,9 @@ type RunOptions struct {
157157
SSHSources map[string]*sshagent.Source `json:"-"`
158158
// RunMounts are unparsed mounts to be added for this run
159159
RunMounts []string
160-
// Map of stages and container mountpoint if any from stage executor
160+
// Map of already-mounted stages, images, and container mountpoints
161+
// which entries in `RunMounts` might be referring to. If a value for
162+
// the "" key is present, it points to the context directory.
161163
StageMountPoints map[string]internal.StageMountDetails
162164
// IDs of mounted images to be unmounted before returning
163165
// Deprecated: before 1.39, these images would not be consistently

tests/bud.bats

+1-1
Original file line numberDiff line numberDiff line change
@@ -716,7 +716,7 @@ symlink(subdir)"
716716
_prefetch busybox
717717
run_buildah 125 build -t testbud3 $WITH_POLICY_JSON $BUDFILES/dockerignore3
718718
expect_output --substring 'building.*"COPY test1.txt /upload/test1.txt".*no such file or directory'
719-
expect_output --substring $(realpath "$BUDFILES/dockerignore3/.dockerignore")
719+
expect_output --substring 'filtered out using /[^ ]*/.dockerignore'
720720
}
721721

722722
@test "bud with .dockerignore #4" {

0 commit comments

Comments
 (0)