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  }