Skip to content

Commit 305a77e

Browse files
committed
Implement spec reference capability
* Tests * Implementation * Examples Signed-off-by: Yury Tsarev <[email protected]>
1 parent 5847027 commit 305a77e

File tree

8 files changed

+655
-14
lines changed

8 files changed

+655
-14
lines changed

example/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ crossplane render xr.yaml user-validation-example-status-ref.yaml functions.yaml
5252
crossplane render xr.yaml user-validation-example-context-ref.yaml functions.yaml --function-credentials=./secrets/azure-creds.yaml -rc --extra-resources=envconfig.yaml
5353
```
5454

55+
```shell
56+
crossplane render xr.yaml user-validation-example-spec-ref.yaml functions.yaml --function-credentials=./secrets/azure-creds.yaml -rc
57+
```
58+
5559
### 2. Group Membership
5660

5761
Get all members of a specified Azure AD group:
@@ -70,6 +74,10 @@ crossplane render xr.yaml group-membership-example-status-ref.yaml functions.yam
7074
crossplane render xr.yaml group-membership-example-context-ref.yaml functions.yaml --function-credentials=./secrets/azure-creds.yaml -rc --extra-resources=envconfig.yaml
7175
```
7276

77+
```shell
78+
crossplane render xr.yaml group-membership-example-spec-ref.yaml functions.yaml --function-credentials=./secrets/azure-creds.yaml -rc
79+
```
80+
7381
### 3. Group Object IDs
7482

