Skip to content

Commit dbc5917

Browse files
committed
Added missing JWT keys (needed for upcoming auth upgrade) and better handling of setup content writing.
Signed-off-by: Cédric Foellmi <cedric@onekiloparsec.dev>
1 parent d8d80ed commit dbc5917

2 files changed

Lines changed: 149 additions & 17 deletions

File tree

arcsecond/hosting/local.py

Lines changed: 82 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,25 +31,74 @@ def prompt_shared_data_path() -> str:
3131
return expand_path(chosen)
3232

3333

34+
def _required_env_values():
35+
return {
36+
"SECRET_KEY": _get_random_secret_key(),
37+
"AUTH_JWT_SIGNING_KEY": _get_random_secret_key(),
38+
"AGENT_JWT_SIGNING_KEY": _get_random_secret_key(),
39+
"FIELD_ENCRYPTION_KEY": _get_encryption_key(),
40+
"SHARED_DATA_PATH": prompt_shared_data_path(),
41+
"POSTGRES_USER": POSTGRES_USER,
42+
"POSTGRES_PASSWORD": POSTGRES_PASSWORD,
43+
"POSTGRES_DB": POSTGRES_DB,
44+
}
45+
46+
47+
def _parse_env_keys(lines):
48+
keys = set()
49+
for line in lines:
50+
stripped = line.strip()
51+
if not stripped or stripped.startswith("#") or "=" not in stripped:
52+
continue
53+
key, _ = stripped.split("=", 1)
54+
keys.add(key.strip())
55+
return keys
56+
57+
58+
def _format_env_line(key, value):
59+
if key == "SHARED_DATA_PATH":
60+
return f'{key}="{value}"'
61+
return f"{key}={value}"
62+
63+
3464
def write_env_file():
35-
secret_key = _get_random_secret_key()
36-
field_encryption_key = _get_encryption_key()
37-
shared_data_path = prompt_shared_data_path()
65+
env_path = Path.cwd() / ENV_FILENAME
66+
required_values = _required_env_values()
67+
ordered_required_keys = [
68+
"SECRET_KEY",
69+
"AUTH_JWT_SIGNING_KEY",
70+
"AGENT_JWT_SIGNING_KEY",
71+
"FIELD_ENCRYPTION_KEY",
72+
"SHARED_DATA_PATH",
73+
"POSTGRES_USER",
74+
"POSTGRES_PASSWORD",
75+
"POSTGRES_DB",
76+
]
77+
78+
if env_path.exists():
79+
existing_lines = env_path.read_text(encoding="utf-8").splitlines()
80+
existing_keys = _parse_env_keys(existing_lines)
81+
missing_keys = [key for key in ordered_required_keys if key not in existing_keys]
82+
83+
if not missing_keys:
84+
print(f"{ENV_FILENAME} already contains all required keys.")
85+
return
86+
87+
if existing_lines and existing_lines[-1].strip():
88+
existing_lines.append("")
89+
for key in missing_keys:
90+
existing_lines.append(_format_env_line(key, required_values[key]))
91+
92+
env_path.write_text("\n".join(existing_lines) + "\n", encoding="utf-8")
93+
print(
94+
f"Updated {ENV_FILENAME} at: {env_path} (added keys: {', '.join(missing_keys)})"
95+
)
96+
return
3897

3998
env_contents = "\n".join(
40-
[
41-
f"SECRET_KEY={secret_key}",
42-
f"FIELD_ENCRYPTION_KEY={field_encryption_key}",
43-
f'SHARED_DATA_PATH="{shared_data_path}"',
44-
f"POSTGRES_USER={POSTGRES_USER}",
45-
f"POSTGRES_PASSWORD={POSTGRES_PASSWORD}",
46-
f"POSTGRES_DB={POSTGRES_DB}",
47-
"",
48-
]
99+
[_format_env_line(key, required_values[key]) for key in ordered_required_keys]
49100
)
50-
51-
env_path = Path.cwd() / ENV_FILENAME
52-
env_path.write_text(env_contents, encoding="utf-8")
101+
env_path.write_text(env_contents + "\n", encoding="utf-8")
53102
print(f"Wrote {ENV_FILENAME} to: {env_path}")
54103

55104

@@ -63,9 +112,25 @@ def write_docker_compose_file() -> Path:
63112
# arcsecond/hosting/docker/docker-compose.yml
64113
compose = resources.files("arcsecond.hosting.docker").joinpath("docker-compose.yml")
65114

