Skip to content

CRD Generation Pipeline and Scheme Registration

Reference documentation for how CRD manifests are generated, how Go types are registered with the controller-runtime scheme, and how to install/uninstall CRDs on a Kubernetes cluster.

Overview

The memcached-operator uses controller-gen (from the controller-tools project) to generate two categories of artifacts from the Go type definitions in api/v1alpha1/:

  1. CRD YAML manifests — OpenAPI v3 validation schemas, printer columns, subresources, and resource metadata derived from kubebuilder markers.
  2. DeepCopy methodsDeepCopy, DeepCopyInto, and DeepCopyObject implementations required by the runtime.Object interface.

Both artifacts are checked into the repository. CI verifies they stay in sync with the Go types via make verify-manifests.

Generation Pipeline

make manifests — CRD and RBAC Generation

bash
make manifests

Invokes controller-gen with:

bash
controller-gen rbac:roleName=manager-role crd webhook \
  paths="./..." \
  output:crd:artifacts:config=config/crd/bases

What happens:

  1. controller-gen scans all Go packages (./...) for types annotated with kubebuilder markers (+kubebuilder:object:root=true, +kubebuilder:validation:*, +kubebuilder:printcolumn:*, etc.).
  2. For each root object type (Memcached), it generates a CustomResourceDefinition YAML manifest.
  3. RBAC markers (+kubebuilder:rbac:*) on the reconciler produce ClusterRole rules in config/rbac/role.yaml.

Output:

FileContent
config/crd/bases/memcached.c5c3.io_memcacheds.yamlCRD with OpenAPI schema, printer columns, status subresource
config/rbac/role.yamlClusterRole with RBAC rules from reconciler markers

Key markers that drive CRD generation (defined in api/v1alpha1/memcached_types.go):

MarkerLocationEffect in CRD
+kubebuilder:object:root=trueMemcached, MemcachedListMarks types as CRD root objects
+kubebuilder:subresource:statusMemcachedEnables /status subresource
+kubebuilder:printcolumn:*MemcachedAdds additionalPrinterColumns for kubectl get
+kubebuilder:validation:Minimum=NSpec fieldsSets minimum in OpenAPI schema
+kubebuilder:validation:Maximum=NSpec fieldsSets maximum in OpenAPI schema
+kubebuilder:validation:Pattern=...MaxItemSizeSets pattern in OpenAPI schema
+kubebuilder:validation:Enum=...AntiAffinityPresetSets enum in OpenAPI schema
+kubebuilder:default=...Various fieldsSets default in OpenAPI schema

make generate — DeepCopy Code Generation

bash
make generate

Invokes controller-gen with:

bash
controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."

What happens:

  1. controller-gen scans for types annotated with +kubebuilder:object:generate=true (set as a package-level directive in api/v1alpha1/groupversion_info.go).
  2. For each struct in the package, it generates DeepCopyInto and DeepCopy methods. Root objects additionally get DeepCopyObject.

Output:

FileContent
api/v1alpha1/zz_generated.deepcopy.goDeepCopy* methods for all types in the package

The generated file includes:

  • A DO NOT EDIT header with the license from hack/boilerplate.go.txt.
  • Methods for every struct: Memcached, MemcachedList, MemcachedSpec, MemcachedStatus, MemcachedConfig, HighAvailabilitySpec, PDBSpec, MonitoringSpec, ServiceMonitorSpec, SecuritySpec, SASLSpec, TLSSpec.

Idempotency

Both commands are idempotent. Running make manifests or make generate twice in succession produces identical output. This property is verified by the verify-manifests target and in tests.

Scheme Registration

The controller-runtime scheme maps Go types to Kubernetes GroupVersionKind (GVK) tuples. Three files form the registration chain:

1. api/v1alpha1/groupversion_info.go

Declares the API group identity and creates the scheme builder:

go
var (
    GroupVersion  = schema.GroupVersion{Group: "memcached.c5c3.io", Version: "v1alpha1"}
    SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
    AddToScheme   = SchemeBuilder.AddToScheme
)
  • GroupVersion — the GV tuple used for all types in this package.
  • SchemeBuilder — collects type registrations via Register().
  • AddToScheme — a function variable that callers invoke to register all types with a given runtime.Scheme.

2. api/v1alpha1/memcached_types.go

Registers the CRD root types in init():

go
func init() {
    SchemeBuilder.Register(&Memcached{}, &MemcachedList{})
}

This makes both types discoverable by GVK:

  • memcached.c5c3.io/v1alpha1, Kind=Memcached resolves to *v1alpha1.Memcached
  • memcached.c5c3.io/v1alpha1, Kind=MemcachedList resolves to *v1alpha1.MemcachedList

3. cmd/main.go

Wires the scheme into the controller manager:

