Skip to content

Commit c02d2f1

Browse files
committed
Initial operations support
1 parent 2885a63 commit c02d2f1

File tree

4 files changed

+322
-22
lines changed

4 files changed

+322
-22
lines changed

fn.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
fnv1 "github.com/crossplane/function-sdk-go/proto/v1"
2525
"github.com/crossplane/function-sdk-go/request"
2626
"github.com/crossplane/function-sdk-go/resource"
27+
"github.com/crossplane/function-sdk-go/resource/composite"
2728
"github.com/crossplane/function-sdk-go/response"
2829
)
2930

@@ -138,6 +139,13 @@ func (f *Function) getXRAndStatus(req *fnv1.RunFunctionRequest) (map[string]inte
138139

139140
// getObservedAndDesired gets both observed and desired XR resources
140141
func (f *Function) getObservedAndDesired(req *fnv1.RunFunctionRequest) (*resource.Composite, *resource.Composite, error) {
142+
if req.GetObserved().GetComposite() != nil {
143+
return getObservedAndDesiredInComposition(req)
144+
}
145+
return getObservedAndDesiredInOperation(req)
146+
}
147+
148+
func getObservedAndDesiredInComposition(req *fnv1.RunFunctionRequest) (*resource.Composite, *resource.Composite, error) {
141149
oxr, err := request.GetObservedCompositeResource(req)
142150
if err != nil {
143151
return nil, nil, errors.Wrap(err, "cannot get observed composite resource")
@@ -151,6 +159,45 @@ func (f *Function) getObservedAndDesired(req *fnv1.RunFunctionRequest) (*resourc
151159
return oxr, dxr, nil
152160
}
153161

162+
func getObservedAndDesiredInOperation(req *fnv1.RunFunctionRequest) (*resource.Composite, *resource.Composite, error) {
163+
rr, err := request.GetRequiredResources(req)
164+
if err != nil {
165+
return nil, nil, errors.Wrap(err, "operation: cannot get required resources")
166+
}
167+
168+
rs, found := rr["ops.crossplane.io/watched-resource"]
169+
if !found {
170+
return nil, nil, fmt.Errorf("operation: no resource to process with name %s", "ops.crossplane.io/watched-resource")
171+
}
172+
173+
if len(rs) != 1 {
174+
return nil, nil, fmt.Errorf("operation: incorrect number of resources sent to the function. expected 1, got %d", len(rs))
175+
}
176+
177+
r := rs[0]
178+
if r.Resource == nil {
179+
return nil, nil, errors.New("operation: Resource property in operation resource can not be nil")
180+
}
181+
182+
if len(r.Resource.Object) == 0 {
183+
return nil, nil, errors.New("operation: Resource.Object property in operation resource can not be empty")
184+
}
185+
186+
oxr := &resource.Composite{
187+
Resource: composite.New(),
188+
ConnectionDetails: make(resource.ConnectionDetails),
189+
}
190+
dxr := &resource.Composite{
191+
Resource: composite.New(),
192+
ConnectionDetails: make(resource.ConnectionDetails),
193+
}
194+
195+
oxr.Resource.Object = r.Resource.Object
196+
dxr.Resource.Object = r.Resource.Object
197+
198+
return oxr, dxr, nil
199+
}
200+
154201
// initializeAndCopyData initializes metadata and copies spec
155202
func (f *Function) initializeAndCopyData(oxr, dxr *resource.Composite) {
156203
// Initialize dxr from oxr if needed

fn_test.go

Lines changed: 257 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1660,6 +1660,11 @@ func TestRunFunction(t *testing.T) {
16601660
"queryType": "UserValidation",
16611661
"users": ["user@example.com"]
16621662
}`),
1663+
Observed: &fnv1.State{
1664+
Composite: &fnv1.Resource{
1665+
Resource: resource.MustStructJSON(xr),
1666+
},
1667+
},
16631668
},
16641669
},
16651670
want: want{
@@ -1675,8 +1680,14 @@ func TestRunFunction(t *testing.T) {
16751680
Desired: &fnv1.State{
16761681
Composite: &fnv1.Resource{
16771682
Resource: resource.MustStructJSON(`{
1678-
"apiVersion": "",
1679-
"kind": ""
1683+
"apiVersion": "example.org/v1",
1684+
"kind": "XR",
1685+
"metadata": {
1686+
"name": "cool-xr"
1687+
},
1688+
"spec": {
1689+
"count": 2
1690+
}
16801691
}`),
16811692
},
16821693
},
@@ -1694,6 +1705,11 @@ func TestRunFunction(t *testing.T) {
16941705
"queryType": "UserValidation",
16951706
"users": ["user@example.com"]
16961707
}`),
1708+
Observed: &fnv1.State{
1709+
Composite: &fnv1.Resource{
1710+
Resource: resource.MustStructJSON(xr),
1711+
},
1712+
},
16971713
Credentials: map[string]*fnv1.Credentials{
16981714
"azure-creds": {
16991715
Source: &fnv1.Credentials_CredentialData{CredentialData: creds},
@@ -1714,8 +1730,14 @@ func TestRunFunction(t *testing.T) {
17141730
Desired: &fnv1.State{
17151731
Composite: &fnv1.Resource{
17161732
Resource: resource.MustStructJSON(`{
1717-
"apiVersion": "",
1718-
"kind": ""
1733+
"apiVersion": "example.org/v1",
1734+
"kind": "XR",
1735+
"metadata": {
1736+
"name": "cool-xr"
1737+
},
1738+
"spec": {
1739+
"count": 2
1740+
}
17191741
}`),
17201742
},
17211743
},
@@ -2413,6 +2435,237 @@ func TestRunFunction(t *testing.T) {
24132435
},
24142436
},
24152437
},
2438+
"OperationWithoutWatchedResource": {
2439+
reason: "The Function should return fatal if it runs as operation without a watched resource",
2440+
args: args{
2441+
ctx: context.Background(),
2442+
req: &fnv1.RunFunctionRequest{
2443+
Meta: &fnv1.RequestMeta{Tag: "hello"},
2444+
Input: resource.MustStructJSON(`{
2445+
"apiVersion": "msgraph.fn.crossplane.io/v1alpha1",
2446+
"kind": "Input",
2447+
"queryType": "UserValidation",
2448+
"users": ["user@example.com"],
2449+
"target": "context.validatedUsers"
2450+
}`),
2451+
Credentials: map[string]*fnv1.Credentials{
2452+
"azure-creds": {
2453+
Source: &fnv1.Credentials_CredentialData{CredentialData: creds},
2454+
},
2455+
},
2456+
RequiredResources: map[string]*fnv1.Resources{},
2457+
},
2458+
},
2459+
want: want{
2460+
rsp: &fnv1.RunFunctionResponse{
2461+
Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)},
2462+
Results: []*fnv1.Result{
2463+
{
2464+
Severity: fnv1.Severity_SEVERITY_FATAL,
2465+
Message: `operation: no resource to process with name ops.crossplane.io/watched-resource`,
2466+
Target: fnv1.Target_TARGET_COMPOSITE.Enum(),
2467+
},
2468+
},
2469+
},
2470+
},
2471+
},
2472+
"OperationWithLessThanOneWatchedResource": {
2473+
reason: "The Function should return fatal if it runs as operation with less than one watched resource",
2474+
args: args{
2475+
ctx: context.Background(),
2476+
req: &fnv1.RunFunctionRequest{
2477+
Meta: &fnv1.RequestMeta{Tag: "hello"},
2478+
Input: resource.MustStructJSON(`{
2479+
"apiVersion": "msgraph.fn.crossplane.io/v1alpha1",
2480+
"kind": "Input",
2481+
"queryType": "UserValidation",
2482+
"users": ["user@example.com"],
2483+
"target": "context.validatedUsers"
2484+
}`),
2485+
Credentials: map[string]*fnv1.Credentials{
2486+
"azure-creds": {
2487+
Source: &fnv1.Credentials_CredentialData{CredentialData: creds},
2488+
},
2489+
},
2490+
RequiredResources: map[string]*fnv1.Resources{
2491+
"ops.crossplane.io/watched-resource": {
2492+
Items: nil,
2493+
},
2494+
},
2495+
},
2496+
},
2497+
want: want{
2498+
rsp: &fnv1.RunFunctionResponse{
2499+
Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)},
2500+
Results: []*fnv1.Result{
2501+
{
2502+
Severity: fnv1.Severity_SEVERITY_FATAL,
2503+
Message: `operation: incorrect number of resources sent to the function. expected 1, got 0`,
2504+
Target: fnv1.Target_TARGET_COMPOSITE.Enum(),
2505+
},
2506+
},
2507+
},
2508+
},
2509+
},
2510+
"OperationWithMoreThanOneWatchedResource": {
2511+
reason: "The Function should return fatal if it runs as operation with more than one watched resource",
2512+
args: args{
2513+
ctx: context.Background(),
2514+
req: &fnv1.RunFunctionRequest{
2515+
Meta: &fnv1.RequestMeta{Tag: "hello"},
2516+
Input: resource.MustStructJSON(`{
2517+
"apiVersion": "msgraph.fn.crossplane.io/v1alpha1",
2518+
"kind": "Input",
2519+
"queryType": "UserValidation",
2520+
"users": ["user@example.com"],
2521+
"target": "context.validatedUsers"
2522+
}`),
2523+
Credentials: map[string]*fnv1.Credentials{
2524+
"azure-creds": {
2525+
Source: &fnv1.Credentials_CredentialData{CredentialData: creds},
2526+
},
2527+
},
2528+
RequiredResources: map[string]*fnv1.Resources{
2529+
"ops.crossplane.io/watched-resource": {
2530+
Items: []*fnv1.Resource{
2531+
{
2532+
Resource: resource.MustStructJSON(xr),
2533+
},
2534+
{
2535+
Resource: resource.MustStructJSON(xr),
2536+
},
2537+
},
2538+
},
2539+
},
2540+
},
2541+
},
2542+
want: want{
2543+
rsp: &fnv1.RunFunctionResponse{
2544+
Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)},
2545+
Results: []*fnv1.Result{
2546+
{
2547+
Severity: fnv1.Severity_SEVERITY_FATAL,
2548+
Message: `operation: incorrect number of resources sent to the function. expected 1, got 2`,
2549+
Target: fnv1.Target_TARGET_COMPOSITE.Enum(),
2550+
},
2551+
},
2552+
},
2553+
},
2554+
},
2555+
"OperationWithNilObjectInWatchedResource": {
2556+
reason: "The Function should return fatal if it runs as operation watched resource with zero length Resource.Object",
2557+
args: args{
2558+
ctx: context.Background(),
2559+
req: &fnv1.RunFunctionRequest{
2560+
Meta: &fnv1.RequestMeta{Tag: "hello"},
2561+
Input: resource.MustStructJSON(`{
2562+
"apiVersion": "msgraph.fn.crossplane.io/v1alpha1",
2563+
"kind": "Input",
2564+
"queryType": "UserValidation",
2565+
"users": ["user@example.com"],
2566+
"target": "context.validatedUsers"
2567+
}`),
2568+
Credentials: map[string]*fnv1.Credentials{
2569+
"azure-creds": {
2570+
Source: &fnv1.Credentials_CredentialData{CredentialData: creds},
2571+
},
2572+
},
2573+
RequiredResources: map[string]*fnv1.Resources{
2574+
"ops.crossplane.io/watched-resource": {
2575+
Items: []*fnv1.Resource{
2576+
{},
2577+
},
2578+
},
2579+
},
2580+
},
2581+
},
2582+
want: want{
2583+
rsp: &fnv1.RunFunctionResponse{
2584+
Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)},
2585+
Results: []*fnv1.Result{
2586+
{
2587+
Severity: fnv1.Severity_SEVERITY_FATAL,
2588+
Message: `operation: Resource.Object property in operation resource can not be empty`,
2589+
Target: fnv1.Target_TARGET_COMPOSITE.Enum(),
2590+
},
2591+
},
2592+
},
2593+
},
2594+
},
2595+
"OperationWithWatchedResource": {
2596+
reason: "The Function should return fatal if it runs as operation watched resource with nil Resource",
2597+
args: args{
2598+
ctx: context.Background(),
2599+
req: &fnv1.RunFunctionRequest{
2600+
Meta: &fnv1.RequestMeta{Tag: "hello"},
2601+
Input: resource.MustStructJSON(`{
2602+
"apiVersion": "msgraph.fn.crossplane.io/v1alpha1",
2603+
"kind": "Input",
2604+
"queryType": "UserValidation",
2605+
"users": ["user@example.com"],
2606+
"target": "status.validatedUsers"
2607+
}`),
2608+
Credentials: map[string]*fnv1.Credentials{
2609+
"azure-creds": {
2610+
Source: &fnv1.Credentials_CredentialData{CredentialData: creds},
2611+
},
2612+
},
2613+
RequiredResources: map[string]*fnv1.Resources{
2614+
"ops.crossplane.io/watched-resource": {
2615+
Items: []*fnv1.Resource{
2616+
{
2617+
Resource: resource.MustStructJSON(xr),
2618+
},
2619+
},
2620+
},
2621+
},
2622+
},
2623+
},
2624+
want: want{
2625+
rsp: &fnv1.RunFunctionResponse{
2626+
Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)},
2627+
Conditions: []*fnv1.Condition{
2628+
{
2629+
Type: "FunctionSuccess",
2630+
Status: fnv1.Status_STATUS_CONDITION_TRUE,
2631+
Reason: "Success",
2632+
Target: fnv1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum(),
2633+
},
2634+
},
2635+
Results: []*fnv1.Result{
2636+
{
2637+
Severity: fnv1.Severity_SEVERITY_NORMAL,
2638+
Message: `QueryType: "UserValidation"`,
2639+
Target: fnv1.Target_TARGET_COMPOSITE.Enum(),
2640+
},
2641+
},
2642+
Desired: &fnv1.State{
2643+
Composite: &fnv1.Resource{
2644+
Resource: resource.MustStructJSON(`{
2645+
"apiVersion": "example.org/v1",
2646+
"kind": "XR",
2647+
"metadata": {
2648+
"name": "cool-xr"
2649+
},
2650+
"spec": {
2651+
"count": 2
2652+
},
2653+
"status": {
2654+
"validatedUsers": [
2655+
{
2656+
"id": "test-user-id",
2657+
"displayName": "Test User",
2658+
"userPrincipalName": "user@example.com",
2659+
"mail": "user@example.com"
2660+
}
2661+
]
2662+
}
2663+
}`),
2664+
},
2665+
},
2666+
},
2667+
},
2668+
},
24162669
}
24172670

