Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ spec:
| `servicePrincipalsRef` | string | Reference to resolve a list of service principal names from `spec`, `status` or `context` (e.g., `spec.servicePrincipalConfig.names`) |
| `target` | string | Required. Where to store the query results. Can be `status.<field>` or `context.<field>` |
| `skipQueryWhenTargetHasData` | bool | Optional. When true, will skip the query if the target already has data |
| `queryInterval` | string | Optional. Minimum interval between queries as a Go duration string (e.g. `10m`, `1h`, `90s`). Skips querying Microsoft Graph until the interval has elapsed since the last successful query, independent of reconcile frequency. Only effective in Composition mode with a `status.` target. |
| `FailOnEmpty` | bool | Optional. When true, the function will fail if the `users`, `groups`, or `servicePrincipals` lists are empty, or if their respective reference fields are empty lists. |
| `identity.type` | string | Optional. Type of identity credentials to use. Valid values: `AzureServicePrincipalCredentials`, `AzureWorkloadIdentityCredentials`. Default is `AzureServicePrincipalCredentials` |

Expand All @@ -296,6 +297,59 @@ target: "context.results"
target: "context.[apiextensions.crossplane.io/environment].results"
```

## Throttling Mitigation

Microsoft Graph enforces API request throttling. The function offers two
complementary controls to reduce how often it calls the Graph API:

- `skipQueryWhenTargetHasData` — skips the query whenever the target already
holds data. Useful when the data only needs to be fetched once.
- `queryInterval` — skips the query until a minimum interval has elapsed since
the last successful query, then refreshes the data. Useful when the data
should be kept reasonably fresh without querying on every reconcile.

These are intended as alternative strategies. If both are set, the interval is
checked first, but `skipQueryWhenTargetHasData` takes over once the target holds
data, so the interval refresh never runs. Pick one per pipeline step; the
function emits a non-fatal warning when both are configured.

### Query Interval

`queryInterval` accepts a Go duration string (for example `10m`, `1h` or `90s`).
When set, the function records the timestamp of its last successful query
alongside the result stored at the status target and skips querying Microsoft
Graph again until the interval has elapsed — regardless of how frequently the
Composition reconciles.

```yaml
target: "status.validatedUsers"
queryInterval: "10m"
```

The timestamp is persisted as an extra `lastQueryTime` element appended to the
result list at the status target, for example:

```yaml
status:
validatedUsers:
- id: "a1b2c3"
displayName: "Jane Doe"
userPrincipalName: "jane@example.com"
mail: "jane@example.com"
- lastQueryTime: "2026-07-16T10:00:00Z"
```

Because the interval check reads the persisted timestamp back from the XR status
on subsequent reconciles, `queryInterval` is only effective in **Composition
mode with a `status.` target**. It has no effect for `context.` targets (context
is not persisted across reconciles) or in Operation mode, where the query
cadence is controlled by the `CronOperation` schedule or `WatchOperation`
instead.

> Note: consumers of the result list should ignore the trailing element that
> carries `lastQueryTime` (it has no `id`), as it is metadata rather than a query
> result.

## Using Reference Fields

You can reference values from XR spec, status, or context instead of hardcoding them:
Expand Down
20 changes: 20 additions & 0 deletions example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,23 @@ crossplane render xr.yaml service-principal-example-context-ref.yaml functions.y
```shell
crossplane render xr.yaml service-principal-example-spec-ref.yaml functions.yaml --function-credentials=./secrets/azure-creds.yaml -rc
```

### 5. Query Interval (Throttling)

Throttle calls to Microsoft Graph with `queryInterval` (a Go duration string, e.g. `10m`). On a successful query the function appends a `lastQueryTime` timestamp to the results at the status target and, on later reconciles, skips querying until the interval has elapsed. It is only effective in Composition mode with a `status.` target.

Run the query and observe the appended `lastQueryTime` element under `status.validatedUsers`:

```shell
crossplane render xr.yaml user-validation-example-query-interval.yaml functions.yaml --function-credentials=./secrets/azure-creds.yaml -r
```

To observe the skip, render against an XR that already holds a recent `lastQueryTime`. `crossplane render` treats the XR file as the observed composite, so the function reads that timestamp and, while the interval has not elapsed, skips the query and emits a `FunctionSkip`/`IntervalLimit` condition instead of calling Graph:

```shell
crossplane render xr-with-last-query-time.yaml user-validation-example-query-interval.yaml functions.yaml --function-credentials=./secrets/azure-creds.yaml -r
```

> `xr-with-last-query-time.yaml` uses a far-future `lastQueryTime` so the skip is deterministic; set it to a real recent time to test the natural elapsed boundary. The credentials are not used on the skip path (no Graph call is made), so they need not be valid for this command.

> **macOS note:** these commands use `-r` (function results) rather than `-rc`. The `-c`/`--include-context` flag makes `crossplane render` v2.x run an internal context-extraction step over a unix socket bind-mounted into its Docker helper container, which Docker Desktop for macOS does not support (`connect: operation not supported`) — the render then hangs with no output. Since the query-interval results are written to a `status.` target, `-c` is unnecessary here. This is a known CLI bug ([crossplane/cli#161](https://github.com/crossplane/cli/issues/161)), fixed by [#163](https://github.com/crossplane/cli/pull/163) (context function now listens on TCP) but not yet in a tagged release as of CLI v2.4.0. Until then, drop `-c` on macOS, or run `crossplane render` on Linux / a CLI built from `main` if you need context output.
59 changes: 59 additions & 0 deletions example/e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# queryInterval e2e (Composition mode)

Manifests to exercise the `queryInterval` throttling feature end-to-end on a real
cluster, where the time-based skip/refresh loop can actually be observed across
reconciles (something `crossplane render` cannot show).

## Files

| File | Purpose |
|------|---------|
| `function.yaml` | Installs `function-msgraph` (pin the tag you want to test) |
| `composition.yaml` | UserValidation → `status.validatedUsers`, `queryInterval: "2m"` |
| `xr.yaml` | A composite resource instance |

Reuses `../definition.yaml` (XRD) and the `azure-account-creds` secret built from
`../secrets/azure-creds.yaml` — see [Update Credentials](../README.md#update-credentials).
Populate it locally; keep real values out of commits.

## Run

```shell
kind create cluster --name msgraph-e2e
helm install crossplane crossplane-stable/crossplane \
-n crossplane-system --create-namespace --version 2.3.3 --wait

