Implement manual versioned Synology deployments for S6
All checks were successful
Build, test and publish / verify (push) Successful in 6m34s
All checks were successful
Build, test and publish / verify (push) Successful in 6m34s
This commit is contained in:
197
ci/ReleaseShellTests.java
Normal file
197
ci/ReleaseShellTests.java
Normal file
@@ -0,0 +1,197 @@
|
||||
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/dance-schools"));
|
||||
CiTests.equal(false, calls.contains(" down"));
|
||||
CiTests.equal(false, calls.contains("prune"));
|
||||
}
|
||||
});
|
||||
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 script = Files.readString(Path.of("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"),
|
||||
"BIND_IP=127.0.0.1\nFRONTEND_PORT=8088\nDOCKER_SUDO=0\nDOCKER_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/agiledancers-backend@sha256:" + "1".repeat(64);
|
||||
}
|
||||
|
||||
String frontend() {
|
||||
return "nas:3000/owner/agiledancers-frontend@sha256:" + "2".repeat(64);
|
||||
}
|
||||
|
||||
void prepare() throws Exception {
|
||||
call("lock", null, "one").checked();
|
||||
call("preflight", null, "one").checked();
|
||||
call("stage", Files.readString(Path.of("deploy/compose.yaml")), "one")
|
||||
.checked();
|
||||
String result = call("prepare", null, "one", "v1.0.0", ReleaseTests.SHA, "nas:3000/owner/agiledancers-")
|
||||
.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
|
||||
file=$2
|
||||
shift 4
|
||||
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
|
||||
""";
|
||||
}
|
||||
Reference in New Issue
Block a user