istio
468 строк · 16.9 Кб
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
15package serviceentry16
17import (18"net/netip"19"strings"20"time"21
22"istio.io/api/label"23networking "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"27labelutil "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"38netutil "istio.io/istio/pkg/util/net"39"istio.io/istio/pkg/util/sets"40)
41
42func convertPort(port *networking.ServicePort) *model.Port {43return &model.Port{44Name: port.Name,45Port: int(port.Number),46Protocol: protocol.Parse(port.Protocol),47}48}
49
50type HostAddress struct {51host string52address string53}
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.
60func ServiceToServiceEntry(svc *model.Service, proxy *model.Proxy) *config.Config {61gvk := gvk.ServiceEntry62se := &networking.ServiceEntry{63// Host is fully qualified: name, namespace, domainSuffix64Hosts: []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//70Addresses: svc.GetAddresses(proxy),71
72// Location: 0,73
74// Internal resolution:75// - Passthrough - for ClusterIP=None and no ExternalName76// - ClientSideLB - regular ClusterIP clusters (VIP, resolved via EDS)77// - DNSLB - if ExternalName is specified. Also meshExternal is set.78
79// This is based on alpha.istio.io/canonical-serviceaccounts and80// alpha.istio.io/kubernetes-serviceaccounts.81SubjectAltNames: svc.ServiceAccounts,82}83
84if len(svc.Attributes.LabelSelectors) > 0 {85se.WorkloadSelector = &networking.WorkloadSelector{Labels: svc.Attributes.LabelSelectors}86}87
88// Based on networking.istio.io/exportTo annotation89for k := range svc.Attributes.ExportTo {90// k is Private or Public91se.ExportTo = append(se.ExportTo, string(k))92}93
94if svc.MeshExternal {95se.Location = networking.ServiceEntry_MESH_EXTERNAL // 0 - default96} else {97se.Location = networking.ServiceEntry_MESH_INTERNAL98}99
100// Reverse in convertServices. Note that enum values are different101// TODO: make the enum match, should be safe (as long as they're used as enum)102var resolution networking.ServiceEntry_Resolution103switch svc.Resolution {104case model.Passthrough: // 2105resolution = networking.ServiceEntry_NONE // 0106case model.DNSLB: // 1107resolution = networking.ServiceEntry_DNS // 2108case model.DNSRoundRobinLB: // 3109resolution = networking.ServiceEntry_DNS_ROUND_ROBIN // 3110case model.ClientSideLB: // 0111resolution = networking.ServiceEntry_STATIC // 1112}113se.Resolution = resolution114
115// Port is mapped from ServicePort116for _, p := range svc.Ports {117se.Ports = append(se.Ports, &networking.ServicePort{118Number: uint32(p.Port),119Name: p.Name,120// Protocol is converted to protocol.Instance - reverse conversion will use the name.121Protocol: string(p.Protocol),122// TODO: target port123})124}125
126cfg := &config.Config{127Meta: config.Meta{128GroupVersionKind: gvk,129Name: "synthetic-" + svc.Attributes.Name,130Namespace: svc.Attributes.Namespace,131CreationTimestamp: svc.CreationTime,132ResourceVersion: svc.ResourceVersion,133},134Spec: se,135}136
137// TODO: WorkloadSelector138
139// TODO: preserve ServiceRegistry. The reverse conversion sets it to 'external'140// TODO: preserve UID ? It seems MCP didn't preserve it - but that code path was not used much.141
142// TODO: ClusterExternalPorts map - for NodePort services, with "traffic.istio.io/nodeSelector" ann143// It's a per-cluster map144
145// TODO: ClusterExternalAddresses - for LB types, per cluster. Populated from K8S, missing146// in SE. Used for multi-network support.147return cfg148}
149
150// convertServices transforms a ServiceEntry config to a list of internal Service objects.
151func convertServices(cfg config.Config) []*model.Service {152serviceEntry := cfg.Spec.(*networking.ServiceEntry)153creationTime := cfg.CreationTimestamp154
155var resolution model.Resolution156switch serviceEntry.Resolution {157case networking.ServiceEntry_NONE:158resolution = model.Passthrough159case networking.ServiceEntry_DNS:160resolution = model.DNSLB161case networking.ServiceEntry_DNS_ROUND_ROBIN:162resolution = model.DNSRoundRobinLB163case networking.ServiceEntry_STATIC:164resolution = model.ClientSideLB165}166
167svcPorts := make(model.PortList, 0, len(serviceEntry.Ports))168var portOverrides map[uint32]uint32169for _, port := range serviceEntry.Ports {170svcPorts = append(svcPorts, convertPort(port))171if resolution == model.Passthrough && port.TargetPort != 0 {172if portOverrides == nil {173portOverrides = map[uint32]uint32{}174}175portOverrides[port.Number] = port.TargetPort176}177}178
179var exportTo sets.Set[visibility.Instance]180if len(serviceEntry.ExportTo) > 0 {181exportTo = sets.NewWithLength[visibility.Instance](len(serviceEntry.ExportTo))182for _, e := range serviceEntry.ExportTo {183exportTo.Insert(visibility.Instance(e))184}185}186
187var labelSelectors map[string]string188if serviceEntry.WorkloadSelector != nil {189labelSelectors = serviceEntry.WorkloadSelector.Labels190}191hostAddresses := []*HostAddress{}192for _, hostname := range serviceEntry.Hosts {193if len(serviceEntry.Addresses) > 0 {194for _, address := range serviceEntry.Addresses {195// Check if address is an IP first because that is the most common case.196if netutil.IsValidIPAddress(address) {197hostAddresses = append(hostAddresses, &HostAddress{hostname, address})198} else if cidr, cidrErr := netip.ParsePrefix(address); cidrErr == nil {199newAddress := address200if cidr.Bits() == cidr.Addr().BitLen() {201// /32 mask. Remove the /32 and make it a normal IP address202newAddress = cidr.Addr().String()203}204hostAddresses = append(hostAddresses, &HostAddress{hostname, newAddress})205}206}207} else {208hostAddresses = append(hostAddresses, &HostAddress{hostname, constants.UnspecifiedIP})209}210}211
212return buildServices(hostAddresses, cfg.Name, cfg.Namespace, svcPorts, serviceEntry.Location, resolution,213exportTo, labelSelectors, serviceEntry.SubjectAltNames, creationTime, cfg.Labels, portOverrides)214}
215
216func buildServices(hostAddresses []*HostAddress, name, namespace string, ports model.PortList, location networking.ServiceEntry_Location,217resolution model.Resolution, exportTo sets.Set[visibility.Instance], selectors map[string]string, saccounts []string,218ctime time.Time, labels map[string]string, overrides map[uint32]uint32,219) []*model.Service {220out := make([]*model.Service, 0, len(hostAddresses))221lbls := labels222if features.CanonicalServiceForMeshExternalServiceEntry && location == networking.ServiceEntry_MESH_EXTERNAL {223lbls = ensureCanonicalServiceLabels(name, labels)224}225for _, ha := range hostAddresses {226out = append(out, &model.Service{227CreationTime: ctime,228MeshExternal: location == networking.ServiceEntry_MESH_EXTERNAL,229Hostname: host.Name(ha.host),230DefaultAddress: ha.address,231Ports: ports,232Resolution: resolution,233Attributes: model.ServiceAttributes{234ServiceRegistry: provider.External,235PassthroughTargetPorts: overrides,236Name: ha.host,237Namespace: namespace,238Labels: lbls,239ExportTo: exportTo,240LabelSelectors: selectors,241},242ServiceAccounts: saccounts,243})244}245return out246}
247
248func ensureCanonicalServiceLabels(name string, srcLabels map[string]string) map[string]string {249if srcLabels == nil {250srcLabels = make(map[string]string)251}252_, svcLabelFound := srcLabels[model.IstioCanonicalServiceLabelName]253_, revLabelFound := srcLabels[model.IstioCanonicalServiceRevisionLabelName]254if svcLabelFound && revLabelFound {255return srcLabels256}257
258srcLabels[model.IstioCanonicalServiceLabelName], srcLabels[model.IstioCanonicalServiceRevisionLabelName] = labels.CanonicalService(srcLabels, name)259return srcLabels260}
261
262func (s *Controller) convertEndpoint(service *model.Service, servicePort *networking.ServicePort,263wle *networking.WorkloadEntry, configKey *configKey, clusterID cluster.ID,264) *model.ServiceInstance {265var instancePort uint32266addr := wle.GetAddress()267// priority level: unixAddress > we.ports > se.port.targetPort > se.port.number268if strings.HasPrefix(addr, model.UnixAddressPrefix) {269instancePort = 0270addr = strings.TrimPrefix(addr, model.UnixAddressPrefix)271} else if port, ok := wle.Ports[servicePort.Name]; ok && port > 0 {272instancePort = port273} else if servicePort.TargetPort > 0 {274instancePort = servicePort.TargetPort275} else {276// final fallback is to the service port value277instancePort = servicePort.Number278}279
280tlsMode := getTLSModeFromWorkloadEntry(wle)281sa := ""282if wle.ServiceAccount != "" {283sa = spiffe.MustGenSpiffeURI(service.Attributes.Namespace, wle.ServiceAccount)284}285networkID := s.workloadEntryNetwork(wle)286locality := wle.Locality287if locality == "" && len(wle.Labels[model.LocalityLabel]) > 0 {288locality = model.GetLocalityLabel(wle.Labels[model.LocalityLabel])289}290labels := labelutil.AugmentLabels(wle.Labels, clusterID, locality, "", networkID)291return &model.ServiceInstance{292Endpoint: &model.IstioEndpoint{293Address: addr,294EndpointPort: instancePort,295ServicePortName: servicePort.Name,296Network: network.ID(wle.Network),297Locality: model.Locality{298Label: locality,299ClusterID: clusterID,300},301LbWeight: wle.Weight,302Labels: labels,303TLSMode: tlsMode,304ServiceAccount: sa,305// Workload entry config name is used as workload name, which will appear in metric label.306// After VM auto registry is introduced, workload group annotation should be used for workload name.307WorkloadName: configKey.name,308Namespace: configKey.namespace,309},310Service: service,311ServicePort: convertPort(servicePort),312}313}
314
315// convertWorkloadEntryToServiceInstances translates a WorkloadEntry into ServiceEndpoints. This logic is largely the
316// same as the ServiceEntry convertServiceEntryToInstances.
317func (s *Controller) convertWorkloadEntryToServiceInstances(wle *networking.WorkloadEntry, services []*model.Service,318se *networking.ServiceEntry, configKey *configKey, clusterID cluster.ID,319) []*model.ServiceInstance {320out := make([]*model.ServiceInstance, 0)321for _, service := range services {322for _, port := range se.Ports {323out = append(out, s.convertEndpoint(service, port, wle, configKey, clusterID))324}325}326return out327}
328
329func (s *Controller) convertServiceEntryToInstances(cfg config.Config, services []*model.Service) []*model.ServiceInstance {330out := make([]*model.ServiceInstance, 0)331serviceEntry := cfg.Spec.(*networking.ServiceEntry)332if serviceEntry == nil {333return nil334}335if services == nil {336services = convertServices(cfg)337}338for _, service := range services {339for _, serviceEntryPort := range serviceEntry.Ports {340if len(serviceEntry.Endpoints) == 0 && serviceEntry.WorkloadSelector == nil &&341(serviceEntry.Resolution == networking.ServiceEntry_DNS || serviceEntry.Resolution == networking.ServiceEntry_DNS_ROUND_ROBIN) {342// Note: only convert the hostname to service instance if WorkloadSelector is not set343// when service entry has discovery type DNS and no endpoints344// we create endpoints from service's host345// Do not use serviceentry.hosts as a service entry is converted into346// multiple services (one for each host)347endpointPort := serviceEntryPort.Number348if serviceEntryPort.TargetPort > 0 {349endpointPort = serviceEntryPort.TargetPort350}351out = append(out, &model.ServiceInstance{352Endpoint: &model.IstioEndpoint{353Address: string(service.Hostname),354EndpointPort: endpointPort,355ServicePortName: serviceEntryPort.Name,356Labels: nil,357TLSMode: model.DisabledTLSModeLabel,358},359Service: service,360ServicePort: convertPort(serviceEntryPort),361})362} else {363for _, endpoint := range serviceEntry.Endpoints {364out = append(out, s.convertEndpoint(service, serviceEntryPort, endpoint, &configKey{}, s.clusterID))365}366}367}368}369return out370}
371
372func getTLSModeFromWorkloadEntry(wle *networking.WorkloadEntry) string {373// * Use security.istio.io/tlsMode if its present374// * If not, set TLS mode if ServiceAccount is specified375tlsMode := model.DisabledTLSModeLabel376if val, exists := wle.Labels[label.SecurityTlsMode.Name]; exists {377tlsMode = val378} else if wle.ServiceAccount != "" {379tlsMode = model.IstioMutualTLSModeLabel380}381
382return tlsMode383}
384
385// The workload instance has pointer to the service and its service port.
386// We need to create our own but we can retain the endpoint already created.
387func convertWorkloadInstanceToServiceInstance(workloadInstance *model.WorkloadInstance, serviceEntryServices []*model.Service,388serviceEntry *networking.ServiceEntry,389) []*model.ServiceInstance {390out := make([]*model.ServiceInstance, 0)391for _, service := range serviceEntryServices {392for _, serviceEntryPort := range serviceEntry.Ports {393// note: this is same as workloadentry handler394// endpoint port will first use the port defined in wle with same port name,395// if not port name not match, use the targetPort specified in ServiceEntry396// if both not matched, fallback to ServiceEntry port number.397var targetPort uint32398if port, ok := workloadInstance.PortMap[serviceEntryPort.Name]; ok && port > 0 {399targetPort = port400} else if serviceEntryPort.TargetPort > 0 {401targetPort = serviceEntryPort.TargetPort402} else {403targetPort = serviceEntryPort.Number404}405ep := workloadInstance.Endpoint.ShallowCopy()406ep.ServicePortName = serviceEntryPort.Name407ep.EndpointPort = targetPort408ep.ComputeEnvoyEndpoint(nil)409out = append(out, &model.ServiceInstance{410Endpoint: ep,411Service: service,412ServicePort: convertPort(serviceEntryPort),413})414}415}416return out417}
418
419// Convenience function to convert a workloadEntry into a WorkloadInstance object encoding the endpoint (without service
420// port names) and the namespace - k8s will consume this workload instance when selecting workload entries
421func (s *Controller) convertWorkloadEntryToWorkloadInstance(cfg config.Config, clusterID cluster.ID) *model.WorkloadInstance {422we := ConvertWorkloadEntry(cfg)423addr := we.GetAddress()424dnsServiceEntryOnly := false425if strings.HasPrefix(addr, model.UnixAddressPrefix) {426// k8s can't use uds for service objects427dnsServiceEntryOnly = true428}429if addr != "" && !netutil.IsValidIPAddress(addr) {430// k8s can't use workloads with hostnames in the address field.431dnsServiceEntryOnly = true432}433tlsMode := getTLSModeFromWorkloadEntry(we)434sa := ""435if we.ServiceAccount != "" {436sa = spiffe.MustGenSpiffeURI(cfg.Namespace, we.ServiceAccount)437}438networkID := s.workloadEntryNetwork(we)439locality := we.Locality440if locality == "" && len(we.Labels[model.LocalityLabel]) > 0 {441locality = model.GetLocalityLabel(we.Labels[model.LocalityLabel])442}443labels := labelutil.AugmentLabels(we.Labels, clusterID, locality, "", networkID)444return &model.WorkloadInstance{445Endpoint: &model.IstioEndpoint{446Address: addr,447// Not setting ports here as its done by k8s controller448Network: network.ID(we.Network),449Locality: model.Locality{450Label: locality,451ClusterID: clusterID,452},453LbWeight: we.Weight,454Namespace: cfg.Namespace,455// Workload entry config name is used as workload name, which will appear in metric label.456// After VM auto registry is introduced, workload group annotation should be used for workload name.457WorkloadName: cfg.Name,458Labels: labels,459TLSMode: tlsMode,460ServiceAccount: sa,461},462PortMap: we.Ports,463Namespace: cfg.Namespace,464Name: cfg.Name,465Kind: model.WorkloadEntryKind,466DNSServiceEntryOnly: dnsServiceEntryOnly,467}468}
469