send empty X-Registry-Auth for anonymous pushes

Since Docker 28.3.3 the daemon rejects a push that carries no
`X-Registry-Auth` header [1]. The SDK already sets this header when it
finds credentials, so the breakage happens only on anonymous pushes.

During `PushTask.push_image()` we now check whether the SDK can resolve
credentials for the target registry; if it cannot, we inject
`auth_config={}`, causing the SDK to send the minimal "{}" header that
satisfies the daemon while leaving authenticated pushes unchanged.

Drop this addition when [2] is fixed.

[1] https://github.com/moby/moby/pull/50371
[2] https://github.com/docker/docker-py/issues/3348

Closes-Bug: #2119619

Change-Id: I7a2f3fce223afd74741b40bf62836b325fca5b19
Signed-off-by: Bartosz Bezak <bartosz@stackhpc.com>
(cherry picked from commit ddac7ca1ed)
This commit is contained in:
Bartosz Bezak
2025-08-04 11:31:26 +02:00
committed by Michal Nasiadka
parent b83d409a23
commit aa554d1e8e
2 changed files with 25 additions and 5 deletions

View File

@@ -119,6 +119,16 @@ class PushTask(EngineTask):
def push_image(self, image):
kwargs = dict(stream=True, decode=True)
# NOTE(bbezak): Docker ≥ 28.3.3 rejects a push with no
# X-Registry-Auth header (moby/moby#50371, docker-py#3348).
# If the SDK cannot find creds for this registry, we inject
# an empty {} so the daemon still accepts the request.
# TODO(bbezak): Remove fallback once docker-py handles empty auth
if self.conf.engine == engine.Engine.DOCKER.value:
from docker.auth import resolve_authconfig
if not resolve_authconfig(self.engine_client.api._auth_configs,
registry=self.conf.registry):
kwargs.setdefault("auth_config", {})
for response in self.engine_client.images.push(image.canonical_name,
**kwargs):

View File

@@ -81,10 +81,12 @@ class TasksTest(base.TestCase):
@mock.patch(engine_client)
def test_push_image(self, mock_client):
self.engine_client = mock_client
mock_client().api._auth_configs = {}
pusher = tasks.PushTask(self.conf, self.image)
pusher.run()
mock_client().images.push.assert_called_once_with(
self.image.canonical_name, decode=True, stream=True)
self.image.canonical_name,
decode=True, stream=True, auth_config={})
self.assertTrue(pusher.success)
@mock.patch.dict(os.environ, clear=True)
@@ -92,11 +94,13 @@ class TasksTest(base.TestCase):
def test_push_image_failure(self, mock_client):
"""failure on connecting Docker API"""
self.engine_client = mock_client
mock_client().api._auth_configs = {}
mock_client().images.push.side_effect = Exception
pusher = tasks.PushTask(self.conf, self.image)
pusher.run()
mock_client().images.push.assert_called_once_with(
self.image.canonical_name, decode=True, stream=True)
self.image.canonical_name,
decode=True, stream=True, auth_config={})
self.assertFalse(pusher.success)
self.assertEqual(utils.Status.PUSH_ERROR, self.image.status)
@@ -105,11 +109,13 @@ class TasksTest(base.TestCase):
def test_push_image_failure_retry(self, mock_client):
"""failure on connecting Docker API, success on retry"""
self.engine_client = mock_client
mock_client().api._auth_configs = {}
mock_client().images.push.side_effect = [Exception, []]
pusher = tasks.PushTask(self.conf, self.image)
pusher.run()
mock_client().images.push.assert_called_once_with(
self.image.canonical_name, decode=True, stream=True)
self.image.canonical_name,
decode=True, stream=True, auth_config={})
self.assertFalse(pusher.success)
self.assertEqual(utils.Status.PUSH_ERROR, self.image.status)
@@ -125,12 +131,14 @@ class TasksTest(base.TestCase):
def test_push_image_failure_error(self, mock_client):
"""Docker connected, failure to push"""
self.engine_client = mock_client
mock_client().api._auth_configs = {}
mock_client().images.push.return_value = [{'errorDetail': {'message':
'mock push fail'}}]
pusher = tasks.PushTask(self.conf, self.image)
pusher.run()
mock_client().images.push.assert_called_once_with(
self.image.canonical_name, decode=True, stream=True)
self.image.canonical_name,
decode=True, stream=True, auth_config={})
self.assertFalse(pusher.success)
self.assertEqual(utils.Status.PUSH_ERROR, self.image.status)
@@ -139,12 +147,14 @@ class TasksTest(base.TestCase):
def test_push_image_failure_error_retry(self, mock_client):
"""Docker connected, failure to push, success on retry"""
self.engine_client = mock_client
mock_client().api._auth_configs = {}
mock_client().images.push.return_value = [{'errorDetail': {'message':
'mock push fail'}}]
pusher = tasks.PushTask(self.conf, self.image)
pusher.run()
mock_client().images.push.assert_called_once_with(
self.image.canonical_name, decode=True, stream=True)
self.image.canonical_name,
decode=True, stream=True, auth_config={})
self.assertFalse(pusher.success)
self.assertEqual(utils.Status.PUSH_ERROR, self.image.status)