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

grpc / grpc-java / #19624

07 Jan 2025 04:33AM CUT coverage: 88.565% (-0.003%) from 88.568%
#19624

push

github

web-flow
xds: Parsing xDS Cluster Metadata (#11741)

33683 of 38032 relevant lines covered (88.56%)

0.89 hits per line

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

92.83
/../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

22
import com.google.auto.value.AutoValue;
23
import com.google.common.annotations.VisibleForTesting;
24
import com.google.common.base.MoreObjects;
25
import com.google.common.base.Strings;
26
import com.google.common.collect.ImmutableList;
27
import com.google.common.collect.ImmutableMap;
28
import com.google.protobuf.Any;
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.Metadata;
37
import io.envoyproxy.envoy.config.core.v3.RoutingPriority;
38
import io.envoyproxy.envoy.config.core.v3.SocketAddress;
39
import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment;
40
import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateValidationContext;
41
import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext;
42
import io.grpc.LoadBalancerRegistry;
43
import io.grpc.NameResolver;
44
import io.grpc.internal.GrpcUtil;
45
import io.grpc.internal.ServiceConfigUtil;
46
import io.grpc.internal.ServiceConfigUtil.LbConfig;
47
import io.grpc.xds.EnvoyServerProtoData.OutlierDetection;
48
import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext;
49
import io.grpc.xds.MetadataRegistry.MetadataValueParser;
50
import io.grpc.xds.XdsClusterResource.CdsUpdate;
51
import io.grpc.xds.client.XdsClient.ResourceUpdate;
52
import io.grpc.xds.client.XdsResourceType;
53
import io.grpc.xds.internal.ProtobufJsonConverter;
54
import io.grpc.xds.internal.security.CommonTlsContextUtil;
55
import java.util.List;
56
import java.util.Locale;
57
import java.util.Map;
58
import java.util.Set;
59
import javax.annotation.Nullable;
60

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

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

84
  private static final XdsClusterResource instance = new XdsClusterResource();
1✔
85

86
  public static XdsClusterResource getInstance() {
87
    return instance;
1✔
88
  }
89

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

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

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

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

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

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

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

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

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

166
    // Validate the LB config by trying to parse it with the corresponding LB provider.
167
    LbConfig lbConfig = ServiceConfigUtil.unwrapLoadBalancingConfig(lbPolicyConfig);
1✔
168
    NameResolver.ConfigOrError configOrError = loadBalancerRegistry.getProvider(
1✔
169
        lbConfig.getPolicyName()).parseLoadBalancingPolicyConfig(
1✔
170
        lbConfig.getRawConfigValue());
1✔
171
    if (configOrError.getError() != null) {
1✔
172
      throw new ResourceInvalidException(structOrError.getErrorDetail());
×
173
    }
174

175
    updateBuilder.lbPolicyConfig(lbPolicyConfig);
1✔
176
    updateBuilder.filterMetadata(
1✔
177
        ImmutableMap.copyOf(cluster.getMetadata().getFilterMetadataMap()));
1✔
178

179
    try {
180
      ImmutableMap<String, Object> parsedFilterMetadata =
1✔
181
          parseClusterMetadata(cluster.getMetadata());
1✔
182
      updateBuilder.parsedMetadata(parsedFilterMetadata);
1✔
183
    } catch (InvalidProtocolBufferException e) {
×
184
      throw new ResourceInvalidException(
×
185
          "Failed to parse xDS filter metadata for cluster '" + cluster.getName() + "': "
×
186
              + e.getMessage(), e);
×
187
    }
1✔
188

189
    return updateBuilder.build();
1✔
190
  }
191

192
  /**
193
   * Parses cluster metadata into a structured map.
194
   *
195
   * <p>Values in {@code typed_filter_metadata} take precedence over
196
   * {@code filter_metadata} when keys overlap, following Envoy API behavior. See
197
   * <a href="https://github.com/envoyproxy/envoy/blob/main/api/envoy/config/core/v3/base.proto#L217-L259">
198
   *   Envoy metadata documentation </a> for details.
199
   *
200
   * @param metadata the {@link Metadata} containing the fields to parse.
201
   * @return an immutable map of parsed metadata.
202
   * @throws InvalidProtocolBufferException if parsing {@code typed_filter_metadata} fails.
203
   */
