Files
core/test/onboarding/verify_release_update.py
T

246 lines
11 KiB
Python

#!/usr/bin/env python3
"""Actual-version install, upgrade and rollback in an owned container directory."""
import argparse
import json
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import time
from install_support import load_installer
from verify_prepared_release import execute, install_assets, response, worker_project
class LocalDaemon:
def __init__(self, machine):
self.machine = machine
self.launcher = machine / "bin/dever"
self.child = None
self.errors = None
def __enter__(self):
self.errors = tempfile.TemporaryFile()
self.child = subprocess.Popen(
[str(self.machine / "bin/deverd"), "--root", str(self.machine)],
env={}, stdout=subprocess.DEVNULL, stderr=self.errors,
)
try:
deadline = time.monotonic() + 10
while time.monotonic() < deadline:
if self.child.poll() is not None:
self.errors.seek(0)
raise RuntimeError(self.errors.read(65536).decode(errors="replace"))
if (self.machine / "state/deverd.sock").is_socket():
self.command("cache", "status")
return self
time.sleep(0.05)
raise RuntimeError("owned compiler daemon did not become ready")
except BaseException:
self.__exit__(None, None, None)
raise
def command(self, *arguments):
return execute([self.launcher, *arguments], timeout=240)
def __exit__(self, *_):
if self.child is not None and self.child.poll() is None:
self.child.terminate()
try:
self.child.wait(timeout=10)
except subprocess.TimeoutExpired:
self.child.kill()
self.child.wait(timeout=10)
if self.errors is not None:
self.errors.close()
def write(root, relative, contents):
path = root / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(contents)
def profile_project(root, profile):
sqlite = {"type": "sqlite", "path": "data/owned.sqlite", "max_connections": 1}
postgres = {"type": "postgres", "url": "postgres://fixture@127.0.0.1:1/unused", "tls": "disabled"}
settings = {
"base": {}, "sqlite": {"database": {"default": sqlite}},
"postgres": {"database": {"default": postgres}},
"both": {"database": {"default": sqlite, "unused": postgres}},
}[profile]
write(root, "config/setting.json", json.dumps(settings))
if profile in ("sqlite", "both"):
write(root, "module/sample/value/model.dever", "type Value { name: Text(1, 32) }\n")
write(root, "module/sample/value/api.dever", "cmd exercise = app.exercise\nlong cmd stages = app.stages\n")
source = '''exercise() (okay: Bool) {
saved = model.create({ name = "pack" })
count = model.count()
removed = model.delete(saved.id)
okay = count == 1 and removed == 1
}
stages() (okay: Bool) {
id = store()
dever.task.sleep(1)
okay = finish(id)
}
transaction store() (id: model.id) {
saved = model.create({ name = "stage" })
id = saved.id
}
transaction finish(id: model.id) (okay: Bool) {
count = model.count()
removed = model.delete(id)
okay = count == 1 and removed == 1
}
'''
else:
write(root, "module/sample/value/api.dever", "cmd exercise = app.exercise\n")
source = "exercise() (okay: Bool) { dever.task.sleep(1)\n okay = true }\n"
write(root, "module/sample/value/app.dever", source)
def fresh_install(args, manifest, passed):
image = args.work / "fresh"
image.mkdir()
install_assets(args.assets, image, manifest, args.trusted_key)
machine = image / "opt/dever"
programs = []
with LocalDaemon(machine) as daemon:
if daemon.command("version") != manifest["version"]:
raise RuntimeError("fresh installation selected the wrong version")
skill = machine / "versions" / manifest["version"] / "skills/dever-language"
if daemon.command("skill", "path") != str(skill):
raise RuntimeError("installed skill is not paired with the compiler")
if not (skill / "assets/template/module/hello/greeting/app.dever").is_file():
raise RuntimeError("release has no complete project template")
extensions = machine / "cache/extensions"
if extensions.exists() and any(extensions.iterdir()):
raise RuntimeError("fresh base installation installed optional extensions")
project = image / "hello"
daemon.command("new", project)
response(daemon.command("run", project, "--", "hello.greeting.greet", '{"name":"Dever"}'))
passed("fresh_split_base_install_and_full_skill_template")
for profile in ("base", "sqlite", "postgres", "both"):
project = image / f"profile-{profile}"
profile_project(project, profile)
response(daemon.command("run", project, "--", "sample.value.exercise", "{}"), True)
if profile in ("sqlite", "both"):
response(daemon.command("run", project, "--", "sample.value.stages", "{}"), True)
program = project / "program"
daemon.command("build", project, "--output", program)
shutil.rmtree(project / "module")
programs.append((program, "sample.value.exercise", True))
passed("four_runtime_profiles_sqlite_and_long_transactions")
for ecosystem in ("pip", "npm", "go"):
execute([args.extension_helper, machine, args.assets, manifest["version"], f"runtime-{ecosystem}-{manifest['platform']}"])
project = worker_project(image, ecosystem)
daemon.command("lib", "add", project)
response(daemon.command("run", project, "--", "sample.worker.probe", "{}"), True)
program = project / "program"
daemon.command("build", project, "--output", program)
shutil.rmtree(project / "module")
programs.append((program, "sample.worker.probe", True))
passed(f"signed_optional_{ecosystem}_managed_run_and_build")
for program, command, expected in programs:
response(execute([program, command, "{}"]), expected)
passed("source_free_standalone_programs_without_running_daemon")
shutil.rmtree(image)
def upgrade(args, manifest, previous, passed):
image = args.work / "upgrade"
image.mkdir()
original_umask = os.umask(0o022)
try:
load_installer().install(argparse.Namespace(
version=previous["version"], release=args.previous_release,
system_root=image, no_start=True, trusted_key=args.trusted_key,
))
finally:
os.umask(original_umask)
machine = image / "opt/dever"
project = image / "existing-project"
with LocalDaemon(machine) as daemon:
if daemon.command("version") != previous["version"]:
raise RuntimeError("previous installation did not select its actual binary version")
daemon.command("new", project)
response(daemon.command("run", project, "--", "hello.greeting.greet", '{"name":"Dever"}'))
original = (project / "config/setting.json").read_bytes()
passed("actual_previous_release_installed_and_program_runs")
install_assets(args.assets, image, manifest, args.trusted_key)
with LocalDaemon(machine) as daemon:
if daemon.command("version") != manifest["version"]:
raise RuntimeError("upgrade did not select the actual new compiler version")
response(daemon.command("run", project, "--", "hello.greeting.greet", '{"name":"Dever"}'))
if (project / "config/setting.json").read_bytes() != original:
raise RuntimeError("upgrade modified project configuration")
passed("actual_new_release_upgrade_preserves_existing_project")
daemon.command("use", previous["version"])
if daemon.command("version") != previous["version"]:
raise RuntimeError("rollback did not restore the previous compiler")
response(daemon.command("run", project, "--", "hello.greeting.greet", '{"name":"Dever"}'))
passed("rollback_runs_actual_previous_compiler")
daemon.command("use", manifest["version"])
if daemon.command("version") != manifest["version"]:
raise RuntimeError("switch after rollback did not restore the new compiler")
response(daemon.command("run", project, "--", "hello.greeting.greet", '{"name":"Dever"}'))
passed("new_release_runs_again_after_rollback")
shutil.rmtree(image)
def accept(args):
manifest = json.loads((args.release / "manifest.json").read_bytes())
previous = json.loads((args.previous_release / "manifest.json").read_bytes())
if tuple(map(int, manifest["version"].split("."))) <= tuple(map(int, previous["version"].split("."))):
raise ValueError("upgrade requires distinct increasing actual release versions")
args.work.mkdir(mode=0o700)
result = {
"previous_version": previous["version"], "version": manifest["version"],
"checks": [], "errors": {}, "transport": "local signed split assets",
"trust": "explicit acceptance key; not proof of production signing",
"host_language_absence": "not_verified", "gpu": "not_run",
"postgres_server": "not_run", "online_update": "not_run",
"global_service_actions": "not_run",
}
def passed(name):
result["checks"].append(name)
print(f"PASS {name}", flush=True)
try:
for name, stage in (
("fresh", lambda: fresh_install(args, manifest, passed)),
("upgrade", lambda: upgrade(args, manifest, previous, passed)),
):
try:
stage()
except (OSError, ValueError, RuntimeError, subprocess.SubprocessError) as error:
result["errors"][name] = str(error)
print(f"FAIL {name}: {error}", flush=True)
result["status"] = "failed" if result["errors"] else "passed"
return not result["errors"]
finally:
args.report.write_text(json.dumps(result, indent=2) + "\n")
def main():
parser = argparse.ArgumentParser(description=__doc__)
for name in ("previous-release", "release", "assets", "trusted-key", "work", "report", "extension-helper"):
parser.add_argument(f"--{name}", required=True, type=Path)
args = parser.parse_args()
if os.geteuid() != 0:
parser.error("requires root in an owned validation container; never touches its installed service")
for name, value in vars(args).items():
if not value.is_absolute():
parser.error(f"{name} must be an explicit absolute path")
if args.work.exists() or args.work.is_symlink() or args.report.exists():
parser.error("acceptance work/report must be new owned paths")
raise SystemExit(0 if accept(args) else 1)
if __name__ == "__main__":
main()