Files
agiledancers/generated/ci/ReleaseShellTests.java
Wanja Krah c6a495a93c
All checks were successful
Build, test and publish / verify (push) Successful in 8m54s
S19: consume versioned engineering resources and move landscape documentation
2026-11-08 15:23:10 +01:00

245 lines
12 KiB
Java

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermissions;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Runs the actual NAS script with disposable command adapters, no Docker daemon. */
class ReleaseShellTests {
static void run() throws Exception {
ReleaseTests.test("actual NAS script prepares digest images and checks the published endpoint", () -> {
try (var f = new Fixture()) {
f.prepare();
f.call("pending", "{}", "one").checked();
CiTests.equal("S6_HEALTHY", f.call("apply", null, "one", "one").checked());
String calls = Files.readString(f.directory.resolve("calls"));
CiTests.equal(true, calls.contains("http://backend:8080/q/health/ready"));
CiTests.equal(true, calls.contains("http://127.0.0.1:8088/api/items"));
CiTests.equal(false, calls.contains(" down"));
CiTests.equal(false, calls.contains("prune"));
}
});
ReleaseTests.test("actual NAS script rejects a missing database password before pulling images", () -> {
try (var f = new Fixture()) {
Path config = f.directory.resolve("production.env");
Files.writeString(
config, Files.readString(config).replace("DATABASE_PASSWORD=fixture-only-password\n", ""));
f.call("lock", null, "one").checked();
Commands.Result result = f.call("preflight", null, "one");
CiTests.equal(1, result.code());
CiTests.equal(true, result.output().contains("Missing DATABASE_PASSWORD"));
CiTests.equal(false, Files.exists(f.directory.resolve("live.env")));
}
});
ReleaseTests.test("actual NAS script passes database configuration through sudo", () -> {
try (var f = new Fixture()) {
f.executable("sudo", "#!/bin/sh\n[ \"$1\" != -n ] || shift\nunset DATABASE_PASSWORD\nexec \"$@\"\n");
Path config = f.directory.resolve("production.env");
Files.writeString(config, Files.readString(config).replace("DOCKER_SUDO=0", "DOCKER_SUDO=1"));
f.prepare();
CiTests.equal(
false,
Files.readString(f.directory.resolve("releases/one/release.env"))
.contains("PASSWORD"));
}
});
ReleaseTests.test("actual NAS script rejects wrong image revision before replacement", () -> {
try (var f = new Fixture()) {
f.env.put("WRONG_REVISION", "1");
CiTests.failure(f::prepare);
CiTests.equal(false, Files.exists(f.directory.resolve("live.env")));
}
});
ReleaseTests.test("actual NAS script rejects unavailable image before replacement", () -> {
try (var f = new Fixture()) {
f.env.put("FAIL_PULL", "1");
CiTests.failure(f::prepare);
CiTests.equal(false, Files.exists(f.directory.resolve("live.env")));
}
});
ReleaseTests.test("actual NAS script rejects a changed retained release record", () -> {
try (var f = new Fixture()) {
f.prepare();
CiTests.failure(() -> f.call(
"ensure", null, "one", "one", "v9.0.0", ReleaseTests.SHA, f.backend(), f.frontend())
.checked());
f.call("ensure", null, "one", "one", "v1.0.0", ReleaseTests.SHA, f.backend(), f.frontend())
.checked();
}
});
ReleaseTests.test("actual NAS replacement timeout is bounded and leaves recovery journal", () -> {
try (var f = new Fixture()) {
f.prepare();
f.call("pending", "{}", "one").checked();
f.env.put("FAIL_HEALTH", "1");
CiTests.equal(1, f.call("apply", null, "one", "one", "1").code());
CiTests.equal(true, Files.exists(f.directory.resolve("pending.json")));
// Termination releases only the operation fence, not the deployment lock.
CiTests.equal(false, Files.exists(f.directory.resolve("deploy.lock/operation")));
CiTests.equal(true, Files.exists(f.directory.resolve("deploy.lock/owner")));
f.env.remove("FAIL_HEALTH");
f.call("apply", null, "one", "one").checked();
}
});
ReleaseTests.test("actual NAS script fences overlapping remote replacements", () -> {
try (var f = new Fixture()) {
f.prepare();
Files.createDirectory(f.directory.resolve("deploy.lock/operation"));
CiTests.failure(() -> f.call("apply", null, "one", "one").checked());
CiTests.equal(false, Files.exists(f.directory.resolve("live.env")));
}
});
ReleaseTests.test("actual NAS script stores state atomically and archives resolved journal", () -> {
try (var f = new Fixture()) {
f.call("lock", null, "one").checked();
f.call("pending", "{\"attempt\":\"one\"}", "one").checked();
f.call("commit", "{\"current\":\"one\"}", "one").checked();
CiTests.equal(
"{\"current\":\"one\"}", f.call("state", null, "one").checked());
f.call("event", "{\"outcome\":\"success\"}", "one").checked();
f.call("finish", null, "one").checked();
f.call("unlock", null, "one").checked();
CiTests.equal(true, Files.exists(f.directory.resolve("history/one.json")));
CiTests.equal(false, Files.exists(f.directory.resolve("pending.json")));
}
});
}
static class Fixture implements AutoCloseable {
final Path directory = Files.createTempDirectory("nas-script-test-");
final Map<String, String> env = new HashMap<>(System.getenv());
final String health = "application_health() {\n"
+ "frontend=$(compose ps -q frontend)\n"
+ "docker_cmd exec \"$frontend\" wget -q -T 5 -O /dev/null http://backend:8080/q/health/ready || return"
+ " 1\n"
+ "curl --fail http://127.0.0.1:8088/api/items -o /dev/null\n"
+ "}\n";
final String script = "COMPOSE_PROJECT=recipe-demo-test\n"
+ "DEPLOY_ROLES='backend frontend'\n"
+ "DEPENDENCY_SERVICES=database\n"
+ "TARGET_PLATFORM=linux/amd64\n"
+ "REQUIRED_SECRETS=DATABASE_PASSWORD\n"
+ health + "\n"
+ Files.readString(Path.of("generated/deploy/remote.sh"));
final Commands commands = new Commands(Duration.ofMinutes(1));
Fixture() throws Exception {
Files.createDirectory(directory.resolve("bin"));
executable("docker", DOCKER);
executable(
"curl",
"#!/bin/sh\nprintf 'curl %s\\n' \"$*\" >> \"$FIXTURE/calls\"\n[ \"${FAIL_HEALTH:-0}\" = 0 ]\n");
env.put("PATH", directory.resolve("bin") + ":" + env.get("PATH"));
env.put("FIXTURE", directory.toString());
Files.createDirectory(directory.resolve("docker-auth"));
Files.writeString(directory.resolve("docker-auth/config.json"), "{}");
Files.writeString(
directory.resolve("production.env"),
"DATABASE_PASSWORD=fixture-only-password\n"
+ "BIND_IP=127.0.0.1\n"
+ "FRONTEND_PORT=8088\n"
+ "DOCKER_SUDO=0\n"
+ "DOCKER_BIN="
+ directory.resolve("bin/docker") + "\nDOCKER_CONFIG=" + directory.resolve("docker-auth")
+ "\n");
}
void executable(String name, String body) throws Exception {
Path path = directory.resolve("bin").resolve(name);
Files.writeString(path, body);
Files.setPosixFilePermissions(path, PosixFilePermissions.fromString("rwx------"));
}
Commands.Result call(String action, String input, String owner, String... args) throws Exception {
var command = new ArrayList<>(
List.of("timeout", "-k", "1", "10", "sh", "-c", script, "--", action, directory.toString(), owner));
command.addAll(List.of(args));
commands.renew(Duration.ofSeconds(15));
return commands.execute(command, directory, env, input, false);
}
String backend() {
return "nas:3000/owner/recipe-demo-backend@sha256:" + "1".repeat(64);
}
String frontend() {
return "nas:3000/owner/recipe-demo-frontend@sha256:" + "2".repeat(64);
}
void prepare() throws Exception {
call("lock", null, "one").checked();
call("preflight", null, "one").checked();
call("stage", "services: {}\n", "one").checked();
call("stage-health", health, "one").checked();
String result = call("prepare", null, "one", "v1.0.0", ReleaseTests.SHA, "nas:3000/owner/recipe-demo-")
.checked();
CiTests.equal(true, result.contains("S6_BACKEND=" + backend()));
CiTests.equal(true, result.contains("S6_FRONTEND=" + frontend()));
}
@Override
public void close() throws Exception {
Pipeline.deleteTree(directory);
}
}
static final String DOCKER = """
#!/bin/sh
set -eu
printf 'docker %s\n' "$*" >> "$FIXTURE/calls"
[ "$1" != --config ] || shift 2
case "$1" in
info) echo linux/x86_64;;
pull) [ "${FAIL_PULL:-0}" = 0 ];;
image)
format=$4; image=$5
case "$image" in *backend*) role=backend; digit=1;; *) role=frontend; digit=2;; esac
case "$format" in
*revision*)
if [ "${WRONG_REVISION:-0}" = 1 ]; then echo wrong; else printf '%040d\n' 0 | tr 0 a; fi;;
*Architecture*) echo linux/amd64;;
*RepoDigests*)
printf '%s@sha256:' "${image%:*}"; printf '%064d\n' 0 | tr 0 "$digit";;
*Id*) echo "sha256:$role";;
*) exit 1;;
esac;;
compose)
shift
[ "$1" != version ] || exit 0
shift 4
[ "$1" = --env-file ] || exit 1
production=$2
shift 2
[ "$1" = --env-file ] || exit 1
file=$2
shift 4
# Compose must read the secret from disk, even after sudo clears the environment.
test -f "$production"
. "$production"
[ "$DATABASE_PASSWORD" = fixture-only-password ] || exit 15
case "$1" in
config) test -f "$file";;
up) cp "$file" "$FIXTURE/live.env";;
ps) eval 'role=${'"$#"'}'; echo "$role";;
logs) echo 'sample container diagnostics';;
*) exit 1;;
esac;;
inspect)
. "$FIXTURE/live.env"
format=$3; id=$4
case "$format" in
*State.Running*) echo true;;
*Image*) echo "sha256:$id";;
*revision*) echo "$RELEASE_SHA";;
*release*) echo "$RELEASE_VERSION";;
*) exit 1;;
esac;;
exec) [ "${FAIL_HEALTH:-0}" = 0 ];;
*) exit 1;;
esac
""";
}