Copilot commented on code in PR #843:
URL: https://github.com/apache/solr-operator/pull/843#discussion_r4467945515


##########
controllers/solr_gateway_util.go:
##########
@@ -0,0 +1,339 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package controllers
+
+import (
+       "context"
+       "fmt"
+
+       solrv1beta1 "github.com/apache/solr-operator/api/v1beta1"
+       "github.com/apache/solr-operator/controllers/util"
+       "github.com/go-logr/logr"
+       corev1 "k8s.io/api/core/v1"
+       "k8s.io/apimachinery/pkg/api/errors"
+       "k8s.io/apimachinery/pkg/labels"
+       "k8s.io/apimachinery/pkg/types"
+       "sigs.k8s.io/controller-runtime/pkg/client"
+       "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+)
+
+func (r *SolrCloudReconciler) reconcileGatewayResources(ctx context.Context, 
cloud *solrv1beta1.SolrCloud, solrNodeNames []string, logger logr.Logger) error 
{
+       extAddressabilityOpts := cloud.Spec.SolrAddressability.External
+
+       // If not using Gateway method, clean up any existing HTTPRoutes and 
BackendTLSPolicy resources
+       if extAddressabilityOpts == nil || extAddressabilityOpts.Method != 
solrv1beta1.Gateway {
+               return r.cleanupGatewayResources(ctx, cloud, logger)
+       }
+
+       // Validate that gateway config is provided
+       if extAddressabilityOpts.Gateway == nil || 
len(extAddressabilityOpts.Gateway.ParentRefs) == 0 {
+               r.Recorder.Event(cloud, corev1.EventTypeWarning, 
"InvalidConfiguration", "gateway.parentRefs is required when using 
method=Gateway")
+               return fmt.Errorf("gateway.parentRefs is required when using 
method=Gateway")
+       }
+
+       // Validate that BackendTLSPolicy requires solrTLS to be configured
+       if extAddressabilityOpts.HasBackendTLSPolicy() && cloud.Spec.SolrTLS == 
nil {
+               r.Recorder.Event(cloud, corev1.EventTypeWarning, 
"InvalidConfiguration", "gateway.backendTLSPolicy requires spec.solrTLS to be 
configured, because BackendTLSPolicy instructs the gateway to use TLS when 
connecting to Solr backends")
+               return fmt.Errorf("invalid config: 
`spec.customSolrKubeOptions.addressability.external.gateway.backendTLSPolicy` 
requires `spec.solrTLS` to be configured, because BackendTLSPolicy instructs 
the gateway to use TLS when connecting to Solr backends")
+       }
+
+       if err := r.reconcileGatewayHTTPRoutes(ctx, cloud, 
extAddressabilityOpts, solrNodeNames, logger); err != nil {
+               return err
+       }
+
+       return r.reconcileGatewayBackendTLSPolicies(ctx, cloud, 
extAddressabilityOpts, solrNodeNames, logger)
+}
+
+func (r *SolrCloudReconciler) reconcileGatewayHTTPRoutes(ctx context.Context, 
cloud *solrv1beta1.SolrCloud, extAddressabilityOpts 
*solrv1beta1.ExternalAddressability, solrNodeNames []string, logger 
logr.Logger) error {
+       // Reconcile Common HTTPRoute (if not hidden)
+       if !extAddressabilityOpts.HideCommon {
+               commonHTTPRoute := util.GenerateCommonHTTPRoute(cloud, 
solrNodeNames)
+               commonHTTPRouteLogger := logger.WithValues("httproute", 
commonHTTPRoute.Name)
+               foundCommonHTTPRoute := &gatewayv1.HTTPRoute{}
+               err := r.Get(ctx, types.NamespacedName{Name: 
commonHTTPRoute.Name, Namespace: commonHTTPRoute.Namespace}, 
foundCommonHTTPRoute)
+               if err != nil && errors.IsNotFound(err) {
+                       commonHTTPRouteLogger.Info("Creating common HTTPRoute")
+                       if err = controllerutil.SetControllerReference(cloud, 
commonHTTPRoute, r.Scheme); err == nil {
+                               err = r.Create(ctx, commonHTTPRoute)
+                       }
+               } else if err == nil {
+                       var needsUpdate bool
+                       needsUpdate, err = util.OvertakeControllerRef(cloud, 
foundCommonHTTPRoute, r.Scheme)
+                       needsUpdate = util.CopyHTTPRouteFields(commonHTTPRoute, 
foundCommonHTTPRoute, commonHTTPRouteLogger) || needsUpdate
+
+                       if needsUpdate && err == nil {
+                               commonHTTPRouteLogger.Info("Updating common 
HTTPRoute")
+                               err = r.Update(ctx, foundCommonHTTPRoute)
+                       }
+               }
+               if err != nil {
+                       return err
+               }
+       } else {
+               // Delete common HTTPRoute if it exists but should be hidden
+               foundCommonHTTPRoute := &gatewayv1.HTTPRoute{}
+               err := r.Get(ctx, types.NamespacedName{Name: 
cloud.CommonHTTPRouteName(), Namespace: cloud.GetNamespace()}, 
foundCommonHTTPRoute)
+               if err == nil {
+                       logger.Info("Deleting common HTTPRoute 
(hideCommon=true)")
+                       err = r.Delete(ctx, foundCommonHTTPRoute)
+                       if err != nil && !errors.IsNotFound(err) {
+                               return err
+                       }
+               }
+       }
+
+       // Reconcile Node HTTPRoutes (if not hidden)
+       if !extAddressabilityOpts.HideNodes {
+               for _, nodeName := range solrNodeNames {
+                       nodeHTTPRoute := util.GenerateNodeHTTPRoute(cloud, 
nodeName)
+                       nodeHTTPRouteLogger := logger.WithValues("httproute", 
nodeHTTPRoute.Name)
+                       foundNodeHTTPRoute := &gatewayv1.HTTPRoute{}
+                       err := r.Get(ctx, types.NamespacedName{Name: 
nodeHTTPRoute.Name, Namespace: nodeHTTPRoute.Namespace}, foundNodeHTTPRoute)
+                       if err != nil && errors.IsNotFound(err) {
+                               nodeHTTPRouteLogger.Info("Creating node 
HTTPRoute")
+                               if err = 
controllerutil.SetControllerReference(cloud, nodeHTTPRoute, r.Scheme); err == 
nil {
+                                       err = r.Create(ctx, nodeHTTPRoute)
+                               }
+                       } else if err == nil {
+                               var needsUpdate bool
+                               needsUpdate, err = 
util.OvertakeControllerRef(cloud, foundNodeHTTPRoute, r.Scheme)
+                               needsUpdate = 
util.CopyHTTPRouteFields(nodeHTTPRoute, foundNodeHTTPRoute, 
nodeHTTPRouteLogger) || needsUpdate
+
+                               if needsUpdate && err == nil {
+                                       nodeHTTPRouteLogger.Info("Updating node 
HTTPRoute")
+                                       err = r.Update(ctx, foundNodeHTTPRoute)
+                               }
+                       }
+                       if err != nil {
+                               return err
+                       }
+               }
+       }
+
+       // Cleanup orphaned node HTTPRoutes (when scaling down or 
hideNodes=true)
+       httpRouteList := &gatewayv1.HTTPRouteList{}
+       listOps := &client.ListOptions{
+               Namespace:     cloud.Namespace,
+               LabelSelector: labels.SelectorFromSet(cloud.SharedLabels()),
+       }
+       if err := r.List(ctx, httpRouteList, listOps); err != nil {
+               return err
+       }
+       for _, httpRoute := range httpRouteList.Items {
+               // Skip the common HTTPRoute
+               if httpRoute.Name == cloud.CommonHTTPRouteName() {
+                       continue
+               }
+
+               // Delete if hideNodes is true, or if this node no longer 
exists (scale-down)
+               shouldDelete := extAddressabilityOpts.HideNodes
+               if !shouldDelete {
+                       nodeExists := false
+                       for _, nodeName := range solrNodeNames {
+                               if httpRoute.Name == 
cloud.NodeHTTPRouteName(nodeName) {
+                                       nodeExists = true
+                                       break
+                               }
+                       }
+                       shouldDelete = !nodeExists
+               }
+
+               if shouldDelete {
+                       if extAddressabilityOpts.HideNodes {
+                               logger.Info("Deleting node HTTPRoute 
(hideNodes=true)", "httproute", httpRoute.Name)
+                       } else {
+                               logger.Info("Deleting orphaned node HTTPRoute", 
"httproute", httpRoute.Name)
+                       }
+                       if err := r.Delete(ctx, &httpRoute); err != nil && 
!errors.IsNotFound(err) {
+                               return err
+                       }
+               }
+       }
+
+       return nil
+}
+
+func (r *SolrCloudReconciler) reconcileGatewayBackendTLSPolicies(ctx 
context.Context, cloud *solrv1beta1.SolrCloud, extAddressabilityOpts 
*solrv1beta1.ExternalAddressability, solrNodeNames []string, logger 
logr.Logger) error {
+       listOps := &client.ListOptions{
+               Namespace:     cloud.Namespace,
+               LabelSelector: labels.SelectorFromSet(cloud.SharedLabels()),
+       }
+
+       // If BackendTLSPolicy is not configured, clean up any existing 
BackendTLSPolicy resources
+       if !extAddressabilityOpts.HasBackendTLSPolicy() {
+               backendTLSPolicyList := &gatewayv1.BackendTLSPolicyList{}
+               if err := r.List(ctx, backendTLSPolicyList, listOps); err != 
nil {
+                       return err
+               }
+               if len(backendTLSPolicyList.Items) > 0 {
+                       logger.Info("Cleaning up BackendTLSPolicy resources 
(BackendTLSPolicy disabled)")
+                       for _, policy := range backendTLSPolicyList.Items {
+                               if err := r.Delete(ctx, &policy); err != nil && 
!errors.IsNotFound(err) {
+                                       return err
+                               }
+                       }
+               }
+               return nil
+       }
+
+       // Reconcile Common BackendTLSPolicy (if not hidden)
+       if !extAddressabilityOpts.HideCommon {
+               commonPolicy := util.GenerateCommonBackendTLSPolicy(cloud)
+               if commonPolicy != nil {
+                       commonPolicyLogger := 
logger.WithValues("backendtlspolicy", commonPolicy.Name)
+                       foundCommonPolicy := &gatewayv1.BackendTLSPolicy{}
+                       err := r.Get(ctx, types.NamespacedName{Name: 
commonPolicy.Name, Namespace: commonPolicy.Namespace}, foundCommonPolicy)
+                       if err != nil && errors.IsNotFound(err) {
+                               commonPolicyLogger.Info("Creating common 
BackendTLSPolicy")
+                               if err = 
controllerutil.SetControllerReference(cloud, commonPolicy, r.Scheme); err == 
nil {
+                                       err = r.Create(ctx, commonPolicy)
+                               }
+                       } else if err == nil {
+                               var needsUpdate bool
+                               needsUpdate, err = 
util.OvertakeControllerRef(cloud, foundCommonPolicy, r.Scheme)
+                               needsUpdate = 
util.CopyBackendTLSPolicyFields(commonPolicy, foundCommonPolicy, 
commonPolicyLogger) || needsUpdate
+
+                               if needsUpdate && err == nil {
+                                       commonPolicyLogger.Info("Updating 
common BackendTLSPolicy")
+                                       err = r.Update(ctx, foundCommonPolicy)
+                               }
+                       }
+                       if err != nil {
+                               return err
+                       }
+               }
+       } else {
+               // Delete common BackendTLSPolicy if it exists but should be 
hidden
+               foundCommonPolicy := &gatewayv1.BackendTLSPolicy{}
+               err := r.Get(ctx, types.NamespacedName{Name: 
cloud.CommonBackendTLSPolicyName(), Namespace: cloud.GetNamespace()}, 
foundCommonPolicy)
+               if err == nil {
+                       logger.Info("Deleting common BackendTLSPolicy 
(hideCommon=true)")
+                       err = r.Delete(ctx, foundCommonPolicy)
+                       if err != nil && !errors.IsNotFound(err) {
+                               return err
+                       }
+               }
+       }
+
+       // Reconcile Node BackendTLSPolicies (if not hidden)
+       if !extAddressabilityOpts.HideNodes {
+               for _, nodeName := range solrNodeNames {
+                       nodePolicy := util.GenerateNodeBackendTLSPolicy(cloud, 
nodeName)
+                       if nodePolicy != nil {
+                               nodePolicyLogger := 
logger.WithValues("backendtlspolicy", nodePolicy.Name)
+                               foundNodePolicy := &gatewayv1.BackendTLSPolicy{}
+                               err := r.Get(ctx, types.NamespacedName{Name: 
nodePolicy.Name, Namespace: nodePolicy.Namespace}, foundNodePolicy)
+                               if err != nil && errors.IsNotFound(err) {
+                                       nodePolicyLogger.Info("Creating node 
BackendTLSPolicy")
+                                       if err = 
controllerutil.SetControllerReference(cloud, nodePolicy, r.Scheme); err == nil {
+                                               err = r.Create(ctx, nodePolicy)
+                                       }
+                               } else if err == nil {
+                                       var needsUpdate bool
+                                       needsUpdate, err = 
util.OvertakeControllerRef(cloud, foundNodePolicy, r.Scheme)
+                                       needsUpdate = 
util.CopyBackendTLSPolicyFields(nodePolicy, foundNodePolicy, nodePolicyLogger) 
|| needsUpdate
+
+                                       if needsUpdate && err == nil {
+                                               nodePolicyLogger.Info("Updating 
node BackendTLSPolicy")
+                                               err = r.Update(ctx, 
foundNodePolicy)
+                                       }
+                               }
+                               if err != nil {
+                                       return err
+                               }
+                       }
+               }
+       }
+
+       // Cleanup node BackendTLSPolicies (when hideNodes is true or when 
scaling down)
+       backendTLSPolicyList := &gatewayv1.BackendTLSPolicyList{}
+       if err := r.List(ctx, backendTLSPolicyList, listOps); err != nil {
+               return err
+       }
+       for _, policy := range backendTLSPolicyList.Items {
+               // Skip the common BackendTLSPolicy
+               if policy.Name == cloud.CommonBackendTLSPolicyName() {
+                       continue
+               }
+
+               // Delete if hideNodes is true, or if this node no longer 
exists (scale-down)
+               shouldDelete := extAddressabilityOpts.HideNodes
+               if !shouldDelete {
+                       // Check if this node still exists
+                       nodeExists := false
+                       for _, nodeName := range solrNodeNames {
+                               if policy.Name == 
cloud.NodeBackendTLSPolicyName(nodeName) {
+                                       nodeExists = true
+                                       break
+                               }
+                       }
+                       shouldDelete = !nodeExists
+               }
+
+               if shouldDelete {
+                       if extAddressabilityOpts.HideNodes {
+                               logger.Info("Deleting node BackendTLSPolicy 
(hideNodes=true)", "backendtlspolicy", policy.Name)
+                       } else {
+                               logger.Info("Deleting orphaned node 
BackendTLSPolicy", "backendtlspolicy", policy.Name)
+                       }
+                       if err := r.Delete(ctx, &policy); err != nil && 
!errors.IsNotFound(err) {
+                               return err
+                       }
+               }
+       }
+
+       return nil
+}
+
+func (r *SolrCloudReconciler) cleanupGatewayResources(ctx context.Context, 
cloud *solrv1beta1.SolrCloud, logger logr.Logger) error {
+       listOps := &client.ListOptions{
+               Namespace:     cloud.Namespace,
+               LabelSelector: labels.SelectorFromSet(cloud.SharedLabels()),

Review Comment:
   Label-only cleanup deletes independently managed HTTPRoutes and 
BackendTLSPolicies carrying `solr-cloud: <name>`, including for clouds that 
have never used Gateway mode. Require a controller owner reference matching 
this SolrCloud's UID before deleting any listed resource. Apply the same guard 
to orphan and disabled-policy cleanup in this file at lines 138, 186, and 269.



##########
api/v1beta1/solrcloud_types.go:
##########
@@ -626,6 +639,107 @@ type SolrIngressTLSTermination struct {
        TLSSecret string `json:"tlsSecret,omitempty"`
 }
 
+// SolrGatewayOptions defines how a SolrCloud should be exposed via Kubernetes 
Gateway API
+type SolrGatewayOptions struct {
+       // ParentRefs specifies the Gateway(s) to attach HTTPRoutes to.
+       // This is required when using method=Gateway.
+       //
+       // The referenced Gateway must already exist and be managed by your 
platform team.
+       // The Solr Operator only manages the HTTPRoute resources.
+       //
+       // +kubebuilder:validation:MinItems=1
+       ParentRefs []GatewayParentReference `json:"parentRefs"`
+
+       // AdditionalHostnames specifies extra hostnames to include in the 
common HTTPRoute.
+       // These are appended to the auto-generated hostnames derived from 
DomainName and AdditionalDomainNames.
+       // This is useful for adding alias hostnames that should also route to 
the common Solr service.
+       //
+       // +optional
+       // +kubebuilder:validation:MaxItems=16
+       AdditionalHostnames []string `json:"additionalHostnames,omitempty"`

Review Comment:
   HTTPRoute permits at most 16 hostnames in total, but this field allows 16 
additional hostnames before `GenerateCommonHTTPRoute()` prepends the primary 
domain and any additional domains. Supplying the allowed 16 aliases therefore 
always produces an invalid route (at least 17 entries), causing route creation 
to fail and reconciliation to return an error. Please validate the combined 
hostname count, or split the hostnames across multiple routes, rather than 
limiting only this slice.



##########
controllers/util/gateway_util.go:
##########
@@ -0,0 +1,214 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package util
+
+import (
+       solr "github.com/apache/solr-operator/api/v1beta1"
+       "github.com/go-logr/logr"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+)
+
+// GenerateCommonHTTPRoute creates an HTTPRoute for the common Solr service
+func GenerateCommonHTTPRoute(solrCloud *solr.SolrCloud, nodeNames []string) 
*gatewayv1.HTTPRoute {
+       labels := solrCloud.SharedLabelsWith(solrCloud.GetLabels())
+       var annotations map[string]string
+
+       gatewayOpts := solrCloud.Spec.SolrAddressability.External.Gateway
+       if gatewayOpts != nil {
+               labels = MergeLabelsOrAnnotations(labels, gatewayOpts.Labels)
+               annotations = MergeLabelsOrAnnotations(annotations, 
gatewayOpts.Annotations)
+       }
+
+       extOpts := solrCloud.Spec.SolrAddressability.External
+
+       // Create advertised domain name and possible additional domain names
+       allDomains := append([]string{extOpts.DomainName}, 
extOpts.AdditionalDomainNames...)
+       hostnames := make([]gatewayv1.Hostname, 0, 
len(allDomains)+len(gatewayOpts.AdditionalHostnames))
+       for _, domain := range allDomains {
+               hostname := 
gatewayv1.Hostname(solrCloud.ExternalCommonUrl(domain, false))
+               hostnames = append(hostnames, hostname)
+       }
+       // Append user-specified additional hostnames for the common route
+       for _, h := range gatewayOpts.AdditionalHostnames {
+               hostnames = append(hostnames, gatewayv1.Hostname(h))
+       }
+
+       // Convert parentRefs from our type to Gateway API type
+       parentRefs := make([]gatewayv1.ParentReference, 
len(gatewayOpts.ParentRefs))
+       for i, ref := range gatewayOpts.ParentRefs {
+               parentRef := gatewayv1.ParentReference{
+                       Name: gatewayv1.ObjectName(ref.Name),
+               }
+               if ref.Namespace != nil {
+                       namespace := gatewayv1.Namespace(*ref.Namespace)
+                       parentRef.Namespace = &namespace
+               }
+               if ref.SectionName != nil {
+                       sectionName := gatewayv1.SectionName(*ref.SectionName)
+                       parentRef.SectionName = &sectionName
+               }
+               parentRefs[i] = parentRef
+       }
+
+       // Determine backend port
+       backendPort := 
gatewayv1.PortNumber(solrCloud.Spec.SolrAddressability.CommonServicePort)
+
+       httpRoute := &gatewayv1.HTTPRoute{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:        solrCloud.CommonHTTPRouteName(),
+                       Namespace:   solrCloud.GetNamespace(),
+                       Labels:      labels,
+                       Annotations: annotations,
+               },
+               Spec: gatewayv1.HTTPRouteSpec{
+                       CommonRouteSpec: gatewayv1.CommonRouteSpec{
+                               ParentRefs: parentRefs,
+                       },
+                       Hostnames: hostnames,
+                       Rules: []gatewayv1.HTTPRouteRule{
+                               {
+                                       BackendRefs: []gatewayv1.HTTPBackendRef{
+                                               {
+                                                       BackendRef: 
gatewayv1.BackendRef{
+                                                               
BackendObjectReference: gatewayv1.BackendObjectReference{
+                                                                       Name: 
gatewayv1.ObjectName(solrCloud.CommonServiceName()),
+                                                                       Port: 
&backendPort,
+                                                               },
+                                                       },
+                                               },
+                                       },
+                               },
+                       },
+               },
+       }
+
+       return httpRoute
+}
+
+// GenerateNodeHTTPRoute creates an HTTPRoute for individual Solr nodes
+func GenerateNodeHTTPRoute(solrCloud *solr.SolrCloud, nodeName string) 
*gatewayv1.HTTPRoute {
+       labels := solrCloud.SharedLabelsWith(solrCloud.GetLabels())
+       var annotations map[string]string
+
+       gatewayOpts := solrCloud.Spec.SolrAddressability.External.Gateway
+       labels = MergeLabelsOrAnnotations(labels, gatewayOpts.Labels)
+       annotations = MergeLabelsOrAnnotations(annotations, 
gatewayOpts.Annotations)
+
+       extOpts := solrCloud.Spec.SolrAddressability.External
+
+       // Create hostnames for all domains
+       allDomains := append([]string{extOpts.DomainName}, 
extOpts.AdditionalDomainNames...)
+       hostnames := make([]gatewayv1.Hostname, 0, len(allDomains))
+       for _, domain := range allDomains {
+               hostname := 
gatewayv1.Hostname(solrCloud.ExternalNodeUrl(nodeName, domain, false))
+               hostnames = append(hostnames, hostname)
+       }
+
+       // Convert parentRefs
+       parentRefs := make([]gatewayv1.ParentReference, 
len(gatewayOpts.ParentRefs))
+       for i, ref := range gatewayOpts.ParentRefs {
+               parentRef := gatewayv1.ParentReference{
+                       Name: gatewayv1.ObjectName(ref.Name),
+               }
+               if ref.Namespace != nil {
+                       namespace := gatewayv1.Namespace(*ref.Namespace)
+                       parentRef.Namespace = &namespace
+               }
+               if ref.SectionName != nil {
+                       sectionName := gatewayv1.SectionName(*ref.SectionName)
+                       parentRef.SectionName = &sectionName
+               }
+               parentRefs[i] = parentRef
+       }
+
+       // Determine backend port (uses NodePort which may be overridden)
+       backendPort := gatewayv1.PortNumber(solrCloud.NodePort())
+
+       httpRoute := &gatewayv1.HTTPRoute{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:        solrCloud.NodeHTTPRouteName(nodeName),
+                       Namespace:   solrCloud.GetNamespace(),
+                       Labels:      labels,
+                       Annotations: annotations,
+               },
+               Spec: gatewayv1.HTTPRouteSpec{
+                       CommonRouteSpec: gatewayv1.CommonRouteSpec{
+                               ParentRefs: parentRefs,
+                       },
+                       Hostnames: hostnames,
+                       Rules: []gatewayv1.HTTPRouteRule{
+                               {
+                                       BackendRefs: []gatewayv1.HTTPBackendRef{
+                                               {
+                                                       BackendRef: 
gatewayv1.BackendRef{
+                                                               
BackendObjectReference: gatewayv1.BackendObjectReference{
+                                                                       Name: 
gatewayv1.ObjectName(nodeName),
+                                                                       Port: 
&backendPort,
+                                                               },
+                                                       },
+                                               },
+                                       },
+                               },
+                       },
+               },
+       }
+
+       return httpRoute
+}
+
+// CopyHTTPRouteFields copies the fields from one HTTPRoute to another
+// Returns true if there are differences between the two objects
+func CopyHTTPRouteFields(from, to *gatewayv1.HTTPRoute, logger logr.Logger) 
bool {
+       requireUpdate := false
+
+       // Copy labels
+       if !DeepEqualWithNils(to.Labels, from.Labels) {
+               logger.Info("HTTPRoute labels have changed")
+               requireUpdate = true
+               to.Labels = from.Labels
+       }
+
+       // Copy annotations
+       if !DeepEqualWithNils(to.Annotations, from.Annotations) {
+               logger.Info("HTTPRoute annotations have changed")
+               requireUpdate = true
+               to.Annotations = from.Annotations
+       }
+
+       // Copy spec
+       if !DeepEqualWithNils(to.Spec.ParentRefs, from.Spec.ParentRefs) {
+               logger.Info("HTTPRoute parentRefs have changed")
+               requireUpdate = true
+               to.Spec.ParentRefs = from.Spec.ParentRefs

Review Comment:
   The generated routes omit fields that the Gateway API defaults, including 
parent-reference group/kind and rule matches, backend group/kind, and weight. 
These deep comparisons therefore report differences after an otherwise correct 
route is read back, causing unnecessary updates for every route on subsequent 
reconciliations. Populate or normalize those defaults in both route generators 
or before comparison, and add a round-trip test showing that an unchanged route 
requires no update.



##########
api/v1beta1/solrcloud_types.go:
##########
@@ -1340,8 +1474,8 @@ func (sc *SolrCloud) UsesIndividualNodeServices() bool {
 }
 
 func (extOpts *ExternalAddressability) UsesIndividualNodeServices() bool {
-       // LoadBalancer and Ingress will not work with headless services if 
each pod needs to be exposed externally.
-       return extOpts != nil && !extOpts.HideNodes && (extOpts.Method == 
Ingress || extOpts.Method == LoadBalancer)
+       // LoadBalancer, Ingress, and Gateway will not work with headless 
services if each pod needs to be exposed externally.
+       return extOpts != nil && !extOpts.HideNodes && (extOpts.Method == 
Ingress || extOpts.Method == LoadBalancer || extOpts.Method == Gateway)

Review Comment:
   Gateway uses per-node Services, but `ExternalAddressability.withDefaults()` 
still defaults `nodePortOverride` to 80/443 only for Ingress. With 
`useExternalAddress: true` and no override, Solr advertises the pod port 
(8983), so clients discovering nodes through ZooKeeper target an unserved port 
on a Gateway listening on 80/443. Extend the port defaulting to Gateway while 
preserving explicit overrides, and add a regression test for the advertised 
port.



##########
tests/e2e/solrcloud_gateway_test.go:
##########
@@ -0,0 +1,282 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package e2e
+
+import (
+       "context"
+       solrv1beta1 "github.com/apache/solr-operator/api/v1beta1"
+       "github.com/apache/solr-operator/controllers/util"
+       . "github.com/onsi/ginkgo/v2"
+       . "github.com/onsi/gomega"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+)
+
+var _ = Describe("E2E - SolrCloud - Gateway API", func() {

Review Comment:
   These specs are not programmatically focused, whereas the existing e2e 
suites use `FDescribe`/`FIt` (for example, `solrcloud_ingress_test.go:29,62`). 
Ginkgo's focus filtering therefore skips all three new Gateway cases in the 
normal suite run, leaving this feature unexercised. Please reconcile the 
suite's focus configuration so these cases are included, preferably by removing 
the existing programmatic focus rather than adding more focused tests.



##########
tests/e2e/solrcloud_gateway_test.go:
##########
@@ -0,0 +1,282 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package e2e
+
+import (
+       "context"
+       solrv1beta1 "github.com/apache/solr-operator/api/v1beta1"
+       "github.com/apache/solr-operator/controllers/util"
+       . "github.com/onsi/ginkgo/v2"
+       . "github.com/onsi/gomega"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+)
+
+var _ = Describe("E2E - SolrCloud - Gateway API", func() {
+       var (
+               solrCloud        *solrv1beta1.SolrCloud
+               gatewayNamespace = "default"
+               gatewayName      = "test-gateway"
+       )
+
+       BeforeEach(func() {
+               solrCloud = generateBaseSolrCloud(1)
+               solrCloud.Spec.SolrAddressability = 
solrv1beta1.SolrAddressabilityOptions{
+                       External: &solrv1beta1.ExternalAddressability{
+                               Method:             solrv1beta1.Gateway,
+                               UseExternalAddress: true,
+                               DomainName:         testDomain,
+                               Gateway: &solrv1beta1.SolrGatewayOptions{
+                                       ParentRefs: 
[]solrv1beta1.GatewayParentReference{
+                                               {
+                                                       Name:      gatewayName,
+                                                       Namespace: 
&gatewayNamespace,
+                                               },
+                                       },
+                               },
+                       },
+               }
+       })
+
+       JustBeforeEach(func(ctx context.Context) {
+               By("creating the SolrCloud")
+               Expect(k8sClient.Create(ctx, solrCloud)).To(Succeed())
+
+               DeferCleanup(func(ctx context.Context) {
+                       cleanupTest(ctx, solrCloud)
+               })
+
+               By("Waiting for the SolrCloud to come up healthy")
+               solrCloud = expectSolrCloudToBeReady(ctx, solrCloud)
+
+               By("creating a first Solr Collection")
+               createAndQueryCollection(ctx, solrCloud, "basic", 1, 1)
+       })
+
+       Context("Can Remove HTTPRoutes and Services when changing 
addressability", func() {
+
+               It("Can adapt to changing needs", func(ctx context.Context) {
+                       By("testing the Solr StatefulSet")
+                       statefulSet := expectStatefulSet(ctx, solrCloud, 
solrCloud.StatefulSetName())
+                       // Pod Annotations test
+                       
Expect(statefulSet.Spec.Template.Annotations).To(HaveKeyWithValue(util.ServiceTypeAnnotation,
 util.PerNodeServiceType), "Since external address is used for advertising, the 
perNode service should be specified in the pod annotations.")
+
+                       By("testing the Solr Common Service")
+                       expectService(ctx, solrCloud, 
solrCloud.CommonServiceName(), statefulSet.Spec.Selector.MatchLabels, false)
+
+                       By("ensuring the Solr Headless Service does not exist")
+                       expectNoService(ctx, solrCloud, 
solrCloud.HeadlessServiceName(), "Headless service shouldn't exist, but it 
does.")
+
+                       By("making sure the individual Solr Node Services exist 
and route correctly")
+                       nodeNames := solrCloud.GetAllSolrPodNames()
+                       Expect(nodeNames).To(HaveLen(1), "SolrCloud has 
incorrect number of nodeNames.")
+                       for _, nodeName := range nodeNames {
+                               expectService(ctx, solrCloud, nodeName, 
util.MergeLabelsOrAnnotations(statefulSet.Spec.Selector.MatchLabels, 
map[string]string{"statefulset.kubernetes.io/pod-name": nodeName}), false)
+                       }
+
+                       By("making sure Common HTTPRoute was created correctly")
+                       expectHTTPRoute(ctx, solrCloud, 
solrCloud.CommonHTTPRouteName())
+
+                       By("making sure Node HTTPRoutes were created correctly")
+                       for _, nodeName := range nodeNames {
+                               expectHTTPRoute(ctx, solrCloud, 
solrCloud.NodeHTTPRouteName(nodeName))
+                       }
+
+                       By("Turning off node external addressability and making 
sure the node services are deleted")
+                       expectSolrCloudWithChecks(ctx, solrCloud, func(g 
Gomega, found *solrv1beta1.SolrCloud) {
+                               
found.Spec.SolrAddressability.External.HideNodes = true
+                               
found.Spec.SolrAddressability.External.UseExternalAddress = false
+                               g.Expect(k8sClient.Update(ctx, 
found)).To(Succeed(), "Couldn't update the solrCloud to not advertise the nodes 
externally.")
+                       })
+
+                       // Since node external addressability is off, but 
common external addressability is on, the common HTTPRoute should exist, but 
the node HTTPRoutes should not
+                       expectHTTPRoute(ctx, solrCloud, 
solrCloud.CommonHTTPRouteName())
+
+                       expectService(ctx, solrCloud, 
solrCloud.HeadlessServiceName(), statefulSet.Spec.Selector.MatchLabels, true)
+
+                       for _, nodeName := range nodeNames {
+                               eventuallyExpectNoHTTPRoute(ctx, solrCloud, 
solrCloud.NodeHTTPRouteName(nodeName))
+                       }
+                       pod := expectPodNow(ctx, solrCloud, 
solrCloud.GetSolrPodName(0))
+                       
Expect(pod.Annotations).To(HaveKeyWithValue(util.ServiceTypeAnnotation, 
util.HeadlessServiceType))

Review Comment:
   Deleting the node HTTPRoutes does not mean the pods have finished switching 
to headless-service addressing: routes are reconciled before the managed 
rolling update, and `expectPodNow` reads only once. This can observe the old 
pod annotation or a temporarily missing pod and fail the test. Please wait for 
node-Service cleanup and for a replacement pod to exist before asserting its 
annotation.



##########
tests/e2e/solrcloud_gateway_test.go:
##########
@@ -0,0 +1,282 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package e2e
+
+import (
+       "context"
+       solrv1beta1 "github.com/apache/solr-operator/api/v1beta1"
+       "github.com/apache/solr-operator/controllers/util"
+       . "github.com/onsi/ginkgo/v2"
+       . "github.com/onsi/gomega"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+)
+
+var _ = Describe("E2E - SolrCloud - Gateway API", func() {
+       var (
+               solrCloud        *solrv1beta1.SolrCloud
+               gatewayNamespace = "default"
+               gatewayName      = "test-gateway"
+       )
+
+       BeforeEach(func() {
+               solrCloud = generateBaseSolrCloud(1)
+               solrCloud.Spec.SolrAddressability = 
solrv1beta1.SolrAddressabilityOptions{
+                       External: &solrv1beta1.ExternalAddressability{
+                               Method:             solrv1beta1.Gateway,
+                               UseExternalAddress: true,
+                               DomainName:         testDomain,
+                               Gateway: &solrv1beta1.SolrGatewayOptions{
+                                       ParentRefs: 
[]solrv1beta1.GatewayParentReference{
+                                               {
+                                                       Name:      gatewayName,
+                                                       Namespace: 
&gatewayNamespace,
+                                               },
+                                       },
+                               },
+                       },
+               }
+       })
+
+       JustBeforeEach(func(ctx context.Context) {
+               By("creating the SolrCloud")
+               Expect(k8sClient.Create(ctx, solrCloud)).To(Succeed())
+
+               DeferCleanup(func(ctx context.Context) {
+                       cleanupTest(ctx, solrCloud)
+               })
+
+               By("Waiting for the SolrCloud to come up healthy")
+               solrCloud = expectSolrCloudToBeReady(ctx, solrCloud)
+
+               By("creating a first Solr Collection")
+               createAndQueryCollection(ctx, solrCloud, "basic", 1, 1)
+       })
+
+       Context("Can Remove HTTPRoutes and Services when changing 
addressability", func() {
+
+               It("Can adapt to changing needs", func(ctx context.Context) {
+                       By("testing the Solr StatefulSet")
+                       statefulSet := expectStatefulSet(ctx, solrCloud, 
solrCloud.StatefulSetName())
+                       // Pod Annotations test
+                       
Expect(statefulSet.Spec.Template.Annotations).To(HaveKeyWithValue(util.ServiceTypeAnnotation,
 util.PerNodeServiceType), "Since external address is used for advertising, the 
perNode service should be specified in the pod annotations.")
+
+                       By("testing the Solr Common Service")
+                       expectService(ctx, solrCloud, 
solrCloud.CommonServiceName(), statefulSet.Spec.Selector.MatchLabels, false)
+
+                       By("ensuring the Solr Headless Service does not exist")
+                       expectNoService(ctx, solrCloud, 
solrCloud.HeadlessServiceName(), "Headless service shouldn't exist, but it 
does.")
+
+                       By("making sure the individual Solr Node Services exist 
and route correctly")
+                       nodeNames := solrCloud.GetAllSolrPodNames()
+                       Expect(nodeNames).To(HaveLen(1), "SolrCloud has 
incorrect number of nodeNames.")
+                       for _, nodeName := range nodeNames {
+                               expectService(ctx, solrCloud, nodeName, 
util.MergeLabelsOrAnnotations(statefulSet.Spec.Selector.MatchLabels, 
map[string]string{"statefulset.kubernetes.io/pod-name": nodeName}), false)
+                       }
+
+                       By("making sure Common HTTPRoute was created correctly")
+                       expectHTTPRoute(ctx, solrCloud, 
solrCloud.CommonHTTPRouteName())
+
+                       By("making sure Node HTTPRoutes were created correctly")
+                       for _, nodeName := range nodeNames {
+                               expectHTTPRoute(ctx, solrCloud, 
solrCloud.NodeHTTPRouteName(nodeName))
+                       }
+
+                       By("Turning off node external addressability and making 
sure the node services are deleted")
+                       expectSolrCloudWithChecks(ctx, solrCloud, func(g 
Gomega, found *solrv1beta1.SolrCloud) {
+                               
found.Spec.SolrAddressability.External.HideNodes = true
+                               
found.Spec.SolrAddressability.External.UseExternalAddress = false
+                               g.Expect(k8sClient.Update(ctx, 
found)).To(Succeed(), "Couldn't update the solrCloud to not advertise the nodes 
externally.")
+                       })
+
+                       // Since node external addressability is off, but 
common external addressability is on, the common HTTPRoute should exist, but 
the node HTTPRoutes should not
+                       expectHTTPRoute(ctx, solrCloud, 
solrCloud.CommonHTTPRouteName())
+
+                       expectService(ctx, solrCloud, 
solrCloud.HeadlessServiceName(), statefulSet.Spec.Selector.MatchLabels, true)
+
+                       for _, nodeName := range nodeNames {
+                               eventuallyExpectNoHTTPRoute(ctx, solrCloud, 
solrCloud.NodeHTTPRouteName(nodeName))
+                       }
+                       pod := expectPodNow(ctx, solrCloud, 
solrCloud.GetSolrPodName(0))
+                       
Expect(pod.Annotations).To(HaveKeyWithValue(util.ServiceTypeAnnotation, 
util.HeadlessServiceType))
+
+                       By("Turning off common external addressability and 
making sure the HTTPRoutes are deleted")
+                       expectSolrCloudWithChecks(ctx, solrCloud, func(g 
Gomega, found *solrv1beta1.SolrCloud) {
+                               found.Spec.SolrAddressability.External = nil
+                               g.Expect(k8sClient.Update(ctx, 
found)).To(Succeed(), "Couldn't update the solrCloud to remove external 
addressability")
+                       })
+                       eventuallyExpectNoHTTPRoute(ctx, solrCloud, 
solrCloud.CommonHTTPRouteName())
+
+                       By("Turning back on common external addressability and 
making sure the headless service is deleted")
+                       expectSolrCloudWithChecks(ctx, solrCloud, func(g 
Gomega, found *solrv1beta1.SolrCloud) {
+                               found.Spec.SolrAddressability = 
solrv1beta1.SolrAddressabilityOptions{
+                                       External: 
&solrv1beta1.ExternalAddressability{
+                                               Method:             
solrv1beta1.Gateway,
+                                               UseExternalAddress: true,
+                                               DomainName:         testDomain,
+                                               Gateway: 
&solrv1beta1.SolrGatewayOptions{
+                                                       ParentRefs: 
[]solrv1beta1.GatewayParentReference{
+                                                               {
+                                                                       Name:   
   gatewayName,
+                                                                       
Namespace: &gatewayNamespace,
+                                                               },
+                                                       },
+                                               },
+                                       },
+                               }
+                               g.Expect(k8sClient.Update(ctx, 
found)).To(Succeed(), "Couldn't update the solrCloud to add external 
addressability")
+                       })
+                       expectHTTPRoute(ctx, solrCloud, 
solrCloud.CommonHTTPRouteName())
+
+                       By("testing the Solr Common Service")
+                       expectService(ctx, solrCloud, 
solrCloud.CommonServiceName(), statefulSet.Spec.Selector.MatchLabels, false)
+
+                       By("ensuring the Solr Headless Service does not exist")
+                       expectNoService(ctx, solrCloud, 
solrCloud.HeadlessServiceName(), "Headless service shouldn't exist, but it 
does.")

Review Comment:
   The recreated HTTPRoute is available before the pods finish rolling back to 
per-node addressing. `cleanupUnconfiguredServices` deliberately retains the 
headless Service until all pods have switched, but `expectNoService` uses 
`Consistently` and fails immediately if it still exists. Please use the 
eventual-deletion helper here so the test waits for that asynchronous cleanup.



##########
tests/e2e/solrcloud_gateway_test.go:
##########
@@ -0,0 +1,282 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package e2e
+
+import (
+       "context"
+       solrv1beta1 "github.com/apache/solr-operator/api/v1beta1"
+       "github.com/apache/solr-operator/controllers/util"
+       . "github.com/onsi/ginkgo/v2"
+       . "github.com/onsi/gomega"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+)
+
+var _ = Describe("E2E - SolrCloud - Gateway API", func() {
+       var (
+               solrCloud        *solrv1beta1.SolrCloud
+               gatewayNamespace = "default"
+               gatewayName      = "test-gateway"
+       )
+
+       BeforeEach(func() {
+               solrCloud = generateBaseSolrCloud(1)
+               solrCloud.Spec.SolrAddressability = 
solrv1beta1.SolrAddressabilityOptions{
+                       External: &solrv1beta1.ExternalAddressability{
+                               Method:             solrv1beta1.Gateway,
+                               UseExternalAddress: true,
+                               DomainName:         testDomain,
+                               Gateway: &solrv1beta1.SolrGatewayOptions{
+                                       ParentRefs: 
[]solrv1beta1.GatewayParentReference{
+                                               {
+                                                       Name:      gatewayName,
+                                                       Namespace: 
&gatewayNamespace,
+                                               },
+                                       },
+                               },
+                       },
+               }
+       })
+
+       JustBeforeEach(func(ctx context.Context) {
+               By("creating the SolrCloud")
+               Expect(k8sClient.Create(ctx, solrCloud)).To(Succeed())
+
+               DeferCleanup(func(ctx context.Context) {
+                       cleanupTest(ctx, solrCloud)
+               })
+
+               By("Waiting for the SolrCloud to come up healthy")
+               solrCloud = expectSolrCloudToBeReady(ctx, solrCloud)
+
+               By("creating a first Solr Collection")
+               createAndQueryCollection(ctx, solrCloud, "basic", 1, 1)
+       })
+
+       Context("Can Remove HTTPRoutes and Services when changing 
addressability", func() {
+
+               It("Can adapt to changing needs", func(ctx context.Context) {
+                       By("testing the Solr StatefulSet")
+                       statefulSet := expectStatefulSet(ctx, solrCloud, 
solrCloud.StatefulSetName())
+                       // Pod Annotations test
+                       
Expect(statefulSet.Spec.Template.Annotations).To(HaveKeyWithValue(util.ServiceTypeAnnotation,
 util.PerNodeServiceType), "Since external address is used for advertising, the 
perNode service should be specified in the pod annotations.")
+
+                       By("testing the Solr Common Service")
+                       expectService(ctx, solrCloud, 
solrCloud.CommonServiceName(), statefulSet.Spec.Selector.MatchLabels, false)
+
+                       By("ensuring the Solr Headless Service does not exist")
+                       expectNoService(ctx, solrCloud, 
solrCloud.HeadlessServiceName(), "Headless service shouldn't exist, but it 
does.")
+
+                       By("making sure the individual Solr Node Services exist 
and route correctly")
+                       nodeNames := solrCloud.GetAllSolrPodNames()
+                       Expect(nodeNames).To(HaveLen(1), "SolrCloud has 
incorrect number of nodeNames.")
+                       for _, nodeName := range nodeNames {
+                               expectService(ctx, solrCloud, nodeName, 
util.MergeLabelsOrAnnotations(statefulSet.Spec.Selector.MatchLabels, 
map[string]string{"statefulset.kubernetes.io/pod-name": nodeName}), false)
+                       }
+
+                       By("making sure Common HTTPRoute was created correctly")
+                       expectHTTPRoute(ctx, solrCloud, 
solrCloud.CommonHTTPRouteName())
+
+                       By("making sure Node HTTPRoutes were created correctly")
+                       for _, nodeName := range nodeNames {
+                               expectHTTPRoute(ctx, solrCloud, 
solrCloud.NodeHTTPRouteName(nodeName))
+                       }
+
+                       By("Turning off node external addressability and making 
sure the node services are deleted")
+                       expectSolrCloudWithChecks(ctx, solrCloud, func(g 
Gomega, found *solrv1beta1.SolrCloud) {
+                               
found.Spec.SolrAddressability.External.HideNodes = true
+                               
found.Spec.SolrAddressability.External.UseExternalAddress = false
+                               g.Expect(k8sClient.Update(ctx, 
found)).To(Succeed(), "Couldn't update the solrCloud to not advertise the nodes 
externally.")
+                       })
+
+                       // Since node external addressability is off, but 
common external addressability is on, the common HTTPRoute should exist, but 
the node HTTPRoutes should not
+                       expectHTTPRoute(ctx, solrCloud, 
solrCloud.CommonHTTPRouteName())
+
+                       expectService(ctx, solrCloud, 
solrCloud.HeadlessServiceName(), statefulSet.Spec.Selector.MatchLabels, true)
+
+                       for _, nodeName := range nodeNames {
+                               eventuallyExpectNoHTTPRoute(ctx, solrCloud, 
solrCloud.NodeHTTPRouteName(nodeName))
+                       }
+                       pod := expectPodNow(ctx, solrCloud, 
solrCloud.GetSolrPodName(0))
+                       
Expect(pod.Annotations).To(HaveKeyWithValue(util.ServiceTypeAnnotation, 
util.HeadlessServiceType))
+
+                       By("Turning off common external addressability and 
making sure the HTTPRoutes are deleted")
+                       expectSolrCloudWithChecks(ctx, solrCloud, func(g 
Gomega, found *solrv1beta1.SolrCloud) {
+                               found.Spec.SolrAddressability.External = nil
+                               g.Expect(k8sClient.Update(ctx, 
found)).To(Succeed(), "Couldn't update the solrCloud to remove external 
addressability")
+                       })
+                       eventuallyExpectNoHTTPRoute(ctx, solrCloud, 
solrCloud.CommonHTTPRouteName())
+
+                       By("Turning back on common external addressability and 
making sure the headless service is deleted")
+                       expectSolrCloudWithChecks(ctx, solrCloud, func(g 
Gomega, found *solrv1beta1.SolrCloud) {
+                               found.Spec.SolrAddressability = 
solrv1beta1.SolrAddressabilityOptions{
+                                       External: 
&solrv1beta1.ExternalAddressability{
+                                               Method:             
solrv1beta1.Gateway,
+                                               UseExternalAddress: true,
+                                               DomainName:         testDomain,
+                                               Gateway: 
&solrv1beta1.SolrGatewayOptions{
+                                                       ParentRefs: 
[]solrv1beta1.GatewayParentReference{
+                                                               {
+                                                                       Name:   
   gatewayName,
+                                                                       
Namespace: &gatewayNamespace,
+                                                               },
+                                                       },
+                                               },
+                                       },
+                               }
+                               g.Expect(k8sClient.Update(ctx, 
found)).To(Succeed(), "Couldn't update the solrCloud to add external 
addressability")
+                       })
+                       expectHTTPRoute(ctx, solrCloud, 
solrCloud.CommonHTTPRouteName())
+
+                       By("testing the Solr Common Service")
+                       expectService(ctx, solrCloud, 
solrCloud.CommonServiceName(), statefulSet.Spec.Selector.MatchLabels, false)
+
+                       By("ensuring the Solr Headless Service does not exist")
+                       expectNoService(ctx, solrCloud, 
solrCloud.HeadlessServiceName(), "Headless service shouldn't exist, but it 
does.")
+               })
+       })
+
+       Context("BackendTLSPolicy Management", func() {

Review Comment:
   Both specs inherit `generateBaseSolrCloud(1)`, which leaves `spec.solrTLS` 
nil, and neither enables TLS before adding a BackendTLSPolicy. The controller 
rejects that configuration, so the tests time out waiting for policies that it 
will never create. Prepare a TLS-enabled fixture, with the required certificate 
and password Secrets, before the outer `JustBeforeEach` creates the cloud.



##########
tests/scripts/manage_e2e_tests.sh:
##########
@@ -128,6 +133,28 @@ ERR
   fi
 }
 
+function start_cloud_provider_kind() {
+  # cloud-provider-kind is a host-level daemon (LoadBalancer + Gateway API 
support) shared by every KinD
+  # cluster on the host, so only one instance is needed. Skip if it is already 
running.
+  if pgrep -f "cloud-provider-kind" >/dev/null 2>&1; then
+    printf "cloud-provider-kind is already running.\n\n"
+    return
+  fi
+  printf "Starting cloud-provider-kind\n\n"
+  # nohup + disown so the daemon outlives this script invocation (e.g. the 
create-cluster action)
+  nohup cloud-provider-kind >"${TMPDIR:-/tmp}/cloud-provider-kind.log" 2>&1 &
+  disown
+}
+
+function stop_cloud_provider_kind_if_unused() {
+  # The daemon is shared by every KinD cluster on the host, so only stop it 
once none of the integration-test
+  # clusters (named with the "solr-op-e2e" prefix) remain. This leaves it 
running for unrelated KinD clusters
+  # and for other concurrent test runs.
+  if ! kind get clusters 2>/dev/null | grep -q "^${CLUSTER_NAME_PREFIX}"; then
+    pkill -f "cloud-provider-kind" 2>/dev/null || true
+  fi

Review Comment:
   After the last Solr e2e cluster is deleted, this condition stops the 
host-wide provider even if unrelated KinD clusters still use it, including a 
provider reused by `start_cloud_provider_kind()`. Stop it only when no KinD 
clusters remain, and leave it running if listing clusters fails.



##########
tests/e2e/solrcloud_gateway_test.go:
##########
@@ -0,0 +1,282 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package e2e
+
+import (
+       "context"
+       solrv1beta1 "github.com/apache/solr-operator/api/v1beta1"
+       "github.com/apache/solr-operator/controllers/util"
+       . "github.com/onsi/ginkgo/v2"
+       . "github.com/onsi/gomega"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+)
+
+var _ = Describe("E2E - SolrCloud - Gateway API", func() {
+       var (
+               solrCloud        *solrv1beta1.SolrCloud
+               gatewayNamespace = "default"
+               gatewayName      = "test-gateway"
+       )
+
+       BeforeEach(func() {
+               solrCloud = generateBaseSolrCloud(1)
+               solrCloud.Spec.SolrAddressability = 
solrv1beta1.SolrAddressabilityOptions{
+                       External: &solrv1beta1.ExternalAddressability{
+                               Method:             solrv1beta1.Gateway,
+                               UseExternalAddress: true,
+                               DomainName:         testDomain,
+                               Gateway: &solrv1beta1.SolrGatewayOptions{
+                                       ParentRefs: 
[]solrv1beta1.GatewayParentReference{
+                                               {
+                                                       Name:      gatewayName,
+                                                       Namespace: 
&gatewayNamespace,
+                                               },
+                                       },
+                               },
+                       },
+               }
+       })
+
+       JustBeforeEach(func(ctx context.Context) {
+               By("creating the SolrCloud")
+               Expect(k8sClient.Create(ctx, solrCloud)).To(Succeed())
+
+               DeferCleanup(func(ctx context.Context) {
+                       cleanupTest(ctx, solrCloud)
+               })
+
+               By("Waiting for the SolrCloud to come up healthy")
+               solrCloud = expectSolrCloudToBeReady(ctx, solrCloud)
+
+               By("creating a first Solr Collection")
+               createAndQueryCollection(ctx, solrCloud, "basic", 1, 1)
+       })
+
+       Context("Can Remove HTTPRoutes and Services when changing 
addressability", func() {
+
+               It("Can adapt to changing needs", func(ctx context.Context) {
+                       By("testing the Solr StatefulSet")
+                       statefulSet := expectStatefulSet(ctx, solrCloud, 
solrCloud.StatefulSetName())
+                       // Pod Annotations test
+                       
Expect(statefulSet.Spec.Template.Annotations).To(HaveKeyWithValue(util.ServiceTypeAnnotation,
 util.PerNodeServiceType), "Since external address is used for advertising, the 
perNode service should be specified in the pod annotations.")
+
+                       By("testing the Solr Common Service")
+                       expectService(ctx, solrCloud, 
solrCloud.CommonServiceName(), statefulSet.Spec.Selector.MatchLabels, false)
+
+                       By("ensuring the Solr Headless Service does not exist")
+                       expectNoService(ctx, solrCloud, 
solrCloud.HeadlessServiceName(), "Headless service shouldn't exist, but it 
does.")
+
+                       By("making sure the individual Solr Node Services exist 
and route correctly")
+                       nodeNames := solrCloud.GetAllSolrPodNames()
+                       Expect(nodeNames).To(HaveLen(1), "SolrCloud has 
incorrect number of nodeNames.")
+                       for _, nodeName := range nodeNames {
+                               expectService(ctx, solrCloud, nodeName, 
util.MergeLabelsOrAnnotations(statefulSet.Spec.Selector.MatchLabels, 
map[string]string{"statefulset.kubernetes.io/pod-name": nodeName}), false)
+                       }
+
+                       By("making sure Common HTTPRoute was created correctly")
+                       expectHTTPRoute(ctx, solrCloud, 
solrCloud.CommonHTTPRouteName())
+
+                       By("making sure Node HTTPRoutes were created correctly")
+                       for _, nodeName := range nodeNames {
+                               expectHTTPRoute(ctx, solrCloud, 
solrCloud.NodeHTTPRouteName(nodeName))
+                       }
+
+                       By("Turning off node external addressability and making 
sure the node services are deleted")
+                       expectSolrCloudWithChecks(ctx, solrCloud, func(g 
Gomega, found *solrv1beta1.SolrCloud) {
+                               
found.Spec.SolrAddressability.External.HideNodes = true
+                               
found.Spec.SolrAddressability.External.UseExternalAddress = false
+                               g.Expect(k8sClient.Update(ctx, 
found)).To(Succeed(), "Couldn't update the solrCloud to not advertise the nodes 
externally.")
+                       })
+
+                       // Since node external addressability is off, but 
common external addressability is on, the common HTTPRoute should exist, but 
the node HTTPRoutes should not
+                       expectHTTPRoute(ctx, solrCloud, 
solrCloud.CommonHTTPRouteName())
+
+                       expectService(ctx, solrCloud, 
solrCloud.HeadlessServiceName(), statefulSet.Spec.Selector.MatchLabels, true)
+
+                       for _, nodeName := range nodeNames {
+                               eventuallyExpectNoHTTPRoute(ctx, solrCloud, 
solrCloud.NodeHTTPRouteName(nodeName))
+                       }
+                       pod := expectPodNow(ctx, solrCloud, 
solrCloud.GetSolrPodName(0))
+                       
Expect(pod.Annotations).To(HaveKeyWithValue(util.ServiceTypeAnnotation, 
util.HeadlessServiceType))
+
+                       By("Turning off common external addressability and 
making sure the HTTPRoutes are deleted")
+                       expectSolrCloudWithChecks(ctx, solrCloud, func(g 
Gomega, found *solrv1beta1.SolrCloud) {
+                               found.Spec.SolrAddressability.External = nil
+                               g.Expect(k8sClient.Update(ctx, 
found)).To(Succeed(), "Couldn't update the solrCloud to remove external 
addressability")
+                       })
+                       eventuallyExpectNoHTTPRoute(ctx, solrCloud, 
solrCloud.CommonHTTPRouteName())
+
+                       By("Turning back on common external addressability and 
making sure the headless service is deleted")
+                       expectSolrCloudWithChecks(ctx, solrCloud, func(g 
Gomega, found *solrv1beta1.SolrCloud) {
+                               found.Spec.SolrAddressability = 
solrv1beta1.SolrAddressabilityOptions{
+                                       External: 
&solrv1beta1.ExternalAddressability{
+                                               Method:             
solrv1beta1.Gateway,
+                                               UseExternalAddress: true,
+                                               DomainName:         testDomain,
+                                               Gateway: 
&solrv1beta1.SolrGatewayOptions{
+                                                       ParentRefs: 
[]solrv1beta1.GatewayParentReference{
+                                                               {
+                                                                       Name:   
   gatewayName,
+                                                                       
Namespace: &gatewayNamespace,
+                                                               },
+                                                       },
+                                               },
+                                       },
+                               }
+                               g.Expect(k8sClient.Update(ctx, 
found)).To(Succeed(), "Couldn't update the solrCloud to add external 
addressability")
+                       })
+                       expectHTTPRoute(ctx, solrCloud, 
solrCloud.CommonHTTPRouteName())
+
+                       By("testing the Solr Common Service")
+                       expectService(ctx, solrCloud, 
solrCloud.CommonServiceName(), statefulSet.Spec.Selector.MatchLabels, false)
+
+                       By("ensuring the Solr Headless Service does not exist")
+                       expectNoService(ctx, solrCloud, 
solrCloud.HeadlessServiceName(), "Headless service shouldn't exist, but it 
does.")
+               })
+       })
+
+       Context("BackendTLSPolicy Management", func() {
+               var (
+                       caCertConfigMapName = "solr-ca-cert"
+               )
+
+               It("Creates and manages BackendTLSPolicy resources", func(ctx 
context.Context) {
+                       By("verifying BackendTLSPolicy resources do not exist 
initially")
+                       expectNoBackendTLSPolicy(ctx, solrCloud, 
solrCloud.CommonBackendTLSPolicyName())
+                       nodeNames := solrCloud.GetAllSolrPodNames()
+                       for _, nodeName := range nodeNames {
+                               expectNoBackendTLSPolicy(ctx, solrCloud, 
solrCloud.NodeBackendTLSPolicyName(nodeName))
+                       }
+
+                       By("enabling BackendTLSPolicy with CA certificate 
reference")
+                       expectSolrCloudWithChecks(ctx, solrCloud, func(g 
Gomega, found *solrv1beta1.SolrCloud) {
+                               
found.Spec.SolrAddressability.External.Gateway.BackendTLSPolicy = 
&solrv1beta1.SolrBackendTLSPolicy{
+                                       CACertificateRefs: 
[]solrv1beta1.GatewayCertificateReference{
+                                               {
+                                                       Name: 
caCertConfigMapName,
+                                               },
+                                       },
+                               }
+                               g.Expect(k8sClient.Update(ctx, 
found)).To(Succeed(), "Couldn't update the solrCloud to add BackendTLSPolicy")
+                       })
+
+                       By("verifying Common BackendTLSPolicy was created 
correctly")
+                       commonPolicy := expectBackendTLSPolicyWithChecks(ctx, 
solrCloud, solrCloud.CommonBackendTLSPolicyName(), func(g Gomega, policy 
*gatewayv1.BackendTLSPolicy) {
+                               g.Expect(policy.Spec.TargetRefs).To(HaveLen(1), 
"BackendTLSPolicy should have one target ref")
+                               
g.Expect(string(policy.Spec.TargetRefs[0].Name)).To(Equal(solrCloud.CommonServiceName()),
 "BackendTLSPolicy should target common service")
+                               
g.Expect(string(policy.Spec.TargetRefs[0].Kind)).To(Equal("Service"), 
"BackendTLSPolicy should target Service kind")
+                               
g.Expect(string(policy.Spec.Validation.Hostname)).To(Equal(solrCloud.CommonServiceName()),
 "BackendTLSPolicy hostname should match service name")
+                               
g.Expect(policy.Spec.Validation.CACertificateRefs).To(HaveLen(1), 
"BackendTLSPolicy should have one CA cert ref")
+                               
g.Expect(string(policy.Spec.Validation.CACertificateRefs[0].Name)).To(Equal(caCertConfigMapName),
 "CA cert ref name should match")
+                               
g.Expect(string(policy.Spec.Validation.CACertificateRefs[0].Kind)).To(Equal("ConfigMap"),
 "CA cert ref kind should default to ConfigMap")
+                               
g.Expect(string(policy.Spec.Validation.CACertificateRefs[0].Group)).To(Equal(""),
 "CA cert ref group should be empty (core API)")
+                       })
+                       Expect(commonPolicy).ToNot(BeNil())
+
+                       By("verifying Node BackendTLSPolicy resources were 
created correctly")
+                       for _, nodeName := range nodeNames {
+                               nodePolicy := 
expectBackendTLSPolicyWithChecks(ctx, solrCloud, 
solrCloud.NodeBackendTLSPolicyName(nodeName), func(g Gomega, policy 
*gatewayv1.BackendTLSPolicy) {
+                                       
g.Expect(policy.Spec.TargetRefs).To(HaveLen(1), "BackendTLSPolicy should have 
one target ref")
+                                       
g.Expect(string(policy.Spec.TargetRefs[0].Name)).To(Equal(nodeName), 
"BackendTLSPolicy should target node service")
+                                       
g.Expect(string(policy.Spec.Validation.Hostname)).To(Equal(nodeName), 
"BackendTLSPolicy hostname should match node service name")

Review Comment:
   The policy generators use external common and node hostnames for TLS 
validation, not bare Service names, so these assertions fail even with a valid 
TLS fixture. Compare against `ExternalCommonUrl(domainName, false)` here and 
`ExternalNodeUrl(nodeName, domainName, false)` at line 196.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to