Skip to content

xds: CEL implementation#12770

Open
shivaspeaks wants to merge 4 commits intogrpc:masterfrom
shivaspeaks:cel
Open

xds: CEL implementation#12770
shivaspeaks wants to merge 4 commits intogrpc:masterfrom
shivaspeaks:cel

Conversation

@shivaspeaks
Copy link
Copy Markdown
Member

@shivaspeaks shivaspeaks commented Apr 22, 2026

This PR contains the core CEL evaluation logic and gRPC environment setup as defined in gRFC A106.

This is being carved out from #12640

@shivaspeaks
Copy link
Copy Markdown
Member Author

Pasting @AgraVator's comment here from #12640 (comment):

To support future attributes beyond just headers (like the aforementioned transport info or cluster metadata), we might want to consider a more pluggable way to populate MatchContext.

Currently, it has a fixed set of fields. A map-based approach or a registry of attribute providers might make it easier to add new attributes without changing the core MatchContext class every time.

@kannanjgithub
Copy link
Copy Markdown
Contributor

Currently, it has a fixed set of fields. A map-based approach or a registry of attribute providers might make it easier to add new attributes without changing the core MatchContext class every time.

I don't see what benefit is there in having a different class for each attribute like referer or user-agent. The list of Envoy request attributes is fixed and there is never a need to have some of them provided and other not.

Rather than think in terms of organizing our code in terms of plugin, we should think in terms of providing the evaluation context for the Cel runtime. With A106 supporting only request attributes for now we have this in GrpcCelEnvironment:

  @Override
  public Optional<Object> find(String name) {
    if (name.equals("request")) {
      return Optional.of(new LazyRequestMap(this));
    }
    return Optional.empty();
  }

that provides a map to Cel runtime for evaluating request.* variables. That map is backed by MatchContext (earlier implementation) that only knows about returning values for request.* keys. There is no need to further granularize how MatchContext implements that. One switch-case block will do.
Then for future unified matcher requirements you would check if name.equals("connection") and return a different map backed by something other than MatchContext. At that point we could may be rename MatchContext to RequestMatchContext and have a different one say ConnectionMatchContext the LazyRequestMap for connection variable evaluation uses.

Comment on lines +43 to +49
if (func == StandardFunction.STRING) {
return false;
}
if (func == StandardFunction.ADD) {
return !over.equals(AddOverload.ADD_STRING)
&& !over.equals(AddOverload.ADD_LIST);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Negative tests need to be added by creating AST and creating runtime with these disallowed expression types.

return null;
}
String headerName = ((String) key).toLowerCase(java.util.Locale.ROOT);
if ("te".equals(headerName)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Add some comment why.

sb.append(",");
}
first = false;
sb.append(BaseEncoding.base64().encode(value));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Add a .omitPadding()

BaseEncoding.base64().omitPadding().encode(value);

to keep it consistent with ext_proc filter that omits padding.


