Skip to main content

function_sdk_rust/
server.rs

1//! A spec-compliant gRPC server runtime for composition functions.
2
3use std::future::Future;
4use std::net::SocketAddr;
5use std::path::{Path, PathBuf};
6use std::pin::Pin;
7use std::sync::Arc;
8use std::sync::atomic::{AtomicBool, Ordering};
9
10use prometheus_client::registry::Registry;
11use tonic::transport::{Certificate, Identity, Server as GrpcServer, ServerTlsConfig};
12use tonic::{Request, Response, Status};
13use tonic_health::ServingStatus;
14use tonic_health::server::HealthReporter;
15
16use crate::Error;
17use crate::proto::v1::function_runner_service_server::{
18    FunctionRunnerService, FunctionRunnerServiceServer, SERVICE_NAME,
19};
20use crate::proto::v1::{RunFunctionRequest, RunFunctionResponse};
21
22/// CLI arguments required by the Crossplane composition function spec.
23#[derive(clap::Parser, Debug)]
24#[command(version, about = "A Crossplane composition function")]
25pub struct Args {
26    /// Emit debug logs.
27    #[arg(short, long, env = "DEBUG", default_value_t = false)]
28    pub debug: bool,
29
30    /// Address at which to listen for gRPC connections.
31    #[arg(long, default_value = "0.0.0.0:9443")]
32    pub address: String,
33
34    /// Directory containing tls.crt, tls.key, and ca.crt; serve using mTLS.
35    #[arg(long, env = "TLS_SERVER_CERTS_DIR")]
36    pub tls_certs_dir: Option<PathBuf>,
37
38    /// Run without mTLS credentials. If set, --tls-certs-dir is ignored.
39    #[arg(long, default_value_t = false)]
40    pub insecure: bool,
41
42    /// Maximum size in bytes of gRPC messages the function accepts.
43    /// Defaults to the gRPC default of 4MB.
44    #[arg(long)]
45    pub max_recv_message_size: Option<usize>,
46
47    /// Address at which to serve metrics at /metrics (OpenMetrics 1.0, or
48    /// the classic Prometheus text format for an Accept header that asks
49    /// for it) - the Go SDK's default; empty disables them.
50    #[arg(long, env = "METRICS_ADDRESS", default_value = ":8080")]
51    pub metrics_address: String,
52}
53
54/// Starts a gRPC server and serves RunFunctionRequests until SIGTERM or
55/// SIGINT, then shuts down gracefully.
56///
57/// Serves with mTLS from `--tls-certs-dir` (tls.crt and tls.key must be the
58/// function's PEM-encoded certificate and key; ca.crt a PEM-encoded CA used
59/// to authenticate Crossplane) unless `--insecure` is set. gRPC server
60/// reflection is enabled for both the v1 and v1alpha reflection APIs, and
61/// the gRPC health service reports the function as serving.
62///
63/// Unless `--metrics-address` is empty, the gRPC server metrics the Go SDK
64/// serves (`grpc_server_started_total` and friends, same names and labels)
65/// are served there at /metrics - OpenMetrics 1.0 as the main format, the
66/// classic Prometheus text format for scrapers that ask for it.
67///
68/// For options beyond the command-line `args`, use [`Server`].
69pub async fn serve<F: FunctionRunnerService>(function: F, args: &Args) -> Result<(), Error> {
70    Server::new(function, args).serve().await
71}
72
73type BoxError = Box<dyn std::error::Error + Send + Sync>;
74type ReadyFuture = Pin<Box<dyn Future<Output = Result<(), BoxError>> + Send>>;
75
76/// A function server with options beyond the command-line [`Args`]. Create
77/// it with [`Server::new`], chain the options, then call [`Server::serve`];
78/// with no options it serves exactly like [`serve`]. New options arrive as
79/// new methods, so code that builds a server keeps compiling.
80///
81/// ```no_run
82/// # use function_sdk_rust::proto::v1::function_runner_service_server::FunctionRunnerService;
83/// # async fn run(function: impl FunctionRunnerService, args: &function_sdk_rust::Args) -> Result<(), function_sdk_rust::Error> {
84/// function_sdk_rust::Server::new(function, args)
85///     .ready(async { Ok::<(), std::io::Error>(()) })
86///     .serve()
87///     .await
88/// # }
89/// ```
90pub struct Server<'a, F> {
91    function: F,
92    args: &'a Args,
93    ready: Option<ReadyFuture>,
94    metrics_registry: Option<Registry>,
95}
96
97impl<'a, F: FunctionRunnerService> Server<'a, F> {
98    /// Creates a server for `function`, configured by the command-line `args`.
99    pub fn new(function: F, args: &'a Args) -> Self {
100        Self {
101            function,
102            args,
103            ready: None,
104            metrics_registry: None,
105        }
106    }
107
108    /// Holds the function back until `ready` completes, for a function that
109    /// must finish loading before it can run. The server listens at once,
110    /// but reports NOT_SERVING on the gRPC health service and answers
111    /// RunFunction with UNAVAILABLE. Ok flips the function to SERVING; an
112    /// error or a panic stops the server, and [`Server::serve`] returns it
113    /// as [`Error::NotReady`].
114    pub fn ready<R, E>(mut self, ready: R) -> Self
115    where
116        R: Future<Output = Result<(), E>> + Send + 'static,
117        E: Into<BoxError>,
118    {
119        self.ready = Some(Box::pin(async move { ready.await.map_err(Into::into) }));
120        self
121    }
122
123    /// Serves `registry` at /metrics, like function-sdk-go's
124    /// `WithMetricsRegistry`: register the function's own series in it
125    /// first, and the SDK adds its `grpc_server_*` series at the root. With
126    /// `--metrics-address` empty nothing is served.
127    ///
128    /// Build the registry and its metrics with a `prometheus-client` version
129    /// that is semver-compatible with the SDK's own dependency on it (while
130    /// prometheus-client is 0.x, the same minor version). Cargo then resolves
131    /// both to one copy of the crate; a registry from an incompatible version
132    /// is a different type and does not compile here.
133    pub fn metrics_registry(mut self, registry: Registry) -> Self {
134        self.metrics_registry = Some(registry);
135        self
136    }
137
138    /// Serves RunFunctionRequests until SIGTERM or SIGINT, then shuts down
139    /// gracefully. See [`serve`] for what is served.
140    pub async fn serve(self) -> Result<(), Error> {
141        let Server {
142            function,
143            args,
144            ready,
145            metrics_registry,
146        } = self;
147        let address: SocketAddr = args.address.parse()?;
148
149        let mut builder = GrpcServer::builder();
150        if !args.insecure {
151            let dir = args
152                .tls_certs_dir
153                .as_deref()
154                .ok_or(Error::MissingTlsCertsDir)?;
155            builder = builder.tls_config(tls_config(dir)?)?;
156        }
157
158        let is_ready = Arc::new(AtomicBool::new(false));
159        let mut function_service = FunctionRunnerServiceServer::new(Gate {
160            function,
161            ready: is_ready.clone(),
162        });
163        if let Some(size) = args.max_recv_message_size {
164            function_service = function_service.max_decoding_message_size(size);
165        }
166
167        let reflection_v1 = tonic_reflection::server::Builder::configure()
168            .register_encoded_file_descriptor_set(crate::proto::FILE_DESCRIPTOR_SET)
169            .build_v1()?;
170        let reflection_v1alpha = tonic_reflection::server::Builder::configure()
171            .register_encoded_file_descriptor_set(crate::proto::FILE_DESCRIPTOR_SET)
172            .build_v1alpha()?;
173
174        // The health statuses are set before the server listens, so no
175        // probe sees SERVING ahead of the readiness future.
176        let (health_reporter, health_service) = tonic_health::server::health_reporter();
177        let (not_ready_tx, not_ready_rx) = tokio::sync::oneshot::channel::<BoxError>();
178        match ready {
179            None => {
180                is_ready.store(true, Ordering::Release);
181                set_health(&health_reporter, ServingStatus::Serving).await;
182            }
183            Some(ready) => {
184                set_health(&health_reporter, ServingStatus::NotServing).await;
185                // The future runs in a task of its own so that a panic in it
186                // fails readiness instead of leaving the server NOT_SERVING.
187                let loading = tokio::spawn(ready);
188                tokio::spawn(async move {
189                    match loading.await.unwrap_or_else(|e| Err(e.into())) {
190                        Ok(()) => {
191                            is_ready.store(true, Ordering::Release);
192                            set_health(&health_reporter, ServingStatus::Serving).await;
193                            tracing::info!("function is ready");
194                        }
195                        Err(e) => {
196                            let _ = not_ready_tx.send(e);
197                        }
198                    }
199                });
200            }
201        }
202
203        // The metrics endpoint runs beside the gRPC server, like the Go SDK's;
204        // a failure to serve it is logged, never the function's failure. The
205        // counting layer below is always installed - with the endpoint disabled
206        // the counts are simply never served, which is indistinguishable from
207        // the Go SDK installing no interceptor.
208        if !args.metrics_address.is_empty() {
209            crate::metrics::initialize();
210            let metrics_address = args.metrics_address.clone();
211            let registry = crate::metrics::served_registry(metrics_registry);
212            tokio::spawn(async move {
213                if let Err(e) = crate::metrics::serve(&metrics_address, registry).await {
214                    tracing::error!(error = %e, "cannot serve metrics");
215                }
216            });
217        }
218
219        tracing::info!(%address, insecure = args.insecure, "serving FunctionRunnerService");
220
221        // A readiness failure shuts the server down like a signal does.
222        // Without a readiness future, or once it succeeds, the receive never
223        // yields Ok, and select! waits for the signal alone.
224        let mut not_ready = None;
225        builder
226            .layer(crate::metrics::MetricsLayer)
227            .add_service(function_service)
228            .add_service(health_service)
229            .add_service(reflection_v1)
230            .add_service(reflection_v1alpha)
231            .serve_with_shutdown(address, async {
232                tokio::select! {
233                    () = shutdown_signal() => {}
234                    Ok(e) = not_ready_rx => not_ready = Some(e),
235                }
236            })
237            .await?;
238
239        match not_ready {
240            Some(e) => Err(Error::NotReady(e)),
241            None => Ok(()),
242        }
243    }
244}
245
246/// Sets the overall ("") and the FunctionRunnerService health status.
247async fn set_health(reporter: &HealthReporter, status: ServingStatus) {
248    for service in ["", SERVICE_NAME] {
249        reporter.set_service_status(service, status).await;
250    }
251}
252
253/// Answers RunFunction with UNAVAILABLE until the function is ready.
254struct Gate<F> {
255    function: F,
256    ready: Arc<AtomicBool>,
257}
258
259#[tonic::async_trait]
260impl<F: FunctionRunnerService> FunctionRunnerService for Gate<F> {
261    async fn run_function(
262        &self,
263        request: Request<RunFunctionRequest>,
264    ) -> Result<Response<RunFunctionResponse>, Status> {
265        if !self.ready.load(Ordering::Acquire) {
266            return Err(Status::unavailable("the function is not ready"));
267        }
268        self.function.run_function(request).await
269    }
270}
271
272fn tls_config(dir: &Path) -> Result<ServerTlsConfig, Error> {
273    let read = |name: &str| {
274        let path = dir.join(name);
275        std::fs::read(&path).map_err(|source| Error::ReadCertificate { path, source })
276    };
277    let cert = read("tls.crt")?;
278    let key = read("tls.key")?;
279    let ca = read("ca.crt")?;
280
281    Ok(ServerTlsConfig::new()
282        .identity(Identity::from_pem(cert, key))
283        .client_ca_root(Certificate::from_pem(ca))
284        .client_auth_optional(false))
285}
286
287#[cfg(unix)]
288async fn shutdown_signal() {
289    let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
290        .expect("cannot install SIGTERM handler");
291    tokio::select! {
292        _ = sigterm.recv() => {}
293        _ = tokio::signal::ctrl_c() => {}
294    }
295    tracing::info!("shutting down");
296}
297
298#[cfg(not(unix))]
299async fn shutdown_signal() {
300    let _ = tokio::signal::ctrl_c().await;
301    tracing::info!("shutting down");
302}