Skip to content

feat: support for advertised capabilities and required schemas#256

Merged
jbw976 merged 6 commits intocrossplane:mainfrom
jbw976:schemas
Feb 18, 2026
Merged

feat: support for advertised capabilities and required schemas#256
jbw976 merged 6 commits intocrossplane:mainfrom
jbw976:schemas

Conversation

@jbw976
Copy link
Member

@jbw976 jbw976 commented Feb 8, 2026

Description of your changes

Similar to the python SDK update in crossplane/function-sdk-python#193, this PR updates the Go SDK to support advertised capabilities and required schemas, which were added to Crossplane in crossplane/crossplane#7022.

I have:

  • Read and followed Crossplane's contribution process.
  • Run make reviewable to ensure this PR is ready for review.

How has this code been tested

I have tested these changes locally in a function and verified that requesting schemas works as expected and capabilities are being advertised as expected.

…nd required schemas

Signed-off-by: Jared Watts <jbw976@gmail.com>
Signed-off-by: Jared Watts <jbw976@gmail.com>
Signed-off-by: Jared Watts <jbw976@gmail.com>
Signed-off-by: Jared Watts <jbw976@gmail.com>
@coderabbitai
Copy link

coderabbitai bot commented Feb 8, 2026

📝 Walkthrough

Walkthrough

Adds capability advertisement and required-schema negotiation to function execution protos (v1 and v1beta1): new Capability enum, SchemaSelector and Schema messages, required_schemas/schemas/capabilities fields; Go helpers to read/advertise capabilities and retrieve/require OpenAPI v3 schemas; tests and small test-output formatting tweaks.

Changes

Cohort / File(s) Summary
Protocol Buffer Definitions
proto/v1/run_function.proto, proto/v1beta1/run_function.proto
Added Capability enum; added required_schemas to RunFunctionRequest, capabilities to RequestMeta, and schemas to Requirements. Introduced SchemaSelector and Schema messages. Minor comment normalization.
Request package (helpers & tests)
request/request.go, request/request_test.go
New helpers: GetRequiredResource, GetRequiredSchemas, GetRequiredSchema, AdvertisesCapabilities, HasCapability. Tests covering schema retrieval and capability checking; added imports for slices, structpb, and protocmp.
Response package (helpers & tests)
response/response.go, response/response_test.go
Added RequireSchema(rsp, name, apiVersion, kind) to populate Requirements.Schemas; tests added/adjusted (including stricter JSON unmarshal handling in test helper).
SDK test formatting
sdk_test.go
Example test output now compacts JSON with json.Compact before printing.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Function as Function (plugin)
participant Runner as Runner / Engine
participant Store as Schema Store (CRD/API server)
Function->>Runner: RunFunctionRequest (includes capabilities, required_schemas)
Runner->>Runner: Inspect Request.Meta.capabilities
alt Runner supports required_schemas
Runner->>Store: Resolve SchemaSelector(s) for requested names
Store-->>Runner: OpenAPI v3 schema(s) (Struct)
Runner-->>Function: RunFunctionResponse (resolved schemas OR requirements to fetch)
else Runner does not support capabilities
Runner-->>Function: RunFunctionResponse (no schema resolution; may include Requirements.Schemas)
end

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Thanks — this looks complete; let me know if you want the diagram expanded (e.g., old vs new flow) or a focused review checklist for the proto and Go API changes.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: adding support for advertised capabilities and required schemas to the Go SDK.
Description check ✅ Passed The description clearly explains the changes, references related work in Python SDK and Crossplane, and documents testing performed and contribution process compliance.
Breaking Changes ✅ Passed PR introduces only additive changes to public Go API with new exported functions and proto fields, no breaking modifications to existing signatures.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

// a protobuf Struct, or nil if the schema was not found. Returns nil both when
// Crossplane has not yet resolved the requirement and when it was resolved but
// the schema wasn't found. To distinguish between these cases, check whether
// the name exists in req.GetRequiredSchemas().
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth adding GetRequiredSchemas (plural)? I'm on the fence. It'd be nice for documentation purposes and to stay consistent with GetRequiredResources but I imagine it wouldn't actually do much.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To distinguish between these cases, check whether the name exists in req.GetRequiredSchemas().

It occurs to me that a better UX would be for this to return (*structpb.Struct, bool) with the bool indicating whether the resource was resolved. I'd like to stay consistent with GetRequiredResource though. I could be convinced a better UX is worth a breaking change here, given this SDK is pre-1.0.

Copy link
Member Author

