Skip to content

Commit 6661524

Browse files
committed
Add named resource selector
1 parent 8b1b3a8 commit 6661524

11 files changed

Lines changed: 327 additions & 88 deletions

File tree

backend/controllers/serviceexportrequest/serviceexportrequest_reconcile.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,9 @@ func (r *reconciler) ensureExports(ctx context.Context, cl client.Client, cache
268268
// Validate validates if the APIServiceExportRequest is in a valid state.
269269
// Currently it validates if all requested schemas are of the same scope and
270270
// if claimable apis are allowed and valid.
271+
//
272+
// TODO: Move this to validatingAdmissionWebhook as this is not really part of reconciliation.
273+
// https://github.com/kube-bind/kube-bind/issues/325
271274
func (r *reconciler) validate(ctx context.Context, cl client.Client, req *kubebindv1alpha2.APIServiceExportRequest) error {
272275
exportedSchemas, err := r.getExportedSchemas(ctx, cl)
273276
if err != nil {
@@ -326,10 +329,32 @@ func (r *reconciler) validate(ctx context.Context, cl client.Client, req *kubebi
326329
"Resource %s is not a valid claimable API",
327330
claim.GroupResource.String(),
328331
)
332+
req.Status.Phase = kubebindv1alpha2.APIServiceExportRequestPhaseFailed
333+
req.Status.TerminalMessage = conditions.GetMessage(req, kubebindv1alpha2.APIServiceExportConditionPermissionClaim)
329334
return fmt.Errorf("resource %s is not a valid claimable API", claim.GroupResource.String())
330335
}
331336
}
332337

338+
// Add validation for duplicate group/resource combinations
339+
seenGroupResources := make(map[string]bool)
340+
for _, claim := range req.Spec.PermissionClaims {
341+
key := claim.Group + "/" + claim.Resource
342+
if seenGroupResources[key] {
343+
conditions.MarkFalse(
344+
req,
345+
kubebindv1alpha2.APIServiceExportConditionPermissionClaim,
346+
"DuplicatePermissionClaim",
347+
conditionsapi.ConditionSeverityError,
348+
"Duplicate permission claim found for group/resource %s",
349+
claim.GroupResource.String(),
350+
)
351+
req.Status.Phase = kubebindv1alpha2.APIServiceExportRequestPhaseFailed
352+
req.Status.TerminalMessage = conditions.GetMessage(req, kubebindv1alpha2.APIServiceExportConditionPermissionClaim)
353+
return fmt.Errorf("duplicate permission claim found for group/resource %s", claim.GroupResource.String())
354+
}
355+
seenGroupResources[key] = true
356+
}
357+
333358
return nil
334359
}
335360

contrib/kcp/README.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ k ws use :root:kube-bind
5151
--cookie-signing-key=bGMHz7SR9XcI9JdDB68VmjQErrjbrAR9JdVqjAOKHzE= \
5252
--cookie-encryption-key=wadqi4u+w0bqnSrVFtM38Pz2ykYVIeeadhzT34XlC1Y= \
5353
--schema-source apiresourceschemas \
54-
--consumer-scope=namespaced
54+
--consumer-scope=cluster
5555
```
5656

5757

@@ -90,7 +90,7 @@ kubectl kcp bind apiexport root:provider:cowboys-stable
9090
```bash
9191
kubectl get logicalcluster
9292
# NAME PHASE URL AGE
93-
# cluster Ready https://192.168.2.166:6443/clusters/xwhgz3kv8l67wx87
93+
# cluster Ready https://192.168.2.166:6443/clusters/2es2zlav713acqlr
9494
```
9595

9696
9. Now we gonna initiate consumer:
@@ -104,15 +104,15 @@ kubectl ws create consumer --enter
104104
10. Bind the thing:
105105

106106
```bash
107-
./bin/kubectl-bind http://127.0.0.1:8080/clusters/xwhgz3kv8l67wx87/exports --dry-run -o yaml > apiserviceexport.yaml
107+
./bin/kubectl-bind http://127.0.0.1:8080/clusters/2es2zlav713acqlr/exports --dry-run -o yaml > apiserviceexport.yaml
108108

