@@ -2,6 +2,7 @@ package main
22
33import (
44 "context"
5+ "strings"
56 "testing"
67
78 "github.com/crossplane/crossplane-runtime/v2/pkg/fieldpath"
@@ -160,7 +161,7 @@ func TestRunFunction(t *testing.T) {
160161 Meta : & fnv1.ResponseMeta {Tag : "hello" , Ttl : durationpb .New (response .DefaultTTL )},
161162 Results : []* fnv1.Result {},
162163 Requirements : & fnv1.Requirements {
163- Resources : map [string ]* fnv1.ResourceSelector {
164+ ExtraResources : map [string ]* fnv1.ResourceSelector {
164165 "obj-0" : {
165166 ApiVersion : "apiextensions.crossplane.io/v1beta1" ,
166167 Kind : "EnvironmentConfig" ,
@@ -244,7 +245,7 @@ func TestRunFunction(t *testing.T) {
244245 }` ),
245246 },
246247 },
247- RequiredResources : map [string ]* fnv1.Resources {
248+ ExtraResources : map [string ]* fnv1.Resources {
248249 "obj-0" : {
249250 Items : []* fnv1.Resource {
250251 {
@@ -418,7 +419,7 @@ func TestRunFunction(t *testing.T) {
418419 Meta : & fnv1.ResponseMeta {Tag : "hello" , Ttl : durationpb .New (response .DefaultTTL )},
419420 Results : []* fnv1.Result {},
420421 Requirements : & fnv1.Requirements {
421- Resources : map [string ]* fnv1.ResourceSelector {
422+ ExtraResources : map [string ]* fnv1.ResourceSelector {
422423 "obj-0" : {
423424 ApiVersion : "apiextensions.crossplane.io/v1beta1" ,
424425 Kind : "EnvironmentConfig" ,
@@ -555,7 +556,7 @@ func TestRunFunction(t *testing.T) {
555556 }` ),
556557 },
557558 },
558- RequiredResources : map [string ]* fnv1.Resources {
559+ ExtraResources : map [string ]* fnv1.Resources {
559560 "environment-config-0" : {
560561 Items : []* fnv1.Resource {},
561562 },
@@ -589,7 +590,74 @@ func TestRunFunction(t *testing.T) {
589590 },
590591 },
591592 Requirements : & fnv1.Requirements {
592- Resources : map [string ]* fnv1.ResourceSelector {
593+ ExtraResources : map [string ]* fnv1.ResourceSelector {
594+ "obj-0" : {
595+ ApiVersion : "apiextensions.crossplane.io/v1beta1" ,
596+ Kind : "EnvironmentConfig" ,
597+ Match : & fnv1.ResourceSelector_MatchName {
598+ MatchName : "my-env-config" ,
599+ },
600+ },
601+ },
602+ },
603+ },
604+ },
605+ },
606+ "IgnoresRequiredResources" : {
607+ reason : "The Function should ignore the v2-only RequiredResources field and only read the legacy ExtraResources one." ,
608+ args : args {
609+ req : & fnv1.RunFunctionRequest {
610+ Meta : & fnv1.RequestMeta {Tag : "hello" },
611+ Observed : & fnv1.State {
612+ Composite : & fnv1.Resource {
613+ Resource : resource .MustStructJSON (`{
614+ "apiVersion": "test.crossplane.io/v1alpha1",
615+ "kind": "XR",
616+ "metadata": {
617+ "name": "my-xr"
618+ }
619+ }` ),
620+ },
621+ },
622+ RequiredResources : map [string ]* fnv1.Resources {
623+ "obj-0" : {
624+ Items : []* fnv1.Resource {
625+ {
626+ Resource : resource .MustStructJSON (`{
627+ "apiVersion": "apiextensions.crossplane.io/v1beta1",
628+ "kind": "EnvironmentConfig",
629+ "metadata": {
630+ "name": "my-env-config"
631+ }
632+ }` ),
633+ },
634+ },
635+ },
636+ },
637+ Input : resource .MustStructJSON (`{
638+ "apiVersion": "extra-resources.fn.crossplane.io/v1beta1",
639+ "kind": "Input",
640+ "spec": {
641+ "extraResources": [
642+ {
643+ "type": "Reference",
644+ "into": "obj-0",
645+ "kind": "EnvironmentConfig",
646+ "apiVersion": "apiextensions.crossplane.io/v1beta1",
647+ "ref": {
648+ "name": "my-env-config"
649+ }
650+ }
651+ ]
652+ }
653+ }` ),
654+ },
655+ },
656+ want : want {
657+ rsp : & fnv1.RunFunctionResponse {
658+ Meta : & fnv1.ResponseMeta {Tag : "hello" , Ttl : durationpb .New (response .DefaultTTL )},
659+ Requirements : & fnv1.Requirements {
660+ ExtraResources : map [string ]* fnv1.ResourceSelector {
593661 "obj-0" : {
594662 ApiVersion : "apiextensions.crossplane.io/v1beta1" ,
595663 Kind : "EnvironmentConfig" ,
@@ -618,7 +686,7 @@ func TestRunFunction(t *testing.T) {
618686 }` ),
619687 },
620688 },
621- RequiredResources : map [string ]* fnv1.Resources {
689+ ExtraResources : map [string ]* fnv1.Resources {
622690 "obj-0" : {
623691 Items : []* fnv1.Resource {
624692 {
@@ -660,7 +728,7 @@ func TestRunFunction(t *testing.T) {
660728 Meta : & fnv1.ResponseMeta {Tag : "hello" , Ttl : durationpb .New (response .DefaultTTL )},
661729 Results : []* fnv1.Result {},
662730 Requirements : & fnv1.Requirements {
663- Resources : map [string ]* fnv1.ResourceSelector {
731+ ExtraResources : map [string ]* fnv1.ResourceSelector {
664732 "obj-0" : {
665733 ApiVersion : "apiextensions.crossplane.io/v1beta1" ,
666734 Kind : "EnvironmentConfig" ,
@@ -734,6 +802,138 @@ func resourceWithFieldPathValue(path string, value any) resource.Required {
734802 }
735803}
736804
805+ func TestVerifyAndSortExtras (t * testing.T ) {
806+ nsResource := func (ns , name string ) resource.Required {
807+ return resource.Required {
808+ Resource : & unstructured.Unstructured {
809+ Object : map [string ]any {
810+ "apiVersion" : "example.org/v1" ,
811+ "kind" : "Thing" ,
812+ "metadata" : map [string ]any {"name" : name , "namespace" : ns },
813+ },
814+ },
815+ }
816+ }
817+
818+ clusterResource := func (name string ) resource.Required {
819+ return resource.Required {
820+ Resource : & unstructured.Unstructured {
821+ Object : map [string ]any {
822+ "apiVersion" : "example.org/v1" ,
823+ "kind" : "Thing" ,
824+ "metadata" : map [string ]any {"name" : name },
825+ },
826+ },
827+ }
828+ }
829+
830+ cases := map [string ]struct {
831+ reason string
832+ in * v1beta1.Input
833+ extra map [string ][]resource.Required
834+ wantNames []string
835+ wantErr string
836+ }{
837+ "SelectorNamespaceFilter" : {
838+ reason : "A namespace on a Selector drops resources from other namespaces before counting." ,
839+ in : & v1beta1.Input {Spec : v1beta1.InputSpec {ExtraResources : []v1beta1.ResourceSource {{
840+ Type : v1beta1 .ResourceSourceTypeSelector ,
841+ Into : "objs" ,
842+ Namespace : ptr .To ("ns-a" ),
843+ Selector : & v1beta1.ResourceSourceSelector {},
844+ }}}},
845+ extra : map [string ][]resource.Required {"objs" : {nsResource ("ns-a" , "keep" ), nsResource ("ns-b" , "drop" )}},
846+ wantNames : []string {"keep" },
847+ },
848+ "SelectorNamespaceFilterAppliesBeforeMinMatch" : {
849+ reason : "Resources dropped by the namespace filter do not count towards minMatch." ,
850+ in : & v1beta1.Input {Spec : v1beta1.InputSpec {ExtraResources : []v1beta1.ResourceSource {{
851+ Type : v1beta1 .ResourceSourceTypeSelector ,
852+ Into : "objs" ,
853+ Namespace : ptr .To ("ns-a" ),
854+ Selector : & v1beta1.ResourceSourceSelector {MinMatch : ptr.To [uint64 ](2 )},
855+ }}}},
856+ extra : map [string ][]resource.Required {"objs" : {nsResource ("ns-a" , "keep" ), nsResource ("ns-b" , "drop" )}},
857+ wantErr : `expected at least 2 extra resources "objs", got 1` ,
858+ },
859+ "SelectorNamespaceFilterAppliesBeforeSortAndMaxMatch" : {
860+ reason : "The namespace filter runs before sorting and truncation, so maxMatch selects from the in-namespace resources only." ,
861+ in : & v1beta1.Input {Spec : v1beta1.InputSpec {ExtraResources : []v1beta1.ResourceSource {{
862+ Type : v1beta1 .ResourceSourceTypeSelector ,
863+ Into : "objs" ,
864+ Namespace : ptr .To ("ns-a" ),
865+ Selector : & v1beta1.ResourceSourceSelector {
866+ MaxMatch : ptr.To [uint64 ](2 ),
867+ SortByFieldPath : "metadata.name" ,
868+ },
869+ }}}},
870+ extra : map [string ][]resource.Required {"objs" : {
871+ nsResource ("ns-a" , "c" ),
872+ nsResource ("ns-b" , "a" ),
873+ nsResource ("ns-a" , "b" ),
874+ nsResource ("ns-a" , "a" ),
875+ }},
876+ wantNames : []string {"a" , "b" },
877+ },
878+ "SelectorNamespaceOnClusterScopedKind" : {
879+ reason : "Cluster-scoped resources have an empty namespace, so setting one selects nothing rather than everything." ,
880+ in : & v1beta1.Input {Spec : v1beta1.InputSpec {ExtraResources : []v1beta1.ResourceSource {{
881+ Type : v1beta1 .ResourceSourceTypeSelector ,
882+ Into : "objs" ,
883+ Namespace : ptr .To ("ns-a" ),
884+ Selector : & v1beta1.ResourceSourceSelector {MinMatch : ptr.To [uint64 ](1 )},
885+ }}}},
886+ extra : map [string ][]resource.Required {"objs" : {clusterResource ("env-a" ), clusterResource ("env-b" )}},
887+ wantErr : `expected at least 1 extra resources "objs", got 0` ,
888+ },
889+ "SelectorEmptyNamespaceKeepsClusterScoped" : {
890+ reason : "An empty namespace is not the same as an unset one: it matches the empty namespace of cluster-scoped resources." ,
891+ in : & v1beta1.Input {Spec : v1beta1.InputSpec {ExtraResources : []v1beta1.ResourceSource {{
892+ Type : v1beta1 .ResourceSourceTypeSelector ,
893+ Into : "objs" ,
894+ Namespace : ptr .To ("" ),
895+ Selector : & v1beta1.ResourceSourceSelector {},
896+ }}}},
897+ extra : map [string ][]resource.Required {"objs" : {clusterResource ("cluster-scoped" ), nsResource ("ns-a" , "namespaced" )}},
898+ wantNames : []string {"cluster-scoped" },
899+ },
900+ "ReferenceNamespaceHint" : {
901+ reason : "A Reference that set a namespace but resolved nothing returns a namespace hint." ,
902+ in : & v1beta1.Input {Spec : v1beta1.InputSpec {ExtraResources : []v1beta1.ResourceSource {{
903+ Type : v1beta1 .ResourceSourceTypeReference ,
904+ Into : "obj" ,
905+ Namespace : ptr .To ("ns-a" ),
906+ Ref : & v1beta1.ResourceSourceReference {Name : "missing" },
907+ }}}},
908+ extra : map [string ][]resource.Required {"obj" : {}},
909+ wantErr : `required extra resource "obj" not found in namespace "ns-a"` ,
910+ },
911+ }
912+
913+ for name , tc := range cases {
914+ t .Run (name , func (t * testing.T ) {
915+ got , err := verifyAndSortExtras (tc .in , tc .extra )
916+ if tc .wantErr != "" {
917+ if err == nil || ! strings .Contains (err .Error (), tc .wantErr ) {
918+ t .Fatalf ("%s\n want error containing %q, got: %v" , tc .reason , tc .wantErr , err )
919+ }
920+ return
921+ }
922+ if err != nil {
923+ t .Fatalf ("%s\n unexpected error: %v" , tc .reason , err )
924+ }
925+ var gotNames []string
926+ for _ , o := range got ["objs" ].([]any ) {
927+ md := o .(map [string ]any )["metadata" ].(map [string ]any )
928+ gotNames = append (gotNames , md ["name" ].(string ))
929+ }
930+ if diff := cmp .Diff (tc .wantNames , gotNames ); diff != "" {
931+ t .Errorf ("%s\n -want +got:\n %s" , tc .reason , diff )
932+ }
933+ })
934+ }
935+ }
936+
737937func TestSortExtrasByFieldPath (t * testing.T ) {
738938 type args struct {
739939 extras []resource.Required
0 commit comments