Skip to content
Merged
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
26 changes: 24 additions & 2 deletions fn.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import (
"path/filepath"
"strings"
"text/template"
"time"

"dario.cat/mergo"
"github.com/crossplane-contrib/function-go-templating/input/v1beta1"
"github.com/crossplane/crossplane-runtime/v2/pkg/fieldpath"
"github.com/crossplane/crossplane-runtime/v2/pkg/meta"
"google.golang.org/protobuf/encoding/protojson"
"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"
Expand Down Expand Up @@ -44,6 +46,7 @@ type Function struct {

log logging.Logger
fsys fs.FS
ttl time.Duration
defaultSource string
defaultOptions string
}
Expand All @@ -58,17 +61,18 @@ type YamlErrorContext struct {
const (
annotationKeyCompositionResourceName = "gotemplating.fn.crossplane.io/composition-resource-name"
annotationKeyReady = "gotemplating.fn.crossplane.io/ready"
annotationKeyTTL = "gotemplating.fn.crossplane.io/ttl"

metaAPIVersion = "meta.gotemplating.fn.crossplane.io/v1alpha1"
)

// RunFunction runs the Function.
func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) (*fnv1.RunFunctionResponse, error) { //nolint:gocognit // this function needs to be refactored
f.log.Debug("Running Function", "tag", req.GetMeta().GetTag())
in := &v1beta1.GoTemplate{}

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

in := &v1beta1.GoTemplate{}
if err := request.GetInput(req, in); err != nil {
response.Fatal(rsp, errors.Wrapf(err, "cannot get Function input from %T", req))
return rsp, nil
Expand All @@ -80,6 +84,13 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest)
}
}

if in.TTL != "" {
ttlDuration, err := time.ParseDuration(in.TTL)
if err == nil {
rsp.Meta.Ttl = durationpb.New(ttlDuration)
}
}

tg, err := NewTemplateSourceGetter(f.fsys, req.GetContext(), in)
if err != nil {
response.Fatal(rsp, errors.Wrap(err, "invalid function input"))
Expand Down Expand Up @@ -202,6 +213,17 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest)
// Initialize the requirements.
requirements := &fnv1.Requirements{ExtraResources: make(map[string]*fnv1.ResourceSelector), Resources: make(map[string]*fnv1.ResourceSelector)}

// Override the TTL if specified in the observed composite.
if v, found := observedComposite.Resource.GetAnnotations()[annotationKeyTTL]; found {
t, err := time.ParseDuration(v)
if err != nil {
f.log.Debug("Ignoring Ttl annotation, wrong format", v)
}
rsp.Meta.Ttl = durationpb.New(t)
// Remove meta annotation.
meta.RemoveAnnotations(observedComposite.Resource, annotationKeyTTL)
}

