This is an automated email from the ASF dual-hosted git repository.
pierrejeambrun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new a8c062f781 Generate SBOMs for providers (#35667)
a8c062f781 is described below
commit a8c062f781db3c6000f8be76389aa774b2a45c3f
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Thu Nov 16 02:51:21 2023 +0800
Generate SBOMs for providers (#35667)
* Working POC
* Small adjustements and cleaning
---
.../src/airflow_breeze/commands/sbom_commands.py | 178 ++++++++++++----
.../commands/sbom_commands_config.py | 1 +
dev/breeze/src/airflow_breeze/utils/cdxgen.py | 231 +++++++++++++++------
dev/breeze/src/airflow_breeze/utils/path_utils.py | 1 +
.../breeze/output_sbom_update-sbom-information.svg | 52 +++--
.../breeze/output_sbom_update-sbom-information.txt | 2 +-
6 files changed, 338 insertions(+), 127 deletions(-)
diff --git a/dev/breeze/src/airflow_breeze/commands/sbom_commands.py
b/dev/breeze/src/airflow_breeze/commands/sbom_commands.py
index 8fc30faf72..1e01c9b7a9 100644
--- a/dev/breeze/src/airflow_breeze/commands/sbom_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/sbom_commands.py
@@ -18,6 +18,7 @@
from __future__ import annotations
import json
+import os
import sys
from pathlib import Path
@@ -29,10 +30,14 @@ from airflow_breeze.global_constants import (
PROVIDER_DEPENDENCIES,
)
from airflow_breeze.utils.cdxgen import (
+ PROVIDER_REQUIREMENTS_DIR_PATH,
SbomApplicationJob,
+ SbomCoreJob,
+ SbomProviderJob,
build_all_airflow_versions_base_image,
get_cdxgen_port_mapping,
get_requirements_for_provider,
+ list_providers_from_providers_requirements,
)
from airflow_breeze.utils.ci_group import ci_group
from airflow_breeze.utils.click_utils import BreezeGroup
@@ -57,7 +62,7 @@ from airflow_breeze.utils.parallel import (
check_async_run_results,
run_with_pool,
)
-from airflow_breeze.utils.path_utils import AIRFLOW_TMP_DIR_PATH,
PROVIDER_METADATA_JSON_FILE_PATH
+from airflow_breeze.utils.path_utils import FILES_SBOM_DIR,
PROVIDER_METADATA_JSON_FILE_PATH
from airflow_breeze.utils.shared_options import get_dry_run
@@ -71,10 +76,11 @@ def sbom():
SBOM_INDEX_TEMPLATE = """
+{% set project_name = " " + provider_id + " " if provider_id else " " -%}
<html>
-<head><title>CycloneDX SBOMs for Apache Airflow {{ version }}</title></head>
+<head><title>CycloneDX SBOMs for Apache Airflow{{project_name}}{{ version
}}</title></head>
<body>
- <h1>CycloneDX SBOMs for Apache Airflow {{ version }}</h1>
+ <h1>CycloneDX SBOMs for Apache Airflow{{project_name}}{{ version }}</h1>
<ul>
{% for sbom_file in sbom_files %}
<li><a href="{{ sbom_file.name }}">{{ sbom_file.name }}</a></li>
@@ -119,6 +125,14 @@ SBOM_INDEX_TEMPLATE = """
@option_verbose
@option_dry_run
@option_answer
[email protected](
+ "--package-filter",
+ help="List of packages to consider. You can use `apache-airflow` for core "
+ "or `apache-airflow-providers` to consider all the providers.",
+ type=BetterChoice(["apache-airflow-providers", "apache-airflow"]),
+ required=False,
+ default="apache-airflow",
+)
def update_sbom_information(
airflow_site_directory: Path,
airflow_version: str | None,
@@ -130,6 +144,7 @@ def update_sbom_information(
include_success_outputs: bool,
skip_cleanup: bool,
force: bool,
+ package_filter: tuple[str, ...],
):
import jinja2
from jinja2 import StrictUndefined
@@ -148,55 +163,114 @@ def update_sbom_information(
python_versions = ALL_HISTORICAL_PYTHON_VERSIONS
else:
python_versions = [python]
- application_root_path = AIRFLOW_TMP_DIR_PATH
+ application_root_path = FILES_SBOM_DIR
start_cdxgen_server(application_root_path, run_in_parallel, parallelism)
jobs_to_run: list[SbomApplicationJob] = []
- apache_airflow_directory = airflow_site_directory / "docs-archive" /
"apache-airflow"
+ airflow_site_archive_directory = airflow_site_directory / "docs-archive"
- for airflow_v in airflow_versions:
- airflow_version_dir = apache_airflow_directory / airflow_v
- if not airflow_version_dir.exists():
- get_console().print(f"[warning]The {airflow_version_dir} does not
exist. Skipping")
- continue
- destination_dir = airflow_version_dir / "sbom"
- if destination_dir.exists():
+ def _dir_exists_warn_and_should_skip(dir: Path, force: bool) -> bool:
+ if dir.exists():
if not force:
- get_console().print(f"[warning]The {destination_dir} already
exists. Skipping")
- continue
+ get_console().print(f"[warning]The {dir} already exists.
Skipping")
+ return True
else:
- get_console().print(f"[warning]The {destination_dir} already
exists. Forcing update")
+ get_console().print(f"[warning]The {dir} already exists.
Forcing update")
+ return False
+ return False
- destination_dir.mkdir(parents=True, exist_ok=True)
+ if package_filter == "apache-airflow":
+ # Create core jobs
+ apache_airflow_documentation_directory =
airflow_site_archive_directory / "apache-airflow"
- get_console().print(f"[info]Attempting to update sbom for
{airflow_v}.")
- get_console().print(f"[success]The {destination_dir} exists.
Proceeding.")
- for python_version in python_versions:
- target_sbom_file_name =
f"apache-airflow-sbom-{airflow_v}-python{python_version}.json"
- target_sbom_path = destination_dir / target_sbom_file_name
- if target_sbom_path.exists():
- if not force:
- get_console().print(f"[warning]The {target_sbom_path}
already exists. Skipping")
+ for airflow_v in airflow_versions:
+ airflow_version_dir = apache_airflow_documentation_directory /
airflow_v
+ if not airflow_version_dir.exists():
+ get_console().print(f"[warning]The {airflow_version_dir} does
not exist. Skipping")
+ continue
+ destination_dir = airflow_version_dir / "sbom"
+
+ if _dir_exists_warn_and_should_skip(destination_dir, force):
+ continue
+
+ destination_dir.mkdir(parents=True, exist_ok=True)
+
+ get_console().print(f"[info]Attempting to update sbom for
{airflow_v}.")
+ for python_version in python_versions:
+ target_sbom_file_name =
f"apache-airflow-sbom-{airflow_v}-python{python_version}.json"
+ target_sbom_path = destination_dir / target_sbom_file_name
+
+ if _dir_exists_warn_and_should_skip(target_sbom_path, force):
continue
- else:
- get_console().print(f"[warning]The {target_sbom_path}
already exists. Forcing update")
- jobs_to_run.append(
- SbomApplicationJob(
- airflow_version=airflow_v,
- python_version=python_version,
- application_root_path=application_root_path,
-
include_provider_dependencies=include_provider_dependencies,
- target_path=target_sbom_path,
+
+ jobs_to_run.append(
+ SbomCoreJob(
+ airflow_version=airflow_v,
+ python_version=python_version,
+ application_root_path=application_root_path,
+
include_provider_dependencies=include_provider_dependencies,
+ target_path=target_sbom_path,
+ )
)
+ elif package_filter == "apache-airflow-providers":
+ # Create providers jobs
+ user_confirm(
+ "You are about to update sbom information for providers, did you
refresh the "
+ "providers requirements with the command `breeze sbom
generate-providers-requirements`?",
+ quit_allowed=False,
+ default_answer=Answer.YES,
+ )
+ for (
+ node_name,
+ provider_id,
+ provider_version,
+ provider_version_documentation_directory,
+ ) in
list_providers_from_providers_requirements(airflow_site_archive_directory):
+ destination_dir = provider_version_documentation_directory / "sbom"
+
+ if _dir_exists_warn_and_should_skip(destination_dir, force):
+ continue
+
+ destination_dir.mkdir(parents=True, exist_ok=True)
+
+ get_console().print(
+ f"[info]Attempting to update sbom for {provider_id} version
{provider_version}."
+ )
+
+ python_versions = set(
+ dir_name.replace("python", "")
+ for dir_name in os.listdir(PROVIDER_REQUIREMENTS_DIR_PATH /
node_name)
)
+
+ for python_version in python_versions:
+ target_sbom_file_name = (
+
f"apache-airflow-sbom-{provider_id}-{provider_version}-python{python_version}.json"
+ )
+ target_sbom_path = destination_dir / target_sbom_file_name
+
+ if _dir_exists_warn_and_should_skip(target_sbom_path, force):
+ continue
+
+ jobs_to_run.append(
+ SbomProviderJob(
+ provider_id=provider_id,
+ provider_version=provider_version,
+ python_version=python_version,
+ target_path=target_sbom_path,
+ folder_name=node_name,
+ )
+ )
+
+ if len(jobs_to_run) == 0:
+ get_console().print("[info]Nothing to do, there is no job to process")
+ return
+
if run_in_parallel:
parallelism = min(parallelism, len(jobs_to_run))
get_console().print(f"[info]Running {len(jobs_to_run)} jobs in
parallel")
- with ci_group(f"Generating SBoMs for
{airflow_versions}:{python_versions}"):
- all_params = [
- f"Generate SBoMs for
{job.airflow_version}:{job.python_version}" for job in jobs_to_run
- ]
+ with ci_group(f"Generating SBOMs for {jobs_to_run}"):
+ all_params = [f"Generate SBOMs for {job.get_job_name()}" for job
in jobs_to_run]
with run_with_pool(
parallelism=parallelism,
all_params=all_params,
@@ -217,7 +291,7 @@ def update_sbom_information(
]
check_async_run_results(
results=results,
- success="All SBoMs were generated successfully",
+ success="All SBOMs were generated successfully",
outputs=outputs,
include_success_outputs=include_success_outputs,
skip_cleanup=skip_cleanup,
@@ -226,20 +300,36 @@ def update_sbom_information(
for job in jobs_to_run:
produce_sbom_for_application_via_cdxgen_server(job, output=None)
- for airflow_v in airflow_versions:
- airflow_version_dir = apache_airflow_directory / airflow_v
- destination_dir = airflow_version_dir / "sbom"
+ html_template = SBOM_INDEX_TEMPLATE
+
+ def _generate_index(destination_dir: Path, provider_id: str | None,
version: str) -> None:
destination_index_path = destination_dir / "index.html"
get_console().print(f"[info]Generating index for {destination_dir}")
sbom_files = sorted(destination_dir.glob("apache-airflow-sbom-*"))
- html_template = SBOM_INDEX_TEMPLATE
if not get_dry_run():
destination_index_path.write_text(
jinja2.Template(html_template, autoescape=True,
undefined=StrictUndefined).render(
- version=airflow_v, sbom_files=sbom_files
+ provider_id=provider_id,
+ version=version,
+ sbom_files=sbom_files,
)
)
+ if package_filter == "apache-airflow":
+ for airflow_v in airflow_versions:
+ airflow_version_dir = apache_airflow_documentation_directory /
airflow_v
+ destination_dir = airflow_version_dir / "sbom"
+ _generate_index(destination_dir, None, airflow_v)
+ elif package_filter == "apache-airflow-providers":
+ for (
+ node_name,
+ provider_id,
+ provider_version,
+ provider_version_documentation_directory,
+ ) in
list_providers_from_providers_requirements(airflow_site_archive_directory):
+ destination_dir = provider_version_documentation_directory / "sbom"
+ _generate_index(destination_dir, provider_id, provider_version)
+
@sbom.command(name="build-all-airflow-images", help="Generate images with
airflow versions pre-installed")
@option_historical_python_version
@@ -283,7 +373,6 @@ def build_all_airflow_images(
build_all_airflow_versions_base_image,
kwds={
"python_version": python_version,
- "confirm": False,
"output": outputs[index],
},
)
@@ -300,7 +389,6 @@ def build_all_airflow_images(
for python_version in python_versions:
build_all_airflow_versions_base_image(
python_version=python_version,
- confirm=False,
output=None,
)
diff --git a/dev/breeze/src/airflow_breeze/commands/sbom_commands_config.py
b/dev/breeze/src/airflow_breeze/commands/sbom_commands_config.py
index ce17e4a258..9fffaaaaca 100644
--- a/dev/breeze/src/airflow_breeze/commands/sbom_commands_config.py
+++ b/dev/breeze/src/airflow_breeze/commands/sbom_commands_config.py
@@ -34,6 +34,7 @@ SBOM_PARAMETERS: dict[str, list[dict[str, str | list[str]]]]
= {
"--airflow-version",
"--python",
"--include-provider-dependencies",
+ "--package-filter",
"--force",
],
},
diff --git a/dev/breeze/src/airflow_breeze/utils/cdxgen.py
b/dev/breeze/src/airflow_breeze/utils/cdxgen.py
index ee43715f2f..3bcb4cd21f 100644
--- a/dev/breeze/src/airflow_breeze/utils/cdxgen.py
+++ b/dev/breeze/src/airflow_breeze/utils/cdxgen.py
@@ -23,9 +23,11 @@ import os
import signal
import sys
import time
+from abc import abstractmethod
from dataclasses import dataclass
from multiprocessing.pool import Pool
from pathlib import Path
+from typing import Generator
import yaml
@@ -38,7 +40,7 @@ from airflow_breeze.utils.github import (
download_constraints_file,
download_file_from_github,
)
-from airflow_breeze.utils.path_utils import AIRFLOW_SOURCES_ROOT, FILES_DIR
+from airflow_breeze.utils.path_utils import AIRFLOW_SOURCES_ROOT,
FILES_SBOM_DIR
from airflow_breeze.utils.run_utils import run_command
from airflow_breeze.utils.shared_options import get_dry_run
@@ -129,8 +131,34 @@ def get_all_airflow_versions_image_name(python_version:
str) -> str:
return
f"ghcr.io/apache/airflow/airflow-dev/all-airflow/python{python_version}"
+def list_providers_from_providers_requirements(
+ airflow_site_archive_directory: Path,
+) -> Generator[tuple[str, str, str, Path], None, None]:
+ for node_name in os.listdir(PROVIDER_REQUIREMENTS_DIR_PATH):
+ if not node_name.startswith("provider"):
+ continue
+
+ provider_id, provider_version = node_name.rsplit("-", 1)
+
+ provider_documentation_directory = (
+ airflow_site_archive_directory
+ / f"apache-airflow-providers-{provider_id.replace('provider-',
'').replace('.', '-')}"
+ )
+ provider_version_documentation_directory =
provider_documentation_directory / provider_version
+
+ if not provider_version_documentation_directory.exists():
+ get_console().print(
+ f"[warning]The {provider_version_documentation_directory} does
not exist. Skipping"
+ )
+ continue
+
+ yield (node_name, provider_id, provider_version,
provider_version_documentation_directory)
+
+
TARGET_DIR_NAME = "provider_requirements"
-DOCKER_FILE_PREFIX = f"/files/{TARGET_DIR_NAME}/"
+
+PROVIDER_REQUIREMENTS_DIR_PATH = FILES_SBOM_DIR / TARGET_DIR_NAME
+DOCKER_FILE_PREFIX = f"/files/sbom/{TARGET_DIR_NAME}/"
def get_requirements_for_provider(
@@ -148,19 +176,23 @@ def get_requirements_for_provider(
) / "provider.yaml"
provider_version =
yaml.safe_load(provider_file.read_text())["versions"][0]
- target_dir = FILES_DIR / TARGET_DIR_NAME
airflow_core_file_name =
f"airflow-{airflow_version}-python{python_version}-requirements.txt"
- airflow_core_path = target_dir / airflow_core_file_name
-
- provider_with_core_file_name =
f"python{python_version}-with-core-requirements.txt"
- provider_without_core_file_name =
f"python{python_version}-without-core-requirements.txt"
+ airflow_core_path = PROVIDER_REQUIREMENTS_DIR_PATH / airflow_core_file_name
provider_folder_name = f"provider-{provider_id}-{provider_version}"
- provider_folder_path = target_dir / provider_folder_name
- provider_with_core_path = provider_folder_path /
provider_with_core_file_name
- provider_without_core_file = provider_folder_path /
provider_without_core_file_name
+ provider_folder_path = PROVIDER_REQUIREMENTS_DIR_PATH /
provider_folder_name
+
+ provider_with_core_folder_path = provider_folder_path /
f"python{python_version}" / "with-core"
+ provider_with_core_folder_path.mkdir(exist_ok=True, parents=True)
+ provider_with_core_path = provider_with_core_folder_path /
"requirements.txt"
- docker_file_provider_folder_prefix =
f"{DOCKER_FILE_PREFIX}/{provider_folder_name}/"
+ provider_without_core_folder_path = provider_folder_path /
f"python{python_version}" / "without-core"
+ provider_without_core_folder_path.mkdir(exist_ok=True, parents=True)
+ provider_without_core_file = provider_without_core_folder_path /
"requirements.txt"
+
+ docker_file_provider_with_core_folder_prefix = (
+
f"{DOCKER_FILE_PREFIX}{provider_folder_name}/python{python_version}/with-core/"
+ )
if (
os.path.exists(provider_with_core_path)
@@ -185,8 +217,8 @@ mkdir -pv {DOCKER_FILE_PREFIX}
/opt/airflow/airflow-{airflow_version}/bin/pip install
apache-airflow=={airflow_version} \
apache-airflow-providers-{provider_id}=={provider_version}
/opt/airflow/airflow-{airflow_version}/bin/pip freeze | sort > \
- {docker_file_provider_folder_prefix}{provider_with_core_file_name}
-chown --recursive {os.getuid()}:{os.getgid()}
{DOCKER_FILE_PREFIX}{provider_with_core_file_name}
+ {docker_file_provider_with_core_folder_prefix}requirements.txt
+chown --recursive {os.getuid()}:{os.getgid()}
{DOCKER_FILE_PREFIX}{provider_folder_name}
"""
provider_command_result = run_command(
[
@@ -223,20 +255,19 @@ chown --recursive {os.getuid()}:{os.getgid()}
{DOCKER_FILE_PREFIX}{provider_with
get_console(output=output).print(provider_packages)
provider_without_core_file.write_text("".join(f"{p}\n" for p in
provider_packages))
get_console(output=output).print(
- f"[success]Generated {provider_id}:{provider_version}:{python_version}
requirements in "
+ f"[success]Generated
{provider_id}:{provider_version}:python{python_version} requirements in "
f"{provider_without_core_file}"
)
return (
provider_command_result.returncode,
- f"Provider requirements generated for
{provider_id}:{provider_version}:{python_version}",
+ f"Provider requirements generated for
{provider_id}:{provider_version}:python{python_version}",
)
def build_all_airflow_versions_base_image(
python_version: str,
output: Output | None,
- confirm: bool = True,
) -> tuple[int, str]:
"""
Build an image with all airflow versions pre-installed in separate
virtualenvs.
@@ -288,11 +319,131 @@
constraints-{airflow_version}/constraints-{python_version}.txt
@dataclass
class SbomApplicationJob:
- airflow_version: str
python_version: str
+ target_path: Path
+
+ @abstractmethod
+ def produce(self, output: Output | None, port: int) -> tuple[int, str]:
+ pass
+
+ @abstractmethod
+ def get_job_name(self) -> str:
+ pass
+
+
+@dataclass
+class SbomCoreJob(SbomApplicationJob):
+ airflow_version: str
application_root_path: Path
include_provider_dependencies: bool
- target_path: Path
+
+ def get_job_name(self) -> str:
+ return f"{self.airflow_version}:python{self.python_version}"
+
+ def download_dependency_files(self, output: Output | None) -> bool:
+ source_dir = self.application_root_path / self.airflow_version /
f"python{self.python_version}"
+ source_dir.mkdir(parents=True, exist_ok=True)
+ lock_file_relative_path = "airflow/www/yarn.lock"
+ download_file_from_github(
+ tag=self.airflow_version, path=lock_file_relative_path,
output_file=source_dir / "yarn.lock"
+ )
+ if not download_constraints_file(
+ airflow_version=self.airflow_version,
+ python_version=self.python_version,
+ include_provider_dependencies=self.include_provider_dependencies,
+ output_file=source_dir / "requirements.txt",
+ ):
+ get_console(output=output).print(
+ f"[warning]Failed to download constraints file for "
+ f"{self.airflow_version} and {self.python_version}. Skipping"
+ )
+ return False
+ return True
+
+ def produce(self, output: Output | None, port: int) -> tuple[int, str]:
+ import requests
+
+ get_console(output=output).print(
+ f"[info]Updating sbom for Airflow {self.airflow_version} and
python {self.python_version}"
+ )
+ if not self.download_dependency_files(output):
+ return 0, f"SBOM Generate
{self.airflow_version}:{self.python_version}"
+
+ get_console(output=output).print(
+ f"[info]Generating sbom for Airflow {self.airflow_version} and
python {self.python_version} with cdxgen"
+ )
+ url = (
+
f"http://127.0.0.1:{port}/sbom?path=/app/{self.airflow_version}/python{self.python_version}&"
+
f"project-name=apache-airflow&project-version={self.airflow_version}&multiProject=true"
+ )
+
+ get_console(output=output).print(
+ f"[info]Triggering sbom generation in {self.airflow_version} via
{url}"
+ )
+ if not get_dry_run():
+ response = requests.get(url)
+ if response.status_code != 200:
+ get_console(output=output).print(
+ f"[error]Generation for Airflow
{self.airflow_version}:python{self.python_version} "
+ f"failed. Status code {response.status_code}"
+ )
+ return (
+ response.status_code,
+ f"SBOM Generate
{self.airflow_version}:python{self.python_version}",
+ )
+ self.target_path.write_bytes(response.content)
+ get_console(output=output).print(
+ f"[success]Generated SBOM for
{self.airflow_version}:python{self.python_version}"
+ )
+
+ return 0, f"SBOM Generate
{self.airflow_version}:python{self.python_version}"
+
+
+@dataclass
+class SbomProviderJob(SbomApplicationJob):
+ provider_id: str
+ provider_version: str
+ folder_name: str
+
+ def get_job_name(self) -> str:
+ return
f"{self.provider_id}:{self.provider_version}:python{self.python_version}"
+
+ def produce(self, output: Output | None, port: int) -> tuple[int, str]:
+ import requests
+
+ get_console(output=output).print(
+ f"[info]Updating sbom for provider {self.provider_id} version
{self.provider_version} and python "
+ f"{self.python_version}"
+ )
+ get_console(output=output).print(
+ f"[info]Generating sbom for provider {self.provider_id} version
{self.provider_version} and "
+ f"python {self.python_version}"
+ )
+ url = (
+
f"http://127.0.0.1:{port}/sbom?path=/app/{TARGET_DIR_NAME}/{self.folder_name}/python{self.python_version}/without-core&"
+
f"project-name={self.provider_version}&project-version={self.provider_version}&multiProject=true"
+ )
+
+ get_console(output=output).print(f"[info]Triggering sbom generation
via {url}")
+
+ if not get_dry_run():
+ response = requests.get(url)
+ if response.status_code != 200:
+ get_console(output=output).print(
+ f"[error]Generation for Airflow
{self.provider_id}:{self.provider_version}:"
+ f"{self.python_version} failed. Status code
{response.status_code}"
+ )
+ return (
+ response.status_code,
+ f"SBOM Generate
{self.provider_id}:{self.provider_version}:{self.python_version}",
+ )
+ self.target_path.write_bytes(response.content)
+ get_console(output=output).print(
+ f"[success]Generated SBOM for
{self.provider_id}:{self.provider_version}:"
+ f"{self.python_version}"
+ )
+
+ return 0, f"SBOM Generate
{self.provider_id}:{self.provider_version}:{self.python_version}"
def produce_sbom_for_application_via_cdxgen_server(
@@ -306,52 +457,10 @@ def produce_sbom_for_application_via_cdxgen_server(
in case parallel processing is used
:return: tuple with exit code and output
"""
- import requests
if port_map is None:
port = 9090
else:
port = port_map[multiprocessing.current_process().name]
get_console(output=output).print(f"[info]Using port {port}")
- get_console(output=output).print(
- f"[info]Updating sbom for Airflow {job.airflow_version} and python
{job.python_version}"
- )
- source_dir = job.application_root_path / job.airflow_version /
job.python_version
- source_dir.mkdir(parents=True, exist_ok=True)
- lock_file_relative_path = "airflow/www/yarn.lock"
- download_file_from_github(
- tag=job.airflow_version, path=lock_file_relative_path,
output_file=source_dir / "yarn.lock"
- )
- if not download_constraints_file(
- airflow_version=job.airflow_version,
- python_version=job.python_version,
- include_provider_dependencies=job.include_provider_dependencies,
- output_file=source_dir / "requirements.txt",
- ):
- get_console(output=output).print(
- f"[warning]Failed to download constraints file for "
- f"{job.airflow_version} and {job.python_version}. Skipping"
- )
- return 0, f"SBOM Generate {job.airflow_version}:{job.python_version}"
- get_console(output=output).print(
- f"[info]Generating sbom for Airflow {job.airflow_version} and python
{job.python_version} with cdxgen"
- )
- url = (
-
f"http://127.0.0.1:{port}/sbom?path=/app/{job.airflow_version}/{job.python_version}&"
-
f"project-name=apache-airflow&project-version={job.airflow_version}&multiProject=true"
- )
- get_console(output=output).print(f"[info]Triggering sbom generation in
{job.airflow_version} via {url}")
- if not get_dry_run():
- response = requests.get(url)
- if response.status_code != 200:
- get_console(output=output).print(
- f"[error]Generation for Airflow
{job.airflow_version}:{job.python_version} failed. "
- f"Status code {response.status_code}"
- )
- return response.status_code, f"SBOM Generate
{job.airflow_version}:{job.python_version}"
- job.target_path.write_bytes(response.content)
- get_console(output=output).print(
- f"[success]Generated SBOM for
{job.airflow_version}:{job.python_version}"
- )
-
- return 0, f"SBOM Generate {job.airflow_version}:{job.python_version}"
+ return job.produce(output, port)
diff --git a/dev/breeze/src/airflow_breeze/utils/path_utils.py
b/dev/breeze/src/airflow_breeze/utils/path_utils.py
index d67868888c..cb48c5a104 100644
--- a/dev/breeze/src/airflow_breeze/utils/path_utils.py
+++ b/dev/breeze/src/airflow_breeze/utils/path_utils.py
@@ -279,6 +279,7 @@ WWW_ASSET_OUT_FILE = WWW_CACHE_DIR / "asset_compile.out"
WWW_ASSET_OUT_DEV_MODE_FILE = WWW_CACHE_DIR / "asset_compile_dev_mode.out"
DAGS_DIR = AIRFLOW_SOURCES_ROOT / "dags"
FILES_DIR = AIRFLOW_SOURCES_ROOT / "files"
+FILES_SBOM_DIR = FILES_DIR / "sbom"
HOOKS_DIR = AIRFLOW_SOURCES_ROOT / "hooks"
KUBE_DIR = AIRFLOW_SOURCES_ROOT / ".kube"
LOGS_DIR = AIRFLOW_SOURCES_ROOT / "logs"
diff --git a/images/breeze/output_sbom_update-sbom-information.svg
b/images/breeze/output_sbom_update-sbom-information.svg
index d15f3de36f..802927e439 100644
--- a/images/breeze/output_sbom_update-sbom-information.svg
+++ b/images/breeze/output_sbom_update-sbom-information.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 806.4"
xmlns="http://www.w3.org/2000/svg">
+<svg class="rich-terminal" viewBox="0 0 1482 879.5999999999999"
xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@@ -45,7 +45,7 @@
<defs>
<clipPath id="breeze-sbom-update-sbom-information-clip-terminal">
- <rect x="0" y="0" width="1463.0" height="755.4" />
+ <rect x="0" y="0" width="1463.0" height="828.5999999999999" />
</clipPath>
<clipPath id="breeze-sbom-update-sbom-information-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -137,9 +137,18 @@
<clipPath id="breeze-sbom-update-sbom-information-line-29">
<rect x="0" y="709.1" width="1464" height="24.65"/>
</clipPath>
+<clipPath id="breeze-sbom-update-sbom-information-line-30">
+ <rect x="0" y="733.5" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-sbom-update-sbom-information-line-31">
+ <rect x="0" y="757.9" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-sbom-update-sbom-information-line-32">
+ <rect x="0" y="782.3" width="1464" height="24.65"/>
+ </clipPath>
</defs>
- <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="1480" height="804.4" rx="8"/><text
class="breeze-sbom-update-sbom-information-title" fill="#c5c8c6"
text-anchor="middle" x="740"
y="27">Command: sbom update-sbom-information</text>
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="1480" height="877.6" rx="8"/><text
class="breeze-sbom-update-sbom-information-title" fill="#c5c8c6"
text-anchor="middle" x="740"
y="27">Command: sbom update-sbom-information</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -163,23 +172,26 @@
</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="288.4"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-11)">│</text><text
class="breeze-sbom-update-sbom-information-r1" x="488" y="288.4"
textLength="951.6"
clip-path="url(#breeze-sbom-update-sbom-information-line-11)">versions)                            
[...]
</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="312.8"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-12)">│</text><text
class="breeze-sbom-update-sbom-information-r7" x="488" y="312.8"
textLength="951.6"
clip-path="url(#breeze-sbom-update-sbom-information-line-12)">(3.6 | 3.7 | 3.8 | 3.9 | 3.10 | 3.11)               
[...]
</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="337.2"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-13)">│</text><text
class="breeze-sbom-update-sbom-information-r4" x="61" y="337.2"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-13)">-</text><text
class="breeze-sbom-update-sbom-information-r4" x="73.2" y="337.2"
textLength="97.6"
clip-path="url(#breeze-sbom-update-sbom-information-line-13)">-include</text><te
[...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="361.6"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-14)">│</text><text
class="breeze-sbom-update-sbom-information-r4" x="61" y="361.6"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-14)">-</text><text
class="breeze-sbom-update-sbom-information-r4" x="73.2" y="361.6"
textLength="73.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-14)">-force</text><text
[...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="386"
textLength="1464"
clip-path="url(#breeze-sbom-update-sbom-information-line-15)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-sbom-update-sbom-information-r1" x="1464" y="386"
textLength="12.2" clip-path="url(#breeze-sbom-update-sbom-information-line-15)">
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="410.4"
textLength="24.4"
clip-path="url(#breeze-sbom-update-sbom-information-line-16)">╭─</text><text
class="breeze-sbom-update-sbom-information-r5" x="24.4" y="410.4"
textLength="219.6"
clip-path="url(#breeze-sbom-update-sbom-information-line-16)"> Parallel running </text><text
class="breeze-sbom-update-sbom-information-r5" x="244" y="410.4"
textLength="1195.6" clip-path="url(#breeze-sbom-update-sbom-inf [...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="434.8"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-17)">│</text><text
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="434.8"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-17)">-</text><text
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="434.8"
textLength="48.8"
clip-path="url(#breeze-sbom-update-sbom-information-line-17)">-run</text><text
[...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="459.2"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-18)">│</text><text
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="459.2"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-18)">-</text><text
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="459.2"
textLength="146.4"
clip-path="url(#breeze-sbom-update-sbom-information-line-18)">-parallelism</t
[...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="483.6"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-19)">│</text><text
class="breeze-sbom-update-sbom-information-r7" x="378.2" y="483.6"
textLength="915"
clip-path="url(#breeze-sbom-update-sbom-information-line-19)">(INTEGER RANGE)                          
[...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="508"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-20)">│</text><text
class="breeze-sbom-update-sbom-information-r5" x="378.2" y="508"
textLength="915"
clip-path="url(#breeze-sbom-update-sbom-information-line-20)">[default: 4; 1<=x<=8]                        
[...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="532.4"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-21)">│</text><text
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="532.4"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-21)">-</text><text
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="532.4"
textLength="61"
clip-path="url(#breeze-sbom-update-sbom-information-line-21)">-skip</text><text
[...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="556.8"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-22)">│</text><text
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="556.8"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-22)">-</text><text
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="556.8"
textLength="73.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-22)">-debug</text><te
[...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="581.2"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-23)">│</text><text
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="581.2"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-23)">-</text><text
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="581.2"
textLength="97.6"
clip-path="url(#breeze-sbom-update-sbom-information-line-23)">-include</text><
[...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="605.6"
textLength="1464"
clip-path="url(#breeze-sbom-update-sbom-information-line-24)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-sbom-update-sbom-information-r1" x="1464" y="605.6"
textLength="12.2" clip-path="url(#breeze-sbom-update-sbom-information-line-24)">
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="630"
textLength="24.4"
clip-path="url(#breeze-sbom-update-sbom-information-line-25)">╭─</text><text
class="breeze-sbom-update-sbom-information-r5" x="24.4" y="630"
textLength="195.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-25)"> Common options </text><text
class="breeze-sbom-update-sbom-information-r5" x="219.6" y="630"
textLength="1220" clip-path="url(#breeze-sbom-update-sbom-information [...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="654.4"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-26)">│</text><text
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="654.4"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-26)">-</text><text
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="654.4"
textLength="97.6"
clip-path="url(#breeze-sbom-update-sbom-information-line-26)">-verbose</text><
[...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="678.8"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-27)">│</text><text
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="678.8"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-27)">-</text><text
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="678.8"
textLength="48.8"
clip-path="url(#breeze-sbom-update-sbom-information-line-27)">-dry</text><text
[...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="703.2"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-28)">│</text><text
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="703.2"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-28)">-</text><text
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="703.2"
textLength="85.4"
clip-path="url(#breeze-sbom-update-sbom-information-line-28)">-answer</text><t
[...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="727.6"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-29)">│</text><text
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="727.6"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-29)">-</text><text
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="727.6"
textLength="61"
clip-path="url(#breeze-sbom-update-sbom-information-line-29)">-help</text><text
[...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="752"
textLength="1464"
clip-path="url(#breeze-sbom-update-sbom-information-line-30)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-sbom-update-sbom-information-r1" x="1464" y="752"
textLength="12.2" clip-path="url(#breeze-sbom-update-sbom-information-line-30)">
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="361.6"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-14)">│</text><text
class="breeze-sbom-update-sbom-information-r4" x="61" y="361.6"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-14)">-</text><text
class="breeze-sbom-update-sbom-information-r4" x="73.2" y="361.6"
textLength="97.6"
clip-path="url(#breeze-sbom-update-sbom-information-line-14)">-package</text><te
[...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="386"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-15)">│</text><text
class="breeze-sbom-update-sbom-information-r1" x="488" y="386"
textLength="951.6"
clip-path="url(#breeze-sbom-update-sbom-information-line-15)">`apache-airflow-providers` to consider all the providers.                &#
[...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="410.4"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-16)">│</text><text
class="breeze-sbom-update-sbom-information-r7" x="488" y="410.4"
textLength="951.6"
clip-path="url(#breeze-sbom-update-sbom-information-line-16)">(apache-airflow-providers | apache-airflow)                    
[...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="434.8"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-17)">│</text><text
class="breeze-sbom-update-sbom-information-r4" x="61" y="434.8"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-17)">-</text><text
class="breeze-sbom-update-sbom-information-r4" x="73.2" y="434.8"
textLength="73.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-17)">-force</text><text
[...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="459.2"
textLength="1464"
clip-path="url(#breeze-sbom-update-sbom-information-line-18)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-sbom-update-sbom-information-r1" x="1464" y="459.2"
textLength="12.2" clip-path="url(#breeze-sbom-update-sbom-information-line-18)">
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="483.6"
textLength="24.4"
clip-path="url(#breeze-sbom-update-sbom-information-line-19)">╭─</text><text
class="breeze-sbom-update-sbom-information-r5" x="24.4" y="483.6"
textLength="219.6"
clip-path="url(#breeze-sbom-update-sbom-information-line-19)"> Parallel running </text><text
class="breeze-sbom-update-sbom-information-r5" x="244" y="483.6"
textLength="1195.6" clip-path="url(#breeze-sbom-update-sbom-inf [...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="508"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-20)">│</text><text
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="508"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-20)">-</text><text
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="508"
textLength="48.8"
clip-path="url(#breeze-sbom-update-sbom-information-line-20)">-run</text><text
class [...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="532.4"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-21)">│</text><text
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="532.4"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-21)">-</text><text
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="532.4"
textLength="146.4"
clip-path="url(#breeze-sbom-update-sbom-information-line-21)">-parallelism</t
[...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="556.8"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-22)">│</text><text
class="breeze-sbom-update-sbom-information-r7" x="378.2" y="556.8"
textLength="915"
clip-path="url(#breeze-sbom-update-sbom-information-line-22)">(INTEGER RANGE)                          
[...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="581.2"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-23)">│</text><text
class="breeze-sbom-update-sbom-information-r5" x="378.2" y="581.2"
textLength="915"
clip-path="url(#breeze-sbom-update-sbom-information-line-23)">[default: 4; 1<=x<=8]                       &
[...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="605.6"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-24)">│</text><text
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="605.6"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-24)">-</text><text
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="605.6"
textLength="61"
clip-path="url(#breeze-sbom-update-sbom-information-line-24)">-skip</text><text
[...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="630"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-25)">│</text><text
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="630"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-25)">-</text><text
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="630"
textLength="73.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-25)">-debug</text><text
cla [...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="654.4"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-26)">│</text><text
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="654.4"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-26)">-</text><text
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="654.4"
textLength="97.6"
clip-path="url(#breeze-sbom-update-sbom-information-line-26)">-include</text><
[...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="678.8"
textLength="1464"
clip-path="url(#breeze-sbom-update-sbom-information-line-27)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-sbom-update-sbom-information-r1" x="1464" y="678.8"
textLength="12.2" clip-path="url(#breeze-sbom-update-sbom-information-line-27)">
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="703.2"
textLength="24.4"
clip-path="url(#breeze-sbom-update-sbom-information-line-28)">╭─</text><text
class="breeze-sbom-update-sbom-information-r5" x="24.4" y="703.2"
textLength="195.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-28)"> Common options </text><text
class="breeze-sbom-update-sbom-information-r5" x="219.6" y="703.2"
textLength="1220" clip-path="url(#breeze-sbom-update-sbom-infor [...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="727.6"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-29)">│</text><text
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="727.6"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-29)">-</text><text
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="727.6"
textLength="97.6"
clip-path="url(#breeze-sbom-update-sbom-information-line-29)">-verbose</text><
[...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="752"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-30)">│</text><text
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="752"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-30)">-</text><text
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="752"
textLength="48.8"
clip-path="url(#breeze-sbom-update-sbom-information-line-30)">-dry</text><text
class [...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="776.4"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-31)">│</text><text
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="776.4"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-31)">-</text><text
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="776.4"
textLength="85.4"
clip-path="url(#breeze-sbom-update-sbom-information-line-31)">-answer</text><t
[...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="800.8"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-32)">│</text><text
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="800.8"
textLength="12.2"
clip-path="url(#breeze-sbom-update-sbom-information-line-32)">-</text><text
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="800.8"
textLength="61"
clip-path="url(#breeze-sbom-update-sbom-information-line-32)">-help</text><text
[...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="825.2"
textLength="1464"
clip-path="url(#breeze-sbom-update-sbom-information-line-33)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-sbom-update-sbom-information-r1" x="1464" y="825.2"
textLength="12.2" clip-path="url(#breeze-sbom-update-sbom-information-line-33)">
</text>
</g>
</g>
diff --git a/images/breeze/output_sbom_update-sbom-information.txt
b/images/breeze/output_sbom_update-sbom-information.txt
index 0418b1c425..e31da63f35 100644
--- a/images/breeze/output_sbom_update-sbom-information.txt
+++ b/images/breeze/output_sbom_update-sbom-information.txt
@@ -1 +1 @@
-653be48be70b4b7ff5172d491aadc694
+e49cf33ae18ab7d16af7ef3b73035a10