@jbw976 jbw976 Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds reasonable to have a (*structpb.Struct, bool) return here - I agree that's a better UX than the current "nil means two things" ambiguity. The (value, bool) pattern is more idiomatic Go, so it'll feel natural to consumers of this function.

For consistency across GetRequiredResource* and GetRequiredSchema*, let's do the following:

  • Update GetRequiredSchema to return (*structpb.Struct, bool) where bool indicates whether Crossplane resolved the requirement
  • Add GetRequiredSchemas (plural) for consistency with GetRequiredResources - likely a thin wrapper that doesn't do a ton
  • Add GetRequiredResource (singular) with the same (value, bool) pattern to stay consistent with GetRequiredSchema. This singular version doesn't actually exist yet, so it doesn't require a signature change.

None of those are breaking changes because the only changing signature is in GetRequiredSchema which is new in this PR.

With the Python SDK, the get_required_schema API has already shipped in https://github.com/crossplane/function-sdk-python/releases/tag/v0.11.0, so changing anything there would indeed be a breaking change. However, Claude tells me that returning dict | None is already idiomatic Python for "not found." I'm good with each SDK following the conventions of its own language rather than mirror each other 1:1. Sound reasonable?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i pushed a new commit that implemented the approach described above, let me know what folks think! 🙇‍♂️

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
request/request.go (1)

134-154: Nice API design — the three-value return matches GetRequiredSchema well.

One small question: the error from resource.AsObject (line 148) propagates unwrapped, which is consistent with GetRequiredResources above (line 125). However, the coding guidelines ask that error messages include context about what the user was trying to do. Would it be worth wrapping with something like errors.Wrapf(err, "cannot get required resource %q", name) to help users diagnose which resource failed? Totally up to you — I can see the argument for consistency with GetRequiredResources too.

🔧 Optional: wrap the error for better diagnostics
 	for _, i := range rrs.GetItems() {
 		r := &resource.Required{Resource: &unstructured.Unstructured{}}
 		if err := resource.AsObject(i.GetResource(), r.Resource); err != nil {
-			return nil, true, err
+			return nil, true, errors.Wrapf(err, "cannot get required resource %q", name)
 		}
 		out = append(out, *r)
 	}

As per coding guidelines, **/*.go: "Ensure all error messages are meaningful to end users, not just developers - avoid technical jargon, include context about what the user was trying to do, and suggest next steps when possible."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@request/request.go` around lines 134 - 154, Wrap the error returned by
resource.AsObject in GetRequiredResource with contextual information so users
know which required resource failed to parse; replace the direct return of err
(from resource.AsObject in the loop inside GetRequiredResource) with a wrapped
error that includes the resource name (name) and a short message like "cannot
get required resource %q" to improve diagnostics while preserving the original
error.
request/request_test.go (1)

469-553: Thorough test coverage for GetRequiredResource — nice work!

The test cases cover the key scenarios well (no resources, wrong name, found). One thing I noticed: line 548 uses cmp.Diff(tc.want.err, err) without cmpopts.EquateErrors(). This works because all current cases have nil errors, but it would panic or give confusing diffs if you later add an error test case. I see existing tests in this file follow the same pattern, so this is pre-existing — just flagging for awareness. Could you confirm whether that's intentional or something to address across the board later?

🔧 Optional: use cmpopts.EquateErrors() for future-proofing
+	"github.com/google/go-cmp/cmp/cmpopts"
-			if diff := cmp.Diff(tc.want.err, err); diff != "" {
+			if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" {

As per coding guidelines, **/*_test.go: "use cmp.Diff with cmpopts.EquateErrors() for error testing."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@request/request_test.go` around lines 469 - 553, Update the error comparison
in TestGetRequiredResource to use cmpopts.EquateErrors() so error diffs don't
panic or misreport: change the last assertion to cmp.Diff(tc.want.err, err,
cmpopts.EquateErrors()) and add the cmpopts import
("github.com/google/go-cmp/cmp/cmpopts") at the top of the test file; refer to
TestGetRequiredResource and the cmp.Diff(...) that compares tc.want.err and err.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@request/request_test.go`:
- Around line 469-553: Update the error comparison in TestGetRequiredResource to
use cmpopts.EquateErrors() so error diffs don't panic or misreport: change the
last assertion to cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()) and add the
cmpopts import ("github.com/google/go-cmp/cmp/cmpopts") at the top of the test
file; refer to TestGetRequiredResource and the cmp.Diff(...) that compares
tc.want.err and err.