7583
Get object IDs for specified Azure AD groups:
@@ -88,6 +96,10 @@ crossplane render xr.yaml group-objectids-example-status-ref.yaml functions.yaml
8896
crossplane render xr.yaml group-objectids-example-context-ref.yaml functions.yaml --function-credentials=./secrets/azure-creds.yaml -rc --extra-resources=envconfig.yaml
8997
```
9098

99+
```shell
100+
crossplane render xr.yaml group-objectids-example-spec-ref.yaml functions.yaml --function-credentials=./secrets/azure-creds.yaml -rc
101+
```
102+
91103
### 4. Service Principal Details
92104

93105
Get details of specified service principals:
@@ -105,3 +117,7 @@ crossplane render xr.yaml service-principal-example-status-ref.yaml functions.ya
105117
```shell
106118
crossplane render xr.yaml service-principal-example-context-ref.yaml functions.yaml --function-credentials=./secrets/azure-creds.yaml -rc --extra-resources=envconfig.yaml
107119
```
120+
121+
```shell
122+
crossplane render xr.yaml service-principal-example-spec-ref.yaml functions.yaml --function-credentials=./secrets/azure-creds.yaml -rc
123+
```
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
apiVersion: apiextensions.crossplane.io/v1
2+
kind: Composition
3+
metadata:
4+
name: group-membership-example-spec-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+
# - User.Read.All (if groups contain users)
10+
# - Application.Read.All (if groups contain service principals)
11+
spec:
12+
compositeTypeRef:
13+
apiVersion: example.crossplane.io/v1
14+
kind: XR
15+
mode: Pipeline
16+
pipeline:
17+
- step: get-group-members
18+
functionRef:
19+
name: function-msgraph
20+
input:
21+
apiVersion: msgraph.fn.crossplane.io/v1alpha1
22+
kind: Input
23+
queryType: GroupMembership
24+
# Using spec reference to get group name
25+
groupRef: "spec.groupConfig.name"
26+
# The function will automatically select standard fields:
27+
# - id, displayName, mail, userPrincipalName, appId, description
28+
target: "status.groupMembers"
29+
skipQueryWhenTargetHasData: true
30+
credentials:
31+
- name: azure-creds
32+
source: Secret
33+
secretRef:
34+
namespace: upbound-system
35+
name: azure-account-creds
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
apiVersion: apiextensions.crossplane.io/v1
2+
kind: Composition
3+
metadata:
4+
name: group-objectids-example-spec-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+
# Using spec reference to get group names
23+
groupsRef: "spec.groupConfig.names"
24+
target: "status.groupObjectIDs"
25+
skipQueryWhenTargetHasData: true
26+
credentials:
27+
- name: azure-creds
28+
source: Secret
29+
secretRef:
30+
namespace: upbound-system
31+
name: azure-account-creds
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
apiVersion: apiextensions.crossplane.io/v1
2+
kind: Composition
3+
metadata:
4+
name: service-principal-example-spec-ref
5+
spec:
6+
compositeTypeRef:
7+
apiVersion: example.crossplane.io/v1
8+
kind: XR
9+
mode: Pipeline
10+
pipeline:
11+
- step: get-service-principal-details
12+
functionRef:
13+
name: function-msgraph
14+
input:
15+
apiVersion: msgraph.fn.crossplane.io/v1alpha1
16+
kind: Input
17+
queryType: ServicePrincipalDetails
18+
# Using spec reference to get service principal names
19+
servicePrincipalsRef: "spec.servicePrincipalConfig.names"
20+
target: "status.servicePrincipalDetails"
21+
skipQueryWhenTargetHasData: true
22+
credentials:
23+
- name: azure-creds
24+
source: Secret
25+
secretRef:
26+
namespace: upbound-system
27+
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-spec-ref
5+
# Important: This function example requires an Azure AD app registration with Microsoft Graph API permissions:
6+
# - User.Read.All
7+
# - Directory.Read.All
8+
spec:
9+
compositeTypeRef:
10+
apiVersion: example.crossplane.io/v1
11+
kind: XR
12+
mode: Pipeline
13+
pipeline:
14+
- step: validate-user
15+
functionRef:
16+
name: function-msgraph
17+
input:
18+
apiVersion: msgraph.fn.crossplane.io/v1alpha1
19+
kind: Input
20+
queryType: UserValidation
21+
# Using spec reference to get user emails
22+
usersRef: "spec.userAccess.emails"
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: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,36 @@
1-
# Replace this with your XR!
1+
# Example XR with both spec references and status fields
22
apiVersion: example.crossplane.io/v1
33
kind: XR
44
metadata:
55
name: example-xr
6-
spec: {}
6+
spec:
7+
# Group config for group references
8+
groupConfig:
9+
# For single group reference (groupRef)
10+
name: test-fn-msgraph
11+
# For multiple group references (groupsRef)
12+
names:
13+
- "Developers"
14+
- "All Engineering"
15+
- "All Company"
16+
- "test-fn-msgraph"
17+
# User access config for user references
18+
userAccess:
19+
# For user validation (usersRef)
20+
emails:
21+
22+
23+
24+
# Service principal config for service principal references
25+
servicePrincipalConfig:
26+
# For service principal details (servicePrincipalsRef)
27+
names:
28+
- "MyServiceApp"
29+
- "ApiConnector"
30+
- "yury-upbound-oidc-provider"
31+
732
status:
33+
# For testing status.field references
834
group:
935
name: test-fn-msgraph
1036
groups:

fn.go

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,23 @@ func (f *Function) parseInputAndCredentials(req *fnv1.RunFunctionRequest, rsp *f
104104

105105
// getXRAndStatus retrieves status and desired XR, handling initialization if needed
106106
func (f *Function) getXRAndStatus(req *fnv1.RunFunctionRequest) (map[string]interface{}, *resource.Composite, error) {
107-
// Get both observed and desired XR
107+
// Get composite resources
108+
oxr, dxr, err := f.getObservedAndDesired(req)
109+
if err != nil {
110+
return nil, nil, err
111+
}
112+
113+
// Initialize and copy data
114+
f.initializeAndCopyData(oxr, dxr)
115+
116+
// Get status
117+
xrStatus := f.getStatusFromResources(oxr, dxr)
118+
119+
return xrStatus, dxr, nil
120+
}
121+
122+
// getObservedAndDesired gets both observed and desired XR resources
123+
func (f *Function) getObservedAndDesired(req *fnv1.RunFunctionRequest) (*resource.Composite, *resource.Composite, error) {
108124
oxr, err := request.GetObservedCompositeResource(req)
109125
if err != nil {
110126
return nil, nil, errors.Wrap(err, "cannot get observed composite resource")
@@ -115,31 +131,47 @@ func (f *Function) getXRAndStatus(req *fnv1.RunFunctionRequest) (map[string]inte
115131
return nil, nil, errors.Wrap(err, "cannot get desired composite resource")
116132
}
117133

118-
xrStatus := make(map[string]interface{})
134+
return oxr, dxr, nil
135+
}
119136

137+
// initializeAndCopyData initializes metadata and copies spec
138+
func (f *Function) initializeAndCopyData(oxr, dxr *resource.Composite) {
120139
// Initialize dxr from oxr if needed
121140
if dxr.Resource.GetKind() == "" {
122141
dxr.Resource.SetAPIVersion(oxr.Resource.GetAPIVersion())
123142
dxr.Resource.SetKind(oxr.Resource.GetKind())
124143
dxr.Resource.SetName(oxr.Resource.GetName())
125144
}
126145

146+
// Copy spec from observed to desired XR to preserve it
147+
xrSpec := make(map[string]interface{})
148+
if err := oxr.Resource.GetValueInto("spec", &xrSpec); err == nil && len(xrSpec) > 0 {
149+
if err := dxr.Resource.SetValue("spec", xrSpec); err != nil {
150+
f.log.Debug("Cannot set spec in desired XR", "error", err)
151+
}
152+
}
153+
}
154+
155+
// getStatusFromResources gets status from desired or observed XR
156+
func (f *Function) getStatusFromResources(oxr, dxr *resource.Composite) map[string]interface{} {
157+
xrStatus := make(map[string]interface{})
158+
127159
// First try to get status from desired XR (pipeline changes)
128160
if dxr.Resource.GetKind() != "" {
129-
err = dxr.Resource.GetValueInto("status", &xrStatus)
161+
err := dxr.Resource.GetValueInto("status", &xrStatus)
130162
if err == nil && len(xrStatus) > 0 {
131-
return xrStatus, dxr, nil
163+
return xrStatus
132164
}
133165
f.log.Debug("Cannot get status from Desired XR or it's empty")
134166
}
135167

136168
// Fallback to observed XR status
137-
err = oxr.Resource.GetValueInto("status", &xrStatus)
169+
err := oxr.Resource.GetValueInto("status", &xrStatus)
138170
if err != nil {
139171
f.log.Debug("Cannot get status from Observed XR")
140172
}
141173

142-
return xrStatus, dxr, nil
174+
return xrStatus
143175
}
144176

145177
// checkStatusTargetHasData checks if the status target has data.
@@ -1026,7 +1058,7 @@ func (f *Function) checkContextTargetHasData(req *fnv1.RunFunctionRequest, in *v
10261058
return false
10271059
}
10281060

1029-
// resolveGroupRef resolves the group name from a reference in status or context.
1061+
// resolveGroupRef resolves the group name from a reference in spec, status or context.
10301062
func (f *Function) resolveGroupRef(req *fnv1.RunFunctionRequest, groupRef *string) (string, error) {
10311063
if groupRef == nil || *groupRef == "" {
10321064
return "", errors.New("empty groupRef provided")
@@ -1040,6 +1072,8 @@ func (f *Function) resolveGroupRef(req *fnv1.RunFunctionRequest, groupRef *strin
10401072
return f.resolveFromStatus(req, refKey)
10411073
case strings.HasPrefix(refKey, "context."):
10421074
return f.resolveFromContext(req, refKey)
1075+
case strings.HasPrefix(refKey, "spec."):
1076+
return f.resolveFromSpec(req, refKey)
10431077
default:
10441078
return "", errors.Errorf("unsupported groupRef format: %s", refKey)
10451079
}
@@ -1071,7 +1105,30 @@ func (f *Function) resolveFromContext(req *fnv1.RunFunctionRequest, refKey strin
10711105
return value, nil
10721106
}
10731107

1074-
// resolveStringArrayRef resolves a list of string values from a reference in status or context
1108+
// resolveFromSpec resolves a reference from XR spec
1109+
func (f *Function) resolveFromSpec(req *fnv1.RunFunctionRequest, refKey string) (string, error) {
1110+
// Use getXRAndStatus to ensure spec is copied to desired XR
1111+
_, dxr, err := f.getXRAndStatus(req)
1112+
if err != nil {
1113+
return "", errors.Wrap(err, "cannot get XR status and desired XR")
1114+
}
1115+
1116+
// Get spec from the desired XR (which now has the spec copied from observed)
1117+
xrSpec := make(map[string]interface{})
1118+
err = dxr.Resource.GetValueInto("spec", &xrSpec)
1119+
if err != nil {
1120+
return "", errors.Wrap(err, "cannot get XR spec")
1121+
}
1122+
1123+
specField := strings.TrimPrefix(refKey, "spec.")
1124+
value, ok := GetNestedKey(xrSpec, specField)
1125+
if !ok {
1126+
return "", errors.Errorf("cannot resolve groupRef: %s not found", refKey)
1127+
}
1128+
return value, nil
1129+
}
1130+
1131+
// resolveStringArrayRef resolves a list of string values from a reference in spec, status or context
10751132
func (f *Function) resolveStringArrayRef(req *fnv1.RunFunctionRequest, ref *string, refType string) ([]*string, error) {
10761133
if ref == nil || *ref == "" {
10771134
return nil, errors.Errorf("empty %s provided", refType)
@@ -1090,6 +1147,8 @@ func (f *Function) resolveStringArrayRef(req *fnv1.RunFunctionRequest, ref *stri
10901147
result, err = f.resolveStringArrayFromStatus(req, refKey)
10911148
case strings.HasPrefix(refKey, "context."):
10921149
result, err = f.resolveStringArrayFromContext(req, refKey)
1150+
case strings.HasPrefix(refKey, "spec."):
1151+
result, err = f.resolveStringArrayFromSpec(req, refKey)
10931152
default:
10941153
return nil, errors.Errorf("unsupported %s format: %s", refType, refKey)
10951154
}
@@ -1122,6 +1181,25 @@ func (f *Function) resolveStringArrayFromContext(req *fnv1.RunFunctionRequest, r
11221181
return f.extractStringArrayFromMap(contextMap, contextField, refKey)
11231182
}
11241183

1184+
// resolveStringArrayFromSpec resolves a list of string values from XR spec
1185+
func (f *Function) resolveStringArrayFromSpec(req *fnv1.RunFunctionRequest, refKey string) ([]*string, error) {
1186+
// Use getXRAndStatus to ensure spec is copied to desired XR
1187+
_, dxr, err := f.getXRAndStatus(req)
1188+
if err != nil {
1189+
return nil, errors.Wrap(err, "cannot get XR status and desired XR")
1190+
}
1191+
1192+
// Get spec from the desired XR (which now has the spec copied from observed)
1193+
xrSpec := make(map[string]interface{})
1194+
err = dxr.Resource.GetValueInto("spec", &xrSpec)
1195+
if err != nil {
1196+
return nil, errors.Wrap(err, "cannot get XR spec")
1197+
}
1198+
1199+
specField := strings.TrimPrefix(refKey, "spec.")
1200+
return f.extractStringArrayFromMap(xrSpec, specField, refKey)
1201+
}
1202+
11251203
// resolveGroupsRef resolves a list of group names from a reference in status or context
11261204
func (f *Function) resolveGroupsRef(req *fnv1.RunFunctionRequest, groupsRef *string) ([]*string, error) {
11271205
return f.resolveStringArrayRef(req, groupsRef, "groupsRef")

0 commit comments

Comments
 (0)