From cc9eb925dc7733244f07d1526f91c4efe7278145 Mon Sep 17 00:00:00 2001 From: ezgidemirel Date: Fri, 27 Oct 2023 17:47:49 +0300 Subject: [PATCH] implement CompositeConnectionDetails and Ready state Signed-off-by: ezgidemirel --- examples/composition.yaml | 14 +++ fn.go | 107 ++++++++++++++--------- fn_test.go | 179 ++++++++++++++++++++++++++++++++++++-- go.mod | 2 +- template.go | 16 ++-- 5 files changed, 262 insertions(+), 56 deletions(-) diff --git a/examples/composition.yaml b/examples/composition.yaml index 235d04a..329f57d 100644 --- a/examples/composition.yaml +++ b/examples/composition.yaml @@ -51,6 +51,20 @@ spec: name: sample-access-key-secret-{{ $i }} namespace: crossplane-system {{- end }} + --- + apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1 + kind: CompositeConnectionDetails + metadata: + annotations: + crossplane.io/composition-resource-name: connection-details + name: connection-details + {{ if eq $.observed.resources nil }} + data: {} + {{ else }} + data: + username: {{ ( index $.observed.resources "sample-access-key-0" ).connectionDetails.username }} + password: {{ ( index $.observed.resources "sample-access-key-0" ).connectionDetails.password }} + {{ end }} - step: ready functionRef: name: function-auto-ready diff --git a/fn.go b/fn.go index ad414cf..2b1c0a5 100644 --- a/fn.go +++ b/fn.go @@ -3,10 +3,10 @@ package main import ( "bytes" "context" + "encoding/base64" "fmt" "io" - "golang.org/x/exp/maps" "google.golang.org/protobuf/encoding/protojson" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/util/json" @@ -19,9 +19,10 @@ import ( fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1" "github.com/crossplane/function-sdk-go/request" "github.com/crossplane/function-sdk-go/resource" + fn "github.com/crossplane/function-sdk-go/resource" "github.com/crossplane/function-sdk-go/response" - "github.com/crossplane/function-go-templating/input/v1beta1" + "github.com/crossplane-contrib/function-go-templating/input/v1beta1" ) // Function returns whatever response you ask it to. @@ -32,14 +33,19 @@ type Function struct { } const ( - invalidFunctionFmt = "invalid function input: %s" - wrongTempErr = "templates are required either inline or from filesystem path" - cannotGetErr = "cannot get the function cd" - cannotParseErr = "cannot parse the provided templates" + errFmtInvalidFunction = "invalid function input: %s" + errFmtInvalidReadyValue = "%s is invalid, ready annotation must be True, Unspecified, or False" + errFmtInvalidMetaType = "invalid meta kind %s" + + errCannotGet = "cannot get the function input" + errCannotParse = "cannot parse the provided templates" ) const ( - AnnotationKeyCompositionResourceName = "crossplane.io/composition-resource-name" + annotationKeyCompositionResourceName = "crossplane.io/composition-resource-name" + annotationKeyReady = "meta.gotemplating.fn.crossplane.io/ready" + + metaApiVersion = "meta.gotemplating.fn.crossplane.io/v1alpha1" ) // RunFunction runs the Function. @@ -50,24 +56,19 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ in := &v1beta1.Input{} if err := request.GetInput(req, in); err != nil { - response.Fatal(rsp, errors.Wrapf(err, "cannot get Function cd from %T", req)) - return rsp, nil - } - - if !isValidInputSource(in) { - response.Fatal(rsp, errors.New(fmt.Sprintf(invalidFunctionFmt, wrongTempErr))) + response.Fatal(rsp, errors.Wrapf(err, "cannot get Function input from %T", req)) return rsp, nil } tg, err := NewTemplateSourceGetter(in) if err != nil { - response.Fatal(rsp, errors.Wrap(err, fmt.Sprintf(invalidFunctionFmt, cannotGetErr))) + response.Fatal(rsp, errors.Wrap(err, fmt.Sprintf(errFmtInvalidFunction, errCannotGet))) return rsp, nil } - tmpl, err := GetNewTemplateWithFunctionMaps().Parse(tg.GetTemplate()) + tmpl, err := GetNewTemplateWithFunctionMaps().Parse(tg.GetTemplates()) if err != nil { - response.Fatal(rsp, errors.Wrap(err, fmt.Sprintf(invalidFunctionFmt, cannotParseErr))) + response.Fatal(rsp, errors.Wrap(err, fmt.Sprintf(errFmtInvalidFunction, errCannotParse))) return rsp, nil } @@ -112,37 +113,37 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ return rsp, nil } - // Convert the rendered manifests to a list of desired composed resources. + // Get the desired composed resources from the request. dcd, err := request.GetDesiredComposedResources(req) if err != nil { response.Fatal(rsp, errors.Wrap(err, "cannot get desired composed resources")) return rsp, nil } + // Convert the rendered manifests to a list of desired composed resources. for _, obj := range objs { cd := resource.NewDesiredComposed() cd.Resource.Unstructured = *obj.DeepCopy() - // update desired composite status only + // Update only the status of the desired composite resource. if cd.Resource.GetAPIVersion() == dxr.Resource.GetAPIVersion() && cd.Resource.GetKind() == dxr.Resource.GetKind() { - dst, err := dxr.Resource.GetStringObject("status") - - if err != nil && !fieldpath.IsNotFound(err) { + dst := make(map[string]any) + if err := dxr.Resource.GetValueInto("status", &dst); err != nil && !fieldpath.IsNotFound(err) { response.Fatal(rsp, errors.Wrap(err, "cannot get desired composite status")) return rsp, nil } - if fieldpath.IsNotFound(err) { - dst = make(map[string]string) - } - - src, err := cd.Resource.GetStringObject("status") - if err != nil && !fieldpath.IsNotFound(err) { + src := make(map[string]any) + if err := cd.Resource.GetValueInto("status", &src); err != nil && !fieldpath.IsNotFound(err) { response.Fatal(rsp, errors.Wrap(err, "cannot get desired composite status")) return rsp, nil } - maps.Copy(dst, src) + for k, v := range src { + fmt.Println(k, v) + dst[k] = v + } + if err := fieldpath.Pave(dxr.Resource.Object).SetValue("status", dst); err != nil { response.Fatal(rsp, errors.Wrap(err, "cannot set desired composite status")) return rsp, nil @@ -151,9 +152,42 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ continue } + // Set composite resource's connection details. + if cd.Resource.GetAPIVersion() == metaApiVersion { + switch obj.GetKind() { + case "CompositeConnectionDetails": + con, _ := cd.Resource.GetStringObject("data") + for k, v := range con { + d, _ := base64.StdEncoding.DecodeString(v) //nolint:errcheck // k8s returns secret values encoded + dxr.ConnectionDetails[k] = d + } + default: + response.Fatal(rsp, fmt.Errorf(errFmtInvalidMetaType, obj.GetKind())) + return rsp, nil + } + + continue + } + + // Set ready state. + if v, found := cd.Resource.GetAnnotations()[annotationKeyReady]; found { + if v != string(resource.ReadyTrue) && v != string(resource.ReadyUnspecified) && v != string(resource.ReadyFalse) { + response.Fatal(rsp, fmt.Errorf(fmt.Sprintf(errFmtInvalidFunction, errFmtInvalidReadyValue), v)) + return rsp, nil + } + + cd.Ready = fn.Ready(v) + + // remove meta annotation + ann := cd.Resource.GetAnnotations() + delete(ann, annotationKeyReady) + cd.Resource.SetAnnotations(ann) + } + + // Add resource to the desired composed resources map. name, err := getCompositionResourceName(obj) if err != nil { - response.Fatal(rsp, errors.Wrap(err, "cannot get composition resource name")) + response.Fatal(rsp, errors.Wrapf(err, "cannot get composition resource name of %s", obj.GetName())) return rsp, nil } @@ -192,21 +226,10 @@ func convertToMap(req *fnv1beta1.RunFunctionRequest) (map[string]interface{}, er return mReq, nil } -func isValidInputSource(in *v1beta1.Input) bool { - switch in.Source { - case v1beta1.InlineSource: - return in.Inline != nil - case v1beta1.FileSystemSource: - return in.FileSystem != nil - default: - return false - } -} - func getCompositionResourceName(obj *unstructured.Unstructured) (resource.Name, error) { - if v, found := obj.GetAnnotations()[AnnotationKeyCompositionResourceName]; found { + if v, found := obj.GetAnnotations()[annotationKeyCompositionResourceName]; found { return resource.Name(v), nil } - return "", errors.Errorf("%s annotation not found", AnnotationKeyCompositionResourceName) + return "", errors.Errorf("%s annotation not found", annotationKeyCompositionResourceName) } diff --git a/fn_test.go b/fn_test.go index 9da6962..e18a72a 100644 --- a/fn_test.go +++ b/fn_test.go @@ -16,7 +16,7 @@ import ( "github.com/crossplane/function-sdk-go/resource" "github.com/crossplane/function-sdk-go/response" - "github.com/crossplane/function-go-templating/input/v1beta1" + "github.com/crossplane-contrib/function-go-templating/input/v1beta1" ) var ( @@ -25,6 +25,11 @@ var ( cdWrongTmpl = `{"apiVersion":"example.org/v1","kind":"CD","metadata":{"name":"cool-cd","labels":{"belongsTo":{{.invalid-key}}}}}` cdMissingKind = `{"apiVersion":"example.org/v1"}` cdMissingResourceName = `{"apiVersion":"example.org/v1","kind":"CD","metadata":{"name":"cool-cd"}}` + cdWithReadyWrong = `{"apiVersion":"example.org/v1","kind":"CD","metadata":{"annotations":{"crossplane.io/composition-resource-name":"cool-cd","meta.gotemplating.fn.crossplane.io/ready":"wrongValue"},"name":"cool-cd"}}` + cdWithReadyTrue = `{"apiVersion":"example.org/v1","kind":"CD","metadata":{"annotations":{"crossplane.io/composition-resource-name":"cool-cd","meta.gotemplating.fn.crossplane.io/ready":"True"},"name":"cool-cd"}}` + + metaResourceInvalid = `{"apiVersion":"meta.gotemplating.fn.crossplane.io/v1alpha1","kind":"InvalidMeta"}` + metaResourceConDet = `{"apiVersion":"meta.gotemplating.fn.crossplane.io/v1alpha1","kind":"CompositeConnectionDetails","data":{"key":"dmFsdWU="}}` // encoded string "value" xr = `{"apiVersion":"example.org/v1","kind":"XR","metadata":{"name":"cool-xr"},"spec":{"count":2}}` xrWithStatus = `{"apiVersion":"example.org/v1","kind":"XR","metadata":{"name":"cool-xr"},"spec":{"count":2},"status":{"ready":"true"}}` @@ -64,7 +69,7 @@ func TestRunFunction(t *testing.T) { Results: []*fnv1beta1.Result{ { Severity: fnv1beta1.Severity_SEVERITY_FATAL, - Message: fmt.Sprintf(invalidFunctionFmt, wrongTempErr), + Message: "invalid function input: cannot get the function input: invalid input source: wrong", }, }, }, @@ -81,7 +86,7 @@ func TestRunFunction(t *testing.T) { Results: []*fnv1beta1.Result{ { Severity: fnv1beta1.Severity_SEVERITY_FATAL, - Message: fmt.Sprintf(invalidFunctionFmt, wrongTempErr), + Message: "invalid function input: cannot get the function input: invalid input source: ", }, }, }, @@ -104,7 +109,7 @@ func TestRunFunction(t *testing.T) { Results: []*fnv1beta1.Result{ { Severity: fnv1beta1.Severity_SEVERITY_FATAL, - Message: fmt.Sprintf("cannot get composition resource name: %s annotation not found", AnnotationKeyCompositionResourceName), + Message: fmt.Sprintf("cannot get composition resource name of cool-cd: %s annotation not found", annotationKeyCompositionResourceName), }, }, }, @@ -366,7 +371,171 @@ func TestRunFunction(t *testing.T) { Results: []*fnv1beta1.Result{ { Severity: fnv1beta1.Severity_SEVERITY_FATAL, - Message: "invalid function input: cannot get the function cd: cannot read tmpl from the folder {testdata/wrong}: lstat testdata/wrong: no such file or directory", + Message: "invalid function input: cannot get the function input: cannot read tmpl from the folder {testdata/wrong}: lstat testdata/wrong: no such file or directory", + }, + }, + }, + }, + }, + "ReadyStatusAnnotationNotValid": { + reason: "The Function should return a fatal result if the ready annotation is not valid.", + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Input: resource.MustStructObject( + &v1beta1.Input{ + Source: v1beta1.InlineSource, + Inline: &v1beta1.InputSourceInline{Template: cdWithReadyWrong}, + }), + Observed: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1beta1.Result{ + { + Severity: fnv1beta1.Severity_SEVERITY_FATAL, + Message: fmt.Sprintf(errFmtInvalidFunction, fmt.Sprintf(errFmtInvalidReadyValue, "wrongValue")), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + }, + "ReadyStatusAnnotation": { + reason: "The Function should return desired composed resource with True ready state.", + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Input: resource.MustStructObject( + &v1beta1.Input{ + Source: v1beta1.InlineSource, + Inline: &v1beta1.InputSourceInline{Template: cdWithReadyTrue}, + }), + Observed: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*fnv1beta1.Resource{ + "cool-cd": { + Resource: resource.MustStructJSON(cd), + }, + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1beta1.Result{ + { + Severity: fnv1beta1.Severity_SEVERITY_NORMAL, + Message: fmt.Sprintf("I was run with cd source %q", v1beta1.InlineSource), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*fnv1beta1.Resource{ + "cool-cd": { + Resource: resource.MustStructJSON(cd), + Ready: 1, + }, + }, + }, + }, + }, + }, + "InvalidMetaKind": { + reason: "The Function should return a fatal result if the meta kind is invalid.", + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Input: resource.MustStructObject( + &v1beta1.Input{ + Source: v1beta1.InlineSource, + Inline: &v1beta1.InputSourceInline{Template: metaResourceInvalid}, + }), + Observed: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1beta1.Result{ + { + Severity: fnv1beta1.Severity_SEVERITY_FATAL, + Message: fmt.Sprintf(errFmtInvalidMetaType, "InvalidMeta"), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + }, + "CompositeConnectionDetails": { + reason: "The Function should return the desired composite with CompositeConnectionDetails.", + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Input: resource.MustStructObject( + &v1beta1.Input{ + Source: v1beta1.InlineSource, + Inline: &v1beta1.InputSourceInline{Template: metaResourceConDet}, + }), + Observed: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1beta1.Result{ + { + Severity: fnv1beta1.Severity_SEVERITY_NORMAL, + Message: fmt.Sprintf("I was run with cd source %q", v1beta1.InlineSource), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + ConnectionDetails: map[string][]byte{"key": []byte("value")}, }, }, }, diff --git a/go.mod b/go.mod index c708fb5..f44e908 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/crossplane/function-go-templating +module github.com/crossplane-contrib/function-go-templating go 1.20 diff --git a/template.go b/template.go index 7b6a326..b16d0cd 100644 --- a/template.go +++ b/template.go @@ -6,15 +6,15 @@ import ( "github.com/crossplane/crossplane-runtime/pkg/errors" - "github.com/crossplane/function-go-templating/input/v1beta1" + "github.com/crossplane-contrib/function-go-templating/input/v1beta1" ) const dotCharacter = 46 // TemplateGetter interface is used to read templates from different sources type TemplateGetter interface { - // GetTemplate returns the templates from the datasource - GetTemplate() string + // GetTemplates returns the templates from the datasource + GetTemplates() string } // NewTemplateSourceGetter returns a TemplateGetter based on the cd source @@ -25,7 +25,7 @@ func NewTemplateSourceGetter(in *v1beta1.Input) (TemplateGetter, error) { case v1beta1.FileSystemSource: return newFileSource(in) default: - return nil, errors.Errorf("invalid cd source: %s", in.Source) + return nil, errors.Errorf("invalid input source: %s", in.Source) } } @@ -40,8 +40,8 @@ type FileSource struct { Template string } -// GetTemplate returns the inline template -func (is *InlineSource) GetTemplate() string { +// GetTemplates returns the inline template +func (is *InlineSource) GetTemplates() string { return is.Template } @@ -51,8 +51,8 @@ func newInlineSource(in *v1beta1.Input) (*InlineSource, error) { }, nil } -// GetTemplate returns the templates in the folder -func (fs *FileSource) GetTemplate() string { +// GetTemplates returns the templates in the folder +func (fs *FileSource) GetTemplates() string { return fs.Template }