Skip to content

Commit b3408c3

Browse files
committed
apis/apibindings: store served group resources consistently on LogicalCluster
Signed-off-by: Dr. Stefan Schimanski <[email protected]>
1 parent 3e06890 commit b3408c3

14 files changed

+1595
-91
lines changed

config/shard/logicalcluster.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
apiVersion: core.kcp.io/v1alpha1
2+
kind: LogicalCluster
3+
metadata:
4+
name: cluster
5+
spec: {}

pkg/admission/crdnooverlappinggvr/crdnooverlappinggvr_admission.go

Lines changed: 69 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,25 @@ import (
2020
"context"
2121
"fmt"
2222
"io"
23+
"strings"
24+
"time"
2325

2426
"github.com/kcp-dev/logicalcluster/v3"
2527

2628
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
27-
"k8s.io/apimachinery/pkg/labels"
29+
"k8s.io/apimachinery/pkg/api/errors"
30+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31+
"k8s.io/apimachinery/pkg/runtime/schema"
2832
"k8s.io/apiserver/pkg/admission"
2933
"k8s.io/apiserver/pkg/endpoints/request"
30-
"k8s.io/kubernetes/pkg/controlplane/apiserver"
34+
"k8s.io/client-go/util/retry"
35+
"k8s.io/utils/ptr"
3136

3237
"github.com/kcp-dev/kcp/pkg/reconciler/apis/apibinding"
33-
apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1"
38+
corev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1"
39+
kcpclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster"
3440
kcpinformers "github.com/kcp-dev/kcp/sdk/client/informers/externalversions"
35-
apisv1alpha1listers "github.com/kcp-dev/kcp/sdk/client/listers/apis/v1alpha1"
41+
corev1alpha1listers "github.com/kcp-dev/kcp/sdk/client/listers/core/v1alpha1"
3642
)
3743

3844
const (
@@ -44,14 +50,18 @@ func Register(plugins *admission.Plugins) {
4450
func(_ io.Reader) (admission.Interface, error) {
4551
return &crdNoOverlappingGVRAdmission{
4652
Handler: admission.NewHandler(admission.Create),
53+
now: metav1.Now,
4754
}, nil
4855
})
4956
}
5057

5158
type crdNoOverlappingGVRAdmission struct {
5259
*admission.Handler
5360

54-
apiBindingClusterLister apisv1alpha1listers.APIBindingClusterLister
61+
updateLogicalCluster func(ctx context.Context, logicalCluster *corev1alpha1.LogicalCluster, opts metav1.UpdateOptions) (*corev1alpha1.LogicalCluster, error)
62+
logicalclusterLister corev1alpha1listers.LogicalClusterClusterLister
63+
64+
now func() metav1.Time
5565
}
5666

5767
// Ensure that the required admission interfaces are implemented.
@@ -60,12 +70,21 @@ var _ = admission.InitializationValidator(&crdNoOverlappingGVRAdmission{})
6070

6171
func (p *crdNoOverlappingGVRAdmission) SetKcpInformers(local, global kcpinformers.SharedInformerFactory) {
6272
p.SetReadyFunc(local.Apis().V1alpha1().APIBindings().Informer().HasSynced)
63-
p.apiBindingClusterLister = local.Apis().V1alpha1().APIBindings().Lister()
73+
p.logicalclusterLister = local.Core().V1alpha1().LogicalClusters().Lister()
74+
}
75+
76+
func (p *crdNoOverlappingGVRAdmission) SetKcpClusterClient(c kcpclientset.ClusterInterface) {
77+
p.updateLogicalCluster = func(ctx context.Context, logicalCluster *corev1alpha1.LogicalCluster, opts metav1.UpdateOptions) (*corev1alpha1.LogicalCluster, error) {
78+
return c.CoreV1alpha1().LogicalClusters().Cluster(logicalcluster.From(logicalCluster).Path()).Update(ctx, logicalCluster, opts)
79+
}
6480
}
6581

