@@ -10,7 +10,9 @@ import (
10
10
"google.golang.org/protobuf/testing/protocmp"
11
11
"google.golang.org/protobuf/types/known/durationpb"
12
12
"google.golang.org/protobuf/types/known/structpb"
13
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
13
14
"k8s.io/apimachinery/pkg/runtime"
15
+ "k8s.io/apimachinery/pkg/runtime/schema"
14
16
"k8s.io/utils/ptr"
15
17
16
18
"github.com/crossplane/crossplane-runtime/pkg/logging"
@@ -643,6 +645,279 @@ func TestRunFunction(t *testing.T) {
643
645
},
644
646
},
645
647
},
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
+ })}}},
646
921
}
647
922
648
923
for name , tc := range cases {
@@ -660,3 +935,24 @@ func TestRunFunction(t *testing.T) {
660
935
})
661
936
}
662
937
}
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