Repository navigation
Use the SDK's serve() so the function is one function - #23
Merged
stevendborrelli merged 6 commits intoAug 29, 2026
Merged
Conversation
The SDK grew a serve module in v0.7.0, so a function no longer has to hand-assemble its own gRPC server. This adopts it. src/main.ts drops from 85 lines to 6: the commander flag parsing, pino logger construction, server startup and signal handling were boilerplate nobody had edited since it was scaffolded. serve() does all of it. That boilerplate had also drifted into a bug. It trapped only SIGINT, so Kubernetes — which sends SIGTERM — was killing the function ungracefully on every rollout. serve() handles both. src/function.ts becomes a ComposeFunction rather than a class implementing FunctionHandler. serve() hands it a response already built from the request, so to(req) goes away, and ComposeResponse narrows desired to non-optional, so resources can be written straight to rsp.desired.resources. The getDesiredComposedResources() and setDesiredComposedResources() round trip is redundant once you hold that map directly, since to() aliases it rather than copying. commander and pino are no longer direct dependencies; the composition logic is otherwise untouched. Verified behaviour-preserving: the previous Function class and the new compose produce identical responses across every test-cases/ fixture, and crossplane render against a live server produces the same 17 documents as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README noted that TypeScript embedded functions need a crossplane CLI including crossplane/cli#170, but never said how to get one — leaving a contributor to work out that it means building a fork from source before anything else in the Development section will run. Add the clone and build steps, matching what CI's `cli` job does, ahead of Generating Schemas since the CLI is a prerequisite for it. A CLI built from source reports an empty client version, so the section says so rather than offering `crossplane version --client` as a check that would look like a broken install. Also document the function's -h/--help flag, which it gained along with the SDK's serve(). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
XRStatus was hand-written as `{ [key: string]: string | string[] }`, which
restates the status block in apis/network/definition.yaml and accepts any
field name at all. The CLI already generates a typed model for the XR, so
use it: XRStatus is now NonNullable<INetwork['status']>, and the XRD stays
the single source of truth.
Building the status by spreading `cond && { field }` would have made that
pointless — object spreads are not excess-property checked, so a misspelt
field still compiled. Assign the fields instead, which is checked:
`xrStatus.vpcID = vpcId` now fails with "Property 'vpcID' does not exist
... Did you mean 'vpcId'?" where before it compiled and silently wrote a
field no consumer reads.
Runtime behaviour is unchanged — fields are still omitted rather than set
to undefined when empty. crossplane render output is byte-identical, and
the existing status propagation test still passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every composed resource was converted with the same three lines:
desiredComposed['igw'] = Resource.fromJSON({
resource: igw.toJSON(),
});
fromModel() is the SDK helper for exactly this — it calls toJSON() on a
kubernetes-models object and wraps the result — so the ten call sites
collapse to one line each and Resource is no longer imported.
It is a drop-in: Resource.fromJSON already defaults connectionDetails to
{} and ready to READY_UNSPECIFIED, which is what fromModel() passes
explicitly, so the two produce identical protobuf encodings. crossplane
render output is byte-identical.
validate() stays an explicit separate call. No SDK helper validates, and
keeping it where the resource is constructed is what makes a schema
violation name the offending resource.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The "Updating the Function" example still showed the pre-fromModel way of adding a composed resource, which is the one snippet a contributor is most likely to copy. Update it, and say why validate() remains a separate call rather than looking like a leftover. Also correct the project tree: main.ts no longer assembles a gRPC server. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The function catches errors and turns them into a fatal result, but nothing
exercised that branch — every fixture was a well-formed Network. The branch
matters more than it looks: the SDK types an observed resource as
{ [key: string]: any }, so every parameter read off the XR is `any` and
TypeScript cannot catch a missing one. validate() is the only thing between
a missing field and a malformed resource reaching the API server.
Add three cases around an XR with no region: a fatal result naming the
field, no composed resources emitted, and the same input composing cleanly
once region is present.
Confirmed the tests are not vacuous by stripping the validate() calls, at
which point the first two fail and the function silently emits a VPC with
no region.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description of your changes
The TypeScript function SDK grew a
servemodule in v0.7.0, so a composition function no longer has to hand-assemble its own gRPC server. This adopts it, and picks up two other SDK/codegen affordances the function was not using.Use
serve()for the entrypointsrc/main.tsdrops from 85 lines to 6. The commander flag parsing, pino logger construction, server startup and signal handling were scaffolded boilerplate that nobody had edited since:src/function.tsbecomes aComposeFunctioninstead of a class implementingFunctionHandler.serve()hands it a response already built from the request, soto(req)goes away, andComposeResponsenarrowsdesiredto non-optional so resources are written straight torsp.desired.resourceswith no!. ThegetDesiredComposedResources()/setDesiredComposedResources()round trip is redundant once you hold that map directly, sinceto()aliases it rather than copying.commanderandpinoare no longer direct dependencies.Take the XR status type from the generated model
XRStatuswas hand-written as{ [key: string]: string | string[] }, restating the status block inapis/network/definition.yamland accepting any field name. It now comes from the model the CLI already generates:The status is built by assignment rather than by spreading
cond && { field }, because object spreads are not excess-property checked — with the spread form a misspelt field still compiled, making the precise type worthless at the one place it mattered. NowxrStatus.vpcID = vpcIdfails withProperty 'vpcID' does not exist ... Did you mean 'vpcId'?.Use
fromModel()for composed resourcesTen call sites of
become
desiredComposed['igw'] = fromModel(igw).validate()stays a separate explicit call — no SDK helper validates, and since every value read off the XR isanyat runtime, it is the only thing standing between a missing field and a malformed resource reaching the API server.Docs
The README never said how to get a
crossplaneCLI with crossplane/cli#170, which is a prerequisite for everything in the Development section — added the clone and build steps. Also refreshed the "Updating the Function" example, which showed the superseded pattern.Testing
Verified behaviour-preserving, not merely green:
Functionclass and the newcomposeproduce identical responses across every fixture intest/test-cases/, checked by running both against the same inputs and asserting deep equality. This also confirms the SDK change is additive — the old class still compiles and runs unmodified on 0.7.0.crossplane renderoutput is byte-identical before and after every commit here.Resource.fromJSONandfromModelwere compared directly: identical protobuf encodings, so the refactor is a true drop-in.build,lintandformat:checkclean.--helprenders and the server shuts down cleanly onSIGTERMwith exit 0.test/test-helpers.tsandtsconfig.eslint.jsoncarry formatting-only changes fromnpm run format; they had pre-existing drift.I have:
🤖 Generated with Claude Code