diff --git a/pkg/reconciler/cache/cachedresources/cachedresources_controller.go b/pkg/reconciler/cache/cachedresources/cachedresources_controller.go index 5d743263165..6587b65340f 100644 --- a/pkg/reconciler/cache/cachedresources/cachedresources_controller.go +++ b/pkg/reconciler/cache/cachedresources/cachedresources_controller.go @@ -68,7 +68,6 @@ func NewController( kcpClusterClient kcpclientset.ClusterInterface, kcpCacheClient kcpclientset.ClusterInterface, dynamicClient kcpdynamic.ClusterInterface, - cacheDynamicClient kcpdynamic.ClusterInterface, kubeClusterClient kcpkubernetesclientset.ClusterInterface, namespaceInformer kcpcorev1informers.NamespaceClusterInformer, @@ -93,8 +92,7 @@ func NewController( kcpClient: kcpClusterClient, kcpCacheClient: kcpCacheClient, - dynamicClient: dynamicClient, - cacheDynamicClient: cacheDynamicClient, + dynamicClient: dynamicClient, dynRESTMapper: dynRESTMapper, @@ -165,8 +163,7 @@ type Controller struct { kcpClient kcpclientset.ClusterInterface kcpCacheClient kcpclientset.ClusterInterface - dynamicClient kcpdynamic.ClusterInterface - cacheDynamicClient kcpdynamic.ClusterInterface + dynamicClient kcpdynamic.ClusterInterface dynRESTMapper *dynamicrestmapper.DynamicRESTMapper diff --git a/pkg/reconciler/cache/cachedresources/cachedresources_reconcile.go b/pkg/reconciler/cache/cachedresources/cachedresources_reconcile.go index d54aeb2cf39..3d425bc6a87 100644 --- a/pkg/reconciler/cache/cachedresources/cachedresources_reconcile.go +++ b/pkg/reconciler/cache/cachedresources/cachedresources_reconcile.go @@ -83,7 +83,7 @@ func (c *Controller) reconcile(ctx context.Context, cluster logicalcluster.Name, }, &replication{ shardName: c.shardName, - dynamicCacheClient: c.dynamicClient, + dynamicClusterClient: c.dynamicClient, kcpCacheClient: c.kcpCacheClient, dynRESTMapper: c.dynRESTMapper, cacheKcpInformers: c.cacheKcpInformers, diff --git a/pkg/reconciler/cache/cachedresources/cachedresources_reconcile_replication.go b/pkg/reconciler/cache/cachedresources/cachedresources_reconcile_replication.go index 8506a6470bd..670fb7b890b 100644 --- a/pkg/reconciler/cache/cachedresources/cachedresources_reconcile_replication.go +++ b/pkg/reconciler/cache/cachedresources/cachedresources_reconcile_replication.go @@ -41,7 +41,7 @@ import ( // Or deletes the replication controller if the published resource is being deleted. type replication struct { shardName string - dynamicCacheClient kcpdynamic.ClusterInterface + dynamicClusterClient kcpdynamic.ClusterInterface kcpCacheClient kcpclientset.ClusterInterface dynRESTMapper *dynamicrestmapper.DynamicRESTMapper cacheKcpInformers kcpinformers.SharedInformerFactory @@ -103,8 +103,9 @@ func (r *replication) reconcile(ctx context.Context, cachedResource *cachev1alph c, err := replicationcontroller.NewController( r.shardName, - r.dynamicCacheClient, + r.dynamicClusterClient, r.kcpCacheClient, + cluster, gvr, replicated, callback, diff --git a/pkg/reconciler/cache/cachedresources/replication/replication_controller.go b/pkg/reconciler/cache/cachedresources/replication/replication_controller.go index d96ac59fd7b..63745fc4e66 100644 --- a/pkg/reconciler/cache/cachedresources/replication/replication_controller.go +++ b/pkg/reconciler/cache/cachedresources/replication/replication_controller.go @@ -32,6 +32,7 @@ import ( kcpcache "github.com/kcp-dev/apimachinery/v2/pkg/cache" kcpdynamic "github.com/kcp-dev/client-go/dynamic" + "github.com/kcp-dev/logicalcluster/v3" cachev1alpha1 "github.com/kcp-dev/sdk/apis/cache/v1alpha1" kcpclientset "github.com/kcp-dev/sdk/client/clientset/versioned/cluster" @@ -53,8 +54,9 @@ const ( // NewController returns a new replication controller. func NewController( shardName string, - dynamicCacheClient kcpdynamic.ClusterInterface, + dynamicClusterClient kcpdynamic.ClusterInterface, kcpCacheClient kcpclientset.ClusterInterface, + cluster logicalcluster.Name, gvr schema.GroupVersionResource, replicated *ReplicatedGVR, callback func(), @@ -68,12 +70,12 @@ func NewController( Name: ControllerName, }, ), - dynamicCacheClient: dynamicCacheClient, - kcpCacheClient: kcpCacheClient, - replicated: replicated, - callback: callback, - cleanupFuncs: make([]func(), 0), - localLabelSelector: localLabelSelector, + dynamicClusterClient: dynamicClusterClient, + kcpCacheClient: kcpCacheClient, + replicated: replicated, + callback: callback, + cleanupFuncs: make([]func(), 0), + localLabelSelector: localLabelSelector, } localHandler, err := c.replicated.Local.AddEventHandler(cache.ResourceEventHandlerFuncs{ @@ -88,10 +90,33 @@ func NewController( _ = c.replicated.Local.RemoveEventHandler(localHandler) }) - globalHandler, err := c.replicated.Global.AddEventHandler(cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { c.enqueueCacheObject(obj) }, - UpdateFunc: func(_, obj interface{}) { c.enqueueCacheObject(obj) }, - DeleteFunc: func(obj interface{}) { c.enqueueCacheObject(obj) }, + globalHandler, err := c.replicated.Global.AddEventHandler(cache.FilteringResourceEventHandler{ + FilterFunc: func(obj interface{}) bool { + cachedObj := obj.(*cachev1alpha1.CachedObject) + labels := cachedObj.Labels + if labels == nil { + return false + } + + // Skip CachedObjects that are not coming from the source CachedResource. + + if logicalcluster.From(cachedObj) != cluster { + return false + } + + if gvr.Group != labels[LabelKeyObjectGroup] || + gvr.Version != labels[LabelKeyObjectVersion] || + gvr.Resource != labels[LabelKeyObjectResource] { + return false + } + + return true + }, + Handler: cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { c.enqueueCacheObject(obj) }, + UpdateFunc: func(_, obj interface{}) { c.enqueueCacheObject(obj) }, + DeleteFunc: func(obj interface{}) { c.enqueueCacheObject(obj) }, + }, }) if err != nil { return nil, err @@ -114,12 +139,6 @@ func (c *Controller) enqueueObject(obj interface{}, gvr schema.GroupVersionResou } func (c *Controller) enqueueCacheObject(obj interface{}) { - key, err := kcpcache.DeletionHandlingMetaClusterNamespaceKeyFunc(obj) - if err != nil { - utilruntime.HandleError(err) - return - } - // This way we extract what is the original GVR of the object that we are replicating. cr, ok := obj.(*cachev1alpha1.CachedObject) if !ok { @@ -133,7 +152,7 @@ func (c *Controller) enqueueCacheObject(obj interface{}) { Version: labels[LabelKeyObjectVersion], Resource: labels[LabelKeyObjectResource], } - + key := kcpcache.ToClusterAwareKey(string(logicalcluster.From(cr)), labels[LabelKeyObjectOriginalNamespace], labels[LabelKeyObjectOriginalName]) gvrKey := fmt.Sprintf("%s.%s.%s::%s", gvr.Version, gvr.Resource, gvr.Group, key) c.queue.Add(gvrKey) } @@ -203,8 +222,8 @@ type Controller struct { shardName string queue workqueue.TypedRateLimitingInterface[string] - dynamicCacheClient kcpdynamic.ClusterInterface - kcpCacheClient kcpclientset.ClusterInterface + dynamicClusterClient kcpdynamic.ClusterInterface + kcpCacheClient kcpclientset.ClusterInterface replicated *ReplicatedGVR diff --git a/pkg/reconciler/cache/cachedresources/replication/replication_reconcile.go b/pkg/reconciler/cache/cachedresources/replication/replication_reconcile.go index 8de90ed7742..8f2bca6f265 100644 --- a/pkg/reconciler/cache/cachedresources/replication/replication_reconcile.go +++ b/pkg/reconciler/cache/cachedresources/replication/replication_reconcile.go @@ -84,7 +84,7 @@ func (c *Controller) reconcile(ctx context.Context, gvrKey string) error { r := &replicationReconciler{ shardName: c.shardName, localLabelSelector: c.localLabelSelector, - getLocalCopy: func(cluster logicalcluster.Name, namespace, name string) (*unstructured.Unstructured, error) { + getLocalPartialObjectMetadata: func(cluster logicalcluster.Name, namespace, name string) (*unstructured.Unstructured, error) { gvr := gvrFromKey key := kcpcache.ToClusterAwareKey(cluster.String(), namespace, name) obj, exists, err := c.replicated.Local.GetIndexer().GetByKey(key) @@ -110,7 +110,7 @@ func (c *Controller) reconcile(ctx context.Context, gvrKey string) error { u.SetAPIVersion(gvr.GroupVersion().String()) return u, nil }, - getGlobalCopy: func(cluster logicalcluster.Name, namespace, name string) (*unstructured.Unstructured, error) { + getCachedObject: func(ctx context.Context, cluster logicalcluster.Name, namespace, name string) (*cachev1alpha1.CachedObject, error) { gvr := gvrFromKey if gvr.Group == "" { gvr.Group = "core" @@ -128,28 +128,53 @@ func (c *Controller) reconcile(ctx context.Context, gvrKey string) error { return nil, fmt.Errorf("found multiple objects for %v|%v/%v", cluster, namespace, name) } - obj := objs[0] + return objs[0].(*cachev1alpha1.CachedObject), nil + }, + getGlobalCopyFromCachedObject: func(cachedObj *cachev1alpha1.CachedObject) (*unstructured.Unstructured, error) { + gvr := gvrFromKey - u, err := toUnstructured(obj) + u, err := toUnstructured(&cachedObj.Spec.Raw) if err != nil { return nil, err } - if _, ok := obj.(*unstructured.Unstructured); ok { - u = u.DeepCopy() - } - + u = u.DeepCopy() u.SetKind(c.replicated.Kind) u.SetAPIVersion(gvr.GroupVersion().String()) - return u, nil }, - createObject: func(ctx context.Context, cluster logicalcluster.Name, obj *unstructured.Unstructured) (*cachev1alpha1.CachedObject, error) { + getLocalCopy: func(ctx context.Context, cluster logicalcluster.Name, namespace, name string) (*unstructured.Unstructured, error) { + gvr := gvrFromKey + + obj, err := c.dynamicClusterClient.Cluster(cluster.Path()). + Resource(gvrFromKey). + Namespace(namespace). + Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + + obj.SetKind(c.replicated.Kind) + obj.SetAPIVersion(gvr.GroupVersion().String()) + + // Append system annotations to the object. + annotations := obj.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations[genericrequest.ShardAnnotationKey] = c.shardName + annotations[AnnotationKeyOriginalResourceUID] = string(obj.GetUID()) + annotations[AnnotationKeyOriginalResourceVersion] = obj.GetResourceVersion() + obj.SetAnnotations(annotations) + + return obj, nil + }, + createObject: func(ctx context.Context, cluster logicalcluster.Name, local *unstructured.Unstructured) (*cachev1alpha1.CachedObject, error) { gvr := gvrFromKey if gvr.Group == "" { gvr.Group = "core" } - objBytes, err := json.Marshal(obj) + objBytes, err := json.Marshal(local) if err != nil { return nil, err } @@ -159,9 +184,9 @@ func (c *Controller) reconcile(ctx context.Context, gvrKey string) error { APIVersion: cachev1alpha1.SchemeGroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ - Name: GenCachedObjectName(gvr, obj.GetNamespace(), obj.GetName()), - Labels: obj.GetLabels(), - Annotations: obj.GetAnnotations(), + Name: GenCachedObjectName(gvr, local.GetNamespace(), local.GetName()), + Labels: local.GetLabels(), + Annotations: local.GetAnnotations(), CreationTimestamp: metav1.NewTime(time.Now()), }, Spec: cachev1alpha1.CachedObjectSpec{ @@ -176,19 +201,19 @@ func (c *Controller) reconcile(ctx context.Context, gvrKey string) error { cacheObj.Labels[LabelKeyObjectGroup] = gvr.Group cacheObj.Labels[LabelKeyObjectVersion] = gvr.Version cacheObj.Labels[LabelKeyObjectResource] = gvr.Resource - cacheObj.Labels[LabelKeyObjectOriginalName] = obj.GetName() - cacheObj.Labels[LabelKeyObjectOriginalNamespace] = obj.GetNamespace() + cacheObj.Labels[LabelKeyObjectOriginalName] = local.GetName() + cacheObj.Labels[LabelKeyObjectOriginalNamespace] = local.GetNamespace() u, err := c.kcpCacheClient.Cluster(cluster.Path()).CacheV1alpha1().CachedObjects().Create(ctx, cacheObj, metav1.CreateOptions{}) return u, err }, - updateObject: func(ctx context.Context, cluster logicalcluster.Name, obj *unstructured.Unstructured) (*cachev1alpha1.CachedObject, error) { + updateCachedObjectWithLocalUnstructured: func(ctx context.Context, cluster logicalcluster.Name, origCachedObj *cachev1alpha1.CachedObject, local *unstructured.Unstructured) (*cachev1alpha1.CachedObject, error) { gvr := gvrFromKey if gvr.Group == "" { gvr.Group = "core" } - objBytes, err := json.Marshal(obj) + objBytes, err := json.Marshal(local) if err != nil { return nil, err } @@ -199,10 +224,10 @@ func (c *Controller) reconcile(ctx context.Context, gvrKey string) error { APIVersion: cachev1alpha1.SchemeGroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ - Name: GenCachedObjectName(gvr, obj.GetNamespace(), obj.GetName()), - Labels: obj.GetLabels(), - Annotations: obj.GetAnnotations(), - ResourceVersion: obj.GetResourceVersion(), + Name: GenCachedObjectName(gvr, local.GetNamespace(), local.GetName()), + Labels: origCachedObj.GetLabels(), + Annotations: origCachedObj.GetAnnotations(), + ResourceVersion: origCachedObj.GetResourceVersion(), }, Spec: cachev1alpha1.CachedObjectSpec{ Raw: runtime.RawExtension{Raw: objBytes}, @@ -216,8 +241,8 @@ func (c *Controller) reconcile(ctx context.Context, gvrKey string) error { cacheObj.Labels[LabelKeyObjectGroup] = gvr.Group cacheObj.Labels[LabelKeyObjectVersion] = gvr.Version cacheObj.Labels[LabelKeyObjectResource] = gvr.Resource - cacheObj.Labels[LabelKeyObjectOriginalName] = obj.GetName() - cacheObj.Labels[LabelKeyObjectOriginalNamespace] = obj.GetNamespace() + cacheObj.Labels[LabelKeyObjectOriginalName] = local.GetName() + cacheObj.Labels[LabelKeyObjectOriginalNamespace] = local.GetNamespace() return c.kcpCacheClient.Cluster(cluster.Path()).CacheV1alpha1().CachedObjects().Update(ctx, cacheObj, metav1.UpdateOptions{}) }, @@ -245,12 +270,14 @@ type replicationReconciler struct { deleted bool localLabelSelector labels.Selector - getLocalCopy func(cluster logicalcluster.Name, namespace, name string) (*unstructured.Unstructured, error) - getGlobalCopy func(cluster logicalcluster.Name, namespace, name string) (*unstructured.Unstructured, error) + getLocalPartialObjectMetadata func(cluster logicalcluster.Name, namespace, name string) (*unstructured.Unstructured, error) + getCachedObject func(ctx context.Context, cluster logicalcluster.Name, namespace, name string) (*cachev1alpha1.CachedObject, error) + getGlobalCopyFromCachedObject func(cachedObj *cachev1alpha1.CachedObject) (*unstructured.Unstructured, error) + getLocalCopy func(ctx context.Context, cluster logicalcluster.Name, namespace, name string) (*unstructured.Unstructured, error) - createObject func(ctx context.Context, cluster logicalcluster.Name, obj *unstructured.Unstructured) (*cachev1alpha1.CachedObject, error) - updateObject func(ctx context.Context, cluster logicalcluster.Name, obj *unstructured.Unstructured) (*cachev1alpha1.CachedObject, error) - deleteObject func(ctx context.Context, cluster logicalcluster.Name, ns, name string) error + createObject func(ctx context.Context, cluster logicalcluster.Name, local *unstructured.Unstructured) (*cachev1alpha1.CachedObject, error) + updateCachedObjectWithLocalUnstructured func(ctx context.Context, cluster logicalcluster.Name, cachedObj *cachev1alpha1.CachedObject, local *unstructured.Unstructured) (*cachev1alpha1.CachedObject, error) + deleteObject func(ctx context.Context, cluster logicalcluster.Name, ns, name string) error } // reconcile makes sure that the object under the given key from the local shard is replicated to the cache server. @@ -270,7 +297,7 @@ func (r *replicationReconciler) reconcile(ctx context.Context, key string) error return nil } - localCopy, err := r.getLocalCopy(clusterName, ns, name) + localPartialObjMeta, err := r.getLocalPartialObjectMetadata(clusterName, ns, name) if err != nil && !apierrors.IsNotFound(err) { utilruntime.HandleError(err) return nil @@ -278,21 +305,20 @@ func (r *replicationReconciler) reconcile(ctx context.Context, key string) error localExists := !apierrors.IsNotFound(err) // we only replicate objects that match the label selector. - if localExists && r.localLabelSelector != nil && !r.localLabelSelector.Matches(labels.Set(localCopy.GetLabels())) { + if localExists && r.localLabelSelector != nil && !r.localLabelSelector.Matches(labels.Set(localPartialObjMeta.GetLabels())) { logger.V(2).WithValues("cluster", clusterName, "namespace", ns, "name", name).Info("Object does not match label selector, skipping") return nil } - globalCopy, err := r.getGlobalCopy(clusterName, ns, name) + cachedObj, err := r.getCachedObject(ctx, clusterName, ns, name) if err != nil && !apierrors.IsNotFound(err) { - utilruntime.HandleError(err) - return nil + return err } - globalExists := !apierrors.IsNotFound(err) + cachedObjExists := !apierrors.IsNotFound(err) // local is gone or being deleted. Delete in cache. - if !localExists || !localCopy.GetDeletionTimestamp().IsZero() { - if !globalExists { + if !localExists || !localPartialObjMeta.GetDeletionTimestamp().IsZero() { + if !cachedObjExists { return nil } @@ -305,24 +331,30 @@ func (r *replicationReconciler) reconcile(ctx context.Context, key string) error return nil } - // local exists, global doesn't. Create in cache. - if !globalExists { - originalRV := localCopy.GetResourceVersion() - originalUID := localCopy.GetUID() - - localCopy.SetResourceVersion("") - annotations := localCopy.GetAnnotations() - if annotations == nil { - annotations = map[string]string{} + var globalCopy *unstructured.Unstructured + if cachedObjExists { + globalCopy, err = r.getGlobalCopyFromCachedObject(cachedObj) + if err != nil { + return err + } + // Exit early if there were no changes on the resource. + if localPartialObjMeta.GetResourceVersion() != "" && globalCopy.GetResourceVersion() == localPartialObjMeta.GetResourceVersion() { + logger.V(4).Info("Object is up to date") + return nil } - annotations[genericrequest.ShardAnnotationKey] = r.shardName - annotations[AnnotationKeyOriginalResourceVersion] = originalRV - annotations[AnnotationKeyOriginalResourceUID] = string(originalUID) + } - localCopy.SetAnnotations(annotations) + // The local DDSIF informer yields only PartialObjectMetadata, and we need the full object for replication. + localCopy, err := r.getLocalCopy(ctx, clusterName, ns, name) + if err != nil { + // Return any error we get. If it's NotFound, we want to requeue in that case too: the local DDSIF + // informer probably hasn't caught up yet, and we may need to clean up the replicated CachedObject. + return err + } + if !cachedObjExists { logger.V(2).Info("Creating object in global cache") - _, err := r.createObject(ctx, clusterName, localCopy) + _, err = r.createObject(ctx, clusterName, localCopy) return err } @@ -341,6 +373,6 @@ func (r *replicationReconciler) reconcile(ctx context.Context, key string) error } logger.V(2).WithValues("kind", globalCopy.GetKind(), "namespace", globalCopy.GetNamespace(), "name", globalCopy.GetName()).Info("Updating object in global cache") - _, err = r.updateObject(ctx, clusterName, globalCopy) // no need for patch because there is only this actor + _, err = r.updateCachedObjectWithLocalUnstructured(ctx, clusterName, cachedObj, localCopy) // no need for patch because there is only this actor return err } diff --git a/pkg/server/controllers.go b/pkg/server/controllers.go index ade3b233303..0125f5cf0df 100644 --- a/pkg/server/controllers.go +++ b/pkg/server/controllers.go @@ -1742,7 +1742,6 @@ func (s *Server) installCacheController(ctx context.Context, config *rest.Config kcpClusterClient, s.KcpCacheClusterClient, dynamicClient, - s.CacheDynamicClient, s.KubeClusterClient, s.KubeSharedInformerFactory.Core().V1().Namespaces(), s.KubeSharedInformerFactory.Core().V1().Secrets(), diff --git a/test/e2e/virtualresources/cachedresources/vr_cachedresources_test.go b/test/e2e/virtualresources/cachedresources/vr_cachedresources_test.go index e541e255c3f..2e42020dd7b 100644 --- a/test/e2e/virtualresources/cachedresources/vr_cachedresources_test.go +++ b/test/e2e/virtualresources/cachedresources/vr_cachedresources_test.go @@ -367,17 +367,30 @@ func TestCachedResources(t *testing.T) { // Eventually we should see two of each in the resourcesCounter, // i.e. one for each consumer workspace. t.Logf("Creating a Cowboy in %q", providerPath) - cowboyOne, err := wildwestClusterClient.Cluster(providerPath).WildwestV1alpha1().Cowboys("default").Create(ctx, &wildwestv1alpha1.Cowboy{ + cowboyOne := createCowboy(t, ctx, wildwestClusterClient, providerPath, &wildwestv1alpha1.Cowboy{ ObjectMeta: metav1.ObjectMeta{ - Name: "cowboys-1", + Name: "cowboys-1", + Namespace: "default", }, - }, metav1.CreateOptions{}) + Spec: wildwestv1alpha1.CowboySpec{ + Intent: "cowboys-1-spec", + }, + Status: wildwestv1alpha1.CowboyStatus{ + Result: "cowboys-1-status", + }, + }) require.NoError(t, err) t.Logf("Creating a Sheriff in %q", providerPath) - sheriffOne, err := createSheriff(ctx, kcpDynClusterClient, logicalcluster.Name(providerPath.String()), &wildwestv1alpha1.Sheriff{ + sheriffOne := createSheriff(t, ctx, kcpDynClusterClient, logicalcluster.Name(providerPath.String()), &wildwestv1alpha1.Sheriff{ ObjectMeta: metav1.ObjectMeta{ Name: "sheriffs-1", }, + Spec: wildwestv1alpha1.SheriffSpec{ + Intent: "sheriffs-1-spec", + }, + Status: wildwestv1alpha1.SheriffStatus{ + Result: "sheriffs-1-status", + }, }) require.NoError(t, err) @@ -530,22 +543,8 @@ func normalizeUnstructuredMap(origObj map[string]interface{}) map[string]interfa delete(meta, "uid") delete(meta, "generation") delete(meta, "managedFields") - - ann, hasAnn := meta["annotations"].(map[string]interface{}) - if hasAnn { - // TODO(gman0): HACK! https://github.com/kcp-dev/kcp/issues/3478 - // Partial metadata objects have this annotation added. - // This will go away once we have full objects. - delete(ann, "kcp.io/original-api-version") - } } - // TODO(gman0): HACK! https://github.com/kcp-dev/kcp/issues/3478 - // We need to remove spec and status, because we're getting only metadata for now. - // This will go away once we have full objects. - delete(obj, "spec") - delete(obj, "status") - return obj } @@ -566,11 +565,11 @@ func listSheriffs(ctx context.Context, c kcpdynamic.ClusterInterface, cluster lo return &sheriffs, nil } -func createSheriff(ctx context.Context, c kcpdynamic.ClusterInterface, cluster logicalcluster.Name, sheriff *wildwestv1alpha1.Sheriff) (*wildwestv1alpha1.Sheriff, error) { +func createSheriff(t *testing.T, ctx context.Context, c kcpdynamic.ClusterInterface, cluster logicalcluster.Name, sheriff *wildwestv1alpha1.Sheriff) *wildwestv1alpha1.Sheriff { + t.Helper() + m, err := runtime.DefaultUnstructuredConverter.ToUnstructured(sheriff) - if err != nil { - return nil, err - } + require.NoError(t, err) u := &unstructured.Unstructured{ Object: m, } @@ -580,11 +579,44 @@ func createSheriff(ctx context.Context, c kcpdynamic.ClusterInterface, cluster l u, err = c.Cluster(cluster.Path()).Resource( wildwestv1alpha1.SchemeGroupVersion.WithResource("sheriffs"), ).Create(ctx, u, metav1.CreateOptions{}) - if err != nil { - return nil, err - } + require.NoError(t, err) createdSheriff := &wildwestv1alpha1.Sheriff{} err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, createdSheriff) - return createdSheriff, err + require.NoError(t, err) + + createdSheriff.Status = sheriff.Status + m, err = runtime.DefaultUnstructuredConverter.ToUnstructured(createdSheriff) + require.NoError(t, err) + u.Object = m + + u, err = c.Cluster(cluster.Path()).Resource( + wildwestv1alpha1.SchemeGroupVersion.WithResource("sheriffs"), + ).UpdateStatus(ctx, u, metav1.UpdateOptions{}) + require.NoError(t, err) + + sheriffWithStatus := &wildwestv1alpha1.Sheriff{} + err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, sheriffWithStatus) + require.NoError(t, err) + + require.Equal(t, sheriff.Spec, sheriffWithStatus.Spec, "created Sheriff should have Spec") + require.Equal(t, sheriff.Status, sheriffWithStatus.Status, "created Sheriff should have Status") + + return sheriffWithStatus +} + +func createCowboy(t *testing.T, ctx context.Context, c wildwestclientset.ClusterInterface, path logicalcluster.Path, cowboy *wildwestv1alpha1.Cowboy) *wildwestv1alpha1.Cowboy { + t.Helper() + + createdCowboy, err := c.Cluster(path).WildwestV1alpha1().Cowboys(cowboy.Namespace).Create(ctx, cowboy, metav1.CreateOptions{}) + require.NoError(t, err) + + createdCowboy.Status = cowboy.Status + cowboyWithStatus, err := c.Cluster(path).WildwestV1alpha1().Cowboys(cowboy.Namespace).UpdateStatus(ctx, createdCowboy, metav1.UpdateOptions{}) + require.NoError(t, err) + + require.Equal(t, cowboy.Spec, cowboyWithStatus.Spec, "created Cowboy should have Spec") + require.Equal(t, cowboy.Status, cowboyWithStatus.Status, "created Cowboy should have Status") + + return cowboyWithStatus }