mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 18:32:13 +00:00
134 lines
8.2 KiB
Python
134 lines
8.2 KiB
Python
"""Only public loopback origins bind by exact Compose port and static target."""
|
|
import copy
|
|
from pathlib import Path
|
|
import sys
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
ROOT=Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0,str(ROOT/'src'));sys.path.insert(0,str(ROOT/'remote'))
|
|
from proxmenux_oci.custom import template_from_compose
|
|
from proxmenux_oci import stack
|
|
from proxmenux_oci.stack import assess,public_environment_bindings,StackError,build_stack,DefaultsUI
|
|
from proxmenux_oci.installer import DEFAULT_MODE
|
|
import install_generic_stack as runtime
|
|
|
|
SOURCE='''services:
|
|
web:
|
|
image: nginx:alpine
|
|
ports: ['9001:8080']
|
|
environment: {PUBLIC_URI: 'http://localhost:9001/app?x=1#view'}
|
|
depends_on: [backend, mail]
|
|
backend:
|
|
image: example/backend:latest
|
|
environment: {PUBLIC_URI: 'http://localhost:9001/app?x=1#view', EXPLICIT_URL: 'https://example.com:9443/keep'}
|
|
mail:
|
|
image: axllent/mailpit:v1
|
|
ports: ['8025:8025']
|
|
environment: {PUBLIC_URI: 'http://localhost:8025/mail'}
|
|
'''
|
|
|
|
def fixture(dynamic=False,jobs=False):
|
|
result=[]
|
|
for name,vmid,main,address in [('backend',4801,False,None),('mail',4802,False,'172.30.249.11/24'),('web',4800,True,'dhcp' if dynamic else '172.30.249.10/24')]:
|
|
target='mail' if name=='mail' else 'web';port=8025 if name=='mail' else 8080
|
|
value='http://@STACK_LAN_IP@:'+str(port)+'/app?x=1#view'
|
|
result.append({'name':name,'vmid':vmid,'main':main,'frontend':name!='backend','frontend_ipv4':address,
|
|
'healthcheck':{'type':'running'},'deployment':{'environment':[{'name':'PUBLIC_URI','value':value}]},
|
|
'public_environment_bindings':{'PUBLIC_URI':{'target_service':target,'container_port':port,'value':value}}})
|
|
if jobs:result.append({'name':'migration','vmid':4803,'main':False,'healthcheck':{'type':'completed'},'deployment':{'environment':[]}})
|
|
return result
|
|
|
|
def build(template,dynamic=False):
|
|
with patch('proxmenux_oci.installer.ask_storage',return_value='local-zfs'), \
|
|
patch('proxmenux_oci.installer.ask_bridge',return_value='vmbr0'), \
|
|
patch('proxmenux_oci.host.timezone',return_value='UTC'), \
|
|
patch('proxmenux_oci.network.ask_addresses',side_effect=lambda ui,bridge,labels: ({name:('dhcp' if dynamic else '172.30.249.11/24' if name=='mail' else '172.30.249.10/24') for name in labels},None)):
|
|
return build_stack(template,DefaultsUI(),DEFAULT_MODE)
|
|
|
|
class PublicEnvironmentTests(unittest.TestCase):
|
|
def source(self,text=SOURCE):
|
|
template=template_from_compose(text)
|
|
return template,template['compose_stack']['services']
|
|
|
|
def test_origin_port_maps_shared_url_to_web_and_mail_to_itself(self):
|
|
template,services=self.source();bindings=public_environment_bindings(services,'web')
|
|
self.assertEqual(assess(template),[])
|
|
self.assertEqual(bindings['backend']['PUBLIC_URI']['target_service'],'web')
|
|
self.assertEqual(bindings['backend']['PUBLIC_URI']['container_port'],8080)
|
|
self.assertEqual(bindings['backend']['PUBLIC_URI']['value'],'http://@STACK_LAN_IP@:8080/app?x=1#view')
|
|
self.assertEqual(bindings['mail']['PUBLIC_URI']['target_service'],'mail')
|
|
self.assertNotIn('EXPLICIT_URL',bindings['backend'])
|
|
|
|
def test_compiler_carries_targets_and_resolves_static_before_first_start(self):
|
|
template,_=self.source();plan=build(template)
|
|
services={service['name']:service for service in plan['services']}
|
|
self.assertEqual(services['backend']['public_environment_bindings']['PUBLIC_URI']['target_service'],'web')
|
|
with patch.object(runtime,'run') as run:runtime.prepare_public_environment(plan['services'])
|
|
values={item['name']:item['value'] for item in services['backend']['deployment']['environment']}
|
|
self.assertEqual(values['PUBLIC_URI'],'http://172.30.249.10:8080/app?x=1#view')
|
|
self.assertEqual(values['EXPLICIT_URL'],'https://example.com:9443/keep')
|
|
run.assert_not_called()
|
|
|
|
def test_final_environment_override_is_not_replaced_by_original_compose_binding(self):
|
|
template,_=self.source();original=stack.resolve_environments
|
|
def final_override(*args,**kwargs):
|
|
values=original(*args,**kwargs)
|
|
values['backend']['PUBLIC_URI']='https://operator.example.com/custom'
|
|
return values
|
|
with patch.object(stack,'resolve_environments',side_effect=final_override):plan=build(template)
|
|
backend=next(s for s in plan['services'] if s['name']=='backend')
|
|
self.assertNotIn('PUBLIC_URI',backend.get('public_environment_bindings',{}))
|
|
runtime.prepare_public_environment(plan['services'])
|
|
self.assertEqual(next(e['value'] for e in backend['deployment']['environment'] if e['name']=='PUBLIC_URI'),'https://operator.example.com/custom')
|
|
|
|
def test_internal_health_metrics_and_admin_urls_are_never_automatic_public_fields(self):
|
|
extra="OTEL_EXPORTER_OTLP_ENDPOINT: 'http://localhost:4318', HEALTHCHECK_URL: 'http://127.0.0.1:9090', LOCAL_ADMIN_API: 'http://localhost:9999', TEXT: 'not:a[url', "
|
|
template,services=self.source(SOURCE.replace('EXPLICIT_URL:',extra+'EXPLICIT_URL:'))
|
|
self.assertEqual(assess(template),[])
|
|
bindings=public_environment_bindings(services,'web')['backend']
|
|
for key in ['OTEL_EXPORTER_OTLP_ENDPOINT','HEALTHCHECK_URL','LOCAL_ADMIN_API','TEXT']:self.assertNotIn(key,bindings)
|
|
plan=build(template);backend=next(s for s in plan['services'] if s['name']=='backend');runtime.prepare_public_environment(plan['services'])
|
|
values={e['name']:e['value'] for e in backend['deployment']['environment']}
|
|
self.assertEqual(values['OTEL_EXPORTER_OTLP_ENDPOINT'],'http://localhost:4318')
|
|
self.assertEqual(values['HEALTHCHECK_URL'],'http://127.0.0.1:9090')
|
|
self.assertEqual(values['LOCAL_ADMIN_API'],'http://localhost:9999')
|
|
|
|
def test_ambiguous_host_port_is_blocked(self):
|
|
template,services=self.source(SOURCE.replace("['8025:8025']","['9001:8025']"))
|
|
with self.assertRaisesRegex(StackError,'Ambiguous'):public_environment_bindings(services,'web')
|
|
self.assertTrue(any('Ambiguous' in error for error in assess(template)))
|
|
|
|
def test_unmatched_public_origin_does_not_guess_even_single_frontend(self):
|
|
_,services=self.source(SOURCE.replace('localhost:9001','localhost:9999'))
|
|
with self.assertRaisesRegex(StackError,'no matching'):public_environment_bindings(services,'web')
|
|
only_web=[service for service in services if service['name']!='mail']
|
|
with self.assertRaisesRegex(StackError,'no matching'):public_environment_bindings(only_web,'web')
|
|
|
|
def test_missing_origin_port_does_not_fall_back_to_the_main(self):
|
|
_,services=self.source(SOURCE.replace('localhost:9001','localhost'))
|
|
with self.assertRaisesRegex(StackError,'no matching'):public_environment_bindings(services,'web')
|
|
|
|
def test_static_bindings_with_migration_do_not_run_any_commands(self):
|
|
with patch.object(runtime,'run') as run:runtime.prepare_public_environment(fixture(jobs=True))
|
|
run.assert_not_called()
|
|
|
|
def test_every_automatic_dhcp_binding_is_refused_before_any_command(self):
|
|
for jobs in (False,True):
|
|
with self.subTest(jobs=jobs),patch.object(runtime,'run') as run,self.assertRaisesRegex(RuntimeError,'Automatic public URLs'):
|
|
runtime.prepare_public_environment(fixture(dynamic=True,jobs=jobs))
|
|
run.assert_not_called()
|
|
template,_=self.source()
|
|
with self.assertRaisesRegex(StackError,'Automatic public URLs'):build(template,dynamic=True)
|
|
|
|
def test_explicit_public_urls_allow_dhcp_and_are_unchanged(self):
|
|
template,_=self.source(SOURCE.replace('localhost','operator.example.com'))
|
|
plan=build(template,dynamic=True)
|
|
with patch.object(runtime,'run') as run:runtime.prepare_public_environment(plan['services'])
|
|
self.assertTrue(all(not s.get('public_environment_bindings') for s in plan['services']))
|
|
backend=next(s for s in plan['services'] if s['name']=='backend')
|
|
self.assertEqual(next(e['value'] for e in backend['deployment']['environment'] if e['name']=='PUBLIC_URI'),'http://operator.example.com:9001/app?x=1#view')
|
|
run.assert_not_called()
|
|
|
|
if __name__=='__main__':unittest.main()
|