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
3 changes: 1 addition & 2 deletions deploy/install/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ This directory holds the Core/Web installer, the `oac` command and the node inst

## Versions and the lock

- Install only into an empty directory or over an [incomplete installation](#new-installations), or repair a complete installation of the same source revision. Refuse older formats and different revisions before changing anything; keep their data and direct the operator to install separately. Distributions carry only current installation code: no conversion, migration or binary replacement. Keep the refusal checks and their tests.
- Install only into an empty directory or over an [incomplete installation](#new-installations), or repair a complete installation of the same source revision. Require the current state format and matching source revision before changing installation files or services. Preserve data when those checks fail.
- The packaged `oac.pyz` embeds its build revision and refuses a `state.json` whose `source_commit` differs.
- The installer and every mutating `oac` command share `.oac.lock`. The installer holds it across creation, payload, native service and launcher repair, and apply, calling the already-locked apply implementation without locking again. Never replace the lock file; its inode must stay stable. Only the cleanup of a new installation, with the directory the installer created, and `oac uninstall` unlink it, last and while holding it. `locked` refuses a lock whose path no longer names the file it locked.

Expand All @@ -67,7 +67,6 @@ This directory holds the Core/Web installer, the `oac` command and the node inst
- `docker` prints its weaker isolation and needs a y/N confirmation or `--accept-docker-risks` before anything is created.
- `e2b` needs a non-loopback HTTPS `public_url`, `--e2b-api-key-file` and `--e2b-template`; otherwise the installer refuses before installing anything.
- A Docker or microsandbox selection with a loopback `public_url` is saved, but no node can serve it until `public_url` is guest-reachable HTTPS.
- `--sandbox-provider` and `--provider` fail with a message naming `--sandbox`.

## Accounts and permissions

Expand Down
32 changes: 1 addition & 31 deletions deploy/install/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@
}
SETTING_FLAGS = ("core_only", "web_only", "native_core", *(
flag.removeprefix("--").replace("-", "_") for flag in SETTING_ARGUMENTS))
RETIRED_NODE_FLAGS = ("--sandbox-provider and --provider are retired: use --sandbox docker|microsandbox|e2b|none. "
"The installer no longer adds this host as a node; add it with Add node on the Nodes page in Web.")
AVOID = 20 # An omitted Core or Web port moves at most this far above its default.
DOCKER_RISKS = """Docker sandboxes isolate less than microsandbox, the default:
- Containers share the node's kernel, so a container escape reaches the host;
Expand Down Expand Up @@ -163,36 +161,18 @@ def arguments(argv=None):
parser.add_argument("--e2b-template", help="With --sandbox e2b: the ready template build, template-id:build-uuid")
parser.add_argument("--e2b-api-url", help="With --sandbox e2b: compatible service HTTPS API origin")
parser.add_argument("--e2b-domain", help="With --sandbox e2b: compatible service data-plane domain")
parser.add_argument("--sandbox-provider", nargs="?", const=True, help=argparse.SUPPRESS)
parser.add_argument("--provider", nargs="?", const=True, help=argparse.SUPPRESS)
parser.add_argument("--install-dir", type=Path)
for flag, (_, node) in SETTING_ARGUMENTS.items():
value_type = int if node.get("type") == "integer" else public_origin if config_model.annotation(node, "check") == "origin" else str
parser.add_argument(flag, type=value_type, help=node["description"])
parser.add_argument("--core-key-file", type=Path, help="Web-only: private file containing the existing Core's Core key")
parser.add_argument("--config", type=Path, help="Seed a new installation's config.json from this file")
parser.add_argument("--convert", action="store_true",
help=argparse.SUPPRESS)
parser.add_argument("--yes", action="store_true", help=argparse.SUPPRESS)
parser.add_argument("--admin-token-file", type=Path, help=argparse.SUPPRESS)
parser.add_argument("--status", action="store_true", help=argparse.SUPPRESS)
parser.add_argument("--stop", action="store_true", help=argparse.SUPPRESS)
args = parser.parse_args(argv)
if args.convert or args.yes:
parser.error(oac_cli.UNSUPPORTED_VERSION)
args.explicit_install_dir = args.install_dir is not None
args.install_dir = args.install_dir or Path.home() / ".oac/core"
if args.admin_token_file:
parser.error("--admin-token-file was renamed; use --core-key-file")
if args.sandbox_provider is not None or args.provider is not None:
parser.error(RETIRED_NODE_FLAGS)
for retired in ("status", "stop"):
if getattr(args, retired):
parser.error(f"--{retired} is retired; run {args.install_dir / 'oac'} {retired}")
if not args.install_dir.is_absolute():
parser.error("--install-dir must be absolute")
args.given = [name for name, value in vars(args).items()
if name not in ("install_dir", "given", "explicit_install_dir") and value not in (None, False)]
if name not in ("install_dir", "given") and value not in (None, False)]
return args


Expand Down Expand Up @@ -492,8 +472,6 @@ def publish(source):
if destination.is_symlink():
raise InstallError("Invalid node payload directory")
destination.mkdir(mode=0o700, exist_ok=True)
if (destination / "manifest.json").exists():
raise InstallError(oac_cli.UNSUPPORTED_VERSION)
revision = publish(bundle)
pointer = destination / "active.json"
if pointer.is_symlink():
Expand Down Expand Up @@ -677,10 +655,6 @@ def main(argv=None):
if root.is_symlink() or root.resolve() != root:
raise InstallError("Installation directory must be canonical and not a symlink")
bundle = Path(__file__).resolve().parent
if not args.explicit_install_dir:
old = Path.home() / ".parsar/core"
if (old / "state.json").exists() or (old / "installation.json").exists():
raise InstallError(oac_cli.UNSUPPORTED_VERSION)
# Settings and listeners take seconds to check, so they come before hashing the bundle.
prepared = None
if layout(root) == "empty":
Expand All @@ -701,14 +675,10 @@ def main(argv=None):


def check_release(root, manifest):
if (root / "installation.json").exists():
raise InstallError(oac_cli.UNSUPPORTED_VERSION)
if (root / "state.json").exists():
state = oac_cli.load_state(root)
if state.get("source_commit") != manifest["source_commit"]:
raise InstallError(oac_cli.UNSUPPORTED_VERSION)
if (root / "node-payload/manifest.json").exists():
raise InstallError(oac_cli.UNSUPPORTED_VERSION)


def prepare_fresh(args):
Expand Down
6 changes: 2 additions & 4 deletions deploy/install/oac_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@


SOURCE_COMMIT = None # Set by the packaged entrypoint from its build revision.
UNSUPPORTED_VERSION = ("This installation version or historical conversion is not supported; "
UNSUPPORTED_VERSION = ("This installation version is not supported; "
"preserve its data and reinstall into a new empty directory. Nothing was changed.")
INCOMPLETE = ("This installation did not finish installing. Rerun the installer command, which removes what is "
"left and installs again, or remove it with oac uninstall.")
Expand Down Expand Up @@ -144,7 +144,7 @@ def load_config(root):

def load_state(root):
state = json.loads(read_private(root / "state.json", "state.json"))
if (state.get("format") != 2 or state.get("converted_from") or state.get("renamed_from")
if (state.get("format") != 2
or SOURCE_COMMIT is not None and state.get("source_commit") != SOURCE_COMMIT):
raise OacError(UNSUPPORTED_VERSION)
return state
Expand Down Expand Up @@ -1169,8 +1169,6 @@ def uninstall(root, yes=False, interactive=None, out=print):

def main(argv=None, root=None, out=print):
root = Path(root) if root else Path(sys.argv[0]).resolve().parent
if Path(sys.argv[0]).name == "parsar":
raise OacError(f"parsar was renamed to oac. Run {root / 'oac'} <command>.")
parser = argparse.ArgumentParser(prog=str(root / "oac"), description=__doc__.split("\n\n")[0])
commands = parser.add_subparsers(dest="command", required=True)
commands.add_parser("status", help="Show service health, addresses and configuration drift")
Expand Down
40 changes: 0 additions & 40 deletions deploy/install/test_convert.py

This file was deleted.

55 changes: 10 additions & 45 deletions deploy/install/test_install.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""install.sh acceptance behavior: fresh install, repair, retired flags and bundle checks."""
"""install.sh acceptance behavior: fresh install, repair and bundle checks."""

import contextlib
import io
Expand Down Expand Up @@ -320,7 +320,7 @@ def test_web_repair_refuses_old_paired_core_before_payload_or_state_writes(self)
self.install()
self.assertEqual(before, self.snapshot())

def test_different_revision_and_conversion_refuse_before_mutation(self):
def test_different_revision_refuses_before_mutation(self):
self.install()
state = self.document("state.json")
state["source_commit"] = "b" * 40
Expand All @@ -329,9 +329,6 @@ def test_different_revision_and_conversion_refuse_before_mutation(self):
with self.assertRaisesRegex(install.InstallError, "not supported;.*reinstall"):
self.install()
self.assertEqual(before, self.snapshot())
with self.assertRaises(SystemExit):
self.install("--convert")
self.assertEqual(before, self.snapshot())

def test_fresh_install_writes_config_json_and_the_layout(self):
previous = os.umask(0)
Expand Down Expand Up @@ -459,31 +456,17 @@ def test_rerun_reads_config_json_rejects_flags_and_repairs(self):
with contextlib.redirect_stdout(self.output):
run_installer(install, other, ["--install-dir", self.root])

def test_retired_flags_and_old_layouts_name_their_replacement(self):
for flag, command in (("--status", "oac status"), ("--stop", "oac stop")):
output = io.StringIO()
with self.subTest(flag=flag), contextlib.redirect_stderr(output), self.assertRaises(SystemExit):
install.arguments(["--install-dir", str(self.root), flag])
self.assertIn(f"{flag} is retired; run {self.root / command}", output.getvalue())
for flags in (["--sandbox-provider"], ["--sandbox-provider", "true", "--provider", "docker"], ["--provider", "docker"]):
output = io.StringIO()
with self.subTest(flags=flags), contextlib.redirect_stderr(output), self.assertRaises(SystemExit):
install.arguments(flags)
self.assertIn("are retired: use --sandbox docker|microsandbox|e2b|none. The installer no longer adds this "
"host as a node", output.getvalue())
output = io.StringIO()
with contextlib.redirect_stderr(output), self.assertRaises(SystemExit):
install.arguments(["--admin-token-file", str(self.key_file())])
self.assertIn("--core-key-file", output.getvalue())
def test_unsupported_state_format_refuses_before_lock_creation(self):
self.root.mkdir()
(self.root / "installation.json").write_text("{}")
with self.assertRaisesRegex(install.InstallError, "not supported;.*reinstall"):
self.install()
(self.root / "installation.json").unlink()
(self.root / "state.json").write_text('{"format": 1, "generated": {}}')
(self.root / "state.json").chmod(0o600)
install.oac_cli.save_state(self.root, {"format": 1})
before = self.snapshot()
with self.assertRaisesRegex(install.oac_cli.OacError, "not supported;.*reinstall"):
self.install()
with self.assertRaisesRegex(install.oac_cli.OacError, "not supported;.*reinstall"):
with install.oac_cli.locked(self.root):
self.fail("Unsupported installation acquired the lock")
self.assertEqual(before, self.snapshot())
self.assertFalse((self.root / ".oac.lock").exists())

def test_a_live_installation_missing_config_json_is_never_told_to_start_over(self):
self.install()
Expand Down Expand Up @@ -674,24 +657,6 @@ def test_node_payload_exports_only_matched_distribution_files(self):
with self.assertRaisesRegex(install.InstallError, "node payload differs"):
install.prepare_node_payload(self.root, self.document("state.json"), self.bundle)

def test_legacy_flat_payload_is_refused_without_conversion(self):
import shutil
self.install()
payload = self.root / "node-payload"
old = payload / "releases" / ("a" * 40)
for path in old.rglob("*"):
if path.is_file():
target = payload / path.relative_to(old)
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(path, target)
shutil.rmtree(payload / "releases")
(payload / "active.json").unlink()
bundle, _ = make_bundle(self.work / "bundle-b", dict(MANIFEST, source_commit="b" * 40))
before = self.snapshot()
with self.assertRaisesRegex(install.InstallError, "not supported;.*reinstall"):
install.prepare_node_payload(self.root, self.document("state.json"), bundle)
self.assertEqual(before, self.snapshot())

def test_bundle_verifies_transferred_bytes_and_checksum_list(self):
self.assertEqual(install.verify_bundle(self.bundle)["source_commit"], "a" * 40)
for name in ("images/core.tar", "manifest.json", "oac.pyz", "config.schema.json"):
Expand Down
26 changes: 0 additions & 26 deletions deploy/install/test_rename.py

This file was deleted.

4 changes: 2 additions & 2 deletions docs/getting-started/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ Nodes on other hosts keep running. To uninstall them the usual way, remove them

## Installation version policy

An installation runs one release for its whole life. In-place version upgrades, downgrades and historical conversions are not supported. Nothing migrates data between releases.
An installation runs one release for its whole life. In-place version upgrades and downgrades are not supported. Nothing migrates data between releases.

To move to a new release, install it into a new, empty directory, with its own database, Core key and nodes, and add nodes from its Web. Keep the old installation, its data and its nodes until their work is finished. Nodes run the program of the console that added them and are never upgraded in place; Core accepts only nodes that speak its own node protocol.

Expand All @@ -175,7 +175,7 @@ The installer and mutating `oac` commands hold the same installation lock, `.oac
| `Automatic HTTPS needs ports 80 and 443 …` | Free the port the message names, install without `--public-url` and set up the domain later, or install with `--ingress external` and use your own [reverse proxy](install-options.md#https-and-the-reverse-proxy) |
| `Installation directory is not empty …` | Use an empty `--install-dir` |
| `This installation is configured by …/config.json …` | Flags only seed a new installation: edit `config.json` and run `oac apply`. To start over with other flags, [uninstall](#uninstall) it first |
| `This installation version or historical conversion is not supported …` | The target directory holds an installation of another release, or a default install found one at `~/.parsar/core`. Keep it, and install into another empty `--install-dir` ([version policy](#installation-version-policy)) |
| `This installation version is not supported …` | The target installation's state format or source revision does not match this release. Keep it, and install into another empty `--install-dir` ([version policy](#installation-version-policy)) |
| `generated/<file> was edited by hand` | Put the change in `config.json`, then `oac apply --discard-edits` |
| `config.json has changes that are not applied` | Run `oac apply` |
| `Core rejects secrets/core.key …` | Run `oac apply`, which restarts Core with the key's digest |
Expand Down
Loading