Skip to content

Commit 0ade4c6

Browse files
committed
Add dynamic groupsRef for GroupObjectIDs query
* Tests * Implementation * Examples Signed-off-by: Yury Tsarev <yury@upbound.io>
1 parent 535dc7d commit 0ade4c6

11 files changed

+482
-13
lines changed

example/envconfig.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ metadata:
55
data:
66
group:
77
name: test-fn-msgraph
8+
groups:
9+
- test-fn-msgraph

example/group-membership-example-context-ref.yaml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
apiVersion: apiextensions.crossplane.io/v1
22
kind: Composition
33
metadata:
4-
name: group-membership-example
5-
# Important: This function requires an Azure AD app registration with Microsoft Graph API permissions:
6-
# - Group.Read.All
7-
# - Directory.Read.All
8-
# - User.Read.All (if groups contain users)
9-
# - Application.Read.All (if groups contain service principals)
4+
name: group-membership-example-context-ref
105
spec:
116
compositeTypeRef:
127
apiVersion: example.crossplane.io/v1

example/group-membership-example-status-ref.yaml

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
apiVersion: apiextensions.crossplane.io/v1
22
kind: Composition
33
metadata:
4-
name: group-membership-example
5-
annotations:
6-
# Important: This function requires an Azure AD app registration with Microsoft Graph API permissions:
7-
# - Group.Read.All
8-
# - Directory.Read.All
9-
# - User.Read.All (if groups contain users)
10-
# - Application.Read.All (if groups contain service principals)
4+
name: group-membership-example-status-ref
115
spec:
126
compositeTypeRef:
137
apiVersion: example.crossplane.io/v1
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
apiVersion: apiextensions.crossplane.io/v1
2+
kind: Composition
3+
metadata:
4+
name: group-objectids-example-context-ref
5+
spec:
6+
compositeTypeRef:
7+
apiVersion: example.crossplane.io/v1
8+
kind: XR
9+
mode: Pipeline
10+
pipeline:
11+
- step: environmentConfigs
12+
functionRef:
13+
name: crossplane-contrib-function-environment-configs
14+
input:
15+
apiVersion: environmentconfigs.fn.crossplane.io/v1beta1
16+
kind: Input
17+
spec:
18+
environmentConfigs:
19+
- type: Reference
20+
ref:
21+
name: example-config
22+
- step: get-group-objectids
23+
functionRef:
24+
name: function-msgraph
25+
input:
26+
apiVersion: msgraph.fn.crossplane.io/v1alpha1
27+
kind: Input
28+
queryType: GroupObjectIDs
29+
groupsRef: context.[apiextensions.crossplane.io/environment].groups
30+
target: "status.groupObjectIDs"
31+
skipQueryWhenTargetHasData: true
32+
credentials:
33+
- name: azure-creds
34+
source: Secret
35+
secretRef:
36+
namespace: upbound-system
37+
name: azure-account-creds
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
apiVersion: apiextensions.crossplane.io/v1
2+
kind: Composition
3+
metadata:
4+
name: group-objectids-example-status-ref
5+
annotations:
6+
# Important: This function requires an Azure AD app registration with Microsoft Graph API permissions:
7+
# - Group.Read.All
8+
# - Directory.Read.All
9+
spec:
10+
compositeTypeRef:
11+
apiVersion: example.crossplane.io/v1
12+
kind: XR
13+
mode: Pipeline
14+
pipeline:
15+
- step: get-group-objectids
16+
functionRef:
17+
name: function-msgraph
18+
input:
19+
apiVersion: msgraph.fn.crossplane.io/v1alpha1
20+
kind: Input
21+
queryType: GroupObjectIDs
22+
groupsRef: status.groups
23+
target: "status.groupObjectIDs"
24+
skipQueryWhenTargetHasData: true
25+
credentials:
26+
- name: azure-creds
27+
source: Secret
28+
secretRef:
29+
namespace: upbound-system
30+
name: azure-account-creds

example/xr.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ spec: {}
77
status:
88
group:
99
name: test-fn-msgraph
10+
groups:
11+
- test-fn-msgraph

fn.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,16 @@ func (f *Function) validateAndPrepareInput(_ context.Context, req *fnv1.RunFunct
879879
return false
880880
}
881881

