diff --git a/pkg/client/fake/client.go b/pkg/client/fake/client.go index 2a07bd40b2..a9613468b4 100644 --- a/pkg/client/fake/client.go +++ b/pkg/client/fake/client.go @@ -95,7 +95,8 @@ type fakeClient struct { // indexesLock must be held when accessing indexes. indexesLock sync.RWMutex - returnManagedFields bool + returnManagedFields bool + setCreationTimestamp bool } var _ client.WithWatch = &fakeClient{} @@ -131,6 +132,7 @@ type ClientBuilder struct { interceptorFuncs *interceptor.Funcs typeConverters []managedfields.TypeConverter returnManagedFields bool + setCreationTimestamp bool isBuilt bool // indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK. @@ -256,6 +258,13 @@ func (f *ClientBuilder) WithReturnManagedFields() *ClientBuilder { return f } +// WithSetCreationTimestamp configures the fake client to set metadata.creationTimestamp on Create and first Apply. +// on objects. +func (f *ClientBuilder) WithSetCreationTimestamp() *ClientBuilder { + f.setCreationTimestamp = true + return f +} + // Build builds and returns a new fake client. func (f *ClientBuilder) Build() client.WithWatch { if f.isBuilt { @@ -307,6 +316,7 @@ func (f *ClientBuilder) Build() client.WithWatch { scheme: f.scheme, withStatusSubresource: withStatusSubResource, usesFieldManagedObjectTracker: usesFieldManagedObjectTracker, + setCreationTimestamp: f.setCreationTimestamp, } for _, obj := range f.initObject { @@ -332,6 +342,7 @@ func (f *ClientBuilder) Build() client.WithWatch { indexes: f.indexes, withStatusSubresource: withStatusSubResource, returnManagedFields: f.returnManagedFields, + setCreationTimestamp: f.setCreationTimestamp, } if f.interceptorFuncs != nil { @@ -933,6 +944,16 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client // Overwrite it unconditionally, this matches the apiserver behavior // which allows to set it on create, but will then ignore it. obj.SetResourceVersion("1") + + if c.setCreationTimestamp { + now, err := time.Parse(time.RFC3339, time.Now().Format(time.RFC3339)) + if err != nil { + return apierrors.NewInternalError(err) + } + obj.SetCreationTimestamp(metav1.Time{ + Time: now, + }) + } } else { // SSA deletionTimestamp updates are silently ignored obj.SetDeletionTimestamp(oldAccessor.GetDeletionTimestamp()) diff --git a/pkg/client/fake/client_test.go b/pkg/client/fake/client_test.go index 209ccc67fe..ed81ee5fce 100644 --- a/pkg/client/fake/client_test.go +++ b/pkg/client/fake/client_test.go @@ -414,6 +414,7 @@ var _ = Describe("Fake client", func() { Expect(err).ToNot(HaveOccurred()) Expect(obj).To(Equal(newcm)) Expect(obj.ObjectMeta.ResourceVersion).To(Equal("1")) + Expect(obj.ObjectMeta.CreationTimestamp.IsZero()).To(BeTrue()) }) It("should error on create with set resourceVersion", func(ctx SpecContext) { @@ -1229,7 +1230,6 @@ var _ = Describe("Fake client", func() { Expect(err).ToNot(HaveOccurred()) Expect(newObj.Finalizers).To(BeEmpty()) }) - } Context("with default scheme.Scheme", func() { @@ -1473,6 +1473,48 @@ var _ = Describe("Fake client", func() { }) }) + Context("with SetCreationTimestamp option", func() { + BeforeEach(func() { + cl = NewClientBuilder(). + WithSetCreationTimestamp(). + Build() + }) + + It("should be able to Create and set metadata.CreationTimestamp", func(ctx SpecContext) { + By("Creating a new configmap") + newcm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cm-with-creation-timestamp", + Namespace: "ns2", + }, + } + err := cl.Create(ctx, newcm) + Expect(err).ToNot(HaveOccurred()) + + By("Getting the new configmap") + namespacedName := types.NamespacedName{ + Name: "test-cm-with-creation-timestamp", + Namespace: "ns2", + } + obj := &corev1.ConfigMap{} + err = cl.Get(ctx, namespacedName, obj) + Expect(err).ToNot(HaveOccurred()) + Expect(obj).To(Equal(newcm)) + Expect(obj.ObjectMeta.ResourceVersion).To(Equal("1")) + Expect(obj.ObjectMeta.CreationTimestamp.IsZero()).ToNot(BeTrue()) + }) + + It("sets creatioTimestamp on SSA create when required to do so", func(ctx SpecContext) { + obj := corev1applyconfigurations. + ConfigMap("foo-with-creation-timestamp", "default"). + WithData(map[string]string{"some": "data"}) + + cl := NewClientBuilder().WithSetCreationTimestamp().Build() + Expect(cl.Apply(ctx, obj, client.FieldOwner("foo"))).NotTo(HaveOccurred()) + Expect(obj.CreationTimestamp.IsZero()).To(BeFalse()) + }) + }) + It("should set the ResourceVersion to 999 when adding an object to the tracker", func(ctx SpecContext) { cl := NewClientBuilder().WithObjects(&corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "cm"}}).Build() @@ -3068,6 +3110,16 @@ var _ = Describe("Fake client", func() { Expect(obj.ResourceVersion).To(BeEquivalentTo(ptr.To("1"))) }) + It("does not set creatioTimestamp on SSA create", func(ctx SpecContext) { + obj := corev1applyconfigurations. + ConfigMap("foo", "default"). + WithData(map[string]string{"some": "data"}) + + cl := NewClientBuilder().Build() + Expect(cl.Apply(ctx, obj, client.FieldOwner("foo"))).NotTo(HaveOccurred()) + Expect(obj.CreationTimestamp.IsZero()).To(BeTrue()) + }) + It("ignores a passed resourceVersion on SSA create", func(ctx SpecContext) { obj := corev1applyconfigurations. ConfigMap("foo", "default"). diff --git a/pkg/client/fake/versioned_tracker.go b/pkg/client/fake/versioned_tracker.go index bbe3ac9b0d..1a1b752563 100644 --- a/pkg/client/fake/versioned_tracker.go +++ b/pkg/client/fake/versioned_tracker.go @@ -22,6 +22,7 @@ import ( "fmt" "runtime/debug" "strconv" + "time" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" @@ -34,6 +35,7 @@ import ( "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/testing" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" ) @@ -44,6 +46,7 @@ type versionedTracker struct { scheme *runtime.Scheme withStatusSubresource sets.Set[schema.GroupVersionKind] usesFieldManagedObjectTracker bool + setCreationTimestamp bool } func (t versionedTracker) Add(obj runtime.Object) error { @@ -111,12 +114,24 @@ func (t versionedTracker) Create(gvr schema.GroupVersionResource, obj runtime.Ob return apierrors.NewBadRequest("resourceVersion can not be set for Create requests") } accessor.SetResourceVersion("1") + + originalCreationTimestamp := accessor.GetCreationTimestamp() + if t.setCreationTimestamp { + now, err := time.Parse(time.RFC3339, time.Now().Format(time.RFC3339)) + if err != nil { + return apierrors.NewInternalError(err) + } + accessor.SetCreationTimestamp(metav1.Time{ + Time: now, + }) + } obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj) if err != nil { return err } if err := t.upstream.Create(gvr, obj, ns, opts...); err != nil { accessor.SetResourceVersion("") + accessor.SetCreationTimestamp(originalCreationTimestamp) return err }