Skip to content

Commit d271aef

Browse files
committed
Dynamic usersRef implementation
* Tests * Implementation * Examples Signed-off-by: Yury Tsarev <[email protected]>
1 parent ac5ad53 commit d271aef

File tree

10 files changed

+501
-31
lines changed

10 files changed

+501
-31
lines changed

example/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ Validate if specified Azure AD users exist:
4242
crossplane render xr.yaml user-validation-example.yaml functions.yaml --function-credentials=./secrets/azure-creds.yaml -rc
4343
```
4444

45+
Dynamic `usersRef` variations:
46+
47+
```shell
48+
crossplane render xr.yaml user-validation-example-status-ref.yaml functions.yaml --function-credentials=./secrets/azure-creds.yaml -rc
49+
```
50+
51+
```shell
52+
crossplane render xr.yaml user-validation-example-context-ref.yaml functions.yaml --function-credentials=./secrets/azure-creds.yaml -rc --extra-resources=envconfig.yaml
53+
```
54+
4555
### 2. Group Membership
4656

4757
Get all members of a specified Azure AD group:

example/envconfig.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ data:
77
name: test-fn-msgraph
88
groups:
99
- test-fn-msgraph
10+
users:
11+
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: user-validation-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: validate-users
23+
functionRef:
24+
name: function-msgraph
25+
input:
26+
apiVersion: msgraph.fn.crossplane.io/v1alpha1
27+
kind: Input
28+
queryType: UserValidation
29+
usersRef: context.[apiextensions.crossplane.io/environment].users
30+
target: "status.validatedUsers"
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: user-validation-example-status-ref
5+
annotations:
6+
# Important: This function requires an Azure AD app registration with Microsoft Graph API permissions:
7+
# - User.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: validate-users
16+
functionRef:
17+
name: function-msgraph
18+
input:
19+
apiVersion: msgraph.fn.crossplane.io/v1alpha1
20+
kind: Input
21+
queryType: UserValidation
22+
usersRef: status.users
23+
target: "status.validatedUsers"
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
@@ -9,3 +9,5 @@ status:
99
name: test-fn-msgraph
1010
groups:
1111
- test-fn-msgraph
12+
users:
13+

fn.go

Lines changed: 90 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -887,30 +887,65 @@ func (f *Function) validateAndPrepareInput(_ context.Context, req *fnv1.RunFunct
887887
return true
888888
}
889889

890-
// processReferences handles resolving references like groupRef and groupsRef
890+
// processReferences handles resolving references like groupRef, groupsRef, and usersRef
891891
func (f *Function) processReferences(req *fnv1.RunFunctionRequest, in *v1beta1.Input, rsp *fnv1.RunFunctionResponse) bool {
892-
// Process groupRef if it exists for GroupMembership query type
893-
if in.QueryType == "GroupMembership" && in.GroupRef != nil && *in.GroupRef != "" {
894-
groupName, err := f.resolveGroupRef(req, in.GroupRef)
895-
if err != nil {
896-
response.Fatal(rsp, err)
897-
return false
898-
}
899-
in.Group = &groupName
900-
f.log.Info("Resolved GroupRef to group", "group", groupName, "groupRef", *in.GroupRef)
892+
// Process references based on query type
893+
switch in.QueryType {
894+
case "GroupMembership":
895+
return f.processGroupRef(req, in, rsp)
896+
case "GroupObjectIDs":
897+
return f.processGroupsRef(req, in, rsp)
898+
case "UserValidation":
899+
return f.processUsersRef(req, in, rsp)
901900
}
901+
return true
902+
}
902903

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)
904+
// processGroupRef handles resolving the groupRef reference for GroupMembership query type
905+
func (f *Function) processGroupRef(req *fnv1.RunFunctionRequest, in *v1beta1.Input, rsp *fnv1.RunFunctionResponse) bool {
906+
if in.GroupRef == nil || *in.GroupRef == "" {
907+
return true
912908
}
913909

910+
groupName, err := f.resolveGroupRef(req, in.GroupRef)
911+
if err != nil {
912+
response.Fatal(rsp, err)
913+
return false
914+
}
915+
in.Group = &groupName
916+
f.log.Info("Resolved GroupRef to group", "group", groupName, "groupRef", *in.GroupRef)
917+
return true
918+
}
919+
920+
// processGroupsRef handles resolving the groupsRef reference for GroupObjectIDs query type
921+
func (f *Function) processGroupsRef(req *fnv1.RunFunctionRequest, in *v1beta1.Input, rsp *fnv1.RunFunctionResponse) bool {
922+
if in.GroupsRef == nil || *in.GroupsRef == "" {
923+
return true
924+
}
925+
926+
groupNames, err := f.resolveGroupsRef(req, in.GroupsRef)
927+
if err != nil {
928+
response.Fatal(rsp, err)
929+
return false
930+
}
931+
in.Groups = groupNames
932+
f.log.Info("Resolved GroupsRef to groups", "groupCount", len(groupNames), "groupsRef", *in.GroupsRef)
933+
return true
934+
}
935+
936+
// processUsersRef handles resolving the usersRef reference for UserValidation query type
937+
func (f *Function) processUsersRef(req *fnv1.RunFunctionRequest, in *v1beta1.Input, rsp *fnv1.RunFunctionResponse) bool {
938+
if in.UsersRef == nil || *in.UsersRef == "" {
939+
return true
940+
}
941+
942+
userNames, err := f.resolveUsersRef(req, in.UsersRef)
943+
if err != nil {
944+
response.Fatal(rsp, err)
945+
return false
946+
}
947+
in.Users = userNames
948+
f.log.Info("Resolved UsersRef to users", "userCount", len(userNames), "usersRef", *in.UsersRef)
914949
return true
915950
}
916951

@@ -1018,27 +1053,41 @@ func (f *Function) resolveFromContext(req *fnv1.RunFunctionRequest, refKey strin
10181053
return value, nil
10191054
}
10201055

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")
1056+
// resolveStringArrayRef resolves a list of string values from a reference in status or context
1057+
func (f *Function) resolveStringArrayRef(req *fnv1.RunFunctionRequest, ref *string, refType string) ([]*string, error) {
1058+
if ref == nil || *ref == "" {
1059+
return nil, errors.Errorf("empty %s provided", refType)
10251060
}
10261061

1027-
refKey := *groupsRef
1062+
refKey := *ref
1063+
1064+
var (
1065+
result []*string
1066+
err error
1067+
)
10281068

10291069
// Use proper switch statement instead of if-else chain
10301070
switch {
10311071
case strings.HasPrefix(refKey, "status."):
1032-
return f.resolveGroupsFromStatus(req, refKey)
1072+
result, err = f.resolveStringArrayFromStatus(req, refKey)
10331073
case strings.HasPrefix(refKey, "context."):
1034-
return f.resolveGroupsFromContext(req, refKey)
1074+
result, err = f.resolveStringArrayFromContext(req, refKey)
10351075
default:
1036-
return nil, errors.Errorf("unsupported groupsRef format: %s", refKey)
1076+
return nil, errors.Errorf("unsupported %s format: %s", refType, refKey)
1077+
}
1078+
1079+
// If we got an error and it contains "groupsRef" but we're looking for a different ref type,
1080+
// replace it with the correct ref type
1081+
if err != nil && refType != "groupsRef" && strings.Contains(err.Error(), "groupsRef") {
1082+
errMsg := err.Error()
1083+
return nil, errors.New(strings.ReplaceAll(errMsg, "groupsRef", refType))
10371084
}
1085+
1086+
return result, err
10381087
}
10391088

1040-
// resolveGroupsFromStatus resolves a list of group names from XR status
1041-
func (f *Function) resolveGroupsFromStatus(req *fnv1.RunFunctionRequest, refKey string) ([]*string, error) {
1089+
// resolveStringArrayFromStatus resolves a list of string values from XR status
1090+
func (f *Function) resolveStringArrayFromStatus(req *fnv1.RunFunctionRequest, refKey string) ([]*string, error) {
10421091
xrStatus, _, err := f.getXRAndStatus(req)
10431092
if err != nil {
10441093
return nil, errors.Wrap(err, "cannot get XR status")
@@ -1048,13 +1097,23 @@ func (f *Function) resolveGroupsFromStatus(req *fnv1.RunFunctionRequest, refKey
10481097
return f.extractStringArrayFromMap(xrStatus, statusField, refKey)
10491098
}
10501099

1051-
// resolveGroupsFromContext resolves a list of group names from function context
1052-
func (f *Function) resolveGroupsFromContext(req *fnv1.RunFunctionRequest, refKey string) ([]*string, error) {
1100+
// resolveStringArrayFromContext resolves a list of string values from function context
1101+
func (f *Function) resolveStringArrayFromContext(req *fnv1.RunFunctionRequest, refKey string) ([]*string, error) {
10531102
contextMap := req.GetContext().AsMap()
10541103
contextField := strings.TrimPrefix(refKey, "context.")
10551104
return f.extractStringArrayFromMap(contextMap, contextField, refKey)
10561105
}
10571106

1107+
// resolveGroupsRef resolves a list of group names from a reference in status or context
1108+
func (f *Function) resolveGroupsRef(req *fnv1.RunFunctionRequest, groupsRef *string) ([]*string, error) {
1109+
return f.resolveStringArrayRef(req, groupsRef, "groupsRef")
1110+
}
1111+
1112+
// resolveUsersRef resolves a list of user names from a reference in status or context
1113+
func (f *Function) resolveUsersRef(req *fnv1.RunFunctionRequest, usersRef *string) ([]*string, error) {
1114+
return f.resolveStringArrayRef(req, usersRef, "usersRef")
1115+
}
1116+
10581117
// extractStringArrayFromMap extracts a string array from a map using nested key
10591118
func (f *Function) extractStringArrayFromMap(dataMap map[string]interface{}, field, refKey string) ([]*string, error) {
10601119
parts, err := ParseNestedKey(field)

0 commit comments

Comments
 (0)