|
1 | 1 | import collections.abc |
2 | 2 | import logging |
| 3 | +import tarfile |
3 | 4 |
|
4 | 5 | import cnudie.access |
5 | 6 | import cnudie.retrieve_async |
@@ -183,7 +184,7 @@ def iter_content_for_resource_node( |
183 | 184 | access = resource_node.resource.access |
184 | 185 |
|
185 | 186 | if access.type is ocm.AccessType.OCI_REGISTRY: |
186 | | - return oci.image_layers_as_tarfile_generator( |
| 187 | + return image_layers_as_tarfile_generator( |
187 | 188 | image_reference=access.imageReference, |
188 | 189 | oci_client=oci_client, |
189 | 190 | include_config_blob=False, |
@@ -225,3 +226,70 @@ def iter_content_for_resource_node( |
225 | 226 |
|
226 | 227 | else: |
227 | 228 | raise RuntimeError(f'Unsupported access type: {access.type}') |
| 229 | + |
| 230 | + |
| 231 | +def image_layers_as_tarfile_generator( |
| 232 | + image_reference: str, |
| 233 | + oci_client: oci.client.Client, |
| 234 | + chunk_size=tarfile.RECORDSIZE, |
| 235 | + include_config_blob=True, |
| 236 | + fallback_to_first_subimage_if_index=False, |
| 237 | +) -> collections.abc.Generator[bytes, None, None]: |
| 238 | + ''' |
| 239 | + returns a generator yielding a tar-archive with the passed oci-image's layer-blobs as |
| 240 | + members. This is somewhat similar to the result of a `docker save` with the notable difference |
| 241 | + that the cfg-blob is discarded. |
| 242 | + This function is useful to e.g. upload file system contents of an oci-container-image to some |
| 243 | + scanning-tool (provided it supports the extraction of tar-archives) |
| 244 | + If include_config_blob is set to False the config blob will be ignored. |
| 245 | +
|
| 246 | + If fallback_to_first_subimage_if_index is set to True, in case of oci-image-manifest-list the |
| 247 | + first sub-manifest is taken. |
| 248 | + ''' |
| 249 | + manifest = oci_client.manifest( |
| 250 | + image_reference=image_reference, |
| 251 | + accept=oci.model.MimeTypes.prefer_multiarch, |
| 252 | + ) |
| 253 | + |
| 254 | + image_reference = oci.model.OciImageReference.to_image_ref(image_reference) |
| 255 | + |
| 256 | + if fallback_to_first_subimage_if_index and isinstance(manifest, oci.model.OciImageManifestList): |
| 257 | + logger.warn( |
| 258 | + f'image-index handling not fully implemented - will only scan first image, ' |
| 259 | + f'{image_reference=}, {manifest.mediaType=}' |
| 260 | + ) |
| 261 | + manifest_ref = manifest.manifests[0] |
| 262 | + manifest = oci_client.manifest( |
| 263 | + image_reference=f'{image_reference.ref_without_tag}@{manifest_ref.digest}', |
| 264 | + ) |
| 265 | + |
| 266 | + blob_refs = manifest.blobs() if include_config_blob else manifest.layers |
| 267 | + |
| 268 | + if not include_config_blob: |
| 269 | + logger.debug('skipping config blob') |
| 270 | + |
| 271 | + def resolve_blob( |
| 272 | + blob_ref: oci.model.OciBlobRef, |
| 273 | + image_reference: str, |
| 274 | + ) -> ioutil.BlobDescriptor: |
| 275 | + content = oci_client.blob( |
| 276 | + image_reference=image_reference, |
| 277 | + digest=blob_ref.digest, |
| 278 | + stream=True, |
| 279 | + ).iter_content(chunk_size=chunk_size) |
| 280 | + |
| 281 | + return ioutil.BlobDescriptor( |
| 282 | + content=content, |
| 283 | + size=blob_ref.size, |
| 284 | + name=f'{blob_ref.digest}.tar', |
| 285 | + ) |
| 286 | + |
| 287 | + return tarutil.concat_blobs_as_tarstream( |
| 288 | + blobs=( |
| 289 | + resolve_blob( |
| 290 | + blob_ref=blob_ref, |
| 291 | + image_reference=image_reference, |
| 292 | + ) |
| 293 | + for blob_ref in blob_refs |
| 294 | + ), |
| 295 | + ) |
0 commit comments