Skip to content

Commit 8d96d01

Browse files
l46kokcopybara-github
authored andcommitted
Internal Changes
PiperOrigin-RevId: 910292784
1 parent d08c424 commit 8d96d01

10 files changed

Lines changed: 235 additions & 21 deletions

File tree

bundle/src/main/java/dev/cel/bundle/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ java_library(
104104
":required_fields_checker",
105105
"//:auto_value",
106106
"//bundle:cel",
107+
"//checker:proto_type_mask",
107108
"//checker:standard_decl",
108109
"//common:compiler_common",
109110
"//common:container",

bundle/src/main/java/dev/cel/bundle/CelEnvironment.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import dev.cel.checker.CelStandardDeclarations;
3131
import dev.cel.checker.CelStandardDeclarations.StandardFunction;
3232
import dev.cel.checker.CelStandardDeclarations.StandardOverload;
33+
import dev.cel.checker.ProtoTypeMask;
3334
import dev.cel.common.CelContainer;
3435
import dev.cel.common.CelFunctionDecl;
3536
import dev.cel.common.CelOptions;
@@ -134,6 +135,9 @@ public abstract class CelEnvironment {
134135
/** Limits to set in the environment. */
135136
public abstract ImmutableSet<Limit> limits();
136137

138+
/** Context variable to enable in the environment. */
139+
public abstract Optional<ContextVariable> contextVariable();
140+
137141
/** Builder for {@link CelEnvironment}. */
138142
@AutoValue.Builder
139143
public abstract static class Builder {
@@ -199,6 +203,8 @@ public Builder setLimits(Limit... limits) {
199203

200204
public abstract Builder setLimits(ImmutableSet<Limit> limits);
201205

206+
public abstract Builder setContextVariable(ContextVariable contextVariable);
207+
202208
abstract CelEnvironment autoBuild();
203209

204210
@CheckReturnValue
@@ -258,6 +264,12 @@ public CelCompiler extend(CelCompiler celCompiler, CelOptions celOptions)
258264

259265
applyStandardLibrarySubset(compilerBuilder);
260266

267+
contextVariable()
268+
.ifPresent(
269+
cv ->
270+
compilerBuilder.addProtoTypeMasks(
271+
ProtoTypeMask.ofAllFields(cv.typeName()).withFieldsAsVariableDeclarations()));
272+
261273
return compilerBuilder.build();
262274
} catch (RuntimeException e) {
263275
throw new CelEnvironmentException(e.getMessage(), e);
@@ -406,6 +418,17 @@ private static CanonicalCelExtension getExtensionOrThrow(String extensionName) {
406418
return extension;
407419
}
408420

421+
/** Represents a context variable declaration. */
422+
@AutoValue
423+
public abstract static class ContextVariable {
424+
/** Fully qualified type name of the context variable. */
425+
public abstract String typeName();
426+
427+
public static ContextVariable create(String typeName) {
428+
return new AutoValue_CelEnvironment_ContextVariable(typeName);
429+
}
430+
}
431+
409432
/** Represents a policy variable declaration. */
410433
@AutoValue
411434
public abstract static class VariableDecl {

bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.google.common.collect.ImmutableSet;
2929
import com.google.errorprone.annotations.CanIgnoreReturnValue;
3030
import dev.cel.bundle.CelEnvironment.Alias;
31+
import dev.cel.bundle.CelEnvironment.ContextVariable;
3132
import dev.cel.bundle.CelEnvironment.ExtensionConfig;
3233
import dev.cel.bundle.CelEnvironment.FunctionDecl;
3334
import dev.cel.bundle.CelEnvironment.LibrarySubset;
@@ -320,6 +321,36 @@ private ImmutableSet<String> parseAbbreviations(ParserContext<Node> ctx, Node no
320321
return builder.build();
321322
}
322323

324+
private ContextVariable parseContextVariable(ParserContext<Node> ctx, Node node) {
325+
long id = ctx.collectMetadata(node);
326+
if (!assertYamlType(ctx, id, node, YamlNodeType.MAP)) {
327+
return ContextVariable.create("");
328+
}
329+
330+
MappingNode mapNode = (MappingNode) node;
331+
String typeName = "";
332+
for (NodeTuple nodeTuple : mapNode.getValue()) {
333+
Node keyNode = nodeTuple.getKeyNode();
334+
long keyId = ctx.collectMetadata(keyNode);
335+
Node valueNode = nodeTuple.getValueNode();
336+
String keyName = ((ScalarNode) keyNode).getValue();
337+
switch (keyName) {
338+
case "type_name":
339+
typeName = newString(ctx, valueNode);
340+
break;
341+
default:
342+
ctx.reportError(keyId, String.format("Unsupported context_variable tag: %s", keyName));
343+
break;
344+
}
345+
}
346+
347+
if (typeName.isEmpty()) {
348+
ctx.reportError(id, "Missing required attribute(s): type_name");
349+
}
350+
351+
return ContextVariable.create(typeName);
352+
}
353+
323354
private ImmutableSet<VariableDecl> parseVariables(ParserContext<Node> ctx, Node node) {
324355
long valueId = ctx.collectMetadata(node);
325356
ImmutableSet.Builder<VariableDecl> variableSetBuilder = ImmutableSet.builder();
@@ -900,6 +931,9 @@ private CelEnvironment.Builder parseConfig(ParserContext<Node> ctx, Node node) {
900931
case "limits":
901932
builder.setLimits(parseLimits(ctx, valueNode));
902933
break;
934+
case "context_variable":
935+
builder.setContextVariable(parseContextVariable(ctx, valueNode));
936+
break;
903937
default:
904938
ctx.reportError(id, "Unknown config tag: " + fieldName);
905939
// continue handling the rest of the nodes

conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ java_library(
1212
deps = [
1313
"//:auto_value",
1414
"//bundle:cel",
15+
"//common/formats:value_string",
16+
"//common/formats:yaml_helper",
17+
"//policy",
18+
"//policy:parser",
19+
"//policy:parser_factory",
20+
"//policy:policy_parser_context",
1521
"//runtime:function_binding",
1622
"//testing/testrunner:cel_expression_source",
1723
"//testing/testrunner:cel_test_context",
@@ -23,5 +29,6 @@ java_library(
2329
"@maven//:com_google_guava_guava",
2430
"@maven//:com_google_protobuf_protobuf_java",
2531
"@maven//:junit_junit",
32+
"@maven//:org_yaml_snakeyaml",
2633
],
2734
)
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dev.cel.conformance.policy;
16+
17+
import static dev.cel.common.formats.YamlHelper.assertYamlType;
18+
19+
import dev.cel.common.formats.ValueString;
20+
import dev.cel.common.formats.YamlHelper.YamlNodeType;
21+
import dev.cel.policy.CelPolicy;
22+
import dev.cel.policy.CelPolicy.Match;
23+
import dev.cel.policy.CelPolicyParser.TagVisitor;
24+
import dev.cel.policy.PolicyParserContext;
25+
import java.util.IdentityHashMap;
26+
import java.util.Map;
27+
import org.yaml.snakeyaml.nodes.Node;
28+
import org.yaml.snakeyaml.nodes.SequenceNode;
29+
30+
/**
31+
* K8sTestTagHandler is a {@link TagVisitor} implementation to support parsing Kubernetes
32+
* ValidatingAdmissionPolicy structures in conformance tests.
33+
*/
34+
public final class K8sTestTagHandler implements TagVisitor<Node> {
35+
36+
private final Map<Match.Builder, Boolean> messageExpressionSeen = new IdentityHashMap<>();
37+
38+
@Override
39+
public void visitPolicyTag(
40+
PolicyParserContext<Node> ctx,
41+
long id,
42+
String tagName,
43+
Node node,
44+
CelPolicy.Builder policyBuilder) {
45+
switch (tagName) {
46+
case "kind":
47+
policyBuilder.putMetadata("kind", ctx.newYamlString(node).value());
48+
break;
49+
case "metadata":
50+
assertYamlType(ctx, id, node, YamlNodeType.MAP);
51+
break;
52+
case "spec":
53+
CelPolicy.Rule spec = ctx.parseRule(ctx, policyBuilder, node);
54+
policyBuilder.setRule(spec);
55+
break;
56+
default:
57+
ctx.reportError(id, String.format("Unsupported policy tag: %s", tagName));
58+
break;
59+
}
60+
}
61+
62+
@Override
63+
public void visitRuleTag(
64+
PolicyParserContext<Node> ctx,
65+
long id,
66+
String tagName,
67+
Node node,
68+
CelPolicy.Builder policyBuilder,
69+
CelPolicy.Rule.Builder ruleBuilder) {
70+
switch (tagName) {
71+
case "failurePolicy":
72+
policyBuilder.putMetadata(tagName, ctx.newYamlString(node).value());
73+
break;
74+
case "matchConstraints":
75+
assertYamlType(ctx, id, node, YamlNodeType.MAP);
76+
break;
77+
case "validations":
78+
if (!assertYamlType(ctx, id, node, YamlNodeType.LIST)) {
79+
return;
80+
}
81+
SequenceNode seqNode = (SequenceNode) node;
82+
for (Node valNode : seqNode.getValue()) {
83+
ruleBuilder.addMatches(ctx.parseMatch(ctx, policyBuilder, valNode));
84+
}
85+
break;
86+
default:
87+
ctx.reportError(id, String.format("Unsupported rule tag: %s", tagName));
88+
break;
89+
}
90+
}
91+
92+
@Override
93+
public void visitMatchTag(
94+
PolicyParserContext<Node> ctx,
95+
long id,
96+
String tagName,
97+
Node node,
98+
CelPolicy.Builder policyBuilder,
99+
CelPolicy.Match.Builder matchBuilder) {
100+
switch (tagName) {
101+
case "expression":
102+
// The K8s expression to validate must return false in order to generate a violation
103+
// message.
104+
ValueString condition = ctx.newSourceString(node);
105+
String invertedCondition = "!(" + condition.value() + ")";
106+
matchBuilder.setCondition(ValueString.of(condition.id(), invertedCondition));
107+
108+
// If we haven't handled messageExpression yet, set the default output.
109+
if (!messageExpressionSeen.getOrDefault(matchBuilder, false)) {
110+
matchBuilder.setResult(
111+
Match.Result.ofOutput(ValueString.of(ctx.nextId(), "'invalid admission request'")));
112+
}
113+
break;
114+
case "messageExpression":
115+
matchBuilder.setResult(Match.Result.ofOutput(ctx.newSourceString(node)));
116+
messageExpressionSeen.put(matchBuilder, true);
117+
break;
118+
default:
119+
ctx.reportError(id, String.format("Unsupported match tag: %s", tagName));
120+
break;
121+
}
122+
}
123+
}

conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import dev.cel.bundle.Cel;
1919
import dev.cel.bundle.CelFactory;
2020
import dev.cel.expr.conformance.proto3.TestAllTypes;
21+
import dev.cel.policy.CelPolicyParserFactory;
2122
import dev.cel.runtime.CelFunctionBinding;
2223
import dev.cel.testing.testrunner.CelExpressionSource;
2324
import dev.cel.testing.testrunner.CelTestContext;
@@ -77,6 +78,15 @@ public void evaluate() throws Throwable {
7778
TestAllTypes.getDescriptor().getFile(),
7879
Struct.getDescriptor().getFile());
7980

81+
// Scopes the custom Kubernetes tag visitor exclusively to k8s tests to prevent non-standard
82+
// grammar leakage.
83+
if (name.startsWith("k8s/")) {
84+
contextBuilder.setCelPolicyParser(
85+
CelPolicyParserFactory.newYamlParserBuilder()
86+
.addTagVisitor(new K8sTestTagHandler())
87+
.build());
88+
}
89+
8090
Path yamlConfigPath = Paths.get(dirPath, "config.yaml");
8191
Path textprotoConfigPath = Paths.get(dirPath, "config.textproto");
8292

testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ java_library(
161161
deps = [
162162
":cel_expression_source",
163163
":default_result_matcher",
164+
":registry_utils",
164165
":result_matcher",
165166
"//:auto_value",
166167
"//bundle:cel",

testing/src/main/java/dev/cel/testing/testrunner/CelTestContext.java

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -140,21 +140,21 @@ public Optional<CelDescriptors> celDescriptors() {
140140
return Optional.empty();
141141
}
142142

143+
/** Returns a unified set of {@link CelDescriptors} combined from all descriptor sources. */
143144
@Memoized
144-
public Optional<TypeRegistry> typeRegistry() {
145+
public Optional<CelDescriptors> mergedDescriptors() {
145146
if (fileTypes().isEmpty() && !fileDescriptorSetPath().isPresent()) {
146147
return Optional.empty();
147148
}
148-
TypeRegistry.Builder builder = TypeRegistry.newBuilder();
149-
if (!fileTypes().isEmpty()) {
150-
builder.add(
151-
CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(fileTypes())
152-
.messageTypeDescriptors());
153-
}
154-
if (celDescriptors().isPresent()) {
155-
builder.add(celDescriptors().get().messageTypeDescriptors());
156-
}
157-
return Optional.of(builder.build());
149+
ImmutableSet.Builder<FileDescriptor> allFiles =
150+
ImmutableSet.<FileDescriptor>builder().addAll(fileTypes());
151+
celDescriptors().ifPresent(d -> allFiles.addAll(d.fileDescriptors()));
152+
return Optional.of(CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(allFiles.build()));
153+
}
154+
155+
@Memoized
156+
public Optional<TypeRegistry> typeRegistry() {
157+
return mergedDescriptors().map(RegistryUtils::getTypeRegistry);
158158
}
159159

160160
public abstract Optional<ExtensionRegistry> extensionRegistry();

testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -360,18 +360,33 @@ private static Object getEvaluationResultWithMessage(
360360
}
361361

362362
private static Message unpackAny(Any any, CelTestContext celTestContext) throws IOException {
363-
if (!celTestContext.fileDescriptorSetPath().isPresent()) {
364-
throw new IllegalArgumentException(
365-
"Proto descriptors are required for unpacking Any messages.");
363+
TypeRegistry typeRegistry =
364+
celTestContext
365+
.typeRegistry()
366+
.orElseThrow(
367+
() ->
368+
new IllegalArgumentException(
369+
"Proto descriptors or type registry are required for unpacking Any"
370+
+ " messages."));
371+
372+
Descriptor descriptor = typeRegistry.getDescriptorForTypeUrl(any.getTypeUrl());
373+
if (descriptor == null) {
374+
throw new IllegalArgumentException("Descriptor not found for type URL: " + any.getTypeUrl());
366375
}
367-
Descriptor descriptor =
368-
RegistryUtils.getTypeRegistry(celTestContext.celDescriptors().get())
369-
.getDescriptorForTypeUrl(any.getTypeUrl());
376+
377+
ExtensionRegistry extensionRegistry =
378+
celTestContext
379+
.extensionRegistry()
380+
.orElseGet(
381+
() ->
382+
celTestContext
383+
.mergedDescriptors()
384+
.map(RegistryUtils::getExtensionRegistry)
385+
.orElseGet(ExtensionRegistry::getEmptyRegistry));
386+
370387
return DynamicMessage.getDefaultInstance(descriptor)
371388
.getParserForType()
372-
.parseFrom(
373-
any.getValue(),
374-
RegistryUtils.getExtensionRegistry(celTestContext.celDescriptors().get()));
389+
.parseFrom(any.getValue(), extensionRegistry);
375390
}
376391

377392
private static Message getEvaluatedContextExpr(

testing/src/test/java/dev/cel/testing/testrunner/TestRunnerLibraryTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ public void runTest_missingProtoDescriptors_failure() throws Exception {
262262

263263
assertThat(thrown)
264264
.hasMessageThat()
265-
.contains("Proto descriptors are required for unpacking Any messages.");
265+
.contains("Proto descriptors or type registry are required for unpacking Any messages");
266266
}
267267

268268
@Test

0 commit comments

Comments
 (0)