go
func init() {
    utilruntime.Must(clientgoscheme.AddToScheme(scheme))
    utilruntime.Must(memcachedv1alpha1.AddToScheme(scheme))
}

The init() function registers:

  1. Core Kubernetes types (clientgoscheme) — Pods, Services, Deployments, etc.
  2. Memcached types (memcachedv1alpha1.AddToScheme) — Memcached and MemcachedList.

The resulting scheme is passed to the controller-runtime Manager, which uses it for serialization, deserialization, and watch configuration.

Registration Flow Diagram

text
groupversion_info.go              memcached_types.go              cmd/main.go
─────────────────────             ─────────────────               ──────────
GroupVersion{                     func init() {                   func init() {
  "memcached.c5c3.io",             SchemeBuilder.Register(          utilruntime.Must(
  "v1alpha1",                         &Memcached{},                   memcachedv1alpha1
}                                     &MemcachedList{},                 .AddToScheme(scheme))
                                    )                               }
SchemeBuilder = &scheme.Builder{  }
  GroupVersion: GroupVersion,                                     ctrl.NewManager(cfg, ctrl.Options{
}                                                                   Scheme: scheme,
                                                                  })
AddToScheme = SchemeBuilder
                .AddToScheme

CRD Installation and Removal

Install CRDs

bash
make install

Runs:

bash
kustomize build config/crd | kubectl apply -f -

This:

  1. Builds the CRD manifest using config/crd/kustomization.yaml, which references bases/memcached.c5c3.io_memcacheds.yaml.
  2. Applies the CRD to the cluster in ~/.kube/config.
  3. After installation, the API server accepts Memcached CR CRUD operations under memcached.c5c3.io/v1alpha1.

Uninstall CRDs

bash
make uninstall

Runs:

bash
kustomize build config/crd | kubectl delete --ignore-not-found=false -f -

Removes the CRD and all Memcached custom resources from the cluster.

Verify Generated Artifacts

bash
make verify-manifests

Re-runs both make manifests and make generate, then checks for uncommitted changes:

bash
git diff --exit-code -- config/crd/ api/v1alpha1/zz_generated.deepcopy.go

If the working tree is dirty, the target fails with an error message instructing the developer to run make manifests generate and commit the result. This target is intended for CI pipelines to detect stale generated artifacts.

Generated CRD Structure

The generated CRD at config/crd/bases/memcached.c5c3.io_memcacheds.yaml contains:

SectionValue
apiVersionapiextensions.k8s.io/v1
kindCustomResourceDefinition
metadata.namememcacheds.memcached.c5c3.io
spec.groupmemcached.c5c3.io
spec.scopeNamespaced
spec.names.kindMemcached
spec.names.listKindMemcachedList
spec.names.pluralmemcacheds
spec.names.singularmemcached
spec.versions[0].namev1alpha1

Printer Columns

ColumnTypeJSONPath
Replicasinteger.spec.replicas
Readyinteger.status.readyReplicas
Agedate.metadata.creationTimestamp

Validation Constraints

FieldConstraint
spec.replicasminimum: 0, maximum: 64, default: 1
spec.imagedefault: memcached:1.6
spec.memcached.maxMemoryMBminimum: 16, maximum: 65536, default: 64
spec.memcached.maxConnectionsminimum: 1, maximum: 65536, default: 1024
spec.memcached.threadsminimum: 1, maximum: 128, default: 4
spec.memcached.maxItemSizepattern: ^[0-9]+(k|m)$, default: 1m
spec.highAvailability.antiAffinityPresetenum: [soft, hard], default: soft
spec.monitoring.exporterImagedefault: prom/memcached-exporter:v0.15.4

Status Subresource

The CRD enables the /status subresource (spec.versions[0].subresources.status), which means:

  • Status updates via the /status subresource do not increment metadata.generation.
  • Spec updates via the main resource endpoint do increment metadata.generation.
  • The reconciler can update status independently of spec changes.

Common Tasks

Adding a new field to the CRD

  1. Add the field to the appropriate struct in api/v1alpha1/memcached_types.go with JSON tags and kubebuilder markers.
  2. Run make manifests generate to regenerate the CRD YAML and DeepCopy methods.
  3. Verify the generated output: make verify-manifests should pass.
  4. Commit the Go type changes and the regenerated files together.

Adding a new validation constraint

Add a kubebuilder marker above the field:

go
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=100
// +kubebuilder:default=10
MyField int32 `json:"myField,omitempty"`

Then run make manifests to regenerate the CRD.

Adding a new printer column

Add a +kubebuilder:printcolumn marker to the root type (Memcached):

go
// +kubebuilder:printcolumn:name="MyCol",type="string",JSONPath=".spec.myField"

Then run make manifests to regenerate the CRD.