@@ -23,6 +23,7 @@ import (
2323 "github.com/getkin/kin-openapi/openapi3"
2424 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2525 "k8s.io/apimachinery/pkg/runtime"
26+ "k8s.io/apimachinery/pkg/runtime/schema"
2627 "sigs.k8s.io/controller-runtime/pkg/client"
2728 "sigs.k8s.io/yaml"
2829
@@ -31,15 +32,107 @@ import (
3132 "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/crapi/unstructured"
3233)
3334
35+ // NewTranslator creates a translator for a particular CRD version. It is also
36+ // locked into a particular API majorVersion.
37+ //
38+ // Given the following example resource:
39+ //
40+ // apiVersion: atlas.generated.mongodb.com/v1
41+ // kind: SearchIndex
42+ // metadata:
43+ // name: search-index
44+ // spec:
45+ // v20250312:
46+ //
47+ // In the above case crdVersion is "v1" and majorVersion is "v20250312".
48+ func NewTranslator (scheme * runtime.Scheme , crd * apiextensionsv1.CustomResourceDefinition , crdVersion string , majorVersion string ) (Translator , error ) {
49+ specVersion := crds .SelectVersion (& crd .Spec , crdVersion )
50+ if err := crds .AssertMajorVersion (specVersion , crd .Spec .Names .Kind , majorVersion ); err != nil {
51+ return nil , fmt .Errorf ("failed to assert major version %s in CRD: %w" , majorVersion , err )
52+ }
53+ var mappingSchema openapi3.Schema
54+ mappingString , ok := crd .Annotations ["api-mappings" ]
55+ if ok && mappingString != "" {
56+ jsonBytes , err := yaml .YAMLToJSON ([]byte (mappingString ))
57+ if err != nil {
58+ return nil , fmt .Errorf ("failed to convert 'api-mappings' YAML to JSON: %w" , err )
59+ }
60+ if err := json .Unmarshal (jsonBytes , & mappingSchema ); err != nil {
61+ return nil , fmt .Errorf ("failed to unmarshal 'api-mappings' JSON into schema: %w" , err )
62+ }
63+ }
64+
65+ return & translator {
66+ scheme : scheme ,
67+ majorVersion : majorVersion ,
68+ gvk : schema.GroupVersionKind {Group : crd .Spec .Group , Version : crdVersion , Kind : crd .Spec .Names .Kind },
69+ mappingSchema : & openapi3.SchemaRef {Value : & mappingSchema },
70+ }, nil
71+ }
72+
73+ // NewPerVersionTranslators creates a set of translators indexed by SDK versions
74+ //
75+ // Given the following example resource:
76+ //
77+ // apiVersion: atlas.generated.mongodb.com/v1
78+ // kind: SearchIndex
79+ // metadata:
80+ // name: search-index
81+ // spec:
82+ // v20250312:
83+ // ...
84+ // v20250810:
85+ //
86+ // In the above case crdVersion is "v1" and versions can be "v20250312"
87+ // and/or "v20250810".
88+ func NewPerVersionTranslators (scheme * runtime.Scheme , crd * apiextensionsv1.CustomResourceDefinition , crdVersion string , versions ... string ) (map [string ]Translator , error ) {
89+ translators := map [string ]Translator {}
90+ specVersion := crds .SelectVersion (& crd .Spec , crdVersion )
91+ for _ , version := range versions {
92+ if err := crds .AssertMajorVersion (specVersion , crd .Spec .Names .Kind , version ); err != nil {
93+ return nil , fmt .Errorf ("failed to assert major version %s in CRD: %w" , version , err )
94+ }
95+ var mappingSchema openapi3.Schema
96+ mappingString , ok := crd .Annotations ["api-mappings" ]
97+ if ok && mappingString != "" {
98+ jsonBytes , err := yaml .YAMLToJSON ([]byte (mappingString ))
99+ if err != nil {
100+ return nil , fmt .Errorf ("failed to convert 'api-mappings' YAML to JSON: %w" , err )
101+ }
102+ if err := json .Unmarshal (jsonBytes , & mappingSchema ); err != nil {
103+ return nil , fmt .Errorf ("failed to unmarshal 'api-mappings' JSON into schema: %w" , err )
104+ }
105+ }
106+
107+ translators [version ] = & translator {
108+ scheme : scheme ,
109+ majorVersion : version ,
110+ gvk : schema.GroupVersionKind {Group : crd .Spec .Group , Version : crdVersion , Kind : crd .Spec .Names .Kind },
111+ mappingSchema : & openapi3.SchemaRef {Value : & mappingSchema },
112+ }
113+ }
114+ return translators , nil
115+ }
116+
34117// translator implements Translator to translate from a given CRD to and from
35118// a given SDK version using the same upstream OpenAPI schema
36119type translator struct {
37120 scheme * runtime.Scheme
38121 majorVersion string
122+ gvk schema.GroupVersionKind
39123 mappingSchema * openapi3.SchemaRef
40124}
41125
42126func (tr * translator ) ToAPI (target any , source client.Object , objs ... client.Object ) error {
127+ if isNil (source ) {
128+ return fmt .Errorf ("source is nil" )
129+ }
130+ if isNil (target ) {
131+ return fmt .Errorf ("target is nil" )
132+ }
133+ if err := checkGVK (tr .scheme , source , tr .gvk ); err != nil {
134+ return fmt .Errorf ("Source GVK check failed: %w" , err )
135+ }
43136 unstructuredSrc , err := unstructured .ToUnstructured (source )
44137 if err != nil {
45138 return fmt .Errorf ("failed to convert k8s source value to unstructured: %w" , err )
@@ -71,6 +164,15 @@ func (tr *translator) ToAPI(target any, source client.Object, objs ...client.Obj
71164}
72165
73166func (tr * translator ) FromAPI (target client.Object , source any , objs ... client.Object ) ([]client.Object , error ) {
167+ if isNil (source ) {
168+ return nil , fmt .Errorf ("source is nil" )
169+ }
170+ if isNil (target ) {
171+ return nil , fmt .Errorf ("target is nil" )
172+ }
173+ if err := checkGVK (tr .scheme , target , tr .gvk ); err != nil {
174+ return nil , fmt .Errorf ("Target GVK check failed: %w" , err )
175+ }
74176 sourceUnstructured , err := unstructured .ToUnstructured (source )
75177 if err != nil {
76178 return nil , fmt .Errorf ("failed to convert API source value to unstructured: %w" , err )
@@ -137,86 +239,6 @@ func (tr *translator) Scheme() *runtime.Scheme {
137239 return tr .scheme
138240}
139241
140- // NewTranslator creates a translator for a particular CRD version. It is also
141- // locked into a particular API majorVersion.
142- //
143- // Given the following example resource:
144- //
145- // apiVersion: atlas.generated.mongodb.com/v1
146- // kind: SearchIndex
147- // metadata:
148- // name: search-index
149- // spec:
150- // v20250312:
151- //
152- // In the above case crdVersion is "v1" and majorVersion is "v20250312".
153- func NewTranslator (scheme * runtime.Scheme , crd * apiextensionsv1.CustomResourceDefinition , crdVersion string , majorVersion string ) (Translator , error ) {
154- specVersion := crds .SelectVersion (& crd .Spec , crdVersion )
155- if err := crds .AssertMajorVersion (specVersion , crd .Spec .Names .Kind , majorVersion ); err != nil {
156- return nil , fmt .Errorf ("failed to assert major version %s in CRD: %w" , majorVersion , err )
157- }
158- var mappingSchema openapi3.Schema
159- mappingString , ok := crd .Annotations ["api-mappings" ]
160- if ok && mappingString != "" {
161- jsonBytes , err := yaml .YAMLToJSON ([]byte (mappingString ))
162- if err != nil {
163- return nil , fmt .Errorf ("failed to convert 'api-mappings' YAML to JSON: %w" , err )
164- }
165- if err := json .Unmarshal (jsonBytes , & mappingSchema ); err != nil {
166- return nil , fmt .Errorf ("failed to unmarshal 'api-mappings' JSON into schema: %w" , err )
167- }
168- }
169-
170- return & translator {
171- scheme : scheme ,
172- majorVersion : majorVersion ,
173- mappingSchema : & openapi3.SchemaRef {Value : & mappingSchema },
174- }, nil
175- }
176-
177- // NewPerVersionTranslators creates a set of translators indexed by SDK versions
178- //
179- // Given the following example resource:
180- //
181- // apiVersion: atlas.generated.mongodb.com/v1
182- // kind: SearchIndex
183- // metadata:
184- // name: search-index
185- // spec:
186- // v20250312:
187- // ...
188- // v20250810:
189- //
190- // In the above case crdVersion is "v1" and versions can be "v20250312"
191- // and/or "v20250810".
192- func NewPerVersionTranslators (scheme * runtime.Scheme , crd * apiextensionsv1.CustomResourceDefinition , crdVersion string , versions ... string ) (map [string ]Translator , error ) {
193- translators := map [string ]Translator {}
194- specVersion := crds .SelectVersion (& crd .Spec , crdVersion )
195- for _ , version := range versions {
196- if err := crds .AssertMajorVersion (specVersion , crd .Spec .Names .Kind , version ); err != nil {
197- return nil , fmt .Errorf ("failed to assert major version %s in CRD: %w" , version , err )
198- }
199- var mappingSchema openapi3.Schema
200- mappingString , ok := crd .Annotations ["api-mappings" ]
201- if ok && mappingString != "" {
202- jsonBytes , err := yaml .YAMLToJSON ([]byte (mappingString ))
203- if err != nil {
204- return nil , fmt .Errorf ("failed to convert 'api-mappings' YAML to JSON: %w" , err )
205- }
206- if err := json .Unmarshal (jsonBytes , & mappingSchema ); err != nil {
207- return nil , fmt .Errorf ("failed to unmarshal 'api-mappings' JSON into schema: %w" , err )
208- }
209- }
210-
211- translators [version ] = & translator {
212- scheme : scheme ,
213- majorVersion : version ,
214- mappingSchema : & openapi3.SchemaRef {Value : & mappingSchema },
215- }
216- }
217- return translators , nil
218- }
219-
220242// collapseReferences finds all Kubernetes references, solves them and collapses
221243// them by replacing their values from the reference (e.g Kubernetes secret or
222244// group), into the corresponding API request value
@@ -246,3 +268,32 @@ func propertyValueOrNil(schema *openapi3.Schema, propertyName string) *openapi3.
246268 }
247269 return nil
248270}
271+
272+ // isNil properly checks if an interface{} value is nil, including nil pointers
273+ // assigned to interfaces (which are not == nil in Go)
274+ func isNil (v any ) bool {
275+ if v == nil {
276+ return true
277+ }
278+ rv := reflect .ValueOf (v )
279+ switch rv .Kind () {
280+ case reflect .Chan , reflect .Func , reflect .Interface , reflect .Map , reflect .Ptr , reflect .Slice :
281+ return rv .IsNil ()
282+ }
283+ return false
284+ }
285+
286+ func checkGVK (scheme * runtime.Scheme , target client.Object , gvk schema.GroupVersionKind ) error {
287+ actualGVK := target .GetObjectKind ().GroupVersionKind ()
288+ if actualGVK .Kind == "" || actualGVK .GroupVersion ().String () == "" {
289+ gvks , _ , err := scheme .ObjectKinds (target )
290+ if err != nil || len (gvks ) == 0 {
291+ return fmt .Errorf ("failed to infer GroupVersionKind for resource from scheme: %w" , err )
292+ }
293+ actualGVK = gvks [0 ]
294+ }
295+ if actualGVK .Kind != gvk .Kind || actualGVK .GroupVersion ().String () != gvk .GroupVersion ().String () {
296+ return fmt .Errorf ("target must be a %q but got %q" , gvk , actualGVK )
297+ }
298+ return nil
299+ }
0 commit comments