|  | 
|  | 1 | +/* | 
|  | 2 | +Copyright 2025 The KCP Authors. | 
|  | 3 | +
 | 
|  | 4 | +Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | 5 | +you may not use this file except in compliance with the License. | 
|  | 6 | +You may obtain a copy of the License at | 
|  | 7 | +
 | 
|  | 8 | +    http://www.apache.org/licenses/LICENSE-2.0 | 
|  | 9 | +
 | 
|  | 10 | +Unless required by applicable law or agreed to in writing, software | 
|  | 11 | +distributed under the License is distributed on an "AS IS" BASIS, | 
|  | 12 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | 13 | +See the License for the specific language governing permissions and | 
|  | 14 | +limitations under the License. | 
|  | 15 | +*/ | 
|  | 16 | + | 
|  | 17 | +package dynamicrestmapper | 
|  | 18 | + | 
|  | 19 | +import ( | 
|  | 20 | +	"context" | 
|  | 21 | +	"fmt" | 
|  | 22 | +	"time" | 
|  | 23 | + | 
|  | 24 | +	"github.com/go-logr/logr" | 
|  | 25 | + | 
|  | 26 | +	apiextensionshelpers "k8s.io/apiextensions-apiserver/pkg/apihelpers" | 
|  | 27 | +	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" | 
|  | 28 | +	apierrors "k8s.io/apimachinery/pkg/api/errors" | 
|  | 29 | +	"k8s.io/apimachinery/pkg/api/meta" | 
|  | 30 | +	"k8s.io/apimachinery/pkg/runtime/schema" | 
|  | 31 | +	utilruntime "k8s.io/apimachinery/pkg/util/runtime" | 
|  | 32 | +	"k8s.io/apimachinery/pkg/util/wait" | 
|  | 33 | +	"k8s.io/client-go/tools/cache" | 
|  | 34 | +	"k8s.io/client-go/util/workqueue" | 
|  | 35 | +	"k8s.io/klog/v2" | 
|  | 36 | + | 
|  | 37 | +	kcpcache "github.com/kcp-dev/apimachinery/v2/pkg/cache" | 
|  | 38 | +	kcpapiextensionsv1informers "github.com/kcp-dev/client-go/apiextensions/informers/apiextensions/v1" | 
|  | 39 | +	"github.com/kcp-dev/logicalcluster/v3" | 
|  | 40 | + | 
|  | 41 | +	"github.com/kcp-dev/kcp/pkg/logging" | 
|  | 42 | +	"github.com/kcp-dev/kcp/pkg/tombstone" | 
|  | 43 | +	builtinschemas "github.com/kcp-dev/kcp/pkg/virtual/apiexport/schemas/builtin" | 
|  | 44 | +) | 
|  | 45 | + | 
|  | 46 | +const ( | 
|  | 47 | +	BuiltinTypesControllerName = "kcp-dynamicrestmapper-builtin" | 
|  | 48 | +) | 
|  | 49 | + | 
|  | 50 | +var systemCRDClusterName = logicalcluster.Name("system:system-crds") | 
|  | 51 | + | 
|  | 52 | +type BuiltinTypesController struct { | 
|  | 53 | +	queue workqueue.TypedRateLimitingInterface[string] | 
|  | 54 | + | 
|  | 55 | +	state         *DynamicRESTMapper | 
|  | 56 | +	groupVersions map[string]string | 
|  | 57 | + | 
|  | 58 | +	getCRD func(clusterName logicalcluster.Name, name string) (*apiextensionsv1.CustomResourceDefinition, error) | 
|  | 59 | +} | 
|  | 60 | + | 
|  | 61 | +func NewBuiltinTypesController( | 
|  | 62 | +	ctx context.Context, | 
|  | 63 | +	state *DynamicRESTMapper, | 
|  | 64 | +	crdInformer kcpapiextensionsv1informers.CustomResourceDefinitionClusterInformer, | 
|  | 65 | +) (*BuiltinTypesController, error) { | 
|  | 66 | +	c := &BuiltinTypesController{ | 
|  | 67 | +		state: state, | 
|  | 68 | +		queue: workqueue.NewTypedRateLimitingQueueWithConfig( | 
|  | 69 | +			workqueue.DefaultTypedControllerRateLimiter[string](), | 
|  | 70 | +			workqueue.TypedRateLimitingQueueConfig[string]{ | 
|  | 71 | +				Name: BuiltinTypesControllerName, | 
|  | 72 | +			}, | 
|  | 73 | +		), | 
|  | 74 | +		groupVersions: make(map[string]string), | 
|  | 75 | +		getCRD: func(clusterName logicalcluster.Name, name string) (*apiextensionsv1.CustomResourceDefinition, error) { | 
|  | 76 | +			return crdInformer.Lister().Cluster(clusterName).Get(name) | 
|  | 77 | +		}, | 
|  | 78 | +	} | 
|  | 79 | + | 
|  | 80 | +	// Populate the builtin RESTMapper with core types. | 
|  | 81 | +	// We assume those never change, so we add mapping for them only during initialization. | 
|  | 82 | + | 
|  | 83 | +	for i := range builtinschemas.BuiltInAPIs { | 
|  | 84 | +		group := builtinschemas.BuiltInAPIs[i].GroupVersion.Group | 
|  | 85 | +		version := builtinschemas.BuiltInAPIs[i].GroupVersion.Version | 
|  | 86 | +		if version > c.groupVersions[group] { | 
|  | 87 | +			c.groupVersions[group] = version | 
|  | 88 | +		} | 
|  | 89 | +		c.state.builtin.add(newTypeMeta( | 
|  | 90 | +			builtinschemas.BuiltInAPIs[i].GroupVersion.Group, | 
|  | 91 | +			builtinschemas.BuiltInAPIs[i].GroupVersion.Version, | 
|  | 92 | +			builtinschemas.BuiltInAPIs[i].Names.Kind, | 
|  | 93 | +			builtinschemas.BuiltInAPIs[i].Names.Singular, | 
|  | 94 | +			builtinschemas.BuiltInAPIs[i].Names.Plural, | 
|  | 95 | +			resourceScopeToRESTScope(builtinschemas.BuiltInAPIs[i].ResourceScope)), | 
|  | 96 | +		) | 
|  | 97 | +	} | 
|  | 98 | + | 
|  | 99 | +	logger := logging.WithReconciler(klog.Background(), BuiltinTypesControllerName) | 
|  | 100 | + | 
|  | 101 | +	// System CRDs could change over time, and so we build that part of the mapper dynamically. | 
|  | 102 | + | 
|  | 103 | +	// We are only interested in system CRDs. | 
|  | 104 | +	_, _ = crdInformer.Informer().Cluster("system:system-crds").AddEventHandler(cache.ResourceEventHandlerFuncs{ | 
|  | 105 | +		AddFunc: func(obj interface{}) { | 
|  | 106 | +			c.enqueueCRD(tombstone.Obj[*apiextensionsv1.CustomResourceDefinition](obj), logger) | 
|  | 107 | +		}, | 
|  | 108 | +		UpdateFunc: func(oldObj, newObj interface{}) { | 
|  | 109 | +			c.enqueueCRD(tombstone.Obj[*apiextensionsv1.CustomResourceDefinition](newObj), logger) | 
|  | 110 | +		}, | 
|  | 111 | +		DeleteFunc: func(obj interface{}) { | 
|  | 112 | +			c.enqueueCRD(tombstone.Obj[*apiextensionsv1.CustomResourceDefinition](obj), logger) | 
|  | 113 | +		}, | 
|  | 114 | +	}) | 
|  | 115 | + | 
|  | 116 | +	return c, nil | 
|  | 117 | +} | 
|  | 118 | + | 
|  | 119 | +func (c *BuiltinTypesController) enqueueCRD(crd *apiextensionsv1.CustomResourceDefinition, logger logr.Logger) { | 
|  | 120 | +	if !apiextensionshelpers.IsCRDConditionTrue(crd, apiextensionsv1.Established) { | 
|  | 121 | +		// The CRD is not ready yet. Nothing to do, we'll get notified on the next update event. | 
|  | 122 | +		return | 
|  | 123 | +	} | 
|  | 124 | + | 
|  | 125 | +	// CRD name is enforced to be in format "<Resource>.<Group>", e.g. "cowboys.wildwest.dev". | 
|  | 126 | +	key, err := kcpcache.MetaClusterNamespaceKeyFunc(crd) | 
|  | 127 | +	if err != nil { | 
|  | 128 | +		utilruntime.HandleError(err) | 
|  | 129 | +		return | 
|  | 130 | +	} | 
|  | 131 | + | 
|  | 132 | +	logger.V(4).Info("queueing system CRD") | 
|  | 133 | +	c.queue.Add(key) | 
|  | 134 | +} | 
|  | 135 | + | 
|  | 136 | +func (c *BuiltinTypesController) Start(ctx context.Context, numThreads int) { | 
|  | 137 | +	defer utilruntime.HandleCrash() | 
|  | 138 | +	defer c.queue.ShutDown() | 
|  | 139 | + | 
|  | 140 | +	logger := logging.WithReconciler(klog.FromContext(ctx), BuiltinTypesControllerName) | 
|  | 141 | +	ctx = klog.NewContext(ctx, logger) | 
|  | 142 | +	logger.Info("Starting controller") | 
|  | 143 | +	defer logger.Info("Shutting down controller") | 
|  | 144 | + | 
|  | 145 | +	for range numThreads { | 
|  | 146 | +		go wait.UntilWithContext(ctx, c.startWorker, time.Second) | 
|  | 147 | +	} | 
|  | 148 | + | 
|  | 149 | +	<-ctx.Done() | 
|  | 150 | +} | 
|  | 151 | + | 
|  | 152 | +func (c *BuiltinTypesController) startWorker(ctx context.Context) { | 
|  | 153 | +	for c.processNextWorkItem(ctx) { | 
|  | 154 | +	} | 
|  | 155 | +} | 
|  | 156 | + | 
|  | 157 | +func (c *BuiltinTypesController) processNextWorkItem(ctx context.Context) bool { | 
|  | 158 | +	// Wait until there is a new item in the working queue | 
|  | 159 | +	key, quit := c.queue.Get() | 
|  | 160 | +	if quit { | 
|  | 161 | +		return false | 
|  | 162 | +	} | 
|  | 163 | + | 
|  | 164 | +	logger := logging.WithQueueKey(klog.FromContext(ctx), key) | 
|  | 165 | +	ctx = klog.NewContext(ctx, logger) | 
|  | 166 | +	logger.Info("processing key") | 
|  | 167 | + | 
|  | 168 | +	// No matter what, tell the queue we're done with this key, to unblock | 
|  | 169 | +	// other workers. | 
|  | 170 | +	defer c.queue.Done(key) | 
|  | 171 | + | 
|  | 172 | +	if err := c.process(ctx, key); err != nil { | 
|  | 173 | +		utilruntime.HandleError(fmt.Errorf("%q controller failed to sync %q, err: %w", DynamicTypesControllerName, key, err)) | 
|  | 174 | +		c.queue.AddRateLimited(key) | 
|  | 175 | +		return true | 
|  | 176 | +	} | 
|  | 177 | +	c.queue.Forget(key) | 
|  | 178 | +	return true | 
|  | 179 | +} | 
|  | 180 | + | 
|  | 181 | +func (c *BuiltinTypesController) gatherGVKRsForCRD(crd *apiextensionsv1.CustomResourceDefinition) []typeMeta { | 
|  | 182 | +	if crd == nil { | 
|  | 183 | +		return nil | 
|  | 184 | +	} | 
|  | 185 | +	gvkrs := make([]typeMeta, 0, len(crd.Spec.Versions)) | 
|  | 186 | +	for _, version := range crd.Spec.Versions { | 
|  | 187 | +		if !version.Served { | 
|  | 188 | +			continue | 
|  | 189 | +		} | 
|  | 190 | + | 
|  | 191 | +		gvkrs = append(gvkrs, newTypeMeta( | 
|  | 192 | +			crd.Spec.Group, | 
|  | 193 | +			version.Name, | 
|  | 194 | +			crd.Status.AcceptedNames.Kind, | 
|  | 195 | +			crd.Status.AcceptedNames.Singular, | 
|  | 196 | +			crd.Status.AcceptedNames.Plural, | 
|  | 197 | +			resourceScopeToRESTScope(crd.Spec.Scope), | 
|  | 198 | +		)) | 
|  | 199 | +	} | 
|  | 200 | +	return gvkrs | 
|  | 201 | +} | 
|  | 202 | + | 
|  | 203 | +func (c *BuiltinTypesController) gatherGVKRsForMappedGroupResource(gr schema.GroupResource) ([]typeMeta, error) { | 
|  | 204 | +	gvkrs, err := c.state.builtin.getGVKRs(gr) | 
|  | 205 | +	if err != nil { | 
|  | 206 | +		if meta.IsNoMatchError(err) { | 
|  | 207 | +			return nil, nil | 
|  | 208 | +		} | 
|  | 209 | +		return nil, err | 
|  | 210 | +	} | 
|  | 211 | +	return gvkrs, nil | 
|  | 212 | +} | 
|  | 213 | + | 
|  | 214 | +func (c *BuiltinTypesController) process(ctx context.Context, key string) error { | 
|  | 215 | +	_, _, name, err := kcpcache.SplitMetaClusterNamespaceKey(key) | 
|  | 216 | +	if err != nil { | 
|  | 217 | +		return err | 
|  | 218 | +	} | 
|  | 219 | + | 
|  | 220 | +	logger := logging.WithQueueKey(klog.FromContext(ctx), key) | 
|  | 221 | + | 
|  | 222 | +	// CRD name is enforced to be in format "<Resource>.<Group>", e.g. "cowboys.wildwest.dev". | 
|  | 223 | +	gr := schema.ParseGroupResource(name) | 
|  | 224 | + | 
|  | 225 | +	crd, err := c.getCRD(systemCRDClusterName, name) | 
|  | 226 | +	if err != nil && !apierrors.IsNotFound(err) { | 
|  | 227 | +		return err | 
|  | 228 | +	} | 
|  | 229 | + | 
|  | 230 | +	// Remove and add the mapping -- this way we can refresh any existing mappings. | 
|  | 231 | +	typeMetaToRemove, err := c.gatherGVKRsForMappedGroupResource(gr) | 
|  | 232 | +	if err != nil { | 
|  | 233 | +		return err | 
|  | 234 | +	} | 
|  | 235 | +	typeMetaToAdd := c.gatherGVKRsForCRD(crd) | 
|  | 236 | + | 
|  | 237 | +	logger.V(4).Info("applying mappings") | 
|  | 238 | + | 
|  | 239 | +	c.state.lock.Lock() | 
|  | 240 | +	defer c.state.lock.Unlock() | 
|  | 241 | +	c.state.builtin.apply(typeMetaToRemove, typeMetaToAdd) | 
|  | 242 | +	return nil | 
|  | 243 | +} | 
0 commit comments