Skip to content

Commit 0602b3e

Browse files
committed
Add input ttl
Signed-off-by: Bob Haddleton <bob.haddleton@nokia.com>
1 parent 37db6f1 commit 0602b3e

10 files changed

Lines changed: 247 additions & 6 deletions

File tree

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@
88
*.so
99
*.dylib
1010

11+
# Jetbrains IDEs
12+
.idea
13+
1114
# Test binary, built with `go test -c`
1215
*.test
1316

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

1720
# Dependency directories (remove the comment below to include it)
18-
# vendor/
21+
vendor/
1922

2023
# Go workspace file
2124
go.work

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,43 @@ $ crossplane render xr.yaml composition-k8s.yaml functions.yaml -o observed-k8s.
122122
See the [composition functions documentation][docs-functions] to learn more
123123
about `crossplane render`.
124124

125+
## Function Response Caching
126+
127+
You can set the `ttl` input to control the Function response cache time-to-live.
128+
This is useful for tuning reconciliation behavior in large compositions.
129+
130+
```yaml
131+
- step: auto-detect-ready-resources
132+
functionRef:
133+
name: function-auto-ready
134+
input:
135+
apiVersion: autoready.fn.crossplane.io/v1beta1
136+
kind: Input
137+
ttl: 5m
138+
```
139+
140+
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
141+
in the composition function input. Use a `DeploymentRuntimeConfig` to set this parameter.
142+
143+
```yaml
144+
apiVersion: pkg.crossplane.io/v1beta1
145+
kind: DeploymentRuntimeConfig
146+
metadata:
147+
name: function-auto-ready
148+
spec:
149+
deploymentTemplate:
150+
spec:
151+
selector: {}
152+
template:
153+
spec:
154+
containers:
155+
- name: package-runtime
156+
args:
157+
- --debug
158+
- --ttl="5m"
159+
```
160+
161+
125162
## Developing this function
126163

127164
This function uses [Go][go], [Docker][docker], and the [Crossplane CLI][cli] to