882+
// Process references based on query type
883+
if !f.processReferences(req, in, rsp) {
884+
return false
885+
}
886+
887+
return true
888+
}
889+
890+
// processReferences handles resolving references like groupRef and groupsRef
891+
func (f *Function) processReferences(req *fnv1.RunFunctionRequest, in *v1beta1.Input, rsp *fnv1.RunFunctionResponse) bool {
882892
// Process groupRef if it exists for GroupMembership query type
883893
if in.QueryType == "GroupMembership" && in.GroupRef != nil && *in.GroupRef != "" {
884894
groupName, err := f.resolveGroupRef(req, in.GroupRef)
@@ -890,6 +900,17 @@ func (f *Function) validateAndPrepareInput(_ context.Context, req *fnv1.RunFunct
890900
f.log.Info("Resolved GroupRef to group", "group", groupName, "groupRef", *in.GroupRef)
891901
}
892902

903+
// Process groupsRef if it exists for GroupObjectIDs query type
904+
if in.QueryType == "GroupObjectIDs" && in.GroupsRef != nil && *in.GroupsRef != "" {
905+
groupNames, err := f.resolveGroupsRef(req, in.GroupsRef)
906+
if err != nil {
907+
response.Fatal(rsp, err)
908+
return false
909+
}
910+
in.Groups = groupNames
911+
f.log.Info("Resolved GroupsRef to groups", "groupCount", len(groupNames), "groupsRef", *in.GroupsRef)
912+
}
913+
893914
return true
894915
}
895916

@@ -996,3 +1017,77 @@ func (f *Function) resolveFromContext(req *fnv1.RunFunctionRequest, refKey strin
9961017
}
9971018
return value, nil
9981019
}
1020+
1021+
// resolveGroupsRef resolves a list of group names from a reference in status or context.
1022+
func (f *Function) resolveGroupsRef(req *fnv1.RunFunctionRequest, groupsRef *string) ([]*string, error) {
1023+
if groupsRef == nil || *groupsRef == "" {
1024+
return nil, errors.New("empty groupsRef provided")
1025+
}
1026+
1027+
refKey := *groupsRef
1028+
1029+
// Use proper switch statement instead of if-else chain
1030+
switch {
1031+
case strings.HasPrefix(refKey, "status."):
1032+
return f.resolveGroupsFromStatus(req, refKey)
1033+
case strings.HasPrefix(refKey, "context."):
1034+
return f.resolveGroupsFromContext(req, refKey)
1035+
default:
1036+
return nil, errors.Errorf("unsupported groupsRef format: %s", refKey)
1037+
}
1038+
}
1039+
1040+
// resolveGroupsFromStatus resolves a list of group names from XR status
1041+
func (f *Function) resolveGroupsFromStatus(req *fnv1.RunFunctionRequest, refKey string) ([]*string, error) {
1042+
xrStatus, _, err := f.getXRAndStatus(req)
1043+
if err != nil {
1044+
return nil, errors.Wrap(err, "cannot get XR status")
1045+
}
1046+
1047+
statusField := strings.TrimPrefix(refKey, "status.")
1048+
return f.extractStringArrayFromMap(xrStatus, statusField, refKey)
1049+
}
1050+
1051+
// resolveGroupsFromContext resolves a list of group names from function context
1052+
func (f *Function) resolveGroupsFromContext(req *fnv1.RunFunctionRequest, refKey string) ([]*string, error) {
1053+
contextMap := req.GetContext().AsMap()
1054+
contextField := strings.TrimPrefix(refKey, "context.")
1055+
return f.extractStringArrayFromMap(contextMap, contextField, refKey)
1056+
}
1057+
1058+
// extractStringArrayFromMap extracts a string array from a map using nested key
1059+
func (f *Function) extractStringArrayFromMap(dataMap map[string]interface{}, field, refKey string) ([]*string, error) {
1060+
parts, err := ParseNestedKey(field)
1061+
if err != nil {
1062+
return nil, errors.Wrap(err, "invalid field key")
1063+
}
1064+
1065+
currentValue := interface{}(dataMap)
1066+
for _, k := range parts {
1067+
if nestedMap, ok := currentValue.(map[string]interface{}); ok {
1068+
if nextValue, exists := nestedMap[k]; exists {
1069+
currentValue = nextValue
1070+
} else {
1071+
return nil, errors.Errorf("cannot resolve groupsRef: %s not found", refKey)
1072+
}
1073+
} else {
1074+
return nil, errors.Errorf("cannot resolve groupsRef: %s not a map", refKey)
1075+
}
1076+
}
1077+
1078+
// The current value should be a slice of strings
1079+
if strArray, ok := currentValue.([]interface{}); ok {
1080+
result := make([]*string, 0, len(strArray))
1081+
for _, val := range strArray {
1082+
if strVal, ok := val.(string); ok {
1083+
strCopy := strVal // Create a new string to avoid pointing to a loop variable
1084+
result = append(result, &strCopy)
1085+
}
1086+
}
1087+
if len(result) > 0 {
1088+
return result, nil
1089+
}
1090+
}
1091+
1092+
return nil, errors.Errorf("cannot resolve groupsRef: %s not a string array or empty", refKey)
1093+
}

0 commit comments

Comments
 (0)