@@ -47,9 +47,17 @@ type LxcNetwork = {
4747 addresses ?: LxcAddress [ ] ;
4848} ;
4949
50- type LxcInstance = {
50+ type LxdDevice = {
51+ type ?: string ;
52+ listen ?: string ;
53+ connect ?: string ;
54+ } ;
55+
56+ export type LxcInstance = {
5157 name ?: string ;
5258 config ?: Record < string , string > ;
59+ devices ?: Record < string , LxdDevice > ;
60+ expanded_devices ?: Record < string , LxdDevice > ;
5361 state ?: {
5462 network ?: Record < string , LxcNetwork > ;
5563 } ;
@@ -84,18 +92,25 @@ type TransportProxyItem = { kind: "tcp" | "udp"; hostPort: number; containerPort
8492
8593type ProxyBackendProtocol = "tcp" | "udp" ;
8694
87- type ProxyBackendSpec = {
95+ export type ProxyBackendSpec = {
8896 key : string ;
8997 containerName : string ;
9098 proto : ProxyBackendProtocol ;
9199 targetPort : number ;
92100 deviceName : string ;
93101} ;
94102
95- type ProxyBackendStateEntry = ProxyBackendSpec & {
103+ export type ProxyBackendStateEntry = ProxyBackendSpec & {
96104 hostPort : number ;
97105} ;
98106
107+ export type ExistingProxyBackendDevice = {
108+ containerName : string ;
109+ deviceName : string ;
110+ listen ?: string ;
111+ hostPort ?: number ;
112+ } ;
113+
99114export type ProxyBackendTarget = {
100115 address : string ;
101116 port : number ;
@@ -797,7 +812,7 @@ function proxyBackendDeviceName(key: string, proto: ProxyBackendProtocol, target
797812 return `${ PROXY_BACKEND_DEVICE_PREFIX } -${ proto } -${ targetPort } -${ hash } ` ;
798813}
799814
800- function collectDesiredProxyBackendSpecs ( containers : LxcInstance [ ] ) : ProxyBackendSpec [ ] {
815+ export function collectDesiredProxyBackendSpecs ( containers : LxcInstance [ ] ) : ProxyBackendSpec [ ] {
801816 const specs = new Map < string , ProxyBackendSpec > ( ) ;
802817
803818 for ( const container of containersWithProxyLabels ( containers ) ) {
@@ -829,6 +844,166 @@ function collectDesiredProxyBackendSpecs(containers: LxcInstance[]): ProxyBacken
829844 return [ ...specs . values ( ) ] . sort ( ( left , right ) => left . key . localeCompare ( right . key ) ) ;
830845}
831846
847+ function validProxyBackendHostPort ( port : number | undefined ) : port is number {
848+ return (
849+ typeof port === "number" &&
850+ Number . isInteger ( port ) &&
851+ port >= PROXY_BACKEND_BASE_PORT &&
852+ port <= PROXY_BACKEND_MAX_PORT
853+ ) ;
854+ }
855+
856+ export function parseProxyListenPort ( listen : unknown ) : number | null {
857+ if ( typeof listen !== "string" ) {
858+ return null ;
859+ }
860+
861+ const match = listen . match ( / ^ (?: t c p | u d p ) : .+ : ( \d + ) $ / ) ;
862+ if ( ! match ) {
863+ return null ;
864+ }
865+
866+ const port = Number ( match [ 1 ] ) ;
867+ return Number . isInteger ( port ) && port >= 1 && port <= 65535 ? port : null ;
868+ }
869+
870+ export function collectExistingProxyBackendDevices ( containers : LxcInstance [ ] ) : ExistingProxyBackendDevice [ ] {
871+ const devices = new Map < string , ExistingProxyBackendDevice > ( ) ;
872+
873+ for ( const container of containers ) {
874+ if ( ! container . name ) {
875+ continue ;
876+ }
877+
878+ for ( const deviceMap of [ container . devices , container . expanded_devices ] ) {
879+ if ( ! deviceMap ) {
880+ continue ;
881+ }
882+
883+ for ( const [ deviceName , device ] of Object . entries ( deviceMap ) ) {
884+ if ( ! deviceName . startsWith ( `${ PROXY_BACKEND_DEVICE_PREFIX } -` ) || device ?. type !== "proxy" ) {
885+ continue ;
886+ }
887+
888+ const listenPort = parseProxyListenPort ( device . listen ) ;
889+ devices . set ( `${ container . name } \0${ deviceName } ` , {
890+ containerName : container . name ,
891+ deviceName,
892+ listen : device . listen ,
893+ ...( listenPort === null ? { } : { hostPort : listenPort } )
894+ } ) ;
895+ }
896+ }
897+ }
898+
899+ return [ ...devices . values ( ) ] . sort (
900+ ( left , right ) => left . containerName . localeCompare ( right . containerName ) || left . deviceName . localeCompare ( right . deviceName )
901+ ) ;
902+ }
903+
904+ function proxyBackendDeviceNamesByContainer ( specs : ProxyBackendSpec [ ] ) : Map < string , Set < string > > {
905+ const names = new Map < string , Set < string > > ( ) ;
906+
907+ for ( const spec of specs ) {
908+ let containerNames = names . get ( spec . containerName ) ;
909+ if ( ! containerNames ) {
910+ containerNames = new Set < string > ( ) ;
911+ names . set ( spec . containerName , containerNames ) ;
912+ }
913+ containerNames . add ( spec . deviceName ) ;
914+ }
915+
916+ return names ;
917+ }
918+
919+ export function findStaleExistingProxyBackendDevices (
920+ existingDevices : ExistingProxyBackendDevice [ ] ,
921+ specs : ProxyBackendSpec [ ]
922+ ) : ExistingProxyBackendDevice [ ] {
923+ const desiredNamesByContainer = proxyBackendDeviceNamesByContainer ( specs ) ;
924+ return existingDevices . filter ( ( device ) => {
925+ const desiredNames = desiredNamesByContainer . get ( device . containerName ) ;
926+ return desiredNames !== undefined && ! desiredNames . has ( device . deviceName ) ;
927+ } ) ;
928+ }
929+
930+ function proxyBackendDeviceIdentity ( device : { containerName : string ; deviceName : string } ) : string {
931+ return `${ device . containerName } \0${ device . deviceName } ` ;
932+ }
933+
934+ export function planProxyBackendEntries (
935+ specs : ProxyBackendSpec [ ] ,
936+ previous : ProxyBackendStateEntry [ ] ,
937+ keptExistingDevices : ExistingProxyBackendDevice [ ]
938+ ) : { entries : ProxyBackendStateEntry [ ] ; errors : string [ ] } {
939+ const previousByKey = new Map < string , ProxyBackendStateEntry > ( ) ;
940+ const existingByDesiredKey = new Map < string , ExistingProxyBackendDevice > ( ) ;
941+ const desiredKeyByDevice = new Map ( specs . map ( ( spec ) => [ proxyBackendDeviceIdentity ( spec ) , spec . key ] ) ) ;
942+ const keptDevicesByPort = new Map < number , ExistingProxyBackendDevice [ ] > ( ) ;
943+ const usedPorts = new Set < number > ( ) ;
944+
945+ for ( const entry of previous ) {
946+ if ( ! previousByKey . has ( entry . key ) ) {
947+ previousByKey . set ( entry . key , entry ) ;
948+ }
949+ if ( validProxyBackendHostPort ( entry . hostPort ) ) {
950+ usedPorts . add ( entry . hostPort ) ;
951+ }
952+ }
953+
954+ for ( const device of keptExistingDevices ) {
955+ if ( validProxyBackendHostPort ( device . hostPort ) ) {
956+ usedPorts . add ( device . hostPort ) ;
957+ const devices = keptDevicesByPort . get ( device . hostPort ) ?? [ ] ;
958+ devices . push ( device ) ;
959+ keptDevicesByPort . set ( device . hostPort , devices ) ;
960+ }
961+
962+ const desiredKey = desiredKeyByDevice . get ( proxyBackendDeviceIdentity ( device ) ) ;
963+ if ( desiredKey && ! existingByDesiredKey . has ( desiredKey ) ) {
964+ existingByDesiredKey . set ( desiredKey , device ) ;
965+ }
966+ }
967+
968+ const entries : ProxyBackendStateEntry [ ] = [ ] ;
969+ const errors : string [ ] = [ ] ;
970+
971+ const portAvailableForSpec = ( port : number , spec : ProxyBackendSpec ) : boolean => {
972+ const devices = keptDevicesByPort . get ( port ) ?? [ ] ;
973+ return devices . every ( ( device ) => device . containerName === spec . containerName && device . deviceName === spec . deviceName ) ;
974+ } ;
975+
976+ for ( const spec of specs ) {
977+ const previousEntry = previousByKey . get ( spec . key ) ;
978+ const existingDevice = existingByDesiredKey . get ( spec . key ) ;
979+ let hostPort =
980+ previousEntry ?. hostPort &&
981+ validProxyBackendHostPort ( previousEntry . hostPort ) &&
982+ portAvailableForSpec ( previousEntry . hostPort , spec ) &&
983+ ! entries . some ( ( entry ) => entry . hostPort === previousEntry . hostPort )
984+ ? previousEntry . hostPort
985+ : undefined ;
986+
987+ if ( ! hostPort && validProxyBackendHostPort ( existingDevice ?. hostPort ) && ! entries . some ( ( entry ) => entry . hostPort === existingDevice . hostPort ) ) {
988+ hostPort = existingDevice . hostPort ;
989+ }
990+
991+ if ( ! hostPort ) {
992+ try {
993+ hostPort = allocateProxyBackendPort ( usedPorts ) ;
994+ } catch ( error ) {
995+ errors . push ( `${ spec . containerName } : ${ String ( error ) . replace ( / ^ E r r o r : / , "" ) } ` ) ;
996+ continue ;
997+ }
998+ }
999+
1000+ usedPorts . add ( hostPort ) ;
1001+ entries . push ( { ...spec , hostPort } ) ;
1002+ }
1003+
1004+ return { entries, errors } ;
1005+ }
1006+
8321007function isProxyBackendStateEntry ( value : unknown ) : value is ProxyBackendStateEntry {
8331008 if ( ! value || typeof value !== "object" ) {
8341009 return false ;
@@ -879,7 +1054,7 @@ async function readLxdProxyDeviceValue(containerName: string, deviceName: string
8791054 return result . stdout . trim ( ) ;
8801055}
8811056
882- async function removeLxdProxyDevice ( entry : ProxyBackendStateEntry ) : Promise < string | null > {
1057+ async function removeLxdProxyDevice ( entry : { containerName : string ; deviceName : string } ) : Promise < string | null > {
8831058 const result = await runAllowFailure ( [
8841059 "timeout" ,
8851060 "30s" ,
@@ -940,6 +1115,19 @@ async function syncLxdProxyBackends(containers: LxcInstance[]): Promise<{ target
9401115 const previousByKey = new Map < string , ProxyBackendStateEntry > ( ) ;
9411116 const errors : string [ ] = [ ] ;
9421117
1118+ const existingDevices = collectExistingProxyBackendDevices ( containers ) ;
1119+ const staleExistingDevices = findStaleExistingProxyBackendDevices ( existingDevices , specs ) ;
1120+ const staleDeviceIdentities = new Set ( staleExistingDevices . map ( proxyBackendDeviceIdentity ) ) ;
1121+ const failedStaleDeviceRemovals = new Set < string > ( ) ;
1122+
1123+ for ( const device of staleExistingDevices ) {
1124+ const removeError = await removeLxdProxyDevice ( device ) ;
1125+ if ( removeError ) {
1126+ errors . push ( removeError ) ;
1127+ failedStaleDeviceRemovals . add ( proxyBackendDeviceIdentity ( device ) ) ;
1128+ }
1129+ }
1130+
9431131 for ( const entry of previous ) {
9441132 if ( ! previousByKey . has ( entry . key ) ) {
9451133 previousByKey . set ( entry . key , entry ) ;
@@ -952,41 +1140,26 @@ async function syncLxdProxyBackends(containers: LxcInstance[]): Promise<{ target
9521140 }
9531141 }
9541142
955- const usedPorts = new Set (
956- previous
957- . map ( ( entry ) => entry . hostPort )
958- . filter ( ( port ) => Number . isInteger ( port ) && port >= PROXY_BACKEND_BASE_PORT && port <= PROXY_BACKEND_MAX_PORT )
959- ) ;
1143+ const keptExistingDevices = existingDevices . filter ( ( device ) => {
1144+ const identity = proxyBackendDeviceIdentity ( device ) ;
1145+ return ! staleDeviceIdentities . has ( identity ) || failedStaleDeviceRemovals . has ( identity ) ;
1146+ } ) ;
1147+ const { entries : plannedEntries , errors : allocationErrors } = planProxyBackendEntries ( specs , previous , keptExistingDevices ) ;
1148+ errors . push ( ...allocationErrors ) ;
1149+
9601150 const next : ProxyBackendStateEntry [ ] = [ ] ;
9611151
962- for ( const spec of specs ) {
963- const previousEntry = previousByKey . get ( spec . key ) ;
964- let hostPort =
965- previousEntry ?. hostPort &&
966- previousEntry . hostPort >= PROXY_BACKEND_BASE_PORT &&
967- previousEntry . hostPort <= PROXY_BACKEND_MAX_PORT &&
968- ! next . some ( ( entry ) => entry . hostPort === previousEntry . hostPort )
969- ? previousEntry . hostPort
970- : undefined ;
971- if ( ! hostPort ) {
972- try {
973- hostPort = allocateProxyBackendPort ( usedPorts ) ;
974- } catch ( error ) {
975- errors . push ( `${ spec . containerName } : ${ String ( error ) . replace ( / ^ E r r o r : / , "" ) } ` ) ;
976- continue ;
977- }
978- }
979- usedPorts . add ( hostPort ) ;
1152+ for ( const entry of plannedEntries ) {
1153+ const previousEntry = previousByKey . get ( entry . key ) ;
9801154
981- if ( previousEntry && previousEntry . deviceName !== spec . deviceName ) {
1155+ if ( previousEntry && previousEntry . deviceName !== entry . deviceName ) {
9821156 const removeError = await removeLxdProxyDevice ( previousEntry ) ;
9831157 if ( removeError ) {
9841158 errors . push ( removeError ) ;
9851159 continue ;
9861160 }
9871161 }
9881162
989- const entry : ProxyBackendStateEntry = { ...spec , hostPort } ;
9901163 const deviceError = await ensureLxdProxyDevice ( entry ) ;
9911164 if ( deviceError ) {
9921165 errors . push ( deviceError ) ;
0 commit comments