Skip to content

Commit

Permalink
chore: WIP
Browse files Browse the repository at this point in the history
Signed-off-by: Andrew Lilley Brinker <[email protected]>
  • Loading branch information
alilleybrinker committed Sep 13, 2024
1 parent 3c953c4 commit b1c3bd3
Showing 1 changed file with 129 additions and 1 deletion.
130 changes: 129 additions & 1 deletion site/content/rfds/0006-rust-plugin-sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,138 @@ details of how those queries are defined is provided in the next section.

### The `Query` Trait

TODO: Write this.
This is the trait that defines a single query for a plugin. Note that
one major constraint on the design of this trait is that we want it to be
trait-object compatible (normal Rust terminology here is "object safe"
though I prefer to more recently-proposed term "dyn safe"). To be dyn
safe, a trait has to meet certain requirements. In our case that means this
trait can't have a `const` `NAME` field for the name of the query, as
constants like this are not dyn safe.

The design for the `Query` trait will therefore look like this:

```rust
#[async_trait]
/// Defines a single query for the plugin.
pub trait Query {
/// Get the input schema for the query.
fn input_schema(&self) -> JsonSchema;

/// Get the output schema for the query.
fn output_schema(&self) -> JsonSchema;

/// Run the plugin, optionally making queries to other plugins.
async fn run(&self, engine: &mut PluginEngine, input: JsonValue) -> Result<JsonValue, Error>;
}
```

Here, `Error` is a type we've defined for error handling, and `JsonValue`
is `serde_json::Value`. This interface allows queries to use the `PluginEngine`
type to make queries to other plugins.

Note that from the perspective of a query author, this is a very nice and
ergonomic model. A query is just a single asynchronous function, where
making other queries is just an await point. Any details of handling this
asynchronous operation correctly, or of handling the execution of the
underlying query protocol, are hidden.

### The `PluginEngine` Type

The `PluginEngine` is an opaque handle for making queries to the
`PluginServer`. Its interface looks like:

```rust
pub struct PluginEngine {
// Not specified here...
}

impl PluginEngine {
async fn query(&mut self, target: Into<QueryTarget>, input: JsonValue) -> Result<JsonValue, Error> {
// ...
}
}
```

As you can see, this looks very similar to the `run` function for queries,
but includes a `target` which specifies the plugin and query to call. The full
details of how this `target` works are specific below.

### The `QueryTarget` Type

The `QueryTarget` type is a wrapper for three pieces of information, the
plugin publisher, plugin name, and query name. Query name may optionally be
empty to call the default query of a plugin. Informally, its representation
is effectively:

```rust
struct QueryTarget {
publisher: String,
plugin: String,
query: Option<String>,
}
```

`Into<QueryTarget>` will be implemented for `&str`, and will attempt to
parse a `/`-separated string of these parts into a `QueryTarget` the `Into`
bound on the `PluginEngine::query` function is provided to make the API
friendlier to use. With this design, the API looks like:

```rust
// Assuming a variable "target_date" has been defined.
let result = engine.query("mitre/activity/num_weeks_since", json!({ "target_date": target_date }))?;
```

### The `PluginServer` Type

The `PluginServer` type is what turns the end-user's `Plugin`-implementing
type and uses it to actually run the gRPC server interface. Note that we do
mean _server_ interface; in the Hipcheck architecture, plugins are run as
separate processes which expose a gRPC server for which `hc` is the client.

The `PluginServer` type handles all the fiddly operation of the plugin gRPC
protocol, including validating inputs, converting inputs and outputs to and
from gRPC message types, reporting gRPC-friendly errors, running the
query protocol request/response flow, chunking query responses, and
un-chunking query requests.

The public API of the `PluginServer` type is:

```rust
pub struct PluginServer<P> {
plugin: P,
}

impl<P: Plugin> PluginServer<P> {
/// Create a new plugin server for the provided plugin.
pub fn register(plugin: P) -> PluginServer<P> {
PluginServer { plugin }
}

/// Run the plugin server on the provided port.
pub async fn listen(self, port: u16) -> Result<()> {
let service = PluginServiceServer::new(self);
let host = format!("127.0.0.1:{}", port).parse().unwrap();

Server::builder()
.add_service(service)
.serve(host)
.await
.map_err(Error::FailedToStartServer)?;

Ok(())
}
}
```

This means that the only thing the end-user of the plugin SDK can do with
a `PluginServer` is 1) create it, and 2) start running it on a specific
port.

The `PluginServiceServer` type comes from the gRPC code generated by
`prost` in concert with `tonic`. Our `PluginServer` type also implements
the `PluginService` trait defined by our generated gRPC code, and it's
this interface that connects our `PluginServer` to `tonic`.

## Scope Limits

We've purposefully kept the scope of this initial SDK definition as minimal
Expand Down

0 comments on commit b1c3bd3

Please sign in to comment.