In `@request/request.go`:
- Around line 134-154: Wrap the error returned by resource.AsObject in
GetRequiredResource with contextual information so users know which required
resource failed to parse; replace the direct return of err (from
resource.AsObject in the loop inside GetRequiredResource) with a wrapped error
that includes the resource name (name) and a short message like "cannot get
required resource %q" to improve diagnostics while preserving the original
error.

…and GetRequiredSchema*

Signed-off-by: Jared Watts <jbw976@gmail.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
request/request.go (1)

134-153: Consider extracting shared logic with GetRequiredResources.

Thanks for this clean API addition! I notice that GetRequiredResource (lines 136–153) and GetRequiredResources (lines 119–132) share nearly identical item-parsing logic. Would it make sense to extract the inner loop into a small helper (e.g., parseRequiredItems) and have both functions call it? This would reduce the duplication and make future changes (e.g., populating additional fields on resource.Required) less error-prone.

Totally fine to defer this if you'd prefer to keep the PR focused — just wanted to flag it for discussion.

♻️ Possible helper extraction
+func parseRequiredItems(rrs *v1.Resources) ([]resource.Required, error) {
+	out := make([]resource.Required, 0, len(rrs.GetItems()))
+	for _, i := range rrs.GetItems() {
+		r := &resource.Required{Resource: &unstructured.Unstructured{}}
+		if err := resource.AsObject(i.GetResource(), r.Resource); err != nil {
+			return nil, err
+		}
+		out = append(out, *r)
+	}
+	return out, nil
+}

Then GetRequiredResources and GetRequiredResource can both delegate to it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@request/request.go` around lines 134 - 153, Extract the duplicated loop that
converts proto items to []resource.Required into a helper (e.g.,
parseRequiredItems) and have both GetRequiredResources and GetRequiredResource
call it; the helper should accept the proto Items slice (or the
ResourceRequirementSet/rrs.GetItems()) and return ([]resource.Required, error),
performing the current logic that allocates resource.Required with Resource:
&unstructured.Unstructured{}, calls resource.AsObject on each i.GetResource(),
and appends to the output slice, so callers only handle presence checks and the
boolean resolution flag while reusing the shared parsing code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@request/request.go`:
- Around line 134-153: Extract the duplicated loop that converts proto items to
[]resource.Required into a helper (e.g., parseRequiredItems) and have both
GetRequiredResources and GetRequiredResource call it; the helper should accept
the proto Items slice (or the ResourceRequirementSet/rrs.GetItems()) and return
([]resource.Required, error), performing the current logic that allocates
resource.Required with Resource: &unstructured.Unstructured{}, calls
resource.AsObject on each i.GetResource(), and appends to the output slice, so
callers only handle presence checks and the boolean resolution flag while
reusing the shared parsing code.

}
}

func TestGetRequiredSchemas(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetRequiredSchema has a dedicated SchemaResolvedButNotFound case, but the TestGetRequiredSchemas doesn't verify that an empty Schema{} maps to a nil value in the output. Worth adding since GetRequiredSchemas silently flattens that distinction:

"ResolvedButNotFound": {
    reason: "A schema resolved but not found should appear as nil in the output map.",
    req: &v1.RunFunctionRequest{
        RequiredSchemas: map[string]*v1.Schema{"test": {}},
    },
    want: map[string]*structpb.Struct{"test": nil},
},

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool, i'll add this test case!

// AdvertisesCapabilities returns true if Crossplane advertises its capabilities
// in the request metadata. Crossplane v2.2 and later advertise capabilities. If
// this returns false, the calling Crossplane predates capability advertisement
// and HasCapability will always return False, even for features the older
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False should be lowercase false — it refers to the Go keyword

for _, i := range rrs.GetItems() {
r := &resource.Required{Resource: &unstructured.Unstructured{}}
if err := resource.AsObject(i.GetResource(), r.Resource); err != nil {
return nil, true, err
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in Line36 we wrapping the errors - is it fine here without context?

perhaps something like
return nil, true, errors.Wrap(err, "cannot convert required resource to object")

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think coderabbit said the same thing - i was choosing to remain consistent with the error handling behavior of the existing GetRequiredResources() function. It does not wrap this same error either.

Copy link
Member

@haarchri haarchri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jbw976 thanks, overall the changes are well-structured. Proto is kept in sync across v1 and v1beta1

only a few nits ;)

…ved but not found

Signed-off-by: Jared Watts <jbw976@gmail.com>
Copy link
Member Author

@jbw976 jbw976 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review @haarchri, I've incorporated/responded to all of your feedback! 🙇‍♂️

@jbw976 jbw976 merged commit 553e025 into crossplane:main Feb 18, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Comments