istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/serviceregistry/serviceentry/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 serviceentry 16 17 import ( 18 "net/netip" 19 "strings" 20 "time" 21 22 "istio.io/api/label" 23 networking "istio.io/api/networking/v1alpha3" 24 "istio.io/istio/pilot/pkg/features" 25 "istio.io/istio/pilot/pkg/model" 26 "istio.io/istio/pilot/pkg/serviceregistry/provider" 27 labelutil "istio.io/istio/pilot/pkg/serviceregistry/util/label" 28 "istio.io/istio/pkg/cluster" 29 "istio.io/istio/pkg/config" 30 "istio.io/istio/pkg/config/constants" 31 "istio.io/istio/pkg/config/host" 32 "istio.io/istio/pkg/config/protocol" 33 "istio.io/istio/pkg/config/schema/gvk" 34 "istio.io/istio/pkg/config/visibility" 35 "istio.io/istio/pkg/kube/labels" 36 "istio.io/istio/pkg/network" 37 "istio.io/istio/pkg/spiffe" 38 netutil "istio.io/istio/pkg/util/net" 39 "istio.io/istio/pkg/util/sets" 40 ) 41 42 func convertPort(port *networking.ServicePort) *model.Port { 43 return &model.Port{ 44 Name: port.Name, 45 Port: int(port.Number), 46 Protocol: protocol.Parse(port.Protocol), 47 } 48 } 49 50 type HostAddress struct { 51 host string 52 address string 53 } 54 55 // ServiceToServiceEntry converts from internal Service representation to ServiceEntry 56 // This does not include endpoints - they'll be represented as EndpointSlice or EDS. 57 // 58 // See convertServices() for the reverse conversion, used by Istio to handle ServiceEntry configs. 59 // See kube.ConvertService for the conversion from K8S to internal Service. 60 func ServiceToServiceEntry(svc *model.Service, proxy *model.Proxy) *config.Config { 61 gvk := gvk.ServiceEntry 62 se := &networking.ServiceEntry{ 63 // Host is fully qualified: name, namespace, domainSuffix 64 Hosts: []string{string(svc.Hostname)}, 65 66 // Internal Service and K8S Service have a single Address. 67 // ServiceEntry can represent multiple - but we are not using that. SE may be merged. 68 // Will be 0.0.0.0 if not specified as ClusterIP or ClusterIP==None. In such case resolution is Passthrough. 69 Addresses: svc.GetAddresses(proxy), 70 71 // This is based on alpha.istio.io/canonical-serviceaccounts and 72 // alpha.istio.io/kubernetes-serviceaccounts. 73 SubjectAltNames: svc.ServiceAccounts, 74 } 75 76 if len(svc.Attributes.LabelSelectors) > 0 { 77 se.WorkloadSelector = &networking.WorkloadSelector{Labels: svc.Attributes.LabelSelectors} 78 } 79 80 // Based on networking.istio.io/exportTo annotation 81 for k := range svc.Attributes.ExportTo { 82 // k is Private or Public 83 se.ExportTo = append(se.ExportTo, string(k)) 84 } 85 86 if svc.MeshExternal { 87 se.Location = networking.ServiceEntry_MESH_EXTERNAL // 0 - default 88 } else { 89 se.Location = networking.ServiceEntry_MESH_INTERNAL 90 } 91 92 // Reverse in convertServices. Note that enum values are different 93 var resolution networking.ServiceEntry_Resolution 94 switch svc.Resolution { 95 case model.Passthrough: // 2 96 resolution = networking.ServiceEntry_NONE // 0 97 case model.DNSLB: // 1 98 resolution = networking.ServiceEntry_DNS // 2 99 case model.DNSRoundRobinLB: // 3 100 resolution = networking.ServiceEntry_DNS_ROUND_ROBIN // 3 101 case model.ClientSideLB: // 0 102 resolution = networking.ServiceEntry_STATIC // 1 103 } 104 se.Resolution = resolution 105 106 // Port is mapped from ServicePort 107 for _, p := range svc.Ports { 108 se.Ports = append(se.Ports, &networking.ServicePort{ 109 Number: uint32(p.Port), 110 Name: p.Name, 111 // Protocol is converted to protocol.Instance - reverse conversion will use the name. 112 Protocol: string(p.Protocol), 113 // TODO: target port 114 }) 115 } 116 117 cfg := &config.Config{ 118 Meta: config.Meta{ 119 GroupVersionKind: gvk, 120 Name: "synthetic-" + svc.Attributes.Name, 121 Namespace: svc.Attributes.Namespace, 122 CreationTimestamp: svc.CreationTime, 123 ResourceVersion: svc.ResourceVersion, 124 }, 125 Spec: se, 126 } 127 128 // TODO: WorkloadSelector 129 130 // TODO: preserve ServiceRegistry. The reverse conversion sets it to 'external' 131 // TODO: preserve UID ? It seems MCP didn't preserve it - but that code path was not used much. 132 133 // TODO: ClusterExternalPorts map - for NodePort services, with "traffic.istio.io/nodeSelector" ann 134 // It's a per-cluster map 135 136 // TODO: ClusterExternalAddresses - for LB types, per cluster. Populated from K8S, missing 137 // in SE. Used for multi-network support. 138 return cfg 139 } 140 141 // convertServices transforms a ServiceEntry config to a list of internal Service objects. 142 func convertServices(cfg config.Config) []*model.Service { 143 serviceEntry := cfg.Spec.(*networking.ServiceEntry) 144 creationTime := cfg.CreationTimestamp 145 146 var resolution model.Resolution 147 switch serviceEntry.Resolution { 148 case networking.ServiceEntry_NONE: 149 resolution = model.Passthrough 150 case networking.ServiceEntry_DNS: 151 resolution = model.DNSLB 152 case networking.ServiceEntry_DNS_ROUND_ROBIN: 153 resolution = model.DNSRoundRobinLB 154 case networking.ServiceEntry_STATIC: 155 resolution = model.ClientSideLB 156 } 157 158 svcPorts := make(model.PortList, 0, len(serviceEntry.Ports)) 159 var portOverrides map[uint32]uint32 160 for _, port := range serviceEntry.Ports { 161 svcPorts = append(svcPorts, convertPort(port)) 162 if resolution == model.Passthrough && port.TargetPort != 0 { 163 if portOverrides == nil { 164 portOverrides = map[uint32]uint32{} 165 } 166 portOverrides[port.Number] = port.TargetPort 167 } 168 } 169 170 var exportTo sets.Set[visibility.Instance] 171 if len(serviceEntry.ExportTo) > 0 { 172 exportTo = sets.NewWithLength[visibility.Instance](len(serviceEntry.ExportTo)) 173 for _, e := range serviceEntry.ExportTo { 174 exportTo.Insert(visibility.Instance(e)) 175 } 176 } 177 178 var labelSelectors map[string]string 179 if serviceEntry.WorkloadSelector != nil { 180 labelSelectors = serviceEntry.WorkloadSelector.Labels 181 } 182 hostAddresses := []*HostAddress{} 183 for _, hostname := range serviceEntry.Hosts { 184 if len(serviceEntry.Addresses) > 0 { 185 for _, address := range serviceEntry.Addresses { 186 // Check if address is an IP first because that is the most common case. 187 if netutil.IsValidIPAddress(address) { 188 hostAddresses = append(hostAddresses, &HostAddress{hostname, address}) 189 } else if cidr, cidrErr := netip.ParsePrefix(address); cidrErr == nil { 190 newAddress := address 191 if cidr.Bits() == cidr.Addr().BitLen() { 192 // /32 mask. Remove the /32 and make it a normal IP address 193 newAddress = cidr.Addr().String() 194 } 195 hostAddresses = append(hostAddresses, &HostAddress{hostname, newAddress}) 196 } 197 } 198 } else { 199 hostAddresses = append(hostAddresses, &HostAddress{hostname, constants.UnspecifiedIP}) 200 } 201 } 202 203 return buildServices(hostAddresses, cfg.Name, cfg.Namespace, svcPorts, serviceEntry.Location, resolution, 204 exportTo, labelSelectors, serviceEntry.SubjectAltNames, creationTime, cfg.Labels, portOverrides) 205 } 206 207 func buildServices(hostAddresses []*HostAddress, name, namespace string, ports model.PortList, location networking.ServiceEntry_Location, 208 resolution model.Resolution, exportTo sets.Set[visibility.Instance], selectors map[string]string, saccounts []string, 209 ctime time.Time, labels map[string]string, overrides map[uint32]uint32, 210 ) []*model.Service { 211 out := make([]*model.Service, 0, len(hostAddresses)) 212 lbls := labels 213 if features.CanonicalServiceForMeshExternalServiceEntry && location == networking.ServiceEntry_MESH_EXTERNAL { 214 lbls = ensureCanonicalServiceLabels(name, labels) 215 } 216 for _, ha := range hostAddresses { 217 out = append(out, &model.Service{ 218 CreationTime: ctime, 219 MeshExternal: location == networking.ServiceEntry_MESH_EXTERNAL, 220 Hostname: host.Name(ha.host), 221 DefaultAddress: ha.address, 222 Ports: ports, 223 Resolution: resolution, 224 Attributes: model.ServiceAttributes{ 225 ServiceRegistry: provider.External, 226 PassthroughTargetPorts: overrides, 227 Name: ha.host, 228 Namespace: namespace, 229 Labels: lbls, 230 ExportTo: exportTo, 231 LabelSelectors: selectors, 232 }, 233 ServiceAccounts: saccounts, 234 }) 235 } 236 return out 237 } 238 239 func ensureCanonicalServiceLabels(name string, srcLabels map[string]string) map[string]string { 240 if srcLabels == nil { 241 srcLabels = make(map[string]string) 242 } 243 _, svcLabelFound := srcLabels[model.IstioCanonicalServiceLabelName] 244 _, revLabelFound := srcLabels[model.IstioCanonicalServiceRevisionLabelName] 245 if svcLabelFound && revLabelFound { 246 return srcLabels 247 } 248 249 srcLabels[model.IstioCanonicalServiceLabelName], srcLabels[model.IstioCanonicalServiceRevisionLabelName] = labels.CanonicalService(srcLabels, name) 250 return srcLabels 251 } 252 253 func (s *Controller) convertEndpoint(service *model.Service, servicePort *networking.ServicePort, 254 wle *networking.WorkloadEntry, configKey *configKey, clusterID cluster.ID, 255 ) *model.ServiceInstance { 256 var instancePort uint32 257 addr := wle.GetAddress() 258 // priority level: unixAddress > we.ports > se.port.targetPort > se.port.number 259 if strings.HasPrefix(addr, model.UnixAddressPrefix) { 260 instancePort = 0 261 addr = strings.TrimPrefix(addr, model.UnixAddressPrefix) 262 } else if port, ok := wle.Ports[servicePort.Name]; ok && port > 0 { 263 instancePort = port 264 } else if servicePort.TargetPort > 0 { 265 instancePort = servicePort.TargetPort 266 } else { 267 // final fallback is to the service port value 268 instancePort = servicePort.Number 269 } 270 271 tlsMode := getTLSModeFromWorkloadEntry(wle) 272 sa := "" 273 if wle.ServiceAccount != "" { 274 sa = spiffe.MustGenSpiffeURI(service.Attributes.Namespace, wle.ServiceAccount) 275 } 276 networkID := s.workloadEntryNetwork(wle) 277 locality := wle.Locality 278 if locality == "" && len(wle.Labels[model.LocalityLabel]) > 0 { 279 locality = model.GetLocalityLabel(wle.Labels[model.LocalityLabel]) 280 } 281 labels := labelutil.AugmentLabels(wle.Labels, clusterID, locality, "", networkID) 282 return &model.ServiceInstance{ 283 Endpoint: &model.IstioEndpoint{ 284 Address: addr, 285 EndpointPort: instancePort, 286 ServicePortName: servicePort.Name, 287 288 LegacyClusterPortKey: int(servicePort.Number), 289 Network: network.ID(wle.Network), 290 Locality: model.Locality{ 291 Label: locality, 292 ClusterID: clusterID, 293 }, 294 LbWeight: wle.Weight, 295 Labels: labels, 296 TLSMode: tlsMode, 297 ServiceAccount: sa, 298 // Workload entry config name is used as workload name, which will appear in metric label. 299 // After VM auto registry is introduced, workload group annotation should be used for workload name. 300 WorkloadName: configKey.name, 301 Namespace: configKey.namespace, 302 }, 303 Service: service, 304 ServicePort: convertPort(servicePort), 305 } 306 } 307 308 // convertWorkloadEntryToServiceInstances translates a WorkloadEntry into ServiceEndpoints. This logic is largely the 309 // same as the ServiceEntry convertServiceEntryToInstances. 310 func (s *Controller) convertWorkloadEntryToServiceInstances(wle *networking.WorkloadEntry, services []*model.Service, 311 se *networking.ServiceEntry, configKey *configKey, clusterID cluster.ID, 312 ) []*model.ServiceInstance { 313 out := make([]*model.ServiceInstance, 0) 314 for _, service := range services { 315 for _, port := range se.Ports { 316 out = append(out, s.convertEndpoint(service, port, wle, configKey, clusterID)) 317 } 318 } 319 return out 320 } 321 322 func (s *Controller) convertServiceEntryToInstances(cfg config.Config, services []*model.Service) []*model.ServiceInstance { 323 out := make([]*model.ServiceInstance, 0) 324 serviceEntry := cfg.Spec.(*networking.ServiceEntry) 325 if serviceEntry == nil { 326 return nil 327 } 328 if services == nil { 329 services = convertServices(cfg) 330 } 331 for _, service := range services { 332 for _, serviceEntryPort := range serviceEntry.Ports { 333 if len(serviceEntry.Endpoints) == 0 && serviceEntry.WorkloadSelector == nil && 334 (serviceEntry.Resolution == networking.ServiceEntry_DNS || serviceEntry.Resolution == networking.ServiceEntry_DNS_ROUND_ROBIN) { 335 // Note: only convert the hostname to service instance if WorkloadSelector is not set 336 // when service entry has discovery type DNS and no endpoints 337 // we create endpoints from service's host 338 // Do not use serviceentry.hosts as a service entry is converted into 339 // multiple services (one for each host) 340 endpointPort := serviceEntryPort.Number 341 if serviceEntryPort.TargetPort > 0 { 342 endpointPort = serviceEntryPort.TargetPort 343 } 344 out = append(out, &model.ServiceInstance{ 345 Endpoint: &model.IstioEndpoint{ 346 Address: string(service.Hostname), 347 EndpointPort: endpointPort, 348 ServicePortName: serviceEntryPort.Name, 349 LegacyClusterPortKey: int(serviceEntryPort.Number), 350 Labels: nil, 351 TLSMode: model.DisabledTLSModeLabel, 352 }, 353 Service: service, 354 ServicePort: convertPort(serviceEntryPort), 355 }) 356 } else { 357 for _, endpoint := range serviceEntry.Endpoints { 358 out = append(out, s.convertEndpoint(service, serviceEntryPort, endpoint, &configKey{}, s.clusterID)) 359 } 360 } 361 } 362 } 363 return out 364 } 365 366 func getTLSModeFromWorkloadEntry(wle *networking.WorkloadEntry) string { 367 // * Use security.istio.io/tlsMode if its present 368 // * If not, set TLS mode if ServiceAccount is specified 369 tlsMode := model.DisabledTLSModeLabel 370 if val, exists := wle.Labels[label.SecurityTlsMode.Name]; exists { 371 tlsMode = val 372 } else if wle.ServiceAccount != "" { 373 tlsMode = model.IstioMutualTLSModeLabel 374 } 375 376 return tlsMode 377 } 378 379 // The workload instance has pointer to the service and its service port. 380 // We need to create our own but we can retain the endpoint already created. 381 func convertWorkloadInstanceToServiceInstance(workloadInstance *model.WorkloadInstance, serviceEntryServices []*model.Service, 382 serviceEntry *networking.ServiceEntry, 383 ) []*model.ServiceInstance { 384 out := make([]*model.ServiceInstance, 0) 385 for _, service := range serviceEntryServices { 386 for _, serviceEntryPort := range serviceEntry.Ports { 387 // note: this is same as workloadentry handler 388 // endpoint port will first use the port defined in wle with same port name, 389 // if not port name not match, use the targetPort specified in ServiceEntry 390 // if both not matched, fallback to ServiceEntry port number. 391 var targetPort uint32 392 if port, ok := workloadInstance.PortMap[serviceEntryPort.Name]; ok && port > 0 { 393 targetPort = port 394 } else if serviceEntryPort.TargetPort > 0 { 395 targetPort = serviceEntryPort.TargetPort 396 } else { 397 targetPort = serviceEntryPort.Number 398 } 399 ep := workloadInstance.Endpoint.ShallowCopy() 400 ep.ServicePortName = serviceEntryPort.Name 401 ep.LegacyClusterPortKey = int(serviceEntryPort.Number) 402 403 ep.EndpointPort = targetPort 404 out = append(out, &model.ServiceInstance{ 405 Endpoint: ep, 406 Service: service, 407 ServicePort: convertPort(serviceEntryPort), 408 }) 409 } 410 } 411 return out 412 } 413 414 // Convenience function to convert a workloadEntry into a WorkloadInstance object encoding the endpoint (without service 415 // port names) and the namespace - k8s will consume this workload instance when selecting workload entries 416 func (s *Controller) convertWorkloadEntryToWorkloadInstance(cfg config.Config, clusterID cluster.ID) *model.WorkloadInstance { 417 we := ConvertWorkloadEntry(cfg) 418 addr := we.GetAddress() 419 dnsServiceEntryOnly := false 420 if strings.HasPrefix(addr, model.UnixAddressPrefix) { 421 // k8s can't use uds for service objects 422 dnsServiceEntryOnly = true 423 } 424 if addr != "" && !netutil.IsValidIPAddress(addr) { 425 // k8s can't use workloads with hostnames in the address field. 426 dnsServiceEntryOnly = true 427 } 428 tlsMode := getTLSModeFromWorkloadEntry(we) 429 sa := "" 430 if we.ServiceAccount != "" { 431 sa = spiffe.MustGenSpiffeURI(cfg.Namespace, we.ServiceAccount) 432 } 433 networkID := s.workloadEntryNetwork(we) 434 locality := we.Locality 435 if locality == "" && len(we.Labels[model.LocalityLabel]) > 0 { 436 locality = model.GetLocalityLabel(we.Labels[model.LocalityLabel]) 437 } 438 labels := labelutil.AugmentLabels(we.Labels, clusterID, locality, "", networkID) 439 return &model.WorkloadInstance{ 440 Endpoint: &model.IstioEndpoint{ 441 Address: addr, 442 // Not setting ports here as its done by k8s controller 443 Network: network.ID(we.Network), 444 Locality: model.Locality{ 445 Label: locality, 446 ClusterID: clusterID, 447 }, 448 LbWeight: we.Weight, 449 Namespace: cfg.Namespace, 450 // Workload entry config name is used as workload name, which will appear in metric label. 451 // After VM auto registry is introduced, workload group annotation should be used for workload name. 452 WorkloadName: cfg.Name, 453 Labels: labels, 454 TLSMode: tlsMode, 455 ServiceAccount: sa, 456 }, 457 PortMap: we.Ports, 458 Namespace: cfg.Namespace, 459 Name: cfg.Name, 460 Kind: model.WorkloadEntryKind, 461 DNSServiceEntryOnly: dnsServiceEntryOnly, 462 } 463 }