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

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package ingestion
     5  
     6  import (
     7  	"sort"
     8  	"time"
     9  
    10  	corev1 "k8s.io/api/core/v1"
    11  	networkingv1 "k8s.io/api/networking/v1"
    12  
    13  	"github.com/cilium/cilium/operator/pkg/ingress/annotations"
    14  	"github.com/cilium/cilium/operator/pkg/model"
    15  	"github.com/cilium/cilium/pkg/logging/logfields"
    16  )
    17  
    18  // Ingress translates an Ingress resource to a HTTPListener.
    19  // This function does not check IngressClass (via field or annotation).
    20  // It's expected that only relevant Ingresses will have this function called on them.
    21  func Ingress(ing networkingv1.Ingress, defaultSecretNamespace, defaultSecretName string, enforcedHTTPS bool, insecureListenerPort, secureListenerPort uint32, defaultRequestTimeout time.Duration) []model.HTTPListener {
    22  	// First, we make a map of HTTPListeners, with the hostname
    23  	// as the key, so that we can make sure we match up any
    24  	// TLS config with rules that match it.
    25  	// This is to approximate a set, keyed by hostname, so we can
    26  	// coalesce the config from a single Ingress.
    27  	// Coalescing the config from multiple Ingress resources is left for
    28  	// the transform component that takes a model and outputs CiliumEnvoyConfig
    29  	// or other resources.
    30  	insecureListenerMap := make(map[string]model.HTTPListener)
    31  
    32  	sourceResource := model.FullyQualifiedResource{
    33  		Name:      ing.Name,
    34  		Namespace: ing.Namespace,
    35  		Group:     "",
    36  		Version:   "v1",
    37  		Kind:      "Ingress",
    38  		UID:       string(ing.UID),
    39  	}
    40  
    41  	// Setup timeout for use in all routes
    42  	timeout := model.Timeout{}
    43  	if defaultRequestTimeout != 0 {
    44  		timeout.Request = model.AddressOf(defaultRequestTimeout)
    45  	}
    46  	if v, err := annotations.GetAnnotationRequestTimeout(&ing); err != nil {
    47  		// If the annotation is invalid, we log a warning and use the default value
    48  		log.WithField(logfields.Ingress, ing.Namespace+"/"+ing.Name).
    49  			Warn("Invalid request timeout annotation, using default value")
    50  	} else if v != nil {
    51  		timeout.Request = model.AddressOf(*v)
    52  	}
    53  
    54  	if ing.Spec.DefaultBackend != nil {
    55  		// There's a default backend set up
    56  
    57  		// get the details for the default backend
    58  
    59  		backend := model.Backend{}
    60  		backend.Name = ing.Spec.DefaultBackend.Service.Name
    61  		backend.Namespace = ing.Namespace
    62  
    63  		backend.Port = &model.BackendPort{}
    64  
    65  		if ing.Spec.DefaultBackend.Service.Port.Name != "" {
    66  			backend.Port.Name = ing.Spec.DefaultBackend.Service.Port.Name
    67  		}
    68  
    69  		if ing.Spec.DefaultBackend.Service.Port.Number != 0 {
    70  			backend.Port.Port = uint32(ing.Spec.DefaultBackend.Service.Port.Number)
    71  		}
    72  
    73  		l := model.HTTPListener{
    74  			Hostname: "*",
    75  			Routes: []model.HTTPRoute{
    76  				{
    77  					Backends: []model.Backend{
    78  						backend,
    79  					},
    80  					Timeout: timeout,
    81  				},
    82  			},
    83  			Port:    insecureListenerPort,
    84  			Service: getService(ing),
    85  		}
    86  
    87  		l.Sources = model.AddSource(l.Sources, sourceResource)
    88  
    89  		insecureListenerMap["*"] = l
    90  	}
    91  
    92  	// Now, we range across the rules, adding them in as listeners.
    93  	for _, rule := range ing.Spec.Rules {
    94  
    95  		host := "*"
    96  
    97  		if rule.Host != "" {
    98  			host = rule.Host
    99  		}
   100  
   101  		l, ok := insecureListenerMap[host]
   102  		l.Port = insecureListenerPort
   103  		l.Sources = model.AddSource(l.Sources, sourceResource)
   104  		if !ok {
   105  			l.Name = "ing-" + ing.Name + "-" + ing.Namespace + "-" + host
   106  		}
   107  
   108  		l.Hostname = host
   109  		if rule.HTTP == nil {
   110  			log.WithField(logfields.Ingress, ing.Namespace+"/"+ing.Name).
   111  				Warn("Invalid Ingress rule without spec.rules.HTTP defined, skipping rule")
   112  			continue
   113  		}
   114  
   115  		for _, path := range rule.HTTP.Paths {
   116  
   117  			route := model.HTTPRoute{
   118  				Timeout: timeout,
   119  			}
   120  
   121  			switch *path.PathType {
   122  			case networkingv1.PathTypeExact:
   123  				route.PathMatch.Exact = path.Path
   124  			case networkingv1.PathTypePrefix:
   125  				route.PathMatch.Prefix = path.Path
   126  			case networkingv1.PathTypeImplementationSpecific:
   127  				route.PathMatch.Regex = path.Path
   128  			}
   129  
   130  			backend := model.Backend{
   131  				Name:      path.Backend.Service.Name,
   132  				Namespace: ing.Namespace,
   133  			}
   134  			if path.Backend.Service != nil {
   135  				backend.Port = &model.BackendPort{}
   136  				if path.Backend.Service.Port.Name != "" {
   137  					backend.Port.Name = path.Backend.Service.Port.Name
   138  				}
   139  				if path.Backend.Service.Port.Number != 0 {
   140  					backend.Port.Port = uint32(path.Backend.Service.Port.Number)
   141  				}
   142  			}
   143  			route.Backends = append(route.Backends, backend)
   144  			l.Routes = append(l.Routes, route)
   145  			l.Service = getService(ing)
   146  		}
   147  
   148  		insecureListenerMap[host] = l
   149  	}
   150  
   151  	secureListenerMap := make(map[string]model.HTTPListener)
   152  
   153  	// Before we check for TLS config, we need to see if the force-https annotation
   154  	// is set.
   155  	forceHTTPsannotation := annotations.GetAnnotationForceHTTPSEnabled(&ing)
   156  	forceHTTPs := false
   157  
   158  	// We only care about enforcedHTTPS if the annotation is unset
   159  	if (forceHTTPsannotation == nil && enforcedHTTPS) || (forceHTTPsannotation != nil && *forceHTTPsannotation) {
   160  		forceHTTPs = true
   161  	}
   162  
   163  	// First, we check for TLS config, and set them up with Listeners to return.
   164  	for _, tlsConfig := range ing.Spec.TLS {
   165  		for _, host := range tlsConfig.Hosts {
   166  
   167  			l, ok := secureListenerMap[host]
   168  			if !ok {
   169  				l, ok = insecureListenerMap[host]
   170  				if !ok {
   171  					l, ok = insecureListenerMap["*"]
   172  					if !ok {
   173  						continue
   174  					}
   175  				}
   176  			}
   177  
   178  			if tlsConfig.SecretName != "" {
   179  				l.TLS = []model.TLSSecret{
   180  					{
   181  						Name: tlsConfig.SecretName,
   182  						// Secret has to be in the same namespace as the Ingress.
   183  						Namespace: ing.Namespace,
   184  					},
   185  				}
   186  			} else if defaultSecretNamespace != "" && defaultSecretName != "" {
   187  				l.TLS = []model.TLSSecret{
   188  					{
   189  						Name:      defaultSecretName,
   190  						Namespace: defaultSecretNamespace,
   191  					},
   192  				}
   193  			}
   194  
   195  			l.Port = secureListenerPort
   196  			l.Hostname = host
   197  			l.Service = getService(ing)
   198  			l.ForceHTTPtoHTTPSRedirect = forceHTTPs
   199  			secureListenerMap[host] = l
   200  
   201  			defaultListener, ok := insecureListenerMap["*"]
   202  			if ok {
   203  				// A default listener already exists, each Host in TLSConfig.Hosts
   204  				// needs to have a Listener configured that's a copy of it.
   205  				if tlsConfig.SecretName != "" {
   206  					defaultListener.TLS = []model.TLSSecret{
   207  						{
   208  							Name: tlsConfig.SecretName,
   209  							// Secret has to be in the same namespace as the Ingress.
   210  							Namespace: ing.Namespace,
   211  						},
   212  					}
   213  				} else if defaultSecretNamespace != "" && defaultSecretName != "" {
   214  					defaultListener.TLS = []model.TLSSecret{
   215  						{
   216  							Name:      defaultSecretName,
   217  							Namespace: defaultSecretNamespace,
   218  						},
   219  					}
   220  				}
   221  				defaultListener.Hostname = host
   222  				defaultListener.Port = secureListenerPort
   223  				secureListenerMap[host] = defaultListener
   224  
   225  			}
   226  		}
   227  	}
   228  
   229  	listenerSlice := make([]model.HTTPListener, 0, len(insecureListenerMap)+len(secureListenerMap))
   230  	listenerSlice = appendValuesInKeyOrder(insecureListenerMap, listenerSlice)
   231  	listenerSlice = appendValuesInKeyOrder(secureListenerMap, listenerSlice)
   232  
   233  	return listenerSlice
   234  }
   235  
   236  // IngressPassthrough translates an Ingress resource with the tls-passthrough annotation to a TLSListener.
   237  // This function does not check IngressClass (via field or annotation).
   238  // It's expected that only relevant Ingresses will have this function called on them.
   239  //
   240  // Ingress objects with SSL Passthrough enabled have the following properties:
   241  //
   242  // * must have a host set
   243  // * rules with paths other than '/' are ignored
   244  // * default backends are ignored
   245  func IngressPassthrough(ing networkingv1.Ingress, listenerPort uint32) []model.TLSPassthroughListener {
   246  	// First, we make a map of TLSListeners, with the hostname
   247  	// as the key, so that we can make sure we match up any
   248  	// TLS config with rules that match it.
   249  	// This is to approximate a set, keyed by hostname, so we can
   250  	// coalesce the config from a single Ingress.
   251  	// Coalescing the config from multiple Ingress resources is left for
   252  	// the transform component that takes a model and outputs CiliumEnvoyConfig
   253  	// or other resources.
   254  	tlsListenerMap := make(map[string]model.TLSPassthroughListener)
   255  
   256  	sourceResource := model.FullyQualifiedResource{
   257  		Name:      ing.Name,
   258  		Namespace: ing.Namespace,
   259  		Group:     "",
   260  		Version:   "v1",
   261  		Kind:      "Ingress",
   262  		UID:       string(ing.UID),
   263  	}
   264  
   265  	// Note that there's no support for default backends in SSL Passthrough
   266  	// mode.
   267  	if ing.Spec.DefaultBackend != nil {
   268  		log.WithField(logfields.Ingress, ing.Namespace+"/"+ing.Name).
   269  			Warn("Invalid SSL Passthrough Ingress rule with a default backend, skipping default backend config")
   270  	}
   271  
   272  	// Now, we range across the rules, adding them in as listeners.
   273  	for _, rule := range ing.Spec.Rules {
   274  
   275  		// SSL Passthrough Ingress objects must have a host set.
   276  		if rule.Host == "" {
   277  			log.WithField(logfields.Ingress, ing.Namespace+"/"+ing.Name).
   278  				Warn("Invalid SSL Passthrough Ingress rule without spec.rules.host defined, skipping rule")
   279  			continue
   280  		}
   281  
   282  		host := rule.Host
   283  
   284  		l, ok := tlsListenerMap[host]
   285  		l.Port = listenerPort
   286  		l.Sources = model.AddSource(l.Sources, sourceResource)
   287  		if !ok {
   288  			l.Name = "ing-" + ing.Name + "-" + ing.Namespace + "-" + host
   289  		}
   290  
   291  		l.Hostname = host
   292  
   293  		if rule.HTTP == nil {
   294  			log.WithField(logfields.Ingress, ing.Namespace+"/"+ing.Name).
   295  				Warn("Invalid SSL Passthrough Ingress rule without spec.rules.HTTP defined, skipping rule")
   296  			continue
   297  		}
   298  
   299  		for _, path := range rule.HTTP.Paths {
   300  			// SSL Passthrough objects must only have path of '/'
   301  			if path.Path != "/" {
   302  				log.WithField(logfields.Ingress, ing.Namespace+"/"+ing.Name).
   303  					Warn("Invalid SSL Passthrough Ingress rule with path not equal to '/', skipping rule")
   304  				continue
   305  			}
   306  
   307  			route := model.TLSPassthroughRoute{
   308  				Hostnames: []string{
   309  					host,
   310  				},
   311  			}
   312  
   313  			backend := model.Backend{
   314  				Name:      path.Backend.Service.Name,
   315  				Namespace: ing.Namespace,
   316  			}
   317  			if path.Backend.Service != nil {
   318  				backend.Port = &model.BackendPort{}
   319  				if path.Backend.Service.Port.Name != "" {
   320  					backend.Port.Name = path.Backend.Service.Port.Name
   321  				}
   322  				if path.Backend.Service.Port.Number != 0 {
   323  					backend.Port.Port = uint32(path.Backend.Service.Port.Number)
   324  				}
   325  			}
   326  			route.Backends = append(route.Backends, backend)
   327  			l.Routes = append(l.Routes, route)
   328  			l.Service = getService(ing)
   329  		}
   330  
   331  		// If there aren't any routes, then don't add the Listener
   332  		if len(l.Routes) == 0 {
   333  			log.WithField(logfields.Ingress, ing.Namespace+"/"+ing.Name).
   334  				Warn("Invalid SSL Passthrough Ingress with no valid rules, skipping")
   335  			continue
   336  		}
   337  
   338  		tlsListenerMap[host] = l
   339  	}
   340  
   341  	listenerSlice := make([]model.TLSPassthroughListener, 0, len(tlsListenerMap))
   342  	listenerSlice = appendValuesInKeyOrder(tlsListenerMap, listenerSlice)
   343  
   344  	return listenerSlice
   345  }
   346  
   347  func getService(ing networkingv1.Ingress) *model.Service {
   348  	if annotations.GetAnnotationServiceType(&ing) != string(corev1.ServiceTypeNodePort) {
   349  		return nil
   350  	}
   351  
   352  	m := &model.Service{
   353  		Type: string(corev1.ServiceTypeNodePort),
   354  	}
   355  	scopedLog := log.WithField(logfields.Ingress, ing.Namespace+"/"+ing.Name)
   356  	secureNodePort, err := annotations.GetAnnotationSecureNodePort(&ing)
   357  	if err != nil {
   358  		scopedLog.WithError(err).Warn("Invalid secure node port annotation, random port will be used")
   359  	} else {
   360  		m.SecureNodePort = secureNodePort
   361  	}
   362  
   363  	insureNodePort, err := annotations.GetAnnotationInsecureNodePort(&ing)
   364  	if err != nil {
   365  		scopedLog.WithError(err).Warn("Invalid insecure node port annotation, random port will be used")
   366  	} else {
   367  		m.InsecureNodePort = insureNodePort
   368  	}
   369  
   370  	return m
   371  }
   372  
   373  // appendValuesInKeyOrder ensures that the slice of listeners is stably sorted by
   374  // appending the values of the map in order of the keys to the appendSlice.
   375  func appendValuesInKeyOrder[T model.HTTPListener | model.TLSPassthroughListener](listenerMap map[string]T, appendSlice []T) []T {
   376  	var keys []string
   377  
   378  	for key := range listenerMap {
   379  		keys = append(keys, key)
   380  	}
   381  
   382  	sort.Strings(keys)
   383  	for _, key := range keys {
   384  		appendSlice = append(appendSlice, listenerMap[key])
   385  	}
   386  
   387  	return appendSlice
   388  }