Add managed LadyPoly WebUI using Deck music API

This commit is contained in:
Mikei386
2026-09-30 22:32:17 +02:00
parent 3aa2301a53
commit cf3e354434
13 changed files with 218 additions and 10 deletions
+15 -7
View File
@@ -20,6 +20,8 @@ def config_files(source,destination):
p=Path(source)
if destination.startswith('/run/secrets/'):
return {'file':blob(p)} if p.exists() else {}
if destination=='/data' and p.is_dir():
return {name:blob(p/name) for name in ('settings.json','deck-connection.json') if (p/name).is_file()}
if destination.endswith('/Data') and p.is_dir():
files={}
for name in ('Settings.fds','Backends.fds'):
@@ -43,7 +45,7 @@ def export(manager):
for row in manager.inventory():
x=json.loads(command(['inspect',row['id']]))[0];c=x['Config'];h=x['HostConfig'];image=json.loads(command(['image','inspect',x['Image']]))[0]
registry=next((d for d in image.get('RepoDigests',[]) if '/' in d.split('@')[0] and ('.' in d.split('/')[0] or ':' in d.split('/')[0])),None)
if not registry and row['name']!='athena-swarm-ui':registry=next(iter(image.get('RepoDigests',[])),None)
if not registry and row['name'] not in ('athena-swarm-ui','athena-ladypoly'):registry=next(iter(image.get('RepoDigests',[])),None)
mounts=[]
for i,m in enumerate(x['Mounts']):
source=m['Source'];relative=None
@@ -51,7 +53,7 @@ def export(manager):
try:relative=str(Path(source).relative_to(deck))
except ValueError:pass
mounts.append(dict(index=i,target=m['Destination'],read_only=not m['RW'],deck_path=relative,files=config_files(source,m['Destination']),was_file=Path(source).is_file()))
result.append(dict(name=row['name'],image=c['Image'],registry=registry,application=c.get('Labels',{}).get(APP,''),labels={k:v for k,v in (c.get('Labels') or {}).items() if k.startswith('io.athena-deck.')},env=c.get('Env') or [],entrypoint=c.get('Entrypoint'),cmd=c.get('Cmd'),uid=app_uid(row['id'],c.get('User') or ''),user=c.get('User') or '',workdir=c.get('WorkingDir') or '',network=h['NetworkMode'],ports=h.get('PortBindings') or {},restart=h.get('RestartPolicy',{}).get('Name') or 'no',memory=h.get('Memory') or 0,nanocpus=h.get('NanoCpus') or 0,running=x['State']['Running'],mounts=mounts,build_recipe='swarm-ui' if row['name']=='athena-swarm-ui' else None,unsupported=bool(h.get('Privileged') or h.get('DeviceRequests') or h.get('Devices') or h.get('CapAdd'))))
result.append(dict(name=row['name'],image=c['Image'],registry=registry,application=c.get('Labels',{}).get(APP,''),labels={k:v for k,v in (c.get('Labels') or {}).items() if k.startswith('io.athena-deck.')},env=c.get('Env') or [],entrypoint=c.get('Entrypoint'),cmd=c.get('Cmd'),uid=app_uid(row['id'],c.get('User') or ''),user=c.get('User') or '',workdir=c.get('WorkingDir') or '',network=h['NetworkMode'],ports=h.get('PortBindings') or {},restart=h.get('RestartPolicy',{}).get('Name') or 'no',memory=h.get('Memory') or 0,nanocpus=h.get('NanoCpus') or 0,running=x['State']['Running'],mounts=mounts,build_recipe='swarm-ui' if row['name']=='athena-swarm-ui' else 'ladypoly' if row['name']=='athena-ladypoly' else None,unsupported=bool(h.get('Privileged') or h.get('DeviceRequests') or h.get('Devices') or h.get('CapAdd'))))
return {'services':result,'configured':bool(deck)}
def validate(c):
@@ -59,8 +61,9 @@ def validate(c):
if any(c.get('labels',{}).get(k)!=v for k,v in LABELS.items()):raise ValueError('Nur ausdrücklich Deck zugeordnete Anwendungscontainer erlaubt.')
if c.get('unsupported'):raise ValueError('GPU-/privilegierter Container benötigt manuelle Einrichtung.')
if c.get('network') not in ('bridge','default','host'):raise ValueError('Benutzerdefiniertes Docker-Netzwerk benötigt manuelle Einrichtung.')
if c.get('network')=='host' and c['name'] not in ('athena-swarm-ui','ltx-deskweb'):raise ValueError('Host-Netzwerk nur für bekannte Deck-Oberflächen erlaubt.')
if c.get('build_recipe') not in (None,'swarm-ui') or c.get('build_recipe')=='swarm-ui' and (c['name']!='athena-swarm-ui' or c.get('image')!='athena-swarm-ui:de7b834'):raise ValueError('Unbekanntes Build-Rezept.')
if c.get('network')=='host' and c['name'] not in ('athena-swarm-ui','ltx-deskweb','athena-ladypoly'):raise ValueError('Host-Netzwerk nur für bekannte Deck-Oberflächen erlaubt.')
recipes={'swarm-ui':('athena-swarm-ui','athena-swarm-ui:de7b834'),'ladypoly':('athena-ladypoly','athena-ladypoly:3960afc-deck1')}
if c.get('build_recipe') is not None and recipes.get(c['build_recipe'])!=(c['name'],c.get('image')):raise ValueError('Unbekanntes Build-Rezept.')
ref=c.get('registry') or c.get('image','')
if not isinstance(ref,str) or not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._/@:-]{0,300}',ref):raise ValueError('Ungültige Image-Referenz.')
if c.get('restart') not in ('no','always','unless-stopped','on-failure'):raise ValueError('Ungültige Neustartregel.')
@@ -93,7 +96,9 @@ def validate(c):
native_media=rel in media and c['name']=='ltx-deskweb' and (target,m['read_only'])==media[rel] and not m['was_file']
if rel is not None and not native_media and (rel not in ('models','video/comfy-work/models','video/comfy-client-token') or not m['read_only']):raise ValueError('Deck-Volume nicht freigegeben; nur bekannte Modell-/Tokenpfade und LTX-Medienordner erlaubt.')
for name,data in m.get('files',{}).items():
if name not in ('file','Settings.fds','Backends.fds'):raise ValueError('Unbekannte Konfigurationsdatei.')
allowed=('file','Settings.fds','Backends.fds')
if c['name']=='athena-ladypoly' and m['target']=='/data':allowed+=('settings.json','deck-connection.json')
if name not in allowed:raise ValueError('Unbekannte Konfigurationsdatei.')
if not isinstance(data,str) or len(base64.b64decode(data,validate=True))>MAX_FILE:raise ValueError('Ungültige Konfigurationsdatei.')
return c
@@ -110,8 +115,10 @@ def restore(manager,c):
# Never replace or restart an existing service during a restore.
if x['Config']['Image'] not in (c['image'],c.get('registry')):raise ValueError('Vorhandener Container verwendet ein anderes Image; manuell prüfen.')
return {'state':'reused','message':'Vorhandener markierter Container unverändert übernommen.'}
if c.get('build_recipe')=='swarm-ui':
source=Path(__file__).parent/'swarm-ui'
if c['name']=='athena-ladypoly':
with socket.socket() as probe:probe.bind(('127.0.0.1',8127))
if c.get('build_recipe') in ('swarm-ui','ladypoly'):
source=Path(__file__).parent/c['build_recipe']
if not (source/'Dockerfile').is_file():raise ValueError('Gepinntes Swarm-Build-Rezept fehlt im Systemhelfer.')
command(['build','-t',c['image'],str(source)],1800)
else:
@@ -135,6 +142,7 @@ def restore(manager,c):
if base.is_symlink():raise ValueError('Unsicheres Dienstverzeichnis.')
base.mkdir(parents=True,exist_ok=True,mode=0o700)
args=['create','--name',name,'--network',c['network'],'--restart',c['restart'],'--cap-drop','ALL','--security-opt','no-new-privileges:true','--pids-limit','256']
if c['name']=='athena-ladypoly':args+=['--read-only','--tmpfs','/tmp:rw,noexec,nosuid,size=128m,uid=65534,gid=65534']
if c['memory']:args+=['--memory',str(c['memory'])]
if c['nanocpus']:args+=['--cpus',str(c['nanocpus']/1e9)]
for k,v in c['labels'].items():