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  }