fn.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import (
44
"context"
55
"time"
66

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

10+
"github.com/crossplane/function-auto-ready/input/v1beta1"
911
"github.com/crossplane/function-sdk-go/errors"
1012
"github.com/crossplane/function-sdk-go/logging"
1113
fnv1 "github.com/crossplane/function-sdk-go/proto/v1"
@@ -23,14 +25,28 @@ type Function struct {
2325
fnv1.UnimplementedFunctionRunnerServiceServer
2426

2527
log logging.Logger
26-
TTL time.Duration
28+
ttl time.Duration
2729
}
2830

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

33-
rsp := response.To(req, f.TTL)
35+
rsp := response.To(req, f.ttl)
36+
37+
in := &v1beta1.Input{}
38+
if err := request.GetInput(req, in); err != nil {
39+
response.Fatal(rsp, errors.Wrapf(err, "cannot get Function input from %T", req))
40+
return rsp, nil
41+
}
42+
if in.TTL != "" {
43+
dur, err := time.ParseDuration(in.TTL)
44+
if err != nil {
45+
response.Fatal(rsp, errors.Wrapf(err, "cannot set ttl"))
46+
return rsp, nil
47+
}
48+
rsp.Meta.Ttl = durationpb.New(dur)
49+
}
3450

3551
oxr, err := request.GetObservedCompositeResource(req)
3652
if err != nil {

fn_test.go

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package main
33
import (
44
"context"
55
"testing"
6+
"time"
67

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

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

307309
if diff := cmp.Diff(tc.want.rsp, rsp, protocmp.Transform()); diff != "" {
@@ -314,3 +316,63 @@ func TestRunFunction(t *testing.T) {
314316
})
315317
}
316318
}
319+
320+
func TestRunFunctionCacheTTL(t *testing.T) {
321+
xr := `{"apiVersion":"example.org/v1","kind":"XR","metadata":{"name":"cool-xr"},"spec":{"count":1}}`
322+
323+
cases := map[string]struct {
324+
reason string
325+
input *v1beta1.Input
326+
want *fnv1.RunFunctionResponse
327+
}{
328+
"InputTTL": {
329+
reason: "Set the response ttl value from the input specified",
330+
input: &v1beta1.Input{TTL: "5m"},
331+
want: &fnv1.RunFunctionResponse{
332+
Meta: &fnv1.ResponseMeta{Ttl: durationpb.New(5 * time.Minute)},
333+
Desired: &fnv1.State{
334+
Composite: &fnv1.Resource{
335+
Resource: resource.MustStructJSON(xr),
336+
},
337+
Resources: map[string]*fnv1.Resource{
338+
"second": {
339+
Resource: resource.MustStructJSON(xr),
340+
},
341+
},
342+
},
343+
},
344+
},
345+
}
346+
347+
for name, tc := range cases {
348+
t.Run(name, func(t *testing.T) {
349+
f := &Function{log: logging.NewNopLogger()}
350+
req := &fnv1.RunFunctionRequest{
351+
Input: resource.MustStructObject(tc.input),
352+
Observed: &fnv1.State{
353+
Composite: &fnv1.Resource{
354+
Resource: resource.MustStructJSON(xr),
355+
},
356+
Resources: map[string]*fnv1.Resource{},
357+
},
358+
Desired: &fnv1.State{
359+
Composite: &fnv1.Resource{
360+
Resource: resource.MustStructJSON(xr),
361+
},
362+
Resources: map[string]*fnv1.Resource{
363+
"second": {
364+
Resource: resource.MustStructJSON(xr),
365+
},
366+
},
367+
},
368+
}
369+
rsp, err := f.RunFunction(context.Background(), req)
370+
if diff := cmp.Diff(tc.want, rsp, protocmp.Transform()); diff != "" {
371+
t.Errorf("%s\nf.RunFunction(...): -want rsp, +got rsp:\n%s", tc.reason, diff)
372+
}
373+
if diff := cmp.Diff(nil, err, cmpopts.EquateErrors()); diff != "" {
374+
t.Errorf("%s\nf.RunFunction(...): -want err, +got err:\n%s", tc.reason, diff)
375+
}
376+
})
377+
}
378+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
google.golang.org/protobuf v1.36.11
1111
k8s.io/api v0.35.4
1212
k8s.io/apimachinery v0.35.4
13+
sigs.k8s.io/controller-tools v0.20.0
1314
)
1415

1516
require (
@@ -86,7 +87,6 @@ require (
8687
k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 // indirect
8788
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 // indirect
8889
sigs.k8s.io/controller-runtime v0.23.1 // indirect
89-
sigs.k8s.io/controller-tools v0.20.0 // indirect
9090
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
9191
sigs.k8s.io/randfill v1.0.0 // indirect
9292
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect

input/generate.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//go:build generate
2+
// +build generate
3+
4+
// NOTE(negz): See the below link for details on what is happening here.
5+
// https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module
6+
7+
// Remove existing and generate new input manifests
8+
//go:generate rm -rf ../package/input/
9+
//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
10+
11+
package input
12+
13+
import (
14+
_ "sigs.k8s.io/controller-tools/cmd/controller-gen" //nolint:typecheck
15+
)

input/v1beta1/input.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Package v1beta1 contains the input type for this Function
2+
// +kubebuilder:object:generate=true
3+
// +groupName=autoready.fn.crossplane.io
4+
// +versionName=v1beta1
5+
package v1beta1
6+
7+
import (
8+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9+
)
10+
11+
// This isn't a custom resource, in the sense that we never install its CRD.
12+
// It is a KRM-like object, so we generate a CRD to describe its schema.
13+
14+
// Input is used to provide inputs to this Function.
15+
// +kubebuilder:object:root=true
16+
// +kubebuilder:storageversion
17+
// +kubebuilder:resource:categories=crossplane
18+
type Input struct {
19+
metav1.TypeMeta `json:",inline"`
20+
21+
metav1.ObjectMeta `json:"metadata,omitempty"`
22+
23+
// TTL for which a response can be cached in time.Duration format
24+
// +kubebuilder:default="1m0s"
25+
// +optional
26+
TTL string `json:"ttl"`
27+
}

input/v1beta1/zz_generated.deepcopy.go

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func (c *CLI) Run() error {
3333
ttl = *c.TTL
3434
}
3535

36-
return function.Serve(&Function{log: log, TTL: ttl},
36+
return function.Serve(&Function{log: log, ttl: ttl},
3737
function.Listen(c.Network, c.Address),
3838
function.MTLSCertificates(c.TLSCertsDir),
3939
function.Insecure(c.Insecure),
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
apiVersion: apiextensions.k8s.io/v1
3+
kind: CustomResourceDefinition
4+
metadata:
5+
annotations:
6+
controller-gen.kubebuilder.io/version: v0.20.0
7+
name: inputs.autoready.fn.crossplane.io
8+
spec:
9+
group: autoready.fn.crossplane.io
10+
names:
11+
categories:
12+
- crossplane
13+
kind: Input
14+
listKind: InputList
15+
plural: inputs
16+
singular: input
17+
scope: Namespaced
18+
versions:
19+
- name: v1beta1
20+
schema:
21+
openAPIV3Schema:
22+
description: Input is used to provide inputs to this Function.
23+
properties:
24+
apiVersion:
25+
description: |-
26+
APIVersion defines the versioned schema of this representation of an object.
27+
Servers should convert recognized schemas to the latest internal value, and
28+
may reject unrecognized values.
29+
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
30+
type: string
31+
kind:
32+
description: |-
33+
Kind is a string value representing the REST resource this object represents.
34+
Servers may infer this from the endpoint the client submits requests to.
35+
Cannot be updated.
36+
In CamelCase.
37+
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
38+
type: string
39+
metadata:
40+
type: object
41+
ttl:
42+
default: 1m0s
43+
description: TTL for which a response can be cached in time.Duration format
44+
type: string
45+
type: object
46+
served: true
47+
storage: true

0 commit comments

Comments
 (0)