204
  private static ImmutableMap<String, Object> parseClusterMetadata(Metadata metadata)
205
      throws InvalidProtocolBufferException {
206
    ImmutableMap.Builder<String, Object> parsedMetadata = ImmutableMap.builder();
1✔
207

208
    MetadataRegistry registry = MetadataRegistry.getInstance();
1✔
209
    // Process typed_filter_metadata
210
    for (Map.Entry<String, Any> entry : metadata.getTypedFilterMetadataMap().entrySet()) {
1✔
211
      String key = entry.getKey();
1✔
212
      Any value = entry.getValue();
1✔
213
      MetadataValueParser parser = registry.findParser(value.getTypeUrl());
1✔
214
      if (parser != null) {
1✔
215
        Object parsedValue = parser.parse(value);
1✔
216
        parsedMetadata.put(key, parsedValue);
1✔
217
      }
218
    }
1✔
219
    // building once to reuse in the next loop
220
    ImmutableMap<String, Object> intermediateParsedMetadata = parsedMetadata.build();
1✔
221

222
    // Process filter_metadata for remaining keys
223
    for (Map.Entry<String, Struct> entry : metadata.getFilterMetadataMap().entrySet()) {
1✔
224
      String key = entry.getKey();
1✔
225
      if (!intermediateParsedMetadata.containsKey(key)) {
1✔
226
        Struct structValue = entry.getValue();
1✔
227
        Object jsonValue = ProtobufJsonConverter.convertToJson(structValue);
1✔
228
        parsedMetadata.put(key, jsonValue);
1✔
229
      }
230
    }
1✔
231

232
    return parsedMetadata.build();
1✔
233
  }
234

235
  private static StructOrError<CdsUpdate.Builder> parseAggregateCluster(Cluster cluster) {
236
    String clusterName = cluster.getName();
1✔
237
    Cluster.CustomClusterType customType = cluster.getClusterType();
1✔
238
    String typeName = customType.getName();
1✔
239
    if (!typeName.equals(AGGREGATE_CLUSTER_TYPE_NAME)) {
1✔
240
      return StructOrError.fromError(
×
241
          "Cluster " + clusterName + ": unsupported custom cluster type: " + typeName);
242
    }
243
    io.envoyproxy.envoy.extensions.clusters.aggregate.v3.ClusterConfig clusterConfig;
244
    try {
245
      clusterConfig = unpackCompatibleType(customType.getTypedConfig(),
1✔
246
          io.envoyproxy.envoy.extensions.clusters.aggregate.v3.ClusterConfig.class,
247
          TYPE_URL_CLUSTER_CONFIG, null);
248
    } catch (InvalidProtocolBufferException e) {
×
249
      return StructOrError.fromError("Cluster " + clusterName + ": malformed ClusterConfig: " + e);
×
250
    }
1✔
251
    return StructOrError.fromStruct(CdsUpdate.forAggregate(
1✔
252
        clusterName, clusterConfig.getClustersList()));
1✔
253
  }
254

