1616package translate
1717
1818import (
19+ "bytes"
20+ "encoding/json"
1921 "fmt"
2022 "reflect"
2123
24+ "github.com/go-logr/logr"
25+ "github.com/santhosh-tekuri/jsonschema/v5"
2226 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2327 "sigs.k8s.io/controller-runtime/pkg/client"
2428
@@ -36,20 +40,28 @@ type PtrClientObj[T any] interface {
3640// A translator is an immutable configuration object, it can be safely shared
3741// across threads
3842type Translator struct {
39- crd CRDInfo
4043 majorVersion string
44+ jsonSchema * jsonschema.Schema
45+ annotations map [string ]string
46+ }
47+
48+ // Request holds common parameters for all translation request
49+ type Request struct {
50+ Translator * Translator
51+ Log logr.Logger
52+ Dependencies []client.Object
4153}
4254
4355// APIImporter can translate itself into Kubernetes Objects.
4456// Use to customize or accelerate translations ad-hoc
4557type APIImporter [T any , P PtrClientObj [T ]] interface {
46- FromAPI (t * Translator , target P ) ([]client.Object , error )
58+ FromAPI (tr * Request , target P ) ([]client.Object , error )
4759}
4860
4961// APIExporter can translate itself to an API Object.
5062// Use to customize or accelerate translations ad-hoc
5163type APIExporter [T any ] interface {
52- ToAPI (t * Translator , target * T ) error
64+ ToAPI (tr * Request , target * T ) error
5365}
5466
5567// NewTranslator creates a translator for a particular CRD and major version pairs,
@@ -65,81 +77,123 @@ type APIExporter[T any] interface {
6577// v20250312:
6678//
6779// In the above case crdVersion is "v1" and majorVersion is "v20250312".
68- func NewTranslator (crd * apiextensionsv1.CustomResourceDefinition , crdVersion string , majorVersion string ) * Translator {
80+ func NewTranslator (crd * apiextensionsv1.CustomResourceDefinition , crdVersion string , majorVersion string ) (* Translator , error ) {
81+ specVersion := selectVersion (& crd .Spec , crdVersion )
82+ if err := assertMajorVersion (specVersion , crd .Spec .Names .Kind , majorVersion ); err != nil {
83+ return nil , fmt .Errorf ("failed to assert major version %s in CRD: %w" , majorVersion , err )
84+ }
85+
86+ schemaBytes , err := json .Marshal (specVersion .Schema .OpenAPIV3Schema )
87+ if err != nil {
88+ return nil , fmt .Errorf ("failed to marshal CRD schema to JSON: %w" , err )
89+ }
90+
91+ // Compile the schema. You could cache this compiler for performance.
92+ compiler := jsonschema .NewCompiler ()
93+ if err := compiler .AddResource ("schema.json" , bytes .NewReader (schemaBytes )); err != nil {
94+ return nil , fmt .Errorf ("failed to add schema resource: %w" , err )
95+ }
96+ schema , err := compiler .Compile ("schema.json" )
97+ if err != nil {
98+ return nil , fmt .Errorf ("failed to compile schema: %w" , err )
99+ }
100+
69101 return & Translator {
70- crd : CRDInfo {definition : crd , version : crdVersion },
71102 majorVersion : majorVersion ,
103+ jsonSchema : schema ,
104+ annotations : crd .Annotations ,
105+ }, nil
106+ }
107+
108+ func assertMajorVersion (specVersion * apiextensionsv1.CustomResourceDefinitionVersion , kind string , majorVersion string ) error {
109+ props , err := getOpenAPIProperties (kind , specVersion )
110+ if err != nil {
111+ return fmt .Errorf ("failed to enumerate CRD schema properties: %w" , err )
112+ }
113+ specProps , err := getSpecPropertiesFor (kind , props , "spec" )
114+ if err != nil {
115+ return fmt .Errorf ("failed to enumerate CRD spec properties: %w" , err )
116+ }
117+ _ , ok := specProps [majorVersion ]
118+ if ! ok {
119+ return fmt .Errorf ("failed to match the CRD spec version %q in schema" , majorVersion )
120+ }
121+ return nil
122+ }
123+
124+ func (t * Translator ) Validate (unstructuredObj map [string ]any ) error {
125+ if err := t .jsonSchema .Validate (unstructuredObj ); err != nil {
126+ return fmt .Errorf ("object validation failed against CRD schema: %w" , err )
72127 }
128+ return nil
73129}
74130
75131// FromAPI translaters a source API structure into a Kubernetes object.
76- func FromAPI [S any , T any , P PtrClientObj [T ]](t * Translator , target P , source * S , objs ... client. Object ) ([]client.Object , error ) {
132+ func FromAPI [S any , T any , P PtrClientObj [T ]](r * Request , target P , source * S ) ([]client.Object , error ) {
77133 importer , ok := any (source ).(APIImporter [T , P ])
78134 if ok {
79- return importer .FromAPI (t , target )
135+ return importer .FromAPI (r , target )
80136 }
81137 sourceUnstructured , err := unstructured .ToUnstructured (source )
82138 if err != nil {
83139 return nil , fmt .Errorf ("failed to convert API source value to unstructured: %w" , err )
84140 }
85141
86- targetUnstructured := map [string ]any {}
142+ targetUnstructured , err := unstructured .ToUnstructured (target )
143+ if err != nil {
144+ return nil , fmt .Errorf ("failed to convert target value to unstructured: %w" , err )
145+ }
87146
88- versionedSpec := map [ string ] any {}
89- unstructured . CopyFields ( versionedSpec , sourceUnstructured )
90- if err := unstructured . CreateField ( targetUnstructured , versionedSpec , "spec" , t . majorVersion ); err != nil {
91- return nil , fmt .Errorf ("failed to create versioned spec object in unstructured target: %w" , err )
147+ versionedSpec , err := unstructured . AccessOrCreateField (
148+ targetUnstructured , map [ string ] any {}, "spec" , r . Translator . majorVersion )
149+ if err != nil {
150+ return nil , fmt .Errorf ("failed to ensure versioned spec object in unstructured target: %w" , err )
92151 }
152+ unstructured .CopyFields (versionedSpec , sourceUnstructured )
153+
93154 versionedSpecEntry := map [string ]any {}
94155 unstructured .CopyFields (versionedSpecEntry , sourceUnstructured )
95156 versionedSpec ["entry" ] = versionedSpecEntry
96157
97158 versionedStatus := map [string ]any {}
98159 unstructured .CopyFields (versionedStatus , sourceUnstructured )
99- if err := unstructured .CreateField (targetUnstructured , versionedStatus , "status" , t .majorVersion ); err != nil {
160+ if err := unstructured .CreateField (targetUnstructured , versionedStatus , "status" , r . Translator .majorVersion ); err != nil {
100161 return nil , fmt .Errorf ("failed to create versioned status object in unsstructured target: %w" , err )
101162 }
102163
103- extraObjects , err := ExpandMappings (t , targetUnstructured , target , objs ... )
164+ extraObjects , err := ExpandMappings (r , targetUnstructured , target )
104165 if err != nil {
105166 return nil , fmt .Errorf ("failed to process API mappings: %w" , err )
106167 }
168+ if err := r .Translator .Validate (targetUnstructured ); err != nil {
169+ return nil , fmt .Errorf ("failed to validate unstructured object output: %w" , err )
170+ }
107171 if err := unstructured .FromUnstructured (target , targetUnstructured ); err != nil {
108172 return nil , fmt .Errorf ("failed set structured kubernetes object from unstructured: %w" , err )
109173 }
110174 return append ([]client.Object {target }, extraObjects ... ), nil
111175}
112176
113177// ToAPI translates a source Kubernetes spec into a target API structure
114- func ToAPI [T any ](t * Translator , target * T , source client. Object , objs ... client.Object ) error {
178+ func ToAPI [T any ](r * Request , target * T , source client.Object ) error {
115179 exporter , ok := (source ).(APIExporter [T ])
116180 if ok {
117- return exporter .ToAPI (t , target )
118- }
119- specVersion := selectVersion (& t .crd .definition .Spec , t .crd .version )
120- kind := t .crd .definition .Spec .Names .Kind
121- props , err := getOpenAPIProperties (kind , specVersion )
122- if err != nil {
123- return fmt .Errorf ("failed to enumerate CRD schema properties: %w" , err )
124- }
125- specProps , err := getSpecPropertiesFor (kind , props , "spec" )
126- if err != nil {
127- return fmt .Errorf ("failed to enumerate CRD spec properties: %w" , err )
128- }
129- if _ , ok := specProps [t .majorVersion ]; ! ok {
130- return fmt .Errorf ("failed to match the CRD spec version %q in schema" , t .majorVersion )
181+ return exporter .ToAPI (r , target )
131182 }
132183 unstructuredSrc , err := unstructured .ToUnstructured (source )
133184 if err != nil {
134185 return fmt .Errorf ("failed to convert k8s source value to unstructured: %w" , err )
135186 }
187+ if err := r .Translator .Validate (unstructuredSrc ); err != nil {
188+ return fmt .Errorf ("failed to validate unstructured object input: %w" , err )
189+ }
136190 targetUnstructured := map [string ]any {}
137- value , err := unstructured .AccessField [map [string ]any ](unstructuredSrc , "spec" , t .majorVersion )
191+ value , err := unstructured .AccessField [map [string ]any ](unstructuredSrc , "spec" , r . Translator .majorVersion )
138192 if err != nil {
139193 return fmt .Errorf ("failed to access source spec value: %w" , err )
140194 }
141195
142- if err := CollapseMappings (t , value , source , objs ... ); err != nil {
196+ if err := CollapseMappings (r , value , source ); err != nil {
143197 return fmt .Errorf ("failed to process API mappings: %w" , err )
144198 }
145199
0 commit comments