Skip to content

Commit 20c14b4

Browse files
dmitriplotnikovcopybara-github
authored andcommitted
Add compile_cel_policy Starlark macro and CelPolicyCompilerTool CLI.
Provides a Bazel `compile_cel_policy` rule and backing `CelPolicyCompilerTool` CLI to compile YAML CEL policies into `CheckedExpr` protobuf artifacts (`binarypb`, `textpb`, or `textproto`) at build time. Supports YAML environment configurations, transitive proto descriptor sets, and optional `SelectOptimizer` field selection optimization (`--optimize_field_selection`). PiperOrigin-RevId: 995482167
1 parent fa4a240 commit 20c14b4

7 files changed

Lines changed: 1439 additions & 0 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
load("@rules_java//java:defs.bzl", "java_binary", "java_library")
2+
3+
package(
4+
default_applicable_licenses = [
5+
"//:license",
6+
],
7+
default_visibility = [
8+
"//policy/tools:__pkg__",
9+
],
10+
)
11+
12+
java_library(
13+
name = "tools",
14+
srcs = ["CelPolicyCompilerTool.java"],
15+
tags = [
16+
],
17+
deps = [
18+
"//bundle:cel",
19+
"//bundle:environment",
20+
"//bundle:environment_yaml_parser",
21+
"//common:cel_ast",
22+
"//common:cel_descriptor_util",
23+
"//common:options",
24+
"//common:proto_ast",
25+
"//common:proto_v1alpha1_ast",
26+
"//extensions",
27+
"//extensions:optional_library",
28+
"//optimizer/optimizers:common_subexpression_elimination",
29+
"//optimizer/optimizers:constant_folding",
30+
"//optimizer/optimizers:select_optimizer",
31+
"//parser:macro",
32+
"//policy",
33+
"//policy:compiler",
34+
"//policy:compiler_builder",
35+
"//policy:compiler_factory",
36+
"//policy:parser",
37+
"//policy:parser_factory",
38+
"@maven//:com_google_guava_guava",
39+
"@maven//:com_google_protobuf_protobuf_java",
40+
"@maven//:info_picocli_picocli",
41+
],
42+
)
43+
44+
java_binary(
45+
name = "cel_policy_compiler_tool",
46+
main_class = "dev.cel.policy.tools.CelPolicyCompilerTool",
47+
neverlink = 1,
48+
runtime_deps = [":tools"],
49+
)
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
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.policy.tools;
16+
17+
import static java.nio.charset.StandardCharsets.UTF_8;
18+
19+
import com.google.common.base.Ascii;
20+
import com.google.common.collect.ImmutableList;
21+
import com.google.common.collect.ImmutableSet;
22+
import com.google.protobuf.DescriptorProtos.FileDescriptorSet;
23+
import com.google.protobuf.Descriptors.FileDescriptor;
24+
import com.google.protobuf.ExtensionRegistry;
25+
import com.google.protobuf.Message;
26+
import com.google.protobuf.TextFormat;
27+
import dev.cel.bundle.Cel;
28+
import dev.cel.bundle.CelBuilder;
29+
import dev.cel.bundle.CelEnvironment;
30+
import dev.cel.bundle.CelEnvironmentYamlParser;
31+
import dev.cel.bundle.CelFactory;
32+
import dev.cel.common.CelAbstractSyntaxTree;
33+
import dev.cel.common.CelDescriptorUtil;
34+
import dev.cel.common.CelOptions;
35+
import dev.cel.common.CelProtoAbstractSyntaxTree;
36+
import dev.cel.common.CelProtoV1Alpha1AbstractSyntaxTree;
37+
import dev.cel.extensions.CelExtensions;
38+
import dev.cel.extensions.CelOptionalLibrary;
39+
import dev.cel.optimizer.optimizers.ConstantFoldingOptimizer;
40+
import dev.cel.optimizer.optimizers.SelectOptimizer;
41+
import dev.cel.optimizer.optimizers.SelectOptimizer.SelectOptimizerOptions;
42+
import dev.cel.optimizer.optimizers.SubexpressionOptimizer;
43+
import dev.cel.optimizer.optimizers.SubexpressionOptimizer.SubexpressionOptimizerOptions;
44+
import dev.cel.parser.CelStandardMacro;
45+
import dev.cel.policy.CelPolicy;
46+
import dev.cel.policy.CelPolicyCompiler;
47+
import dev.cel.policy.CelPolicyCompilerBuilder;
48+
import dev.cel.policy.CelPolicyCompilerFactory;
49+
import dev.cel.policy.CelPolicyParser;
50+
import dev.cel.policy.CelPolicyParserFactory;
51+
import java.io.BufferedWriter;
52+
import java.io.FileOutputStream;
53+
import java.io.IOException;
54+
import java.io.OutputStreamWriter;
55+
import java.io.PrintWriter;
56+
import java.nio.file.Files;
57+
import java.nio.file.Path;
58+
import java.nio.file.Paths;
59+
import java.util.concurrent.Callable;
60+
import picocli.CommandLine;
61+
import picocli.CommandLine.Model.CommandSpec;
62+
import picocli.CommandLine.Option;
63+
import picocli.CommandLine.Parameters;
64+
import picocli.CommandLine.Spec;
65+
66+
/**
67+
* CelPolicyCompilerTool is a binary tool that compiles a CEL policy (.celpolicy or YAML) into a
68+
* CheckedExpr protobuf message and writes it to a file or stdout.
69+
*/
70+
public final class CelPolicyCompilerTool implements Callable<Integer> {
71+
72+
@Spec private CommandSpec spec;
73+
74+
@Option(
75+
names = {"--policy"},
76+
description = "Path to the CEL policy file")
77+
private String policyPath = "";
78+
79+
@SuppressWarnings("FieldCanBeFinal") // Populated reflectively by picocli
80+
@Parameters(
81+
index = "0",
82+
arity = "0..1",
83+
description = "Positional path to the CEL policy file if --policy is not specified")
84+
private String positionalPolicyPath = "";
85+
86+
@Option(
87+
names = {"--config", "--environment_path"},
88+
description = "Path to the CEL environment (in YAML)")
89+
private String configPath = "";
90+
91+
@Option(
92+
names = {"--base_config"},
93+
description = "Path to the base CEL environment (in YAML)")
94+
private String baseConfigPath = "";
95+
96+
@Option(
97+
names = {"--transitive_descriptor_set", "--file_descriptor_set"},
98+
description = "Path to the transitive set of descriptors")
99+
private String transitiveDescriptorSetPath = "";
100+
101+
@Option(
102+
names = {"--output"},
103+
description = "Output path for the compiled binarypb/textpb")
104+
private String output = "";
105+
106+
@Option(
107+
names = {"--output_format"},
108+
defaultValue = "binarypb",
109+
description = "Output format: binarypb, textpb, or textproto")
110+
private String outputFormat = "binarypb";
111+
112+
@Option(
113+
names = {"--output_version"},
114+
defaultValue = "canonical",
115+
description = "Output version: canonical or v1alpha1")
116+
private String outputVersion = "canonical";
117+
118+
@Option(
119+
names = {"--optimize_field_selection"},
120+
description = "Optimize field selection for version skew mitigation")
121+
private boolean optimizeFieldSelection = false;
122+
123+
private static final CelOptions CEL_OPTIONS =
124+
CelOptions.current()
125+
.populateMacroCalls(true)
126+
.enableHeterogeneousNumericComparisons(true)
127+
.build();
128+
129+
private PrintWriter out() {
130+
return spec != null
131+
? spec.commandLine().getOut()
132+
: new PrintWriter(new OutputStreamWriter(System.out, UTF_8), true);
133+
}
134+
135+
private PrintWriter err() {
136+
return spec != null
137+
? spec.commandLine().getErr()
138+
: new PrintWriter(new OutputStreamWriter(System.err, UTF_8), true);
139+
}
140+
141+
@Override
142+
public Integer call() {
143+
String effectivePolicyPath = policyPath.isEmpty() ? positionalPolicyPath : policyPath;
144+
if (effectivePolicyPath.isEmpty()) {
145+
err()
146+
.println(
147+
"Error: Policy file path must be specified via --policy or as a positional"
148+
+ " argument.");
149+
return -1;
150+
}
151+
152+
Cel cel;
153+
ImmutableSet<FileDescriptor> transitiveFileDescriptors;
154+
try {
155+
CelBuilder celBuilder =
156+
CelFactory.plannerCelBuilder()
157+
.setOptions(CEL_OPTIONS)
158+
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
159+
.addCompilerLibraries(
160+
CelOptionalLibrary.INSTANCE,
161+
CelExtensions.bindings(),
162+
CelExtensions.comprehensions())
163+
.addRuntimeLibraries(CelOptionalLibrary.INSTANCE);
164+
165+
if (!transitiveDescriptorSetPath.isEmpty()) {
166+
transitiveFileDescriptors =
167+
CelDescriptorUtil.getFileDescriptorsFromFileDescriptorSet(
168+
load(transitiveDescriptorSetPath));
169+
celBuilder.addFileTypes(transitiveFileDescriptors);
170+
} else {
171+
transitiveFileDescriptors = ImmutableSet.of();
172+
}
173+
174+
cel = celBuilder.build();
175+
176+
CelEnvironmentYamlParser environmentYamlParser = CelEnvironmentYamlParser.newInstance();
177+
if (!baseConfigPath.isEmpty()) {
178+
validatePolicyFileType(baseConfigPath, "base CEL environment");
179+
String baseYaml = new String(readFileBytes(baseConfigPath), UTF_8);
180+
CelEnvironment baseEnv = environmentYamlParser.parse(baseYaml, baseConfigPath);
181+
cel = baseEnv.extend(cel, CEL_OPTIONS);
182+
}
183+
184+
if (!configPath.isEmpty()) {
185+
validatePolicyFileType(configPath, "CEL environment");
186+
String envYaml = new String(readFileBytes(configPath), UTF_8);
187+
CelEnvironment env = environmentYamlParser.parse(envYaml, configPath);
188+
cel = env.extend(cel, CEL_OPTIONS);
189+
}
190+
} catch (Exception e) {
191+
err().printf("Failed to create a CEL compilation environment. Reason: %s%n", e.getMessage());
192+
return -1;
193+
}
194+
195+
CelPolicy policy;
196+
try {
197+
CelPolicyParser policyParser =
198+
CelPolicyParserFactory.newYamlParserBuilder().enableSimpleVariables(true).build();
199+
String policyYaml = new String(readFileBytes(effectivePolicyPath), UTF_8);
200+
policy = policyParser.parse(policyYaml, effectivePolicyPath);
201+
} catch (Exception e) {
202+
err()
203+
.printf(
204+
"Failed to parse CEL policy: [%s]. Reason: %s%n",
205+
effectivePolicyPath, e.getMessage());
206+
return -1;
207+
}
208+
209+
try {
210+
CelPolicyCompilerBuilder policyCompilerBuilder =
211+
CelPolicyCompilerFactory.newPolicyCompiler(cel);
212+
213+
if (optimizeFieldSelection) {
214+
policyCompilerBuilder.setOptimizers(
215+
ImmutableList.of(
216+
ConstantFoldingOptimizer.getInstance(),
217+
SubexpressionOptimizer.newInstance(
218+
SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()),
219+
SelectOptimizer.newInstance(
220+
SelectOptimizerOptions.newBuilder().build(), transitiveFileDescriptors)));
221+
}
222+
223+
CelPolicyCompiler policyCompiler = policyCompilerBuilder.build();
224+
CelAbstractSyntaxTree ast = policyCompiler.compile(policy);
225+
226+
writeOutput(ast, output, outputFormat, outputVersion);
227+
} catch (Exception e) {
228+
err()
229+
.printf(
230+
"%nFailed to compile CEL policy: [%s].%nReason: %s%n%n",
231+
effectivePolicyPath, e.getMessage());
232+
return -1;
233+
}
234+
235+
return 0;
236+
}
237+
238+
private void writeOutput(
239+
CelAbstractSyntaxTree ast, String filePath, String format, String version)
240+
throws IOException {
241+
Message checkedExpr;
242+
if (Ascii.equalsIgnoreCase("v1alpha1", version)) {
243+
checkedExpr = CelProtoV1Alpha1AbstractSyntaxTree.fromCelAst(ast).toCheckedExpr();
244+
} else if (Ascii.equalsIgnoreCase("canonical", version)) {
245+
checkedExpr = CelProtoAbstractSyntaxTree.fromCelAst(ast).toCheckedExpr();
246+
} else {
247+
throw new IllegalArgumentException(
248+
"Unsupported output version: " + version + ". Supported versions: canonical, v1alpha1");
249+
}
250+
251+
boolean isText =
252+
Ascii.equalsIgnoreCase("textpb", format) || Ascii.equalsIgnoreCase("textproto", format);
253+
if (!isText && !Ascii.equalsIgnoreCase("binarypb", format)) {
254+
throw new IllegalArgumentException(
255+
"Unsupported output format: "
256+
+ format
257+
+ ". Supported formats: binarypb, textpb, textproto");
258+
}
259+
260+
if (filePath.isEmpty() || filePath.equals("-")) {
261+
if (isText) {
262+
PrintWriter writer = out();
263+
TextFormat.printer().print(checkedExpr, writer);
264+
writer.flush();
265+
} else {
266+
checkedExpr.writeTo(System.out);
267+
System.out.flush();
268+
}
269+
} else {
270+
Path path = Paths.get(filePath);
271+
if (path.getParent() != null) {
272+
Files.createDirectories(path.getParent());
273+
}
274+
if (isText) {
275+
try (BufferedWriter writer = Files.newBufferedWriter(path, UTF_8)) {
276+
TextFormat.printer().print(checkedExpr, writer);
277+
}
278+
} else {
279+
try (FileOutputStream outputStream = new FileOutputStream(path.toFile())) {
280+
checkedExpr.writeTo(outputStream);
281+
}
282+
}
283+
}
284+
}
285+
286+
private static void validatePolicyFileType(String path, String description) {
287+
String lower = Ascii.toLowerCase(path.trim());
288+
if (!lower.endsWith(".yaml") && !lower.endsWith(".yml") && !lower.endsWith(".celpolicy")) {
289+
throw new IllegalArgumentException(
290+
String.format("Only YAML format is supported for %s. Got: %s", description, path));
291+
}
292+
}
293+
294+
private static FileDescriptorSet load(String descriptorSetPath) {
295+
try {
296+
byte[] descriptorBytes = readFileBytes(descriptorSetPath);
297+
return FileDescriptorSet.parseFrom(descriptorBytes, ExtensionRegistry.getEmptyRegistry());
298+
} catch (IOException e) {
299+
throw new IllegalArgumentException(
300+
"Failed to load FileDescriptorSet from path: " + descriptorSetPath, e);
301+
}
302+
}
303+
304+
private static byte[] readFileBytes(String path) throws IOException {
305+
return Files.readAllBytes(Paths.get(path));
306+
}
307+
308+
public static void main(String[] args) {
309+
CelPolicyCompilerTool compilerTool = new CelPolicyCompilerTool();
310+
CommandLine cmd = new CommandLine(compilerTool);
311+
cmd.setTrimQuotes(false);
312+
int exitCode = cmd.execute(args);
313+
System.exit(exitCode);
314+
}
315+
316+
CelPolicyCompilerTool() {}
317+
}

0 commit comments

Comments
 (0)