Skip to content

Commit 1edc4d8

Browse files
authoredJan 7, 2025··
xds: Parsing xDS Cluster Metadata (#11741)
1 parent 4222f77 commit 1edc4d8

File tree

6 files changed

+470
-1
lines changed

6 files changed

+470
-1
lines changed
 

‎xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java

+21
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.google.protobuf.Any;
2323
import com.google.protobuf.InvalidProtocolBufferException;
2424
import com.google.protobuf.Message;
25+
import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.Audience;
2526
import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.GcpAuthnFilterConfig;
2627
import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.TokenCacheConfig;
2728
import io.grpc.CallCredentials;
@@ -36,6 +37,7 @@
3637
import io.grpc.Status;
3738
import io.grpc.auth.MoreCallCredentials;
3839
import io.grpc.xds.Filter.ClientInterceptorBuilder;
40+
import io.grpc.xds.MetadataRegistry.MetadataValueParser;
3941
import java.util.LinkedHashMap;
4042
import java.util.Map;
4143
import java.util.concurrent.ScheduledExecutorService;
@@ -219,4 +221,23 @@ V getOrInsert(K key, Function<K, V> create) {
219221
return cache.computeIfAbsent(key, create);
220222
}
221223
}
224+
225+
static class AudienceMetadataParser implements MetadataValueParser {
226+
227+
@Override
228+
public String getTypeUrl() {
229+
return "type.googleapis.com/envoy.extensions.filters.http.gcp_authn.v3.Audience";
230+
}
231+
232+
@Override
233+
public String parse(Any any) throws InvalidProtocolBufferException {
234+
Audience audience = any.unpack(Audience.class);
235+
String url = audience.getUrl();
236+
if (url.isEmpty()) {
237+
throw new InvalidProtocolBufferException(
238+
"Audience URL is empty. Metadata value must contain a valid URL.");
239+
}
240+
return url;
241+
}
242+
}
222243
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2024 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 com.google.common.annotations.VisibleForTesting;
20+
import com.google.protobuf.Any;
21+
import com.google.protobuf.InvalidProtocolBufferException;
22+
import io.grpc.xds.GcpAuthenticationFilter.AudienceMetadataParser;
23+
import java.util.HashMap;
24+
import java.util.Map;
25+
26+
/**
27+
* Registry for parsing cluster metadata values.
28+
*
29+
* <p>This class maintains a mapping of type URLs to {@link MetadataValueParser} instances,
30+
* allowing for the parsing of different metadata types.
31+
*/
32+
final class MetadataRegistry {
33+
private static final MetadataRegistry INSTANCE = new MetadataRegistry();
34+
35+
private final Map<String, MetadataValueParser> supportedParsers = new HashMap<>();
36+
37+
private MetadataRegistry() {
38+
registerParser(new AudienceMetadataParser());
39+
}
40+
41+
static MetadataRegistry getInstance() {
42+
return INSTANCE;
43+
}
44+
45+
MetadataValueParser findParser(String typeUrl) {
46+
return supportedParsers.get(typeUrl);
47+
}
48+
49+
@VisibleForTesting
50+
void registerParser(MetadataValueParser parser) {
51+
supportedParsers.put(parser.getTypeUrl(), parser);
52+
}
53+
54+
void removeParser(MetadataValueParser parser) {
55+
supportedParsers.remove(parser.getTypeUrl());
56+
}
57+
58+
interface MetadataValueParser {
59+
60+
String getTypeUrl();
61+
62+
/**
63+
* Parses the given {@link Any} object into a specific metadata value.
64+
*
65+
* @param any the {@link Any} object to parse.
66+
* @return the parsed metadata value.
67+
* @throws InvalidProtocolBufferException if the parsing fails.
68+
*/
69+
Object parse(Any any) throws InvalidProtocolBufferException;
70+
}
71+
}

‎xds/src/main/java/io/grpc/xds/XdsClusterResource.java

+64-1
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@
2525
import com.google.common.base.Strings;
2626
import com.google.common.collect.ImmutableList;
2727
import com.google.common.collect.ImmutableMap;
28+
import com.google.protobuf.Any;
2829
import com.google.protobuf.Duration;
2930
import com.google.protobuf.InvalidProtocolBufferException;
3031
import com.google.protobuf.Message;
3132
import com.google.protobuf.Struct;
3233
import com.google.protobuf.util.Durations;
3334
import io.envoyproxy.envoy.config.cluster.v3.CircuitBreakers.Thresholds;
3435
import io.envoyproxy.envoy.config.cluster.v3.Cluster;
36+
import io.envoyproxy.envoy.config.core.v3.Metadata;
3537
import io.envoyproxy.envoy.config.core.v3.RoutingPriority;
3638
import io.envoyproxy.envoy.config.core.v3.SocketAddress;
3739
import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment;
@@ -44,12 +46,15 @@
4446
import io.grpc.internal.ServiceConfigUtil.LbConfig;
4547
import io.grpc.xds.EnvoyServerProtoData.OutlierDetection;
4648
import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext;
49+
import io.grpc.xds.MetadataRegistry.MetadataValueParser;
4750
import io.grpc.xds.XdsClusterResource.CdsUpdate;
4851
import io.grpc.xds.client.XdsClient.ResourceUpdate;
4952
import io.grpc.xds.client.XdsResourceType;
53+
import io.grpc.xds.internal.ProtobufJsonConverter;
5054
import io.grpc.xds.internal.security.CommonTlsContextUtil;
5155
import java.util.List;
5256
import java.util.Locale;
57+
import java.util.Map;
5358
import java.util.Set;
5459
import javax.annotation.Nullable;
5560

