ADD/COPY --link support added
What type of PR is this? /kind feature What this PR does / why we need it: It implements --link for COPY and ADD instructions and enables the creation of cachable layers that can be reused independently across builds. Follows buildkit `--link` specifications How to verify it bats tests/bud.bats Which issue(s) this PR fixes: Fixes #4325 Does this PR introduce a user-facing change? Yes, gives extra functionality to Containerfiles Signed-off-by: Joshua Arrevillaga <2004jarrevillaga@gmail.com>
This commit is contained in:
parent
7a243f955e
commit
eea4838d88
132
add.go
132
add.go
|
@ -33,6 +33,7 @@ import (
|
|||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/moby/sys/userns"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
@ -102,6 +103,15 @@ type AddAndCopyOptions struct {
|
|||
Parents bool
|
||||
// Timestamp is a timestamp to override on all content as it is being read.
|
||||
Timestamp *time.Time
|
||||
// Link, when set to true, creates an independent layer containing the copied content
|
||||
// that sits on top of existing layers. This layer can be cached and reused
|
||||
// separately, and is not affected by filesystem changes from previous instructions.
|
||||
Link bool
|
||||
// BuildMetadata is consulted only when Link is true. Contains metadata used by
|
||||
// imagebuildah for cache evaluation of linked layers (inheritLabels, unsetAnnotations,
|
||||
// inheritAnnotations, newAnnotations). This field is internally managed and should
|
||||
// not be set by external API users.
|
||||
BuildMetadata string
|
||||
}
|
||||
|
||||
// gitURLFragmentSuffix matches fragments to use as Git reference and build
|
||||
|
@ -495,15 +505,75 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
|
|||
}
|
||||
destUIDMap, destGIDMap := convertRuntimeIDMaps(b.IDMappingOptions.UIDMap, b.IDMappingOptions.GIDMap)
|
||||
|
||||
// Create the target directory if it doesn't exist yet.
|
||||
var putRoot, putDir, stagingDir string
|
||||
var createdDirs []string
|
||||
var latestTimestamp time.Time
|
||||
|
||||
mkdirOptions := copier.MkdirOptions{
|
||||
UIDMap: destUIDMap,
|
||||
GIDMap: destGIDMap,
|
||||
ChownNew: chownDirs,
|
||||
}
|
||||
|
||||
// If --link is specified, we create a staging directory to hold the content
|
||||
// that will then become an independent layer
|
||||
if options.Link {
|
||||
containerDir, err := b.store.ContainerDirectory(b.ContainerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting container directory for %q: %w", b.ContainerID, err)
|
||||
}
|
||||
|
||||
stagingDir, err = os.MkdirTemp(containerDir, "link-stage-")
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating staging directory for link %q: %w", b.ContainerID, err)
|
||||
}
|
||||
|
||||
putRoot = stagingDir
|
||||
|
||||
cleanDest := filepath.Clean(destination)
|
||||
|
||||
if strings.Contains(cleanDest, "..") {
|
||||
return fmt.Errorf("invalid destination path %q: contains path traversal", destination)
|
||||
}
|
||||
|
||||
if renameTarget != "" {
|
||||
putDir = filepath.Dir(filepath.Join(stagingDir, cleanDest))
|
||||
} else {
|
||||
putDir = filepath.Join(stagingDir, cleanDest)
|
||||
}
|
||||
|
||||
putDirAbs, err := filepath.Abs(putDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve absolute path: %w", err)
|
||||
}
|
||||
|
||||
stagingDirAbs, err := filepath.Abs(stagingDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve staging directory absolute path: %w", err)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(putDirAbs, stagingDirAbs+string(os.PathSeparator)) && putDirAbs != stagingDirAbs {
|
||||
return fmt.Errorf("destination path %q escapes staging directory", destination)
|
||||
}
|
||||
if err := copier.Mkdir(putRoot, putDirAbs, mkdirOptions); err != nil {
|
||||
return fmt.Errorf("ensuring target directory exists: %w", err)
|
||||
}
|
||||
tempPath := putDir
|
||||
for tempPath != stagingDir && tempPath != filepath.Dir(tempPath) {
|
||||
if _, err := os.Stat(tempPath); err == nil {
|
||||
createdDirs = append(createdDirs, tempPath)
|
||||
}
|
||||
tempPath = filepath.Dir(tempPath)
|
||||
}
|
||||
} else {
|
||||
if err := copier.Mkdir(mountPoint, extractDirectory, mkdirOptions); err != nil {
|
||||
return fmt.Errorf("ensuring target directory exists: %w", err)
|
||||
}
|
||||
|
||||
putRoot = extractDirectory
|
||||
putDir = extractDirectory
|
||||
}
|
||||
|
||||
// Copy each source in turn.
|
||||
for _, src := range sources {
|
||||
var multiErr *multierror.Error
|
||||
|
@ -580,7 +650,7 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
|
|||
ChmodFiles: nil,
|
||||
IgnoreDevices: userns.RunningInUserNS(),
|
||||
}
|
||||
putErr = copier.Put(extractDirectory, extractDirectory, putOptions, io.TeeReader(pipeReader, hasher))
|
||||
putErr = copier.Put(putRoot, putDir, putOptions, io.TeeReader(pipeReader, hasher))
|
||||
}
|
||||
hashCloser.Close()
|
||||
pipeReader.Close()
|
||||
|
@ -658,6 +728,9 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
|
|||
itemsCopied++
|
||||
}
|
||||
st := localSourceStat.Results[globbed]
|
||||
if options.Link && st.ModTime.After(latestTimestamp) {
|
||||
latestTimestamp = st.ModTime
|
||||
}
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
|
@ -741,12 +814,13 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
|
|||
ChmodFiles: nil,
|
||||
IgnoreDevices: userns.RunningInUserNS(),
|
||||
}
|
||||
putErr = copier.Put(extractDirectory, extractDirectory, putOptions, io.TeeReader(pipeReader, hasher))
|
||||
putErr = copier.Put(putRoot, putDir, putOptions, io.TeeReader(pipeReader, hasher))
|
||||
}
|
||||
hashCloser.Close()
|
||||
pipeReader.Close()
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
if getErr != nil {
|
||||
getErr = fmt.Errorf("reading %q: %w", src, getErr)
|
||||
|
@ -776,6 +850,58 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
|
|||
return fmt.Errorf("no items matching glob %q copied (%d filtered out%s): %w", localSourceStat.Glob, len(localSourceStat.Globbed), excludesFile, syscall.ENOENT)
|
||||
}
|
||||
}
|
||||
|
||||
if options.Link {
|
||||
if !latestTimestamp.IsZero() {
|
||||
for _, dir := range createdDirs {
|
||||
if err := os.Chtimes(dir, latestTimestamp, latestTimestamp); err != nil {
|
||||
logrus.Warnf("failed to set timestamp on directory %q: %v", dir, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
var created time.Time
|
||||
if options.Timestamp != nil {
|
||||
created = *options.Timestamp
|
||||
} else if !latestTimestamp.IsZero() {
|
||||
created = latestTimestamp
|
||||
} else {
|
||||
created = time.Unix(0, 0).UTC()
|
||||
}
|
||||
|
||||
command := "ADD"
|
||||
if !extract {
|
||||
command = "COPY"
|
||||
}
|
||||
|
||||
contentType, digest := b.ContentDigester.Digest()
|
||||
summary := contentType
|
||||
if digest != "" {
|
||||
if summary != "" {
|
||||
summary = summary + ":"
|
||||
}
|
||||
summary = summary + digest.Encoded()
|
||||
logrus.Debugf("added content from --link %s", summary)
|
||||
}
|
||||
|
||||
createdBy := "/bin/sh -c #(nop) " + command + " --link " + summary + " in " + destination + " " + options.BuildMetadata
|
||||
history := v1.History{
|
||||
Created: &created,
|
||||
CreatedBy: createdBy,
|
||||
Comment: b.HistoryComment(),
|
||||
}
|
||||
|
||||
linkedLayer := LinkedLayer{
|
||||
History: history,
|
||||
BlobPath: stagingDir,
|
||||
}
|
||||
|
||||
b.AppendedLinkedLayers = append(b.AppendedLinkedLayers, linkedLayer)
|
||||
|
||||
if err := b.Save(); err != nil {
|
||||
return fmt.Errorf("saving builder state after queuing linked layer: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ type addCopyResults struct {
|
|||
excludes []string
|
||||
parents bool
|
||||
timestamp string
|
||||
link bool
|
||||
}
|
||||
|
||||
func createCommand(addCopy string, desc string, short string, opts *addCopyResults) *cobra.Command {
|
||||
|
@ -74,6 +75,7 @@ func applyFlagVars(flags *pflag.FlagSet, opts *addCopyResults) {
|
|||
flags.StringVar(&opts.chown, "chown", "", "set the user and group ownership of the destination content")
|
||||
flags.StringVar(&opts.chmod, "chmod", "", "set the access permissions of the destination content")
|
||||
flags.StringVar(&opts.creds, "creds", "", "use `[username[:password]]` for accessing registries when pulling images")
|
||||
flags.BoolVar(&opts.link, "link", false, "enable layer caching for this operation (creates an independent layer)")
|
||||
if err := flags.MarkHidden("creds"); err != nil {
|
||||
panic(fmt.Sprintf("error marking creds as hidden: %v", err))
|
||||
}
|
||||
|
@ -263,6 +265,7 @@ func addAndCopyCmd(c *cobra.Command, args []string, verb string, iopts addCopyRe
|
|||
MaxRetries: iopts.retry,
|
||||
Parents: iopts.parents,
|
||||
Timestamp: timestamp,
|
||||
Link: iopts.link,
|
||||
}
|
||||
if iopts.contextdir != "" {
|
||||
var excludes []string
|
||||
|
|
|
@ -65,6 +65,13 @@ can be used.
|
|||
|
||||
Path to an alternative .containerignore (.dockerignore) file. Requires \-\-contextdir be specified.
|
||||
|
||||
**--link**
|
||||
|
||||
Create an independent image layer for the added files instead of modifying the working
|
||||
container's filesystem. If `buildah run` creates a file and `buildah add --link` adds a file
|
||||
to the same path, the file from `buildah add --link` will be present in the committed image.
|
||||
The --link layer is applied after all container filesystem changes at commit time.
|
||||
|
||||
**--quiet**, **-q**
|
||||
|
||||
Refrain from printing a digest of the added content.
|
||||
|
|
|
@ -65,6 +65,13 @@ is preserved.
|
|||
|
||||
Path to an alternative .containerignore (.dockerignore) file. Requires \-\-contextdir be specified.
|
||||
|
||||
**--link**
|
||||
|
||||
Create an independent image layer for the added files instead of modifying the working
|
||||
container's filesystem. If `buildah run` creates a file and `buildah copy --link` adds a file
|
||||
to the same path, the file from `buildah copy --link` will be present in the committed image.
|
||||
The --link layer is applied after all container filesystem changes at commit time.
|
||||
|
||||
**--parents**
|
||||
|
||||
Preserve leading directories in the paths of items being copied, relative to either the
|
||||
|
|
|
@ -76,6 +76,8 @@ type StageExecutor struct {
|
|||
stage *imagebuilder.Stage
|
||||
didExecute bool
|
||||
argsFromContainerfile []string
|
||||
hasLink bool
|
||||
isLastStep bool
|
||||
}
|
||||
|
||||
// Preserve informs the stage executor that from this point on, it needs to
|
||||
|
@ -359,8 +361,11 @@ func (s *StageExecutor) Copy(excludes []string, copies ...imagebuilder.Copy) err
|
|||
}
|
||||
return errors.New("COPY --keep-git-dir is not supported")
|
||||
}
|
||||
if cp.Link {
|
||||
return errors.New("COPY --link is not supported")
|
||||
if cp.Link && s.executor.layers {
|
||||
s.hasLink = true
|
||||
} else if cp.Link {
|
||||
s.executor.logger.Warn("--link is not supported when building without --layers, ignoring --link")
|
||||
s.hasLink = false
|
||||
}
|
||||
if len(cp.Excludes) > 0 {
|
||||
excludes = append(slices.Clone(excludes), cp.Excludes...)
|
||||
|
@ -564,6 +569,7 @@ func (s *StageExecutor) performCopy(excludes []string, copies ...imagebuilder.Co
|
|||
sources = append(sources, src)
|
||||
}
|
||||
}
|
||||
labelsAndAnnotations := s.buildMetadata(s.isLastStep, true)
|
||||
options := buildah.AddAndCopyOptions{
|
||||
Chmod: copy.Chmod,
|
||||
Chown: copy.Chown,
|
||||
|
@ -583,6 +589,8 @@ func (s *StageExecutor) performCopy(excludes []string, copies ...imagebuilder.Co
|
|||
MaxRetries: s.executor.maxPullPushRetries,
|
||||
RetryDelay: s.executor.retryPullPushDelay,
|
||||
Parents: copy.Parents,
|
||||
Link: s.hasLink,
|
||||
BuildMetadata: labelsAndAnnotations,
|
||||
}
|
||||
if len(copy.Files) > 0 {
|
||||
// If we are copying heredoc files, we need to temporary place
|
||||
|
@ -1330,6 +1338,8 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
|
|||
logRusage()
|
||||
moreInstructions := i < len(children)-1
|
||||
lastInstruction := !moreInstructions
|
||||
|
||||
s.isLastStep = lastStage && lastInstruction
|
||||
// Resolve any arguments in this instruction.
|
||||
step := ib.Step()
|
||||
if err := step.Resolve(node); err != nil {
|
||||
|
@ -1795,6 +1805,8 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
|
|||
return "", nil, false, fmt.Errorf("preparing container for next step: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
s.hasLink = false
|
||||
}
|
||||
|
||||
return imgID, ref, onlyBaseImage, nil
|
||||
|
@ -1888,30 +1900,13 @@ func (s *StageExecutor) getCreatedBy(node *parser.Node, addedContentSummary stri
|
|||
if node == nil {
|
||||
return "/bin/sh", nil
|
||||
}
|
||||
inheritLabels := ""
|
||||
unsetAnnotations := ""
|
||||
inheritAnnotations := ""
|
||||
newAnnotations := ""
|
||||
// If --inherit-label was manually set to false then update history.
|
||||
if s.executor.inheritLabels == types.OptionalBoolFalse {
|
||||
inheritLabels = "|inheritLabels=false"
|
||||
}
|
||||
if isLastStep {
|
||||
for _, annotation := range s.executor.unsetAnnotations {
|
||||
unsetAnnotations += "|unsetAnnotation=" + annotation
|
||||
}
|
||||
// If --inherit-annotation was manually set to false then update history.
|
||||
if s.executor.inheritAnnotations == types.OptionalBoolFalse {
|
||||
inheritAnnotations = "|inheritAnnotations=false"
|
||||
}
|
||||
// If new annotations are added, they must be added as part of the last step of the build,
|
||||
// so mention in history that new annotations were added inorder to make sure the builds
|
||||
// can either reuse layers or burst the cache depending upon new annotations.
|
||||
if len(s.executor.annotations) > 0 {
|
||||
newAnnotations += strings.Join(s.executor.annotations, ",")
|
||||
}
|
||||
}
|
||||
switch strings.ToUpper(node.Value) {
|
||||
|
||||
command := strings.ToUpper(node.Value)
|
||||
addcopy := command == "ADD" || command == "COPY"
|
||||
|
||||
labelsAndAnnotations := s.buildMetadata(isLastStep, addcopy)
|
||||
|
||||
switch command {
|
||||
case "ARG":
|
||||
for _, variable := range strings.Fields(node.Original) {
|
||||
if variable != "ARG" {
|
||||
|
@ -1919,7 +1914,7 @@ func (s *StageExecutor) getCreatedBy(node *parser.Node, addedContentSummary stri
|
|||
}
|
||||
}
|
||||
buildArgs := s.getBuildArgsKey()
|
||||
return "/bin/sh -c #(nop) ARG " + buildArgs + inheritLabels + unsetAnnotations + inheritAnnotations + newAnnotations, nil
|
||||
return "/bin/sh -c #(nop) ARG " + buildArgs + labelsAndAnnotations, nil
|
||||
case "RUN":
|
||||
shArg := ""
|
||||
buildArgs := s.getBuildArgsResolvedForRun()
|
||||
|
@ -1999,16 +1994,20 @@ func (s *StageExecutor) getCreatedBy(node *parser.Node, addedContentSummary stri
|
|||
if buildArgs != "" {
|
||||
result = result + "|" + strconv.Itoa(len(strings.Split(buildArgs, " "))) + " " + buildArgs + " "
|
||||
}
|
||||
result = result + "/bin/sh -c " + shArg + heredoc + appendCheckSum + inheritLabels + unsetAnnotations + inheritAnnotations + newAnnotations
|
||||
result = result + "/bin/sh -c " + shArg + heredoc + appendCheckSum + labelsAndAnnotations
|
||||
return result, nil
|
||||
case "ADD", "COPY":
|
||||
destination := node
|
||||
for destination.Next != nil {
|
||||
destination = destination.Next
|
||||
}
|
||||
return "/bin/sh -c #(nop) " + strings.ToUpper(node.Value) + " " + addedContentSummary + " in " + destination.Value + " " + inheritLabels + " " + unsetAnnotations + " " + inheritAnnotations + " " + newAnnotations, nil
|
||||
hasLink := ""
|
||||
if s.hasLink {
|
||||
hasLink = " --link"
|
||||
}
|
||||
return "/bin/sh -c #(nop) " + strings.ToUpper(node.Value) + hasLink + " " + addedContentSummary + " in " + destination.Value + " " + labelsAndAnnotations, nil
|
||||
default:
|
||||
return "/bin/sh -c #(nop) " + node.Original + inheritLabels + unsetAnnotations + inheritAnnotations + newAnnotations, nil
|
||||
return "/bin/sh -c #(nop) " + node.Original + labelsAndAnnotations, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2326,6 +2325,7 @@ func (s *StageExecutor) intermediateImageExists(ctx context.Context, currNode *p
|
|||
if s.builder.TopLayer != imageParentLayerID {
|
||||
continue
|
||||
}
|
||||
|
||||
// Next we double check that the history of this image is equivalent to the previous
|
||||
// lines in the Dockerfile up till the point we are at in the build.
|
||||
manifestType, history, diffIDs, err := s.executor.getImageTypeAndHistoryAndDiffIDs(ctx, image.ID)
|
||||
|
@ -2495,6 +2495,7 @@ func (s *StageExecutor) commit(ctx context.Context, createdBy string, emptyLayer
|
|||
Squash: squash,
|
||||
OmitHistory: s.executor.commonBuildOptions.OmitHistory,
|
||||
EmptyLayer: emptyLayer,
|
||||
OmitLayerHistoryEntry: s.hasLink,
|
||||
BlobDirectory: s.executor.blobDirectory,
|
||||
SignBy: s.executor.signBy,
|
||||
MaxRetries: s.executor.maxPullPushRetries,
|
||||
|
@ -2581,3 +2582,34 @@ func (s *StageExecutor) EnsureContainerPathAs(path, user string, mode *os.FileMo
|
|||
logrus.Debugf("EnsureContainerPath %q (owner %q, mode %o) in %q", path, user, mode, s.builder.ContainerID)
|
||||
return s.builder.EnsureContainerPathAs(path, user, mode)
|
||||
}
|
||||
|
||||
func (s *StageExecutor) buildMetadata(isLastStep bool, addcopy bool) string {
|
||||
inheritLabels := ""
|
||||
unsetAnnotations := ""
|
||||
inheritAnnotations := ""
|
||||
newAnnotations := ""
|
||||
// If --inherit-label was manually set to false then update history.
|
||||
if s.executor.inheritLabels == types.OptionalBoolFalse {
|
||||
inheritLabels = "|inheritLabels=false"
|
||||
}
|
||||
if isLastStep {
|
||||
for _, annotation := range s.executor.unsetAnnotations {
|
||||
unsetAnnotations += "|unsetAnnotation=" + annotation
|
||||
}
|
||||
// If --inherit-annotation was manually set to false then update history.
|
||||
if s.executor.inheritAnnotations == types.OptionalBoolFalse {
|
||||
inheritAnnotations = "|inheritAnnotations=false"
|
||||
}
|
||||
// If new annotations are added, they must be added as part of the last step of the build,
|
||||
// so mention in history that new annotations were added inorder to make sure the builds
|
||||
// can either reuse layers or burst the cache depending upon new annotations.
|
||||
if len(s.executor.annotations) > 0 {
|
||||
newAnnotations += strings.Join(s.executor.annotations, ",")
|
||||
}
|
||||
}
|
||||
|
||||
if addcopy {
|
||||
return inheritLabels + " " + unsetAnnotations + " " + inheritAnnotations + " " + newAnnotations
|
||||
}
|
||||
return inheritLabels + unsetAnnotations + inheritAnnotations + newAnnotations
|
||||
}
|
||||
|
|
117
tests/add.bats
117
tests/add.bats
|
@ -392,3 +392,120 @@ EOF
|
|||
run_buildah copy --chmod=0755 "$cid" $TEST_SCRATCH_DIR/context/check-dates.sh /
|
||||
run_buildah run "$cid" sh -x /check-dates.sh
|
||||
}
|
||||
|
||||
@test "add-link-flag" {
|
||||
createrandom ${TEST_SCRATCH_DIR}/randomfile
|
||||
createrandom ${TEST_SCRATCH_DIR}/other-randomfile
|
||||
|
||||
run_buildah from $WITH_POLICY_JSON scratch
|
||||
cid=$output
|
||||
run_buildah mount $cid
|
||||
root=$output
|
||||
|
||||
run_buildah config --workingdir=/ $cid
|
||||
|
||||
# Test 1: Simple add
|
||||
run_buildah add --link $cid ${TEST_SCRATCH_DIR}/randomfile
|
||||
|
||||
# Test 2: Add with rename (file to file with different name)
|
||||
run_buildah add --link $cid ${TEST_SCRATCH_DIR}/randomfile /renamed-file
|
||||
|
||||
# Test 3: Multiple files to directory
|
||||
mkdir $root/subdir
|
||||
run_buildah add --link $cid ${TEST_SCRATCH_DIR}/randomfile ${TEST_SCRATCH_DIR}/other-randomfile /subdir
|
||||
|
||||
run_buildah unmount $cid
|
||||
run_buildah commit $WITH_POLICY_JSON $cid add-link-image
|
||||
|
||||
run_buildah inspect --type=image add-link-image
|
||||
layers=$(echo "$output" | jq -r '.OCIv1.rootfs.diff_ids | length')
|
||||
if [ "$layers" -lt 3 ]; then
|
||||
echo "Expected at least 3 layers from 3 --link operations, but found $layers"
|
||||
echo "Layers found:"
|
||||
echo "$output" | jq -r '.OCIv1.rootfs.diff_ids[]'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
run_buildah from $WITH_POLICY_JSON add-link-image
|
||||
newcid=$output
|
||||
run_buildah mount $newcid
|
||||
newroot=$output
|
||||
|
||||
test -s $newroot/randomfile
|
||||
cmp ${TEST_SCRATCH_DIR}/randomfile $newroot/randomfile
|
||||
|
||||
test -s $newroot/renamed-file
|
||||
cmp ${TEST_SCRATCH_DIR}/randomfile $newroot/renamed-file
|
||||
|
||||
test -s $newroot/subdir/randomfile
|
||||
cmp ${TEST_SCRATCH_DIR}/randomfile $newroot/subdir/randomfile
|
||||
test -s $newroot/subdir/other-randomfile
|
||||
cmp ${TEST_SCRATCH_DIR}/other-randomfile $newroot/subdir/other-randomfile
|
||||
}
|
||||
|
||||
@test "add-link-archive" {
|
||||
createrandom ${TEST_SCRATCH_DIR}/file1
|
||||
createrandom ${TEST_SCRATCH_DIR}/file2
|
||||
|
||||
tar -c -C ${TEST_SCRATCH_DIR} -f ${TEST_SCRATCH_DIR}/archive.tar file1 file2
|
||||
|
||||
run_buildah from $WITH_POLICY_JSON scratch
|
||||
cid=$output
|
||||
|
||||
run_buildah config --workingdir=/ $cid
|
||||
|
||||
run_buildah add --link $cid ${TEST_SCRATCH_DIR}/archive.tar
|
||||
|
||||
run_buildah add --link $cid ${TEST_SCRATCH_DIR}/archive.tar /destdir/
|
||||
|
||||
run_buildah commit $WITH_POLICY_JSON $cid add-link-archive-image
|
||||
|
||||
run_buildah inspect --type=image add-link-archive-image
|
||||
layers=$(echo "$output" | jq -r '.OCIv1.rootfs.diff_ids | length')
|
||||
if [ "$layers" -lt 2 ]; then
|
||||
echo "Expected at least 2 layers from 2 --link operations, but found $layers"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
run_buildah from $WITH_POLICY_JSON add-link-archive-image
|
||||
newcid=$output
|
||||
run_buildah mount $newcid
|
||||
newroot=$output
|
||||
|
||||
test -s $newroot/file1
|
||||
cmp ${TEST_SCRATCH_DIR}/file1 $newroot/file1
|
||||
test -s $newroot/file2
|
||||
cmp ${TEST_SCRATCH_DIR}/file2 $newroot/file2
|
||||
|
||||
test -s $newroot/destdir/file1
|
||||
cmp ${TEST_SCRATCH_DIR}/file1 $newroot/destdir/file1
|
||||
test -s $newroot/destdir/file2
|
||||
cmp ${TEST_SCRATCH_DIR}/file2 $newroot/destdir/file2
|
||||
}
|
||||
|
||||
@test "add-link-directory" {
|
||||
mkdir -p ${TEST_SCRATCH_DIR}/testdir/subdir
|
||||
createrandom ${TEST_SCRATCH_DIR}/testdir/file1
|
||||
createrandom ${TEST_SCRATCH_DIR}/testdir/subdir/file2
|
||||
|
||||
run_buildah from $WITH_POLICY_JSON scratch
|
||||
cid=$output
|
||||
|
||||
run_buildah config --workingdir=/ $cid
|
||||
|
||||
run_buildah add --link $cid ${TEST_SCRATCH_DIR}/testdir /testdir
|
||||
|
||||
run_buildah commit $WITH_POLICY_JSON $cid add-link-dir-image
|
||||
|
||||
run_buildah from $WITH_POLICY_JSON add-link-dir-image
|
||||
newcid=$output
|
||||
run_buildah mount $newcid
|
||||
newroot=$output
|
||||
|
||||
test -d $newroot/testdir
|
||||
test -s $newroot/testdir/file1
|
||||
test -s $newroot/testdir/subdir/file2
|
||||
|
||||
cmp ${TEST_SCRATCH_DIR}/testdir/file1 $newroot/testdir/file1
|
||||
cmp ${TEST_SCRATCH_DIR}/testdir/subdir/file2 $newroot/testdir/subdir/file2
|
||||
}
|
||||
|
|
247
tests/bud.bats
247
tests/bud.bats
|
@ -8146,3 +8146,250 @@ EOF
|
|||
assert $status = 0 "error running jq"
|
||||
assert "$output" = "$datestamp" "SOURCE_DATE_EPOCH build arg didn't affect image config creation date"
|
||||
}
|
||||
|
||||
@test "bud --link consistent diffID with --no-cache" {
|
||||
_prefetch alpine
|
||||
local contextdir=${TEST_SCRATCH_DIR}/bud/link-diffid-nocache
|
||||
mkdir -p $contextdir
|
||||
|
||||
cat > $contextdir/Dockerfile << EOF
|
||||
FROM alpine
|
||||
RUN echo "before link layer" > /before.txt
|
||||
COPY --link test.txt /test.txt
|
||||
RUN echo "after link layer" > /after.txt
|
||||
RUN ls -l /test.txt
|
||||
RUN cat /test.txt
|
||||
EOF
|
||||
|
||||
echo "test content" > $contextdir/test.txt
|
||||
|
||||
run_buildah build --no-cache --layers $WITH_POLICY_JSON -t oci:${TEST_SCRATCH_DIR}/oci-layout1 $contextdir
|
||||
|
||||
run_buildah build --no-cache --layers $WITH_POLICY_JSON -t oci:${TEST_SCRATCH_DIR}/oci-layout2 $contextdir
|
||||
|
||||
diffid1=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-layout1 2)
|
||||
diffid2=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-layout2 2)
|
||||
echo $diffid1
|
||||
echo $diffid2
|
||||
|
||||
assert "$diffid1" = "$diffid2" "COPY --link should produce identical diffIDs with --no-cache"
|
||||
}
|
||||
|
||||
@test "bud --link consistent diffID with different base images" {
|
||||
_prefetch alpine busybox
|
||||
local contextdir=${TEST_SCRATCH_DIR}/bud/link-diffid-bases
|
||||
mkdir -p $contextdir
|
||||
|
||||
echo "shared content" > $contextdir/shared.txt
|
||||
|
||||
cat > $contextdir/Containerfile.alpine << EOF
|
||||
FROM alpine
|
||||
RUN echo "alpine setup" > /setup.txt
|
||||
COPY --link shared.txt /shared.txt
|
||||
RUN echo "alpine complete" > /complete.txt
|
||||
RUN cat /shared.txt
|
||||
EOF
|
||||
|
||||
cat > $contextdir/Containerfile.busybox << EOF
|
||||
FROM busybox
|
||||
RUN echo "busybox setup" > /setup.txt
|
||||
COPY --link shared.txt /shared.txt
|
||||
RUN echo "busybox complete" > /complete.txt
|
||||
RUN cat /shared.txt
|
||||
EOF
|
||||
|
||||
run_buildah build --no-cache --layers $WITH_POLICY_JSON -f $contextdir/Containerfile.alpine -t oci:${TEST_SCRATCH_DIR}/oci-layout-alpine $contextdir
|
||||
|
||||
run_buildah build --no-cache --layers $WITH_POLICY_JSON -f $contextdir/Containerfile.busybox -t oci:${TEST_SCRATCH_DIR}/oci-layout-busybox $contextdir
|
||||
|
||||
diffid_alpine=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-layout-alpine 2)
|
||||
diffid_busybox=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-layout-busybox 2)
|
||||
|
||||
assert "$diffid_alpine" = "$diffid_busybox" "COPY --link should produce identical diffIDs regardless of base image"
|
||||
}
|
||||
|
||||
@test "bud --link consistent diffID with multiple files" {
|
||||
_prefetch alpine
|
||||
local contextdir=${TEST_SCRATCH_DIR}/bud/link-diffid-multi
|
||||
mkdir -p $contextdir/subdir
|
||||
|
||||
echo "file1" > $contextdir/file1.txt
|
||||
echo "file2" > $contextdir/file2.txt
|
||||
echo "subfile" > $contextdir/subdir/sub.txt
|
||||
|
||||
cat > $contextdir/Dockerfile << EOF
|
||||
FROM alpine
|
||||
RUN echo "setup" > /setup.txt
|
||||
COPY --link file1.txt file2.txt /files/
|
||||
ADD --link subdir /subdir
|
||||
RUN echo "complete" > /complete.txt
|
||||
RUN ls -l /files/
|
||||
EOF
|
||||
|
||||
run_buildah build --no-cache --layers $WITH_POLICY_JSON -t oci:${TEST_SCRATCH_DIR}/oci-layout-multi1 $contextdir
|
||||
|
||||
run_buildah build --no-cache --layers $WITH_POLICY_JSON -t oci:${TEST_SCRATCH_DIR}/oci-layout-multi2 $contextdir
|
||||
|
||||
copy_diffid1=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-layout-multi1 2)
|
||||
copy_diffid2=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-layout-multi2 2)
|
||||
add_diffid1=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-layout-multi1 3)
|
||||
add_diffid2=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-layout-multi2 3)
|
||||
|
||||
assert "$copy_diffid1" = "$copy_diffid2" "COPY --link with multiple files should have consistent diffID"
|
||||
assert "$add_diffid1" = "$add_diffid2" "ADD --link should have consistent diffID"
|
||||
}
|
||||
|
||||
@test "bud --link with glob patterns consistent diffID" {
|
||||
_prefetch alpine
|
||||
local contextdir=${TEST_SCRATCH_DIR}/bud/link-diffid-glob
|
||||
mkdir -p $contextdir
|
||||
|
||||
echo "test1" > $contextdir/test1.txt
|
||||
echo "test2" > $contextdir/test2.txt
|
||||
echo "json1" > $contextdir/data1.json
|
||||
echo "json2" > $contextdir/data2.json
|
||||
|
||||
cat > $contextdir/Dockerfile << EOF
|
||||
FROM alpine
|
||||
COPY --link test*.txt /tests/
|
||||
COPY --link *.json /data/
|
||||
RUN echo "globbing complete" > /complete.txt
|
||||
RUN ls -l /tests/
|
||||
RUN ls -l /data/
|
||||
EOF
|
||||
|
||||
run_buildah build --no-cache --layers $WITH_POLICY_JSON -t oci:${TEST_SCRATCH_DIR}/oci-glob-1 $contextdir
|
||||
|
||||
run_buildah build --no-cache --layers $WITH_POLICY_JSON -t oci:${TEST_SCRATCH_DIR}/oci-glob-2 $contextdir
|
||||
|
||||
glob_txt_diffid1=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-glob-1 1)
|
||||
glob_txt_diffid2=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-glob-2 1)
|
||||
glob_json_diffid1=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-glob-1 2)
|
||||
glob_json_diffid2=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-glob-2 2)
|
||||
|
||||
assert "$glob_txt_diffid1" = "$glob_txt_diffid2" "COPY --link with glob *.txt should have consistent diffID"
|
||||
assert "$glob_json_diffid1" = "$glob_json_diffid2" "COPY --link with glob *.json should have consistent diffID"
|
||||
}
|
||||
|
||||
@test "bud --link cache behavior basic" {
|
||||
_prefetch alpine
|
||||
local contextdir=${TEST_SCRATCH_DIR}/bud/link-cache
|
||||
mkdir -p $contextdir
|
||||
|
||||
echo "test content" > $contextdir/testfile.txt
|
||||
|
||||
cat > $contextdir/Dockerfile << EOF
|
||||
FROM alpine
|
||||
COPY --link testfile.txt /testfile.txt
|
||||
RUN echo "build complete" > /complete.txt
|
||||
RUN cat /testfile.txt
|
||||
EOF
|
||||
|
||||
# First build
|
||||
run_buildah build --layers $WITH_POLICY_JSON -t link-cache1 $contextdir
|
||||
|
||||
run_buildah build --layers $WITH_POLICY_JSON -t link-cache2 $contextdir
|
||||
assert "$output" =~ "Using cache"
|
||||
|
||||
# Modify content
|
||||
echo "modified content" > $contextdir/testfile.txt
|
||||
|
||||
run_buildah build --layers $WITH_POLICY_JSON -t link-cache3 $contextdir
|
||||
assert "$output" !~ "STEP 2/3: COPY --link testfile.txt /testfile.txt"$'\n'".*Using cache"
|
||||
}
|
||||
|
||||
@test "bud --link with chmod and chown" {
|
||||
_prefetch alpine
|
||||
local contextdir=${TEST_SCRATCH_DIR}/bud/link-perms
|
||||
mkdir -p $contextdir
|
||||
|
||||
echo "test content" > $contextdir/testfile.txt
|
||||
|
||||
cat > $contextdir/Dockerfile << EOF
|
||||
FROM alpine
|
||||
COPY --link --chmod=755 --chown=1000:1000 testfile.txt /testfile.txt
|
||||
RUN stat -c '%u:%g %a' /testfile.txt
|
||||
EOF
|
||||
|
||||
run_buildah build --layers $WITH_POLICY_JSON -t link-perms $contextdir
|
||||
expect_output --substring "1000:1000 755"
|
||||
}
|
||||
|
||||
@test "bud --link multi-stage build cache" {
|
||||
_prefetch alpine
|
||||
local contextdir=${TEST_SCRATCH_DIR}/bud/link-multistage
|
||||
mkdir -p $contextdir
|
||||
|
||||
echo "stage1 content" > $contextdir/stage1.txt
|
||||
echo "stage2 content" > $contextdir/stage2.txt
|
||||
|
||||
cat > $contextdir/Dockerfile << EOF
|
||||
FROM alpine AS stage1
|
||||
ADD --link stage1.txt /stage1.txt
|
||||
RUN echo "stage1 complete" > /complete.txt
|
||||
RUN cat /stage1.txt
|
||||
|
||||
FROM alpine AS stage2
|
||||
COPY --from=stage1 /stage1.txt /from-stage1.txt
|
||||
ADD --link stage2.txt /stage2.txt
|
||||
RUN echo "stage2 complete" > /complete.txt
|
||||
RUN cat /stage2.txt
|
||||
|
||||
EOF
|
||||
|
||||
run_buildah build --layers $WITH_POLICY_JSON -t link-multistage1 $contextdir
|
||||
|
||||
run_buildah build --layers $WITH_POLICY_JSON -t link-multistage2 $contextdir
|
||||
assert "$output" =~ "Using cache"
|
||||
|
||||
run_buildah from --name test-ctr link-multistage2
|
||||
run_buildah run test-ctr cat /from-stage1.txt
|
||||
expect_output "stage1 content"
|
||||
run_buildah run test-ctr cat /stage2.txt
|
||||
expect_output "stage2 content"
|
||||
run_buildah rm test-ctr
|
||||
}
|
||||
|
||||
@test "bud --link ADD with remote URL consistent diffID" {
|
||||
_prefetch alpine
|
||||
local contextdir=${TEST_SCRATCH_DIR}/bud/link-url
|
||||
mkdir -p $contextdir
|
||||
|
||||
cat > $contextdir/Dockerfile << EOF
|
||||
FROM alpine
|
||||
ADD --link https://github.com/moby/moby/raw/master/README.md /README.md
|
||||
RUN echo "remote add complete" > /complete.txt
|
||||
RUN cat /README.md
|
||||
EOF
|
||||
|
||||
run_buildah build --no-cache --layers $WITH_POLICY_JSON -t oci:${TEST_SCRATCH_DIR}/oci-url1 $contextdir
|
||||
run_buildah build --no-cache --layers $WITH_POLICY_JSON -t oci:${TEST_SCRATCH_DIR}/oci-url2 $contextdir
|
||||
|
||||
diffid1=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-url1 1)
|
||||
diffid2=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-url2 1)
|
||||
|
||||
assert "$diffid1" = "$diffid2" "ADD --link with URL should have consistent diffID"
|
||||
}
|
||||
|
||||
@test "bud --link respects dockerignore" {
|
||||
_prefetch alpine
|
||||
local contextdir=${TEST_SCRATCH_DIR}/bud/link-ignore
|
||||
mkdir -p $contextdir
|
||||
|
||||
echo "included" > $contextdir/included.txt
|
||||
echo "excluded" > $contextdir/excluded.txt
|
||||
|
||||
echo "excluded.txt" > $contextdir/.dockerignore
|
||||
|
||||
cat > $contextdir/Dockerfile << EOF
|
||||
FROM alpine
|
||||
RUN echo "Starting" > /start.txt
|
||||
COPY --link *.txt /files/
|
||||
RUN echo "Ending" > /end.txt
|
||||
RUN ls -l /files/
|
||||
EOF
|
||||
|
||||
run_buildah build --layers $WITH_POLICY_JSON -t link-ignore $contextdir
|
||||
expect_output --substring "included.txt"
|
||||
assert "$output" !~ "excluded.txt"
|
||||
}
|
||||
|
|
|
@ -668,3 +668,95 @@ parents/y/b.txt"
|
|||
run_buildah run $ctr stat -c %u:%g /random-file-6
|
||||
assert 789:789
|
||||
}
|
||||
|
||||
@test "copy-link-flag" {
|
||||
createrandom ${TEST_SCRATCH_DIR}/randomfile
|
||||
createrandom ${TEST_SCRATCH_DIR}/other-randomfile
|
||||
|
||||
run_buildah from $WITH_POLICY_JSON scratch
|
||||
cid=$output
|
||||
run_buildah mount $cid
|
||||
root=$output
|
||||
|
||||
run_buildah config --workingdir=/ $cid
|
||||
run_buildah copy --link $cid ${TEST_SCRATCH_DIR}/randomfile
|
||||
|
||||
run_buildah copy --link $cid ${TEST_SCRATCH_DIR}/randomfile ${TEST_SCRATCH_DIR}/other-randomfile /subdir/
|
||||
run_buildah unmount $cid
|
||||
run_buildah commit $WITH_POLICY_JSON $cid copy-link-image
|
||||
|
||||
run_buildah inspect --type=image copy-link-image
|
||||
layers=$(echo "$output" | jq -r '.OCIv1.rootfs.diff_ids | length')
|
||||
assert "$layers" -eq 3 "Expected 3 layers from 2 --link operations and base, but found $layers"
|
||||
|
||||
run_buildah from $WITH_POLICY_JSON copy-link-image
|
||||
newcid=$output
|
||||
run_buildah mount $newcid
|
||||
newroot=$output
|
||||
|
||||
test -s $newroot/randomfile
|
||||
cmp ${TEST_SCRATCH_DIR}/randomfile $newroot/randomfile
|
||||
test -s $newroot/subdir/randomfile
|
||||
cmp ${TEST_SCRATCH_DIR}/randomfile $newroot/subdir/randomfile
|
||||
test -s $newroot/subdir/other-randomfile
|
||||
cmp ${TEST_SCRATCH_DIR}/other-randomfile $newroot/subdir/other-randomfile
|
||||
}
|
||||
|
||||
@test "copy-link-directory" {
|
||||
mkdir -p ${TEST_SCRATCH_DIR}/sourcedir
|
||||
createrandom ${TEST_SCRATCH_DIR}/sourcedir/file1
|
||||
createrandom ${TEST_SCRATCH_DIR}/sourcedir/file2
|
||||
|
||||
run_buildah from $WITH_POLICY_JSON scratch
|
||||
cid=$output
|
||||
|
||||
run_buildah config --workingdir=/ $cid
|
||||
run_buildah copy --link $cid ${TEST_SCRATCH_DIR}/sourcedir /destdir
|
||||
|
||||
run_buildah commit $WITH_POLICY_JSON $cid copy-link-dir-image
|
||||
|
||||
run_buildah from $WITH_POLICY_JSON copy-link-dir-image
|
||||
newcid=$output
|
||||
run_buildah mount $newcid
|
||||
newroot=$output
|
||||
|
||||
test -d $newroot/destdir
|
||||
test -s $newroot/destdir/file1
|
||||
cmp ${TEST_SCRATCH_DIR}/sourcedir/file1 $newroot/destdir/file1
|
||||
test -s $newroot/destdir/file2
|
||||
cmp ${TEST_SCRATCH_DIR}/sourcedir/file2 $newroot/destdir/file2
|
||||
}
|
||||
|
||||
@test "copy-link-with-chown" {
|
||||
createrandom ${TEST_SCRATCH_DIR}/randomfile
|
||||
|
||||
_prefetch busybox
|
||||
run_buildah from --quiet $WITH_POLICY_JSON busybox
|
||||
cid=$output
|
||||
|
||||
run_buildah copy --link --chown bin:bin $cid ${TEST_SCRATCH_DIR}/randomfile /tmp/random
|
||||
|
||||
run_buildah commit $WITH_POLICY_JSON $cid copy-link-chown-image
|
||||
|
||||
run_buildah from $WITH_POLICY_JSON copy-link-chown-image
|
||||
newcid=$output
|
||||
run_buildah run $newcid ls -l /tmp/random
|
||||
expect_output --substring "bin.*bin"
|
||||
}
|
||||
|
||||
@test "copy-link-with-chmod" {
|
||||
createrandom ${TEST_SCRATCH_DIR}/randomfile
|
||||
|
||||
_prefetch busybox
|
||||
run_buildah from --quiet $WITH_POLICY_JSON busybox
|
||||
cid=$output
|
||||
|
||||
run_buildah copy --link --chmod 777 $cid ${TEST_SCRATCH_DIR}/randomfile /tmp/random
|
||||
|
||||
run_buildah commit $WITH_POLICY_JSON $cid copy-link-chmod-image
|
||||
|
||||
run_buildah from $WITH_POLICY_JSON copy-link-chmod-image
|
||||
newcid=$output
|
||||
run_buildah run $newcid ls -l /tmp/random
|
||||
expect_output --substring "rwxrwxrwx"
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue