design/one-pager-function-capabilities.md
Crossplane's function protocol has evolved since v1beta1. Features like required resources, credentials, conditions, and required schemas were added in subsequent releases. When a function uses one of these features with an older version of Crossplane, Crossplane silently ignores the unknown fields. The function has no way to know whether Crossplane will honor its request.
For example, a function that requests schemas via requirements.schemas can't
distinguish between "Crossplane fetched the schema but found nothing" and
"Crossplane doesn't understand schema requests at all".
Add a Capability enum and capabilities field to RequestMeta:
enum Capability {
CAPABILITY_UNSPECIFIED = 0;
CAPABILITY_CAPABILITIES = 1; // v2.2
CAPABILITY_REQUIRED_RESOURCES = 2; // v1.15
CAPABILITY_CREDENTIALS = 3; // v1.16
CAPABILITY_CONDITIONS = 4; // v1.17
CAPABILITY_REQUIRED_SCHEMAS = 5; // v2.2
}
message RequestMeta {
string tag = 1;
repeated Capability capabilities = 2;
}
Crossplane populates capabilities with all features it supports when calling
functions. Functions check for a capability before relying on the corresponding
feature, falling back gracefully when the capability is absent.
CAPABILITY_CAPABILITIES is the bootstrap capability. Its presence tells the
function that Crossplane advertises capabilities. If another capability is
absent, the function knows Crossplane doesn't support it - not that Crossplane
predates capability advertisement entirely.
Functions check for a capability before relying on the corresponding feature.
The function SDKs will provide a has_capability helper. Usage in Python:
async def RunFunction(
self, req: fnv1.RunFunctionRequest, _: grpc.aio.ServicerContext
) -> fnv1.RunFunctionResponse:
rsp = response.to(req)
if request.has_capability(req, fnv1.CAPABILITY_REQUIRED_SCHEMAS):
# Request the schema - Crossplane will populate it next iteration
response.require_schema(rsp, "xr", "example.org/v1", "MyXR")
schema = request.get_required_schema(req, "xr")
if schema:
# Use schema for validation
pass
else:
# Crossplane doesn't support schemas - fall back or skip validation
pass
return rsp
Go and other languages work the same way.
Use repeated string capabilities instead of an enum. This is more flexible -
Crossplane could add capabilities without a proto change. However, the
capabilities we're advertising are inherently tied to proto fields. You can't
use a new capability without updating to a proto that has the corresponding
field. Given this coupling, enum provides better type safety and documentation
with no practical downside.
Add a int32 protocol_version field instead of listing capabilities. Functions
would need to know which version introduced which feature. This is less
self-documenting and harder to extend than explicit capability flags.
Use gRPC's server reflection API to let functions discover what fields exist. This is complex - functions would need to query the reflection API and parse protobuf descriptors to determine support. It also doesn't distinguish between "field exists in proto" and "Crossplane actually implements this feature".
Protobuf supports self-describing messages by embedding a
FileDescriptorSet in the message. Crossplane could include the proto schema
for RunFunctionRequest in each request. Functions could parse the descriptor
to discover what fields exist. This is heavyweight - descriptors add
significant message size - and like gRPC reflection doesn't distinguish between
"field exists" and "feature implemented".
Functions could request a feature (e.g. schemas), then check whether Crossplane populated the response on the next iteration. This works but requires an extra round-trip, complicates function logic, and relies on subtle proto unknown field preservation semantics.