Skip to content
Open
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
81 changes: 80 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,85 @@ This function implements resource-specific health checks for standard Kubernetes

For all other resource types (Crossplane managed resources, custom resources, etc.), the function falls back to checking the standard Ready status condition.

## CEL-based health checks (alpha)

Some resource types — notably Crossplane `Configuration` and `Provider`
packages, Cluster API `Cluster` and many more — do not surface a `Ready` status condition, so the default
fallback never considers them ready. To handle these cases the function
supports user-defined readiness expressions written in
[CEL][cel], gated behind the `CELHealthcheckCustomizations` alpha feature
gate.

Enable the feature gate when running the function:

```shell
function-auto-ready --feature-gates=CELHealthcheckCustomizations=true
```

Customizations are supplied as a map keyed by `<group>_<version>_<kind>`
(the group's dots replaced with underscores; the core group is the empty
string). Each value is a CEL expression evaluated against the observed
composed resource, bound to the variable `object`. The expression must
return a boolean — any other result, or an evaluation error, is treated
as not ready and surfaces a Warning on the response. When a customization
exists for a given GVK it takes precedence over the built-in health check.

There are two ways to supply customizations, and they can be combined:

### Inline CEL rules (`celHealthCheckCustomization`)

Define rules directly in the function input. This is the simplest option
when the rules are static and do not need to vary across environments:

```yaml
- step: automatically-detect-ready-composed-resources
functionRef:
name: function-auto-ready
input:
apiVersion: autoready.fn.crossplane.io/v1alpha1
kind: Input
celHealthCheckCustomization:
pkg.crossplane.io_v1_Configuration: >-
object.status.conditions.exists(c, c.type == 'Installed' && c.status == 'True') &&
object.status.conditions.exists(c, c.type == 'Healthy' && c.status == 'True')
cluster.cluster.x-k8s.io_v1beta1_Cluster: >-
object.status.conditions.exists(c, c.type == 'Ready' && c.status == 'True')
```

### CEL rules from the function context (`celHealthCheckCustomizationFrom`)

Read the map from the function's request context by providing a
[field path][fieldpath]. This is useful when rules need to vary per
environment or be shared across multiple compositions:

```yaml
- step: automatically-detect-ready-composed-resources
functionRef:
name: function-auto-ready
input:
apiVersion: autoready.fn.crossplane.io/v1alpha1
kind: Input
celHealthCheckCustomizationFrom: "[apiextensions.crossplane.io/environment].celHealthCheckCustomizations"
```

Any source that populates the function context can supply the map —
typically `function-environment-configs` reading an `EnvironmentConfig`,
but an earlier pipeline step works just as well.

### Combining both sources

Both fields can be set at the same time. Context-provided rules are loaded
first; inline rules are then merged on top of them. Inline entries take
precedence, so they can selectively override context-provided rules for
specific GVKs without replacing the entire map.

See [`example/cel-healthcheck`](example/cel-healthcheck/) for a runnable
example that checks the `Installed` and `Healthy` conditions on a
Crossplane `Configuration`.

[cel]: https://github.com/google/cel-spec
[fieldpath]: https://pkg.go.dev/github.com/crossplane/function-sdk-go/request

In this example, the [Go Templating][fn-go-templating] function is used to add
a desired composed resource - an Amazon Web Services S3 Bucket. Once Crossplane
has created the Bucket, the Auto Ready function will let Crossplane know when it
Expand Down Expand Up @@ -106,7 +185,7 @@ spec:
name: function-auto-ready
```

See the [example](example) directory for an example you can run locally using
See the [example](example/basic/) directory for an example you can run locally using
the Crossplane CLI:

```shell
Expand Down
72 changes: 72 additions & 0 deletions cel/resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package cel

import (
"github.com/crossplane/function-sdk-go/resource"
"github.com/google/cel-go/cel"
celtypes "github.com/google/cel-go/common/types"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
)

type Resolver struct {
HealthCheckRegistry map[string]string
}

const (
errCelQueryFailedToCompile = "failed to compile query"
errCelQueryReturnTypeNotBool = "celQuery does not return a bool type"
errCelQueryFailedToCreateProgram = "failed to create program from the cel query"
errCelQueryFailedToEvalProgram = "failed to eval the program"
errCelQueryFailedToCreateEnvironment = "cel query failed to create environment"
)