24182671
for name, tc := range cases {

go.mod

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ require (
88
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0
99
github.com/alecthomas/kong v1.12.1
1010
github.com/crossplane/crossplane-runtime v1.20.0
11-
github.com/crossplane/function-sdk-go v0.4.0
11+
github.com/crossplane/function-sdk-go v0.5.0-rc.0.0.20250805171053-2910b68d255d
1212
github.com/google/go-cmp v0.7.0
1313
github.com/microsoft/kiota-authentication-azure-go v1.3.1
1414
github.com/microsoftgraph/msgraph-sdk-go v1.84.0
@@ -77,15 +77,15 @@ require (
7777
golang.org/x/crypto v0.41.0 // indirect
7878
golang.org/x/mod v0.27.0 // indirect
7979
golang.org/x/net v0.43.0 // indirect
80-
golang.org/x/oauth2 v0.27.0 // indirect
80+
golang.org/x/oauth2 v0.28.0 // indirect
8181
golang.org/x/sync v0.16.0 // indirect
8282
golang.org/x/sys v0.35.0 // indirect
8383
golang.org/x/term v0.34.0 // indirect
8484
golang.org/x/text v0.28.0 // indirect
8585
golang.org/x/time v0.9.0 // indirect
8686
golang.org/x/tools v0.36.0 // indirect
87-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
88-
google.golang.org/grpc v1.72.1 // indirect
87+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
88+
google.golang.org/grpc v1.73.0 // indirect
8989
gopkg.in/inf.v0 v0.9.1 // indirect
9090
gopkg.in/yaml.v2 v2.4.0 // indirect
9191
gopkg.in/yaml.v3 v3.0.1 // indirect

0 commit comments

Comments
 (0)