fix(bundle): bind deployment metadata to the selected release
This commit is contained in:
@@ -118,8 +118,11 @@ cp .env.example .env
|
||||
docker compose -f docker-compose.enterprise-clean.yml up -d
|
||||
```
|
||||
|
||||
Полный enterprise-бандл содержит три образа; importer проверяет явно выбранный versioned manifest,
|
||||
контрольные суммы и идентичности образов. Старые невёрсионированные и light manifest он отказывает.
|
||||
Полный enterprise-бандл содержит три образа; importer требует manifest v2 и проверяет контрольные суммы
|
||||
образов, их идентичности и SHA256 сгенерированных Compose и `.env.example` до обращения к Docker.
|
||||
Храните каждый релиз полным отдельным каталогом: после перезаписи Compose другим релизом старый manifest
|
||||
получит отказ. Редактируйте скопированный `.env`, сохраняя `.env.example` неизменным.
|
||||
Manifest v1, невёрсионированные и light manifest importer отказывает.
|
||||
`bundle:light` содержит только application-образ: для offline Compose нужно отдельно предоставить
|
||||
и загрузить `postgres:16-alpine`, скопировать `.env.light.example` в `.env` и сохранить его точный `IMAGE`.
|
||||
Наличие PostgreSQL на целевом хосте нельзя считать гарантированным light-архивом.
|
||||
|
||||
@@ -140,11 +140,15 @@ def test_actual_light_generator_requires_its_exported_tag_and_explicit_deploymen
|
||||
# #region Test.ReleaseTemplates.EnterpriseManifest [C:2] [TYPE Function]
|
||||
def test_actual_enterprise_manifest_is_versioned_and_keeps_all_existing_metadata(tmp_path):
|
||||
dist, _stdout = _generate(tmp_path,
|
||||
'generate_deploy_compose release-manifest-fixture superset-tools-backend:release-manifest-fixture superset-tools-frontend:release-manifest-fixture superset-tools-postgres:release-manifest-fixture; '
|
||||
'generate_bundle_env release-manifest-fixture; '
|
||||
'generate_manifest release-manifest-fixture superset-tools-backend:release-manifest-fixture superset-tools-frontend:release-manifest-fixture superset-tools-postgres:release-manifest-fixture')
|
||||
manifest = json.loads((dist / "manifest.release-manifest-fixture.json").read_text())
|
||||
assert set(manifest) == {"manifest_version", "bundle_kind", "release_tag", "generated_at_utc", "images",
|
||||
"compose_file", "env_template", "env_bootstrap_fields", "checksums_file"}
|
||||
assert manifest["manifest_version"] == 1
|
||||
"compose_file", "env_template", "env_bootstrap_fields", "checksums_file", "compose_sha256", "env_template_sha256"}
|
||||
assert manifest["manifest_version"] == 2
|
||||
assert manifest["compose_sha256"] == hashlib.sha256((dist / manifest["compose_file"]).read_bytes()).hexdigest()
|
||||
assert manifest["env_template_sha256"] == hashlib.sha256((dist / manifest["env_template"]).read_bytes()).hexdigest()
|
||||
assert manifest["bundle_kind"] == "enterprise"
|
||||
assert manifest["release_tag"] == "release-manifest-fixture"
|
||||
assert [image["role"] for image in manifest["images"]] == ["postgres", "backend", "frontend"]
|
||||
|
||||
22
build.sh
22
build.sh
@@ -551,11 +551,18 @@ volumes:
|
||||
DEPLOY
|
||||
}
|
||||
|
||||
# #region Tooling.Build.BundleManifest [C:3] [TYPE Function]
|
||||
# @PRE Deployment Compose and env template have already been generated for this bundle.
|
||||
# @POST Enterprise-v2 binds exact deployment file bytes as well as the three image archive identities.
|
||||
# @RATIONALE Fixed deployment filenames can be replaced by another release in a shared output directory.
|
||||
# @REJECTED Version1 manifests cannot detect mixed-release Compose and are not an import fallback.
|
||||
generate_manifest() {
|
||||
local tag="$1" backend_tag="$2" frontend_tag="$3" postgres_tag="$4"
|
||||
local suffix="${5:-}"
|
||||
|
||||
local backend_id frontend_id postgres_id
|
||||
local backend_id frontend_id postgres_id compose_sha256 env_template_sha256
|
||||
compose_sha256="$(sha256sum "${DIST_ROOT}/docker-compose.enterprise-clean.yml" | awk '{print $1}')" || return 1
|
||||
env_template_sha256="$(sha256sum "${DIST_ROOT}/.env.example" | awk '{print $1}')" || return 1
|
||||
backend_id="$(get_image_id "${backend_tag}")"
|
||||
frontend_id="$(get_image_id "${frontend_tag}")"
|
||||
postgres_id="$(get_image_id "${postgres_tag}")"
|
||||
@@ -575,7 +582,9 @@ postgres_image_id=${postgres_id}
|
||||
postgres_repo_digest=$(get_repo_digest "${postgres_tag}")
|
||||
postgres_archive=superset-tools-postgres.${tag}${suffix}.tar.xz
|
||||
compose_file=docker-compose.enterprise-clean.yml
|
||||
compose_sha256=${compose_sha256}
|
||||
env_template=.env.example
|
||||
env_template_sha256=${env_template_sha256}
|
||||
env_bootstrap_fields=INITIAL_ADMIN_CREATE,INITIAL_ADMIN_USERNAME,INITIAL_ADMIN_PASSWORD,INITIAL_ADMIN_EMAIL
|
||||
checksums_file=sha256sums.${tag}${suffix}.txt
|
||||
generated_at_utc=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
@@ -583,7 +592,7 @@ EOF
|
||||
|
||||
cat > "${DIST_ROOT}/manifest.${tag}${suffix}.json" <<EOF
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"manifest_version": 2,
|
||||
"bundle_kind": "enterprise",
|
||||
"release_tag": "${tag}",
|
||||
"generated_at_utc": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
|
||||
@@ -611,7 +620,9 @@ EOF
|
||||
}
|
||||
],
|
||||
"compose_file": "docker-compose.enterprise-clean.yml",
|
||||
"compose_sha256": "${compose_sha256}",
|
||||
"env_template": ".env.example",
|
||||
"env_template_sha256": "${env_template_sha256}",
|
||||
"env_bootstrap_fields": [
|
||||
"INITIAL_ADMIN_CREATE",
|
||||
"INITIAL_ADMIN_USERNAME",
|
||||
@@ -622,6 +633,7 @@ EOF
|
||||
}
|
||||
EOF
|
||||
}
|
||||
# #endregion Tooling.Build.BundleManifest
|
||||
|
||||
# ======================================================================
|
||||
# GENERATE BUNDLE ENV TEMPLATE (with bundled postgres defaults)
|
||||
@@ -766,12 +778,10 @@ bundle_release() {
|
||||
"superset-tools-frontend.${tag}.tar.xz" \
|
||||
"superset-tools-postgres.${tag}.tar.xz" > "sha256sums.${tag}.txt")
|
||||
|
||||
# Generate deploy compose + manifest
|
||||
# Generate both deployment files before binding their exact bytes in the manifest.
|
||||
generate_deploy_compose "${tag}" "${backend_tag}" "${frontend_tag}" "${postgres_tag}"
|
||||
generate_manifest "${tag}" "${backend_tag}" "${frontend_tag}" "${postgres_tag}"
|
||||
|
||||
# Generate env template for the bundle
|
||||
generate_bundle_env "${tag}"
|
||||
generate_manifest "${tag}" "${backend_tag}" "${frontend_tag}" "${postgres_tag}"
|
||||
cp "${SCRIPT_DIR}/scripts/import_docker_bundle.py" "${DIST_ROOT}/import_docker_bundle.py"
|
||||
cp "${SCRIPT_DIR}/scripts/docker_bundle_contract.py" "${DIST_ROOT}/docker_bundle_contract.py"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
## @{ Tooling.DockerBundle.Contract [C:4] [TYPE ADR]
|
||||
@BRIEF Full enterprise-v1 offline image import, validated before Docker mutation.
|
||||
@BRIEF Full enterprise-v2 offline image import with bound deployment metadata, validated before Docker operations.
|
||||
@RATIONALE A checksum does not establish image tags, image identity, publisher authenticity or source provenance.
|
||||
@REJECTED Unversioned/light manifests and shell xz pipelines are not accepted compatibility paths.
|
||||
|
||||
@@ -12,7 +12,9 @@ python3 import_docker_bundle.py --bundle-dir . \
|
||||
--manifest manifest.v1.0.0.json --expected-release-tag v1.0.0
|
||||
```
|
||||
|
||||
The manifest must contain exactly `manifest_version`, `bundle_kind`, `release_tag`, `generated_at_utc`, `images`, `compose_file`, `env_template`, `env_bootstrap_fields`, and `checksums_file`. Version is integer1, kind `enterprise`, release equals the explicitly requested Docker tag, timestamp is UTC `YYYY-MM-DDTHH:MM:SSZ`. The deployment fields are exactly `docker-compose.enterprise-clean.yml`, `.env.example`, the ordered `INITIAL_ADMIN_CREATE/USERNAME/PASSWORD/EMAIL` names, and `sha256sums.<tag>.txt`.
|
||||
The manifest must contain exactly `manifest_version`, `bundle_kind`, `release_tag`, `generated_at_utc`, `images`, `compose_file`, `compose_sha256`, `env_template`, `env_template_sha256`, `env_bootstrap_fields`, and `checksums_file`. Version is integer2, kind `enterprise`, release equals the explicitly requested Docker tag, timestamp is UTC `YYYY-MM-DDTHH:MM:SSZ`. The deployment fields are exactly `docker-compose.enterprise-clean.yml`, `.env.example`, the ordered `INITIAL_ADMIN_CREATE/USERNAME/PASSWORD/EMAIL` names, and `sha256sums.<tag>.txt`.
|
||||
|
||||
The producer generates both deployment files before publishing the manifest. Each required metadata digest is lowercase64hex SHA256 of the exact corresponding file bytes. The importer reads each file through its regular no-follow descriptor with a 1MiB bound and verifies both hashes before any Docker query or load. Replacing the shared Compose with another release or editing the template makes the earlier manifest refuse; preserve each release as a complete separate directory. The operator's editable `.env` is not part of this verification. Version1 has no fallback. These two hashes remain separate from the exact three archive checksum lines and do not establish authenticity or parse Compose semantics.
|
||||
|
||||
Images contain exactly three unique roles `postgres`, `backend`, `frontend`. Each record has exactly `role`, `image`, `image_id`, `repo_digest`, `archive`: image is `superset-tools-<role>:<tag>`, ID is lowercase `sha256:<64hex>`, archive is `superset-tools-<role>.<tag>.tar.xz`. Repository digests are informational explicit digest references or `unavailable`. The checksum file has exactly three SHA256 lines, one per expected archive, without duplicates or extras. Unversioned, single-image and light bundles require another explicit schema/kind and are refused here.
|
||||
|
||||
@@ -28,5 +30,5 @@ After complete offline validation, all three target tags are checked before the
|
||||
|
||||
CLI stdout is JSON (except `--help`): exit0 means `verified`, `imported` or `already_present`; exit2 is preflight refusal with no load attempt; exit3 is load/post-inspect/input-change failure after a role attempt. `verified_roles`, `loaded_roles`, `existing_roles`, `attempted_roles`, fixed `code`, and `partial_load_possible` distinguish completed work from possible partial Docker mutation. Pipeline RCs are retained when available. `--verify-only` performs no Docker query/mutation and reports `target_state: not_checked`.
|
||||
|
||||
Checksums provide integrity, **not authenticity**. Output reports `authenticity: not_established_by_checksums` and `source_identity: {status: unavailable, reason: manifest_v1_has_no_source_identity}`. Tags, local Git and plausible metadata never substitute for absent source provenance. A failed import can leave already-loaded images; inspect that explicit state before a separately chosen retry. Never infer a successful deployment from image import alone.
|
||||
Checksums provide integrity, **not authenticity**. Output reports `authenticity: not_established_by_checksums` and `source_identity: {status: unavailable, reason: manifest_v2_has_no_source_identity}`. Tags, local Git and plausible metadata never substitute for absent source provenance. A failed import can leave already-loaded images; inspect that explicit state before a separately chosen retry. Never infer a successful deployment from image import alone.
|
||||
## @} Tooling.DockerBundle.Contract
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# #region Tooling.DockerBundle.Import [C:4] [TYPE Module] [SEMANTICS bundle,import,identity]
|
||||
# @BRIEF Verify a complete enterprise-v1 image bundle before loading any image into Docker.
|
||||
# @BRIEF Verify a complete enterprise-v2 image bundle and bound deployment metadata before Docker operations.
|
||||
# @INVARIANT All archive/checksum identities pass before Docker mutation; verified open descriptors are reused for load.
|
||||
# @RATIONALE Bundle checksums prove byte integrity, not publisher authenticity or absent source provenance.
|
||||
# @REJECTED Shell xz pipelines and tag-only verification can hide decompressor failure or load a foreign image/tag.
|
||||
@@ -25,7 +25,7 @@ from docker_bundle_contract import IMAGE_ID, Refusal, decode, validate_archive
|
||||
ROLES = ("postgres", "backend", "frontend")
|
||||
TAG = re.compile(r"[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}")
|
||||
FIELDS = {"manifest_version", "bundle_kind", "release_tag", "generated_at_utc", "images", "compose_file",
|
||||
"env_template", "env_bootstrap_fields", "checksums_file"}
|
||||
"env_template", "env_bootstrap_fields", "checksums_file", "compose_sha256", "env_template_sha256"}
|
||||
IMAGE_FIELDS = {"role", "image", "image_id", "repo_digest", "archive"}
|
||||
ENV_FIELDS = ["INITIAL_ADMIN_CREATE", "INITIAL_ADMIN_USERNAME", "INITIAL_ADMIN_PASSWORD", "INITIAL_ADMIN_EMAIL"]
|
||||
DOCKER_QUERY_TIMEOUT = 10
|
||||
@@ -108,7 +108,7 @@ def manifest(raw, expected):
|
||||
raise Refusal("invalid_expected_release_tag")
|
||||
value = decode(raw)
|
||||
if (not isinstance(value, dict) or set(value) != FIELDS or type(value["manifest_version"]) is not int
|
||||
or value["manifest_version"] != 1 or value["bundle_kind"] != "enterprise"):
|
||||
or value["manifest_version"] != 2 or value["bundle_kind"] != "enterprise"):
|
||||
raise Refusal("unsupported_bundle_manifest")
|
||||
if value["release_tag"] != expected:
|
||||
raise Refusal("release_tag_conflict")
|
||||
@@ -121,6 +121,9 @@ def manifest(raw, expected):
|
||||
if (value["compose_file"] != "docker-compose.enterprise-clean.yml" or value["env_template"] != ".env.example"
|
||||
or value["env_bootstrap_fields"] != ENV_FIELDS or value["checksums_file"] != f"sha256sums.{expected}.txt"):
|
||||
raise Refusal("manifest_deployment_contract")
|
||||
for field in ("compose_sha256", "env_template_sha256"):
|
||||
if not isinstance(value[field], str) or re.fullmatch(r"[0-9a-f]{64}", value[field]) is None:
|
||||
raise Refusal("manifest_deployment_checksum")
|
||||
if not isinstance(value["images"], list) or len(value["images"]) != 3:
|
||||
raise Refusal("manifest_image_roles")
|
||||
images = {}
|
||||
@@ -234,13 +237,19 @@ def load(archive):
|
||||
def execute(directory, manifest_name, expected, verify_only=False):
|
||||
result = {"status": "refused", "code": None, "mode": "verify-only" if verify_only else "import",
|
||||
"verified_roles": [], "loaded_roles": [], "existing_roles": [], "attempted_roles": [],
|
||||
"partial_load_possible": False, "source_identity": {"status": "unavailable", "reason": "manifest_v1_has_no_source_identity"},
|
||||
"partial_load_possible": False, "source_identity": {"status": "unavailable", "reason": "manifest_v2_has_no_source_identity"},
|
||||
"authenticity": "not_established_by_checksums"}
|
||||
try:
|
||||
with ExitStack() as stack:
|
||||
bundle = Bundle(directory)
|
||||
stack.callback(bundle.close)
|
||||
value, images = manifest(bundle.open(manifest_name, stack).read(1 << 20), expected)
|
||||
metadata = []
|
||||
for name, digest in (("compose_file", "compose_sha256"), ("env_template", "env_template_sha256")):
|
||||
item = bundle.open(value[name], stack)
|
||||
if hashlib.sha256(item.read(1 << 20)).hexdigest() != value[digest]:
|
||||
raise Refusal("deployment_metadata_checksum_conflict")
|
||||
metadata.append(item)
|
||||
sums = checksums(bundle.open(value["checksums_file"], stack).read(65536), images)
|
||||
archives = {image["role"]: bundle.open(image["archive"], stack) for image in images}
|
||||
for image in images:
|
||||
@@ -255,6 +264,8 @@ def execute(directory, manifest_name, expected, verify_only=False):
|
||||
validate_archive(archive.stream, image)
|
||||
archive.stable()
|
||||
result["verified_roles"].append(image["role"])
|
||||
for item in metadata:
|
||||
item.stable()
|
||||
if verify_only:
|
||||
result.update(status="verified", target_state="not_checked")
|
||||
return 0, result
|
||||
@@ -263,6 +274,8 @@ def execute(directory, manifest_name, expected, verify_only=False):
|
||||
for image in images:
|
||||
if present[image["role"]]:
|
||||
continue
|
||||
for item in metadata:
|
||||
item.stable()
|
||||
result["attempted_roles"].append(image["role"])
|
||||
xz_rc, docker_rc = load(archives[image["role"]])
|
||||
result.update(xz_rc=xz_rc, docker_load_rc=docker_rc)
|
||||
@@ -273,6 +286,8 @@ def execute(directory, manifest_name, expected, verify_only=False):
|
||||
result["loaded_roles"].append(image["role"])
|
||||
for image in images:
|
||||
inspect(image)
|
||||
for item in metadata:
|
||||
item.stable()
|
||||
result.update(status="imported" if result["loaded_roles"] else "already_present")
|
||||
return 0, result
|
||||
except (Refusal, OSError, EOFError, lzma.LZMAError, tarfile.TarError, ValueError, TypeError, KeyError) as error:
|
||||
|
||||
116
scripts/tests/test_bundle_metadata_binding.py
Normal file
116
scripts/tests/test_bundle_metadata_binding.py
Normal file
@@ -0,0 +1,116 @@
|
||||
# #region Test.Tooling.DockerBundle.Metadata [C:3] [TYPE Module] [SEMANTICS bundle,metadata,release]
|
||||
# @TEST_INVARIANT The actual producer binds both generated deployment files before publishing manifestv2; any mismatch refuses before Docker.
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from test_import_docker_bundle import archive_bytes, importer
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
# #region Test.Tooling.DockerBundle.Metadata.Generator [C:3] [TYPE Function]
|
||||
def generate(root, tag):
|
||||
archive_dir = root / "inert-exports"
|
||||
archive_dir.mkdir(exist_ok=True)
|
||||
target = root / "bundle"
|
||||
identity = None
|
||||
for role in importer.ROLES:
|
||||
archive, identity = archive_bytes(f"superset-tools-{role}:{tag}", modern=True)
|
||||
(archive_dir / f"superset-tools-{role}.{tag}.tar.xz").write_bytes(archive)
|
||||
text = (ROOT / "build.sh").read_text()
|
||||
actual = text[text.index("generate_deploy_compose() {"):text.index("bundle_light() {")]
|
||||
prefix = f"""
|
||||
set -euo pipefail
|
||||
umask 077
|
||||
DIST_ROOT={shlex.quote(str(target))}
|
||||
SCRIPT_DIR={shlex.quote(str(ROOT))}
|
||||
FIXTURE_EXPORTS={shlex.quote(str(archive_dir))}
|
||||
FIXTURE_ID={shlex.quote(identity)}
|
||||
build_backend(){{ return 0; }}
|
||||
build_frontend(){{ return 0; }}
|
||||
ensure_image_present(){{ return 0; }}
|
||||
run_bundle_smoke(){{ return 0; }}
|
||||
docker(){{ [ "$1" = tag ] || return 99; }}
|
||||
get_image_id(){{ printf '%s' "$FIXTURE_ID"; }}
|
||||
get_repo_digest(){{ printf 'unavailable'; }}
|
||||
export_image(){{ cp "$FIXTURE_EXPORTS/${{1%%:*}}.${{1#*:}}.tar.xz" "$2"; }}
|
||||
"""
|
||||
result = subprocess.run(["/usr/bin/bash", "-c", prefix + actual + "\nbundle_release " + shlex.quote(tag)],
|
||||
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)
|
||||
assert result.returncode == 0, result.stderr.decode()
|
||||
return target
|
||||
# #endregion Test.Tooling.DockerBundle.Metadata.Generator
|
||||
|
||||
# #region Test.Tooling.DockerBundle.Metadata.BeforeDocker [C:3] [TYPE Function]
|
||||
def without_docker(directory, name, tag):
|
||||
actual = subprocess.Popen
|
||||
def boundary(argv, **kwargs):
|
||||
assert argv[0] != "docker", "metadata refusal must precede even read-only Docker inspection"
|
||||
return actual(argv, **kwargs)
|
||||
with patch.object(importer.subprocess, "Popen", side_effect=boundary):
|
||||
return importer.execute(directory, name, tag, verify_only=False)
|
||||
# #endregion Test.Tooling.DockerBundle.Metadata.BeforeDocker
|
||||
|
||||
# #region Test.Tooling.DockerBundle.Metadata.Cases [C:3] [TYPE Function]
|
||||
def test_actual_bundle_generator_publishes_v2_after_both_metadata_files(tmp_path):
|
||||
root = generate(tmp_path, "fresh")
|
||||
value = json.loads((root / "manifest.fresh.json").read_text())
|
||||
assert value["manifest_version"] == 2
|
||||
assert value["compose_sha256"] == hashlib.sha256((root / value["compose_file"]).read_bytes()).hexdigest()
|
||||
assert value["env_template_sha256"] == hashlib.sha256((root / value["env_template"]).read_bytes()).hexdigest()
|
||||
rc, result = importer.execute(root, "manifest.fresh.json", "fresh", True)
|
||||
assert rc == 0, result
|
||||
assert result["status"] == "verified"
|
||||
|
||||
|
||||
def test_second_release_shared_compose_refuses_old_manifest_before_docker(tmp_path):
|
||||
root = generate(tmp_path, "old")
|
||||
before = (root / "manifest.old.json").read_bytes()
|
||||
generate(tmp_path, "new")
|
||||
assert (root / "manifest.old.json").read_bytes() == before
|
||||
assert "superset-tools-backend:new" in (root / "docker-compose.enterprise-clean.yml").read_text()
|
||||
rc, result = without_docker(root, "manifest.old.json", "old")
|
||||
assert rc == 2
|
||||
assert result["code"] == "deployment_metadata_checksum_conflict"
|
||||
assert result["attempted_roles"] == result["loaded_roles"] == []
|
||||
|
||||
|
||||
def test_env_template_tamper_refuses_before_docker(tmp_path):
|
||||
root = generate(tmp_path, "fresh")
|
||||
with (root / ".env.example").open("a") as output:
|
||||
output.write("\n# Modified template\n")
|
||||
rc, result = without_docker(root, "manifest.fresh.json", "fresh")
|
||||
assert rc == 2
|
||||
assert result["code"] == "deployment_metadata_checksum_conflict"
|
||||
assert not result["partial_load_possible"]
|
||||
|
||||
|
||||
def test_unbound_v1_has_no_fallback(tmp_path):
|
||||
root = generate(tmp_path, "fresh")
|
||||
path = root / "manifest.fresh.json"
|
||||
value = json.loads(path.read_text())
|
||||
value["manifest_version"] = 1
|
||||
value.pop("compose_sha256", None)
|
||||
value.pop("env_template_sha256", None)
|
||||
path.write_text(json.dumps(value))
|
||||
rc, result = without_docker(root, path.name, "fresh")
|
||||
assert rc == 2
|
||||
assert result["code"] == "unsupported_bundle_manifest"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["docker-compose.enterprise-clean.yml", ".env.example"])
|
||||
def test_metadata_symlink_refuses_before_docker(tmp_path, name):
|
||||
root = generate(tmp_path, "fresh")
|
||||
original = root / (name + ".original")
|
||||
(root / name).rename(original)
|
||||
(root / name).symlink_to(original)
|
||||
rc, result = without_docker(root, "manifest.fresh.json", "fresh")
|
||||
assert rc == 2
|
||||
assert result["loaded_roles"] == []
|
||||
# #endregion Test.Tooling.DockerBundle.Metadata.Cases
|
||||
# #endregion Test.Tooling.DockerBundle.Metadata
|
||||
@@ -62,9 +62,13 @@ def bundle(root, modern=False):
|
||||
name = f"superset-tools-{role}.{TAG}.tar.xz"
|
||||
(root / name).write_bytes(archive)
|
||||
images.append({"role": role, "image": image, "image_id": identity, "repo_digest": "unavailable", "archive": name})
|
||||
value = {"manifest_version": 1, "bundle_kind": "enterprise", "release_tag": TAG,
|
||||
compose, env = b"services: {}\n", b"# Synthetic public env template\n"
|
||||
(root / "docker-compose.enterprise-clean.yml").write_bytes(compose)
|
||||
(root / ".env.example").write_bytes(env)
|
||||
value = {"manifest_version": 2, "bundle_kind": "enterprise", "release_tag": TAG,
|
||||
"generated_at_utc": "2026-10-23T00:00:00Z", "images": images,
|
||||
"compose_file": "docker-compose.enterprise-clean.yml", "env_template": ".env.example",
|
||||
"compose_sha256": hashlib.sha256(compose).hexdigest(), "env_template_sha256": hashlib.sha256(env).hexdigest(),
|
||||
"env_bootstrap_fields": importer.ENV_FIELDS, "checksums_file": f"sha256sums.{TAG}.txt"}
|
||||
write_manifest(root, value)
|
||||
return value
|
||||
|
||||
Reference in New Issue
Block a user