OPA (Open Policy Agent) is a policy enforcement engine that can be used for a variety of purposes. OPA's access policies are written in a language called rego. A CNCF-graduated project, it's been incorporated into a number of different products.You can see the list of adopters here.
We chose OPA to enforce database access policies because of its flexibility to write polices as per policy author's need and familiarity in the cloud-native ecosystem.
OPA gives three options to enforce access polices:
go library
rest service
WASM
The inspektor dataplane is written in rust, so we cannot use the go library to enforce policies in inspektor. For the simplicity, we decided to use WASM to evaluate access policies rather than run a separate rest service.
Rego policies can be compiled into a wasm module using OPA. The compiled WASM module expose necessary functions to evaluate polices in other language.
WASM Compilation
burrego crate was built by people at kuberwardern to evaluate rego policies in rust. In this tutorial, we will learn how to evaluate wasm-compiled rego using the burrego crate.
Let's first write a rego programme to evaluate before moving on to the evaluation itself. In the given rego program, set the rule hello to true if the given input message input.message is world.
package play default hello = false hello { m := input.message m == "world" }
To make use of the policy, run the following command to compile the policy to wasm for the entrypoint play/hello
opa build -t wasm -e play/hello policy.rego
The above command will create a bundle.tar.gz file. The tar files contain the following files.
/data.json /policy.rego /policy.wasm /.manifest
For this tutorial, we care only about the policy.wasm file, since policy.wasm file is the compiled wasm module of rego policy.
Rust integration
let's add a burrego crate as a dependency to our rust program.
Evaluator::new will take policy as an input and return the Evaluator object.
let policy =fs::read("./policy.wasm").unwrap(); letmut evaluator =Evaluator::new( String::from("demo-policy"), &policy, &DEFAULT_HOST_CALLBACKS, ).unwrap();
during the evaluation, the entrypoint id is specified to evaluate the entrypoints. Using the entrypoint_id function, the id of the entry point can be retrieved. We are retrieving the entrypoint id for play/hello in the following snippet.
let entrypoint_id = evaluator.entrypoint_id(&"play/hello")
The policy will be evaluated using evaluate function. The evaluate function takes entrypoint's id, input and data as paramenter/
let input =serde_json::from_str(r#"{"message":"world"}"#).unwrap(); let data =serde_json::from_str("{}").unwrap(); let hello = evaluator.evaluate(entrypoint_id,&input,&data).unwrap(); println!("{}", hello);
We got true for play/hello entrypoint because we passed message as world. We would have received a false result if we had used a different value.
[{"result":true}]
I hope you learned how to use evaluate opa policies in rust. Feel free to join our community in discord where you can follow our development and participate.
Many of us use profiler to measure the CPU or memory consumed by the piece of code. This led me to figure out how profilers work.
To learn about profiling, I groked a popular profiling crate pprof-rs. This library is used to measure the CPU usage of a rust program.
If you are also interested in contributing to open source code or want to learn how to read complex project source code. I would highly recommend Contributing to Complex Projects by Mitchell Hashimoto.
let's just profile a sample rust program and see how pprof used.
Here is the modifled example program that I've taken from pprof-rs. You find the full source here.
The sample program calculates the number of prime numbers from 1 to 50000.
fnmain(){ let prime_numbers =prepare_prime_numbers(); // start profiling let guard =pprof::ProfilerGuardBuilder::default() .frequency(100) .build() .unwrap(); letmut v =0; for i in1..50000{ // use `is_prime_number1` function only if the incoming value // i is divisable by 3. if i %3==0{ ifis_prime_number1(i,&prime_numbers){ v +=1; } } else{ ifis_prime_number2(i,&prime_numbers){ v +=1; } } } println!("Prime numbers: {}", v); // stop profiling and generate the profiled report. ifletOk(report)= guard.report().build(){ letmut file =File::create("profile.pb").unwrap(); let profile = report.pprof().unwrap(); letmut content =Vec::new(); profile.write_to_vec(&mut content).unwrap(); file.write_all(&content).unwrap(); }; }
In the above example, We started profiling at the beginning of the program using ProfilerGuardBuilder
let guard =pprof::ProfilerGuardBuilder::default() .frequency(100) .build() .unwrap();
At the end of the program, we generated and wrote the report to profile.pb file.
The report is generated by running the program and it's visualized using google's pprof
~/go/bin/pprof --http=localhost:8080 profile.pb
After executing the above command, pprof will let you to visualize the profile at http://localhost:8080
From the visualized profile, you can clearly see that is_prime_number2 have consumed more cpu than is_prime_number1. That's because is_prime_number1 is used only the given number is divisible by 3.
Now, that we learned how to profile rust program using pprof-rs. Let's learn how pprof-rs works internally.
Please don't get too worn out yet! So far, we've learned the basics of profiler and how to use pprof-rs. Before we begin internal working of profiler, let's take a sip of water to rehydrate ourselves.
Before we get into pprof-rs code, let's learn cpu profiling in theory.
Profiler pause the program in certain interval of time and resumes after sampling the current stack trace. While sampling, it takes each stack frame and increments its count. Then the sampled data is then used to create a flamegraph or something similar.
stack traces: stack traces are the list of call stack of function calls. for eg: is_prime_number_1 -> main
When you start the profiling with ProfilerGuardBuilder, pprof-rs will register signal handler and timer to specify how often programs is supposed to pause.
let guard =pprof::ProfilerGuardBuilder::default() .frequency(100) .build() .unwrap();
registering signal handler
perf_signal_handler callback function is registered for SIGPROF signal. whenever SIGPROF signal emitted, perf_signal_handler is invoked.
SIGPROF: This signal typically indicates expiration of a timer that measures both CPU time used by the current process, and CPU time expended on behalf of the process by the system. Such a timer is used to implement code profiling facilities, hence the name of this signal.
Since we registered perf_signal_handler function to handle SIGPROF signals, it is invoked whenever a SIGPROF signal is emitted. perf_signal_handler takes ucontext as one of the arguments. ucontext contains the current instruction pointer of machine code that is being executed.
Using that instruction pointer, current call stack trace is retrivied. That is done using backtrace crate. The collected backtrace and thread name is passed to
the profiler.sample for sampling.
extern"C"fnperf_signal_handler( _signal: c_int, _siginfo:*mutlibc::siginfo_t, ucontext:*mutlibc::c_void, ){ ifletSome(mut guard)=PROFILER.try_write(){ ifletOk(profiler)= guard.as_mut(){ letmut bt:SmallVec<[<TraceImplasTrace>::Frame;MAX_DEPTH]>= SmallVec::with_capacity(MAX_DEPTH); // ucontext is passed to trace method to retrive // stack frame of current instruction pointer. TraceImpl::trace(ucontext,|frame|{ bt.push(frame.clone()); }); let current_thread =unsafe{libc::pthread_self()}; letmut name =[0;MAX_THREAD_NAME]; let name_ptr =&mut name as*mut[libc::c_char]as*mutlibc::c_char; write_thread_name(current_thread,&mut name); let name =unsafe{std::ffi::CStr::from_ptr(name_ptr)}; profiler.sample(bt, name.to_bytes(), current_thread asu64); } } }
sampling
profiler.sample interally calls a hashmap to insert stack frame and its count. As a side note, this is a custom implementation of the hashmap, rather than the rust's built-in hashmap. That's because heap allocation is forbidden inside signal handler so hashmap can't grow dynamically.
A stack frame with least count is evicted from the hashmap if the incoming stack frame can't find a place in it, and a temporary file is created to store the evicted stack frame.
pubfnflamegraph_with_options<W>( &self, writer:W, options:&mutflamegraph::Options, )->Result<()> where W:Write, { let lines:Vec<String>=self .data .iter() .map(|(key, value)|{ letmut line = key.thread_name_or_id(); line.push(';') for frame in key.frames.iter().rev(){ for symbol in frame.iter().rev(){ line.push_str(&format!("{}", symbol)); line.push(';'); } line.pop().unwrap_or_default(); line.push_str(&format!(" {}", value)) line }) .collect(); if!lines.is_empty(){ flamegraph::from_lines(options, lines.iter().map(|s|&**s), writer).unwrap(); Ok(()) }
flamegraph crate will generate differential flamegraph from folded stack line.
Example
main;prime_number1 3 main;prime_number2 5
pprof-rs also encodes the sampled data in google's pprof format which let you plot interactive graphs.
In recent days I've been excited about continuous profiling and it has been a hot thing in the observability space. Follow these amazing open-source projects parca and pyroscope to know more about continuous profiling.
Every programming language has a set of keywords that are only used for certain things. In rust, for example, the keyword for is used to represent looping.
Because keywords have meaning in programming languages, they cannot be used to name a function or variable. for example, the keywords for or in cannot be used as variable names.
Although keywords are not intended to be used to name variables, you can do so in rust by using a raw identifier.
The program below will not compile in rust because in is a reserved keyword.
#[derive(Debug)] struct Test{ in: String } fn main() { let a = Test{ in: "sadf".to_string() }; println!("{:?}", a); }
output:
error: expected identifier, found keyword `in` --> src/main.rs:4:5 | 4 | in: String | ^^ expected identifier, found keyword | help: you can escape reserved keywords to use them as identifiers | 4 | r#in: String | ~~~~ error: expected identifier, found keyword `in` --> src/main.rs:9:9 | 9 | in: "sadf".to_string() | ^^ expected identifier, found keyword | help: you can escape reserved keywords to use them as identifiers | 9 | r#in: "sadf".to_string() | ~~~~
However, we can make the program work by prefixing the keyword with r#.
r# tells the compiler that the incoming token is an identifier rather than a keyword.
#[derive(Debug)] struct Test{ r#in: String } fn main() { let a = Test{ r#in: "sadf".to_string() }; println!("{:?}", a); }
output:
Test { in: "sadf" }
It's very useful for rust because it allows rust to introduce new keywords.
Assume we have a crate built with the 2015 rust edition that exposes the identifier try. Later, try was reserved for a feature in the 2018 edition. As a result, we must use a raw identifier to call try