Skip to content

Commit 1160364

Browse files
committed
tests: render with environment
Signed-off-by: Philippe Scorsolini <p.scorsolini@gmail.com>
1 parent 74aefe6 commit 1160364

File tree

1 file changed

+296
-0
lines changed

1 file changed

+296
-0
lines changed

fn_test.go

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import (
1010
"google.golang.org/protobuf/testing/protocmp"
1111
"google.golang.org/protobuf/types/known/durationpb"
1212
"google.golang.org/protobuf/types/known/structpb"
13+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1314
"k8s.io/apimachinery/pkg/runtime"
15+
"k8s.io/apimachinery/pkg/runtime/schema"
1416
"k8s.io/utils/ptr"
1517

1618
"github.com/crossplane/crossplane-runtime/pkg/logging"
@@ -643,6 +645,279 @@ func TestRunFunction(t *testing.T) {
643645
},
644646
},
645647
},
648+
"PatchToCompositeWithEnvironmentPatches": {
649+
reason: "A basic ToCompositeFieldPath patch should work with environment.patches.",
650+
args: args{
651+
req: &fnv1beta1.RunFunctionRequest{
652+
Input: resource.MustStructObject(&v1beta1.Resources{
653+
Resources: []v1beta1.ComposedTemplate{
654+
{
655+
Name: "cool-resource",
656+
Base: &runtime.RawExtension{Raw: []byte(`{"apiVersion":"example.org/v1","kind":"CD"}`)},
657+
}},
658+
Environment: &v1beta1.Environment{
659+
Patches: []v1beta1.EnvironmentPatch{
660+
{
661+
Type: v1beta1.PatchTypeFromEnvironmentFieldPath,
662+
Patch: v1beta1.Patch{
663+
FromFieldPath: ptr.To[string]("data.widgets"),
664+
ToFieldPath: ptr.To[string]("spec.watchers"),
665+
Transforms: []v1beta1.Transform{
666+
{
667+
Type: v1beta1.TransformTypeConvert,
668+
Convert: &v1beta1.ConvertTransform{
669+
ToType: v1beta1.TransformIOTypeInt64,
670+
},
671+
},
672+
{
673+
Type: v1beta1.TransformTypeMath,
674+
Math: &v1beta1.MathTransform{
675+
Type: v1beta1.MathTransformTypeMultiply,
676+
Multiply: ptr.To[int64](3),
677+
}}}}}}}}),
678+
Observed: &fnv1beta1.State{
679+
Composite: &fnv1beta1.Resource{
680+
Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD","spec":{}}`),
681+
},
682+
Resources: map[string]*fnv1beta1.Resource{},
683+
},
684+
Context: contextWithEnvironment(map[string]interface{}{
685+
"widgets": "10",
686+
})},
687+
},
688+
want: want{
689+
rsp: &fnv1beta1.RunFunctionResponse{
690+
Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)},
691+
Desired: &fnv1beta1.State{
692+
Composite: &fnv1beta1.Resource{
693+
// spec.watchers = 10 * 3 = 30
694+
Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD","spec":{"watchers":30}}`),
695+
},
696+
Resources: map[string]*fnv1beta1.Resource{
697+
"cool-resource": {
698+
Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD"}`),
699+
}}},
700+
Context: contextWithEnvironment(map[string]interface{}{
701+
"widgets": "10",
702+
})}}},
703+
"EnvironmentPatchToEnvironment": {
704+
reason: "A basic ToEnvironment patch should work with environment.patches.",
705+
args: args{
706+
req: &fnv1beta1.RunFunctionRequest{
707+
Input: resource.MustStructObject(&v1beta1.Resources{
708+
Resources: []v1beta1.ComposedTemplate{
709+
{
710+
Name: "cool-resource",
711+
Base: &runtime.RawExtension{Raw: []byte(`{"apiVersion":"example.org/v1","kind":"CD"}`)},
712+
}},
713+
Environment: &v1beta1.Environment{
714+
Patches: []v1beta1.EnvironmentPatch{
715+
{
716+
Type: v1beta1.PatchTypeToEnvironmentFieldPath,
717+
Patch: v1beta1.Patch{
718+
FromFieldPath: ptr.To[string]("spec.watchers"),
719+
ToFieldPath: ptr.To[string]("data.widgets"),
720+
Transforms: []v1beta1.Transform{
721+
{
722+
Type: v1beta1.TransformTypeMath,
723+
Math: &v1beta1.MathTransform{
724+
Type: v1beta1.MathTransformTypeMultiply,
725+
Multiply: ptr.To[int64](3),
726+
},
727+
},
728+
{
729+
Type: v1beta1.TransformTypeConvert,
730+
Convert: &v1beta1.ConvertTransform{
731+
ToType: v1beta1.TransformIOTypeString,
732+
},
733+
}}}}}}}),
734+
Observed: &fnv1beta1.State{
735+
Composite: &fnv1beta1.Resource{
736+
Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD","spec":{"watchers":10}}`),
737+
},
738+
Resources: map[string]*fnv1beta1.Resource{},
739+
},
740+
Context: contextWithEnvironment(nil)},
741+
},
742+
want: want{
743+
rsp: &fnv1beta1.RunFunctionResponse{
744+
Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)},
745+
Desired: &fnv1beta1.State{
746+
Composite: &fnv1beta1.Resource{
747+
Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD"}`),
748+
},
749+
Resources: map[string]*fnv1beta1.Resource{
750+
"cool-resource": {
751+
Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD"}`),
752+
}}},
753+
Context: contextWithEnvironment(map[string]interface{}{
754+
"widgets": "30",
755+
})}}},
756+
"PatchComposedResourceFromEnvironment": {
757+
reason: "A basic FromEnvironmentPatch should work if defined at spec.resources[*].patches.",
758+
args: args{
759+
req: &fnv1beta1.RunFunctionRequest{
760+
Input: resource.MustStructObject(&v1beta1.Resources{
761+
Resources: []v1beta1.ComposedTemplate{{
762+
Name: "cool-resource",
763+
Base: &runtime.RawExtension{Raw: []byte(`{"apiVersion":"example.org/v1","kind":"CD"}`)},
764+
Patches: []v1beta1.ComposedPatch{{
765+
Type: v1beta1.PatchTypeFromEnvironmentFieldPath,
766+
Patch: v1beta1.Patch{
767+
FromFieldPath: ptr.To[string]("data.widgets"),
768+
ToFieldPath: ptr.To[string]("spec.watchers"),
769+
Transforms: []v1beta1.Transform{{
770+
Type: v1beta1.TransformTypeConvert,
771+
Convert: &v1beta1.ConvertTransform{
772+
ToType: v1beta1.TransformIOTypeInt64,
773+
},
774+
}, {
775+
Type: v1beta1.TransformTypeMath,
776+
Math: &v1beta1.MathTransform{
777+
Type: v1beta1.MathTransformTypeMultiply,
778+
Multiply: ptr.To[int64](3),
779+
},
780+
}}}}},
781+
}}}),
782+
Observed: &fnv1beta1.State{
783+
Composite: &fnv1beta1.Resource{
784+
Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD","spec":{}}`),
785+
},
786+
Resources: map[string]*fnv1beta1.Resource{},
787+
},
788+
Context: contextWithEnvironment(map[string]interface{}{
789+
"widgets": "10",
790+
})},
791+
},
792+
want: want{
793+
rsp: &fnv1beta1.RunFunctionResponse{
794+
Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)},
795+
Desired: &fnv1beta1.State{
796+
Composite: &fnv1beta1.Resource{
797+
Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD"}`),
798+
},
799+
Resources: map[string]*fnv1beta1.Resource{
800+
"cool-resource": {
801+
// spec.watchers = 10 * 3 = 30
802+
Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD","spec":{"watchers":30}}`),
803+
}}},
804+
Context: contextWithEnvironment(map[string]interface{}{
805+
"widgets": "10",
806+
})}}},
807+
808+
"PatchComposedResourceFromEnvironmentShadowedNotSet": {
809+
reason: "A basic FromEnvironmentPatch should work if defined at spec.resources[*].patches, even if a successive patch shadows it and its source is not set.",
810+
args: args{
811+
req: &fnv1beta1.RunFunctionRequest{
812+
Input: resource.MustStructObject(&v1beta1.Resources{
813+
Resources: []v1beta1.ComposedTemplate{{
814+
Name: "cool-resource",
815+
Base: &runtime.RawExtension{Raw: []byte(`{"apiVersion":"example.org/v1","kind":"CD"}`)},
816+
Patches: []v1beta1.ComposedPatch{{
817+
Type: v1beta1.PatchTypeFromEnvironmentFieldPath,
818+
Patch: v1beta1.Patch{
819+
FromFieldPath: ptr.To[string]("data.widgets"),
820+
ToFieldPath: ptr.To[string]("spec.watchers"),
821+
Transforms: []v1beta1.Transform{{
822+
Type: v1beta1.TransformTypeConvert,
823+
Convert: &v1beta1.ConvertTransform{
824+
ToType: v1beta1.TransformIOTypeInt64,
825+
},
826+
}, {
827+
Type: v1beta1.TransformTypeMath,
828+
Math: &v1beta1.MathTransform{
829+
Type: v1beta1.MathTransformTypeMultiply,
830+
Multiply: ptr.To[int64](3),
831+
},
832+
}}}},
833+
{
834+
Type: v1beta1.PatchTypeFromCompositeFieldPath,
835+
Patch: v1beta1.Patch{
836+
FromFieldPath: ptr.To[string]("spec.watchers"),
837+
ToFieldPath: ptr.To[string]("spec.watchers"),
838+
}}}}}}),
839+
Observed: &fnv1beta1.State{
840+
Composite: &fnv1beta1.Resource{
841+
Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD","spec":{}}`),
842+
},
843+
Resources: map[string]*fnv1beta1.Resource{},
844+
},
845+
Context: contextWithEnvironment(map[string]interface{}{
846+
"widgets": "10",
847+
})},
848+
},
849+
want: want{
850+
rsp: &fnv1beta1.RunFunctionResponse{
851+
Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)},
852+
Desired: &fnv1beta1.State{
853+
Composite: &fnv1beta1.Resource{
854+
Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD"}`),
855+
},
856+
Resources: map[string]*fnv1beta1.Resource{
857+
"cool-resource": {
858+
// spec.watchers = 10 * 3 = 30
859+
Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD","spec":{"watchers":30}}`),
860+
}}},
861+
Context: contextWithEnvironment(map[string]interface{}{
862+
"widgets": "10",
863+
})}}},
864+
"PatchComposedResourceFromEnvironmentShadowedSet": {
865+
reason: "A basic FromEnvironmentPatch should work if defined at spec.resources[*].patches, even if a successive patch shadows it and its source is set.",
866+
args: args{
867+
req: &fnv1beta1.RunFunctionRequest{
868+
Input: resource.MustStructObject(&v1beta1.Resources{
869+
Resources: []v1beta1.ComposedTemplate{{
870+
Name: "cool-resource",
871+
Base: &runtime.RawExtension{Raw: []byte(`{"apiVersion":"example.org/v1","kind":"CD"}`)},
872+
Patches: []v1beta1.ComposedPatch{{
873+
Type: v1beta1.PatchTypeFromEnvironmentFieldPath,
874+
Patch: v1beta1.Patch{
875+
FromFieldPath: ptr.To[string]("data.widgets"),
876+
ToFieldPath: ptr.To[string]("spec.watchers"),
877+
Transforms: []v1beta1.Transform{{
878+
Type: v1beta1.TransformTypeConvert,
879+
Convert: &v1beta1.ConvertTransform{
880+
ToType: v1beta1.TransformIOTypeInt64,
881+
},
882+
}, {
883+
Type: v1beta1.TransformTypeMath,
884+
Math: &v1beta1.MathTransform{
885+
Type: v1beta1.MathTransformTypeMultiply,
886+
Multiply: ptr.To[int64](3),
887+
},
888+
}}}},
889+
{
890+
Type: v1beta1.PatchTypeFromCompositeFieldPath,
891+
Patch: v1beta1.Patch{
892+
FromFieldPath: ptr.To[string]("spec.watchers"),
893+
ToFieldPath: ptr.To[string]("spec.watchers"),
894+
}}}}}}),
895+
Observed: &fnv1beta1.State{
896+
Composite: &fnv1beta1.Resource{
897+
// I want this in the environment, 42
898+
Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD","spec":{"watchers":42}}`),
899+
},
900+
Resources: map[string]*fnv1beta1.Resource{},
901+
},
902+
Context: contextWithEnvironment(map[string]interface{}{
903+
"widgets": "10",
904+
})},
905+
},
906+
want: want{
907+
rsp: &fnv1beta1.RunFunctionResponse{
908+
Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)},
909+
Desired: &fnv1beta1.State{
910+
Composite: &fnv1beta1.Resource{
911+
Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD"}`),
912+
},
913+
Resources: map[string]*fnv1beta1.Resource{
914+
"cool-resource": {
915+
// spec.watchers comes from the composite resource, 42
916+
Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD","spec":{"watchers":42}}`),
917+
}}},
918+
Context: contextWithEnvironment(map[string]interface{}{
919+
"widgets": "10",
920+
})}}},
646921
}
647922

