@@ -53,11 +53,18 @@ type reconciler struct {
5353}
5454
5555func (r * reconciler ) reconcile (ctx context.Context , cl client.Client , cache cache.Cache , req * kubebindv1alpha2.APIServiceExportRequest ) error {
56+ // We must ensure schemas are created in form of boundSchemas first for the validation.
57+ // Worst case scenario if validation fails, we will reuse schemas for same consumer once issues are fixed.
5658 if err := r .ensureBoundSchemas (ctx , cl , cache , req ); err != nil {
5759 conditions .SetSummary (req )
5860 return err
5961 }
6062
63+ if err := r .validate (ctx , cl , req ); err != nil {
64+ conditions .SetSummary (req )
65+ return err
66+ }
67+
6168 if err := r .ensureExports (ctx , cl , cache , req ); err != nil {
6269 conditions .SetSummary (req )
6370 return err
@@ -72,97 +79,74 @@ func (r *reconciler) reconcile(ctx context.Context, cl client.Client, cache cach
7279 return nil
7380}
7481
75- func (r * reconciler ) ensureBoundSchemas (ctx context.Context , cl client.Client , cache cache.Cache , req * kubebindv1alpha2.APIServiceExportRequest ) error {
76- // Ensure all bound schemas exist
77- for _ , res := range req .Spec .Resources {
78- parts := strings .SplitN (r .schemaSource , "." , 3 )
79- if len (parts ) != 3 { // We check this in validation, but just in case.
80- return fmt .Errorf ("malformed schema source: %q" , r .schemaSource )
81- }
82+ // exportedSchemas are the schemas exported by the current backend.
83+ // Keys are resource.version.group string for quick resolve.
84+ type exportedSchemas map [string ]* kubebindv1alpha2.BoundSchema
85+
86+ // getExportedSchemas will list all schemas, exported by current backend.
87+ // Important: getExportedSchemas is using client.Client to list resources, not cache.
88+ // This is due to fact we use dynamic client and unstructured.Unstructured to get schemas and it
89+ // does not quite work with dynamic cache informers:
90+ // failed to get informer for *unstructured.UnstructuredList apis.kcp.io/v1alpha1, Kind=APIResourceSchemaList: failed to find newly started informer for apis.kcp.io/v1alpha1, Kind=APIResourceSchema"}.
91+ func (r * reconciler ) getExportedSchemas (ctx context.Context , cl client.Client ) (exportedSchemas , error ) {
92+ parts := strings .SplitN (r .schemaSource , "." , 3 )
93+ if len (parts ) != 3 { // We check this in validation, but just in case.
94+ return nil , fmt .Errorf ("malformed schema source: %q" , r .schemaSource )
95+ }
8296
83- gvk := schema.GroupVersionKind {
84- Kind : parts [0 ],
85- Version : parts [1 ],
86- Group : parts [2 ],
87- }
97+ gvk := schema.GroupVersionKind {
98+ Kind : parts [0 ],
99+ Version : parts [1 ],
100+ Group : parts [2 ],
101+ }
88102
89- // Ensure we have the List kind
90- listGVK := gvk
91- if ! strings .HasSuffix (listGVK .Kind , "List" ) {
92- listGVK .Kind += "List"
93- }
103+ // Ensure we have the List kind
104+ listGVK := gvk
105+ if ! strings .HasSuffix (listGVK .Kind , "List" ) {
106+ listGVK .Kind += "List"
107+ }
94108
95- list := & unstructured.UnstructuredList {}
96- list .SetGroupVersionKind (listGVK )
109+ list := & unstructured.UnstructuredList {}
110+ list .SetGroupVersionKind (listGVK )
97111
98- // TODO(mjudeikis): This is hardcoded here and in handlers.go for now.
99- labelSelector := labels.Set {
100- resources .ExportedCRDsLabel : "true" ,
101- }
112+ // TODO(mjudeikis): This is hardcoded here and in handlers.go for now.
113+ labelSelector := labels.Set {
114+ resources .ExportedCRDsLabel : "true" ,
115+ }
102116
103- listOpts := []client.ListOption {}
104- listOpts = append (listOpts , client.MatchingLabelsSelector {Selector : labelSelector .AsSelector ()})
117+ listOpts := []client.ListOption {}
118+ listOpts = append (listOpts , client.MatchingLabelsSelector {Selector : labelSelector .AsSelector ()})
105119
106- if err := cl .List (ctx , list , listOpts ... ); err != nil {
107- return err
108- }
120+ if err := cl .List (ctx , list , listOpts ... ); err != nil {
121+ return nil , err
122+ }
109123
110- for _ , item := range list .Items {
111- var schemaFailed bool
112- obj := item .UnstructuredContent ()
113- group , ok , err := unstructured .NestedString (obj , "spec" , "group" )
114- if ! ok || err != nil || group == "" {
115- klog .FromContext (ctx ).Error (err , "Skipping invalid schema: missing group" , "ns" , item .GetNamespace (), "name" , item .GetName ())
116- schemaFailed = true
117- }
118- plural , ok , err := unstructured .NestedString (obj , "spec" , "names" , "plural" )
119- if ! ok || err != nil || plural == "" {
120- klog .FromContext (ctx ).Error (err , "Skipping invalid schema: missing names.plural" , "ns" , item .GetNamespace (), "name" , item .GetName ())
121- schemaFailed = true
122- }
124+ var boundSchemas exportedSchemas = make (map [string ]* kubebindv1alpha2.BoundSchema , len (list .Items ))
125+ for _ , item := range list .Items {
126+ boundSchema , err := helpers .UnstructuredToBoundSchema (item )
127+ if err != nil {
128+ return nil , err
129+ }
130+ boundSchemas [boundSchema .ResourceGroupName ()] = boundSchema
131+ }
123132
124- scope , ok , err := unstructured .NestedString (obj , "spec" , "scope" )
125- if ! ok || err != nil || scope == "" {
126- klog .FromContext (ctx ).Error (err , "Skipping invalid schema: missing scope" , "ns" , item .GetNamespace (), "name" , item .GetName ())
127- schemaFailed = true
128- }
133+ return boundSchemas , nil
134+ }
129135
130- if schemaFailed {
131- conditions .MarkFalse (
132- req ,
133- kubebindv1alpha2 .APIServiceExportRequestConditionExportsReady ,
134- "APIServiceExportRequestInvalid" ,
135- conditionsapi .ConditionSeverityError ,
136- "APIServiceExportRequest %s is invalid: resource %s/%s has invalid schema" ,
137- req .Name , group , plural ,
138- )
139- req .Status .Phase = kubebindv1alpha2 .APIServiceExportRequestPhaseFailed
140- return fmt .Errorf ("resource %s/%s is invalid" , group , plural )
141- }
136+ func (r * reconciler ) ensureBoundSchemas (ctx context.Context , cl client.Client , cache cache.Cache , req * kubebindv1alpha2.APIServiceExportRequest ) error {
137+ exportedSchemas , err := r .getExportedSchemas (ctx , cl )
138+ if err != nil {
139+ return err
140+ }
142141
143- if group == res .Group && plural == res .Resource {
144- // Important: This checks if the resource are correctly scoped. If consumer is namespaced, we can't allow this.
145- // We terminate early to prevent triggering other controllers.
146- if r .informerScope .String () != scope && r .informerScope != kubebindv1alpha2 .ClusterScope {
147- conditions .MarkFalse (
148- req ,
149- kubebindv1alpha2 .APIServiceExportRequestConditionExportsReady ,
150- "APIServiceExportRequestInvalid" ,
151- conditionsapi .ConditionSeverityError ,
152- "APIServiceExportRequest %s is invalid: resource %s/%s has scope %q which is incompatible with backend informer scope %q" ,
153- req .Name , group , plural , scope , r .informerScope ,
154- )
155- req .Status .Phase = kubebindv1alpha2 .APIServiceExportRequestPhaseFailed
156- req .Status .TerminalMessage = conditions .GetMessage (req , kubebindv1alpha2 .APIServiceExportRequestConditionExportsReady )
157- // We can't proceed with this request.
158- return fmt .Errorf ("resource %s/%s has scope %q which is incompatible with backend informer scope %q" , group , plural , scope , r .informerScope )
159- }
142+ // Ensure all bound schemas exist
143+ for _ , res := range req .Spec .Resources {
144+ if len (res .Versions ) == 0 {
145+ continue
146+ }
160147
161- // https://github.com/kube-bind/kube-bind/issues/297 to fix.
162- boundSchema , err := helpers .UnstructuredToBoundSchema (item )
163- if err != nil {
164- return err
165- }
148+ for _ , boundSchema := range exportedSchemas {
149+ if boundSchema .Spec .Group == res .Group && boundSchema .Spec .Names .Plural == res .Resource {
166150 boundSchema .Name = res .ResourceGroupName ()
167151 boundSchema .Namespace = req .Namespace
168152 boundSchema .Spec .InformerScope = r .informerScope
@@ -258,6 +242,7 @@ func (r *reconciler) ensureExports(ctx context.Context, cl client.Client, cache
258242 Versions : res .Versions ,
259243 })
260244 }
245+ export .Spec .PermissionClaims = req .Spec .PermissionClaims
261246
262247 logger .V (1 ).Info ("Creating APIServiceExport" , "name" , export .Name , "namespace" , export .Namespace )
263248 if err := r .createServiceExport (ctx , cl , export ); err != nil {
@@ -283,3 +268,85 @@ func (r *reconciler) ensureExports(ctx context.Context, cl client.Client, cache
283268
284269 return nil
285270}
271+
272+ // Validate validates if the APIServiceExportRequest is in a valid state.
273+ // Currently it validates if all requested schemas are of the same scope and
274+ // if claimable apis are allowed and valid.
275+ func (r * reconciler ) validate (ctx context.Context , cl client.Client , req * kubebindv1alpha2.APIServiceExportRequest ) error {
276+ exportedSchemas , err := r .getExportedSchemas (ctx , cl )
277+ if err != nil {
278+ return err
279+ }
280+
281+ if len (exportedSchemas ) == 0 {
282+ conditions .MarkFalse (
283+ req ,
284+ kubebindv1alpha2 .APIServiceExportRequestConditionExportsReady ,
285+ "SchemaNotFound" ,
286+ conditionsapi .ConditionSeverityError ,
287+ "SchemaNotFound not found" ,
288+ )
289+ return fmt .Errorf ("no exported schemas found" )
290+ }
291+
292+ scopes := make ([]apiextensionsv1.ResourceScope , 0 , len (exportedSchemas ))
293+ for _ , res := range req .Spec .Resources {
294+ boundSchema , ok := exportedSchemas [res .ResourceGroupName ()]
295+ if ! ok {
296+ conditions .MarkFalse (
297+ req ,
298+ kubebindv1alpha2 .APIServiceExportRequestConditionExportsReady ,
299+ "SchemaNotFound" ,
300+ conditionsapi .ConditionSeverityError ,
301+ "Schema %s not found" ,
302+ res .ResourceGroupName (),
303+ )
304+ return fmt .Errorf ("schema %s not found" , res .ResourceGroupName ())
305+ }
306+ scopes = append (scopes , boundSchema .Spec .Scope )
307+ }
308+
309+ // Check CRD scopes matches.
310+ if len (scopes ) > 1 {
311+ first := scopes [0 ]
312+ for _ , scope := range scopes [1 :] {
313+ if scope != first {
314+ conditions .MarkFalse (req ,
315+ kubebindv1alpha2 .APIServiceExportRequestConditionExportsReady ,
316+ "DifferentScopes" ,
317+ conditionsapi .ConditionSeverityError ,
318+ "Different scopes found: %v" ,
319+ scopes ,
320+ )
321+ return fmt .Errorf ("different scopes found for claimed resources: %v" , scopes )
322+ }
323+ }
324+ }
325+
326+ // Add validation if claimable apis are valid here
327+ for _ , claim := range req .Spec .PermissionClaims {
328+ if ! isClaimableAPI (claim ) {
329+ conditions .MarkFalse (
330+ req ,
331+ kubebindv1alpha2 .APIServiceExportConditionPermissionClaim ,
332+ "InvalidPermissionClaim" ,
333+ conditionsapi .ConditionSeverityError ,
334+ "Resource %s is not a valid claimable API" ,
335+ claim .GroupResource .String (),
336+ )
337+ return fmt .Errorf ("resource %s is not a valid claimable API" , claim .GroupResource .String ())
338+ }
339+ }
340+
341+ return nil
342+ }
343+
344+ // isClaimableAPI checks if a permission claim is for a claimable API.
345+ func isClaimableAPI (claim kubebindv1alpha2.PermissionClaim ) bool {
346+ for _ , api := range kubebindv1alpha2 .ClaimableAPIs {
347+ if claim .Group == api .GroupVersionResource .Group && claim .Resource == api .Names .Plural {
348+ return true
349+ }
350+ }
351+ return false
352+ }
0 commit comments