github.com/cilium/cilium@v1.16.2/operator/pkg/ingress/ingress_reconcile.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package ingress
     5  
     6  import (
     7  	"cmp"
     8  	"context"
     9  	"fmt"
    10  	"slices"
    11  	"strings"
    12  
    13  	"github.com/sirupsen/logrus"
    14  	corev1 "k8s.io/api/core/v1"
    15  	networkingv1 "k8s.io/api/networking/v1"
    16  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    17  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    18  	"k8s.io/apimachinery/pkg/types"
    19  	ctrl "sigs.k8s.io/controller-runtime"
    20  	"sigs.k8s.io/controller-runtime/pkg/client"
    21  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    22  
    23  	controllerruntime "github.com/cilium/cilium/operator/pkg/controller-runtime"
    24  	"github.com/cilium/cilium/operator/pkg/ingress/annotations"
    25  	"github.com/cilium/cilium/operator/pkg/model"
    26  	"github.com/cilium/cilium/operator/pkg/model/ingestion"
    27  	ciliumv2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2"
    28  	"github.com/cilium/cilium/pkg/logging/logfields"
    29  )
    30  
    31  const (
    32  	defaultPassthroughPort         = uint32(443)
    33  	defaultInsecureHTTPPort        = uint32(80)
    34  	defaultSecureHTTPPort          = uint32(443)
    35  	defaultHostNetworkListenerPort = uint32(8080)
    36  )
    37  
    38  func (r *ingressReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    39  	scopedLog := r.logger.WithFields(logrus.Fields{
    40  		logfields.Controller: "ingress",
    41  		logfields.Resource:   req.NamespacedName,
    42  	})
    43  
    44  	scopedLog.Info("Reconciling Ingress")
    45  	ingress := &networkingv1.Ingress{}
    46  	if err := r.client.Get(ctx, req.NamespacedName, ingress); err != nil {
    47  		if !k8serrors.IsNotFound(err) {
    48  			return controllerruntime.Fail(fmt.Errorf("failed to get Ingress: %w", err))
    49  		}
    50  		// Ingress deleted -> try to cleanup shared CiliumEnvoyConfig
    51  		// Resources from LB mode dedicated are deleted via K8s Garbage Collection (OwnerReferences)
    52  		scopedLog.Debug("Trying to cleanup potentially existing resources of deleted Ingress")
    53  		if err := r.tryCleanupSharedResources(ctx); err != nil {
    54  			return controllerruntime.Fail(err)
    55  		}
    56  
    57  		return controllerruntime.Success()
    58  	}
    59  
    60  	// Ingress gets deleted via foreground deletion (DeletionTimestamp set)
    61  	// -> abort and wait for the actual deletion to trigger a reconcile
    62  	if ingress.GetDeletionTimestamp() != nil {
    63  		scopedLog.Debug("Ingress is marked for deletion - waiting for actual deletion")
    64  		return controllerruntime.Success()
    65  	}
    66  
    67  	// Ingress is no longer managed by Cilium.
    68  	// Trying to cleanup resources.
    69  	if !isCiliumManagedIngress(ctx, r.client, r.logger, *ingress) {
    70  		scopedLog.Debug("Trying to cleanup potentially existing resources of unmanaged Ingress")
    71  		if err := r.tryCleanupSharedResources(ctx); err != nil {
    72  			return controllerruntime.Fail(err)
    73  		}
    74  
    75  		if err := r.tryCleanupDedicatedResources(ctx, req.NamespacedName); err != nil {
    76  			return controllerruntime.Fail(err)
    77  		}
    78  
    79  		scopedLog.Debug("Trying to cleanup Ingress status of unmanaged Ingress")
    80  		if err := r.tryCleanupIngressStatus(ctx, ingress); err != nil {
    81  			// One attempt to cleanup the status of the Ingress.
    82  			// Don't fail (and retry) on an error, as this might result in
    83  			// interferences with the new responsible Ingress controller.
    84  			scopedLog.WithError(err).Warn("Failed to cleanup Ingress status")
    85  		}
    86  
    87  		scopedLog.Info("Successfully cleaned Ingress resources")
    88  		return controllerruntime.Success()
    89  	}
    90  
    91  	// Creation / Update of Ingress resources depending on the loadbalancer mode
    92  	// Trying to cleanup the resources of the "other" mode (potential change of mode)
    93  	if r.isEffectiveLoadbalancerModeDedicated(ingress) {
    94  		scopedLog.Debug("Updating dedicated resources")
    95  		if err := r.createOrUpdateDedicatedResources(ctx, ingress, scopedLog); err != nil {
    96  			if k8serrors.IsForbidden(err) && k8serrors.HasStatusCause(err, corev1.NamespaceTerminatingCause) {
    97  				// The creation of one of the resources failed because the
    98  				// namespace is terminating. The ingress itself is also expected
    99  				// to be marked for deletion, but we haven't yet received the
   100  				// corresponding event, so let's not print an error message.
   101  				scopedLog.Info("Aborting reconciliation because namespace is being terminated")
   102  				return controllerruntime.Success()
   103  			}
   104  
   105  			return controllerruntime.Fail(err)
   106  		}
   107  
   108  		// Trying to cleanup shared resources (potential change of LB mode)
   109  		scopedLog.Debug("Trying to cleanup potentially existing shared resources")
   110  		if err := r.tryCleanupSharedResources(ctx); err != nil {
   111  			return controllerruntime.Fail(err)
   112  		}
   113  	} else {
   114  		scopedLog.Debug("Updating shared resources")
   115  		if err := r.createOrUpdateSharedResources(ctx); err != nil {
   116  			return controllerruntime.Fail(err)
   117  		}
   118  
   119  		// Trying to cleanup dedicated resources (potential change of LB mode)
   120  		scopedLog.Debug("Trying to cleanup potentially existing dedicated resources")
   121  		if err := r.tryCleanupDedicatedResources(ctx, req.NamespacedName); err != nil {
   122  			return controllerruntime.Fail(err)
   123  		}
   124  	}
   125  
   126  	// Update status
   127  	scopedLog.Debug("Updating Ingress status")
   128  	if err := r.updateIngressLoadbalancerStatus(ctx, ingress); err != nil {
   129  		return controllerruntime.Fail(fmt.Errorf("failed to update Ingress loadbalancer status: %w", err))
   130  	}
   131  
   132  	scopedLog.Info("Successfully reconciled Ingress")
   133  	return controllerruntime.Success()
   134  }
   135  
   136  func (r *ingressReconciler) createOrUpdateDedicatedResources(ctx context.Context, ingress *networkingv1.Ingress, scopedLog logrus.FieldLogger) error {
   137  	desiredCiliumEnvoyConfig, desiredService, desiredEndpoints, err := r.buildDedicatedResources(ctx, ingress, scopedLog)
   138  	if err != nil {
   139  		return fmt.Errorf("failed to build dedicated resources: %w", err)
   140  	}
   141  
   142  	if err := r.createOrUpdateService(ctx, desiredService); err != nil {
   143  		return err
   144  	}
   145  
   146  	if err := r.createOrUpdateCiliumEnvoyConfig(ctx, desiredCiliumEnvoyConfig); err != nil {
   147  		return err
   148  	}
   149  
   150  	if err := r.createOrUpdateEndpoints(ctx, desiredEndpoints); err != nil {
   151  		return err
   152  	}
   153  
   154  	return nil
   155  }
   156  
   157  // propagateIngressAnnotationsAndLabels propagates Ingress annotation and label if required.
   158  // This is applicable only for dedicated LB mode.
   159  // For shared LB mode, the service annotation and label are defined in other higher level (e.g. helm).
   160  func (r *ingressReconciler) propagateIngressAnnotationsAndLabels(ingress *networkingv1.Ingress, objectMeta *metav1.ObjectMeta) {
   161  	// Same lbAnnotationPrefixes config option is used for annotation and label propagation
   162  	if len(r.lbAnnotationPrefixes) > 0 {
   163  		objectMeta.Annotations = mergeMap(objectMeta.Annotations, ingress.Annotations, r.lbAnnotationPrefixes...)
   164  		objectMeta.Labels = mergeMap(objectMeta.Labels, ingress.Labels, r.lbAnnotationPrefixes...)
   165  	}
   166  }
   167  
   168  func (r *ingressReconciler) createOrUpdateSharedResources(ctx context.Context) error {
   169  	// In shared loadbalancing mode, only the CiliumEnvoyConfig is managed by the Operator.
   170  	// Service and Endpoints are created by the Helm Chart.
   171  	desiredCiliumEnvoyConfig, err := r.buildSharedResources(ctx)
   172  	if err != nil {
   173  		return fmt.Errorf("failed to build shared resources: %w", err)
   174  	}
   175  
   176  	if err := r.createOrUpdateCiliumEnvoyConfig(ctx, desiredCiliumEnvoyConfig); err != nil {
   177  		return err
   178  	}
   179  
   180  	return nil
   181  }
   182  
   183  func (r *ingressReconciler) tryCleanupDedicatedResources(ctx context.Context, ingressNamespacedName types.NamespacedName) error {
   184  	resources := map[client.Object]types.NamespacedName{
   185  		&corev1.Service{}:             {Namespace: ingressNamespacedName.Namespace, Name: fmt.Sprintf("%s-%s", ciliumIngressPrefix, ingressNamespacedName.Name)},
   186  		&corev1.Endpoints{}:           {Namespace: ingressNamespacedName.Namespace, Name: fmt.Sprintf("%s-%s", ciliumIngressPrefix, ingressNamespacedName.Name)},
   187  		&ciliumv2.CiliumEnvoyConfig{}: {Namespace: ingressNamespacedName.Namespace, Name: fmt.Sprintf("%s-%s-%s", ciliumIngressPrefix, ingressNamespacedName.Namespace, ingressNamespacedName.Name)},
   188  	}
   189  
   190  	for k, v := range resources {
   191  		if err := r.tryDeletingResource(ctx, k, v); err != nil {
   192  			return err
   193  		}
   194  	}
   195  
   196  	return nil
   197  }
   198  
   199  func (r *ingressReconciler) tryCleanupSharedResources(ctx context.Context) error {
   200  	// In shared loadbalancing mode, only the CiliumEnvoyConfig is managed by the Operator.
   201  	// Service and Endpoints are created by the Helm Chart.
   202  	desiredCiliumEnvoyConfig, err := r.buildSharedResources(ctx)
   203  	if err != nil {
   204  		return fmt.Errorf("failed to build shared resources: %w", err)
   205  	}
   206  
   207  	if err := r.createOrUpdateCiliumEnvoyConfig(ctx, desiredCiliumEnvoyConfig); err != nil {
   208  		return err
   209  	}
   210  
   211  	return nil
   212  }
   213  
   214  func (r *ingressReconciler) buildSharedResources(ctx context.Context) (*ciliumv2.CiliumEnvoyConfig, error) {
   215  	ingressList := networkingv1.IngressList{}
   216  	if err := r.client.List(ctx, &ingressList); err != nil {
   217  		return nil, fmt.Errorf("failed to list Ingresses: %w", err)
   218  	}
   219  
   220  	passthroughPort, insecureHTTPPort, secureHTTPPort := r.getSharedListenerPorts()
   221  
   222  	m := &model.Model{}
   223  	allSharedIngresses := ingressList.Items
   224  	slices.SortStableFunc(allSharedIngresses, func(a, b networkingv1.Ingress) int {
   225  		return cmp.Compare(a.Namespace+"/"+a.Name, b.Namespace+"/"+b.Name)
   226  	})
   227  
   228  	for _, item := range allSharedIngresses {
   229  		if !isCiliumManagedIngress(ctx, r.client, r.logger, item) || r.isEffectiveLoadbalancerModeDedicated(&item) || item.GetDeletionTimestamp() != nil {
   230  			continue
   231  		}
   232  		if annotations.GetAnnotationTLSPassthroughEnabled(&item) {
   233  			m.TLSPassthrough = append(m.TLSPassthrough, ingestion.IngressPassthrough(item, passthroughPort)...)
   234  		} else {
   235  			m.HTTP = append(m.HTTP, ingestion.Ingress(item, r.defaultSecretNamespace, r.defaultSecretName, r.enforcedHTTPS, insecureHTTPPort, secureHTTPPort, r.defaultRequestTimeout)...)
   236  		}
   237  	}
   238  
   239  	return r.cecTranslator.Translate(r.ciliumNamespace, r.sharedResourcesName, m)
   240  }
   241  
   242  func (r *ingressReconciler) getSharedListenerPorts() (uint32, uint32, uint32) {
   243  	if !r.hostNetworkEnabled {
   244  		return defaultPassthroughPort, defaultInsecureHTTPPort, defaultSecureHTTPPort
   245  	}
   246  
   247  	if r.hostNetworkSharedPort > 0 {
   248  		return r.hostNetworkSharedPort, r.hostNetworkSharedPort, r.hostNetworkSharedPort
   249  	}
   250  
   251  	return defaultHostNetworkListenerPort, defaultHostNetworkListenerPort, defaultHostNetworkListenerPort
   252  }
   253  
   254  func (r *ingressReconciler) buildDedicatedResources(ctx context.Context, ingress *networkingv1.Ingress, scopedLog logrus.FieldLogger) (*ciliumv2.CiliumEnvoyConfig, *corev1.Service, *corev1.Endpoints, error) {
   255  	passthroughPort, insecureHTTPPort, secureHTTPPort := r.getDedicatedListenerPorts(ingress)
   256  
   257  	m := &model.Model{}
   258  
   259  	if annotations.GetAnnotationTLSPassthroughEnabled(ingress) {
   260  		m.TLSPassthrough = append(m.TLSPassthrough, ingestion.IngressPassthrough(*ingress, passthroughPort)...)
   261  	} else {
   262  		m.HTTP = append(m.HTTP, ingestion.Ingress(*ingress, r.defaultSecretNamespace, r.defaultSecretName, r.enforcedHTTPS, insecureHTTPPort, secureHTTPPort, r.defaultRequestTimeout)...)
   263  	}
   264  
   265  	cec, svc, ep, err := r.dedicatedTranslator.Translate(m)
   266  	if err != nil {
   267  		return nil, nil, nil, fmt.Errorf("failed to translate model into resources: %w", err)
   268  	}
   269  
   270  	r.propagateIngressAnnotationsAndLabels(ingress, &svc.ObjectMeta)
   271  
   272  	if svc.Spec.Type == corev1.ServiceTypeLoadBalancer {
   273  		lbClass := annotations.GetAnnotationLoadBalancerClass(ingress)
   274  		if lbClass != nil {
   275  			svc.Spec.LoadBalancerClass = lbClass
   276  		}
   277  	}
   278  
   279  	eTP, err := annotations.GetAnnotationServiceExternalTrafficPolicy(ingress)
   280  	if err != nil {
   281  		scopedLog.WithError(err).Warn("Failed to get externalTrafficPolicy annotation from Ingress object")
   282  	}
   283  	svc.Spec.ExternalTrafficPolicy = corev1.ServiceExternalTrafficPolicy(eTP)
   284  
   285  	// Explicitly set the controlling OwnerReference on the CiliumEnvoyConfig
   286  	if err := controllerutil.SetControllerReference(ingress, cec, r.client.Scheme()); err != nil {
   287  		return nil, nil, nil, fmt.Errorf("failed to set controller reference on CiliumEnvoyConfig: %w", err)
   288  	}
   289  
   290  	return cec, svc, ep, err
   291  }
   292  
   293  func (r *ingressReconciler) getDedicatedListenerPorts(ingress *networkingv1.Ingress) (uint32, uint32, uint32) {
   294  	if !r.hostNetworkEnabled {
   295  		return defaultPassthroughPort, defaultInsecureHTTPPort, defaultSecureHTTPPort
   296  	}
   297  
   298  	port, err := annotations.GetAnnotationHostListenerPort(ingress)
   299  	if err != nil {
   300  		r.logger.WithError(err).Warnf("Failed to parse host port - using default listener port")
   301  		return defaultHostNetworkListenerPort, defaultHostNetworkListenerPort, defaultHostNetworkListenerPort
   302  	} else if port == nil || *port == 0 {
   303  		r.logger.Warnf("No host port defined in annotation - using default listener port")
   304  		return defaultHostNetworkListenerPort, defaultHostNetworkListenerPort, defaultHostNetworkListenerPort
   305  	} else {
   306  		return *port, *port, *port
   307  	}
   308  }
   309  
   310  func (r *ingressReconciler) createOrUpdateCiliumEnvoyConfig(ctx context.Context, desiredCEC *ciliumv2.CiliumEnvoyConfig) error {
   311  	cec := desiredCEC.DeepCopy()
   312  
   313  	// Delete CiliumEnvoyConfig if no resources are defined.
   314  	// Otherwise, the subsequent CreateOrUpdate will fail as spec.resources is required field.
   315  	if len(cec.Spec.Resources) == 0 {
   316  		err := r.client.Delete(ctx, cec)
   317  		if err != nil && !k8serrors.IsNotFound(err) {
   318  			return fmt.Errorf("failed to delete CiliumEnvoyConfig: %w", err)
   319  		}
   320  		return nil
   321  	}
   322  
   323  	result, err := controllerutil.CreateOrUpdate(ctx, r.client, cec, func() error {
   324  		cec.Spec = desiredCEC.Spec
   325  		cec.OwnerReferences = desiredCEC.OwnerReferences
   326  		cec.Annotations = mergeMap(cec.Annotations, desiredCEC.Annotations)
   327  		cec.Labels = mergeMap(cec.Labels, desiredCEC.Labels)
   328  
   329  		return nil
   330  	})
   331  	if err != nil {
   332  		return fmt.Errorf("failed to create or update CiliumEnvoyConfig: %w", err)
   333  	}
   334  
   335  	r.logger.Debugf("CiliumEnvoyConfig %s has been %s", client.ObjectKeyFromObject(cec), result)
   336  
   337  	return nil
   338  }
   339  
   340  func (r *ingressReconciler) createOrUpdateService(ctx context.Context, desiredService *corev1.Service) error {
   341  	svc := desiredService.DeepCopy()
   342  
   343  	result, err := controllerutil.CreateOrUpdate(ctx, r.client, svc, func() error {
   344  		// Save and restore loadBalancerClass
   345  		// e.g. if a mutating webhook writes this field
   346  		lbClass := svc.Spec.LoadBalancerClass
   347  		svc.Spec = desiredService.Spec
   348  		svc.Spec.LoadBalancerClass = lbClass
   349  		svc.Spec.ExternalTrafficPolicy = desiredService.Spec.ExternalTrafficPolicy
   350  
   351  		svc.OwnerReferences = desiredService.OwnerReferences
   352  		svc.Annotations = mergeMap(svc.Annotations, desiredService.Annotations)
   353  		svc.Labels = mergeMap(svc.Labels, desiredService.Labels)
   354  
   355  		return nil
   356  	})
   357  	if err != nil {
   358  		return fmt.Errorf("failed to create or update Service: %w", err)
   359  	}
   360  
   361  	r.logger.Debugf("Service %s has been %s", client.ObjectKeyFromObject(svc), result)
   362  
   363  	return nil
   364  }
   365  
   366  func (r *ingressReconciler) createOrUpdateEndpoints(ctx context.Context, desiredEndpoints *corev1.Endpoints) error {
   367  	ep := desiredEndpoints.DeepCopy()
   368  
   369  	result, err := controllerutil.CreateOrUpdate(ctx, r.client, ep, func() error {
   370  		ep.Subsets = desiredEndpoints.Subsets
   371  		ep.OwnerReferences = desiredEndpoints.OwnerReferences
   372  		ep.Annotations = mergeMap(ep.Annotations, desiredEndpoints.Annotations)
   373  		ep.Labels = mergeMap(ep.Labels, desiredEndpoints.Labels)
   374  
   375  		return nil
   376  	})
   377  	if err != nil {
   378  		return fmt.Errorf("failed to create or update Endpoints: %w", err)
   379  	}
   380  
   381  	r.logger.Debugf("Endpoints %s has been %s", client.ObjectKeyFromObject(ep), result)
   382  
   383  	return nil
   384  }
   385  
   386  // mergeMap merges the content from src into dst. Existing entries are overwritten.
   387  // If keyPrefixes are provided, only keys matching one of the prefixes are merged.
   388  func mergeMap(dst, src map[string]string, keyPrefixes ...string) map[string]string {
   389  	if src == nil {
   390  		return dst
   391  	}
   392  
   393  	if dst == nil {
   394  		dst = map[string]string{}
   395  	}
   396  
   397  	for key, value := range src {
   398  		if len(keyPrefixes) == 0 || atLeastOnePrefixMatches(key, keyPrefixes) {
   399  			dst[key] = value
   400  		}
   401  	}
   402  
   403  	return dst
   404  }
   405  
   406  func atLeastOnePrefixMatches(s string, prefixes []string) bool {
   407  	for _, p := range prefixes {
   408  		if strings.HasPrefix(s, p) {
   409  			return true
   410  		}
   411  	}
   412  
   413  	return false
   414  }
   415  
   416  func (r *ingressReconciler) tryDeletingResource(ctx context.Context, object client.Object, namespacedName types.NamespacedName) error {
   417  	if err := r.client.Get(ctx, namespacedName, object); err != nil {
   418  		if !k8serrors.IsNotFound(err) {
   419  			return fmt.Errorf("failed to get existing %T: %w", object, err)
   420  		}
   421  		return nil
   422  	}
   423  
   424  	if err := r.client.Delete(ctx, object); err != nil {
   425  		return fmt.Errorf("failed to delete existing %T: %w", object, err)
   426  	}
   427  
   428  	return nil
   429  }
   430  
   431  func (r *ingressReconciler) updateIngressLoadbalancerStatus(ctx context.Context, ingress *networkingv1.Ingress) error {
   432  	serviceNamespacedName := types.NamespacedName{}
   433  	if r.isEffectiveLoadbalancerModeDedicated(ingress) {
   434  		serviceNamespacedName.Namespace = ingress.Namespace
   435  		serviceNamespacedName.Name = fmt.Sprintf("%s-%s", ciliumIngressPrefix, ingress.Name)
   436  	} else {
   437  		serviceNamespacedName.Namespace = r.ciliumNamespace
   438  		serviceNamespacedName.Name = r.sharedResourcesName
   439  	}
   440  
   441  	loadbalancerService := corev1.Service{}
   442  	if err := r.client.Get(ctx, serviceNamespacedName, &loadbalancerService); err != nil {
   443  		if !k8serrors.IsNotFound(err) {
   444  			return fmt.Errorf("failed to get loadbalancer Service: %w", err)
   445  		}
   446  
   447  		// Reconcile will be triggered if the loadbalancer Service is updated
   448  		return nil
   449  	}
   450  
   451  	ingress.Status.LoadBalancer.Ingress = convertToNetworkV1IngressLoadBalancerIngress(loadbalancerService.Status.LoadBalancer.Ingress)
   452  
   453  	if err := r.client.Status().Update(ctx, ingress); err != nil {
   454  		return fmt.Errorf("failed to write Ingress status: %w", err)
   455  	}
   456  
   457  	return nil
   458  }
   459  
   460  func (r *ingressReconciler) tryCleanupIngressStatus(ctx context.Context, ingress *networkingv1.Ingress) error {
   461  	ingress.Status.LoadBalancer.Ingress = []networkingv1.IngressLoadBalancerIngress{}
   462  
   463  	if err := r.client.Status().Update(ctx, ingress); err != nil {
   464  		return fmt.Errorf("failed to update Ingress status: %w", err)
   465  	}
   466  
   467  	return nil
   468  }
   469  
   470  func convertToNetworkV1IngressLoadBalancerIngress(lbIngresses []corev1.LoadBalancerIngress) []networkingv1.IngressLoadBalancerIngress {
   471  	if lbIngresses == nil {
   472  		return nil
   473  	}
   474  
   475  	ingLBIngs := make([]networkingv1.IngressLoadBalancerIngress, 0, len(lbIngresses))
   476  	for _, lbIng := range lbIngresses {
   477  		ports := make([]networkingv1.IngressPortStatus, 0, len(lbIng.Ports))
   478  		for _, port := range lbIng.Ports {
   479  			ports = append(ports, networkingv1.IngressPortStatus{
   480  				Port:     port.Port,
   481  				Protocol: corev1.Protocol(port.Protocol),
   482  				Error:    port.Error,
   483  			})
   484  		}
   485  		ingLBIngs = append(ingLBIngs,
   486  			networkingv1.IngressLoadBalancerIngress{
   487  				IP:       lbIng.IP,
   488  				Hostname: lbIng.Hostname,
   489  				Ports:    ports,
   490  			})
   491  	}
   492  
   493  	return ingLBIngs
   494  }
   495  
   496  func (r *ingressReconciler) isEffectiveLoadbalancerModeDedicated(ingress *networkingv1.Ingress) bool {
   497  	value := annotations.GetAnnotationIngressLoadbalancerMode(ingress)
   498  	switch value {
   499  	case annotations.LoadbalancerModeDedicated:
   500  		return true
   501  	case annotations.LoadbalancerModeShared:
   502  		return false
   503  	default:
   504  		return r.defaultLoadbalancerMode == annotations.LoadbalancerModeDedicated
   505  	}
   506  }