This repository is a template for building Crossplane composition functions in TypeScript using the @crossplane-org/function-sdk-typescript.
- Overview
- Running the Example Package
- Installing the Package
- Development Prerequisites
- Project Structure
- Installation
- Development
- Building and Packaging
- Implementation Guide
- TypeScript Configuration
- GitHub Actions
- Dependencies
- Notes
- Troubleshooting
- License
- Author
This template provides a full Typescript project for developing Crossplane functions that can transform, validate, and generate Kubernetes resources within Crossplane compositions.
The initial src/function.ts creates sample Deployment, Ingress, Service, and ServiceAccount resources and can be customized to create any type of Kubernetes resource.
For an example of configuring cloud resources, refer to configuration-aws-network-ts.
The configuration and function are published to the Upbound Marketplace, and can be installed into a Crossplane environment.
The Configuration package will install the function package, which contains a Node docker image and the source code as a dependency.
apiVersion: pkg.crossplane.io/v1
kind: Configuration
metadata:
name: configuration-template-typescript
spec:
package: xpkg.upbound.io/function-template-typescript:v0.1.0Once installed, confirm that the package and dependencies are installed:
crossplane beta trace con
figuration.pkg configuration-template-typescript
NAME VERSION INSTALLED HEALTHY STATE STATUS
Configuration/configuration-template-typescript v0.1.0 True True - HealthyPackageRevision
├─ ConfigurationRevision/configuration-template-typescript-93b73b00eb21 v0.1.0 - - Active
├─ Function/crossplane-contrib-function-auto-ready v0.6.0 True True - HealthyPackageRevision
│ └─ FunctionRevision/crossplane-contrib-function-auto-ready-59868730b9a9 v0.6.0 - - Active
└─ Function/upbound-function-template-typescript-function v0.1.0 True True - HealthyPackageRevision
└─ FunctionRevision/upbound-function-template-typescript-function-cd83fe939bc7 v0.1.0 -Once the package is installed and healthy, create the target namespace and install the example:
$ kubectl apply -f examples/app/ns.yaml
namespace/example created
$ kubectl apply -f examples/apps/example.yaml
app.platform.upbound.io/hello-app createdUse crossplane beta trace to validate the Composition:
crossplane beta trace -n example app.platform.upbound.io/hello-app
NAME SYNCED READY STATUS
App/hello-app (example) True True Available
├─ Deployment/hello-app-7ff730da5be9 (example) - -
├─ ServiceAccount/my-service-account (example) - -
└─ Service/hello-app-ab25df85445e (example) - -Next, examine the resources in the namespace:
kubectl get all,sa -n example -l app.kubernetes.io/instance=hello-app
NAME READY STATUS RESTARTS AGE
pod/hello-app-7ff730da5be9-76975c8c4c-mth2n 1/1 Running 0 5m36s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/hello-app-ab25df85445e ClusterIP 10.96.153.148 <none> 8080/TCP 5m36s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/hello-app-7ff730da5be9 1/1 1 1 5m36s
NAME DESIRED CURRENT READY AGE
replicaset.apps/hello-app-7ff730da5be9-76975c8c4c 1 1 1 5m36s
NAME SECRETS AGE
serviceaccount/my-service-account 0 5m36sChange the settings in examples/apps/example.yaml and observe the generated resources.
To develop Compositions using Typescript, the following is recommended:
- Node.js 24 or later recommended.
- npm
- Docker (for building the Node container image)
- Both TypeScript 5+ and TypeScript 7 (tsgo) are supported.
.
├── src/ # Source files
│ ├── function.ts # Main function implementation
│ ├── function.test.ts # Function tests
│ ├── test-helpers.ts # Test utilities for loading YAML test cases
│ └── main.ts # Entry point and server setup
├── test-cases/ # YAML-based test cases
│ ├── README.md # Test case documentation
│ └── example.yaml # Example test case
├── examples/ # Example Crossplane resources
│ ├── apps/ # Example application resources
│ └── functions.yaml # Function pipeline configuration
├── scripts/ # Build and deployment scripts
│ ├── function-docker-build.sh
│ ├── function-xpkg-build.sh
│ ├── function-xpkg-push.sh
│ ├── configuration-xpkg-build.sh
│ └── configuration-xpkg-push.sh
├── package-configuration/ # Configuration package metadata
│ ├── apis # Crossplane Composition Files
│ │ └── apps # Directory for the Kubernetes App Kind
│ │ ├── composition.yaml # Crossplane Composition Pipeline Definition
│ │ └── definition.yaml # Crossplane CompositeResourceDefinition
│ └── crossplane.yaml # Configuration package manifest
├── package-function/ # Function package metadata
│ └── crossplane.yaml # Function package manifest
├── dist/ # Compiled JavaScript output (generated)
├── _build/ # Build artifacts (generated)
├── package.json # Dependencies and scripts
├── tsconfig.json # TypeScript configuration
├── tsconfig.eslint.json # TypeScript ESLint configuration
├── jest.config.js # Jest test configuration
├── eslint.config.js # ESLint configuration
├── env # Environment variables for build scripts
└── Dockerfile # Container image definition
- Clone this repository
- Install dependencies:
npm installCompile TypeScript to JavaScript using TypeScript 5:
npm run tscTypeScript 7 (tsgo) is the default engine for the build:
npm run build
# or
npm run tsgoCheck types without emitting files:
npm run check-typesRun tests using Jest:
npm testTypeScript tests are written in src/function.test.ts. YAML-based test cases can be created using the test-cases/ directory. See test-cases/README.md for more information on creating tests. Example Crossplane resources for testing are provided in the examples/ directory.
The project includes ESLint and Prettier for code quality:
# Lint code
npm run lint
# Auto-fix linting issues
npm run lint:fix
# Format code with Prettier
npm run format
# Check formatting without changing files
npm run format:checkRun the function server in insecure mode for local testing:
npm run local
# or
node dist/main.js --insecure --debugOnce the function is running locally, crossplane render can be used to render examples:
crossplane render examples/app/example.yaml package-configuration/apis/apps/composition.yaml examples/functions.yaml--address- Address to listen for gRPC connections (default:0.0.0.0:9443)-d, --debug- Enable debug logging--insecure- Run without mTLS credentials (for local development)--tls-server-certs-dir- Directory containing mTLS certificates (default:/tls/server)
Crossplane Packages are used to deploy the Function and and dependencies to a Crossplane environment. Each of the package types serves a distinct purpose:
- Configuration Packages contain the API (Composite Resource Definition) and Composition (Pipeline Steps). Configuration packages can pull in other packages types as dependencies.
- The Function Package contains the TypeScript files bundled in a runnable Node Docker container.
- Optional Packages that contain support for managing external APIs like GCP, AWS, and Azure.
flowchart LR
subgraph CP [Configuration Package]
XRD(CompositeResourceDefinition XRD)
C(Composition)
end
CP -->|dependsOn| FP
subgraph FP [Function Package]
ED[Embedded Docker image]
end
CP -->|dependsOn| Providers
subgraph Providers [Optional Providers]
AWS[provider-upjet-aws]
SQL[provider-sql]
end
This template repository includes Github Actions for building and pushing the images.
Scripts are provided that can also be run via npm to build and publish packages.
To build the docker image run:
npm run function-docker-buildThe images will be saved as tar files that can be
packed into a Function Package:
tree _build/docker_images
_build/docker_images
├── function-template-typescript-function-runtime-amd64-v0.1.0.tar
└── function-template-typescript-function-runtime-arm64-v0.1.0.tarThe Dockerfile uses a multi-stage build:
- Build stage: Uses
node:24(LTS) to install dependencies and compile TypeScript - Runtime stage: Uses
gcr.io/distroless/nodejs24-debian12for a minimal, secure runtime that includes the compiled TypeScript source.
Refer to the scripts directory for examples of multi-platform builds.
Now that runnable Docker images have been generated, they can be embedded into a Function package.
First Update the Function Package crossplane.yaml to the name of the Function.
Update the metadata.name and metadata.annotations in the crossplane.yaml file.
Build the function as a Crossplane package (xpkg):
# Build the function package
npm run function-xpkg-buildFunction packages will be generated for arm64 and amd64 in
the _build/xpkg directory:
$ tree _build/xpkg
_build/xpkg
├── function-template-typescript-function-amd64-v0.1.0.xpkg
└── function-template-typescript-function-arm64-v0.1.0.xpkgThese packages can be pushed to any Docker Registry using crossplane xpkg push. Update the XPKG_REPO in the env
file to change the target repository.
# Push to a registry
npm run function-xpkg-push
# Build and push in one step
npm run function-build-allWith the Function package created, the Configuration Package can be generated. This package will install the Function Package as a dependency.
First Update the Configuration Package crossplane.yaml to the name of the Configuration.
Update the metadata.name and metadata.annotations in the crossplane.yaml file.
Next update the spec.dependsOn field to include the function Docker image and any other dependencies, like function-auto-ready.
spec:
dependsOn:
- apiVersion: pkg.crossplane.io/v1
kind: Function
package: xpkg.upbound.io/crossplane-contrib/function-auto-ready
version: '>=v0.6.0'
# Make this match your function
- apiVersion: pkg.crossplane.io/v1
kind: Function
package: xpkg.upbound.io/upbound/function-template-typescript-function
version: '>=v0.1.0'A Crossplane Composition requires a CompositeResourceDefinition (XRD) and Composite. These
are located in the package-configuration/apis directory.
Since the Kind in the template function is an App, we create a subdirectory apps.
- package-configuration/apis/apps/definition.yaml contains the XRD definition.
- package-configuration/apis/apps/composition.yaml contains the Composition pipeline.
Update the composition.yaml file to have the functionRef of the first pipeline step to refer to the name
of the function once it is installed. Crossplane creates a function name of <docker repository>-<function-name>,
so xpkg.upbound.io/upbound/function-template-typescript-function would have a functionRef.name of
upbound-function-template-typescript-function.
Update the value with the name that represents the Docker registry and image where the function was pushed.
- functionRef:
name: upbound-function-template-typescript-function
step: appBuild the Crossplane configuration package:
# Build configuration package
npm run configuration-xpkg-buildThe _build/xpkg directory will contain the multi-platform function
images and the Configuration package image:
tree _build/xpkg
_build/xpkg
├── function-template-typescript-function-amd64-v0.1.0.xpkg
├── function-template-typescript-function-arm64-v0.1.0.xpkg
└── function-template-typescript-v0.1.0.xpkgPush this package to a Docker registry:
# Push configuration package
npm run configuration-xpkg-pushLocal build scripts are located in the scripts/ directory and can be customized. Common settings are contained
in the env file.
Edit src/function.ts to implement your function logic. The main interface is:
export class Function implements FunctionHandler {
async RunFunction(req: RunFunctionRequest, logger?: Logger): Promise<RunFunctionResponse> {
// Your function logic here
}
}The SDK provides helper functions for working with Crossplane resources:
getObservedCompositeResource(req)- Get the observed composite resource (XR)getDesiredCompositeResource(req)- Get the desired composite resourcegetObservedComposedResources(req)- Get observed composed resourcesgetDesiredComposedResources(req)- Get desired composed resourcessetDesiredComposedResources(rsp, resources)- Set desired composed resourcesResource.fromJSON()- Create resources from JSONnormal(rsp, message)- Add a normal condition to the responsefatal(rsp, message)- Add a fatal condition to the responseto(req)- Create a minimal response from a request
See src/function.ts for examples of using these SDK functions.
import { Resource } from '@crossplane-org/function-sdk-typescript';
// Create from JSON
const resource = Resource.fromJSON({
resource: {
apiVersion: 'v1',
kind: 'ConfigMap',
metadata: {
name: 'my-config',
namespace: 'default',
},
data: {
key: 'value',
},
},
});
// Add to desired composed resources
desiredComposed['my-config'] = resource;The template includes kubernetes-models for type-safe K8s resource creation:
import { Pod } from 'kubernetes-models/v1';
const pod = new Pod({
metadata: {
name: 'my-pod',
namespace: 'default',
},
spec: {
containers: [
{
name: 'app',
image: 'nginx:latest',
},
],
},
});
pod.validate(); // Validate the resource
desiredComposed['my-pod'] = Resource.fromJSON({ resource: pod.toJSON() });Create YAML test cases in the test-cases/ directory. Each test case defines:
- Input: The observed composite resource and context
- Expected: Resource counts, types, and validation rules
See test-cases/example.yaml for an example. Tests use src/test-helpers.ts to load and validate YAML test cases.
This template uses strict TypeScript settings:
strict: true- All strict type checking optionsnoUncheckedIndexedAccess: true- Safer array/object accessexactOptionalPropertyTypes: true- Stricter optional propertiesverbatimModuleSyntax: true- Explicit import/export syntax
The SDK directory is excluded from compilation to avoid conflicts with different TypeScript settings.
This project includes automated CI/CD workflows in the .github/workflows/ directory:
CI Workflow (ci.yaml)
The main CI workflow runs automatically on:
- Pushes to
mainorrelease-*branches - Pull requests
- Manual dispatch with optional version override
Jobs:
-
version - Computes the package version
- Uses
npm pkg get versionfrom package.json - Generates pseudo-version:
v{version}-{timestamp}-{git-sha}(e.g.,v0.1.1-20231101115142-1091066df799) - Can be overridden with manual workflow dispatch input
- Uses
-
lint - Code quality checks
- Runs
npm run lintusing ESLint - Validates code style and catches common errors
- Runs
-
test - Runs the test suite
- Executes
npm testwith Jest - Validates function logic and YAML test cases
- Executes
-
check-types - TypeScript type checking
- Runs
npm run check-types - Ensures type safety without emitting files
- Runs
-
build-configuration-package - Builds the Crossplane configuration package
- Uses Crossplane CLI to build the configuration from package-configuration/ directory
- Uploads the configuration
.xpkgas an artifact
-
build-function-packages - Builds function packages for multiple architectures
- Builds Docker images for both
amd64andarm64architectures - Uses Docker Buildx with QEMU for cross-platform builds
- Leverages GitHub Actions cache for faster builds
- Embeds runtime images into Crossplane function packages (
.xpkg) - Uploads architecture-specific packages as artifacts
- Builds Docker images for both
-
push - Publishes packages to registries
- Downloads all built packages from previous jobs
- Pushes multi-platform function package to the configured OCI registry
- Pushes configuration package to the registry
- Only runs if
XPKG_ACCESS_IDandXPKG_TOKENsecrets are configured - Defaults to GitHub Container Registry (
ghcr.io)
Configuration:
- Node.js 24 (LTS)
- Crossplane CLI (stable channel, current version)
- Can push to Upbound registry or any OCI-compatible registry
Tag Workflow (tag.yml)
Manual workflow for creating Git tags:
- Triggered via workflow dispatch only
- Requires version (e.g.,
v0.1.0) and message inputs - Creates an annotated Git tag using the provided information
- Useful for marking releases
Usage:
- Go to Actions tab in GitHub
- Select "Tag" workflow
- Click "Run workflow"
- Enter version and tag message
- Confirm to create the tag
@crossplane-org/function-sdk-typescript- Crossplane function SDKcommander- CLI argument parsingpino- Structured loggingkubernetes-models- Type-safe Kubernetes resource modelstypescript- TypeScript compiler@types/node- Node.js type definitionsyaml- YAML parsing for test cases
@typescript/native-preview- TypeScript 7 (tsgo) native preview toolingjest- Testing frameworkts-jest- TypeScript support for Jest@types/jest- Jest type definitionseslint- Lintingtypescript-eslint- TypeScript ESLint pluginprettier- Code formattingglob- File pattern matching for test discovery
- The SDK is now available as
@crossplane-org/function-sdk-typescripton npm - mTLS is enabled by default when running in production (disable with
--insecurefor local dev) - Tests are automatically discovered from YAML files in the test-cases/ directory
- Build scripts in scripts/ handle Docker and xpkg packaging
If you encounter TypeScript errors:
- Run
npm installto ensure dependencies are properly installed - Try clearing the build directory:
npm run clean - Check that TypeScript version is 5+ or use tsgo (TypeScript 7)
If tests fail:
- Verify test case YAML files are properly formatted in test-cases/
- Check that expected resources match what your function generates
- Run tests with verbose output:
NODE_OPTIONS=--experimental-vm-modules jest --verbose
If the Docker build fails:
- Ensure all dependencies in package.json are available
- Check that the build completes successfully locally first:
npm run build - Verify Docker has access to the project directory
Apache-2.0
Steven Borrelli steve@borrelli.org