Skip to content

Commit 79f099e

Browse files
authored
CLOUDP-365467: Test unstructured (#3020)
Signed-off-by: jose.vazquez <[email protected]>
1 parent f9b8010 commit 79f099e

File tree

6 files changed

+731
-83
lines changed

6 files changed

+731
-83
lines changed

internal/crapi/crapi_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,7 @@ func TestToAPIAllRefs(t *testing.T) {
527527
},
528528
},
529529
},
530+
// nolint:dupl
530531
want: admin2025.GroupAlertsConfig{
531532
Enabled: pointer.MakePtr(true),
532533
EventTypeName: pointer.MakePtr("some-event"),

internal/crapi/translator.go

Lines changed: 131 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -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
36119
type translator struct {
37120
scheme *runtime.Scheme
38121
majorVersion string
122+
gvk schema.GroupVersionKind
39123
mappingSchema *openapi3.SchemaRef
40124
}
41125

42126
func (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

73166
func (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+
}

internal/crapi/translator_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2025 MongoDB Inc
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
16+
package crapi
17+
18+
import (
19+
"testing"
20+
21+
"github.com/stretchr/testify/assert"
22+
"github.com/stretchr/testify/require"
23+
"sigs.k8s.io/controller-runtime/pkg/client"
24+
25+
v1 "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/crapi/testdata/samples/v1"
26+
"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer"
27+
)
28+
29+
func TestIsNil(t *testing.T) {
30+
for _, tc := range []struct {
31+
title string
32+
value any
33+
want bool
34+
}{
35+
{title: "nil Group", value: (*v1.Group)(nil), want: true},
36+
{title: "nil interface", value: nil, want: true},
37+
{title: "nil pointer", value: (*int)(nil), want: true},
38+
{title: "nil slice", value: []int(nil), want: true},
39+
{title: "nil map", value: map[string]int(nil), want: true},
40+
{title: "non-nil interface", value: 1, want: false},
41+
{title: "non-nil pointer", value: pointer.MakePtr(1), want: false},
42+
{title: "non-nil slice", value: []int{1}, want: false},
43+
{title: "non-nil map", value: map[string]int{"1": 1}, want: false},
44+
} {
45+
t.Run(tc.title, func(t *testing.T) {
46+
got := isNil(tc.value)
47+
assert.Equal(t, tc.want, got)
48+
})
49+
}
50+
}
51+
52+
func TestIsNilObject(t *testing.T) {
53+
for _, tc := range []struct {
54+
title string
55+
value client.Object
56+
want bool
57+
}{
58+
{title: "nil struct", value: (*v1.Group)(nil), want: true},
59+
{title: "non-nil map", value: &v1.Group{}, want: false},
60+
} {
61+
t.Run(tc.title, func(t *testing.T) {
62+
got := isNil(tc.value)
63+
require.False(t, tc.value == nil)
64+
assert.Equal(t, tc.want, got)
65+
})
66+
}
67+
}

internal/crapi/unstructured/field.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ func GetOrCreateField[T any](obj map[string]any, defaultValue T, fields ...strin
149149
}
150150
var emptyValue T
151151
if errors.Is(err, ErrNotFound) {
152-
if err := CreateField(obj, defaultValue, fields...); err != nil {
152+
if err := RecursiveCreateField(obj, defaultValue, fields...); err != nil {
153153
return emptyValue, fmt.Errorf("failed to create field at path %v: %w", fields, err)
154154
}
155155
return defaultValue, nil

internal/crapi/unstructured/field_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,11 @@ func TestGetOrCreateField(t *testing.T) {
5454
want: "some string",
5555
},
5656
{
57-
title: "fail to create branch",
57+
title: "creates deep branch",
5858
obj: map[string]any{},
5959
path: []string{"deep", "field"},
6060
defaultValue: "some string",
61-
wantErr: "field \"deep\": not found",
61+
want: "some string",
6262
},
6363
{
6464
title: "read existing",

0 commit comments

Comments
 (0)