func (r Resolver) GetHealthCheck(gvk schema.GroupVersionKind) (celQuery string, found bool) {
gvkKey := gvk.Group + "_" + gvk.Version + "_" + gvk.Kind

celQuery, found = r.HealthCheckRegistry[gvkKey]
return
}

func (r Resolver) HealthDeriveFromCelQuery(celQuery string, obj map[string]any) (ready resource.Ready, err error) {
ready = resource.ReadyUnspecified

env, err := cel.NewEnv(
cel.Variable("object", cel.AnyType),
)
if err != nil {
err = errors.Wrap(err, errCelQueryFailedToCreateEnvironment)
return ready, err
}

ast, iss := env.Compile(celQuery)
if iss.Err() != nil {
err = errors.Wrap(iss.Err(), errCelQueryFailedToCompile)
return ready, err
}

if !ast.OutputType().IsExactType(cel.BoolType) {
err = errors.New(errCelQueryReturnTypeNotBool)
return ready, err
}

program, err := env.Program(ast)
if err != nil {
err = errors.Wrap(err, errCelQueryFailedToCreateProgram)
return ready, err
}

val, _, err := program.Eval(map[string]any{
"object": obj,
})
if err != nil {
err = errors.Wrap(err, errCelQueryFailedToEvalProgram)
return ready, err
}

if val == celtypes.True {
ready = resource.ReadyTrue
} else {
ready = resource.ReadyFalse
}
return ready, err
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
92 changes: 92 additions & 0 deletions example/cel-healthcheck/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Example: CEL-based readiness checks for custom resources

This example shows how to configure `function-auto-ready` to evaluate readiness for
resource types it does not have a built-in health check for, by supplying a
[CEL][cel] expression per GVK.

By default, `function-auto-ready` falls back to checking the standard
`Ready` status condition for any resource type it does not recognize. Some
resources — like Crossplane `Configuration` packages — never surface a
`Ready` condition, and instead report their state via `Installed` and
`Healthy` conditions. For these resources you can provide a CEL expression
that evaluates the observed object and returns a boolean.

## How it works

The composition has three pipeline steps:

1. **`create-k8s-resources`** (`function-go-templating`) — renders a
`pkg.crossplane.io/v1` `Configuration` as a composed resource.
2. **`fetch-cel-healthcheck-customizations`** (`function-environment-configs`) —
loads the `healthcheck-customizations` `EnvironmentConfig` and merges its
`data` into the composition environment under the
`apiextensions.crossplane.io/environment` context key.
3. **`automatically-detect-ready-composed-resources`** (`function-auto-ready`) —
reads CEL customizations from the environment via
`celHealthCheckCustomizationFrom` and uses them when evaluating readiness.

### The CEL customization map

`extra-resources.yaml` defines the customizations keyed by
`<group>_<version>_<kind>` (the group's dots replaced with underscores; the
core group is the empty string):

```yaml
data:
celHealthCheckCustomizations:
pkg.crossplane.io_v1_Configuration: >
object.status.conditions.exists(c, c.type == "Installed" && c.status == "True")
&& object.status.conditions.exists(c, c.type == "Healthy" && c.status == "True")
```

The CEL expression is evaluated against the observed composed resource, which
is bound to the variable `object`. The expression must return a boolean; any
other result, or an evaluation error, is treated as not ready.

### Wiring it into the function

`composition.yaml` points the function at the environment key that holds the
map:

```yaml
- step: automatically-detect-ready-composed-resources
functionRef:
name: function-auto-ready
input:
apiVersion: autoready.fn.crossplane.io/v1alpha1
kind: Input
celHealthCheckCustomizationFrom: "[apiextensions.crossplane.io/environment].celHealthCheckCustomizations"
```

The value of `celHealthCheckCustomizationFrom` is a
[field path][fieldpath] into the function's request context. Any source that
populates that context (environment configs, an earlier function, etc.) can
supply the map.

## Running the example

In a separate shell run the local process of function-auto-ready from root of the repository:
```shell
go run . --insecure --feature-gates=CELHealthcheckCustomizations=true
```

Run render command from example's directory:
```shell
crossplane render \
--extra-resources extra-resources.yaml \
--observed-resources observed.yaml \
--include-context \
xr.yaml composition.yaml functions.yaml
```

`observed.yaml` simulates a `Configuration` that already exists in the cluster
with both `Installed=True` and `Healthy=True`. Because the supplied CEL
expression evaluates to `true` against that observed state, `function-auto-ready`
marks the composed resource — and therefore the XR — ready.

To see the fallback behavior, edit `observed.yaml` to flip one of the
conditions to `False` and re-run; the XR should no longer be reported as
ready.

[cel]: https://github.com/google/cel-spec
[fieldpath]: https://pkg.go.dev/github.com/crossplane/function-sdk-go/request
48 changes: 48 additions & 0 deletions example/cel-healthcheck/composition.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: example-k8s-service-cluster-configurations
spec:
compositeTypeRef:
apiVersion: example.crossplane.io/v1
kind: XR
mode: Pipeline
pipeline:
- step: create-k8s-resources
functionRef:
name: function-go-templating
input:
apiVersion: gotemplating.fn.crossplane.io/v1beta1
kind: GoTemplate
source: Inline
inline:
template: |
---
apiVersion: pkg.crossplane.io/v1
kind: Configuration
metadata:
name: configuration-database-platform
annotations:
gotemplating.fn.crossplane.io/composition-resource-name: configuration-database-platform
spec:
ignoreCrossplaneConstraints: false
package: xpkg.crossplane.io/configuration-database-platform:1.0.0
- step: fetch-cel-healthcheck-customizations
functionRef:
name: function-environment-configs
input:
apiVersion: environmentconfigs.fn.crossplane.io/v1beta1
kind: Input
spec:
environmentConfigs:
- type: Reference
ref:
name: healthcheck-customizations
# This function will automatically detect readiness using resource-specific health checks
- step: automatically-detect-ready-composed-resources
functionRef:
name: function-auto-ready
input:
apiVersion: autoready.fn.crossplane.io/v1beta1
kind: Input
celHealthCheckCustomizationFrom: "[apiextensions.crossplane.io/environment].celHealthCheckCustomizations"
7 changes: 7 additions & 0 deletions example/cel-healthcheck/extra-resources.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: apiextensions.crossplane.io/v1beta1
kind: EnvironmentConfig
metadata:
name: healthcheck-customizations
data:
celHealthCheckCustomizations:
pkg.crossplane.io_v1_Configuration: 'object.status.conditions.exists(c, c.type == "Installed" && c.status == "True") && object.status.conditions.exists(c, c.type == "Healthy" && c.status == "True")'
25 changes: 25 additions & 0 deletions example/cel-healthcheck/functions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
apiVersion: pkg.crossplane.io/v1beta1
kind: Function
metadata:
name: function-environment-configs
spec:
package: xpkg.crossplane.io/crossplane-contrib/function-environment-configs:v0.6.0
---
apiVersion: pkg.crossplane.io/v1beta1
kind: Function
metadata:
name: function-go-templating
spec:
package: xpkg.crossplane.io/crossplane-contrib/function-go-templating:v0.9.2
---
apiVersion: pkg.crossplane.io/v1beta1
kind: Function
metadata:
name: function-auto-ready
annotations:
# This tells crossplane beta render to connect to the function locally.
render.crossplane.io/runtime: Development
spec:
# This is ignored when using the Development runtime.
package: function-auto-ready
31 changes: 31 additions & 0 deletions example/cel-healthcheck/observed.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# This file simulates observed resources as they would appear in a running cluster
# Use this with: crossplane render xr.yaml composition.yaml functions.yaml -o observed.yaml
---
apiVersion: pkg.crossplane.io/v1
kind: Configuration
metadata:
name: configuration-database-platform
annotations:
crossplane.io/composition-resource-name: configuration-database-platform
spec:
ignoreCrossplaneConstraints: false
package: xpkg.crossplane.io/configuration-database-platform:1.0.0
packagePullPolicy: Always
revisionActivationPolicy: Automatic
revisionHistoryLimit: 1
skipDependencyResolution: false
status:
conditions:
- lastTransitionTime: '2026-04-08T13:18:16Z'
observedGeneration: 13
reason: HealthyPackageRevision
status: 'True'
type: Healthy
- lastTransitionTime: '2026-04-08T13:18:12Z'
observedGeneration: 13
reason: ActivePackageRevision
status: 'True'
type: Installed
currentIdentifier: xpkg.crossplane.io/configuration-database-platform:1.0.0
currentRevision: configuration-database-platform-a75c2c6ed34f
resolvedPackage: xpkg.crossplane.io/configuration-database-platform:1.0.0
7 changes: 7 additions & 0 deletions example/cel-healthcheck/xr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Replace this with your XR!
apiVersion: example.crossplane.io/v1
kind: XR
metadata:
name: example-xr
spec:
region: us-east-2
Loading
Loading