From 437ce1467c818ce49394a10a74a73c3d3b034b81 Mon Sep 17 00:00:00 2001 From: Julia Kreger <juliaashleykreger@gmail.com> Date: Tue, 11 Feb 2025 06:59:34 -0800 Subject: [PATCH] OCI: Send the auth header to IPA This change takes the identified authorization header and sends it in the command to IPA as an argument. This enables a future IPA patch to recognize an authorization rejection, and to leverage the header to authenticate to the remote image service. Also addresses a case where we neglect to preserve the auth token in the case of a container URL reference with digest value and adds a corresponding test which didn't exist either. Change-Id: I8346eb56e90a5a3e2bc68a9e5cd345121f734245 --- ironic/common/image_service.py | 2 + ironic/drivers/modules/agent.py | 6 +++ .../tests/unit/common/test_image_service.py | 50 +++++++++++++++++++ .../tests/unit/drivers/modules/test_agent.py | 10 ++++ 4 files changed, 68 insertions(+) diff --git a/ironic/common/image_service.py b/ironic/common/image_service.py index c514a6e0ef..f656f18cab 100644 --- a/ironic/common/image_service.py +++ b/ironic/common/image_service.py @@ -615,6 +615,7 @@ class OciImageService(BaseImageService): # Identify the blob URL from the defining manifest for IPA. image_url = self._client.get_blob_url(image_href, manifest['digest']) + cached_auth = self._client.get_cached_auth() return { # Return an OCI url in case Ironic is doing the download 'oci_image_manifest_url': image_href, @@ -627,6 +628,7 @@ class OciImageService(BaseImageService): # We can't look up, we're pointed at a manifest URL # with limited information. 'image_disk_format': 'unknown', + 'image_request_authorization_secret': cached_auth, } # Query the remote API for a list index list of manifests diff --git a/ironic/drivers/modules/agent.py b/ironic/drivers/modules/agent.py index cc4d107c42..525d0ce8c2 100644 --- a/ironic/drivers/modules/agent.py +++ b/ironic/drivers/modules/agent.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import base64 from urllib import parse as urlparse from oslo_log import log @@ -682,6 +683,11 @@ class AgentDeploy(CustomAgentDeploy): image_info['os_hash_value'] = node.instance_info[ 'image_os_hash_value'] + if node.instance_info.get('image_request_authorization_secret'): + ah = node.instance_info.get('image_request_authorization_secret') + ah = base64.standard_b64encode(ah.encode()) + image_info['image_request_authorization'] = ah + proxies = {} for scheme in ('http', 'https'): proxy_param = 'image_%s_proxy' % scheme diff --git a/ironic/tests/unit/common/test_image_service.py b/ironic/tests/unit/common/test_image_service.py index fd619b5a0f..2623614981 100644 --- a/ironic/tests/unit/common/test_image_service.py +++ b/ironic/tests/unit/common/test_image_service.py @@ -949,6 +949,56 @@ class OciImageServiceTestCase(base.TestCase): 'sha256:f2981621c1bf821ce44c1cb31c507abe6293d8eea646b029c6b9' 'dc773fa7821a') + @mock.patch.object(ociclient, 'get_manifest', autospec=True) + @mock.patch.object(ociclient, 'get_artifact_index', autospec=True) + def test_identify_specific_image_specific_digest( + self, mock_get_artifact_index, mock_get_manifest): + + mock_get_manifest.return_value = { + 'schemaVersion': 2, + 'mediaType': 'application/vnd.oci.image.manifest.v1+json', + 'config': { + 'mediaType': 'application/vnd.oci.empty.v1+json', + 'digest': ('sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21' + 'fe77e8310c060f61caaff8a'), + 'size': 2, + 'data': 'e30='}, + 'layers': [ + { + 'mediaType': 'application/zstd', + 'digest': ('sha256:047caa9c410038075055e1e41d520fc975a097' + '97838541174fa3066e58ebd8ea'), + 'size': 1060062418, + 'annotations': { + 'org.opencontainers.image.title': ('podman-machine.' + 'x86_64.applehv.' + 'raw.zst')} + } + ] + } + + expected_data = { + 'image_checksum': ('047caa9c410038075055e1e41d520fc975a0979783' + '8541174fa3066e58ebd8ea'), + 'image_disk_format': 'unknown', + 'image_request_authorization_secret': None, + 'image_url': ('https://localhost/v2/podman/machine-os/blobs/' + 'sha256:047caa9c410038075055e1e41d520fc975a097' + '97838541174fa3066e58ebd8ea'), + 'oci_image_manifest_url': ('oci://localhost/podman/machine-os' + '@sha256:9d046091b3dbeda26e1f4364a' + '116ca8d94284000f103da7310e3a4703d' + 'f1d3e4') + } + url = ('oci://localhost/podman/machine-os@sha256:9d046091b3dbeda26e' + '1f4364a116ca8d94284000f103da7310e3a4703df1d3e4') + img_data = self.service.identify_specific_image( + url, cpu_arch='amd64') + self.assertEqual(expected_data, img_data) + mock_get_artifact_index.assert_not_called() + mock_get_manifest.assert_called_once_with( + mock.ANY, url) + @mock.patch.object(ociclient, 'get_manifest', autospec=True) @mock.patch.object(ociclient, 'get_artifact_index', autospec=True) diff --git a/ironic/tests/unit/drivers/modules/test_agent.py b/ironic/tests/unit/drivers/modules/test_agent.py index 3e75b608c8..1bde0cfe0f 100644 --- a/ironic/tests/unit/drivers/modules/test_agent.py +++ b/ironic/tests/unit/drivers/modules/test_agent.py @@ -1438,6 +1438,16 @@ class TestAgentDeploy(CommonTestsMixin, db_base.DbTestCase): } ) + def test_write_image_oci_authorization(self): + i_info = self.node.instance_info + i_info['image_request_authorization_secret'] = 'Bearer f00' + self.node.instance_info = i_info + self._test_write_image( + additional_expected_image_info={ + 'image_request_authorization': b'QmVhcmVyIGYwMA==' + } + ) + def test_write_image_partition_image(self): self.node.provision_state = states.DEPLOYWAIT self.node.target_provision_state = states.ACTIVE