mirror of
https://github.com/docker/cli.git
synced 2026-08-25 02:24:25 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7dcaa6fdb | ||
|
|
400b45f682 | ||
|
|
38887ec4ed | ||
|
|
904aef7f69 | ||
|
|
f08e60eb09 | ||
|
|
abfd89157d | ||
|
|
519eb45d03 | ||
|
|
8717af7168 | ||
|
|
e9452d6e78 | ||
|
|
a6014a702b | ||
|
|
2465fca604 | ||
|
|
52b15cc571 | ||
|
|
13590921a4 | ||
|
|
0b50545471 |
@@ -11,7 +11,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
review:
|
||||
uses: docker/docker-agent-action/.github/workflows/review-pr.yml@774b6e0e60d6c648b0f2dc43bd5221377a0a7420 # v2.0.2
|
||||
uses: docker/docker-agent-action/.github/workflows/review-pr.yml@baf90543d81f5de59751dfd10e6cf45e21a5a982 # v2.0.3
|
||||
permissions:
|
||||
contents: read # Read repository files and PR diffs
|
||||
pull-requests: write # Post review comments
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -687,15 +688,13 @@ func (options *serviceOptions) makeEnv() ([]string, error) {
|
||||
}
|
||||
currentEnv := make([]string, 0, len(envVariables))
|
||||
for _, env := range envVariables { // need to process each var, in order
|
||||
k, _, _ := strings.Cut(env, "=")
|
||||
for i, current := range currentEnv { // remove duplicates
|
||||
if current == env {
|
||||
continue // no update required, may hide this behind flag to preserve order of envVariables
|
||||
}
|
||||
if strings.HasPrefix(current, k+"=") {
|
||||
currentEnv = append(currentEnv[:i], currentEnv[i+1:]...)
|
||||
}
|
||||
if slices.Contains(currentEnv, env) {
|
||||
continue // no update required, may hide this behind flag to preserve order of envVariables
|
||||
}
|
||||
k, _, _ := strings.Cut(env, "=")
|
||||
currentEnv = slices.DeleteFunc(currentEnv, func(current string) bool { // remove duplicates
|
||||
return strings.HasPrefix(current, k+"=")
|
||||
})
|
||||
currentEnv = append(currentEnv, env)
|
||||
}
|
||||
|
||||
|
||||
@@ -373,3 +373,49 @@ func TestToServiceSysCtls(t *testing.T) {
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(service.TaskTemplate.ContainerSpec.Sysctls, expected))
|
||||
}
|
||||
|
||||
func TestMakeEnv(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
env []string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
doc: "no duplicates",
|
||||
env: []string{"one=1", "two=2"},
|
||||
expected: []string{"one=1", "two=2"},
|
||||
},
|
||||
{
|
||||
doc: "same variable repeated",
|
||||
env: []string{"one=1", "one=1"},
|
||||
expected: []string{"one=1"},
|
||||
},
|
||||
{
|
||||
doc: "same variable repeated, then overridden",
|
||||
env: []string{"one=1", "one=1", "one=2"},
|
||||
expected: []string{"one=2"},
|
||||
},
|
||||
{
|
||||
doc: "repeated variable last",
|
||||
env: []string{"one=1", "two=2", "two=2"},
|
||||
expected: []string{"one=1", "two=2"},
|
||||
},
|
||||
{
|
||||
doc: "last value wins",
|
||||
env: []string{"one=1", "two=2", "one=3"},
|
||||
expected: []string{"two=2", "one=3"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
o := newServiceOptions()
|
||||
for _, env := range tc.env {
|
||||
assert.NilError(t, o.env.Set(env))
|
||||
}
|
||||
actual, err := o.makeEnv()
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(tc.expected, actual))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1213,11 +1213,9 @@ func updateHosts(flags *pflag.FlagSet, hosts *[]string) error {
|
||||
if rm.IPAddr != "" && rm.IPAddr != ip {
|
||||
continue
|
||||
}
|
||||
for i, h := range hostNames {
|
||||
if h == rm.Host {
|
||||
hostNames = append(hostNames[:i], hostNames[i+1:]...)
|
||||
}
|
||||
}
|
||||
hostNames = slices.DeleteFunc(hostNames, func(h string) bool {
|
||||
return h == rm.Host
|
||||
})
|
||||
}
|
||||
if len(hostNames) > 0 {
|
||||
newHosts = append(newHosts, fmt.Sprintf("%s %s", ip, strings.Join(hostNames, " ")))
|
||||
|
||||
@@ -1727,3 +1727,18 @@ func TestUpdateUlimits(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateHostsRemoveRepeatedHost(t *testing.T) {
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
flags.Set("host-rm", "host1")
|
||||
|
||||
//nolint:dupword // ignore "Duplicate words (host1) found"
|
||||
hosts := []string{"127.0.0.1 host1 host1 host2", "127.0.0.2 host2 host1 host1"}
|
||||
|
||||
err := updateHosts(flags, &hosts)
|
||||
assert.NilError(t, err)
|
||||
|
||||
// All occurrences of `host1` should be removed, also if the same host
|
||||
// is listed multiple times in the same entry.
|
||||
assert.Check(t, is.DeepEqual([]string{"127.0.0.1 host2", "127.0.0.2 host2"}, hosts))
|
||||
}
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ require (
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mattn/go-runewidth v0.0.24
|
||||
github.com/moby/go-archive v0.3.0
|
||||
github.com/moby/go-archive v0.3.3
|
||||
github.com/moby/moby/api v1.55.0
|
||||
github.com/moby/moby/client v0.5.1
|
||||
github.com/moby/patternmatcher v0.6.1
|
||||
|
||||
+6
-2
@@ -107,8 +107,8 @@ github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhg
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.3.0 h1:nos4BtzzUIqB406BgQnWGMI4qib9BZ8XUHU+ucv/n1c=
|
||||
github.com/moby/go-archive v0.3.0/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
|
||||
github.com/moby/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME=
|
||||
github.com/moby/go-archive v0.3.3/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
|
||||
github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
|
||||
github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw=
|
||||
@@ -121,6 +121,10 @@ github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk=
|
||||
github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I=
|
||||
github.com/moby/sys/mount v0.3.5 h1:eS3fsZTjHaBihwjp4/+5Z3jxqLXYsbwxqpVSfFv3M00=
|
||||
github.com/moby/sys/mount v0.3.5/go.mod h1:WUQDO+/uCiCIkIztx8SrwIDVn2dtMFRBebRhpDFT71M=
|
||||
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
|
||||
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
|
||||
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
|
||||
github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o=
|
||||
github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0=
|
||||
|
||||
+183
-78
@@ -17,6 +17,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/containerd/log"
|
||||
"github.com/moby/go-archive/internal/archiveoptions"
|
||||
"github.com/moby/patternmatcher"
|
||||
"github.com/moby/sys/sequential"
|
||||
"github.com/moby/sys/user"
|
||||
@@ -81,9 +82,22 @@ type (
|
||||
// were probably in the archive for a reason, so set this option at
|
||||
// your own peril.
|
||||
BestEffortXattrs bool
|
||||
|
||||
// internalOptions contains options for use by packages within this module.
|
||||
internalOptions *archiveoptions.Options
|
||||
}
|
||||
)
|
||||
|
||||
// WithProcSelfFD returns a copy of opts prepared for extraction in a
|
||||
// filesystem context where /proc/self/fd may not be accessible by path.
|
||||
//
|
||||
// The caller must invoke the returned cleanup function after extraction
|
||||
// completes. On platforms that do not use /proc/self/fd for extraction,
|
||||
// the returned cleanup function is a no-op.
|
||||
func WithProcSelfFD(opts *TarOptions) (*TarOptions, func(), error) {
|
||||
return withProcSelfFD(opts)
|
||||
}
|
||||
|
||||
// Archiver implements the Archiver interface and allows the reuse of most utility functions of
|
||||
// this package with a pluggable Untar function. Also, to facilitate the passing of specific id
|
||||
// mappings for untar, an Archiver can be created with maps which will then be passed to Untar operations.
|
||||
@@ -439,6 +453,90 @@ func (ta *tarAppender) addTarFile(srcPath, archivePath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveArchivePath resolves intermediate symlinks in name using chroot-like
|
||||
// semantics when os.Root cannot traverse them. The final path component is
|
||||
// intentionally preserved because archive extraction may create or replace it.
|
||||
//
|
||||
// This is a compatibility workaround rather than the preferred long-term
|
||||
// implementation. It resolves the path separately before the actual operation,
|
||||
// so a concurrent filesystem change may cause the operation to affect a
|
||||
// different path within root. The subsequent os.Root operation still confines
|
||||
// the operation to root and prevents such a change from escaping it.
|
||||
//
|
||||
// Paths with missing components are supported. Existing symlinks are resolved,
|
||||
// and any remaining nonexistent components are retained for later creation.
|
||||
//
|
||||
// This helper should eventually be replaced by handle-relative resolution and
|
||||
// operations with resolve-in-root semantics, avoiding the resolution/use race
|
||||
// and repeated path traversal.
|
||||
func resolveArchivePath(root *os.Root, name string) (string, error) {
|
||||
parent, base := filepath.Split(name)
|
||||
if parent == "" {
|
||||
return name, nil
|
||||
}
|
||||
|
||||
parent = filepath.Clean(parent)
|
||||
|
||||
// Follow the final parent component: it is an intermediate component of name,
|
||||
// and an absolute symlink there must trigger the resolve-in-root fallback.
|
||||
_, statErr := root.Stat(parent)
|
||||
switch {
|
||||
case statErr == nil:
|
||||
return name, nil
|
||||
case !os.IsNotExist(statErr) && !isPathEscapes(statErr):
|
||||
return "", statErr
|
||||
}
|
||||
|
||||
// Resolve the parent both to handle ENOENT from missing components or dangling
|
||||
// symlinks, and to determine whether an os.Root breakout was caused by an
|
||||
// absolute symlink. Relative symlink escapes preserve the original Stat error.
|
||||
resolved, err := resolveFSRootPath(root.Name(), parent)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if isPathEscapes(statErr) && (!resolved.followedAbsoluteLink || resolved.relativeEscapeBeforeAbsolute) {
|
||||
return "", statErr
|
||||
}
|
||||
|
||||
relParent, err := filepath.Rel(root.Name(), resolved.path)
|
||||
if err != nil {
|
||||
return "", breakoutError(fmt.Errorf(
|
||||
"could not make resolved parent %q relative to root %q: %w",
|
||||
resolved.path,
|
||||
root.Name(),
|
||||
err,
|
||||
))
|
||||
}
|
||||
if relParent != "." && !filepath.IsLocal(relParent) {
|
||||
return "", breakoutError(fmt.Errorf(
|
||||
"resolved parent %q escapes root %q",
|
||||
resolved.path,
|
||||
root.Name(),
|
||||
))
|
||||
}
|
||||
|
||||
return filepath.Join(relParent, base), nil
|
||||
}
|
||||
|
||||
// resolveHardlinkTarget validates a POSIX hardlink target and resolves it to
|
||||
// the native, root-relative filesystem path used for extraction.
|
||||
func resolveHardlinkTarget(root *os.Root, linkname string) (string, error) {
|
||||
cleaned := path.Clean(linkname)
|
||||
if strings.HasPrefix(cleaned, "/") {
|
||||
// Some image builders (e.g. kaniko) write hardlink targets as absolute
|
||||
// paths. Resolve those relative to the extraction root, with chroot-like
|
||||
// semantics matching absolute symlink targets. Strip the root from the
|
||||
// original linkname rather than the cleaned one so that ".." components
|
||||
// are not collapsed against "/" but instead rejected below.
|
||||
cleaned = path.Clean(strings.TrimLeft(linkname, "/"))
|
||||
}
|
||||
if cleaned == "." || !filepath.IsLocal(cleaned) {
|
||||
return "", breakoutError(fmt.Errorf("invalid hardlink target %q", linkname))
|
||||
}
|
||||
return resolveArchivePath(root, filepath.FromSlash(cleaned))
|
||||
}
|
||||
|
||||
// createTarFile extracts a single tar entry into the given root. dstPath is the
|
||||
// root-relative path of the entry being extracted, in native (host-separator)
|
||||
// form so it can be passed directly to os.Root methods and fsRootPath.
|
||||
@@ -447,6 +545,7 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
|
||||
Lchown = true
|
||||
inUserns, bestEffortXattrs bool
|
||||
chownOpts *ChownOpts
|
||||
internalOpts *archiveoptions.Options
|
||||
)
|
||||
|
||||
// TODO(thaJeztah): make opts a required argument.
|
||||
@@ -455,6 +554,7 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
|
||||
inUserns = opts.InUserNS // TODO(thaJeztah): consider deprecating opts.InUserNS and detect locally.
|
||||
chownOpts = opts.ChownOpts
|
||||
bestEffortXattrs = opts.BestEffortXattrs
|
||||
internalOpts = opts.internalOptions
|
||||
}
|
||||
|
||||
// hdr.Mode is in linux format, which we can use for sycalls,
|
||||
@@ -462,6 +562,15 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
|
||||
// so use hdrInfo.Mode() (they differ for e.g. setuid bits)
|
||||
hdrInfo := hdr.FileInfo()
|
||||
|
||||
var hardlinkTarget string
|
||||
if hdr.Typeflag == tar.TypeLink {
|
||||
var err error
|
||||
hardlinkTarget, err = resolveHardlinkTarget(root, hdr.Linkname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
switch hdr.Typeflag {
|
||||
case tar.TypeDir:
|
||||
// Create directory unless it already exists as one; merge in that case.
|
||||
@@ -511,13 +620,7 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
|
||||
}
|
||||
|
||||
case tar.TypeLink:
|
||||
// Defence in depth: root.Link's containment is limited when
|
||||
// dest is a volume root.
|
||||
linkname := path.Clean(hdr.Linkname)
|
||||
if linkname == "." || !filepath.IsLocal(linkname) {
|
||||
return breakoutError(fmt.Errorf("invalid hardlink target %q", hdr.Linkname))
|
||||
}
|
||||
if err := root.Link(filepath.FromSlash(linkname), dstPath); err != nil {
|
||||
if err := root.Link(hardlinkTarget, dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -593,7 +696,7 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
|
||||
|
||||
// There is no LChmod, so ignore mode for symlink. Also, this
|
||||
// must happen after chown, as that can modify the file mode
|
||||
if err := handleLChmod(root, dstPath, hdr, hdrInfo); err != nil {
|
||||
if err := handleLChmod(root, dstPath, hardlinkTarget, hdr, hdrInfo, internalOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -608,7 +711,7 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
|
||||
}
|
||||
case tar.TypeLink:
|
||||
// Follow the hardlink only when its target is not itself a symlink.
|
||||
fi, err := root.Lstat(filepath.FromSlash(path.Clean(hdr.Linkname)))
|
||||
fi, err := root.Lstat(hardlinkTarget)
|
||||
if err == nil && fi.Mode()&os.ModeSymlink == 0 {
|
||||
if err := chtimes(root, dstPath, aTime, mTime); err != nil {
|
||||
return err
|
||||
@@ -698,13 +801,13 @@ func (t *Tarballer) Do() {
|
||||
|
||||
defer func() {
|
||||
// Make sure to check the error on Close.
|
||||
if err := ta.TarWriter.Close(); err != nil {
|
||||
if err := ta.TarWriter.Close(); err != nil && !errors.Is(err, io.ErrClosedPipe) {
|
||||
log.G(context.TODO()).Errorf("Can't close tar writer: %s", err)
|
||||
}
|
||||
if err := t.compressWriter.Close(); err != nil {
|
||||
if err := t.compressWriter.Close(); err != nil && !errors.Is(err, io.ErrClosedPipe) {
|
||||
log.G(context.TODO()).Errorf("Can't close compress writer: %s", err)
|
||||
}
|
||||
if err := t.pipeWriter.Close(); err != nil {
|
||||
if err := t.pipeWriter.Close(); err != nil && !errors.Is(err, io.ErrClosedPipe) {
|
||||
log.G(context.TODO()).Errorf("Can't close pipe writer: %s", err)
|
||||
}
|
||||
}()
|
||||
@@ -931,7 +1034,10 @@ loop:
|
||||
// dstPath is the native (host-separator) form of the entry name,
|
||||
// used at all filesystem boundaries (os.Root methods, fsRootPath).
|
||||
// hdr.Name stays POSIX (forward-slash) for logical string checks.
|
||||
dstPath := filepath.FromSlash(hdr.Name)
|
||||
dstPath, err := resolveArchivePath(root, filepath.FromSlash(hdr.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If dstPath exists we almost always just want to remove and replace it.
|
||||
// The only exception is when it is a directory *and* the file from
|
||||
@@ -969,7 +1075,7 @@ loop:
|
||||
//
|
||||
// This must be done before whiteoutConverter.ConvertRead, which
|
||||
// may set xattrs on the directory or create whiteout files.
|
||||
if err := createImpliedDirectories(root, hdr, options); err != nil {
|
||||
if err := createImpliedDirectories(root, dstPath, options); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1024,81 +1130,80 @@ func unrepresentableOnWindows(hdr *tar.Header) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// createImpliedDirectories will create all parent directories of the current path with default permissions, if they do
|
||||
// not already exist. This is possible as the tar format supports 'implicit' directories, where their existence is
|
||||
// defined by the paths of files in the tar, but there are no header entries for the directories themselves, and thus
|
||||
// we most both create them and choose metadata like permissions.
|
||||
// createImpliedDirectories creates all parent directories of dstPath with
|
||||
// default permissions if they do not already exist. This is necessary because
|
||||
// the tar format permits implicit directories whose existence is defined only
|
||||
// by file paths, without corresponding directory headers from which metadata
|
||||
// could be restored.
|
||||
//
|
||||
// The caller must have normalized hdr.Name (no leading ".." components).
|
||||
// All directory creation is performed via root so it is bounded within the
|
||||
// destination at the OS level (openat(2) semantics), preventing escape via
|
||||
// symlinks in the destination tree.
|
||||
func createImpliedDirectories(root *os.Root, hdr *tar.Header, options *TarOptions) error {
|
||||
// For non-directory entries, ensure that the parent directory exists.
|
||||
if hdr.Typeflag != tar.TypeDir {
|
||||
parent := filepath.FromSlash(path.Dir(strings.TrimSuffix(hdr.Name, "/")))
|
||||
// Skip when the parent is the root itself; nothing to create.
|
||||
if parent == "." || parent == "" {
|
||||
return nil
|
||||
}
|
||||
if _, err := root.Lstat(parent); err == nil {
|
||||
return nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
// RootPair() is confined inside this loop as most cases will not require a call, so we can spend some
|
||||
// unneeded function calls in the uncommon case to encapsulate logic -- implied directories are a niche
|
||||
// usage that reduces the portability of an image.
|
||||
uid, gid := options.IDMap.RootPair()
|
||||
// The caller must pass a normalized, root-relative local path. Any archive-path
|
||||
// conversion and resolve-in-root handling must already have been applied.
|
||||
// Directory creation is performed through root, so it remains confined to the
|
||||
// extraction destination even if the destination tree changes concurrently.
|
||||
func createImpliedDirectories(root *os.Root, dstPath string, options *TarOptions) error {
|
||||
parent := filepath.Dir(dstPath)
|
||||
|
||||
// Similar to [user.MkdirAllAndChown]
|
||||
//
|
||||
// [user.MkdirAllAndChown]: https://pkg.go.dev/github.com/moby/sys/user#MkdirAllAndChown
|
||||
var cur string
|
||||
for c := range strings.SplitSeq(parent, string(os.PathSeparator)) {
|
||||
if c == "" {
|
||||
continue
|
||||
}
|
||||
cur = filepath.Join(cur, c)
|
||||
if err := root.Mkdir(cur, ImpliedDirectoryMode); err != nil {
|
||||
if !errors.Is(err, os.ErrExist) {
|
||||
return err
|
||||
}
|
||||
// Skip when the parent is the root itself; nothing to create.
|
||||
if parent == "." || parent == "" {
|
||||
return nil
|
||||
}
|
||||
if _, err := root.Lstat(parent); err == nil {
|
||||
return nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
// RootPair() is confined inside this loop as most cases will not require a call, so we can spend some
|
||||
// unneeded function calls in the uncommon case to encapsulate logic -- implied directories are a niche
|
||||
// usage that reduces the portability of an image.
|
||||
uid, gid := options.IDMap.RootPair()
|
||||
|
||||
fi, err := root.Stat(cur)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
continue
|
||||
}
|
||||
return &os.PathError{Op: "mkdir", Path: cur, Err: syscall.ENOTDIR}
|
||||
// Similar to [user.MkdirAllAndChown]
|
||||
//
|
||||
// [user.MkdirAllAndChown]: https://pkg.go.dev/github.com/moby/sys/user#MkdirAllAndChown
|
||||
var cur string
|
||||
for c := range strings.SplitSeq(parent, string(os.PathSeparator)) {
|
||||
if c == "" {
|
||||
continue
|
||||
}
|
||||
cur = filepath.Join(cur, c)
|
||||
if err := root.Mkdir(cur, ImpliedDirectoryMode); err != nil {
|
||||
if !errors.Is(err, os.ErrExist) {
|
||||
return err
|
||||
}
|
||||
if options.NoLchown {
|
||||
continue
|
||||
}
|
||||
// Only the successful Mkdir case is newly-created.
|
||||
dir, err := root.Open(cur)
|
||||
|
||||
fi, err := root.Stat(cur)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if uid != 0 || gid != 0 {
|
||||
if err := dir.Chown(uid, gid); err != nil {
|
||||
_ = dir.Close()
|
||||
return err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
continue
|
||||
}
|
||||
// root.Mkdir applies the mode subject to the process umask, so
|
||||
// re-apply it with Chmod to guarantee ImpliedDirectoryMode
|
||||
// independent of umask, matching the previous MkdirAllAndChown
|
||||
// behavior.
|
||||
if err := dir.Chmod(ImpliedDirectoryMode); err != nil {
|
||||
return &os.PathError{Op: "mkdir", Path: cur, Err: syscall.ENOTDIR}
|
||||
}
|
||||
if options.NoLchown {
|
||||
continue
|
||||
}
|
||||
// Only the successful Mkdir case is newly-created.
|
||||
dir, err := root.Open(cur)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if uid != 0 || gid != 0 {
|
||||
if err := dir.Chown(uid, gid); err != nil {
|
||||
_ = dir.Close()
|
||||
return err
|
||||
}
|
||||
if err := dir.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// root.Mkdir applies the mode subject to the process umask, so
|
||||
// re-apply it with Chmod to guarantee ImpliedDirectoryMode
|
||||
// independent of umask, matching the previous MkdirAllAndChown
|
||||
// behavior.
|
||||
if err := dir.Chmod(ImpliedDirectoryMode); err != nil {
|
||||
_ = dir.Close()
|
||||
return err
|
||||
}
|
||||
if err := dir.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
@@ -8,10 +8,28 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/moby/go-archive/internal/archiveoptions"
|
||||
"github.com/moby/sys/userns"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func withProcSelfFD(opts *TarOptions) (*TarOptions, func(), error) {
|
||||
procSelfFD, err := os.Open("/proc/self/fd")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var prepared TarOptions
|
||||
if opts != nil {
|
||||
prepared = *opts
|
||||
}
|
||||
prepared.internalOptions = &archiveoptions.Options{
|
||||
ProcSelfFD: procSelfFD,
|
||||
}
|
||||
|
||||
return &prepared, func() { _ = procSelfFD.Close() }, nil
|
||||
}
|
||||
|
||||
func getWhiteoutConverter(format WhiteoutFormat) tarWhiteoutConverter {
|
||||
if format == OverlayWhiteoutFormat {
|
||||
return newOverlayWhiteoutConverter()
|
||||
|
||||
+8
@@ -2,6 +2,14 @@
|
||||
|
||||
package archive
|
||||
|
||||
func withProcSelfFD(opts *TarOptions) (*TarOptions, func(), error) {
|
||||
var prepared TarOptions
|
||||
if opts != nil {
|
||||
prepared = *opts
|
||||
}
|
||||
return &prepared, func() {}, nil
|
||||
}
|
||||
|
||||
func getWhiteoutConverter(format WhiteoutFormat) tarWhiteoutConverter {
|
||||
return nil
|
||||
}
|
||||
|
||||
+7
-19
@@ -8,11 +8,11 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/moby/go-archive/internal/archiveoptions"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
@@ -88,7 +88,7 @@ func handleTarTypeBlockCharFifo(root *os.Root, hdr *tar.Header, dstPath string)
|
||||
// handleLChmod applies the mode from hdrInfo to dstPath within root, skipping
|
||||
// symlinks (there is no lchmod). For hardlinks, the mode is applied only when
|
||||
// the link target is itself not a symlink.
|
||||
func handleLChmod(root *os.Root, dstPath string, hdr *tar.Header, hdrInfo os.FileInfo) error {
|
||||
func handleLChmod(root *os.Root, dstPath string, hardlinkTarget string, hdr *tar.Header, hdrInfo os.FileInfo, opts *archiveoptions.Options) error {
|
||||
switch hdr.Typeflag {
|
||||
case tar.TypeSymlink:
|
||||
return nil
|
||||
@@ -96,21 +96,21 @@ func handleLChmod(root *os.Root, dstPath string, hdr *tar.Header, hdrInfo os.Fil
|
||||
case tar.TypeLink:
|
||||
// If the target is a symlink, there is no way to chmod the hardlink
|
||||
// without following it.
|
||||
fi, err := root.Lstat(filepath.FromSlash(path.Clean(hdr.Linkname)))
|
||||
fi, err := root.Lstat(hardlinkTarget)
|
||||
if err != nil || fi.Mode()&os.ModeSymlink != 0 {
|
||||
return nil
|
||||
}
|
||||
return chmodNoSymlink(root, dstPath, hdrInfo.Mode())
|
||||
return chmodNoSymlink(root, dstPath, hdrInfo.Mode(), opts)
|
||||
|
||||
default:
|
||||
return chmodNoSymlink(root, dstPath, hdrInfo.Mode())
|
||||
return chmodNoSymlink(root, dstPath, hdrInfo.Mode(), opts)
|
||||
}
|
||||
}
|
||||
|
||||
// chmodNoSymlink applies mode to a non-symlink entry.
|
||||
//
|
||||
// Callers must have already excluded symlink entries.
|
||||
func chmodNoSymlink(root *os.Root, name string, mode os.FileMode) error {
|
||||
func chmodNoSymlink(root *os.Root, name string, mode os.FileMode, opts *archiveoptions.Options) error {
|
||||
parent, err := root.OpenFile(filepath.Dir(name), os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -127,19 +127,7 @@ func chmodNoSymlink(root *os.Root, name string, mode os.FileMode) error {
|
||||
}
|
||||
|
||||
// Fallback for systems that cannot perform fchmodat with AT_SYMLINK_NOFOLLOW.
|
||||
// Open the entry without following symlinks and apply the mode through the
|
||||
// resulting file descriptor.
|
||||
// #nosec G115 -- ignore integer overflow conversion for parent.Fd
|
||||
fd, err := unix.Openat(int(parent.Fd()), base, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK, 0)
|
||||
if err != nil {
|
||||
return &os.PathError{Op: "openat", Path: name, Err: err}
|
||||
}
|
||||
defer unix.Close(fd)
|
||||
|
||||
if err := unix.Fchmod(fd, perm); err != nil {
|
||||
return &os.PathError{Op: "fchmod", Path: name, Err: err}
|
||||
}
|
||||
return nil
|
||||
return chmodNoSymlinkFallback(int(parent.Fd()), base, name, perm, opts) // #nosec G115 -- ignore integer overflow conversion for parent.Fd
|
||||
}
|
||||
|
||||
// fileModeToPerm returns the subset of an os.FileMode that can be applied
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ func handleTarTypeBlockCharFifo(root *os.Root, hdr *tar.Header, path string) err
|
||||
}
|
||||
|
||||
// handleLChmod is a no-op on Windows because chmod is not supported.
|
||||
func handleLChmod(root *os.Root, path string, hdr *tar.Header, hdrInfo os.FileInfo) error {
|
||||
func handleLChmod(root *os.Root, dstPath string, hardlinkTarget string, hdr *tar.Header, hdrInfo os.FileInfo, opts any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package archive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"github.com/moby/go-archive/internal/archiveoptions"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// chmodNoSymlinkFallback applies mode without following the final path
|
||||
// component on systems without fchmodat2 support.
|
||||
//
|
||||
// Callers must have already excluded symlink entries.
|
||||
func chmodNoSymlinkFallback(parentFD int, base, name string, perm uint32, opts *archiveoptions.Options) error {
|
||||
fd, err := unix.Openat(parentFD, base, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return &os.PathError{Op: "openat", Path: name, Err: err}
|
||||
}
|
||||
defer unix.Close(fd)
|
||||
|
||||
if opts != nil && opts.ProcSelfFD != nil {
|
||||
err := unix.Fchmodat(int(opts.ProcSelfFD.Fd()), strconv.Itoa(fd), perm, 0)
|
||||
// Keep the os.File alive until fchmodat has finished using its descriptor.
|
||||
runtime.KeepAlive(opts.ProcSelfFD)
|
||||
if err != nil {
|
||||
return &os.PathError{
|
||||
Op: "fchmodat",
|
||||
Path: name,
|
||||
Err: fmt.Errorf("via pre-opened /proc/self/fd/%d: %w", fd, err),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
procPath := "/proc/self/fd/" + strconv.Itoa(fd)
|
||||
if err := unix.Chmod(procPath, perm); err != nil {
|
||||
return &os.PathError{
|
||||
Op: "chmod",
|
||||
Path: name,
|
||||
Err: fmt.Errorf("via %s: %w", procPath, err),
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
//go:build !linux && !windows
|
||||
|
||||
package archive
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/moby/go-archive/internal/archiveoptions"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// chmodNoSymlinkFallback applies mode without following the final path
|
||||
// component on systems without fchmodat2 support.
|
||||
//
|
||||
// Callers must have already excluded symlink entries.
|
||||
func chmodNoSymlinkFallback(parentFD int, base, name string, perm uint32, _ *archiveoptions.Options) error {
|
||||
fd, err := unix.Openat(parentFD, base, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return &os.PathError{Op: "openat", Path: name, Err: err}
|
||||
}
|
||||
defer unix.Close(fd)
|
||||
|
||||
if err := unix.Fchmod(fd, perm); err != nil {
|
||||
return &os.PathError{Op: "fchmod", Path: name, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+18
-18
@@ -29,8 +29,9 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
|
||||
tr := tar.NewReader(layer)
|
||||
|
||||
var dirs []unpackedDir
|
||||
// unpackedPaths tracks root-relative paths already written in this layer
|
||||
// so that the AUFS opaque-whiteout walk knows which paths to preserve.
|
||||
// unpackedPaths tracks resolved, native-separator, root-relative paths
|
||||
// already written in this layer so that the AUFS opaque-whiteout walk
|
||||
// knows which paths to preserve.
|
||||
unpackedPaths := make(map[string]struct{})
|
||||
|
||||
if options == nil {
|
||||
@@ -71,12 +72,6 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure that the parent directory exists.
|
||||
err = createImpliedDirectories(root, hdr, options)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Skip AUFS metadata dirs
|
||||
if strings.HasPrefix(hdr.Name, WhiteoutMetaPrefix) {
|
||||
// Regular files inside /.wh..wh.plnk can be used as hardlink targets
|
||||
@@ -109,10 +104,15 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
|
||||
// dstPath is the native (host-separator) form of the entry name,
|
||||
// used at all filesystem boundaries (os.Root methods, fsRootPath).
|
||||
// The tar-header name (hdr.Name) is POSIX, so convert it here.
|
||||
dstPath := filepath.FromSlash(hdr.Name)
|
||||
base := filepath.Base(dstPath)
|
||||
|
||||
if strings.HasPrefix(base, WhiteoutPrefix) {
|
||||
dstPath, err := resolveArchivePath(root, filepath.FromSlash(hdr.Name))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// Ensure that the parent directory exists.
|
||||
if err := createImpliedDirectories(root, dstPath, options); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if base := filepath.Base(dstPath); strings.HasPrefix(base, WhiteoutPrefix) {
|
||||
dir := filepath.Dir(dstPath)
|
||||
if base == WhiteoutOpaqueDir {
|
||||
_, err := root.Lstat(dir)
|
||||
@@ -144,9 +144,9 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
|
||||
return err
|
||||
}
|
||||
|
||||
// unpackedPaths is keyed by root-relative slash paths; convert
|
||||
// filepath.WalkDir's native path before looking it up.
|
||||
if _, exists := unpackedPaths[filepath.ToSlash(rel)]; !exists {
|
||||
// unpackedPaths is keyed by resolved, native-separator,
|
||||
// root-relative paths, matching filepath.WalkDir's paths.
|
||||
if _, exists := unpackedPaths[rel]; !exists {
|
||||
return root.RemoveAll(rel)
|
||||
}
|
||||
return nil
|
||||
@@ -206,9 +206,9 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
|
||||
if hdr.Typeflag == tar.TypeDir {
|
||||
dirs = append(dirs, unpackedDir{hdr: hdr, name: dstPath})
|
||||
}
|
||||
// unpackedPaths is keyed by the POSIX (forward-slash) name so it
|
||||
// matches the ToSlash'd lookup in the opaque-whiteout walk above.
|
||||
unpackedPaths[hdr.Name] = struct{}{}
|
||||
// Record the resolved, native-separator, root-relative path so it
|
||||
// matches the paths produced by the opaque-whiteout walk.
|
||||
unpackedPaths[dstPath] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// Package archiveoptions defines internal options shared between archive and
|
||||
// chrootarchive.
|
||||
package archiveoptions
|
||||
|
||||
import "os"
|
||||
|
||||
// Options contains extraction resources supplied by internal callers.
|
||||
type Options struct {
|
||||
// ProcSelfFD references /proc/self/fd as opened before entering a chroot.
|
||||
// The caller retains ownership of the file.
|
||||
ProcSelfFD *os.File
|
||||
}
|
||||
+45
-15
@@ -24,31 +24,47 @@ import (
|
||||
|
||||
var errTooManyLinks = errors.New("too many links")
|
||||
|
||||
type fsRootPathResult struct {
|
||||
path string
|
||||
followedAbsoluteLink bool
|
||||
relativeEscapeBeforeAbsolute bool
|
||||
}
|
||||
|
||||
// fsRootPath joins a path with a root, evaluating and bounding any
|
||||
// symlink to the root directory.
|
||||
func fsRootPath(root, path string) (string, error) {
|
||||
result, err := resolveFSRootPath(root, path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result.path, nil
|
||||
}
|
||||
|
||||
func resolveFSRootPath(root, path string) (fsRootPathResult, error) {
|
||||
result := fsRootPathResult{path: root}
|
||||
if path == "" {
|
||||
return root, nil
|
||||
return result, nil
|
||||
}
|
||||
var linksWalked int // to protect against cycles
|
||||
for {
|
||||
i := linksWalked
|
||||
newpath, err := walkLinks(root, path, &linksWalked)
|
||||
newpath, err := walkLinks(root, path, &linksWalked, &result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return fsRootPathResult{}, err
|
||||
}
|
||||
path = newpath
|
||||
if i == linksWalked {
|
||||
newpath = filepath.Join(string(os.PathSeparator), newpath)
|
||||
if path == newpath {
|
||||
return filepath.Join(root, newpath), nil
|
||||
result.path = filepath.Join(root, newpath)
|
||||
return result, nil
|
||||
}
|
||||
path = newpath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func walkLink(root, path string, linksWalked *int) (newpath string, islink bool, err error) {
|
||||
func walkLink(root, path string, linksWalked *int, result *fsRootPathResult) (newpath string, islink bool, err error) {
|
||||
if *linksWalked > 255 {
|
||||
return "", false, errTooManyLinks
|
||||
}
|
||||
@@ -74,37 +90,51 @@ func walkLink(root, path string, linksWalked *int) (newpath string, islink bool,
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
if filepath.IsAbs(newpath) {
|
||||
result.followedAbsoluteLink = true
|
||||
} else if !result.followedAbsoluteLink {
|
||||
// Record an escape before a later absolute link can make the original
|
||||
// os.Root error appear eligible for resolve-in-root fallback.
|
||||
relativeDir, err := filepath.Rel(string(os.PathSeparator), filepath.Dir(path))
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
resolved := filepath.Join(relativeDir, newpath)
|
||||
if resolved != "." && !filepath.IsLocal(resolved) {
|
||||
result.relativeEscapeBeforeAbsolute = true
|
||||
}
|
||||
}
|
||||
|
||||
*linksWalked++
|
||||
return newpath, true, nil
|
||||
}
|
||||
|
||||
func walkLinks(root, path string, linksWalked *int) (string, error) {
|
||||
func walkLinks(root, path string, linksWalked *int, result *fsRootPathResult) (string, error) {
|
||||
switch dir, file := filepath.Split(path); {
|
||||
case dir == "":
|
||||
newpath, _, err := walkLink(root, file, linksWalked)
|
||||
newpath, _, err := walkLink(root, file, linksWalked, result)
|
||||
return newpath, err
|
||||
case file == "":
|
||||
if os.IsPathSeparator(dir[len(dir)-1]) {
|
||||
if dir == string(os.PathSeparator) {
|
||||
return dir, nil
|
||||
}
|
||||
return walkLinks(root, dir[:len(dir)-1], linksWalked)
|
||||
return walkLinks(root, dir[:len(dir)-1], linksWalked, result)
|
||||
}
|
||||
newpath, _, err := walkLink(root, dir, linksWalked)
|
||||
newpath, _, err := walkLink(root, dir, linksWalked, result)
|
||||
return newpath, err
|
||||
|
||||
default:
|
||||
newdir, err := walkLinks(root, dir, linksWalked)
|
||||
newdir, err := walkLinks(root, dir, linksWalked, result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
newpath, islink, err := walkLink(root, filepath.Join(newdir, file), linksWalked)
|
||||
newpath, islink, err := walkLink(root, filepath.Join(newdir, file), linksWalked, result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !islink {
|
||||
return newpath, nil
|
||||
}
|
||||
if filepath.IsAbs(newpath) {
|
||||
if !islink || filepath.IsAbs(newpath) {
|
||||
return newpath, nil
|
||||
}
|
||||
return filepath.Join(newdir, newpath), nil
|
||||
|
||||
Vendored
+2
-1
@@ -162,10 +162,11 @@ github.com/mattn/go-runewidth
|
||||
# github.com/moby/docker-image-spec v1.3.1
|
||||
## explicit; go 1.18
|
||||
github.com/moby/docker-image-spec/specs-go/v1
|
||||
# github.com/moby/go-archive v0.3.0
|
||||
# github.com/moby/go-archive v0.3.3
|
||||
## explicit; go 1.25
|
||||
github.com/moby/go-archive
|
||||
github.com/moby/go-archive/compression
|
||||
github.com/moby/go-archive/internal/archiveoptions
|
||||
github.com/moby/go-archive/tarheader
|
||||
# github.com/moby/moby/api v1.55.0
|
||||
## explicit; go 1.24
|
||||
|
||||
Reference in New Issue
Block a user