Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- run: cargo test --workspace

# Without its default server feature the SDK is the messages and the
# helpers, which a WebAssembly guest depends on: they must keep building
# natively and for wasm, where the server's dependencies do not.
no-server:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable
with:
components: clippy
targets: wasm32-wasip1
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- run: cargo clippy -p function-sdk-rust --no-default-features --tests -- -D warnings
- run: cargo test -p function-sdk-rust --no-default-features
- run: cargo build -p function-sdk-rust --no-default-features --target wasm32-wasip1

# The generated code in sdk/src/generated is checked in. Verify it matches
# what the codegen tool produces from the vendored proto. Only the .rs
# outputs are compared; the encoded descriptor set may differ byte-wise
Expand Down
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ for working with requests and responses.

- `sdk/` - the `function-sdk-rust` crate.
- `proto/v1/run_function.proto` - the vendored protocol definition.
- `src/generated/` - checked-in code generated from the proto: prost types,
the tonic gRPC server and client, protojson serde impls (pbjson), and the
encoded file descriptor set used for gRPC server reflection.
- `src/generated/` - checked-in code generated from the proto: prost types
(`v1.rs`), the tonic gRPC server and client (`v1.tonic.rs`), protojson
serde impls from pbjson (`v1.serde.rs`), and the encoded file descriptor
set used for gRPC server reflection (`fileset.bin`).
- `src/{server,request,response,resource,logging}.rs` - the hand-written
SDK: runtime and helpers.
- `codegen/` - maintainer tool that regenerates `sdk/src/generated`.
Expand Down Expand Up @@ -60,6 +61,20 @@ impl FunctionRunnerService for Function {

See `example/` for a complete function with a CLI entrypoint.

## Features

The server side is the crate's `server` feature, on by default: `serve` and
`Args`, `logging`, the generated tonic client and server under `proto::v1`,
`proto::FILE_DESCRIPTOR_SET`, and the dependencies behind them (tokio, tonic
with its TLS stack, clap, prometheus-client). A function built on the SDK
keeps all of it.

With `default-features = false` the crate is the protocol messages plus the
`request`, `response` and `resource` helpers. That set builds for WebAssembly
targets, where tokio and the TLS stack do not, and is what a function compiled
to a WebAssembly module depends on: [function-wasm]'s Rust scaffold, for one,
whose host serves the gRPC side.

## Development

```shell
Expand Down Expand Up @@ -123,6 +138,7 @@ with a few deliberate differences:
[functions]: https://docs.crossplane.io/latest/composition/compositions/
[function-sdk-python]: https://github.com/crossplane/function-sdk-python
[function-sdk-go]: https://github.com/crossplane/function-sdk-go
[function-wasm]: https://github.com/jonasz-lasut/function-wasm
[spec]: https://github.com/crossplane/crossplane/blob/main/contributing/specifications/functions.md
[proto]: https://github.com/crossplane/crossplane/tree/main/proto/fn/v1
[tonic-prometheus-layer]: https://crates.io/crates/tonic-prometheus-layer
Expand Down
50 changes: 44 additions & 6 deletions codegen/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
//! Maintainer tool: requires `protoc` on PATH. SDK users never run this
//! because the generated code is committed.

use std::cell::RefCell;
use std::error::Error;
use std::fs;
use std::path::PathBuf;
use std::rc::Rc;

use prost_build::{Service, ServiceGenerator};

const PROTO: &str = "sdk/proto/v1/run_function.proto";
const INCLUDE: &str = "sdk/proto";
Expand All @@ -16,17 +20,27 @@ fn main() -> Result<(), Box<dyn Error>> {
let tmp = tempfile::tempdir()?;
let descriptor = tmp.path().join("fileset.bin");

// prost hands its service generator the file the messages go into. tonic's
// output is routed to a buffer of its own instead, so that the gRPC client
// and server land in a file the SDK includes only with its `server`
// feature, while the messages stay feature-free.
let grpc = Rc::new(RefCell::new(String::new()));
let tonic = tonic_prost_build::configure()
.build_client(true)
.build_server(true)
.service_generator();

let mut config = prost_build::Config::new();
config
.file_descriptor_set_path(&descriptor)
.compile_well_known_types()
.extern_path(".google.protobuf", "::pbjson_types");

tonic_prost_build::configure()
.build_client(true)
.build_server(true)
.extern_path(".google.protobuf", "::pbjson_types")
.service_generator(Box::new(Redirected {
inner: tonic,
out: Rc::clone(&grpc),
}))
.out_dir(tmp.path())
.compile_with_config(config, &[root.join(PROTO)], &[root.join(INCLUDE)])?;
.compile_protos(&[root.join(PROTO)], &[root.join(INCLUDE)])?;

pbjson_build::Builder::new()
.register_descriptors(&fs::read(&descriptor)?)?
Expand All @@ -52,11 +66,35 @@ fn main() -> Result<(), Box<dyn Error>> {
fs::copy(&path, out.join(target))?;
println!("wrote {}", out.join(target).display());
}
fs::write(out.join("v1.tonic.rs"), grpc.borrow().as_bytes())?;
println!("wrote {}", out.join("v1.tonic.rs").display());
fs::copy(&descriptor, out.join("fileset.bin"))?;
println!("wrote {}", out.join("fileset.bin").display());
Ok(())
}

/// A service generator that writes to its own buffer rather than to the
/// message file prost hands it.
struct Redirected {
inner: Box<dyn ServiceGenerator>,
out: Rc<RefCell<String>>,
}

impl ServiceGenerator for Redirected {
fn generate(&mut self, service: Service, _message_file: &mut String) {
self.inner.generate(service, &mut self.out.borrow_mut());
}

fn finalize(&mut self, _message_file: &mut String) {
self.inner.finalize(&mut self.out.borrow_mut());
}

fn finalize_package(&mut self, package: &str, _message_file: &mut String) {
self.inner
.finalize_package(package, &mut self.out.borrow_mut());
}
}

fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
Expand Down
61 changes: 47 additions & 14 deletions sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,61 @@ readme = "README.md"
keywords = ["crossplane", "kubernetes", "grpc"]
categories = ["api-bindings"]

# The server side (server.rs, metrics.rs, logging.rs, the generated tonic
# client and server, FILE_DESCRIPTOR_SET) is a default feature. Without it
# (default-features = false) the crate is the protocol messages and the
# request/response/resource helpers, which also build for WebAssembly targets,
# where tokio and the TLS stack behind tonic do not: what a function compiled
# to a WebAssembly module depends on.
[features]
default = ["server"]
server = [
"dep:bytes",
"dep:clap",
"dep:http",
"dep:http-body",
"dep:prometheus-client",
"dep:tokio",
"dep:tonic",
"dep:tonic-health",
"dep:tonic-prost",
"dep:tonic-reflection",
"dep:tower-layer",
"dep:tower-service",
"dep:tracing",
"dep:tracing-subscriber",
]

[dependencies]
bytes = "1"
http = "1"
http-body = "1"
prometheus-client = "0.25"
tonic = { version = "0.14", features = ["tls-aws-lc"] }
tonic-prost = "0.14"
tonic-health = "0.14"
tonic-reflection = "0.14"
# The messages and the helpers: every feature set, every target.
prost = "0.14"
pbjson = "0.9"
pbjson-types = "0.9"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal"] }
clap = { version = "4", features = ["derive", "env"] }
tracing = "0.1"
tower-layer = "0.3"
tower-service = "0.3"
tracing-subscriber = { version = "0.3", features = ["json"] }
sha2 = "0.11"
thiserror = "2"

# The server side, with the server feature.
bytes = { version = "1", optional = true }
clap = { version = "4", features = ["derive", "env"], optional = true }
http = { version = "1", optional = true }
http-body = { version = "1", optional = true }
prometheus-client = { version = "0.25", optional = true }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal"], optional = true }
tonic = { version = "0.14", features = ["tls-aws-lc"], optional = true }
tonic-health = { version = "0.14", optional = true }
tonic-prost = { version = "0.14", optional = true }
tonic-reflection = { version = "0.14", optional = true }
tower-layer = { version = "0.3", optional = true }
tower-service = { version = "0.3", optional = true }
tracing = { version = "0.1", optional = true }
tracing-subscriber = { version = "0.3", features = ["json"], optional = true }

[dev-dependencies]
tokio-test = "0.4"

# The serving test needs the server it serves with.
[[test]]
name = "serve"
required-features = ["server"]
11 changes: 9 additions & 2 deletions sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ A composition function is a gRPC server implementing the
`FunctionRunnerService` defined by Crossplane's `apiextensions.fn.proto.v1`
API. This crate provides:

- `proto::v1` - generated protocol types with protojson serde support, plus
the tonic gRPC server and client.
- `proto::v1` - generated protocol types with protojson serde support, plus,
with the `server` feature, the tonic gRPC server and client.
- `server` - a function-spec-compliant runtime: mTLS or insecure serving,
gRPC server reflection, the gRPC health service, graceful shutdown, and
the standard CLI arguments.
Expand All @@ -18,5 +18,12 @@ API. This crate provides:
`serde::Serialize` source.
- `logging` - JSON-lines logging with a human-readable debug mode.

The `server` feature, on by default, carries the server side: `server`,
`logging`, the tonic client and server, and the dependencies behind them.
With `default-features = false` the crate is the protocol types and the
`request`, `response` and `resource` helpers, which also build for
WebAssembly targets: what a function compiled to a WebAssembly module
depends on.

See the [repository](https://github.com/crossplane/function-sdk-rust) for a
complete example function.
Loading
Loading