Skip to content

Commit 5aba679

Browse files
committed
Separate refs to its own package
1 parent adc2f58 commit 5aba679

File tree

9 files changed

+224
-193
lines changed

9 files changed

+224
-193
lines changed

internal/autogen/translate/crd.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,98 @@
1616
package translate
1717

1818
import (
19+
"bytes"
20+
"encoding/json"
1921
"fmt"
2022

23+
"github.com/santhosh-tekuri/jsonschema/v5"
2124
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2225
)
2326

27+
// crdTranslator implements Translator to translate from a given CRD to and from
28+
// a given SDK version using the same upstream OpenAPI schema
29+
type crdTranslator struct {
30+
majorVersion string
31+
jsonSchema *jsonschema.Schema
32+
annotations map[string]string
33+
}
34+
35+
func (tr *crdTranslator) Annotation(annotation string) string {
36+
return tr.annotations[annotation]
37+
}
38+
39+
func (tr *crdTranslator) MajorVersion() string {
40+
return tr.majorVersion
41+
}
42+
43+
func (tr *crdTranslator) Validate(unstructuredObj map[string]any) error {
44+
if err := tr.jsonSchema.Validate(unstructuredObj); err != nil {
45+
return fmt.Errorf("object validation failed against CRD schema: %w", err)
46+
}
47+
return nil
48+
}
49+
50+
// NewTranslator creates a translator for a particular CRD and major version pairs,
51+
// and with a particular set of known Kubernetes object dependencies.
52+
//
53+
// Given the following example resource:
54+
//
55+
// apiVersion: atlas.generated.mongodb.com/v1
56+
// kind: SearchIndex
57+
// metadata:
58+
// name: search-index
59+
// spec:
60+
// v20250312:
61+
//
62+
// In the above case crdVersion is "v1" and majorVersion is "v20250312".
63+
func NewTranslator(crd *apiextensionsv1.CustomResourceDefinition, crdVersion string, majorVersion string) (Translator, error) {
64+
specVersion := selectVersion(&crd.Spec, crdVersion)
65+
if err := assertMajorVersion(specVersion, crd.Spec.Names.Kind, majorVersion); err != nil {
66+
return nil, fmt.Errorf("failed to assert major version %s in CRD: %w", majorVersion, err)
67+
}
68+
schema, err := compileCRDSchema(specVersion.Schema.OpenAPIV3Schema)
69+
if err != nil {
70+
return nil, fmt.Errorf("failed to compile schema: %w", err)
71+
}
72+
return &crdTranslator{
73+
majorVersion: majorVersion,
74+
jsonSchema: schema,
75+
annotations: crd.Annotations,
76+
}, nil
77+
}
78+
79+
func assertMajorVersion(specVersion *apiextensionsv1.CustomResourceDefinitionVersion, kind string, majorVersion string) error {
80+
props, err := getOpenAPIProperties(kind, specVersion)
81+
if err != nil {
82+
return fmt.Errorf("failed to enumerate CRD schema properties: %w", err)
83+
}
84+
specProps, err := getSpecPropertiesFor(kind, props, "spec")
85+
if err != nil {
86+
return fmt.Errorf("failed to enumerate CRD spec properties: %w", err)
87+
}
88+
_, ok := specProps[majorVersion]
89+
if !ok {
90+
return fmt.Errorf("failed to match the CRD spec version %q in schema", majorVersion)
91+
}
92+
return nil
93+
}
94+
95+
func compileCRDSchema(openAPISchema *apiextensionsv1.JSONSchemaProps) (*jsonschema.Schema, error) {
96+
schemaBytes, err := json.Marshal(openAPISchema)
97+
if err != nil {
98+
return nil, fmt.Errorf("failed to marshal CRD schema to JSON: %w", err)
99+
}
100+
compiler := jsonschema.NewCompiler()
101+
if err := compiler.AddResource("schema.json", bytes.NewReader(schemaBytes)); err != nil {
102+
return nil, fmt.Errorf("failed to add schema resource: %w", err)
103+
}
104+
schema, err := compiler.Compile("schema.json")
105+
if err != nil {
106+
return nil, fmt.Errorf("failed to compile schema: %w", err)
107+
}
108+
return schema, nil
109+
}
110+
24111
// selectVersion returns the version from the CRD spec that matches the given version string
25112
func selectVersion(spec *apiextensionsv1.CustomResourceDefinitionSpec, version string) *apiextensionsv1.CustomResourceDefinitionVersion {
26113
if len(spec.Versions) == 0 {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package refs
2+
3+
import "sigs.k8s.io/controller-runtime/pkg/client"
4+
5+
// context points to the main Kubernetes object being translated,
6+
// and holds related existing & added Kubernetes dependencies
7+
type context struct {
8+
main client.Object
9+
m map[client.ObjectKey]client.Object
10+
added []client.Object
11+
}
12+
13+
func newMapContext(main client.Object, deps []client.Object) *context {
14+
m := map[client.ObjectKey]client.Object{}
15+
for _, obj := range deps {
16+
m[client.ObjectKeyFromObject(obj)] = obj
17+
}
18+
return &context{main: main, m: m}
19+
}
20+
21+
func (mc *context) find(name string) client.Object {
22+
key := client.ObjectKey{Name: name, Namespace: mc.main.GetNamespace()}
23+
return mc.m[key]
24+
}
25+
26+
func (mc *context) has(name string) bool {
27+
return mc.find(name) != nil
28+
}
29+
30+
func (mc *context) add(obj client.Object) {
31+
mc.m[client.ObjectKeyFromObject(obj)] = obj
32+
mc.added = append(mc.added, obj)
33+
}

internal/autogen/translate/mapping.go renamed to internal/autogen/translate/refs/handler.go

Lines changed: 25 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,12 @@
1313
// limitations under the License.
1414
//
1515

16-
package translate
16+
package refs
1717

1818
import (
1919
"errors"
2020
"fmt"
2121

22-
"github.com/stretchr/testify/assert/yaml"
2322
"sigs.k8s.io/controller-runtime/pkg/client"
2423

2524
"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/autogen/translate/unstructured"
@@ -31,102 +30,19 @@ const (
3130
SecretProperySelector = "$.data.#"
3231
)
3332

34-
type mapContext struct {
35-
main client.Object
36-
m map[client.ObjectKey]client.Object
37-
added []client.Object
38-
}
39-
40-
func newMapContext(main client.Object, deps []client.Object) *mapContext {
41-
m := map[client.ObjectKey]client.Object{}
42-
for _, obj := range deps {
43-
m[client.ObjectKeyFromObject(obj)] = obj
44-
}
45-
return &mapContext{main: main, m: m}
46-
}
47-
48-
func (mc *mapContext) find(name string) client.Object {
49-
key := client.ObjectKey{Name: name, Namespace: mc.main.GetNamespace()}
50-
return mc.m[key]
51-
}
52-
53-
func (mc *mapContext) has(name string) bool {
54-
return mc.find(name) != nil
55-
}
56-
57-
func (mc *mapContext) add(obj client.Object) {
58-
mc.m[client.ObjectKeyFromObject(obj)] = obj
59-
mc.added = append(mc.added, obj)
60-
}
61-
62-
type mapper struct {
63-
*mapContext
33+
// Handler hodls the context needed to expand or collapse the references on an
34+
// Kubernetes object translaion to and from API data
35+
type Handler struct {
36+
*context
6437
expand bool
6538
}
6639

67-
func newExpanderMapper(main client.Object, deps []client.Object) *mapper {
68-
return newMapper(true, main, deps)
69-
}
70-
71-
func newCollarserMapper(main client.Object, deps []client.Object) *mapper {
72-
return newMapper(false, main, deps)
40+
func NewHandler(main client.Object, deps []client.Object) *Handler {
41+
return &Handler{context: newMapContext(main, deps)}
7342
}
7443

75-
func newMapper(expand bool, main client.Object, deps []client.Object) *mapper {
76-
return &mapper{
77-
mapContext: newMapContext(main, deps),
78-
expand: expand,
79-
}
80-
}
81-
82-
func ExpandMappings(r *Request, obj map[string]any, main client.Object) ([]client.Object, error) {
83-
em := newExpanderMapper(main, r.Dependencies)
84-
mappingsYML := r.Translator.annotations[APIMAppingsAnnotation]
85-
if mappingsYML == "" {
86-
return []client.Object{}, nil
87-
}
88-
mappings := map[string]any{}
89-
if err := yaml.Unmarshal([]byte(mappingsYML), mappings); err != nil {
90-
return nil, fmt.Errorf("failed to unmarshal mappings YAML: %w", err)
91-
}
92-
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-
}
104-
}
105-
return em.added, nil
106-
}
107-
108-
func CollapseMappings(r *Request, spec map[string]any, main client.Object) error {
109-
cm := newCollarserMapper(main, r.Dependencies)
110-
mappingsYML := r.Translator.annotations[APIMAppingsAnnotation]
111-
if mappingsYML == "" {
112-
return nil
113-
}
114-
mappings := map[string]any{}
115-
if err := yaml.Unmarshal([]byte(mappingsYML), mappings); err != nil {
116-
return fmt.Errorf("failed to unmarshal mappings YAML: %w", err)
117-
}
118-
props, err := unstructured.AccessField[map[string]any](mappings,
119-
"properties", "spec", "properties", r.Translator.majorVersion, "properties")
120-
if errors.Is(err, unstructured.ErrNotFound) {
121-
return nil
122-
}
123-
if err != nil {
124-
return fmt.Errorf("failed to access the API mapping properties for the spec: %w", err)
125-
}
126-
return cm.mapProperties([]string{}, props, spec)
127-
}
128-
129-
func (m *mapper) expandMappingsAt(obj, mappings map[string]any, fields ...string) error {
44+
func (h *Handler) ExpandMappings(obj, mappings map[string]any, fields ...string) error {
45+
h.expand = true
13046
expandedPath := []string{"properties"}
13147
for _, field := range fields {
13248
expandedPath = append(expandedPath, field, "properties")
@@ -142,13 +58,22 @@ func (m *mapper) expandMappingsAt(obj, mappings map[string]any, fields ...string
14258
if err != nil {
14359
return fmt.Errorf("failed to access object's %v: %w", fields, err)
14460
}
145-
if err := m.mapProperties([]string{}, props, field); err != nil {
61+
if err := h.mapProperties([]string{}, props, field); err != nil {
14662
return fmt.Errorf("failed to process properties from API into %v: %w", fields, err)
14763
}
14864
return nil
14965
}
15066

151-
func (m *mapper) mapProperties(path []string, props, obj map[string]any) error {
67+
func (h *Handler) Added() []client.Object {
68+
return h.added
69+
}
70+
71+
func (h *Handler) CollapseReferences(path []string, props, obj map[string]any) error {
72+
h.expand = false
73+
return h.mapProperties(path, props, obj)
74+
}
75+
76+
func (m *Handler) mapProperties(path []string, props, obj map[string]any) error {
15277
for key, prop := range props {
15378
mapping, ok := (prop).(map[string]any)
15479
if !ok {
@@ -185,7 +110,7 @@ func (m *mapper) mapProperties(path []string, props, obj map[string]any) error {
185110
return nil
186111
}
187112

188-
func (m *mapper) mapArray(path []string, mapping map[string]any, list []any) error {
113+
func (m *Handler) mapArray(path []string, mapping map[string]any, list []any) error {
189114
mapItems, err := unstructured.AccessField[map[string]any](mapping, "items", "properties")
190115
if err != nil {
191116
return fmt.Errorf("failed to access %q: %w", unstructured.Base(path), err)
@@ -210,7 +135,7 @@ func (m *mapper) mapArray(path []string, mapping map[string]any, list []any) err
210135
return nil
211136
}
212137

213-
func (m *mapper) mapObject(path []string, mapName string, mapping, obj map[string]any) error {
138+
func (m *Handler) mapObject(path []string, mapName string, mapping, obj map[string]any) error {
214139
if mapping["properties"] != nil {
215140
props, err := unstructured.AccessField[map[string]any](mapping, "properties")
216141
if err != nil {
@@ -224,16 +149,16 @@ func (m *mapper) mapObject(path []string, mapName string, mapping, obj map[strin
224149
return fmt.Errorf("unsupported extension at %v with fields %v", path, unstructured.FieldsOf(mapping))
225150
}
226151

227-
func (m *mapper) mapReference(path []string, mappingName string, mapping, obj map[string]any) error {
152+
func (m *Handler) mapReference(path []string, mappingName string, mapping, obj map[string]any) error {
228153
rm := refMapping{}
229154
if err := unstructured.FromUnstructured(&rm, mapping); err != nil {
230155
return fmt.Errorf("failed to parse a reference mapping: %w", err)
231156
}
232157
ref := newRef(mappingName, &rm)
233158
if m.expand {
234-
return ref.Expand(m.mapContext, path, obj)
159+
return ref.Expand(m.context, path, obj)
235160
}
236-
return ref.Collapse(m.mapContext, path, obj)
161+
return ref.Collapse(m.context, path, obj)
237162
}
238163

239164
func entryMatchingMapping(mapName string, mapping map[string]any, list []any, expand bool) (string, map[string]any, error) {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
package translate
15+
package refs
1616

1717
import (
1818
"fmt"

internal/autogen/translate/names_test.go renamed to internal/autogen/translate/refs/names_test.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
package translate_test
15+
package refs_test
1616

1717
import (
1818
"testing"
1919

20+
"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/autogen/translate/refs"
2021
"github.com/stretchr/testify/assert"
21-
22-
"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/autogen/translate"
2322
)
2423

2524
func TestPrefixedName(t *testing.T) {
@@ -51,7 +50,7 @@ func TestPrefixedName(t *testing.T) {
5150
},
5251
} {
5352
t.Run(tc.title, func(t *testing.T) {
54-
got := translate.PrefixedName(tc.prefix, tc.name, tc.args...)
53+
got := refs.PrefixedName(tc.prefix, tc.name, tc.args...)
5554
assert.Equal(t, tc.want, got)
5655
})
5756
}

0 commit comments

Comments
 (0)