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  }