// Convert the rendered manifests to a list of desired composed resources.
for _, obj := range objs {
cd := resource.NewDesiredComposed()
Expand Down
81 changes: 81 additions & 0 deletions fn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"embed"
"fmt"
"testing"
"time"

"github.com/crossplane-contrib/function-go-templating/input/v1beta1"
"github.com/crossplane/crossplane-runtime/v2/pkg/logging"
Expand Down Expand Up @@ -55,6 +56,7 @@ metadata:
xrWithReadyTrueAndResourceName = `{"apiVersion":"example.org/v1","kind":"XR","metadata":{"annotations":{"gotemplating.fn.crossplane.io/composition-resource-name":"xr-as-composed","gotemplating.fn.crossplane.io/ready":"True"},"name":"cool-xr"},"spec":{"count":2}}`
xrWithReadyTrueAndStatus = `{"apiVersion":"example.org/v1","kind":"XR","metadata":{"annotations":{"gotemplating.fn.crossplane.io/ready":"True"},"name":"cool-xr"},"spec":{"count":2},"status":{"phase":"Ready","message":"Composite resource is ready"}}`
xrWithStatusUpdate = `{"apiVersion":"example.org/v1","kind":"XR","metadata":{"annotations":{"gotemplating.fn.crossplane.io/ready":"False"},"name":"cool-xr"},"spec":{"count":2},"status":{"phase":"Updating","newField":"added"}}`
xrWithTTL = `{"apiVersion":"example.org/v1","kind":"XR","metadata":{"annotations":{"gotemplating.fn.crossplane.io/ttl": "5m"},"name":"cool-xr"},"spec":{"count":2}}`

claimConditions = `{"apiVersion":"meta.gotemplating.fn.crossplane.io/v1alpha1","kind":"ClaimConditions","conditions":[{"type":"TestCondition","status":"False","reason":"InstallFail","message":"failed to install","target":"ClaimAndComposite"},{"type":"ConditionTrue","status":"True","reason":"this condition is true","message":"we are true","target":"Composite"},{"type":"DatabaseReady","status":"True","reason":"Ready","message":"Database is ready"}]}`
claimConditionsReservedKey = `{"apiVersion":"meta.gotemplating.fn.crossplane.io/v1alpha1","kind":"ClaimConditions","conditions":[{"type":"Ready","status":"False","reason":"InstallFail","message":"I am using a reserved Condition","target":"ClaimAndComposite"}]}`
Expand Down Expand Up @@ -1887,13 +1889,92 @@ func TestRunFunction(t *testing.T) {
},
},
},
"CustomInputTtl": {
reason: "The Function should use a custom TTL when instructed.",
args: args{
req: &fnv1.RunFunctionRequest{
Meta: &fnv1.RequestMeta{Tag: "templates"},
Input: resource.MustStructObject(
&v1beta1.GoTemplate{
Source: v1beta1.InlineSource,
Inline: &v1beta1.TemplateSourceInline{Template: cdTmpl},
TTL: "20m",
}),
Observed: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(xr),
},
},
Desired: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(xr),
},
},
},
},
want: want{
rsp: &fnv1.RunFunctionResponse{
Meta: &fnv1.ResponseMeta{Tag: "templates", Ttl: durationpb.New(20 * time.Minute)},
Desired: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(xr),
},
Resources: map[string]*fnv1.Resource{
"cool-cd": {
Resource: resource.MustStructJSON(`{"apiVersion": "example.org/v1","kind":"CD","metadata":{"annotations":{},"name":"cool-cd","labels":{"belongsTo":"cool-xr"}}}`),
},
},
},
},
},
},
"CustomAnnotationTtl": {
reason: "The Function should use a custom TTL when given in the XR annotation.",
args: args{
req: &fnv1.RunFunctionRequest{
Meta: &fnv1.RequestMeta{Tag: "templates"},
Input: resource.MustStructObject(
&v1beta1.GoTemplate{
Source: v1beta1.InlineSource,
Inline: &v1beta1.TemplateSourceInline{Template: cdTmpl},
TTL: "20m", // this should be overridden by the annotation
}),
Observed: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(xrWithTTL),
},
},
Desired: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(xrWithTTL),
},
},
},
},
want: want{
rsp: &fnv1.RunFunctionResponse{
Meta: &fnv1.ResponseMeta{Tag: "templates", Ttl: durationpb.New(5 * time.Minute)},
Desired: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(xrWithTTL),
},
Resources: map[string]*fnv1.Resource{
"cool-cd": {
Resource: resource.MustStructJSON(`{"apiVersion": "example.org/v1","kind":"CD","metadata":{"annotations":{},"name":"cool-cd","labels":{"belongsTo":"cool-xr"}}}`),
},
},
},
},
},
},
}

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
f := &Function{
log: logging.NewNopLogger(),
fsys: testdataFS,
ttl: response.DefaultTTL,
defaultSource: tc.args.defaultSource,
defaultOptions: tc.args.defaultOptions,
}
Expand Down
4 changes: 4 additions & 0 deletions input/v1beta1/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ type GoTemplate struct {
Inline *TemplateSourceInline `json:"inline,omitempty"`
// FileSystem is the folder path where the templates are located
FileSystem *TemplateSourceFileSystem `json:"fileSystem,omitempty"`
// TTL for which a response can be cached in time.Duration format
// +kubebuilder:default="1m0s"
// +optional
TTL string `json:"ttl"`
// Environment is the key that defines the location of the templates in the environment
Environment *TemplateSourceEnvironment `json:"environment,omitempty"`
// Options to set for the template engine. Valid options are documented at https://pkg.go.dev/text/template#Template.Option
Expand Down
16 changes: 15 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
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.
Expand All @@ -15,6 +18,7 @@ type CLI struct {
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."`
TTL string `default:"${defaultTTL}" help:"Function global setting for response TTL."`
MaxRecvMessageSize int `default:"4" help:"Maximum size of received messages in MB."`
DefaultSource string `default:"" env:"FUNCTION_GO_TEMPLATING_DEFAULT_SOURCE" help:"Default template source to use when input is not provided to the function."`
DefaultOptions string `default:"" env:"FUNCTION_GO_TEMPLATING_DEFAULT_OPTIONS" help:"Comma-separated default template options to use when input is not provided to the function."`
Expand All @@ -27,12 +31,18 @@ func (c *CLI) Run() error {
return err
}

ttl, err := time.ParseDuration(c.TTL)
if err != nil {
return err
}

return function.Serve(
&Function{
log: log,
fsys: &osFS{},
defaultSource: c.DefaultSource,
defaultOptions: c.DefaultOptions,
ttl: ttl,
},
function.Listen(c.Network, c.Address),
function.MTLSCertificates(c.TLSCertsDir),
Expand All @@ -41,6 +51,10 @@ func (c *CLI) Run() error {
}

func main() {
ctx := kong.Parse(&CLI{}, kong.Description("A Crossplane Composition Function."))
ctx := kong.Parse(
&CLI{},
kong.Description("A Crossplane Composition Function."),
kong.Vars{"defaultTTL": response.DefaultTTL.String()},
)
ctx.FatalIfErrorf(ctx.Run())
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ spec:
description: Source specifies the different types of input sources that
can be used with this function
type: string
ttl:
default: 1m0s
description: TTL for which a response can be cached in time.Duration format
type: string
required:
- source
type: object
Expand Down
Loading