istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/serviceregistry/kube/conversion.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package kube
    16  
    17  import (
    18  	"fmt"
    19  	"strings"
    20  
    21  	corev1 "k8s.io/api/core/v1"
    22  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    23  	"sigs.k8s.io/gateway-api/apis/v1beta1"
    24  
    25  	"istio.io/api/annotation"
    26  	"istio.io/api/label"
    27  	"istio.io/istio/pilot/pkg/features"
    28  	"istio.io/istio/pilot/pkg/model"
    29  	"istio.io/istio/pilot/pkg/serviceregistry/provider"
    30  	"istio.io/istio/pkg/cluster"
    31  	"istio.io/istio/pkg/config/constants"
    32  	"istio.io/istio/pkg/config/host"
    33  	"istio.io/istio/pkg/config/kube"
    34  	"istio.io/istio/pkg/config/visibility"
    35  	"istio.io/istio/pkg/spiffe"
    36  	"istio.io/istio/pkg/util/sets"
    37  )
    38  
    39  func convertPort(port corev1.ServicePort) *model.Port {
    40  	return &model.Port{
    41  		Name:     port.Name,
    42  		Port:     int(port.Port),
    43  		Protocol: kube.ConvertProtocol(port.Port, port.Name, port.Protocol, port.AppProtocol),
    44  	}
    45  }
    46  
    47  func ConvertService(svc corev1.Service, domainSuffix string, clusterID cluster.ID) *model.Service {
    48  	addrs := []string{constants.UnspecifiedIP}
    49  	resolution := model.ClientSideLB
    50  	externalName := ""
    51  	nodeLocal := false
    52  
    53  	if svc.Spec.Type == corev1.ServiceTypeExternalName && svc.Spec.ExternalName != "" {
    54  		externalName = svc.Spec.ExternalName
    55  		if features.EnableExternalNameAlias {
    56  			resolution = model.Alias
    57  		} else {
    58  			resolution = model.DNSLB
    59  		}
    60  	}
    61  	if svc.Spec.InternalTrafficPolicy != nil && *svc.Spec.InternalTrafficPolicy == corev1.ServiceInternalTrafficPolicyLocal {
    62  		nodeLocal = true
    63  	}
    64  
    65  	if svc.Spec.ClusterIP == corev1.ClusterIPNone { // headless services should not be load balanced
    66  		resolution = model.Passthrough
    67  	} else if svc.Spec.ClusterIP != "" {
    68  		addrs[0] = svc.Spec.ClusterIP
    69  		if len(svc.Spec.ClusterIPs) > 1 {
    70  			addrs = svc.Spec.ClusterIPs
    71  		}
    72  	}
    73  
    74  	ports := make([]*model.Port, 0, len(svc.Spec.Ports))
    75  	for _, port := range svc.Spec.Ports {
    76  		ports = append(ports, convertPort(port))
    77  	}
    78  
    79  	var exportTo sets.Set[visibility.Instance]
    80  	serviceaccounts := make([]string, 0)
    81  	if svc.Annotations[annotation.AlphaCanonicalServiceAccounts.Name] != "" {
    82  		serviceaccounts = append(serviceaccounts, strings.Split(svc.Annotations[annotation.AlphaCanonicalServiceAccounts.Name], ",")...)
    83  	}
    84  	if svc.Annotations[annotation.AlphaKubernetesServiceAccounts.Name] != "" {
    85  		for _, ksa := range strings.Split(svc.Annotations[annotation.AlphaKubernetesServiceAccounts.Name], ",") {
    86  			serviceaccounts = append(serviceaccounts, kubeToIstioServiceAccount(ksa, svc.Namespace))
    87  		}
    88  	}
    89  	if svc.Annotations[annotation.NetworkingExportTo.Name] != "" {
    90  		namespaces := strings.Split(svc.Annotations[annotation.NetworkingExportTo.Name], ",")
    91  		exportTo = sets.NewWithLength[visibility.Instance](len(namespaces))
    92  		for _, ns := range namespaces {
    93  			exportTo.Insert(visibility.Instance(ns))
    94  		}
    95  	}
    96  
    97  	istioService := &model.Service{
    98  		Hostname: ServiceHostname(svc.Name, svc.Namespace, domainSuffix),
    99  		ClusterVIPs: model.AddressMap{
   100  			Addresses: map[cluster.ID][]string{
   101  				clusterID: addrs,
   102  			},
   103  		},
   104  		Ports:           ports,
   105  		DefaultAddress:  addrs[0],
   106  		ServiceAccounts: serviceaccounts,
   107  		MeshExternal:    len(externalName) > 0,
   108  		Resolution:      resolution,
   109  		CreationTime:    svc.CreationTimestamp.Time,
   110  		ResourceVersion: svc.ResourceVersion,
   111  		Attributes: model.ServiceAttributes{
   112  			ServiceRegistry: provider.Kubernetes,
   113  			Name:            svc.Name,
   114  			Namespace:       svc.Namespace,
   115  			Labels:          svc.Labels,
   116  			ExportTo:        exportTo,
   117  			LabelSelectors:  svc.Spec.Selector,
   118  		},
   119  	}
   120  
   121  	switch svc.Spec.Type {
   122  	case corev1.ServiceTypeNodePort:
   123  		if _, ok := svc.Annotations[annotation.TrafficNodeSelector.Name]; !ok {
   124  			// only do this for istio ingress-gateway services
   125  			break
   126  		}
   127  		// store the service port to node port mappings
   128  		portMap := make(map[uint32]uint32)
   129  		for _, p := range svc.Spec.Ports {
   130  			portMap[uint32(p.Port)] = uint32(p.NodePort)
   131  		}
   132  		istioService.Attributes.ClusterExternalPorts = map[cluster.ID]map[uint32]uint32{clusterID: portMap}
   133  		// address mappings will be done elsewhere
   134  	case corev1.ServiceTypeLoadBalancer:
   135  		if len(svc.Status.LoadBalancer.Ingress) > 0 {
   136  			var lbAddrs []string
   137  			for _, ingress := range svc.Status.LoadBalancer.Ingress {
   138  				if len(ingress.IP) > 0 {
   139  					lbAddrs = append(lbAddrs, ingress.IP)
   140  				} else if len(ingress.Hostname) > 0 {
   141  					// DO NOT resolve the DNS here. In environments like AWS, the ELB hostname
   142  					// does not have a repeatable DNS address and IPs resolved at an earlier point
   143  					// in time may not work. So, when we get just hostnames instead of IPs, we need
   144  					// to smartly switch from EDS to strict_dns rather than doing the naive thing of
   145  					// resolving the DNS name and hoping the resolution is one-time task.
   146  					lbAddrs = append(lbAddrs, ingress.Hostname)
   147  				}
   148  			}
   149  			if len(lbAddrs) > 0 {
   150  				if istioService.Attributes.ClusterExternalAddresses == nil {
   151  					istioService.Attributes.ClusterExternalAddresses = &model.AddressMap{}
   152  				}
   153  				istioService.Attributes.ClusterExternalAddresses.SetAddressesFor(clusterID, lbAddrs)
   154  			}
   155  		}
   156  	}
   157  
   158  	istioService.Attributes.Type = string(svc.Spec.Type)
   159  	istioService.Attributes.ExternalName = externalName
   160  	istioService.Attributes.NodeLocal = nodeLocal
   161  	if len(svc.Spec.ExternalIPs) > 0 {
   162  		if istioService.Attributes.ClusterExternalAddresses == nil {
   163  			istioService.Attributes.ClusterExternalAddresses = &model.AddressMap{}
   164  		}
   165  		istioService.Attributes.ClusterExternalAddresses.AddAddressesFor(clusterID, svc.Spec.ExternalIPs)
   166  	}
   167  
   168  	return istioService
   169  }
   170  
   171  func ExternalNameEndpoints(svc *model.Service) []*model.IstioEndpoint {
   172  	if svc.Attributes.ExternalName == "" {
   173  		return nil
   174  	}
   175  	out := make([]*model.IstioEndpoint, 0, len(svc.Ports))
   176  
   177  	discoverabilityPolicy := model.AlwaysDiscoverable
   178  	if features.EnableMCSServiceDiscovery {
   179  		// MCS spec does not allow export of external name services.
   180  		// See https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#exporting-services.
   181  		discoverabilityPolicy = model.DiscoverableFromSameCluster
   182  	}
   183  	for _, portEntry := range svc.Ports {
   184  		out = append(out, &model.IstioEndpoint{
   185  			Address:               svc.Attributes.ExternalName,
   186  			EndpointPort:          uint32(portEntry.Port),
   187  			ServicePortName:       portEntry.Name,
   188  			Labels:                svc.Attributes.Labels,
   189  			DiscoverabilityPolicy: discoverabilityPolicy,
   190  		})
   191  	}
   192  	return out
   193  }
   194  
   195  // ServiceHostname produces FQDN for a k8s service
   196  func ServiceHostname(name, namespace, domainSuffix string) host.Name {
   197  	return host.Name(name + "." + namespace + "." + "svc" + "." + domainSuffix) // Format: "%s.%s.svc.%s"
   198  }
   199  
   200  // ServiceHostnameForKR calls ServiceHostname with the name and namespace of the given kubernetes resource.
   201  func ServiceHostnameForKR(obj metav1.Object, domainSuffix string) host.Name {
   202  	return ServiceHostname(obj.GetName(), obj.GetNamespace(), domainSuffix)
   203  }
   204  
   205  // kubeToIstioServiceAccount converts a K8s service account to an Istio service account
   206  func kubeToIstioServiceAccount(saname string, ns string) string {
   207  	return spiffe.MustGenSpiffeURI(ns, saname)
   208  }
   209  
   210  // SecureNamingSAN creates the secure naming used for SAN verification from pod metadata
   211  func SecureNamingSAN(pod *corev1.Pod) string {
   212  	return spiffe.MustGenSpiffeURI(pod.Namespace, pod.Spec.ServiceAccountName)
   213  }
   214  
   215  // PodTLSMode returns the tls mode associated with the pod if pod has been injected with sidecar
   216  func PodTLSMode(pod *corev1.Pod) string {
   217  	if pod == nil {
   218  		return model.DisabledTLSModeLabel
   219  	}
   220  	return model.GetTLSModeFromEndpointLabels(pod.Labels)
   221  }
   222  
   223  // IsAutoPassthrough determines if a listener should use auto passthrough mode. This is used for
   224  // multi-network. In the Istio API, this is an explicit tls.Mode. However, this mode is not part of
   225  // the gateway-api, and leaks implementation details. We already have an API to declare a Gateway as
   226  // a multi-network gateway, so we will use this as a signal.
   227  // A user who wishes to expose multi-network connectivity should create a listener named "tls-passthrough"
   228  // with TLS.Mode Passthrough.
   229  // For some backwards compatibility, we assume any listener with TLS specified and a port matching
   230  // 15443 (or the label-override for gateway port) is auto-passthrough as well.
   231  func IsAutoPassthrough(gwLabels map[string]string, l v1beta1.Listener) bool {
   232  	if l.TLS == nil {
   233  		return false
   234  	}
   235  	if hasListenerMode(l, constants.ListenerModeAutoPassthrough) {
   236  		return true
   237  	}
   238  	_, networkSet := gwLabels[label.TopologyNetwork.Name]
   239  	if !networkSet {
   240  		return false
   241  	}
   242  	expectedPort := "15443"
   243  	if port, f := gwLabels[label.NetworkingGatewayPort.Name]; f {
   244  		expectedPort = port
   245  	}
   246  	return fmt.Sprint(l.Port) == expectedPort
   247  }
   248  
   249  func hasListenerMode(l v1beta1.Listener, mode string) bool {
   250  	// TODO if we add a hybrid mode for detecting HBONE/passthrough, also check that here
   251  	return l.TLS != nil && l.TLS.Options != nil && string(l.TLS.Options[constants.ListenerModeOption]) == mode
   252  }