Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/validate-configurator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ jobs:
tofu_version: '1.12.6'
tofu_wrapper: false
- name: Check operator plan guard
run: node --test infra/prepare-local.test.mjs infra/plan-guard.test.mjs infra/state-key.test.mjs infra/ci/context.test.mjs infra/ci/artifact.test.mjs infra/ci/prepare.test.mjs infra/ci/review.test.mjs infra/ci/timeout.test.mjs infra/ci/buildpack-source.test.mjs
run: node --test infra/prepare-local.test.mjs infra/plan-guard.test.mjs infra/state-key.test.mjs infra/ci/context.test.mjs infra/ci/artifact.test.mjs infra/ci/prepare.test.mjs infra/ci/review.test.mjs infra/ci/timeout.test.mjs infra/ci/buildpack-source.test.mjs infra/ci/image-staging.test.mjs
- name: Check formatting
run: tofu -chdir=infra fmt -check -recursive
- name: Prepare disposable validation encryption
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { constants } from "node:fs";
import { copyFile, lstat, mkdir, mkdtemp, rm } from "node:fs/promises";
import { copyFile, lstat, mkdir, mkdtemp } from "node:fs/promises";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { LocalPlanRunner } from "../apps/api/dist/plans/local.js";
import { platformFirewallImage } from "../packages/contracts/dist/index.js";
import { removeStagingWork } from "./remove-staging-work.mjs";

const directory = resolve(process.argv[2]);
const repository = fileURLToPath(new URL("../../../", import.meta.url));
Expand Down Expand Up @@ -90,5 +91,5 @@ try {
"PASS: both thin CF archives below 384 MiB restore the exact image-qualified Runner identity during staging; runtime package/disk footprint unchanged.",
);
} finally {
await rm(work, { recursive: true, force: true });
await removeStagingWork(work);
}
16 changes: 16 additions & 0 deletions landing-zone-configurator/app/scripts/remove-staging-work.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { chmod, lstat, readdir, rm } from "node:fs/promises";
import { join } from "node:path";

export async function removeStagingWork(directory) {
async function writableDirectories(path) {
const info = await lstat(path);
if (!info.isDirectory()) return;
await chmod(path, info.mode | 0o700);
for (const name of await readdir(path))
await writableDirectories(join(path, name));
}
// Provider mirrors stay immutable through qualification. Only the temporary
// staging tree needs writable directories for removal; never follow links.
await writableDirectories(directory);
await rm(directory, { recursive: true, force: true });
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ Hosted-Archive lassen ausschließlich das gepinnte `firewall-image.qcow2` aus; l
Archive bleiben eigenständig vollständig. CI qualifiziert zunächst das vollständige imagegestützte Paket,
extrahiert dann beide schlanken Archive, stellt das Image über den tatsächlichen Buildpack-
Einstiegspunkt wieder her und prüft bytegenaue Runner-Gleichheit sowie dieselbe Paketidentität.
Jedes komprimierte CF-Archiv muss kleiner als 384 MiB sein.
Jedes komprimierte CF-Archiv muss kleiner als 384 MiB sein. Nach der Prüfung
macht die Bereinigung nur Verzeichnisse im temporären Staging-Baum beschreibbar
und folgt keinen Symlinks. Das ursprüngliche Runner-/Provider-Verzeichnis bleibt
unverändert.

`prepare-release.mjs` bindet `LZC_IMAGE_BUILDPACK` an
`lzc-image-buildpack-<approved-source-sha>`, auch bei Promotion.
Expand Down
4 changes: 3 additions & 1 deletion landing-zone-configurator/deploy/cloud-foundry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ Hosted archives omit only the pinned `firewall-image.qcow2`; local Runner
archives remain self-contained. CI first qualifies the full image-backed package,
then extracts both thin archives, restores the image through the actual buildpack
entrypoint and verifies byte-for-byte Runner equality and the same package identity.
Each compressed CF archive must be smaller than 384 MiB.
Each compressed CF archive must be smaller than 384 MiB. After qualification,
cleanup makes only directories inside the temporary staging tree writable,
without following symlinks. The original Runner/provider mirror stays unchanged.

`prepare-release.mjs` binds `LZC_IMAGE_BUILDPACK` to
`lzc-image-buildpack-<approved-source-sha>`, including during promotion.
Expand Down
43 changes: 43 additions & 0 deletions landing-zone-configurator/infra/ci/image-staging.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { resolve } from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { prepareFirewallImage } from "../../deploy/cloud-foundry/firewall-image.mjs";
import { removeStagingWork } from "../../app/scripts/remove-staging-work.mjs";
import { buildpackTag, verifyBuildpackSource } from "./buildpack-source.mjs";

const repository = fileURLToPath(new URL("../../../", import.meta.url));
Expand Down Expand Up @@ -385,3 +386,45 @@ esac
await rm(work, { recursive: true, force: true });
}
});

test("staging cleanup removes immutable provider directories without following links into the original package", async () => {
const work = await mkdtemp(resolve(tmpdir(), "lzc-staging-cleanup-"));
const staged = resolve(work, "staging-check");
const original = resolve(work, "original-providers");
const mirror = resolve(staged, "runner/providers/registry.opentofu.org");
try {
await mkdir(mirror, { recursive: true });
await mkdir(original);
const provider = resolve(original, "verified-provider");
await writeFile(provider, "immutable provider");
await chmod(provider, 0o444);
await chmod(original, 0o555);
await writeFile(resolve(mirror, "staged-provider"), "immutable provider", {
mode: 0o444,
});
await symlink(original, resolve(staged, "original-mirror"));
await chmod(mirror, 0o555);
await chmod(resolve(mirror, ".."), 0o555);
const before = await lstat(provider);
if (process.getuid?.() !== 0)
await assert.rejects(rm(mirror, { recursive: true, force: true }), {
code: "EACCES",
});
await removeStagingWork(staged);
await assert.rejects(lstat(staged), { code: "ENOENT" });
assert.equal((await lstat(original)).mode & 0o777, 0o555);
assert.equal((await lstat(provider)).mode, before.mode);
assert.equal((await lstat(provider)).ino, before.ino);
assert.equal(await readFile(provider, "utf8"), "immutable provider");
const check = await readFile(
resolve(
repository,
"landing-zone-configurator/app/scripts/check-staged-runner.mjs",
),
"utf8",
);
assert.match(check, /finally \{\s*await removeStagingWork\(work\);\s*\}/);
} finally {
await removeStagingWork(work);
}
});
Loading