Skip to content

Commit 878e501

Browse files
authored
fix: update publishService/statusAdress logic in gateway and ingress controller files (#2730) (#2732)
1 parent 0d225ad commit 878e501

6 files changed

Lines changed: 589 additions & 18 deletions

File tree

docs/en/latest/reference/example.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1092,6 +1092,8 @@ spec:
10921092
- 10.24.87.13
10931093
```
10941094

1095+
Each entry in `statusAddress` can be an IP address or a hostname. The controller automatically sets the address type on the Gateway status — `IPAddress` for valid IPs and `Hostname` for everything else.
1096+
10951097
</TabItem>
10961098

10971099
<TabItem value="ingress">
@@ -1116,6 +1118,8 @@ spec:
11161118
- 10.24.87.13
11171119
```
11181120

1121+
Each entry in `statusAddress` can be an IP address or a hostname. The controller automatically sets the `IP` field for valid IPs and the `Hostname` field for everything else in the Ingress load balancer status.
1122+
11191123
To configure the `publishService`:
11201124

11211125
```yaml
@@ -1133,7 +1137,10 @@ spec:
11331137
publishService: apisix-gateway
11341138
```
11351139

1136-
When using `publishService`, make sure your gateway Service is of `LoadBalancer` type the address can be populated. The controller will use the endpoint of this Service to update the status information of the Ingress resource. The format can be either `namespace/svc-name` or simply `svc-name` if the default namespace is correctly set.
1140+
When using `publishService`, the controller will use the endpoint of this Service to update the status information of the Ingress resource. The format can be either `namespace/svc-name` or simply `svc-name` if the default namespace is correctly set.
1141+
1142+
- If the Service is of `LoadBalancer` type, the controller uses its external IP or hostname.
1143+
- If the Service is of `ClusterIP` type, the controller propagates the hostname from any Ingress resources that reference that Service.
11371144

11381145
</TabItem>
11391146

internal/controller/gateway_controller.go

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import (
2121
"context"
2222
"errors"
2323
"fmt"
24+
"net"
25+
"reflect"
2426

2527
"github.com/go-logr/logr"
2628
corev1 "k8s.io/api/core/v1"
@@ -178,20 +180,26 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
178180
msg: "gateway proxy not found",
179181
}
180182
} else {
181-
if len(gateway.Status.Addresses) != len(gatewayProxy.Spec.StatusAddress) {
182-
for _, addr := range gatewayProxy.Spec.StatusAddress {
183-
if addr == "" {
184-
continue
185-
}
186-
addrs = append(addrs,
187-
gatewayv1.GatewayStatusAddress{
188-
Value: addr,
189-
},
190-
)
183+
for _, addr := range gatewayProxy.Spec.StatusAddress {
184+
if addr == "" {
185+
continue
191186
}
187+
addrType := gatewayv1.IPAddressType
188+
if net.ParseIP(addr) == nil {
189+
addrType = gatewayv1.HostnameAddressType
190+
}
191+
addrs = append(addrs,
192+
gatewayv1.GatewayStatusAddress{
193+
Type: &addrType,
194+
Value: addr,
195+
},
196+
)
192197
}
193198
}
194199

200+
// deduplicate in case statusAddress contains repeated values
201+
addrs = deduplicateGatewayStatusAddresses(addrs)
202+
195203
listenerStatuses, err := getListenerStatus(ctx, r.Client, gateway)
196204
if err != nil {
197205
r.Log.Error(err, "failed to get listener status", "gateway", req.NamespacedName)
@@ -207,8 +215,9 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
207215

208216
accepted := SetGatewayConditionAccepted(gateway, acceptStatus.status, acceptStatus.msg)
209217
programmed := SetGatewayConditionProgrammed(gateway, conditionProgrammedStatus, conditionProgrammedMsg)
210-
if accepted || programmed || len(addrs) > 0 || len(listenerStatuses) > 0 {
211-
if len(addrs) > 0 {
218+
addressesChanged := !reflect.DeepEqual(gateway.Status.Addresses, addrs)
219+
if accepted || programmed || addressesChanged || len(listenerStatuses) > 0 {
220+
if addressesChanged {
212221
gateway.Status.Addresses = addrs
213222
}
214223
if len(listenerStatuses) > 0 {

internal/controller/ingress_controller.go

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ package controller
2020
import (
2121
"context"
2222
"fmt"
23+
"net"
2324
"reflect"
2425

2526
"github.com/go-logr/logr"
@@ -694,9 +695,13 @@ func (r *IngressReconciler) updateStatus(ctx context.Context, tctx *provider.Tra
694695
if addr == "" {
695696
continue
696697
}
697-
loadBalancerStatus.Ingress = append(loadBalancerStatus.Ingress, networkingv1.IngressLoadBalancerIngress{
698-
IP: addr,
699-
})
698+
lbIngress := networkingv1.IngressLoadBalancerIngress{}
699+
if net.ParseIP(addr) != nil {
700+
lbIngress.IP = addr
701+
} else {
702+
lbIngress.Hostname = addr
703+
}
704+
loadBalancerStatus.Ingress = append(loadBalancerStatus.Ingress, lbIngress)
700705
}
701706
} else {
702707
// 2. if the IngressStatusAddress is not configured, try to use the PublishService
@@ -717,7 +722,8 @@ func (r *IngressReconciler) updateStatus(ctx context.Context, tctx *provider.Tra
717722
return fmt.Errorf("failed to get publish service %s: %w", publishService, err)
718723
}
719724

720-
if svc.Spec.Type == corev1.ServiceTypeLoadBalancer {
725+
switch svc.Spec.Type {
726+
case corev1.ServiceTypeLoadBalancer:
721727
// get the LoadBalancer IP and Hostname of the service
722728
for _, ip := range svc.Status.LoadBalancer.Ingress {
723729
if ip.IP != "" {
@@ -731,12 +737,53 @@ func (r *IngressReconciler) updateStatus(ctx context.Context, tctx *provider.Tra
731737
})
732738
}
733739
}
740+
case corev1.ServiceTypeClusterIP:
741+
// For ClusterIP services, propagate load balancer status from any other
742+
// Ingress that lists this service as a backend (e.g. a cloud LB Ingress
743+
// fronting the APISIX ClusterIP Service). Uses ServiceIndexRef, which
744+
// indexes Ingresses by spec.rules[].http.paths[].backend.service.name.
745+
ingressList := &networkingv1.IngressList{}
746+
if err := r.List(ctx, ingressList, client.MatchingFields{
747+
indexer.ServiceIndexRef: indexer.GenIndexKey(namespace, name),
748+
}); err != nil {
749+
return fmt.Errorf("failed to list ingresses for ClusterIP service %s/%s: %w", namespace, name, err)
750+
}
751+
if len(ingressList.Items) == 0 {
752+
r.Log.V(1).Info("no Ingress found with this ClusterIP service as a backend; status will not be propagated",
753+
"service", namespace+"/"+name)
754+
}
755+
for _, ing := range ingressList.Items {
756+
// Skip the current Ingress being reconciled to avoid a
757+
// self-referential loop: updating its own status would trigger
758+
// a new reconcile, which would collect its own (just-written)
759+
// hostname again and potentially repeat indefinitely.
760+
if ing.Namespace == ingress.Namespace && ing.Name == ingress.Name {
761+
continue
762+
}
763+
for _, lb := range ing.Status.LoadBalancer.Ingress {
764+
if lb.IP != "" {
765+
loadBalancerStatus.Ingress = append(loadBalancerStatus.Ingress, networkingv1.IngressLoadBalancerIngress{
766+
IP: lb.IP,
767+
})
768+
}
769+
if lb.Hostname != "" {
770+
loadBalancerStatus.Ingress = append(loadBalancerStatus.Ingress, networkingv1.IngressLoadBalancerIngress{
771+
Hostname: lb.Hostname,
772+
})
773+
}
774+
}
775+
}
734776
}
735777
}
736778
}
737779

780+
// deduplicate load balancer ingress entries that may arise when multiple
781+
// source Ingresses carry the same address (ClusterIP case) or when
782+
// statusAddress contains repeated values.
783+
loadBalancerStatus.Ingress = deduplicateLoadBalancerIngress(loadBalancerStatus.Ingress)
784+
738785
// update the load balancer status
739-
if len(loadBalancerStatus.Ingress) > 0 && !reflect.DeepEqual(ingress.Status.LoadBalancer, loadBalancerStatus) {
786+
if !reflect.DeepEqual(ingress.Status.LoadBalancer, loadBalancerStatus) {
740787
ingress.Status.LoadBalancer = loadBalancerStatus
741788
r.Updater.Update(status.Update{
742789
NamespacedName: utils.NamespacedName(ingress),

internal/controller/utils.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1621,3 +1621,28 @@ func ExtractIngressClass(obj client.Object) string {
16211621
panic(fmt.Errorf("unhandled object type %T for extracting ingress class", obj))
16221622
}
16231623
}
1624+
1625+
// deduplicateLoadBalancerIngress removes duplicate IngressLoadBalancerIngress entries in-place,
1626+
// comparing by IP and Hostname (Ports are ignored for dedup purposes).
1627+
func deduplicateLoadBalancerIngress(entries []networkingv1.IngressLoadBalancerIngress) []networkingv1.IngressLoadBalancerIngress {
1628+
slices.SortFunc(entries, func(a, b networkingv1.IngressLoadBalancerIngress) int {
1629+
if c := strings.Compare(a.IP, b.IP); c != 0 {
1630+
return c
1631+
}
1632+
return strings.Compare(a.Hostname, b.Hostname)
1633+
})
1634+
return slices.CompactFunc(entries, func(a, b networkingv1.IngressLoadBalancerIngress) bool {
1635+
return a.IP == b.IP && a.Hostname == b.Hostname
1636+
})
1637+
}
1638+
1639+
// deduplicateGatewayStatusAddresses removes duplicate GatewayStatusAddress entries in-place,
1640+
// comparing by Value field (AddressType is a pointer so cannot be used as map key).
1641+
func deduplicateGatewayStatusAddresses(addrs []gatewayv1.GatewayStatusAddress) []gatewayv1.GatewayStatusAddress {
1642+
slices.SortFunc(addrs, func(a, b gatewayv1.GatewayStatusAddress) int {
1643+
return strings.Compare(a.Value, b.Value)
1644+
})
1645+
return slices.CompactFunc(addrs, func(a, b gatewayv1.GatewayStatusAddress) bool {
1646+
return a.Value == b.Value
1647+
})
1648+
}

test/e2e/gatewayapi/gateway.go

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,4 +539,190 @@ spec:
539539
Expect(string(getListener.SupportedKinds[0].Kind)).To(Equal("UDPRoute"), "udp listener supported kind content")
540540
})
541541
})
542+
543+
Context("Gateway Status Address", func() {
544+
var gatewayProxyWithStatusAddressYaml = `
545+
apiVersion: apisix.apache.org/v1alpha1
546+
kind: GatewayProxy
547+
metadata:
548+
name: apisix-proxy-config
549+
namespace: %s
550+
spec:
551+
statusAddress:
552+
- %s
553+
provider:
554+
type: ControlPlane
555+
controlPlane:
556+
endpoints:
557+
- %s
558+
auth:
559+
type: AdminKey
560+
adminKey:
561+
value: "%s"
562+
`
563+
var defaultGatewayClass = `
564+
apiVersion: gateway.networking.k8s.io/v1
565+
kind: GatewayClass
566+
metadata:
567+
name: %s
568+
spec:
569+
controllerName: "%s"
570+
`
571+
var defaultGateway = `
572+
apiVersion: gateway.networking.k8s.io/v1
573+
kind: Gateway
574+
metadata:
575+
name: %s
576+
spec:
577+
gatewayClassName: %s
578+
listeners:
579+
- name: http
580+
protocol: HTTP
581+
port: 80
582+
infrastructure:
583+
parametersRef:
584+
group: apisix.apache.org
585+
kind: GatewayProxy
586+
name: apisix-proxy-config
587+
`
588+
getGatewayAddresses := func(gatewayName string) ([]gatewayv1.GatewayStatusAddress, error) {
589+
var gateway gatewayv1.Gateway
590+
if err := s.GetKubeClient().Get(context.Background(), k8stypes.NamespacedName{
591+
Name: gatewayName,
592+
Namespace: s.Namespace(),
593+
}, &gateway); err != nil {
594+
return nil, err
595+
}
596+
return gateway.Status.Addresses, nil
597+
}
598+
599+
assertGatewayAddress := func(gatewayName, expectedValue string, expectedType gatewayv1.AddressType) {
600+
s.RetryAssertion(func() error {
601+
addrs, err := getGatewayAddresses(gatewayName)
602+
if err != nil {
603+
return err
604+
}
605+
if len(addrs) == 0 {
606+
return fmt.Errorf("expected at least 1 status address, got 0")
607+
}
608+
addr := addrs[0]
609+
if addr.Value != expectedValue {
610+
return fmt.Errorf("expected address value %s, got %s", expectedValue, addr.Value)
611+
}
612+
if addr.Type == nil {
613+
return fmt.Errorf("expected address type to be set, got nil")
614+
}
615+
if *addr.Type != expectedType {
616+
return fmt.Errorf("expected address type %s, got %s", expectedType, *addr.Type)
617+
}
618+
return nil
619+
}).ShouldNot(HaveOccurred(), "check Gateway status address")
620+
}
621+
622+
createGatewayClassAndGateway := func(gatewayClassName, gatewayName string) {
623+
By("create GatewayClass")
624+
Expect(s.CreateResourceFromStringWithNamespace(
625+
fmt.Sprintf(defaultGatewayClass, gatewayClassName, s.GetControllerName()), ""),
626+
).NotTo(HaveOccurred(), "creating GatewayClass")
627+
628+
By("create Gateway")
629+
Expect(s.CreateResourceFromStringWithNamespace(
630+
fmt.Sprintf(defaultGateway, gatewayName, gatewayClassName), s.Namespace()),
631+
).NotTo(HaveOccurred(), "creating Gateway")
632+
}
633+
634+
checkGatewayStatusAddressType := func(addrValue string, expectedType gatewayv1.AddressType) {
635+
gatewayClassName := s.Namespace()
636+
637+
By("create GatewayProxy with statusAddress")
638+
gatewayProxy := fmt.Sprintf(gatewayProxyWithStatusAddressYaml,
639+
s.Namespace(), addrValue, s.Deployer.GetAdminEndpoint(), s.AdminKey())
640+
Expect(s.CreateResourceFromString(gatewayProxy)).NotTo(HaveOccurred(), "creating GatewayProxy")
641+
642+
gatewayName := s.Namespace()
643+
createGatewayClassAndGateway(gatewayClassName, gatewayName)
644+
645+
By("check Gateway status address type")
646+
assertGatewayAddress(gatewayName, addrValue, expectedType)
647+
}
648+
649+
It("sets IPAddress type when statusAddress is an IP", func() {
650+
checkGatewayStatusAddressType("192.168.1.100", gatewayv1.IPAddressType)
651+
})
652+
653+
It("sets Hostname type when statusAddress is a hostname", func() {
654+
checkGatewayStatusAddressType("mygateway.example.com", gatewayv1.HostnameAddressType)
655+
})
656+
657+
It("deduplicates repeated statusAddress entries", func() {
658+
gatewayClassName := s.Namespace()
659+
gatewayName := s.Namespace()
660+
addr := "192.168.1.100"
661+
662+
By("create GatewayProxy with the same IP listed twice in statusAddress")
663+
gatewayProxy := fmt.Sprintf(`
664+
apiVersion: apisix.apache.org/v1alpha1
665+
kind: GatewayProxy
666+
metadata:
667+
name: apisix-proxy-config
668+
namespace: %s
669+
spec:
670+
statusAddress:
671+
- %s
672+
- %s
673+
provider:
674+
type: ControlPlane
675+
controlPlane:
676+
endpoints:
677+
- %s
678+
auth:
679+
type: AdminKey
680+
adminKey:
681+
value: "%s"
682+
`, s.Namespace(), addr, addr, s.Deployer.GetAdminEndpoint(), s.AdminKey())
683+
Expect(s.CreateResourceFromString(gatewayProxy)).NotTo(HaveOccurred(), "creating GatewayProxy")
684+
685+
createGatewayClassAndGateway(gatewayClassName, gatewayName)
686+
687+
By("verify only one address appears in Gateway status despite duplicate input")
688+
s.RetryAssertion(func() error {
689+
addrs, err := getGatewayAddresses(gatewayName)
690+
if err != nil {
691+
return err
692+
}
693+
if len(addrs) != 1 {
694+
return fmt.Errorf("expected exactly 1 status address after dedup, got %d", len(addrs))
695+
}
696+
if addrs[0].Value != addr {
697+
return fmt.Errorf("expected address value %s, got %s", addr, addrs[0].Value)
698+
}
699+
return nil
700+
}).ShouldNot(HaveOccurred(), "check Gateway status address deduplication")
701+
})
702+
703+
It("updates status when statusAddress value changes without count change", func() {
704+
gatewayClassName := s.Namespace()
705+
gatewayName := s.Namespace()
706+
initialAddr := "192.168.1.100"
707+
updatedAddr := "updated.example.com"
708+
709+
By("create GatewayProxy with initial statusAddress")
710+
gatewayProxy := fmt.Sprintf(gatewayProxyWithStatusAddressYaml,
711+
s.Namespace(), initialAddr, s.Deployer.GetAdminEndpoint(), s.AdminKey())
712+
Expect(s.CreateResourceFromString(gatewayProxy)).NotTo(HaveOccurred(), "creating GatewayProxy")
713+
714+
createGatewayClassAndGateway(gatewayClassName, gatewayName)
715+
716+
By("verify initial status address is set")
717+
assertGatewayAddress(gatewayName, initialAddr, gatewayv1.IPAddressType)
718+
719+
By("update GatewayProxy with different statusAddress (same count)")
720+
updatedGatewayProxy := fmt.Sprintf(gatewayProxyWithStatusAddressYaml,
721+
s.Namespace(), updatedAddr, s.Deployer.GetAdminEndpoint(), s.AdminKey())
722+
Expect(s.CreateResourceFromString(updatedGatewayProxy)).NotTo(HaveOccurred(), "updating GatewayProxy")
723+
724+
By("verify status address is updated to new value and type")
725+
assertGatewayAddress(gatewayName, updatedAddr, gatewayv1.HostnameAddressType)
726+
})
727+
})
542728
})

0 commit comments

Comments
 (0)