Skip to content

Extending

Adding a New CRD

1. Define the types

Create api/v1alpha1/<name>_types.go:

package v1alpha1

import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
type MyCRD struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`
    Spec              MyCRDSpec   `json:"spec,omitempty"`
    Status            MyCRDStatus `json:"status,omitempty"`
}

type MyCRDSpec struct {
    // +kubebuilder:validation:Required
    SomeField string `json:"someField"`
}

type MyCRDStatus struct {
    Phase      string             `json:"phase,omitempty"`
    Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
type MyCRDList struct {
    metav1.TypeMeta `json:",inline"`
    metav1.ListMeta `json:"metadata,omitempty"`
    Items           []MyCRD `json:"items"`
}

func init() {
    SchemeBuilder.Register(&MyCRD{}, &MyCRDList{})
}

Rules:

  • Must be cluster-scoped (+kubebuilder:resource:scope=Cluster)
  • Status must include []metav1.Condition
  • Register in init() via SchemeBuilder

2. Generate code

make codegen

This produces:

  • api/v1alpha1/zz_generated.deepcopy.go (updated)
  • config/crd/bases/drop.corewire.io_mycrds.yaml
  • RBAC roles in config/rbac/

3. Write the controller

Create internal/controller/<name>_controller.go:

package controller

import (
    "context"

    "k8s.io/apimachinery/pkg/runtime"
    ctrl "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/client"
    "sigs.k8s.io/controller-runtime/pkg/log"

    dropv1alpha1 "github.com/Breee/drop/api/v1alpha1"
)

type MyCRDReconciler struct {
    client.Client
    Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=drop.corewire.io,resources=mycrds,verbs=get;list;watch;update;patch
// +kubebuilder:rbac:groups=drop.corewire.io,resources=mycrds/status,verbs=get;update;patch

func (r *MyCRDReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    log := log.FromContext(ctx)

    var obj dropv1alpha1.MyCRD
    if err := r.Get(ctx, req.NamespacedName, &obj); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    log.Info("reconciling", "name", obj.Name)

    // Business logic here

    return ctrl.Result{}, nil
}

func (r *MyCRDReconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        For(&dropv1alpha1.MyCRD{}).
        Complete(r)
}

4. Register in cmd/main.go

if err = (&controller.MyCRDReconciler{
    Client: mgr.GetClient(),
    Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
    setupLog.Error(err, "unable to create controller", "controller", "MyCRD")
    os.Exit(1)
}

5. Add tests

Unit testinternal/controller/<name>_controller_test.go:

  • Use envtest suite
  • Create the resource, trigger reconciliation, assert status

E2E testtest/e2e/<name>-basic/chainsaw-test.yaml:

  • Apply resource, assert expected status/children

Sampleconfig/samples/drop_v1alpha1_<name>.yaml:

  • Minimal valid resource for testing

6. Regenerate docs

make docs-gen

This updates llms.txt, AGENTS.md, .cursorrules, knowledge.yaml, and the copilot instructions.

Last updated on