255
  private static StructOrError<CdsUpdate.Builder> parseNonAggregateCluster(
256
      Cluster cluster, Set<String> certProviderInstances, ServerInfo serverInfo) {
257
    String clusterName = cluster.getName();
1✔
258
    ServerInfo lrsServerInfo = null;
1✔
259
    Long maxConcurrentRequests = null;
1✔
260
    UpstreamTlsContext upstreamTlsContext = null;
1✔
261
    OutlierDetection outlierDetection = null;
1✔
262
    if (cluster.hasLrsServer()) {
1✔
263
      if (!cluster.getLrsServer().hasSelf()) {
1✔
264
        return StructOrError.fromError(
×
265
            "Cluster " + clusterName + ": only support LRS for the same management server");
266
      }
267
      lrsServerInfo = serverInfo;
1✔
268
    }
269
    if (cluster.hasCircuitBreakers()) {
1✔
270
      List<Thresholds> thresholds = cluster.getCircuitBreakers().getThresholdsList();
1✔
271
      for (Thresholds threshold : thresholds) {
1✔
272
        if (threshold.getPriority() != RoutingPriority.DEFAULT) {
1✔
273
          continue;
1✔
274
        }
275
        if (threshold.hasMaxRequests()) {
1✔
276
          maxConcurrentRequests = Integer.toUnsignedLong(threshold.getMaxRequests().getValue());
1✔
277
        }
278
      }
1✔
279
    }
280
    if (cluster.getTransportSocketMatchesCount() > 0) {
1✔
281
      return StructOrError.fromError("Cluster " + clusterName
1✔
282
          + ": transport-socket-matches not supported.");
283
    }
284
    if (cluster.hasTransportSocket()) {
1✔
285
      if (!TRANSPORT_SOCKET_NAME_TLS.equals(cluster.getTransportSocket().getName())) {
1✔
286
        return StructOrError.fromError("transport-socket with name "
1✔
287
            + cluster.getTransportSocket().getName() + " not supported.");
1✔
288
      }
289
      try {
290
        upstreamTlsContext = UpstreamTlsContext.fromEnvoyProtoUpstreamTlsContext(
1✔
291
            validateUpstreamTlsContext(
1✔
292
                unpackCompatibleType(cluster.getTransportSocket().getTypedConfig(),
1✔
293
                io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext.class,
294
                TYPE_URL_UPSTREAM_TLS_CONTEXT, TYPE_URL_UPSTREAM_TLS_CONTEXT_V2),
295
                certProviderInstances));
296
      } catch (InvalidProtocolBufferException | ResourceInvalidException e) {
1✔
297
        return StructOrError.fromError(
1✔
298
            "Cluster " + clusterName + ": malformed UpstreamTlsContext: " + e);
299
      }
1✔
300
    }
301

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

312
    Cluster.DiscoveryType type = cluster.getType();
1✔
313
    if (type == Cluster.DiscoveryType.EDS) {
1✔
314
      String edsServiceName = null;
1✔
315
      io.envoyproxy.envoy.config.cluster.v3.Cluster.EdsClusterConfig edsClusterConfig =
1✔
316
          cluster.getEdsClusterConfig();
1✔
317
      if (!edsClusterConfig.getEdsConfig().hasAds()
1✔
318
          && ! edsClusterConfig.getEdsConfig().hasSelf()) {
1✔
319
        return StructOrError.fromError(
1✔
320
            "Cluster " + clusterName + ": field eds_cluster_config must be set to indicate to use"
321
                + " EDS over ADS or self ConfigSource");
322
      }
323
      // If the service_name field is set, that value will be used for the EDS request.
324
      if (!edsClusterConfig.getServiceName().isEmpty()) {
1✔
325
        edsServiceName = edsClusterConfig.getServiceName();
1✔
326
      }
327
      // edsServiceName is required if the CDS resource has an xdstp name.
328
      if ((edsServiceName == null) && clusterName.toLowerCase(Locale.ROOT).startsWith("xdstp:")) {
1✔
329
        return StructOrError.fromError(
1✔
330
            "EDS service_name must be set when Cluster resource has an xdstp name");
331
      }
332
      return StructOrError.fromStruct(CdsUpdate.forEds(
1✔
333
          clusterName, edsServiceName, lrsServerInfo, maxConcurrentRequests, upstreamTlsContext,
334
          outlierDetection));
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, upstreamTlsContext));
370
    }
371
    return StructOrError.fromError(
×
372
        "Cluster " + clusterName + ": unsupported built-in discovery type: " + type);
373
  }
374

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

427
    return outlierDetection;
1✔
428
  }
429

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

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

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

547
  private static String getIdentityCertInstanceName(CommonTlsContext commonTlsContext) {
548
    if (commonTlsContext.hasTlsCertificateProviderInstance()) {
1✔
549
      return commonTlsContext.getTlsCertificateProviderInstance().getInstanceName();
1✔
550
    } else if (commonTlsContext.hasTlsCertificateCertificateProviderInstance()) {
1✔
551
      return commonTlsContext.getTlsCertificateCertificateProviderInstance().getInstanceName();
1✔
552
    }
553
    return null;
1✔
554
  }
