pub mod access;
pub mod collections;
pub mod context;
pub mod expr;
pub mod functions;
pub mod helpers;
pub mod literals;
pub mod operators;
#[cfg(feature = "k8s-vap")]
#[cfg_attr(docsrs, doc(cfg(feature = "k8s-vap")))]
pub mod vap;
use std::collections::{BTreeSet, HashMap};
use anyhow::Context;
use cel::{common::ast::Expr, parser::Parser};
pub use context::ExtensionKey;
use context::{CompilerContext, CompilerEnv};
use ferricel_types::{
extensions::{BuilderChainDecl, ExtensionDecl, UsedExtension},
functions::RuntimeFunction,
};
use walrus::{FunctionBuilder, FunctionId, ModuleConfig, ValType};
const RUNTIME_BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/runtime.wasm"));
use crate::schema::ProtoSchema;
pub struct Builder {
proto_descriptor: Option<Vec<u8>>,
container: Option<String>,
logger: slog::Logger,
extensions: BTreeSet<ExtensionDecl>,
builder_chains: Vec<BuilderChainDecl>,
}
impl Builder {
pub fn new() -> Self {
Self {
proto_descriptor: None,
container: None,
logger: slog::Logger::root(slog::Discard, slog::o!()),
extensions: BTreeSet::new(),
builder_chains: Vec::new(),
}
}
pub fn with_logger(mut self, logger: slog::Logger) -> Self {
self.logger = logger;
self
}
pub fn with_proto_descriptor(mut self, bytes: Vec<u8>) -> Result<Self, anyhow::Error> {
ProtoSchema::from_descriptor_set(&bytes)?;
self.proto_descriptor = Some(bytes);
Ok(self)
}
pub fn with_container(mut self, container: impl Into<String>) -> Self {
self.container = Some(container.into());
self
}
pub fn with_extension(mut self, decl: ExtensionDecl) -> Self {
self.extensions.insert(decl);
self
}
pub fn with_builder_chain(mut self, decl: BuilderChainDecl) -> Self {
self.builder_chains.push(decl);
self
}
pub fn build(self) -> Compiler {
let schema = self.proto_descriptor.as_ref().map(|b| {
ProtoSchema::from_descriptor_set(b).expect("proto descriptor already validated")
});
Compiler {
schema,
container: self.container,
logger: self.logger,
extensions: self.extensions,
builder_chains: self.builder_chains,
}
}
}
impl Default for Builder {
fn default() -> Self {
Self::new()
}
}
pub struct Compiler {
schema: Option<ProtoSchema>,
container: Option<String>,
logger: slog::Logger,
extensions: BTreeSet<ExtensionDecl>,
builder_chains: Vec<BuilderChainDecl>,
}
impl Compiler {
pub fn compile(&self, cel_code: &str) -> Result<Vec<u8>, anyhow::Error> {
let mut module = ModuleConfig::new().parse(RUNTIME_BYTES)?;
let mut functions = HashMap::new();
for func in RuntimeFunction::iter() {
let id = module.exports.get_func(func.name()).with_context(|| {
format!(
"Runtime function '{}' not found in module exports",
func.name()
)
})?;
functions.insert(func, id);
if !func.is_exported() {
module.exports.remove(func.name())?;
}
}
let env = CompilerEnv { functions };
let root_ast = Parser::new()
.enable_optional_syntax(true)
.parse(cel_code)
.map_err(|e| anyhow::anyhow!("Parse error: {:?}", e))?;
let ctx = CompilerContext::new(
self.schema.clone(),
self.container.clone(),
self.logger.clone(),
&self.extensions,
&self.builder_chains,
);
let evaluate_id = build_evaluate_function(&mut module, &env, &ctx, &root_ast.expr)?;
module.exports.add("evaluate", evaluate_id);
let evaluate_proto_id =
build_evaluate_proto_function(&mut module, &env, &ctx, &root_ast.expr)?;
module.exports.add("evaluate_proto", evaluate_proto_id);
walrus::passes::gc::run(&mut module);
add_producers_entries(&mut module);
module.customs.add(walrus::RawCustomSection {
name: "ferricel.cel-source".to_string(),
data: cel_code.as_bytes().to_vec(),
});
let used: Vec<UsedExtension> = ctx.used_extensions.borrow().iter().cloned().collect();
module.customs.add(walrus::RawCustomSection {
name: "ferricel.extensions".to_string(),
data: serde_json::to_vec(&used).context("Failed to serialize used extensions")?,
});
Ok(module.emit_wasm())
}
#[cfg(feature = "k8s-vap")]
#[cfg_attr(docsrs, doc(cfg(feature = "k8s-vap")))]
pub fn compile_vap(&self, vap_yaml: &str) -> Result<Vec<u8>, anyhow::Error> {
let policy = parse_vap_yaml(vap_yaml)?;
self.compile_vap_from_policy(&policy)
}
#[cfg(feature = "k8s-vap")]
#[cfg_attr(docsrs, doc(cfg(feature = "k8s-vap")))]
pub fn compile_vap_from_policy(
&self,
policy: &k8s_openapi::api::admissionregistration::v1::ValidatingAdmissionPolicy,
) -> Result<Vec<u8>, anyhow::Error> {
Ok(self.build_vap_module(policy)?.emit_wasm())
}
#[cfg(feature = "k8s-vap")]
fn build_vap_module(
&self,
policy: &k8s_openapi::api::admissionregistration::v1::ValidatingAdmissionPolicy,
) -> Result<walrus::Module, anyhow::Error> {
let spec = policy
.spec
.as_ref()
.ok_or_else(|| anyhow::anyhow!("ValidatingAdmissionPolicy has no spec"))?;
if spec.validations.as_deref().unwrap_or(&[]).is_empty() {
anyhow::bail!("ValidatingAdmissionPolicy must have at least one validation");
}
let extensions = self.extensions.clone();
let mut builder_chains = self.builder_chains.clone();
builder_chains.push(vap::kw_k8s_chain());
let mut module = ModuleConfig::new().parse(RUNTIME_BYTES)?;
let mut functions = HashMap::new();
for func in RuntimeFunction::iter() {
let id = module.exports.get_func(func.name()).with_context(|| {
format!(
"Runtime function '{}' not found in module exports",
func.name()
)
})?;
functions.insert(func, id);
if !func.is_exported() {
module.exports.remove(func.name())?;
}
}
let env = CompilerEnv { functions };
let ctx = CompilerContext::new(
self.schema.clone(),
self.container.clone(),
self.logger.clone(),
&extensions,
&builder_chains,
);
let evaluate_id = vap::build_vap_evaluate_function(&mut module, &env, &ctx, spec)?;
module.exports.add("evaluate", evaluate_id);
if spec.param_kind.is_some() {
ctx.record_extension(Some("kw.k8s"), "get");
}
walrus::passes::gc::run(&mut module);
add_producers_entries(&mut module);
let vap_yaml = yaml_serde::to_string(policy)
.context("Failed to serialize ValidatingAdmissionPolicy to YAML")?;
module.customs.add(walrus::RawCustomSection {
name: "ferricel.vap-source".to_string(),
data: vap_yaml.into_bytes(),
});
let used: Vec<UsedExtension> = ctx.used_extensions.borrow().iter().cloned().collect();
module.customs.add(walrus::RawCustomSection {
name: "ferricel.extensions".to_string(),
data: serde_json::to_vec(&used).context("Failed to serialize used extensions")?,
});
Ok(module)
}
}
fn add_producers_entries(module: &mut walrus::Module) {
module.producers.add_language("CEL", "");
module
.producers
.add_processed_by("ferricel", env!("CARGO_PKG_VERSION"));
}
pub fn extensions_used(wasm: &[u8]) -> Result<Vec<UsedExtension>, anyhow::Error> {
Ok(crate::inspect(wasm)?.extensions)
}
fn build_evaluate_function(
module: &mut walrus::Module,
env: &CompilerEnv,
ctx: &CompilerContext,
expr: &Expr,
) -> Result<FunctionId, anyhow::Error> {
let mut func = FunctionBuilder::new(&mut module.types, &[ValType::I64], &[ValType::I64]);
let bindings_encoded_arg = module.locals.add(ValType::I64);
let mut body = func.func_body();
body.local_get(bindings_encoded_arg)
.call(env.get(RuntimeFunction::DeserializeJson))
.call(env.get(RuntimeFunction::InitBindings));
expr::compile_expr(expr, &mut body, env, ctx, module)?;
body.call(env.get(RuntimeFunction::SerializeResult));
Ok(func.finish(vec![bindings_encoded_arg], &mut module.funcs))
}
fn build_evaluate_proto_function(
module: &mut walrus::Module,
env: &CompilerEnv,
ctx: &CompilerContext,
expr: &Expr,
) -> Result<FunctionId, anyhow::Error> {
let mut func = FunctionBuilder::new(&mut module.types, &[ValType::I64], &[ValType::I64]);
let bindings_encoded_arg = module.locals.add(ValType::I64);
let mut body = func.func_body();
body.local_get(bindings_encoded_arg)
.call(env.get(RuntimeFunction::DeserializeProto))
.call(env.get(RuntimeFunction::InitBindings));
expr::compile_expr(expr, &mut body, env, ctx, module)?;
body.call(env.get(RuntimeFunction::SerializeResult));
Ok(func.finish(vec![bindings_encoded_arg], &mut module.funcs))
}
#[cfg(feature = "k8s-vap")]
pub(crate) fn parse_vap_yaml(
yaml: &str,
) -> Result<k8s_openapi::api::admissionregistration::v1::ValidatingAdmissionPolicy, anyhow::Error> {
use k8s_openapi::api::admissionregistration::v1::ValidatingAdmissionPolicy;
use serde::Deserialize as _;
let mut iter = yaml_serde::Deserializer::from_str(yaml);
let first = iter
.next()
.ok_or_else(|| anyhow::anyhow!("YAML is empty, expected a ValidatingAdmissionPolicy"))?;
let policy = ValidatingAdmissionPolicy::deserialize(first)
.map_err(|e| anyhow::anyhow!("Failed to parse ValidatingAdmissionPolicy YAML: {}", e))?;
if iter.next().is_some() {
anyhow::bail!(
"Expected exactly one ValidatingAdmissionPolicy document, but found more than one"
);
}
Ok(policy)
}