Skip to content

Commit adc2f58

Browse files
committed
Several minor fixes and improvements
Signed-off-by: jose.vazquez <jose.vazquez@mongodb.com>
1 parent c71896b commit adc2f58

File tree

10 files changed

+542
-318
lines changed

10 files changed

+542
-318
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ require (
161161
k8s.io/apiextensions-apiserver v0.34.1
162162
k8s.io/klog/v2 v2.130.1
163163
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
164-
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
164+
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
165165
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
166166
sigs.k8s.io/yaml v1.6.0
167167
)

internal/autogen/translate/crd.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,24 +39,24 @@ func selectVersion(spec *apiextensionsv1.CustomResourceDefinitionSpec, version s
3939

4040
func getOpenAPIProperties(kind string, version *apiextensionsv1.CustomResourceDefinitionVersion) (map[string]apiextensionsv1.JSONSchemaProps, error) {
4141
if version == nil {
42-
return nil, fmt.Errorf("missing version %q from %v spec", version, kind)
42+
return nil, fmt.Errorf("missing version (nil) from %v spec", kind)
4343
}
4444
if version.Schema == nil {
45-
return nil, fmt.Errorf("missing version %q schema from %v spec", version, kind)
45+
return nil, fmt.Errorf("missing version schema from %v spec", kind)
4646
}
4747
if version.Schema.OpenAPIV3Schema == nil {
48-
return nil, fmt.Errorf("missing version %q OpenAPI Schema from %v spec", version, kind)
48+
return nil, fmt.Errorf("missing version OpenAPI Schema from %v spec", kind)
4949
}
5050
if version.Schema.OpenAPIV3Schema.Properties == nil {
51-
return nil, fmt.Errorf("missing version %q OpenAPI Properties from %v spec", version, kind)
51+
return nil, fmt.Errorf("missing version OpenAPI Properties from %v spec", kind)
5252
}
5353
return version.Schema.OpenAPIV3Schema.Properties, nil
5454
}
5555

5656
func getSpecPropertiesFor(kind string, props map[string]apiextensionsv1.JSONSchemaProps, field string) (map[string]apiextensionsv1.JSONSchemaProps, error) {
5757
prop, ok := props[field]
5858
if !ok {
59-
return nil, fmt.Errorf(" kind %q spec is missing field %q on", field, kind)
59+
return nil, fmt.Errorf("kind %q spec is missing field %q on", kind, field)
6060
}
6161
if prop.Type != "object" {
6262
return nil, fmt.Errorf("kind %q field %q expected to be object but is %v", kind, field, prop.Type)
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
// Copyright 2025 Google LLC
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+
package translate
16+
17+
import (
18+
"fmt"
19+
"testing"
20+
21+
"github.com/stretchr/testify/require"
22+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
23+
)
24+
25+
func TestSelectVersion(t *testing.T) {
26+
// Define some sample versions to be reused in the tests
27+
v1 := apiextensionsv1.CustomResourceDefinitionVersion{Name: "v1", Served: true}
28+
v2 := apiextensionsv1.CustomResourceDefinitionVersion{Name: "v2", Served: true}
29+
v1beta1 := apiextensionsv1.CustomResourceDefinitionVersion{Name: "v1beta1", Served: true}
30+
31+
testCases := []struct {
32+
name string
33+
spec *apiextensionsv1.CustomResourceDefinitionSpec
34+
version string
35+
want *apiextensionsv1.CustomResourceDefinitionVersion
36+
}{
37+
{
38+
name: "should return nil when spec has no versions",
39+
spec: &apiextensionsv1.CustomResourceDefinitionSpec{
40+
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{},
41+
},
42+
version: "v1",
43+
want: nil,
44+
},
45+
{
46+
name: "should return the first version when version string is empty",
47+
spec: &apiextensionsv1.CustomResourceDefinitionSpec{
48+
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{v1beta1, v1},
49+
},
50+
version: "",
51+
want: &v1beta1,
52+
},
53+
{
54+
name: "should return the matching version when it exists",
55+
spec: &apiextensionsv1.CustomResourceDefinitionSpec{
56+
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{v1, v2},
57+
},
58+
version: "v2",
59+
want: &v2,
60+
},
61+
{
62+
name: "should return nil when the requested version does not exist",
63+
spec: &apiextensionsv1.CustomResourceDefinitionSpec{
64+
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{v1, v2},
65+
},
66+
version: "v3",
67+
want: nil,
68+
},
69+
{
70+
name: "should handle a single version in the spec",
71+
spec: &apiextensionsv1.CustomResourceDefinitionSpec{
72+
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{v1},
73+
},
74+
version: "v1",
75+
want: &v1,
76+
},
77+
}
78+
79+
for _, tc := range testCases {
80+
t.Run(tc.name, func(t *testing.T) {
81+
got := selectVersion(tc.spec, tc.version)
82+
require.Equal(t, tc.want, got)
83+
})
84+
}
85+
}
86+
87+
func TestGetOpenAPIProperties(t *testing.T) {
88+
const kind = "MyTestCRD"
89+
90+
happyPathVersion := &apiextensionsv1.CustomResourceDefinitionVersion{
91+
Name: "v1",
92+
Schema: &apiextensionsv1.CustomResourceValidation{
93+
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
94+
Properties: map[string]apiextensionsv1.JSONSchemaProps{
95+
"spec": {
96+
Type: "object",
97+
Properties: map[string]apiextensionsv1.JSONSchemaProps{
98+
"fieldA": {Type: "string"},
99+
},
100+
},
101+
},
102+
},
103+
},
104+
}
105+
106+
testCases := []struct {
107+
name string
108+
version *apiextensionsv1.CustomResourceDefinitionVersion
109+
wantProperties map[string]apiextensionsv1.JSONSchemaProps
110+
wantErrMsg string
111+
}{
112+
{
113+
name: "should return properties",
114+
version: happyPathVersion,
115+
wantProperties: happyPathVersion.Schema.OpenAPIV3Schema.Properties,
116+
wantErrMsg: "",
117+
},
118+
{
119+
name: "should return error if version is nil",
120+
version: nil,
121+
wantProperties: nil,
122+
wantErrMsg: fmt.Sprintf("missing version (nil) from %v spec", kind),
123+
},
124+
{
125+
name: "should return error if schema is nil",
126+
version: &apiextensionsv1.CustomResourceDefinitionVersion{
127+
Name: "v1",
128+
Schema: nil, // The point of failure
129+
},
130+
wantProperties: nil,
131+
wantErrMsg: fmt.Sprintf("missing version schema from %v spec", kind),
132+
},
133+
{
134+
name: "error - should return error if OpenAPIV3Schema is nil",
135+
version: &apiextensionsv1.CustomResourceDefinitionVersion{
136+
Name: "v1",
137+
Schema: &apiextensionsv1.CustomResourceValidation{
138+
OpenAPIV3Schema: nil, // The point of failure
139+
},
140+
},
141+
wantProperties: nil,
142+
wantErrMsg: fmt.Sprintf("missing version OpenAPI Schema from %v spec", kind),
143+
},
144+
{
145+
name: "should return error if Properties map is nil",
146+
version: &apiextensionsv1.CustomResourceDefinitionVersion{
147+
Name: "v1",
148+
Schema: &apiextensionsv1.CustomResourceValidation{
149+
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
150+
Properties: nil, // The point of failure
151+
},
152+
},
153+
},
154+
wantProperties: nil,
155+
wantErrMsg: fmt.Sprintf("missing version OpenAPI Properties from %v spec", kind),
156+
},
157+
}
158+
159+
for _, tc := range testCases {
160+
t.Run(tc.name, func(t *testing.T) {
161+
gotProperties, err := getOpenAPIProperties(kind, tc.version)
162+
if tc.wantErrMsg != "" {
163+
require.Error(t, err)
164+
require.EqualError(t, err, tc.wantErrMsg)
165+
require.Nil(t, gotProperties)
166+
} else {
167+
require.NoError(t, err)
168+
require.Equal(t, tc.wantProperties, gotProperties)
169+
}
170+
})
171+
}
172+
}
173+
174+
func TestGetSpecPropertiesFor(t *testing.T) {
175+
const kind = "MyTestCRD"
176+
177+
// Reusable properties for the test cases
178+
nestedProperties := map[string]apiextensionsv1.JSONSchemaProps{
179+
"replicas": {Type: "integer"},
180+
"image": {Type: "string"},
181+
}
182+
183+
testCases := []struct {
184+
name string
185+
props map[string]apiextensionsv1.JSONSchemaProps
186+
field string
187+
wantProperties map[string]apiextensionsv1.JSONSchemaProps
188+
wantErrMsg string
189+
}{
190+
{
191+
name: "should return nested properties",
192+
props: map[string]apiextensionsv1.JSONSchemaProps{
193+
"spec": {
194+
Type: "object",
195+
Properties: nestedProperties,
196+
},
197+
"status": {
198+
Type: "object",
199+
},
200+
},
201+
field: "spec",
202+
wantProperties: nestedProperties,
203+
wantErrMsg: "", // Expect no error
204+
},
205+
{
206+
name: "field is missing from properties map",
207+
props: map[string]apiextensionsv1.JSONSchemaProps{
208+
"status": {Type: "object"},
209+
},
210+
field: "spec", // This field does not exist
211+
wantProperties: nil,
212+
wantErrMsg: fmt.Sprintf("kind %q spec is missing field %q on", kind, "spec"),
213+
},
214+
{
215+
name: "field is not of type object",
216+
props: map[string]apiextensionsv1.JSONSchemaProps{
217+
"spec": {Type: "string"}, // The point of failure
218+
},
219+
field: "spec",
220+
wantProperties: nil,
221+
wantErrMsg: fmt.Sprintf("kind %q field %q expected to be object but is %v", kind, "spec", "string"),
222+
},
223+
{
224+
name: "field is an object but has no nested properties",
225+
props: map[string]apiextensionsv1.JSONSchemaProps{
226+
"spec": {
227+
Type: "object",
228+
Properties: nil, // The nested map is nil
229+
},
230+
},
231+
field: "spec",
232+
wantProperties: nil, // Expecting a nil map is correct here
233+
wantErrMsg: "",
234+
},
235+
}
236+
237+
for _, tc := range testCases {
238+
t.Run(tc.name, func(t *testing.T) {
239+
// Act
240+
gotProperties, err := getSpecPropertiesFor(kind, tc.props, tc.field)
241+
242+
// Assert
243+
if tc.wantErrMsg != "" {
244+
// We expect an error
245+
require.Error(t, err)
246+
require.EqualError(t, err, tc.wantErrMsg)
247+
require.Nil(t, gotProperties)
248+
} else {
249+
// We expect success
250+
require.NoError(t, err)
251+
require.Equal(t, tc.wantProperties, gotProperties)
252+
}
253+
})
254+
}
255+
}

internal/autogen/translate/mapping.go

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package translate
1818
import (
1919
"errors"
2020
"fmt"
21-
"reflect"
2221

2322
"github.com/stretchr/testify/assert/yaml"
2423
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -91,14 +90,17 @@ func ExpandMappings(r *Request, obj map[string]any, main client.Object) ([]clien
9190
return nil, fmt.Errorf("failed to unmarshal mappings YAML: %w", err)
9291
}
9392

94-
if err := em.expandMappingsAt(obj, mappings, "spec", r.Translator.majorVersion); err != nil {
95-
return nil, fmt.Errorf("failed to map properties of spec from API to Kubernetes: %w", err)
96-
}
97-
if err := em.expandMappingsAt(obj, mappings, "spec", r.Translator.majorVersion, "entry"); err != nil {
98-
return nil, fmt.Errorf("failed to map properties of spec from API to Kubernetes: %w", err)
99-
}
100-
if err := em.expandMappingsAt(obj, mappings, "status", r.Translator.majorVersion); err != nil {
101-
return nil, fmt.Errorf("failed to map properties of status from API to Kubernetes: %w", err)
93+
for _, entry := range []struct {
94+
title string
95+
path []string
96+
}{
97+
{title: "spec", path: []string{"spec", r.Translator.majorVersion}},
98+
{title: "spec entry", path: []string{"spec", r.Translator.majorVersion, "entry"}},
99+
{title: "status", path: []string{"status", r.Translator.majorVersion}},
100+
} {
101+
if err := em.expandMappingsAt(obj, mappings, entry.path...); err != nil {
102+
return nil, fmt.Errorf("failed to map properties of %q from API to Kubernetes: %w", entry.title, err)
103+
}
102104
}
103105
return em.added, nil
104106
}
@@ -124,13 +126,6 @@ func CollapseMappings(r *Request, spec map[string]any, main client.Object) error
124126
return cm.mapProperties([]string{}, props, spec)
125127
}
126128

127-
func findEntryPathInTarget(targetType reflect.Type) []string {
128-
if targetType.String() == "admin.CreateAlertConfigurationApiParams" {
129-
return []string{"GroupAlertsConfig"}
130-
}
131-
return []string{}
132-
}
133-
134129
func (m *mapper) expandMappingsAt(obj, mappings map[string]any, fields ...string) error {
135130
expandedPath := []string{"properties"}
136131
for _, field := range fields {
@@ -200,7 +195,10 @@ func (m *mapper) mapArray(path []string, mapping map[string]any, list []any) err
200195
if !ok {
201196
return fmt.Errorf("expected field %q at %v to be a map but was: %T", mapName, path, mapItem)
202197
}
203-
key, entry := entryMatchingMapping(mapName, mapping, list, m.expand)
198+
key, entry, err := entryMatchingMapping(mapName, mapping, list, m.expand)
199+
if err != nil {
200+
return fmt.Errorf("failed to match mapping within array item %q at %v: %w", mapItem, path, err)
201+
}
204202
if entry == nil {
205203
continue
206204
}
@@ -238,28 +236,36 @@ func (m *mapper) mapReference(path []string, mappingName string, mapping, obj ma
238236
return ref.Collapse(m.mapContext, path, obj)
239237
}
240238

241-
func entryMatchingMapping(mapName string, mapping map[string]any, list []any, expand bool) (string, map[string]any) {
239+
func entryMatchingMapping(mapName string, mapping map[string]any, list []any, expand bool) (string, map[string]any, error) {
242240
key := mapName
243241
if expand {
244242
refMap := refMapping{}
245243
if err := unstructured.FromUnstructured(&refMap, mapping); err != nil {
246-
return "", nil // not a ref, cannot reverse mapping dfrom API property name
244+
return "", nil, fmt.Errorf("not a reference, cannot reverse mapping from API property name")
247245
}
248246
path := resolveXPath(refMap.XOpenAPIMapping.Property)
249247
key = unstructured.Base(path)
250248
}
251-
return key, findByExistingKey(list, key)
249+
m, err := findByExistingUniqueKey(list, key)
250+
return key, m, err
252251
}
253252

254-
func findByExistingKey(list []any, key string) map[string]any {
253+
func findByExistingUniqueKey(list []any, key string) (map[string]any, error) {
254+
candidates := []map[string]any{}
255255
for _, item := range list {
256256
obj, ok := (item).(map[string]any)
257257
if !ok {
258258
continue
259259
}
260260
if _, ok := obj[key]; ok {
261-
return obj
261+
candidates = append(candidates, obj)
262262
}
263263
}
264-
return nil
264+
if len(candidates) == 1 {
265+
return candidates[0], nil
266+
}
267+
if len(candidates) > 1 {
268+
return nil, fmt.Errorf("too many matches for key %q: %v", key, candidates)
269+
}
270+
return nil, nil
265271
}

0 commit comments

Comments
 (0)