Skip to content

Commit f472c63

Browse files
committed
xds: Implementation of Unified Matcher and CEL Integration
1 parent e636df5 commit f472c63

21 files changed

+3260
-4
lines changed

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ checkstyle = "com.puppycrawl.tools:checkstyle:10.26.1"
3434
# checkstyle 10.0+ requires Java 11+
3535
# See https://checkstyle.sourceforge.io/releasenotes_old_8-35_10-26.html#Release_10.0
3636
# checkForUpdates: checkstylejava8:9.+
37+
cel-runtime = "dev.cel:runtime:0.11.1"
38+
cel-compiler = "dev.cel:compiler:0.11.1"
3739
checkstylejava8 = "com.puppycrawl.tools:checkstyle:9.3"
3840
commons-math3 = "org.apache.commons:commons-math3:3.6.1"
3941
conscrypt = "org.conscrypt:conscrypt-openjdk-uber:2.5.2"

xds/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ java_library(
4141
artifact("com.google.errorprone:error_prone_annotations"),
4242
artifact("com.google.guava:guava"),
4343
artifact("com.google.re2j:re2j"),
44+
artifact("dev.cel:cel-bundle"),
4445
artifact("io.netty:netty-buffer"),
4546
artifact("io.netty:netty-codec"),
4647
artifact("io.netty:netty-common"),

xds/build.gradle

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ dependencies {
5656
libraries.re2j,
5757
libraries.auto.value.annotations,
5858
libraries.protobuf.java.util
59+
implementation(libraries.cel.runtime) {
60+
exclude group: 'com.google.protobuf', module: 'protobuf-java'
61+
}
62+
implementation(libraries.cel.compiler) {
63+
exclude group: 'com.google.protobuf', module: 'protobuf-java'
64+
}
5965
def nettyDependency = implementation project(':grpc-netty')
6066

6167
testImplementation project(':grpc-api')
@@ -175,13 +181,15 @@ tasks.named("javadoc").configure {
175181
exclude 'io/grpc/xds/XdsNameResolverProvider.java'
176182
exclude 'io/grpc/xds/internal/**'
177183
exclude 'io/grpc/xds/Internal*'
184+
exclude 'dev/cel/**'
178185
}
179186

180187
def prefixName = 'io.grpc.xds'
181188
tasks.named("shadowJar").configure {
182189
archiveClassifier = null
183190
dependencies {
184191
include(project(':grpc-xds'))
192+
include(dependency('dev.cel:.*'))
185193
}
186194
// Relocated packages commonly need exclusions in jacocoTestReport and javadoc
187195
// Keep in sync with BUILD.bazel's JAR_JAR_RULES
@@ -198,6 +206,8 @@ tasks.named("shadowJar").configure {
198206
// TODO: missing java_package option in .proto
199207
relocate 'udpa.annotations', "${prefixName}.shaded.udpa.annotations"
200208
relocate 'xds.annotations', "${prefixName}.shaded.xds.annotations"
209+
relocate 'dev.cel', "${prefixName}.shaded.dev.cel"
210+
relocate 'cel', "${prefixName}.shaded.cel"
201211
exclude "**/*.proto"
202212
}
203213

xds/src/main/java/io/grpc/xds/internal/MatcherParser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public static Matchers.StringMatcher parseStringMatcher(
9090
return Matchers.StringMatcher.forSafeRegEx(
9191
Pattern.compile(proto.getSafeRegex().getRegex()));
9292
case CONTAINS:
93-
return Matchers.StringMatcher.forContains(proto.getContains());
93+
return Matchers.StringMatcher.forContains(proto.getContains(), proto.getIgnoreCase());
9494
case MATCHPATTERN_NOT_SET:
9595
default:
9696
throw new IllegalArgumentException(

xds/src/main/java/io/grpc/xds/internal/Matchers.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,10 +257,15 @@ public static StringMatcher forSafeRegEx(Pattern regEx) {
257257
}
258258

259259
/** The input string should contain this substring. */
260-
public static StringMatcher forContains(String contains) {
260+
public static StringMatcher forContains(String contains, boolean ignoreCase) {
261261
checkNotNull(contains, "contains");
262262
return StringMatcher.create(null, null, null, null, contains,
263-
false/* doesn't matter */);
263+
ignoreCase);
264+
}
265+
266+
/** The input string should contain this substring. */
267+
public static StringMatcher forContains(String contains) {
268+
return forContains(contains, false);
264269
}
265270

266271
/** Returns the matching result for this string. */
@@ -281,7 +286,9 @@ public boolean matches(String args) {
281286
? args.toLowerCase(Locale.ROOT).endsWith(suffix().toLowerCase(Locale.ROOT))
282287
: args.endsWith(suffix());
283288
} else if (contains() != null) {
284-
return args.contains(contains());
289+
return ignoreCase()
290+
? args.toLowerCase(Locale.ROOT).contains(contains().toLowerCase(Locale.ROOT))
291+
: args.contains(contains());
285292
}
286293
return regEx().matches(args);
287294
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2026 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.matcher;
18+
19+
import dev.cel.common.CelAbstractSyntaxTree;
20+
import dev.cel.common.CelOptions;
21+
import dev.cel.common.types.SimpleType;
22+
import dev.cel.compiler.CelCompiler;
23+
import dev.cel.compiler.CelCompilerFactory;
24+
import dev.cel.runtime.CelRuntime;
25+
import dev.cel.runtime.CelRuntimeFactory;
26+
27+
/**
28+
* Shared utilities for CEL-based matchers and extractors.
29+
*/
30+
final class CelCommon {
31+
private static final CelOptions CEL_OPTIONS = CelOptions.newBuilder()
32+
.enableComprehension(false)
33+
.enableStringConversion(false)
34+
.enableStringConcatenation(false)
35+
.enableListConcatenation(false)
36+
.maxRegexProgramSize(100)
37+
.build();
38+
39+
static final CelCompiler COMPILER = CelCompilerFactory.standardCelCompilerBuilder()
40+
.addVar("request", SimpleType.DYN)
41+
.setOptions(CEL_OPTIONS)
42+
.build();
43+
44+
static final CelRuntime RUNTIME = CelRuntimeFactory.standardCelRuntimeBuilder()
45+
.setOptions(CEL_OPTIONS)
46+
.build();
47+
48+
private CelCommon() {}
49+
50+
static void checkAllowedVariables(CelAbstractSyntaxTree ast) {
51+
// Basic validation to ensure only supported variables (request) are used.
52+
// This iterates over the reference map generated by the type checker.
53+
for (java.util.Map.Entry<Long, dev.cel.common.ast.CelReference> entry :
54+
ast.getReferenceMap().entrySet()) {
55+
dev.cel.common.ast.CelReference ref = entry.getValue();
56+
// If overload_id is empty, it's a variable reference or type name.
57+
// We only support "request".
58+
if (ref.value() == null && ref.overloadIds().isEmpty()) {
59+
if (!"request".equals(ref.name())) {
60+
throw new IllegalArgumentException(
61+
"CEL expression references unknown variable: " + ref.name());
62+
}
63+
}
64+
}
65+
}
66+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2026 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.matcher;
18+
19+
import com.google.common.annotations.VisibleForTesting;
20+
import dev.cel.common.CelAbstractSyntaxTree;
21+
import dev.cel.common.CelValidationException;
22+
import dev.cel.common.types.SimpleType;
23+
import dev.cel.runtime.CelEvaluationException;
24+
import dev.cel.runtime.CelRuntime;
25+
26+
27+
28+
/**
29+
* Executes compiled CEL expressions.
30+
*/
31+
public final class CelMatcher {
32+
33+
34+
private final CelRuntime.Program program;
35+
36+
private CelMatcher(CelRuntime.Program program) {
37+
this.program = program;
38+
}
39+
40+
/**
41+
* Compiles the AST into a CelMatcher.
42+
*/
43+
public static CelMatcher compile(CelAbstractSyntaxTree ast)
44+
throws CelValidationException, CelEvaluationException {
45+
if (ast.getResultType() != SimpleType.BOOL) {
46+
throw new IllegalArgumentException(
47+
"CEL expression must evaluate to boolean, got: " + ast.getResultType());
48+
}
49+
CelCommon.checkAllowedVariables(ast);
50+
CelRuntime.Program program = CelCommon.RUNTIME.createProgram(ast);
51+
return new CelMatcher(program);
52+
}
53+
54+
/**
55+
* Compiles the CEL expression string into a CelMatcher.
56+
*/
57+
@VisibleForTesting
58+
public static CelMatcher compile(String expression)
59+
throws CelValidationException, CelEvaluationException {
60+
CelAbstractSyntaxTree ast = CelCommon.COMPILER.compile(expression).getAst();
61+
return compile(ast);
62+
}
63+
64+
/**
65+
* Evaluates the CEL expression against the input activation.
66+
*/
67+
public boolean match(Object input) throws CelEvaluationException {
68+
Object result;
69+
if (input instanceof dev.cel.runtime.CelVariableResolver) {
70+
result = program.eval((dev.cel.runtime.CelVariableResolver) input);
71+
} else if (input instanceof java.util.Map) {
72+
@SuppressWarnings("unchecked")
73+
java.util.Map<String, ?> mapInput = (java.util.Map<String, ?>) input;
74+
result = program.eval(mapInput);
75+
} else {
76+
throw new CelEvaluationException(
77+
"Unsupported input type for CEL evaluation: " + input.getClass().getName());
78+
}
79+
// Validated to be boolean during compile check ideally, or we cast safely
80+
if (result instanceof Boolean) {
81+
return (Boolean) result;
82+
}
83+
throw new CelEvaluationException(
84+
"CEL expression must evaluate to boolean, got: " + result.getClass().getName());
85+
}
86+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright 2026 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.matcher;
18+
19+
import dev.cel.common.CelAbstractSyntaxTree;
20+
import dev.cel.common.CelValidationException;
21+
import dev.cel.common.types.SimpleType;
22+
import dev.cel.runtime.CelEvaluationException;
23+
import dev.cel.runtime.CelRuntime;
24+
25+
26+
/**
27+
* Executes compiled CEL expressions that extract a string.
28+
*/
29+
public final class CelStringExtractor {
30+
31+
32+
private final CelRuntime.Program program;
33+
34+
private CelStringExtractor(CelRuntime.Program program) {
35+
this.program = program;
36+
}
37+
38+
/**
39+
* Compiles the AST into a CelStringExtractor.
40+
*/
41+
public static CelStringExtractor compile(CelAbstractSyntaxTree ast)
42+
throws CelValidationException, CelEvaluationException {
43+
if (ast.getResultType() != SimpleType.STRING && ast.getResultType() != SimpleType.DYN) {
44+
throw new IllegalArgumentException(
45+
"CEL expression must evaluate to string, got: " + ast.getResultType());
46+
}
47+
CelCommon.checkAllowedVariables(ast);
48+
CelRuntime.Program program = CelCommon.RUNTIME.createProgram(ast);
49+
return new CelStringExtractor(program);
50+
}
51+
52+
/**
53+
* Compiles the CEL expression string into a CelStringExtractor.
54+
*/
55+
public static CelStringExtractor compile(String expression)
56+
throws CelValidationException, CelEvaluationException {
57+
CelAbstractSyntaxTree ast = CelCommon.COMPILER.compile(expression).getAst();
58+
return compile(ast);
59+
}
60+
61+
/**
62+
* Evaluates the CEL expression against the input activation and returns the string result.
63+
* Returns null if the result is not a string.
64+
*/
65+
public String extract(Object input) throws CelEvaluationException {
66+
Object result;
67+
if (input instanceof dev.cel.runtime.CelVariableResolver) {
68+
result = program.eval((dev.cel.runtime.CelVariableResolver) input);
69+
} else if (input instanceof java.util.Map) {
70+
@SuppressWarnings("unchecked")
71+
java.util.Map<String, ?> mapInput = (java.util.Map<String, ?>) input;
72+
result = program.eval(mapInput);
73+
} else {
74+
throw new CelEvaluationException(
75+
"Unsupported input type for CEL evaluation: " + input.getClass().getName());
76+
}
77+
78+
if (result instanceof String) {
79+
return (String) result;
80+
}
81+
// Return null key for non-string results (which will likely match nothing or be handled)
82+
return null;
83+
}
84+
}

0 commit comments

Comments
 (0)