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:
committed by
Michal Nasiadka
parent
b83d409a23
commit
aa554d1e8e
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user