xds: Parsing xDS Cluster Metadata (#11741)

This commit is contained in:
MV Shiva 2025-01-07 10:03:13 +05:30 committed by GitHub
parent 4222f77587
commit 1edc4d84d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 470 additions and 1 deletions

View File

@ -22,6 +22,7 @@ import com.google.common.primitives.UnsignedLongs;
import com.google.protobuf.Any;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.Audience;
import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.GcpAuthnFilterConfig;
import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.TokenCacheConfig;
import io.grpc.CallCredentials;
@ -36,6 +37,7 @@ import io.grpc.MethodDescriptor;
import io.grpc.Status;
import io.grpc.auth.MoreCallCredentials;
import io.grpc.xds.Filter.ClientInterceptorBuilder;
import io.grpc.xds.MetadataRegistry.MetadataValueParser;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
@ -219,4 +221,23 @@ final class GcpAuthenticationFilter implements Filter, ClientInterceptorBuilder
return cache.computeIfAbsent(key, create);
}
}
static class AudienceMetadataParser implements MetadataValueParser {
@Override
public String getTypeUrl() {
return "type.googleapis.com/envoy.extensions.filters.http.gcp_authn.v3.Audience";
}
@Override
public String parse(Any any) throws InvalidProtocolBufferException {
Audience audience = any.unpack(Audience.class);
String url = audience.getUrl();
if (url.isEmpty()) {
throw new InvalidProtocolBufferException(
"Audience URL is empty. Metadata value must contain a valid URL.");
}
return url;
}
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright 2024 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.xds;
import com.google.common.annotations.VisibleForTesting;
import com.google.protobuf.Any;
import com.google.protobuf.InvalidProtocolBufferException;
import io.grpc.xds.GcpAuthenticationFilter.AudienceMetadataParser;
import java.util.HashMap;
import java.util.Map;
/**
* Registry for parsing cluster metadata values.
*
* <p>This class maintains a mapping of type URLs to {@link MetadataValueParser} instances,
* allowing for the parsing of different metadata types.
*/
final class MetadataRegistry {
private static final MetadataRegistry INSTANCE = new MetadataRegistry();
private final Map<String, MetadataValueParser> supportedParsers = new HashMap<>();
private MetadataRegistry() {
registerParser(new AudienceMetadataParser());
}
static MetadataRegistry getInstance() {
return INSTANCE;
}
MetadataValueParser findParser(String typeUrl) {
return supportedParsers.get(typeUrl);
}
@VisibleForTesting
void registerParser(MetadataValueParser parser) {
supportedParsers.put(parser.getTypeUrl(), parser);
}
void removeParser(MetadataValueParser parser) {
supportedParsers.remove(parser.getTypeUrl());
}
interface MetadataValueParser {
String getTypeUrl();
/**
* Parses the given {@link Any} object into a specific metadata value.
*
* @param any the {@link Any} object to parse.
* @return the parsed metadata value.
* @throws InvalidProtocolBufferException if the parsing fails.
*/
Object parse(Any any) throws InvalidProtocolBufferException;
}
}

View File

@ -25,6 +25,7 @@ import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.protobuf.Any;
import com.google.protobuf.Duration;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
@ -32,6 +33,7 @@ import com.google.protobuf.Struct;
import com.google.protobuf.util.Durations;
import io.envoyproxy.envoy.config.cluster.v3.CircuitBreakers.Thresholds;
import io.envoyproxy.envoy.config.cluster.v3.Cluster;
import io.envoyproxy.envoy.config.core.v3.Metadata;
import io.envoyproxy.envoy.config.core.v3.RoutingPriority;
import io.envoyproxy.envoy.config.core.v3.SocketAddress;
import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment;
@ -44,12 +46,15 @@ import io.grpc.internal.ServiceConfigUtil;
import io.grpc.internal.ServiceConfigUtil.LbConfig;
import io.grpc.xds.EnvoyServerProtoData.OutlierDetection;
import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext;
import io.grpc.xds.MetadataRegistry.MetadataValueParser;
import io.grpc.xds.XdsClusterResource.CdsUpdate;
import io.grpc.xds.client.XdsClient.ResourceUpdate;
import io.grpc.xds.client.XdsResourceType;
import io.grpc.xds.internal.ProtobufJsonConverter;
import io.grpc.xds.internal.security.CommonTlsContextUtil;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
@ -171,9 +176,62 @@ class XdsClusterResource extends XdsResourceType<CdsUpdate> {
updateBuilder.filterMetadata(
ImmutableMap.copyOf(cluster.getMetadata().getFilterMetadataMap()));
try {
ImmutableMap<String, Object> parsedFilterMetadata =
parseClusterMetadata(cluster.getMetadata());
updateBuilder.parsedMetadata(parsedFilterMetadata);
} catch (InvalidProtocolBufferException e) {
throw new ResourceInvalidException(
"Failed to parse xDS filter metadata for cluster '" + cluster.getName() + "': "
+ e.getMessage(), e);
}
return updateBuilder.build();
}
/**
* Parses cluster metadata into a structured map.
*
* <p>Values in {@code typed_filter_metadata} take precedence over
* {@code filter_metadata} when keys overlap, following Envoy API behavior. See
* <a href="https://github.com/envoyproxy/envoy/blob/main/api/envoy/config/core/v3/base.proto#L217-L259">
* Envoy metadata documentation </a> for details.
*
* @param metadata the {@link Metadata} containing the fields to parse.
* @return an immutable map of parsed metadata.
* @throws InvalidProtocolBufferException if parsing {@code typed_filter_metadata} fails.
*/
private static ImmutableMap<String, Object> parseClusterMetadata(Metadata metadata)
throws InvalidProtocolBufferException {
ImmutableMap.Builder<String, Object> parsedMetadata = ImmutableMap.builder();
MetadataRegistry registry = MetadataRegistry.getInstance();
// Process typed_filter_metadata
for (Map.Entry<String, Any> entry : metadata.getTypedFilterMetadataMap().entrySet()) {
String key = entry.getKey();
Any value = entry.getValue();
MetadataValueParser parser = registry.findParser(value.getTypeUrl());
if (parser != null) {
Object parsedValue = parser.parse(value);
parsedMetadata.put(key, parsedValue);
}
}
// building once to reuse in the next loop
ImmutableMap<String, Object> intermediateParsedMetadata = parsedMetadata.build();
// Process filter_metadata for remaining keys
for (Map.Entry<String, Struct> entry : metadata.getFilterMetadataMap().entrySet()) {
String key = entry.getKey();
if (!intermediateParsedMetadata.containsKey(key)) {
Struct structValue = entry.getValue();
Object jsonValue = ProtobufJsonConverter.convertToJson(structValue);
parsedMetadata.put(key, jsonValue);
}
}
return parsedMetadata.build();
}
private static StructOrError<CdsUpdate.Builder> parseAggregateCluster(Cluster cluster) {
String clusterName = cluster.getName();
Cluster.CustomClusterType customType = cluster.getClusterType();
@ -573,13 +631,16 @@ class XdsClusterResource extends XdsResourceType<CdsUpdate> {
abstract ImmutableMap<String, Struct> filterMetadata();
abstract ImmutableMap<String, Object> parsedMetadata();
private static Builder newBuilder(String clusterName) {
return new AutoValue_XdsClusterResource_CdsUpdate.Builder()
.clusterName(clusterName)
.minRingSize(0)
.maxRingSize(0)
.choiceCount(0)
.filterMetadata(ImmutableMap.of());
.filterMetadata(ImmutableMap.of())
.parsedMetadata(ImmutableMap.of());
}
static Builder forAggregate(String clusterName, List<String> prioritizedClusterNames) {
@ -698,6 +759,8 @@ class XdsClusterResource extends XdsResourceType<CdsUpdate> {
protected abstract Builder filterMetadata(ImmutableMap<String, Struct> filterMetadata);
protected abstract Builder parsedMetadata(ImmutableMap<String, Object> parsedMetadata);
abstract CdsUpdate build();
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright 2024 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.xds.internal;
import com.google.protobuf.Struct;
import com.google.protobuf.Value;
import io.grpc.Internal;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Converter for Protobuf {@link Struct} to JSON-like {@link Map}.
*/
@Internal
public final class ProtobufJsonConverter {
private ProtobufJsonConverter() {}
public static Map<String, Object> convertToJson(Struct struct) {
Map<String, Object> result = new HashMap<>();
for (Map.Entry<String, Value> entry : struct.getFieldsMap().entrySet()) {
result.put(entry.getKey(), convertValue(entry.getValue()));
}
return result;
}
private static Object convertValue(Value value) {
switch (value.getKindCase()) {
case STRUCT_VALUE:
return convertToJson(value.getStructValue());
case LIST_VALUE:
return value.getListValue().getValuesList().stream()
.map(ProtobufJsonConverter::convertValue)
.collect(Collectors.toList());
case NUMBER_VALUE:
return value.getNumberValue();
case STRING_VALUE:
return value.getStringValue();
case BOOL_VALUE:
return value.getBoolValue();
case NULL_VALUE:
return null;
default:
throw new IllegalArgumentException("Unknown Value type: " + value.getKindCase());
}
}
}

View File

@ -50,6 +50,7 @@ import io.envoyproxy.envoy.config.core.v3.ConfigSource;
import io.envoyproxy.envoy.config.core.v3.DataSource;
import io.envoyproxy.envoy.config.core.v3.HttpProtocolOptions;
import io.envoyproxy.envoy.config.core.v3.Locality;
import io.envoyproxy.envoy.config.core.v3.Metadata;
import io.envoyproxy.envoy.config.core.v3.PathConfigSource;
import io.envoyproxy.envoy.config.core.v3.RuntimeFractionalPercent;
import io.envoyproxy.envoy.config.core.v3.SelfConfigSource;
@ -84,6 +85,7 @@ import io.envoyproxy.envoy.config.route.v3.WeightedCluster;
import io.envoyproxy.envoy.extensions.filters.common.fault.v3.FaultDelay;
import io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort;
import io.envoyproxy.envoy.extensions.filters.http.fault.v3.HTTPFault;
import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.Audience;
import io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBACPerRoute;
import io.envoyproxy.envoy.extensions.filters.http.router.v3.Router;
import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager;
@ -127,6 +129,7 @@ import io.grpc.xds.ClusterSpecifierPlugin.PluginConfig;
import io.grpc.xds.Endpoints.LbEndpoint;
import io.grpc.xds.Endpoints.LocalityLbEndpoints;
import io.grpc.xds.Filter.FilterConfig;
import io.grpc.xds.MetadataRegistry.MetadataValueParser;
import io.grpc.xds.RouteLookupServiceClusterSpecifierPlugin.RlsPluginConfig;
import io.grpc.xds.VirtualHost.Route;
import io.grpc.xds.VirtualHost.Route.RouteAction;
@ -2341,6 +2344,173 @@ public class GrpcXdsClientImplDataTest {
LoadBalancerRegistry.getDefaultRegistry());
}
@Test
public void processCluster_parsesMetadata()
throws ResourceInvalidException, InvalidProtocolBufferException {
MetadataRegistry metadataRegistry = MetadataRegistry.getInstance();
MetadataValueParser testParser =
new MetadataValueParser() {
@Override
public String getTypeUrl() {
return "type.googleapis.com/test.Type";
}
@Override
public Object parse(Any value) {
assertThat(value.getValue().toStringUtf8()).isEqualTo("test");
return value.getValue().toStringUtf8() + "_processed";
}
};
metadataRegistry.registerParser(testParser);
Any typedFilterMetadata = Any.newBuilder()
.setTypeUrl("type.googleapis.com/test.Type")
.setValue(ByteString.copyFromUtf8("test"))
.build();
Struct filterMetadata = Struct.newBuilder()
.putFields("key1", Value.newBuilder().setStringValue("value1").build())
.putFields("key2", Value.newBuilder().setNumberValue(42).build())
.build();
Metadata metadata = Metadata.newBuilder()
.putTypedFilterMetadata("TYPED_FILTER_METADATA", typedFilterMetadata)
.putFilterMetadata("FILTER_METADATA", filterMetadata)
.build();
Cluster cluster = Cluster.newBuilder()
.setName("cluster-foo.googleapis.com")
.setType(DiscoveryType.EDS)
.setEdsClusterConfig(
EdsClusterConfig.newBuilder()
.setEdsConfig(
ConfigSource.newBuilder()
.setAds(AggregatedConfigSource.getDefaultInstance()))
.setServiceName("service-foo.googleapis.com"))
.setLbPolicy(LbPolicy.ROUND_ROBIN)
.setMetadata(metadata)
.build();
CdsUpdate update = XdsClusterResource.processCluster(
cluster, null, LRS_SERVER_INFO,
LoadBalancerRegistry.getDefaultRegistry());
ImmutableMap<String, Object> expectedParsedMetadata = ImmutableMap.of(
"TYPED_FILTER_METADATA", "test_processed",
"FILTER_METADATA", ImmutableMap.of(
"key1", "value1",
"key2", 42.0));
assertThat(update.parsedMetadata()).isEqualTo(expectedParsedMetadata);
metadataRegistry.removeParser(testParser);
}
@Test
public void processCluster_parsesAudienceMetadata()
throws ResourceInvalidException, InvalidProtocolBufferException {
MetadataRegistry.getInstance();
Audience audience = Audience.newBuilder()
.setUrl("https://example.com")
.build();
Any audienceMetadata = Any.newBuilder()
.setTypeUrl("type.googleapis.com/envoy.extensions.filters.http.gcp_authn.v3.Audience")
.setValue(audience.toByteString())
.build();
Struct filterMetadata = Struct.newBuilder()
.putFields("key1", Value.newBuilder().setStringValue("value1").build())
.putFields("key2", Value.newBuilder().setNumberValue(42).build())
.build();
Metadata metadata = Metadata.newBuilder()
.putTypedFilterMetadata("AUDIENCE_METADATA", audienceMetadata)
.putFilterMetadata("FILTER_METADATA", filterMetadata)
.build();
Cluster cluster = Cluster.newBuilder()
.setName("cluster-foo.googleapis.com")
.setType(DiscoveryType.EDS)
.setEdsClusterConfig(
EdsClusterConfig.newBuilder()
.setEdsConfig(
ConfigSource.newBuilder()
.setAds(AggregatedConfigSource.getDefaultInstance()))
.setServiceName("service-foo.googleapis.com"))
.setLbPolicy(LbPolicy.ROUND_ROBIN)
.setMetadata(metadata)
.build();
CdsUpdate update = XdsClusterResource.processCluster(
cluster, null, LRS_SERVER_INFO,
LoadBalancerRegistry.getDefaultRegistry());
ImmutableMap<String, Object> expectedParsedMetadata = ImmutableMap.of(
"AUDIENCE_METADATA", "https://example.com",
"FILTER_METADATA", ImmutableMap.of(
"key1", "value1",
"key2", 42.0));
assertThat(update.parsedMetadata()).isEqualTo(expectedParsedMetadata);
}
@Test
public void processCluster_metadataKeyCollision_resolvesToTypedMetadata()
throws ResourceInvalidException, InvalidProtocolBufferException {
MetadataRegistry metadataRegistry = MetadataRegistry.getInstance();
MetadataValueParser testParser =
new MetadataValueParser() {
@Override
public String getTypeUrl() {
return "type.googleapis.com/test.Type";
}
@Override
public Object parse(Any value) {
return "typedMetadataValue";
}
};
metadataRegistry.registerParser(testParser);
Any typedFilterMetadata = Any.newBuilder()
.setTypeUrl("type.googleapis.com/test.Type")
.setValue(ByteString.copyFromUtf8("test"))
.build();
Struct filterMetadata = Struct.newBuilder()
.putFields("key1", Value.newBuilder().setStringValue("filterMetadataValue").build())
.build();
Metadata metadata = Metadata.newBuilder()
.putTypedFilterMetadata("key1", typedFilterMetadata)
.putFilterMetadata("key1", filterMetadata)
.build();
Cluster cluster = Cluster.newBuilder()
.setName("cluster-foo.googleapis.com")
.setType(DiscoveryType.EDS)
.setEdsClusterConfig(
EdsClusterConfig.newBuilder()
.setEdsConfig(
ConfigSource.newBuilder()
.setAds(AggregatedConfigSource.getDefaultInstance()))
.setServiceName("service-foo.googleapis.com"))
.setLbPolicy(LbPolicy.ROUND_ROBIN)
.setMetadata(metadata)
.build();
CdsUpdate update = XdsClusterResource.processCluster(
cluster, null, LRS_SERVER_INFO,
LoadBalancerRegistry.getDefaultRegistry());
ImmutableMap<String, Object> expectedParsedMetadata = ImmutableMap.of(
"key1", "typedMetadataValue");
assertThat(update.parsedMetadata()).isEqualTo(expectedParsedMetadata);
metadataRegistry.removeParser(testParser);
}
@Test
public void parseServerSideListener_invalidTrafficDirection() throws ResourceInvalidException {
Listener listener =

View File

@ -0,0 +1,83 @@
/*
* Copyright 2024 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.xds.internal;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import com.google.common.collect.ImmutableMap;
import com.google.protobuf.ListValue;
import com.google.protobuf.Struct;
import com.google.protobuf.Value;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public class ProtobufJsonConverterTest {
@Test
public void testEmptyStruct() {
Struct emptyStruct = Struct.newBuilder().build();
Map<String, Object> result = ProtobufJsonConverter.convertToJson(emptyStruct);
assertThat(result).isEmpty();
}
@Test
public void testStructWithValues() {
Struct struct = Struct.newBuilder()
.putFields("stringKey", Value.newBuilder().setStringValue("stringValue").build())
.putFields("numberKey", Value.newBuilder().setNumberValue(123.45).build())
.putFields("boolKey", Value.newBuilder().setBoolValue(true).build())
.putFields("nullKey", Value.newBuilder().setNullValueValue(0).build())
.putFields("structKey", Value.newBuilder()
.setStructValue(Struct.newBuilder()
.putFields("nestedKey", Value.newBuilder().setStringValue("nestedValue").build())
.build())
.build())
.putFields("listKey", Value.newBuilder()
.setListValue(ListValue.newBuilder()
.addValues(Value.newBuilder().setNumberValue(1).build())
.addValues(Value.newBuilder().setStringValue("two").build())
.addValues(Value.newBuilder().setBoolValue(false).build())
.build())
.build())
.build();
Map<String, Object> result = ProtobufJsonConverter.convertToJson(struct);
Map<String, Object> goldenResult = new HashMap<>();
goldenResult.put("stringKey", "stringValue");
goldenResult.put("numberKey", 123.45);
goldenResult.put("boolKey", true);
goldenResult.put("nullKey", null);
goldenResult.put("structKey", ImmutableMap.of("nestedKey", "nestedValue"));
goldenResult.put("listKey", Arrays.asList(1.0, "two", false));
assertEquals(goldenResult, result);
}
@Test(expected = IllegalArgumentException.class)
public void testUnknownValueType() {
Value unknownValue = Value.newBuilder().build(); // Default instance with no kind case set.
ProtobufJsonConverter.convertToJson(
Struct.newBuilder().putFields("unknownKey", unknownValue).build());
}
}