Take authorized_keys on the cidata drive instead of ssh.json

The JSON wrapper was the one piece of autoinstall input that wasn't an
existing format: the other files are the configurator's own output, and
the keys end up in sshd's authorized_keys anyway. So take that file
directly -- one key per line, comments and key options work as they do
everywhere else -- and drop the parsing in between. The only thing JSON
bought was parse-error detection, and the phase still fails loudly when
the file carries no usable keys.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier HanssonandClaude Fable 5 committed 2026-08-04 06:08:33 -07:00
1 parent be68a76a44
commit f11a70c6ba
9 files changed
+80 -86

No files matched your search

+6 -6
View File
@@ -37,25 +37,25 @@ These are the configurator's own output files, so the way to get a starting set
| `user_full_name.txt` | No | Git full name |
| `user_email_address.txt` | No | Git email |
| `user_encrypt_installation.txt` | No | `true` when `user_configuration.json` carries a `disk_encryption` block; defaults to false |
| `ssh.json` | No | JSON array of SSH public keys |
| `authorized_keys` | No | SSH public keys in sshd's own format, one per line |
Both required files must be present or the installer falls back to the configurator. Generate the password hash for `user_credentials.json` with `openssl passwd -6 "yourpassword"`.
Encryption itself is configured by the `disk_encryption` block inside `user_configuration.json` — which carries the passphrase in plaintext, so treat a drive built from an encrypted install accordingly. The flag file must match it: it drives the encrypted install's SDDM autologin and the final boot validation, not the encryption.
`ssh.json` is a JSON array of public keys:
`authorized_keys` is the same file sshd reads — copy your own or write one key per line:
```json
["ssh-ed25519 AAAA... you@host"]
```
ssh-ed25519 AAAA... you@host
```
When `ssh.json` is present, autoinstall writes the keys to the user's `~/.ssh/authorized_keys`, enables `sshd`, and adds a `ufw allow ssh` rule — a stock Omarchy install ships openssh with the service disabled and its firewall opens neither port 22 nor anything else beyond LocalSend. Networking needs nothing extra; NetworkManager is already enabled with DHCP. Password SSH authentication is left at the distro default. An unparseable or empty `ssh.json` fails the install rather than producing a machine nobody can reach.
When `authorized_keys` is present, autoinstall installs it as the user's `~/.ssh/authorized_keys`, enables `sshd`, and adds a `ufw allow ssh` rule — a stock Omarchy install ships openssh with the service disabled and its firewall opens neither port 22 nor anything else beyond LocalSend. Networking needs nothing extra; NetworkManager is already enabled with DHCP. Password SSH authentication is left at the distro default. An `authorized_keys` with no usable keys fails the install rather than producing a machine nobody can reach.
### Building the drive
```bash
mkdir cidata
cp user_configuration.json user_credentials.json ssh.json cidata/
cp user_configuration.json user_credentials.json authorized_keys cidata/
genisoimage -output cidata.iso -volid cidata -joliet -rock cidata/
```
+1 -1
View File
@@ -116,4 +116,4 @@ rm -f /run/omarchy-install/state.json
--full-name-file /root/user_full_name.txt \
--email-file /root/user_email_address.txt \
--encrypt-file /root/user_encrypt_installation.txt \
--ssh-keys-file /root/ssh.json
--authorized-keys-file /root/authorized_keys
@@ -38,15 +38,15 @@ mount -o ro "$device" "$mnt" || exit 1
loaded=1
if [[ -f $mnt/user_configuration.json && -f $mnt/user_credentials.json ]]; then
files=("$mnt"/user_configuration.json "$mnt"/user_credentials.json)
for file in user_full_name.txt user_email_address.txt user_encrypt_installation.txt ssh.json; do
for file in user_full_name.txt user_email_address.txt user_encrypt_installation.txt authorized_keys; do
[[ -f $mnt/$file ]] && files+=("$mnt/$file")
done
if cp "${files[@]}" "$dest"/; then
loaded=0
else
# A partial copy must not leave files behind: the wizard cleans up only
# what it writes itself, so a half-copied ssh.json would leak into the
# interactive install that follows.
# what it writes itself, so a half-copied authorized_keys would leak into
# the interactive install that follows.
for file in "${files[@]}"; do
rm -f "$dest/${file##*/}"
done
@@ -11,13 +11,13 @@ set -eEo pipefail
# doesn't choke on flags it doesn't recognize.
while [[ $# -gt 0 ]]; do
case $1 in
--config) export OMARCHY_INSTALL_CONFIG="$2"; shift 2 ;;
--creds) export OMARCHY_INSTALL_CREDS="$2"; shift 2 ;;
--full-name-file) export OMARCHY_INSTALL_FULL_NAME_FILE="$2"; shift 2 ;;
--email-file) export OMARCHY_INSTALL_EMAIL_FILE="$2"; shift 2 ;;
--encrypt-file) export OMARCHY_INSTALL_ENCRYPT_FILE="$2"; shift 2 ;;
--ssh-keys-file) export OMARCHY_INSTALL_SSH_KEYS_FILE="$2"; shift 2 ;;
*) echo "omarchy-iso-install: unknown arg $1" >&2; exit 2 ;;
--config) export OMARCHY_INSTALL_CONFIG="$2"; shift 2 ;;
--creds) export OMARCHY_INSTALL_CREDS="$2"; shift 2 ;;
--full-name-file) export OMARCHY_INSTALL_FULL_NAME_FILE="$2"; shift 2 ;;
--email-file) export OMARCHY_INSTALL_EMAIL_FILE="$2"; shift 2 ;;
--encrypt-file) export OMARCHY_INSTALL_ENCRYPT_FILE="$2"; shift 2 ;;
--authorized-keys-file) export OMARCHY_INSTALL_AUTHORIZED_KEYS_FILE="$2"; shift 2 ;;
*) echo "omarchy-iso-install: unknown arg $1" >&2; exit 2 ;;
esac
done
@@ -18,7 +18,7 @@ class InstallContext:
full_name: str
email: str
encrypt: bool
ssh_keys_path: Path | None
authorized_keys_path: Path | None
user_configuration: dict
user_credentials: dict
@@ -60,7 +60,7 @@ class InstallContext:
full_name=_read_text(os.environ.get("OMARCHY_INSTALL_FULL_NAME_FILE")),
email=_read_text(os.environ.get("OMARCHY_INSTALL_EMAIL_FILE")),
encrypt=_read_text(os.environ.get("OMARCHY_INSTALL_ENCRYPT_FILE")).lower() in ("true", "yes", "1"),
ssh_keys_path=_optional_path(os.environ.get("OMARCHY_INSTALL_SSH_KEYS_FILE")),
authorized_keys_path=_optional_path(os.environ.get("OMARCHY_INSTALL_AUTHORIZED_KEYS_FILE")),
user_configuration=user_configuration,
user_credentials=json.loads(creds_path.read_text()),
arch_config_path=arch_config_path,
@@ -107,8 +107,8 @@ def _default_omarchy_install(user_configuration: dict) -> dict[str, Any]:
def _optional_path(path: str | None) -> Path | None:
"""A path the caller always passes but that need not exist. The live ISO
passes --ssh-keys-file on every install; only autoinstall drives put a file
there."""
passes --authorized-keys-file on every install; only autoinstall drives put
a file there."""
if not path:
return None
p = Path(path)
@@ -22,7 +22,6 @@ Phase ordering (full-disk and protected/pre-mounted):
from __future__ import annotations
import hashlib
import json
import os
import re
import shutil
@@ -1314,10 +1313,10 @@ def configure_login(ctx: InstallContext) -> None:
# ─────────────────────────────────────────────────────────────────────────────
def configure_ssh_access(ctx: InstallContext) -> None:
if ctx.ssh_keys_path is None:
if ctx.authorized_keys_path is None:
return
keys = _authorized_keys(ctx.ssh_keys_path)
keys = _authorized_keys(ctx.authorized_keys_path)
info(f"› installing {len(keys)} SSH key(s) for {ctx.username}")
ssh_dir = ctx.target / "home" / ctx.username / ".ssh"
@@ -1362,21 +1361,20 @@ def configure_ssh_access(ctx: InstallContext) -> None:
def _authorized_keys(path: Path) -> list[str]:
"""Parse the autoinstall ssh.json: a JSON array of public key strings.
"""Read the autoinstall authorized_keys: sshd's own format, one public key
per line, with blank lines and # comments dropped.
Raise rather than skip on anything unusable. An install that "succeeds"
into a machine nobody can log into is worse than one that stops with the
reason on screen.
"""
try:
keys = json.loads(path.read_text())
except (OSError, json.JSONDecodeError) as exc:
raise RuntimeError(f"{path} is not readable JSON: {exc}") from exc
lines = path.read_text().splitlines()
except OSError as exc:
raise RuntimeError(f"{path} is not readable: {exc}") from exc
if not isinstance(keys, list) or not all(isinstance(key, str) for key in keys):
raise RuntimeError(f"{path} must be a JSON array of public key strings")
keys = [key.strip() for key in keys if key.strip()]
keys = [line.strip() for line in lines]
keys = [key for key in keys if key and not key.startswith("#")]
if not keys:
raise RuntimeError(f"{path} contains no SSH keys")
+11 -11
View File
@@ -68,11 +68,11 @@ Do not take `manifests/fresh-4-semantic.json` as the authority on any of this. I
- Look for `/dev/disk/by-label/cidata` then `/dev/disk/by-label/CIDATA`; exit non-zero if neither exists.
- Mount read-only on a temp mountpoint; exit non-zero if the mount fails.
- Require both `user_configuration.json` and `user_credentials.json` on the drive — this is the same pair `InstallContext.from_env` treats as mandatory. Missing either means the drive is not an autoinstall drive; unmount and exit non-zero.
- Copy the required pair plus, when present, `user_full_name.txt`, `user_email_address.txt`, `user_encrypt_installation.txt`, and `ssh.json` into `/root` with a single `cp`, and exit non-zero if the copy fails — a "loaded" drive with missing inputs would fail the install later with a worse error.
- Copy the required pair plus, when present, `user_full_name.txt`, `user_email_address.txt`, `user_encrypt_installation.txt`, and `authorized_keys` into `/root` with a single `cp`, and exit non-zero if the copy fails — a "loaded" drive with missing inputs would fail the install later with a worse error.
- Unmount before exiting. Leave nothing mounted for the install to trip over.
- The script takes an optional path prefix so the unit tests can point it at a sandbox; production callers pass nothing.
- Replace the bare `./configurator` call in `.automated_script.sh` with: run `omarchy-cidata-load`; on success export `OMARCHY_UI_INTERACTIVE=no`, on failure run `./configurator`.
- Pass `--ssh-keys-file /root/ssh.json` to `omarchy-iso-install` in the dashboard invocation, alongside the existing five flags. Always pass it; the orchestrator no-ops when the file is absent, which keeps the interactive path on the identical command line.
- Pass `--authorized-keys-file /root/authorized_keys` to `omarchy-iso-install` in the dashboard invocation, alongside the existing five flags. Always pass it; the orchestrator no-ops when the file is absent, which keeps the interactive path on the identical command line.
### Notes
- The three text files are optional by construction: `_read_text` in `context.py` returns `""` for a missing path and `encrypt` falls back to false. Do not add stricter checks here than the orchestrator itself applies.
@@ -116,23 +116,23 @@ The dashboard is the only process that owns the visible UI. Suppressing its prom
- `configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py`
### Changes
- `omarchy-iso-install`: add `--ssh-keys-file` to the arg loop, exporting `OMARCHY_INSTALL_SSH_KEYS_FILE`. Same shape as the five existing flags, ahead of the `unknown arg` catch-all (`:19`).
- `context.py`: add an `ssh_keys_path: Path | None` field populated from that variable in `from_env`, `None` when unset or when the file does not exist.
- `omarchy-iso-install`: add `--authorized-keys-file` to the arg loop, exporting `OMARCHY_INSTALL_AUTHORIZED_KEYS_FILE`. Same shape as the five existing flags, ahead of the `unknown arg` catch-all (`:19`).
- `context.py`: add an `authorized_keys_path: Path | None` field populated from that variable in `from_env`, `None` when unset or when the file does not exist.
- `phases_impl.py`: add `configure_ssh_access(ctx)`:
- Return immediately when `ctx.ssh_keys_path` is `None`.
- Parse the file as a JSON array of public key strings; write them one per line to `<target>/home/<user>/.ssh/authorized_keys`.
- Return immediately when `ctx.authorized_keys_path` is `None`.
- Read the file as sshd's own authorized_keys format, dropping blank lines and # comments; write the keys one per line to `<target>/home/<user>/.ssh/authorized_keys`.
- `.ssh` at `0700`, `authorized_keys` at `0600`, both owned by the user via `arch-chroot <target> chown -R <user>:<user>`. Do not hardcode uid 1000 as PR #72 did — ask the target for it.
- `arch-chroot <target> systemctl enable sshd.service`. Works cleanly in the chroot.
- `arch-chroot <target> ufw allow ssh`. This one exits **non-zero** in a chroot (`ERROR: problem running`) because ufw cannot reach netfilter, but it writes the rule to `/etc/ufw/user.rules` before failing, and that file is what `ufw.service` loads on first boot. So ignore the exit status and assert the rule landed in the file instead. No ufw-presence check: ufw is part of Omarchy's default package set, and the rule assertion already fails loudly if it is somehow missing.
- A malformed or empty `ssh.json` must fail the phase loudly. A machine that installs "successfully" and is then unreachable is worse than one that stops with an error on screen.
- An `authorized_keys` with no usable keys must fail the phase loudly. A machine that installs "successfully" and is then unreachable is worse than one that stops with an error on screen.
- `main.py`: register `("Configuring SSH access", configure_ssh_access)` in `build_phases` after `configure_login` (`:52`) and before `validate_boot`.
### Model to follow
`configure_login` (`phases_impl.py:1251`) — direct writes against `ctx.target`, `arch-chroot` reserved for ownership and `systemctl`. Match its structure.
### Acceptance criteria
- Install with `ssh.json`: `ssh <user>@<vm>` works with the corresponding private key on first boot, no password.
- Install without `ssh.json`: phase runs, does nothing, install unaffected; `~/.ssh` is not created.
- Install with `authorized_keys`: `ssh <user>@<vm>` works with the corresponding private key on first boot, no password.
- Install without `authorized_keys`: phase runs, does nothing, install unaffected; `~/.ssh` is not created.
- `authorized_keys` is `0600` and owned by the install user, not root.
- Port 22 is open in ufw on the installed system. Test this by connecting, not by reading the phase log — the rule is written from a chroot that cannot verify it.
- Password SSH auth is left at the distro default — autoinstall does not enable it.
@@ -147,7 +147,7 @@ The dashboard is the only process that owns the visible UI. Suppressing its prom
### Changes
- New "Autoinstall" section covering: what the `cidata` drive is, the file table, how to produce the drive, and a Proxmox `qm create` example with disk-first boot order so the empty disk falls through to the ISO on the first boot and boots the installed system afterward.
- Show `openssl passwd -6` for the credential hash and the `ssh.json` array format.
- Show `openssl passwd -6` for the credential hash and the `authorized_keys` format.
- State plainly that the config files are the configurator's own output — the documented way to get a starting `user_configuration.json` is to run one interactive install and copy what it wrote.
- Do not repeat PR #72's networking claims. NetworkManager comes enabled with DHCP on a stock install; sshd ships disabled, which is why the SSH phase exists.
@@ -168,7 +168,7 @@ The dashboard is the only process that owns the visible UI. Suppressing its prom
### Negative tests
- Drive labeled `cidata` with no files → configurator runs.
- Drive with `user_configuration.json` only → configurator runs.
- Malformed `ssh.json` → install stops in the remote-access phase with a visible error, does not reboot into an unreachable machine.
- An `authorized_keys` with no usable keys → install stops in the remote-access phase with a visible error, does not reboot into an unreachable machine.
- Install failure under autoinstall (e.g. a disk target that does not exist) → failure screen renders, process exits non-zero, no hang.
### Regression surface
+8 -8
View File
@@ -88,9 +88,9 @@ write_required_pair
echo "Jeff" >"$sandbox/media/user_full_name.txt"
echo "jeff@example.com" >"$sandbox/media/user_email_address.txt"
echo "false" >"$sandbox/media/user_encrypt_installation.txt"
echo '["ssh-ed25519 AAAA jeff@host"]' >"$sandbox/media/ssh.json"
echo 'ssh-ed25519 AAAA jeff@host' >"$sandbox/media/authorized_keys"
run_load || fail "full file set loads"
for file in user_configuration.json user_credentials.json user_full_name.txt user_email_address.txt user_encrypt_installation.txt ssh.json; do
for file in user_configuration.json user_credentials.json user_full_name.txt user_email_address.txt user_encrypt_installation.txt authorized_keys; do
[[ -f $sandbox/root/$file ]] || fail "full file set copies $file"
done
grep -q '^umount ' "$TEST_LOG" || fail "full file set unmounts the drive"
@@ -109,16 +109,16 @@ new_sandbox
attach_drive cidata
write_required_pair
run_load || fail "required pair alone loads"
[[ ! -e $sandbox/root/ssh.json ]] || fail "required pair alone copies no optional files"
[[ ! -e $sandbox/root/authorized_keys ]] || fail "required pair alone copies no optional files"
pass "required pair alone loads without optional files"
# Optional files are copied individually when present.
new_sandbox
attach_drive cidata
write_required_pair
echo '["ssh-ed25519 AAAA jeff@host"]' >"$sandbox/media/ssh.json"
run_load || fail "required pair plus ssh.json loads"
[[ -f $sandbox/root/ssh.json ]] || fail "ssh.json is copied when present"
echo 'ssh-ed25519 AAAA jeff@host' >"$sandbox/media/authorized_keys"
run_load || fail "required pair plus authorized_keys loads"
[[ -f $sandbox/root/authorized_keys ]] || fail "authorized_keys is copied when present"
[[ ! -e $sandbox/root/user_full_name.txt ]] || fail "absent optional files are not copied"
pass "present optional files are copied, absent ones skipped"
@@ -162,9 +162,9 @@ pass "copy failure falls back and unmounts"
new_sandbox
attach_drive cidata
write_required_pair
echo '["ssh-ed25519 AAAA jeff@host"]' >"$sandbox/media/ssh.json"
echo 'ssh-ed25519 AAAA jeff@host' >"$sandbox/media/authorized_keys"
mkdir "$sandbox/root/user_credentials.json"
! run_load 2>/dev/null || fail "partial copy exits non-zero"
[[ ! -e $sandbox/root/user_configuration.json ]] || fail "partial copy removes what it copied"
[[ ! -e $sandbox/root/ssh.json ]] || fail "partial copy leaves no ssh.json behind"
[[ ! -e $sandbox/root/authorized_keys ]] || fail "partial copy leaves no authorized_keys behind"
pass "partial copy cleans up what it copied"
+31 -35
View File
@@ -5,7 +5,6 @@ the tests run it against a temp directory with subprocess.run recorded and the
ufw side effect (writing user.rules) simulated by the fake.
"""
import json
import stat
import sys
import tempfile
@@ -56,12 +55,12 @@ class ConfigureSshAccessTest(unittest.TestCase):
return CompletedProcess(cmd, 1)
return CompletedProcess(cmd, 0)
def ctx(self, ssh_keys=None, ssh_keys_text=None):
ssh_keys_path = None
if ssh_keys is not None or ssh_keys_text is not None:
ssh_keys_path = self.target / "ssh.json"
ssh_keys_path.write_text(ssh_keys_text if ssh_keys_text is not None else json.dumps(ssh_keys))
return types.SimpleNamespace(target=self.target, username="jeff", ssh_keys_path=ssh_keys_path)
def ctx(self, authorized_keys=None):
authorized_keys_path = None
if authorized_keys is not None:
authorized_keys_path = self.target / "authorized_keys"
authorized_keys_path.write_text(authorized_keys)
return types.SimpleNamespace(target=self.target, username="jeff", authorized_keys_path=authorized_keys_path)
def configure(self, **kwargs):
phases_impl.configure_ssh_access(self.ctx(**kwargs))
@@ -72,42 +71,47 @@ class ConfigureSshAccessTest(unittest.TestCase):
def chrooted(self, program):
return [cmd for cmd in self.calls if cmd[:2] == ["arch-chroot", str(self.target)] and cmd[2] == program]
def test_no_ssh_json_is_a_no_op(self):
def test_no_authorized_keys_is_a_no_op(self):
self.configure()
self.assertEqual(self.calls, [])
self.assertFalse((self.target / "home" / "jeff" / ".ssh").exists())
def test_writes_keys_one_per_line(self):
self.configure(ssh_keys=["ssh-ed25519 AAAA jeff@host", "ssh-rsa BBBB jeff@work"])
def test_installs_keys_one_per_line(self):
self.configure(authorized_keys="ssh-ed25519 AAAA jeff@host\nssh-rsa BBBB jeff@work\n")
self.assertEqual(self.authorized_keys().read_text(), "ssh-ed25519 AAAA jeff@host\nssh-rsa BBBB jeff@work\n")
def test_strips_whitespace_and_drops_blank_entries(self):
self.configure(ssh_keys=[" ssh-ed25519 AAAA jeff@host\n", "", " "])
def test_drops_blank_lines_and_comments(self):
self.configure(authorized_keys="# work laptop\n\n ssh-ed25519 AAAA jeff@host \n")
self.assertEqual(self.authorized_keys().read_text(), "ssh-ed25519 AAAA jeff@host\n")
def test_keys_with_options_pass_through(self):
key = 'command="/usr/bin/true",no-pty ssh-ed25519 AAAA jeff@host'
self.configure(authorized_keys=f"{key}\n")
self.assertEqual(self.authorized_keys().read_text(), f"{key}\n")
def test_ssh_dir_is_private(self):
self.configure(ssh_keys=["ssh-ed25519 AAAA jeff@host"])
self.configure(authorized_keys="ssh-ed25519 AAAA jeff@host\n")
mode = stat.S_IMODE((self.target / "home" / "jeff" / ".ssh").stat().st_mode)
self.assertEqual(mode, 0o700)
def test_authorized_keys_is_private(self):
self.configure(ssh_keys=["ssh-ed25519 AAAA jeff@host"])
self.configure(authorized_keys="ssh-ed25519 AAAA jeff@host\n")
self.assertEqual(stat.S_IMODE(self.authorized_keys().stat().st_mode), 0o600)
def test_chowns_ssh_dir_to_the_user(self):
self.configure(ssh_keys=["ssh-ed25519 AAAA jeff@host"])
self.configure(authorized_keys="ssh-ed25519 AAAA jeff@host\n")
self.assertEqual(self.chrooted("chown"), [
["arch-chroot", str(self.target), "chown", "-R", "jeff:jeff", "/home/jeff/.ssh"],
])
def test_enables_sshd(self):
self.configure(ssh_keys=["ssh-ed25519 AAAA jeff@host"])
self.configure(authorized_keys="ssh-ed25519 AAAA jeff@host\n")
self.assertEqual(self.chrooted("systemctl"), [
["arch-chroot", str(self.target), "systemctl", "enable", "sshd.service"],
])
def test_allows_ssh_through_ufw_despite_chroot_exit_status(self):
self.configure(ssh_keys=["ssh-ed25519 AAAA jeff@host"])
self.configure(authorized_keys="ssh-ed25519 AAAA jeff@host\n")
self.assertEqual(self.chrooted("ufw"), [
["arch-chroot", str(self.target), "ufw", "allow", "ssh"],
])
@@ -115,27 +119,19 @@ class ConfigureSshAccessTest(unittest.TestCase):
def test_fails_when_ufw_does_not_record_the_rule(self):
self.ufw_writes_rule = False
with self.assertRaisesRegex(RuntimeError, "allow rule for port 22"):
self.configure(ssh_keys=["ssh-ed25519 AAAA jeff@host"])
self.configure(authorized_keys="ssh-ed25519 AAAA jeff@host\n")
def test_malformed_json_fails_the_phase(self):
with self.assertRaisesRegex(RuntimeError, "not readable JSON"):
self.configure(ssh_keys_text="not json")
def test_non_array_json_fails_the_phase(self):
with self.assertRaisesRegex(RuntimeError, "JSON array of public key strings"):
self.configure(ssh_keys={"key": "ssh-ed25519 AAAA"})
def test_non_string_entries_fail_the_phase(self):
with self.assertRaisesRegex(RuntimeError, "JSON array of public key strings"):
self.configure(ssh_keys=["ssh-ed25519 AAAA jeff@host", 42])
def test_empty_key_list_fails_the_phase(self):
def test_empty_file_fails_the_phase(self):
with self.assertRaisesRegex(RuntimeError, "contains no SSH keys"):
self.configure(ssh_keys=[])
self.configure(authorized_keys="")
def test_blank_only_key_list_fails_the_phase(self):
def test_blank_only_file_fails_the_phase(self):
with self.assertRaisesRegex(RuntimeError, "contains no SSH keys"):
self.configure(ssh_keys=["", " "])
self.configure(authorized_keys="\n \n")
def test_comment_only_file_fails_the_phase(self):
with self.assertRaisesRegex(RuntimeError, "contains no SSH keys"):
self.configure(authorized_keys="# no keys here\n")
class OptionalPathTest(unittest.TestCase):
@@ -144,7 +140,7 @@ class OptionalPathTest(unittest.TestCase):
self.assertIsNone(_optional_path(None))
self.assertIsNone(_optional_path(""))
self.assertIsNone(_optional_path("/does/not/exist/ssh.json"))
self.assertIsNone(_optional_path("/does/not/exist/authorized_keys"))
def test_existing_path_is_returned(self):
from orchestrator.context import _optional_path