@@ -526,3 +526,157 @@ func TestComputeIPCandidates(t *testing.T) {
526526 }
527527 })
528528}
529+
530+ // candidateForOverlayIndex mirrors the indexing logic in highIPCandidates:
531+ // compute the full band, then return only the IP for the given overlay index.
532+ func candidateForOverlayIndex (ipNet * net.IPNet , peerIPs []string , nodeAddr string , overlayContainers int , overlayIndex int ) net.IP {
533+ candidates := computeIPCandidates (ipNet , peerIPs , nodeAddr , overlayContainers )
534+ if overlayIndex >= len (candidates ) {
535+ return nil
536+ }
537+ return candidates [overlayIndex ]
538+ }
539+
540+ func TestOverlayIndexSelection (t * testing.T ) {
541+ t .Run ("two containers on same node get different IPs" , func (t * testing.T ) {
542+ ipNet := mustParseCIDR (t , "10.0.64.0/21" )
543+ peers := []string {"10.0.0.1" , "10.0.0.2" , "10.0.0.3" }
544+
545+ ip0 := candidateForOverlayIndex (ipNet , peers , "10.0.0.1" , 2 , 0 )
546+ ip1 := candidateForOverlayIndex (ipNet , peers , "10.0.0.1" , 2 , 1 )
547+
548+ if ip0 == nil || ip1 == nil {
549+ t .Fatalf ("got nil: ip0=%v ip1=%v" , ip0 , ip1 )
550+ }
551+ if ip0 .Equal (ip1 ) {
552+ t .Errorf ("both containers got same IP: %s" , ip0 )
553+ }
554+ if ip0 .String () != "10.0.71.254" {
555+ t .Errorf ("container 0: got %s, want 10.0.71.254" , ip0 )
556+ }
557+ if ip1 .String () != "10.0.71.253" {
558+ t .Errorf ("container 1: got %s, want 10.0.71.253" , ip1 )
559+ }
560+ })
561+
562+ t .Run ("three containers on same node all get unique IPs" , func (t * testing.T ) {
563+ ipNet := mustParseCIDR (t , "10.0.1.0/24" )
564+ peers := []string {"10.0.0.1" , "10.0.0.2" }
565+
566+ seen := map [string ]int {}
567+ for i := range 3 {
568+ ip := candidateForOverlayIndex (ipNet , peers , "10.0.0.1" , 3 , i )
569+ if ip == nil {
570+ t .Fatalf ("container %d got nil" , i )
571+ }
572+ if prev , exists := seen [ip .String ()]; exists {
573+ t .Errorf ("container %d and %d both got %s" , prev , i , ip )
574+ }
575+ seen [ip .String ()] = i
576+ }
577+ })
578+
579+ t .Run ("overlay index out of bounds returns nil" , func (t * testing.T ) {
580+ ipNet := mustParseCIDR (t , "10.0.1.0/24" )
581+ peers := []string {"10.0.0.1" }
582+
583+ ip := candidateForOverlayIndex (ipNet , peers , "10.0.0.1" , 2 , 2 )
584+ if ip != nil {
585+ t .Errorf ("index 2 with 2 containers: got %s, want nil" , ip )
586+ }
587+
588+ ip = candidateForOverlayIndex (ipNet , peers , "10.0.0.1" , 2 , 99 )
589+ if ip != nil {
590+ t .Errorf ("index 99 with 2 containers: got %s, want nil" , ip )
591+ }
592+ })
593+
594+ t .Run ("containers on different nodes never collide" , func (t * testing.T ) {
595+ ipNet := mustParseCIDR (t , "10.0.64.0/21" )
596+ peers := []string {"10.0.0.1" , "10.0.0.2" , "10.0.0.3" , "10.0.0.4" }
597+ overlayContainers := 3
598+
599+ seen := map [string ]string {}
600+ for _ , peer := range peers {
601+ for i := range overlayContainers {
602+ ip := candidateForOverlayIndex (ipNet , peers , peer , overlayContainers , i )
603+ if ip == nil {
604+ continue
605+ }
606+ key := ip .String ()
607+ label := fmt .Sprintf ("%s[%d]" , peer , i )
608+ if prev , exists := seen [key ]; exists {
609+ t .Errorf ("IP %s assigned to %s and %s" , key , prev , label )
610+ }
611+ seen [key ] = label
612+ }
613+ }
614+ })
615+
616+ t .Run ("simulates run() overlay index counting" , func (t * testing.T ) {
617+ // Config: [non-overlay, heartbeat (overlay), non-overlay, metricbeat (overlay)]
618+ type fakeContainer struct {
619+ name string
620+ overlay bool
621+ }
622+ containers := []fakeContainer {
623+ {"logger" , false },
624+ {"heartbeat" , true },
625+ {"proxy" , false },
626+ {"metricbeat" , true },
627+ }
628+
629+ ipNet := mustParseCIDR (t , "10.0.1.0/24" )
630+ peers := []string {"10.0.0.1" , "10.0.0.2" }
631+
632+ overlayIndex := 0
633+ results := map [string ]net.IP {}
634+ for _ , c := range containers {
635+ currentIndex := overlayIndex
636+ if c .overlay {
637+ overlayIndex ++
638+ }
639+ if c .overlay {
640+ ip := candidateForOverlayIndex (ipNet , peers , "10.0.0.1" , 2 , currentIndex )
641+ results [c .name ] = ip
642+ }
643+ }
644+
645+ if results ["heartbeat" ].String () != "10.0.1.254" {
646+ t .Errorf ("heartbeat: got %s, want 10.0.1.254" , results ["heartbeat" ])
647+ }
648+ if results ["metricbeat" ].String () != "10.0.1.253" {
649+ t .Errorf ("metricbeat: got %s, want 10.0.1.253" , results ["metricbeat" ])
650+ }
651+ if results ["heartbeat" ].Equal (results ["metricbeat" ]) {
652+ t .Errorf ("heartbeat and metricbeat got same IP: %s" , results ["heartbeat" ])
653+ }
654+ })
655+
656+ t .Run ("/30 with 2 containers — second has no room on second peer" , func (t * testing.T ) {
657+ // /30: 2 usable IPs. Peer 0 band = [.2, .1]. Peer 1 band = [.0] but .0 = network → empty.
658+ // So peer 1 container 0 gets nil, container 1 also nil.
659+ ipNet := mustParseCIDR (t , "10.0.0.0/30" )
660+ peers := []string {"10.0.0.1" , "10.0.0.2" }
661+
662+ // Peer 0: band has 1 IP (.2), container 0 gets it, container 1 gets nil
663+ ip := candidateForOverlayIndex (ipNet , peers , "10.0.0.1" , 2 , 0 )
664+ if ip == nil || ip .String () != "10.0.0.2" {
665+ t .Errorf ("peer0 container0: got %v, want 10.0.0.2" , ip )
666+ }
667+ ip = candidateForOverlayIndex (ipNet , peers , "10.0.0.1" , 2 , 1 )
668+ if ip == nil || ip .String () != "10.0.0.1" {
669+ t .Errorf ("peer0 container1: got %v, want 10.0.0.1" , ip )
670+ }
671+
672+ // Peer 1: startOffset = 1+1*2 = 3, broadcast-3 = .0 = network → empty band
673+ ip = candidateForOverlayIndex (ipNet , peers , "10.0.0.2" , 2 , 0 )
674+ if ip != nil {
675+ t .Errorf ("peer1 container0: got %s, want nil" , ip )
676+ }
677+ ip = candidateForOverlayIndex (ipNet , peers , "10.0.0.2" , 2 , 1 )
678+ if ip != nil {
679+ t .Errorf ("peer1 container1: got %s, want nil" , ip )
680+ }
681+ })
682+ }
0 commit comments