648923
for name, tc := range cases {
@@ -660,3 +935,24 @@ func TestRunFunction(t *testing.T) {
660935
})
661936
}
662937
}
938+
939+
// Crossplane sends as context a fake resource:
940+
// { "apiVersion": "internal.crossplane.io/v1alpha1", "kind": "Environment", "data": {... the actual environment content ...} }
941+
// See: https://github.com/crossplane/crossplane/blob/806f0d20d146f6f4f1735c5ec6a7dc78923814b3/internal/controller/apiextensions/composite/environment_fetcher.go#L85C1-L85C1
942+
// That's because the patching code expects a resource to be able to use
943+
// runtime.DefaultUnstructuredConverter.FromUnstructured to convert it back to
944+
// an object. This is also why all patches need to specify the full path from data.
945+
func contextWithEnvironment(data map[string]interface{}) *structpb.Struct {
946+
if data == nil {
947+
data = map[string]interface{}{}
948+
}
949+
u := unstructured.Unstructured{Object: map[string]interface{}{
950+
"data": data,
951+
}}
952+
u.SetGroupVersionKind(schema.GroupVersionKind{Group: "internal.crossplane.io", Version: "v1alpha1", Kind: "Environment"})
953+
d, err := structpb.NewStruct(u.UnstructuredContent())
954+
if err != nil {
955+
panic(err)
956+
}
957+
return &structpb.Struct{Fields: map[string]*structpb.Value{fncontext.KeyEnvironment: structpb.NewStructValue(d)}}
958+
}

0 commit comments

Comments
 (0)