public MatchContext(Metadata metadata, @Nullable String path,
@Nullable String host, @Nullable String method,
@Nullable String id) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The gRFC says this should be metadata["x-request-id"]`.

private final String id;

public MatchContext(Metadata metadata, @Nullable String path,
@Nullable String host, @Nullable String method,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think path, host and method can be null. We should remove nullable and add checkNotNull assertions so it fails if the caller passes null values for any of them.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Introduces the foundational CEL (Common Expression Language) runtime integration for gRPC xDS matching (per gRFC A106), including a gRPC request/headers CEL environment, a string-extractor utility, and the build/shading wiring needed to ship CEL with the xDS artifact.

Changes:

  • Added CEL runtime utilities for allowed-reference validation, program execution, and request attribute resolution (CelCommon, CelStringExtractor, GrpcCelEnvironment, HeadersWrapper, MatchContext).
  • Added unit tests covering CEL environment behavior, disabled features, and string extraction behavior.
  • Integrated CEL dependencies into Gradle/Bazel builds and updated shading/relocation rules for the xDS artifact.

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
xds/src/main/java/io/grpc/xds/internal/matcher/CelCommon.java Defines CEL runtime options/functions and validates allowed references.
xds/src/main/java/io/grpc/xds/internal/matcher/CelStringExtractor.java Compiles and evaluates CEL expressions intended to produce strings (with optional default).
xds/src/main/java/io/grpc/xds/internal/matcher/GrpcCelEnvironment.java Provides request variable resolution for CEL evaluation based on gRPC request context.
xds/src/main/java/io/grpc/xds/internal/matcher/HeadersWrapper.java Exposes request metadata/pseudo-headers as a Map for CEL header lookup.
xds/src/main/java/io/grpc/xds/internal/matcher/MatchContext.java Simple container/builder for request attributes used by the CEL environment.
xds/src/test/java/io/grpc/xds/internal/matcher/CelCommonTest.java Tests CelCommon.checkAllowedReferences() allow/deny behavior.
xds/src/test/java/io/grpc/xds/internal/matcher/CelEnvironmentTest.java Tests the CEL environment and header map behavior, including disabled features.
xds/src/test/java/io/grpc/xds/internal/matcher/CelMatcherTestHelper.java Test helper for compiling CEL ASTs with restricted declarations.
xds/src/test/java/io/grpc/xds/internal/matcher/CelStringExtractorTest.java Tests string extraction behavior (success, non-string, eval error, defaults).
xds/build.gradle Adds CEL dependencies and shading/relocation for the xDS artifact.
xds/BUILD.bazel Adds CEL dependencies and jarjar relocation rules for Bazel builds.
repositories.bzl Adds CEL artifacts to the shared Bazel third-party artifact list.
MODULE.bazel Adds CEL artifacts to Bazel module artifact list.
gradle/libs.versions.toml Declares CEL versions for Gradle dependency management.
build.gradle Adds a Maven snapshots repository to all subprojects’ repositories.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +111 to +124
public Set<String> keySet() {
return ImmutableSet.<String>builder()
.addAll(context.getMetadata().keys())
.addAll(PSEUDO_HEADERS)
.build();
}

@Override
public int size() {
// Metadata.keys() returns a Set of unique keys, so we can just add the sizes.
// Note: This counts the number of unique header names, which is consistent with
// keySet().size().
return context.getMetadata().keys().size() + PSEUDO_HEADERS.size();
}
Comment on lines +90 to +108
@Override
public boolean containsKey(Object key) {
if (!(key instanceof String)) {
return false;
}
String headerName = ((String) key).toLowerCase(java.util.Locale.ROOT);
if ("te".equals(headerName)) {
return false;
}
if (PSEUDO_HEADERS.contains(headerName)) {
return true;
}
if (headerName.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
return context.getMetadata().containsKey(
Metadata.Key.of(headerName, Metadata.BINARY_BYTE_MARSHALLER));
}
return context.getMetadata().containsKey(
Metadata.Key.of(headerName, Metadata.ASCII_STRING_MARSHALLER));
}
Comment on lines +62 to +67
@Nullable
private String getHeader(String headerName) {
if (headerName.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
Iterable<byte[]> values = context.getMetadata().getAll(
Metadata.Key.of(headerName, Metadata.BINARY_BYTE_MARSHALLER));
if (values == null) {
Comment on lines +71 to +76
private static final Pattern ALLOWED_OVERLOAD_ID_PREFIX_PATTERN = Pattern.compile(
"^(size|matches|contains|startsWith|endsWith|starts_with|ends_with|"
+ "timestamp|duration|in|index|has|int|uint|double|string|bytes|bool|"
+ "less|less_equals|greater|greater_equals|"
+ "add|subtract|multiply|divide|modulo|negate)"
+ "[0-9]*(_.*)?$");
assertAllowed("int(1) == 1");
assertAllowed("uint(1) == 1u");
assertAllowed("double(1) == 1.0");
assertAllowed("string(1) == '1'");
Comment on lines +66 to +85
* fails.
*/
public String extract(Object input) throws CelEvaluationException {
if (input instanceof CelVariableResolver) {
try {
Object result = program.eval((CelVariableResolver) input);

if (result instanceof String) {
return (String) result;
}
} catch (CelEvaluationException e) {
if (defaultValue == null) {
throw e;
}
}
} else if (defaultValue == null) {
throw new CelEvaluationException(
"Unsupported input type for CEL evaluation: "
+ (input == null ? "null" : input.getClass().getName()));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants