Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

xds: Parsing xDS Cluster Metadata #11741

Merged
merged 6 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions xds/src/main/java/io/grpc/xds/ClusterMetadataRegistry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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;
import javax.annotation.Nullable;

/**
* Registry for parsing cluster metadata values.
*
* <p>This class maintains a mapping of type URLs to {@link ClusterMetadataValueParser} instances,
* allowing for the parsing of different metadata types.
*/
final class ClusterMetadataRegistry {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't have "Cluster" in its name. It is for all xds metadata.

private static final ClusterMetadataRegistry INSTANCE = new ClusterMetadataRegistry();

private final Map<String, ClusterMetadataValueParser> supportedParsers = new HashMap<>();

private ClusterMetadataRegistry() {
registerParser(new AudienceMetadataParser());
}

static ClusterMetadataRegistry getInstance() {
return INSTANCE;
}

@Nullable
ClusterMetadataValueParser findParser(String typeUrl) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have a @Nullable annotation as the underlying get(key) method could return null if the key (typeUrl) is not found in the HashMap

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Nullable is not required in grpc-java. It is inconsistent, and without a null checker, it is nothing more than documentation.

return supportedParsers.get(typeUrl);
}

@VisibleForTesting
void registerParser(ClusterMetadataValueParser parser) {
supportedParsers.put(parser.getTypeUrl(), parser);
}

void removeParser(ClusterMetadataValueParser parser) {
supportedParsers.remove(parser.getTypeUrl());
}

interface ClusterMetadataValueParser {

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;
}
}
21 changes: 21 additions & 0 deletions xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
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;
Expand All @@ -35,6 +36,7 @@
import io.grpc.MethodDescriptor;
import io.grpc.Status;
import io.grpc.auth.MoreCallCredentials;
import io.grpc.xds.ClusterMetadataRegistry.ClusterMetadataValueParser;
import io.grpc.xds.Filter.ClientInterceptorBuilder;
import java.util.LinkedHashMap;
import java.util.Map;
Expand Down Expand Up @@ -219,4 +221,23 @@
return cache.computeIfAbsent(key, create);
}
}

static class AudienceMetadataParser implements ClusterMetadataValueParser {

@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(

Check warning on line 237 in xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java

View check run for this annotation

Codecov / codecov/patch

xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java#L237

Added line #L237 was not covered by tests
"Audience URL is empty. Metadata value must contain a valid URL.");
}
return url;
}
}
}
65 changes: 64 additions & 1 deletion xds/src/main/java/io/grpc/xds/XdsClusterResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@
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;
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;
Expand All @@ -42,14 +44,17 @@
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.ServiceConfigUtil;
import io.grpc.internal.ServiceConfigUtil.LbConfig;
import io.grpc.xds.ClusterMetadataRegistry.ClusterMetadataValueParser;
import io.grpc.xds.EnvoyServerProtoData.OutlierDetection;
import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext;
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;

Expand Down Expand Up @@ -169,9 +174,62 @@
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);
}

Check warning on line 186 in xds/src/main/java/io/grpc/xds/XdsClusterResource.java

View check run for this annotation

Codecov / codecov/patch

xds/src/main/java/io/grpc/xds/XdsClusterResource.java#L183-L186

Added lines #L183 - L186 were not covered by tests
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();

ClusterMetadataRegistry registry = ClusterMetadataRegistry.getInstance();
// Process typed_filter_metadata
for (Map.Entry<String, Any> entry : metadata.getTypedFilterMetadataMap().entrySet()) {
String key = entry.getKey();
Any value = entry.getValue();
ClusterMetadataValueParser 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();
Expand Down Expand Up @@ -571,13 +629,16 @@

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) {
Expand Down Expand Up @@ -696,6 +757,8 @@

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

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

abstract CdsUpdate build();
}
}
Expand Down
63 changes: 63 additions & 0 deletions xds/src/main/java/io/grpc/xds/internal/ProtobufJsonConverter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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;
import javax.annotation.Nullable;

/**
* 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;
}

@Nullable
private static Object convertValue(Value value) {
switch (value.getKindCase()) {
case STRUCT_VALUE:
return convertToJson(value.getStructValue());

Check warning on line 46 in xds/src/main/java/io/grpc/xds/internal/ProtobufJsonConverter.java

View check run for this annotation

Codecov / codecov/patch

xds/src/main/java/io/grpc/xds/internal/ProtobufJsonConverter.java#L46

Added line #L46 was not covered by tests
case LIST_VALUE:
return value.getListValue().getValuesList().stream()
.map(ProtobufJsonConverter::convertValue)
.collect(Collectors.toList());

Check warning on line 50 in xds/src/main/java/io/grpc/xds/internal/ProtobufJsonConverter.java

View check run for this annotation

Codecov / codecov/patch

xds/src/main/java/io/grpc/xds/internal/ProtobufJsonConverter.java#L48-L50

Added lines #L48 - L50 were not covered by tests
case NUMBER_VALUE:
return value.getNumberValue();
case STRING_VALUE:
return value.getStringValue();
case BOOL_VALUE:
return value.getBoolValue();

Check warning on line 56 in xds/src/main/java/io/grpc/xds/internal/ProtobufJsonConverter.java

View check run for this annotation

Codecov / codecov/patch

xds/src/main/java/io/grpc/xds/internal/ProtobufJsonConverter.java#L56

Added line #L56 was not covered by tests
case NULL_VALUE:
return null;

Check warning on line 58 in xds/src/main/java/io/grpc/xds/internal/ProtobufJsonConverter.java

View check run for this annotation

Codecov / codecov/patch

xds/src/main/java/io/grpc/xds/internal/ProtobufJsonConverter.java#L58

Added line #L58 was not covered by tests
default:
throw new IllegalArgumentException("Unknown Value type: " + value.getKindCase());

Check warning on line 60 in xds/src/main/java/io/grpc/xds/internal/ProtobufJsonConverter.java

View check run for this annotation

Codecov / codecov/patch

xds/src/main/java/io/grpc/xds/internal/ProtobufJsonConverter.java#L60

Added line #L60 was not covered by tests
}
}
}
Loading
Loading