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 }