Add managed LadyPoly WebUI using Deck music API
This commit is contained in:
+15
-7
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user