From 2ea0248f818e5189b3ae467c65096ad0bd5ab7b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonasz=20Ma=C5=82ecki?= Date: Wed, 7 Oct 2026 14:04:05 +0200 Subject: [PATCH 1/2] refactor(codegen): emit tonic code to its own file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tonic's service generator appended the gRPC client and server to prost's message file. A wrapping ServiceGenerator routes its output into sdk/src/generated/v1.tonic.rs instead, so the messages and the gRPC code can be included independently. The generated bytes are unchanged: the old v1.rs is the new v1.rs followed by v1.tonic.rs. Signed-off-by: Jonasz Małecki --- README.md | 7 +- codegen/src/main.rs | 50 +++++- sdk/src/generated/v1.rs | 310 ---------------------------------- sdk/src/generated/v1.tonic.rs | 310 ++++++++++++++++++++++++++++++++++ sdk/src/proto.rs | 3 +- 5 files changed, 360 insertions(+), 320 deletions(-) create mode 100644 sdk/src/generated/v1.tonic.rs diff --git a/README.md b/README.md index ead7b0c..b43a713 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/codegen/src/main.rs b/codegen/src/main.rs index c982d13..4106ddc 100644 --- a/codegen/src/main.rs +++ b/codegen/src/main.rs @@ -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"; @@ -16,17 +20,27 @@ fn main() -> Result<(), Box> { 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)?)? @@ -52,11 +66,35 @@ fn main() -> Result<(), Box> { 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, + out: Rc>, +} + +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() diff --git a/sdk/src/generated/v1.rs b/sdk/src/generated/v1.rs index 5c3816e..8d5bb4d 100644 --- a/sdk/src/generated/v1.rs +++ b/sdk/src/generated/v1.rs @@ -569,313 +569,3 @@ impl Status { } } } -/// Generated client implementations. -pub mod function_runner_service_client { - #![allow( - unused_variables, - dead_code, - missing_docs, - clippy::wildcard_imports, - clippy::let_unit_value, - )] - use tonic::codegen::*; - use tonic::codegen::http::Uri; - /// A FunctionRunnerService is a function. - #[derive(Debug, Clone)] - pub struct FunctionRunnerServiceClient { - inner: tonic::client::Grpc, - } - impl FunctionRunnerServiceClient { - /// Attempt to create a new client by connecting to a given endpoint. - pub async fn connect(dst: D) -> Result - where - D: TryInto, - D::Error: Into, - { - let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; - Ok(Self::new(conn)) - } - } - impl FunctionRunnerServiceClient - where - T: tonic::client::GrpcService, - T::Error: Into, - T::ResponseBody: Body + std::marker::Send + 'static, - ::Error: Into + std::marker::Send, - { - pub fn new(inner: T) -> Self { - let inner = tonic::client::Grpc::new(inner); - Self { inner } - } - pub fn with_origin(inner: T, origin: Uri) -> Self { - let inner = tonic::client::Grpc::with_origin(inner, origin); - Self { inner } - } - pub fn with_interceptor( - inner: T, - interceptor: F, - ) -> FunctionRunnerServiceClient> - where - F: tonic::service::Interceptor, - T::ResponseBody: Default, - T: tonic::codegen::Service< - http::Request, - Response = http::Response< - >::ResponseBody, - >, - >, - , - >>::Error: Into + std::marker::Send + std::marker::Sync, - { - FunctionRunnerServiceClient::new(InterceptedService::new(inner, interceptor)) - } - /// Compress requests with the given encoding. - /// - /// This requires the server to support it otherwise it might respond with an - /// error. - #[must_use] - pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.inner = self.inner.send_compressed(encoding); - self - } - /// Enable decompressing responses. - #[must_use] - pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.inner = self.inner.accept_compressed(encoding); - self - } - /// Limits the maximum size of a decoded message. - /// - /// Default: `4MB` - #[must_use] - pub fn max_decoding_message_size(mut self, limit: usize) -> Self { - self.inner = self.inner.max_decoding_message_size(limit); - self - } - /// Limits the maximum size of an encoded message. - /// - /// Default: `usize::MAX` - #[must_use] - pub fn max_encoding_message_size(mut self, limit: usize) -> Self { - self.inner = self.inner.max_encoding_message_size(limit); - self - } - /// RunFunction runs the function. - pub async fn run_function( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic_prost::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/apiextensions.fn.proto.v1.FunctionRunnerService/RunFunction", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert( - GrpcMethod::new( - "apiextensions.fn.proto.v1.FunctionRunnerService", - "RunFunction", - ), - ); - self.inner.unary(req, path, codec).await - } - } -} -/// Generated server implementations. -pub mod function_runner_service_server { - #![allow( - unused_variables, - dead_code, - missing_docs, - clippy::wildcard_imports, - clippy::let_unit_value, - )] - use tonic::codegen::*; - /// Generated trait containing gRPC methods that should be implemented for use with FunctionRunnerServiceServer. - #[async_trait] - pub trait FunctionRunnerService: std::marker::Send + std::marker::Sync + 'static { - /// RunFunction runs the function. - async fn run_function( - &self, - request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; - } - /// A FunctionRunnerService is a function. - #[derive(Debug)] - pub struct FunctionRunnerServiceServer { - inner: Arc, - accept_compression_encodings: EnabledCompressionEncodings, - send_compression_encodings: EnabledCompressionEncodings, - max_decoding_message_size: Option, - max_encoding_message_size: Option, - } - impl FunctionRunnerServiceServer { - pub fn new(inner: T) -> Self { - Self::from_arc(Arc::new(inner)) - } - pub fn from_arc(inner: Arc) -> Self { - Self { - inner, - accept_compression_encodings: Default::default(), - send_compression_encodings: Default::default(), - max_decoding_message_size: None, - max_encoding_message_size: None, - } - } - pub fn with_interceptor( - inner: T, - interceptor: F, - ) -> InterceptedService - where - F: tonic::service::Interceptor, - { - InterceptedService::new(Self::new(inner), interceptor) - } - /// Enable decompressing requests with the given encoding. - #[must_use] - pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.accept_compression_encodings.enable(encoding); - self - } - /// Compress responses with the given encoding, if the client supports it. - #[must_use] - pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.send_compression_encodings.enable(encoding); - self - } - /// Limits the maximum size of a decoded message. - /// - /// Default: `4MB` - #[must_use] - pub fn max_decoding_message_size(mut self, limit: usize) -> Self { - self.max_decoding_message_size = Some(limit); - self - } - /// Limits the maximum size of an encoded message. - /// - /// Default: `usize::MAX` - #[must_use] - pub fn max_encoding_message_size(mut self, limit: usize) -> Self { - self.max_encoding_message_size = Some(limit); - self - } - } - impl tonic::codegen::Service> - for FunctionRunnerServiceServer - where - T: FunctionRunnerService, - B: Body + std::marker::Send + 'static, - B::Error: Into + std::marker::Send + 'static, - { - type Response = http::Response; - type Error = std::convert::Infallible; - type Future = BoxFuture; - fn poll_ready( - &mut self, - _cx: &mut Context<'_>, - ) -> Poll> { - Poll::Ready(Ok(())) - } - fn call(&mut self, req: http::Request) -> Self::Future { - match req.uri().path() { - "/apiextensions.fn.proto.v1.FunctionRunnerService/RunFunction" => { - #[allow(non_camel_case_types)] - struct RunFunctionSvc(pub Arc); - impl< - T: FunctionRunnerService, - > tonic::server::UnaryService - for RunFunctionSvc { - type Response = super::RunFunctionResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = Arc::clone(&self.0); - let fut = async move { - ::run_function(&inner, request) - .await - }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let max_decoding_message_size = self.max_decoding_message_size; - let max_encoding_message_size = self.max_encoding_message_size; - let inner = self.inner.clone(); - let fut = async move { - let method = RunFunctionSvc(inner); - let codec = tonic_prost::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec) - .apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ) - .apply_max_message_size_config( - max_decoding_message_size, - max_encoding_message_size, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - _ => { - Box::pin(async move { - let mut response = http::Response::new( - tonic::body::Body::default(), - ); - let headers = response.headers_mut(); - headers - .insert( - tonic::Status::GRPC_STATUS, - (tonic::Code::Unimplemented as i32).into(), - ); - headers - .insert( - http::header::CONTENT_TYPE, - tonic::metadata::GRPC_CONTENT_TYPE, - ); - Ok(response) - }) - } - } - } - } - impl Clone for FunctionRunnerServiceServer { - fn clone(&self) -> Self { - let inner = self.inner.clone(); - Self { - inner, - accept_compression_encodings: self.accept_compression_encodings, - send_compression_encodings: self.send_compression_encodings, - max_decoding_message_size: self.max_decoding_message_size, - max_encoding_message_size: self.max_encoding_message_size, - } - } - } - /// Generated gRPC service name - pub const SERVICE_NAME: &str = "apiextensions.fn.proto.v1.FunctionRunnerService"; - impl tonic::server::NamedService for FunctionRunnerServiceServer { - const NAME: &'static str = SERVICE_NAME; - } -} diff --git a/sdk/src/generated/v1.tonic.rs b/sdk/src/generated/v1.tonic.rs new file mode 100644 index 0000000..a1e60ba --- /dev/null +++ b/sdk/src/generated/v1.tonic.rs @@ -0,0 +1,310 @@ +/// Generated client implementations. +pub mod function_runner_service_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// A FunctionRunnerService is a function. + #[derive(Debug, Clone)] + pub struct FunctionRunnerServiceClient { + inner: tonic::client::Grpc, + } + impl FunctionRunnerServiceClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl FunctionRunnerServiceClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> FunctionRunnerServiceClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + FunctionRunnerServiceClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + /// RunFunction runs the function. + pub async fn run_function( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/apiextensions.fn.proto.v1.FunctionRunnerService/RunFunction", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "apiextensions.fn.proto.v1.FunctionRunnerService", + "RunFunction", + ), + ); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +pub mod function_runner_service_server { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with FunctionRunnerServiceServer. + #[async_trait] + pub trait FunctionRunnerService: std::marker::Send + std::marker::Sync + 'static { + /// RunFunction runs the function. + async fn run_function( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + } + /// A FunctionRunnerService is a function. + #[derive(Debug)] + pub struct FunctionRunnerServiceServer { + inner: Arc, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + impl FunctionRunnerServiceServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> + for FunctionRunnerServiceServer + where + T: FunctionRunnerService, + B: Body + std::marker::Send + 'static, + B::Error: Into + std::marker::Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + match req.uri().path() { + "/apiextensions.fn.proto.v1.FunctionRunnerService/RunFunction" => { + #[allow(non_camel_case_types)] + struct RunFunctionSvc(pub Arc); + impl< + T: FunctionRunnerService, + > tonic::server::UnaryService + for RunFunctionSvc { + type Response = super::RunFunctionResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::run_function(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = RunFunctionSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + let mut response = http::Response::new( + tonic::body::Body::default(), + ); + let headers = response.headers_mut(); + headers + .insert( + tonic::Status::GRPC_STATUS, + (tonic::Code::Unimplemented as i32).into(), + ); + headers + .insert( + http::header::CONTENT_TYPE, + tonic::metadata::GRPC_CONTENT_TYPE, + ); + Ok(response) + }) + } + } + } + } + impl Clone for FunctionRunnerServiceServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + /// Generated gRPC service name + pub const SERVICE_NAME: &str = "apiextensions.fn.proto.v1.FunctionRunnerService"; + impl tonic::server::NamedService for FunctionRunnerServiceServer { + const NAME: &'static str = SERVICE_NAME; + } +} diff --git a/sdk/src/proto.rs b/sdk/src/proto.rs index 887609e..890c90b 100644 --- a/sdk/src/proto.rs +++ b/sdk/src/proto.rs @@ -3,11 +3,12 @@ //! //! Regenerate with `cargo run -p codegen` after changing the vendored proto. -/// Types and the gRPC service for the v1 FunctionRunnerService API. +/// Types and the gRPC client and server for the v1 FunctionRunnerService API. pub mod v1 { #![allow(clippy::all, clippy::pedantic)] include!("generated/v1.rs"); include!("generated/v1.serde.rs"); + include!("generated/v1.tonic.rs"); } /// Encoded FileDescriptorSet of the v1 API, used for gRPC server reflection. From 2e156fbf5f3ecc15d874f679851557e12fa331fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonasz=20Ma=C5=82ecki?= Date: Wed, 7 Oct 2026 14:04:05 +0200 Subject: [PATCH 2/2] feat: gate the server behind a default feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The crate had no Cargo features: tokio, tonic with its TLS stack, clap, prometheus-client and the generated tonic client and server were unconditional, so a WebAssembly guest that wants only the messages and the request/response/resource helpers could not depend on it. A server feature, on by default, now carries server.rs, metrics.rs, logging.rs, the server-side Error variants, the generated tonic client and server and FILE_DESCRIPTOR_SET, with their dependencies optional behind it. Every current dependent keeps the whole crate; with default-features = false the messages and the helpers remain and build for wasm32-wasip1 and wasm32-wasip2. CI builds that set natively and for wasm32-wasip1. Fixes #18 Signed-off-by: Jonasz Małecki --- .github/workflows/ci.yml | 16 +++++++++++ README.md | 15 ++++++++++ sdk/Cargo.toml | 61 +++++++++++++++++++++++++++++++--------- sdk/README.md | 11 ++++++-- sdk/src/lib.rs | 27 ++++++++++++++---- sdk/src/proto.rs | 5 +++- 6 files changed, 113 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7bf8f29..ef75793 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/README.md b/README.md index b43a713..487ec61 100644 --- a/README.md +++ b/README.md @@ -61,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 @@ -124,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 diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index d81bee7..b5798a9 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -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"] diff --git a/sdk/README.md b/sdk/README.md index 2b1ac3a..80322ac 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -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. @@ -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. diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index d4758f1..58c91ce 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -1,10 +1,9 @@ //! A Rust SDK for writing Crossplane composition functions. //! //! Composition functions are gRPC servers implementing the -//! [`FunctionRunnerService`] defined by Crossplane's -//! `apiextensions.fn.proto.v1` API. This SDK provides the generated protocol -//! types, a spec-compliant server runtime, and helpers for working with -//! requests and responses. +//! `FunctionRunnerService` defined by Crossplane's `apiextensions.fn.proto.v1` +//! API. This SDK provides the generated protocol types, a spec-compliant +//! server runtime, and helpers for working with requests and responses. //! //! The most important protocol rule: desired state is a fully specified //! server-side apply intent, not a merge patch. Build responses with @@ -12,18 +11,31 @@ //! pipeline steps are copied forward; anything left out is deleted from the //! cluster. //! -//! [`FunctionRunnerService`]: proto::v1::function_runner_service_server::FunctionRunnerService +//! # Features +//! +//! - `server` (enabled by default): the server side - `serve` and `Args`, +//! `logging`, the generated tonic client and server under `proto::v1`, and +//! `proto::FILE_DESCRIPTOR_SET` - with the dependencies behind it (tokio, +//! tonic and its TLS stack, clap, prometheus-client). Without it, with +//! `default-features = false`, the crate is the protocol messages and the +//! [`request`], [`response`] and [`resource`] helpers, which also build for +//! WebAssembly targets: what a function compiled to a WebAssembly module +//! depends on. +#[cfg(feature = "server")] mod metrics; pub mod context; +#[cfg(feature = "server")] pub mod logging; pub mod proto; pub mod request; pub mod resource; pub mod response; +#[cfg(feature = "server")] pub mod server; +#[cfg(feature = "server")] pub use server::{Args, serve}; /// Errors returned by the SDK. @@ -46,14 +58,17 @@ pub enum Error { MissingResource, /// The listen address could not be parsed. + #[cfg(feature = "server")] #[error("cannot parse listen address: {0}")] InvalidAddress(#[from] std::net::AddrParseError), /// Neither --tls-certs-dir nor --insecure was supplied. + #[cfg(feature = "server")] #[error("no credentials were provided - supply --tls-certs-dir or use --insecure")] MissingTlsCertsDir, /// A TLS certificate or key could not be read. + #[cfg(feature = "server")] #[error("cannot read TLS certificate or key from {path}: {source}")] ReadCertificate { path: std::path::PathBuf, @@ -61,10 +76,12 @@ pub enum Error { }, /// The gRPC server failed. + #[cfg(feature = "server")] #[error("gRPC server error: {0}")] Transport(#[from] tonic::transport::Error), /// The gRPC reflection service could not be built. + #[cfg(feature = "server")] #[error("cannot build gRPC reflection service: {0}")] Reflection(#[from] tonic_reflection::server::Error), } diff --git a/sdk/src/proto.rs b/sdk/src/proto.rs index 890c90b..63f0fde 100644 --- a/sdk/src/proto.rs +++ b/sdk/src/proto.rs @@ -3,13 +3,16 @@ //! //! Regenerate with `cargo run -p codegen` after changing the vendored proto. -/// Types and the gRPC client and server for the v1 FunctionRunnerService API. +/// Types and, with the `server` feature, the gRPC client and server for the +/// v1 FunctionRunnerService API. pub mod v1 { #![allow(clippy::all, clippy::pedantic)] include!("generated/v1.rs"); include!("generated/v1.serde.rs"); + #[cfg(feature = "server")] include!("generated/v1.tonic.rs"); } /// Encoded FileDescriptorSet of the v1 API, used for gRPC server reflection. +#[cfg(feature = "server")] pub const FILE_DESCRIPTOR_SET: &[u8] = include_bytes!("generated/fileset.bin");