Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion docs/en/latest/reference/example.md
Original file line number Diff line number Diff line change
Expand Up @@ -1092,6 +1092,8 @@ spec:
- 10.24.87.13
```

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.

</TabItem>

<TabItem value="ingress">
Expand All @@ -1116,6 +1118,8 @@ spec:
- 10.24.87.13
```

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.

To configure the `publishService`:

```yaml
Expand All @@ -1133,7 +1137,10 @@ spec:
publishService: apisix-gateway
```

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.
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.

- If the Service is of `LoadBalancer` type, the controller uses its external IP or hostname.
- If the Service is of `ClusterIP` type, the controller propagates the hostname from any Ingress resources that reference that Service.

</TabItem>

Expand Down
33 changes: 21 additions & 12 deletions internal/controller/gateway_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (
"context"
"errors"
"fmt"
"net"
"reflect"

"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -178,20 +180,26 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
msg: "gateway proxy not found",
}
} else {
if len(gateway.Status.Addresses) != len(gatewayProxy.Spec.StatusAddress) {
for _, addr := range gatewayProxy.Spec.StatusAddress {
if addr == "" {
continue
}
addrs = append(addrs,
gatewayv1.GatewayStatusAddress{
Value: addr,
},
)
for _, addr := range gatewayProxy.Spec.StatusAddress {
if addr == "" {
continue
}
addrType := gatewayv1.IPAddressType
if net.ParseIP(addr) == nil {
addrType = gatewayv1.HostnameAddressType
}
addrs = append(addrs,
gatewayv1.GatewayStatusAddress{
Type: &addrType,
Value: addr,
},
)
}
}

// deduplicate in case statusAddress contains repeated values
addrs = deduplicateGatewayStatusAddresses(addrs)

listenerStatuses, err := getListenerStatus(ctx, r.Client, gateway)
if err != nil {
r.Log.Error(err, "failed to get listener status", "gateway", req.NamespacedName)
Expand All @@ -207,8 +215,9 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct

accepted := SetGatewayConditionAccepted(gateway, acceptStatus.status, acceptStatus.msg)
programmed := SetGatewayConditionProgrammed(gateway, conditionProgrammedStatus, conditionProgrammedMsg)
if accepted || programmed || len(addrs) > 0 || len(listenerStatuses) > 0 {
if len(addrs) > 0 {
addressesChanged := !reflect.DeepEqual(gateway.Status.Addresses, addrs)
if accepted || programmed || addressesChanged || len(listenerStatuses) > 0 {
if addressesChanged {
gateway.Status.Addresses = addrs
}
if len(listenerStatuses) > 0 {
Expand Down
57 changes: 52 additions & 5 deletions internal/controller/ingress_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package controller
import (
"context"
"fmt"
"net"
"reflect"

"github.com/go-logr/logr"
Expand Down Expand Up @@ -694,9 +695,13 @@ func (r *IngressReconciler) updateStatus(ctx context.Context, tctx *provider.Tra
if addr == "" {
continue
}
loadBalancerStatus.Ingress = append(loadBalancerStatus.Ingress, networkingv1.IngressLoadBalancerIngress{
IP: addr,
})
lbIngress := networkingv1.IngressLoadBalancerIngress{}
if net.ParseIP(addr) != nil {
lbIngress.IP = addr
} else {
lbIngress.Hostname = addr
}
loadBalancerStatus.Ingress = append(loadBalancerStatus.Ingress, lbIngress)
}
} else {
// 2. if the IngressStatusAddress is not configured, try to use the PublishService
Expand All @@ -717,7 +722,8 @@ func (r *IngressReconciler) updateStatus(ctx context.Context, tctx *provider.Tra
return fmt.Errorf("failed to get publish service %s: %w", publishService, err)
}

if svc.Spec.Type == corev1.ServiceTypeLoadBalancer {
switch svc.Spec.Type {
case corev1.ServiceTypeLoadBalancer:
// get the LoadBalancer IP and Hostname of the service
for _, ip := range svc.Status.LoadBalancer.Ingress {
if ip.IP != "" {
Expand All @@ -731,12 +737,53 @@ func (r *IngressReconciler) updateStatus(ctx context.Context, tctx *provider.Tra
})
}
}
case corev1.ServiceTypeClusterIP:
// For ClusterIP services, propagate load balancer status from any other
// Ingress that lists this service as a backend (e.g. a cloud LB Ingress
// fronting the APISIX ClusterIP Service). Uses ServiceIndexRef, which
// indexes Ingresses by spec.rules[].http.paths[].backend.service.name.
ingressList := &networkingv1.IngressList{}
if err := r.List(ctx, ingressList, client.MatchingFields{
indexer.ServiceIndexRef: indexer.GenIndexKey(namespace, name),
}); err != nil {
return fmt.Errorf("failed to list ingresses for ClusterIP service %s/%s: %w", namespace, name, err)
}
if len(ingressList.Items) == 0 {
r.Log.V(1).Info("no Ingress found with this ClusterIP service as a backend; status will not be propagated",
"service", namespace+"/"+name)
}
for _, ing := range ingressList.Items {
// Skip the current Ingress being reconciled to avoid a
// self-referential loop: updating its own status would trigger
// a new reconcile, which would collect its own (just-written)
// hostname again and potentially repeat indefinitely.
if ing.Namespace == ingress.Namespace && ing.Name == ingress.Name {
continue
}
for _, lb := range ing.Status.LoadBalancer.Ingress {
if lb.IP != "" {
loadBalancerStatus.Ingress = append(loadBalancerStatus.Ingress, networkingv1.IngressLoadBalancerIngress{
IP: lb.IP,
})
}
if lb.Hostname != "" {
loadBalancerStatus.Ingress = append(loadBalancerStatus.Ingress, networkingv1.IngressLoadBalancerIngress{
Hostname: lb.Hostname,
})
}
}
}
}
}
}

// deduplicate load balancer ingress entries that may arise when multiple
// source Ingresses carry the same address (ClusterIP case) or when
// statusAddress contains repeated values.
loadBalancerStatus.Ingress = deduplicateLoadBalancerIngress(loadBalancerStatus.Ingress)

// update the load balancer status
if len(loadBalancerStatus.Ingress) > 0 && !reflect.DeepEqual(ingress.Status.LoadBalancer, loadBalancerStatus) {
if !reflect.DeepEqual(ingress.Status.LoadBalancer, loadBalancerStatus) {
ingress.Status.LoadBalancer = loadBalancerStatus
r.Updater.Update(status.Update{
NamespacedName: utils.NamespacedName(ingress),
Expand Down
25 changes: 25 additions & 0 deletions internal/controller/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -1621,3 +1621,28 @@ func ExtractIngressClass(obj client.Object) string {
panic(fmt.Errorf("unhandled object type %T for extracting ingress class", obj))
}
}

// deduplicateLoadBalancerIngress removes duplicate IngressLoadBalancerIngress entries in-place,
// comparing by IP and Hostname (Ports are ignored for dedup purposes).
func deduplicateLoadBalancerIngress(entries []networkingv1.IngressLoadBalancerIngress) []networkingv1.IngressLoadBalancerIngress {
slices.SortFunc(entries, func(a, b networkingv1.IngressLoadBalancerIngress) int {
if c := strings.Compare(a.IP, b.IP); c != 0 {
return c
}
return strings.Compare(a.Hostname, b.Hostname)
})
return slices.CompactFunc(entries, func(a, b networkingv1.IngressLoadBalancerIngress) bool {
return a.IP == b.IP && a.Hostname == b.Hostname
})
}

// deduplicateGatewayStatusAddresses removes duplicate GatewayStatusAddress entries in-place,
// comparing by Value field (AddressType is a pointer so cannot be used as map key).
func deduplicateGatewayStatusAddresses(addrs []gatewayv1.GatewayStatusAddress) []gatewayv1.GatewayStatusAddress {
slices.SortFunc(addrs, func(a, b gatewayv1.GatewayStatusAddress) int {
return strings.Compare(a.Value, b.Value)
})
return slices.CompactFunc(addrs, func(a, b gatewayv1.GatewayStatusAddress) bool {
return a.Value == b.Value
})
}
186 changes: 186 additions & 0 deletions test/e2e/gatewayapi/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -539,4 +539,190 @@ spec:
Expect(string(getListener.SupportedKinds[0].Kind)).To(Equal("UDPRoute"), "udp listener supported kind content")
})
})

Context("Gateway Status Address", func() {
var gatewayProxyWithStatusAddressYaml = `
apiVersion: apisix.apache.org/v1alpha1
kind: GatewayProxy
metadata:
name: apisix-proxy-config
namespace: %s
spec:
statusAddress:
- %s
provider:
type: ControlPlane
controlPlane:
endpoints:
- %s
auth:
type: AdminKey
adminKey:
value: "%s"
`
var defaultGatewayClass = `
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: %s
spec:
controllerName: "%s"
`
var defaultGateway = `
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: %s
spec:
gatewayClassName: %s
listeners:
- name: http
protocol: HTTP
port: 80
infrastructure:
parametersRef:
group: apisix.apache.org
kind: GatewayProxy
name: apisix-proxy-config
`
getGatewayAddresses := func(gatewayName string) ([]gatewayv1.GatewayStatusAddress, error) {
var gateway gatewayv1.Gateway
if err := s.GetKubeClient().Get(context.Background(), k8stypes.NamespacedName{
Name: gatewayName,
Namespace: s.Namespace(),
}, &gateway); err != nil {
return nil, err
}
return gateway.Status.Addresses, nil
}

assertGatewayAddress := func(gatewayName, expectedValue string, expectedType gatewayv1.AddressType) {
s.RetryAssertion(func() error {
addrs, err := getGatewayAddresses(gatewayName)
if err != nil {
return err
}
if len(addrs) == 0 {
return fmt.Errorf("expected at least 1 status address, got 0")
}
addr := addrs[0]
if addr.Value != expectedValue {
return fmt.Errorf("expected address value %s, got %s", expectedValue, addr.Value)
}
if addr.Type == nil {
return fmt.Errorf("expected address type to be set, got nil")
}
if *addr.Type != expectedType {
return fmt.Errorf("expected address type %s, got %s", expectedType, *addr.Type)
}
return nil
}).ShouldNot(HaveOccurred(), "check Gateway status address")
}

createGatewayClassAndGateway := func(gatewayClassName, gatewayName string) {
By("create GatewayClass")
Expect(s.CreateResourceFromStringWithNamespace(
fmt.Sprintf(defaultGatewayClass, gatewayClassName, s.GetControllerName()), ""),
).NotTo(HaveOccurred(), "creating GatewayClass")

By("create Gateway")
Expect(s.CreateResourceFromStringWithNamespace(
fmt.Sprintf(defaultGateway, gatewayName, gatewayClassName), s.Namespace()),
).NotTo(HaveOccurred(), "creating Gateway")
}

checkGatewayStatusAddressType := func(addrValue string, expectedType gatewayv1.AddressType) {
gatewayClassName := s.Namespace()

By("create GatewayProxy with statusAddress")
gatewayProxy := fmt.Sprintf(gatewayProxyWithStatusAddressYaml,
s.Namespace(), addrValue, s.Deployer.GetAdminEndpoint(), s.AdminKey())
Expect(s.CreateResourceFromString(gatewayProxy)).NotTo(HaveOccurred(), "creating GatewayProxy")

gatewayName := s.Namespace()
createGatewayClassAndGateway(gatewayClassName, gatewayName)

By("check Gateway status address type")
assertGatewayAddress(gatewayName, addrValue, expectedType)
}

It("sets IPAddress type when statusAddress is an IP", func() {
checkGatewayStatusAddressType("192.168.1.100", gatewayv1.IPAddressType)
})

It("sets Hostname type when statusAddress is a hostname", func() {
checkGatewayStatusAddressType("mygateway.example.com", gatewayv1.HostnameAddressType)
})

It("deduplicates repeated statusAddress entries", func() {
gatewayClassName := s.Namespace()
gatewayName := s.Namespace()
addr := "192.168.1.100"

By("create GatewayProxy with the same IP listed twice in statusAddress")
gatewayProxy := fmt.Sprintf(`
apiVersion: apisix.apache.org/v1alpha1
kind: GatewayProxy
metadata:
name: apisix-proxy-config
namespace: %s
spec:
statusAddress:
- %s
- %s
provider:
type: ControlPlane
controlPlane:
endpoints:
- %s
auth:
type: AdminKey
adminKey:
value: "%s"
`, s.Namespace(), addr, addr, s.Deployer.GetAdminEndpoint(), s.AdminKey())
Expect(s.CreateResourceFromString(gatewayProxy)).NotTo(HaveOccurred(), "creating GatewayProxy")

createGatewayClassAndGateway(gatewayClassName, gatewayName)

By("verify only one address appears in Gateway status despite duplicate input")
s.RetryAssertion(func() error {
addrs, err := getGatewayAddresses(gatewayName)
if err != nil {
return err
}
if len(addrs) != 1 {
return fmt.Errorf("expected exactly 1 status address after dedup, got %d", len(addrs))
}
if addrs[0].Value != addr {
return fmt.Errorf("expected address value %s, got %s", addr, addrs[0].Value)
}
return nil
}).ShouldNot(HaveOccurred(), "check Gateway status address deduplication")
})

It("updates status when statusAddress value changes without count change", func() {
gatewayClassName := s.Namespace()
gatewayName := s.Namespace()
initialAddr := "192.168.1.100"
updatedAddr := "updated.example.com"

By("create GatewayProxy with initial statusAddress")
gatewayProxy := fmt.Sprintf(gatewayProxyWithStatusAddressYaml,
s.Namespace(), initialAddr, s.Deployer.GetAdminEndpoint(), s.AdminKey())
Expect(s.CreateResourceFromString(gatewayProxy)).NotTo(HaveOccurred(), "creating GatewayProxy")

createGatewayClassAndGateway(gatewayClassName, gatewayName)

By("verify initial status address is set")
assertGatewayAddress(gatewayName, initialAddr, gatewayv1.IPAddressType)

By("update GatewayProxy with different statusAddress (same count)")
updatedGatewayProxy := fmt.Sprintf(gatewayProxyWithStatusAddressYaml,
s.Namespace(), updatedAddr, s.Deployer.GetAdminEndpoint(), s.AdminKey())
Expect(s.CreateResourceFromString(updatedGatewayProxy)).NotTo(HaveOccurred(), "updating GatewayProxy")

By("verify status address is updated to new value and type")
assertGatewayAddress(gatewayName, updatedAddr, gatewayv1.HostnameAddressType)
})
})
})
Loading
Loading