From 69286f4057cf4fbfc59580e005d08c5de58da7db Mon Sep 17 00:00:00 2001 From: Isaac Hermida Date: Mon, 20 Apr 2026 11:24:19 +0200 Subject: [PATCH] Generate unique DCP package ids https://onedigi.atlassian.net/browse/DEL-10035 https://onedigi.atlassian.net/browse/DEL-10081 Signed-off-by: Isaac Hermida --- meta-digi-containers/README.md | 27 ++++--- .../images/dey-image-container-artifact.inc | 6 +- meta-digi-containers/scripts/generate-dcp.py | 72 ++++++++++++++++--- 3 files changed, 84 insertions(+), 21 deletions(-) diff --git a/meta-digi-containers/README.md b/meta-digi-containers/README.md index 2da5b4d7b..b3a4a5035 100644 --- a/meta-digi-containers/README.md +++ b/meta-digi-containers/README.md @@ -68,8 +68,11 @@ The script generates exactly one runtime artifact per execution. Notes: - `--output-dir` is optional. If omitted, the script writes the DCP to the current directory. -- The output file name is always derived from manifest fields as: - - `_artifact__.tar.gz` +- The generator appends a unique suffix to the input `package_id` using the + `created_at` timestamp encoded in base36 milliseconds. +- The output file name is always derived from the generated package ID and + manifest fields as: + - `_artifact__.tar.gz` - For Podman payloads, the final internal payload name is always `payload/image.tar` - For LXC payloads, the generator requires a Yocto-style `.tar.gz` bundle, validates its layout, and stores it inside the DCP using the original file name and format without re-packaging it. @@ -87,9 +90,9 @@ python3 meta-digi-containers/scripts/generate-dcp.py \ --payload /tmp/image.tar ``` -This generates: +This generates a bundle named like: -- `./flutter-demo_artifact_podman_ccmp25-dvk.tar.gz` +- `./flutter-demo-_artifact_podman_ccmp25-dvk.tar.gz` ## Digi Remote Manager metrics support @@ -97,6 +100,10 @@ This generates: publish container statistics through the local CCCS Python API. For generated DCPs, per-container DRM sampling is enabled through `registration_defaults.stats_publish` in the artifact manifest. +Those manifest defaults establish the initial runtime policy on the target. +After installation, mutable policy such as `autostart`, `monitor`, `restart`, and +`stats_publish` can be inspected or updated through the container manager `config` +`get` and `set` operations without regenerating the DCP. The image recipe generates the DCP automatically from the following variables: - `CONTAINER_STATS_PUBLISH_ENABLED` @@ -169,14 +176,16 @@ Outputs are generated in: Final outputs: -- `${CONTAINER_PACKAGE_ID}_artifact_podman_.tar.gz` -- `${CONTAINER_PACKAGE_ID}_artifact_lxc_.tar.gz` +- `${CONTAINER_PACKAGE_ID}-_artifact_podman_.tar.gz` +- `${CONTAINER_PACKAGE_ID}-_artifact_lxc_.tar.gz` Notes: -- The generator script makes the DCP filename using `package_id`, `runtime`, and - `device_types[0]` fields from the manifest file. -- In Yocto builds, `CONTAINER_PACKAGE_ID` defaults to `${CONTAINER_NAME}` +- The generator script appends a base36-encoded millisecond timestamp suffix to + the input `package_id`, stores that generated value in the final + `manifest.json`, and uses it in the DCP file name. +- In Yocto builds, `CONTAINER_PACKAGE_ID` defaults to `${CONTAINER_NAME}` before + the generator adds the unique suffix. Intermediate outputs generated during the build (LXC bundle, Podman archive, OCI/rootfs files) are removed automatically at the end of artifact creation. diff --git a/meta-digi-containers/recipes-core/images/dey-image-container-artifact.inc b/meta-digi-containers/recipes-core/images/dey-image-container-artifact.inc index fe1bb8e46..81de04ae9 100644 --- a/meta-digi-containers/recipes-core/images/dey-image-container-artifact.inc +++ b/meta-digi-containers/recipes-core/images/dey-image-container-artifact.inc @@ -109,14 +109,14 @@ open(sys.argv[1], "w", encoding="utf-8").write(json.dumps(payload, indent=2) + " changelog_arg="--changelog ${template_dir}/metadata/changelog.txt" fi - python3 "${generator_script}" \ + generated_output="$(python3 "${generator_script}" \ --manifest "${manifest_path}" \ --payload "${src_payload}" \ --output-dir "${DEPLOY_DIR_IMAGE}" \ ${readme_arg} \ - ${changelog_arg} + ${changelog_arg})" - generated_output="${DEPLOY_DIR_IMAGE}/${CONTAINER_PACKAGE_ID}_artifact_${runtime}_${primary_device_type}.tar.gz" + generated_output="$(printf '%s' "${generated_output}" | tail -n 1)" if [ ! -s "${generated_output}" ]; then bbfatal "Container artifact bundle not generated correctly: ${generated_output}" fi diff --git a/meta-digi-containers/scripts/generate-dcp.py b/meta-digi-containers/scripts/generate-dcp.py index e073cfb2c..0341c47c5 100755 --- a/meta-digi-containers/scripts/generate-dcp.py +++ b/meta-digi-containers/scripts/generate-dcp.py @@ -15,11 +15,33 @@ import sys import tarfile import tempfile +BASE36_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz" + def fail(message: str) -> "NoReturn": raise SystemExit(f"error: {message}") +def to_base36(value: int) -> str: + if value < 0: + fail("cannot convert negative values to base36") + if value == 0: + return "0" + result: list[str] = [] + while value: + value, remainder = divmod(value, 36) + result.append(BASE36_ALPHABET[remainder]) + return "".join(reversed(result)) + + +def format_created_at(timestamp: datetime) -> str: + return timestamp.astimezone(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z") + + +def build_generated_package_id(base_package_id: str, *, created_at_ms: int) -> str: + return f"{base_package_id}-{to_base36(created_at_ms)}" + + def load_manifest(path: Path) -> dict: try: data = json.loads(path.read_text(encoding="utf-8")) @@ -167,8 +189,8 @@ def sha256(path: Path) -> str: return digest.hexdigest() -def build_output_name(manifest: dict) -> str: - return f"{manifest['package_id']}_artifact_{manifest['runtime']}_{manifest['device_types'][0]}.tar.gz" +def build_output_name(*, package_id: str, runtime: str, device_type: str) -> str: + return f"{package_id}_artifact_{runtime}_{device_type}.tar.gz" def ensure_lxc_layout(payload: Path) -> tuple[str, str]: @@ -188,9 +210,16 @@ def ensure_lxc_layout(payload: Path) -> tuple[str, str]: return dirs[0].name, dirs[0].name fail("invalid payload: LXC archive must contain rootfs/ and config") -def write_default_readme(path: Path, manifest: dict, *, created_at: str, payload_name: str) -> None: +def write_default_readme( + path: Path, + manifest: dict, + *, + package_id: str, + created_at: str, + payload_name: str, +) -> None: content = ( - f"Package: {manifest['package_id']}\n" + f"Package: {package_id}\n" f"Version: {manifest['version']}\n" f"Runtime: {manifest['runtime']}\n" f"Platform: {manifest['device_types'][0]}\n" @@ -218,9 +247,17 @@ def write_manifest(path: Path, manifest: dict) -> None: path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") -def build_final_manifest(base: dict, *, artifact_type: str, digest: str, size_bytes: int, created_at: str) -> dict: +def build_final_manifest( + base: dict, + *, + package_id: str, + artifact_type: str, + digest: str, + size_bytes: int, + created_at: str, +) -> dict: output = { - "package_id": base["package_id"], + "package_id": package_id, "version": base["version"], "runtime": base["runtime"], "artifact_type": artifact_type, @@ -281,9 +318,19 @@ def main() -> int: except OSError as exc: fail(f"cannot create output directory {output_dir}: {exc}") - output_name = build_output_name(manifest) + created_at_dt = datetime.now(timezone.utc) + created_at_ms = int(created_at_dt.timestamp() * 1000) + created_at = format_created_at(created_at_dt) + generated_package_id = build_generated_package_id( + manifest["package_id"], + created_at_ms=created_at_ms, + ) + output_name = build_output_name( + package_id=generated_package_id, + runtime=manifest["runtime"], + device_type=manifest["device_types"][0], + ) output_path = output_dir / output_name - created_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") with tempfile.TemporaryDirectory(prefix="dcp-bundle-") as tmpdir: staging_root = Path(tmpdir) @@ -311,6 +358,7 @@ def main() -> int: final_manifest = build_final_manifest( manifest, + package_id=generated_package_id, artifact_type=artifact_type, digest=digest, size_bytes=size_bytes, @@ -321,7 +369,13 @@ def main() -> int: stage_metadata( readme_path, metadata_dir / "README.txt", - lambda dst: write_default_readme(dst, manifest, created_at=created_at, payload_name=payload_name), + lambda dst: write_default_readme( + dst, + manifest, + package_id=generated_package_id, + created_at=created_at, + payload_name=payload_name, + ), ) stage_metadata(changelog_path, metadata_dir / "changelog.txt", write_default_changelog)