555

556
  private static String getRootCertInstanceName(CommonTlsContext commonTlsContext) {
557
    if (commonTlsContext.hasValidationContext()) {
1✔
558
      if (commonTlsContext.getValidationContext().hasCaCertificateProviderInstance()) {
1✔
559
        return commonTlsContext.getValidationContext().getCaCertificateProviderInstance()
1✔
560
            .getInstanceName();
1✔
561
      }
562
    } else if (commonTlsContext.hasCombinedValidationContext()) {
1✔
563
      CommonTlsContext.CombinedCertificateValidationContext combinedCertificateValidationContext
1✔
564
          = commonTlsContext.getCombinedValidationContext();
1✔
565
      if (combinedCertificateValidationContext.hasDefaultValidationContext()
1✔
566
          && combinedCertificateValidationContext.getDefaultValidationContext()
1✔
567
          .hasCaCertificateProviderInstance()) {
1✔
568
        return combinedCertificateValidationContext.getDefaultValidationContext()
×
569
            .getCaCertificateProviderInstance().getInstanceName();
×
570
      } else if (combinedCertificateValidationContext
1✔
571
          .hasValidationContextCertificateProviderInstance()) {
1✔
572
        return combinedCertificateValidationContext
1✔
573
            .getValidationContextCertificateProviderInstance().getInstanceName();
1✔
574
      }
575
    }
576
    return null;
1✔
577
  }
578

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

584
    abstract ClusterType clusterType();
585

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

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

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

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

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

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

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

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

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

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

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

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

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

636
    private static Builder newBuilder(String clusterName) {
637
      return new AutoValue_XdsClusterResource_CdsUpdate.Builder()
1✔
638
          .clusterName(clusterName)
1✔
639
          .minRingSize(0)
1✔
640
          .maxRingSize(0)
1✔
641
          .choiceCount(0)
1✔
642
          .filterMetadata(ImmutableMap.of())
1✔
643
          .parsedMetadata(ImmutableMap.of());
1✔
644
    }
645

646
    static Builder forAggregate(String clusterName, List<String> prioritizedClusterNames) {
647
      checkNotNull(prioritizedClusterNames, "prioritizedClusterNames");
1✔
648
      return newBuilder(clusterName)
1✔
649
          .clusterType(ClusterType.AGGREGATE)
1✔
650
          .prioritizedClusterNames(ImmutableList.copyOf(prioritizedClusterNames));
1✔
651
    }
652

653
    static Builder forEds(String clusterName, @Nullable String edsServiceName,
654
                          @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests,
655
                          @Nullable UpstreamTlsContext upstreamTlsContext,
656
                          @Nullable OutlierDetection outlierDetection) {
657
      return newBuilder(clusterName)
1✔
658
          .clusterType(ClusterType.EDS)
1✔
659
          .edsServiceName(edsServiceName)
1✔
660
          .lrsServerInfo(lrsServerInfo)
1✔
661
          .maxConcurrentRequests(maxConcurrentRequests)
1✔
662
          .upstreamTlsContext(upstreamTlsContext)
1✔
663
          .outlierDetection(outlierDetection);
1✔
664
    }
665

666
    static Builder forLogicalDns(String clusterName, String dnsHostName,
667
                                 @Nullable ServerInfo lrsServerInfo,
668
                                 @Nullable Long maxConcurrentRequests,
669
                                 @Nullable UpstreamTlsContext upstreamTlsContext) {
670
      return newBuilder(clusterName)
1✔
671
          .clusterType(ClusterType.LOGICAL_DNS)
1✔
672
          .dnsHostName(dnsHostName)
1✔
673
          .lrsServerInfo(lrsServerInfo)
1✔
674
          .maxConcurrentRequests(maxConcurrentRequests)
1✔
675
          .upstreamTlsContext(upstreamTlsContext);
1✔
676
    }
677

678
    enum ClusterType {
1✔
679
      EDS, LOGICAL_DNS, AGGREGATE
1✔
680
    }
681

682
    enum LbPolicy {
×
683
      ROUND_ROBIN, RING_HASH, LEAST_REQUEST
×
684
    }
