github.com/verrazzano/verrazzano@v1.7.0/application-operator/controllers/ingresstrait/ingresstrait_controller.go (about)

     1  // Copyright (c) 2020, 2023, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package ingresstrait
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	certapiv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
    11  	certv1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1"
    12  	"github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2"
    13  	"github.com/crossplane/oam-kubernetes-runtime/pkg/oam"
    14  	"github.com/gertd/go-pluralize"
    15  	vzapi "github.com/verrazzano/verrazzano/application-operator/apis/oam/v1alpha1"
    16  	"github.com/verrazzano/verrazzano/application-operator/constants"
    17  	"github.com/verrazzano/verrazzano/application-operator/controllers"
    18  	"github.com/verrazzano/verrazzano/application-operator/controllers/clusters"
    19  	vznav "github.com/verrazzano/verrazzano/application-operator/controllers/navigation"
    20  	"github.com/verrazzano/verrazzano/application-operator/controllers/reconcileresults"
    21  	"github.com/verrazzano/verrazzano/application-operator/metricsexporter"
    22  	vzconst "github.com/verrazzano/verrazzano/pkg/constants"
    23  	vzctrl "github.com/verrazzano/verrazzano/pkg/controller"
    24  	vzlogInit "github.com/verrazzano/verrazzano/pkg/log"
    25  	"github.com/verrazzano/verrazzano/pkg/log/vzlog"
    26  	vzstring "github.com/verrazzano/verrazzano/pkg/string"
    27  	"github.com/verrazzano/verrazzano/platform-operator/controllers/verrazzano/component/common"
    28  	"go.uber.org/zap"
    29  	"google.golang.org/protobuf/types/known/durationpb"
    30  	istionet "istio.io/api/networking/v1alpha3"
    31  	"istio.io/api/security/v1beta1"
    32  	v1beta12 "istio.io/api/type/v1beta1"
    33  	istioclient "istio.io/client-go/pkg/apis/networking/v1alpha3"
    34  	clisecurity "istio.io/client-go/pkg/apis/security/v1beta1"
    35  	appsv1 "k8s.io/api/apps/v1"
    36  	corev1 "k8s.io/api/core/v1"
    37  	k8net "k8s.io/api/networking/v1"
    38  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    39  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    40  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    41  	"k8s.io/apimachinery/pkg/runtime"
    42  	"k8s.io/apimachinery/pkg/types"
    43  	"reflect"
    44  	ctrl "sigs.k8s.io/controller-runtime"
    45  	"sigs.k8s.io/controller-runtime/pkg/client"
    46  	"sigs.k8s.io/controller-runtime/pkg/controller"
    47  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    48  	"sigs.k8s.io/controller-runtime/pkg/event"
    49  	"sigs.k8s.io/controller-runtime/pkg/handler"
    50  	"sigs.k8s.io/controller-runtime/pkg/predicate"
    51  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    52  	"sigs.k8s.io/controller-runtime/pkg/source"
    53  	"strings"
    54  	"time"
    55  )
    56  
    57  const (
    58  	gatewayAPIVersion         = "networking.istio.io/v1alpha3"
    59  	gatewayKind               = "Gateway"
    60  	virtualServiceAPIVersion  = "networking.istio.io/v1alpha3"
    61  	virtualServiceKind        = "VirtualService"
    62  	certificateAPIVersion     = "cert-manager.io/v1"
    63  	certificateKind           = "Certificate"
    64  	serviceAPIVersion         = "v1"
    65  	serviceKind               = "Service"
    66  	clusterIPNone             = "None"
    67  	verrazzanoClusterIssuer   = "verrazzano-cluster-issuer"
    68  	httpServiceNamePrefix     = "http"
    69  	weblogicOperatorSelector  = "weblogic.createdByOperator"
    70  	wlProxySSLHeader          = "WL-Proxy-SSL"
    71  	wlProxySSLHeaderVal       = "true"
    72  	destinationRuleAPIVersion = "networking.istio.io/v1alpha3"
    73  	destinationRuleKind       = "DestinationRule"
    74  	authzPolicyAPIVersion     = "security.istio.io/v1beta1"
    75  	authzPolicyKind           = "AuthorizationPolicy"
    76  	controllerName            = "ingresstrait"
    77  	httpsProtocol             = "HTTPS"
    78  	istioIngressGateway       = "istio-ingressgateway"
    79  	finalizerName             = "ingresstrait.finalizers.verrazzano.io"
    80  )
    81  
    82  // The port names used by WebLogic operator that do not have http prefix.
    83  // Reference: https://github.com/oracle/weblogic-kubernetes-operator/blob/main/operator/src/main/resources/scripts/model_wdt_mii_filter.py
    84  var (
    85  	weblogicPortNames = []string{"tcp-cbt", "tcp-ldap", "tcp-iiop", "tcp-snmp", "tcp-default", "tls-ldaps",
    86  		"tls-default", "tls-cbts", "tls-iiops", "tcp-internal-t3", "internal-t3"}
    87  )
    88  
    89  // Reconciler is used to reconcile an IngressTrait object
    90  type Reconciler struct {
    91  	client.Client
    92  	Controller controller.Controller
    93  	Log        *zap.SugaredLogger
    94  	Scheme     *runtime.Scheme
    95  }
    96  
    97  // SetupWithManager creates a controller and adds it to the manager, and sets up any watches
    98  func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) (err error) {
    99  	r.Controller, err = ctrl.NewControllerManagedBy(mgr).
   100  		WithOptions(controller.Options{
   101  			RateLimiter: controllers.NewDefaultRateLimiter(),
   102  		}).
   103  		For(&vzapi.IngressTrait{}).
   104  		Build(r)
   105  	if err != nil {
   106  		return err
   107  	}
   108  	return r.setupWatches()
   109  }
   110  
   111  // Reconcile reconciles an IngressTrait with other related resources required for ingress.
   112  // This also results in the status of the ingress trait resource being updated.
   113  // +kubebuilder:rbac:groups=oam.verrazzano.io,resources=ingresstraits,verbs=get;list;watch;create;update;patch;delete
   114  // +kubebuilder:rbac:groups=oam.verrazzano.io,resources=ingresstraits/status,verbs=get;update;patch
   115  func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
   116  	if ctx == nil {
   117  		return ctrl.Result{}, errors.New("context cannot be nil")
   118  	}
   119  
   120  	// We do not want any resource to get reconciled if it is in namespace kube-system
   121  	// This is due to a bug found in OKE, it should not affect functionality of any vz operators
   122  	// If this is the case then return success
   123  	counterMetricObject, errorCounterMetricObject, reconcileDurationMetricObject, zapLogForMetrics, err := metricsexporter.ExposeControllerMetrics(controllerName, metricsexporter.IngresstraitReconcileCounter, metricsexporter.IngresstraitReconcileError, metricsexporter.IngresstraitReconcileDuration)
   124  	if err != nil {
   125  		return ctrl.Result{}, err
   126  	}
   127  	reconcileDurationMetricObject.TimerStart()
   128  	defer reconcileDurationMetricObject.TimerStop()
   129  
   130  	if req.Namespace == vzconst.KubeSystem {
   131  		log := zap.S().With(vzlogInit.FieldResourceNamespace, req.Namespace, vzlogInit.FieldResourceName, req.Name, vzlogInit.FieldController, controllerName)
   132  		log.Infof("Ingress trait resource %v should not be reconciled in kube-system namespace, ignoring", req.NamespacedName)
   133  		return reconcile.Result{}, nil
   134  	}
   135  	var trait *vzapi.IngressTrait
   136  	if trait, err = r.fetchTrait(ctx, req.NamespacedName, zap.S()); err != nil {
   137  		return clusters.IgnoreNotFoundWithLog(err, zap.S())
   138  	}
   139  	// If the trait no longer exists return success.
   140  	if trait == nil {
   141  		return reconcile.Result{}, nil
   142  	}
   143  	log, err := clusters.GetResourceLogger("ingresstrait", req.NamespacedName, trait)
   144  	if err != nil {
   145  		errorCounterMetricObject.Inc(zapLogForMetrics, err)
   146  		zap.S().Errorf("Failed to create controller logger for ingress trait resource: %v", err)
   147  		return clusters.NewRequeueWithDelay(), nil
   148  	}
   149  	log.Oncef("Reconciling ingress trait resource %v, generation %v", req.NamespacedName, trait.Generation)
   150  
   151  	res, err := r.doReconcile(ctx, trait, log)
   152  	if clusters.ShouldRequeue(res) {
   153  		return res, nil
   154  	}
   155  	// Never return an error since it has already been logged and we don't want the
   156  	// controller runtime to log again (with stack trace).  Just re-queue if there is an error.
   157  	if err != nil {
   158  		errorCounterMetricObject.Inc(zapLogForMetrics, err)
   159  		return clusters.NewRequeueWithDelay(), nil
   160  	}
   161  
   162  	log.Oncef("Finished reconciling ingress trait %v", req.NamespacedName)
   163  	counterMetricObject.Inc(zapLogForMetrics, err)
   164  	return ctrl.Result{}, nil
   165  }
   166  
   167  // doReconcile performs the reconciliation operations for the ingress trait
   168  func (r *Reconciler) doReconcile(ctx context.Context, trait *vzapi.IngressTrait, log vzlog.VerrazzanoLogger) (ctrl.Result, error) {
   169  	// If the ingress trait no longer exists or is being deleted then cleanup the associated cert and secret resources
   170  	if isIngressTraitBeingDeleted(trait) {
   171  		log.Debugf("Deleting ingress trait %v", trait)
   172  		if err := cleanup(trait, r.Client, log); err != nil {
   173  			// Requeue without error to avoid higher level log message
   174  			return reconcile.Result{Requeue: true}, nil
   175  		}
   176  		// resource cleanup has succeeded, remove the finalizer
   177  		if err := r.removeFinalizerIfRequired(ctx, trait, log); err != nil {
   178  			return vzctrl.NewRequeueWithDelay(2, 3, time.Second), nil
   179  		}
   180  		return reconcile.Result{}, nil
   181  	}
   182  
   183  	// add finalizer
   184  	if err := r.addFinalizerIfRequired(ctx, trait, log); err != nil {
   185  		return vzctrl.NewRequeueWithDelay(2, 3, time.Second), nil
   186  	}
   187  
   188  	// Create or update the child resources of the trait and collect the outcomes.
   189  	status, result, err := r.createOrUpdateChildResources(ctx, trait, log)
   190  	if err != nil {
   191  		return reconcile.Result{}, err
   192  	} else if result.Requeue {
   193  		return result, nil
   194  	}
   195  
   196  	// Update the status of the trait resource using the outcomes of the create or update.
   197  	return r.updateTraitStatus(ctx, trait, status)
   198  }
   199  
   200  // isIngressTraitBeingDeleted determines if the ingress trait is in the process of being deleted.
   201  // This is done checking for a non-nil deletion timestamp.
   202  func isIngressTraitBeingDeleted(trait *vzapi.IngressTrait) bool {
   203  	return trait != nil && trait.GetDeletionTimestamp() != nil
   204  }
   205  
   206  // removeFinalizerIfRequired removes the finalizer from the ingress trait if required
   207  // The finalizer is only removed if the ingress trait is being deleted and the finalizer had been added
   208  func (r *Reconciler) removeFinalizerIfRequired(ctx context.Context, trait *vzapi.IngressTrait, log vzlog.VerrazzanoLogger) error {
   209  	if !trait.DeletionTimestamp.IsZero() && vzstring.SliceContainsString(trait.Finalizers, finalizerName) {
   210  		log.Debugf("Removing finalizer from ingress trait %s", trait.Name)
   211  		trait.Finalizers = vzstring.RemoveStringFromSlice(trait.Finalizers, finalizerName)
   212  		err := r.Update(ctx, trait)
   213  		return vzlogInit.ConflictWithLog(fmt.Sprintf("Failed to remove finalizer from ingress trait %s", trait.Name), err, zap.S())
   214  	}
   215  	return nil
   216  }
   217  
   218  // addFinalizerIfRequired adds the finalizer to the ingress trait if required
   219  // The finalizer is only added if the ingress trait is not being deleted and the finalizer has not previously been added
   220  func (r *Reconciler) addFinalizerIfRequired(ctx context.Context, trait *vzapi.IngressTrait, log vzlog.VerrazzanoLogger) error {
   221  	if trait.GetDeletionTimestamp().IsZero() && !vzstring.SliceContainsString(trait.Finalizers, finalizerName) {
   222  		log.Debugf("Adding finalizer for ingress trait %s", trait.Name)
   223  		trait.Finalizers = append(trait.Finalizers, finalizerName)
   224  		err := r.Update(ctx, trait)
   225  		_, err = vzlogInit.IgnoreConflictWithLog(fmt.Sprintf("Failed to add finalizer to ingress trait %s", trait.Name), err, zap.S())
   226  		return err
   227  	}
   228  	return nil
   229  }
   230  
   231  // createOrUpdateChildResources creates or updates the Gateway and VirtualService resources that
   232  // should be used to setup ingress to the service.
   233  // This is the cardinality:
   234  //
   235  //	1 Gateway per Application
   236  //	1 Gateway server per IngressTrait
   237  //	1 VirtualService per IngressTrait rule
   238  func (r *Reconciler) createOrUpdateChildResources(ctx context.Context, trait *vzapi.IngressTrait, log vzlog.VerrazzanoLogger) (*reconcileresults.ReconcileResults, ctrl.Result, error) {
   239  	status := reconcileresults.ReconcileResults{}
   240  	rules := trait.Spec.Rules
   241  	// If there are no rules, create a single default rule
   242  	if len(rules) == 0 {
   243  		rules = []vzapi.IngressRule{{}}
   244  	}
   245  
   246  	// Create a list of unique hostnames across all rules in the trait
   247  	allHostsForTrait := r.coallateAllHostsForTrait(trait, status)
   248  	// Generate the certificate and secret for all hosts in the trait rules
   249  	secretName := r.createOrUseGatewaySecret(ctx, trait, allHostsForTrait, &status, log)
   250  	if secretName != "" {
   251  		gwName, err := buildGatewayName(trait)
   252  		if err != nil {
   253  			status.Errors = append(status.Errors, err)
   254  		} else {
   255  			// The Gateway is shared across all ingress traits for the app, update it with all known hosts for the trait
   256  			// - Must create GW before service so that external DNS sees the GW once the service is created
   257  			gateway, err := r.createOrUpdateGateway(ctx, trait, allHostsForTrait, gwName, secretName, &status, log)
   258  			if err != nil {
   259  				return &status, ctrl.Result{}, err
   260  			}
   261  			for index, rule := range rules {
   262  				// Find the services associated with the trait in the application configuration.
   263  				var services []*corev1.Service
   264  				services, err := r.fetchServicesFromTrait(ctx, trait, log)
   265  				if err != nil {
   266  					return &status, reconcile.Result{}, err
   267  				} else if len(services) == 0 {
   268  					// This will be the case if the service has not started yet so we requeue and try again.
   269  					return &status, reconcile.Result{Requeue: true, RequeueAfter: clusters.GetRandomRequeueDelay()}, err
   270  				}
   271  
   272  				// Get the list of hosts for this rule.  A virtual service can have the same hosts as another virtual service
   273  				// using the same gateway.  Istio will combine the rules to figure out the routing.
   274  				// See https://istio.io/latest/docs/ops/best-practices/traffic-management/#split-virtual-services
   275  				vsHosts, err := createHostsFromIngressTraitRule(r, rule, trait)
   276  				if err != nil {
   277  					status.Errors = append(status.Errors, err)
   278  				}
   279  
   280  				vsName := fmt.Sprintf("%s-rule-%d-vs", trait.Name, index)
   281  				drName := fmt.Sprintf("%s-rule-%d-dr", trait.Name, index)
   282  				authzPolicyName := fmt.Sprintf("%s-rule-%d-authz", trait.Name, index)
   283  				r.createOrUpdateVirtualService(ctx, trait, rule, vsHosts, vsName, services, gateway, &status, log)
   284  				r.createOrUpdateDestinationRule(ctx, trait, rule, drName, &status, log, services)
   285  				r.createOrUpdateAuthorizationPolicies(ctx, trait, rule, authzPolicyName, allHostsForTrait, &status, log)
   286  			}
   287  		}
   288  	}
   289  	return &status, ctrl.Result{}, nil
   290  }
   291  
   292  func (r *Reconciler) coallateAllHostsForTrait(trait *vzapi.IngressTrait, status reconcileresults.ReconcileResults) []string {
   293  	allHosts := []string{}
   294  	var err error
   295  	for _, rule := range trait.Spec.Rules {
   296  		if allHosts, err = createHostsFromIngressTraitRule(r, rule, trait, allHosts...); err != nil {
   297  			status.Errors = append(status.Errors, err)
   298  		}
   299  	}
   300  	return allHosts
   301  }
   302  
   303  // buildGatewayName will generate a gateway name from the namespace and application name of the provided trait. Returns
   304  // an error if the app name is not available.
   305  func buildGatewayName(trait *vzapi.IngressTrait) (string, error) {
   306  	appName, ok := trait.Labels[oam.LabelAppName]
   307  	if !ok {
   308  		return "", errors.New("OAM app name label missing from metadata, unable to generate gateway name")
   309  	}
   310  	gwName := fmt.Sprintf("%s-%s-gw", trait.Namespace, appName)
   311  	return gwName, nil
   312  }
   313  
   314  // buildCertificateName will construct a cert name from the trait.
   315  func buildCertificateName(trait *vzapi.IngressTrait) string {
   316  	return fmt.Sprintf("%s-%s-cert", trait.Namespace, trait.Name)
   317  }
   318  
   319  // buildCertificateSecretName will construct a cert secret name from the trait.
   320  func buildCertificateSecretName(trait *vzapi.IngressTrait) string {
   321  	return fmt.Sprintf("%s-%s-cert-secret", trait.Namespace, trait.Name)
   322  }
   323  
   324  // buildLegacyCertificateName will generate a cert name used by older version of Verrazzano
   325  func buildLegacyCertificateName(trait *vzapi.IngressTrait) string {
   326  	appName, ok := trait.Labels[oam.LabelAppName]
   327  	if !ok {
   328  		return ""
   329  	}
   330  	return fmt.Sprintf("%s-%s-cert", trait.Namespace, appName)
   331  }
   332  
   333  // buildLegacyCertificateSecretName will generate a cert secret name used by older version of Verrazzano
   334  func buildLegacyCertificateSecretName(trait *vzapi.IngressTrait) string {
   335  	appName, ok := trait.Labels[oam.LabelAppName]
   336  	if !ok {
   337  		return ""
   338  	}
   339  	return fmt.Sprintf("%s-%s-cert-secret", trait.Namespace, appName)
   340  }
   341  
   342  // updateTraitStatus updates the trait's status conditions and resources if they have changed.
   343  func (r *Reconciler) updateTraitStatus(ctx context.Context, trait *vzapi.IngressTrait, status *reconcileresults.ReconcileResults) (reconcile.Result, error) {
   344  	resources := status.CreateResources()
   345  	if status.ContainsErrors() || !reflect.DeepEqual(trait.Status.Resources, resources) {
   346  		trait.Status = vzapi.IngressTraitStatus{
   347  			ConditionedStatus: status.CreateConditionedStatus(),
   348  			Resources:         resources}
   349  		// Requeue to prevent potential conflict errors being logged.
   350  		return reconcile.Result{Requeue: true}, r.Status().Update(ctx, trait)
   351  	}
   352  	return reconcile.Result{}, nil
   353  }
   354  
   355  // fetchTrait attempts to get a trait given a namespaced name.
   356  // Will return nil for the trait and no error if the trait does not exist.
   357  func (r *Reconciler) fetchTrait(ctx context.Context, nsn types.NamespacedName, log *zap.SugaredLogger) (*vzapi.IngressTrait, error) {
   358  	var trait vzapi.IngressTrait
   359  	log.Debugf("Fetching trait %s", nsn.Name)
   360  	if err := r.Get(ctx, nsn, &trait); err != nil {
   361  		if k8serrors.IsNotFound(err) {
   362  			log.Debugf("Trait %s is not found: %v", nsn.Name, err)
   363  			return nil, nil
   364  		}
   365  		log.Debugf("Failed to fetch trait %s", nsn.Name)
   366  		return nil, err
   367  	}
   368  	return &trait, nil
   369  }
   370  
   371  // fetchWorkloadDefinition fetches the workload definition of the provided workload.
   372  // The definition is found by converting the workload APIVersion and Kind to a CRD resource name.
   373  // for example core.oam.dev/v1alpha2.ContainerizedWorkload would be converted to
   374  // containerizedworkloads.core.oam.dev.  Workload definitions are always found in the default
   375  // namespace.
   376  func (r *Reconciler) fetchWorkloadDefinition(ctx context.Context, workload *unstructured.Unstructured, log vzlog.VerrazzanoLogger) (*v1alpha2.WorkloadDefinition, error) {
   377  	workloadAPIVer, _, _ := unstructured.NestedString(workload.Object, "apiVersion")
   378  	workloadKind, _, _ := unstructured.NestedString(workload.Object, "kind")
   379  	workloadName := convertAPIVersionAndKindToNamespacedName(workloadAPIVer, workloadKind)
   380  	workloadDef := v1alpha2.WorkloadDefinition{}
   381  	err := r.Get(ctx, workloadName, &workloadDef)
   382  	if err != nil {
   383  		if k8serrors.IsNotFound(err) {
   384  			return nil, nil
   385  		}
   386  		log.Errorf("Failed to fetch workload %s definition: %v", workloadName, err)
   387  		return nil, err
   388  	}
   389  	return &workloadDef, nil
   390  }
   391  
   392  // fetchWorkloadChildren finds the children resource of a workload resource.
   393  // Both the workload and the returned array of children are unstructured maps of primitives.
   394  // Finding children is done by first looking to the workflow definition of the provided workload.
   395  // The workload definition contains a set of child resource types supported by the workload.
   396  // The namespace of the workload is then searched for child resources of the supported types.
   397  func (r *Reconciler) fetchWorkloadChildren(ctx context.Context, workload *unstructured.Unstructured, log vzlog.VerrazzanoLogger) ([]*unstructured.Unstructured, error) {
   398  	var err error
   399  	var workloadDefinition *v1alpha2.WorkloadDefinition
   400  
   401  	// Attempt to fetch workload definition based on the workload GVK.
   402  	if workloadDefinition, err = r.fetchWorkloadDefinition(ctx, workload, log); err != nil {
   403  		log.Debug("Workload definition not found")
   404  	}
   405  	if workloadDefinition != nil {
   406  		// If the workload definition is found then fetch child resources of the declared child types
   407  		var children []*unstructured.Unstructured
   408  		if children, err = r.fetchChildResourcesByAPIVersionKinds(ctx, workload.GetNamespace(), workload.GetUID(), workloadDefinition.Spec.ChildResourceKinds, log); err != nil {
   409  			return nil, err
   410  		}
   411  		return children, nil
   412  	} else if workload.GetAPIVersion() == appsv1.SchemeGroupVersion.String() {
   413  		// Else if this is a native resource then use the workload itself as the child
   414  		log.Debugf("Found native workload: %v", workload)
   415  		return []*unstructured.Unstructured{workload}, nil
   416  	} else if workload.GetAPIVersion() == corev1.SchemeGroupVersion.String() &&
   417  		workload.GetKind() == "Service" {
   418  		// limits v1 workloads to services only
   419  		log.Debugf("Found service workload: %v", workload)
   420  		return []*unstructured.Unstructured{workload}, nil
   421  	} else {
   422  		// Else return an error that the workload type is not supported by this trait.
   423  		log.Debug("Workload not supported by trait")
   424  		return nil, fmt.Errorf("workload not supported by trait")
   425  	}
   426  }
   427  
   428  // fetchChildResourcesByAPIVersionKinds find all of the child resource of specific kinds
   429  // having a specific parent UID.  The child kinds are APIVersion and Kind
   430  // (e.g. apps/v1.Deployment or v1.Service).  The objects of these resource kinds are listed
   431  // and the ones having the correct parent UID are collected and accumulated and returned.
   432  // This is used to collect a subset children of a particular parent object.
   433  // ctx - The calling context
   434  // namespace - The namespace to search for children objects
   435  // parentUID - The parent UID a child must have to be included in the result.
   436  // childResKinds - The set of resource kinds a child's resource kind must in to be included in the result.
   437  func (r *Reconciler) fetchChildResourcesByAPIVersionKinds(ctx context.Context, namespace string, parentUID types.UID, childResKinds []v1alpha2.ChildResourceKind, log vzlog.VerrazzanoLogger) ([]*unstructured.Unstructured, error) {
   438  	var childResources []*unstructured.Unstructured
   439  	log.Debug("Fetch child resources")
   440  	for _, childResKind := range childResKinds {
   441  		resources := unstructured.UnstructuredList{}
   442  		resources.SetAPIVersion(childResKind.APIVersion)
   443  		resources.SetKind(childResKind.Kind + "List") // Only required by "fake" client used in tests.
   444  		if err := r.List(ctx, &resources, client.InNamespace(namespace), client.MatchingLabels(childResKind.Selector)); err != nil {
   445  			log.Errorf("Failed listing child resources: %v", err)
   446  			return nil, err
   447  		}
   448  		for i, item := range resources.Items {
   449  			for _, owner := range item.GetOwnerReferences() {
   450  				if owner.UID == parentUID {
   451  					log.Debugf("Found child %s.%s:%s", item.GetAPIVersion(), item.GetKind(), item.GetName())
   452  					childResources = append(childResources, &resources.Items[i])
   453  					break
   454  				}
   455  			}
   456  		}
   457  	}
   458  	return childResources, nil
   459  }
   460  
   461  // createOrUseGatewaySecret will create a certificate that will be embedded in an secret or leverage an existing secret
   462  // if one is configured in the ingress.
   463  func (r *Reconciler) createOrUseGatewaySecret(ctx context.Context, trait *vzapi.IngressTrait, hostsForTrait []string, status *reconcileresults.ReconcileResults, log vzlog.VerrazzanoLogger) string {
   464  	var secretName string
   465  
   466  	if trait.Spec.TLS != (vzapi.IngressSecurity{}) {
   467  		secretName = r.validateConfiguredSecret(trait, status)
   468  	} else {
   469  		cleanupCert(buildLegacyCertificateName(trait), r.Client, log)
   470  		cleanupSecret(buildLegacyCertificateSecretName(trait), r.Client, log)
   471  		secretName = r.createGatewayCertificate(ctx, trait, hostsForTrait, status, log)
   472  	}
   473  
   474  	return secretName
   475  }
   476  
   477  // createGatewayCertificate creates a certificate that is leveraged by the cert manager to create a certificate embedded
   478  // in a secret.  That secret will be leveraged by the gateway to provide TLS/HTTPS endpoints for deployed applications.
   479  // There will be one gateway generated per application.  The generated virtual services will be routed via the
   480  // application-wide gateway.  This implementation addresses a known Istio traffic management issue
   481  // (see https://istio.io/v1.7/docs/ops/common-problems/network-issues/#404-errors-occur-when-multiple-gateways-configured-with-same-tls-certificate)
   482  func (r *Reconciler) createGatewayCertificate(ctx context.Context, trait *vzapi.IngressTrait, hostsForTrait []string, status *reconcileresults.ReconcileResults, log vzlog.VerrazzanoLogger) string {
   483  	//ensure trait does not specify hosts.  should be moved to ingress trait validating webhook
   484  	for _, rule := range trait.Spec.Rules {
   485  		if len(rule.Hosts) != 0 {
   486  			log.Debug("Host(s) specified in the trait rules will likely not correlate to the generated certificate CN." +
   487  				" Please redeploy after removing the hosts or specifying a secret with the given hosts in its SAN list")
   488  			break
   489  		}
   490  	}
   491  
   492  	certName := buildCertificateName(trait)
   493  	secretName := buildCertificateSecretName(trait)
   494  	certificate := &certapiv1.Certificate{
   495  		TypeMeta: metav1.TypeMeta{
   496  			Kind:       certificateKind,
   497  			APIVersion: certificateAPIVersion,
   498  		},
   499  		ObjectMeta: metav1.ObjectMeta{
   500  			Namespace: constants.IstioSystemNamespace,
   501  			Name:      certName,
   502  		}}
   503  
   504  	res, err := controllerutil.CreateOrUpdate(ctx, r.Client, certificate, func() error {
   505  		certificate.Spec = certapiv1.CertificateSpec{
   506  			DNSNames:   hostsForTrait,
   507  			SecretName: secretName,
   508  			IssuerRef: certv1.ObjectReference{
   509  				Name: verrazzanoClusterIssuer,
   510  				Kind: "ClusterIssuer",
   511  			},
   512  		}
   513  
   514  		return nil
   515  	})
   516  	ref := vzapi.QualifiedResourceRelation{APIVersion: certificateAPIVersion, Kind: certificateKind, Name: certificate.Name, Role: "certificate"}
   517  	status.Relations = append(status.Relations, ref)
   518  	status.Results = append(status.Results, res)
   519  	status.Errors = append(status.Errors, err)
   520  
   521  	if err != nil {
   522  		log.Errorf("Failed to create or update gateway secret containing certificate: %v", err)
   523  		return ""
   524  	}
   525  
   526  	return secretName
   527  }
   528  
   529  // validateConfiguredSecret ensures that a secret is specified and the trait rules specify a "hosts" setting.  The
   530  // specification of a secret implies that a certificate was created for specific hosts that differ than the host names
   531  // generated by the runtime (when no hosts are specified).
   532  func (r *Reconciler) validateConfiguredSecret(trait *vzapi.IngressTrait, status *reconcileresults.ReconcileResults) string {
   533  	secretName := trait.Spec.TLS.SecretName
   534  	if secretName != "" {
   535  		// if a secret is specified then host(s) must be provided for all rules
   536  		for _, rule := range trait.Spec.Rules {
   537  			if len(rule.Hosts) == 0 {
   538  				err := errors.New("all rules must specify at least one host when a secret is specified for TLS transport")
   539  				ref := vzapi.QualifiedResourceRelation{APIVersion: "v1", Kind: "Secret", Name: secretName, Role: "secret"}
   540  				status.Relations = append(status.Relations, ref)
   541  				status.Errors = append(status.Errors, err)
   542  				status.Results = append(status.Results, controllerutil.OperationResultNone)
   543  				return ""
   544  			}
   545  		}
   546  	}
   547  	return secretName
   548  }
   549  
   550  // createOrUpdateGateway creates or updates the Gateway child resource of the trait.
   551  // Results are added to the status object.
   552  func (r *Reconciler) createOrUpdateGateway(ctx context.Context, trait *vzapi.IngressTrait, hostsForTrait []string, gwName string, secretName string, status *reconcileresults.ReconcileResults, log vzlog.VerrazzanoLogger) (*istioclient.Gateway, error) {
   553  	// Create a gateway populating only gwName metadata.
   554  	// This is used as default if the gateway needs to be created.
   555  	gateway := &istioclient.Gateway{
   556  		TypeMeta: metav1.TypeMeta{
   557  			APIVersion: gatewayAPIVersion,
   558  			Kind:       gatewayKind},
   559  		ObjectMeta: metav1.ObjectMeta{
   560  			Namespace: trait.Namespace,
   561  			Name:      gwName}}
   562  
   563  	res, err := common.CreateOrUpdateProtobuf(ctx, r.Client, gateway, func() error {
   564  		return r.mutateGateway(gateway, trait, hostsForTrait, secretName)
   565  	})
   566  
   567  	// Return if no changes
   568  	if err == nil && res == controllerutil.OperationResultNone {
   569  		return gateway, nil
   570  	}
   571  
   572  	ref := vzapi.QualifiedResourceRelation{APIVersion: gatewayAPIVersion, Kind: gatewayKind, Name: gwName, Role: "gateway"}
   573  	status.Relations = append(status.Relations, ref)
   574  	status.Results = append(status.Results, res)
   575  	status.Errors = append(status.Errors, err)
   576  
   577  	if err != nil {
   578  		log.Errorf("Failed to create or update gateway: %v", err)
   579  		return nil, err
   580  	}
   581  
   582  	return gateway, nil
   583  }
   584  
   585  // mutateGateway mutates the output Gateway child resource.
   586  func (r *Reconciler) mutateGateway(gateway *istioclient.Gateway, trait *vzapi.IngressTrait, hostsForTrait []string, secretName string) error {
   587  	// Create/update the server entry related to the IngressTrait in the Gateway
   588  	server := &istionet.Server{
   589  		Name:  trait.Name,
   590  		Hosts: hostsForTrait,
   591  		Port: &istionet.Port{
   592  			Name:     formatGatewaySeverPortName(trait.Name),
   593  			Number:   443,
   594  			Protocol: httpsProtocol,
   595  		},
   596  		Tls: &istionet.ServerTLSSettings{
   597  			Mode:           istionet.ServerTLSSettings_SIMPLE,
   598  			CredentialName: secretName,
   599  		},
   600  	}
   601  	gateway.Spec.Servers = r.updateGatewayServersList(gateway.Spec.Servers, server)
   602  
   603  	// Set the spec content.
   604  	gateway.Spec.Selector = map[string]string{"istio": "ingressgateway"}
   605  
   606  	// Set the owner reference.
   607  	appName, ok := trait.Labels[oam.LabelAppName]
   608  	if ok {
   609  		appConfig := &v1alpha2.ApplicationConfiguration{}
   610  		err := r.Get(context.TODO(), types.NamespacedName{Namespace: trait.Namespace, Name: appName}, appConfig)
   611  		if err != nil {
   612  			return err
   613  		}
   614  		err = controllerutil.SetControllerReference(appConfig, gateway, r.Scheme)
   615  		if err != nil {
   616  			return err
   617  		}
   618  	}
   619  	return nil
   620  }
   621  
   622  func formatGatewaySeverPortName(traitName string) string {
   623  	return fmt.Sprintf("https-%s", traitName)
   624  }
   625  
   626  // updateGatewayServersList Update/add the Server entry for the IngressTrait to the gateway servers list
   627  // Each gateway server entry has a TLS field for the certificate.  This corresponds to the IngressTrait TLS field.
   628  func (r *Reconciler) updateGatewayServersList(servers []*istionet.Server, server *istionet.Server) []*istionet.Server {
   629  	if len(servers) == 0 {
   630  		servers = append(servers, server)
   631  		r.Log.Debugf("Added new server for %s", server.Name)
   632  		return servers
   633  	}
   634  	if len(servers) == 1 && len(servers[0].Name) == 0 && servers[0].Port.Name == "https" {
   635  		// upgrade case, before 1.3 all VirtualServices associated with a Gateway shared a single unnamed Server object
   636  		// - replace the empty name server with the named one
   637  		servers[0] = server
   638  		r.Log.Debugf("Replaced server %s", server.Name)
   639  		return servers
   640  	}
   641  	for index, existingServer := range servers {
   642  		if existingServer.Name == server.Name {
   643  			r.Log.Debugf("Updating server %s", server.Name)
   644  			servers[index] = server
   645  			return servers
   646  		}
   647  	}
   648  	servers = append(servers, server)
   649  	return servers
   650  }
   651  
   652  // findHost searches for a host in the provided list. If found it will
   653  // return it's key, otherwise it will return -1 and a bool of false.
   654  func findHost(hosts []string, newHost string) (int, bool) {
   655  	for i, host := range hosts {
   656  		if strings.EqualFold(host, newHost) {
   657  			return i, true
   658  		}
   659  	}
   660  	return -1, false
   661  }
   662  
   663  // createOrUpdateVirtualService creates or updates the VirtualService child resource of the trait.
   664  // Results are added to the status object.
   665  func (r *Reconciler) createOrUpdateVirtualService(ctx context.Context, trait *vzapi.IngressTrait, rule vzapi.IngressRule,
   666  	allHostsForTrait []string, name string, services []*corev1.Service, gateway *istioclient.Gateway,
   667  	status *reconcileresults.ReconcileResults, log vzlog.VerrazzanoLogger) {
   668  	// Create a virtual service populating only name metadata.
   669  	// This is used as default if the virtual service needs to be created.
   670  	virtualService := &istioclient.VirtualService{
   671  		TypeMeta: metav1.TypeMeta{
   672  			APIVersion: virtualServiceAPIVersion,
   673  			Kind:       virtualServiceKind},
   674  		ObjectMeta: metav1.ObjectMeta{
   675  			Namespace: trait.Namespace,
   676  			Name:      name}}
   677  
   678  	res, err := common.CreateOrUpdateProtobuf(ctx, r.Client, virtualService, func() error {
   679  		return r.mutateVirtualService(virtualService, trait, rule, allHostsForTrait, services, gateway)
   680  	})
   681  
   682  	ref := vzapi.QualifiedResourceRelation{APIVersion: virtualServiceAPIVersion, Kind: virtualServiceKind, Name: name, Role: "virtualservice"}
   683  	status.Relations = append(status.Relations, ref)
   684  	status.Results = append(status.Results, res)
   685  	status.Errors = append(status.Errors, err)
   686  
   687  	if err != nil {
   688  		log.Errorf("Failed to create or update virtual service: %v", err)
   689  	}
   690  }
   691  
   692  // mutateVirtualService mutates the output virtual service resource
   693  func (r *Reconciler) mutateVirtualService(virtualService *istioclient.VirtualService, trait *vzapi.IngressTrait, rule vzapi.IngressRule, allHostsForTrait []string, services []*corev1.Service, gateway *istioclient.Gateway) error {
   694  	// Set the spec content.
   695  	var err error
   696  	virtualService.Spec.Gateways = []string{gateway.Name}
   697  	virtualService.Spec.Hosts = allHostsForTrait // We may set this multiple times if there are multiple rules, but should be OK
   698  	matches := []*istionet.HTTPMatchRequest{}
   699  	paths := getPathsFromRule(rule)
   700  	for _, path := range paths {
   701  		matches = append(matches, &istionet.HTTPMatchRequest{
   702  			Uri: createVirtualServiceMatchURIFromIngressTraitPath(path)})
   703  	}
   704  	dest, err := createDestinationFromRuleOrService(rule, services)
   705  	if err != nil {
   706  		return err
   707  	}
   708  	route := istionet.HTTPRoute{
   709  		Match: matches,
   710  		Route: []*istionet.HTTPRouteDestination{dest}}
   711  	if vznav.IsWeblogicWorkloadKind(trait) {
   712  		route.Headers = &istionet.Headers{
   713  			Request: &istionet.Headers_HeaderOperations{
   714  				Add: map[string]string{
   715  					wlProxySSLHeader: wlProxySSLHeaderVal,
   716  				},
   717  			},
   718  		}
   719  	}
   720  	virtualService.Spec.Http = []*istionet.HTTPRoute{&route}
   721  
   722  	// Set the owner reference.
   723  	_ = controllerutil.SetControllerReference(trait, virtualService, r.Scheme)
   724  	return nil
   725  }
   726  
   727  // createOfUpdateDestinationRule creates or updates the DestinationRule.
   728  func (r *Reconciler) createOrUpdateDestinationRule(ctx context.Context, trait *vzapi.IngressTrait, rule vzapi.IngressRule, name string, status *reconcileresults.ReconcileResults, log vzlog.VerrazzanoLogger, services []*corev1.Service) {
   729  	if rule.Destination.HTTPCookie != nil {
   730  		destinationRule := &istioclient.DestinationRule{
   731  			TypeMeta: metav1.TypeMeta{
   732  				APIVersion: destinationRuleAPIVersion,
   733  				Kind:       destinationRuleKind},
   734  			ObjectMeta: metav1.ObjectMeta{
   735  				Namespace: trait.Namespace,
   736  				Name:      name},
   737  		}
   738  		namespace := &corev1.Namespace{}
   739  		namespaceErr := r.Client.Get(ctx, client.ObjectKey{Namespace: "", Name: trait.Namespace}, namespace)
   740  		if namespaceErr != nil {
   741  			log.Errorf("Failed to retrieve namespace resource: %v", namespaceErr)
   742  		}
   743  
   744  		res, err := common.CreateOrUpdateProtobuf(ctx, r.Client, destinationRule, func() error {
   745  			return r.mutateDestinationRule(destinationRule, trait, rule, services, namespace)
   746  		})
   747  
   748  		ref := vzapi.QualifiedResourceRelation{APIVersion: destinationRuleAPIVersion, Kind: destinationRuleKind, Name: name, Role: "destinationrule"}
   749  		status.Relations = append(status.Relations, ref)
   750  		status.Results = append(status.Results, res)
   751  		status.Errors = append(status.Errors, err)
   752  
   753  		if err != nil {
   754  			log.Errorf("Failed to create or update destination rule: %v", err)
   755  		}
   756  	}
   757  }
   758  
   759  // mutateDestinationRule changes the destination rule based upon a traits configuration
   760  func (r *Reconciler) mutateDestinationRule(destinationRule *istioclient.DestinationRule, trait *vzapi.IngressTrait, rule vzapi.IngressRule, services []*corev1.Service, namespace *corev1.Namespace) error {
   761  	dest, err := createDestinationFromRuleOrService(rule, services)
   762  	if err != nil {
   763  		return err
   764  	}
   765  
   766  	mode := istionet.ClientTLSSettings_DISABLE
   767  	value, ok := namespace.Labels["istio-injection"]
   768  	if ok && value == "enabled" {
   769  		mode = istionet.ClientTLSSettings_ISTIO_MUTUAL
   770  	}
   771  	destinationRule.Spec = istionet.DestinationRule{
   772  		Host: dest.Destination.Host,
   773  		TrafficPolicy: &istionet.TrafficPolicy{
   774  			Tls: &istionet.ClientTLSSettings{
   775  				Mode: mode,
   776  			},
   777  			LoadBalancer: &istionet.LoadBalancerSettings{
   778  				LbPolicy: &istionet.LoadBalancerSettings_ConsistentHash{
   779  					ConsistentHash: &istionet.LoadBalancerSettings_ConsistentHashLB{
   780  						HashKey: &istionet.LoadBalancerSettings_ConsistentHashLB_HttpCookie{
   781  							HttpCookie: &istionet.LoadBalancerSettings_ConsistentHashLB_HTTPCookie{
   782  								Name: rule.Destination.HTTPCookie.Name,
   783  								Path: rule.Destination.HTTPCookie.Path,
   784  								Ttl:  durationpb.New(rule.Destination.HTTPCookie.TTL * time.Second)},
   785  						},
   786  					},
   787  				},
   788  			},
   789  		},
   790  	}
   791  
   792  	return controllerutil.SetControllerReference(trait, destinationRule, r.Scheme)
   793  }
   794  
   795  // createOrUpdateAuthorizationPolicies creates or updates the AuthorizationPolicy associated with the
   796  // paths defined in the ingress rule.
   797  //
   798  // Ingress AuthorizationPolicies are used in conjunction with RequestPolicies (created by the user) to handle
   799  // requests with JWT headers. If any path uses an AuthorizationPolicy, we need to add a rule in that AuthorizationPolicy
   800  // for every path. This is needed otherwise a request to a path without an AuthorizationPolicy will get
   801  // rejected.  For example, if the /greet endpoint has an AuthorizationPolicy, the / endpoint will get rejected unless
   802  // we have a rule for path / as shown in the following example (the first rule):
   803  //
   804  //	 rules:
   805  //	- to:
   806  //	  - operation:
   807  //	      hosts:
   808  //	      - hello-helidon.hello-helidon.1.2.3.4.nip.io
   809  //	      paths:
   810  //	      - /
   811  //	- from:
   812  //	  - source:
   813  //	      requestPrincipals:     ====>  This is the indicator that an AuthorizationPolicy is needed
   814  //	      - '*'
   815  //	  to:
   816  //	  - operation:
   817  //	      hosts:
   818  //	      - hello-helidon.hello-helidon.1.2.3.4.nip.io
   819  //	      paths:
   820  //	      - /greet
   821  func (r *Reconciler) createOrUpdateAuthorizationPolicies(ctx context.Context, trait *vzapi.IngressTrait, rule vzapi.IngressRule, namePrefix string, hosts []string, status *reconcileresults.ReconcileResults, log vzlog.VerrazzanoLogger) {
   822  	// If any path needs an AuthorizationPolicy then add one for every path
   823  	var addAuthPolicy bool
   824  	for _, path := range rule.Paths {
   825  		if path.Policy != nil {
   826  			addAuthPolicy = true
   827  		}
   828  	}
   829  	for _, path := range rule.Paths {
   830  		if addAuthPolicy {
   831  			requireFrom := true
   832  
   833  			// Add a policy rule if one is missing
   834  			if path.Policy == nil {
   835  				path.Policy = &vzapi.AuthorizationPolicy{
   836  					Rules: []*vzapi.AuthorizationRule{{}},
   837  				}
   838  				// No from field required, this is just a path being added
   839  				requireFrom = false
   840  			}
   841  
   842  			pathSuffix := strings.Replace(path.Path, "/", "", -1)
   843  			policyName := namePrefix
   844  			if pathSuffix != "" {
   845  				policyName = fmt.Sprintf("%s-%s", policyName, pathSuffix)
   846  			}
   847  			// Create the AuthorizationPolicy resource.
   848  			// Note that this is created in istio-system. If we create this in the application namespace,
   849  			// which is also a valid option, requests to the protected endpoint without a JWT token bypass
   850  			// the JWT check because the default AuthorizationPolicy (e.g. hello-helidon) allows access from the
   851  			// Istio IngressGateway to the application.  This problem is solved by putting the AuthorizationPolicy
   852  			// in the istio-system namespace.
   853  			authzPolicy := &clisecurity.AuthorizationPolicy{
   854  				TypeMeta: metav1.TypeMeta{
   855  					Kind:       authzPolicyKind,
   856  					APIVersion: authzPolicyAPIVersion,
   857  				},
   858  				ObjectMeta: metav1.ObjectMeta{
   859  					Name:      policyName,
   860  					Namespace: constants.IstioSystemNamespace,
   861  					Labels:    map[string]string{constants.LabelIngressTraitNsn: getIngressTraitNsn(trait.Namespace, trait.Name)},
   862  				},
   863  			}
   864  			res, err := common.CreateOrUpdateProtobuf(ctx, r.Client, authzPolicy, func() error {
   865  				return r.mutateAuthorizationPolicy(authzPolicy, path.Policy, path.Path, hosts, requireFrom)
   866  			})
   867  
   868  			ref := vzapi.QualifiedResourceRelation{APIVersion: authzPolicyAPIVersion, Kind: authzPolicyKind, Name: namePrefix, Role: "authorizationpolicy"}
   869  			status.Relations = append(status.Relations, ref)
   870  			status.Results = append(status.Results, res)
   871  			status.Errors = append(status.Errors, err)
   872  
   873  			if err != nil {
   874  				log.Errorf("Failed to create or update authorization policy: %v", err)
   875  			}
   876  		}
   877  	}
   878  }
   879  
   880  // mutateAuthorizationPolicy changes the destination rule based upon a trait's configuration
   881  func (r *Reconciler) mutateAuthorizationPolicy(authzPolicy *clisecurity.AuthorizationPolicy, vzPolicy *vzapi.AuthorizationPolicy, path string, hosts []string, requireFrom bool) error {
   882  	policyRules := make([]*v1beta1.Rule, len(vzPolicy.Rules))
   883  	var err error
   884  	for i, authzRule := range vzPolicy.Rules {
   885  		policyRules[i], err = createAuthorizationPolicyRule(authzRule, path, hosts, requireFrom)
   886  		if err != nil {
   887  			return err
   888  		}
   889  	}
   890  
   891  	authzPolicy.Spec = v1beta1.AuthorizationPolicy{
   892  		Selector: &v1beta12.WorkloadSelector{
   893  			MatchLabels: map[string]string{"istio": "ingressgateway"},
   894  		},
   895  		Rules: policyRules,
   896  	}
   897  
   898  	return nil
   899  }
   900  
   901  // createAuthorizationPolicyRule uses the provided information to create an istio authorization policy rule
   902  func createAuthorizationPolicyRule(rule *vzapi.AuthorizationRule, path string, hosts []string, requireFrom bool) (*v1beta1.Rule, error) {
   903  	authzRule := v1beta1.Rule{}
   904  
   905  	if requireFrom && rule.From == nil {
   906  		return nil, fmt.Errorf("Authorization Policy requires 'From' clause")
   907  	}
   908  	if rule.From != nil {
   909  		authzRule.From = []*v1beta1.Rule_From{
   910  			{Source: &v1beta1.Source{
   911  				RequestPrincipals: rule.From.RequestPrincipals},
   912  			},
   913  		}
   914  	}
   915  
   916  	if len(path) > 0 {
   917  		authzRule.To = []*v1beta1.Rule_To{{
   918  			Operation: &v1beta1.Operation{
   919  				Hosts: hosts,
   920  				Paths: []string{path},
   921  			},
   922  		}}
   923  	}
   924  
   925  	if rule.When != nil {
   926  		conditions := []*v1beta1.Condition{}
   927  		for _, vzCondition := range rule.When {
   928  			condition := &v1beta1.Condition{
   929  				Key:    vzCondition.Key,
   930  				Values: vzCondition.Values,
   931  			}
   932  			conditions = append(conditions, condition)
   933  		}
   934  		authzRule.When = conditions
   935  	}
   936  
   937  	return &authzRule, nil
   938  }
   939  
   940  // setupWatches Sets up watches for the IngressTrait controller
   941  func (r *Reconciler) setupWatches() error {
   942  	// Set up a watch on the Console/Authproxy ingress to watch for changes in the Domain name;
   943  	err := r.Controller.Watch(
   944  		&source.Kind{Type: &k8net.Ingress{}},
   945  		// The handler for the Watch is a map function to map the detected change into requests to reconcile any
   946  		// existing ingress traits and invoke the IngressTrait Reconciler; this should cause us to update the
   947  		// VS and GW records for the associated apps.
   948  		handler.EnqueueRequestsFromMapFunc(func(a client.Object) []reconcile.Request {
   949  			return r.createIngressTraitReconcileRequests()
   950  		}),
   951  		predicate.Funcs{
   952  			UpdateFunc: func(updateEvent event.UpdateEvent) bool {
   953  				return r.isConsoleIngressUpdated(updateEvent)
   954  			},
   955  		},
   956  	)
   957  	if err != nil {
   958  		return err
   959  	}
   960  	// Set up a watch on the istio-ingressgateway service to watch for changes in the address;
   961  	err = r.Controller.Watch(
   962  		&source.Kind{Type: &corev1.Service{}},
   963  		// The handler for the Watch is a map function to map the detected change into requests to reconcile any
   964  		// existing ingress traits and invoke the IngressTrait Reconciler; this should cause us to update the
   965  		// VS and GW records for the associated apps.
   966  		handler.EnqueueRequestsFromMapFunc(func(a client.Object) []reconcile.Request {
   967  			return r.createIngressTraitReconcileRequests()
   968  		}),
   969  		predicate.Funcs{
   970  			UpdateFunc: func(updateEvent event.UpdateEvent) bool {
   971  				return r.isIstioIngressGatewayUpdated(updateEvent)
   972  			},
   973  		},
   974  	)
   975  	if err != nil {
   976  		return err
   977  	}
   978  	return nil
   979  }
   980  
   981  // isConsoleIngressUpdated Predicate func used by the Ingress watcher, returns true if the TLS settings have changed;
   982  // - this is largely to attempt to scope the change detection to Host name changes
   983  func (r *Reconciler) isConsoleIngressUpdated(updateEvent event.UpdateEvent) bool {
   984  	oldIngress := updateEvent.ObjectOld.(*k8net.Ingress)
   985  	// We only need to check the Authproxy/console Ingress
   986  	if oldIngress.Namespace != vzconst.VerrazzanoSystemNamespace || oldIngress.Name != constants.VzConsoleIngress {
   987  		return false
   988  	}
   989  	newIngress := updateEvent.ObjectNew.(*k8net.Ingress)
   990  	if !reflect.DeepEqual(oldIngress.Spec, newIngress.Spec) {
   991  		r.Log.Infof("Ingress %s/%s has changed", oldIngress.Namespace, oldIngress.Name)
   992  		return true
   993  	}
   994  	return false
   995  }
   996  
   997  // isIstioIngressGatewayUpdated Predicate func used by the watcher
   998  func (r *Reconciler) isIstioIngressGatewayUpdated(updateEvent event.UpdateEvent) bool {
   999  	oldSvc := updateEvent.ObjectOld.(*corev1.Service)
  1000  	if oldSvc.Namespace != vzconst.IstioSystemNamespace || oldSvc.Name != istioIngressGateway {
  1001  		return false
  1002  	}
  1003  	newSvc := updateEvent.ObjectNew.(*corev1.Service)
  1004  	if !reflect.DeepEqual(oldSvc.Spec, newSvc.Spec) {
  1005  		r.Log.Infof("Service %s/%s has changed", oldSvc.Namespace, oldSvc.Name)
  1006  		return true
  1007  	}
  1008  	return false
  1009  }
  1010  
  1011  // createIngressTraitReconcileRequests Used by the Console ingress watcher to map a detected change in the ingress
  1012  //
  1013  //	to requests to reconcile any existing application IngressTrait objects
  1014  func (r *Reconciler) createIngressTraitReconcileRequests() []reconcile.Request {
  1015  	requests := []reconcile.Request{}
  1016  
  1017  	ingressTraitList := vzapi.IngressTraitList{}
  1018  	if err := r.List(context.TODO(), &ingressTraitList, &client.ListOptions{}); err != nil {
  1019  		r.Log.Errorf("Failed to list ingress traits: %v", err)
  1020  		return requests
  1021  	}
  1022  
  1023  	for _, ingressTrait := range ingressTraitList.Items {
  1024  		requests = append(requests, reconcile.Request{
  1025  			NamespacedName: types.NamespacedName{
  1026  				Namespace: ingressTrait.Namespace,
  1027  				Name:      ingressTrait.Name,
  1028  			},
  1029  		})
  1030  	}
  1031  	r.Log.Infof("Requesting ingress trait reconcile: %v", requests)
  1032  	return requests
  1033  }
  1034  
  1035  // createDestinationFromRuleOrService creates a destination from either the rule or the service.
  1036  // If the rule contains destination information that is used.
  1037  // Otherwise, the appropriate service is selected and its information is used.
  1038  func createDestinationFromRuleOrService(rule vzapi.IngressRule, services []*corev1.Service) (*istionet.HTTPRouteDestination, error) {
  1039  	if len(rule.Destination.Host) > 0 {
  1040  		dest := &istionet.HTTPRouteDestination{Destination: &istionet.Destination{Host: rule.Destination.Host}}
  1041  		if rule.Destination.Port != 0 {
  1042  			dest.Destination.Port = &istionet.PortSelector{Number: rule.Destination.Port}
  1043  		}
  1044  		return dest, nil
  1045  	}
  1046  	if rule.Destination.Port != 0 {
  1047  		return createDestinationMatchRulePort(services, rule.Destination.Port)
  1048  	}
  1049  	return createDestinationFromService(services)
  1050  }
  1051  
  1052  // getPathsFromRule gets the paths from a trait.
  1053  // If the trait has no paths a default path is returned.
  1054  func getPathsFromRule(rule vzapi.IngressRule) []vzapi.IngressPath {
  1055  	paths := rule.Paths
  1056  	// If there are no paths create a default.
  1057  	if len(paths) == 0 {
  1058  		paths = []vzapi.IngressPath{{Path: "/", PathType: "prefix"}}
  1059  	}
  1060  	return paths
  1061  }
  1062  
  1063  // createDestinationFromService selects a Service and creates a virtual service destination for the selected service.
  1064  // If the selected service does not have a port, it is not included in the destination. If the selected service
  1065  // declares port(s), it selects the appropriate one and add it to the destination.
  1066  func createDestinationFromService(services []*corev1.Service) (*istionet.HTTPRouteDestination, error) {
  1067  	selectedService, err := selectServiceForDestination(services)
  1068  	if err != nil {
  1069  		return nil, err
  1070  	}
  1071  	dest := istionet.HTTPRouteDestination{
  1072  		Destination: &istionet.Destination{Host: selectedService.Name}}
  1073  	// If the selected service declares port(s), select the appropriate port and add it to the destination.
  1074  	if len(selectedService.Spec.Ports) > 0 {
  1075  		selectedPort, err := selectPortForDestination(selectedService)
  1076  		if err != nil {
  1077  			return nil, err
  1078  		}
  1079  		dest.Destination.Port = &istionet.PortSelector{Number: uint32(selectedPort.Port)}
  1080  	}
  1081  	return &dest, nil
  1082  }
  1083  
  1084  // selectServiceForDestination selects a Service to be used for virtual service destination.
  1085  // The service is selected based on the following logic:
  1086  //   - If there is one service, return that service.
  1087  //   - If there are multiple services and one service with cluster-IP, select that service.
  1088  //   - If there are multiple services, select the service with HTTP or WebLogic port. If there is no such service or
  1089  //     multiple such services, return an error. A port is evaluated as an HTTP port if the service has a port named
  1090  //     with the prefix "http" and as a WebLogic port if the port name is from the known WebLogic non-http prefixed
  1091  //     port names used by the WebLogic operator.
  1092  func selectServiceForDestination(services []*corev1.Service) (*corev1.Service, error) {
  1093  	var clusterIPServices []*corev1.Service
  1094  	var allowedServices []*corev1.Service
  1095  	var allowedClusterIPServices []*corev1.Service
  1096  
  1097  	// If there is only one service, return that service
  1098  	if len(services) == 1 {
  1099  		return services[0], nil
  1100  	}
  1101  	// Multiple services case
  1102  	for _, service := range services {
  1103  		if service.Spec.ClusterIP != "" && service.Spec.ClusterIP != clusterIPNone {
  1104  			clusterIPServices = append(clusterIPServices, service)
  1105  		}
  1106  		allowedPorts := append(getHTTPPorts(service), getWebLogicPorts(service)...)
  1107  		if len(allowedPorts) > 0 {
  1108  			allowedServices = append(allowedServices, service)
  1109  		}
  1110  		if service.Spec.ClusterIP != "" && service.Spec.ClusterIP != clusterIPNone && len(allowedPorts) > 0 {
  1111  			allowedClusterIPServices = append(allowedClusterIPServices, service)
  1112  		}
  1113  	}
  1114  	// If there is no service with cluster-IP or no service with allowed port, return an error.
  1115  	if len(clusterIPServices) == 0 && len(allowedServices) == 0 {
  1116  		return nil, fmt.Errorf("unable to select default service for destination")
  1117  	} else if len(clusterIPServices) == 1 {
  1118  		// If there is only one service with cluster IP, return that service.
  1119  		return clusterIPServices[0], nil
  1120  	} else if len(allowedClusterIPServices) == 1 {
  1121  		// If there is only one http/WebLogic service with cluster IP, return that service.
  1122  		return allowedClusterIPServices[0], nil
  1123  	} else if len(allowedServices) == 1 {
  1124  		// If there is only one http/WebLogic service, return that service.
  1125  		return allowedServices[0], nil
  1126  	}
  1127  	// In all other cases, return error.
  1128  	return nil, fmt.Errorf("unable to select the service for destination. The service port " +
  1129  		"should be named with prefix \"http\" if there are multiple services OR the IngressTrait must specify the port")
  1130  }
  1131  
  1132  // selectPortForDestination selects a Service port to be used for virtual service destination port.
  1133  // The port is selected based on the following logic:
  1134  //   - If there is one port, return that port.
  1135  //   - If there are multiple ports, select the http/WebLogic port.
  1136  //   - If there are multiple ports and more than one http/WebLogic port, return an error.
  1137  //   - If there are multiple ports and none of then are http/WebLogic ports, return an error.
  1138  func selectPortForDestination(service *corev1.Service) (corev1.ServicePort, error) {
  1139  	servicePorts := service.Spec.Ports
  1140  	// If there is only one port, return that port
  1141  	if len(servicePorts) == 1 {
  1142  		return servicePorts[0], nil
  1143  	}
  1144  	allowedPorts := append(getHTTPPorts(service), getWebLogicPorts(service)...)
  1145  	// If there are multiple ports and one http/WebLogic port, return that port
  1146  	if len(servicePorts) > 1 && len(allowedPorts) == 1 {
  1147  		return allowedPorts[0], nil
  1148  	}
  1149  	// If there are multiple ports and none of them are http/WebLogic ports, return an error
  1150  	if len(servicePorts) > 1 && len(allowedPorts) < 1 {
  1151  		return corev1.ServicePort{}, fmt.Errorf("unable to select the service port for destination. The service port " +
  1152  			"should be named with prefix \"http\" if there are multiple ports OR the IngressTrait must specify the port")
  1153  	}
  1154  	// If there are multiple http/WebLogic ports, return an error
  1155  	if len(allowedPorts) > 1 {
  1156  		return corev1.ServicePort{}, fmt.Errorf("unable to select the service port for destination. Only one service " +
  1157  			"port should be named with prefix \"http\" OR the IngressTrait must specify the port")
  1158  	}
  1159  	return corev1.ServicePort{}, fmt.Errorf("unable to select default port for destination")
  1160  }
  1161  
  1162  // createDestinationMatchRulePort fetches a Service matching the specified rule port and creates virtual service
  1163  // destination.
  1164  func createDestinationMatchRulePort(services []*corev1.Service, rulePort uint32) (*istionet.HTTPRouteDestination, error) {
  1165  	var eligibleServices []*corev1.Service
  1166  	for _, service := range services {
  1167  		for _, servicePort := range service.Spec.Ports {
  1168  			if servicePort.Port == int32(rulePort) {
  1169  				eligibleServices = append(eligibleServices, service)
  1170  			}
  1171  		}
  1172  	}
  1173  	selectedService, err := selectServiceForDestination(eligibleServices)
  1174  	if err != nil {
  1175  		return nil, err
  1176  	}
  1177  	if selectedService != nil {
  1178  		dest := istionet.HTTPRouteDestination{
  1179  			Destination: &istionet.Destination{Host: selectedService.Name}}
  1180  		// Set the port to rule destination port
  1181  		dest.Destination.Port = &istionet.PortSelector{Number: rulePort}
  1182  		return &dest, nil
  1183  	}
  1184  	return nil, fmt.Errorf("unable to select service for specified destination port %d", rulePort)
  1185  }
  1186  
  1187  // getHTTPPorts returns all the service ports having the prefix "http" in their names.
  1188  func getHTTPPorts(service *corev1.Service) []corev1.ServicePort {
  1189  	var httpPorts []corev1.ServicePort
  1190  	for _, servicePort := range service.Spec.Ports {
  1191  		// Check if service port name has the http prefix
  1192  		if strings.HasPrefix(servicePort.Name, httpServiceNamePrefix) {
  1193  			httpPorts = append(httpPorts, servicePort)
  1194  		}
  1195  	}
  1196  	return httpPorts
  1197  }
  1198  
  1199  // getWebLogicPorts returns WebLogic ports if any present for the service. A port is evaluated as a WebLogic port if
  1200  // the port name is from the known WebLogic non-http prefixed port names used by the WebLogic operator.
  1201  func getWebLogicPorts(service *corev1.Service) []corev1.ServicePort {
  1202  	var webLogicPorts []corev1.ServicePort
  1203  	selectorMap := service.Spec.Selector
  1204  	value, ok := selectorMap[weblogicOperatorSelector]
  1205  	if !ok || value == "false" {
  1206  		return webLogicPorts
  1207  	}
  1208  	for _, servicePort := range service.Spec.Ports {
  1209  		// Check if service port name is one of the predefined WebLogic port names
  1210  		for _, webLogicPortName := range weblogicPortNames {
  1211  			if servicePort.Name == webLogicPortName {
  1212  				webLogicPorts = append(webLogicPorts, servicePort)
  1213  			}
  1214  		}
  1215  	}
  1216  	return webLogicPorts
  1217  }
  1218  
  1219  // createVirtualServiceMatchURIFromIngressTraitPath create the virtual service match uri map from an ingress trait path
  1220  // This is primarily used to setup defaults when either path or type are not present in the ingress path.
  1221  // If the provided ingress path doesn't contain a path it is default to /
  1222  // If the provided ingress path doesn't contain a type it is defaulted to prefix if path is / and exact otherwise.
  1223  func createVirtualServiceMatchURIFromIngressTraitPath(path vzapi.IngressPath) *istionet.StringMatch {
  1224  	// Default path to /
  1225  	p := strings.TrimSpace(path.Path)
  1226  	if p == "" {
  1227  		p = "/"
  1228  	}
  1229  
  1230  	// If path is / default type to prefix
  1231  	// If path is not / default to exact
  1232  	t := strings.ToLower(strings.TrimSpace(path.PathType))
  1233  	if t == "" {
  1234  		if p == "/" {
  1235  			t = "prefix"
  1236  		} else {
  1237  			t = "exact"
  1238  		}
  1239  	}
  1240  
  1241  	switch t {
  1242  	case "regex":
  1243  		return &istionet.StringMatch{MatchType: &istionet.StringMatch_Regex{Regex: p}}
  1244  	case "prefix":
  1245  		return &istionet.StringMatch{MatchType: &istionet.StringMatch_Prefix{Prefix: p}}
  1246  	default:
  1247  		return &istionet.StringMatch{MatchType: &istionet.StringMatch_Exact{Exact: p}}
  1248  	}
  1249  }
  1250  
  1251  // createHostsFromIngressTraitRule creates an array of hosts from an ingress rule, appending to an optionally provided input list
  1252  // - It filters out wildcard hosts or hosts that are empty.
  1253  // - If there are no valid hosts provided for the rule, then a DNS host name is automatically generated and used.
  1254  // - A hostname can only appear once
  1255  func createHostsFromIngressTraitRule(cli client.Reader, rule vzapi.IngressRule, trait *vzapi.IngressTrait, toList ...string) ([]string, error) {
  1256  	validHosts := toList
  1257  	useDefaultHost := true
  1258  	for _, h := range rule.Hosts {
  1259  		h = strings.TrimSpace(h)
  1260  		if _, hostAlreadyPresent := findHost(validHosts, h); hostAlreadyPresent {
  1261  			// Avoid duplicates
  1262  			useDefaultHost = false
  1263  			continue
  1264  		}
  1265  		// Ignore empty or wildcard hostname
  1266  		if len(h) == 0 || strings.Contains(h, "*") {
  1267  			continue
  1268  		}
  1269  		h = strings.ToLower(strings.TrimSpace(h))
  1270  		validHosts = append(validHosts, h)
  1271  		useDefaultHost = false
  1272  	}
  1273  	// Add done if a host was added to the host list
  1274  	if !useDefaultHost {
  1275  		return validHosts, nil
  1276  	}
  1277  
  1278  	// Generate a default hostname
  1279  	hostName, err := buildAppFullyQualifiedHostName(cli, trait)
  1280  	if err != nil {
  1281  		return nil, err
  1282  	}
  1283  	// Only add the generated hostname if it doesn't exist in hte list
  1284  	if _, hostAlreadyPresent := findHost(validHosts, hostName); !hostAlreadyPresent {
  1285  		validHosts = append(validHosts, hostName)
  1286  	}
  1287  	return validHosts, nil
  1288  }
  1289  
  1290  // fetchServicesFromTrait traverses from an ingress trait resource to the related service resources and returns it.
  1291  // This is done by first finding the workload related to the trait.
  1292  // Then the child resources of the workload are founds.
  1293  // Finally those child resources are scanned to find Service resources which are returned.
  1294  func (r *Reconciler) fetchServicesFromTrait(ctx context.Context, trait *vzapi.IngressTrait, log vzlog.VerrazzanoLogger) ([]*corev1.Service, error) {
  1295  	var err error
  1296  
  1297  	// Fetch workload resource
  1298  	var workload *unstructured.Unstructured
  1299  	if workload, err = vznav.FetchWorkloadFromTrait(ctx, r.Client, log, trait); err != nil || workload == nil {
  1300  		return nil, err
  1301  	}
  1302  
  1303  	// Fetch workload child resources
  1304  	var children []*unstructured.Unstructured
  1305  	if children, err = r.fetchWorkloadChildren(ctx, workload, log); err != nil {
  1306  		return nil, err
  1307  	}
  1308  
  1309  	// Find the services from within the list of unstructured child resources
  1310  	var services []*corev1.Service
  1311  	services, err = r.extractServicesFromUnstructuredChildren(children, log)
  1312  	if err != nil {
  1313  		return nil, err
  1314  	}
  1315  
  1316  	return services, nil
  1317  }
  1318  
  1319  // extractServicesFromUnstructuredChildren finds and returns Services in an array of unstructured child service.
  1320  // The children array is scanned looking for Service's APIVersion and Kind,
  1321  // If found the unstructured data is converted to a Service object and returned.
  1322  // children - An array of unstructured children
  1323  func (r *Reconciler) extractServicesFromUnstructuredChildren(children []*unstructured.Unstructured, log vzlog.VerrazzanoLogger) ([]*corev1.Service, error) {
  1324  	var services []*corev1.Service
  1325  
  1326  	for _, child := range children {
  1327  		if child.GetAPIVersion() == serviceAPIVersion && child.GetKind() == serviceKind {
  1328  			var service corev1.Service
  1329  			err := runtime.DefaultUnstructuredConverter.FromUnstructured(child.UnstructuredContent(), &service)
  1330  			if err != nil {
  1331  				// maybe we should continue here and hope that another child can be converted?
  1332  				return nil, err
  1333  			}
  1334  			services = append(services, &service)
  1335  		}
  1336  	}
  1337  
  1338  	if len(services) > 0 {
  1339  		return services, nil
  1340  	}
  1341  
  1342  	// Log that the child service was not found and return a nil service
  1343  	log.Debug("No child service found")
  1344  	return services, nil
  1345  }
  1346  
  1347  // convertAPIVersionAndKindToNamespacedName converts APIVersion and Kind of CR to a CRD namespaced name.
  1348  // For example CR APIVersion.Kind core.oam.dev/v1alpha2.ContainerizedWorkload would be converted
  1349  // to containerizedworkloads.core.oam.dev in the default (i.e. "") namespace.
  1350  // apiVersion - The CR APIVersion
  1351  // kind - The CR Kind
  1352  func convertAPIVersionAndKindToNamespacedName(apiVersion string, kind string) types.NamespacedName {
  1353  	grp, ver := controllers.ConvertAPIVersionToGroupAndVersion(apiVersion)
  1354  	res := pluralize.NewClient().Plural(strings.ToLower(kind))
  1355  	grpVerRes := metav1.GroupVersionResource{
  1356  		Group:    grp,
  1357  		Version:  ver,
  1358  		Resource: res,
  1359  	}
  1360  	name := grpVerRes.Resource + "." + grpVerRes.Group
  1361  	return types.NamespacedName{Namespace: "", Name: name}
  1362  }
  1363  
  1364  // buildAppFullyQualifiedHostName generates a DNS host name for the application using the following structure:
  1365  // <app>.<namespace>.<dns-subdomain>  where
  1366  //
  1367  //	app is the OAM application name
  1368  //	namespace is the namespace of the OAM application
  1369  //	dns-subdomain is The DNS subdomain name
  1370  //
  1371  // For example: sales.cars.example.com
  1372  func buildAppFullyQualifiedHostName(cli client.Reader, trait *vzapi.IngressTrait) (string, error) {
  1373  	appName, ok := trait.Labels[oam.LabelAppName]
  1374  	if !ok {
  1375  		return "", errors.New("OAM app name label missing from metadata, unable to add ingress trait")
  1376  	}
  1377  	domainName, err := buildNamespacedDomainName(cli, trait)
  1378  	if err != nil {
  1379  		return "", err
  1380  	}
  1381  	return fmt.Sprintf("%s.%s", appName, domainName), nil
  1382  }
  1383  
  1384  // buildNamespacedDomainName generates a domain name for the application using the following structure:
  1385  // <namespace>.<dns-subdomain>  where
  1386  //
  1387  //	namespace is the namespace of the OAM application
  1388  //	dns-subdomain is The DNS subdomain name
  1389  //
  1390  // For example: cars.example.com
  1391  func buildNamespacedDomainName(cli client.Reader, trait *vzapi.IngressTrait) (string, error) {
  1392  	const externalDNSKey = "external-dns.alpha.kubernetes.io/target"
  1393  	const wildcardDomainKey = "verrazzano.io/dns.wildcard.domain"
  1394  
  1395  	// Extract the domain name from the Verrazzano ingress
  1396  	ingress := k8net.Ingress{}
  1397  	err := cli.Get(context.TODO(), types.NamespacedName{Name: constants.VzConsoleIngress, Namespace: constants.VerrazzanoSystemNamespace}, &ingress)
  1398  	if err != nil {
  1399  		return "", err
  1400  	}
  1401  	externalDNSAnno, ok := ingress.Annotations[externalDNSKey]
  1402  	if !ok || len(externalDNSAnno) == 0 {
  1403  		return "", fmt.Errorf("Annotation %s missing from Verrazzano ingress, unable to generate DNS name", externalDNSKey)
  1404  	}
  1405  
  1406  	domain := externalDNSAnno[len(constants.VzConsoleIngress)+1:]
  1407  
  1408  	// Get the DNS wildcard domain from the annotation if it exist.  This annotation is only available
  1409  	// when the install is using DNS type wildcard (nip.io, sslip.io, etc.)
  1410  	suffix := ""
  1411  	wildcardDomainAnno, ok := ingress.Annotations[wildcardDomainKey]
  1412  	if ok {
  1413  		suffix = wildcardDomainAnno
  1414  	}
  1415  
  1416  	// Build the domain name using Istio info
  1417  	if len(suffix) != 0 {
  1418  		domain, err = buildDomainNameForWildcard(cli, trait, suffix)
  1419  		if err != nil {
  1420  			return "", err
  1421  		}
  1422  	}
  1423  	return fmt.Sprintf("%s.%s", trait.Namespace, domain), nil
  1424  }
  1425  
  1426  // buildDomainNameForWildcard generates a domain name in the format of "<IP>.<wildcard-domain>"
  1427  // Get the IP from Istio resources
  1428  func buildDomainNameForWildcard(cli client.Reader, trait *vzapi.IngressTrait, suffix string) (string, error) {
  1429  	istio := corev1.Service{}
  1430  	err := cli.Get(context.TODO(), types.NamespacedName{Name: istioIngressGateway, Namespace: constants.IstioSystemNamespace}, &istio)
  1431  	if err != nil {
  1432  		return "", err
  1433  	}
  1434  	var IP string
  1435  	if istio.Spec.Type == corev1.ServiceTypeLoadBalancer || istio.Spec.Type == corev1.ServiceTypeNodePort {
  1436  		if len(istio.Spec.ExternalIPs) > 0 {
  1437  			IP = istio.Spec.ExternalIPs[0]
  1438  		} else if len(istio.Status.LoadBalancer.Ingress) > 0 {
  1439  			IP = istio.Status.LoadBalancer.Ingress[0].IP
  1440  		} else {
  1441  			return "", fmt.Errorf("%s is missing loadbalancer IP", istioIngressGateway)
  1442  		}
  1443  	} else {
  1444  		return "", fmt.Errorf("unsupported service type %s for istio_ingress", string(istio.Spec.Type))
  1445  	}
  1446  	domain := IP + "." + suffix
  1447  	return domain, nil
  1448  }
  1449  
  1450  func getIngressTraitNsn(namespace string, name string) string {
  1451  	return fmt.Sprintf("%s-%s", namespace, name)
  1452  }