vendor: github.com/docker/docker 185651d26bc6 (master, v28.0-dev)

full diff: https://github.com/moby/moby/compare/v28.0.4...185651d26bc6281b199a5b7ff1942b53e4f17b96

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

vendor: moby with atomicwriter

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2025-03-10 17:22:01 +01:00
parent 90b48f8eb5
commit 2bf317ad5f
31 changed files with 501 additions and 284 deletions

View File

@ -3,7 +3,7 @@ package api // import "github.com/docker/docker/api"
// Common constants for daemon and client.
const (
// DefaultVersion of the current REST API.
DefaultVersion = "1.48"
DefaultVersion = "1.49"
// MinSupportedAPIVersion is the minimum API version that can be supported
// by the API server, specified as "major.minor". Note that the daemon

View File

@ -19,10 +19,10 @@ produces:
consumes:
- "application/json"
- "text/plain"
basePath: "/v1.48"
basePath: "/v1.49"
info:
title: "Docker Engine API"
version: "1.48"
version: "1.49"
x-logo:
url: "https://docs.docker.com/assets/images/logo-docker-main.png"
description: |
@ -55,8 +55,8 @@ info:
the URL is not supported by the daemon, a HTTP `400 Bad Request` error message
is returned.
If you omit the version-prefix, the current version of the API (v1.48) is used.
For example, calling `/info` is the same as calling `/v1.48/info`. Using the
If you omit the version-prefix, the current version of the API (v1.49) is used.
For example, calling `/info` is the same as calling `/v1.49/info`. Using the
API without a version-prefix is deprecated and will be removed in a future release.
Engine releases in the near future should support this version of the API,
@ -6856,6 +6856,8 @@ definitions:
description: "The network pool size"
type: "integer"
example: "24"
FirewallBackend:
$ref: "#/definitions/FirewallInfo"
Warnings:
description: |
List of warnings / informational messages about missing features, or
@ -6939,6 +6941,37 @@ definitions:
default: "plugins.moby"
example: "plugins.moby"
FirewallInfo:
description: |
Information about the daemon's firewalling configuration.
This field is currently only used on Linux, and omitted on other platforms.
type: "object"
x-nullable: true
properties:
Driver:
description: |
The name of the firewall backend driver.
type: "string"
example: "nftables"
Info:
description: |
Information about the firewall backend, provided as
"label" / "value" pairs.
<p><br /></p>
> **Note**: The information returned in this field, including the
> formatting of values and labels, should not be considered stable,
> and may change without notice.
type: "array"
items:
type: "array"
items:
type: "string"
example:
- ["ReloadedAt", "2025-01-01T00:00:00Z"]
# PluginsInfo is a temp struct holding Plugins name
# registered with docker daemon. It is used by Info struct
PluginsInfo:
@ -10537,6 +10570,7 @@ paths:
If not provided, the full multi-platform image will be saved.
Example: `{"os": "linux", "architecture": "arm", "variant": "v5"}`
tags: ["Image"]
/images/get:
get:
summary: "Export several images"
@ -10571,6 +10605,16 @@ paths:
type: "array"
items:
type: "string"
- name: "platform"
type: "string"
in: "query"
description: |
JSON encoded OCI platform describing a platform which will be used
to select a platform-specific image to be saved if the image is
multi-platform.
If not provided, the full multi-platform image will be saved.
Example: `{"os": "linux", "architecture": "arm", "variant": "v5"}`
tags: ["Image"]
/images/load:
post:

View File

@ -128,11 +128,12 @@ type InspectResponse struct {
// compatibility.
Descriptor *ocispec.Descriptor `json:"Descriptor,omitempty"`
// Manifests is a list of image manifests available in this image. It
// Manifests is a list of image manifests available in this image. It
// provides a more detailed view of the platform-specific image manifests or
// other image-attached data like build attestations.
//
// Only available if the daemon provides a multi-platform image store.
// Only available if the daemon provides a multi-platform image store, the client
// requests manifests AND does not request a specific platform.
//
// WARNING: This is experimental and may change at any time without any backward
// compatibility.

View File

@ -106,6 +106,11 @@ type LoadOptions struct {
type InspectOptions struct {
// Manifests returns the image manifests.
Manifests bool
// Platform selects the specific platform of a multi-platform image to inspect.
//
// This option is only available for API version 1.49 and up.
Platform *ocispec.Platform
}
// SaveOptions holds parameters to save images.

View File

@ -73,6 +73,7 @@ type Info struct {
SecurityOptions []string
ProductLicense string `json:",omitempty"`
DefaultAddressPools []NetworkAddressPool `json:",omitempty"`
FirewallBackend *FirewallInfo `json:"FirewallBackend,omitempty"`
CDISpecDirs []string
Containerd *ContainerdInfo `json:",omitempty"`
@ -151,3 +152,11 @@ type NetworkAddressPool struct {
Base string
Size int
}
// FirewallInfo describes the firewall backend.
type FirewallInfo struct {
// Driver is the name of the firewall backend driver.
Driver string `json:"Driver"`
// Info is a list of label/value pairs, containing information related to the firewall.
Info [][2]string `json:"Info,omitempty"`
}

View File

@ -32,7 +32,7 @@ func (cli *Client) ContainerCommit(ctx context.Context, containerID string, opti
if tagged, ok := ref.(reference.Tagged); ok {
tag = tagged.Tag()
}
repository = reference.FamiliarName(ref)
repository = ref.Name()
}
query := url.Values{}

View File

@ -21,7 +21,7 @@ func (cli *Client) ImageCreate(ctx context.Context, parentReference string, opti
}
query := url.Values{}
query.Set("fromImage", reference.FamiliarName(ref))
query.Set("fromImage", ref.Name())
query.Set("tag", getAPITagFromNamedRef(ref))
if options.Platform != "" {
query.Set("platform", strings.ToLower(options.Platform))

View File

@ -32,6 +32,17 @@ func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts
query.Set("manifests", "1")
}
if opts.apiOptions.Platform != nil {
if err := cli.NewVersionError(ctx, "1.49", "platform"); err != nil {
return image.InspectResponse{}, err
}
platform, err := encodePlatform(opts.apiOptions.Platform)
if err != nil {
return image.InspectResponse{}, err
}
query.Set("platform", platform)
}
resp, err := cli.get(ctx, "/images/"+imageID+"/json", query, nil)
defer ensureReaderClosed(resp)
if err != nil {

View File

@ -4,6 +4,7 @@ import (
"bytes"
"github.com/docker/docker/api/types/image"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// ImageInspectOption is a type representing functional options for the image inspect operation.
@ -36,6 +37,17 @@ func ImageInspectWithManifests(manifests bool) ImageInspectOption {
})
}
// ImageInspectWithPlatform sets platform API option for the image inspect operation.
// This option is only available for API version 1.49 and up.
// With this option set, the image inspect operation will return information for the
// specified platform variant of the multi-platform image.
func ImageInspectWithPlatform(platform *ocispec.Platform) ImageInspectOption {
return imageInspectOptionFunc(func(clientOpts *imageInspectOpts) error {
clientOpts.apiOptions.Platform = platform
return nil
})
}
// ImageInspectWithAPIOpts sets the API options for the image inspect operation.
func ImageInspectWithAPIOpts(opts image.InspectOptions) ImageInspectOption {
return imageInspectOptionFunc(func(clientOpts *imageInspectOpts) error {

View File

@ -26,7 +26,7 @@ func (cli *Client) ImagePull(ctx context.Context, refStr string, options image.P
}
query := url.Values{}
query.Set("fromImage", reference.FamiliarName(ref))
query.Set("fromImage", ref.Name())
if !options.All {
query.Set("tag", getAPITagFromNamedRef(ref))
}

View File

@ -29,7 +29,6 @@ func (cli *Client) ImagePush(ctx context.Context, image string, options image.Pu
return nil, errors.New("cannot push a digest reference")
}
name := reference.FamiliarName(ref)
query := url.Values{}
if !options.All {
ref = reference.TagNameOnly(ref)
@ -52,13 +51,13 @@ func (cli *Client) ImagePush(ctx context.Context, image string, options image.Pu
query.Set("platform", string(pJson))
}
resp, err := cli.tryImagePush(ctx, name, query, options.RegistryAuth)
resp, err := cli.tryImagePush(ctx, ref.Name(), query, options.RegistryAuth)
if errdefs.IsUnauthorized(err) && options.PrivilegeFunc != nil {
newAuthHeader, privilegeErr := options.PrivilegeFunc(ctx)
if privilegeErr != nil {
return nil, privilegeErr
}
resp, err = cli.tryImagePush(ctx, name, query, newAuthHeader)
resp, err = cli.tryImagePush(ctx, ref.Name(), query, newAuthHeader)
}
if err != nil {
return nil, err

View File

@ -26,7 +26,7 @@ func (cli *Client) ImageTag(ctx context.Context, source, target string) error {
ref = reference.TagNameOnly(ref)
query := url.Values{}
query.Set("repo", reference.FamiliarName(ref))
query.Set("repo", ref.Name())
if tagged, ok := ref.(reference.Tagged); ok {
query.Set("tag", tagged.Tag())
}

View File

@ -75,7 +75,7 @@ func sameFsTime(a, b time.Time) bool {
// Changes walks the path rw and determines changes for the files in the path,
// with respect to the parent layers
func Changes(layers []string, rw string) ([]Change, error) {
return changes(layers, rw, aufsDeletedFile, aufsMetadataSkip)
return collectChanges(layers, rw, aufsDeletedFile, aufsMetadataSkip)
}
func aufsMetadataSkip(path string) (skip bool, err error) {
@ -103,7 +103,7 @@ type (
deleteChange func(string, string, os.FileInfo) (string, error)
)
func changes(layers []string, rw string, dc deleteChange, sc skipChange) ([]Change, error) {
func collectChanges(layers []string, rw string, dc deleteChange, sc skipChange) ([]Change, error) {
var (
changes []Change
changedDirs = make(map[string]struct{})

View File

@ -1,224 +0,0 @@
package atomicwriter
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
)
func validateDestination(fileName string) error {
if fileName == "" {
return errors.New("file name is empty")
}
// Deliberately using Lstat here to match the behavior of [os.Rename],
// which is used when completing the write and does not resolve symlinks.
//
// TODO(thaJeztah): decide whether we want to disallow symlinks or to follow them.
if fi, err := os.Lstat(fileName); err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("failed to stat output path: %w", err)
}
} else if err := validateFileMode(fi.Mode()); err != nil {
return err
}
if dir := filepath.Dir(fileName); dir != "" && dir != "." {
if _, err := os.Stat(dir); errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("invalid file path: %w", err)
}
}
return nil
}
func validateFileMode(mode os.FileMode) error {
switch {
case mode.IsRegular():
return nil // Regular file
case mode&os.ModeDir != 0:
return errors.New("cannot write to a directory")
// TODO(thaJeztah): decide whether we want to disallow symlinks or to follow them.
// case mode&os.ModeSymlink != 0:
// return errors.New("cannot write to a symbolic link directly")
case mode&os.ModeNamedPipe != 0:
return errors.New("cannot write to a named pipe (FIFO)")
case mode&os.ModeSocket != 0:
return errors.New("cannot write to a socket")
case mode&os.ModeDevice != 0:
if mode&os.ModeCharDevice != 0 {
return errors.New("cannot write to a character device file")
}
return errors.New("cannot write to a block device file")
case mode&os.ModeSetuid != 0:
return errors.New("cannot write to a setuid file")
case mode&os.ModeSetgid != 0:
return errors.New("cannot write to a setgid file")
case mode&os.ModeSticky != 0:
return errors.New("cannot write to a sticky bit file")
default:
// Unknown file mode; let's assume it works
return nil
}
}
// New returns a WriteCloser so that writing to it writes to a
// temporary file and closing it atomically changes the temporary file to
// destination path. Writing and closing concurrently is not allowed.
// NOTE: umask is not considered for the file's permissions.
func New(filename string, perm os.FileMode) (io.WriteCloser, error) {
if err := validateDestination(filename); err != nil {
return nil, err
}
abspath, err := filepath.Abs(filename)
if err != nil {
return nil, err
}
f, err := os.CreateTemp(filepath.Dir(abspath), ".tmp-"+filepath.Base(filename))
if err != nil {
return nil, err
}
return &atomicFileWriter{
f: f,
fn: abspath,
perm: perm,
}, nil
}
// WriteFile atomically writes data to a file named by filename and with the specified permission bits.
// NOTE: umask is not considered for the file's permissions.
func WriteFile(filename string, data []byte, perm os.FileMode) error {
f, err := New(filename, perm)
if err != nil {
return err
}
n, err := f.Write(data)
if err == nil && n < len(data) {
err = io.ErrShortWrite
f.(*atomicFileWriter).writeErr = err
}
if err1 := f.Close(); err == nil {
err = err1
}
return err
}
type atomicFileWriter struct {
f *os.File
fn string
writeErr error
written bool
perm os.FileMode
}
func (w *atomicFileWriter) Write(dt []byte) (int, error) {
w.written = true
n, err := w.f.Write(dt)
if err != nil {
w.writeErr = err
}
return n, err
}
func (w *atomicFileWriter) Close() (retErr error) {
defer func() {
if err := os.Remove(w.f.Name()); !errors.Is(err, os.ErrNotExist) && retErr == nil {
retErr = err
}
}()
if err := w.f.Sync(); err != nil {
_ = w.f.Close()
return err
}
if err := w.f.Close(); err != nil {
return err
}
if err := os.Chmod(w.f.Name(), w.perm); err != nil {
return err
}
if w.writeErr == nil && w.written {
return os.Rename(w.f.Name(), w.fn)
}
return nil
}
// WriteSet is used to atomically write a set
// of files and ensure they are visible at the same time.
// Must be committed to a new directory.
type WriteSet struct {
root string
}
// NewWriteSet creates a new atomic write set to
// atomically create a set of files. The given directory
// is used as the base directory for storing files before
// commit. If no temporary directory is given the system
// default is used.
func NewWriteSet(tmpDir string) (*WriteSet, error) {
td, err := os.MkdirTemp(tmpDir, "write-set-")
if err != nil {
return nil, err
}
return &WriteSet{
root: td,
}, nil
}
// WriteFile writes a file to the set, guaranteeing the file
// has been synced.
func (ws *WriteSet) WriteFile(filename string, data []byte, perm os.FileMode) error {
f, err := ws.FileWriter(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
if err != nil {
return err
}
n, err := f.Write(data)
if err == nil && n < len(data) {
err = io.ErrShortWrite
}
if err1 := f.Close(); err == nil {
err = err1
}
return err
}
type syncFileCloser struct {
*os.File
}
func (w syncFileCloser) Close() error {
err := w.File.Sync()
if err1 := w.File.Close(); err == nil {
err = err1
}
return err
}
// FileWriter opens a file writer inside the set. The file
// should be synced and closed before calling commit.
func (ws *WriteSet) FileWriter(name string, flag int, perm os.FileMode) (io.WriteCloser, error) {
f, err := os.OpenFile(filepath.Join(ws.root, name), flag, perm)
if err != nil {
return nil, err
}
return syncFileCloser{f}, nil
}
// Cancel cancels the set and removes all temporary data
// created in the set.
func (ws *WriteSet) Cancel() error {
return os.RemoveAll(ws.root)
}
// Commit moves all created files to the target directory. The
// target directory must not exist and the parent of the target
// directory must exist.
func (ws *WriteSet) Commit(target string) error {
return os.Rename(ws.root, target)
}
// String returns the location the set is writing to.
func (ws *WriteSet) String() string {
return ws.root
}

View File

@ -3,11 +3,15 @@ package idtools
import (
"fmt"
"os"
"github.com/moby/sys/user"
)
// IDMap contains a single entry for user namespace range remapping. An array
// of IDMap entries represents the structure that will be provided to the Linux
// kernel for creating a user namespace.
//
// Deprecated: use [user.IDMap] instead.
type IDMap struct {
ContainerID int `json:"container_id"`
HostID int `json:"host_id"`
@ -17,28 +21,42 @@ type IDMap struct {
// MkdirAllAndChown creates a directory (include any along the path) and then modifies
// ownership to the requested uid/gid. If the directory already exists, this
// function will still change ownership and permissions.
//
// Deprecated: use [user.MkdirAllAndChown] instead.
func MkdirAllAndChown(path string, mode os.FileMode, owner Identity) error {
return mkdirAs(path, mode, owner, true, true)
return user.MkdirAllAndChown(path, mode, owner.UID, owner.GID)
}
// MkdirAndChown creates a directory and then modifies ownership to the requested uid/gid.
// If the directory already exists, this function still changes ownership and permissions.
// Note that unlike os.Mkdir(), this function does not return IsExist error
// in case path already exists.
//
// Deprecated: use [user.MkdirAndChown] instead.
func MkdirAndChown(path string, mode os.FileMode, owner Identity) error {
return mkdirAs(path, mode, owner, false, true)
return user.MkdirAndChown(path, mode, owner.UID, owner.GID)
}
// MkdirAllAndChownNew creates a directory (include any along the path) and then modifies
// ownership ONLY of newly created directories to the requested uid/gid. If the
// directories along the path exist, no change of ownership or permissions will be performed
//
// Deprecated: use [user.MkdirAllAndChown] with the [user.WithOnlyNew] option instead.
func MkdirAllAndChownNew(path string, mode os.FileMode, owner Identity) error {
return mkdirAs(path, mode, owner, true, false)
return user.MkdirAllAndChown(path, mode, owner.UID, owner.GID, user.WithOnlyNew)
}
// GetRootUIDGID retrieves the remapped root uid/gid pair from the set of maps.
// If the maps are empty, then the root uid/gid will default to "real" 0/0
//
// Deprecated: use [(user.IdentityMapping).RootPair] instead.
func GetRootUIDGID(uidMap, gidMap []IDMap) (int, int, error) {
return getRootUIDGID(uidMap, gidMap)
}
// getRootUIDGID retrieves the remapped root uid/gid pair from the set of maps.
// If the maps are empty, then the root uid/gid will default to "real" 0/0
func getRootUIDGID(uidMap, gidMap []IDMap) (int, int, error) {
uid, err := toHost(0, uidMap)
if err != nil {
return -1, -1, err
@ -101,11 +119,36 @@ type IdentityMapping struct {
GIDMaps []IDMap `json:"GIDMaps"`
}
// FromUserIdentityMapping converts a [user.IdentityMapping] to an [idtools.IdentityMapping].
//
// Deprecated: use [user.IdentityMapping] directly, this is transitioning to user package.
func FromUserIdentityMapping(u user.IdentityMapping) IdentityMapping {
return IdentityMapping{
UIDMaps: fromUserIDMap(u.UIDMaps),
GIDMaps: fromUserIDMap(u.GIDMaps),
}
}
func fromUserIDMap(u []user.IDMap) []IDMap {
if u == nil {
return nil
}
m := make([]IDMap, len(u))
for i := range u {
m[i] = IDMap{
ContainerID: int(u[i].ID),
HostID: int(u[i].ParentID),
Size: int(u[i].Count),
}
}
return m
}
// RootPair returns a uid and gid pair for the root user. The error is ignored
// because a root user always exists, and the defaults are correct when the uid
// and gid maps are empty.
func (i IdentityMapping) RootPair() Identity {
uid, gid, _ := GetRootUIDGID(i.UIDMaps, i.GIDMaps)
uid, gid, _ := getRootUIDGID(i.UIDMaps, i.GIDMaps)
return Identity{UID: uid, GID: gid}
}
@ -144,6 +187,8 @@ func (i IdentityMapping) Empty() bool {
}
// CurrentIdentity returns the identity of the current process
//
// Deprecated: use [os.Getuid] and [os.Getegid] instead.
func CurrentIdentity() Identity {
return Identity{UID: os.Getuid(), GID: os.Getegid()}
}

View File

@ -1,166 +0,0 @@
//go:build !windows
package idtools
import (
"fmt"
"os"
"path/filepath"
"strconv"
"syscall"
"github.com/moby/sys/user"
)
func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting bool) error {
path, err := filepath.Abs(path)
if err != nil {
return err
}
stat, err := os.Stat(path)
if err == nil {
if !stat.IsDir() {
return &os.PathError{Op: "mkdir", Path: path, Err: syscall.ENOTDIR}
}
if !chownExisting {
return nil
}
// short-circuit -- we were called with an existing directory and chown was requested
return setPermissions(path, mode, owner, stat)
}
// make an array containing the original path asked for, plus (for mkAll == true)
// all path components leading up to the complete path that don't exist before we MkdirAll
// so that we can chown all of them properly at the end. If chownExisting is false, we won't
// chown the full directory path if it exists
var paths []string
if os.IsNotExist(err) {
paths = []string{path}
}
if mkAll {
// walk back to "/" looking for directories which do not exist
// and add them to the paths array for chown after creation
dirPath := path
for {
dirPath = filepath.Dir(dirPath)
if dirPath == "/" {
break
}
if _, err = os.Stat(dirPath); err != nil && os.IsNotExist(err) {
paths = append(paths, dirPath)
}
}
if err = os.MkdirAll(path, mode); err != nil {
return err
}
} else if err = os.Mkdir(path, mode); err != nil {
return err
}
// even if it existed, we will chown the requested path + any subpaths that
// didn't exist when we called MkdirAll
for _, pathComponent := range paths {
if err = setPermissions(pathComponent, mode, owner, nil); err != nil {
return err
}
}
return nil
}
// LookupUser uses traditional local system files lookup (from libcontainer/user) on a username
//
// Deprecated: use [user.LookupUser] instead
func LookupUser(name string) (user.User, error) {
return user.LookupUser(name)
}
// LookupUID uses traditional local system files lookup (from libcontainer/user) on a uid
//
// Deprecated: use [user.LookupUid] instead
func LookupUID(uid int) (user.User, error) {
return user.LookupUid(uid)
}
// LookupGroup uses traditional local system files lookup (from libcontainer/user) on a group name,
//
// Deprecated: use [user.LookupGroup] instead
func LookupGroup(name string) (user.Group, error) {
return user.LookupGroup(name)
}
// setPermissions performs a chown/chmod only if the uid/gid don't match what's requested
// Normally a Chown is a no-op if uid/gid match, but in some cases this can still cause an error, e.g. if the
// dir is on an NFS share, so don't call chown unless we absolutely must.
// Likewise for setting permissions.
func setPermissions(p string, mode os.FileMode, owner Identity, stat os.FileInfo) error {
if stat == nil {
var err error
stat, err = os.Stat(p)
if err != nil {
return err
}
}
if stat.Mode().Perm() != mode.Perm() {
if err := os.Chmod(p, mode.Perm()); err != nil {
return err
}
}
ssi := stat.Sys().(*syscall.Stat_t)
if ssi.Uid == uint32(owner.UID) && ssi.Gid == uint32(owner.GID) {
return nil
}
return os.Chown(p, owner.UID, owner.GID)
}
// LoadIdentityMapping takes a requested username and
// using the data from /etc/sub{uid,gid} ranges, creates the
// proper uid and gid remapping ranges for that user/group pair
func LoadIdentityMapping(name string) (IdentityMapping, error) {
// TODO: Consider adding support for calling out to "getent"
usr, err := user.LookupUser(name)
if err != nil {
return IdentityMapping{}, fmt.Errorf("could not get user for username %s: %v", name, err)
}
subuidRanges, err := lookupSubRangesFile("/etc/subuid", usr)
if err != nil {
return IdentityMapping{}, err
}
subgidRanges, err := lookupSubRangesFile("/etc/subgid", usr)
if err != nil {
return IdentityMapping{}, err
}
return IdentityMapping{
UIDMaps: subuidRanges,
GIDMaps: subgidRanges,
}, nil
}
func lookupSubRangesFile(path string, usr user.User) ([]IDMap, error) {
uidstr := strconv.Itoa(usr.Uid)
rangeList, err := user.ParseSubIDFileFilter(path, func(sid user.SubID) bool {
return sid.Name == usr.Name || sid.Name == uidstr
})
if err != nil {
return nil, err
}
if len(rangeList) == 0 {
return nil, fmt.Errorf("no subuid ranges found for user %q", usr.Name)
}
idMap := []IDMap{}
containerID := 0
for _, idrange := range rangeList {
idMap = append(idMap, IDMap{
ContainerID: containerID,
HostID: int(idrange.SubID),
Size: int(idrange.Count),
})
containerID = containerID + int(idrange.Count)
}
return idMap, nil
}

View File

@ -1,9 +1,5 @@
package idtools
import (
"os"
)
const (
SeTakeOwnershipPrivilege = "SeTakeOwnershipPrivilege"
)
@ -14,11 +10,3 @@ const (
ContainerUserSidString = "S-1-5-93-2-2"
)
// This is currently a wrapper around [os.MkdirAll] since currently
// permissions aren't set through this path, the identity isn't utilized.
// Ownership is handled elsewhere, but in the future could be support here
// too.
func mkdirAs(path string, _ os.FileMode, _ Identity, _, _ bool) error {
return os.MkdirAll(path, 0)
}

View File

@ -4,7 +4,7 @@ import (
"io"
"os"
"github.com/docker/docker/pkg/atomicwriter"
"github.com/moby/sys/atomicwriter"
)
// NewAtomicFileWriter returns WriteCloser so that writing to it writes to a

View File

@ -294,12 +294,15 @@ func isCIDRMatch(cidrs []*registry.NetIPNet, URLHost string) bool {
return false
}
// ValidateMirror validates an HTTP(S) registry mirror. It is used by the daemon
// to validate the daemon configuration.
func ValidateMirror(val string) (string, error) {
uri, err := url.Parse(val)
// ValidateMirror validates and normalizes an HTTP(S) registry mirror. It
// returns an error if the given mirrorURL is invalid, or the normalized
// format for the URL otherwise.
//
// It is used by the daemon to validate the daemon configuration.
func ValidateMirror(mirrorURL string) (string, error) {
uri, err := url.Parse(mirrorURL)
if err != nil {
return "", invalidParamWrapf(err, "invalid mirror: %q is not a valid URI", val)
return "", invalidParamWrapf(err, "invalid mirror: %q is not a valid URI", mirrorURL)
}
if uri.Scheme != "http" && uri.Scheme != "https" {
return "", invalidParamf("invalid mirror: unsupported scheme %q in %q", uri.Scheme, uri)
@ -312,7 +315,7 @@ func ValidateMirror(val string) (string, error) {
uri.User = url.UserPassword(uri.User.Username(), "xxxxx")
return "", invalidParamf("invalid mirror: username/password not allowed in URI %q", uri)
}
return strings.TrimSuffix(val, "/") + "/", nil
return strings.TrimSuffix(mirrorURL, "/") + "/", nil
}
// ValidateIndexName validates an index name. It is used by the daemon to
@ -414,7 +417,6 @@ func newRepositoryInfo(config *serviceConfig, name reference.Named) *RepositoryI
func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) {
indexName := normalizeIndexName(reference.Domain(reposName))
if indexName == IndexName {
officialRepo := !strings.ContainsRune(reference.FamiliarName(reposName), '/')
return &RepositoryInfo{
Name: reference.TrimNamed(reposName),
Index: &registry.IndexInfo{
@ -423,21 +425,16 @@ func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) {
Secure: true,
Official: true,
},
Official: officialRepo,
Official: !strings.ContainsRune(reference.FamiliarName(reposName), '/'),
}, nil
}
insecure := false
if isInsecure(indexName) {
insecure = true
}
return &RepositoryInfo{
Name: reference.TrimNamed(reposName),
Index: &registry.IndexInfo{
Name: indexName,
Mirrors: []string{},
Secure: !insecure,
Secure: !isInsecure(indexName),
},
}, nil
}

View File

@ -29,15 +29,15 @@ func hostCertsDir(hostname string) string {
}
// newTLSConfig constructs a client TLS configuration based on server defaults
func newTLSConfig(hostname string, isSecure bool) (*tls.Config, error) {
func newTLSConfig(ctx context.Context, hostname string, isSecure bool) (*tls.Config, error) {
// PreferredServerCipherSuites should have no effect
tlsConfig := tlsconfig.ServerDefault()
tlsConfig.InsecureSkipVerify = !isSecure
if isSecure {
hostDir := hostCertsDir(hostname)
log.G(context.TODO()).Debugf("hostDir: %s", hostDir)
if err := ReadCertsDirectory(tlsConfig, hostDir); err != nil {
log.G(ctx).Debugf("hostDir: %s", hostDir)
if err := loadTLSConfig(ctx, hostDir, tlsConfig); err != nil {
return nil, err
}
}
@ -58,12 +58,24 @@ func hasFile(files []os.DirEntry, name string) bool {
// including roots and certificate pairs and updates the
// provided TLS configuration.
func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error {
return loadTLSConfig(context.TODO(), directory, tlsConfig)
}
// loadTLSConfig reads the directory for TLS certificates including roots and
// certificate pairs, and updates the provided TLS configuration.
func loadTLSConfig(ctx context.Context, directory string, tlsConfig *tls.Config) error {
fs, err := os.ReadDir(directory)
if err != nil && !os.IsNotExist(err) {
if err != nil {
if os.IsNotExist(err) {
return nil
}
return invalidParam(err)
}
for _, f := range fs {
if ctx.Err() != nil {
return ctx.Err()
}
switch filepath.Ext(f.Name()) {
case ".crt":
if tlsConfig.RootCAs == nil {
@ -74,7 +86,7 @@ func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error {
tlsConfig.RootCAs = systemPool
}
fileName := filepath.Join(directory, f.Name())
log.G(context.TODO()).Debugf("crt: %s", fileName)
log.G(ctx).Debugf("crt: %s", fileName)
data, err := os.ReadFile(fileName)
if err != nil {
return err
@ -83,7 +95,7 @@ func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error {
case ".cert":
certName := f.Name()
keyName := certName[:len(certName)-5] + ".key"
log.G(context.TODO()).Debugf("cert: %s", filepath.Join(directory, certName))
log.G(ctx).Debugf("cert: %s", filepath.Join(directory, certName))
if !hasFile(fs, keyName) {
return invalidParamf("missing key %s for client certificate %s. CA certificates must use the extension .crt", keyName, certName)
}
@ -95,7 +107,7 @@ func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error {
case ".key":
keyName := f.Name()
certName := keyName[:len(keyName)-4] + ".cert"
log.G(context.TODO()).Debugf("key: %s", filepath.Join(directory, keyName))
log.G(ctx).Debugf("key: %s", filepath.Join(directory, keyName))
if !hasFile(fs, certName) {
return invalidParamf("missing client certificate %s for key %s", certName, keyName)
}

View File

@ -84,7 +84,6 @@ func (s *Service) Search(ctx context.Context, searchFilters filters.Args, term s
}
func (s *Service) searchUnfiltered(ctx context.Context, term string, limit int, authConfig *registry.AuthConfig, headers http.Header) (*registry.SearchResults, error) {
// TODO Use ctx when searching for repositories
if hasScheme(term) {
return nil, invalidParamf("invalid repository name: repository name (%s) should not have a scheme", term)
}
@ -100,7 +99,7 @@ func (s *Service) searchUnfiltered(ctx context.Context, term string, limit int,
remoteName = strings.TrimPrefix(remoteName, "library/")
}
endpoint, err := newV1Endpoint(index, headers)
endpoint, err := newV1Endpoint(ctx, index, headers)
if err != nil {
return nil, err
}
@ -131,7 +130,7 @@ func (s *Service) searchUnfiltered(ctx context.Context, term string, limit int,
}
}
return newSession(client, endpoint).searchRepositories(remoteName, limit)
return newSession(client, endpoint).searchRepositories(ctx, remoteName, limit)
}
// splitReposSearchTerm breaks a search term into an index name and remote name

View File

@ -4,6 +4,7 @@ import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"net/http"
"net/url"
"strings"
@ -31,8 +32,8 @@ type v1Endpoint struct {
// newV1Endpoint parses the given address to return a registry endpoint.
// TODO: remove. This is only used by search.
func newV1Endpoint(index *registry.IndexInfo, headers http.Header) (*v1Endpoint, error) {
tlsConfig, err := newTLSConfig(index.Name, index.Secure)
func newV1Endpoint(ctx context.Context, index *registry.IndexInfo, headers http.Header) (*v1Endpoint, error) {
tlsConfig, err := newTLSConfig(ctx, index.Name, index.Secure)
if err != nil {
return nil, err
}
@ -50,7 +51,10 @@ func newV1Endpoint(index *registry.IndexInfo, headers http.Header) (*v1Endpoint,
// Try HTTPS ping to registry
endpoint.URL.Scheme = "https"
if _, err := endpoint.ping(); err != nil {
if _, err := endpoint.ping(ctx); err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return nil, err
}
if endpoint.IsSecure {
// If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry`
// in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fall back to HTTP.
@ -58,9 +62,9 @@ func newV1Endpoint(index *registry.IndexInfo, headers http.Header) (*v1Endpoint,
}
// registry is insecure and HTTPS failed, fallback to HTTP.
log.G(context.TODO()).WithError(err).Debugf("error from registry %q marked as insecure - insecurely falling back to HTTP", endpoint)
log.G(ctx).WithError(err).Debugf("error from registry %q marked as insecure - insecurely falling back to HTTP", endpoint)
endpoint.URL.Scheme = "http"
if _, err2 := endpoint.ping(); err2 != nil {
if _, err2 := endpoint.ping(ctx); err2 != nil {
return nil, invalidParamf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2)
}
}
@ -109,7 +113,7 @@ func (e *v1Endpoint) String() string {
}
// ping returns a v1PingResult which indicates whether the registry is standalone or not.
func (e *v1Endpoint) ping() (v1PingResult, error) {
func (e *v1Endpoint) ping(ctx context.Context) (v1PingResult, error) {
if e.String() == IndexServer {
// Skip the check, we know this one is valid
// (and we never want to fallback to http in case of error)
@ -117,14 +121,17 @@ func (e *v1Endpoint) ping() (v1PingResult, error) {
}
pingURL := e.String() + "_ping"
log.G(context.TODO()).WithField("url", pingURL).Debug("attempting v1 ping for registry endpoint")
req, err := http.NewRequest(http.MethodGet, pingURL, nil)
log.G(ctx).WithField("url", pingURL).Debug("attempting v1 ping for registry endpoint")
req, err := http.NewRequestWithContext(ctx, http.MethodGet, pingURL, nil)
if err != nil {
return v1PingResult{}, invalidParam(err)
}
resp, err := e.client.Do(req)
if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return v1PingResult{}, err
}
return v1PingResult{}, invalidParam(err)
}
@ -136,7 +143,7 @@ func (e *v1Endpoint) ping() (v1PingResult, error) {
if v == "1" || strings.EqualFold(v, "true") {
info.Standalone = true
}
log.G(context.TODO()).Debugf("v1PingResult.Standalone (from X-Docker-Registry-Standalone header): %t", info.Standalone)
log.G(ctx).Debugf("v1PingResult.Standalone (from X-Docker-Registry-Standalone header): %t", info.Standalone)
return info, nil
}
@ -146,11 +153,11 @@ func (e *v1Endpoint) ping() (v1PingResult, error) {
Standalone: true,
}
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
log.G(context.TODO()).WithError(err).Debug("error unmarshaling _ping response")
log.G(ctx).WithError(err).Debug("error unmarshaling _ping response")
// don't stop here. Just assume sane defaults
}
log.G(context.TODO()).Debugf("v1PingResult.Standalone: %t", info.Standalone)
log.G(ctx).Debugf("v1PingResult.Standalone: %t", info.Standalone)
return info, nil
}

View File

@ -26,8 +26,8 @@ type session struct {
}
type authTransport struct {
http.RoundTripper
*registry.AuthConfig
base http.RoundTripper
authConfig *registry.AuthConfig
alwaysSetBasicAuth bool
token []string
@ -54,8 +54,8 @@ func newAuthTransport(base http.RoundTripper, authConfig *registry.AuthConfig, a
base = http.DefaultTransport
}
return &authTransport{
RoundTripper: base,
AuthConfig: authConfig,
base: base,
authConfig: authConfig,
alwaysSetBasicAuth: alwaysSetBasicAuth,
modReq: make(map[*http.Request]*http.Request),
}
@ -114,7 +114,7 @@ func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) {
// a 302 redirect is detected by looking at the Referrer header as go http package adds said header.
// This is safe as Docker doesn't set Referrer in other scenarios.
if orig.Header.Get("Referer") != "" && !trustedLocation(orig) {
return tr.RoundTripper.RoundTrip(orig)
return tr.base.RoundTrip(orig)
}
req := cloneRequest(orig)
@ -123,22 +123,22 @@ func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) {
tr.mu.Unlock()
if tr.alwaysSetBasicAuth {
if tr.AuthConfig == nil {
if tr.authConfig == nil {
return nil, errors.New("unexpected error: empty auth config")
}
req.SetBasicAuth(tr.Username, tr.Password)
return tr.RoundTripper.RoundTrip(req)
req.SetBasicAuth(tr.authConfig.Username, tr.authConfig.Password)
return tr.base.RoundTrip(req)
}
// Don't override
if req.Header.Get("Authorization") == "" {
if req.Header.Get("X-Docker-Token") == "true" && tr.AuthConfig != nil && len(tr.Username) > 0 {
req.SetBasicAuth(tr.Username, tr.Password)
if req.Header.Get("X-Docker-Token") == "true" && tr.authConfig != nil && len(tr.authConfig.Username) > 0 {
req.SetBasicAuth(tr.authConfig.Username, tr.authConfig.Password)
} else if len(tr.token) > 0 {
req.Header.Set("Authorization", "Token "+strings.Join(tr.token, ","))
}
}
resp, err := tr.RoundTripper.RoundTrip(req)
resp, err := tr.base.RoundTrip(req)
if err != nil {
tr.mu.Lock()
delete(tr.modReq, orig)
@ -164,7 +164,7 @@ func (tr *authTransport) CancelRequest(req *http.Request) {
type canceler interface {
CancelRequest(*http.Request)
}
if cr, ok := tr.RoundTripper.(canceler); ok {
if cr, ok := tr.base.(canceler); ok {
tr.mu.Lock()
modReq := tr.modReq[req]
delete(tr.modReq, req)
@ -179,7 +179,7 @@ func authorizeClient(client *http.Client, authConfig *registry.AuthConfig, endpo
// If we're working with a standalone private registry over HTTPS, send Basic Auth headers
// alongside all our requests.
if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" {
info, err := endpoint.ping()
info, err := endpoint.ping(context.TODO())
if err != nil {
return err
}
@ -213,7 +213,7 @@ func newSession(client *http.Client, endpoint *v1Endpoint) *session {
const defaultSearchLimit = 25
// searchRepositories performs a search against the remote repository
func (r *session) searchRepositories(term string, limit int) (*registry.SearchResults, error) {
func (r *session) searchRepositories(ctx context.Context, term string, limit int) (*registry.SearchResults, error) {
if limit == 0 {
limit = defaultSearchLimit
}
@ -221,9 +221,9 @@ func (r *session) searchRepositories(term string, limit int) (*registry.SearchRe
return nil, invalidParamf("limit %d is outside the range of [1, 100]", limit)
}
u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(fmt.Sprintf("%d", limit))
log.G(context.TODO()).WithField("url", u).Debug("searchRepositories")
log.G(ctx).WithField("url", u).Debug("searchRepositories")
req, err := http.NewRequest(http.MethodGet, u, nil)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if err != nil {
return nil, invalidParamWrapf(err, "error building request")
}

View File

@ -24,6 +24,9 @@ type Service struct {
// an engine.
func NewService(options ServiceOptions) (*Service, error) {
config, err := newServiceConfig(options)
if err != nil {
return nil, err
}
return &Service{config: config}, err
}
@ -103,6 +106,8 @@ func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, use
// ResolveRepository splits a repository name into its components
// and configuration of the associated registry.
//
// Deprecated: this function was only used internally and is no longer used. It will be removed in the next release.
func (s *Service) ResolveRepository(name reference.Named) (*RepositoryInfo, error) {
s.mu.RLock()
defer s.mu.RUnlock()
@ -110,12 +115,30 @@ func (s *Service) ResolveRepository(name reference.Named) (*RepositoryInfo, erro
return newRepositoryInfo(s.config, name), nil
}
// ResolveAuthConfig looks up authentication for the given reference from the
// given authConfigs.
//
// IMPORTANT: This function is for internal use and should not be used by external projects.
func (s *Service) ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, ref reference.Named) registry.AuthConfig {
s.mu.RLock()
defer s.mu.RUnlock()
// Simplified version of "newIndexInfo" without handling of insecure
// registries and mirrors, as we don't need that information to resolve
// the auth-config.
indexName := normalizeIndexName(reference.Domain(ref))
registryInfo, ok := s.config.IndexConfigs[indexName]
if !ok {
registryInfo = &registry.IndexInfo{Name: indexName}
}
return ResolveAuthConfig(authConfigs, registryInfo)
}
// APIEndpoint represents a remote API endpoint
type APIEndpoint struct {
Mirror bool
URL *url.URL
AllowNondistributableArtifacts bool // Deprecated: non-distributable artifacts are deprecated and enabled by default. This field will be removed in the next release.
Official bool
Official bool // Deprecated: this field was only used internally, and will be removed in the next release.
TrimHostname bool // Deprecated: hostname is now trimmed unconditionally for remote names. This field will be removed in the next release.
TLSConfig *tls.Config
}

View File

@ -1,6 +1,7 @@
package registry // import "github.com/docker/docker/registry"
import (
"context"
"net/url"
"strings"
@ -8,6 +9,7 @@ import (
)
func (s *Service) lookupV2Endpoints(hostname string, includeMirrors bool) ([]APIEndpoint, error) {
ctx := context.TODO()
var endpoints []APIEndpoint
if hostname == DefaultNamespace || hostname == IndexHostname {
if includeMirrors {
@ -19,7 +21,8 @@ func (s *Service) lookupV2Endpoints(hostname string, includeMirrors bool) ([]API
if err != nil {
return nil, invalidParam(err)
}
mirrorTLSConfig, err := newTLSConfig(mirrorURL.Host, s.config.isSecureIndex(mirrorURL.Host))
// TODO(thaJeztah); this should all be memoized when loading the config. We're resolving mirrors and loading TLS config every time.
mirrorTLSConfig, err := newTLSConfig(ctx, mirrorURL.Host, s.config.isSecureIndex(mirrorURL.Host))
if err != nil {
return nil, err
}
@ -39,7 +42,7 @@ func (s *Service) lookupV2Endpoints(hostname string, includeMirrors bool) ([]API
return endpoints, nil
}
tlsConfig, err := newTLSConfig(hostname, s.config.isSecureIndex(hostname))
tlsConfig, err := newTLSConfig(ctx, hostname, s.config.isSecureIndex(hostname))
if err != nil {
return nil, err
}