Skip to content

Commit b0a1b5a

Browse files
committed
CEL variable resolver
1 parent f913e88 commit b0a1b5a

File tree

6 files changed

+309
-44
lines changed

6 files changed

+309
-44
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,8 @@ public static String deserializeHeader(Metadata metadata, String headerName) {
6060
Iterable<String> values = metadata.getAll(key);
6161
return values == null ? null : String.join(",", values);
6262
}
63+
64+
public static boolean containsHeader(Metadata metadata, String headerName) {
65+
return metadata.containsKey(Metadata.Key.of(headerName, Metadata.ASCII_STRING_MARSHALLER));
66+
}
6367
}

xds/src/main/java/io/grpc/xds/internal/matchers/CelMatcher.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,6 @@ public String description() {
7676

7777
@Override
7878
public boolean test(HttpMatchInput httpMatchInput) {
79-
// if (httpMatchInput.headers().keys().isEmpty()) {
80-
// return false;
81-
// }
82-
// TODO(sergiitk): [IMPL] convert headers to cel args
83-
return program.eval(httpMatchInput.serverCall(), httpMatchInput.headers());
79+
return program.eval(httpMatchInput);
8480
}
8581
}

xds/src/main/java/io/grpc/xds/internal/matchers/GrpcCelEnvironment.java

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,31 @@
1616

1717
package io.grpc.xds.internal.matchers;
1818

19-
import com.google.common.base.Strings;
20-
import com.google.common.collect.ImmutableMap;
19+
import com.google.common.base.Splitter;
2120
import dev.cel.common.CelAbstractSyntaxTree;
21+
import dev.cel.common.CelErrorCode;
2222
import dev.cel.common.CelOptions;
23+
import dev.cel.common.CelRuntimeException;
2324
import dev.cel.common.types.SimpleType;
2425
import dev.cel.runtime.CelEvaluationException;
2526
import dev.cel.runtime.CelRuntime;
2627
import dev.cel.runtime.CelRuntimeFactory;
27-
import io.grpc.Metadata;
28-
import io.grpc.ServerCall;
28+
import dev.cel.runtime.CelVariableResolver;
2929
import io.grpc.Status;
30-
import io.grpc.xds.internal.MetadataHelper;
30+
import java.util.List;
31+
import java.util.Optional;
32+
import javax.annotation.Nullable;
3133

