From 671614ddf0ae0b0066c3e220685a9ac12e835749 Mon Sep 17 00:00:00 2001 From: Bob Haddleton Date: Wed, 24 Jun 2026 16:03:20 -0500 Subject: [PATCH] Add ttl CLI parameter and function input value Signed-off-by: Bob Haddleton --- fn.go | 14 +++++++- fn_test.go | 36 ++++++++++++++++++- input/v1beta1/resources.go | 4 +++ main.go | 21 +++++++---- .../input/pt.fn.crossplane.io_resources.yaml | 4 +++ 5 files changed, 71 insertions(+), 8 deletions(-) diff --git a/fn.go b/fn.go index ec678c6..50b4883 100644 --- a/fn.go +++ b/fn.go @@ -4,12 +4,14 @@ import ( "context" "fmt" "maps" + "time" "github.com/crossplane-contrib/function-patch-and-transform/input/v1beta1" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/fieldpath" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" + "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/structpb" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/util/json" @@ -27,6 +29,7 @@ type Function struct { fnv1.UnimplementedFunctionRunnerServiceServer log logging.Logger + ttl time.Duration } // RunFunction runs the Function. @@ -37,7 +40,7 @@ func (f *Function) RunFunction(ctx context.Context, req *fnv1.RunFunctionRequest log.Debug("Running Function") // TODO(negz): We can probably use a longer TTL if all resources are ready. - rsp := response.To(req, response.DefaultTTL) + rsp := response.To(req, f.ttl) input := &v1beta1.Resources{} if err := request.GetInput(req, input); err != nil { @@ -45,6 +48,15 @@ func (f *Function) RunFunction(ctx context.Context, req *fnv1.RunFunctionRequest return rsp, nil } + if input.TTL != "" { + dur, err := time.ParseDuration(input.TTL) + if err != nil { + response.Fatal(rsp, errors.Wrapf(err, "cannot set ttl")) + return rsp, nil + } + rsp.Meta.Ttl = durationpb.New(dur) + } + // Our input is an opaque object nested in a Composition, so unfortunately // it won't handle validation for us. if err := ValidateResources(input); err != nil { diff --git a/fn_test.go b/fn_test.go index 768939a..61df98e 100644 --- a/fn_test.go +++ b/fn_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "testing" + "time" "github.com/crossplane-contrib/function-patch-and-transform/input/v1beta1" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" @@ -1535,7 +1536,7 @@ func TestRunFunction(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { - f := &Function{log: logging.NewNopLogger()} + 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 != "" { @@ -1549,6 +1550,39 @@ func TestRunFunction(t *testing.T) { } } +func TestRunFunctionCacheTTL(t *testing.T) { + f := &Function{log: logging.NewNopLogger(), ttl: response.DefaultTTL} + rsp, err := f.RunFunction(context.Background(), &fnv1.RunFunctionRequest{ + Input: resource.MustStructObject(&v1beta1.Resources{ + TTL: "5m", + Resources: []v1beta1.ComposedTemplate{ + { + Name: "cool-resource", + Base: &runtime.RawExtension{Raw: []byte(`{"apiVersion":"example.org/v1","kind":"CD"}`)}, + }, + }, + }), + Observed: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"XR"}`), + }, + }, + Desired: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"XR"}`), + }, + }, + }) + if err != nil { + t.Fatalf("RunFunction(...): %v", err) + } + + want := durationpb.New(5 * time.Minute) + if diff := cmp.Diff(want, rsp.GetMeta().GetTtl(), protocmp.Transform()); diff != "" { + t.Errorf("RunFunction(...): -want TTL, +got TTL:\n%s", diff) + } +} + // Crossplane sends as context a fake resource: // { "apiVersion": "internal.crossplane.io/v1alpha1", "kind": "Environment", ... the actual environment content ... } // See: https://github.com/crossplane/crossplane/blob/806f0d20d146f6f4f1735c5ec6a7dc78923814b3/internal/controller/apiextensions/composite/environment_fetcher.go#L85C1-L85C1 diff --git a/input/v1beta1/resources.go b/input/v1beta1/resources.go index 1da06be..f8cbc49 100644 --- a/input/v1beta1/resources.go +++ b/input/v1beta1/resources.go @@ -36,6 +36,10 @@ type Resources struct { // composite resource is created. Resources []ComposedTemplate `json:"resources"` + // TTL for which a response can be cached in time.Duration format. Default "1m". + // +optional + TTL string `json:"ttl,omitempty"` + // WriteConnectionSecretToRef specifies the name and namespace of a Secret // to which any connection details for this composite resource should be // written. This field is only used for Crossplane v2 composite resources. diff --git a/main.go b/main.go index 4a56689..ba7109e 100644 --- a/main.go +++ b/main.go @@ -2,20 +2,24 @@ package main import ( + "time" + "github.com/alecthomas/kong" "github.com/crossplane/function-sdk-go" + "github.com/crossplane/function-sdk-go/response" ) // CLI of this Function. type CLI struct { Debug bool `help:"Emit debug logs in addition to info logs." short:"d"` - Network string `default:"tcp" help:"Network on which to listen for gRPC connections."` - Address string `default:":9443" help:"Address at which to listen for gRPC connections."` - TLSCertsDir string `env:"TLS_SERVER_CERTS_DIR" help:"Directory containing server certs (tls.key, tls.crt) and the CA used to verify client certificates (ca.crt)"` - Insecure bool `help:"Run without mTLS credentials. If you supply this flag --tls-server-certs-dir will be ignored."` - MaxRecvMessageSize int `default:"4" help:"Maximum size of received messages in MB."` + Network string `default:"tcp" help:"Network on which to listen for gRPC connections."` + Address string `default:":9443" help:"Address at which to listen for gRPC connections."` + TLSCertsDir string `env:"TLS_SERVER_CERTS_DIR" help:"Directory containing server certs (tls.key, tls.crt) and the CA used to verify client certificates (ca.crt)"` + Insecure bool `help:"Run without mTLS credentials. If you supply this flag --tls-server-certs-dir will be ignored."` + MaxRecvMessageSize int `default:"4" help:"Maximum size of received messages in MB."` + TTL *time.Duration `default:"1m" help:"Time to live for function response."` } // Run this Function. @@ -25,7 +29,12 @@ func (c *CLI) Run() error { return err } - return function.Serve(&Function{log: log}, + ttl := response.DefaultTTL + if c.TTL != nil { + ttl = *c.TTL + } + + return function.Serve(&Function{log: log, ttl: ttl}, function.Listen(c.Network, c.Address), function.MTLSCertificates(c.TLSCertsDir), function.Insecure(c.Insecure), diff --git a/package/input/pt.fn.crossplane.io_resources.yaml b/package/input/pt.fn.crossplane.io_resources.yaml index b6d280a..d1ac5b8 100644 --- a/package/input/pt.fn.crossplane.io_resources.yaml +++ b/package/input/pt.fn.crossplane.io_resources.yaml @@ -1286,6 +1286,10 @@ spec: - name type: object type: array + ttl: + description: TTL for which a response can be cached in time.Duration format. + Default "1m". + type: string writeConnectionSecretToRef: description: |- WriteConnectionSecretToRef specifies the name and namespace of a Secret