istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/xds/delta.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 xds 16 17 import ( 18 "errors" 19 "fmt" 20 "strconv" 21 "strings" 22 "time" 23 24 discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 25 "google.golang.org/grpc/codes" 26 "google.golang.org/grpc/peer" 27 "google.golang.org/grpc/status" 28 29 "istio.io/istio/pilot/pkg/features" 30 istiogrpc "istio.io/istio/pilot/pkg/grpc" 31 "istio.io/istio/pilot/pkg/model" 32 "istio.io/istio/pilot/pkg/networking/util" 33 v3 "istio.io/istio/pilot/pkg/xds/v3" 34 istiolog "istio.io/istio/pkg/log" 35 "istio.io/istio/pkg/slices" 36 "istio.io/istio/pkg/util/sets" 37 "istio.io/istio/pkg/xds" 38 ) 39 40 var deltaLog = istiolog.RegisterScope("delta", "delta xds debugging") 41 42 func (s *DiscoveryServer) StreamDeltas(stream DeltaDiscoveryStream) error { 43 if knativeEnv != "" && firstRequest.Load() { 44 // How scaling works in knative is the first request is the "loading" request. During 45 // loading request, concurrency=1. Once that request is done, concurrency is enabled. 46 // However, the XDS stream is long lived, so the first request would block all others. As a 47 // result, we should exit the first request immediately; clients will retry. 48 firstRequest.Store(false) 49 return status.Error(codes.Unavailable, "server warmup not complete; try again") 50 } 51 // Check if server is ready to accept clients and process new requests. 52 // Currently ready means caches have been synced and hence can build 53 // clusters correctly. Without this check, InitContext() call below would 54 // initialize with empty config, leading to reconnected Envoys loosing 55 // configuration. This is an additional safety check inaddition to adding 56 // cachesSynced logic to readiness probe to handle cases where kube-proxy 57 // ip tables update latencies. 58 // See https://github.com/istio/istio/issues/25495. 59 if !s.IsServerReady() { 60 return errors.New("server is not ready to serve discovery information") 61 } 62 63 ctx := stream.Context() 64 peerAddr := "0.0.0.0" 65 if peerInfo, ok := peer.FromContext(ctx); ok { 66 peerAddr = peerInfo.Addr.String() 67 } 68 69 if err := s.WaitForRequestLimit(stream.Context()); err != nil { 70 deltaLog.Warnf("ADS: %q exceeded rate limit: %v", peerAddr, err) 71 return status.Errorf(codes.ResourceExhausted, "request rate limit exceeded: %v", err) 72 } 73 74 ids, err := s.authenticate(ctx) 75 if err != nil { 76 return status.Error(codes.Unauthenticated, err.Error()) 77 } 78 if ids != nil { 79 deltaLog.Debugf("Authenticated XDS: %v with identity %v", peerAddr, ids) 80 } else { 81 deltaLog.Debugf("Unauthenticated XDS: %v", peerAddr) 82 } 83 84 // InitContext returns immediately if the context was already initialized. 85 if err = s.globalPushContext().InitContext(s.Env, nil, nil); err != nil { 86 // Error accessing the data - log and close, maybe a different pilot replica 87 // has more luck 88 deltaLog.Warnf("Error reading config %v", err) 89 return status.Error(codes.Unavailable, "error reading config") 90 } 91 con := newDeltaConnection(peerAddr, stream) 92 93 // Do not call: defer close(con.pushChannel). The push channel will be garbage collected 94 // when the connection is no longer used. Closing the channel can cause subtle race conditions 95 // with push. According to the spec: "It's only necessary to close a channel when it is important 96 // to tell the receiving goroutines that all data have been sent." 97 98 // Block until either a request is received or a push is triggered. 99 // We need 2 go routines because 'read' blocks in Recv(). 100 go s.receiveDelta(con, ids) 101 102 // Wait for the proxy to be fully initialized before we start serving traffic. Because 103 // initialization doesn't have dependencies that will block, there is no need to add any timeout 104 // here. Prior to this explicit wait, we were implicitly waiting by receive() not sending to 105 // reqChannel and the connection not being enqueued for pushes to pushChannel until the 106 // initialization is complete. 107 <-con.InitializedCh() 108 109 for { 110 // Go select{} statements are not ordered; the same channel can be chosen many times. 111 // For requests, these are higher priority (client may be blocked on startup until these are done) 112 // and often very cheap to handle (simple ACK), so we check it first. 113 select { 114 case req, ok := <-con.deltaReqChan: 115 if ok { 116 if err := s.processDeltaRequest(req, con); err != nil { 117 return err 118 } 119 } else { 120 // Remote side closed connection or error processing the request. 121 return <-con.ErrorCh() 122 } 123 case <-con.StopCh(): 124 return nil 125 default: 126 } 127 // If there wasn't already a request, poll for requests and pushes. Note: if we have a huge 128 // amount of incoming requests, we may still send some pushes, as we do not `continue` above; 129 // however, requests will be handled ~2x as much as pushes. This ensures a wave of requests 130 // cannot completely starve pushes. However, this scenario is unlikely. 131 select { 132 case req, ok := <-con.deltaReqChan: 133 if ok { 134 if err := s.processDeltaRequest(req, con); err != nil { 135 return err 136 } 137 } else { 138 // Remote side closed connection or error processing the request. 139 return <-con.ErrorCh() 140 } 141 case ev := <-con.PushCh(): 142 pushEv := ev.(*Event) 143 err := s.pushConnectionDelta(con, pushEv) 144 pushEv.done() 145 if err != nil { 146 return err 147 } 148 case <-con.StopCh(): 149 return nil 150 } 151 } 152 } 153 154 // Compute and send the new configuration for a connection. 155 func (s *DiscoveryServer) pushConnectionDelta(con *Connection, pushEv *Event) error { 156 pushRequest := pushEv.pushRequest 157 158 if pushRequest.Full { 159 // Update Proxy with current information. 160 s.computeProxyState(con.proxy, pushRequest) 161 } 162 163 if !s.ProxyNeedsPush(con.proxy, pushRequest) { 164 deltaLog.Debugf("Skipping push to %v, no updates required", con.ID()) 165 if pushRequest.Full { 166 // Only report for full versions, incremental pushes do not have a new version 167 reportAllEventsForProxyNoPush(con, s.StatusReporter, pushRequest.Push.LedgerVersion) 168 } 169 return nil 170 } 171 172 // Send pushes to all generators 173 // Each Generator is responsible for determining if the push event requires a push 174 wrl := con.watchedResourcesByOrder() 175 for _, w := range wrl { 176 if err := s.pushDeltaXds(con, w, pushRequest); err != nil { 177 return err 178 } 179 } 180 181 if pushRequest.Full { 182 // Report all events for unwatched resources. Watched resources will be reported in pushXds or on ack. 183 reportEventsForUnWatched(con, s.StatusReporter, pushRequest.Push.LedgerVersion) 184 } 185 186 proxiesConvergeDelay.Record(time.Since(pushRequest.Start).Seconds()) 187 return nil 188 } 189 190 func (s *DiscoveryServer) receiveDelta(con *Connection, identities []string) { 191 defer func() { 192 close(con.deltaReqChan) 193 close(con.ErrorCh()) 194 // Close the initialized channel, if its not already closed, to prevent blocking the stream 195 select { 196 case <-con.InitializedCh(): 197 default: 198 close(con.InitializedCh()) 199 } 200 }() 201 firstRequest := true 202 for { 203 req, err := con.deltaStream.Recv() 204 if err != nil { 205 if istiogrpc.IsExpectedGRPCError(err) { 206 deltaLog.Infof("ADS: %q %s terminated", con.Peer(), con.ID()) 207 return 208 } 209 con.ErrorCh() <- err 210 deltaLog.Errorf("ADS: %q %s terminated with error: %v", con.Peer(), con.ID(), err) 211 xds.TotalXDSInternalErrors.Increment() 212 return 213 } 214 // This should be only set for the first request. The node id may not be set - for example malicious clients. 215 if firstRequest { 216 // probe happens before envoy sends first xDS request 217 if req.TypeUrl == v3.HealthInfoType { 218 log.Warnf("ADS: %q %s send health check probe before normal xDS request", con.Peer(), con.ID()) 219 continue 220 } 221 firstRequest = false 222 if req.Node == nil || req.Node.Id == "" { 223 con.ErrorCh() <- status.New(codes.InvalidArgument, "missing node information").Err() 224 return 225 } 226 if err := s.initConnection(req.Node, con, identities); err != nil { 227 con.ErrorCh() <- err 228 return 229 } 230 defer s.closeConnection(con) 231 deltaLog.Infof("ADS: new delta connection for node:%s", con.ID()) 232 } 233 234 select { 235 case con.deltaReqChan <- req: 236 case <-con.deltaStream.Context().Done(): 237 deltaLog.Infof("ADS: %q %s terminated with stream closed", con.Peer(), con.ID()) 238 return 239 } 240 } 241 } 242 243 func (conn *Connection) sendDelta(res *discovery.DeltaDiscoveryResponse, newResourceNames []string) error { 244 sendResonse := func() error { 245 start := time.Now() 246 defer func() { xds.RecordSendTime(time.Since(start)) }() 247 return conn.deltaStream.Send(res) 248 } 249 err := sendResonse() 250 if err == nil { 251 if !strings.HasPrefix(res.TypeUrl, v3.DebugType) { 252 conn.proxy.UpdateWatchedResource(res.TypeUrl, func(wr *model.WatchedResource) *model.WatchedResource { 253 if wr == nil { 254 wr = &model.WatchedResource{TypeUrl: res.TypeUrl} 255 } 256 // some resources dynamically update ResourceNames. Most don't though 257 if newResourceNames != nil { 258 wr.ResourceNames = newResourceNames 259 } 260 wr.NonceSent = res.Nonce 261 if features.EnableUnsafeDeltaTest { 262 wr.LastResources = applyDelta(wr.LastResources, res) 263 } 264 return wr 265 }) 266 } 267 } else if status.Convert(err).Code() == codes.DeadlineExceeded { 268 deltaLog.Infof("Timeout writing %s: %v", conn.ID(), v3.GetShortType(res.TypeUrl)) 269 xds.ResponseWriteTimeouts.Increment() 270 } 271 return err 272 } 273 274 // processDeltaRequest is handling one request. This is currently called from the 'main' thread, which also 275 // handles 'push' requests and close - the code will eventually call the 'push' code, and it needs more mutex 276 // protection. Original code avoided the mutexes by doing both 'push' and 'process requests' in same thread. 277 func (s *DiscoveryServer) processDeltaRequest(req *discovery.DeltaDiscoveryRequest, con *Connection) error { 278 stype := v3.GetShortType(req.TypeUrl) 279 deltaLog.Debugf("ADS:%s: REQ %s resources sub:%d unsub:%d nonce:%s", stype, 280 con.ID(), len(req.ResourceNamesSubscribe), len(req.ResourceNamesUnsubscribe), req.ResponseNonce) 281 282 if req.TypeUrl == v3.HealthInfoType { 283 s.handleWorkloadHealthcheck(con.proxy, deltaToSotwRequest(req)) 284 return nil 285 } 286 if strings.HasPrefix(req.TypeUrl, v3.DebugType) { 287 return s.pushDeltaXds(con, 288 &model.WatchedResource{TypeUrl: req.TypeUrl, ResourceNames: req.ResourceNamesSubscribe}, 289 &model.PushRequest{Full: true, Push: con.proxy.LastPushContext}) 290 } 291 292 if s.StatusReporter != nil { 293 s.StatusReporter.RegisterEvent(con.ID(), req.TypeUrl, req.ResponseNonce) 294 } 295 296 shouldRespond := s.shouldRespondDelta(con, req) 297 if !shouldRespond { 298 return nil 299 } 300 301 subs, _ := deltaWatchedResources(nil, req) 302 request := &model.PushRequest{ 303 Full: true, 304 Push: con.proxy.LastPushContext, 305 Reason: model.NewReasonStats(model.ProxyRequest), 306 307 // The usage of LastPushTime (rather than time.Now()), is critical here for correctness; This time 308 // is used by the XDS cache to determine if a entry is stale. If we use Now() with an old push context, 309 // we may end up overriding active cache entries with stale ones. 310 Start: con.proxy.LastPushTime, 311 Delta: model.ResourceDelta{ 312 // Record sub/unsub, but drop synthetic wildcard info 313 Subscribed: sets.New(subs...), 314 Unsubscribed: sets.New(req.ResourceNamesUnsubscribe...).Delete("*"), 315 }, 316 } 317 // SidecarScope for the proxy may has not been updated based on this pushContext. 318 // It can happen when `processRequest` comes after push context has been updated(s.initPushContext), 319 // but before proxy's SidecarScope has been updated(s.updateProxy). 320 if con.proxy.SidecarScope != nil && con.proxy.SidecarScope.Version != request.Push.PushVersion { 321 s.computeProxyState(con.proxy, request) 322 } 323 324 err := s.pushDeltaXds(con, con.proxy.GetWatchedResource(req.TypeUrl), request) 325 if err != nil { 326 return err 327 } 328 // Anytime we get a CDS request on reconnect, we should always push EDS as well. 329 // It is always the server's responsibility to send EDS after CDS, regardless if 330 // Envoy asks for it or not (See https://github.com/envoyproxy/envoy/issues/33607 for more details). 331 // Without this logic, there are cases where the clusters we send could stay warming forever, 332 // expecting an EDS response. Note that in SotW, Envoy sends an EDS request after the delayed 333 // CDS request; however, this is not guaranteed in delta, and has been observed to cause issues 334 // with EDS and SDS. 335 // This can happen with the following sequence 336 // 1. Envoy disconnects and reconnects to Istiod. 337 // 2. Envoy sends EDS request and we respond with it. 338 // 3. Envoy sends CDS request and we respond with clusters. 339 // 4. Envoy detects a change in cluster state and tries to warm those clusters but never sends 340 // an EDS request for them. 341 // 5. Therefore, any initial CDS request should always trigger an EDS response 342 // to let Envoy finish cluster warming. 343 // Refer to https://github.com/envoyproxy/envoy/issues/13009 for some more details on this type of issues. 344 if req.TypeUrl != v3.ClusterType { 345 return nil 346 } 347 return s.forceEDSPush(con) 348 } 349 350 func (s *DiscoveryServer) forceEDSPush(con *Connection) error { 351 if dwr := con.proxy.GetWatchedResource(v3.EndpointType); dwr != nil { 352 request := &model.PushRequest{ 353 Full: true, 354 Push: con.proxy.LastPushContext, 355 Reason: model.NewReasonStats(model.DependentResource), 356 Start: con.proxy.LastPushTime, 357 } 358 deltaLog.Infof("ADS:%s: FORCE %s PUSH for warming.", v3.GetShortType(v3.EndpointType), con.ID()) 359 return s.pushDeltaXds(con, dwr, request) 360 } 361 return nil 362 } 363 364 // shouldRespondDelta determines whether this request needs to be responded back. It applies the ack/nack rules as per xds protocol 365 // using WatchedResource for previous state and discovery request for the current state. 366 func (s *DiscoveryServer) shouldRespondDelta(con *Connection, request *discovery.DeltaDiscoveryRequest) bool { 367 stype := v3.GetShortType(request.TypeUrl) 368 369 // If there is an error in request that means previous response is erroneous. 370 // We do not have to respond in that case. In this case request's version info 371 // will be different from the version sent. But it is fragile to rely on that. 372 if request.ErrorDetail != nil { 373 errCode := codes.Code(request.ErrorDetail.Code) 374 deltaLog.Warnf("ADS:%s: ACK ERROR %s %s:%s", stype, con.ID(), errCode.String(), request.ErrorDetail.GetMessage()) 375 xds.IncrementXDSRejects(request.TypeUrl, con.proxy.ID, errCode.String()) 376 return false 377 } 378 379 deltaLog.Debugf("ADS:%s REQUEST %v: sub:%v unsub:%v initial:%v", stype, con.ID(), 380 request.ResourceNamesSubscribe, request.ResourceNamesUnsubscribe, request.InitialResourceVersions) 381 previousInfo := con.proxy.GetWatchedResource(request.TypeUrl) 382 383 // This can happen in two cases: 384 // 1. Envoy initially send request to Istiod 385 // 2. Envoy reconnect to Istiod i.e. Istiod does not have 386 // information about this typeUrl, but Envoy sends response nonce - either 387 // because Istiod is restarted or Envoy disconnects and reconnects. 388 // We should always respond with the current resource names. 389 if previousInfo == nil { 390 con.proxy.Lock() 391 defer con.proxy.Unlock() 392 393 if len(request.InitialResourceVersions) > 0 { 394 deltaLog.Debugf("ADS:%s: RECONNECT %s %s resources:%v", stype, con.ID(), request.ResponseNonce, len(request.InitialResourceVersions)) 395 } else { 396 deltaLog.Debugf("ADS:%s: INIT %s %s", stype, con.ID(), request.ResponseNonce) 397 } 398 399 res, wildcard := deltaWatchedResources(nil, request) 400 con.proxy.WatchedResources[request.TypeUrl] = &model.WatchedResource{ 401 TypeUrl: request.TypeUrl, 402 ResourceNames: res, 403 Wildcard: wildcard, 404 } 405 return true 406 } 407 408 // If there is mismatch in the nonce, that is a case of expired/stale nonce. 409 // A nonce becomes stale following a newer nonce being sent to Envoy. 410 // TODO: due to concurrent unsubscribe, this probably doesn't make sense. Do we need any logic here? 411 if request.ResponseNonce != "" && request.ResponseNonce != previousInfo.NonceSent { 412 deltaLog.Debugf("ADS:%s: REQ %s Expired nonce received %s, sent %s", stype, 413 con.ID(), request.ResponseNonce, previousInfo.NonceSent) 414 xds.ExpiredNonce.With(typeTag.Value(v3.GetMetricType(request.TypeUrl))).Increment() 415 return false 416 } 417 // If it comes here, that means nonce match. This an ACK. We should record 418 // the ack details and respond if there is a change in resource names. 419 var previousResources, currentResources []string 420 var alwaysRespond bool 421 con.proxy.UpdateWatchedResource(request.TypeUrl, func(wr *model.WatchedResource) *model.WatchedResource { 422 previousResources = wr.ResourceNames 423 currentResources, _ = deltaWatchedResources(previousResources, request) 424 wr.NonceAcked = request.ResponseNonce 425 wr.ResourceNames = currentResources 426 alwaysRespond = wr.AlwaysRespond 427 wr.AlwaysRespond = false 428 return wr 429 }) 430 431 subChanged := !slices.EqualUnordered(previousResources, currentResources) 432 // Spontaneous DeltaDiscoveryRequests from the client. 433 // This can be done to dynamically add or remove elements from the tracked resource_names set. 434 // In this case response_nonce is empty. 435 spontaneousReq := request.ResponseNonce == "" 436 // It is invalid in the below two cases: 437 // 1. no subscribed resources change from spontaneous delta request. 438 // 2. subscribed resources changes from ACK. 439 if spontaneousReq && !subChanged || !spontaneousReq && subChanged { 440 deltaLog.Errorf("ADS:%s: Subscribed resources check mismatch: %v vs %v", stype, spontaneousReq, subChanged) 441 if features.EnableUnsafeAssertions { 442 panic(fmt.Sprintf("ADS:%s: Subscribed resources check mismatch: %v vs %v", stype, spontaneousReq, subChanged)) 443 } 444 } 445 446 // Envoy can send two DiscoveryRequests with same version and nonce 447 // when it detects a new resource. We should respond if they change. 448 if !subChanged { 449 // We should always respond "alwaysRespond" marked requests to let Envoy finish warming 450 // even though Nonce match and it looks like an ACK. 451 if alwaysRespond { 452 deltaLog.Infof("ADS:%s: FORCE RESPONSE %s for warming.", stype, con.ID()) 453 return true 454 } 455 456 deltaLog.Debugf("ADS:%s: ACK %s %s", stype, con.ID(), request.ResponseNonce) 457 return false 458 } 459 deltaLog.Debugf("ADS:%s: RESOURCE CHANGE previous resources: %v, new resources: %v %s %s", stype, 460 previousResources, currentResources, con.ID(), request.ResponseNonce) 461 462 return true 463 } 464 465 // Push a Delta XDS resource for the given connection. 466 func (s *DiscoveryServer) pushDeltaXds(con *Connection, w *model.WatchedResource, req *model.PushRequest) error { 467 if w == nil { 468 return nil 469 } 470 gen := s.findGenerator(w.TypeUrl, con) 471 if gen == nil { 472 return nil 473 } 474 t0 := time.Now() 475 476 originalW := w 477 // If delta is set, client is requesting new resources or removing old ones. We should just generate the 478 // new resources it needs, rather than the entire set of known resources. 479 // Note: we do not need to account for unsubscribed resources as these are handled by parent removal; 480 // See https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol#deleting-resources. 481 // This means if there are only removals, we will not respond. 482 var logFiltered string 483 if !req.Delta.IsEmpty() && !requiresResourceNamesModification(w.TypeUrl) { 484 // Some types opt out of this and natively handle req.Delta 485 logFiltered = " filtered:" + strconv.Itoa(len(w.ResourceNames)-len(req.Delta.Subscribed)) 486 w = &model.WatchedResource{ 487 TypeUrl: w.TypeUrl, 488 ResourceNames: req.Delta.Subscribed.UnsortedList(), 489 } 490 } 491 492 var res model.Resources 493 var deletedRes model.DeletedResources 494 var logdata model.XdsLogDetails 495 var usedDelta bool 496 var err error 497 switch g := gen.(type) { 498 case model.XdsDeltaResourceGenerator: 499 res, deletedRes, logdata, usedDelta, err = g.GenerateDeltas(con.proxy, req, w) 500 if features.EnableUnsafeDeltaTest { 501 fullRes, l, _ := g.Generate(con.proxy, originalW, req) 502 s.compareDiff(con, originalW, fullRes, res, deletedRes, usedDelta, req.Delta, l.Incremental) 503 } 504 case model.XdsResourceGenerator: 505 res, logdata, err = g.Generate(con.proxy, w, req) 506 } 507 if err != nil || (res == nil && deletedRes == nil) { 508 // If we have nothing to send, report that we got an ACK for this version. 509 if s.StatusReporter != nil { 510 s.StatusReporter.RegisterEvent(con.ID(), w.TypeUrl, req.Push.LedgerVersion) 511 } 512 return err 513 } 514 defer func() { recordPushTime(w.TypeUrl, time.Since(t0)) }() 515 resp := &discovery.DeltaDiscoveryResponse{ 516 ControlPlane: ControlPlane(), 517 TypeUrl: w.TypeUrl, 518 // TODO: send different version for incremental eds 519 SystemVersionInfo: req.Push.PushVersion, 520 Nonce: nonce(req.Push.LedgerVersion), 521 Resources: res, 522 } 523 currentResources := slices.Map(res, func(r *discovery.Resource) string { 524 return r.Name 525 }) 526 if usedDelta { 527 resp.RemovedResources = deletedRes 528 } else if req.Full { 529 // similar to sotw 530 subscribed := sets.New(w.ResourceNames...) 531 removed := subscribed.DeleteAll(currentResources...) 532 resp.RemovedResources = sets.SortedList(removed) 533 } 534 var newResourceNames []string 535 if shouldSetWatchedResources(w) { 536 // Set the new watched resources. Do not write to w directly, as it can be a copy from the 'filtered' logic above 537 if usedDelta { 538 // Apply the delta 539 newResourceNames = sets.SortedList(sets.New(w.ResourceNames...). 540 DeleteAll(resp.RemovedResources...). 541 InsertAll(currentResources...)) 542 } else { 543 newResourceNames = currentResources 544 } 545 } 546 if neverRemoveDelta(w.TypeUrl) { 547 resp.RemovedResources = nil 548 } 549 if len(resp.RemovedResources) > 0 { 550 deltaLog.Debugf("ADS:%v REMOVE for node:%s %v", v3.GetShortType(w.TypeUrl), con.ID(), resp.RemovedResources) 551 } 552 553 configSize := ResourceSize(res) 554 configSizeBytes.With(typeTag.Value(w.TypeUrl)).Record(float64(configSize)) 555 556 ptype := "PUSH" 557 info := "" 558 if logdata.Incremental { 559 ptype = "PUSH INC" 560 } 561 if len(logdata.AdditionalInfo) > 0 { 562 info = " " + logdata.AdditionalInfo 563 } 564 if len(logFiltered) > 0 { 565 info += logFiltered 566 } 567 568 if err := con.sendDelta(resp, newResourceNames); err != nil { 569 logger := deltaLog.Debugf 570 if recordSendError(w.TypeUrl, err) { 571 logger = deltaLog.Warnf 572 } 573 logger("%s: Send failure for node:%s resources:%d size:%s%s: %v", 574 v3.GetShortType(w.TypeUrl), con.proxy.ID, len(res), util.ByteCount(configSize), info, err) 575 return err 576 } 577 578 switch { 579 case !req.Full: 580 if deltaLog.DebugEnabled() { 581 deltaLog.Debugf("%s: %s%s for node:%s resources:%d size:%s%s", 582 v3.GetShortType(w.TypeUrl), ptype, req.PushReason(), con.proxy.ID, len(res), util.ByteCount(configSize), info) 583 } 584 default: 585 debug := "" 586 if deltaLog.DebugEnabled() { 587 // Add additional information to logs when debug mode enabled. 588 debug = " nonce:" + resp.Nonce + " version:" + resp.SystemVersionInfo 589 } 590 deltaLog.Infof("%s: %s%s for node:%s resources:%d removed:%d size:%v%s%s", 591 v3.GetShortType(w.TypeUrl), ptype, req.PushReason(), con.proxy.ID, len(res), len(resp.RemovedResources), 592 util.ByteCount(ResourceSize(res)), info, debug) 593 } 594 595 return nil 596 } 597 598 // requiresResourceNamesModification checks if a generator needs mutable access to w.ResourceNames. 599 // This is used when resources are spontaneously pushed during Delta XDS 600 func requiresResourceNamesModification(url string) bool { 601 return url == v3.AddressType || url == v3.WorkloadType 602 } 603 604 // neverRemoveDelta checks if a type should never remove resources 605 func neverRemoveDelta(url string) bool { 606 // https://github.com/envoyproxy/envoy/issues/32823 607 // We want to garbage collect extensions when they are no longer referenced, rather than delete immediately 608 return url == v3.ExtensionConfigurationType 609 } 610 611 // shouldSetWatchedResources indicates whether we should set the watched resources for a given type. 612 // for some type like `Address` we customly handle it in the generator 613 func shouldSetWatchedResources(w *model.WatchedResource) bool { 614 if requiresResourceNamesModification(w.TypeUrl) { 615 // These handle it directly in the generator 616 return false 617 } 618 // Else fallback based on type 619 return xds.IsWildcardTypeURL(w.TypeUrl) 620 } 621 622 func newDeltaConnection(peerAddr string, stream DeltaDiscoveryStream) *Connection { 623 return &Connection{ 624 Connection: xds.NewConnection(peerAddr, nil), 625 deltaStream: stream, 626 deltaReqChan: make(chan *discovery.DeltaDiscoveryRequest, 1), 627 } 628 } 629 630 // To satisfy methods that need DiscoveryRequest. Not suitable for real usage 631 func deltaToSotwRequest(request *discovery.DeltaDiscoveryRequest) *discovery.DiscoveryRequest { 632 return &discovery.DiscoveryRequest{ 633 Node: request.Node, 634 ResourceNames: request.ResourceNamesSubscribe, 635 TypeUrl: request.TypeUrl, 636 ResponseNonce: request.ResponseNonce, 637 ErrorDetail: request.ErrorDetail, 638 } 639 } 640 641 // deltaWatchedResources returns current watched resources of delta xds 642 func deltaWatchedResources(existing []string, request *discovery.DeltaDiscoveryRequest) ([]string, bool) { 643 res := sets.New(existing...) 644 res.InsertAll(request.ResourceNamesSubscribe...) 645 // This is set by Envoy on first request on reconnection so that we are aware of what Envoy knows 646 // and can continue the xDS session properly. 647 for k := range request.InitialResourceVersions { 648 res.Insert(k) 649 } 650 res.DeleteAll(request.ResourceNamesUnsubscribe...) 651 wildcard := false 652 // A request is wildcard if they explicitly subscribe to "*" or subscribe to nothing 653 if res.Contains("*") { 654 wildcard = true 655 res.Delete("*") 656 } 657 // "if the client sends a request but has never explicitly subscribed to any resource names, the 658 // server should treat that identically to how it would treat the client having explicitly 659 // subscribed to *" 660 // NOTE: this means you cannot subscribe to nothing, which is useful for on-demand loading; to workaround this 661 // Istio clients will send and initial request both subscribing+unsubscribing to `*`. 662 if len(request.ResourceNamesSubscribe) == 0 { 663 wildcard = true 664 } 665 return res.UnsortedList(), wildcard 666 }