github.com/verrazzano/verrazzano@v1.7.0/pkg/ingresstrait/ingresstrait.go (about)

     1  // Copyright (c) 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  	"fmt"
     9  	vzapi "github.com/verrazzano/verrazzano/application-operator/apis/oam/v1alpha1"
    10  	corev1 "k8s.io/api/core/v1"
    11  	k8net "k8s.io/api/networking/v1"
    12  	"k8s.io/apimachinery/pkg/types"
    13  	"sigs.k8s.io/controller-runtime/pkg/client"
    14  	"strings"
    15  )
    16  
    17  func CoallateAllHostsForTrait(cli client.Client, trait *vzapi.IngressTrait, appName string, appNamespace string) ([]string, error) {
    18  	allHosts := []string{}
    19  	var err error
    20  	for _, rule := range trait.Spec.Rules {
    21  		if allHosts, err = CreateHostsFromIngressTraitRule(cli, rule, trait, appName, appNamespace, allHosts...); err != nil {
    22  
    23  			print(err)
    24  			return nil, err
    25  		}
    26  	}
    27  	return allHosts, nil
    28  }
    29  
    30  func CreateHostsFromIngressTraitRule(cli client.Client, rule vzapi.IngressRule, trait *vzapi.IngressTrait, appName string, appNamespace string, toList ...string) ([]string, error) {
    31  
    32  	validHosts := toList
    33  	useDefaultHost := true
    34  	for _, h := range rule.Hosts {
    35  		h = strings.TrimSpace(h)
    36  		if _, hostAlreadyPresent := findHost(validHosts, h); hostAlreadyPresent {
    37  			// Avoid duplicates
    38  			useDefaultHost = false
    39  			continue
    40  		}
    41  		// Ignore empty or wildcard hostname
    42  		if len(h) == 0 || strings.Contains(h, "*") {
    43  			continue
    44  		}
    45  		h = strings.ToLower(strings.TrimSpace(h))
    46  		validHosts = append(validHosts, h)
    47  		useDefaultHost = false
    48  	}
    49  	// Add done if a host was added to the host list
    50  	if !useDefaultHost {
    51  		return validHosts, nil
    52  	}
    53  
    54  	// Generate a default hostname
    55  
    56  	hostName, err := buildAppFullyQualifiedHostName(cli, trait, appName, appNamespace)
    57  
    58  	if err != nil {
    59  		return nil, err
    60  	}
    61  	// Only add the generated hostname if it doesn't exist in hte list
    62  	if _, hostAlreadyPresent := findHost(validHosts, hostName); !hostAlreadyPresent {
    63  		validHosts = append(validHosts, hostName)
    64  	}
    65  	return validHosts, nil
    66  }
    67  
    68  // buildAppFullyQualifiedHostName generates a DNS host name for the application using the following structure:
    69  // <app>.<namespace>.<dns-subdomain>  where
    70  //
    71  //	app is the OAM application name
    72  //	namespace is the namespace of the OAM application
    73  //	dns-subdomain is The DNS subdomain name
    74  
    75  func buildAppFullyQualifiedHostName(cli client.Client, trait *vzapi.IngressTrait, appName string, appNamespace string) (string, error) {
    76  
    77  	domainName, err := buildNamespacedDomainName(cli, trait, appNamespace)
    78  
    79  	if err != nil {
    80  		return "", err
    81  	}
    82  	return fmt.Sprintf("%s.%s", appName, domainName), nil
    83  
    84  }
    85  
    86  // buildNamespacedDomainName generates a domain name for the application using the following structure:
    87  // <namespace>.<dns-subdomain>  where
    88  //
    89  //	namespace is the namespace of the OAM application
    90  //	dns-subdomain is The DNS subdomain name
    91  
    92  func buildNamespacedDomainName(cli client.Client, trait *vzapi.IngressTrait, appNamespace string) (string, error) {
    93  
    94  	const externalDNSKey = "external-dns.alpha.kubernetes.io/target"
    95  	const wildcardDomainKey = "verrazzano.io/dns.wildcard.domain"
    96  
    97  	// Extract the domain name from the Verrazzano ingress
    98  	ingress := k8net.Ingress{}
    99  	err := cli.Get(context.TODO(), types.NamespacedName{Name: "verrazzano-ingress", Namespace: "verrazzano-system"}, &ingress)
   100  	if err != nil {
   101  		return "", err
   102  	}
   103  	externalDNSAnno, ok := ingress.Annotations[externalDNSKey]
   104  	if !ok || len(externalDNSAnno) == 0 {
   105  		return "", fmt.Errorf("Annotation %s missing from Verrazzano ingress, unable to generate DNS name", externalDNSKey)
   106  	}
   107  
   108  	domainArray := strings.SplitN(externalDNSAnno, ".", 2)
   109  	domain := domainArray[0]
   110  	if len(domainArray) >= 2 {
   111  		domain = domainArray[1]
   112  	}
   113  
   114  	// Get the DNS wildcard domain from the annotation if it exist.  This annotation is only available
   115  	// when the install is using DNS type wildcard (nip.io, sslip.io, etc.)
   116  	suffix := ""
   117  	wildcardDomainAnno, ok := ingress.Annotations[wildcardDomainKey]
   118  	if ok {
   119  		suffix = wildcardDomainAnno
   120  	}
   121  
   122  	// Build the domain name using Istio info
   123  	if len(suffix) != 0 {
   124  		domain, err = buildDomainNameForWildcard(cli, suffix)
   125  		if err != nil {
   126  			return "", err
   127  		}
   128  	}
   129  
   130  	return fmt.Sprintf("%s.%s", appNamespace, domain), nil
   131  
   132  }
   133  
   134  // findHost searches for a host in the provided list. If found it will
   135  // return it's key, otherwise it will return -1 and a bool of false.
   136  func findHost(hosts []string, newHost string) (int, bool) {
   137  	for i, host := range hosts {
   138  		if strings.EqualFold(host, newHost) {
   139  			return i, true
   140  		}
   141  	}
   142  	return -1, false
   143  }
   144  
   145  // buildDomainNameForWildcard generates a domain name in the format of "<IP>.<wildcard-domain>"
   146  // Get the IP from Istio resources
   147  func buildDomainNameForWildcard(cli client.Reader, suffix string) (string, error) {
   148  	istioIngressGateway := "istio-ingressgateway"
   149  	istioSystemNamespace := "istio-system"
   150  	istio := corev1.Service{}
   151  	err := cli.Get(context.TODO(), types.NamespacedName{Name: istioIngressGateway, Namespace: istioSystemNamespace}, &istio)
   152  	if err != nil {
   153  		return "", err
   154  	}
   155  	var IP string
   156  	if istio.Spec.Type == corev1.ServiceTypeLoadBalancer || istio.Spec.Type == corev1.ServiceTypeNodePort {
   157  		if len(istio.Spec.ExternalIPs) > 0 {
   158  			IP = istio.Spec.ExternalIPs[0]
   159  		} else if len(istio.Status.LoadBalancer.Ingress) > 0 {
   160  			IP = istio.Status.LoadBalancer.Ingress[0].IP
   161  		} else {
   162  			return "", fmt.Errorf("%s is missing loadbalancer IP", istioIngressGateway)
   163  		}
   164  	} else {
   165  		return "", fmt.Errorf("unsupported service type %s for istio_ingress", string(istio.Spec.Type))
   166  	}
   167  	domain := IP + "." + suffix
   168  	return domain, nil
   169  }