github.com/cilium/cilium@v1.16.2/operator/pkg/model/translation/cec_translator.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package translation 5 6 import ( 7 "cmp" 8 "fmt" 9 goslices "slices" 10 "sort" 11 12 envoy_config_cluster_v3 "github.com/cilium/proxy/go/envoy/config/cluster/v3" 13 envoy_config_route_v3 "github.com/cilium/proxy/go/envoy/config/route/v3" 14 "golang.org/x/exp/maps" 15 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 17 "github.com/cilium/cilium/operator/pkg/model" 18 "github.com/cilium/cilium/pkg/k8s" 19 ciliumv2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" 20 slim_metav1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1" 21 "github.com/cilium/cilium/pkg/slices" 22 ) 23 24 const ( 25 secureHost = "secure" 26 insecureHost = "insecure" 27 28 AppProtocolH2C = "kubernetes.io/h2c" 29 AppProtocolWS = "kubernetes.io/ws" 30 AppProtocolWSS = "kubernetes.io/wss" 31 ) 32 33 var _ CECTranslator = (*cecTranslator)(nil) 34 35 // cecTranslator is the translator from model to CiliumEnvoyConfig 36 // 37 // This translator is used for shared LB mode. 38 // - only one instance of CiliumEnvoyConfig with two listeners (secure and 39 // in-secure). 40 // - no LB service and endpoint 41 type cecTranslator struct { 42 secretsNamespace string 43 useProxyProtocol bool 44 useAppProtocol bool 45 useAlpn bool 46 47 hostNetworkEnabled bool 48 hostNetworkNodeLabelSelector *slim_metav1.LabelSelector 49 ipv4Enabled bool 50 ipv6Enabled bool 51 52 // hostNameSuffixMatch is a flag to control whether the host name suffix match. 53 // Hostnames that are prefixed with a wildcard label (`*.`) are interpreted 54 // as a suffix match. That means that a match for `*.example.com` would match 55 // both `test.example.com`, and `foo.test.example.com`, but not `example.com`. 56 hostNameSuffixMatch bool 57 58 idleTimeoutSeconds int 59 60 xffNumTrustedHops uint32 61 } 62 63 // NewCECTranslator returns a new translator 64 func NewCECTranslator(secretsNamespace string, useProxyProtocol bool, useAppProtocol bool, hostNameSuffixMatch bool, idleTimeoutSeconds int, 65 hostNetworkEnabled bool, hostNetworkNodeLabelSelector *slim_metav1.LabelSelector, ipv4Enabled bool, ipv6Enabled bool, 66 xffNumTrustedHops uint32, 67 ) CECTranslator { 68 return &cecTranslator{ 69 secretsNamespace: secretsNamespace, 70 useProxyProtocol: useProxyProtocol, 71 useAppProtocol: useAppProtocol, 72 useAlpn: false, 73 hostNameSuffixMatch: hostNameSuffixMatch, 74 idleTimeoutSeconds: idleTimeoutSeconds, 75 xffNumTrustedHops: xffNumTrustedHops, 76 hostNetworkEnabled: hostNetworkEnabled, 77 hostNetworkNodeLabelSelector: hostNetworkNodeLabelSelector, 78 ipv4Enabled: ipv4Enabled, 79 ipv6Enabled: ipv6Enabled, 80 } 81 } 82 83 func (i *cecTranslator) WithUseAlpn(useAlpn bool) { 84 i.useAlpn = useAlpn 85 } 86 87 func (i *cecTranslator) Translate(namespace string, name string, model *model.Model) (*ciliumv2.CiliumEnvoyConfig, error) { 88 cec := &ciliumv2.CiliumEnvoyConfig{ 89 ObjectMeta: metav1.ObjectMeta{ 90 Namespace: namespace, 91 Name: name, 92 Labels: map[string]string{ 93 k8s.UseOriginalSourceAddressLabel: "false", 94 }, 95 }, 96 } 97 98 cec.Spec.BackendServices = i.getBackendServices(model) 99 cec.Spec.Services = i.getServicesWithPorts(namespace, name, model) 100 cec.Spec.Resources = i.getResources(model) 101 102 if i.hostNetworkEnabled { 103 cec.Spec.NodeSelector = i.hostNetworkNodeLabelSelector 104 } 105 106 return cec, nil 107 } 108 109 func (i *cecTranslator) getBackendServices(m *model.Model) []*ciliumv2.Service { 110 var res []*ciliumv2.Service 111 112 for ns, v := range getNamespaceNamePortsMap(m) { 113 for name, ports := range v { 114 res = append(res, &ciliumv2.Service{ 115 Name: name, 116 Namespace: ns, 117 Ports: ports, 118 }) 119 } 120 } 121 122 // Make sure the result is sorted by namespace and name to avoid any 123 // nondeterministic behavior. 124 sort.Slice(res, func(i, j int) bool { 125 if res[i].Namespace != res[j].Namespace { 126 return res[i].Namespace < res[j].Namespace 127 } 128 if res[i].Name != res[j].Name { 129 return res[i].Name < res[j].Name 130 } 131 return res[i].Ports[0] < res[j].Ports[0] 132 }) 133 return res 134 } 135 136 func (i *cecTranslator) getServicesWithPorts(namespace string, name string, m *model.Model) []*ciliumv2.ServiceListener { 137 // Find all the ports used in the model and build a set of them 138 allPorts := make(map[uint16]struct{}) 139 140 for _, hl := range m.HTTP { 141 if _, ok := allPorts[uint16(hl.Port)]; !ok { 142 allPorts[uint16(hl.Port)] = struct{}{} 143 } 144 } 145 for _, tlsl := range m.TLSPassthrough { 146 if _, ok := allPorts[uint16(tlsl.Port)]; !ok { 147 allPorts[uint16(tlsl.Port)] = struct{}{} 148 } 149 } 150 151 ports := maps.Keys(allPorts) 152 // ensure the ports are stably sorted 153 goslices.SortStableFunc(ports, func(a, b uint16) int { 154 return cmp.Compare(a, b) 155 }) 156 157 return []*ciliumv2.ServiceListener{ 158 { 159 Namespace: namespace, 160 Name: model.Shorten(name), 161 Ports: ports, 162 }, 163 } 164 } 165 166 func (i *cecTranslator) getResources(m *model.Model) []ciliumv2.XDSResource { 167 var res []ciliumv2.XDSResource 168 169 res = append(res, i.getListener(m)...) 170 res = append(res, i.getEnvoyHTTPRouteConfiguration(m)...) 171 res = append(res, i.getClusters(m)...) 172 173 return res 174 } 175 176 func tlsSecretsToHostnames(httpListeners []model.HTTPListener) map[model.TLSSecret][]string { 177 tlsSecretsToHostnames := make(map[model.TLSSecret][]string) 178 for _, h := range httpListeners { 179 for _, s := range h.TLS { 180 tlsSecretsToHostnames[s] = append(tlsSecretsToHostnames[s], h.Hostname) 181 } 182 } 183 184 return tlsSecretsToHostnames 185 } 186 187 func tlsPassthroughBackendsToHostnames(tlsPassthroughListeners []model.TLSPassthroughListener) map[string][]string { 188 tlsPassthroughBackendsToHostnames := make(map[string][]string) 189 for _, h := range tlsPassthroughListeners { 190 for _, route := range h.Routes { 191 for _, backend := range route.Backends { 192 key := fmt.Sprintf("%s:%s:%s", backend.Namespace, backend.Name, backend.Port.GetPort()) 193 tlsPassthroughBackendsToHostnames[key] = append(tlsPassthroughBackendsToHostnames[key], route.Hostnames...) 194 } 195 } 196 } 197 198 return tlsPassthroughBackendsToHostnames 199 } 200 201 // getListener returns the listener for the given model. 202 // - HTTP non-TLS filters 203 // - HTTP TLS filters 204 // - TLS passthrough filters 205 func (i *cecTranslator) getListener(m *model.Model) []ciliumv2.XDSResource { 206 if len(m.HTTP) == 0 && len(m.TLSPassthrough) == 0 { 207 return nil 208 } 209 210 mutatorFuncs := []ListenerMutator{} 211 if i.useProxyProtocol { 212 mutatorFuncs = append(mutatorFuncs, WithProxyProtocol()) 213 } 214 215 if i.useAlpn { 216 mutatorFuncs = append(mutatorFuncs, WithAlpn()) 217 } 218 219 if i.hostNetworkEnabled { 220 mutatorFuncs = append(mutatorFuncs, WithHostNetworkPort(m, i.ipv4Enabled, i.ipv6Enabled)) 221 } 222 223 if i.xffNumTrustedHops > 0 { 224 mutatorFuncs = append(mutatorFuncs, WithXffNumTrustedHops(i.xffNumTrustedHops)) 225 } 226 227 l, _ := newListenerWithDefaults("listener", i.secretsNamespace, len(m.HTTP) > 0, tlsSecretsToHostnames(m.HTTP), tlsPassthroughBackendsToHostnames(m.TLSPassthrough), mutatorFuncs...) 228 return []ciliumv2.XDSResource{l} 229 } 230 231 // getRouteConfiguration returns the route configuration for the given model. 232 func (i *cecTranslator) getEnvoyHTTPRouteConfiguration(m *model.Model) []ciliumv2.XDSResource { 233 var res []ciliumv2.XDSResource 234 235 type hostnameRedirect struct { 236 hostname string 237 redirect bool 238 } 239 240 portHostNameRedirect := map[string][]hostnameRedirect{} 241 hostNamePortRoutes := map[string]map[string][]model.HTTPRoute{} 242 243 for _, l := range m.HTTP { 244 for _, r := range l.Routes { 245 port := insecureHost 246 if l.TLS != nil { 247 port = secureHost 248 } 249 250 if len(r.Hostnames) == 0 { 251 hnr := hostnameRedirect{ 252 hostname: l.Hostname, 253 redirect: l.ForceHTTPtoHTTPSRedirect, 254 } 255 portHostNameRedirect[port] = append(portHostNameRedirect[port], hnr) 256 if _, ok := hostNamePortRoutes[l.Hostname]; !ok { 257 hostNamePortRoutes[l.Hostname] = map[string][]model.HTTPRoute{} 258 } 259 hostNamePortRoutes[l.Hostname][port] = append(hostNamePortRoutes[l.Hostname][port], r) 260 continue 261 } 262 for _, h := range r.Hostnames { 263 hnr := hostnameRedirect{ 264 hostname: h, 265 redirect: l.ForceHTTPtoHTTPSRedirect, 266 } 267 portHostNameRedirect[port] = append(portHostNameRedirect[port], hnr) 268 if _, ok := hostNamePortRoutes[h]; !ok { 269 hostNamePortRoutes[h] = map[string][]model.HTTPRoute{} 270 } 271 hostNamePortRoutes[h][port] = append(hostNamePortRoutes[h][port], r) 272 } 273 } 274 } 275 276 for _, port := range []string{insecureHost, secureHost} { 277 hostNames, exists := portHostNameRedirect[port] 278 if !exists { 279 continue 280 } 281 var virtualhosts []*envoy_config_route_v3.VirtualHost 282 283 redirectedHost := map[string]struct{}{} 284 // Add HTTPs redirect virtual host for secure host 285 if port == insecureHost { 286 for _, h := range slices.Unique(portHostNameRedirect[secureHost]) { 287 if h.redirect { 288 vhs, _ := NewVirtualHostWithDefaults(hostNamePortRoutes[h.hostname][secureHost], VirtualHostParameter{ 289 HostNames: []string{h.hostname}, 290 HTTPSRedirect: true, 291 HostNameSuffixMatch: i.hostNameSuffixMatch, 292 ListenerPort: m.HTTP[0].Port, 293 }) 294 virtualhosts = append(virtualhosts, vhs) 295 redirectedHost[h.hostname] = struct{}{} 296 } 297 } 298 } 299 for _, h := range slices.Unique(hostNames) { 300 if port == insecureHost { 301 if _, ok := redirectedHost[h.hostname]; ok { 302 continue 303 } 304 } 305 routes, exists := hostNamePortRoutes[h.hostname][port] 306 if !exists { 307 continue 308 } 309 vhs, _ := NewVirtualHostWithDefaults(routes, VirtualHostParameter{ 310 HostNames: []string{h.hostname}, 311 HTTPSRedirect: false, 312 HostNameSuffixMatch: i.hostNameSuffixMatch, 313 ListenerPort: m.HTTP[0].Port, 314 }) 315 virtualhosts = append(virtualhosts, vhs) 316 } 317 318 // the route name should match the value in http connection manager 319 // otherwise the request will be dropped by envoy 320 routeName := fmt.Sprintf("listener-%s", port) 321 goslices.SortStableFunc(virtualhosts, func(a, b *envoy_config_route_v3.VirtualHost) int { return cmp.Compare(a.Name, b.Name) }) 322 rc, _ := NewRouteConfiguration(routeName, virtualhosts) 323 res = append(res, rc) 324 } 325 326 return res 327 } 328 329 func getClusterName(ns, name, port string) string { 330 // the name is having the format of "namespace:name:port" 331 // -> slash would prevent ParseResources from rewriting with CEC namespace and name! 332 return fmt.Sprintf("%s:%s:%s", ns, name, port) 333 } 334 335 func getClusterServiceName(ns, name, port string) string { 336 // the name is having the format of "namespace/name:port" 337 return fmt.Sprintf("%s/%s:%s", ns, name, port) 338 } 339 340 func (i *cecTranslator) getClusters(m *model.Model) []ciliumv2.XDSResource { 341 envoyClusters := map[string]ciliumv2.XDSResource{} 342 var sortedClusterNames []string 343 344 for ns, v := range getNamespaceNamePortsMapForHTTP(m) { 345 for name, ports := range v { 346 for _, port := range ports { 347 clusterName := getClusterName(ns, name, port) 348 clusterServiceName := getClusterServiceName(ns, name, port) 349 sortedClusterNames = append(sortedClusterNames, clusterName) 350 mutators := []ClusterMutator{ 351 WithConnectionTimeout(5), 352 WithIdleTimeout(i.idleTimeoutSeconds), 353 WithClusterLbPolicy(int32(envoy_config_cluster_v3.Cluster_ROUND_ROBIN)), 354 WithOutlierDetection(true), 355 } 356 357 if isGRPCService(m, ns, name, port) { 358 mutators = append(mutators, WithProtocol(HTTPVersion2)) 359 } else if i.useAppProtocol { 360 appProtocol := getAppProtocol(m, ns, name, port) 361 362 switch appProtocol { 363 case AppProtocolH2C: 364 mutators = append(mutators, WithProtocol(HTTPVersion2)) 365 default: 366 // When --use-app-protocol is used, envoy will set upstream protocol to HTTP/1.1 367 mutators = append(mutators, WithProtocol(HTTPVersion1)) 368 } 369 } 370 envoyClusters[clusterName], _ = NewHTTPCluster(clusterName, clusterServiceName, mutators...) 371 } 372 } 373 } 374 for ns, v := range getNamespaceNamePortsMapForTLS(m) { 375 for name, ports := range v { 376 for _, port := range ports { 377 clusterName := getClusterName(ns, name, port) 378 clusterServiceName := getClusterServiceName(ns, name, port) 379 sortedClusterNames = append(sortedClusterNames, clusterName) 380 envoyClusters[clusterName], _ = NewTCPClusterWithDefaults(clusterName, clusterServiceName) 381 } 382 } 383 } 384 385 sort.Strings(sortedClusterNames) 386 res := make([]ciliumv2.XDSResource, len(sortedClusterNames)) 387 for i, name := range sortedClusterNames { 388 res[i] = envoyClusters[name] 389 } 390 391 return res 392 } 393 394 func isGRPCService(m *model.Model, ns string, name string, port string) bool { 395 var res bool 396 397 for _, l := range m.HTTP { 398 for _, r := range l.Routes { 399 if !r.IsGRPC { 400 continue 401 } 402 for _, be := range r.Backends { 403 if be.Name == name && be.Namespace == ns && be.Port != nil && be.Port.GetPort() == port { 404 return true 405 } 406 } 407 } 408 } 409 return res 410 } 411 412 func getAppProtocol(m *model.Model, ns string, name string, port string) string { 413 for _, l := range m.HTTP { 414 for _, r := range l.Routes { 415 for _, be := range r.Backends { 416 if be.Name == name && be.Namespace == ns && be.Port != nil && be.Port.GetPort() == port { 417 if be.AppProtocol != nil { 418 return *be.AppProtocol 419 } 420 } 421 } 422 } 423 } 424 425 return "" 426 } 427 428 // getNamespaceNamePortsMap returns a map of namespace -> name -> ports. 429 // it gets all HTTP and TLS routes. 430 // The ports are sorted and unique. 431 func getNamespaceNamePortsMap(m *model.Model) map[string]map[string][]string { 432 namespaceNamePortMap := map[string]map[string][]string{} 433 for _, l := range m.HTTP { 434 for _, r := range l.Routes { 435 for _, be := range r.Backends { 436 namePortMap, exist := namespaceNamePortMap[be.Namespace] 437 if exist { 438 namePortMap[be.Name] = slices.SortedUnique(append(namePortMap[be.Name], be.Port.GetPort())) 439 } else { 440 namePortMap = map[string][]string{ 441 be.Name: {be.Port.GetPort()}, 442 } 443 } 444 namespaceNamePortMap[be.Namespace] = namePortMap 445 } 446 mergeBackendsInNamespaceNamePortMap(r.Backends, namespaceNamePortMap) 447 448 for _, rm := range r.RequestMirrors { 449 if rm.Backend == nil { 450 continue 451 } 452 mergeBackendsInNamespaceNamePortMap([]model.Backend{*rm.Backend}, namespaceNamePortMap) 453 } 454 } 455 } 456 457 for _, l := range m.TLSPassthrough { 458 for _, r := range l.Routes { 459 mergeBackendsInNamespaceNamePortMap(r.Backends, namespaceNamePortMap) 460 } 461 } 462 463 return namespaceNamePortMap 464 } 465 466 // getNamespaceNamePortsMapForHTTP returns a map of namespace -> name -> ports. 467 // The ports are sorted and unique. 468 func getNamespaceNamePortsMapForHTTP(m *model.Model) map[string]map[string][]string { 469 namespaceNamePortMap := map[string]map[string][]string{} 470 for _, l := range m.HTTP { 471 for _, r := range l.Routes { 472 mergeBackendsInNamespaceNamePortMap(r.Backends, namespaceNamePortMap) 473 for _, rm := range r.RequestMirrors { 474 if rm.Backend == nil { 475 continue 476 } 477 mergeBackendsInNamespaceNamePortMap([]model.Backend{*rm.Backend}, namespaceNamePortMap) 478 } 479 } 480 } 481 return namespaceNamePortMap 482 } 483 484 // getNamespaceNamePortsMapFroTLS returns a map of namespace -> name -> ports. 485 // The ports are sorted and unique. 486 func getNamespaceNamePortsMapForTLS(m *model.Model) map[string]map[string][]string { 487 namespaceNamePortMap := map[string]map[string][]string{} 488 for _, l := range m.TLSPassthrough { 489 for _, r := range l.Routes { 490 mergeBackendsInNamespaceNamePortMap(r.Backends, namespaceNamePortMap) 491 } 492 } 493 return namespaceNamePortMap 494 } 495 496 func mergeBackendsInNamespaceNamePortMap(backends []model.Backend, namespaceNamePortMap map[string]map[string][]string) { 497 for _, be := range backends { 498 namePortMap, exist := namespaceNamePortMap[be.Namespace] 499 if exist { 500 namePortMap[be.Name] = slices.SortedUnique(append(namePortMap[be.Name], be.Port.GetPort())) 501 } else { 502 namePortMap = map[string][]string{ 503 be.Name: {be.Port.GetPort()}, 504 } 505 } 506 namespaceNamePortMap[be.Namespace] = namePortMap 507 } 508 }