diff --git a/add.go b/add.go index 3e7a8f3937..fa06eddce5 100644 --- a/add.go +++ b/add.go @@ -112,6 +112,9 @@ type AddAndCopyOptions struct { // inheritAnnotations, newAnnotations). This field is internally managed and should // not be set by external API users. BuildMetadata string + // FollowSymlink controls whether symlinks should be followed when copying content. + // When set to false, symlinks are not dereferenced. + FollowSymlink types.OptionalBool } // gitURLFragmentSuffix matches fragments to use as Git reference and build @@ -610,18 +613,19 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption return } getOptions := copier.GetOptions{ - UIDMap: srcUIDMap, - GIDMap: srcGIDMap, - Excludes: options.Excludes, - ExpandArchives: extract, - ChownDirs: chownDirs, - ChmodDirs: chmodDirsFiles, - ChownFiles: chownFiles, - ChmodFiles: chmodDirsFiles, - StripSetuidBit: options.StripSetuidBit, - StripSetgidBit: options.StripSetgidBit, - StripStickyBit: options.StripStickyBit, - Timestamp: options.Timestamp, + UIDMap: srcUIDMap, + GIDMap: srcGIDMap, + Excludes: options.Excludes, + ExpandArchives: extract, + ChownDirs: chownDirs, + ChmodDirs: chmodDirsFiles, + ChownFiles: chownFiles, + ChmodFiles: chmodDirsFiles, + StripSetuidBit: options.StripSetuidBit, + StripSetgidBit: options.StripSetgidBit, + StripStickyBit: options.StripStickyBit, + NoDerefSymlinks: options.FollowSymlink == types.OptionalBoolFalse, + Timestamp: options.Timestamp, } writer := io.WriteCloser(pipeWriter) repositoryDir := filepath.Join(cloneDir, subdir) @@ -777,19 +781,20 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption return false, false, nil }) getOptions := copier.GetOptions{ - UIDMap: srcUIDMap, - GIDMap: srcGIDMap, - Excludes: options.Excludes, - ExpandArchives: extract, - ChownDirs: chownDirs, - ChmodDirs: chmodDirsFiles, - ChownFiles: chownFiles, - ChmodFiles: chmodDirsFiles, - StripSetuidBit: options.StripSetuidBit, - StripSetgidBit: options.StripSetgidBit, - StripStickyBit: options.StripStickyBit, - Parents: options.Parents, - Timestamp: options.Timestamp, + UIDMap: srcUIDMap, + GIDMap: srcGIDMap, + Excludes: options.Excludes, + ExpandArchives: extract, + ChownDirs: chownDirs, + ChmodDirs: chmodDirsFiles, + ChownFiles: chownFiles, + ChmodFiles: chmodDirsFiles, + StripSetuidBit: options.StripSetuidBit, + StripSetgidBit: options.StripSetgidBit, + StripStickyBit: options.StripStickyBit, + Parents: options.Parents, + NoDerefSymlinks: options.FollowSymlink == types.OptionalBoolFalse, + Timestamp: options.Timestamp, } getErr = copier.Get(contextDir, contextDir, getOptions, []string{globbedToGlobbable(globbed)}, writer) closeErr = writer.Close() diff --git a/cmd/buildah/addcopy.go b/cmd/buildah/addcopy.go index cfa8ebf870..5e5da38053 100644 --- a/cmd/buildah/addcopy.go +++ b/cmd/buildah/addcopy.go @@ -16,6 +16,7 @@ import ( "go.podman.io/buildah/pkg/cli" "go.podman.io/buildah/pkg/parse" "go.podman.io/common/pkg/auth" + "go.podman.io/image/v5/types" "go.podman.io/storage" ) @@ -42,6 +43,7 @@ type addCopyResults struct { parents bool timestamp string link bool + noFollowSymlinks bool } func createCommand(addCopy string, desc string, short string, opts *addCopyResults) *cobra.Command { @@ -77,6 +79,7 @@ func applyFlagVars(flags *pflag.FlagSet, opts *addCopyResults) { 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)") + flags.BoolVar(&opts.noFollowSymlinks, "no-follow-symlinks", false, "do not follow symlinks when copying content (copy the symlink itself)") if err := flags.MarkHidden("creds"); err != nil { panic(fmt.Sprintf("error marking creds as hidden: %v", err)) } @@ -251,6 +254,11 @@ func addAndCopyCmd(c *cobra.Command, args []string, verb string, iopts addCopyRe timestamp = &t } + followSymlink := types.OptionalBoolUndefined + if iopts.noFollowSymlinks { + followSymlink = types.OptionalBoolFalse + } + options := buildah.AddAndCopyOptions{ Chmod: iopts.chmod, Chown: iopts.chown, @@ -267,6 +275,7 @@ func addAndCopyCmd(c *cobra.Command, args []string, verb string, iopts addCopyRe Parents: iopts.parents, Timestamp: timestamp, Link: iopts.link, + FollowSymlink: followSymlink, } if iopts.contextdir != "" { var excludes []string diff --git a/docs/buildah-add.1.md b/docs/buildah-add.1.md index e13e3040db..cad623cf71 100644 --- a/docs/buildah-add.1.md +++ b/docs/buildah-add.1.md @@ -74,6 +74,11 @@ container's filesystem. If `buildah run` creates a file and `buildah add --link` 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. +**--no-follow-symlinks** + +When a local source is a symbolic link, copy the link as a symbolic link +instead of dereferencing the link and copying the contents of the target. + **--quiet**, **-q** Refrain from printing a digest of the added content. diff --git a/docs/buildah-copy.1.md b/docs/buildah-copy.1.md index 834aeab9b6..6383031a27 100644 --- a/docs/buildah-copy.1.md +++ b/docs/buildah-copy.1.md @@ -74,6 +74,11 @@ container's filesystem. If `buildah run` creates a file and `buildah copy --link 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. +**--no-follow-symlinks** + +Don't follow and dereference the symlinks when copying the files. Instead, copy +the symlinks themselves. + **--parents** Preserve leading directories in the paths of items being copied, relative to either the diff --git a/tests/add.bats b/tests/add.bats index 6dd2600b73..347ad535e2 100644 --- a/tests/add.bats +++ b/tests/add.bats @@ -509,3 +509,72 @@ EOF cmp ${TEST_SCRATCH_DIR}/testdir/file1 $newroot/testdir/file1 cmp ${TEST_SCRATCH_DIR}/testdir/subdir/file2 $newroot/testdir/subdir/file2 } + +@test "add-symlink-root-follow-default" { + createrandom ${TEST_SCRATCH_DIR}/file + ln -s ./file ${TEST_SCRATCH_DIR}/symlink + + run_buildah from $WITH_POLICY_JSON scratch + cid=$output + + run_buildah add $cid ${TEST_SCRATCH_DIR}/symlink /dest + + run_buildah mount $cid + root=$output + ls -lahR $root + cmp ${TEST_SCRATCH_DIR}/file $root/dest + test -f $root/dest +} + +@test "add-symlink-root-no-follow" { + createrandom ${TEST_SCRATCH_DIR}/file + ln -s ./file ${TEST_SCRATCH_DIR}/symlink + + run_buildah from $WITH_POLICY_JSON scratch + cid=$output + + # The symlink needs to point to something existing + run_buildah add --no-follow-symlinks $cid ${TEST_SCRATCH_DIR}/file /file + run_buildah add --no-follow-symlinks $cid ${TEST_SCRATCH_DIR}/symlink /dest + + run_buildah mount $cid + root=$output + ls -lahR $root + cmp ${TEST_SCRATCH_DIR}/file $root/dest + test -L $root/dest + test "$(readlink $root/dest)" = "./file" +} + +@test "add-symlink-child-follow-default" { + mkdir ${TEST_SCRATCH_DIR}/src + createrandom ${TEST_SCRATCH_DIR}/src/file + ln -s ./file ${TEST_SCRATCH_DIR}/src/symlink + + run_buildah from $WITH_POLICY_JSON scratch + cid=$output + + run_buildah add $cid ${TEST_SCRATCH_DIR}/src /dest + + run_buildah mount $cid + root=$output + ls -lahR $root + cmp ${TEST_SCRATCH_DIR}/src/file $root/dest/symlink + test -f $root/dest/symlink +} + +@test "add-symlink-child-no-follow" { + mkdir ${TEST_SCRATCH_DIR}/src + createrandom ${TEST_SCRATCH_DIR}/src/file + ln -s ./file ${TEST_SCRATCH_DIR}/src/symlink + + run_buildah from $WITH_POLICY_JSON scratch + cid=$output + + run_buildah add --no-follow-symlinks $cid ${TEST_SCRATCH_DIR}/src /dest + + run_buildah mount $cid + root=$output + ls -lahR $root + test -L $root/dest/symlink + test "$(readlink $root/dest/symlink)" = "./file" +}