109109
# Extract secret for binding process. Note that secret name is not the same as output from command above. Check secret
110110
# name by running `kubectl get secret -n kube-bind`
111-
kubectl get secret kubeconfig-ntgtl -n kube-bind -o jsonpath='{.data.kubeconfig}' | base64 -d > remote.kubeconfig
111+
kubectl get secret kubeconfig-pr2xk -n kube-bind -o jsonpath='{.data.kubeconfig}' | base64 -d > remote.kubeconfig
112112

113-
./bin/kubectl-bind apiservice --remote-kubeconfig remote.kubeconfig -f contrib/kcp/deploy/examples/apiserviceexport-namespaced.yaml --skip-konnector --remote-namespace kube-bind-wg2tb
113+
./bin/kubectl-bind apiservice --remote-kubeconfig remote.kubeconfig -f contrib/kcp/deploy/examples/apiserviceexport-namespaced.yaml --skip-konnector --remote-namespace kube-bind-68mqq
114114

115-
./bin/kubectl-bind apiservice --remote-kubeconfig remote.kubeconfig -f contrib/kcp/deploy/examples/apiserviceexport-cluster.yaml --skip-konnector --remote-namespace kube-bind-wg2tb
115+
./bin/kubectl-bind apiservice --remote-kubeconfig remote.kubeconfig -f contrib/kcp/deploy/examples/apiserviceexport-cluster.yaml --skip-konnector --remote-namespace kube-bind-68mqq
116116

117117

118118
export KUBECONFIG=.kcp/consumer.kubeconfig
@@ -142,3 +142,11 @@ kubectl label cm provider app=wildwest -n kube-bind-lxj5k-default
142142

143143
kubectl create cm consumer -n default
144144
kubectl label cm consumer app=wildwest -n default
145+
146+
kubectl create secret generic provider-secret
147+
kubectl label secret provider-secret app=wildwest
148+
149+
kubectl create namespace bob
150+
kubectl create secret generic wildwest-secrets1 -n bob
151+
kubectl label secret wildwest-secrets1 app=wildwest -n bob
152+
```

contrib/kcp/deploy/examples/apiserviceexport-namespaced.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,13 @@ spec:
1515
labelSelector:
1616
matchLabels:
1717
app: wildwest
18+
- apiGroup: ""
19+
resource: secrets
20+
selector:
21+
labelSelector:
22+
matchLabels:
23+
app: wildwest
24+
namedResource:
25+
- name: wildwest-secrets
26+
namespace: bob
1827
status: {}

contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ spec:
5454
crd: {}
5555
- group: kube-bind.io
5656
name: apiserviceexportrequests
57-
schema: v250926-b973783.apiserviceexportrequests.kube-bind.io
57+
schema: v250926-8b1b3a8.apiserviceexportrequests.kube-bind.io
5858
storage:
5959
crd: {}
6060
- group: kube-bind.io

contrib/kcp/deploy/resources/apiresourceschema-apiserviceexportrequests.kube-bind.io.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ apiVersion: apis.kcp.io/v1alpha1
22
kind: APIResourceSchema
33
metadata:
44
creationTimestamp: null
5-
name: v250926-b973783.apiserviceexportrequests.kube-bind.io
5+
name: v250926-8b1b3a8.apiserviceexportrequests.kube-bind.io
66
spec:
77
conversion:
88
strategy: None

pkg/konnector/controllers/cluster/claimedresources/claimedresources_controller.go

Lines changed: 31 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import (
3939
"github.com/kube-bind/kube-bind/pkg/indexers"
4040
"github.com/kube-bind/kube-bind/pkg/konnector/controllers/cluster/serviceexport/multinsinformer"
4141
"github.com/kube-bind/kube-bind/pkg/konnector/controllers/dynamic"
42+
"github.com/kube-bind/kube-bind/pkg/resources"
4243
kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2"
4344
bindclientset "github.com/kube-bind/kube-bind/sdk/client/clientset/versioned"
4445
bindlisters "github.com/kube-bind/kube-bind/sdk/client/listers/kubebind/v1alpha2"
@@ -105,6 +106,8 @@ func NewController(
105106

106107
providerNamespace: providerNamespace,
107108

109+
namespaceCreationNotifyChan: namespaceCreationNotifyChan,
110+
108111
readReconciler: readReconciler{
109112
getServiceNamespace: func(upstreamNamespace string) (*kubebindv1alpha2.APIServiceNamespace, error) {
110113
sns, err := serviceNamespaceInformer.Informer().GetIndexer().ByIndex(indexers.ServiceNamespaceByNamespace, upstreamNamespace)
@@ -207,43 +210,7 @@ type controller struct {
207210
}
208211

209212
func (c *controller) isClaimed(obj *unstructured.Unstructured) bool {
210-
// Empty selector selects everything
211-
if c.claim.Selector.LabelSelector == nil && len(c.claim.Selector.NamedResource) == 0 {
212-
return true
213-
}
214-
215-
// Both label selector and named resources must match if both are specified
216-
labelSelectorMatches := true
217-
namedResourceMatches := true
218-
219-
// Check label selector if specified
220-
if c.claim.Selector.LabelSelector != nil {
221-
selector, err := metav1.LabelSelectorAsSelector(c.claim.Selector.LabelSelector)
222-
if err != nil {
223-
return false
224-
}
225-
l := obj.GetLabels()
226-
if l == nil {
227-
l = make(map[string]string)
228-
}
229-
labelSelectorMatches = selector.Matches(labels.Set(l))
230-
}
231-
232-
// Check named resources if specified
233-
if len(c.claim.Selector.NamedResource) > 0 {
234-
namedResourceMatches = false // Default to false, must match at least one
235-
for _, nr := range c.claim.Selector.NamedResource {
236-
if nr.Namespace != "" && nr.Namespace != obj.GetNamespace() {
237-
continue
238-
}
239-
if nr.Name == obj.GetName() {
240-
namedResourceMatches = true
241-
break
242-
}
243-
}
244-
}
245-
246-
return labelSelectorMatches && namedResourceMatches
213+
return resources.IsClaimed(c.claim.Selector, obj)
247214
}
248215

249216
func (c *controller) enqueueConsumer(logger klog.Logger, obj interface{}) {
@@ -306,6 +273,19 @@ func (c *controller) enqueueConsumerByKey(logger klog.Logger, key string) {
306273
}
307274

308275
func (c *controller) enqueueProvider(logger klog.Logger, obj interface{}) {
276+
// handle tombstones
277+
if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok {
278+
obj = tombstone.Obj
279+
}
280+
o, ok := obj.(*unstructured.Unstructured)
281+
if !ok {
282+
runtime.HandleError(fmt.Errorf("unexpected type %T in enqueueProvider", obj))
283+
return
284+
}
285+
if !c.isClaimed(o) {
286+
return
287+
}
288+
309289
providerKey, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
310290
if err != nil {
311291
runtime.HandleError(err)
@@ -372,6 +352,16 @@ func (c *controller) enqueueServiceNamespace(logger klog.Logger, obj interface{}
372352
return
373353
}
374354
for _, obj := range objs {
355+
unstructuredObj, ok := obj.(*unstructured.Unstructured)
356+
if !ok {
357+
continue
358+
}
359+
360+
// Check if the provider object is actually claimed using the full selector logic.
361+
if !c.isClaimed(unstructuredObj) {
362+
continue
363+
}
364+
375365
key, err := cache.MetaNamespaceKeyFunc(obj)
376366
if err != nil {
377367
runtime.HandleError(err)
@@ -402,7 +392,10 @@ func (c *controller) enqueueServiceNamespace(logger klog.Logger, obj interface{}
402392
return
403393
}
404394
for _, obj := range objects {
405-
logger.Info("enqueueing consumer object", "obj", obj)
395+
// Check if the object is actually claimed using the full selector logic
396+
if !c.isClaimed(obj) {
397+
continue
398+
}
406399

407400
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
408401
if err != nil {

pkg/konnector/controllers/cluster/claimedresourcesnamespaces/claimedresourcesnamespaces_controller.go

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import (
2424
"k8s.io/apimachinery/pkg/api/errors"
2525
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2626
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
27-
"k8s.io/apimachinery/pkg/labels"
2827
"k8s.io/apimachinery/pkg/runtime/schema"
2928
"k8s.io/apimachinery/pkg/util/runtime"
3029
"k8s.io/apimachinery/pkg/util/wait"
@@ -35,6 +34,7 @@ import (
3534
"k8s.io/klog/v2"
3635

3736
"github.com/kube-bind/kube-bind/pkg/konnector/controllers/dynamic"
37+
"github.com/kube-bind/kube-bind/pkg/resources"
3838
kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2"
3939
bindclientset "github.com/kube-bind/kube-bind/sdk/client/clientset/versioned"
4040
bindlisters "github.com/kube-bind/kube-bind/sdk/client/listers/kubebind/v1alpha2"
@@ -115,21 +115,7 @@ type controller struct {
115115
}
116116

117117
func (c *controller) isClaimed(obj *unstructured.Unstructured) bool {
118-
// Check if obj is selected by label selector
119-
if c.claim.Selector.LabelSelector != nil {
120-
selector, err := metav1.LabelSelectorAsSelector(c.claim.Selector.LabelSelector)
121-
if err != nil {
122-
return false
123-
}
124-
l := obj.GetLabels()
125-
if l == nil {
126-
l = make(map[string]string)
127-
}
128-
129-
return selector.Matches(labels.Set(l))
130-
}
131-
132-
return false
118+
return resources.IsClaimed(c.claim.Selector, obj)
133119
}
134120

135121
func (c *controller) enqueueConsumer(logger klog.Logger, obj interface{}) {
@@ -278,6 +264,25 @@ func (c *controller) ensureServiceNamespace(ctx context.Context, ns string) (cre
278264
return false, fmt.Errorf("failed to create APIServiceNamespace %q: %w", ns, err)
279265
}
280266

267+
// Wait until the servicenamespace status has been updated.
268+
// As this controller does not do anything else, we are ok to block here.
269+
err = wait.PollUntilContextCancel(ctx, 500*time.Millisecond, true, func(ctx context.Context) (done bool, err error) {
270+
sns, err := c.serviceNamespaceInformer.Lister().APIServiceNamespaces(c.providerNamespace).Get(ns)
271+
if err != nil {
272+
if errors.IsNotFound(err) {
273+
return false, nil // keep polling
274+
}
275+
return false, err
276+
}
277+
if sns.Status.Namespace != "" {
278+
return true, nil
279+
}
280+
return false, nil
281+
})
282+
if err != nil {
283+
return false, fmt.Errorf("failed to wait for APIServiceNamespace %q to be ready: %w", ns, err)
284+
}
285+
281286
logger.Info("APIServiceNamespace created successfully")
282287
return true, nil
283288
}

pkg/resources/resources.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package resources
2+
3+
import (
4+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
5+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
6+
"k8s.io/apimachinery/pkg/labels"
7+
8+
kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2"
9+
)
10+
11+
// IsClaimed returns true if the given object matches the given selector and named resources.
12+
func IsClaimed(selector kubebindv1alpha2.Selector, obj *unstructured.Unstructured) bool {
13+
// Empty selector selects everything
14+
if selector.LabelSelector == nil && len(selector.NamedResource) == 0 {
15+
return true
16+
}
17+
18+
// Both label selector and named resources must match if both are specified
19+
labelSelectorMatches := true
20+
namedResourceMatches := true
21+
22+
// Check label selector if specified
23+
if selector.LabelSelector != nil {
24+
selector, err := metav1.LabelSelectorAsSelector(selector.LabelSelector)
25+
if err != nil {
26+
return false
27+
}
28+
l := obj.GetLabels()
29+
if l == nil {
30+
l = make(map[string]string)
31+
}
32+
labelSelectorMatches = selector.Matches(labels.Set(l))
33+
}
34+
35+
// Check named resources if specified
36+
if len(selector.NamedResource) > 0 {
37+
namedResourceMatches = false // Default to false, must match at least one
38+
for _, nr := range selector.NamedResource {
39+
if nr.Namespace != "" && nr.Namespace != obj.GetNamespace() {
40+
continue
41+
}
42+
if nr.Name == obj.GetName() {
43+
namedResourceMatches = true
44+
break
45+
}
46+
}
47+
}
48+
49+
return labelSelectorMatches && namedResourceMatches
50+
}

0 commit comments

Comments
 (0)