Skip to content

Commit 59d56ce

Browse files
committed
Add permissionClaims machinery
Signed-off-by: Mangirdas Judeikis <mangirdas@judeikis.lt> On-behalf-of: @SAP mangirdas.judeikis@sap.com
1 parent c2b253b commit 59d56ce

30 files changed

Lines changed: 2165 additions & 373 deletions

backend/controllers/clusterbinding/clusterbinding_reconcile.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ func (r *reconciler) ensureRBACClusterRole(ctx context.Context, client client.Cl
158158
Resources: []string{"boundschemas"},
159159
Verbs: []string{"get", "list", "watch"},
160160
},
161+
{
162+
APIGroups: []string{kubebindv1alpha2.GroupName},
163+
Resources: []string{"boundschemas/status"},
164+
Verbs: []string{"get", "update", "patch"},
165+
},
161166
}}
162167
for _, export := range exports {
163168
// Collect unique GroupResources and sort for stable rule ordering.

backend/controllers/serviceexportrequest/serviceexportrequest_reconcile.go

Lines changed: 143 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,18 @@ type reconciler struct {
5353
}
5454

5555
func (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+
}

backend/controllers/servicenamespace/servicenamespace_controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ func (r *APIServiceNamespaceReconciler) Reconcile(ctx context.Context, req mcrec
230230

231231
// Fetch the APIServiceNamespace instance
232232
apiServiceNamespace := &kubebindv1alpha2.APIServiceNamespace{}
233-
if err := client.Get(ctx, req.NamespacedName, apiServiceNamespace); err != nil {
233+
if err := cache.Get(ctx, req.NamespacedName, apiServiceNamespace); err != nil {
234234
if errors.IsNotFound(err) {
235235
// Request object not found, could have been deleted after reconcile request.
236236
// Handle deletion logic here

0 commit comments

Comments
 (0)