@@ -20,19 +20,25 @@ import (
20
20
"context"
21
21
"fmt"
22
22
"io"
23
+ "strings"
24
+ "time"
23
25
24
26
"github.com/kcp-dev/logicalcluster/v3"
25
27
26
28
"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"
28
32
"k8s.io/apiserver/pkg/admission"
29
33
"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"
31
36
32
37
"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"
34
40
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"
36
42
)
37
43
38
44
const (
@@ -44,14 +50,18 @@ func Register(plugins *admission.Plugins) {
44
50
func (_ io.Reader ) (admission.Interface , error ) {
45
51
return & crdNoOverlappingGVRAdmission {
46
52
Handler : admission .NewHandler (admission .Create ),
53
+ now : metav1 .Now ,
47
54
}, nil
48
55
})
49
56
}
50
57
51
58
type crdNoOverlappingGVRAdmission struct {
52
59
* admission.Handler
53
60
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
55
65
}
56
66
57
67
// Ensure that the required admission interfaces are implemented.
@@ -60,12 +70,21 @@ var _ = admission.InitializationValidator(&crdNoOverlappingGVRAdmission{})
60
70
61
71
func (p * crdNoOverlappingGVRAdmission ) SetKcpInformers (local , global kcpinformers.SharedInformerFactory ) {
62
72
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
+ }
64
80
}
65
81
66
82
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" )
69
88
}
70
89
return nil
71
90
}
@@ -78,35 +97,60 @@ func (p *crdNoOverlappingGVRAdmission) Validate(ctx context.Context, a admission
78
97
if a .GetKind ().GroupKind () != apiextensions .Kind ("CustomResourceDefinition" ) {
79
98
return nil
80
99
}
81
- cluster , err := request .ClusterNameFrom (ctx )
100
+ if a .GetOperation () != admission .Create {
101
+ return nil
102
+ }
103
+
104
+ clusterName , err := request .ClusterNameFrom (ctx )
82
105
if err != nil {
83
106
return fmt .Errorf ("failed to retrieve cluster from context: %w" , err )
84
107
}
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.
88
111
return nil
89
112
}
90
113
91
114
crd , ok := a .GetObject ().(* apiextensions.CustomResourceDefinition )
92
115
if ! ok {
93
116
return fmt .Errorf ("unexpected type %T" , a .GetObject ())
94
117
}
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
+ }
97
146
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 )
98
150
}
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 ))
106
153
}
107
- return nil
108
- }
109
154
110
- func (p * crdNoOverlappingGVRAdmission ) listAPIBindingsFor (clusterName logicalcluster.Name ) ([]* apisv1alpha1.APIBinding , error ) {
111
- return p .apiBindingClusterLister .Cluster (clusterName ).List (labels .Everything ())
155
+ return nil
112
156
}
0 commit comments