kubectl apply -f example/e2e/function.yaml
kubectl wait function.pkg.crossplane.io/function-msgraph --for=condition=Healthy --timeout=180s

kubectl apply -f example/definition.yaml
kubectl apply -f example/e2e/composition.yaml
kubectl apply -f example/secrets/azure-creds.yaml # populate locally; keep real values out of commits
kubectl apply -f example/e2e/xr.yaml
```

## Verify

```shell
# status carries the results plus a lastQueryTime element
kubectl get xr msgraph-query-interval-e2e -o jsonpath='{.status.validatedUsers}' | jq

# within the interval, reconciles skip (condition is set on the XR)
kubectl get xr msgraph-query-interval-e2e -o jsonpath='{.status.conditions}' | jq # FunctionSkip/IntervalLimit

# count real Graph calls vs skips in the function logs
POD=$(kubectl get pods -n crossplane-system -o name | grep msgraph)
kubectl logs -n crossplane-system "$POD" | grep -c 'Query Type' # real queries
kubectl logs -n crossplane-system "$POD" | grep -c 'interval limit' # skips
```

Force reconciles with `kubectl annotate xr msgraph-query-interval-e2e poke=$(date +%s) --overwrite`.
Poking repeatedly inside the 2m window leaves the query count flat; after 2m
elapses the next reconcile re-queries and `lastQueryTime` advances.

## Notes

- `queryInterval` is effective only in Composition mode with a `status.` target.
- The example XRD resolves to `LegacyCluster` scope under Crossplane v2, so the XR
is cluster-scoped (no namespace).
- Teardown: `kind delete cluster --name msgraph-e2e`.
28 changes: 28 additions & 0 deletions example/e2e/composition.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: user-validation-query-interval-e2e
spec:
compositeTypeRef:
apiVersion: example.crossplane.io/v1
kind: XR
mode: Pipeline
pipeline:
- step: validate-user
functionRef:
name: function-msgraph
input:
apiVersion: msgraph.fn.crossplane.io/v1alpha1
kind: Input
queryType: UserValidation
users:
- "yury@upbound.io"
target: "status.validatedUsers"
# Short interval so the skip/refresh loop is observable within a test session.
queryInterval: "2m"
credentials:
- name: azure-creds
source: Secret
secretRef:
namespace: crossplane-system
name: azure-account-creds
7 changes: 7 additions & 0 deletions example/e2e/function.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
name: function-msgraph
spec:
package: xpkg.upbound.io/upbound/function-msgraph:v0.7.0-rc3
8 changes: 8 additions & 0 deletions example/e2e/xr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: example.crossplane.io/v1
kind: XR
metadata:
name: msgraph-query-interval-e2e
spec:
userAccess:
emails:
- "yury@upbound.io"
38 changes: 38 additions & 0 deletions example/user-validation-example-query-interval.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: user-validation-example-query-interval
# Important: This function example requires an Azure AD app registration with Microsoft Graph API permissions:
# - User.Read.All
# - Directory.Read.All
spec:
compositeTypeRef:
apiVersion: example.crossplane.io/v1
kind: XR
mode: Pipeline
pipeline:
- step: validate-user
functionRef:
name: function-msgraph
input:
apiVersion: msgraph.fn.crossplane.io/v1alpha1
kind: Input
queryType: UserValidation
# Replace these with actual users in your directory
users:
- "admin@example.onmicrosoft.com"
- "user@example.onmicrosoft.com"
- "yury@upbound.io"
target: "status.validatedUsers"
# queryInterval throttles calls to Microsoft Graph independently of how
# often the Composition reconciles. The function records the timestamp of
# its last successful query alongside the result at the status target and
# skips querying again until the interval (a Go duration string) elapses.
# Only effective in Composition mode with a "status." target.
queryInterval: "10m"
credentials:
- name: azure-creds
source: Secret
secretRef:
namespace: crossplane-system
name: azure-account-creds
19 changes: 19 additions & 0 deletions example/xr-with-last-query-time.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# XR that already carries a lastQueryTime in status.validatedUsers, used to
# demonstrate queryInterval skipping. `crossplane render` treats this XR as the
# observed composite, so the function reads the timestamp below and skips the
# Microsoft Graph query while the interval has not elapsed.
#
# The lastQueryTime here is set far in the future so the skip is deterministic
# regardless of when you run the command. Change it to a real recent time
# (within your queryInterval window) to exercise the natural elapsed boundary.
apiVersion: example.crossplane.io/v1
kind: XR
metadata:
name: example-xr
status:
validatedUsers:
- id: "existing-user-id"
displayName: "Existing User"
userPrincipalName: "user@example.onmicrosoft.com"
mail: "user@example.onmicrosoft.com"
- lastQueryTime: "2099-01-01T00:00:00Z"
Loading
Loading