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..73625a4 --- /dev/null +++ b/internal/resources/resource_with_unexported_fields.go @@ -0,0 +1,186 @@ +/* +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" + "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" + "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) ReflectUnexportedFieldsToStatus() { + 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) AddUnexportedField(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) AddUnexportedField(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{}) + + 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/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..169a673 100644 --- a/reconcilers/child_test.go +++ b/reconcilers/child_test.go @@ -2108,3 +2108,226 @@ 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), + }, + 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/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.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..a967d48 100644 --- a/reconcilers/resource_test.go +++ b/reconcilers/resource_test.go @@ -1282,3 +1282,114 @@ 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.ReflectUnexportedFieldsToStatus() + resource.Status.Fields["Reconciler"] = "ran" + 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.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.ReflectUnexportedFieldsToStatus() + resource.Status.AddUnexportedField("Reconciler", "ran") + return nil + }, + } + }, + }, + }, + "no mutated status": { + Request: testRequest, + StatusSubResourceTypes: []client.Object{ + &resources.TestResourceUnexportedFields{}, + }, + GivenObjects: []client.Object{ + 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 { + resource.Status.AddUnexportedField("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)) }