Skip to content

Commit c315692

Browse files
authored
fix: finalize cmd: skip processing blobs of OTA image metadata files/non-image payloads (#84)
This PR fixes an edge condition causing OTA failed due to blob missing caused by finalize cmd's blob storage optimizing when blobs come from OTA image metadata files(like sys_config.yaml) or from otaclient release packages also presented in the OTA image payload. This PR introduces a mechanism to collect blobs' digests that belongs to OTA image metadata files(like sys_config.yaml) or otaclient packages, and skip these digests when optimizing the blobs storage. Other minor fix: 1. Dockerfile: fix alpha/beta/rc build version file generation. With this PR, ota-image-builder will bump to version v0.9.0.
1 parent d4f4281 commit c315692

7 files changed

Lines changed: 85 additions & 37 deletions

File tree

docker/builder_release/Dockerfile

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,11 @@ FROM ${BUILD_BASE} AS app_build
66

77
ARG PYINSTALLER_VER=6.16.0
88

9-
RUN mkdir /build
10-
# minimum required files for building app
11-
COPY ./src /build/src
12-
COPY ./pyproject.toml ./uv.lock ./README.md /build/
13-
9+
COPY . /build
1410
COPY --from=ghcr.io/astral-sh/uv:0.9.4 /uv /uvx /bin/
1511

1612
WORKDIR /build
17-
# NOTE: mount .git for hatch-vcs to determine the version
18-
RUN --mount=type=bind,source=./.git,target=/build/.git,ro \
19-
set -eux; \
13+
RUN set -eux; \
2014
apt-get update -qq; \
2115
apt-get install -y -qq --no-install-recommends \
2216
git python3 python3-setuptools; \

docker/e2e_test_base/Dockerfile

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ FROM ${SYS_IMG} AS sys_img
1010

1111
FROM ${UBUNTU_BASE} AS app_builder
1212

13-
# copy minimum required files for build
14-
COPY ./pyproject.toml ./.python-version ./uv.lock /project/
15-
COPY ./src /project/src
13+
COPY . /project
1614

1715
ARG UV_VERSION
1816

src/ota_image_builder/cmds/finalize.py

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
from ota_image_libs.common import tmp_fname
2424
from ota_image_libs.v1.image_index.utils import ImageIndexHelper
25+
from ota_image_libs.v1.image_manifest.schema import ImageManifest
26+
from ota_image_libs.v1.otaclient_package.schema import OTAClientPackageManifest
2527
from ota_image_libs.v1.resource_table.schema import (
2628
ZstdCompressedResourceTableDescriptor,
2729
)
@@ -89,6 +91,52 @@ def finalize_cmd_args(
8991
finalize_cmd_args.set_defaults(handler=finalize_cmd)
9092

9193

94+
def _collect_protected_resources_digest(_index_helper: ImageIndexHelper) -> set[bytes]:
95+
"""Scan through OTA image, collect blob digests that don't belong to any system image.
96+
97+
When optimizing the blob storage, we MUST skip processing these digests.
98+
The blobs that don't belong any image payload:
99+
1. image_payload: sys_config and file_table files.
100+
2. otaclient_release: manifest.json and release packages.
101+
3. resource_table itself.
102+
103+
NOTE(20251219): an example case is when the system image is dev build, and contains
104+
the pilot-auto source code within the built system image.
105+
This will result in blob of sys_config file is also part of the system image,
106+
thus being processed during blob storage optimization, and the original blob
107+
being removed.
108+
"""
109+
_res: set[bytes] = set()
110+
_resource_dir = _index_helper.image_resource_dir
111+
for manifest_descriptor in _index_helper.image_index.manifests:
112+
_res.add(manifest_descriptor.digest.digest)
113+
if isinstance(manifest_descriptor, ImageManifest.Descriptor):
114+
_manifest = manifest_descriptor.load_metafile_from_resource_dir(
115+
_resource_dir
116+
)
117+
118+
for _file_table_descriptor in _manifest.layers:
119+
_res.add(_file_table_descriptor.digest.digest)
120+
121+
_image_config_descriptor = _manifest.config
122+
_res.add(_image_config_descriptor.digest.digest)
123+
124+
_image_config = _image_config_descriptor.load_metafile_from_resource_dir(
125+
_resource_dir
126+
)
127+
if _sys_config_descriptor := _image_config.sys_config:
128+
_res.add(_sys_config_descriptor.digest.digest)
129+
_res.add(_image_config.file_table.digest.digest)
130+
elif isinstance(manifest_descriptor, OTAClientPackageManifest.Descriptor):
131+
_manifest = manifest_descriptor.load_metafile_from_resource_dir(
132+
_resource_dir
133+
)
134+
_res.add(_manifest.config.digest.digest)
135+
for _payload in _manifest.layers:
136+
_res.add(_payload.digest.digest)
137+
return _res
138+
139+
92140
def finalize_cmd(args: Namespace) -> None:
93141
logger.debug(f"calling {finalize_cmd.__name__} with {args}")
94142
image_root = Path(args.image_root)
@@ -98,6 +146,10 @@ def finalize_cmd(args: Namespace) -> None:
98146
index_helper = ImageIndexHelper(image_root)
99147
logger.info(f"Finalize and optimize OTA image at {image_root} ...")
100148
logger.info("Optimizing the blob storage of the OTA image ...")
149+
protected_resources = _collect_protected_resources_digest(index_helper)
150+
logger.debug(
151+
f"Skip the protected resources: {[d.hex() for d in protected_resources]}"
152+
)
101153

102154
resource_dir = index_helper.image_resource_dir
103155
_old_rstable_descriptor = index_helper.image_index.image_resource_table
@@ -123,7 +175,9 @@ def finalize_cmd(args: Namespace) -> None:
123175
if not args.o_skip_bundle:
124176
logger.info("Apply bundle filter to the blob storage ...")
125177
BundleFilterProcesser(
126-
resource_dir=resource_dir, rst_dbf=_working_rstable
178+
resource_dir=resource_dir,
179+
rst_dbf=_working_rstable,
180+
protected_resources=protected_resources,
127181
).process()
128182
logger.info(
129183
f"Finish applying bundle filter: time cost: {int(time.time() - start_time)}s"
@@ -135,7 +189,9 @@ def finalize_cmd(args: Namespace) -> None:
135189
logger.info("Apply compression filter to the blob storage ...")
136190
_start_time = time.time()
137191
CompressionFilterProcesser(
138-
resource_dir=resource_dir, rst_dbf=_working_rstable
192+
resource_dir=resource_dir,
193+
rst_dbf=_working_rstable,
194+
protected_resources=protected_resources,
139195
).process()
140196
logger.info(
141197
f"Finish applying compression filter: time cost: {int(time.time() - _start_time)}s"
@@ -149,7 +205,9 @@ def finalize_cmd(args: Namespace) -> None:
149205
logger.info("Apply slice filter to the blob storage ...")
150206
_start_time = time.time()
151207
SliceFilterProcesser(
152-
resource_dir=resource_dir, rst_dbf=_working_rstable
208+
resource_dir=resource_dir,
209+
rst_dbf=_working_rstable,
210+
protected_resources=protected_resources,
153211
).process()
154212
logger.info(
155213
f"Finish applying slice filter: time cost: {int(time.time() - _start_time)}s"

src/ota_image_builder/v1/_image_config.py

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,8 @@
1616

1717
import logging
1818
from datetime import datetime
19-
from pathlib import Path
2019
from typing import Any
2120

22-
import yaml
2321
from ota_image_libs.common import AliasEnabledModel
2422
from ota_image_libs.v1.annotation_keys import (
2523
OS,
@@ -50,23 +48,6 @@ class AddImageConfigAnnotations(AliasEnabledModel):
5048
os_version: str | None = Field(alias=OS_VERSION, default=None)
5149

5250

53-
def add_sys_config(sys_cfg_fpath: Path, resource_dir: Path) -> SysConfig.Descriptor:
54-
try:
55-
_raw = yaml.safe_load(sys_cfg_fpath.read_text())
56-
SysConfig.model_validate(_raw)
57-
except Exception as e:
58-
raise ValueError(f"invalid sys_config file {sys_cfg_fpath}") from e
59-
return SysConfig.Descriptor.add_file_to_resource_dir(sys_cfg_fpath, resource_dir)
60-
61-
62-
def add_file_table(
63-
file_table_fpath: Path, resource_dir: Path
64-
) -> ZstdCompressedFileTableDescriptor:
65-
return ZstdCompressedFileTableDescriptor.add_file_to_resource_dir(
66-
file_table_fpath, resource_dir, remove_origin=True
67-
)
68-
69-
7051
def compose_image_config(
7152
*,
7253
file_table_descriptor: ZstdCompressedFileTableDescriptor,

src/ota_image_builder/v1/_resource_process/_bundle_filter.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,16 +69,19 @@ class BundleCompressedResult(NamedTuple):
6969
compressed_size: int
7070

7171

72-
def _batch_entries(
72+
def _batch_entries_with_filter(
7373
entries_to_bundle_gen: Generator[EntryToBeBundled],
7474
*,
7575
expected_bundle_size: int,
7676
min_bundle_ratio: float = MINIMUM_BUNDLE_SIZE_RATIO,
77+
excluded_resources: set[bytes],
7778
) -> Generator[tuple[int, list[EntryToBeBundled]]]:
7879
_batch = []
7980
_this_batch_size = 0
8081
for _entry in entries_to_bundle_gen:
81-
_, _, _entry_size = _entry
82+
_, _digest, _entry_size = _entry
83+
if _digest in excluded_resources:
84+
continue
8285

8386
_batch.append(_entry)
8487
_this_batch_size += _entry_size
@@ -200,7 +203,9 @@ def __init__(
200203
bundle_upper_bound: int = cfg.BUNDLE_UPPER_THRESHOULD,
201204
bundle_blob_size: int = cfg.BUNDLE_SIZE,
202205
bundle_compressed_max_sum: int = cfg.BUNDLES_COMPRESSED_MAXIMUM_SUM,
206+
protected_resources: set[bytes],
203207
) -> None:
208+
self._protected_resources = protected_resources
204209
self._resource_dir = resource_dir
205210
self._db_helper = ResourceTableDBHelper(rst_dbf)
206211
self._lower_bound = bundle_lower_bound
@@ -232,8 +237,10 @@ def process(self):
232237
_row_factory=sqlite3.Row,
233238
) # type: ignore[assignment]
234239
# fmt: on
235-
batch_gen = _batch_entries(
236-
entries_to_bundle_gen, expected_bundle_size=self._bundle_blob_size
240+
batch_gen = _batch_entries_with_filter(
241+
entries_to_bundle_gen,
242+
expected_bundle_size=self._bundle_blob_size,
243+
excluded_resources=self._protected_resources,
237244
)
238245

239246
cctx = zstandard.ZstdCompressor(level=cfg.BUNDLE_ZSTD_COMPRESSION_LEVEL)

src/ota_image_builder/v1/_resource_process/_compression_filter.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ def __init__(
6363
read_size: int = cfg.READ_SIZE,
6464
worker_threads: int = cfg.COMPRESSION_RESOURCE_SCAN_WORKER_THREADS,
6565
concurrent_jobs: int = cfg.COMPRESSION_MAX_CONCURRENT,
66+
protected_resources: set[bytes],
6667
) -> None:
68+
self._protected_resources = protected_resources
6769
self._read_size = read_size
6870
self._resource_dir = resource_dir
6971
self._db_helper = ResourceTableDBHelper(rst_dbf)
@@ -157,6 +159,9 @@ def process(self) -> None:
157159
for _raw_row in rs_orm.orm_select_entries(
158160
_stmt=_stmt, _row_factory=sqlite3.Row
159161
):
162+
if _raw_row[1] in self._protected_resources:
163+
continue
164+
160165
origin_size += _raw_row[-1]
161166
submit_with_se(
162167
self._process_one_entry_at_thread,

src/ota_image_builder/v1/_resource_process/_slice_filter.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ def __init__(
119119
worker_threads: int = cfg.WORKER_THREADS,
120120
concurrent_tasks: int = cfg.SLICE_CONCURRENT_TASKS,
121121
db_update_batch_size: int = cfg.SLICE_UPDATE_BATCH_SIZE,
122+
protected_resources: set[bytes],
122123
) -> None:
124+
self._protected_resources = protected_resources
123125
self._update_batch = db_update_batch_size
124126
self._worker_threads = worker_threads
125127
self._se = Semaphore(concurrent_tasks)
@@ -214,6 +216,9 @@ def process(self):
214216
_row_factory=sqlite3.Row,
215217
):
216218
resource_id, entry_digest, entry_size = _row
219+
if entry_digest in self._protected_resources:
220+
continue
221+
217222
sliced_count += 1
218223
sliced_size += entry_size
219224

0 commit comments

Comments
 (0)