Skip to content

Commit b0f25c4

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

30 files changed

Lines changed: 2152 additions & 322 deletions

apiserviceexport.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
apiVersion: kube-bind.io/v1alpha2
2+
kind: APIServiceExportRequest
3+
metadata:
4+
name: sheriffs.wildwest.dev
5+
spec:
6+
resources:
7+
- group: wildwest.dev
8+
resource: sheriffs
9+
versions:
10+
- v1alpha1
11+
status: {}

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: 147 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,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+
}

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)