Skip to content

Commit 0ac31b7

Browse files
authored
Support nested field in targets, refactor to reduce cyclomatic complexity, update documentation (#7)
* Ability to write into nested status/context fields including environment Signed-off-by: Yury Tsarev <[email protected]> * Enabled nested writing for status with bracket notation Signed-off-by: Yury Tsarev <[email protected]> * Add test for nested XR status with key with dots Signed-off-by: Yury Tsarev <[email protected]> * Refactor Setting and Getting nested fields * Move out common logic * Make key Get/Set code a bit more dry * Rename functions to be non-context specific Signed-off-by: Yury Tsarev <[email protected]> * Refactor and reduce cyclomatic complexity Signed-off-by: Yury Tsarev <[email protected]> * Fully refactor to concise functions Signed-off-by: Yury Tsarev <[email protected]> * Update README with new functionality overview Signed-off-by: Yury Tsarev <[email protected]> * Update XR status update logic to keep already existing fields around Signed-off-by: Yury Tsarev <[email protected]> * Enhance wording readme, remover redundant entry Signed-off-by: Yury Tsarev <[email protected]> --------- Signed-off-by: Yury Tsarev <[email protected]>
1 parent e64181e commit 0ac31b7

File tree

3 files changed

+504
-50
lines changed

3 files changed

+504
-50
lines changed

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Example pipeline step:
1818
apiVersion: azresourcegraph.fn.crossplane.io/v1alpha1
1919
kind: Input
2020
query: "Resources | project name, location, type, id| where type =~ 'Microsoft.Compute/virtualMachines' | order by name desc"
21+
target: "status.azResourceGraphQueryResult"
2122
credentials:
2223
- name: azure-creds
2324
source: Secret
@@ -60,6 +61,63 @@ status:
6061
type: microsoft.compute/virtualmachines
6162
```
6263
64+
### QueryRef
65+
66+
Rather than specifying a direct query string as shown in the example above,
67+
the function allows referencing a query from any arbitrary string within the Context or Status.
68+
69+
#### Context
70+
71+
* Simple context field reference
72+
```yaml
73+
queryRef: "context.azResourceGraphQuery"
74+
```
75+
76+
* Get data from Environment
77+
```yaml
78+
queryRef: "context.[apiextensions.crossplane.io/environment].azResourceGraphQuery"
79+
```
80+
81+
#### XR Status
82+
83+
* Simple XR Status field reference
84+
```yaml
85+
queryRef: "status.azResourceGraphQuery"
86+
```
87+
88+
* Get data from nested field in XR status. Use brackets if key contains dots.
89+
```yaml
90+
queryRef: "status.[fancy.key.with.dots].azResourceGraphQuery"
91+
```
92+
93+
### Targets
94+
95+
Function supports publishing Query Results to different locations.
96+
97+
#### Context
98+
99+
* Simple Context field target
100+
```yaml
101+
target: "context.azResourceGraphQueryResult"
102+
```
103+
104+
* Put results into Environment key
105+
```yaml
106+
target: "context.[apiextensions.crossplane.io/environment].azResourceGraphQuery"
107+
```
108+
109+
#### XR Status
110+
111+
* Simple XR status field target
112+
```yaml
113+
target: "status.azResourceGraphQueryResult"
114+
```
115+
116+
* Put query results to nested field under XR status. Use brackets if key contains dots
117+
```yaml
118+
target: "status.[fancy.key.with.dots].azResourceGraphQueryResult"
119+
```
120+
63121
64122
[azresourcegraph]: https://learn.microsoft.com/en-us/azure/governance/resource-graph/
65123
[azop]: https://marketplace.upbound.io/providers/upbound/provider-family-azure/latest

fn.go

Lines changed: 145 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -74,24 +74,14 @@ func (f *Function) RunFunction(ctx context.Context, req *fnv1.RunFunctionRequest
7474
switch {
7575
case in.QueryRef == nil:
7676
case strings.HasPrefix(*in.QueryRef, "status."):
77-
// The composite resource that actually exists.
78-
oxr, err := request.GetObservedCompositeResource(req)
77+
err := getQueryFromStatus(req, in)
7978
if err != nil {
80-
response.Fatal(rsp, errors.Wrap(err, "cannot get observed composite resource"))
79+
response.Fatal(rsp, err)
8180
return rsp, nil
8281
}
83-
xrStatus := make(map[string]interface{})
84-
err = oxr.Resource.GetValueInto("status", &xrStatus)
85-
if err != nil {
86-
response.Fatal(rsp, errors.Wrap(err, "cannot get XR status"))
87-
return rsp, nil
88-
}
89-
if queryFromXRStatus, ok := GetNestedContextKey(xrStatus, strings.TrimPrefix(*in.QueryRef, "status.")); ok {
90-
in.Query = queryFromXRStatus
91-
}
9282
case strings.HasPrefix(*in.QueryRef, "context."):
9383
functionContext := req.GetContext().AsMap()
94-
if queryFromContext, ok := GetNestedContextKey(functionContext, strings.TrimPrefix(*in.QueryRef, "context.")); ok {
84+
if queryFromContext, ok := GetNestedKey(functionContext, strings.TrimPrefix(*in.QueryRef, "context.")); ok {
9585
in.Query = queryFromContext
9686
}
9787
default:
@@ -118,39 +108,17 @@ func (f *Function) RunFunction(ctx context.Context, req *fnv1.RunFunctionRequest
118108

119109
switch {
120110
case strings.HasPrefix(in.Target, "status."):
121-
// The composite resource that actually exists.
122-
oxr, err := request.GetObservedCompositeResource(req)
123-
if err != nil {
124-
response.Fatal(rsp, errors.Wrap(err, "cannot get observed composite resource"))
125-
return rsp, nil
126-
}
127-
// The composite resource desired by previous functions in the pipeline.
128-
dxr, err := request.GetDesiredCompositeResource(req)
129-
if err != nil {
130-
response.Fatal(rsp, errors.Wrap(err, "cannot get desired composite resource"))
131-
return rsp, nil
132-
}
133-
dxr.Resource.SetAPIVersion(oxr.Resource.GetAPIVersion())
134-
dxr.Resource.SetKind(oxr.Resource.GetKind())
135-
TargetXRStatusField := in.Target
136-
err = dxr.Resource.SetValue(TargetXRStatusField, &results.Data)
111+
err = putQueryResultToStatus(req, rsp, in, results, f)
137112
if err != nil {
138-
response.Fatal(rsp, errors.Wrapf(err, "cannot set field %s to %s for %s", TargetXRStatusField, results.Data, dxr.Resource.GetKind()))
139-
return rsp, nil
140-
}
141-
if err := response.SetDesiredCompositeResource(rsp, dxr); err != nil {
142-
response.Fatal(rsp, errors.Wrapf(err, "cannot set desired composite resource in %T", rsp))
113+
response.Fatal(rsp, err)
143114
return rsp, nil
144115
}
145116
case strings.HasPrefix(in.Target, "context."):
146-
contextField := strings.TrimPrefix(in.Target, "context.")
147-
data, err := structpb.NewValue(results.Data)
117+
err = putQueryResultToContext(req, rsp, in, results, f)
148118
if err != nil {
149-
response.Fatal(rsp, errors.Wrap(err, "cannot convert results data to structpb.Value"))
119+
response.Fatal(rsp, err)
150120
return rsp, nil
151121
}
152-
f.log.Debug("Updating Composition environment", "key", contextField, "data", &results.Data)
153-
response.SetContextKey(rsp, contextField, data)
154122
default:
155123
response.Fatal(rsp, errors.Errorf("Unrecognized target field: %s", in.Target))
156124
return rsp, nil
@@ -229,24 +197,35 @@ func (a *AzureQuery) azQuery(ctx context.Context, azureCreds map[string]string,
229197
return results, nil
230198
}
231199

232-
// GetNestedContextKey retrieves a nested string value from a map using dot notation keys.
233-
func GetNestedContextKey(context map[string]interface{}, key string) (string, bool) {
200+
// ParseNestedKey enables the bracket and dot notation to key reference
201+
func ParseNestedKey(key string) ([]string, error) {
202+
var parts []string
234203
// Regular expression to extract keys, supporting both dot and bracket notation
235-
keyRegex := regexp.MustCompile(`\[([^\[\]]+)\]|([^.\[\]]+)`)
236-
matches := keyRegex.FindAllStringSubmatch(key, -1)
237-
238-
// Extract all keys in the proper order
239-
var keys []string
204+
regex := regexp.MustCompile(`\[([^\[\]]+)\]|([^.\[\]]+)`)
205+
matches := regex.FindAllStringSubmatch(key, -1)
240206
for _, match := range matches {
241207
if match[1] != "" {
242-
keys = append(keys, match[1]) // Bracket key
208+
parts = append(parts, match[1]) // Bracket notation
243209
} else if match[2] != "" {
244-
keys = append(keys, match[2]) // Dot key
210+
parts = append(parts, match[2]) // Dot notation
245211
}
246212
}
247-
currentValue := interface{}(context)
248213

249-
for _, k := range keys {
214+
if len(parts) == 0 {
215+
return nil, errors.New("invalid key")
216+
}
217+
return parts, nil
218+
}
219+
220+
// GetNestedKey retrieves a nested string value from a map using dot notation keys.
221+
func GetNestedKey(context map[string]interface{}, key string) (string, bool) {
222+
parts, err := ParseNestedKey(key)
223+
if err != nil {
224+
return "", false
225+
}
226+
227+
currentValue := interface{}(context)
228+
for _, k := range parts {
250229
// Check if the current value is a map
251230
if nestedMap, ok := currentValue.(map[string]interface{}); ok {
252231
// Get the next value in the nested map
@@ -266,3 +245,119 @@ func GetNestedContextKey(context map[string]interface{}, key string) (string, bo
266245
}
267246
return "", false
268247
}
248+
249+
// SetNestedKey sets a value to a nested key from a map using dot notation keys.
250+
func SetNestedKey(root map[string]interface{}, key string, value interface{}) error {
251+
parts, err := ParseNestedKey(key)
252+
if err != nil {
253+
return err
254+
}
255+
256+
current := root
257+
for i, part := range parts {
258+
if i == len(parts)-1 {
259+
// Set the value at the final key
260+
current[part] = value
261+
return nil
262+
}
263+
264+
// Traverse into nested maps or create them if they don't exist
265+
if next, exists := current[part]; exists {
266+
if nextMap, ok := next.(map[string]interface{}); ok {
267+
current = nextMap
268+
} else {
269+
return fmt.Errorf("key %q exists but is not a map", part)
270+
}
271+
} else {
272+
// Create a new map if the path doesn't exist
273+
newMap := make(map[string]interface{})
274+
current[part] = newMap
275+
current = newMap
276+
}
277+
}
278+
279+
return nil
280+
}
281+
282+
func getQueryFromStatus(req *fnv1.RunFunctionRequest, in *v1beta1.Input) error {
283+
oxr, err := request.GetObservedCompositeResource(req)
284+
if err != nil {
285+
return errors.Wrap(err, "cannot get observed composite resource")
286+
}
287+
xrStatus := make(map[string]interface{})
288+
err = oxr.Resource.GetValueInto("status", &xrStatus)
289+
if err != nil {
290+
return errors.Wrap(err, "cannot get XR status")
291+
}
292+
if queryFromXRStatus, ok := GetNestedKey(xrStatus, strings.TrimPrefix(*in.QueryRef, "status.")); ok {
293+
in.Query = queryFromXRStatus
294+
}
295+
return nil
296+
}
297+
298+
func putQueryResultToStatus(req *fnv1.RunFunctionRequest, rsp *fnv1.RunFunctionResponse, in *v1beta1.Input, results armresourcegraph.ClientResourcesResponse, f *Function) error {
299+
oxr, err := request.GetObservedCompositeResource(req)
300+
if err != nil {
301+
return errors.Wrap(err, "cannot get observed composite resource")
302+
}
303+
// The composite resource desired by previous functions in the pipeline.
304+
dxr, err := request.GetDesiredCompositeResource(req)
305+
if err != nil {
306+
return errors.Wrap(err, "cannot get desired composite resource")
307+
}
308+
dxr.Resource.SetAPIVersion(oxr.Resource.GetAPIVersion())
309+
dxr.Resource.SetKind(oxr.Resource.GetKind())
310+
311+
xrStatus := make(map[string]interface{})
312+
err = oxr.Resource.GetValueInto("status", &xrStatus)
313+
if err != nil {
314+
f.log.Debug("Cannot get status from XR")
315+
}
316+
317+
// Update the specific status field using the reusable function
318+
statusField := strings.TrimPrefix(in.Target, "status.")
319+
err = SetNestedKey(xrStatus, statusField, results.Data)
320+
if err != nil {
321+
return errors.Wrapf(err, "cannot set status field %s to %v", statusField, results.Data)
322+
}
323+
324+
// Write the updated status field back into the composite resource
325+
if err := dxr.Resource.SetValue("status", xrStatus); err != nil {
326+
return errors.Wrap(err, "cannot write updated status back into composite resource")
327+
}
328+
329+
// Save the updated desired composite resource
330+
if err := response.SetDesiredCompositeResource(rsp, dxr); err != nil {
331+
return errors.Wrapf(err, "cannot set desired composite resource in %T", rsp)
332+
}
333+
return nil
334+
}
335+
336+
func putQueryResultToContext(req *fnv1.RunFunctionRequest, rsp *fnv1.RunFunctionResponse, in *v1beta1.Input, results armresourcegraph.ClientResourcesResponse, f *Function) error {
337+
338+
contextField := strings.TrimPrefix(in.Target, "context.")
339+
data, err := structpb.NewValue(results.Data)
340+
if err != nil {
341+
return errors.Wrap(err, "cannot convert results data to structpb.Value")
342+
}
343+
344+
// Convert existing context into a map[string]interface{}
345+
contextMap := req.GetContext().AsMap()
346+
347+
err = SetNestedKey(contextMap, contextField, data.AsInterface())
348+
if err != nil {
349+
return errors.Wrap(err, "failed to update context key")
350+
}
351+
352+
f.log.Debug("Updating Composition Pipeline Context", "key", contextField, "data", &results.Data)
353+
354+
// Convert the updated context back into structpb.Struct
355+
updatedContext, err := structpb.NewStruct(contextMap)
356+
if err != nil {
357+
return errors.Wrap(err, "failed to serialize updated context")
358+
}
359+
360+
// Set the updated context
361+
rsp.Context = updatedContext
362+
return nil
363+
}

0 commit comments

Comments
 (0)