mirror of https://github.com/grpc/grpc-java.git
xds: Parsing xDS Cluster Metadata (#11741)
This commit is contained in:
parent
4222f77587
commit
1edc4d84d4
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 =
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue