From ca011c2cfbcc3a81fa4468f2a4b92965f7d2765e Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Thu, 29 Feb 2024 12:32:50 -0500 Subject: [PATCH 1/2] Allow resources to contain unexported fields Using unexported fields in Kubernetes resources is not common, but does happen. Previously, these fields would cause cmp.Diff to panic. Now, we ignore the content of unexported fields when computing a diff. As the diff is for logging and test assertions this is relatively safe. Semantic equality is used for detecting when a managed resource needs to be updated. Custom equality func can be defined separately as needed. --- internal/resources/dies/dies.go | 66 ++ internal/resources/dies/zz_generated.die.go | 679 ++++++++++++++++++ .../resources/dies/zz_generated.die_test.go | 27 + .../resource_with_unexported_fields.go | 170 +++++ internal/resources/zz_generated.deepcopy.go | 119 +++ reconcilers/child_test.go | 229 ++++++ reconcilers/cmp.go | 24 + reconcilers/resource.go | 4 +- reconcilers/resource_test.go | 121 ++++ reconcilers/resourcemanager.go | 2 +- testing/config.go | 7 +- testing/subreconciler.go | 4 +- testing/webhook.go | 2 +- 13 files changed, 1445 insertions(+), 9 deletions(-) create mode 100644 internal/resources/resource_with_unexported_fields.go create mode 100644 reconcilers/cmp.go diff --git a/internal/resources/dies/dies.go b/internal/resources/dies/dies.go index dea6d16..3877e0e 100644 --- a/internal/resources/dies/dies.go +++ b/internal/resources/dies/dies.go @@ -99,3 +99,69 @@ func (d *TestDuckSpecDie) AddField(key, value string) *TestDuckSpecDie { r.Fields[key] = value }) } + +// +die:object=true +type _ = resources.TestResourceUnexportedFields + +// +die:ignore={unexportedFields} +type _ = resources.TestResourceUnexportedFieldsSpec + +func (d *TestResourceUnexportedFieldsSpecDie) AddField(key, value string) *TestResourceUnexportedFieldsSpecDie { + return d.DieStamp(func(r *resources.TestResourceUnexportedFieldsSpec) { + if r.Fields == nil { + r.Fields = map[string]string{} + } + r.Fields[key] = value + }) +} + +func (d *TestResourceUnexportedFieldsSpecDie) AddUnexportedField(key, value string) *TestResourceUnexportedFieldsSpecDie { + return d.DieStamp(func(r *resources.TestResourceUnexportedFieldsSpec) { + f := r.GetUnexportedFields() + if f == nil { + f = map[string]string{} + } + f[key] = value + r.SetUnexportedFields(f) + }) +} + +func (d *TestResourceUnexportedFieldsSpecDie) TemplateDie(fn func(d *diecorev1.PodTemplateSpecDie)) *TestResourceUnexportedFieldsSpecDie { + return d.DieStamp(func(r *resources.TestResourceUnexportedFieldsSpec) { + d := diecorev1.PodTemplateSpecBlank.DieImmutable(false).DieFeed(r.Template) + fn(d) + r.Template = d.DieRelease() + }) +} + +// +die:ignore={unexportedFields} +type _ = resources.TestResourceUnexportedFieldsStatus + +func (d *TestResourceUnexportedFieldsStatusDie) ConditionsDie(conditions ...*diemetav1.ConditionDie) *TestResourceUnexportedFieldsStatusDie { + return d.DieStamp(func(r *resources.TestResourceUnexportedFieldsStatus) { + r.Conditions = make([]metav1.Condition, len(conditions)) + for i := range conditions { + r.Conditions[i] = conditions[i].DieRelease() + } + }) +} + +func (d *TestResourceUnexportedFieldsStatusDie) AddField(key, value string) *TestResourceUnexportedFieldsStatusDie { + return d.DieStamp(func(r *resources.TestResourceUnexportedFieldsStatus) { + if r.Fields == nil { + r.Fields = map[string]string{} + } + r.Fields[key] = value + }) +} + +func (d *TestResourceUnexportedFieldsStatusDie) AddUnexportedField(key, value string) *TestResourceUnexportedFieldsStatusDie { + return d.DieStamp(func(r *resources.TestResourceUnexportedFieldsStatus) { + f := r.GetUnexportedFields() + if f == nil { + f = map[string]string{} + } + f[key] = value + r.SetUnexportedFields(f) + }) +} diff --git a/internal/resources/dies/zz_generated.die.go b/internal/resources/dies/zz_generated.die.go index 1bfb3e3..a5eb1ba 100644 --- a/internal/resources/dies/zz_generated.die.go +++ b/internal/resources/dies/zz_generated.die.go @@ -2157,3 +2157,682 @@ func (d *TestDuckSpecDie) Fields(v map[string]string) *TestDuckSpecDie { r.Fields = v }) } + +var TestResourceUnexportedFieldsBlank = (&TestResourceUnexportedFieldsDie{}).DieFeed(resources.TestResourceUnexportedFields{}) + +type TestResourceUnexportedFieldsDie struct { + v1.FrozenObjectMeta + mutable bool + r resources.TestResourceUnexportedFields +} + +// DieImmutable returns a new die for the current die's state that is either mutable (`false`) or immutable (`true`). +func (d *TestResourceUnexportedFieldsDie) DieImmutable(immutable bool) *TestResourceUnexportedFieldsDie { + if d.mutable == !immutable { + return d + } + d = d.DeepCopy() + d.mutable = !immutable + return d +} + +// DieFeed returns a new die with the provided resource. +func (d *TestResourceUnexportedFieldsDie) DieFeed(r resources.TestResourceUnexportedFields) *TestResourceUnexportedFieldsDie { + if d.mutable { + d.FrozenObjectMeta = v1.FreezeObjectMeta(r.ObjectMeta) + d.r = r + return d + } + return &TestResourceUnexportedFieldsDie{ + FrozenObjectMeta: v1.FreezeObjectMeta(r.ObjectMeta), + mutable: d.mutable, + r: r, + } +} + +// DieFeedPtr returns a new die with the provided resource pointer. If the resource is nil, the empty value is used instead. +func (d *TestResourceUnexportedFieldsDie) DieFeedPtr(r *resources.TestResourceUnexportedFields) *TestResourceUnexportedFieldsDie { + if r == nil { + r = &resources.TestResourceUnexportedFields{} + } + return d.DieFeed(*r) +} + +// DieFeedJSON returns a new die with the provided JSON. Panics on error. +func (d *TestResourceUnexportedFieldsDie) DieFeedJSON(j []byte) *TestResourceUnexportedFieldsDie { + r := resources.TestResourceUnexportedFields{} + if err := json.Unmarshal(j, &r); err != nil { + panic(err) + } + return d.DieFeed(r) +} + +// DieFeedYAML returns a new die with the provided YAML. Panics on error. +func (d *TestResourceUnexportedFieldsDie) DieFeedYAML(y []byte) *TestResourceUnexportedFieldsDie { + r := resources.TestResourceUnexportedFields{} + if err := yaml.Unmarshal(y, &r); err != nil { + panic(err) + } + return d.DieFeed(r) +} + +// DieFeedYAMLFile returns a new die loading YAML from a file path. Panics on error. +func (d *TestResourceUnexportedFieldsDie) DieFeedYAMLFile(name string) *TestResourceUnexportedFieldsDie { + y, err := osx.ReadFile(name) + if err != nil { + panic(err) + } + return d.DieFeedYAML(y) +} + +// DieFeedRawExtension returns the resource managed by the die as an raw extension. Panics on error. +func (d *TestResourceUnexportedFieldsDie) DieFeedRawExtension(raw runtime.RawExtension) *TestResourceUnexportedFieldsDie { + j, err := json.Marshal(raw) + if err != nil { + panic(err) + } + return d.DieFeedJSON(j) +} + +// DieRelease returns the resource managed by the die. +func (d *TestResourceUnexportedFieldsDie) DieRelease() resources.TestResourceUnexportedFields { + if d.mutable { + return d.r + } + return *d.r.DeepCopy() +} + +// DieReleasePtr returns a pointer to the resource managed by the die. +func (d *TestResourceUnexportedFieldsDie) DieReleasePtr() *resources.TestResourceUnexportedFields { + r := d.DieRelease() + return &r +} + +// DieReleaseUnstructured returns the resource managed by the die as an unstructured object. Panics on error. +func (d *TestResourceUnexportedFieldsDie) DieReleaseUnstructured() *unstructured.Unstructured { + r := d.DieReleasePtr() + u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(r) + if err != nil { + panic(err) + } + return &unstructured.Unstructured{ + Object: u, + } +} + +// DieReleaseJSON returns the resource managed by the die as JSON. Panics on error. +func (d *TestResourceUnexportedFieldsDie) DieReleaseJSON() []byte { + r := d.DieReleasePtr() + j, err := json.Marshal(r) + if err != nil { + panic(err) + } + return j +} + +// DieReleaseYAML returns the resource managed by the die as YAML. Panics on error. +func (d *TestResourceUnexportedFieldsDie) DieReleaseYAML() []byte { + r := d.DieReleasePtr() + y, err := yaml.Marshal(r) + if err != nil { + panic(err) + } + return y +} + +// DieReleaseRawExtension returns the resource managed by the die as an raw extension. Panics on error. +func (d *TestResourceUnexportedFieldsDie) DieReleaseRawExtension() runtime.RawExtension { + j := d.DieReleaseJSON() + raw := runtime.RawExtension{} + if err := json.Unmarshal(j, &raw); err != nil { + panic(err) + } + return raw +} + +// DieStamp returns a new die with the resource passed to the callback function. The resource is mutable. +func (d *TestResourceUnexportedFieldsDie) DieStamp(fn func(r *resources.TestResourceUnexportedFields)) *TestResourceUnexportedFieldsDie { + r := d.DieRelease() + fn(&r) + return d.DieFeed(r) +} + +// Experimental: DieStampAt uses a JSON path (http://goessner.net/articles/JsonPath/) expression to stamp portions of the resource. The callback is invoked with each JSON path match. Panics if the callback function does not accept a single argument of the same type or a pointer to that type as found on the resource at the target location. +// +// Future iterations will improve type coercion from the resource to the callback argument. +func (d *TestResourceUnexportedFieldsDie) DieStampAt(jp string, fn interface{}) *TestResourceUnexportedFieldsDie { + return d.DieStamp(func(r *resources.TestResourceUnexportedFields) { + if ni := reflectx.ValueOf(fn).Type().NumIn(); ni != 1 { + panic(fmtx.Errorf("callback function must have 1 input parameters, found %d", ni)) + } + if no := reflectx.ValueOf(fn).Type().NumOut(); no != 0 { + panic(fmtx.Errorf("callback function must have 0 output parameters, found %d", no)) + } + + cp := jsonpath.New("") + if err := cp.Parse(fmtx.Sprintf("{%s}", jp)); err != nil { + panic(err) + } + cr, err := cp.FindResults(r) + if err != nil { + // errors are expected if a path is not found + return + } + for _, cv := range cr[0] { + arg0t := reflectx.ValueOf(fn).Type().In(0) + + var args []reflectx.Value + if cv.Type().AssignableTo(arg0t) { + args = []reflectx.Value{cv} + } else if cv.CanAddr() && cv.Addr().Type().AssignableTo(arg0t) { + args = []reflectx.Value{cv.Addr()} + } else { + panic(fmtx.Errorf("callback function must accept value of type %q, found type %q", cv.Type(), arg0t)) + } + + reflectx.ValueOf(fn).Call(args) + } + }) +} + +// DieWith returns a new die after passing the current die to the callback function. The passed die is mutable. +func (d *TestResourceUnexportedFieldsDie) DieWith(fns ...func(d *TestResourceUnexportedFieldsDie)) *TestResourceUnexportedFieldsDie { + nd := TestResourceUnexportedFieldsBlank.DieFeed(d.DieRelease()).DieImmutable(false) + for _, fn := range fns { + if fn != nil { + fn(nd) + } + } + return d.DieFeed(nd.DieRelease()) +} + +// DeepCopy returns a new die with equivalent state. Useful for snapshotting a mutable die. +func (d *TestResourceUnexportedFieldsDie) DeepCopy() *TestResourceUnexportedFieldsDie { + r := *d.r.DeepCopy() + return &TestResourceUnexportedFieldsDie{ + FrozenObjectMeta: v1.FreezeObjectMeta(r.ObjectMeta), + mutable: d.mutable, + r: r, + } +} + +var _ runtime.Object = (*TestResourceUnexportedFieldsDie)(nil) + +func (d *TestResourceUnexportedFieldsDie) DeepCopyObject() runtime.Object { + return d.r.DeepCopy() +} + +func (d *TestResourceUnexportedFieldsDie) GetObjectKind() schema.ObjectKind { + r := d.DieRelease() + return r.GetObjectKind() +} + +func (d *TestResourceUnexportedFieldsDie) MarshalJSON() ([]byte, error) { + return json.Marshal(d.r) +} + +func (d *TestResourceUnexportedFieldsDie) UnmarshalJSON(b []byte) error { + if d == TestResourceUnexportedFieldsBlank { + return fmtx.Errorf("cannot unmarshal into the blank die, create a copy first") + } + if !d.mutable { + return fmtx.Errorf("cannot unmarshal into immutable dies, create a mutable version first") + } + r := &resources.TestResourceUnexportedFields{} + err := json.Unmarshal(b, r) + *d = *d.DieFeed(*r) + return err +} + +// APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources +func (d *TestResourceUnexportedFieldsDie) APIVersion(v string) *TestResourceUnexportedFieldsDie { + return d.DieStamp(func(r *resources.TestResourceUnexportedFields) { + r.APIVersion = v + }) +} + +// Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds +func (d *TestResourceUnexportedFieldsDie) Kind(v string) *TestResourceUnexportedFieldsDie { + return d.DieStamp(func(r *resources.TestResourceUnexportedFields) { + r.Kind = v + }) +} + +// MetadataDie stamps the resource's ObjectMeta field with a mutable die. +func (d *TestResourceUnexportedFieldsDie) MetadataDie(fn func(d *v1.ObjectMetaDie)) *TestResourceUnexportedFieldsDie { + return d.DieStamp(func(r *resources.TestResourceUnexportedFields) { + d := v1.ObjectMetaBlank.DieImmutable(false).DieFeed(r.ObjectMeta) + fn(d) + r.ObjectMeta = d.DieRelease() + }) +} + +// SpecDie stamps the resource's spec field with a mutable die. +func (d *TestResourceUnexportedFieldsDie) SpecDie(fn func(d *TestResourceUnexportedFieldsSpecDie)) *TestResourceUnexportedFieldsDie { + return d.DieStamp(func(r *resources.TestResourceUnexportedFields) { + d := TestResourceUnexportedFieldsSpecBlank.DieImmutable(false).DieFeed(r.Spec) + fn(d) + r.Spec = d.DieRelease() + }) +} + +// StatusDie stamps the resource's status field with a mutable die. +func (d *TestResourceUnexportedFieldsDie) StatusDie(fn func(d *TestResourceUnexportedFieldsStatusDie)) *TestResourceUnexportedFieldsDie { + return d.DieStamp(func(r *resources.TestResourceUnexportedFields) { + d := TestResourceUnexportedFieldsStatusBlank.DieImmutable(false).DieFeed(r.Status) + fn(d) + r.Status = d.DieRelease() + }) +} + +func (d *TestResourceUnexportedFieldsDie) Spec(v resources.TestResourceUnexportedFieldsSpec) *TestResourceUnexportedFieldsDie { + return d.DieStamp(func(r *resources.TestResourceUnexportedFields) { + r.Spec = v + }) +} + +func (d *TestResourceUnexportedFieldsDie) Status(v resources.TestResourceUnexportedFieldsStatus) *TestResourceUnexportedFieldsDie { + return d.DieStamp(func(r *resources.TestResourceUnexportedFields) { + r.Status = v + }) +} + +var TestResourceUnexportedFieldsSpecBlank = (&TestResourceUnexportedFieldsSpecDie{}).DieFeed(resources.TestResourceUnexportedFieldsSpec{}) + +type TestResourceUnexportedFieldsSpecDie struct { + mutable bool + r resources.TestResourceUnexportedFieldsSpec +} + +// DieImmutable returns a new die for the current die's state that is either mutable (`false`) or immutable (`true`). +func (d *TestResourceUnexportedFieldsSpecDie) DieImmutable(immutable bool) *TestResourceUnexportedFieldsSpecDie { + if d.mutable == !immutable { + return d + } + d = d.DeepCopy() + d.mutable = !immutable + return d +} + +// DieFeed returns a new die with the provided resource. +func (d *TestResourceUnexportedFieldsSpecDie) DieFeed(r resources.TestResourceUnexportedFieldsSpec) *TestResourceUnexportedFieldsSpecDie { + if d.mutable { + d.r = r + return d + } + return &TestResourceUnexportedFieldsSpecDie{ + mutable: d.mutable, + r: r, + } +} + +// DieFeedPtr returns a new die with the provided resource pointer. If the resource is nil, the empty value is used instead. +func (d *TestResourceUnexportedFieldsSpecDie) DieFeedPtr(r *resources.TestResourceUnexportedFieldsSpec) *TestResourceUnexportedFieldsSpecDie { + if r == nil { + r = &resources.TestResourceUnexportedFieldsSpec{} + } + return d.DieFeed(*r) +} + +// DieFeedJSON returns a new die with the provided JSON. Panics on error. +func (d *TestResourceUnexportedFieldsSpecDie) DieFeedJSON(j []byte) *TestResourceUnexportedFieldsSpecDie { + r := resources.TestResourceUnexportedFieldsSpec{} + if err := json.Unmarshal(j, &r); err != nil { + panic(err) + } + return d.DieFeed(r) +} + +// DieFeedYAML returns a new die with the provided YAML. Panics on error. +func (d *TestResourceUnexportedFieldsSpecDie) DieFeedYAML(y []byte) *TestResourceUnexportedFieldsSpecDie { + r := resources.TestResourceUnexportedFieldsSpec{} + if err := yaml.Unmarshal(y, &r); err != nil { + panic(err) + } + return d.DieFeed(r) +} + +// DieFeedYAMLFile returns a new die loading YAML from a file path. Panics on error. +func (d *TestResourceUnexportedFieldsSpecDie) DieFeedYAMLFile(name string) *TestResourceUnexportedFieldsSpecDie { + y, err := osx.ReadFile(name) + if err != nil { + panic(err) + } + return d.DieFeedYAML(y) +} + +// DieFeedRawExtension returns the resource managed by the die as an raw extension. Panics on error. +func (d *TestResourceUnexportedFieldsSpecDie) DieFeedRawExtension(raw runtime.RawExtension) *TestResourceUnexportedFieldsSpecDie { + j, err := json.Marshal(raw) + if err != nil { + panic(err) + } + return d.DieFeedJSON(j) +} + +// DieRelease returns the resource managed by the die. +func (d *TestResourceUnexportedFieldsSpecDie) DieRelease() resources.TestResourceUnexportedFieldsSpec { + if d.mutable { + return d.r + } + return *d.r.DeepCopy() +} + +// DieReleasePtr returns a pointer to the resource managed by the die. +func (d *TestResourceUnexportedFieldsSpecDie) DieReleasePtr() *resources.TestResourceUnexportedFieldsSpec { + r := d.DieRelease() + return &r +} + +// DieReleaseJSON returns the resource managed by the die as JSON. Panics on error. +func (d *TestResourceUnexportedFieldsSpecDie) DieReleaseJSON() []byte { + r := d.DieReleasePtr() + j, err := json.Marshal(r) + if err != nil { + panic(err) + } + return j +} + +// DieReleaseYAML returns the resource managed by the die as YAML. Panics on error. +func (d *TestResourceUnexportedFieldsSpecDie) DieReleaseYAML() []byte { + r := d.DieReleasePtr() + y, err := yaml.Marshal(r) + if err != nil { + panic(err) + } + return y +} + +// DieReleaseRawExtension returns the resource managed by the die as an raw extension. Panics on error. +func (d *TestResourceUnexportedFieldsSpecDie) DieReleaseRawExtension() runtime.RawExtension { + j := d.DieReleaseJSON() + raw := runtime.RawExtension{} + if err := json.Unmarshal(j, &raw); err != nil { + panic(err) + } + return raw +} + +// DieStamp returns a new die with the resource passed to the callback function. The resource is mutable. +func (d *TestResourceUnexportedFieldsSpecDie) DieStamp(fn func(r *resources.TestResourceUnexportedFieldsSpec)) *TestResourceUnexportedFieldsSpecDie { + r := d.DieRelease() + fn(&r) + return d.DieFeed(r) +} + +// Experimental: DieStampAt uses a JSON path (http://goessner.net/articles/JsonPath/) expression to stamp portions of the resource. The callback is invoked with each JSON path match. Panics if the callback function does not accept a single argument of the same type or a pointer to that type as found on the resource at the target location. +// +// Future iterations will improve type coercion from the resource to the callback argument. +func (d *TestResourceUnexportedFieldsSpecDie) DieStampAt(jp string, fn interface{}) *TestResourceUnexportedFieldsSpecDie { + return d.DieStamp(func(r *resources.TestResourceUnexportedFieldsSpec) { + if ni := reflectx.ValueOf(fn).Type().NumIn(); ni != 1 { + panic(fmtx.Errorf("callback function must have 1 input parameters, found %d", ni)) + } + if no := reflectx.ValueOf(fn).Type().NumOut(); no != 0 { + panic(fmtx.Errorf("callback function must have 0 output parameters, found %d", no)) + } + + cp := jsonpath.New("") + if err := cp.Parse(fmtx.Sprintf("{%s}", jp)); err != nil { + panic(err) + } + cr, err := cp.FindResults(r) + if err != nil { + // errors are expected if a path is not found + return + } + for _, cv := range cr[0] { + arg0t := reflectx.ValueOf(fn).Type().In(0) + + var args []reflectx.Value + if cv.Type().AssignableTo(arg0t) { + args = []reflectx.Value{cv} + } else if cv.CanAddr() && cv.Addr().Type().AssignableTo(arg0t) { + args = []reflectx.Value{cv.Addr()} + } else { + panic(fmtx.Errorf("callback function must accept value of type %q, found type %q", cv.Type(), arg0t)) + } + + reflectx.ValueOf(fn).Call(args) + } + }) +} + +// DieWith returns a new die after passing the current die to the callback function. The passed die is mutable. +func (d *TestResourceUnexportedFieldsSpecDie) DieWith(fns ...func(d *TestResourceUnexportedFieldsSpecDie)) *TestResourceUnexportedFieldsSpecDie { + nd := TestResourceUnexportedFieldsSpecBlank.DieFeed(d.DieRelease()).DieImmutable(false) + for _, fn := range fns { + if fn != nil { + fn(nd) + } + } + return d.DieFeed(nd.DieRelease()) +} + +// DeepCopy returns a new die with equivalent state. Useful for snapshotting a mutable die. +func (d *TestResourceUnexportedFieldsSpecDie) DeepCopy() *TestResourceUnexportedFieldsSpecDie { + r := *d.r.DeepCopy() + return &TestResourceUnexportedFieldsSpecDie{ + mutable: d.mutable, + r: r, + } +} + +func (d *TestResourceUnexportedFieldsSpecDie) Fields(v map[string]string) *TestResourceUnexportedFieldsSpecDie { + return d.DieStamp(func(r *resources.TestResourceUnexportedFieldsSpec) { + r.Fields = v + }) +} + +func (d *TestResourceUnexportedFieldsSpecDie) Template(v corev1.PodTemplateSpec) *TestResourceUnexportedFieldsSpecDie { + return d.DieStamp(func(r *resources.TestResourceUnexportedFieldsSpec) { + r.Template = v + }) +} + +func (d *TestResourceUnexportedFieldsSpecDie) ErrOnMarshal(v bool) *TestResourceUnexportedFieldsSpecDie { + return d.DieStamp(func(r *resources.TestResourceUnexportedFieldsSpec) { + r.ErrOnMarshal = v + }) +} + +func (d *TestResourceUnexportedFieldsSpecDie) ErrOnUnmarshal(v bool) *TestResourceUnexportedFieldsSpecDie { + return d.DieStamp(func(r *resources.TestResourceUnexportedFieldsSpec) { + r.ErrOnUnmarshal = v + }) +} + +var TestResourceUnexportedFieldsStatusBlank = (&TestResourceUnexportedFieldsStatusDie{}).DieFeed(resources.TestResourceUnexportedFieldsStatus{}) + +type TestResourceUnexportedFieldsStatusDie struct { + mutable bool + r resources.TestResourceUnexportedFieldsStatus +} + +// DieImmutable returns a new die for the current die's state that is either mutable (`false`) or immutable (`true`). +func (d *TestResourceUnexportedFieldsStatusDie) DieImmutable(immutable bool) *TestResourceUnexportedFieldsStatusDie { + if d.mutable == !immutable { + return d + } + d = d.DeepCopy() + d.mutable = !immutable + return d +} + +// DieFeed returns a new die with the provided resource. +func (d *TestResourceUnexportedFieldsStatusDie) DieFeed(r resources.TestResourceUnexportedFieldsStatus) *TestResourceUnexportedFieldsStatusDie { + if d.mutable { + d.r = r + return d + } + return &TestResourceUnexportedFieldsStatusDie{ + mutable: d.mutable, + r: r, + } +} + +// DieFeedPtr returns a new die with the provided resource pointer. If the resource is nil, the empty value is used instead. +func (d *TestResourceUnexportedFieldsStatusDie) DieFeedPtr(r *resources.TestResourceUnexportedFieldsStatus) *TestResourceUnexportedFieldsStatusDie { + if r == nil { + r = &resources.TestResourceUnexportedFieldsStatus{} + } + return d.DieFeed(*r) +} + +// DieFeedJSON returns a new die with the provided JSON. Panics on error. +func (d *TestResourceUnexportedFieldsStatusDie) DieFeedJSON(j []byte) *TestResourceUnexportedFieldsStatusDie { + r := resources.TestResourceUnexportedFieldsStatus{} + if err := json.Unmarshal(j, &r); err != nil { + panic(err) + } + return d.DieFeed(r) +} + +// DieFeedYAML returns a new die with the provided YAML. Panics on error. +func (d *TestResourceUnexportedFieldsStatusDie) DieFeedYAML(y []byte) *TestResourceUnexportedFieldsStatusDie { + r := resources.TestResourceUnexportedFieldsStatus{} + if err := yaml.Unmarshal(y, &r); err != nil { + panic(err) + } + return d.DieFeed(r) +} + +// DieFeedYAMLFile returns a new die loading YAML from a file path. Panics on error. +func (d *TestResourceUnexportedFieldsStatusDie) DieFeedYAMLFile(name string) *TestResourceUnexportedFieldsStatusDie { + y, err := osx.ReadFile(name) + if err != nil { + panic(err) + } + return d.DieFeedYAML(y) +} + +// DieFeedRawExtension returns the resource managed by the die as an raw extension. Panics on error. +func (d *TestResourceUnexportedFieldsStatusDie) DieFeedRawExtension(raw runtime.RawExtension) *TestResourceUnexportedFieldsStatusDie { + j, err := json.Marshal(raw) + if err != nil { + panic(err) + } + return d.DieFeedJSON(j) +} + +// DieRelease returns the resource managed by the die. +func (d *TestResourceUnexportedFieldsStatusDie) DieRelease() resources.TestResourceUnexportedFieldsStatus { + if d.mutable { + return d.r + } + return *d.r.DeepCopy() +} + +// DieReleasePtr returns a pointer to the resource managed by the die. +func (d *TestResourceUnexportedFieldsStatusDie) DieReleasePtr() *resources.TestResourceUnexportedFieldsStatus { + r := d.DieRelease() + return &r +} + +// DieReleaseJSON returns the resource managed by the die as JSON. Panics on error. +func (d *TestResourceUnexportedFieldsStatusDie) DieReleaseJSON() []byte { + r := d.DieReleasePtr() + j, err := json.Marshal(r) + if err != nil { + panic(err) + } + return j +} + +// DieReleaseYAML returns the resource managed by the die as YAML. Panics on error. +func (d *TestResourceUnexportedFieldsStatusDie) DieReleaseYAML() []byte { + r := d.DieReleasePtr() + y, err := yaml.Marshal(r) + if err != nil { + panic(err) + } + return y +} + +// DieReleaseRawExtension returns the resource managed by the die as an raw extension. Panics on error. +func (d *TestResourceUnexportedFieldsStatusDie) DieReleaseRawExtension() runtime.RawExtension { + j := d.DieReleaseJSON() + raw := runtime.RawExtension{} + if err := json.Unmarshal(j, &raw); err != nil { + panic(err) + } + return raw +} + +// DieStamp returns a new die with the resource passed to the callback function. The resource is mutable. +func (d *TestResourceUnexportedFieldsStatusDie) DieStamp(fn func(r *resources.TestResourceUnexportedFieldsStatus)) *TestResourceUnexportedFieldsStatusDie { + r := d.DieRelease() + fn(&r) + return d.DieFeed(r) +} + +// Experimental: DieStampAt uses a JSON path (http://goessner.net/articles/JsonPath/) expression to stamp portions of the resource. The callback is invoked with each JSON path match. Panics if the callback function does not accept a single argument of the same type or a pointer to that type as found on the resource at the target location. +// +// Future iterations will improve type coercion from the resource to the callback argument. +func (d *TestResourceUnexportedFieldsStatusDie) DieStampAt(jp string, fn interface{}) *TestResourceUnexportedFieldsStatusDie { + return d.DieStamp(func(r *resources.TestResourceUnexportedFieldsStatus) { + if ni := reflectx.ValueOf(fn).Type().NumIn(); ni != 1 { + panic(fmtx.Errorf("callback function must have 1 input parameters, found %d", ni)) + } + if no := reflectx.ValueOf(fn).Type().NumOut(); no != 0 { + panic(fmtx.Errorf("callback function must have 0 output parameters, found %d", no)) + } + + cp := jsonpath.New("") + if err := cp.Parse(fmtx.Sprintf("{%s}", jp)); err != nil { + panic(err) + } + cr, err := cp.FindResults(r) + if err != nil { + // errors are expected if a path is not found + return + } + for _, cv := range cr[0] { + arg0t := reflectx.ValueOf(fn).Type().In(0) + + var args []reflectx.Value + if cv.Type().AssignableTo(arg0t) { + args = []reflectx.Value{cv} + } else if cv.CanAddr() && cv.Addr().Type().AssignableTo(arg0t) { + args = []reflectx.Value{cv.Addr()} + } else { + panic(fmtx.Errorf("callback function must accept value of type %q, found type %q", cv.Type(), arg0t)) + } + + reflectx.ValueOf(fn).Call(args) + } + }) +} + +// DieWith returns a new die after passing the current die to the callback function. The passed die is mutable. +func (d *TestResourceUnexportedFieldsStatusDie) DieWith(fns ...func(d *TestResourceUnexportedFieldsStatusDie)) *TestResourceUnexportedFieldsStatusDie { + nd := TestResourceUnexportedFieldsStatusBlank.DieFeed(d.DieRelease()).DieImmutable(false) + for _, fn := range fns { + if fn != nil { + fn(nd) + } + } + return d.DieFeed(nd.DieRelease()) +} + +// DeepCopy returns a new die with equivalent state. Useful for snapshotting a mutable die. +func (d *TestResourceUnexportedFieldsStatusDie) DeepCopy() *TestResourceUnexportedFieldsStatusDie { + r := *d.r.DeepCopy() + return &TestResourceUnexportedFieldsStatusDie{ + mutable: d.mutable, + r: r, + } +} + +func (d *TestResourceUnexportedFieldsStatusDie) Status(v apis.Status) *TestResourceUnexportedFieldsStatusDie { + return d.DieStamp(func(r *resources.TestResourceUnexportedFieldsStatus) { + r.Status = v + }) +} + +func (d *TestResourceUnexportedFieldsStatusDie) Fields(v map[string]string) *TestResourceUnexportedFieldsStatusDie { + return d.DieStamp(func(r *resources.TestResourceUnexportedFieldsStatus) { + r.Fields = v + }) +} diff --git a/internal/resources/dies/zz_generated.die_test.go b/internal/resources/dies/zz_generated.die_test.go index 6b5b8c3..7fa28c8 100644 --- a/internal/resources/dies/zz_generated.die_test.go +++ b/internal/resources/dies/zz_generated.die_test.go @@ -95,3 +95,30 @@ func TestTestDuckSpecDie_MissingMethods(t *testingx.T) { t.Errorf("found missing fields for TestDuckSpecDie: %s", diff.List()) } } + +func TestTestResourceUnexportedFieldsDie_MissingMethods(t *testingx.T) { + die := TestResourceUnexportedFieldsBlank + ignore := []string{"TypeMeta", "ObjectMeta"} + diff := testing.DieFieldDiff(die).Delete(ignore...) + if diff.Len() != 0 { + t.Errorf("found missing fields for TestResourceUnexportedFieldsDie: %s", diff.List()) + } +} + +func TestTestResourceUnexportedFieldsSpecDie_MissingMethods(t *testingx.T) { + die := TestResourceUnexportedFieldsSpecBlank + ignore := []string{"unexportedFields"} + diff := testing.DieFieldDiff(die).Delete(ignore...) + if diff.Len() != 0 { + t.Errorf("found missing fields for TestResourceUnexportedFieldsSpecDie: %s", diff.List()) + } +} + +func TestTestResourceUnexportedFieldsStatusDie_MissingMethods(t *testingx.T) { + die := TestResourceUnexportedFieldsStatusBlank + ignore := []string{"unexportedFields"} + diff := testing.DieFieldDiff(die).Delete(ignore...) + if diff.Len() != 0 { + t.Errorf("found missing fields for TestResourceUnexportedFieldsStatusDie: %s", diff.List()) + } +} diff --git a/internal/resources/resource_with_unexported_fields.go b/internal/resources/resource_with_unexported_fields.go new file mode 100644 index 0000000..27816e1 --- /dev/null +++ b/internal/resources/resource_with_unexported_fields.go @@ -0,0 +1,170 @@ +/* +Copyright 2022 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package resources + +import ( + "encoding/json" + "fmt" + + "github.com/vmware-labs/reconciler-runtime/apis" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +var ( + _ webhook.Defaulter = &TestResourceUnexportedFields{} + _ webhook.Validator = &TestResourceUnexportedFields{} + _ client.Object = &TestResourceUnexportedFields{} +) + +// +kubebuilder:object:root=true +// +genclient + +type TestResourceUnexportedFields struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec TestResourceUnexportedFieldsSpec `json:"spec"` + Status TestResourceUnexportedFieldsStatus `json:"status"` +} + +func (r *TestResourceUnexportedFields) Default() { + if r.Spec.Fields == nil { + r.Spec.Fields = map[string]string{} + } + r.Spec.Fields["Defaulter"] = "ran" +} + +func (r *TestResourceUnexportedFields) ValidateCreate() (admission.Warnings, error) { + return nil, r.validate().ToAggregate() +} + +func (r *TestResourceUnexportedFields) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + return nil, r.validate().ToAggregate() +} + +func (r *TestResourceUnexportedFields) ValidateDelete() (admission.Warnings, error) { + return nil, nil +} + +func (r *TestResourceUnexportedFields) validate() field.ErrorList { + errs := field.ErrorList{} + + if r.Spec.Fields != nil { + if _, ok := r.Spec.Fields["invalid"]; ok { + field.Invalid(field.NewPath("spec", "fields", "invalid"), r.Spec.Fields["invalid"], "") + } + } + + return errs +} + +func (r *TestResourceUnexportedFields) CopyUnexportedFields() { + r.Status.unexportedFields = r.Spec.unexportedFields +} + +// +kubebuilder:object:generate=true +type TestResourceUnexportedFieldsSpec struct { + Fields map[string]string `json:"fields,omitempty"` + unexportedFields map[string]string + Template corev1.PodTemplateSpec `json:"template,omitempty"` + + ErrOnMarshal bool `json:"errOnMarhsal,omitempty"` + ErrOnUnmarshal bool `json:"errOnUnmarhsal,omitempty"` +} + +func (r *TestResourceUnexportedFieldsSpec) GetUnexportedFields() map[string]string { + return r.unexportedFields +} + +func (r *TestResourceUnexportedFieldsSpec) SetUnexportedFields(f map[string]string) { + r.unexportedFields = f +} + +func (r *TestResourceUnexportedFieldsSpec) AddUnexportedFields(key, value string) { + if r.unexportedFields == nil { + r.unexportedFields = map[string]string{} + } + r.unexportedFields[key] = value +} + +func (r *TestResourceUnexportedFieldsSpec) MarshalJSON() ([]byte, error) { + if r.ErrOnMarshal { + return nil, fmt.Errorf("ErrOnMarshal true") + } + return json.Marshal(&struct { + Fields map[string]string `json:"fields,omitempty"` + Template corev1.PodTemplateSpec `json:"template,omitempty"` + ErrOnMarshal bool `json:"errOnMarshal,omitempty"` + ErrOnUnmarshal bool `json:"errOnUnmarshal,omitempty"` + }{ + Fields: r.Fields, + Template: r.Template, + ErrOnMarshal: r.ErrOnMarshal, + ErrOnUnmarshal: r.ErrOnUnmarshal, + }) +} + +func (r *TestResourceUnexportedFieldsSpec) UnmarshalJSON(data []byte) error { + type alias struct { + Fields map[string]string `json:"fields,omitempty"` + Template corev1.PodTemplateSpec `json:"template,omitempty"` + ErrOnMarshal bool `json:"errOnMarshal,omitempty"` + ErrOnUnmarshal bool `json:"errOnUnmarshal,omitempty"` + } + a := &alias{} + if err := json.Unmarshal(data, a); err != nil { + return err + } + r.Fields = a.Fields + r.Template = a.Template + r.ErrOnMarshal = a.ErrOnMarshal + r.ErrOnUnmarshal = a.ErrOnUnmarshal + if r.ErrOnUnmarshal { + return fmt.Errorf("ErrOnUnmarshal true") + } + return nil +} + +// +kubebuilder:object:generate=true +type TestResourceUnexportedFieldsStatus struct { + apis.Status `json:",inline"` + Fields map[string]string `json:"fields,omitempty"` + unexportedFields map[string]string +} + +func (r *TestResourceUnexportedFieldsStatus) GetUnexportedFields() map[string]string { + return r.unexportedFields +} + +func (r *TestResourceUnexportedFieldsStatus) SetUnexportedFields(f map[string]string) { + r.unexportedFields = f +} + +func (r *TestResourceUnexportedFieldsStatus) AddUnexportedFields(key, value string) { + if r.unexportedFields == nil { + r.unexportedFields = map[string]string{} + } + r.unexportedFields[key] = value +} + +// +kubebuilder:object:root=true + +type TestResourceUnexportedFieldsList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + Items []TestResourceUnexportedFields `json:"items"` +} + +func init() { + SchemeBuilder.Register(&TestResourceUnexportedFields{}, &TestResourceUnexportedFieldsList{}) +} diff --git a/internal/resources/zz_generated.deepcopy.go b/internal/resources/zz_generated.deepcopy.go index 4899372..9415212 100644 --- a/internal/resources/zz_generated.deepcopy.go +++ b/internal/resources/zz_generated.deepcopy.go @@ -459,3 +459,122 @@ func (in *TestResourceStatus) DeepCopy() *TestResourceStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceUnexportedFields) DeepCopyInto(out *TestResourceUnexportedFields) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceUnexportedFields. +func (in *TestResourceUnexportedFields) DeepCopy() *TestResourceUnexportedFields { + if in == nil { + return nil + } + out := new(TestResourceUnexportedFields) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestResourceUnexportedFields) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceUnexportedFieldsList) DeepCopyInto(out *TestResourceUnexportedFieldsList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TestResourceUnexportedFields, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceUnexportedFieldsList. +func (in *TestResourceUnexportedFieldsList) DeepCopy() *TestResourceUnexportedFieldsList { + if in == nil { + return nil + } + out := new(TestResourceUnexportedFieldsList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestResourceUnexportedFieldsList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceUnexportedFieldsSpec) DeepCopyInto(out *TestResourceUnexportedFieldsSpec) { + *out = *in + if in.Fields != nil { + in, out := &in.Fields, &out.Fields + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.unexportedFields != nil { + in, out := &in.unexportedFields, &out.unexportedFields + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + in.Template.DeepCopyInto(&out.Template) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceUnexportedFieldsSpec. +func (in *TestResourceUnexportedFieldsSpec) DeepCopy() *TestResourceUnexportedFieldsSpec { + if in == nil { + return nil + } + out := new(TestResourceUnexportedFieldsSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceUnexportedFieldsStatus) DeepCopyInto(out *TestResourceUnexportedFieldsStatus) { + *out = *in + in.Status.DeepCopyInto(&out.Status) + if in.Fields != nil { + in, out := &in.Fields, &out.Fields + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.unexportedFields != nil { + in, out := &in.unexportedFields, &out.unexportedFields + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceUnexportedFieldsStatus. +func (in *TestResourceUnexportedFieldsStatus) DeepCopy() *TestResourceUnexportedFieldsStatus { + if in == nil { + return nil + } + out := new(TestResourceUnexportedFieldsStatus) + in.DeepCopyInto(out) + return out +} diff --git a/reconcilers/child_test.go b/reconcilers/child_test.go index 3532bdf..84f091c 100644 --- a/reconcilers/child_test.go +++ b/reconcilers/child_test.go @@ -2108,3 +2108,232 @@ func TestChildReconciler_Unstructured(t *testing.T) { return rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler[*unstructured.Unstructured])(t, c) }) } + +func TestChildReconciler_UnexportedFields(t *testing.T) { + testNamespace := "test-namespace" + testName := "test-resource" + + now := metav1.NewTime(time.Now().Truncate(time.Second)) + + scheme := runtime.NewScheme() + _ = resources.AddToScheme(scheme) + _ = clientgoscheme.AddToScheme(scheme) + + resource := dies.TestResourceBlank. + MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.Namespace(testNamespace) + d.Name(testName) + }). + StatusDie(func(d *dies.TestResourceStatusDie) { + d.ConditionsDie( + diemetav1.ConditionBlank.Type(apis.ConditionReady).Status(metav1.ConditionUnknown).Reason("Initializing"), + ) + }) + resourceReady := resource. + StatusDie(func(d *dies.TestResourceStatusDie) { + d.ConditionsDie( + diemetav1.ConditionBlank.Type(apis.ConditionReady).Status(metav1.ConditionTrue).Reason("Ready"), + ) + }) + + childCreate := dies.TestResourceUnexportedFieldsBlank. + MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.Namespace(testNamespace) + d.Name(testName) + d.ControlledBy(resource, scheme) + }) + childGiven := childCreate. + MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.CreationTimestamp(now) + }) + + defaultChildReconciler := func(c reconcilers.Config) *reconcilers.ChildReconciler[*resources.TestResource, *resources.TestResourceUnexportedFields, *resources.TestResourceUnexportedFieldsList] { + return &reconcilers.ChildReconciler[*resources.TestResource, *resources.TestResourceUnexportedFields, *resources.TestResourceUnexportedFieldsList]{ + DesiredChild: func(ctx context.Context, parent *resources.TestResource) (*resources.TestResourceUnexportedFields, error) { + if len(parent.Spec.Fields) == 0 { + return nil, nil + } + + return &resources.TestResourceUnexportedFields{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: parent.Namespace, + Name: parent.Name, + }, + Spec: resources.TestResourceUnexportedFieldsSpec{ + Fields: parent.Spec.Fields, + Template: parent.Spec.Template, + ErrOnMarshal: parent.Spec.ErrOnMarshal, + ErrOnUnmarshal: parent.Spec.ErrOnUnmarshal, + }, + }, nil + }, + MergeBeforeUpdate: func(current, desired *resources.TestResourceUnexportedFields) { + current.Spec.Fields = desired.Spec.Fields + current.Spec.Template = desired.Spec.Template + current.Spec.ErrOnMarshal = desired.Spec.ErrOnMarshal + current.Spec.ErrOnUnmarshal = desired.Spec.ErrOnUnmarshal + }, + ReflectChildStatusOnParent: func(ctx context.Context, parent *resources.TestResource, child *resources.TestResourceUnexportedFields, err error) { + if err != nil { + switch { + case apierrs.IsAlreadyExists(err): + name := err.(apierrs.APIStatus).Status().Details.Name + parent.Status.MarkNotReady(ctx, "NameConflict", "%q already exists", name) + case apierrs.IsInvalid(err): + name := err.(apierrs.APIStatus).Status().Details.Name + parent.Status.MarkNotReady(ctx, "InvalidChild", "%q was rejected by the api server", name) + } + return + } + if child == nil { + parent.Status.Fields = nil + parent.Status.MarkReady(ctx) + return + } + parent.Status.Fields = child.Status.Fields + parent.Status.MarkReady(ctx) + }, + } + } + + rts := rtesting.SubReconcilerTests[*resources.TestResource]{ + "child is in sync": { + Resource: resourceReady. + SpecDie(func(d *dies.TestResourceSpecDie) { + d.AddField("foo", "bar") + }). + StatusDie(func(d *dies.TestResourceStatusDie) { + d.AddField("foo", "bar") + }). + DieReleasePtr(), + GivenObjects: []client.Object{ + childGiven. + SpecDie(func(d *dies.TestResourceUnexportedFieldsSpecDie) { + d.AddField("foo", "bar") + }). + StatusDie(func(d *dies.TestResourceUnexportedFieldsStatusDie) { + d.AddField("foo", "bar") + }), + }, + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return defaultChildReconciler(c) + }, + }, + }, + "update status": { + Resource: resourceReady. + SpecDie(func(d *dies.TestResourceSpecDie) { + d.AddField("foo", "bar") + }). + DieReleasePtr(), + GivenObjects: []client.Object{ + childGiven. + SpecDie(func(d *dies.TestResourceUnexportedFieldsSpecDie) { + d.AddField("foo", "bar") + }). + StatusDie(func(d *dies.TestResourceUnexportedFieldsStatusDie) { + d.AddField("foo", "bar") + }), + }, + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return defaultChildReconciler(c) + }, + }, + ExpectResource: resourceReady. + SpecDie(func(d *dies.TestResourceSpecDie) { + d.AddField("foo", "bar") + }). + StatusDie(func(d *dies.TestResourceStatusDie) { + d.AddField("foo", "bar") + }). + DieReleasePtr(), + }, + "create child": { + Resource: resource. + SpecDie(func(d *dies.TestResourceSpecDie) { + d.AddField("foo", "bar") + }). + DieReleasePtr(), + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return defaultChildReconciler(c) + }, + }, + ExpectEvents: []rtesting.Event{ + rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Created", + `Created TestResourceUnexportedFields %q`, testName), + }, + ExpectResource: resourceReady. + SpecDie(func(d *dies.TestResourceSpecDie) { + d.AddField("foo", "bar") + }). + DieReleasePtr(), + ExpectCreates: []client.Object{ + childCreate. + SpecDie(func(d *dies.TestResourceUnexportedFieldsSpecDie) { + d.AddField("foo", "bar") + }), + }, + }, + "update child": { + Resource: resourceReady. + SpecDie(func(d *dies.TestResourceSpecDie) { + d.AddField("foo", "bar") + d.AddField("new", "field") + }). + DieReleasePtr(), + GivenObjects: []client.Object{ + childGiven. + SpecDie(func(d *dies.TestResourceUnexportedFieldsSpecDie) { + d.AddField("foo", "bar") + }), + }, + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return defaultChildReconciler(c) + }, + }, + ExpectEvents: []rtesting.Event{ + rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Updated", + `Updated TestResourceUnexportedFields %q`, testName), + }, + ExpectResource: resourceReady. + SpecDie(func(d *dies.TestResourceSpecDie) { + d.AddField("foo", "bar") + d.AddField("new", "field") + }). + DieReleasePtr(), + ExpectUpdates: []client.Object{ + childGiven. + SpecDie(func(d *dies.TestResourceUnexportedFieldsSpecDie) { + d.AddField("foo", "bar") + d.AddField("new", "field") + }), + }, + }, + "delete child": { + Resource: resourceReady.DieReleasePtr(), + GivenObjects: []client.Object{ + childGiven, + }, + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return defaultChildReconciler(c) + }, + }, + ExpectEvents: []rtesting.Event{ + rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Deleted", + `Deleted TestResourceUnexportedFields %q`, testName), + }, + ExpectDeletes: []rtesting.DeleteRef{ + rtesting.NewDeleteRefFromObject(childGiven, scheme), + }, + }, + } + + rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.SubReconcilerTestCase[*resources.TestResource], c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource])(t, c) + }) +} diff --git a/reconcilers/cmp.go b/reconcilers/cmp.go new file mode 100644 index 0000000..2f6e7a7 --- /dev/null +++ b/reconcilers/cmp.go @@ -0,0 +1,24 @@ +/* +Copyright 2024 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package reconcilers + +import ( + "unicode" + "unicode/utf8" + + "github.com/google/go-cmp/cmp" +) + +// IgnoreAllUnexported is a cmp.Option that ignores unexported fields in all structs +var IgnoreAllUnexported = cmp.FilterPath(func(p cmp.Path) bool { + // from cmp.IgnoreUnexported with type info removed + sf, ok := p.Index(-1).(cmp.StructField) + if !ok { + return false + } + r, _ := utf8.DecodeRuneInString(sf.Name()) + return !unicode.IsUpper(r) +}, cmp.Ignore()) diff --git a/reconcilers/resource.go b/reconcilers/resource.go index 67b8d90..4e0157d 100644 --- a/reconcilers/resource.go +++ b/reconcilers/resource.go @@ -233,7 +233,7 @@ func (r *ResourceReconciler[T]) Reconcile(ctx context.Context, req Request) (Res if !equality.Semantic.DeepEqual(resourceStatus, originalResourceStatus) && resource.GetDeletionTimestamp() == nil { if duck.IsDuck(resource, c.Scheme()) { // patch status - log.Info("patching status", "diff", cmp.Diff(originalResourceStatus, resourceStatus)) + log.Info("patching status", "diff", cmp.Diff(originalResourceStatus, resourceStatus, IgnoreAllUnexported)) if patchErr := c.Status().Patch(ctx, resource, client.MergeFrom(originalResource)); patchErr != nil { if !errors.Is(patchErr, ErrQuiet) { log.Error(patchErr, "unable to patch status") @@ -246,7 +246,7 @@ func (r *ResourceReconciler[T]) Reconcile(ctx context.Context, req Request) (Res "Patched status") } else { // update status - log.Info("updating status", "diff", cmp.Diff(originalResourceStatus, resourceStatus)) + log.Info("updating status", "diff", cmp.Diff(originalResourceStatus, resourceStatus, IgnoreAllUnexported)) if updateErr := c.Status().Update(ctx, resource); updateErr != nil { if !errors.Is(updateErr, ErrQuiet) { log.Error(updateErr, "unable to update status") diff --git a/reconcilers/resource_test.go b/reconcilers/resource_test.go index fc9e95f..25323a5 100644 --- a/reconcilers/resource_test.go +++ b/reconcilers/resource_test.go @@ -1282,3 +1282,124 @@ func TestResourceReconciler(t *testing.T) { } }) } + +func TestResourceReconciler_UnexportedFields(t *testing.T) { + testNamespace := "test-namespace" + testName := "test-resource" + testRequest := reconcilers.Request{ + NamespacedName: types.NamespacedName{Namespace: testNamespace, Name: testName}, + } + + scheme := runtime.NewScheme() + _ = resources.AddToScheme(scheme) + + resource := dies.TestResourceUnexportedFieldsBlank. + MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.Namespace(testNamespace) + d.Name(testName) + }). + StatusDie(func(d *dies.TestResourceUnexportedFieldsStatusDie) { + d.ConditionsDie( + diemetav1.ConditionBlank.Type(apis.ConditionReady).Status(metav1.ConditionUnknown).Reason("Initializing"), + ) + }) + + rts := rtesting.ReconcilerTests{ + "mutated exported and unexported status": { + Request: testRequest, + StatusSubResourceTypes: []client.Object{ + &resources.TestResourceUnexportedFields{}, + }, + GivenObjects: []client.Object{ + resource, + }, + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResourceUnexportedFields] { + return &reconcilers.SyncReconciler[*resources.TestResourceUnexportedFields]{ + Sync: func(ctx context.Context, resource *resources.TestResourceUnexportedFields) error { + if resource.Status.Fields == nil { + resource.Status.Fields = map[string]string{} + } + resource.CopyUnexportedFields() + resource.Status.Fields["Reconciler"] = "ran" + resource.Status.AddUnexportedFields("Reconciler", "ran") + return nil + }, + } + }, + }, + ExpectEvents: []rtesting.Event{ + rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "StatusUpdated", + `Updated status`), + }, + ExpectStatusUpdates: []client.Object{ + resource.StatusDie(func(d *dies.TestResourceUnexportedFieldsStatusDie) { + d.AddField("Reconciler", "ran") + d.AddUnexportedField("Reconciler", "ran") + }), + }, + }, + "mutated unexported status": { + Request: testRequest, + StatusSubResourceTypes: []client.Object{ + &resources.TestResourceUnexportedFields{}, + }, + GivenObjects: []client.Object{ + resource, + }, + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResourceUnexportedFields] { + return &reconcilers.SyncReconciler[*resources.TestResourceUnexportedFields]{ + Sync: func(ctx context.Context, resource *resources.TestResourceUnexportedFields) error { + if resource.Status.Fields == nil { + resource.Status.Fields = map[string]string{} + } + resource.CopyUnexportedFields() + resource.Status.AddUnexportedFields("Reconciler", "ran") + return nil + }, + } + }, + }, + ExpectEvents: []rtesting.Event{ + rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "StatusUpdated", + `Updated status`), + }, + ExpectStatusUpdates: []client.Object{ + resource.StatusDie(func(d *dies.TestResourceUnexportedFieldsStatusDie) { + d.AddUnexportedField("Reconciler", "ran") + }), + }, + }, + "no mutated status": { + Request: testRequest, + StatusSubResourceTypes: []client.Object{ + &resources.TestResourceUnexportedFields{}, + }, + GivenObjects: []client.Object{ + resource, + }, + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResourceUnexportedFields] { + return &reconcilers.SyncReconciler[*resources.TestResourceUnexportedFields]{ + Sync: func(ctx context.Context, resource *resources.TestResourceUnexportedFields) error { + if resource.Status.Fields == nil { + resource.Status.Fields = map[string]string{} + } + resource.CopyUnexportedFields() + resource.Spec.Fields["Reconciler"] = "ran" + return nil + }, + } + }, + }, + }, + } + + rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.ReconcilerTestCase, c reconcilers.Config) reconcile.Reconciler { + return &reconcilers.ResourceReconciler[*resources.TestResourceUnexportedFields]{ + Reconciler: rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler[*resources.TestResourceUnexportedFields])(t, c), + Config: c, + } + }) +} diff --git a/reconcilers/resourcemanager.go b/reconcilers/resourcemanager.go index 94f901d..b44f93c 100644 --- a/reconcilers/resourcemanager.go +++ b/reconcilers/resourcemanager.go @@ -203,7 +203,7 @@ func (r *ResourceManager[T]) Manage(ctx context.Context, resource client.Object, log.Info("resource is in sync, no update required") return actual, nil } - log.Info("updating resource", "diff", cmp.Diff(r.sanitize(actual), r.sanitize(current))) + log.Info("updating resource", "diff", cmp.Diff(r.sanitize(actual), r.sanitize(current), IgnoreAllUnexported)) if err := c.Update(ctx, current); err != nil { if !errors.Is(err, ErrQuiet) { log.Error(err, "unable to update resource", "resource", namespaceName(current)) diff --git a/testing/config.go b/testing/config.go index f5e4c07..460f944 100644 --- a/testing/config.go +++ b/testing/config.go @@ -191,7 +191,7 @@ func (c *ExpectConfig) AssertClientCreateExpectations(t *testing.T) { } c.init() - c.compareActions(t, "Create", c.ExpectCreates, c.client.CreateActions, IgnoreLastTransitionTime, SafeDeployDiff, IgnoreTypeMeta, IgnoreCreationTimestamp, IgnoreResourceVersion, cmpopts.EquateEmpty()) + c.compareActions(t, "Create", c.ExpectCreates, c.client.CreateActions, reconcilers.IgnoreAllUnexported, IgnoreLastTransitionTime, IgnoreTypeMeta, IgnoreCreationTimestamp, IgnoreResourceVersion, cmpopts.EquateEmpty()) } // AssertClientUpdateExpectations asserts observed reconciler client update behavior matches the expected client update behavior @@ -201,7 +201,7 @@ func (c *ExpectConfig) AssertClientUpdateExpectations(t *testing.T) { } c.init() - c.compareActions(t, "Update", c.ExpectUpdates, c.client.UpdateActions, IgnoreLastTransitionTime, SafeDeployDiff, IgnoreTypeMeta, IgnoreCreationTimestamp, IgnoreResourceVersion, cmpopts.EquateEmpty()) + c.compareActions(t, "Update", c.ExpectUpdates, c.client.UpdateActions, reconcilers.IgnoreAllUnexported, IgnoreLastTransitionTime, IgnoreTypeMeta, IgnoreCreationTimestamp, IgnoreResourceVersion, cmpopts.EquateEmpty()) } // AssertClientPatchExpectations asserts observed reconciler client patch behavior matches the expected client patch behavior @@ -286,7 +286,7 @@ func (c *ExpectConfig) AssertClientStatusUpdateExpectations(t *testing.T) { } c.init() - c.compareActions(t, "StatusUpdate", c.ExpectStatusUpdates, c.client.StatusUpdateActions, statusSubresourceOnly, IgnoreLastTransitionTime, SafeDeployDiff, cmpopts.EquateEmpty()) + c.compareActions(t, "StatusUpdate", c.ExpectStatusUpdates, c.client.StatusUpdateActions, statusSubresourceOnly, reconcilers.IgnoreAllUnexported, IgnoreLastTransitionTime, cmpopts.EquateEmpty()) } // AssertClientStatusPatchExpectations asserts observed reconciler client status patch behavior matches the expected client status patch behavior @@ -427,6 +427,7 @@ var ( return str != "" && !strings.HasPrefix(str, "Status") }, cmp.Ignore()) + // Deprecated: use reconcilers.IgnoreAllUnexported instead SafeDeployDiff = cmpopts.IgnoreUnexported(resource.Quantity{}) NormalizeLabelSelector = cmp.Transformer("labels.Selector", func(s labels.Selector) *string { diff --git a/testing/subreconciler.go b/testing/subreconciler.go index d8fac64..2298d88 100644 --- a/testing/subreconciler.go +++ b/testing/subreconciler.go @@ -164,7 +164,7 @@ func (tc *SubReconcilerTestCase[T]) Run(t *testing.T, scheme *runtime.Scheme, fa // Set func for verifying stashed values if tc.VerifyStashedValue == nil { tc.VerifyStashedValue = func(t *testing.T, key reconcilers.StashKey, expected, actual interface{}) { - if diff := cmp.Diff(expected, actual, IgnoreLastTransitionTime, SafeDeployDiff, IgnoreTypeMeta, IgnoreCreationTimestamp, IgnoreResourceVersion, cmpopts.EquateEmpty()); diff != "" { + if diff := cmp.Diff(expected, actual, reconcilers.IgnoreAllUnexported, IgnoreLastTransitionTime, IgnoreTypeMeta, IgnoreCreationTimestamp, IgnoreResourceVersion, cmpopts.EquateEmpty()); diff != "" { t.Errorf("ExpectStashedValues[%q] differs (%s, %s): %s", key, DiffRemovedColor.Sprint("-expected"), DiffAddedColor.Sprint("+actual"), ColorizeDiff(diff)) } } @@ -279,7 +279,7 @@ func (tc *SubReconcilerTestCase[T]) Run(t *testing.T, scheme *runtime.Scheme, fa // mirror defaulting of the resource expectedResource.SetResourceVersion("999") } - if diff := cmp.Diff(expectedResource, resource, IgnoreLastTransitionTime, SafeDeployDiff, IgnoreTypeMeta, cmpopts.EquateEmpty()); diff != "" { + if diff := cmp.Diff(expectedResource, resource, reconcilers.IgnoreAllUnexported, IgnoreLastTransitionTime, IgnoreTypeMeta, cmpopts.EquateEmpty()); diff != "" { t.Errorf("ExpectResource differs (%s, %s): %s", DiffRemovedColor.Sprint("-expected"), DiffAddedColor.Sprint("+actual"), ColorizeDiff(diff)) } diff --git a/testing/webhook.go b/testing/webhook.go index 082dfdf..a2fc08f 100644 --- a/testing/webhook.go +++ b/testing/webhook.go @@ -216,7 +216,7 @@ func (tc *AdmissionWebhookTestCase) Run(t *testing.T, scheme *runtime.Scheme, fa }() tc.ExpectedResponse.Complete(*tc.Request) - if diff := cmp.Diff(tc.ExpectedResponse, response); diff != "" { + if diff := cmp.Diff(tc.ExpectedResponse, response, reconcilers.IgnoreAllUnexported); diff != "" { t.Errorf("ExpectedResponse differs (%s, %s): %s", DiffRemovedColor.Sprint("-expected"), DiffAddedColor.Sprint("+actual"), ColorizeDiff(diff)) } From 09436ab28972953af504bd6743a85a9f839911d9 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Tue, 5 Mar 2024 14:53:06 -0500 Subject: [PATCH 2/2] Review feedback Signed-off-by: Scott Andrews --- .../resource_with_unexported_fields.go | 22 ++++- reconcilers/child_test.go | 6 -- reconcilers/cmp_test.go | 83 +++++++++++++++++++ reconcilers/resource_test.go | 28 ++----- 4 files changed, 111 insertions(+), 28 deletions(-) create mode 100644 reconcilers/cmp_test.go diff --git a/internal/resources/resource_with_unexported_fields.go b/internal/resources/resource_with_unexported_fields.go index 27816e1..73625a4 100644 --- a/internal/resources/resource_with_unexported_fields.go +++ b/internal/resources/resource_with_unexported_fields.go @@ -11,6 +11,7 @@ import ( "github.com/vmware-labs/reconciler-runtime/apis" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" @@ -67,7 +68,7 @@ func (r *TestResourceUnexportedFields) validate() field.ErrorList { return errs } -func (r *TestResourceUnexportedFields) CopyUnexportedFields() { +func (r *TestResourceUnexportedFields) ReflectUnexportedFieldsToStatus() { r.Status.unexportedFields = r.Spec.unexportedFields } @@ -89,7 +90,7 @@ func (r *TestResourceUnexportedFieldsSpec) SetUnexportedFields(f map[string]stri r.unexportedFields = f } -func (r *TestResourceUnexportedFieldsSpec) AddUnexportedFields(key, value string) { +func (r *TestResourceUnexportedFieldsSpec) AddUnexportedField(key, value string) { if r.unexportedFields == nil { r.unexportedFields = map[string]string{} } @@ -149,7 +150,7 @@ func (r *TestResourceUnexportedFieldsStatus) SetUnexportedFields(f map[string]st r.unexportedFields = f } -func (r *TestResourceUnexportedFieldsStatus) AddUnexportedFields(key, value string) { +func (r *TestResourceUnexportedFieldsStatus) AddUnexportedField(key, value string) { if r.unexportedFields == nil { r.unexportedFields = map[string]string{} } @@ -167,4 +168,19 @@ type TestResourceUnexportedFieldsList struct { func init() { SchemeBuilder.Register(&TestResourceUnexportedFields{}, &TestResourceUnexportedFieldsList{}) + + if err := equality.Semantic.AddFuncs( + func(a, b TestResourceUnexportedFieldsSpec) bool { + return equality.Semantic.DeepEqual(a.Fields, b.Fields) && + equality.Semantic.DeepEqual(a.Template, b.Template) && + equality.Semantic.DeepEqual(a.ErrOnMarshal, b.ErrOnMarshal) && + equality.Semantic.DeepEqual(a.ErrOnUnmarshal, b.ErrOnUnmarshal) + }, + func(a, b TestResourceUnexportedFieldsStatus) bool { + return equality.Semantic.DeepEqual(a.Status, b.Status) && + equality.Semantic.DeepEqual(a.Fields, b.Fields) + }, + ); err != nil { + panic(err) + } } diff --git a/reconcilers/child_test.go b/reconcilers/child_test.go index 84f091c..169a673 100644 --- a/reconcilers/child_test.go +++ b/reconcilers/child_test.go @@ -2299,12 +2299,6 @@ func TestChildReconciler_UnexportedFields(t *testing.T) { rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Updated", `Updated TestResourceUnexportedFields %q`, testName), }, - ExpectResource: resourceReady. - SpecDie(func(d *dies.TestResourceSpecDie) { - d.AddField("foo", "bar") - d.AddField("new", "field") - }). - DieReleasePtr(), ExpectUpdates: []client.Object{ childGiven. SpecDie(func(d *dies.TestResourceUnexportedFieldsSpecDie) { diff --git a/reconcilers/cmp_test.go b/reconcilers/cmp_test.go new file mode 100644 index 0000000..ceaed11 --- /dev/null +++ b/reconcilers/cmp_test.go @@ -0,0 +1,83 @@ +/* +Copyright 2024 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package reconcilers_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/vmware-labs/reconciler-runtime/internal/resources" + "github.com/vmware-labs/reconciler-runtime/internal/resources/dies" + "github.com/vmware-labs/reconciler-runtime/reconcilers" +) + +type TestResourceUnexportedSpec struct { + spec resources.TestResourceUnexportedFieldsSpec +} + +func TestIgnoreAllUnexported(t *testing.T) { + tests := map[string]struct { + a interface{} + b interface{} + shouldDiff bool + }{ + "nil is equivalent": { + a: nil, + b: nil, + shouldDiff: false, + }, + "different exported fields have a difference": { + a: dies.TestResourceUnexportedFieldsSpecBlank. + AddField("name", "hello"). + DieRelease(), + b: dies.TestResourceUnexportedFieldsSpecBlank. + AddField("name", "world"). + DieRelease(), + shouldDiff: true, + }, + "different unexported fields do not have a difference": { + a: dies.TestResourceUnexportedFieldsSpecBlank. + AddUnexportedField("name", "hello"). + DieRelease(), + b: dies.TestResourceUnexportedFieldsSpecBlank. + AddUnexportedField("name", "world"). + DieRelease(), + shouldDiff: false, + }, + "different exported fields nested in an unexported field do not have a difference": { + a: TestResourceUnexportedSpec{ + spec: dies.TestResourceUnexportedFieldsSpecBlank. + AddField("name", "hello"). + DieRelease(), + }, + b: TestResourceUnexportedSpec{ + spec: dies.TestResourceUnexportedFieldsSpecBlank. + AddField("name", "world"). + DieRelease(), + }, + shouldDiff: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + if name[0:1] == "#" { + t.SkipNow() + } + + diff := cmp.Diff(tc.a, tc.b, reconcilers.IgnoreAllUnexported) + hasDiff := diff != "" + shouldDiff := tc.shouldDiff + + if !hasDiff && shouldDiff { + t.Errorf("expected equality, found diff") + } + if hasDiff && !shouldDiff { + t.Errorf("found diff, expected equality: %s", diff) + } + }) + } +} diff --git a/reconcilers/resource_test.go b/reconcilers/resource_test.go index 25323a5..a967d48 100644 --- a/reconcilers/resource_test.go +++ b/reconcilers/resource_test.go @@ -1320,9 +1320,9 @@ func TestResourceReconciler_UnexportedFields(t *testing.T) { if resource.Status.Fields == nil { resource.Status.Fields = map[string]string{} } - resource.CopyUnexportedFields() + resource.ReflectUnexportedFieldsToStatus() resource.Status.Fields["Reconciler"] = "ran" - resource.Status.AddUnexportedFields("Reconciler", "ran") + resource.Status.AddUnexportedField("Reconciler", "ran") return nil }, } @@ -1354,22 +1354,13 @@ func TestResourceReconciler_UnexportedFields(t *testing.T) { if resource.Status.Fields == nil { resource.Status.Fields = map[string]string{} } - resource.CopyUnexportedFields() - resource.Status.AddUnexportedFields("Reconciler", "ran") + resource.ReflectUnexportedFieldsToStatus() + resource.Status.AddUnexportedField("Reconciler", "ran") return nil }, } }, }, - ExpectEvents: []rtesting.Event{ - rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "StatusUpdated", - `Updated status`), - }, - ExpectStatusUpdates: []client.Object{ - resource.StatusDie(func(d *dies.TestResourceUnexportedFieldsStatusDie) { - d.AddUnexportedField("Reconciler", "ran") - }), - }, }, "no mutated status": { Request: testRequest, @@ -1377,17 +1368,16 @@ func TestResourceReconciler_UnexportedFields(t *testing.T) { &resources.TestResourceUnexportedFields{}, }, GivenObjects: []client.Object{ - resource, + resource.StatusDie(func(d *dies.TestResourceUnexportedFieldsStatusDie) { + d.AddUnexportedField("Test", "ran") + d.AddUnexportedField("Reconciler", "ran") + }), }, Metadata: map[string]interface{}{ "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResourceUnexportedFields] { return &reconcilers.SyncReconciler[*resources.TestResourceUnexportedFields]{ Sync: func(ctx context.Context, resource *resources.TestResourceUnexportedFields) error { - if resource.Status.Fields == nil { - resource.Status.Fields = map[string]string{} - } - resource.CopyUnexportedFields() - resource.Spec.Fields["Reconciler"] = "ran" + resource.Status.AddUnexportedField("Reconciler", "ran") return nil }, }