3232
3333_PR_TAG_RE = re .compile (r"-pr\d+" )
3434
35+ # Stable release tags follow calendar versioning, e.g. "26.3.0". Pre-release tags
36+ # such as "26.3.0-rc1" or "24.11.0-test1" carry a suffix and are excluded.
37+ _STABLE_RELEASE_RE = re .compile (r"^\d+\.\d+\.\d+$" )
38+
3539# Additional images to scan that are not part of the regular versioned release.
3640# These are third-party or infrastructure images referenced by the Stackable platform.
3741ADDITIONAL_IMAGES = [
@@ -144,6 +148,28 @@ def get_harbor_tags(
144148 return recent_tags , latest_tag
145149
146150
151+ def get_latest_releases (count : int , docker_images_dir : str = "docker-images" ) -> list [str ]:
152+ """Return the most recent stable SDP release tags from the docker-images repo.
153+
154+ Releases are calendar-versioned git tags (e.g. "26.3.0"). Pre-release tags
155+ such as "26.3.0-rc1" are ignored. Tags are fetched first so the result is
156+ not limited to whatever was already checked out locally.
157+ """
158+ subprocess .run (["git" , "fetch" , "--tags" , "--force" ], cwd = docker_images_dir )
159+
160+ result = subprocess .run (
161+ ["git" , "tag" ],
162+ cwd = docker_images_dir ,
163+ capture_output = True ,
164+ text = True ,
165+ check = True ,
166+ )
167+
168+ releases = [tag for tag in result .stdout .split () if _STABLE_RELEASE_RE .match (tag )]
169+ releases .sort (key = lambda tag : tuple (int (part ) for part in tag .split ("." )))
170+ return releases [- count :]
171+
172+
147173def get_latest_github_release (owner : str , repo : str ) -> str | None :
148174 """Fetch the tag name of the latest GitHub release for a repository."""
149175 url = f"https://api.github.com/repos/{ owner } /{ repo } /releases/latest"
@@ -334,11 +360,13 @@ def scan_additional_images(secobserve_api_token: str) -> None:
334360
335361
336362def main ():
337- if len (sys .argv ) < 4 :
363+ if len (sys .argv ) < 3 :
338364 print (
339365 "Usage:\n "
340366 "python main.py scan-release <secobserve_api_token> <release>\n "
341367 "or\n "
368+ "python main.py scan-latest <secobserve_api_token>\n "
369+ "or\n "
342370 "python main.py scan-image <secobserve_api_token> <image> <product_name> <product_version>"
343371 )
344372 sys .exit (1 )
@@ -356,111 +384,126 @@ def main():
356384 product_name = sys .argv [4 ]
357385 scan_image (secobserve_api_token , image , product_name , sys .argv [5 ])
358386 sys .exit (0 )
387+ elif sys .argv [1 ] == "scan-latest" :
388+ # Scan the dev release plus the current and previous stable releases.
389+ # The stable releases are discovered dynamically from the docker-images
390+ # git tags so the workflow does not need updating on every release.
391+ secobserve_api_token = sys .argv [2 ]
392+ releases = [DEV_RELEASE ] + get_latest_releases (2 )
393+ print (f"Scanning releases: { releases } " )
394+ for release in releases :
395+ scan_release (secobserve_api_token , release )
396+ sys .exit (0 )
359397 else :
360398 secobserve_api_token = sys .argv [2 ]
361399 release = sys .argv [3 ]
362- checkout = "main" if release == DEV_RELEASE else "tags/" + release
363-
364- subprocess .run (["git" , "fetch" , "--all" ], cwd = "docker-images" )
365- subprocess .run (["git" , "checkout" , checkout ], cwd = "docker-images" )
366- subprocess .run (["git" , "pull" ], cwd = "docker-images" )
367-
368- operators = [
369- "airflow" ,
370- "commons" ,
371- "druid" ,
372- "hbase" ,
373- "hdfs" ,
374- "hive" ,
375- "kafka" ,
376- "listener" ,
377- "nifi" ,
378- "opa" ,
379- "opensearch" ,
380- "secret" ,
381- "spark-k8s" ,
382- "superset" ,
383- "trino" ,
384- "zookeeper" ,
385- ]
386-
387- # Load product version configuration once, outside the arch loop.
388- conf_py_path = "docker-images/conf.py"
389- if os .path .exists (conf_py_path ):
390- print ("Using conf.py based configuration" )
391- sys .path .insert (0 , os .path .abspath ("docker-images" ))
392- from image_tools .args import load_configuration
393- product_versions_config = load_configuration (conf_py_path )
394- use_conf_py = True
395- else :
396- print ("Using boil based configuration" )
397- # boil >= 0.2.0 uses "image list", older versions use "show images"
400+ scan_release (secobserve_api_token , release )
401+
402+
403+ def scan_release (secobserve_api_token : str , release : str ) -> None :
404+ """Scan all operator and product images of a single SDP release."""
405+ checkout = "main" if release == DEV_RELEASE else "tags/" + release
406+
407+ subprocess .run (["git" , "fetch" , "--all" ], cwd = "docker-images" )
408+ subprocess .run (["git" , "checkout" , checkout ], cwd = "docker-images" )
409+ subprocess .run (["git" , "pull" ], cwd = "docker-images" )
410+
411+ operators = [
412+ "airflow" ,
413+ "commons" ,
414+ "druid" ,
415+ "hbase" ,
416+ "hdfs" ,
417+ "hive" ,
418+ "kafka" ,
419+ "listener" ,
420+ "nifi" ,
421+ "opa" ,
422+ "opensearch" ,
423+ "secret" ,
424+ "spark-k8s" ,
425+ "superset" ,
426+ "trino" ,
427+ "zookeeper" ,
428+ ]
429+
430+ # Load product version configuration once, outside the arch loop.
431+ conf_py_path = "docker-images/conf.py"
432+ if os .path .exists (conf_py_path ):
433+ print ("Using conf.py based configuration" )
434+ sys .path .insert (0 , os .path .abspath ("docker-images" ))
435+ from image_tools .args import load_configuration
436+ product_versions_config = load_configuration (conf_py_path )
437+ use_conf_py = True
438+ else :
439+ print ("Using boil based configuration" )
440+ # boil >= 0.2.0 uses "image list", older versions use "show images"
441+ result = subprocess .run (
442+ ["cargo" , "boil" , "image" , "list" ],
443+ cwd = "docker-images" ,
444+ capture_output = True ,
445+ text = True ,
446+ )
447+ if result .returncode != 0 :
398448 result = subprocess .run (
399- ["cargo" , "boil" , "image " , "list " ],
449+ ["cargo" , "boil" , "show " , "images " ],
400450 cwd = "docker-images" ,
401451 capture_output = True ,
402452 text = True ,
403453 )
404- if result .returncode != 0 :
405- result = subprocess .run (
406- ["cargo" , "boil" , "show" , "images" ],
407- cwd = "docker-images" ,
408- capture_output = True ,
409- text = True ,
410- )
411- if result .returncode != 0 :
412- print ("Failed to get product versions:" , result .stderr )
413- sys .exit (1 )
414- product_versions = json .loads (result .stdout )
415- use_conf_py = False
416-
417- for arch in ["amd64" , "arm64" ]:
418- for operator_name in operators :
419- product_name = f"{ operator_name } -operator"
420- scan_image (
421- secobserve_api_token ,
422- f"{ REGISTRY_URL } /sdp/{ product_name } :{ release } -{ arch } " ,
423- product_name ,
424- f"{ release } -{ arch } " ,
425- )
426-
427- if use_conf_py :
428- for product in product_versions_config .products :
429- product_name : str = product ["name" ]
430- if product_name in excluded_products :
431- continue
432- for version_dict in product .get ("versions" , []):
433- version : str = version_dict ["product" ]
434- product_version = f"{ version } -stackable{ release } "
435- scan_image (
436- secobserve_api_token ,
437- f"{ REGISTRY_URL } /sdp/{ product_name } :{ product_version } -{ arch } " ,
438- product_name ,
439- f"{ product_version } -{ arch } " ,
440- )
441- else :
442- for product_name , versions in product_versions .items ():
443- if product_name in excluded_products :
444- continue
445- for version in versions :
446- product_version = f"{ version } -stackable{ release } "
447- scan_image (
448- secobserve_api_token ,
449- f"{ REGISTRY_URL } /sdp/{ product_name } :{ product_version } -{ arch } " ,
450- product_name ,
451- f"{ product_version } -{ arch } " ,
452- )
453-
454- # Scan additional infrastructure/third-party images using Harbor API tag discovery.
455- # This runs once (not per-arch) because tags from Harbor include the arch suffix
456- # already or are arch-agnostic manifests.
457- scan_additional_images (secobserve_api_token )
458-
459- # Scan the latest stackablectl binary from GitHub releases.
460- # Only run for the dev release to avoid redundant scans when multiple releases
461- # are processed in the same workflow run (stackablectl is release-independent).
462- if release == DEV_RELEASE :
463- scan_stackablectl (secobserve_api_token )
454+ if result .returncode != 0 :
455+ print ("Failed to get product versions:" , result .stderr )
456+ sys .exit (1 )
457+ product_versions = json .loads (result .stdout )
458+ use_conf_py = False
459+
460+ for arch in ["amd64" , "arm64" ]:
461+ for operator_name in operators :
462+ product_name = f"{ operator_name } -operator"
463+ scan_image (
464+ secobserve_api_token ,
465+ f"{ REGISTRY_URL } /sdp/{ product_name } :{ release } -{ arch } " ,
466+ product_name ,
467+ f"{ release } -{ arch } " ,
468+ )
469+
470+ if use_conf_py :
471+ for product in product_versions_config .products :
472+ product_name : str = product ["name" ]
473+ if product_name in excluded_products :
474+ continue
475+ for version_dict in product .get ("versions" , []):
476+ version : str = version_dict ["product" ]
477+ product_version = f"{ version } -stackable{ release } "
478+ scan_image (
479+ secobserve_api_token ,
480+ f"{ REGISTRY_URL } /sdp/{ product_name } :{ product_version } -{ arch } " ,
481+ product_name ,
482+ f"{ product_version } -{ arch } " ,
483+ )
484+ else :
485+ for product_name , versions in product_versions .items ():
486+ if product_name in excluded_products :
487+ continue
488+ for version in versions :
489+ product_version = f"{ version } -stackable{ release } "
490+ scan_image (
491+ secobserve_api_token ,
492+ f"{ REGISTRY_URL } /sdp/{ product_name } :{ product_version } -{ arch } " ,
493+ product_name ,
494+ f"{ product_version } -{ arch } " ,
495+ )
496+
497+ # Scan additional infrastructure/third-party images using Harbor API tag discovery.
498+ # This runs once (not per-arch) because tags from Harbor include the arch suffix
499+ # already or are arch-agnostic manifests.
500+ scan_additional_images (secobserve_api_token )
501+
502+ # Scan the latest stackablectl binary from GitHub releases.
503+ # Only run for the dev release to avoid redundant scans when multiple releases
504+ # are processed in the same workflow run (stackablectl is release-independent).
505+ if release == DEV_RELEASE :
506+ scan_stackablectl (secobserve_api_token )
464507
465508
466509def scan_image (
0 commit comments