Typed CustomResources with Provider Extensions
15 minPulumi can manage Kubernetes CustomResources directly, but they are untyped. Every CustomResourceDefinition (CRD) has its own schema, so the provider cannot know the shape of spec in advance.
Adding your CRDs as an extension of the kubernetes provider solves this. The provider reads the CRD's OpenAPI schema, generates a typed SDK for it, and serves the resulting resources itself. You get IDE autocompletion and compile-time type checking without installing a separate code generation tool, and the extension's resources are managed by the same provider instance as the rest of your Kubernetes resources.
This is the recommended approach for typed custom resources. If you are using crd2pulumi today, this tutorial also covers migrating to provider extensions.
- How to add Kubernetes CRDs as an extension of the kubernetes provider
- How to generate a typed SDK from a CRD manifest with
pulumi package add - How to use the generated extension resources across languages
- How to migrate an existing
crd2pulumiproject to provider extensions
- The Pulumi CLI
- A Pulumi Cloud account and an access token
- A running Kubernetes cluster
- The Kubernetes command line tool kubectl installed
- crd2pulumi (only if you are migrating an existing project)
Pulumi can manage Kubernetes CustomResources directly, but they are untyped. Every CustomResourceDefinition (CRD) has its own schema, so the provider cannot know the shape of spec in advance.
Adding your CRDs as an extension of the kubernetes provider solves this. The provider reads the CRD’s OpenAPI schema, generates a typed SDK for it, and serves the resulting resources itself. You get IDE autocompletion and compile-time type checking without installing a separate code generation tool, and the extension’s resources are managed by the same provider instance as the rest of your Kubernetes resources.
This is the recommended approach for typed custom resources. If you are using crd2pulumi today, see Migrating from crd2pulumi below.
Usage#
Place a CRD manifest in your program. The examples below use the Gateway API CRDs.
pulumi package add kubernetes --extension "name=gateway-networking crd-manifest=gateway-api-crds.yaml"This generates an SDK under sdks/gateway-networking and records the package in Pulumi.yaml, so subsequent pulumi up runs reconstruct the same schema without re-reading the manifest.
The generated SDK draws shared types such as ObjectMeta from the base kubernetes package, so your project also needs the pulumi-kubernetes SDK as a dependency.
The supported parameters are:
| Parameter | Required | Description |
|---|---|---|
crd-manifest | Yes | Path to a YAML or JSON file containing one or more CustomResourceDefinitions. Repeat the key to supply several files. |
name | No | Name of the generated package. Defaults to the first manifest’s file name without its extension, falling back to crds. |
version | No | Version of the generated package. A CRD bundle has no inherent version, so any value works as long as it stays stable across runs. Defaults to 1.0.0. |
A single manifest may contain multiple CRDs, and documents that are not CustomResourceDefinitions are skipped. To supply several manifests, repeat the crd-manifest key inside the same quoted string:
pulumi package add kubernetes --extension "name=gateway-networking crd-manifest=gateway-api-crds.yaml crd-manifest=extra-crds.yaml"Using the generated SDK#
Extension resources are tokenized under the extension’s package name, in the form <name>:<group>/<version>:<Kind>. In the generated SDKs the module name is the group’s first segment, so gateway.networking.k8s.io/v1 becomes the gateway.v1 module.
import * as gatewayNetworking from "@pulumi/gateway-networking";
const gatewayClass = new gatewayNetworking.gateway.v1.GatewayClass("example", { metadata: { name: "example-class", }, spec: { controllerName: "example.com/gateway-controller", },});import pulumi_gateway_networking as gateway_networking
gateway_class = gateway_networking.gateway.v1.GatewayClass( "example", metadata={ "name": "example-class", }, spec={ "controller_name": "example.com/gateway-controller", },)The import path is rooted at the module name in the generated SDK’s go.mod, which defaults to the extension name. Below that root, packages are laid out as kubernetes/<group>/<version>.
package main
import ( gatewayv1 "gateway-networking/kubernetes/gateway/v1" metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1" "github.com/pulumi/pulumi/sdk/v3/go/pulumi")
func main() { pulumi.Run(func(ctx *pulumi.Context) error { _, err := gatewayv1.NewGatewayClass(ctx, "example", &gatewayv1.GatewayClassArgs{ Metadata: &metav1.ObjectMetaArgs{ Name: pulumi.String("example-class"), }, Spec: &gatewayv1.GatewayClassSpecArgs{ ControllerName: pulumi.String("example.com/gateway-controller"), }, }) return err })}using Pulumi;using Pulumi.GatewayNetworking.Gateway.V1;using Pulumi.GatewayNetworking.Types.Inputs.Gateway.V1;using Pulumi.Kubernetes.Types.Inputs.Meta.V1;
return await Deployment.RunAsync(() =>{ var gatewayClass = new GatewayClass("example", new GatewayClassArgs { Metadata = new ObjectMetaArgs { Name = "example-class", }, Spec = new GatewayClassSpecArgs { ControllerName = "example.com/gateway-controller", }, });});import com.pulumi.Pulumi;import com.pulumi.gatewaynetworking.gateway.v1.GatewayClass;import com.pulumi.gatewaynetworking.gateway.v1.GatewayClassArgs;import com.pulumi.gatewaynetworking.gateway.v1.inputs.GatewayClassSpecArgs;import com.pulumi.kubernetes.meta.v1.inputs.ObjectMetaArgs;
public class App { public static void main(String[] args) { Pulumi.run(ctx -> { var gatewayClass = new GatewayClass("example", GatewayClassArgs.builder() .metadata(ObjectMetaArgs.builder() .name("example-class") .build()) .spec(GatewayClassSpecArgs.builder() .controllerName("example.com/gateway-controller") .build()) .build()); }); }}resources: test-gateway-class: type: gateway-networking:gateway.networking.k8s.io/v1:GatewayClass properties: metadata: name: example-class spec: controllerName: example.com/gateway-controllerMigrating from crd2pulumi#
The provider registers aliases for the crd2pulumi-style kubernetes:-namespaced tokens, so existing resources are adopted rather than replaced.
Generate the extension package from the same CRD manifests you passed to crd2pulumi:
pulumi package add kubernetes --extension "name=gateway-networking crd-manifest=gateway-api-crds.yaml"Then update your imports. Resource names, property names, and nesting are unchanged, so your call sites stay as they are.
// Beforeimport * as crds from "./crds/nodejs";const gatewayClass = new crds.gateway.v1.GatewayClass("example", { /* ... */ });
// Afterimport * as gatewayNetworking from "@pulumi/gateway-networking";const gatewayClass = new gatewayNetworking.gateway.v1.GatewayClass("example", { /* ... */ });# Beforeimport pulumi_crds as crdsgateway_class = crds.gateway.v1.GatewayClass("example", ...)
# Afterimport pulumi_gateway_networking as gateway_networkinggateway_class = gateway_networking.gateway.v1.GatewayClass("example", ...)// Beforeimport gatewayv1 "example.com/myproject/crds/go/kubernetes/gateway/v1"
// Afterimport gatewayv1 "gateway-networking/kubernetes/gateway/v1"// Beforeusing Pulumi.Crds.Gateway.V1;
// Afterusing Pulumi.GatewayNetworking.Gateway.V1;The two generators lay Java packages out differently, so the group and version segments change shape as well as the package root.
// Beforeimport com.pulumi.crds.gateway.networking.k8s.io_v1.GatewayClass;
// Afterimport com.pulumi.gatewaynetworking.gateway.v1.GatewayClass;Run pulumi preview and confirm the plan reports no replacements, then delete the checked-in crd2pulumi output and remove the tool from your build.