Add isolated Debian installer with credential setup and guarded updates

This commit is contained in:
Mikei386
2026-09-28 15:35:17 +02:00
parent e1a54e3485
commit 2ad844ed20
12 changed files with 618 additions and 8 deletions
+62
View File
@@ -0,0 +1,62 @@
"""Explicit Linux/Docker smoke test: disposable container, no published ports."""
import json
import os
from pathlib import Path
import secrets
import subprocess
import tempfile
import time
NAME='athena-deck-installer-smoke'
IMAGE='athena-deck-installer-test:local'
ROOT=Path(__file__).resolve().parent.parent
def run(*args,data=None,check=True):
result=subprocess.run(args,input=data,text=True,capture_output=True,timeout=120)
if check and result.returncode:raise RuntimeError('Smoke command failed (output withheld): '+args[0])
return result.stdout.strip()
def main():
if run('docker','ps','-aq','--filter','name=^/'+NAME+'$'):
raise RuntimeError('Smoke container already exists; refusing takeover')
ids=run('docker','ps','-aq').splitlines()
before=run('docker','inspect','--format','{{.Id}} {{.State.StartedAt}}',*ids) if ids else ''
with tempfile.TemporaryDirectory(prefix='deck-install-smoke-') as directory:
base=Path(directory)
for name in ('state','bootstrap'):
(base/name).mkdir(mode=0o700);os.chown(base/name,65534,65534)
password=secrets.token_urlsafe(32)
script="""import json,sys,runpy
from unittest.mock import patch
p=json.load(sys.stdin)['password'];values=iter([p,p,''])
with patch('getpass.getpass',side_effect=lambda _:next(values)),patch('sys.stdin.isatty',return_value=True):runpy.run_path('/provision.py',run_name='__main__')
"""
mounts=['-v',str(base/'state')+':/var/lib/deck','-v',str(base/'bootstrap')+':/bootstrap','-v',str(ROOT/'deploy/provision.py')+':/provision.py:ro']
run('docker','run','--rm','-i','--network','none','--user','65534:65534','--cap-drop','ALL','--read-only','-e','PYTHONPATH=/app',*mounts,IMAGE,'python3','-c',script,data=json.dumps({'password':password}))
token=(base/'bootstrap/api-token.txt').read_text().strip()
assert (base/'bootstrap/api-token.txt').stat().st_mode&0o777==0o600
assert token not in (base/'state/auth.json').read_text()
print('PASS interactive provisioner with synthetic inputs; token protected, hashes persisted',flush=True)
try:
run('docker','run','-d','--name',NAME,'--network','none','--read-only','--cap-drop','ALL','--security-opt','no-new-privileges:true','--tmpfs','/tmp:rw,nosuid,nodev,size=8m','-v',str(base/'state')+':/var/lib/deck',IMAGE)
probe="""import json,sys,urllib.request
v=json.load(sys.stdin)
req=urllib.request.Request('http://127.0.0.1:8108/api/v1/status',headers={'Authorization':'Bearer '+v['token']})
with urllib.request.urlopen(req,timeout=3) as r:assert json.load(r)['location']=='Server'
"""
for _ in range(20):
try:run('docker','exec','-i',NAME,'python3','-c',probe,data=json.dumps({'token':token}));break
except RuntimeError:time.sleep(.5)
else:raise RuntimeError('Server not ready')
run('docker','stop',NAME);run('docker','start',NAME)
for _ in range(20):
try:run('docker','exec','-i',NAME,'python3','-c',probe,data=json.dumps({'token':token}));break
except RuntimeError:time.sleep(.5)
else:raise RuntimeError('Server not ready after restart')
print('PASS unprivileged standalone server, API authentication and persistence after restart',flush=True)
after=run('docker','inspect','--format','{{.Id}} {{.State.StartedAt}}',*ids) if ids else ''
assert before==after
print('PASS previously existing container start times unchanged',flush=True)
finally:run('docker','rm','-f',NAME,check=False)
if __name__=='__main__':main()