From 7c9c8ca2c3e730abac65a2ffa4ae902add3a9259 Mon Sep 17 00:00:00 2001 From: dryrun Date: Sun, 25 Oct 2026 20:50:37 +0000 Subject: [PATCH 1/2] ci: wire the named-keys flag as a docker.yml workflow input named_keys_v1 (default false) is sealed into the Phala env as NAMED_KEYS_V1 beside DSTACK_AUTHORIZED_KEYS, so the flip is a normal dispatch and never a hand `phala deploy -e`. The dstack-postgres compose maps it to TINYCLOUD_ENCRYPTION_NETWORK__NAMED_KEYS_V1, sets the deletion ledger path and mounts a deletion-ledger volume; the N0 template hash is updated to match. Details: TC-1777 --- .github/workflows/docker.yml | 20 +++++++++++++++++-- docker-compose.dstack-postgres.yaml | 8 ++++++++ docs/releasing.md | 8 ++++---- scripts/ci/fixtures/tc959-n0-compose.yaml | 8 ++++++++ scripts/ci/test-deploy-recovery.py | 24 +++++++++++++++++++++++ 5 files changed, 62 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index b21a7287..f9feb58b 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -69,6 +69,11 @@ on: required: true type: boolean default: true + named_keys_v1: + description: 'Turn on named keys (TC-1777): sets NAMED_KEYS_V1 in the sealed Phala env. false keeps the routes off.' + required: false + type: boolean + default: false image_version: description: 'Image version without leading v. Builds must match the selected crate; digest promotions must match the recorded image version (rollback_image_version when supplied).' required: false @@ -686,9 +691,10 @@ jobs: reviewed = native + '@sha256:4bdae4fc232d886bb56bfbce4ec62ed4b8fb4e1369a01f6afb73d06dd68c188f' # TC-959: main's N0 compose (#349 c33c2302…) with the 95m ingress body # cap (raw d95bfb70…), the TC-1274 + TC-1317 + TC-1482 + c_index + TC-1840 + TC-1843 Vector config and the - # optional TC-1482 NODE_STATS_READ_TOKEN env line and the TC-950 pool of 10 (raw 192dfb7e…), + # optional TC-1482 NODE_STATS_READ_TOKEN env line, the TC-950 pool of 10 and the TC-1777 named-keys gate + # env lines + deletion-ledger volume (raw 298ea356…; 192dfb7e… before TC-1777), # its single node digest normalized to 64 zeros. - n0_template_hash = '8df7e0f258f6e343d7683b1bd538291a64f6365d8174e7bfe2bf25dfa68160b3' + n0_template_hash = 'c9bcbea9dac90fc2b74d8439146596ba802796a211d1b6d65aeda2286d7ed028' node_pin = re.compile(rb'(?m)^ image: ghcr\.io/tinycloudlabs/tinycloud-node@sha256:([0-9a-f]{64})$') def n0_template(raw): pins = list(node_pin.finditer(raw)) @@ -953,6 +959,7 @@ jobs: PHALA_ENV_CERTBOT_EMAIL: ${{ secrets.PROD_TINYCLOUD_CERTBOT_EMAIL }} PHALA_ENV_BETTERSTACK_SOURCE_TOKEN: ${{ secrets.PROD_TINYCLOUD_BETTERSTACK_SOURCE_TOKEN }} PHALA_ENV_SQL_IDENTITY_FENCE: ${{ inputs.sql_identity_fence }} + PHALA_ENV_NAMED_KEYS_V1: ${{ inputs.named_keys_v1 }} # Update the existing prod CVM (name: tinycloud-node) in the # Tiny Cloud workspace. The PHALA_CLOUD_API_KEY secret is # workspace-scoped and must be issued from that workspace. @@ -978,6 +985,7 @@ jobs: PHALA_ENV_CERTBOT_EMAIL PHALA_ENV_BETTERSTACK_SOURCE_TOKEN PHALA_ENV_SQL_IDENTITY_FENCE + PHALA_ENV_NAMED_KEYS_V1 ) for var in "${required_vars[@]}"; do @@ -991,6 +999,13 @@ jobs: fi done + # TC-1777: the named-keys gate is written into the sealed env as-is, + # so anything but a literal true/false is refused before it is written. + if [ "${PHALA_ENV_NAMED_KEYS_V1}" != "true" ] && [ "${PHALA_ENV_NAMED_KEYS_V1}" != "false" ]; then + echo "::error::named_keys_v1 must be exactly true or false" + exit 1 + fi + # Optional (TC-1482): the same single-line rule, but absence is allowed. if [[ "${PHALA_ENV_NODE_STATS_READ_TOKEN:-}" == *$'\n'* || "${PHALA_ENV_NODE_STATS_READ_TOKEN:-}" == *$'\r'* ]]; then echo "::error::Phala deploy inputs must be single-line values" @@ -1035,6 +1050,7 @@ jobs: printf 'CERTBOT_EMAIL=%s\n' "$PHALA_ENV_CERTBOT_EMAIL" printf 'BETTERSTACK_SOURCE_TOKEN=%s\n' "$PHALA_ENV_BETTERSTACK_SOURCE_TOKEN" printf 'SQL_IDENTITY_FENCE=%s\n' "$PHALA_ENV_SQL_IDENTITY_FENCE" + printf 'NAMED_KEYS_V1=%s\n' "$PHALA_ENV_NAMED_KEYS_V1" # TC-1482: optional. Unset keeps the node starting with # GET /admin/stats disabled (404); never written empty. if [ -n "${PHALA_ENV_NODE_STATS_READ_TOKEN:-}" ]; then diff --git a/docker-compose.dstack-postgres.yaml b/docker-compose.dstack-postgres.yaml index 4cb2232e..1b35326a 100644 --- a/docker-compose.dstack-postgres.yaml +++ b/docker-compose.dstack-postgres.yaml @@ -28,6 +28,7 @@ services: - "8000:8000" volumes: - /var/run/dstack.sock:/var/run/dstack.sock + - deletion-ledger:/var/lib/tinycloud/ledger environment: TINYCLOUD_KEYS_TYPE: Dstack TINYCLOUD_STORAGE_DATABASE: "${DATABASE_URL}" @@ -91,6 +92,12 @@ services: TINYCLOUD_QUOTA_URL: "${TINYCLOUD_QUOTA_URL}" TINYCLOUD_ADMIN_SECRET: "${TINYCLOUD_ADMIN_SECRET}" NODE_STATS_READ_TOKEN: "${NODE_STATS_READ_TOKEN:-}" + # TC-1777: named-keys release gate. Set by the docker.yml named_keys_v1 + # workflow input (default false); never hand-edit the CVM env. + TINYCLOUD_ENCRYPTION_NETWORK__NAMED_KEYS_V1: "${NAMED_KEYS_V1:-false}" + # Append-only record of deleted keys, on the deletion-ledger volume so it + # survives container replacement. Required by the node while the gate is on. + TINYCLOUD_ENCRYPTION_NETWORK__DELETION_LEDGER_PATH: /var/lib/tinycloud/ledger/deletions.jsonl ROCKET_ADDRESS: "0.0.0.0" ROCKET_MAX_BLOCKING: "1024" @@ -180,6 +187,7 @@ services: volumes: cert-data: + deletion-ledger: networks: logship: diff --git a/docs/releasing.md b/docs/releasing.md index 811a3d76..9a88c576 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -499,8 +499,8 @@ Before any CVM mutation the workflow requires: - the main compose, with its one node digest replaced by 64 zeros, to hash to the reviewed N0 template - **`8df7e0f258f6e343d7683b1bd538291a64f6365d8174e7bfe2bf25dfa68160b3`** - (raw `192dfb7e407fa591d4f0232b95e43a41a85dd18add1d4976e70ebdbff5ddebca` normalized; before the TC-1843 per-minute caps on 4xx and 5xx request events and on the 5xx diagnostic, and the `errors_dropped` event (the dropped count), in the Vector config the template was `7c3456dc…`, raw `d0e72bde…`; before the TC-1840 `db_failure_5xx` event (the node's 5xx diagnostic line shipped as an individual event) in the Vector config the template was `c91e5b7a…`, raw `99013c40…`; before the TC-950 pool of 10 (`TINYCLOUD_DATABASE__MAX_CONNECTIONS` 15 to 10) the template was `79469f97…`, raw `32a002ef…`; before the three `current_kv_c_index_*` warning codes in the Vector config (TC-1274 `current_kv` C-index migration) the template was `87b5d12a…`, raw `d1fcd665…`; before the optional TC-1482 `NODE_STATS_READ_TOKEN` env line and the TC-1482 route in the Vector config the template was `7fabba70…`, raw `1228d81927fc0a3fbba85745103e77b3112606f5d314d15974d400b222409350`: the 1.21.0 compose + **`c9bcbea9dac90fc2b74d8439146596ba802796a211d1b6d65aeda2286d7ed028`** + (raw `298ea356adf8d5e562557ab18bd3f5d8cdb816de82b1eaf6e6e50bf888e22765` normalized; before the TC-1777 named-keys gate env lines (`TINYCLOUD_ENCRYPTION_NETWORK__NAMED_KEYS_V1`, `…__DELETION_LEDGER_PATH`) and the `deletion-ledger` volume the template was `8df7e0f2…`, raw `192dfb7e407fa591d4f0232b95e43a41a85dd18add1d4976e70ebdbff5ddebca`; before the TC-1843 per-minute caps on 4xx and 5xx request events and on the 5xx diagnostic, and the `errors_dropped` event (the dropped count), in the Vector config the template was `7c3456dc…`, raw `d0e72bde…`; before the TC-1840 `db_failure_5xx` event (the node's 5xx diagnostic line shipped as an individual event) in the Vector config the template was `c91e5b7a…`, raw `99013c40…`; before the TC-950 pool of 10 (`TINYCLOUD_DATABASE__MAX_CONNECTIONS` 15 to 10) the template was `79469f97…`, raw `32a002ef…`; before the three `current_kv_c_index_*` warning codes in the Vector config (TC-1274 `current_kv` C-index migration) the template was `87b5d12a…`, raw `d1fcd665…`; before the optional TC-1482 `NODE_STATS_READ_TOKEN` env line and the TC-1482 route in the Vector config the template was `7fabba70…`, raw `1228d81927fc0a3fbba85745103e77b3112606f5d314d15974d400b222409350`: the 1.21.0 compose `d95bfb707871c1d626c4a8f2b8d32d354103edbb5077847c6a21fe5f7f6cfa50`, template `51f8e9e9…`, with only the embedded Vector config regenerated for TC-1274's, TC-1317's and TC-1482's route and warning codes (TC-1274 and TC-1317 were template `7fabba70…`, raw `1228d819…`); TC-1274 alone was template `00938226…`, raw @@ -594,8 +594,8 @@ gh workflow run docker.yml -R TinyCloudLabs/tinycloud-node --ref main \ The served/OCI identity remains **1.20.6-dstack / e18578ff9162986ddd813259c60f73866e3e60d1**. With the template lock on main this redeploys exactly main's raw compose -`192dfb7e407fa591d4f0232b95e43a41a85dd18add1d4976e70ebdbff5ddebca` -(the 1.21.0 compose `d95bfb70…` with the TC-1274, TC-1317, TC-1482, `current_kv_c_index_*`, `db_failure_5xx`, the 4xx and 5xx caps and `errors_dropped` Vector config, the optional TC-1482 `NODE_STATS_READ_TOKEN` env line and the TC-950 pool of 10), whatever digest +`298ea356adf8d5e562557ab18bd3f5d8cdb816de82b1eaf6e6e50bf888e22765` +(the 1.21.0 compose `d95bfb70…` with the TC-1274, TC-1317, TC-1482, `current_kv_c_index_*`, `db_failure_5xx`, the 4xx and 5xx caps and `errors_dropped` Vector config, the optional TC-1482 `NODE_STATS_READ_TOKEN` env line, the TC-950 pool of 10, and the TC-1777 `NAMED_KEYS_V1` gate env lines with the `deletion-ledger` volume), whatever digest main's template currently pins. It is not a rollback of a later release across a schema migration. Current pre-N0 tag/pinned equivalents are separately hash-checked; arbitrary same-image config diff --git a/scripts/ci/fixtures/tc959-n0-compose.yaml b/scripts/ci/fixtures/tc959-n0-compose.yaml index 4cb2232e..1b35326a 100644 --- a/scripts/ci/fixtures/tc959-n0-compose.yaml +++ b/scripts/ci/fixtures/tc959-n0-compose.yaml @@ -28,6 +28,7 @@ services: - "8000:8000" volumes: - /var/run/dstack.sock:/var/run/dstack.sock + - deletion-ledger:/var/lib/tinycloud/ledger environment: TINYCLOUD_KEYS_TYPE: Dstack TINYCLOUD_STORAGE_DATABASE: "${DATABASE_URL}" @@ -91,6 +92,12 @@ services: TINYCLOUD_QUOTA_URL: "${TINYCLOUD_QUOTA_URL}" TINYCLOUD_ADMIN_SECRET: "${TINYCLOUD_ADMIN_SECRET}" NODE_STATS_READ_TOKEN: "${NODE_STATS_READ_TOKEN:-}" + # TC-1777: named-keys release gate. Set by the docker.yml named_keys_v1 + # workflow input (default false); never hand-edit the CVM env. + TINYCLOUD_ENCRYPTION_NETWORK__NAMED_KEYS_V1: "${NAMED_KEYS_V1:-false}" + # Append-only record of deleted keys, on the deletion-ledger volume so it + # survives container replacement. Required by the node while the gate is on. + TINYCLOUD_ENCRYPTION_NETWORK__DELETION_LEDGER_PATH: /var/lib/tinycloud/ledger/deletions.jsonl ROCKET_ADDRESS: "0.0.0.0" ROCKET_MAX_BLOCKING: "1024" @@ -180,6 +187,7 @@ services: volumes: cert-data: + deletion-ledger: networks: logship: diff --git a/scripts/ci/test-deploy-recovery.py b/scripts/ci/test-deploy-recovery.py index 0223dd0f..be218eaa 100755 --- a/scripts/ci/test-deploy-recovery.py +++ b/scripts/ci/test-deploy-recovery.py @@ -125,6 +125,7 @@ def event(value): names=[line.split('=',1)[0] for line in Path('.phala-prod.env').read_text().splitlines()] assert 'BETTERSTACK_SOURCE_TOKEN' in names and 'QUOTA_BILLING_ADMIN_SECRET' not in names data=json.loads(state.read_text()); data['values']['env_names']=names; state.write_text(json.dumps(data)) + sealed=dict(row.split('=',1) for row in Path('.phala-prod.env').read_text().splitlines()); event('sealed-named-keys-v1='+sealed.get('NAMED_KEYS_V1','')) if args[-1]=='--check': event('whitelist-check') # What sync.mjs does when the CVM's allowed_envs names a variable the sealed file lacks. @@ -279,6 +280,7 @@ def run_deploy(self, compose=CURRENT, changes=None, *, fail_at="", artifact_erro env["PHALA_ENV_NODE_STATS_READ_TOKEN"] = stats_token if stats_token and stats_token != canary: CANARIES.append(stats_token) if missing_billing: env.pop("QUOTA_BILLING_ADMIN_SECRET") + env["PHALA_ENV_NAMED_KEYS_V1"] = inputs.get("named_keys_v1", "false") context = {"github.event_name": event, "github.ref": ref, "job.success": True, **{"inputs." + key: (value == "true" if value in ("true", "false") else value) for key, value in inputs.items()}} logs = "" @@ -559,6 +561,28 @@ def test_unreleased_digest_and_captured_rollback_are_not_substitutable(self): self.assertNotIn("cvm-deploy", result["events"]) self.assertEqual(result["compose"], N0) + def test_named_keys_gate_is_sealed_as_a_literal_flag_beside_the_ssh_keys(self): + # TC-1777: the flip is a workflow input, so the sealed file (the only CVM env source) + # carries the flag and keeps DSTACK_AUTHORIZED_KEYS on every dispatch. + for value in ("false", "true"): + with self.subTest(value=value): + result = self.run_deploy(changes={"named_keys_v1": value}) + self.assertTrue(result["success"]) + self.assertEqual(result["env_names"].count("NAMED_KEYS_V1"), 1) + self.assertEqual(result["env_names"].count("DSTACK_AUTHORIZED_KEYS"), 1) + sealed = {event for event in result["events"] if event.startswith("sealed-named-keys-v1=")} + self.assertEqual(sealed, {"sealed-named-keys-v1=" + value}) + + def test_named_keys_gate_other_than_true_or_false_is_refused_before_the_file(self): + for value in ("", "TRUE", "1", "yes", "maybe", "true "): + with self.subTest(value=value): + refused = self.run_deploy(changes={"named_keys_v1": value}) + self.assertFalse(refused["success"]) + self.assertEqual(refused["events"].count("cvm-deploy"), 0, "A bad flag must fail before the CVM mutation") + self.assertNotIn("whitelist-check", refused["events"], "The sealed file must not be written first") + if value: + self.assertIn("::error::named_keys_v1 must be exactly true or false", refused["logs"]) + def test_last_restart_precedes_health_inventory_and_quota(self): result = self.run_deploy(N0, {"rollback_pre_n0": "true", "rollback_privacy_mode": "keep-json-file"}, approved_privacy="keep-json-file") self.assertTrue(result["success"]) From 81bfe46b62eb57dff57617c3ee6b407a2e5532fa Mon Sep 17 00:00:00 2001 From: dryrun Date: Sun, 25 Oct 2026 21:19:40 +0000 Subject: [PATCH 2/2] ci: preflight the named-keys gate and ledger path; pin the deletion-ledger mount in the compose proof The sealed-config preflight now passes NAMED_KEYS_V1 and the compose's deletion ledger path to --validate-config. The hosted compose proof expected the tinycloud service to mount only the dstack socket; it now also expects the deletion-ledger volume. Details: TC-1777 --- .github/workflows/docker.yml | 11 ++++++++ scripts/ci/test-deploy-recovery.py | 40 ++++++++++++++++++++++++++++++ scripts/tc959-docker.mjs | 2 +- 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index f9feb58b..39d6d359 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -808,6 +808,7 @@ jobs: TINYCLOUD_SHARE_EMAIL__TRUST_BUNDLE_BASE64: ${{ secrets.PROD_TINYCLOUD_SHARE_TRUST_BUNDLE_BASE64 }} TINYCLOUD_SHARE_EMAIL__POSTGRES_TLS__SSLMODE: verify-full TINYCLOUD_KEYS__TYPE: Dstack + TINYCLOUD_ENCRYPTION_NETWORK__NAMED_KEYS_V1: ${{ inputs.named_keys_v1 }} run: | set -euo pipefail IMAGE="$(sed -n -E 's/^ image: (ghcr.io\/tinycloudlabs\/tinycloud-node(:[^[:space:]]+|@sha256:[0-9a-f]{64}))$/\1/p' docker-compose.dstack-postgres.yaml)" @@ -815,6 +816,14 @@ jobs: echo "::error::Expected exactly one resolved TinyCloud Node image for runtime preflight" exit 1 fi + # TC-1777: validate the same named-keys gate and ledger path the sealed + # deployment will carry. The path is read from the compose being deployed + # (absent from the pre-N0 rollback compose, where it is not passed). + LEDGER_PATH="$(sed -n -E 's/^ TINYCLOUD_ENCRYPTION_NETWORK__DELETION_LEDGER_PATH: ([^[:space:]]+)$/\1/p' docker-compose.dstack-postgres.yaml)" + ledger_args=() + if [ -n "${LEDGER_PATH}" ]; then + ledger_args=(-e "TINYCLOUD_ENCRYPTION_NETWORK__DELETION_LEDGER_PATH=${LEDGER_PATH}") + fi docker run --rm --network none --read-only --cap-drop ALL \ --security-opt no-new-privileges \ -e TINYCLOUD_STORAGE__DATABASE \ @@ -823,6 +832,8 @@ jobs: -e TINYCLOUD_SHARE_EMAIL__TRUST_BUNDLE_BASE64 \ -e TINYCLOUD_SHARE_EMAIL__POSTGRES_TLS__SSLMODE \ -e TINYCLOUD_KEYS__TYPE \ + -e TINYCLOUD_ENCRYPTION_NETWORK__NAMED_KEYS_V1 \ + ${ledger_args[@]+"${ledger_args[@]}"} \ "${IMAGE}" --validate-config # TC-959: only env_keys/encrypted_env may change here. Never copy the diff --git a/scripts/ci/test-deploy-recovery.py b/scripts/ci/test-deploy-recovery.py index be218eaa..febb33c6 100755 --- a/scripts/ci/test-deploy-recovery.py +++ b/scripts/ci/test-deploy-recovery.py @@ -100,6 +100,11 @@ def event(value): print(os.environ['OCI_REVISION'] if 'revision' in args[-1] else os.environ['OCI_LABEL']) elif args[0]=='run': assert args[-1]=='--validate-config' and 'none' in args and '--read-only' in args; event('sealed-preflight') + passed = dict(a.split('=', 1) if '=' in a else (a, os.environ.get(a, '')) for a in (args[i + 1] for i, x in enumerate(args) if x == '-e')) + event('preflight-named-keys=' + passed.get('TINYCLOUD_ENCRYPTION_NETWORK__NAMED_KEYS_V1', '') + ' ledger=' + passed.get('TINYCLOUD_ENCRYPTION_NETWORK__DELETION_LEDGER_PATH', '')) + # Stands for the node's startup rule: named keys on needs an absolute deletion ledger path. + if passed.get('TINYCLOUD_ENCRYPTION_NETWORK__NAMED_KEYS_V1') == 'true' and not passed.get('TINYCLOUD_ENCRYPTION_NETWORK__DELETION_LEDGER_PATH', '').startswith('/'): + print('encryption_network.deletion_ledger_path is required when named_keys_v1 is on', file=sys.stderr); sys.exit(1) elif args[0]=='compose': assert args[-2:]==['config','--quiet']; assert Path('.phala-prod.env').stat().st_mode & 0o777==0o600 else: raise SystemExit('Unexpected Docker boundary') @@ -281,6 +286,7 @@ def run_deploy(self, compose=CURRENT, changes=None, *, fail_at="", artifact_erro if stats_token and stats_token != canary: CANARIES.append(stats_token) if missing_billing: env.pop("QUOTA_BILLING_ADMIN_SECRET") env["PHALA_ENV_NAMED_KEYS_V1"] = inputs.get("named_keys_v1", "false") + env["TINYCLOUD_ENCRYPTION_NETWORK__NAMED_KEYS_V1"] = inputs.get("named_keys_v1", "false") context = {"github.event_name": event, "github.ref": ref, "job.success": True, **{"inputs." + key: (value == "true" if value in ("true", "false") else value) for key, value in inputs.items()}} logs = "" @@ -583,6 +589,40 @@ def test_named_keys_gate_other_than_true_or_false_is_refused_before_the_file(sel if value: self.assertIn("::error::named_keys_v1 must be exactly true or false", refused["logs"]) + def run_preflight(self, compose, named_keys): + # Only the preflight step, against the fake docker: the compose it reads is the test input. + with tempfile.TemporaryDirectory() as directory: + root = Path(directory); binaries = root / "bin"; binaries.mkdir() + binary = binaries / "docker"; binary.write_text(CLI); binary.chmod(0o700) + (root / "docker-compose.dstack-postgres.yaml").write_bytes(compose) + state = root / "state"; state.write_text(json.dumps({"events": [], "values": {}})) + env = {"PATH": str(binaries) + ":" + str(Path(sys.executable).parent) + ":/usr/bin:/bin", "TEST_STATE": str(state), + "TINYCLOUD_ENCRYPTION_NETWORK__NAMED_KEYS_V1": named_keys} + result = subprocess.run(["bash", "--noprofile", "--norc", "-e", "-c", shell_for_step("Preflight sealed runtime configuration")], + cwd=root, env=env, capture_output=True, text=True) + return result.returncode, json.loads(state.read_text())["events"], result.stdout + result.stderr + + def test_preflight_validates_the_named_keys_gate_with_the_composed_ledger_path(self): + ledger = "/var/lib/tinycloud/ledger/deletions.jsonl" + for value in ("false", "true"): + with self.subTest(value=value): + result = self.run_deploy(changes={"named_keys_v1": value}) + self.assertTrue(result["success"]) + self.assertIn(f"preflight-named-keys={value} ledger={ledger}", result["events"]) + + def test_preflight_refuses_the_gate_without_a_ledger_path_only_when_it_is_on(self): + no_path = CURRENT.replace(b" TINYCLOUD_ENCRYPTION_NETWORK__DELETION_LEDGER_PATH: /var/lib/tinycloud/ledger/deletions.jsonl\n", b"") + self.assertNotEqual(no_path, CURRENT) + code, events, logs = self.run_preflight(no_path, "false") + self.assertEqual(code, 0, "Gate off without a path must keep passing") + self.assertIn("preflight-named-keys=false ledger=", events) + code, events, logs = self.run_preflight(no_path, "true") + self.assertNotEqual(code, 0, "Gate on without a ledger path must fail the preflight") + self.assertIn("preflight-named-keys=true ledger=", events) + self.assertIn("deletion_ledger_path is required", logs) + code, events, logs = self.run_preflight(CURRENT, "true") + self.assertEqual(code, 0, "Gate on with the composed ledger path must pass") + def test_last_restart_precedes_health_inventory_and_quota(self): result = self.run_deploy(N0, {"rollback_pre_n0": "true", "rollback_privacy_mode": "keep-json-file"}, approved_privacy="keep-json-file") self.assertTrue(result["success"]) diff --git a/scripts/tc959-docker.mjs b/scripts/tc959-docker.mjs index 1b78b064..f42257cd 100755 --- a/scripts/tc959-docker.mjs +++ b/scripts/tc959-docker.mjs @@ -393,7 +393,7 @@ export async function main(args = process.argv.slice(2)) { assert.equal(service.ports[0].host_ip ?? '0.0.0.0', '0.0.0.0'); } phase = 'compose_mounts'; - assert.deepEqual(production.tinycloud.volumes.map(value => [value.type, value.source, value.target]), [['bind', '/var/run/dstack.sock', '/var/run/dstack.sock']]); + assert.deepEqual(production.tinycloud.volumes.map(value => [value.type, value.source, value.target]), [['bind', '/var/run/dstack.sock', '/var/run/dstack.sock'], ['volume', 'deletion-ledger', '/var/lib/tinycloud/ledger']]); assert.deepEqual(production['dstack-ingress'].volumes.map(value => [value.type, value.source, value.target]).sort((a, b) => a[2].localeCompare(b[2])), [['volume', 'cert-data', '/etc/letsencrypt'], ['bind', '/var/run/dstack.sock', '/var/run/dstack.sock']]); phase = 'compose_networks'; assert.deepEqual(Object.keys(production.tinycloud.networks), ['default']);