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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@
*.so
*.dylib

# Jetbrains IDEs
.idea

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/
vendor/

# Go workspace file
go.work
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,43 @@ $ crossplane render xr.yaml composition-k8s.yaml functions.yaml -o observed-k8s.
See the [composition functions documentation][docs-functions] to learn more
about `crossplane render`.

## Function Response Caching

You can set the `ttl` input to control the Function response cache time-to-live.
This is useful for tuning reconciliation behavior in large compositions.

```yaml
- step: auto-detect-ready-resources
functionRef:
name: function-auto-ready
input:
apiVersion: autoready.fn.crossplane.io/v1beta1
kind: Input
ttl: 5m
```

There is also a `--ttl` input parameter to the function that can be used to set the default TTL used when it is not set
in the composition function input. Use a `DeploymentRuntimeConfig` to set this parameter.

```yaml
apiVersion: pkg.crossplane.io/v1beta1
kind: DeploymentRuntimeConfig
metadata:
name: function-auto-ready
spec:
deploymentTemplate:
spec:
selector: {}
template:
spec:
containers:
- name: package-runtime
args:
- --debug
- --ttl="5m"
```


## Developing this function

This function uses [Go][go], [Docker][docker], and the [Crossplane CLI][cli] to
Expand Down
20 changes: 18 additions & 2 deletions fn.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import (
"context"
"time"

"google.golang.org/protobuf/types/known/durationpb"
corev1 "k8s.io/api/core/v1"

"github.com/crossplane/function-auto-ready/input/v1beta1"
"github.com/crossplane/function-sdk-go/errors"
"github.com/crossplane/function-sdk-go/logging"
fnv1 "github.com/crossplane/function-sdk-go/proto/v1"
Expand All @@ -23,14 +25,28 @@ type Function struct {
fnv1.UnimplementedFunctionRunnerServiceServer

log logging.Logger
TTL time.Duration
ttl time.Duration
}

// RunFunction runs the Function.
func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) (*fnv1.RunFunctionResponse, error) {
f.log.Debug("Running Function", "tag", req.GetMeta().GetTag())

rsp := response.To(req, f.TTL)
rsp := response.To(req, f.ttl)

in := &v1beta1.Input{}
if err := request.GetInput(req, in); err != nil {
response.Fatal(rsp, errors.Wrapf(err, "cannot get Function input from %T", req))
return rsp, nil
}
if in.TTL != "" {
dur, err := time.ParseDuration(in.TTL)
if err != nil {
response.Fatal(rsp, errors.Wrapf(err, "cannot set ttl"))
return rsp, nil
}
rsp.Meta.Ttl = durationpb.New(dur)
}

oxr, err := request.GetObservedCompositeResource(req)
if err != nil {
Expand Down
64 changes: 63 additions & 1 deletion fn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package main
import (
"context"
"testing"
"time"

"github.com/crossplane/function-auto-ready/input/v1beta1"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"google.golang.org/protobuf/testing/protocmp"
Expand Down Expand Up @@ -301,7 +303,7 @@ func TestRunFunction(t *testing.T) {

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
f := &Function{log: logging.NewNopLogger(), TTL: response.DefaultTTL}
f := &Function{log: logging.NewNopLogger(), ttl: response.DefaultTTL}
rsp, err := f.RunFunction(tc.args.ctx, tc.args.req)

if diff := cmp.Diff(tc.want.rsp, rsp, protocmp.Transform()); diff != "" {
Expand All @@ -314,3 +316,63 @@ func TestRunFunction(t *testing.T) {
})
}
}

func TestRunFunctionCacheTTL(t *testing.T) {
xr := `{"apiVersion":"example.org/v1","kind":"XR","metadata":{"name":"cool-xr"},"spec":{"count":1}}`

cases := map[string]struct {
reason string
input *v1beta1.Input
want *fnv1.RunFunctionResponse
}{
"InputTTL": {
reason: "Set the response ttl value from the input specified",
input: &v1beta1.Input{TTL: "5m"},
want: &fnv1.RunFunctionResponse{
Meta: &fnv1.ResponseMeta{Ttl: durationpb.New(5 * time.Minute)},
Desired: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(xr),
},
Resources: map[string]*fnv1.Resource{
"second": {
Resource: resource.MustStructJSON(xr),
},
},
},
},
},
}

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
f := &Function{log: logging.NewNopLogger()}
req := &fnv1.RunFunctionRequest{
Input: resource.MustStructObject(tc.input),
Observed: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(xr),
},
Resources: map[string]*fnv1.Resource{},
},
Desired: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(xr),
},
Resources: map[string]*fnv1.Resource{
"second": {
Resource: resource.MustStructJSON(xr),
},
},
},
}
rsp, err := f.RunFunction(context.Background(), req)
if diff := cmp.Diff(tc.want, rsp, protocmp.Transform()); diff != "" {
t.Errorf("%s\nf.RunFunction(...): -want rsp, +got rsp:\n%s", tc.reason, diff)
}
if diff := cmp.Diff(nil, err, cmpopts.EquateErrors()); diff != "" {
t.Errorf("%s\nf.RunFunction(...): -want err, +got err:\n%s", tc.reason, diff)
}
})
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
google.golang.org/protobuf v1.36.11
k8s.io/api v0.35.4
k8s.io/apimachinery v0.35.4
sigs.k8s.io/controller-tools v0.20.0
)

