mirror of
https://github.com/podman-container-tools/podman.git
synced 2026-09-15 03:57:55 +00:00
Merge pull request #29475 from Luap99/v6.1
[v6.1] bump common and image and backport WSL port forwarding fix
This commit is contained in:
commit
80bd5deabf
12 changed files with 459 additions and 84 deletions
4
go.mod
4
go.mod
|
|
@ -65,8 +65,8 @@ require (
|
|||
github.com/vishvananda/netlink v1.3.1
|
||||
go.etcd.io/bbolt v1.5.0
|
||||
go.podman.io/buildah v1.45.0
|
||||
go.podman.io/common v0.69.0
|
||||
go.podman.io/image/v5 v5.41.0
|
||||
go.podman.io/common v0.69.1
|
||||
go.podman.io/image/v5 v5.41.1
|
||||
go.podman.io/storage v1.64.0
|
||||
golang.org/x/crypto v0.54.0
|
||||
golang.org/x/net v0.57.0
|
||||
|
|
|
|||
8
go.sum
8
go.sum
|
|
@ -435,10 +435,10 @@ go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/
|
|||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.podman.io/buildah v1.45.0 h1:/LAS2Sgf+xmRoLVo5j52HjEPL+g5HKjomnCvCk42iAU=
|
||||
go.podman.io/buildah v1.45.0/go.mod h1:cTVrqG7Hu24euHntdC31/sU2nvqPoLmHmpkz+JIl2Lw=
|
||||
go.podman.io/common v0.69.0 h1:tAM8gJRCgoPmyfXnYP+6VRfOQ6JySgafpY28OhDZpWc=
|
||||
go.podman.io/common v0.69.0/go.mod h1:g1iVXP+Xn/8He+96qpJw75pu054FRDQMiDGf2VKKspg=
|
||||
go.podman.io/image/v5 v5.41.0 h1:xOan4jlwbT4R5qhe3ruDAGJLnOC2z0eR61WhZjHbe4E=
|
||||
go.podman.io/image/v5 v5.41.0/go.mod h1:wfgAlfczPK4ZZw/wn/Av2iJfb3z9Vv+RKWgjHNShzZk=
|
||||
go.podman.io/common v0.69.1 h1:hTIv8wzHJfHAMMZRocjv1849TiWrf5HDhH0eD74qJqY=
|
||||
go.podman.io/common v0.69.1/go.mod h1:g1iVXP+Xn/8He+96qpJw75pu054FRDQMiDGf2VKKspg=
|
||||
go.podman.io/image/v5 v5.41.1 h1:iPrhIt7/aNfRBMuzoxbMUVpQmR7Q75ahnrzkIC4JWLQ=
|
||||
go.podman.io/image/v5 v5.41.1/go.mod h1:wfgAlfczPK4ZZw/wn/Av2iJfb3z9Vv+RKWgjHNShzZk=
|
||||
go.podman.io/storage v1.64.0 h1:ryHCZO+Zl0ZG7OTHF+Qc4C0zLNuoe9W35zgE7mh2pYw=
|
||||
go.podman.io/storage v1.64.0/go.mod h1:ft0DzCRMZs1yn0rszmLYXEEwgd7IIc66JRwSRPUDwwc=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ func (c *Container) bindPorts() ([]*os.File, error) {
|
|||
if !c.runtime.config.Engine.EnablePortReservation || rootless.IsRootless() || !c.config.NetMode.IsBridge() {
|
||||
return nil, nil
|
||||
}
|
||||
return bindPorts(c.convertPortMappings())
|
||||
return bindPorts(c.convertPortMappings(), c.runtime.config.Engine.ForcePortListen)
|
||||
}
|
||||
|
||||
// convertPortMappings will remove the HostIP part from the ports when running inside podman machine.
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ type ociError struct {
|
|||
}
|
||||
|
||||
// Bind ports to keep them closed on the host
|
||||
func bindPorts(ports []types.PortMapping) ([]*os.File, error) {
|
||||
func bindPorts(ports []types.PortMapping, forceListen bool) ([]*os.File, error) {
|
||||
var files []*os.File
|
||||
sctpWarning := true
|
||||
for _, port := range ports {
|
||||
|
|
@ -42,7 +42,7 @@ func bindPorts(ports []types.PortMapping) ([]*os.File, error) {
|
|||
protocols := strings.SplitSeq(port.Protocol, ",")
|
||||
for protocol := range protocols {
|
||||
for i := uint16(0); i < port.Range; i++ {
|
||||
f, err := bindPort(protocol, port.HostIP, port.HostPort+i, isV6, &sctpWarning)
|
||||
f, err := bindPort(protocol, port.HostIP, port.HostPort+i, isV6, &sctpWarning, forceListen)
|
||||
if err != nil {
|
||||
// close all open ports in case of early error so we do not
|
||||
// rely on the garbage collector to close them
|
||||
|
|
@ -60,9 +60,9 @@ func bindPorts(ports []types.PortMapping) ([]*os.File, error) {
|
|||
return files, nil
|
||||
}
|
||||
|
||||
// bindPort reserves a port on the host using socket+bind without listen.
|
||||
// bindPort reserves a port on the host using socket+bind, listen() only when listen is set to true
|
||||
// Dual-stack bind by default unless hostIP is specified.
|
||||
func bindPort(protocol, hostIP string, port uint16, isV6 bool, sctpWarning *bool) (*os.File, error) {
|
||||
func bindPort(protocol, hostIP string, port uint16, isV6 bool, sctpWarning *bool, forceListen bool) (*os.File, error) {
|
||||
switch protocol {
|
||||
case "tcp", "udp":
|
||||
sockType := unix.SOCK_STREAM
|
||||
|
|
@ -94,6 +94,13 @@ func bindPort(protocol, hostIP string, port uint16, isV6 bool, sctpWarning *bool
|
|||
return nil, fmt.Errorf("cannot bind %s port %s: %w", protocol, net.JoinHostPort(hostIP, strconv.FormatUint(uint64(port), 10)), err)
|
||||
}
|
||||
|
||||
if forceListen && sockType == unix.SOCK_STREAM {
|
||||
if err := unix.Listen(fd, 0); err != nil {
|
||||
unix.Close(fd)
|
||||
return nil, fmt.Errorf("cannot listen on %s port %s: %w", protocol, net.JoinHostPort(hostIP, strconv.FormatUint(uint64(port), 10)), err)
|
||||
}
|
||||
}
|
||||
|
||||
return os.NewFile(uintptr(fd), fmt.Sprintf("reservation-%s-%d", protocol, port)), nil
|
||||
|
||||
case "sctp":
|
||||
|
|
|
|||
|
|
@ -1257,4 +1257,25 @@ EOF
|
|||
run_podman rm -f -t0 $cid1
|
||||
}
|
||||
|
||||
# bats test_tags=ci:parallel
|
||||
@test "podman run - test force_port_listen containers.conf option" {
|
||||
skip_if_rootless "force_port_listen is only used as root"
|
||||
skip_if_remote "force_port_listen would need to be set on the server side"
|
||||
myport=$(random_free_port)
|
||||
cname="c1-$(safename)"
|
||||
|
||||
containersconf=$PODMAN_TMPDIR/containers.conf
|
||||
cat >$containersconf <<EOF
|
||||
[engine]
|
||||
force_port_listen = true
|
||||
EOF
|
||||
|
||||
CONTAINERS_CONF_OVERRIDE=$containersconf run_podman run -d --name $cname -p $myport:8080 $IMAGE sleep inf
|
||||
|
||||
run -0 ss -Hlpn sport = $myport
|
||||
assert "$output" =~ "tcp[[:blank:]]LISTEN.*\*:$myport" "tcp port should be in LISTEN state"
|
||||
|
||||
run_podman rm -f -t0 $cname
|
||||
}
|
||||
|
||||
# vim: filetype=sh
|
||||
|
|
|
|||
21
vendor/go.podman.io/common/pkg/config/config.go
generated
vendored
21
vendor/go.podman.io/common/pkg/config/config.go
generated
vendored
|
|
@ -300,9 +300,12 @@ type EngineConfig struct {
|
|||
// host when they are forwarded to containers. When enabled, when ports are
|
||||
// forwarded to containers, they are held open by conmon as long as the
|
||||
// container is running, ensuring that they cannot be reused by other
|
||||
// programs on the host. However, this can cause significant memory usage if
|
||||
// a container has many ports forwarded to it. Disabling this can save
|
||||
// memory.
|
||||
// programs on the host. However, this can cause increased memory usage
|
||||
// if a container has many ports forwarded to it. Disabling this can save
|
||||
// memory but is not recommended as port conflicts are not noticed.
|
||||
// Note this option is only used for rootful container when ports are
|
||||
// forwarded via firewall rules. Rootless containers using pasta(1)
|
||||
// always have to bind each port.
|
||||
EnablePortReservation bool `toml:"enable_port_reservation,omitempty"`
|
||||
|
||||
// Environment variables to be used when running the container engine (e.g., Podman, Buildah). For example "http_proxy=internal.proxy.company.com"
|
||||
|
|
@ -323,6 +326,18 @@ type EngineConfig struct {
|
|||
// information about the container.
|
||||
EventsContainerCreateInspectData bool `toml:"events_container_create_inspect_data,omitempty"`
|
||||
|
||||
// ForcePortListen forces the port reservation to use listen().
|
||||
// For rootful containers when reserving the ports Podman will not use listen()
|
||||
// on TCP ports by default as connections are forwarded via firewall rules.
|
||||
// This is done to ensure if the firewall rule is bypassed, i.e. a connection
|
||||
// to "::1", the connection will be correctly refused and not hang forever as we
|
||||
// never accept() them.
|
||||
// For applications which only monitor the listening state of a socket it means they
|
||||
// will not see the bound port then. In that case this option can be set to true in
|
||||
// order to force Podman to also call listen() on the ports.
|
||||
// Also see the **enable_port_reservation** option.
|
||||
ForcePortListen bool `toml:"force_port_listen,omitempty"`
|
||||
|
||||
// graphRoot internal stores the location of the graphroot
|
||||
graphRoot string
|
||||
|
||||
|
|
|
|||
18
vendor/go.podman.io/common/pkg/config/containers.conf
generated
vendored
18
vendor/go.podman.io/common/pkg/config/containers.conf
generated
vendored
|
|
@ -529,8 +529,10 @@ default_sysctls = [
|
|||
# forwarded to containers. When enabled, when ports are forwarded to containers,
|
||||
# ports are held open by as long as the container is running, ensuring that
|
||||
# they cannot be reused by other programs on the host. However, this can cause
|
||||
# significant memory usage if a container has many ports forwarded to it.
|
||||
# Disabling this can save memory.
|
||||
# increased memory usage if a container has many ports forwarded to it.
|
||||
# Disabling this can save memory but is not recommended as port conflicts are not noticed.
|
||||
# Note this option is only used for rootful container when ports are forwarded via firewall rules.
|
||||
# Rootless containers using pasta(1) always have to bind each port.
|
||||
#
|
||||
#enable_port_reservation = true
|
||||
|
||||
|
|
@ -566,6 +568,18 @@ default_sysctls = [
|
|||
# with detailed information about the container.
|
||||
#events_container_create_inspect_data = false
|
||||
|
||||
# For rootful containers when reserving the ports Podman will not use listen()
|
||||
# on TCP ports by default as connections are forwarded via firewall rules.
|
||||
# This is done to ensure if the firewall rule is bypassed, i.e. a connection
|
||||
# to "::1", the connection will be correctly refused and not hang forever as we
|
||||
# never accept() them.
|
||||
# For applications which only monitor the listening state of a socket it means they
|
||||
# will not see the bound port then. In that case this option can be set to true in
|
||||
# order to force Podman to also call listen() on the ports.
|
||||
# Also see the enable_port_reservation option.
|
||||
#
|
||||
#force_port_listen=false
|
||||
|
||||
# Whenever Podman should log healthcheck events.
|
||||
# With many running healthcheck on short interval Podman will spam the event
|
||||
# log a lot as it generates a event for each single healthcheck run. Because
|
||||
|
|
|
|||
18
vendor/go.podman.io/common/pkg/config/containers.conf-freebsd
generated
vendored
18
vendor/go.podman.io/common/pkg/config/containers.conf-freebsd
generated
vendored
|
|
@ -372,8 +372,10 @@ default_sysctls = [
|
|||
# forwarded to containers. When enabled, when ports are forwarded to containers,
|
||||
# ports are held open by as long as the container is running, ensuring that
|
||||
# they cannot be reused by other programs on the host. However, this can cause
|
||||
# significant memory usage if a container has many ports forwarded to it.
|
||||
# Disabling this can save memory.
|
||||
# increased memory usage if a container has many ports forwarded to it.
|
||||
# Disabling this can save memory but is not recommended as port conflicts are not noticed.
|
||||
# Note this option is only used for rootful container when ports are forwarded via firewall rules.
|
||||
# Rootless containers using pasta(1) always have to bind each port.
|
||||
#
|
||||
#enable_port_reservation = true
|
||||
|
||||
|
|
@ -405,6 +407,18 @@ default_sysctls = [
|
|||
#
|
||||
#events_logger = "file"
|
||||
|
||||
# For rootful containers when reserving the ports Podman will not use listen()
|
||||
# on TCP ports by default as connections are forwarded via firewall rules.
|
||||
# This is done to ensure if the firewall rule is bypassed, i.e. a connection
|
||||
# to "::1", the connection will be correctly refused and not hang forever as we
|
||||
# never accept() them.
|
||||
# For applications which only monitor the listening state of a socket it means they
|
||||
# will not see the bound port then. In that case this option can be set to true in
|
||||
# order to force Podman to also call listen() on the ports.
|
||||
# Also see the enable_port_reservation option.
|
||||
#
|
||||
#force_port_listen=false
|
||||
|
||||
# Whenever Podman should log healthcheck events.
|
||||
# With many running healthcheck on short interval Podman will spam the event
|
||||
# log a lot as it generates a event for each single healthcheck run. Because
|
||||
|
|
|
|||
2
vendor/go.podman.io/common/version/version.go
generated
vendored
2
vendor/go.podman.io/common/version/version.go
generated
vendored
|
|
@ -1,4 +1,4 @@
|
|||
package version
|
||||
|
||||
// Version is the version of the build.
|
||||
const Version = "0.69.0"
|
||||
const Version = "0.69.1"
|
||||
|
|
|
|||
428
vendor/go.podman.io/image/v5/docker/docker_image_src.go
generated
vendored
428
vendor/go.podman.io/image/v5/docker/docker_image_src.go
generated
vendored
|
|
@ -8,15 +8,20 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/registry/api/errcode"
|
||||
v2 "github.com/docker/distribution/registry/api/v2"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
"github.com/sirupsen/logrus"
|
||||
"go.podman.io/image/v5/docker/reference"
|
||||
|
|
@ -48,6 +53,31 @@ type dockerImageSource struct {
|
|||
// State
|
||||
cachedManifest []byte // nil if not loaded yet
|
||||
cachedManifestMIMEType string // Only valid if cachedManifest != nil
|
||||
|
||||
// Mirror fallback: when a blob fetch fails with a fallback-worthy error,
|
||||
// try remaining pull sources before giving up. Protected by mirrorMu.
|
||||
mirrorMu sync.Mutex
|
||||
mirrorOverride *mirrorSource // If non-nil, this is the mirror and physicalRef for the override client
|
||||
prevOverrides []*dockerClient // mirror clients replaced by a newer mirrorOverride; kept open because other goroutines may still be reading blobs through them, closed in Close()
|
||||
remainingSources []sysregistriesv2.PullSource // later pull sources not yet tried, consumed front-to-back during fallback; nil when selected source is already the original source.
|
||||
fallbackSys *types.SystemContext
|
||||
fallbackRegConfig *registryConfiguration
|
||||
}
|
||||
|
||||
type mirrorSource struct {
|
||||
client *dockerClient
|
||||
ref dockerReference
|
||||
}
|
||||
|
||||
type pullEndpoint struct {
|
||||
client *dockerClient
|
||||
ref dockerReference
|
||||
endpointSys *types.SystemContext
|
||||
}
|
||||
|
||||
type sourceAttempt struct {
|
||||
ref reference.Named
|
||||
err error
|
||||
}
|
||||
|
||||
// newImageSource creates a new ImageSource for the specified image reference.
|
||||
|
|
@ -86,12 +116,8 @@ func newImageSource(ctx context.Context, sys *types.SystemContext, ref dockerRef
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
type attempt struct {
|
||||
ref reference.Named
|
||||
err error
|
||||
}
|
||||
attempts := []attempt{}
|
||||
for _, pullSource := range pullSources {
|
||||
attempts := []sourceAttempt{}
|
||||
for i, pullSource := range pullSources {
|
||||
if sys != nil && sys.DockerLogMirrorChoice {
|
||||
logrus.Infof("Trying to access %q", pullSource.Reference)
|
||||
} else {
|
||||
|
|
@ -99,10 +125,15 @@ func newImageSource(ctx context.Context, sys *types.SystemContext, ref dockerRef
|
|||
}
|
||||
s, err := newImageSourceAttempt(ctx, sys, ref, pullSource, registryConfig)
|
||||
if err == nil {
|
||||
if i+1 < len(pullSources) {
|
||||
s.remainingSources = pullSources[i+1:]
|
||||
s.fallbackSys = sys
|
||||
s.fallbackRegConfig = registryConfig
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
logrus.Debugf("Accessing %q failed: %v", pullSource.Reference, err)
|
||||
attempts = append(attempts, attempt{
|
||||
attempts = append(attempts, sourceAttempt{
|
||||
ref: pullSource.Reference,
|
||||
err: err,
|
||||
})
|
||||
|
|
@ -131,6 +162,63 @@ func newImageSource(ctx context.Context, sys *types.SystemContext, ref dockerRef
|
|||
func newImageSourceAttempt(ctx context.Context, sys *types.SystemContext, logicalRef dockerReference, pullSource sysregistriesv2.PullSource,
|
||||
registryConfig *registryConfiguration,
|
||||
) (*dockerImageSource, error) {
|
||||
endpoint, err := newPullClient(sys, logicalRef, pullSource, registryConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := &dockerImageSource{
|
||||
PropertyMethodsInitialize: impl.PropertyMethods(impl.Properties{
|
||||
HasThreadSafeGetBlob: true,
|
||||
}),
|
||||
|
||||
logicalRef: logicalRef,
|
||||
physicalRef: endpoint.ref,
|
||||
c: endpoint.client,
|
||||
}
|
||||
s.Compat = impl.AddCompat(s)
|
||||
|
||||
if err := s.ensureManifestIsLoaded(ctx); err != nil {
|
||||
endpoint.client.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if h, err := sysregistriesv2.AdditionalLayerStoreAuthHelper(endpoint.endpointSys); err == nil && h != "" {
|
||||
acf := map[string]struct {
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
IdentityToken string `json:"identityToken,omitempty"`
|
||||
}{
|
||||
endpoint.ref.ref.String(): {
|
||||
Username: endpoint.client.auth.Username,
|
||||
Password: endpoint.client.auth.Password,
|
||||
IdentityToken: endpoint.client.auth.IdentityToken,
|
||||
},
|
||||
}
|
||||
acfD, err := json.Marshal(acf)
|
||||
if err != nil {
|
||||
logrus.Warnf("failed to marshal auth config: %v", err)
|
||||
} else {
|
||||
cmd := exec.Command(h)
|
||||
cmd.Stdin = bytes.NewReader(acfD)
|
||||
if err := cmd.Run(); err != nil {
|
||||
var stderr string
|
||||
if ee, ok := err.(*exec.ExitError); ok {
|
||||
stderr = string(ee.Stderr)
|
||||
}
|
||||
logrus.Warnf("Failed to call additional-layer-store-auth-helper (stderr:%s): %v", stderr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// newPullClient creates a dockerClient, reference, and endpoint-specific SystemContext
|
||||
// for the given pull source.
|
||||
// The caller must call client.Close() when done.
|
||||
func newPullClient(sys *types.SystemContext, logicalRef dockerReference, pullSource sysregistriesv2.PullSource,
|
||||
registryConfig *registryConfiguration,
|
||||
) (*pullEndpoint, error) {
|
||||
physicalRef, err := newReference(pullSource.Reference, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -152,50 +240,7 @@ func newImageSourceAttempt(ctx context.Context, sys *types.SystemContext, logica
|
|||
client.tlsClientConfig.InsecureSkipVerify = pullSource.Endpoint.Insecure
|
||||
client.namespaceProxy = pullSource.Endpoint.NamespaceProxy
|
||||
|
||||
s := &dockerImageSource{
|
||||
PropertyMethodsInitialize: impl.PropertyMethods(impl.Properties{
|
||||
HasThreadSafeGetBlob: true,
|
||||
}),
|
||||
|
||||
logicalRef: logicalRef,
|
||||
physicalRef: physicalRef,
|
||||
c: client,
|
||||
}
|
||||
s.Compat = impl.AddCompat(s)
|
||||
|
||||
if err := s.ensureManifestIsLoaded(ctx); err != nil {
|
||||
client.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if h, err := sysregistriesv2.AdditionalLayerStoreAuthHelper(endpointSys); err == nil && h != "" {
|
||||
acf := map[string]struct {
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
IdentityToken string `json:"identityToken,omitempty"`
|
||||
}{
|
||||
physicalRef.ref.String(): {
|
||||
Username: client.auth.Username,
|
||||
Password: client.auth.Password,
|
||||
IdentityToken: client.auth.IdentityToken,
|
||||
},
|
||||
}
|
||||
acfD, err := json.Marshal(acf)
|
||||
if err != nil {
|
||||
logrus.Warnf("failed to marshal auth config: %v", err)
|
||||
} else {
|
||||
cmd := exec.Command(h)
|
||||
cmd.Stdin = bytes.NewReader(acfD)
|
||||
if err := cmd.Run(); err != nil {
|
||||
var stderr string
|
||||
if ee, ok := err.(*exec.ExitError); ok {
|
||||
stderr = string(ee.Stderr)
|
||||
}
|
||||
logrus.Warnf("Failed to call additional-layer-store-auth-helper (stderr:%s): %v", stderr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
return &pullEndpoint{client: client, ref: physicalRef, endpointSys: endpointSys}, nil
|
||||
}
|
||||
|
||||
// Reference returns the reference used to set up this source, _as specified by the user_
|
||||
|
|
@ -206,6 +251,18 @@ func (s *dockerImageSource) Reference() types.ImageReference {
|
|||
|
||||
// Close removes resources associated with an initialized ImageSource, if any.
|
||||
func (s *dockerImageSource) Close() error {
|
||||
s.mirrorMu.Lock()
|
||||
prev := s.prevOverrides
|
||||
override := s.mirrorOverride
|
||||
s.prevOverrides = nil
|
||||
s.mirrorOverride = nil
|
||||
s.mirrorMu.Unlock()
|
||||
for _, m := range prev {
|
||||
m.Close()
|
||||
}
|
||||
if override != nil {
|
||||
override.client.Close()
|
||||
}
|
||||
return s.c.Close()
|
||||
}
|
||||
|
||||
|
|
@ -416,9 +473,88 @@ func (s *dockerImageSource) GetBlobAt(ctx context.Context, info types.BlobInfo,
|
|||
if err := info.Digest.Validate(); err != nil { // Make sure info.Digest.String() does not contain any unexpected characters
|
||||
return nil, nil, err
|
||||
}
|
||||
path := fmt.Sprintf(blobsPath, reference.Path(s.physicalRef.ref), info.Digest.String())
|
||||
|
||||
client, physRef, hasRemaining := s.getActiveSource()
|
||||
streams, errs, err := tryGetBlobAt(ctx, client, physRef, info, chunks, headers, hasRemaining)
|
||||
if err == nil || !hasRemaining {
|
||||
return streams, errs, err
|
||||
}
|
||||
if isMirrorTransientError(err) || isMirrorFallbackError(err) {
|
||||
logrus.Debugf("Partial blob %s fetch from %q failed (%v), trying fallback sources", info.Digest, physRef.ref, err)
|
||||
return s.getBlobAtWithMirrorFallback(ctx, info, chunks, headers, err, client)
|
||||
}
|
||||
return streams, errs, err
|
||||
}
|
||||
|
||||
// GetBlob returns a stream for the specified blob, and the blob’s size (or -1 if unknown).
|
||||
// The Digest field in BlobInfo is guaranteed to be provided, Size may be -1 and MediaType may be optionally provided.
|
||||
// May update BlobInfoCache, preferably after it knows for certain that a blob truly exists at a specific location.
|
||||
func (s *dockerImageSource) GetBlob(ctx context.Context, info types.BlobInfo, cache types.BlobInfoCache) (io.ReadCloser, int64, error) {
|
||||
client, physRef, hasRemaining := s.getActiveSource()
|
||||
reader, size, err := tryGetBlob(ctx, client, physRef, info, cache, hasRemaining)
|
||||
if err == nil || !hasRemaining {
|
||||
return reader, size, err
|
||||
}
|
||||
if isMirrorTransientError(err) || isMirrorFallbackError(err) {
|
||||
logrus.Debugf("Blob %s fetch from %q failed (%v), trying fallback sources", info.Digest, physRef.ref, err)
|
||||
return s.getBlobWithMirrorFallback(ctx, info, cache, err, client)
|
||||
}
|
||||
return reader, size, err
|
||||
}
|
||||
|
||||
func (s *dockerImageSource) getActiveSource() (*dockerClient, dockerReference, bool) {
|
||||
s.mirrorMu.Lock()
|
||||
defer s.mirrorMu.Unlock()
|
||||
hasRemaining := len(s.remainingSources) > 0
|
||||
if s.mirrorOverride != nil {
|
||||
return s.mirrorOverride.client, s.mirrorOverride.ref, hasRemaining
|
||||
}
|
||||
return s.c, s.physicalRef, hasRemaining
|
||||
}
|
||||
|
||||
// tryGetBlob attempts to fetch a blob, retrying once on transient errors if fallback mirrors remain.
|
||||
func tryGetBlob(ctx context.Context, client *dockerClient, physRef dockerReference,
|
||||
info types.BlobInfo, cache types.BlobInfoCache, hasRemainingSources bool,
|
||||
) (io.ReadCloser, int64, error) {
|
||||
reader, size, err := client.getBlob(ctx, physRef, info, cache)
|
||||
if err != nil && hasRemainingSources && isMirrorTransientError(err) {
|
||||
logrus.Debugf("Transient error fetching blob %s from %q, retrying: %v", info.Digest, physRef.ref, err)
|
||||
delay := time.Second + rand.N(time.Second/10) // same as retry.IfNecessary first attempt: 1s + 10% jitter
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, 0, fmt.Errorf("%w (while retrying after: %v)", ctx.Err(), err)
|
||||
case <-time.After(delay):
|
||||
}
|
||||
reader, size, err = client.getBlob(ctx, physRef, info, cache)
|
||||
}
|
||||
return reader, size, err
|
||||
}
|
||||
|
||||
// tryGetBlobAt attempts to fetch a partial blob (range request), retrying once on transient errors
|
||||
// if fallback mirrors remain. Mirrors tryGetBlob for the partial-pull (zstd:chunked) path.
|
||||
func tryGetBlobAt(ctx context.Context, client *dockerClient, physRef dockerReference,
|
||||
info types.BlobInfo, chunks []private.ImageSourceChunk, headers map[string][]string, hasRemainingSources bool,
|
||||
) (chan io.ReadCloser, chan error, error) {
|
||||
streams, errs, err := fetchBlobAt(ctx, client, physRef, info, chunks, headers)
|
||||
if err != nil && hasRemainingSources && isMirrorTransientError(err) {
|
||||
logrus.Debugf("Transient error fetching partial blob %s from %q, retrying: %v", info.Digest, physRef.ref, err)
|
||||
delay := time.Second + rand.N(time.Second/10)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, nil, fmt.Errorf("%w (while retrying after: %v)", ctx.Err(), err)
|
||||
case <-time.After(delay):
|
||||
}
|
||||
streams, errs, err = fetchBlobAt(ctx, client, physRef, info, chunks, headers)
|
||||
}
|
||||
return streams, errs, err
|
||||
}
|
||||
|
||||
func fetchBlobAt(ctx context.Context, client *dockerClient, physRef dockerReference,
|
||||
info types.BlobInfo, chunks []private.ImageSourceChunk, headers map[string][]string,
|
||||
) (chan io.ReadCloser, chan error, error) {
|
||||
path := fmt.Sprintf(blobsPath, reference.Path(physRef.ref), info.Digest.String())
|
||||
logrus.Debugf("Downloading %s", path)
|
||||
res, err := s.c.makeRequest(ctx, http.MethodGet, path, headers, nil, v2Auth, nil)
|
||||
res, err := client.makeRequest(ctx, http.MethodGet, path, headers, nil, v2Auth, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
|
@ -436,27 +572,195 @@ func (s *dockerImageSource) GetBlobAt(ctx context.Context, info types.BlobInfo,
|
|||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
streams := make(chan io.ReadCloser)
|
||||
errs := make(chan error)
|
||||
|
||||
go handle206Response(streams, errs, res.Body, chunks, mediaType, params)
|
||||
return streams, errs, nil
|
||||
case http.StatusBadRequest:
|
||||
res.Body.Close()
|
||||
return nil, nil, private.BadPartialRequestError{Status: res.Status}
|
||||
default:
|
||||
err := registryHTTPResponseToError(res)
|
||||
httpErr := registryHTTPResponseToError(res)
|
||||
res.Body.Close()
|
||||
return nil, nil, fmt.Errorf("fetching partial blob: %w", err)
|
||||
return nil, nil, fmt.Errorf("fetching partial blob: %w", httpErr)
|
||||
}
|
||||
}
|
||||
|
||||
// GetBlob returns a stream for the specified blob, and the blob’s size (or -1 if unknown).
|
||||
// The Digest field in BlobInfo is guaranteed to be provided, Size may be -1 and MediaType may be optionally provided.
|
||||
// May update BlobInfoCache, preferably after it knows for certain that a blob truly exists at a specific location.
|
||||
func (s *dockerImageSource) GetBlob(ctx context.Context, info types.BlobInfo, cache types.BlobInfoCache) (io.ReadCloser, int64, error) {
|
||||
return s.c.getBlob(ctx, s.physicalRef, info, cache)
|
||||
// isMirrorFallbackError returns true for errors where the blob is not present
|
||||
// on this source but may be available from another — warranting a fallback attempt.
|
||||
// All tested registries (docker.io, registry.redhat.io, quay.io, registry.access.redhat.com)
|
||||
// return BLOB_UNKNOWN for blob 404s, matching the OCI distribution spec.
|
||||
func isMirrorFallbackError(err error) bool {
|
||||
var ec errcode.ErrorCoder
|
||||
if errors.As(err, &ec) {
|
||||
switch ec.ErrorCode() {
|
||||
case v2.ErrorCodeBlobUnknown:
|
||||
// Blob endpoint returns HTTP 404 with errcode JSON body
|
||||
// {"errors":[{"code":"BLOB_UNKNOWN",...}]}.
|
||||
// registryHTTPResponseToError calls handleErrorResponse → parseHTTPErrorResponse,
|
||||
// then unwraps errcode.Errors to the first errcode.Error.
|
||||
return true
|
||||
case errcode.ErrorCodeTooManyRequests:
|
||||
// makeRequestToResolvedURL already retried 5 times with exponential
|
||||
// backoff. This source's rate limit is exhausted — skip straight to
|
||||
// the next mirror which has its own rate limit budget.
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// UnexpectedHTTPStatusError (capital U) is NOT matched here — for regular blobs,
|
||||
// handleErrorResponse never produces it for 4xx (only for 5xx). For external blobs,
|
||||
// getExternalBlob() produces it via newUnexpectedHTTPStatusError() directly for any
|
||||
// non-200 including 404, but external URLs are fixed and mirror-independent —
|
||||
// a different registry mirror cannot serve them.
|
||||
return false
|
||||
}
|
||||
|
||||
// isMirrorTransientError returns true for errors that are transient; the source
|
||||
// probably has the blob but temporarily cannot serve it.
|
||||
func isMirrorTransientError(err error) bool {
|
||||
// HTTP 5xx: handleErrorResponse returns UnexpectedHTTPStatusError for status
|
||||
// codes outside 400–499. Server-side error, another mirror may succeed.
|
||||
var httpErr UnexpectedHTTPStatusError
|
||||
if errors.As(err, &httpErr) && httpErr.StatusCode >= 500 {
|
||||
return true
|
||||
}
|
||||
|
||||
// Network timeout: makeRequest returns net.Error with Timeout() == true.
|
||||
// The mirror is reachable but slow — worth retrying on another.
|
||||
var netErr net.Error
|
||||
return errors.As(err, &netErr) && netErr.Timeout()
|
||||
}
|
||||
|
||||
// retireStaleOverride checks if the current mirrorOverride was set by another
|
||||
// goroutine and differs from failedClient. If so, it returns the override's
|
||||
// client and ref for the caller to retry. If the override is the same client
|
||||
// that already failed, or is nil, it returns nil. Must be called with mirrorMu held.
|
||||
func (s *dockerImageSource) retireStaleOverride(failedClient *dockerClient) (*dockerClient, dockerReference, bool) {
|
||||
if s.mirrorOverride != nil && s.mirrorOverride.client != failedClient {
|
||||
return s.mirrorOverride.client, s.mirrorOverride.ref, true
|
||||
}
|
||||
return nil, dockerReference{}, false
|
||||
}
|
||||
|
||||
// retireCurrentOverride moves the current mirrorOverride to prevOverrides.
|
||||
// Must be called with mirrorMu held.
|
||||
func (s *dockerImageSource) retireCurrentOverride() {
|
||||
if s.mirrorOverride != nil {
|
||||
s.prevOverrides = append(s.prevOverrides, s.mirrorOverride.client)
|
||||
s.mirrorOverride = nil
|
||||
}
|
||||
}
|
||||
|
||||
func formatFallbackError(attempts []sourceAttempt, originalErr error) error {
|
||||
if len(attempts) == 0 {
|
||||
return originalErr
|
||||
}
|
||||
extras := make([]string, 0, len(attempts))
|
||||
for _, a := range attempts {
|
||||
extras = append(extras, fmt.Sprintf("[%s: %v]", a.ref.String(), a.err))
|
||||
}
|
||||
return fmt.Errorf("(Fallback sources also failed: %s): %w", strings.Join(extras, "\n"), originalErr)
|
||||
}
|
||||
|
||||
func (s *dockerImageSource) getBlobWithMirrorFallback(ctx context.Context, info types.BlobInfo, cache types.BlobInfoCache, originalErr error, failedClient *dockerClient) (io.ReadCloser, int64, error) {
|
||||
// Held for the full fallback loop, including network I/O. This serializes
|
||||
// concurrent blob fetchers during fallback — only one goroutine probes
|
||||
// sources while the rest block on getActiveSource(). Acceptable trade-off:
|
||||
// fallback is rare and serialization prevents thundering-herd probing.
|
||||
s.mirrorMu.Lock()
|
||||
defer s.mirrorMu.Unlock()
|
||||
|
||||
if client, physRef, ok := s.retireStaleOverride(failedClient); ok {
|
||||
reader, size, err := tryGetBlob(ctx, client, physRef, info, cache, len(s.remainingSources) > 0)
|
||||
if err == nil {
|
||||
return reader, size, nil
|
||||
}
|
||||
s.retireCurrentOverride()
|
||||
}
|
||||
|
||||
attempts := []sourceAttempt{}
|
||||
for len(s.remainingSources) > 0 {
|
||||
pullSource := s.remainingSources[0]
|
||||
s.remainingSources = s.remainingSources[1:]
|
||||
|
||||
logrus.Debugf("Trying to access %q", pullSource.Reference)
|
||||
|
||||
fallbackEndpoint, clientErr := newPullClient(s.fallbackSys, s.logicalRef, pullSource, s.fallbackRegConfig)
|
||||
if clientErr != nil {
|
||||
logrus.Debugf("Accessing %q failed: %v", pullSource.Reference, clientErr)
|
||||
attempts = append(attempts, sourceAttempt{ref: pullSource.Reference, err: clientErr})
|
||||
continue
|
||||
}
|
||||
|
||||
reader, size, err := tryGetBlob(ctx, fallbackEndpoint.client, fallbackEndpoint.ref, info, cache, len(s.remainingSources) > 0)
|
||||
if err != nil {
|
||||
fallbackEndpoint.client.Close()
|
||||
logrus.Debugf("Accessing %q failed: %v", pullSource.Reference, err)
|
||||
attempts = append(attempts, sourceAttempt{ref: pullSource.Reference, err: err})
|
||||
if !isMirrorTransientError(err) && !isMirrorFallbackError(err) {
|
||||
// non-transient errors are unlikely to be resolved by trying another source: stop probing further.
|
||||
// break here to log the attempts and return originalErr below.
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
s.retireCurrentOverride()
|
||||
s.mirrorOverride = &mirrorSource{client: fallbackEndpoint.client, ref: fallbackEndpoint.ref}
|
||||
logrus.Debugf("Blob fetch succeeded from fallback source %q, switching to it for future requests", pullSource.Reference)
|
||||
|
||||
return reader, size, nil
|
||||
}
|
||||
return nil, 0, formatFallbackError(attempts, originalErr)
|
||||
}
|
||||
|
||||
func (s *dockerImageSource) getBlobAtWithMirrorFallback(ctx context.Context, info types.BlobInfo, chunks []private.ImageSourceChunk, headers map[string][]string, originalErr error, failedClient *dockerClient) (chan io.ReadCloser, chan error, error) {
|
||||
s.mirrorMu.Lock()
|
||||
defer s.mirrorMu.Unlock()
|
||||
|
||||
if client, physRef, ok := s.retireStaleOverride(failedClient); ok {
|
||||
streams, errs, err := tryGetBlobAt(ctx, client, physRef, info, chunks, headers, len(s.remainingSources) > 0)
|
||||
if err == nil {
|
||||
return streams, errs, nil
|
||||
}
|
||||
s.retireCurrentOverride()
|
||||
}
|
||||
|
||||
attempts := []sourceAttempt{}
|
||||
for len(s.remainingSources) > 0 {
|
||||
pullSource := s.remainingSources[0]
|
||||
s.remainingSources = s.remainingSources[1:]
|
||||
|
||||
logrus.Debugf("Trying to access %q", pullSource.Reference)
|
||||
|
||||
fallbackEndpoint, clientErr := newPullClient(s.fallbackSys, s.logicalRef, pullSource, s.fallbackRegConfig)
|
||||
if clientErr != nil {
|
||||
logrus.Debugf("Accessing %q failed: %v", pullSource.Reference, clientErr)
|
||||
attempts = append(attempts, sourceAttempt{ref: pullSource.Reference, err: clientErr})
|
||||
continue
|
||||
}
|
||||
|
||||
streams, errs, err := tryGetBlobAt(ctx, fallbackEndpoint.client, fallbackEndpoint.ref, info, chunks, headers, len(s.remainingSources) > 0)
|
||||
if err != nil {
|
||||
fallbackEndpoint.client.Close()
|
||||
logrus.Debugf("Accessing %q failed: %v", pullSource.Reference, err)
|
||||
attempts = append(attempts, sourceAttempt{ref: pullSource.Reference, err: err})
|
||||
if !isMirrorTransientError(err) && !isMirrorFallbackError(err) {
|
||||
// non-transient errors are unlikely to be resolved by trying another source: stop probing further.
|
||||
// break here to log the attempts and return originalErr below.
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
s.retireCurrentOverride()
|
||||
s.mirrorOverride = &mirrorSource{client: fallbackEndpoint.client, ref: fallbackEndpoint.ref}
|
||||
logrus.Debugf("Partial blob fetch succeeded from fallback source %q, switching to it for future requests", pullSource.Reference)
|
||||
|
||||
return streams, errs, nil
|
||||
}
|
||||
return nil, nil, formatFallbackError(attempts, originalErr)
|
||||
}
|
||||
|
||||
// GetSignaturesWithFormat returns the image's signatures. It may use a remote (= slow) service.
|
||||
|
|
|
|||
2
vendor/go.podman.io/image/v5/version/version.go
generated
vendored
2
vendor/go.podman.io/image/v5/version/version.go
generated
vendored
|
|
@ -8,7 +8,7 @@ const (
|
|||
// VersionMinor is for functionality in a backwards-compatible manner
|
||||
VersionMinor = 41
|
||||
// VersionPatch is for backwards-compatible bug fixes
|
||||
VersionPatch = 0
|
||||
VersionPatch = 1
|
||||
|
||||
// VersionDev indicates development branch. Releases will be empty string.
|
||||
VersionDev = ""
|
||||
|
|
|
|||
4
vendor/modules.txt
vendored
4
vendor/modules.txt
vendored
|
|
@ -777,7 +777,7 @@ go.podman.io/buildah/pkg/sshagent
|
|||
go.podman.io/buildah/pkg/util
|
||||
go.podman.io/buildah/pkg/volumes
|
||||
go.podman.io/buildah/util
|
||||
# go.podman.io/common v0.69.0
|
||||
# go.podman.io/common v0.69.1
|
||||
## explicit; go 1.25.7
|
||||
go.podman.io/common/internal
|
||||
go.podman.io/common/libimage
|
||||
|
|
@ -843,7 +843,7 @@ go.podman.io/common/pkg/umask
|
|||
go.podman.io/common/pkg/util
|
||||
go.podman.io/common/pkg/version
|
||||
go.podman.io/common/version
|
||||
# go.podman.io/image/v5 v5.41.0
|
||||
# go.podman.io/image/v5 v5.41.1
|
||||
## explicit; go 1.25.7
|
||||
go.podman.io/image/v5/copy
|
||||
go.podman.io/image/v5/directory
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue