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>/<version>/generate.sh to include the resource and pass the --include-skipped-output flag. 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
NotebookInstancebut ProtoInstance), do NOT rename the types file to follow the lowercase KRM Kind name (e.g.notebookinstance_types.go). Thegenerate-typestool 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
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
projectRefif the baseline CRD did not have it) is strictly forbidden. - Run diff-crds: You MUST run
dev/tasks/diff-crdsfrequently (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, orBillingAccountRef) lacks akindfield or retains specific fields (likename/namespaceinOrganizationRef), you must hand-code custom reference types locally in<kind>_types.go. For a project reference without akindfield, you MUST import and userefs.ProjectReffromgithub.com/GoogleCloudPlatform/k8s-config-connector/apis/refsinstead of defining a local structure. - Use Real Reference Types: You must NOT redefine or duplicate existing canonical reference types (such as
StorageBucketReforBigQueryTableRef) locally. Instead, import and use the real, canonical reference types from their respective packages underapis/<service>/<version>(e.g.,github.com/GoogleCloudPlatform/k8s-config-connector/apis/storage/v1beta1forStorageBucketRef). This ensures they correctly implementrefs.Refand successfully resolve with common KCC helper functions. - Match Signatures for automatic validation: The schema builder
scripts/add-validation-to-crdsautomatically adds OpenAPIoneOfblocks depending on field signatures.- If a custom reference type has fields
external,name, andnamespace, its signature is"external,name,namespace", which triggers the generator to automatically append theoneOfreference constraint block.
- If a custom reference type has fields
- Spec-level validations: If the baseline CRD contains spec-level validations (such as requiring exactly one of different parent reference fields), ensure those constraints are registered/hardcoded in
scripts/add-validation-to-crds/parse-crds.goso they are successfully preserved in the generated CRD. - Handling Type Mismatches in generated mappers: If a KRM field type (such as
int64) does not match the proto's field type (such asint32), the generated assignment code will fail compilation. Solve this by writing handcoded mapping functions (e.g.,KindSpec_FromProto/ToProto) in a handcodedmapper.gofile within the direct controller package. The generator will automatically detect these and skip generating the conflicting versions. - Document Handcoded Deviations: For all handcoded mapping functions, you MUST include clear comments above the function explaining exactly why it deviates from the default behavior and why it cannot be automatically generated (e.g., secret reference fields, body string-to-byte-slice mapping, or custom reference type logic).
- Ensure correct +kcc:proto annotation: Always make sure all KRM types have the correct
// +kcc:proto=<proto_message>annotation on the struct definition. Even if you think you have to hand-code the mapper, check that the annotation is present. Running the generator produces a commented-out generated mapper insidemapper.generated.goif the type is annotated. Review that commented-out mapper first before deciding to hand-code; you might be able to fully automate the mapper simply by renaming the Go struct field to match the proto field name exactly (without changing the json tag). - Match Field Formats: Ensure date/time strings have
// +kubebuilder:validation:Format=date-timeto match original formats. - Keep Legacy Reconciler Labels & API Versions: You must retain existing labels such as
// +kubebuilder:metadata:labels="cnrm.cloud.google.com/dcl2crd=true"(ortf2crd=truefor Terraform-based resources) on the struct definition in<kind>_types.go. Removing these labels will prevent the corresponding legacy controllers from being registered correctly for the resource during the transition phase. Additionally, if the baseline CRD contains older versions that must be preserved (e.g.,v1alpha1), you MUST add// +kubebuilder:metadata:labels="internal.cloud.google.com/additional-versions=v1alpha1"to the beta resource structure so that those versions are still generated. - Validation: No new fields should be introduced under status. In particular, do not add
observedStateorexternalRefto the Status struct of the newly generated Go types if they were not already present in the original baseline CRD. Even thoughexternalRefis standard for direct controllers, adding it changes the schema and must be avoided for strict schema-compatibility. The output ofdev/tasks/diff-crdsmust be absolutely empty. - Matching Go struct field names to Proto field names: The mapping generator automatically matches KRM Go struct fields to Proto fields by mapping Go camel-case names to proto snake_case/camel-case field names. If a KRM field is named differently in the CRD (e.g.,
filterLabels), we can rename the Go struct field to match the proto name exactly (e.g., renaming the Go field name toLabelswhile preserving the JSON tagjson:"filterLabels,omitempty"so that there is absolutely no schema change). This allows us to fully leverage automatic mapper generation and completely avoid writing hand-coded mappers. If the mapped type is defined in a different package (likegoogle.api.MonitoredResource), we can also specify multiple services to the--serviceflag ofgenerate-mapper(e.g.--service google.monitoring.v3,google.api) to enable automatic traversal and generation for that shared type.
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 likefuzzers.go. - Use Type-Safe Helpers: Do not call
f.SpecFields.Insert,f.StatusFields.Insert, orf.UnimplementedFields.Insertdirectly on sets. Instead, use the type-safe helper methods defined on theKRMTypedFuzzerstruct:- 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.
- Use
- Move Hand-Coded Mappers: Ensure all hand-coded mapper functions reside in a file called
mappers.gowithin the direct controller package to distinguish them from the generated mapper filemapper.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-crdsto 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-prto regenerate Go clients (and compile-check the changes, run custom linters, format the files, and regenerate static configs). - Finally, run
dev/tasks/generate-resource-reportto update the resource reportsdocs/reports/crd_report.csvanddocs/reports/crd_report.mdto reflect the transition.