require (
Expand Down Expand Up @@ -86,7 +87,6 @@ require (
k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 // indirect
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 // indirect
sigs.k8s.io/controller-runtime v0.23.1 // indirect
sigs.k8s.io/controller-tools v0.20.0 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect
Expand Down
15 changes: 15 additions & 0 deletions input/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//go:build generate
// +build generate

// NOTE(negz): See the below link for details on what is happening here.
// https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module

// Remove existing and generate new input manifests
//go:generate rm -rf ../package/input/
//go:generate go run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen paths=./v1beta1 object crd:crdVersions=v1 output:artifacts:config=../package/input

package input

import (
_ "sigs.k8s.io/controller-tools/cmd/controller-gen" //nolint:typecheck
)
27 changes: 27 additions & 0 deletions input/v1beta1/input.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Package v1beta1 contains the input type for this Function
// +kubebuilder:object:generate=true
// +groupName=autoready.fn.crossplane.io
// +versionName=v1beta1
package v1beta1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// This isn't a custom resource, in the sense that we never install its CRD.
// It is a KRM-like object, so we generate a CRD to describe its schema.

// Input is used to provide inputs to this Function.
// +kubebuilder:object:root=true
// +kubebuilder:storageversion
// +kubebuilder:resource:categories=crossplane
type Input struct {
metav1.TypeMeta `json:",inline"`

metav1.ObjectMeta `json:"metadata,omitempty"`

// TTL for which a response can be cached in time.Duration format
// +kubebuilder:default="1m0s"
// +optional
TTL string `json:"ttl"`
}
34 changes: 34 additions & 0 deletions input/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (c *CLI) Run() error {
ttl = *c.TTL
}

return function.Serve(&Function{log: log, TTL: ttl},
return function.Serve(&Function{log: log, ttl: ttl},
function.Listen(c.Network, c.Address),
function.MTLSCertificates(c.TLSCertsDir),
function.Insecure(c.Insecure),
Expand Down
47 changes: 47 additions & 0 deletions package/input/autoready.fn.crossplane.io_inputs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.20.0
name: inputs.autoready.fn.crossplane.io
spec:
group: autoready.fn.crossplane.io
names:
categories:
- crossplane
kind: Input
listKind: InputList
plural: inputs
singular: input
scope: Namespaced
versions:
- name: v1beta1
schema:
openAPIV3Schema:
description: Input is used to provide inputs to this Function.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
ttl:
default: 1m0s
description: TTL for which a response can be cached in time.Duration format
type: string
type: object
served: true
storage: true
Loading