@@ -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