@@ -171,9 +176,62 @@ static CdsUpdate processCluster(Cluster cluster,
171176
updateBuilder.filterMetadata(
172177
ImmutableMap.copyOf(cluster.getMetadata().getFilterMetadataMap()));
173178

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

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();
207+
208+
MetadataRegistry registry = MetadataRegistry.getInstance();
209+
// Process typed_filter_metadata
210+
for (Map.Entry<String, Any> entry : metadata.getTypedFilterMetadataMap().entrySet()) {
211+
String key = entry.getKey();
212+
Any value = entry.getValue();
213+
MetadataValueParser parser = registry.findParser(value.getTypeUrl());
214+
if (parser != null) {
215+
Object parsedValue = parser.parse(value);
216+
parsedMetadata.put(key, parsedValue);
217+
}
218+
}
219+
// building once to reuse in the next loop
220+
ImmutableMap<String, Object> intermediateParsedMetadata = parsedMetadata.build();
221+
222+
// Process filter_metadata for remaining keys
223+
for (Map.Entry<String, Struct> entry : metadata.getFilterMetadataMap().entrySet()) {
224+
String key = entry.getKey();
225+
if (!intermediateParsedMetadata.containsKey(key)) {
226+
Struct structValue = entry.getValue();
227+
Object jsonValue = ProtobufJsonConverter.convertToJson(structValue);
228+
parsedMetadata.put(key, jsonValue);
229+
}
230+
}
231+
232+
return parsedMetadata.build();
233+
}
234+
177235
private static StructOrError<CdsUpdate.Builder> parseAggregateCluster(Cluster cluster) {
178236
String clusterName = cluster.getName();
179237
Cluster.CustomClusterType customType = cluster.getClusterType();
@@ -573,13 +631,16 @@ abstract static class CdsUpdate implements ResourceUpdate {
573631

574632
abstract ImmutableMap<String, Struct> filterMetadata();
575633

634+
abstract ImmutableMap<String, Object> parsedMetadata();
635+
576636
private static Builder newBuilder(String clusterName) {
577637
return new AutoValue_XdsClusterResource_CdsUpdate.Builder()
578638
.clusterName(clusterName)
579639
.minRingSize(0)
580640
.maxRingSize(0)
581641
.choiceCount(0)
582-
.filterMetadata(ImmutableMap.of());
642+
.filterMetadata(ImmutableMap.of())
643+
.parsedMetadata(ImmutableMap.of());
583644
}
584645

585646
static Builder forAggregate(String clusterName, List<String> prioritizedClusterNames) {
@@ -698,6 +759,8 @@ Builder leastRequestLbPolicy(Integer choiceCount) {
698759

699760
protected abstract Builder filterMetadata(ImmutableMap<String, Struct> filterMetadata);
700761

762+
protected abstract Builder parsedMetadata(ImmutableMap<String, Object> parsedMetadata);
763+
701764
abstract CdsUpdate build();
702765
}
703766
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2024 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.internal;
18+
19+
import com.google.protobuf.Struct;
20+
import com.google.protobuf.Value;
21+
import io.grpc.Internal;
22+
import java.util.HashMap;
23+
import java.util.Map;
24+
import java.util.stream.Collectors;
25+
26+
/**
27+
* Converter for Protobuf {@link Struct} to JSON-like {@link Map}.
28+
*/
29+
@Internal
30+
public final class ProtobufJsonConverter {
31+
private ProtobufJsonConverter() {}
32+
33+
public static Map<String, Object> convertToJson(Struct struct) {
34+
Map<String, Object> result = new HashMap<>();
35+
for (Map.Entry<String, Value> entry : struct.getFieldsMap().entrySet()) {
36+
result.put(entry.getKey(), convertValue(entry.getValue()));
37+
}
38+
return result;
39+
}
40+
41+
private static Object convertValue(Value value) {
42+
switch (value.getKindCase()) {
43+
case STRUCT_VALUE:
44+
return convertToJson(value.getStructValue());
45+
case LIST_VALUE:
46+
return value.getListValue().getValuesList().stream()
47+
.map(ProtobufJsonConverter::convertValue)
48+
.collect(Collectors.toList());
49+
case NUMBER_VALUE:
50+
return value.getNumberValue();
51+
case STRING_VALUE:
52+
return value.getStringValue();
53+
case BOOL_VALUE:
54+
return value.getBoolValue();
55+
case NULL_VALUE:
56+
return null;
57+
default:
58+
throw new IllegalArgumentException("Unknown Value type: " + value.getKindCase());
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)
Please sign in to comment.