diff --git a/docs/en/latest/reference/example.md b/docs/en/latest/reference/example.md index 8c219f8c3b..a9d7a9aeb8 100644 --- a/docs/en/latest/reference/example.md +++ b/docs/en/latest/reference/example.md @@ -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. + @@ -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 @@ -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. diff --git a/internal/controller/gateway_controller.go b/internal/controller/gateway_controller.go index 147bdd1723..748bfe37ed 100644 --- a/internal/controller/gateway_controller.go +++ b/internal/controller/gateway_controller.go @@ -21,6 +21,8 @@ import ( "context" "errors" "fmt" + "net" + "reflect" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" @@ -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) @@ -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 { diff --git a/internal/controller/ingress_controller.go b/internal/controller/ingress_controller.go index f8447ee6c0..e36923731a 100644 --- a/internal/controller/ingress_controller.go +++ b/internal/controller/ingress_controller.go @@ -20,6 +20,7 @@ package controller import ( "context" "fmt" + "net" "reflect" "github.com/go-logr/logr" @@ -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 @@ -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 != "" { @@ -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), diff --git a/internal/controller/utils.go b/internal/controller/utils.go index a00ef9165b..e8a31ab6ef 100644 --- a/internal/controller/utils.go +++ b/internal/controller/utils.go @@ -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 + }) +} diff --git a/test/e2e/gatewayapi/gateway.go b/test/e2e/gatewayapi/gateway.go index 8c5557efee..23a27cd914 100644 --- a/test/e2e/gatewayapi/gateway.go +++ b/test/e2e/gatewayapi/gateway.go @@ -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) + }) + }) }) diff --git a/test/e2e/ingress/ingress.go b/test/e2e/ingress/ingress.go index 6329b681f9..5e56456de0 100644 --- a/test/e2e/ingress/ingress.go +++ b/test/e2e/ingress/ingress.go @@ -30,6 +30,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/stretchr/testify/assert" + networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" @@ -1294,4 +1295,300 @@ spec: Should(Equal(http.StatusOK)) }) }) + + Context("Ingress Status Address", func() { + var gatewayProxyWithPublishServiceYaml = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: GatewayProxy +metadata: + name: apisix-proxy-config + namespace: %s +spec: + publishService: %s/%s + provider: + type: ControlPlane + controlPlane: + endpoints: + - %s + auth: + type: AdminKey + adminKey: + value: "%s" +` + 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 ingressClassYaml = ` +apiVersion: networking.k8s.io/v1 +kind: IngressClass +metadata: + name: %s +spec: + controller: "%s" + parameters: + apiGroup: "apisix.apache.org" + kind: "GatewayProxy" + name: "apisix-proxy-config" + namespace: "%s" + scope: "Namespace" +` + var ingressYaml = ` +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: %s +spec: + ingressClassName: %s + rules: + - host: status.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: %s + port: + number: 80 +` + getIngressLBStatus := func(ingressName string) ([]networkingv1.IngressLoadBalancerIngress, error) { + ing := &networkingv1.Ingress{} + if err := s.K8sClient.Get(context.Background(), types.NamespacedName{ + Name: ingressName, + Namespace: s.Namespace(), + }, ing); err != nil { + return nil, err + } + return ing.Status.LoadBalancer.Ingress, nil + } + + createIngressClass := func(ingressClassName string) { + By("create IngressClass") + Expect(s.CreateResourceFromStringWithNamespace( + fmt.Sprintf(ingressClassYaml, ingressClassName, s.GetControllerName(), s.Namespace()), ""), + ).NotTo(HaveOccurred(), "creating IngressClass") + DeferCleanup(func() { + ingressClass := &networkingv1.IngressClass{} + if err := s.K8sClient.Get(context.Background(), types.NamespacedName{ + Name: ingressClassName, + }, ingressClass); err == nil { + _ = s.K8sClient.Delete(context.Background(), ingressClass) + } + }) + } + + checkIngressStatusAddress := func(addrValue, ingressSuffix, expectedIP, expectedHostname string) { + ingressClassName := s.Namespace() + + By("create GatewayProxy with statusAddress") + gatewayProxy := fmt.Sprintf(gatewayProxyWithStatusAddressYaml, + s.Namespace(), addrValue, s.Deployer.GetAdminEndpoint(), s.AdminKey()) + Expect(s.CreateResourceFromStringWithNamespace(gatewayProxy, s.Namespace())).NotTo(HaveOccurred(), "creating GatewayProxy") + + createIngressClass(ingressClassName) + + By("create Ingress") + ingressName := s.Namespace() + ingressSuffix + Expect(s.CreateResourceFromStringWithNamespace( + fmt.Sprintf(ingressYaml, ingressName, ingressClassName, "httpbin-service-e2e-test"), s.Namespace()), + ).NotTo(HaveOccurred(), "creating Ingress") + + By("check Ingress load balancer status") + s.RetryAssertion(func() error { + lbs, err := getIngressLBStatus(ingressName) + if err != nil { + return err + } + if len(lbs) == 0 { + return fmt.Errorf("expected at least 1 load balancer ingress, got 0") + } + if lbs[0].IP != expectedIP { + return fmt.Errorf("expected IP %s, got %s", expectedIP, lbs[0].IP) + } + if lbs[0].Hostname != expectedHostname { + return fmt.Errorf("expected Hostname %s, got %s", expectedHostname, lbs[0].Hostname) + } + return nil + }).ShouldNot(HaveOccurred(), "check Ingress load balancer status") + } + + It("sets IP field when statusAddress is an IP", func() { + checkIngressStatusAddress("192.168.1.200", "-ip", "192.168.1.200", "") + }) + + It("sets Hostname field when statusAddress is a hostname", func() { + checkIngressStatusAddress("myingress.example.com", "-host", "", "myingress.example.com") + }) + + It("deduplicates repeated statusAddress entries", func() { + addr := "192.168.1.200" + ingressClassName := s.Namespace() + + 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.CreateResourceFromStringWithNamespace(gatewayProxy, s.Namespace())).NotTo(HaveOccurred(), "creating GatewayProxy") + + createIngressClass(ingressClassName) + + By("create Ingress") + ingressName := s.Namespace() + "-dedup" + Expect(s.CreateResourceFromStringWithNamespace( + fmt.Sprintf(ingressYaml, ingressName, ingressClassName, "httpbin-service-e2e-test"), s.Namespace()), + ).NotTo(HaveOccurred(), "creating Ingress") + + By("verify only one address appears in Ingress status despite duplicate input") + s.RetryAssertion(func() error { + lbs, err := getIngressLBStatus(ingressName) + if err != nil { + return err + } + if len(lbs) != 1 { + return fmt.Errorf("expected exactly 1 load balancer ingress after dedup, got %d", len(lbs)) + } + if lbs[0].IP != addr { + return fmt.Errorf("expected IP %s, got %s", addr, lbs[0].IP) + } + return nil + }).ShouldNot(HaveOccurred(), "check Ingress load balancer status deduplication") + }) + + It("propagates hostname from referencing Ingress when publishService is a ClusterIP", func() { + clusterIPSvcName := "apisix-clusterip-svc" + expectedHostname := "clusterip.example.com" + ingressClassName := s.Namespace() + + By("create ClusterIP service") + clusterIPSvc := fmt.Sprintf(` +apiVersion: v1 +kind: Service +metadata: + name: %s + namespace: %s +spec: + type: ClusterIP + selector: + app: httpbin + ports: + - port: 80 + targetPort: 80 +`, clusterIPSvcName, s.Namespace()) + Expect(s.CreateResourceFromStringWithNamespace(clusterIPSvc, s.Namespace())).NotTo(HaveOccurred(), "creating ClusterIP service") + + By("create source Ingress referencing the ClusterIP service") + sourceIngressName := s.Namespace() + "-source" + // The source Ingress intentionally has no ingressClassName — it simulates a + // cloud ALB Ingress managed by a different controller. Without ingressClassName + // our controller will not reconcile it and will not overwrite its status. + sourceIngressYaml := fmt.Sprintf(` +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: %s +spec: + rules: + - host: clusterip.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: %s + port: + number: 80 +`, sourceIngressName, clusterIPSvcName) + Expect(s.CreateResourceFromStringWithNamespace(sourceIngressYaml, s.Namespace())).NotTo(HaveOccurred(), "creating source Ingress") + + By("patch source Ingress status with hostname") + sourceIng := &networkingv1.Ingress{} + Expect(s.K8sClient.Get(context.Background(), types.NamespacedName{ + Name: sourceIngressName, Namespace: s.Namespace(), + }, sourceIng)).NotTo(HaveOccurred(), "getting source Ingress") + sourceIng.Status.LoadBalancer.Ingress = []networkingv1.IngressLoadBalancerIngress{ + {Hostname: expectedHostname}, + } + Expect(s.K8sClient.Status().Update(context.Background(), sourceIng)).NotTo(HaveOccurred(), "patching source Ingress status") + + By("create GatewayProxy with ClusterIP publishService") + gatewayProxy := fmt.Sprintf(gatewayProxyWithPublishServiceYaml, + s.Namespace(), s.Namespace(), clusterIPSvcName, s.Deployer.GetAdminEndpoint(), s.AdminKey()) + Expect(s.CreateResourceFromStringWithNamespace(gatewayProxy, s.Namespace())).NotTo(HaveOccurred(), "creating GatewayProxy") + + createIngressClass(ingressClassName) + + By("create main Ingress") + mainIngressName := s.Namespace() + "-main" + mainIngress := fmt.Sprintf(` +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: %s +spec: + ingressClassName: %s + rules: + - host: status.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: httpbin-service-e2e-test + port: + number: 80 +`, mainIngressName, ingressClassName) + Expect(s.CreateResourceFromStringWithNamespace(mainIngress, s.Namespace())).NotTo(HaveOccurred(), "creating main Ingress") + + By("check main Ingress status propagates hostname from source Ingress") + s.RetryAssertion(func() error { + lbs, err := getIngressLBStatus(mainIngressName) + if err != nil { + return err + } + if len(lbs) == 0 { + return fmt.Errorf("expected at least 1 load balancer ingress, got 0") + } + if lbs[0].Hostname != expectedHostname { + return fmt.Errorf("expected Hostname %s, got %s", expectedHostname, lbs[0].Hostname) + } + return nil + }).ShouldNot(HaveOccurred(), "check main Ingress load balancer status from ClusterIP") + }) + + }) })