Skip to content

Commit 6562a1b

Browse files
committed
feat: scan dev plus latest two releases dynamically
1 parent ff2a4cd commit 6562a1b

2 files changed

Lines changed: 143 additions & 102 deletions

File tree

.github/workflows/scan.yaml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,5 @@ jobs:
3030
chmod +x /usr/local/bin/cyclonedx
3131
- name: Install deps
3232
run: poetry install
33-
- name: Scan dev images
34-
run: poetry run python stack_scanner/main.py scan-release ${{ secrets.SECOBSERVE_API_TOKEN }} 0.0.0-dev
35-
- name: Scan 26.3.0
36-
run: poetry run python stack_scanner/main.py scan-release ${{ secrets.SECOBSERVE_API_TOKEN }} 26.3.0
33+
- name: Scan dev plus current and previous release
34+
run: poetry run python stack_scanner/main.py scan-latest ${{ secrets.SECOBSERVE_API_TOKEN }}

stack_scanner/main.py

Lines changed: 141 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
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.
3741
ADDITIONAL_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+
147173
def 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

336362
def 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

466509
def scan_image(

0 commit comments

Comments
 (0)