• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grpc / grpc-java / #20235

06 Apr 2026 05:19AM UTC coverage: 88.794% (-0.02%) from 88.809%
#20235

push

github

web-flow
xds: fix xDS HTTP CONNECT's transport socket name bug (#12740)

fixes https://github.com/grpc/grpc-java/issues/12739

36007 of 40551 relevant lines covered (88.79%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.22
/../xds/src/main/java/io/grpc/xds/XdsClusterResource.java
1
/*
2
 * Copyright 2022 The gRPC Authors
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package io.grpc.xds;
18

19
import static com.google.common.base.Preconditions.checkNotNull;
20
import static io.grpc.xds.client.Bootstrapper.ServerInfo;
21
import static io.grpc.xds.client.LoadStatsManager2.isEnabledOrcaLrsPropagation;
22

23
import com.google.auto.value.AutoValue;
24
import com.google.common.annotations.VisibleForTesting;
25
import com.google.common.base.MoreObjects;
26
import com.google.common.base.Strings;
27
import com.google.common.collect.ImmutableList;
28
import com.google.common.collect.ImmutableMap;
29
import com.google.protobuf.Duration;
30
import com.google.protobuf.InvalidProtocolBufferException;
31
import com.google.protobuf.Message;
32
import com.google.protobuf.Struct;
33
import com.google.protobuf.util.Durations;
34
import io.envoyproxy.envoy.config.cluster.v3.CircuitBreakers.Thresholds;
35
import io.envoyproxy.envoy.config.cluster.v3.Cluster;
36
import io.envoyproxy.envoy.config.core.v3.RoutingPriority;
37
import io.envoyproxy.envoy.config.core.v3.SocketAddress;
38
import io.envoyproxy.envoy.config.core.v3.TransportSocket;
39
import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment;
40
import io.envoyproxy.envoy.extensions.transport_sockets.http_11_proxy.v3.Http11ProxyUpstreamTransport;
41
import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateValidationContext;
42
import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext;
43
import io.grpc.LoadBalancerRegistry;
44
import io.grpc.NameResolver;
45
import io.grpc.internal.GrpcUtil;
46
import io.grpc.internal.ServiceConfigUtil;
47
import io.grpc.internal.ServiceConfigUtil.LbConfig;
48
import io.grpc.xds.EnvoyServerProtoData.OutlierDetection;
49
import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext;
50
import io.grpc.xds.XdsClusterResource.CdsUpdate;
51
import io.grpc.xds.client.BackendMetricPropagation;
52
import io.grpc.xds.client.XdsClient.ResourceUpdate;
53
import io.grpc.xds.client.XdsResourceType;
54
import io.grpc.xds.internal.security.CommonTlsContextUtil;
55
import java.util.List;
56
import java.util.Locale;
57
import java.util.Set;
58
import javax.annotation.Nullable;
59

60
class XdsClusterResource extends XdsResourceType<CdsUpdate> {
1✔
61
  @VisibleForTesting
62
  static boolean enableLeastRequest =
63
      !Strings.isNullOrEmpty(System.getenv("GRPC_EXPERIMENTAL_ENABLE_LEAST_REQUEST"))
1✔
64
          ? Boolean.parseBoolean(System.getenv("GRPC_EXPERIMENTAL_ENABLE_LEAST_REQUEST"))
×
65
          : Boolean.parseBoolean(
1✔
66
              System.getProperty("io.grpc.xds.experimentalEnableLeastRequest", "true"));
1✔
67
  @VisibleForTesting
68
  public static boolean enableSystemRootCerts =
1✔
69
      GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_SYSTEM_ROOT_CERTS", true);
1✔
70
  static boolean isEnabledXdsHttpConnect =
1✔
71
      GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_HTTP_CONNECT", false);
1✔
72

73
  @VisibleForTesting
74
  static final String AGGREGATE_CLUSTER_TYPE_NAME = "envoy.clusters.aggregate";
75
  static final String ADS_TYPE_URL_CDS =
76
      "type.googleapis.com/envoy.config.cluster.v3.Cluster";
77
  private static final String TYPE_URL_CLUSTER_CONFIG =
78
      "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig";
79
  private static final String TYPE_URL_UPSTREAM_TLS_CONTEXT =
80
      "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext";
81
  private static final String TYPE_URL_UPSTREAM_TLS_CONTEXT_V2 =
82
      "type.googleapis.com/envoy.api.v2.auth.UpstreamTlsContext";
83
  private final LoadBalancerRegistry loadBalancerRegistry
1✔
84
      = LoadBalancerRegistry.getDefaultRegistry();
1✔
85

86
  private static final XdsClusterResource instance = new XdsClusterResource();
1✔
87

88
  public static XdsClusterResource getInstance() {
89
    return instance;
1✔
90
  }
91

92
  @Override
93
  @Nullable
94
  protected String extractResourceName(Message unpackedResource) {
95
    if (!(unpackedResource instanceof Cluster)) {
1✔
96
      return null;
×
97
    }
98
    return ((Cluster) unpackedResource).getName();
1✔
99
  }
100

101
  @Override
102
  public String typeName() {
103
    return "CDS";
1✔
104
  }
105

106
  @Override
107
  public String typeUrl() {
108
    return ADS_TYPE_URL_CDS;
1✔
109
  }
110

111
  @Override
112
  public boolean shouldRetrieveResourceKeysForArgs() {
113
    return true;
1✔
114
  }
115

116
  @Override
117
  protected boolean isFullStateOfTheWorld() {
118
    return true;
1✔
119
  }
120

121
  @Override
122
  @SuppressWarnings("unchecked")
123
  protected Class<Cluster> unpackedClassName() {
124
    return Cluster.class;
1✔
125
  }
126

127
  @Override
128
  protected CdsUpdate doParse(Args args, Message unpackedMessage) throws ResourceInvalidException {
129
    if (!(unpackedMessage instanceof Cluster)) {
1✔
130
      throw new ResourceInvalidException("Invalid message type: " + unpackedMessage.getClass());
×
131
    }
132
    Set<String> certProviderInstances = null;
1✔
133
    if (args.getBootstrapInfo() != null && args.getBootstrapInfo().certProviders() != null) {
1✔
134
      certProviderInstances = args.getBootstrapInfo().certProviders().keySet();
1✔
135
    }
136
    return processCluster((Cluster) unpackedMessage, certProviderInstances,
1✔
137
        args.getServerInfo(), loadBalancerRegistry);
1✔
138
  }
139

140
  @VisibleForTesting
141
  static CdsUpdate processCluster(Cluster cluster,
142
                                  Set<String> certProviderInstances,
143
                                  ServerInfo serverInfo,
144
                                  LoadBalancerRegistry loadBalancerRegistry)
145
      throws ResourceInvalidException {
146
    StructOrError<CdsUpdate.Builder> structOrError;
147
    switch (cluster.getClusterDiscoveryTypeCase()) {
1✔
148
      case TYPE:
149
        structOrError = parseNonAggregateCluster(cluster,
1✔
150
            certProviderInstances, serverInfo);
151
        break;
1✔
152
      case CLUSTER_TYPE:
153
        structOrError = parseAggregateCluster(cluster);
1✔
154
        break;
1✔
155
      case CLUSTERDISCOVERYTYPE_NOT_SET:
156
      default:
157
        throw new ResourceInvalidException(
1✔
158
            "Cluster " + cluster.getName() + ": unspecified cluster discovery type");
1✔
159
    }
160
    if (structOrError.getErrorDetail() != null) {
1✔
161
      throw new ResourceInvalidException(structOrError.getErrorDetail());
1✔
162
    }
163
    CdsUpdate.Builder updateBuilder = structOrError.getStruct();
1✔
164

165
    ImmutableMap<String, ?> lbPolicyConfig = LoadBalancerConfigFactory.newConfig(cluster,
1✔
166
        enableLeastRequest);
167

168
    // Validate the LB config by trying to parse it with the corresponding LB provider.
169
    LbConfig lbConfig = ServiceConfigUtil.unwrapLoadBalancingConfig(lbPolicyConfig);
1✔
170
    NameResolver.ConfigOrError configOrError = loadBalancerRegistry.getProvider(
1✔
171
        lbConfig.getPolicyName()).parseLoadBalancingPolicyConfig(
1✔
172
        lbConfig.getRawConfigValue());
1✔
173
    if (configOrError.getError() != null) {
1✔
174
      throw new ResourceInvalidException(
1✔
175
          "Failed to parse lb config for cluster '" + cluster.getName() + "': "
1✔
176
          + configOrError.getError());
1✔
177
    }
178

179
    updateBuilder.lbPolicyConfig(lbPolicyConfig);
1✔
180
    updateBuilder.filterMetadata(
1✔
181
        ImmutableMap.copyOf(cluster.getMetadata().getFilterMetadataMap()));
1✔
182

183
    try {
184
      MetadataRegistry registry = MetadataRegistry.getInstance();
1✔
185
      ImmutableMap<String, Object> parsedFilterMetadata =
1✔
186
          registry.parseMetadata(cluster.getMetadata());
1✔
187
      updateBuilder.parsedMetadata(parsedFilterMetadata);
1✔
188
    } catch (ResourceInvalidException e) {
×
189
      throw new ResourceInvalidException(
×
190
          "Failed to parse xDS filter metadata for cluster '" + cluster.getName() + "': "
×
191
              + e.getMessage(), e);
×
192
    }
1✔
193

194
    return updateBuilder.build();
1✔
195
  }
196

197
  private static StructOrError<CdsUpdate.Builder> parseAggregateCluster(Cluster cluster) {
198
    String clusterName = cluster.getName();
1✔
199
    Cluster.CustomClusterType customType = cluster.getClusterType();
1✔
200
    String typeName = customType.getName();
1✔
201
    if (!typeName.equals(AGGREGATE_CLUSTER_TYPE_NAME)) {
1✔
202
      return StructOrError.fromError(
×
203
          "Cluster " + clusterName + ": unsupported custom cluster type: " + typeName);
204
    }
205
    io.envoyproxy.envoy.extensions.clusters.aggregate.v3.ClusterConfig clusterConfig;
206
    try {
207
      clusterConfig = unpackCompatibleType(customType.getTypedConfig(),
1✔
208
          io.envoyproxy.envoy.extensions.clusters.aggregate.v3.ClusterConfig.class,
209
          TYPE_URL_CLUSTER_CONFIG, null);
210
    } catch (InvalidProtocolBufferException e) {
×
211
      return StructOrError.fromError("Cluster " + clusterName + ": malformed ClusterConfig: " + e);
×
212
    }
1✔
213
    if (clusterConfig.getClustersList().isEmpty()) {
1✔
214
      return StructOrError.fromError("Cluster " + clusterName
1✔
215
          + ": aggregate ClusterConfig.clusters must not be empty");
216
    }
217
    return StructOrError.fromStruct(CdsUpdate.forAggregate(
1✔
218
        clusterName, clusterConfig.getClustersList()));
1✔
219
  }
220

221
  private static StructOrError<CdsUpdate.Builder> parseNonAggregateCluster(
222
      Cluster cluster, Set<String> certProviderInstances, ServerInfo serverInfo) {
223
    String clusterName = cluster.getName();
1✔
224
    ServerInfo lrsServerInfo = null;
1✔
225
    Long maxConcurrentRequests = null;
1✔
226
    UpstreamTlsContext upstreamTlsContext = null;
1✔
227
    OutlierDetection outlierDetection = null;
1✔
228
    boolean isHttp11ProxyAvailable = false;
1✔
229
    BackendMetricPropagation backendMetricPropagation = null;
1✔
230

231
    if (isEnabledOrcaLrsPropagation) {
1✔
232
      backendMetricPropagation = BackendMetricPropagation.fromMetricSpecs(
1✔
233
          cluster.getLrsReportEndpointMetricsList());
1✔
234
    }
235
    if (cluster.hasLrsServer()) {
1✔
236
      if (!cluster.getLrsServer().hasSelf()) {
1✔
237
        return StructOrError.fromError(
×
238
            "Cluster " + clusterName + ": only support LRS for the same management server");
239
      }
240
      lrsServerInfo = serverInfo;
1✔
241
    }
242
    if (cluster.hasCircuitBreakers()) {
1✔
243
      List<Thresholds> thresholds = cluster.getCircuitBreakers().getThresholdsList();
1✔
244
      for (Thresholds threshold : thresholds) {
1✔
245
        if (threshold.getPriority() != RoutingPriority.DEFAULT) {
1✔
246
          continue;
1✔
247
        }
248
        if (threshold.hasMaxRequests()) {
1✔
249
          maxConcurrentRequests = Integer.toUnsignedLong(threshold.getMaxRequests().getValue());
1✔
250
        }
251
      }
1✔
252
    }
253
    if (cluster.getTransportSocketMatchesCount() > 0) {
1✔
254
      return StructOrError.fromError("Cluster " + clusterName
1✔
255
          + ": transport-socket-matches not supported.");
256
    }
257
    boolean hasTransportSocket = cluster.hasTransportSocket();
1✔
258
    TransportSocket transportSocket = cluster.getTransportSocket();
1✔
259

260
    if (hasTransportSocket && !TRANSPORT_SOCKET_NAME_TLS.equals(transportSocket.getName())
1✔
261
        && !(isEnabledXdsHttpConnect && transportSocket.getTypedConfig().is(
1✔
262
        Http11ProxyUpstreamTransport.class))) {
263
      return StructOrError.fromError(
1✔
264
          "transport-socket with name " + transportSocket.getName() + " not supported.");
1✔
265
    }
266

267
    if (hasTransportSocket && isEnabledXdsHttpConnect && transportSocket.getTypedConfig().is(
1✔
268
        Http11ProxyUpstreamTransport.class)) {
269
      isHttp11ProxyAvailable = true;
1✔
270
      try {
271
        Http11ProxyUpstreamTransport wrappedTransportSocket = transportSocket
1✔
272
            .getTypedConfig().unpack(io.envoyproxy.envoy.extensions.transport_sockets
1✔
273
                .http_11_proxy.v3.Http11ProxyUpstreamTransport.class);
274
        hasTransportSocket = wrappedTransportSocket.hasTransportSocket();
1✔
275
        transportSocket = wrappedTransportSocket.getTransportSocket();
1✔
276
      } catch (InvalidProtocolBufferException e) {
×
277
        return StructOrError.fromError(
×
278
            "Cluster " + clusterName + ": malformed Http11ProxyUpstreamTransport: " + e);
279
      } catch (ClassCastException e) {
×
280
        return StructOrError.fromError(
×
281
            "Cluster " + clusterName
282
                + ": invalid transport_socket type in Http11ProxyUpstreamTransport");
283
      }
1✔
284
    }
285

286
    if (hasTransportSocket && TRANSPORT_SOCKET_NAME_TLS.equals(transportSocket.getName())) {
1✔
287
      try {
288
        upstreamTlsContext = UpstreamTlsContext.fromEnvoyProtoUpstreamTlsContext(
1✔
289
            validateUpstreamTlsContext(
1✔
290
                unpackCompatibleType(transportSocket.getTypedConfig(),
1✔
291
                    io.envoyproxy.envoy.extensions
292
                        .transport_sockets.tls.v3.UpstreamTlsContext.class,
293
                    TYPE_URL_UPSTREAM_TLS_CONTEXT, TYPE_URL_UPSTREAM_TLS_CONTEXT_V2),
294
                certProviderInstances));
295
      } catch (InvalidProtocolBufferException | ResourceInvalidException e) {
1✔
296
        return StructOrError.fromError(
1✔
297
            "Cluster " + clusterName + ": malformed UpstreamTlsContext: " + e);
298
      }
1✔
299
    }
300

301
    if (cluster.hasOutlierDetection()) {
1✔
302
      try {
303
        outlierDetection = OutlierDetection.fromEnvoyOutlierDetection(
1✔
304
            validateOutlierDetection(cluster.getOutlierDetection()));
1✔
305
      } catch (ResourceInvalidException e) {
1✔
306
        return StructOrError.fromError(
1✔
307
            "Cluster " + clusterName + ": malformed outlier_detection: " + e);
308
      }
1✔
309
    }
310

311
    Cluster.DiscoveryType type = cluster.getType();
1✔
312
    if (type == Cluster.DiscoveryType.EDS) {
1✔
313
      String edsServiceName = null;
1✔
314
      io.envoyproxy.envoy.config.cluster.v3.Cluster.EdsClusterConfig edsClusterConfig =
1✔
315
          cluster.getEdsClusterConfig();
1✔
316
      if (!edsClusterConfig.getEdsConfig().hasAds()
1✔
317
          && ! edsClusterConfig.getEdsConfig().hasSelf()) {
1✔
318
        return StructOrError.fromError(
1✔
319
            "Cluster " + clusterName + ": field eds_cluster_config must be set to indicate to use"
320
                + " EDS over ADS or self ConfigSource");
321
      }
322
      // If the service_name field is set, that value will be used for the EDS request.
323
      if (!edsClusterConfig.getServiceName().isEmpty()) {
1✔
324
        edsServiceName = edsClusterConfig.getServiceName();
1✔
325
      }
326
      // edsServiceName is required if the CDS resource has an xdstp name.
327
      if ((edsServiceName == null) && clusterName.toLowerCase(Locale.ROOT).startsWith("xdstp:")) {
1✔
328
        return StructOrError.fromError(
1✔
329
            "EDS service_name must be set when Cluster resource has an xdstp name");
330
      }
331

332
      return StructOrError.fromStruct(CdsUpdate.forEds(
1✔
333
          clusterName, edsServiceName, lrsServerInfo, maxConcurrentRequests, upstreamTlsContext,
334
          outlierDetection, isHttp11ProxyAvailable, backendMetricPropagation));
335
    } else if (type.equals(Cluster.DiscoveryType.LOGICAL_DNS)) {
1✔
336
      if (!cluster.hasLoadAssignment()) {
1✔
337
        return StructOrError.fromError(
×
338
            "Cluster " + clusterName + ": LOGICAL_DNS clusters must have a single host");
339
      }
340
      ClusterLoadAssignment assignment = cluster.getLoadAssignment();
1✔
341
      if (assignment.getEndpointsCount() != 1
1✔
342
          || assignment.getEndpoints(0).getLbEndpointsCount() != 1) {
1✔
343
        return StructOrError.fromError(
×
344
            "Cluster " + clusterName + ": LOGICAL_DNS clusters must have a single "
345
                + "locality_lb_endpoint and a single lb_endpoint");
346
      }
347
      io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint lbEndpoint =
1✔
348
          assignment.getEndpoints(0).getLbEndpoints(0);
1✔
349
      if (!lbEndpoint.hasEndpoint() || !lbEndpoint.getEndpoint().hasAddress()
1✔
350
          || !lbEndpoint.getEndpoint().getAddress().hasSocketAddress()) {
1✔
351
        return StructOrError.fromError(
×
352
            "Cluster " + clusterName
353
                + ": LOGICAL_DNS clusters must have an endpoint with address and socket_address");
354
      }
355
      SocketAddress socketAddress = lbEndpoint.getEndpoint().getAddress().getSocketAddress();
1✔
356
      if (!socketAddress.getResolverName().isEmpty()) {
1✔
357
        return StructOrError.fromError(
×
358
            "Cluster " + clusterName
359
                + ": LOGICAL DNS clusters must NOT have a custom resolver name set");
360
      }
361
      if (socketAddress.getPortSpecifierCase() != SocketAddress.PortSpecifierCase.PORT_VALUE) {
1✔
362
        return StructOrError.fromError(
×
363
            "Cluster " + clusterName
364
                + ": LOGICAL DNS clusters socket_address must have port_value");
365
      }
366
      String dnsHostName = String.format(
1✔
367
          Locale.US, "%s:%d", socketAddress.getAddress(), socketAddress.getPortValue());
1✔
368
      return StructOrError.fromStruct(CdsUpdate.forLogicalDns(
1✔
369
          clusterName, dnsHostName, lrsServerInfo, maxConcurrentRequests,
370
          upstreamTlsContext, isHttp11ProxyAvailable, backendMetricPropagation));
371
    }
372
    return StructOrError.fromError(
×
373
        "Cluster " + clusterName + ": unsupported built-in discovery type: " + type);
374
  }
375

376
  static io.envoyproxy.envoy.config.cluster.v3.OutlierDetection validateOutlierDetection(
377
      io.envoyproxy.envoy.config.cluster.v3.OutlierDetection outlierDetection)
378
      throws ResourceInvalidException {
379
    if (outlierDetection.hasInterval()) {
1✔
380
      if (!Durations.isValid(outlierDetection.getInterval())) {
1✔
381
        throw new ResourceInvalidException("outlier_detection interval is not a valid Duration");
1✔
382
      }
383
      if (hasNegativeValues(outlierDetection.getInterval())) {
1✔
384
        throw new ResourceInvalidException("outlier_detection interval has a negative value");
1✔
385
      }
386
    }
387
    if (outlierDetection.hasBaseEjectionTime()) {
1✔
388
      if (!Durations.isValid(outlierDetection.getBaseEjectionTime())) {
1✔
389
        throw new ResourceInvalidException(
1✔
390
            "outlier_detection base_ejection_time is not a valid Duration");
391
      }
392
      if (hasNegativeValues(outlierDetection.getBaseEjectionTime())) {
1✔
393
        throw new ResourceInvalidException(
1✔
394
            "outlier_detection base_ejection_time has a negative value");
395
      }
396
    }
397
    if (outlierDetection.hasMaxEjectionTime()) {
1✔
398
      if (!Durations.isValid(outlierDetection.getMaxEjectionTime())) {
1✔
399
        throw new ResourceInvalidException(
1✔
400
            "outlier_detection max_ejection_time is not a valid Duration");
401
      }
402
      if (hasNegativeValues(outlierDetection.getMaxEjectionTime())) {
1✔
403
        throw new ResourceInvalidException(
1✔
404
            "outlier_detection max_ejection_time has a negative value");
405
      }
406
    }
407
    if (outlierDetection.hasMaxEjectionPercent()
1✔
408
        && outlierDetection.getMaxEjectionPercent().getValue() > 100) {
1✔
409
      throw new ResourceInvalidException(
1✔
410
          "outlier_detection max_ejection_percent is > 100");
411
    }
412
    if (outlierDetection.hasEnforcingSuccessRate()
1✔
413
        && outlierDetection.getEnforcingSuccessRate().getValue() > 100) {
1✔
414
      throw new ResourceInvalidException(
1✔
415
          "outlier_detection enforcing_success_rate is > 100");
416
    }
417
    if (outlierDetection.hasFailurePercentageThreshold()
1✔
418
        && outlierDetection.getFailurePercentageThreshold().getValue() > 100) {
1✔
419
      throw new ResourceInvalidException(
1✔
420
          "outlier_detection failure_percentage_threshold is > 100");
421
    }
422
    if (outlierDetection.hasEnforcingFailurePercentage()
1✔
423
        && outlierDetection.getEnforcingFailurePercentage().getValue() > 100) {
1✔
424
      throw new ResourceInvalidException(
1✔
425
          "outlier_detection enforcing_failure_percentage is > 100");
426
    }
427

428
    return outlierDetection;
1✔
429
  }
430

431
  static boolean hasNegativeValues(Duration duration) {
432
    return duration.getSeconds() < 0 || duration.getNanos() < 0;
1✔
433
  }
434

435
  @VisibleForTesting
436
  static io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
437
      validateUpstreamTlsContext(
438
      io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext upstreamTlsContext,
439
      Set<String> certProviderInstances)
440
      throws ResourceInvalidException {
441
    if (upstreamTlsContext.hasCommonTlsContext()) {
1✔
442
      validateCommonTlsContext(upstreamTlsContext.getCommonTlsContext(), certProviderInstances,
1✔
443
          false);
444
    } else {
445
      throw new ResourceInvalidException("common-tls-context is required in upstream-tls-context");
1✔
446
    }
447
    return upstreamTlsContext;
1✔
448
  }
449

450
  @VisibleForTesting
451
  static void validateCommonTlsContext(
452
      CommonTlsContext commonTlsContext, Set<String> certProviderInstances, boolean server)
453
      throws ResourceInvalidException {
454
    if (commonTlsContext.hasCustomHandshaker()) {
1✔
455
      throw new ResourceInvalidException(
1✔
456
          "common-tls-context with custom_handshaker is not supported");
457
    }
458
    if (commonTlsContext.hasTlsParams()) {
1✔
459
      throw new ResourceInvalidException("common-tls-context with tls_params is not supported");
1✔
460
    }
461
    if (commonTlsContext.hasValidationContextSdsSecretConfig()) {
1✔
462
      throw new ResourceInvalidException(
1✔
463
          "common-tls-context with validation_context_sds_secret_config is not supported");
464
    }
465
    String certInstanceName = getIdentityCertInstanceName(commonTlsContext);
1✔
466
    if (certInstanceName == null) {
1✔
467
      if (server) {
1✔
468
        throw new ResourceInvalidException(
1✔
469
            "tls_certificate_provider_instance is required in downstream-tls-context");
470
      }
471
      if (commonTlsContext.getTlsCertificatesCount() > 0) {
1✔
472
        throw new ResourceInvalidException(
1✔
473
            "tls_certificate_provider_instance is unset");
474
      }
475
      if (commonTlsContext.getTlsCertificateSdsSecretConfigsCount() > 0) {
1✔
476
        throw new ResourceInvalidException(
1✔
477
            "tls_certificate_provider_instance is unset");
478
      }
479
    } else if (certProviderInstances == null || !certProviderInstances.contains(certInstanceName)) {
1✔
480
      throw new ResourceInvalidException(
1✔
481
          "CertificateProvider instance name '" + certInstanceName
482
              + "' not defined in the bootstrap file.");
483
    }
484
    String rootCaInstanceName = getRootCertInstanceName(commonTlsContext);
1✔
485
    if (rootCaInstanceName == null) {
1✔
486
      if (!server && (!enableSystemRootCerts
1✔
487
          || !CommonTlsContextUtil.isUsingSystemRootCerts(commonTlsContext))) {
1✔
488
        throw new ResourceInvalidException(
1✔
489
            "ca_certificate_provider_instance or system_root_certs is required in "
490
                + "upstream-tls-context");
491
      }
492
    } else {
493
      if (certProviderInstances == null || !certProviderInstances.contains(rootCaInstanceName)) {
1✔
494
        throw new ResourceInvalidException(
1✔
495
            "ca_certificate_provider_instance name '" + rootCaInstanceName
496
                + "' not defined in the bootstrap file.");
497
      }
498
      CertificateValidationContext certificateValidationContext = null;
1✔
499
      if (commonTlsContext.hasValidationContext()) {
1✔
500
        certificateValidationContext = commonTlsContext.getValidationContext();
1✔
501
      } else if (commonTlsContext.hasCombinedValidationContext() && commonTlsContext
1✔
502
          .getCombinedValidationContext().hasDefaultValidationContext()) {
1✔
503
        certificateValidationContext = commonTlsContext.getCombinedValidationContext()
1✔
504
            .getDefaultValidationContext();
1✔
505
      }
506
      if (certificateValidationContext != null) {
1✔
507
        @SuppressWarnings("deprecation") // gRFC A29 predates match_typed_subject_alt_names
508
        int matchSubjectAltNamesCount = certificateValidationContext.getMatchSubjectAltNamesCount();
1✔
509
        if (matchSubjectAltNamesCount > 0 && server) {
1✔
510
          throw new ResourceInvalidException(
1✔
511
              "match_subject_alt_names only allowed in upstream_tls_context");
512
        }
513
        if (certificateValidationContext.getVerifyCertificateSpkiCount() > 0) {
1✔
514
          throw new ResourceInvalidException(
1✔
515
              "verify_certificate_spki in default_validation_context is not supported");
516
        }
517
        if (certificateValidationContext.getVerifyCertificateHashCount() > 0) {
1✔
518
          throw new ResourceInvalidException(
1✔
519
              "verify_certificate_hash in default_validation_context is not supported");
520
        }
521
        if (certificateValidationContext.hasRequireSignedCertificateTimestamp()) {
1✔
522
          throw new ResourceInvalidException(
1✔
523
              "require_signed_certificate_timestamp in default_validation_context is not "
524
                  + "supported");
525
        }
526
        if (certificateValidationContext.hasCrl()) {
1✔
527
          throw new ResourceInvalidException("crl in default_validation_context is not supported");
1✔
528
        }
529
        if (certificateValidationContext.hasCustomValidatorConfig()) {
1✔
530
          throw new ResourceInvalidException(
1✔
531
              "custom_validator_config in default_validation_context is not supported");
532
        }
533
      }
534
    }
535
  }
1✔
536

537
  private static String getIdentityCertInstanceName(CommonTlsContext commonTlsContext) {
538
    if (commonTlsContext.hasTlsCertificateProviderInstance()) {
1✔
539
      return commonTlsContext.getTlsCertificateProviderInstance().getInstanceName();
1✔
540
    }
541
    // Fall back to deprecated field (field 11) for backward compatibility with Istio
542
    @SuppressWarnings("deprecation")
543
    String instanceName = commonTlsContext.hasTlsCertificateCertificateProviderInstance()
1✔
544
        ? commonTlsContext.getTlsCertificateCertificateProviderInstance().getInstanceName()
1✔
545
        : null;
1✔
546
    return instanceName;
1✔
547
  }
548

549
  private static String getRootCertInstanceName(CommonTlsContext commonTlsContext) {
550
    if (commonTlsContext.hasValidationContext()) {
1✔
551
      if (commonTlsContext.getValidationContext().hasCaCertificateProviderInstance()) {
1✔
552
        return commonTlsContext.getValidationContext().getCaCertificateProviderInstance()
1✔
553
            .getInstanceName();
1✔
554
      }
555
    } else if (commonTlsContext.hasCombinedValidationContext()) {
1✔
556
      CommonTlsContext.CombinedCertificateValidationContext combinedCertificateValidationContext
1✔
557
          = commonTlsContext.getCombinedValidationContext();
1✔
558
      if (combinedCertificateValidationContext.hasDefaultValidationContext()
1✔
559
          && combinedCertificateValidationContext.getDefaultValidationContext()
1✔
560
          .hasCaCertificateProviderInstance()) {
1✔
561
        return combinedCertificateValidationContext.getDefaultValidationContext()
1✔
562
            .getCaCertificateProviderInstance().getInstanceName();
1✔
563
      }
564
      // Fall back to deprecated field (field 4) in CombinedValidationContext
565
      @SuppressWarnings("deprecation")
566
      String instanceName = combinedCertificateValidationContext
567
          .hasValidationContextCertificateProviderInstance()
1✔
568
          ? combinedCertificateValidationContext.getValidationContextCertificateProviderInstance()
1✔
569
              .getInstanceName()
1✔
570
          : null;
1✔
571
      if (instanceName != null) {
1✔
572
        return instanceName;
1✔
573
      }
574
    }
575
    return null;
1✔
576
  }
577

578
  /** xDS resource update for cluster-level configuration. */
579
  @AutoValue
580
  abstract static class CdsUpdate implements ResourceUpdate {
1✔
581
    abstract String clusterName();
582

583
    abstract ClusterType clusterType();
584

585
    abstract ImmutableMap<String, ?> lbPolicyConfig();
586

587
    // Only valid if lbPolicy is "ring_hash_experimental".
588
    abstract long minRingSize();
589

590
    // Only valid if lbPolicy is "ring_hash_experimental".
591
    abstract long maxRingSize();
592

593
    // Only valid if lbPolicy is "least_request_experimental".
594
    abstract int choiceCount();
595

596
    // Alternative resource name to be used in EDS requests.
597
    /// Only valid for EDS cluster.
598
    @Nullable
599
    abstract String edsServiceName();
600

601
    // Corresponding DNS name to be used if upstream endpoints of the cluster is resolvable
602
    // via DNS.
603
    // Only valid for LOGICAL_DNS cluster.
604
    @Nullable
605
    abstract String dnsHostName();
606

607
    // Load report server info for reporting loads via LRS.
608
    // Only valid for EDS or LOGICAL_DNS cluster.
609
    @Nullable
610
    abstract ServerInfo lrsServerInfo();
611

612
    // Max number of concurrent requests can be sent to this cluster.
613
    // Only valid for EDS or LOGICAL_DNS cluster.
614
    @Nullable
615
    abstract Long maxConcurrentRequests();
616

617
    // TLS context used to connect to connect to this cluster.
618
    // Only valid for EDS or LOGICAL_DNS cluster.
619
    @Nullable
620
    abstract UpstreamTlsContext upstreamTlsContext();
621

622
    abstract boolean isHttp11ProxyAvailable();
623

624
    // List of underlying clusters making of this aggregate cluster.
625
    // Only valid for AGGREGATE cluster.
626
    @Nullable
627
    abstract ImmutableList<String> prioritizedClusterNames();
628

629
    // Outlier detection configuration.
630
    @Nullable
631
    abstract OutlierDetection outlierDetection();
632

633
    abstract ImmutableMap<String, Struct> filterMetadata();
634

635
    abstract ImmutableMap<String, Object> parsedMetadata();
636

637
    @Nullable
638
    abstract BackendMetricPropagation backendMetricPropagation();
639

640
    private static Builder newBuilder(String clusterName) {
641
      return new AutoValue_XdsClusterResource_CdsUpdate.Builder()
1✔
642
          .clusterName(clusterName)
1✔
643
          .minRingSize(0)
1✔
644
          .maxRingSize(0)
1✔
645
          .choiceCount(0)
1✔
646
          .filterMetadata(ImmutableMap.of())
1✔
647
          .parsedMetadata(ImmutableMap.of())
1✔
648
          .isHttp11ProxyAvailable(false)
1✔
649
          .backendMetricPropagation(null);
1✔
650
    }
651

652
    static Builder forAggregate(String clusterName, List<String> prioritizedClusterNames) {
653
      checkNotNull(prioritizedClusterNames, "prioritizedClusterNames");
1✔
654
      return newBuilder(clusterName)
1✔
655
          .clusterType(ClusterType.AGGREGATE)
1✔
656
          .prioritizedClusterNames(ImmutableList.copyOf(prioritizedClusterNames));
1✔
657
    }
658

659
    static Builder forEds(String clusterName, @Nullable String edsServiceName,
660
                          @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests,
661
                          @Nullable UpstreamTlsContext upstreamTlsContext,
662
                          @Nullable OutlierDetection outlierDetection,
663
                          boolean isHttp11ProxyAvailable,
664
                          BackendMetricPropagation backendMetricPropagation) {
665
      return newBuilder(clusterName)
1✔
666
          .clusterType(ClusterType.EDS)
1✔
667
          .edsServiceName(edsServiceName)
1✔
668
          .lrsServerInfo(lrsServerInfo)
1✔
669
          .maxConcurrentRequests(maxConcurrentRequests)
1✔
670
          .upstreamTlsContext(upstreamTlsContext)
1✔
671
          .outlierDetection(outlierDetection)
1✔
672
          .isHttp11ProxyAvailable(isHttp11ProxyAvailable)
1✔
673
          .backendMetricPropagation(backendMetricPropagation);
1✔
674
    }
675

676
    static Builder forLogicalDns(String clusterName, String dnsHostName,
677
                                 @Nullable ServerInfo lrsServerInfo,
678
                                 @Nullable Long maxConcurrentRequests,
679
                                 @Nullable UpstreamTlsContext upstreamTlsContext,
680
                                 boolean isHttp11ProxyAvailable,
681
                                 BackendMetricPropagation backendMetricPropagation) {
682
      return newBuilder(clusterName)
1✔
683
          .clusterType(ClusterType.LOGICAL_DNS)
1✔
684
          .dnsHostName(dnsHostName)
1✔
685
          .lrsServerInfo(lrsServerInfo)
1✔
686
          .maxConcurrentRequests(maxConcurrentRequests)
1✔
687
          .upstreamTlsContext(upstreamTlsContext)
1✔
688
          .isHttp11ProxyAvailable(isHttp11ProxyAvailable)
1✔
689
          .backendMetricPropagation(backendMetricPropagation);
1✔
690
    }
691

692
    enum ClusterType {
1✔
693
      EDS, LOGICAL_DNS, AGGREGATE
1✔
694
    }
695

696
    enum LbPolicy {
×
697
      ROUND_ROBIN, RING_HASH, LEAST_REQUEST
×
698
    }
699

700
    // FIXME(chengyuanzhang): delete this after UpstreamTlsContext's toString() is fixed.
701
    @Override
702
    public final String toString() {
703
      return MoreObjects.toStringHelper(this)
1✔
704
          .add("clusterName", clusterName())
1✔
705
          .add("clusterType", clusterType())
1✔
706
          .add("lbPolicyConfig", lbPolicyConfig())
1✔
707
          .add("minRingSize", minRingSize())
1✔
708
          .add("maxRingSize", maxRingSize())
1✔
709
          .add("choiceCount", choiceCount())
1✔
710
          .add("edsServiceName", edsServiceName())
1✔
711
          .add("dnsHostName", dnsHostName())
1✔
712
          .add("lrsServerInfo", lrsServerInfo())
1✔
713
          .add("maxConcurrentRequests", maxConcurrentRequests())
1✔
714
          // Exclude upstreamTlsContext and outlierDetection as their string representations are
715
          // cumbersome.
716
          .add("prioritizedClusterNames", prioritizedClusterNames())
1✔
717
          .toString();
1✔
718
    }
719

720
    @AutoValue.Builder
721
    abstract static class Builder {
1✔
722
      // Private, use one of the static factory methods instead.
723
      protected abstract Builder clusterName(String clusterName);
724

725
      // Private, use one of the static factory methods instead.
726
      protected abstract Builder clusterType(ClusterType clusterType);
727

728
      protected abstract Builder lbPolicyConfig(ImmutableMap<String, ?> lbPolicyConfig);
729

730
      Builder roundRobinLbPolicy() {
731
        return this.lbPolicyConfig(ImmutableMap.of("round_robin", ImmutableMap.of()));
1✔
732
      }
733

734
      Builder ringHashLbPolicy(Long minRingSize, Long maxRingSize) {
735
        return this.lbPolicyConfig(ImmutableMap.of("ring_hash_experimental",
×
736
            ImmutableMap.of("minRingSize", minRingSize.doubleValue(), "maxRingSize",
×
737
                maxRingSize.doubleValue())));
×
738
      }
739

740
      Builder leastRequestLbPolicy(Integer choiceCount) {
741
        return this.lbPolicyConfig(ImmutableMap.of("least_request_experimental",
×
742
            ImmutableMap.of("choiceCount", choiceCount.doubleValue())));
×
743
      }
744

745
      // Private, use leastRequestLbPolicy(int).
746
      protected abstract Builder choiceCount(int choiceCount);
747

748
      // Private, use ringHashLbPolicy(long, long).
749
      protected abstract Builder minRingSize(long minRingSize);
750

751
      // Private, use ringHashLbPolicy(long, long).
752
      protected abstract Builder maxRingSize(long maxRingSize);
753

754
      // Private, use CdsUpdate.forEds() instead.
755
      protected abstract Builder edsServiceName(String edsServiceName);
756

757
      // Private, use CdsUpdate.forLogicalDns() instead.
758
      protected abstract Builder dnsHostName(String dnsHostName);
759

760
      // Private, use one of the static factory methods instead.
761
      protected abstract Builder lrsServerInfo(ServerInfo lrsServerInfo);
762

763
      // Private, use one of the static factory methods instead.
764
      protected abstract Builder maxConcurrentRequests(Long maxConcurrentRequests);
765

766
      protected abstract Builder isHttp11ProxyAvailable(boolean isHttp11ProxyAvailable);
767

768
      // Private, use one of the static factory methods instead.
769
      protected abstract Builder upstreamTlsContext(UpstreamTlsContext upstreamTlsContext);
770

771
      // Private, use CdsUpdate.forAggregate() instead.
772
      protected abstract Builder prioritizedClusterNames(List<String> prioritizedClusterNames);
773

774
      protected abstract Builder outlierDetection(OutlierDetection outlierDetection);
775

776
      protected abstract Builder filterMetadata(ImmutableMap<String, Struct> filterMetadata);
777

778
      protected abstract Builder parsedMetadata(ImmutableMap<String, Object> parsedMetadata);
779

780
      protected abstract Builder backendMetricPropagation(
781
          BackendMetricPropagation backendMetricPropagation);
782

783
      abstract CdsUpdate build();
784
    }
785
  }
786
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc