Skip to content

Commit 4801530

Browse files
committed
xds: CEL implementation
1 parent ec10992 commit 4801530

15 files changed

Lines changed: 1405 additions & 0 deletions

MODULE.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [
2121
"com.google.re2j:re2j:1.8",
2222
"com.google.s2a.proto.v2:s2a-proto:0.1.3",
2323
"com.google.truth:truth:1.4.5",
24+
"dev.cel:runtime:0.12.0",
25+
"dev.cel:protobuf:0.12.0",
26+
"dev.cel:common:0.12.0",
2427
"com.squareup.okhttp:okhttp:2.7.5",
2528
"com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day
2629
"io.netty:netty-buffer:4.1.132.Final",

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ subprojects {
4343
ignoreGradleMetadataRedirection()
4444
}
4545
}
46+
maven { url 'https://central.sonatype.com/repository/maven-snapshots/' }
4647
}
4748

4849
tasks.withType(JavaCompile).configureEach {

gradle/libs.versions.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ checkstyle = "com.puppycrawl.tools:checkstyle:10.26.1"
3737
# checkstyle 10.0+ requires Java 11+
3838
# See https://checkstyle.sourceforge.io/releasenotes_old_8-35_10-26.html#Release_10.0
3939
# checkForUpdates: checkstylejava8:9.+
40+
cel-runtime = "dev.cel:runtime:0.12.0"
41+
cel-protobuf = "dev.cel:protobuf:0.12.0"
42+
cel-compiler = "dev.cel:compiler:0.12.0"
4043
checkstylejava8 = "com.puppycrawl.tools:checkstyle:9.3"
4144
commons-math3 = "org.apache.commons:commons-math3:3.6.1"
4245
conscrypt = "org.conscrypt:conscrypt-openjdk-uber:2.5.2"

repositories.bzl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [
2525
"com.google.re2j:re2j:1.8",
2626
"com.google.s2a.proto.v2:s2a-proto:0.1.3",
2727
"com.google.truth:truth:1.4.5",
28+
"dev.cel:runtime:0.12.0",
29+
"dev.cel:protobuf:0.12.0",
30+
"dev.cel:common:0.12.0",
2831
"com.squareup.okhttp:okhttp:2.7.5",
2932
"com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day
3033
"io.netty:netty-buffer:4.1.132.Final",

xds/BUILD.bazel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ 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:runtime"),
45+
artifact("dev.cel:protobuf"),
46+
artifact("dev.cel:common"),
4447
artifact("io.netty:netty-buffer"),
4548
artifact("io.netty:netty-codec"),
4649
artifact("io.netty:netty-common"),
@@ -97,6 +100,8 @@ JAR_JAR_RULES = [
97100
"rule com.google.api.expr.** io.grpc.xds.shaded.com.google.api.expr.@1",
98101
"rule com.google.security.** io.grpc.xds.shaded.com.google.security.@1",
99102
"rule dev.cel.expr.** io.grpc.xds.shaded.dev.cel.expr.@1",
103+
"rule dev.cel.** io.grpc.xds.shaded.dev.cel.@1",
104+
"rule cel.** io.grpc.xds.shaded.cel.@1",
100105
"rule envoy.annotations.** io.grpc.xds.shaded.envoy.annotations.@1",
101106
"rule io.envoyproxy.** io.grpc.xds.shaded.io.envoyproxy.@1",
102107
"rule udpa.annotations.** io.grpc.xds.shaded.udpa.annotations.@1",

xds/build.gradle

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,18 @@ 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.protobuf) {
63+
exclude group: 'com.google.protobuf', module: 'protobuf-java'
64+
}
5965
def nettyDependency = implementation project(':grpc-netty')
6066

6167
testImplementation project(':grpc-api')
6268
testImplementation project(':grpc-rls')
6369
testImplementation project(':grpc-inprocess')
70+
testImplementation libraries.cel.compiler
6471
testImplementation testFixtures(project(':grpc-core')),
6572
testFixtures(project(':grpc-api')),
6673
testFixtures(project(':grpc-util'))
@@ -175,13 +182,15 @@ tasks.named("javadoc").configure {
175182
exclude 'io/grpc/xds/XdsNameResolverProvider.java'
176183
exclude 'io/grpc/xds/internal/**'
177184
exclude 'io/grpc/xds/Internal*'
185+
exclude 'dev/cel/**'
178186
}
179187

180188
def prefixName = 'io.grpc.xds'
181189
tasks.named("shadowJar").configure {
182190
archiveClassifier = null
183191
dependencies {
184192
include(project(':grpc-xds'))
193+
include(dependency('dev.cel:.*'))
185194
}
186195
// Relocated packages commonly need exclusions in jacocoTestReport and javadoc
187196
// Keep in sync with BUILD.bazel's JAR_JAR_RULES
@@ -198,6 +207,8 @@ tasks.named("shadowJar").configure {
198207
// TODO: missing java_package option in .proto
199208
relocate 'udpa.annotations', "${prefixName}.shaded.udpa.annotations"
200209
relocate 'xds.annotations', "${prefixName}.shaded.xds.annotations"
210+
relocate 'dev.cel', "${prefixName}.shaded.dev.cel"
211+
relocate 'cel', "${prefixName}.shaded.cel"
201212
exclude "**/*.proto"
202213
}
203214

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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.collect.ImmutableSet;
20+
import dev.cel.common.CelAbstractSyntaxTree;
21+
import dev.cel.common.CelOptions;
22+
import dev.cel.common.ast.CelReference;
23+
import dev.cel.runtime.CelRuntime;
24+
import dev.cel.runtime.CelRuntimeFactory;
25+
import dev.cel.runtime.CelStandardFunctions;
26+
import dev.cel.runtime.CelStandardFunctions.StandardFunction;
27+
import dev.cel.runtime.standard.AddOperator.AddOverload;
28+
import java.util.Map;
29+
import java.util.regex.Pattern;
30+
31+
/**
32+
* Shared utilities for CEL-based matchers and extractors.
33+
*/
34+
final class CelCommon {
35+
private static final CelOptions CEL_OPTIONS = CelOptions.newBuilder()
36+
.enableComprehension(false)
37+
.maxRegexProgramSize(100)
38+
.build();
39+
private static final String REQUEST_VARIABLE = "request";
40+
private static final CelStandardFunctions FUNCTIONS =
41+
CelStandardFunctions.newBuilder()
42+
.filterFunctions((func, over) -> {
43+
if (func == StandardFunction.STRING) {
44+
return false;
45+
}
46+
if (func == StandardFunction.ADD) {
47+
return !over.equals(AddOverload.ADD_STRING)
48+
&& !over.equals(AddOverload.ADD_LIST);
49+
}
50+
return true;
51+
})
52+
.build();
53+
54+
55+
56+
private static final ImmutableSet<String> ALLOWED_EXACT_OVERLOAD_IDS = ImmutableSet.of(
57+
"equals", "not_equals", "logical_and", "logical_or", "logical_not");
58+
59+
/**
60+
* Regular expression pattern to validate internal CEL overload IDs.
61+
*
62+
* <p>
63+
* Standard CEL operators and conversion functions often have empty names in the
64+
* AST and are identified solely by their overload IDs (e.g., {@code equals} for
65+
* {@code ==}, {@code divide_int64} for {@code /}).
66+
*
67+
* <p>
68+
* This pattern matches allowed overload IDs by their prefixes (e.g.,
69+
* {@code divide}, {@code size}), optionally followed by numeric types
70+
* (e.g., {@code int64}) and type-specific suffixes (e.g., {@code _string},
71+
* {@code _int64}).
72+
*/
73+
private static final Pattern ALLOWED_OVERLOAD_ID_PREFIX_PATTERN = Pattern.compile(
74+
"^(size|matches|contains|startsWith|endsWith|starts_with|ends_with|"
75+
+ "timestamp|duration|in|index|has|int|uint|double|string|bytes|bool|"
76+
+ "less|less_equals|greater|greater_equals|"
77+
+ "add|subtract|multiply|divide|modulo|negate)"
78+
+ "[0-9]*(_.*)?$");
79+
80+
static final CelRuntime RUNTIME = CelRuntimeFactory.standardCelRuntimeBuilder()
81+
.setStandardEnvironmentEnabled(false)
82+
.setStandardFunctions(FUNCTIONS)
83+
.setOptions(CEL_OPTIONS)
84+
.build();
85+
86+
private CelCommon() {}
87+
88+
/**
89+
* Validates that the AST only references the allowed variable ("request")
90+
* and supported functions as defined in gRFC A106.
91+
*/
92+
static void checkAllowedReferences(CelAbstractSyntaxTree ast) {
93+
for (Map.Entry<Long, CelReference> entry : ast.getReferenceMap().entrySet()) {
94+
CelReference ref = entry.getValue();
95+
96+
// Check for variables (where overloadIds is empty)
97+
if (!ref.value().isPresent() && ref.overloadIds().isEmpty()) {
98+
if (!REQUEST_VARIABLE.equals(ref.name())) {
99+
throw new IllegalArgumentException(
100+
"CEL expression references unknown variable: " + ref.name());
101+
}
102+
} else if (!ref.overloadIds().isEmpty()) {
103+
String name = ref.name();
104+
if (name.isEmpty()) {
105+
boolean allowed = false;
106+
for (String id : ref.overloadIds()) {
107+
if (ALLOWED_EXACT_OVERLOAD_IDS.contains(id)
108+
|| ALLOWED_OVERLOAD_ID_PREFIX_PATTERN.matcher(id).matches()) {
109+
allowed = true;
110+
break;
111+
}
112+
}
113+
if (!allowed) {
114+
throw new IllegalArgumentException(
115+
"CEL expression references unknown function with overload IDs: "
116+
+ ref.overloadIds());
117+
}
118+
} else {
119+
throw new IllegalArgumentException(
120+
"CEL expression references unsupported named function: " + name);
121+
}
122+
}
123+
}
124+
}
125+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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.types.SimpleType;
21+
import dev.cel.runtime.CelEvaluationException;
22+
import dev.cel.runtime.CelRuntime;
23+
import dev.cel.runtime.CelVariableResolver;
24+
import javax.annotation.Nullable;
25+
26+
/**
27+
* Executes compiled CEL expressions that extract a string.
28+
*/
29+
public final class CelStringExtractor {
30+
private final CelRuntime.Program program;
31+
@Nullable
32+
private final String defaultValue;
33+
34+
private CelStringExtractor(CelRuntime.Program program, @Nullable String defaultValue) {
35+
this.program = program;
36+
this.defaultValue = defaultValue;
37+
}
38+
39+
/**
40+
* Compiles the AST into a CelStringExtractor with an optional default value.
41+
* Throws an Exception if evaluation fails during compilation setup.
42+
*/
43+
public static CelStringExtractor compile(CelAbstractSyntaxTree ast, @Nullable String defaultValue)
44+
throws CelEvaluationException {
45+
if (ast.getResultType() != SimpleType.STRING && ast.getResultType() != SimpleType.DYN) {
46+
throw new IllegalArgumentException(
47+
"CEL expression must evaluate to string, got: " + ast.getResultType());
48+
}
49+
CelCommon.checkAllowedReferences(ast);
50+
CelRuntime.Program program = CelCommon.RUNTIME.createProgram(ast);
51+
return new CelStringExtractor(program, defaultValue);
52+
}
53+
54+
/**
55+
* Compiles the AST into a CelStringExtractor with no default value.
56+
* Throws an Exception if evaluation fails during compilation setup.
57+
*/
58+
public static CelStringExtractor compile(CelAbstractSyntaxTree ast)
59+
throws CelEvaluationException {
60+
return compile(ast, null);
61+
}
62+
63+
/**
64+
* Evaluates the CEL expression and returns the string result.
65+
* Returns the default value if the result is not a string or if evaluation
66+
* fails.
67+
*/
68+
public String extract(Object input) throws CelEvaluationException {
69+
if (input instanceof CelVariableResolver) {
70+
try {
71+
Object result = program.eval((CelVariableResolver) input);
72+
73+
if (result instanceof String) {
74+
return (String) result;
75+
}
76+
} catch (CelEvaluationException e) {
77+
if (defaultValue == null) {
78+
throw e;
79+
}
80+
}
81+
} else if (defaultValue == null) {
82+
throw new CelEvaluationException(
83+
"Unsupported input type for CEL evaluation: "
84+
+ (input == null ? "null" : input.getClass().getName()));
85+
}
86+
return defaultValue;
87+
}
88+
}

0 commit comments

Comments
 (0)