6682
func (p *crdNoOverlappingGVRAdmission) ValidateInitialization() error {
67-
if p.apiBindingClusterLister == nil {
68-
return fmt.Errorf(PluginName + " plugin needs an APIBindings lister")
83+
if p.logicalclusterLister == nil {
84+
return fmt.Errorf(PluginName + " plugin needs an LogicalCluster lister")
85+
}
86+
if p.updateLogicalCluster == nil {
87+
return fmt.Errorf(PluginName + " plugin needs a KCP cluster client")
6988
}
7089
return nil
7190
}
@@ -78,35 +97,60 @@ func (p *crdNoOverlappingGVRAdmission) Validate(ctx context.Context, a admission
7897
if a.GetKind().GroupKind() != apiextensions.Kind("CustomResourceDefinition") {
7998
return nil
8099
}
81-
cluster, err := request.ClusterNameFrom(ctx)
100+
if a.GetOperation() != admission.Create {
101+
return nil
102+
}
103+
104+
clusterName, err := request.ClusterNameFrom(ctx)
82105
if err != nil {
83106
return fmt.Errorf("failed to retrieve cluster from context: %w", err)
84107
}
85-
clusterName := logicalcluster.Name(cluster.String()) // TODO(sttts): remove this cast once ClusterNameFrom returns a tenancy.Name
86-
// ignore CRDs targeting system and non-root workspaces
87-
if clusterName == apibinding.SystemBoundCRDsClusterName || clusterName == apiserver.LocalAdminCluster {
108+
109+
if clusterName == apibinding.SystemBoundCRDsClusterName {
110+
// bound CRDs will have equal group and resource names.
88111
return nil
89112
}
90113

91114
crd, ok := a.GetObject().(*apiextensions.CustomResourceDefinition)
92115
if !ok {
93116
return fmt.Errorf("unexpected type %T", a.GetObject())
94117
}
95-
apiBindingsForCurrentClusterName, err := p.listAPIBindingsFor(clusterName)
96-
if err != nil {
118+
119+
// (optimistically) lock group resource for LogicalCluster. If this request
120+
// eventually fails, the logicalclustercleanup controller will clean them
121+
// up eventually.
122+
gr := schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Spec.Names.Plural}
123+
var skipped map[schema.GroupResource]apibinding.Lock
124+
err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
125+
lc, err := p.logicalclusterLister.Cluster(clusterName).Get(corev1alpha1.LogicalClusterName)
126+
if errors.IsNotFound(err) && strings.HasPrefix(string(clusterName), "system:") {
127+
// in system logical clusters this is not fatal. We usually don't have a LogicalCluster there.
128+
return nil
129+
} else if err != nil {
130+
return fmt.Errorf("failed to get LogicalCluster in logical cluster %q: %w", clusterName, err)
131+
}
132+
133+
var updated *corev1alpha1.LogicalCluster
134+
updated, _, skipped, err = apibinding.WithLockedResources(nil, time.Now(), lc, []schema.GroupResource{gr}, apibinding.ExpirableLock{
135+
Lock: apibinding.Lock{CRD: true},
136+
CRDExpiry: ptr.To(p.now()),
137+
})
138+
if err != nil {
139+
return fmt.Errorf("failed to lock resources %s in logical cluster %q: %w", gr, logicalcluster.From(crd), err)
140+
}
141+
142+
_, err = p.updateLogicalCluster(ctx, updated, metav1.UpdateOptions{})
143+
if err != nil {
144+
return err
145+
}
97146
return err
147+
})
148+
if err != nil {
149+
return fmt.Errorf("failed to lock resources %s in logical cluster %q: %w", gr, logicalcluster.From(crd), err)
98150
}
99-
for _, apiBindingForCurrentClusterName := range apiBindingsForCurrentClusterName {
100-
for _, boundResource := range apiBindingForCurrentClusterName.Status.BoundResources {
101-
if boundResource.Group == crd.Spec.Group && boundResource.Resource == crd.Spec.Names.Plural {
102-
return admission.NewForbidden(a, fmt.Errorf("cannot create %q CustomResourceDefinition with %q group and %q resource because it overlaps with a bound CustomResourceDefinition for %q APIBinding in %q logical cluster",
103-
crd.Name, crd.Spec.Group, crd.Spec.Names.Plural, apiBindingForCurrentClusterName.Name, clusterName))
104-
}
105-
}
151+
if len(skipped) > 0 {
152+
return admission.NewForbidden(a, fmt.Errorf("cannot create because resource is bound by APIBinding %q", skipped[gr].Name))
106153
}
107-
return nil
108-
}
109154

110-
func (p *crdNoOverlappingGVRAdmission) listAPIBindingsFor(clusterName logicalcluster.Name) ([]*apisv1alpha1.APIBinding, error) {
111-
return p.apiBindingClusterLister.Cluster(clusterName).List(labels.Everything())
155+
return nil
112156
}

0 commit comments

Comments
 (0)