From 6a0a5e75a151f3f261151dae36124014eebed986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonasz=20=C5=81asut-Balcerzak?= Date: Thu, 17 Sep 2026 11:36:00 +0200 Subject: [PATCH] docs(cli): add Rust tab to projects guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document writing the get-started composition function in Rust, next to the Templated YAML, Python, Go and KCL tabs. The example builds the Deployment and the Service from the models the CLI generates into the crossplane-models crate, and renders to the output the page already shows. Note that render needs a longer timeout with a Rust function: the CLI compiles the function and its dependencies from source on every render, which overruns the default of one minute. Add crates.io to the Vale brand list so the page lints clean. Signed-off-by: Jonasz Łasut-Balcerzak --- ...get-started-with-control-plane-projects.md | 184 +++++++++++++++++- utils/vale/styles/Crossplane/brands.txt | 1 + 2 files changed, 183 insertions(+), 2 deletions(-) diff --git a/content/cli/master/get-started/get-started-with-control-plane-projects.md b/content/cli/master/get-started/get-started-with-control-plane-projects.md index dc2125420..fda41058c 100644 --- a/content/cli/master/get-started/get-started-with-control-plane-projects.md +++ b/content/cli/master/get-started/get-started-with-control-plane-projects.md @@ -14,8 +14,8 @@ runs the project on a local development control plane so you can test it without deploying to a shared cluster. {{}} -This guide shows how to write the composition function in Go, Python, KCL, and -templated YAML. You can pick your preferred language. +This guide shows how to write the composition function in Go, Python, Rust, KCL, +and templated YAML. You can pick your preferred language. {{}} A `WebApp` custom resource looks like this: @@ -796,6 +796,179 @@ bindings the CLI generated when you added the Kubernetes dependency, so the compiler checks the resources you create. {{< /tab >}} +{{< tab "Rust" >}} +Rust is a good choice if you want a statically typed, compiled function and +access to the [crates.io](https://crates.io) ecosystem. + +Generate a Rust function named `compose-webapp` and add it to the composition's +pipeline: + +```shell +crossplane function generate compose-webapp apis/webapps/composition.yaml --language rust +``` + +The command scaffolds the function under `functions/compose-webapp/` and adds a +pipeline step to `apis/webapps/composition.yaml`. + +Replace the contents of `functions/compose-webapp/src/function.rs` with the +following function logic: + +```rust +//! Composes a Deployment and a Service for a WebApp. + +use std::collections::BTreeMap; + +use crossplane_models::com::example::platform::v1alpha1::WebApp; +use crossplane_models::io::k8s::api::apps::v1::{Deployment, DeploymentSpec}; +use crossplane_models::io::k8s::api::core::v1::{ + Container, ContainerPort, PodSpec, PodTemplateSpec, Service, ServicePort, ServiceSpec, +}; +use crossplane_models::io::k8s::apimachinery::pkg::apis::meta::v1::{LabelSelector, ObjectMeta}; +use crossplane_models::io::k8s::apimachinery::pkg::util::intstr::IntOrString; +use function_sdk_rust::proto::v1::function_runner_service_server::FunctionRunnerService; +use function_sdk_rust::proto::v1::{RunFunctionRequest, RunFunctionResponse}; +use function_sdk_rust::{resource, response}; +use tonic::{Request, Response, Status}; + +/// The composition function. +#[derive(Debug, Default)] +pub struct Function; + +#[tonic::async_trait] +impl FunctionRunnerService for Function { + async fn run_function( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + let tag = req.meta.as_ref().map(|m| m.tag.clone()).unwrap_or_default(); + tracing::info!(tag, "running function"); + + let mut rsp = response::to(&req, response::DEFAULT_TTL); + + let observed = req.observed.as_ref().and_then(|s| s.composite.as_ref()); + let xr: WebApp = match resource::get(observed) { + Ok(xr) => xr, + Err(e) => { + response::fatal(&mut rsp, format!("cannot get xr: {e}")); + return Ok(Response::new(rsp)); + } + }; + + let metadata = xr.metadata.unwrap_or_default(); + let spec = xr.spec.unwrap_or_default(); + let (Some(name), Some(image)) = (metadata.name, spec.image) else { + response::fatal(&mut rsp, "xr is missing metadata.name or spec.image"); + return Ok(Response::new(rsp)); + }; + + let ports = spec.ports.unwrap_or_default(); + let labels = BTreeMap::from([("app.kubernetes.io/name".to_string(), name.clone())]); + + // Build each resource from its generated model. Default fills in the + // apiVersion and kind, and leaves every field the function doesn't set + // out of the desired state. + let deployment = Deployment { + metadata: Some(ObjectMeta { + name: Some(name.clone()), + namespace: metadata.namespace.clone(), + labels: Some(labels.clone()), + ..Default::default() + }), + spec: Some(DeploymentSpec { + replicas: spec.replicas.map(|r| r as i32), + selector: Some(LabelSelector { + match_labels: Some(labels.clone()), + ..Default::default() + }), + template: Some(PodTemplateSpec { + metadata: Some(ObjectMeta { + labels: Some(labels.clone()), + ..Default::default() + }), + spec: Some(PodSpec { + containers: Some(vec![Container { + name: Some(name.clone()), + image: Some(image), + ports: Some( + ports + .iter() + .map(|p| ContainerPort { + container_port: Some(*p as i32), + ..Default::default() + }) + .collect(), + ), + ..Default::default() + }]), + ..Default::default() + }), + }), + ..Default::default() + }), + ..Default::default() + }; + + let service = Service { + metadata: Some(ObjectMeta { + name: Some(name), + namespace: metadata.namespace, + ..Default::default() + }), + spec: Some(ServiceSpec { + selector: Some(labels), + ports: Some( + ports + .iter() + .map(|p| ServicePort { + protocol: Some("TCP".to_string()), + port: Some(*p as i32), + target_port: Some(IntOrString::Int(*p)), + ..Default::default() + }) + .collect(), + ), + ..Default::default() + }), + ..Default::default() + }; + + let desired = rsp.desired.get_or_insert_default(); + resource::update( + desired + .resources + .entry("deployment".to_string()) + .or_default(), + &deployment, + ) + .map_err(|e| Status::internal(e.to_string()))?; + resource::update( + desired.resources.entry("service".to_string()).or_default(), + &service, + ) + .map_err(|e| Status::internal(e.to_string()))?; + + Ok(Response::new(rsp)) + } +} +``` + +The function reads the observed `WebApp` XR, then builds a `Deployment` and a +`Service` from its `spec`. + +The `crossplane_models` crate holds the bindings the CLI generated from your +XRD and from the Kubernetes dependency, so the compiler checks the resources you +create. The function's `Cargo.toml` depends on the crate by path. Each API group +and version is a module named after the reversed group, which is why the +`WebApp` of `platform.example.com/v1alpha1` is +`crossplane_models::com::example::platform::v1alpha1::WebApp`. + +Every field of a generated model is an `Option`, and a model leaves unset fields +out when it serializes. `..Default::default()` leaves the rest of a struct +unset and fills in the `apiVersion` and `kind` of a resource, so the function's +desired state contains only the fields it sets. +{{< /tab >}} + {{< tab "KCL" >}} [KCL](https://kcl-lang.io) is a good choice for functions with dynamic logic. It's fast and sandboxed. @@ -935,6 +1108,13 @@ functions automatically, so you only pass the example XR and the composition: crossplane composition render examples/webapps/podinfo.yaml apis/webapps/composition.yaml ``` +{{}} +`render` times out after a minute by default. The CLI compiles a Rust function +and its dependencies from source for every render, which can take longer than +that and fails with `context deadline exceeded`. Pass `--timeout 5m` when the +project has a Rust function. +{{}} + The command prints the rendered `Deployment` and `Service` as well as the updates Crossplane would make to the `WebApp` XR: diff --git a/utils/vale/styles/Crossplane/brands.txt b/utils/vale/styles/Crossplane/brands.txt index 39fe16708..2880be14e 100644 --- a/utils/vale/styles/Crossplane/brands.txt +++ b/utils/vale/styles/Crossplane/brands.txt @@ -7,6 +7,7 @@ CloudSQL CNCF KEDA Commonmark +crates.io DockerHub DocSearch FluxCD