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 }