66import logging
77import os
88import tempfile
9+ import time
910
1011import ci .log
1112import cnudie .retrieve
1213import oci .client
14+ import requests .exceptions
1315
1416import bdba .client
1517import bdba .model
@@ -185,6 +187,31 @@ def generate_sbom_with_bdba(
185187 return sbom_result
186188
187189
190+ def find_existing_sbom_metadata (
191+ artefact : odg .model .ComponentArtefactId ,
192+ delivery_service_client : odg_client .DeliveryServiceClient ,
193+ ) -> dict | None :
194+ """
195+ Query for existing SBOM metadata for the given artefact.
196+
197+ Returns the metadata entry if found, None otherwise.
198+ Failures are logged and treated as "no existing SBOM" (safe fallback).
199+ """
200+ try :
201+ metadata_entries = delivery_service_client .query_metadata (
202+ artefacts = [artefact ],
203+ type = odg .model .Datatype .ARTEFACT_SCAN_INFO ,
204+ datasource = odg .model .Datasource .SBOM_GENERATOR ,
205+ )
206+
207+ if metadata_entries :
208+ return metadata_entries [0 ]
209+ return None
210+ except Exception as e :
211+ logger .warning (f'Failed to query existing SBOM metadata for { artefact } : { e } ' )
212+ return None
213+
214+
188215def generate_sbom_for_artefact (
189216 artefact : odg .model .ComponentArtefactId ,
190217 extension_cfg : odg .extensions_cfg .SBOMGeneratorConfig ,
@@ -193,7 +220,7 @@ def generate_sbom_for_artefact(
193220 oci_client : oci .client .Client ,
194221 secret_factory : secret_mgmt .SecretFactory ,
195222 ** kwargs ,
196- ) -> SBOM :
223+ ) -> SBOM | None :
197224 """
198225 Generates Software Bill of Materials (SBOM) for a component artefact.
199226 Resolves the component descriptor from OCM repositories,
@@ -226,6 +253,30 @@ def generate_sbom_for_artefact(
226253 )
227254 return
228255
256+ # Check for existing SBOM and handle based on on_exist configuration
257+ existing_metadata = find_existing_sbom_metadata (
258+ artefact = artefact ,
259+ delivery_service_client = delivery_service_client ,
260+ )
261+
262+ old_digest = None
263+ if existing_metadata :
264+ old_digest = existing_metadata .get ('data' , {}).get ('digest' )
265+
266+ if extension_cfg .on_exist is odg .model .OnExist .SKIP :
267+ logger .info (
268+ f'SBOM already exists for { artefact } , skipping generation (digest: { old_digest } )' ,
269+ )
270+ return None
271+
272+ logger .info (
273+ f'SBOM exists for { artefact } , will overwrite and clean up old blob '
274+ f'(old digest: { old_digest } )' ,
275+ )
276+
277+ else :
278+ logger .info (f'No existing SBOM found for { artefact } , generating new one' )
279+
229280 logger .info (f'Scanning using mode { extension_cfg .generation_mode } ' )
230281
231282 match extension_cfg .generation_mode :
@@ -333,6 +384,68 @@ def generate_sbom_for_artefact(
333384 ],
334385 )
335386
387+ if digest == old_digest :
388+ # digest remained stable, shortcut blob deletion
389+ return sbom_result
390+
391+ # Clean up old blob if overwriting
392+ if old_digest and extension_cfg .on_exist is odg .model .OnExist .OVERWRITE :
393+ max_retries = 3
394+ retry_delay = 5 # Start with 5 seconds
395+
396+ def handle_retry_logic (
397+ error_msg : str ,
398+ current_attempt : int ,
399+ current_delay : int ,
400+ ) -> int :
401+ """Handle retry logic for blob deletion failures. Returns updated delay."""
402+ if current_attempt < max_retries - 1 :
403+ logger .warning (
404+ f'Failed to delete old SBOM blob { old_digest } for { artefact } '
405+ f'(attempt { current_attempt + 1 } /{ max_retries } ): { error_msg } . '
406+ f'Retrying in { current_delay } seconds...' ,
407+ )
408+ time .sleep (current_delay )
409+ return current_delay * 2 # Exponential backoff
410+ else :
411+ logger .error (
412+ f'Failed to delete old SBOM blob { old_digest } for { artefact } '
413+ f'after { max_retries } attempts: { error_msg } . '
414+ 'The blob remains orphaned in storage.' ,
415+ )
416+ return current_delay
417+
418+ for attempt in range (max_retries ):
419+ try :
420+ logger .info (
421+ f'Deleting old SBOM blob: { old_digest } (attempt { attempt + 1 } /{ max_retries } )' ,
422+ )
423+ delivery_service_client .delete_blob (digest = old_digest )
424+ logger .info (f'Successfully deleted old SBOM blob: { old_digest } ' )
425+ break # Success, exit retry loop
426+
427+ except requests .exceptions .HTTPError as e :
428+ # Check if it's a 404 - blob already deleted, mission accomplished
429+ if e .response is not None and e .response .status_code == 404 :
430+ logger .info (
431+ f'Old SBOM blob { old_digest } not found (404), '
432+ 'assuming already deleted - cleanup goal achieved' ,
433+ )
434+ break # No need to retry, blob is gone
435+
436+ error_msg = f'HTTP { e .response .status_code if e .response else "error" } - { e } '
437+ retry_delay = handle_retry_logic (
438+ error_msg = error_msg ,
439+ current_attempt = attempt ,
440+ current_delay = retry_delay ,
441+ )
442+ except Exception as e :
443+ retry_delay = handle_retry_logic (
444+ error_msg = str (e ),
445+ current_attempt = attempt ,
446+ current_delay = retry_delay ,
447+ )
448+
336449 return sbom_result
337450
338451
0 commit comments