istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/serviceregistry/kube/controller/serviceimportcache.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 controller 16 17 import ( 18 "sort" 19 "strings" 20 21 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 23 klabels "k8s.io/apimachinery/pkg/labels" 24 "k8s.io/apimachinery/pkg/types" 25 26 "istio.io/istio/pilot/pkg/features" 27 "istio.io/istio/pilot/pkg/model" 28 "istio.io/istio/pilot/pkg/serviceregistry/kube" 29 "istio.io/istio/pkg/cluster" 30 "istio.io/istio/pkg/config" 31 "istio.io/istio/pkg/config/constants" 32 "istio.io/istio/pkg/config/host" 33 "istio.io/istio/pkg/config/schema/kind" 34 "istio.io/istio/pkg/kube/controllers" 35 "istio.io/istio/pkg/kube/kclient" 36 "istio.io/istio/pkg/kube/kubetypes" 37 "istio.io/istio/pkg/kube/mcs" 38 "istio.io/istio/pkg/slices" 39 netutil "istio.io/istio/pkg/util/net" 40 "istio.io/istio/pkg/util/sets" 41 ) 42 43 const ( 44 mcsDomainSuffix = "." + constants.DefaultClusterSetLocalDomain 45 ) 46 47 type importedService struct { 48 namespacedName types.NamespacedName 49 clusterSetVIP string 50 } 51 52 // serviceImportCache reads and processes Kubernetes Multi-Cluster Services (MCS) ServiceImport 53 // resources. 54 // 55 // An MCS controller is responsible for reading ServiceExport resources in one cluster and generating 56 // ServiceImport in all clusters of the ClusterSet (i.e. mesh). While the serviceExportCache reads 57 // ServiceExport to control the discoverability policy for individual endpoints, this controller 58 // reads ServiceImport in the cluster in order to extract the ClusterSet VIP and generate a 59 // synthetic service for the MCS host (i.e. clusterset.local). The aggregate.Controller will then 60 // merge together the MCS services from all the clusters, filling out the full map of Cluster IPs. 61 // 62 // The synthetic MCS service is a copy of the real k8s Service (e.g. cluster.local) with the same 63 // namespaced name, but with the hostname and VIPs changed to the appropriate ClusterSet values. 64 // The real k8s Service can live anywhere in the mesh and does not have to reside in the same 65 // cluster as the ServiceImport. 66 type serviceImportCache interface { 67 Run(stop <-chan struct{}) 68 HasSynced() bool 69 ImportedServices() []importedService 70 } 71 72 // newServiceImportCache creates a new cache of ServiceImport resources in the cluster. 73 func newServiceImportCache(c *Controller) serviceImportCache { 74 if features.EnableMCSHost { 75 sic := &serviceImportCacheImpl{ 76 Controller: c, 77 } 78 79 sic.serviceImports = kclient.NewDelayedInformer[controllers.Object](sic.client, mcs.ServiceImportGVR, kubetypes.DynamicInformer, kclient.Filter{ 80 ObjectFilter: sic.client.ObjectFilter(), 81 }) 82 // Register callbacks for events. 83 registerHandlers(sic.Controller, sic.serviceImports, "ServiceImports", sic.onServiceImportEvent, nil) 84 sic.opts.MeshServiceController.AppendServiceHandlerForCluster(sic.Cluster(), sic.onServiceEvent) 85 86 return sic 87 } 88 89 // MCS Service discovery is disabled. Use a placeholder cache. 90 return disabledServiceImportCache{} 91 } 92 93 // serviceImportCacheImpl reads ServiceImport resources for a single cluster. 94 type serviceImportCacheImpl struct { 95 *Controller 96 97 serviceImports kclient.Untyped 98 } 99 100 // onServiceEvent is called when the controller receives an event for the kube Service (i.e. cluster.local). 101 // When this happens, we need to update the state of the associated synthetic MCS service. 102 func (ic *serviceImportCacheImpl) onServiceEvent(_, curr *model.Service, event model.Event) { 103 if strings.HasSuffix(curr.Hostname.String(), mcsDomainSuffix) { 104 // Ignore events for MCS services that were triggered by this controller. 105 return 106 } 107 108 // This method is called concurrently from each cluster's queue. Process it in `this` cluster's queue 109 // in order to synchronize event processing. 110 ic.queue.Push(func() error { 111 namespacedName := namespacedNameForService(curr) 112 113 // Lookup the previous MCS service if there was one. 114 mcsHost := serviceClusterSetLocalHostname(namespacedName) 115 prevMcsService := ic.GetService(mcsHost) 116 117 // Get the ClusterSet VIPs for this service in this cluster. Will only be populated if the 118 // service has a ServiceImport in this cluster. 119 vips := ic.getClusterSetIPs(namespacedName) 120 name := namespacedName.Name 121 ns := namespacedName.Namespace 122 123 if len(vips) == 0 || (event == model.EventDelete && 124 ic.opts.MeshServiceController.GetService(kube.ServiceHostname(name, ns, ic.opts.DomainSuffix)) == nil) { 125 if prevMcsService != nil { 126 // There are no vips in this cluster. Just delete the MCS service now. 127 ic.deleteService(prevMcsService) 128 } 129 return nil 130 } 131 132 if prevMcsService != nil { 133 event = model.EventUpdate 134 } else { 135 event = model.EventAdd 136 } 137 138 mcsService := ic.genMCSService(curr, mcsHost, vips) 139 ic.addOrUpdateService(nil, nil, mcsService, event, false) 140 return nil 141 }) 142 } 143 144 func (ic *serviceImportCacheImpl) onServiceImportEvent(_, obj controllers.Object, event model.Event) error { 145 si := controllers.Extract[*unstructured.Unstructured](obj) 146 if si == nil { 147 return nil 148 } 149 150 // We need a full push if the cluster VIP changes. 151 needsFullPush := false 152 153 // Get the updated MCS service. 154 mcsHost := serviceClusterSetLocalHostnameForKR(si) 155 mcsService := ic.GetService(mcsHost) 156 157 ips := GetServiceImportIPs(si) 158 if mcsService == nil { 159 if event == model.EventDelete || len(ips) == 0 { 160 // We never created the service. Nothing to delete. 161 return nil 162 } 163 164 // The service didn't exist prior. Treat it as an add. 165 event = model.EventAdd 166 167 // Create the MCS service, based on the cluster.local service. We get the merged, mesh-wide service 168 // from the aggregate controller so that we don't rely on the service existing in this cluster. 169 realService := ic.opts.MeshServiceController.GetService(kube.ServiceHostnameForKR(si, ic.opts.DomainSuffix)) 170 if realService == nil { 171 log.Warnf("failed processing %s event for ServiceImport %s/%s in cluster %s. No matching service found in cluster", 172 event, si.GetNamespace(), si.GetName(), ic.Cluster()) 173 return nil 174 } 175 176 // Create the MCS service from the cluster.local service. 177 mcsService = ic.genMCSService(realService, mcsHost, ips) 178 } else { 179 if event == model.EventDelete || len(ips) == 0 { 180 ic.deleteService(mcsService) 181 return nil 182 } 183 184 // The service already existed. Treat it as an update. 185 event = model.EventUpdate 186 mcsService = mcsService.DeepCopy() 187 if ic.updateIPs(mcsService, ips) { 188 needsFullPush = true 189 } 190 } 191 192 // Always force a rebuild of the endpoint cache in case this import caused 193 // a change to the discoverability policy. 194 ic.addOrUpdateService(nil, nil, mcsService, event, true) 195 196 // TODO: do we really need a full push, we should do it in `addOrUpdateService`. 197 if needsFullPush { 198 ic.doFullPush(mcsHost, si.GetNamespace()) 199 } 200 201 return nil 202 } 203 204 func (ic *serviceImportCacheImpl) updateIPs(mcsService *model.Service, ips []string) (updated bool) { 205 prevIPs := mcsService.ClusterVIPs.GetAddressesFor(ic.Cluster()) 206 if !slices.Equal(prevIPs, ips) { 207 // Update the VIPs 208 mcsService.ClusterVIPs.SetAddressesFor(ic.Cluster(), ips) 209 updated = true 210 } 211 return 212 } 213 214 func (ic *serviceImportCacheImpl) doFullPush(mcsHost host.Name, ns string) { 215 pushReq := &model.PushRequest{ 216 Full: true, 217 ConfigsUpdated: sets.New(model.ConfigKey{Kind: kind.ServiceEntry, Name: mcsHost.String(), Namespace: ns}), 218 219 Reason: model.NewReasonStats(model.ServiceUpdate), 220 } 221 ic.opts.XDSUpdater.ConfigUpdate(pushReq) 222 } 223 224 // GetServiceImportIPs returns the list of ClusterSet IPs for the ServiceImport. 225 // Exported for testing only. 226 func GetServiceImportIPs(si *unstructured.Unstructured) []string { 227 var ips []string 228 if spec, ok := si.Object["spec"].(map[string]any); ok { 229 if rawIPs, ok := spec["ips"].([]any); ok { 230 for _, rawIP := range rawIPs { 231 ip := rawIP.(string) 232 if netutil.IsValidIPAddress(ip) { 233 ips = append(ips, ip) 234 } 235 } 236 } 237 } 238 sort.Strings(ips) 239 return ips 240 } 241 242 // genMCSService generates an MCS service based on the given real k8s service. The list of vips must be non-empty. 243 func (ic *serviceImportCacheImpl) genMCSService(realService *model.Service, mcsHost host.Name, vips []string) *model.Service { 244 mcsService := realService.DeepCopy() 245 mcsService.Hostname = mcsHost 246 mcsService.DefaultAddress = vips[0] 247 mcsService.ClusterVIPs.Addresses = map[cluster.ID][]string{ 248 ic.Cluster(): vips, 249 } 250 251 return mcsService 252 } 253 254 func (ic *serviceImportCacheImpl) getClusterSetIPs(name types.NamespacedName) []string { 255 si := ic.serviceImports.Get(name.Name, name.Namespace) 256 if si != nil { 257 return GetServiceImportIPs(si.(*unstructured.Unstructured)) 258 } 259 return nil 260 } 261 262 func (ic *serviceImportCacheImpl) ImportedServices() []importedService { 263 sis := ic.serviceImports.List(metav1.NamespaceAll, klabels.Everything()) 264 265 // Iterate over the ServiceImport resources in this cluster. 266 out := make([]importedService, 0, len(sis)) 267 268 ic.RLock() 269 for _, si := range sis { 270 usi := si.(*unstructured.Unstructured) 271 info := importedService{ 272 namespacedName: config.NamespacedName(usi), 273 } 274 275 // Lookup the synthetic MCS service. 276 hostName := serviceClusterSetLocalHostnameForKR(usi) 277 svc := ic.servicesMap[hostName] 278 if svc != nil { 279 if vips := svc.ClusterVIPs.GetAddressesFor(ic.Cluster()); len(vips) > 0 { 280 info.clusterSetVIP = vips[0] 281 } 282 } 283 284 out = append(out, info) 285 } 286 ic.RUnlock() 287 288 return out 289 } 290 291 func (ic *serviceImportCacheImpl) Run(stop <-chan struct{}) { 292 } 293 294 func (ic *serviceImportCacheImpl) HasSynced() bool { 295 return ic.serviceImports.HasSynced() 296 } 297 298 type disabledServiceImportCache struct{} 299 300 var _ serviceImportCache = disabledServiceImportCache{} 301 302 func (c disabledServiceImportCache) Run(stop <-chan struct{}) {} 303 304 func (c disabledServiceImportCache) HasSynced() bool { 305 return true 306 } 307 308 func (c disabledServiceImportCache) ImportedServices() []importedService { 309 // MCS is disabled - returning `nil`, which is semantically different here than an empty list. 310 return nil 311 } 312 313 func (c disabledServiceImportCache) HasCRDInstalled() bool { 314 return false 315 }