Skip to content

Commit d2c4cf9

Browse files
committed
xds: Add CelStringExtractor and CEL dependencies
1 parent fcb12bd commit d2c4cf9

5 files changed

Lines changed: 215 additions & 0 deletions

File tree

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"

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: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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+
30+
/**
31+
* Shared utilities for CEL-based matchers and extractors.
32+
*/
33+
final class CelCommon {
34+
private static final CelOptions CEL_OPTIONS = CelOptions.newBuilder()
35+
.enableComprehension(false)
36+
.maxRegexProgramSize(100)
37+
.build();
38+
private static final String REQUEST_VARIABLE = "request";
39+
private static final CelStandardFunctions FUNCTIONS =
40+
CelStandardFunctions.newBuilder()
41+
.filterFunctions((func, over) -> {
42+
if (func == StandardFunction.STRING) {
43+
return false;
44+
}
45+
if (func == StandardFunction.ADD) {
46+
return !over.equals(AddOverload.ADD_STRING)
47+
&& !over.equals(AddOverload.ADD_LIST);
48+
}
49+
return true;
50+
})
51+
.build();
52+
53+
/**
54+
* Set of allowed function names based on gRFC A106.
55+
*/
56+
private static final ImmutableSet<String> ALLOWED_FUNCTIONS = ImmutableSet.of(
57+
"size", "matches", "contains", "startsWith", "endsWith", "timestamp", "duration",
58+
"int", "uint", "double", "bytes", "bool", "==", "!=", ">", "<", ">=", "<=",
59+
"&&", "||", "!", "+", "-", "*", "/", "%", "in", "has", "or", "equals",
60+
"index_map", "divide_int64", "int64_to_int64", "uint64_to_int64",
61+
"double_to_int64", "string_to_int64", "timestamp_to_int64");
62+
63+
static final CelRuntime RUNTIME = CelRuntimeFactory.standardCelRuntimeBuilder()
64+
.setStandardEnvironmentEnabled(false)
65+
.setStandardFunctions(FUNCTIONS)
66+
.setOptions(CEL_OPTIONS)
67+
.build();
68+
69+
private CelCommon() {}
70+
71+
/**
72+
* Validates that the AST only references the allowed variable ("request")
73+
* and supported functions as defined in gRFC A106.
74+
*/
75+
static void checkAllowedReferences(CelAbstractSyntaxTree ast) {
76+
for (Map.Entry<Long, CelReference> entry : ast.getReferenceMap().entrySet()) {
77+
CelReference ref = entry.getValue();
78+
79+
// Check for variables (where overloadIds is empty)
80+
if (!ref.value().isPresent() && ref.overloadIds().isEmpty()) {
81+
if (!REQUEST_VARIABLE.equals(ref.name())) {
82+
throw new IllegalArgumentException(
83+
"CEL expression references unknown variable: " + ref.name());
84+
}
85+
} else if (!ref.overloadIds().isEmpty()) {
86+
String name = ref.name();
87+
if (name.isEmpty()) {
88+
boolean allowed = false;
89+
for (String id : ref.overloadIds()) {
90+
if (ALLOWED_FUNCTIONS.contains(id)) {
91+
allowed = true;
92+
break;
93+
}
94+
}
95+
if (!allowed) {
96+
throw new IllegalArgumentException(
97+
"CEL expression references unknown function with overload IDs: "
98+
+ ref.overloadIds());
99+
}
100+
} else if (!ALLOWED_FUNCTIONS.contains(name)) {
101+
throw new IllegalArgumentException(
102+
"CEL expression references unknown function: " + name);
103+
}
104+
}
105+
}
106+
}
107+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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+
Object result;
70+
try {
71+
if (input instanceof CelVariableResolver) {
72+
result = program.eval((CelVariableResolver) input);
73+
} else {
74+
throw new CelEvaluationException(
75+
"Unsupported input type for CEL evaluation: " + input.getClass().getName());
76+
}
77+
} catch (CelEvaluationException e) {
78+
if (defaultValue != null) {
79+
return defaultValue;
80+
}
81+
throw e;
82+
}
83+
84+
if (result instanceof String) {
85+
return (String) result;
86+
}
87+
return defaultValue;
88+
}
89+
}

0 commit comments

Comments
 (0)