istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/networking/grpcgen/cds.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 grpcgen 16 17 import ( 18 "fmt" 19 20 cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" 21 core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 22 tls "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" 23 discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 24 25 networking "istio.io/api/networking/v1alpha3" 26 "istio.io/istio/pilot/pkg/features" 27 "istio.io/istio/pilot/pkg/model" 28 corexds "istio.io/istio/pilot/pkg/networking/core" 29 "istio.io/istio/pilot/pkg/networking/util" 30 "istio.io/istio/pilot/pkg/util/protoconv" 31 "istio.io/istio/pkg/config/host" 32 "istio.io/istio/pkg/util/sets" 33 ) 34 35 // BuildClusters handles a gRPC CDS request, used with the 'ApiListener' style of requests. 36 // The main difference is that the request includes Resources to filter. 37 func (g *GrpcConfigGenerator) BuildClusters(node *model.Proxy, push *model.PushContext, names []string) model.Resources { 38 filter := newClusterFilter(names) 39 clusters := make([]*cluster.Cluster, 0, len(names)) 40 for defaultClusterName, subsetFilter := range filter { 41 builder, err := newClusterBuilder(node, push, defaultClusterName, subsetFilter) 42 if err != nil { 43 log.Warn(err) 44 continue 45 } 46 clusters = append(clusters, builder.build()...) 47 } 48 49 resp := make(model.Resources, 0, len(clusters)) 50 for _, c := range clusters { 51 resp = append(resp, &discovery.Resource{ 52 Name: c.Name, 53 Resource: protoconv.MessageToAny(c), 54 }) 55 } 56 if len(resp) == 0 && len(names) == 0 { 57 log.Warnf("did not generate any cds for %s; no names provided", node.ID) 58 } 59 return resp 60 } 61 62 // newClusterFilter maps a non-subset cluster name to the list of actual cluster names (default or subset) actually 63 // requested by the client. gRPC will usually request a group of clusters that are used in the same route; in some 64 // cases this means subsets associated with the same default cluster aren't all expected in the same CDS response. 65 func newClusterFilter(names []string) map[string]sets.String { 66 filter := map[string]sets.String{} 67 for _, name := range names { 68 dir, _, hn, p := model.ParseSubsetKey(name) 69 defaultKey := model.BuildSubsetKey(dir, "", hn, p) 70 sets.InsertOrNew(filter, defaultKey, name) 71 } 72 return filter 73 } 74 75 // clusterBuilder is responsible for building a single default and subset clusters for a service 76 // TODO re-use the v1alpha3.ClusterBuilder: 77 // Most of the logic is similar, I think we can just share the code if we expose: 78 // * BuildSubsetCluster 79 // * BuildDefaultCluster 80 // * BuildClusterOpts and members 81 // * Add something to allow us to override how tlscontext is built 82 type clusterBuilder struct { 83 push *model.PushContext 84 node *model.Proxy 85 86 // guaranteed to be set in init 87 defaultClusterName string 88 hostname host.Name 89 portNum int 90 91 // may not be set 92 svc *model.Service 93 port *model.Port 94 filter sets.String 95 } 96 97 func newClusterBuilder(node *model.Proxy, push *model.PushContext, defaultClusterName string, filter sets.String) (*clusterBuilder, error) { 98 _, _, hostname, portNum := model.ParseSubsetKey(defaultClusterName) 99 if hostname == "" || portNum == 0 { 100 return nil, fmt.Errorf("failed parsing subset key: %s", defaultClusterName) 101 } 102 103 // try to resolve the service and port 104 var port *model.Port 105 svc := push.ServiceForHostname(node, hostname) 106 if svc == nil { 107 return nil, fmt.Errorf("cds gen for %s: did not find service for cluster %s", node.ID, defaultClusterName) 108 } 109 110 port, ok := svc.Ports.GetByPort(portNum) 111 if !ok { 112 return nil, fmt.Errorf("cds gen for %s: did not find port %d in service for cluster %s", node.ID, portNum, defaultClusterName) 113 } 114 115 return &clusterBuilder{ 116 node: node, 117 push: push, 118 119 defaultClusterName: defaultClusterName, 120 hostname: hostname, 121 portNum: portNum, 122 filter: filter, 123 124 svc: svc, 125 port: port, 126 }, nil 127 } 128 129 func (b *clusterBuilder) build() []*cluster.Cluster { 130 var defaultCluster *cluster.Cluster 131 if b.filter.Contains(b.defaultClusterName) { 132 defaultCluster = edsCluster(b.defaultClusterName) 133 if b.svc.Attributes.Labels[features.PersistentSessionLabel] != "" { 134 // see core/v1alpha3/cluster.go 135 defaultCluster.CommonLbConfig.OverrideHostStatus = &core.HealthStatusSet{ 136 Statuses: []core.HealthStatus{ 137 core.HealthStatus_HEALTHY, 138 core.HealthStatus_DRAINING, core.HealthStatus_UNKNOWN, core.HealthStatus_DEGRADED, 139 }, 140 } 141 } 142 } 143 144 subsetClusters := b.applyDestinationRule(defaultCluster) 145 out := make([]*cluster.Cluster, 0, 1+len(subsetClusters)) 146 if defaultCluster != nil { 147 out = append(out, defaultCluster) 148 } 149 return append(out, subsetClusters...) 150 } 151 152 // edsCluster creates a simple cluster to read endpoints from ads/eds. 153 func edsCluster(name string) *cluster.Cluster { 154 return &cluster.Cluster{ 155 Name: name, 156 ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_EDS}, 157 EdsClusterConfig: &cluster.Cluster_EdsClusterConfig{ 158 ServiceName: name, 159 EdsConfig: &core.ConfigSource{ 160 ConfigSourceSpecifier: &core.ConfigSource_Ads{ 161 Ads: &core.AggregatedConfigSource{}, 162 }, 163 }, 164 }, 165 } 166 } 167 168 // applyDestinationRule mutates the default cluster to reflect traffic policies, and returns a set of additional 169 // subset clusters if specified by a destination rule 170 func (b *clusterBuilder) applyDestinationRule(defaultCluster *cluster.Cluster) (subsetClusters []*cluster.Cluster) { 171 if b.svc == nil || b.port == nil { 172 return nil 173 } 174 175 // resolve policy from context 176 destinationRule := corexds.CastDestinationRule(b.node.SidecarScope.DestinationRule( 177 model.TrafficDirectionOutbound, b.node, b.svc.Hostname).GetRule()) 178 trafficPolicy, _ := util.GetPortLevelTrafficPolicy(destinationRule.GetTrafficPolicy(), b.port) 179 180 // setup default cluster 181 b.applyTrafficPolicy(defaultCluster, trafficPolicy) 182 183 // subset clusters 184 if len(destinationRule.GetSubsets()) > 0 { 185 subsetClusters = make([]*cluster.Cluster, 0, len(destinationRule.GetSubsets())) 186 for _, subset := range destinationRule.GetSubsets() { 187 subsetKey := subsetClusterKey(subset.Name, string(b.hostname), b.portNum) 188 if !b.filter.Contains(subsetKey) { 189 continue 190 } 191 c := edsCluster(subsetKey) 192 trafficPolicy := util.MergeSubsetTrafficPolicy(trafficPolicy, subset.TrafficPolicy, b.port) 193 b.applyTrafficPolicy(c, trafficPolicy) 194 subsetClusters = append(subsetClusters, c) 195 } 196 } 197 198 return 199 } 200 201 // applyTrafficPolicy mutates the give cluster (if not-nil) so that the given merged traffic policy applies. 202 func (b *clusterBuilder) applyTrafficPolicy(c *cluster.Cluster, trafficPolicy *networking.TrafficPolicy) { 203 // cluster can be nil if it wasn't requested 204 if c == nil { 205 return 206 } 207 b.applyTLS(c, trafficPolicy) 208 b.applyLoadBalancing(c, trafficPolicy) 209 // TODO status or log when unsupported features are included 210 } 211 212 func (b *clusterBuilder) applyLoadBalancing(c *cluster.Cluster, policy *networking.TrafficPolicy) { 213 switch policy.GetLoadBalancer().GetSimple() { 214 case networking.LoadBalancerSettings_ROUND_ROBIN, networking.LoadBalancerSettings_UNSPECIFIED: 215 // ok 216 default: 217 log.Warnf("cannot apply LbPolicy %s to %s", policy.LoadBalancer.GetSimple(), b.node.ID) 218 } 219 corexds.ApplyRingHashLoadBalancer(c, policy.GetLoadBalancer()) 220 } 221 222 func (b *clusterBuilder) applyTLS(c *cluster.Cluster, policy *networking.TrafficPolicy) { 223 // TODO for now, we leave mTLS *off* by default: 224 // 1. We don't know if the client uses xds.NewClientCredentials; these settings will be ignored if not 225 // 2. We cannot reach servers in PERMISSIVE mode; gRPC doesn't allow us to override the alpn to one of Istio's 226 // 3. Once we support gRPC servers, we have no good way to detect if a server is implemented with xds.NewGrpcServer and will actually support our config 227 // For these reasons, support only explicit tls configuration. 228 switch policy.GetTls().GetMode() { 229 case networking.ClientTLSSettings_DISABLE: 230 // nothing to do 231 case networking.ClientTLSSettings_SIMPLE: 232 // TODO support this 233 case networking.ClientTLSSettings_MUTUAL: 234 // TODO support this 235 case networking.ClientTLSSettings_ISTIO_MUTUAL: 236 tlsCtx := buildUpstreamTLSContext(b.push.ServiceAccounts(b.hostname, b.svc.Attributes.Namespace)) 237 c.TransportSocket = &core.TransportSocket{ 238 Name: transportSocketName, 239 ConfigType: &core.TransportSocket_TypedConfig{TypedConfig: protoconv.MessageToAny(tlsCtx)}, 240 } 241 } 242 } 243 244 // TransportSocket proto message has a `name` field which is expected to be set to exactly this value by the 245 // management server (see grpc/xds/internal/client/xds.go securityConfigFromCluster). 246 const transportSocketName = "envoy.transport_sockets.tls" 247 248 func buildUpstreamTLSContext(sans []string) *tls.UpstreamTlsContext { 249 return &tls.UpstreamTlsContext{ 250 CommonTlsContext: buildCommonTLSContext(sans), 251 } 252 }