istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/serviceregistry/kube/controller/ambient/ambientindex.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 ambient
    16  
    17  import (
    18  	"net/netip"
    19  	"strings"
    20  
    21  	v1 "k8s.io/api/core/v1"
    22  	"sigs.k8s.io/gateway-api/apis/v1beta1"
    23  
    24  	networkingclient "istio.io/client-go/pkg/apis/networking/v1alpha3"
    25  	securityclient "istio.io/client-go/pkg/apis/security/v1beta1"
    26  	"istio.io/istio/pilot/pkg/model"
    27  	"istio.io/istio/pkg/cluster"
    28  	"istio.io/istio/pkg/config/constants"
    29  	"istio.io/istio/pkg/config/labels"
    30  	"istio.io/istio/pkg/config/schema/gvr"
    31  	"istio.io/istio/pkg/config/schema/kind"
    32  	kubeclient "istio.io/istio/pkg/kube"
    33  	"istio.io/istio/pkg/kube/kclient"
    34  	"istio.io/istio/pkg/kube/krt"
    35  	"istio.io/istio/pkg/kube/kubetypes"
    36  	"istio.io/istio/pkg/log"
    37  	"istio.io/istio/pkg/maps"
    38  	"istio.io/istio/pkg/network"
    39  	"istio.io/istio/pkg/slices"
    40  	"istio.io/istio/pkg/util/sets"
    41  	"istio.io/istio/pkg/workloadapi"
    42  )
    43  
    44  type Index interface {
    45  	Lookup(key string) []model.AddressInfo
    46  	All() []model.AddressInfo
    47  	WorkloadsForWaypoint(key model.WaypointKey) []model.WorkloadInfo
    48  	ServicesForWaypoint(key model.WaypointKey) []model.ServiceInfo
    49  	SyncAll()
    50  	HasSynced() bool
    51  	model.AmbientIndexes
    52  }
    53  
    54  var _ Index = &index{}
    55  
    56  type workloadsCollection struct {
    57  	krt.Collection[model.WorkloadInfo]
    58  	ByAddress        *krt.Index[model.WorkloadInfo, networkAddress]
    59  	ByServiceKey     *krt.Index[model.WorkloadInfo, string]
    60  	ByOwningWaypoint *krt.Index[model.WorkloadInfo, networkAddress]
    61  }
    62  
    63  type waypointsCollection struct {
    64  	krt.Collection[Waypoint]
    65  }
    66  
    67  type servicesCollection struct {
    68  	krt.Collection[model.ServiceInfo]
    69  	ByAddress        *krt.Index[model.ServiceInfo, networkAddress]
    70  	ByOwningWaypoint *krt.Index[model.ServiceInfo, networkAddress]
    71  }
    72  
    73  // index maintains an index of ambient WorkloadInfo objects by various keys.
    74  // These are intentionally pre-computed based on events such that lookups are efficient.
    75  type index struct {
    76  	services  servicesCollection
    77  	workloads workloadsCollection
    78  	waypoints waypointsCollection
    79  
    80  	authorizationPolicies krt.Collection[model.WorkloadAuthorization]
    81  	networkUpdateTrigger  *krt.RecomputeTrigger
    82  
    83  	SystemNamespace string
    84  	DomainSuffix    string
    85  	ClusterID       cluster.ID
    86  	XDSUpdater      model.XDSUpdater
    87  	Network         LookupNetwork
    88  }
    89  
    90  type Options struct {
    91  	Client kubeclient.Client
    92  
    93  	Revision        string
    94  	SystemNamespace string
    95  	DomainSuffix    string
    96  	ClusterID       cluster.ID
    97  	XDSUpdater      model.XDSUpdater
    98  	LookupNetwork   LookupNetwork
    99  }
   100  
   101  func New(options Options) Index {
   102  	a := &index{
   103  		networkUpdateTrigger: krt.NewRecomputeTrigger(),
   104  
   105  		SystemNamespace: options.SystemNamespace,
   106  		DomainSuffix:    options.DomainSuffix,
   107  		ClusterID:       options.ClusterID,
   108  		XDSUpdater:      options.XDSUpdater,
   109  		Network:         options.LookupNetwork,
   110  	}
   111  
   112  	filter := kclient.Filter{
   113  		ObjectFilter: options.Client.ObjectFilter(),
   114  	}
   115  	ConfigMaps := krt.NewInformerFiltered[*v1.ConfigMap](options.Client, filter, krt.WithName("ConfigMaps"))
   116  
   117  	authzPolicies := kclient.NewDelayedInformer[*securityclient.AuthorizationPolicy](options.Client,
   118  		gvr.AuthorizationPolicy, kubetypes.StandardInformer, filter)
   119  	AuthzPolicies := krt.WrapClient[*securityclient.AuthorizationPolicy](authzPolicies, krt.WithName("AuthorizationPolicies"))
   120  
   121  	peerAuths := kclient.NewDelayedInformer[*securityclient.PeerAuthentication](options.Client,
   122  		gvr.PeerAuthentication, kubetypes.StandardInformer, filter)
   123  	PeerAuths := krt.WrapClient[*securityclient.PeerAuthentication](peerAuths, krt.WithName("PeerAuthentications"))
   124  
   125  	serviceEntries := kclient.NewDelayedInformer[*networkingclient.ServiceEntry](options.Client,
   126  		gvr.ServiceEntry, kubetypes.StandardInformer, filter)
   127  	ServiceEntries := krt.WrapClient[*networkingclient.ServiceEntry](serviceEntries, krt.WithName("ServiceEntries"))
   128  
   129  	workloadEntries := kclient.NewDelayedInformer[*networkingclient.WorkloadEntry](options.Client,
   130  		gvr.WorkloadEntry, kubetypes.StandardInformer, filter)
   131  	WorkloadEntries := krt.WrapClient[*networkingclient.WorkloadEntry](workloadEntries, krt.WithName("WorkloadEntries"))
   132  
   133  	gatewayClient := kclient.NewDelayedInformer[*v1beta1.Gateway](options.Client, gvr.KubernetesGateway, kubetypes.StandardInformer, filter)
   134  	Gateways := krt.WrapClient[*v1beta1.Gateway](gatewayClient, krt.WithName("Gateways"))
   135  
   136  	gatewayClassClient := kclient.NewDelayedInformer[*v1beta1.GatewayClass](options.Client, gvr.GatewayClass, kubetypes.StandardInformer, filter)
   137  	GatewayClasses := krt.WrapClient[*v1beta1.GatewayClass](gatewayClassClient, krt.WithName("GatewayClasses"))
   138  
   139  	Services := krt.NewInformerFiltered[*v1.Service](options.Client, filter, krt.WithName("Services"))
   140  	Nodes := krt.NewInformerFiltered[*v1.Node](options.Client, kclient.Filter{
   141  		ObjectFilter:    options.Client.ObjectFilter(),
   142  		ObjectTransform: kubeclient.StripNodeUnusedFields,
   143  	}, krt.WithName("Nodes"))
   144  	Pods := krt.NewInformerFiltered[*v1.Pod](options.Client, kclient.Filter{
   145  		ObjectFilter:    options.Client.ObjectFilter(),
   146  		ObjectTransform: kubeclient.StripPodUnusedFields,
   147  	}, krt.WithName("Pods"))
   148  
   149  	// TODO: Should this go ahead and transform the full ns into some intermediary with just the details we care about?
   150  	Namespaces := krt.NewInformer[*v1.Namespace](options.Client, krt.WithName("Namespaces"))
   151  
   152  	MeshConfig := MeshConfigCollection(ConfigMaps, options)
   153  	Waypoints := WaypointsCollection(Gateways, GatewayClasses, Pods)
   154  
   155  	// AllPolicies includes peer-authentication converted policies
   156  	AuthorizationPolicies, AllPolicies := PolicyCollections(AuthzPolicies, PeerAuths, MeshConfig, Waypoints, Pods)
   157  	AllPolicies.RegisterBatch(PushXds(a.XDSUpdater, func(i model.WorkloadAuthorization) model.ConfigKey {
   158  		return model.ConfigKey{Kind: kind.AuthorizationPolicy, Name: i.Authorization.Name, Namespace: i.Authorization.Namespace}
   159  	}), false)
   160  
   161  	// these are workloadapi-style services combined from kube services and service entries
   162  	WorkloadServices := a.ServicesCollection(Services, ServiceEntries, Waypoints, Namespaces)
   163  	ServiceAddressIndex := krt.NewIndex[model.ServiceInfo, networkAddress](WorkloadServices, networkAddressFromService)
   164  	ServiceInfosByOwningWaypoint := krt.NewIndex[model.ServiceInfo, networkAddress](WorkloadServices, func(s model.ServiceInfo) []networkAddress {
   165  		// Filter out waypoint services
   166  		if s.Labels[constants.ManagedGatewayLabel] == constants.ManagedGatewayMeshControllerLabel {
   167  			return nil
   168  		}
   169  		waypoint := s.Service.Waypoint
   170  		if waypoint == nil {
   171  			return nil
   172  		}
   173  		waypointAddress := waypoint.GetAddress()
   174  		if waypointAddress == nil {
   175  			return nil
   176  		}
   177  
   178  		ip := waypointAddress.GetAddress()
   179  		netip, _ := netip.AddrFromSlice(ip)
   180  		netaddr := networkAddress{
   181  			network: waypointAddress.GetNetwork(),
   182  			ip:      netip.String(),
   183  		}
   184  		return append(make([]networkAddress, 1), netaddr)
   185  	})
   186  	WorkloadServices.RegisterBatch(krt.BatchedEventFilter(
   187  		func(a model.ServiceInfo) *workloadapi.Service {
   188  			// Only trigger push if the XDS object changed; the rest is just for computation of others
   189  			return a.Service
   190  		},
   191  		PushXds(a.XDSUpdater, func(i model.ServiceInfo) model.ConfigKey {
   192  			return model.ConfigKey{Kind: kind.Address, Name: i.ResourceName()}
   193  		})), false)
   194  
   195  	Workloads := a.WorkloadsCollection(
   196  		Pods,
   197  		Nodes,
   198  		MeshConfig,
   199  		AuthorizationPolicies,
   200  		PeerAuths,
   201  		Waypoints,
   202  		WorkloadServices,
   203  		WorkloadEntries,
   204  		ServiceEntries,
   205  		AllPolicies,
   206  		Namespaces,
   207  	)
   208  	WorkloadAddressIndex := krt.NewIndex[model.WorkloadInfo, networkAddress](Workloads, networkAddressFromWorkload)
   209  	WorkloadServiceIndex := krt.NewIndex[model.WorkloadInfo, string](Workloads, func(o model.WorkloadInfo) []string {
   210  		return maps.Keys(o.Services)
   211  	})
   212  	WorkloadWaypointIndex := krt.NewIndex[model.WorkloadInfo, networkAddress](Workloads, func(w model.WorkloadInfo) []networkAddress {
   213  		// Filter out waypoints.
   214  		if w.Labels[constants.ManagedGatewayLabel] == constants.ManagedGatewayMeshControllerLabel {
   215  			return nil
   216  		}
   217  		waypoint := w.Waypoint
   218  		if waypoint == nil {
   219  			return nil
   220  		}
   221  		waypointAddress := waypoint.GetAddress()
   222  		if waypointAddress == nil {
   223  			return nil
   224  		}
   225  
   226  		ip := waypointAddress.GetAddress()
   227  		netip, _ := netip.AddrFromSlice(ip)
   228  		netaddr := networkAddress{
   229  			network: waypointAddress.GetNetwork(),
   230  			ip:      netip.String(),
   231  		}
   232  		return append(make([]networkAddress, 1), netaddr)
   233  	})
   234  	// Subtle: make sure we register the event after the Index are created. This ensures when we get the event, the index is populated.
   235  	Workloads.RegisterBatch(krt.BatchedEventFilter(
   236  		func(a model.WorkloadInfo) *workloadapi.Workload {
   237  			// Only trigger push if the XDS object changed; the rest is just for computation of others
   238  			return a.Workload
   239  		},
   240  		PushXds(a.XDSUpdater, func(i model.WorkloadInfo) model.ConfigKey {
   241  			return model.ConfigKey{Kind: kind.Address, Name: i.ResourceName()}
   242  		})), false)
   243  
   244  	a.workloads = workloadsCollection{
   245  		Collection:       Workloads,
   246  		ByAddress:        WorkloadAddressIndex,
   247  		ByServiceKey:     WorkloadServiceIndex,
   248  		ByOwningWaypoint: WorkloadWaypointIndex,
   249  	}
   250  	a.services = servicesCollection{
   251  		Collection:       WorkloadServices,
   252  		ByAddress:        ServiceAddressIndex,
   253  		ByOwningWaypoint: ServiceInfosByOwningWaypoint,
   254  	}
   255  	a.waypoints = waypointsCollection{
   256  		Collection: Waypoints,
   257  	}
   258  	a.authorizationPolicies = AllPolicies
   259  
   260  	return a
   261  }
   262  
   263  // Lookup finds all addresses associated with a given key. Many different key formats are supported; see inline comments.
   264  func (a *index) Lookup(key string) []model.AddressInfo {
   265  	// 1. Workload UID
   266  	if w := a.workloads.GetKey(krt.Key[model.WorkloadInfo](key)); w != nil {
   267  		return []model.AddressInfo{workloadToAddressInfo(w.Workload)}
   268  	}
   269  
   270  	network, ip, found := strings.Cut(key, "/")
   271  	if !found {
   272  		log.Warnf(`key (%v) did not contain the expected "/" character`, key)
   273  		return nil
   274  	}
   275  	networkAddr := networkAddress{network: network, ip: ip}
   276  
   277  	// 2. Workload by IP
   278  	if wls := a.workloads.ByAddress.Lookup(networkAddr); len(wls) > 0 {
   279  		// If there is just one, return it
   280  		if len(wls) == 1 {
   281  			return []model.AddressInfo{modelWorkloadToAddressInfo(wls[0])}
   282  		}
   283  		// Otherwise, try to find a pod - pods have precedence
   284  		pod := slices.FindFunc(wls, func(info model.WorkloadInfo) bool {
   285  			return info.Source == kind.Pod
   286  		})
   287  		if pod != nil {
   288  			return []model.AddressInfo{modelWorkloadToAddressInfo(*pod)}
   289  		}
   290  		// Otherwise just return the first one; all WorkloadEntry have the same weight
   291  		return []model.AddressInfo{modelWorkloadToAddressInfo(wls[0])}
   292  	}
   293  
   294  	// 3. Service
   295  	if svc := a.lookupService(key); svc != nil {
   296  		res := []model.AddressInfo{serviceToAddressInfo(svc.Service)}
   297  		for _, w := range a.workloads.ByServiceKey.Lookup(svc.ResourceName()) {
   298  			res = append(res, workloadToAddressInfo(w.Workload))
   299  		}
   300  		return res
   301  	}
   302  	return nil
   303  }
   304  
   305  func (a *index) lookupService(key string) *model.ServiceInfo {
   306  	// 1. namespace/hostname format
   307  	s := a.services.GetKey(krt.Key[model.ServiceInfo](key))
   308  	if s != nil {
   309  		return s
   310  	}
   311  
   312  	// 2. network/ip format
   313  	network, ip, _ := strings.Cut(key, "/")
   314  	services := a.services.ByAddress.Lookup(networkAddress{
   315  		network: network,
   316  		ip:      ip,
   317  	})
   318  	return slices.First(services)
   319  }
   320  
   321  // All return all known workloads. Result is un-ordered
   322  func (a *index) All() []model.AddressInfo {
   323  	res := []model.AddressInfo{}
   324  	type kindindex struct {
   325  		k     kind.Kind
   326  		index int
   327  	}
   328  	addrm := map[netip.Addr]kindindex{}
   329  	for _, wl := range a.workloads.List() {
   330  		overwrite := -1
   331  		write := true
   332  		for _, addr := range wl.Addresses {
   333  			a := byteIPToAddr(addr)
   334  			if existing, f := addrm[a]; f {
   335  				// This address was already found. We want unique addresses in the result.
   336  				// Pod > WorkloadEntry
   337  				if wl.Source == kind.Pod && existing.k != kind.Pod {
   338  					overwrite = existing.index
   339  					addrm[a] = kindindex{
   340  						k:     wl.Source,
   341  						index: overwrite,
   342  					}
   343  				} else {
   344  					write = false
   345  				}
   346  			}
   347  		}
   348  		if overwrite >= 0 {
   349  			res[overwrite] = workloadToAddressInfo(wl.Workload)
   350  		} else if write {
   351  			res = append(res, workloadToAddressInfo(wl.Workload))
   352  			for _, addr := range wl.Addresses {
   353  				a := byteIPToAddr(addr)
   354  				addrm[a] = kindindex{
   355  					k:     wl.Source,
   356  					index: overwrite,
   357  				}
   358  			}
   359  		}
   360  	}
   361  	for _, s := range a.services.List() {
   362  		res = append(res, serviceToAddressInfo(s.Service))
   363  	}
   364  	return res
   365  }
   366  
   367  // AddressInformation returns all AddressInfo's in the cluster.
   368  // This may be scoped to specific subsets by specifying a non-empty addresses field
   369  func (a *index) AddressInformation(addresses sets.String) ([]model.AddressInfo, sets.String) {
   370  	if len(addresses) == 0 {
   371  		// Full update
   372  		return a.All(), nil
   373  	}
   374  	var res []model.AddressInfo
   375  	var removed []string
   376  	got := sets.New[string]()
   377  	for wname := range addresses {
   378  		wl := a.Lookup(wname)
   379  		if len(wl) == 0 {
   380  			removed = append(removed, wname)
   381  		} else {
   382  			for _, addr := range wl {
   383  				if !got.InsertContains(addr.ResourceName()) {
   384  					res = append(res, addr)
   385  				}
   386  			}
   387  		}
   388  	}
   389  	return res, sets.New(removed...)
   390  }
   391  
   392  func (a *index) ServicesForWaypoint(key model.WaypointKey) []model.ServiceInfo {
   393  	var out []model.ServiceInfo
   394  	for _, addr := range key.Addresses {
   395  		out = append(out, a.services.ByOwningWaypoint.Lookup(networkAddress{
   396  			network: key.Network,
   397  			ip:      addr,
   398  		})...)
   399  	}
   400  	return out
   401  }
   402  
   403  func (a *index) WorkloadsForWaypoint(key model.WaypointKey) []model.WorkloadInfo {
   404  	// TODO: we should be able to handle multiple IPs or a hostname
   405  	if len(key.Addresses) == 0 {
   406  		return nil
   407  	}
   408  	workloads := a.workloads.ByOwningWaypoint.Lookup(networkAddress{
   409  		network: key.Network,
   410  		ip:      key.Addresses[0],
   411  	})
   412  	workloads = model.SortWorkloadsByCreationTime(workloads)
   413  	return workloads
   414  }
   415  
   416  func (a *index) AdditionalPodSubscriptions(
   417  	proxy *model.Proxy,
   418  	allAddresses sets.String,
   419  	currentSubs sets.String,
   420  ) sets.String {
   421  	shouldSubscribe := sets.New[string]()
   422  
   423  	// First, we want to handle VIP subscriptions. Example:
   424  	// Client subscribes to VIP1. Pod1, part of VIP1, is sent.
   425  	// The client wouldn't be explicitly subscribed to Pod1, so it would normally ignore it.
   426  	// Since it is a part of VIP1 which we are subscribe to, add it to the subscriptions
   427  	for addr := range allAddresses {
   428  		for _, wl := range model.ExtractWorkloadsFromAddresses(a.Lookup(addr)) {
   429  			// We may have gotten an update for Pod, but are subscribed to a Service.
   430  			// We need to force a subscription on the Pod as well
   431  			for namespacedHostname := range wl.Services {
   432  				if currentSubs.Contains(namespacedHostname) {
   433  					shouldSubscribe.Insert(wl.ResourceName())
   434  					break
   435  				}
   436  			}
   437  		}
   438  	}
   439  
   440  	// Next, as an optimization, we will send all node-local endpoints
   441  	if nodeName := proxy.Metadata.NodeName; nodeName != "" {
   442  		for _, wl := range model.ExtractWorkloadsFromAddresses(a.All()) {
   443  			if wl.Node == nodeName {
   444  				n := wl.ResourceName()
   445  				if currentSubs.Contains(n) {
   446  					continue
   447  				}
   448  				shouldSubscribe.Insert(n)
   449  			}
   450  		}
   451  	}
   452  
   453  	return shouldSubscribe
   454  }
   455  
   456  func (a *index) SyncAll() {
   457  	a.networkUpdateTrigger.TriggerRecomputation()
   458  }
   459  
   460  func (a *index) HasSynced() bool {
   461  	return a.services.Synced().HasSynced() &&
   462  		a.workloads.Synced().HasSynced() &&
   463  		a.waypoints.Synced().HasSynced() &&
   464  		a.authorizationPolicies.Synced().HasSynced()
   465  }
   466  
   467  type LookupNetwork func(endpointIP string, labels labels.Instance) network.ID
   468  
   469  func PushXds[T any](xds model.XDSUpdater, f func(T) model.ConfigKey) func(events []krt.Event[T], initialSync bool) {
   470  	return func(events []krt.Event[T], initialSync bool) {
   471  		cu := sets.New[model.ConfigKey]()
   472  		for _, e := range events {
   473  			for _, i := range e.Items() {
   474  				cu.Insert(f(i))
   475  			}
   476  		}
   477  		xds.ConfigUpdate(&model.PushRequest{
   478  			Full:           false,
   479  			ConfigsUpdated: cu,
   480  			Reason:         model.NewReasonStats(model.AmbientUpdate),
   481  		})
   482  	}
   483  }