685

686
    // FIXME(chengyuanzhang): delete this after UpstreamTlsContext's toString() is fixed.
687
    @Override
688
    public final String toString() {
689
      return MoreObjects.toStringHelper(this)
1✔
690
          .add("clusterName", clusterName())
1✔
691
          .add("clusterType", clusterType())
1✔
692
          .add("lbPolicyConfig", lbPolicyConfig())
1✔
693
          .add("minRingSize", minRingSize())
1✔
694
          .add("maxRingSize", maxRingSize())
1✔
695
          .add("choiceCount", choiceCount())
1✔
696
          .add("edsServiceName", edsServiceName())
1✔
697
          .add("dnsHostName", dnsHostName())
1✔
698
          .add("lrsServerInfo", lrsServerInfo())
1✔
699
          .add("maxConcurrentRequests", maxConcurrentRequests())
1✔
700
          // Exclude upstreamTlsContext and outlierDetection as their string representations are
701
          // cumbersome.
702
          .add("prioritizedClusterNames", prioritizedClusterNames())
1✔
703
          .toString();
1✔
704
    }
705

706
    @AutoValue.Builder
707
    abstract static class Builder {
1✔
708
      // Private, use one of the static factory methods instead.
709
      protected abstract Builder clusterName(String clusterName);
710

711
      // Private, use one of the static factory methods instead.
712
      protected abstract Builder clusterType(ClusterType clusterType);
713

714
      protected abstract Builder lbPolicyConfig(ImmutableMap<String, ?> lbPolicyConfig);
715

716
      Builder roundRobinLbPolicy() {
717
        return this.lbPolicyConfig(ImmutableMap.of("round_robin", ImmutableMap.of()));
1✔
718
      }
719

720
      Builder ringHashLbPolicy(Long minRingSize, Long maxRingSize) {
721
        return this.lbPolicyConfig(ImmutableMap.of("ring_hash_experimental",
1✔
722
            ImmutableMap.of("minRingSize", minRingSize.doubleValue(), "maxRingSize",
1✔
723
                maxRingSize.doubleValue())));
1✔
724
      }
725

726
      Builder leastRequestLbPolicy(Integer choiceCount) {
727
        return this.lbPolicyConfig(ImmutableMap.of("least_request_experimental",
1✔
728
            ImmutableMap.of("choiceCount", choiceCount.doubleValue())));
1✔
729
      }
730

731
      // Private, use leastRequestLbPolicy(int).
732
      protected abstract Builder choiceCount(int choiceCount);
733

734
      // Private, use ringHashLbPolicy(long, long).
735
      protected abstract Builder minRingSize(long minRingSize);
736

737
      // Private, use ringHashLbPolicy(long, long).
738
      protected abstract Builder maxRingSize(long maxRingSize);
739

740
      // Private, use CdsUpdate.forEds() instead.
741
      protected abstract Builder edsServiceName(String edsServiceName);
742

743
      // Private, use CdsUpdate.forLogicalDns() instead.
744
      protected abstract Builder dnsHostName(String dnsHostName);
745

746
      // Private, use one of the static factory methods instead.
747
      protected abstract Builder lrsServerInfo(ServerInfo lrsServerInfo);
748

749
      // Private, use one of the static factory methods instead.
750
      protected abstract Builder maxConcurrentRequests(Long maxConcurrentRequests);
751

752
      // Private, use one of the static factory methods instead.
753
      protected abstract Builder upstreamTlsContext(UpstreamTlsContext upstreamTlsContext);
754

755
      // Private, use CdsUpdate.forAggregate() instead.
756
      protected abstract Builder prioritizedClusterNames(List<String> prioritizedClusterNames);
757

758
      protected abstract Builder outlierDetection(OutlierDetection outlierDetection);
759

760
      protected abstract Builder filterMetadata(ImmutableMap<String, Struct> filterMetadata);
761

762
      protected abstract Builder parsedMetadata(ImmutableMap<String, Object> parsedMetadata);
763

764
      abstract CdsUpdate build();
765
    }
766
  }
767
}
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

© 2025 Coveralls, Inc