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