66-
with compose.open("rb") as src, dest.open("wb") as dst:
67-
dst.write(src.read())
115+
with compose.open("rb") as src:
116+
expected_content = src.read()
117+
118+
if not dest.exists():
119+
dest.write_bytes(expected_content)
120+
print(f"Wrote docker-compose.yml to: {dest}")
121+
return dest
68122

123+
current_content = dest.read_bytes()
124+
if current_content == expected_content:
125+
print("docker-compose.yml is already up to date.")
126+
return dest
127+
128+
latest_dest = Path.cwd() / "docker-compose.latest.yml"
129+
latest_dest.write_bytes(expected_content)
130+
print(
131+
"docker-compose.yml differs from the latest packaged version. "
132+
f"Wrote new template to: {latest_dest}"
133+
)
69134
return dest
70135

71136

tests/test_hosting_local.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from pathlib import Path
2+
3+
from arcsecond.hosting import local
4+
5+
6+
def test_write_env_file_includes_jwt_signing_keys(tmp_path, monkeypatch):
7+
monkeypatch.chdir(tmp_path)
8+
monkeypatch.setattr(local, "_get_random_secret_key", lambda: "test-secret")
9+
monkeypatch.setattr(local, "_get_encryption_key", lambda: "test-encryption")
10+
monkeypatch.setattr(local, "prompt_shared_data_path", lambda: "/tmp/shared-data")
11+
12+
local.write_env_file()
13+
14+
env_contents = (Path(tmp_path) / ".env").read_text(encoding="utf-8")
15+
assert "SECRET_KEY=test-secret" in env_contents
16+
assert "AUTH_JWT_SIGNING_KEY=test-secret" in env_contents
17+
assert "AGENT_JWT_SIGNING_KEY=test-secret" in env_contents
18+
assert "FIELD_ENCRYPTION_KEY=test-encryption" in env_contents
19+
assert 'SHARED_DATA_PATH="/tmp/shared-data"' in env_contents
20+
21+
22+
def test_write_env_file_preserves_existing_values_and_adds_missing(tmp_path, monkeypatch):
23+
monkeypatch.chdir(tmp_path)
24+
monkeypatch.setattr(local, "_get_random_secret_key", lambda: "generated-secret")
25+
monkeypatch.setattr(local, "_get_encryption_key", lambda: "generated-encryption")
26+
monkeypatch.setattr(local, "prompt_shared_data_path", lambda: "/tmp/generated-shared")
27+
28+
env_path = Path(tmp_path) / ".env"
29+
env_path.write_text(
30+
"\n".join(
31+
[
32+
"SECRET_KEY=existing-secret",
33+
"POSTGRES_USER=existing-user",
34+
"",
35+
]
36+
),
37+
encoding="utf-8",
38+
)
39+
40+
local.write_env_file()
41+
env_contents = env_path.read_text(encoding="utf-8")
42+
43+
assert "SECRET_KEY=existing-secret" in env_contents
44+
assert "POSTGRES_USER=existing-user" in env_contents
45+
assert "AUTH_JWT_SIGNING_KEY=generated-secret" in env_contents
46+
assert "AGENT_JWT_SIGNING_KEY=generated-secret" in env_contents
47+
assert "FIELD_ENCRYPTION_KEY=generated-encryption" in env_contents
48+
assert 'SHARED_DATA_PATH="/tmp/generated-shared"' in env_contents
49+
assert "POSTGRES_PASSWORD=arcsecond_docker" in env_contents
50+
assert "POSTGRES_DB=arcsecond_docker" in env_contents
51+
52+
53+
def test_write_docker_compose_file_keeps_existing_and_writes_latest_when_different(tmp_path, monkeypatch):
54+
monkeypatch.chdir(tmp_path)
55+
56+
local.write_docker_compose_file()
57+
58+
compose_path = Path(tmp_path) / "docker-compose.yml"
59+
original_generated = compose_path.read_text(encoding="utf-8")
60+
61+
compose_path.write_text("custom-compose-content\n", encoding="utf-8")
62+
local.write_docker_compose_file()
63+
64+
latest_path = Path(tmp_path) / "docker-compose.latest.yml"
65+
assert compose_path.read_text(encoding="utf-8") == "custom-compose-content\n"
66+
assert latest_path.exists()
67+
assert latest_path.read_text(encoding="utf-8") == original_generated

0 commit comments

Comments
 (0)