@@ -25,6 +25,12 @@ import (
2525
2626 "github.com/dave/jennifer/jen"
2727 "gopkg.in/yaml.v3"
28+ "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
29+ apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
30+ apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
31+ clientsetscheme "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme"
32+ "k8s.io/apimachinery/pkg/runtime"
33+ "k8s.io/apimachinery/pkg/runtime/serializer"
2834)
2935
3036type ReferenceField struct {
@@ -112,19 +118,19 @@ func parseNextCRDReferences(scanner *bufio.Scanner, targetKind string) ([]Refere
112118}
113119
114120func extractReferencesFromCRD (content []byte , targetKind string ) ([]ReferenceField , error ) {
115- var crd CRDDocument
116- if err := yaml . Unmarshal ( content , & crd ); err != nil {
117- return nil , fmt .Errorf ("failed to unmarshal YAML: %w" , err )
121+ crd , err := Decode ( content )
122+ if err != nil {
123+ return nil , fmt .Errorf ("failed to decode YAML: %w" , err )
118124 }
119125
120- if crd .Kind != "CustomResourceDefinition" || crd . Spec .Names .Kind != targetKind {
126+ if crd .Spec .Names .Kind != targetKind {
121127 return nil , fmt .Errorf ("not target CRD" )
122128 }
123129
124130 var references []ReferenceField
125131
126- if apiMappingsStr , exists := crd .Metadata . Annotations ["api-mappings" ]; exists {
127- refs , err := parseAPIMapping (apiMappingsStr )
132+ if apiMappingsStr , exists := crd .GetAnnotations () ["api-mappings" ]; exists {
133+ refs , err := parseAPIMapping (apiMappingsStr , crd . Spec . Versions [ 0 ]. Schema . OpenAPIV3Schema )
128134 if err == nil {
129135 references = append (references , refs ... )
130136 }
@@ -134,34 +140,36 @@ func extractReferencesFromCRD(content []byte, targetKind string) ([]ReferenceFie
134140 return references , nil
135141}
136142
137- type VersionProperties struct {
138- Properties map [string ]PropertyMapping `yaml:"properties"`
139- }
143+ func Decode (content []byte ) (* apiextensionsv1.CustomResourceDefinition , error ) {
144+ sch := runtime .NewScheme ()
145+ _ = clientsetscheme .AddToScheme (sch )
146+ _ = apiextensions .AddToScheme (sch )
147+ _ = apiextensionsv1 .AddToScheme (sch )
148+ _ = apiextensionsv1 .RegisterConversions (sch )
149+ _ = apiextensionsv1beta1 .AddToScheme (sch )
150+ _ = apiextensionsv1beta1 .RegisterConversions (sch )
140151
141- type PropertyMapping struct {
142- KubernetesMapping * KubernetesMapping `yaml:"x-kubernetes-mapping,omitempty"`
143- OpenAPIMapping * OpenAPIMapping `yaml:"x-openapi-mapping,omitempty"`
144- }
152+ decode := serializer .NewCodecFactory (sch ).UniversalDeserializer ().Decode
145153
146- type KubernetesMapping struct {
147- NameSelector string `yaml:"nameSelector"`
148- Properties []string `yaml:"properties"`
149- Type KubernetesRefType `yaml:"type"`
150- }
154+ obj , _ , err := decode (content , nil , nil )
155+ if err != nil {
156+ return nil , fmt .Errorf ("failed to decode YAML: %w" , err )
157+ }
151158
152- type KubernetesRefType struct {
153- Group string `yaml:"group"`
154- Kind string `yaml:"kind"`
155- Resource string `yaml:"resource"`
156- Version string `yaml:"version"`
157- }
159+ kind := obj .GetObjectKind ().GroupVersionKind ().Kind
160+ if kind != "CustomResourceDefinition" {
161+ return nil , fmt .Errorf ("unexpected kind %q: %w" , kind , err )
162+ }
158163
159- type OpenAPIMapping struct {
160- Property string `yaml:"property"`
161- Type string `yaml:"type,omitempty"`
164+ crd := & apiextensionsv1.CustomResourceDefinition {}
165+ err = sch .Convert (obj , crd , nil )
166+ if err != nil {
167+ return nil , fmt .Errorf ("failed to convert CRD object: %w" , err )
168+ }
169+ return crd , nil
162170}
163171
164- func parseAPIMapping (apiMappingsStr string ) ([]ReferenceField , error ) {
172+ func parseAPIMapping (apiMappingsStr string , schema * apiextensionsv1. JSONSchemaProps ) ([]ReferenceField , error ) {
165173 // Parse the api-mappings as a generic map to handle any nesting level
166174 var mapping map [string ]any
167175 if err := yaml .Unmarshal ([]byte (apiMappingsStr ), & mapping ); err != nil {
@@ -171,7 +179,7 @@ func parseAPIMapping(apiMappingsStr string) ([]ReferenceField, error) {
171179 var references []ReferenceField
172180
173181 // Recursively search for x-kubernetes-mapping
174- findReferences (mapping , "" , & references )
182+ findReferences (mapping , "" , schema , & references )
175183
176184 return references , nil
177185}
@@ -220,7 +228,7 @@ func processKubernetesMapping(v map[string]any, path string, references *[]Refer
220228 }
221229}
222230
223- func findReferences (data any , path string , references * []ReferenceField ) {
231+ func findReferences (data any , path string , schema * apiextensionsv1. JSONSchemaProps , references * []ReferenceField ) {
224232 switch v := data .(type ) {
225233 case map [string ]any :
226234 // Check if this is a kubernetes mapping and process it
@@ -232,16 +240,63 @@ func findReferences(data any, path string, references *[]ReferenceField) {
232240 newPath += "."
233241 }
234242 newPath += key
235- findReferences (value , newPath , references )
243+
244+ // Resolve the child schema for this key and pass it into the recursive call
245+ required , childSchema := getSchemaForPathSegment (schema , key )
246+ _ = required
247+ findReferences (value , newPath , childSchema , references )
236248 }
237249 case []any :
238250 for i , item := range v {
239251 newPath := fmt .Sprintf ("%s[%d]" , path , i )
240- findReferences (item , newPath , references )
252+
253+ // For arrays pass the items schema if available
254+ var childSchema * apiextensionsv1.JSONSchemaProps
255+ if schema != nil {
256+ var required bool
257+ required , childSchema = getSchemaForPathSegment (schema , "items" )
258+ _ = required
259+ }
260+ findReferences (item , newPath , childSchema , references )
241261 }
242262 }
243263}
244264
265+ // getSchemaForPathSegment returns the nested JSONSchemaProps for a given mapping key.
266+ // - "properties" returns the current schema (so inner property names will be looked up in schema.Properties)
267+ // - "items" returns the schema for array items (handles single Schema or first element of JSONSchemas)
268+ // - normal property names return schema.Properties[name] (strips trailing "[index]" if present)
269+ func getSchemaForPathSegment (schema * apiextensionsv1.JSONSchemaProps , key string ) (bool , * apiextensionsv1.JSONSchemaProps ) {
270+ if schema == nil {
271+ return false , nil
272+ }
273+
274+ if key == "properties" {
275+ // The next level will contain actual property names; keep the current schema so those names can be looked up in schema.Properties
276+ return false , schema
277+ }
278+
279+ required := false
280+ for _ , req := range schema .Required {
281+ if req == key {
282+ required = true
283+ break
284+ }
285+ }
286+
287+ if key == "items" {
288+ return false , nil
289+ }
290+
291+ if schema .Properties != nil {
292+ if p , ok := schema .Properties [key ]; ok {
293+ return required , & p
294+ }
295+ }
296+
297+ return false , nil
298+ }
299+
245300func GenerateIndexers (resultPath , crdKind , indexerOutDir string ) error {
246301 references , err := ParseReferenceFields (resultPath , crdKind )
247302 if err != nil {
0 commit comments