| name | crd-mapper-fuzzer-existing-type |
| description | Standards and workflows for creating direct KRM Go types for an existing CRD while maintaining strict schema compatibility. |
CRD Mapper Fuzzer Existing Type
Overview
This skill outlines standard practices when transitioning an existing KCC resource (e.g., from Terraform or DCL) to a direct controller by generating the initial KRM types (_types.go), ensuring strict schema compatibility with the baseline CRD. You must NOT change the schema at all (other than descriptions). Do not add any new fields, including spec.projectRef, unless it was already part of the baseline CRD. You MUST run dev/tasks/diff-crds to check for any schema changes.
Workflow
1. Configure generate.sh
Configure apis/<service>/generate.sh to include the resource.
- Types Generation: Run
generate-types for each version present under apis/<service>/ (e.g. v1alpha1 and v1beta1).
- Mappers Generation: Run
generate-mapper exactly once at the very end of the consolidated script (targeting the highest supported version, e.g. v1beta1) and always pass --multiversion by default. Because generate-mapper scans the entire service API directory, running it once with --multiversion compiles mappers for all versions into mapper.generated.go with version suffixes, avoiding duplicate clashes and accidental file overwrites.
Sourcing ${REPO_ROOT}/dev/tools/goimports.sh, cd'ing to ${REPO_ROOT}/dev/tools/controllerbuilder, and passing --include-skipped-output to both generate-types and generate-mapper ensures that any output otherwise skipped is still generated but commented out. This provides an invaluable reference when manual modifications/hand-coding of types are needed.
- Keep Type File Names Matching Lowercase Proto Message: If the KRM Kind name differs from the underlying Proto message name (e.g. Kind
NotebookInstance but Proto Instance), do NOT rename the types file to follow the lowercase KRM Kind name (e.g. notebookinstance_types.go). The generate-types tool expects the file to be named <lowercase_proto_message_name>_types.go (e.g. instance_types.go). Renaming it will cause generator panics and duplicate/untracked file generation.
go run . generate-types \
--service <proto.package> \
--api-version "<service>.cnrm.cloud.google.com/<version>" \
--include-skipped-output \
--resource <Kind>:<ProtoMessage>
go run . generate-mapper \
--service <proto.package> \
--api-version "<service>.cnrm.cloud.google.com/<version>" \
--include-skipped-output \
--multiversion
2. Standards for Strict Schema Compatibility
When defining the KRM Go type in <kind>_types.go, you must ensure it matches the original CRD schema exactly. You must not add, remove, or modify any fields (including spec.projectRef) in a way that alters the KRM schema:
- Do Not Change the Schema: You must not change the schema when the type already exists. Description changes are OK, but adding/removing/renaming fields (such as adding
projectRef if the baseline CRD did not have it) is strictly forbidden.
- Define Nested Structs as Pointers to Avoid Hand-coded Mappers: If the baseline CRD defines nested structures (even if required), you should define them as pointers in the KRM Go structs rather than value types. This allows the automatic mapping generator
generate-mapper to traverse and map them automatically, completely eliminating the need for hand-coded mapper functions. If the baseline CRD schema requires those nested fields, mark the pointer fields with // +required (or // +kubebuilder:validation:Required) to ensure they remain required in the generated CRD schema.
- Run diff-crds: You MUST run
dev/tasks/diff-crds frequently (and definitely before opening/updating a PR) to identify any schema changes or deviations between the baseline CRD and the generated one. The diff-crds output must be absolutely empty (or contain only minor description reflows if expected).
- No spec.projectRef addition: If the baseline CRD did not contain
spec.projectRef, do not add it to the Spec struct in <kind>_types.go.
- Reference Hand-coding & Manual Edits: If there are schema mismatches, you must manually copy, edit, or hand-code types (e.g. to change or remove fields) until the schemas match perfectly.
- Hand-code custom reference types: If a resource reference structure in the baseline CRD (like
FolderRef, OrganizationRef, or BillingAccountRef) lacks a kind field or retains specific fields (like name/namespace in OrganizationRef), you must hand-code custom reference types locally in <kind>_types.go. For a project reference without a kind field, you MUST import and use refs.ProjectRef from github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs instead of defining a local structure.
- Use Real Reference Types: You must NOT redefine or duplicate existing canonical reference types (such as
StorageBucketRef or BigQueryTableRef) locally. Instead, import and use the real, canonical reference types from their respective packages under (e.g., for ). This ensures they correctly implement and successfully resolve with common KCC helper functions.
3. Fuzzer Best Practices
Fuzzer implementation timing: Note that implementing a fuzzer is NOT required for brownfield resources in the first step (defining direct types). The fuzzer is implemented and run later in the transition lifecycle (e.g., in Step 3 or 4 when writing and validating the controller reconciliation logic).
When writing a KRM round-trip fuzzer, or if a fuzzer already exists:
- File Naming: Ensure the fuzzer is named
<kind>_fuzzer.go (e.g., entitlement_fuzzer.go) in lowercase. Do not use generic names like fuzzers.go.
- Use Type-Safe Helpers: Do not call
f.SpecFields.Insert, f.StatusFields.Insert, or f.UnimplementedFields.Insert directly on sets. Instead, use the type-safe helper methods defined on the KRMTypedFuzzer struct:
- Use
f.SpecField(fieldPath) to mark a field as round-tripping to/from the Spec.
- Use
f.StatusField(fieldPath) to mark a field as round-tripping to/from the Status.
- Use
f.Unimplemented_Identity(fieldPath) for identity/URL fields like .name.
- Use
f.Unimplemented_Internal(fieldPath) for internal service-only/hidden implementation details (e.g., resource fields that are duplicates of KRM metadata or parent references).
- Use
f.Unimplemented_NotYetTriaged(fieldPath) for fields that are not implemented or under development.
- Use
f.Unimplemented_LabelsAnnotations(fieldPath) for labels or annotations.
- Move Hand-Coded Mappers: Ensure all hand-coded mapper functions reside in a file called
mappers.go within the direct controller package to distinguish them from the generated mapper file mapper.generated.go.
No Dedicated Unit Test Needed: There is no need to add a dedicated/standalone _fuzzer_test.go file for the resource. Simply registering the fuzzer using fuzztesting.RegisterKRMFuzzer() in your fuzzer implementation file (e.g. backupvault_fuzzer.go) is fully sufficient. The existing shared testing framework (such as pkg/fuzztesting/fuzztests/fuzz_test.go) will automatically discover and run it.
4. Verification & Acceptance Criteria
- Run
dev/tasks/diff-crds to verify there are absolutely no unintended schema changes.
- Since we are transitioning an existing type, the primary acceptance criterion is "does it generate the same CRD schema".
- Once the schema is identical, run
make ready-pr to regenerate Go clients (and compile-check the changes, run custom linters, format the files, and regenerate static configs).