"""Fresh-install acceptance in a disposable Ubuntu host with a separate Docker daemon. Requires the local Docker engine. The privileged outer container is only for nested Docker; no host socket, host directory, customer VM or production data is mounted. """ import pathlib,subprocess,secrets,json,time,os,hashlib,sys ROOT=pathlib.Path(__file__).resolve().parent.parent SCRIPTS=ROOT.parent/'skeletonworks-scripts' ARTIFACTS=ROOT/'test-results/slice12';ARTIFACTS.mkdir(parents=True,exist_ok=True) name='conductor-slice12-'+secrets.token_hex(5) fqdn='conductor.customer.test' stack='/opt/skeletonworks/conductor/'+fqdn setup='/opt/skeletonworks/scripts/setup-conductor.sh' def command(args,input=None,check=True): r=subprocess.run(args,input=input,text=True,capture_output=True) if check and r.returncode:raise RuntimeError('Command failed: '+str(args[:5])+'\n'+r.stderr[-9000:]) return r def host(args,input=None,check=True):return command(['docker','exec','-i',name]+args,input,check) def write_files(files): host(['python3','-c','import sys,json,pathlib\nfor name,content in json.load(sys.stdin).items():\n p=pathlib.Path(name);p.parent.mkdir(parents=True,exist_ok=True);p.write_text(content)\n'],json.dumps(files)) def snapshot(): return json.loads(host(['python3','-c','import pathlib,hashlib,json\np=pathlib.Path("/opt/skeletonworks")\nprint(json.dumps({str(f):hashlib.sha256(f.read_bytes()).hexdigest() for f in p.rglob("*") if f.is_file()}))']).stdout) def invoke(args,check=True): result=host(['bash',setup]+args,check=check) if check:return json.loads(result.stdout) assert result.returncode!=0,'Expected setup refusal' return result.stderr def milestone(text):print(text,flush=True) try: command(['docker','run','-d','--privileged','--cgroupns=private','--name',name,'--publish','127.0.0.1::443','conductor:slice12-test-host']) for _ in range(80): if host(['docker','info'],check=False).returncode==0:break time.sleep(.25) else:raise RuntimeError('The disposable nested Docker daemon did not start.') assert host(['bash','-c','command -v node'],check=False).returncode!=0 host(['bash','-c','getent group docker >/dev/null || groupadd docker; usermod -aG docker admin; chgrp docker /var/run/docker.sock']) for script in ['setup-conductor.sh','backup-conductor.sh','restore-conductor.sh']: command(['docker','cp',str(SCRIPTS/script),name+':/opt/skeletonworks/scripts/'+script]) host(['chmod','755',setup,'/opt/skeletonworks/scripts/backup-conductor.sh','/opt/skeletonworks/scripts/restore-conductor.sh']) host(['mkdir','-p','/opt/conductor-test']) command(['docker','cp',str(ROOT/'examples/installed-app'),name+':/opt/conductor-test/example-package']) command(['docker','cp',str(ROOT/'scripts/fixtures/slice12/check-backup-rejection.py'),name+':/opt/conductor-test/check-backup-rejection.py']) caddy="""{ local_certs auto_https disable_redirects } unrelated.customer.test { respond "Neighbor preserved" } """ compose="""services: caddy: image: caddy:2.10.2 ports: ["443:443"] volumes: - ./Caddyfile:/etc/caddy/Caddyfile - ./logs:/var/log/caddy - caddy_data:/data - caddy_config:/config networks: [skeletonworks_net] volumes: caddy_data: caddy_config: networks: skeletonworks_net: {external: true} """ write_files({'/opt/skeletonworks/caddy/Caddyfile':caddy,'/opt/skeletonworks/caddy/docker-compose.yml':compose}) host(['crontab','-u','admin','-'],'0 2 * * * echo preserve-unrelated-job\n') before=snapshot() plan=invoke(['--domain','customer.test','--dry-run']) assert plan['url']=='https://'+fqdn and plan['hostNodeRequired'] is False assert plan['image'].endswith('@sha256:ab5e7dcaed065a304660af31e53ffada290b22dc9176ffeb5817f2e7f07ac313') assert snapshot()==before for domain,sub,expected in [('conductor.customer.test','','conductor.customer.test'),('customer.test','portal','portal.customer.test'),('customer.test','portal.other.test','portal.other.test')]: args=['--domain',domain,'--dry-run']+(['--subdomain',sub] if sub else []) assert invoke(args)['url']=='https://'+expected for args in [['--domain'],['--domain','../customer.test'],['--domain','https://customer.test'],['--domain','customer.test','--image','conductor:latest'],['--domain','customer.test','--cpus','0'],['--domain','customer.test','--no-start','--restartcaddy']]: invoke(args,False) nonroot=host(['runuser','-u','admin','--','bash',setup,'--domain','customer.test','--dry-run'],check=False) assert nonroot.returncode!=0 and 'sudo' in nonroot.stderr assert snapshot()==before write_files({'/opt/skeletonworks/conductor/conductor.legacy.test/.env':'preserve-legacy-configuration\n'}) before=snapshot() assert 'SLICE13' in invoke(['--domain','legacy.test'],False) assert snapshot()==before milestone('PASS: protected dry-run, FQDN variants, input rejection and unchanged legacy installation.') prepared=invoke(['--domain','customer.test','--no-start']) assert prepared['status']=='prepared' and not prepared['started'] and not prepared['cronInstalled'] protected=snapshot() assert invoke(['--domain','customer.test','--no-start'])['status']=='prepared' for file in ['.env','.secrets','docker-compose.yml','.setup-plan.json']:assert snapshot()[stack+'/'+file]==protected[stack+'/'+file] assert host(['docker','ps','-aq']).stdout.strip()=='' assert host(['docker','image','ls','-q']).stdout.strip()=='' assert host(['cat','/opt/skeletonworks/caddy/Caddyfile']).stdout==caddy for file in ['.env','.secrets','docker-compose.yml','setup-result.json']: assert host(['stat','-c','%a %U',stack+'/'+file]).stdout.strip()=='600 admin' milestone('PASS: preparation/repeat preserves private keys and makes no container, image, Caddy or cron changes.') started=invoke(['--domain','customer.test','--restartcaddy']) assert started['status']=='success' and started['started'] and started['cronInstalled'] and started['caddyApplied'] assert started['verified']['version']=='v1.1.0' and started['verified']['firstRunSetupRequired'] for file in ['.env','.secrets']:assert snapshot()[stack+'/'+file]==protected[stack+'/'+file] assert 'Neighbor preserved' in host(['cat','/opt/skeletonworks/caddy/Caddyfile']).stdout cron=host(['crontab','-u','admin','-l']).stdout assert cron.count('# skeletonworks-conductor '+fqdn)==1 and 'preserve-unrelated-job' in cron inner=started['container'] metadata=json.loads(host(['docker','inspect',inner]).stdout)[0] assert metadata['HostConfig']['ReadonlyRootfs'] and metadata['Config']['User']=='conductor' assert metadata['HostConfig']['PortBindings']=={} assert any(v['Destination']=='/data' and v['Name']==inner+'-data' for v in metadata['Mounts']) assert json.loads(host(['docker','exec',inner,'node','dist/scripts/installedApps.js','list']).stdout)==[] port=json.loads(command(['docker','inspect',name]).stdout)[0]['NetworkSettings']['Ports']['443/tcp'][0]['HostPort'] milestone('PASS: pinned public image, generated Caddy route, persistent volume, first-run readiness and preserved neighbor/cron.') result=subprocess.run(['node',str(ROOT/'scripts/test-setup-conductor.mjs'),name,port],cwd=ROOT,text=True,capture_output=True) (ARTIFACTS/'browser.log').write_text(result.stdout+'\n'+result.stderr) if result.returncode:raise RuntimeError('Browser/recovery acceptance failed; see test-results/slice12/browser.log:\n'+result.stderr[-9000:]) milestone(result.stdout.strip()) before=snapshot() assert 'SLICE13' in invoke(['--domain','customer.test'],False) assert snapshot()==before assert host(['bash','-c','command -v node'],check=False).returncode!=0 (ARTIFACTS/'result.json').write_text(json.dumps({'passed':True,'environment':'Disposable Ubuntu 24.04 with isolated Docker daemon; no host Node.js','setupChecks':['read-only dry-run','domain/FQDN variants','invalid argument refusal','sudo guidance','old installation preserved','no-start/repeat/resume','key/configuration permissions','verified release digest','healthy first-run setup','persistent app volume','Caddy validation/reload','unrelated Caddy/cron preserved','completed installation refuses fresh setup'],'browserAndRecovery':'passed','liveVmChanged':False},indent=2)+'\n') milestone('PASS: SLICE12 fresh-install acceptance complete. No customer VM was used.') finally: logs=command(['docker','logs',name],check=False) (ARTIFACTS/'test-host.log').write_text(logs.stderr[-12000:]) command(['docker','rm','-f','-v',name],check=False)