istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/xds/workload.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 discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 19 20 "istio.io/istio/pilot/pkg/model" 21 "istio.io/istio/pilot/pkg/util/protoconv" 22 v3 "istio.io/istio/pilot/pkg/xds/v3" 23 "istio.io/istio/pkg/config/schema/kind" 24 "istio.io/istio/pkg/util/sets" 25 ) 26 27 type WorkloadGenerator struct { 28 Server *DiscoveryServer 29 } 30 31 var ( 32 _ model.XdsResourceGenerator = &WorkloadGenerator{} 33 _ model.XdsDeltaResourceGenerator = &WorkloadGenerator{} 34 ) 35 36 // GenerateDeltas computes Workload resources. This is design to be highly optimized to delta updates, 37 // and supports *on-demand* client usage. A client can subscribe with a wildcard subscription and get all 38 // resources (with delta updates), or on-demand and only get responses for specifically subscribed resources. 39 // 40 // Incoming requests may be for VIP or Pod IP addresses. However, all responses are Workload resources, which are pod based. 41 // This means subscribing to a VIP may end up pushing many resources of different name than the request. 42 // On-demand clients are expected to handle this (for wildcard, this is not applicable, as they don't specify any resources at all). 43 func (e WorkloadGenerator) GenerateDeltas( 44 proxy *model.Proxy, 45 req *model.PushRequest, 46 w *model.WatchedResource, 47 ) (model.Resources, model.DeletedResources, model.XdsLogDetails, bool, error) { 48 updatedAddresses := model.ConfigNameOfKind(req.ConfigsUpdated, kind.Address) 49 isReq := req.IsRequest() 50 if len(updatedAddresses) == 0 && len(req.ConfigsUpdated) > 0 { 51 // Nothing changed.. 52 return nil, nil, model.XdsLogDetails{}, false, nil 53 } 54 55 subs := sets.New(w.ResourceNames...) 56 var addresses sets.String 57 if isReq { 58 // this is from request, we only send response for the subscribed address 59 // At t0, a client request A, we only send A and additional resources back to the client. 60 // At t1, a client request B, we only send B and additional resources back to the client, no A here. 61 addresses = req.Delta.Subscribed 62 } else { 63 if w.Wildcard { 64 addresses = updatedAddresses 65 } else { 66 // this is from the external triggers instead of request 67 // send response for all the subscribed intersect with the updated 68 addresses = updatedAddresses.IntersectInPlace(subs) 69 } 70 } 71 72 if !w.Wildcard { 73 // We only need this for on-demand. This allows us to subscribe the client to resources they 74 // didn't explicitly request. 75 // For wildcard, they subscribe to everything already. 76 additional := e.Server.Env.ServiceDiscovery.AdditionalPodSubscriptions(proxy, addresses, subs) 77 if addresses == nil { 78 addresses = sets.New[string]() 79 } 80 addresses.Merge(additional) 81 } 82 83 // TODO: it is needlessly wasteful to do a full sync just because the rest of Istio thought it was "full" 84 // The rest of Istio xDS types would treat `req.Full && len(req.ConfigsUpdated) == 0` as a need to trigger a "full" push. 85 // This is only an escape hatch for a lack of complete mapping of "Input changed -> Output changed". 86 // WDS does not suffer this limitation, so we could almost safely ignore these. 87 // However, other code will merge "Partial push + Full push -> Full push", so skipping full pushes isn't viable. 88 full := (isReq && w.Wildcard) || (!isReq && req.Full && len(req.ConfigsUpdated) == 0) 89 90 // Nothing to do 91 if len(addresses) == 0 && !full { 92 if isReq { 93 // We need to respond for requests, even if we have nothing to respond with 94 return make(model.Resources, 0), nil, model.XdsLogDetails{}, false, nil 95 } 96 // For NOP pushes, no need 97 return nil, nil, model.XdsLogDetails{}, false, nil 98 } 99 resources := make(model.Resources, 0) 100 reqAddresses := addresses 101 if full { 102 reqAddresses = nil 103 } 104 addrs, removed := e.Server.Env.ServiceDiscovery.AddressInformation(reqAddresses) 105 // Note: while "removed" is a weird name for a resource that never existed, this is how the spec works: 106 // https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol#id2 107 have := sets.New[string]() 108 haveAliases := sets.New[string]() 109 for _, addr := range addrs { 110 // TODO(@hzxuzhonghu): calculate removed with aliases in `AddressInformation` 111 aliases := addr.Aliases() 112 removed.DeleteAll(aliases...) 113 n := addr.ResourceName() 114 have.Insert(n) 115 haveAliases.InsertAll(aliases...) 116 switch w.TypeUrl { 117 case v3.WorkloadType: 118 if addr.GetWorkload() != nil { 119 resources = append(resources, &discovery.Resource{ 120 Name: n, 121 Aliases: aliases, 122 Resource: protoconv.MessageToAny(addr.GetWorkload()), // TODO: pre-marshal 123 }) 124 } 125 case v3.AddressType: 126 resources = append(resources, &discovery.Resource{ 127 Name: n, 128 Aliases: aliases, 129 Resource: protoconv.MessageToAny(addr), // TODO: pre-marshal 130 }) 131 } 132 } 133 134 if full { 135 // If it's a full push, AddressInformation won't have info to compute the full set of removals. 136 // Instead, we need can see what resources are missing that we were subscribe to; those were removed. 137 removed = subs.Difference(have).Difference(haveAliases).Merge(removed) 138 } 139 140 if !w.Wildcard { 141 // For on-demand, we may have requested a VIP but gotten Pod IPs back. We need to update 142 // the internal book-keeping to subscribe to the Pods, so that we push updates to those Pods. 143 w.ResourceNames = subs.Merge(have).UnsortedList() 144 } else { 145 // For wildcard, we record all resources that have been pushed and not removed 146 // It was to correctly calculate removed resources during full push alongside with specific address removed. 147 w.ResourceNames = subs.Merge(have).Difference(removed).UnsortedList() 148 } 149 return resources, removed.UnsortedList(), model.XdsLogDetails{}, true, nil 150 } 151 152 func (e WorkloadGenerator) Generate(proxy *model.Proxy, w *model.WatchedResource, req *model.PushRequest) (model.Resources, model.XdsLogDetails, error) { 153 resources, _, details, _, err := e.GenerateDeltas(proxy, req, w) 154 return resources, details, err 155 } 156 157 type WorkloadRBACGenerator struct { 158 Server *DiscoveryServer 159 } 160 161 func (e WorkloadRBACGenerator) GenerateDeltas( 162 proxy *model.Proxy, 163 req *model.PushRequest, 164 w *model.WatchedResource, 165 ) (model.Resources, model.DeletedResources, model.XdsLogDetails, bool, error) { 166 var updatedPolicies sets.Set[model.ConfigKey] 167 if len(req.ConfigsUpdated) != 0 { 168 updatedPolicies = model.ConfigsOfKind(req.ConfigsUpdated, kind.AuthorizationPolicy) 169 // Convert the actual Kubernetes PeerAuthentication policies to the synthetic ones 170 // by adding the prefix 171 // 172 // This is needed because the handler that produces the ConfigUpdate blindly sends 173 // the Kubernetes resource names without context of the synthetic Ambient policies 174 // TODO: Split out PeerAuthentication into a separate handler in 175 // https://github.com/istio/istio/blob/master/pilot/pkg/bootstrap/server.go#L882 176 for p := range model.ConfigsOfKind(req.ConfigsUpdated, kind.PeerAuthentication) { 177 updatedPolicies.Insert(model.ConfigKey{ 178 Name: model.GetAmbientPolicyConfigName(p), 179 Namespace: p.Namespace, 180 Kind: p.Kind, 181 }) 182 } 183 } 184 if len(req.ConfigsUpdated) != 0 && len(updatedPolicies) == 0 { 185 // This was a incremental push for a resource we don't watch... skip 186 return nil, nil, model.DefaultXdsLogDetails, false, nil 187 } 188 189 expected := sets.New[string]() 190 if len(updatedPolicies) > 0 { 191 // Partial update. Removes are ones we request but didn't get back when querying the policies 192 for k := range updatedPolicies { 193 expected.Insert(k.Namespace + "/" + k.Name) 194 } 195 } else { 196 // Full update, expect everything 197 expected.InsertAll(w.ResourceNames...) 198 } 199 200 resources := make(model.Resources, 0) 201 policies := e.Server.Env.ServiceDiscovery.Policies(updatedPolicies) 202 for _, p := range policies { 203 n := p.ResourceName() 204 expected.Delete(n) // delete the generated policy name, left the removed ones 205 resources = append(resources, &discovery.Resource{ 206 Name: n, 207 Resource: protoconv.MessageToAny(p.Authorization), 208 }) 209 } 210 211 return resources, sets.SortedList(expected), model.XdsLogDetails{}, true, nil 212 } 213 214 func (e WorkloadRBACGenerator) Generate(proxy *model.Proxy, w *model.WatchedResource, req *model.PushRequest) (model.Resources, model.XdsLogDetails, error) { 215 resources, _, details, _, err := e.GenerateDeltas(proxy, req, w) 216 return resources, details, err 217 } 218 219 var ( 220 _ model.XdsResourceGenerator = &WorkloadRBACGenerator{} 221 _ model.XdsDeltaResourceGenerator = &WorkloadRBACGenerator{} 222 )