Files

111 lines
7.1 KiB
Python

#!/usr/bin/env python3
"""Review-only three-service manifests, never a catalog/installer/data migrator.
JSON is a YAML subset. Output contains public keys only. No engine, credential,
service, ownership or live-data operations are performed by this generator.
"""
import argparse
import json
from pathlib import Path
import re
def image(value):
if not re.fullmatch(r'(?:sha256:)?[a-f0-9]{64}', value):
raise ValueError('Full immutable local image ID required')
return 'sha256:' + value.removeprefix('sha256:')
def manifests(app_image, mtx_image, blossom_image, server_pubkey, data_root, large_upload_candidate=False):
if not re.fullmatch(r'[a-f0-9]{64}', server_pubkey):
raise ValueError('Explicit public service key required; no secret input or open-upload fallback')
if not re.fullmatch(r'/var/lib/archipelago/fixture-podsteadr-[A-Za-z0-9_-]+', data_root):
raise ValueError('Only a fixture-prefixed managed root inside a disposable VM is permitted')
# The caller must verify the immutable MediaMTX wrapper image has USER1000.
# data_uid is bind provisioning, NOT a runtime user override.
def base(app_id, title, version, immutable, alias, memory, disk):
return {'id': app_id, 'name': title, 'version': version,
'description': 'Fixture-only integration review candidate; not an approved managed installation.',
'category': 'media', 'container_name': alias,
'container': {'image': image(immutable), 'pull_policy': 'never',
'network': 'podsteadr-fixture-net', 'network_aliases': [alias],
'data_uid': '1000:1000'},
'dependencies': [{'storage': disk}],
'resources': {'cpu_limit': 1, 'memory_limit': memory, 'disk_limit': disk},
'security': {'capabilities': [], 'readonly_root': True, 'no_new_privileges': True,
'network_policy': 'isolated'},
'volumes': [{'type': 'tmpfs', 'target': '/tmp', 'options': ['rw','nosuid','nodev','size=64m']}],
'environment': [],
'metadata': {'author': 'Archipelago', 'license': 'MIT', 'tags': ['fixture-only']}}
def bind(app, source, target, writable=False):
app['volumes'].append({'type':'bind', 'source':data_root+'/'+source,
'target':target, 'options':['rw' if writable else 'ro']})
def port(host, container, protocol='tcp'):
return {'host':host, 'container':container, 'protocol':protocol, 'bind':'127.0.0.1',
'auth':'none', 'auth_rationale':'Disposable loopback-only fixture; application protocol authentication remains mandatory. Not approved public exposure.'}
def health(endpoint, path):
return {'type':'http','endpoint':endpoint,'path':path,'interval':'30s','timeout':'5s','retries':3}
app = base('podsteadr', 'Podsteadr fixture', '1.0.0-fixture.1', app_image, 'podsteadr-app', '512Mi', '2Gi')
app['container']['derived_env'] = [{'key':'PODSTEADR_ADMIN_PUBKEYS','template':'{{NODE_IDENTITY_PUBKEYS}}'}]
app['dependencies'] += [{'app_id':'podsteadr-mediamtx'},{'app_id':'podsteadr-blossom'}]
bind(app,'app','/data',True)
bind(app,'recordings','/recordings')
app['ports'] = [port(8095,8095)]
app['environment'] = ['NODE_ENV=production','PORT=8095','DATA_DIR=/data','RECORDINGS_DIR=/recordings',
'PUBLIC_URL=http://127.0.0.1:8095','MEDIAMTX_API_URL=http://mediamtx:9997',
'MEDIAMTX_RTMP_PUBLIC=rtmp://127.0.0.1:1935',
'MEDIAMTX_WHIP_PUBLIC=http://127.0.0.1:8889',
'MEDIAMTX_HLS_PUBLIC=http://127.0.0.1:8890',
'BLOSSOM_URL_DEFAULT=http://127.0.0.1:8098','BLOSSOM_URL_INTERNAL=http://blossom:3000',
'NOSTR_RELAYS=','CASHU_MINT_URL_DEFAULT=http://127.0.0.1:9996']
app['health_check'] = health('http://127.0.0.1:8095','/api/health')
app['interfaces'] = {'main':{'name':'Fixture UI','type':'ui','port':8095,'protocol':'http','path':'/'}}
mtx = base('podsteadr-mediamtx','MediaMTX fixture','1.19.2-fixture.1',mtx_image,'mediamtx','512Mi','10Gi')
bind(mtx,'recordings','/recordings',True)
bind(mtx,'mediamtx.yml','/mediamtx.yml')
config = Path(__file__).with_name('mediamtx-fixture.yml').read_text()
config = config.replace('http://127.0.0.1:8095/api/mediamtx/auth','http://podsteadr-app:8095/api/mediamtx/auth')
config = '# Fixture network aliases; not an installed first-start contract.\n' + '\n'.join(line for line in config.splitlines() if not line.startswith('#')) + '\n'
mtx['files'] = [{'path':data_root+'/mediamtx.yml','overwrite':False,'content':config}]
mtx['ports'] = [port(1935,1935),port(8889,8889),port(8189,8189,'udp'),port(8890,8888)]
mtx['health_check'] = {'type':'tcp','endpoint':'localhost:1935','interval':'30s','timeout':'5s','retries':3}
blossom = base('podsteadr-blossom','Blossom fixture','6.4.1-archy.4',blossom_image,'blossom','512Mi','20Gi')
if large_upload_candidate:
# Caller must supply the qualified opt-in image, not the older package.
# This is fixture configuration, not a released version or activation.
blossom['version'] = '6.4.1-fixture.large1'
blossom['environment'] = ['ARCHY_BLOSSOM_MAX_UPLOAD_BYTES=2147483648']
# Explicit public service key bound to pre-existing synthetic app data. Never
# read/export the secret key, and never mistake native admins for this signer.
blossom['container']['derived_env'] = [{'key':'ARCHY_BLOSSOM_PUBKEYS',
'template':'{{NODE_IDENTITY_PUBKEYS}},'+server_pubkey}]
bind(blossom,'blossom/data','/data',True)
bind(blossom,'blossom/config.json','/config/config.json')
bind(blossom,'blossom/bridge','/bridge')
blossom['files'] = [{'path':data_root+'/blossom/config.json','overwrite':False,'content':'{}'}]
blossom['ports'] = [port(8098,3000)]
blossom['health_check'] = health('http://127.0.0.1:3000','/healthz')
return {entry['id']:{'app':entry} for entry in (app,mtx,blossom)}
def main():
parser = argparse.ArgumentParser(description=__doc__)
for name in ('app-image','mtx-image','blossom-image','server-pubkey','data-root','output'):
parser.add_argument('--'+name,required=True)
parser.add_argument('--large-upload-candidate',action='store_true',help='Explicitly configure qualified 2GiB fixture image; does not enable this on general Blossom')
args = parser.parse_args()
rendered = manifests(args.app_image,args.mtx_image,args.blossom_image,args.server_pubkey,args.data_root,args.large_upload_candidate)
output = Path(args.output)
if output.is_symlink() or not output.is_dir() or any(output.iterdir()):
raise ValueError('Output must be an existing empty non-symlink directory')
for app_id, value in rendered.items():
app_directory = output / app_id
app_directory.mkdir()
with (app_directory/'manifest.yml').open('x') as target:
json.dump(value,target,indent=2)
target.write('\n')
if __name__ == '__main__':
main()