diff --git a/README.md b/README.md index 3a25a71..af9d8f4 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,25 @@ In other words, the following rules apply: See `example/composition-regex.yaml` for a complete example. +### Function Response Caching + +You can set `cacheTTL` to control the Function response cache time-to-live. +This is useful for tuning reconciliation behavior in large compositions. + +```yaml + - step: sequence-creation + functionRef: + name: function-sequencer + input: + apiVersion: sequencer.fn.crossplane.io/v1beta1 + kind: Input + cacheTTL: 5m + rules: + - sequence: + - first + - second +``` + ### Composite Readiness Enabling the `resetCompositeReadiness` flag causes the function to set the Composite's `Ready` flag to `False` when at least one desired resource is deleted from the request. This prevents the Composite resource from entering the `Ready` diff --git a/fn.go b/fn.go index 27ccfad..f08b563 100644 --- a/fn.go +++ b/fn.go @@ -7,12 +7,14 @@ import ( "maps" "regexp" "strings" + "time" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" apiextensionsv1beta1 "github.com/crossplane/crossplane/v2/apis/apiextensions/v1beta1" protectionv1beta1 "github.com/crossplane/crossplane/v2/apis/protection/v1beta1" "github.com/crossplane/function-sequencer/input/v1beta1" + "google.golang.org/protobuf/types/known/durationpb" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" v1 "github.com/crossplane/function-sdk-go/proto/v1" @@ -57,6 +59,14 @@ func (f *Function) RunFunction(_ context.Context, req *v1.RunFunctionRequest) (* response.Fatal(rsp, errors.Wrapf(err, "cannot get Function input from %T", req)) return rsp, nil } + if in.CacheTTL != "" { + dur, err := time.ParseDuration(in.CacheTTL) + if err != nil { + response.Fatal(rsp, errors.Wrapf(err, "cannot set cacheTTL")) + return rsp, nil + } + rsp.Meta.Ttl = durationpb.New(dur) + } // Get the desired composed resources from the request. desiredComposed, err := request.GetDesiredComposedResources(req) diff --git a/fn_test.go b/fn_test.go index bd35bb3..a21c9bd 100644 --- a/fn_test.go +++ b/fn_test.go @@ -3,6 +3,7 @@ package main import ( "context" "testing" + "time" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/crossplane/function-sequencer/input/v1beta1" @@ -1769,3 +1770,101 @@ func TestRunFunction(t *testing.T) { }) } } + +func TestRunFunctionCacheTTL(t *testing.T) { + target := v1.Target_TARGET_COMPOSITE + xr := `{"apiVersion":"example.org/v1","kind":"XR","metadata":{"name":"cool-xr"},"spec":{"count":1}}` + + cases := map[string]struct { + reason string + input *v1beta1.Input + want *v1.RunFunctionResponse + }{ + "ValidCacheTTL": { + reason: "The function should override the response TTL when cacheTTL is provided", + input: &v1beta1.Input{ + CacheTTL: "5m", + Rules: []v1beta1.SequencingRule{ + {Sequence: []resource.Name{"first", "second"}}, + }, + }, + want: &v1.RunFunctionResponse{ + Meta: &v1.ResponseMeta{Ttl: durationpb.New(5 * time.Minute)}, + Results: []*v1.Result{ + { + Severity: v1.Severity_SEVERITY_NORMAL, + Message: "Delaying creation of resource(s) matching \"second\" because \"first\" does not exist yet", + Target: &target, + }, + }, + Desired: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{}, + }, + }, + }, + "InvalidCacheTTL": { + reason: "The function should return a fatal result when cacheTTL cannot be parsed", + input: &v1beta1.Input{ + CacheTTL: "5x", + Rules: []v1beta1.SequencingRule{ + {Sequence: []resource.Name{"first", "second"}}, + }, + }, + want: &v1.RunFunctionResponse{ + Meta: &v1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*v1.Result{ + { + Severity: v1.Severity_SEVERITY_FATAL, + Message: "cannot set cacheTTL: time: unknown unit \"x\" in duration \"5x\"", + Target: &target, + }, + }, + Desired: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{ + "second": { + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + f := &Function{log: logging.NewNopLogger()} + req := &v1.RunFunctionRequest{ + Input: resource.MustStructObject(tc.input), + Observed: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{}, + }, + Desired: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.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) + } + }) + } +} diff --git a/input/v1beta1/input.go b/input/v1beta1/input.go index 62482b9..c564cc2 100644 --- a/input/v1beta1/input.go +++ b/input/v1beta1/input.go @@ -41,6 +41,13 @@ type Input struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` + // CacheTTL sets the time-to-live for the Function response. + // Function response caching is an alpha feature in Crossplane and can + // change in future releases. + // +optional + // +kubebuilder:default:="1m" + CacheTTL string `json:"cacheTTL,omitempty"` + // EnableDeletionSequencing controls the automatic creation of Usage/ClusterUsage resources from the dependency tree // defined by the rule sequences. // +kubebuilder:object:default=false diff --git a/package/input/sequencer.fn.crossplane.io_inputs.yaml b/package/input/sequencer.fn.crossplane.io_inputs.yaml index 5f0149c..d81a079 100644 --- a/package/input/sequencer.fn.crossplane.io_inputs.yaml +++ b/package/input/sequencer.fn.crossplane.io_inputs.yaml @@ -28,6 +28,12 @@ spec: may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string + cacheTTL: + description: |- + CacheTTL sets the time-to-live for the Function response. + Function response caching is an alpha feature in Crossplane and can + change in future releases. + type: string enableDeletionSequencing: description: |- EnableDeletionSequencing controls the automatic creation of Usage/ClusterUsage resources from the dependency tree