3234
/** Unified Matcher API: xds.type.matcher.v3.CelMatcher. */
3335
public class GrpcCelEnvironment {
36+
private static final CelOptions CEL_OPTIONS = CelOptions
37+
.current()
38+
.comprehensionMaxIterations(0)
39+
.resolveTypeDependencies(false)
40+
.build();
3441
private static final CelRuntime CEL_RUNTIME = CelRuntimeFactory
3542
.standardCelRuntimeBuilder()
36-
.setOptions(CelOptions.current().comprehensionMaxIterations(0).build())
43+
.setOptions(CEL_OPTIONS)
3744
.build();
3845

3946
private final CelRuntime.Program program;
@@ -45,17 +52,61 @@ public class GrpcCelEnvironment {
4552
this.program = CEL_RUNTIME.createProgram(ast);
4653
}
4754

48-
public boolean eval(ServerCall<?, ?> serverCall, Metadata metadata) {
49-
ImmutableMap.Builder<String, Object> requestBuilder = ImmutableMap.<String, Object>builder()
50-
.put("method", "POST")
51-
.put("host", Strings.nullToEmpty(serverCall.getAuthority()))
52-
.put("path", "/" + serverCall.getMethodDescriptor().getFullMethodName())
53-
.put("headers", MetadataHelper.metadataToHeaders(metadata));
54-
// TODO(sergiitk): handle other pseudo-headers
55+
public boolean eval(HttpMatchInput httpMatchInput) {
5556
try {
56-
return (boolean) program.eval(ImmutableMap.of("request", requestBuilder.build()));
57-
} catch (CelEvaluationException e) {
57+
GrpcCelVariableResolver requestResolver = new GrpcCelVariableResolver(httpMatchInput);
58+
return (boolean) program.eval(requestResolver);
59+
} catch (CelEvaluationException | ClassCastException e) {
5860
throw Status.fromThrowable(e).asRuntimeException();
5961
}
6062
}
63+
64+
static class GrpcCelVariableResolver implements CelVariableResolver {
65+
private static final Splitter SPLITTER = Splitter.on('.').limit(2);
66+
private final HttpMatchInput httpMatchInput;
67+
68+
GrpcCelVariableResolver(HttpMatchInput httpMatchInput) {
69+
this.httpMatchInput = httpMatchInput;
70+
}
71+
72+
@Override
73+
public Optional<Object> find(String name) {
74+
List<String> components = SPLITTER.splitToList(name);
75+
if (components.size() < 2 || !components.get(0).equals("request")) {
76+
return Optional.empty();
77+
}
78+
return Optional.ofNullable(getRequestField(components.get(1)));
79+
}
80+
81+
@Nullable
82+
private Object getRequestField(String requestField) {
83+
switch (requestField) {
84+
case "headers":
85+
return httpMatchInput.getHeadersWrapper();
86+
case "host":
87+
return httpMatchInput.getHost();
88+
case "id":
89+
return httpMatchInput.getHeadersWrapper().get("x-request-id");
90+
case "method":
91+
return httpMatchInput.getMethod();
92+
case "path":
93+
case "url_path":
94+
return httpMatchInput.getPath();
95+
case "query":
96+
return "";
97+
case "referer":
98+
return httpMatchInput.getHeadersWrapper().get("referer");
99+
case "useragent":
100+
return httpMatchInput.getHeadersWrapper().get("user-agent");
101+
default:
102+
// Throwing instead of Optional.empty() prevents evaluation non-boolean result type
103+
// when comparing unknown fields, f.e. `request.protocol == 'HTTP'` will silently
104+
// fail because `null == "HTTP" is not a valid CEL operation.
105+
throw new CelRuntimeException(
106+
// Similar to dev.cel.runtime.DescriptorMessageProvider#selectField
107+
new IllegalArgumentException("request." + requestField),
108+
CelErrorCode.ATTRIBUTE_NOT_FOUND);
109+
}
110+
}
111+
}
61112
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
* Copyright 2024 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.matchers;
18+
19+
import com.google.common.base.Objects;
20+
import com.google.common.collect.ImmutableSet;
21+
import com.google.errorprone.annotations.DoNotCall;
22+
import io.grpc.xds.internal.MetadataHelper;
23+
import java.util.AbstractMap;
24+
import java.util.Collection;
25+
import java.util.Set;
26+
import java.util.function.BiFunction;
27+
import java.util.function.Function;
28+
import javax.annotation.Nullable;
29+
30+
public final class HeadersWrapper extends AbstractMap<String, String> {
31+
private static final ImmutableSet<String> PSEUDO_HEADERS =
32+
ImmutableSet.of(":method", ":authority", ":path");
33+
private final HttpMatchInput httpMatchInput;
34+
35+
HeadersWrapper(HttpMatchInput httpMatchInput) {
36+
this.httpMatchInput = httpMatchInput;
37+
}
38+
39+
@Override
40+
@Nullable
41+
public String get(Object key) {
42+
String headerName = (String) key;
43+
// Pseudo-headers.
44+
switch (headerName) {
45+
case ":method":
46+
return httpMatchInput.getMethod();
47+
case ":authority":
48+
return httpMatchInput.getHost();
49+
case ":path":
50+
return httpMatchInput.getPath();
51+
default:
52+
return MetadataHelper.deserializeHeader(httpMatchInput.metadata(), headerName);
53+
}
54+
}
55+
56+
@Override
57+
public String getOrDefault(Object key, String defaultValue) {
58+
String value = get(key);
59+
return value != null ? value : defaultValue;
60+
}
61+
62+
@Override
63+
public boolean containsKey(Object key) {
64+
String headerName = (String) key;
65+
if (PSEUDO_HEADERS.contains(headerName)) {
66+
return true;
67+
}
68+
return MetadataHelper.containsHeader(httpMatchInput.metadata(), headerName);
69+
}
70+
71+
@Override
72+
public Set<String> keySet() {
73+
return ImmutableSet.<String>builder()
74+
.addAll(httpMatchInput.metadata().keys())
75+
.addAll(PSEUDO_HEADERS).build();
76+
}
77+
78+
@Override
79+
@DoNotCall("Always throws UnsupportedOperationException")
80+
public Set<Entry<String, String>> entrySet() {
81+
throw new UnsupportedOperationException(
82+
"Should not be called to prevent resolving header values.");
83+
}
84+
85+
@Override
86+
@DoNotCall("Always throws UnsupportedOperationException")
87+
public Collection<String> values() {
88+
throw new UnsupportedOperationException(
89+
"Should not be called to prevent resolving header values.");
90+
}
91+
92+
@Override
93+
public String toString() {
94+
// Prevent iterating to avoid resolving all values on "key not found".
95+
return getClass().getName() + "@" + Integer.toHexString(hashCode());
96+
}
97+
98+
@Override public int hashCode() {
99+
return Objects.hashCode(httpMatchInput.serverCall(), httpMatchInput.metadata());
100+
}
101+
102+
@Override
103+
@DoNotCall("Always throws UnsupportedOperationException")
104+
public void replaceAll(BiFunction<? super String, ? super String, ? extends String> function) {
105+
throw new UnsupportedOperationException();
106+
}
107+
108+
@Override
109+
@DoNotCall("Always throws UnsupportedOperationException")
110+
public String putIfAbsent(String key, String value) {
111+
throw new UnsupportedOperationException();
112+
}
113+
114+
@Override
115+
@DoNotCall("Always throws UnsupportedOperationException")
116+
public boolean remove(Object key, Object value) {
117+
throw new UnsupportedOperationException();
118+
}
119+
120+
@Override
121+
@DoNotCall("Always throws UnsupportedOperationException")
122+
public boolean replace(String key, String oldValue, String newValue) {
123+
throw new UnsupportedOperationException();
124+
}
125+
126+
@Override
127+
@DoNotCall("Always throws UnsupportedOperationException")
128+
public String replace(String key, String value) {
129+
throw new UnsupportedOperationException();
130+
}
131+
132+
@Override
133+
@DoNotCall("Always throws UnsupportedOperationException")
134+
public String computeIfAbsent(
135+
String key, Function<? super String, ? extends String> mappingFunction) {
136+
throw new UnsupportedOperationException();
137+
}
138+
139+
@Override
140+
@DoNotCall("Always throws UnsupportedOperationException")
141+
public String computeIfPresent(
142+
String key, BiFunction<? super String, ? super String, ? extends String> remappingFunction) {
143+
throw new UnsupportedOperationException();
144+
}
145+
146+
@Override
147+
@DoNotCall("Always throws UnsupportedOperationException")
148+
public String compute(
149+
String key, BiFunction<? super String, ? super String, ? extends String> remappingFunction) {
150+
throw new UnsupportedOperationException();
151+
}
152+
153+
@Override
154+
@DoNotCall("Always throws UnsupportedOperationException")
155+
public String merge(
156+
String key, String value,
157+
BiFunction<? super String, ? super String, ? extends String> remappingFunction) {
158+
throw new UnsupportedOperationException();
159+
}
160+
}

xds/src/main/java/io/grpc/xds/internal/matchers/HttpMatchInput.java

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,44 @@
1818
package io.grpc.xds.internal.matchers;
1919

2020
import com.google.auto.value.AutoValue;
21+
import com.google.auto.value.extension.memoized.Memoized;
22+
import com.google.common.base.Strings;
2123
import io.grpc.Metadata;
2224
import io.grpc.ServerCall;
25+
import io.grpc.xds.internal.MetadataHelper;
26+
import java.util.Map;
27+
import javax.annotation.Nullable;
2328

2429
@AutoValue
2530
public abstract class HttpMatchInput {
26-
public abstract Metadata headers();
31+
public abstract Metadata metadata();
2732

2833
// TODO(sergiitk): [IMPL] consider
2934
public abstract ServerCall<?, ?> serverCall();
3035

31-
public static HttpMatchInput create(Metadata headers, ServerCall<?, ?> serverCall) {
32-
return new AutoValue_HttpMatchInput(headers, serverCall);
36+
public static HttpMatchInput create(Metadata metadata, ServerCall<?, ?> serverCall) {
37+
return new AutoValue_HttpMatchInput(metadata, serverCall);
38+
}
39+
40+
public String getMethod() {
41+
return "POST";
42+
}
43+
44+
public String getHost() {
45+
return Strings.nullToEmpty(serverCall().getAuthority());
46+
}
47+
48+
public String getPath() {
49+
return "/" + serverCall().getMethodDescriptor().getFullMethodName();
50+
}
51+
52+
@Nullable
53+
public String getHeader(String headerName) {
54+
return MetadataHelper.deserializeHeader(metadata(), headerName);
55+
}
56+
57+
@Memoized
58+
public Map<String, String> getHeadersWrapper() {
59+
return new HeadersWrapper(this);
3360
}
3461
}

0 commit comments

Comments
 (0)