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 }