@@ -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,70 @@ 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+ // getExportedSchemas will list all schemas, exported by current backend.
83+ // Important: getExportedSchemas is using client.Client to list resources, not cache.
84+ // This is due to fact we use dynamic client and unstructured.Unstructured to get schemas and it
85+ // does not quite work with dynamic cache informers:
86+ // 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"}.
87+ func (r * reconciler ) getExportedSchemas (ctx context.Context , cl client.Client ) (kubebindv1alpha2.ExportedSchemas , error ) {
88+ parts := strings .SplitN (r .schemaSource , "." , 3 )
89+ if len (parts ) != 3 { // We check this in validation, but just in case.
90+ return nil , fmt .Errorf ("malformed schema source: %q" , r .schemaSource )
91+ }
8292
83- gvk := schema.GroupVersionKind {
84- Kind : parts [0 ],
85- Version : parts [1 ],
86- Group : parts [2 ],
87- }
93+ gvk := schema.GroupVersionKind {
94+ Kind : parts [0 ],
95+ Version : parts [1 ],
96+ Group : parts [2 ],
97+ }
8898
89- // Ensure we have the List kind
90- listGVK := gvk
91- if ! strings .HasSuffix (listGVK .Kind , "List" ) {
92- listGVK .Kind += "List"
93- }
99+ // Ensure we have the List kind
100+ listGVK := gvk
101+ if ! strings .HasSuffix (listGVK .Kind , "List" ) {
102+ listGVK .Kind += "List"
103+ }
94104
95- list := & unstructured.UnstructuredList {}
96- list .SetGroupVersionKind (listGVK )
105+ list := & unstructured.UnstructuredList {}
106+ list .SetGroupVersionKind (listGVK )
97107
98- // TODO(mjudeikis): This is hardcoded here and in handlers.go for now.
99- labelSelector := labels.Set {
100- resources .ExportedCRDsLabel : "true" ,
101- }
108+ // TODO(mjudeikis): This is hardcoded here and in handlers.go for now.
109+ labelSelector := labels.Set {
110+ resources .ExportedCRDsLabel : "true" ,
111+ }
102112
103- listOpts := []client.ListOption {}
104- listOpts = append (listOpts , client.MatchingLabelsSelector {Selector : labelSelector .AsSelector ()})
113+ listOpts := []client.ListOption {}
114+ listOpts = append (listOpts , client.MatchingLabelsSelector {Selector : labelSelector .AsSelector ()})
105115
106- if err := cl .List (ctx , list , listOpts ... ); err != nil {
107- return err
108- }
116+ if err := cl .List (ctx , list , listOpts ... ); err != nil {
117+ return nil , err
118+ }
109119
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- }
120+ var boundSchemas kubebindv1alpha2.ExportedSchemas = make (map [string ]* kubebindv1alpha2.BoundSchema , len (list .Items ))
121+ for _ , item := range list .Items {
122+ boundSchema , err := helpers .UnstructuredToBoundSchema (item )
123+ if err != nil {
124+ return nil , err
125+ }
126+ boundSchemas [boundSchema .ResourceGroupName ()] = boundSchema
127+ }
123128
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- }
129+ return boundSchemas , nil
130+ }
129131
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- }
132+ func (r * reconciler ) ensureBoundSchemas (ctx context.Context , cl client.Client , cache cache.Cache , req * kubebindv1alpha2.APIServiceExportRequest ) error {
133+ exportedSchemas , err := r .getExportedSchemas (ctx , cl )
134+ if err != nil {
135+ return err
136+ }
142137
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- }
138+ // Ensure all bound schemas exist
139+ for _ , res := range req .Spec .Resources {
140+ if len (res .Versions ) == 0 {
141+ continue
142+ }
160143
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- }
144+ for _ , boundSchema := range exportedSchemas {
145+ if boundSchema .Spec .Group == res .Group && boundSchema .Spec .Names .Plural == res .Resource {
166146 boundSchema .Name = res .ResourceGroupName ()
167147 boundSchema .Namespace = req .Namespace
168148 boundSchema .Spec .InformerScope = r .informerScope
@@ -258,6 +238,7 @@ func (r *reconciler) ensureExports(ctx context.Context, cl client.Client, cache
258238 Versions : res .Versions ,
259239 })
260240 }
241+ export .Spec .PermissionClaims = req .Spec .PermissionClaims
261242
262243 logger .V (1 ).Info ("Creating APIServiceExport" , "name" , export .Name , "namespace" , export .Namespace )
263244 if err := r .createServiceExport (ctx , cl , export ); err != nil {
@@ -283,3 +264,85 @@ func (r *reconciler) ensureExports(ctx context.Context, cl client.Client, cache
283264
284265 return nil
285266}
267+
268+ // Validate validates if the APIServiceExportRequest is in a valid state.
269+ // Currently it validates if all requested schemas are of the same scope and
270+ // if claimable apis are allowed and valid.
271+ func (r * reconciler ) validate (ctx context.Context , cl client.Client , req * kubebindv1alpha2.APIServiceExportRequest ) error {
272+ exportedSchemas , err := r .getExportedSchemas (ctx , cl )
273+ if err != nil {
274+ return err
275+ }
276+
277+ if len (exportedSchemas ) == 0 {
278+ conditions .MarkFalse (
279+ req ,
280+ kubebindv1alpha2 .APIServiceExportRequestConditionExportsReady ,
281+ "SchemaNotFound" ,
282+ conditionsapi .ConditionSeverityError ,
283+ "SchemaNotFound not found" ,
284+ )
285+ return fmt .Errorf ("no exported schemas found" )
286+ }
287+
288+ scopes := make ([]apiextensionsv1.ResourceScope , 0 , len (exportedSchemas ))
289+ for _ , res := range req .Spec .Resources {
290+ boundSchema , ok := exportedSchemas [res .ResourceGroupName ()]
291+ if ! ok {
292+ conditions .MarkFalse (
293+ req ,
294+ kubebindv1alpha2 .APIServiceExportRequestConditionExportsReady ,
295+ "SchemaNotFound" ,
296+ conditionsapi .ConditionSeverityError ,
297+ "Schema %s not found" ,
298+ res .ResourceGroupName (),
299+ )
300+ return fmt .Errorf ("schema %s not found" , res .ResourceGroupName ())
301+ }
302+ scopes = append (scopes , boundSchema .Spec .Scope )
303+ }
304+
305+ // Check CRD scopes matches.
306+ if len (scopes ) > 1 {
307+ first := scopes [0 ]
308+ for _ , scope := range scopes [1 :] {
309+ if scope != first {
310+ conditions .MarkFalse (req ,
311+ kubebindv1alpha2 .APIServiceExportRequestConditionExportsReady ,
312+ "DifferentScopes" ,
313+ conditionsapi .ConditionSeverityError ,
314+ "Different scopes found: %v" ,
315+ scopes ,
316+ )
317+ return fmt .Errorf ("different scopes found for claimed resources: %v" , scopes )
318+ }
319+ }
320+ }
321+
322+ // Add validation if claimable apis are valid here
323+ for _ , claim := range req .Spec .PermissionClaims {
324+ if ! isClaimableAPI (claim ) {
325+ conditions .MarkFalse (
326+ req ,
327+ kubebindv1alpha2 .APIServiceExportConditionPermissionClaim ,
328+ "InvalidPermissionClaim" ,
329+ conditionsapi .ConditionSeverityError ,
330+ "Resource %s is not a valid claimable API" ,
331+ claim .GroupResource .String (),
332+ )
333+ return fmt .Errorf ("resource %s is not a valid claimable API" , claim .GroupResource .String ())
334+ }
335+ }
336+
337+ return nil
338+ }
339+
340+ // isClaimableAPI checks if a permission claim is for a claimable API.
341+ func isClaimableAPI (claim kubebindv1alpha2.PermissionClaim ) bool {
342+ for _ , api := range kubebindv1alpha2 .ClaimableAPIs {
343+ if claim .Group == api .GroupVersionResource .Group && claim .Resource == api .Names .Plural {
344+ return true
345+ }
346+ }
347+ return false
348+ }
0 commit comments