package lb import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "math/big" "net" "strings" "testing" "time" coreapi "k8s.io/api/core/v1" discovery "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var gwNow = metav1.NewTime(time.Date(2026, 10, 14, 4, 0, 0, 0, time.UTC)) // selfSigned is a certificate and key for the given names, PEM-encoded. func selfSigned(t *testing.T, names ...string) ([]byte, []byte) { t.Helper() key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatal(err) } tmpl := &x509.Certificate{ SerialNumber: big.NewInt(time.Now().UnixNano()), Subject: pkix.Name{CommonName: names[0]}, DNSNames: names, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(24 * time.Hour), KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, } der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) if err != nil { t.Fatal(err) } kb, _ := x509.MarshalECPrivateKey(key) return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: kb}) } func tlsSecret(t *testing.T, namespace, name string, names ...string) *coreapi.Secret { crt, key := selfSigned(t, names...) return &coreapi.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name}, Type: coreapi.SecretTypeTLS, Data: map[string][]byte{coreapi.TLSCertKey: crt, coreapi.TLSPrivateKeyKey: key}} } func gwNodes() []Node { return []Node{ {Name: "cp", Addresses: []net.IP{net.ParseIP("86.107.168.24").To4()}, WeaveIP: net.ParseIP("10.32.0.1").To4(), WeaveMAC: mustMAC("02:00:00:00:00:01")}, {Name: "w15", Addresses: []net.IP{net.ParseIP("86.107.168.15").To4()}, WeaveIP: net.ParseIP("10.44.0.0").To4(), WeaveMAC: mustMAC("02:00:00:00:00:02")}, } } func mustMAC(s string) net.HardwareAddr { m, _ := net.ParseMAC(s) return m } func ourClass() *GatewayClass { return &GatewayClass{ObjectMeta: metav1.ObjectMeta{Name: "reweave", Generation: 1}, Spec: GatewayClassSpec{ControllerName: GatewayControllerName}} } // giteaGateway is the shape Gitea's documentation prescribes: HTTPS with a certificate, plain HTTP, and SSH over TCP. func giteaGateway() *Gateway { return &Gateway{ ObjectMeta: metav1.ObjectMeta{Namespace: "gateway-system", Name: "shared", Generation: 3}, Spec: GatewaySpec{ GatewayClassName: "reweave", Addresses: []GatewayAddress{{Value: "86.107.168.15"}}, Listeners: []Listener{ {Name: "https-gitea", Port: 443, Protocol: ProtocolHTTPS, Hostname: ptr("git.levk.dev"), TLS: &ListenerTLSConfig{CertificateRefs: []SecretObjectReference{{Name: "git-levk-dev"}}}, AllowedRoutes: &AllowedRoutes{Namespaces: &RouteNamespaces{From: ptr("Selector"), Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"kubernetes.io/metadata.name": "gitea"}}}}}, {Name: "http", Port: 80, Protocol: ProtocolHTTP, Hostname: ptr("git.levk.dev"), AllowedRoutes: &AllowedRoutes{Namespaces: &RouteNamespaces{From: ptr("All")}}}, {Name: "ssh", Port: 22, Protocol: ProtocolTCP, AllowedRoutes: &AllowedRoutes{Namespaces: &RouteNamespaces{From: ptr("All")}}}, }, }, } } func giteaService() *coreapi.Service { return &coreapi.Service{ObjectMeta: metav1.ObjectMeta{Namespace: "gitea", Name: "gitea"}, Spec: coreapi.ServiceSpec{ Ports: []coreapi.ServicePort{{Name: "http", Port: 3000}, {Name: "ssh", Port: 22}}, }} } func giteaSlice() *discovery.EndpointSlice { return &discovery.EndpointSlice{ ObjectMeta: metav1.ObjectMeta{Namespace: "gitea", Name: "gitea-abc", Labels: map[string]string{serviceNameLabel: "gitea"}}, AddressType: discovery.AddressTypeIPv4, Ports: []discovery.EndpointPort{{Name: ptr("http"), Port: ptr(int32(3000))}, {Name: ptr("ssh"), Port: ptr(int32(22))}}, Endpoints: []discovery.Endpoint{{Addresses: []string{"10.44.0.12"}, Conditions: discovery.EndpointConditions{Ready: ptr(true)}}}, } } func giteaRoutes() ([]*HTTPRoute, []*TCPRoute) { t0 := metav1.NewTime(time.Date(2026, 10, 14, 1, 0, 0, 0, time.UTC)) https := &HTTPRoute{ ObjectMeta: metav1.ObjectMeta{Namespace: "gitea", Name: "gitea", Generation: 1, CreationTimestamp: t0}, Spec: HTTPRouteSpec{ ParentRefs: []ParentReference{{Namespace: ptr("gateway-system"), Name: "shared", SectionName: ptr("https-gitea")}}, Hostnames: []string{"git.levk.dev"}, Rules: []HTTPRouteRule{{BackendRefs: []HTTPBackendRef{{BackendRef: BackendRef{Name: "gitea", Port: ptr(int32(3000))}}}}}, }, } redirect := &HTTPRoute{ ObjectMeta: metav1.ObjectMeta{Namespace: "gitea", Name: "gitea-redirect", Generation: 1, CreationTimestamp: t0}, Spec: HTTPRouteSpec{ ParentRefs: []ParentReference{{Namespace: ptr("gateway-system"), Name: "shared", SectionName: ptr("http")}}, Rules: []HTTPRouteRule{{Filters: []HTTPRouteFilter{{Type: filterRequestRedirect, RequestRedirect: &HTTPRequestRedirectFilter{Scheme: ptr("https"), StatusCode: ptr(301)}}}}}, }, } ssh := &TCPRoute{ ObjectMeta: metav1.ObjectMeta{Namespace: "gitea", Name: "gitea-ssh", Generation: 1, CreationTimestamp: t0}, Spec: TCPRouteSpec{ ParentRefs: []ParentReference{{Namespace: ptr("gateway-system"), Name: "shared", SectionName: ptr("ssh")}}, Rules: []TCPRouteRule{{BackendRefs: []BackendRef{{Name: "gitea", Port: ptr(int32(22))}}}}, }, } return []*HTTPRoute{https, redirect}, []*TCPRoute{ssh} } func giteaInput(t *testing.T) GatewayInput { hr, tr := giteaRoutes() return GatewayInput{ Classes: []*GatewayClass{ourClass()}, Gateways: []*Gateway{giteaGateway()}, HTTPRoutes: hr, TCPRoutes: tr, Namespaces: []*coreapi.Namespace{ {ObjectMeta: metav1.ObjectMeta{Name: "gitea", Labels: map[string]string{"kubernetes.io/metadata.name": "gitea"}}}, {ObjectMeta: metav1.ObjectMeta{Name: "gateway-system", Labels: map[string]string{"kubernetes.io/metadata.name": "gateway-system"}}}, }, Services: []*coreapi.Service{giteaService()}, Slices: []*discovery.EndpointSlice{giteaSlice()}, Secrets: []*coreapi.Secret{tlsSecret(t, "gateway-system", "git-levk-dev", "git.levk.dev")}, SecretNamespaces: map[string]bool{"gateway-system": true}, Now: gwNow, } } func condOf(cs []metav1.Condition, typ string) metav1.Condition { for _, c := range cs { if c.Type == typ { return c } } return metav1.Condition{} } func TestGatewayServedByItsHolder(t *testing.T) { in := giteaInput(t) d := ComputeGateways("w15", gwNodes(), in) if len(d.Problems) != 0 { t.Fatalf("problems: %v", d.Problems) } // HTTPS and HTTP served by the agent; SSH forwarded in the kernel to Gitea's pod with the client address intact if len(d.Serve) != 2 { t.Fatalf("want the https and http listeners served, got %+v", d.Serve) } https := d.Serve[0] if !https.HTTPS || https.Port != 443 || https.Hostname != "git.levk.dev" || https.Cert == nil || len(https.Routes) != 1 { t.Fatalf("https listener: %+v", https) } be := https.Routes[0].Rules[0].Backends if len(be) != 1 || be[0].Key != "gitea/gitea:3000" || len(be[0].Endpoints) != 1 || be[0].Endpoints[0].String() != "10.44.0.12:3000" { t.Fatalf("https backends: %+v", be) } if len(d.Forwards) != 1 || d.Forwards[0].Ports[0].Port != 22 || d.Forwards[0].Ports[0].Endpoints[0].String() != "10.44.0.12:22" { t.Fatalf("ssh forward: %+v", d.Forwards) } if len(d.Holding) != 1 || d.Holding[0] != "w15" { t.Fatalf("holding: %v", d.Holding) } // the holder writes the Gateway's status: accepted, programmed, its address, each listener with one route if len(d.Gateways) != 1 { t.Fatalf("gateway status writes: %+v", d.Gateways) } st := d.Gateways[0].Status if c := condOf(st.Conditions, condProgrammed); c.Status != metav1.ConditionTrue || c.ObservedGeneration != 3 { t.Fatalf("programmed: %+v", c) } if len(st.Addresses) != 1 || st.Addresses[0].Value != "86.107.168.15" { t.Fatalf("addresses: %+v", st.Addresses) } for _, l := range st.Listeners { if l.AttachedRoutes != 1 || condOf(l.Conditions, condProgrammed).Status != metav1.ConditionTrue { t.Fatalf("listener %s: %+v", l.Name, l) } } // route statuses come from the first node by name, not the holder if len(d.Routes) != 0 || len(d.Classes) != 0 { t.Fatalf("w15 wrote route or class statuses: %+v %+v", d.Routes, d.Classes) } } func TestGatewayElsewhere(t *testing.T) { in := giteaInput(t) d := ComputeGateways("cp", gwNodes(), in) if len(d.Serve) != 0 || len(d.Forwards) != 0 || len(d.Gateways) != 0 { t.Fatalf("cp serves or writes what w15 holds: %+v %+v %+v", d.Serve, d.Forwards, d.Gateways) } // but steers replies of w15's forwards back to it, and writes class and route statuses (first by name) if len(d.Holding) != 1 || d.Holding[0] != "w15" { t.Fatalf("holding: %v", d.Holding) } if len(d.Classes) != 1 || condOf(d.Classes[0].Status.Conditions, condAccepted).Status != metav1.ConditionTrue { t.Fatalf("class status: %+v", d.Classes) } if len(d.Routes) != 3 { t.Fatalf("want 3 route statuses, got %+v", d.Routes) } for _, r := range d.Routes { if len(r.Status.Parents) != 1 || condOf(r.Status.Parents[0].Conditions, condAccepted).Status != metav1.ConditionTrue || condOf(r.Status.Parents[0].Conditions, condResolvedRefs).Status != metav1.ConditionTrue { t.Fatalf("route %s/%s status: %+v", r.Namespace, r.Name, r.Status) } } } func TestGatewayStatusIsWrittenOnce(t *testing.T) { in := giteaInput(t) d := ComputeGateways("w15", gwNodes(), in) in.Gateways[0].Status = d.Gateways[0].Status in.Now = metav1.NewTime(gwNow.Add(time.Hour)) // later passes see the status they wrote if again := ComputeGateways("w15", gwNodes(), in); len(again.Gateways) != 0 { t.Fatalf("unchanged status rewritten: %+v", again.Gateways) } c := ComputeGateways("cp", gwNodes(), in) for _, r := range c.Routes { for _, hr := range in.HTTPRoutes { if hr.Name == r.Name { hr.Status = r.Status } } for _, tr := range in.TCPRoutes { if tr.Name == r.Name { tr.Status = r.Status } } } in.Classes[0].Status = c.Classes[0].Status if again := ComputeGateways("cp", gwNodes(), in); len(again.Routes) != 0 || len(again.Classes) != 0 { t.Fatalf("unchanged route or class status rewritten: %+v %+v", again.Routes, again.Classes) } } func TestGatewayFailsClosedWithoutHolder(t *testing.T) { in := giteaInput(t) in.Gateways[0].Spec.Addresses[0].Value = "198.51.100.7" for _, self := range []string{"cp", "w15"} { d := ComputeGateways(self, gwNodes(), in) if len(d.Serve) != 0 || len(d.Forwards) != 0 || len(d.Holding) != 0 { t.Fatalf("%s serves an address nobody holds: %+v", self, d) } } d := ComputeGateways("cp", gwNodes(), in) if len(d.Gateways) != 1 { t.Fatalf("the first node by name must write the failure: %+v", d.Gateways) } c := condOf(d.Gateways[0].Status.Conditions, condProgrammed) if c.Status != metav1.ConditionFalse || c.Reason != "AddressNotUsable" || len(d.Gateways[0].Status.Addresses) != 0 { t.Fatalf("status: %+v", d.Gateways[0].Status) } } func TestGatewayAddressMustBeOneIPv4(t *testing.T) { in := giteaInput(t) in.Gateways[0].Spec.Addresses = nil d := ComputeGateways("cp", gwNodes(), in) if c := condOf(d.Gateways[0].Status.Conditions, condAccepted); c.Status != metav1.ConditionFalse || c.Reason != "UnsupportedAddress" { t.Fatalf("accepted: %+v", c) } } func TestListenerCertificateReferences(t *testing.T) { cases := map[string]struct { mutate func(in *GatewayInput) reason string }{ "missing Secret": {func(in *GatewayInput) { in.Secrets = nil }, "InvalidCertificateRef"}, "other namespace without grant": {func(in *GatewayInput) { in.Gateways[0].Spec.Listeners[0].TLS.CertificateRefs[0].Namespace = ptr("gitea") in.SecretNamespaces["gitea"] = true }, "RefNotPermitted"}, "namespace the agent cannot read": {func(in *GatewayInput) { in.SecretNamespaces = map[string]bool{} }, "RefNotPermitted"}, "not a certificate": {func(in *GatewayInput) { in.Secrets[0].Data[coreapi.TLSCertKey] = []byte("nope") }, "InvalidCertificateRef"}, } for name, c := range cases { in := giteaInput(t) c.mutate(&in) d := ComputeGateways("w15", gwNodes(), in) var https ListenerStatus for _, l := range d.Gateways[0].Status.Listeners { if l.Name == "https-gitea" { https = l } } if r := condOf(https.Conditions, condResolvedRefs); r.Status != metav1.ConditionFalse || r.Reason != c.reason { t.Errorf("%s: resolvedRefs %+v", name, r) } if p := condOf(https.Conditions, condProgrammed); p.Status != metav1.ConditionFalse { t.Errorf("%s: an HTTPS listener without its certificate must not be programmed", name) } for _, s := range d.Serve { if s.HTTPS { t.Errorf("%s: HTTPS listener served without a certificate", name) } } if a := condOf(d.Gateways[0].Status.Conditions, condAccepted); a.Reason != "ListenersNotValid" || a.Status != metav1.ConditionTrue { t.Errorf("%s: gateway accepted %+v", name, a) } } } func TestCrossNamespaceCertificateWithGrant(t *testing.T) { in := giteaInput(t) in.Gateways[0].Spec.Listeners[0].TLS.CertificateRefs[0].Namespace = ptr("gitea") in.Secrets = []*coreapi.Secret{tlsSecret(t, "gitea", "git-levk-dev", "git.levk.dev")} in.SecretNamespaces = map[string]bool{"gitea": true} in.Grants = []*ReferenceGrant{{ObjectMeta: metav1.ObjectMeta{Namespace: "gitea", Name: "gw"}, Spec: ReferenceGrantSpec{ From: []ReferenceGrantFrom{{Group: GatewayGroup, Kind: kindGateway, Namespace: "gateway-system"}}, To: []ReferenceGrantTo{{Group: "", Kind: kindSecret}}, }}} d := ComputeGateways("w15", gwNodes(), in) if len(d.Serve) != 2 || d.Serve[0].Cert == nil { t.Fatalf("a granted certificate in another namespace must be served: %+v", d.Serve) } } func TestListenerConflicts(t *testing.T) { in := giteaInput(t) l := in.Gateways[0].Spec.Listeners in.Gateways[0].Spec.Listeners = append(l, Listener{Name: "ssh2", Port: 22, Protocol: ProtocolTCP}, // two TCP listeners on 22 Listener{Name: "http-on-443", Port: 443, Protocol: ProtocolHTTP}, // HTTP and HTTPS on 443 ) d := ComputeGateways("w15", gwNodes(), in) want := map[string]string{"ssh": "HostnameConflict", "ssh2": "HostnameConflict", "https-gitea": "ProtocolConflict", "http-on-443": "ProtocolConflict", "http": ""} for _, ls := range d.Gateways[0].Status.Listeners { c := condOf(ls.Conditions, condConflicted) if want[ls.Name] == "" { if c.Status != metav1.ConditionFalse { t.Errorf("%s: conflicted %+v", ls.Name, c) } continue } if c.Status != metav1.ConditionTrue || c.Reason != want[ls.Name] { t.Errorf("%s: want conflicted %s, got %+v", ls.Name, want[ls.Name], c) } } if len(d.Forwards) != 0 { t.Fatalf("conflicted TCP listeners forwarded: %+v", d.Forwards) } if len(d.Serve) != 1 || d.Serve[0].Name != "http" { t.Fatalf("only the plain http listener on 80 is unconflicted: %+v", d.Serve) } } func TestListenerPortTakenByLoadBalancerService(t *testing.T) { in := giteaInput(t) class := Class in.Services = append(in.Services, &coreapi.Service{ObjectMeta: metav1.ObjectMeta{Namespace: "mail", Name: "smtp"}, Spec: coreapi.ServiceSpec{ Type: coreapi.ServiceTypeLoadBalancer, LoadBalancerClass: &class, LoadBalancerIP: "86.107.168.15", Ports: []coreapi.ServicePort{{Port: 22}}, }}) d := ComputeGateways("w15", gwNodes(), in) for _, ls := range d.Gateways[0].Status.Listeners { if ls.Name == "ssh" { if c := condOf(ls.Conditions, condAccepted); c.Status != metav1.ConditionFalse || c.Reason != "PortUnavailable" { t.Fatalf("ssh accepted: %+v", c) } } } if len(d.Forwards) != 0 { t.Fatalf("a listener on a port a Service forwards must not be forwarded: %+v", d.Forwards) } } func TestRouteAttachment(t *testing.T) { cases := map[string]struct { mutate func(in *GatewayInput) route string reason string }{ "unknown section": {func(in *GatewayInput) { in.HTTPRoutes[0].Spec.ParentRefs[0].SectionName = ptr("nope") }, "gitea", "NoMatchingParent"}, "namespace not selected": {func(in *GatewayInput) { in.Namespaces[0].Labels = nil }, "gitea", "NotAllowedByListeners"}, "hostname outside the listener's": {func(in *GatewayInput) { in.HTTPRoutes[0].Spec.Hostnames = []string{"git.example.net"} }, "gitea", "NoMatchingListenerHostname"}, "unsupported filter": {func(in *GatewayInput) { in.HTTPRoutes[0].Spec.Rules[0].Filters = []HTTPRouteFilter{{Type: "RequestMirror"}} }, "gitea", "UnsupportedValue"}, "regular expression path": {func(in *GatewayInput) { in.HTTPRoutes[0].Spec.Rules[0].Matches = []HTTPRouteMatch{{Path: &HTTPPathMatch{Type: ptr("RegularExpression"), Value: ptr("/.*")}}} }, "gitea", "UnsupportedValue"}, "wrong port": {func(in *GatewayInput) { in.TCPRoutes[0].Spec.ParentRefs[0].Port = ptr(int32(2222)) }, "gitea-ssh", "NoMatchingParent"}, } for name, c := range cases { in := giteaInput(t) c.mutate(&in) d := ComputeGateways("cp", gwNodes(), in) found := false for _, r := range d.Routes { if r.Name != c.route { continue } found = true a := condOf(r.Status.Parents[0].Conditions, condAccepted) if a.Status != metav1.ConditionFalse || a.Reason != c.reason { t.Errorf("%s: accepted %+v", name, a) } } if !found { t.Errorf("%s: no status for route %s", name, c.route) } h := ComputeGateways("w15", gwNodes(), in) if c.route == "gitea" { for _, s := range h.Serve { if s.Name == "https-gitea" && len(s.Routes) != 0 { t.Errorf("%s: a route that is not accepted was attached", name) } } } } } func TestBackendNotFoundBreaksTheRule(t *testing.T) { in := giteaInput(t) in.HTTPRoutes[0].Spec.Rules[0].BackendRefs[0].Name = "nope" d := ComputeGateways("cp", gwNodes(), in) for _, r := range d.Routes { if r.Name == "gitea" { if c := condOf(r.Status.Parents[0].Conditions, condResolvedRefs); c.Status != metav1.ConditionFalse || c.Reason != "BackendNotFound" { t.Fatalf("resolvedRefs: %+v", c) } if c := condOf(r.Status.Parents[0].Conditions, condAccepted); c.Status != metav1.ConditionTrue { t.Fatalf("a route with an unresolved backend is still accepted: %+v", c) } } } h := ComputeGateways("w15", gwNodes(), in) for _, s := range h.Serve { if s.Name == "https-gitea" && (len(s.Routes) != 1 || !s.Routes[0].Rules[0].Broken) { t.Fatalf("the rule must be served broken (500): %+v", s.Routes) } } } func TestTCPListenerForwardsTheOldestRoute(t *testing.T) { in := giteaInput(t) later := in.TCPRoutes[0].DeepCopyObject().(*TCPRoute) later.Name = "other-ssh" later.CreationTimestamp = metav1.NewTime(later.CreationTimestamp.Add(time.Minute)) later.Spec.Rules[0].BackendRefs[0].Name = "other" in.TCPRoutes = append(in.TCPRoutes, later) d := ComputeGateways("cp", gwNodes(), in) for _, r := range d.Routes { a := condOf(r.Status.Parents[0].Conditions, condAccepted) switch r.Name { case "gitea-ssh": if a.Status != metav1.ConditionTrue { t.Fatalf("oldest route: %+v", a) } case "other-ssh": if a.Status != metav1.ConditionFalse || !strings.Contains(a.Message, "gitea/gitea-ssh") { t.Fatalf("second route: %+v", a) } } } } func TestOtherControllersParentsKept(t *testing.T) { in := giteaInput(t) theirs := RouteParentStatus{ParentRef: ParentReference{Name: "elsewhere"}, ControllerName: "example.com/other", Conditions: []metav1.Condition{{Type: condAccepted, Status: metav1.ConditionTrue, Reason: "Accepted", LastTransitionTime: gwNow}}} in.HTTPRoutes[0].Status.Parents = []RouteParentStatus{theirs} d := ComputeGateways("cp", gwNodes(), in) for _, r := range d.Routes { if r.Name == "gitea" { if len(r.Status.Parents) != 2 || r.Status.Parents[0].ControllerName != "example.com/other" { t.Fatalf("another controller's entry must be kept: %+v", r.Status.Parents) } } } } func TestOtherClassesIgnored(t *testing.T) { in := giteaInput(t) in.Classes[0].Spec.ControllerName = "example.com/other" for _, self := range []string{"cp", "w15"} { d := ComputeGateways(self, gwNodes(), in) if len(d.Serve)+len(d.Forwards)+len(d.Gateways)+len(d.Routes)+len(d.Classes) != 0 { t.Fatalf("%s acted on another controller's class: %+v", self, d) } } } func TestNarrowHostnames(t *testing.T) { cases := []struct { listener string route []string want []string ok bool }{ {"", nil, nil, true}, {"git.levk.dev", nil, []string{"git.levk.dev"}, true}, {"", []string{"a.example.com"}, []string{"a.example.com"}, true}, {"test.example.com", []string{"*.example.com"}, []string{"test.example.com"}, true}, {"*.example.com", []string{"test.example.com", "test.example.net", "foo.test.example.com"}, []string{"test.example.com", "foo.test.example.com"}, true}, {"*.example.com", []string{"example.com"}, nil, false}, {"*.example.com", []string{"*.a.example.com"}, []string{"*.a.example.com"}, true}, {"*.a.example.com", []string{"*.example.com"}, []string{"*.a.example.com"}, true}, {"git.levk.dev", []string{"git.example.net"}, nil, false}, } for _, c := range cases { got, ok := narrowHostnames(c.listener, c.route) if ok != c.ok || strings.Join(got, ",") != strings.Join(c.want, ",") { t.Errorf("listener %q route %v: got %v %v, want %v %v", c.listener, c.route, got, ok, c.want, c.ok) } } } func TestMergeGateways(t *testing.T) { in := giteaInput(t) nodes := gwNodes() // on the holder: Gitea's SSH forwarded, its HTTP(S) ports let in d := MergeGateways(Compute("w15", nodes, nil, nil), ComputeGateways("w15", nodes, in), "w15", nodes) if len(d.Forwards) != 1 || len(d.Accept) != 2 || len(d.Holders) != 0 { t.Fatalf("holder: forwards %+v accept %+v holders %+v", d.Forwards, d.Accept, d.Holders) } r := Render(d, EgressDesired{}, "w15", &net.IPNet{IP: net.ParseIP("10.32.0.0"), Mask: net.CIDRMask(12, 32)}, nil) for _, want := range []string{ "-A REWEAVE-LB-IN -d 86.107.168.15/32 -p tcp -m tcp --dport 443", "-A REWEAVE-LB-IN -d 86.107.168.15/32 -p tcp -m tcp --dport 80", } { if !strings.Contains(r.Filter, want) { t.Errorf("filter lacks %q:\n%s", want, r.Filter) } } if !strings.Contains(r.Nat, "-A REWEAVE-LB -d 86.107.168.15/32 -p tcp -m tcp --dport 22") || !strings.Contains(r.Nat, "--to-destination 10.44.0.12:22") { t.Errorf("nat lacks the SSH forward:\n%s", r.Nat) } // elsewhere: w15 is a holder whose replies come back to it e := MergeGateways(Compute("cp", nodes, nil, nil), ComputeGateways("cp", nodes, in), "cp", nodes) if len(e.Forwards) != 0 || len(e.Accept) != 0 || len(e.Holders) != 1 || e.Holders[0].Node.Name != "w15" { t.Fatalf("cp: forwards %+v accept %+v holders %+v", e.Forwards, e.Accept, e.Holders) } } func TestUnboundListenerReported(t *testing.T) { in := giteaInput(t) in.Unbound = map[string]string{"gateway-system/shared/https-gitea": "address already in use"} d := ComputeGateways("w15", gwNodes(), in) for _, l := range d.Gateways[0].Status.Listeners { if l.Name != "https-gitea" { continue } if a := condOf(l.Conditions, condAccepted); a.Status != metav1.ConditionFalse || a.Reason != "PortUnavailable" { t.Fatalf("accepted: %+v", a) } if p := condOf(l.Conditions, condProgrammed); p.Status != metav1.ConditionFalse { t.Fatalf("programmed: %+v", p) } } served := false for _, s := range d.Serve { served = served || s.Name == "https-gitea" } if !served { t.Fatal("an unbound listener is still tried on every pass") } }