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 deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

## Installation

`install.sh` downloads its release's `compose.yaml` and port files, checks them against `compose-sha256sums.txt`, writes `.env`, and starts Compose. Core applies database migrations when it starts. The host needs Linux amd64 and Docker Compose 2.26 or newer. [Configuration](../docs/configuration.md) owns the installation layout and settings.
`install.sh` downloads its release's `compose.yaml` and port files, checks them against `compose-sha256sums.txt`, writes `.env`, initializes the data directory and validates the settings with `oac-core check-config`, then starts Compose. A rejected setting stops the install before any service starts. Core applies database migrations when it starts. The host needs Linux amd64 and Docker Compose 2.26 or newer. [Configuration](../docs/configuration.md) owns the installation layout and settings.

`oac` is a Go command (`services/core/cmd/oac`) in the Core image and the ingress image. The host copy implements `apply`, `core-key` and `rotate-core-key`; `core-key --show` runs `oac-web core-key` in the Web container. Start, stop, logs and removal are `docker compose`. `apply` runs `oac-core check-config` before recreating services. The ingress image runs data initialization as `oac init` and contains no Python. No service receives a Docker socket.

Expand Down
13 changes: 13 additions & 0 deletions deploy/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,19 @@ umask 077
docker compose create core
docker compose cp core:/usr/local/bin/oac ./oac
chmod 755 ./oac
# check-config reads the installation id and secrets that init writes, so
# initialize the data directory before validating the settings. A rejected
# configuration must fail here, not after Compose reports a running stack.
if ! initialized="$(docker compose run --rm -T init 2>&1)"; then
printf '%s\n' "$initialized" >&2
echo "Installation initialization failed; no service was started." >&2
exit 1
fi
if ! checked="$(docker compose run --rm -T --no-deps --entrypoint /usr/local/bin/oac-core core check-config 2>&1)"; then
printf '%s\n' "$checked" >&2
echo "Configuration check failed; no service was started." >&2
exit 1
fi
docker compose up -d --wait
)
kept=1
Expand Down
55 changes: 54 additions & 1 deletion deploy/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@


class InstallScriptTests(unittest.TestCase):
def install(self, root, *args, compose_up=0):
def install(self, root, *args, compose_up=0, check_config=0, init=0):
bin_dir = root / "bin"
bin_dir.mkdir(exist_ok=True)
log = root / "docker.log"
Expand All @@ -22,6 +22,17 @@ def install(self, root, *args, compose_up=0):
printf '%s\\n' "$*" >> {log}
if [ "$1" = compose ] && [ "$2" = version ]; then printf 'v2.29.1\\n'; exit 0; fi
if [ "$1" = compose ] && [ "$2" = cp ]; then printf '#!/bin/sh\\n' > ./oac; exit 0; fi
if [ "$1" = compose ] && [ "$2" = run ]; then
case "$*" in
*check-config*)
if [ {check_config} -ne 0 ]; then
printf '%s\\n' 'OAC_PUBLIC_URL must be a canonical HTTPS origin without path, credentials, query or fragment, such as https://core.example; plain HTTP is accepted only for a loopback host' >&2
fi
exit {check_config}
;;
esac
exit {init}
fi
if [ "$1" = compose ] && [ "$2" = up ]; then exit {compose_up}; fi
exit 0
"""))
Expand Down Expand Up @@ -51,6 +62,48 @@ def test_a_failed_first_start_stops_and_removes_the_directory(self):
self.assertIn("compose pull", recorded)
self.assertIn("compose up -d --wait", recorded)

def test_initialization_and_configuration_check_run_before_the_stack_starts(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
completed, recorded = self.install(root, "--public-url", "https://core.example")
self.assertEqual(completed.returncode, 0, completed.stderr)
lines = recorded.splitlines()
initialize = next(index for index, line in enumerate(lines) if line == "compose run --rm -T init")
check = next(index for index, line in enumerate(lines) if line.endswith("core check-config"))
up = next(index for index, line in enumerate(lines) if line == "compose up -d --wait")
self.assertLess(initialize, check, recorded)
self.assertLess(check, up, recorded)
self.assertIn("--no-deps", lines[check])
self.assertIn("--entrypoint /usr/local/bin/oac-core", lines[check])

def test_a_rejected_public_url_fails_without_reporting_success(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
completed, recorded = self.install(root, "--public-url", "http://10.0.0.5:8080", check_config=1)
self.assertNotEqual(completed.returncode, 0)
self.assertIn("plain HTTP is accepted only for a loopback host", completed.stderr)
self.assertNotIn("OpenAgentCore is running.", completed.stdout)
self.assertNotIn("compose up -d --wait", recorded)
self.assertFalse((root / "oac").exists(), "a rejected configuration must remove the directory")

def test_the_insecure_origin_switch_keeps_the_install_succeeding(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
completed, _ = self.install(root, "--public-url", "http://10.0.0.5:8080", "--allow-insecure-origin")
self.assertEqual(completed.returncode, 0, completed.stderr)
self.assertIn("OpenAgentCore is running.", completed.stdout)
self.assertIn("Core key:", completed.stdout)

def test_a_failed_initialization_stops_before_the_stack_starts(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
completed, recorded = self.install(root, "--public-url", "https://core.example", init=1)
self.assertNotEqual(completed.returncode, 0)
self.assertNotIn("OpenAgentCore is running.", completed.stdout)
self.assertNotIn("check-config", recorded)
self.assertNotIn("compose up -d --wait", recorded)
self.assertFalse((root / "oac").exists())

def test_env_holds_only_the_installation_choices(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
Expand Down
Loading