From 796b0a6b8ccdb17cd139d84b606b49bdd29c12f1 Mon Sep 17 00:00:00 2001 From: fabriziopandini Date: Tue, 3 Sep 2024 12:02:19 +0200 Subject: [PATCH] refactor test types, add matcher --- internal/test/builder/v1beta2_transition.go | 412 +++++++++++++++++ .../test/builder/zz_generated.deepcopy.go | 415 ++++++++++++++++++ internal/test/envtest/environment.go | 5 + .../conditions/experimental/aggregate_test.go | 36 +- util/conditions/experimental/getter_test.go | 107 ++--- util/conditions/experimental/matcher.go | 102 +++++ util/conditions/experimental/matcher_test.go | 293 +++++++++++++ util/conditions/experimental/mirror_test.go | 16 +- util/conditions/experimental/setter.go | 5 - util/conditions/experimental/setter_test.go | 57 +-- util/conditions/experimental/summary_test.go | 6 +- 11 files changed, 1305 insertions(+), 149 deletions(-) create mode 100644 internal/test/builder/v1beta2_transition.go create mode 100644 util/conditions/experimental/matcher.go create mode 100644 util/conditions/experimental/matcher_test.go diff --git a/internal/test/builder/v1beta2_transition.go b/internal/test/builder/v1beta2_transition.go new file mode 100644 index 000000000000..8cc90b9e1f6d --- /dev/null +++ b/internal/test/builder/v1beta2_transition.go @@ -0,0 +1,412 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package builder + +import ( + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" +) + +var ( + // TransitionV1beta2GroupVersion is group version used for test CRDs used for validating the v1beta2 transition. + TransitionV1beta2GroupVersion = schema.GroupVersion{Group: "transition.v1beta2.cluster.x-k8s.io", Version: "v1beta1"} + + // Phase0ObjKind is the kind for the Phase0Obj type. + Phase0ObjKind = "Phase0Obj" + // Phase0ObjCRD is a Phase0Obj CRD. + Phase0ObjCRD = phase0ObjCRD(TransitionV1beta2GroupVersion.WithKind(Phase0ObjKind)) + + // Phase1ObjKind is the kind for the Phase1Obj type. + Phase1ObjKind = "Phase1Obj" + // Phase1ObjCRD is a Phase1Obj CRD. + Phase1ObjCRD = phase1ObjCRD(TransitionV1beta2GroupVersion.WithKind(Phase1ObjKind)) + + // Phase2ObjKind is the kind for the Phase2Obj type. + Phase2ObjKind = "Phase2Obj" + // Phase2ObjCRD is a Phase2Obj CRD. + Phase2ObjCRD = phase2ObjCRD(TransitionV1beta2GroupVersion.WithKind(Phase2ObjKind)) + + // Phase3ObjKind is the kind for the Phase3Obj type. + Phase3ObjKind = "Phase3Obj" + // Phase3ObjCRD is a Phase3Obj CRD. + Phase3ObjCRD = phase3ObjCRD(TransitionV1beta2GroupVersion.WithKind(Phase3ObjKind)) + + // schemeBuilder is used to add go types to the GroupVersionKind scheme. + schemeBuilder = runtime.NewSchemeBuilder(addTransitionV1beta2Types) + + // AddTransitionV1beta2ToScheme adds the types for validating the transition to v1Beta2 in this group-version to the given scheme. + AddTransitionV1beta2ToScheme = schemeBuilder.AddToScheme +) + +func addTransitionV1beta2Types(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(TransitionV1beta2GroupVersion, + &Phase0Obj{}, &Phase0ObjList{}, + &Phase1Obj{}, &Phase1ObjList{}, + &Phase2Obj{}, &Phase2ObjList{}, + &Phase3Obj{}, &Phase3ObjList{}, + ) + metav1.AddToGroupVersion(scheme, TransitionV1beta2GroupVersion) + return nil +} + +// Phase0ObjList is a list of Phase0Obj. +// +kubebuilder:object:root=true +type Phase0ObjList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Phase0Obj `json:"items"` +} + +// Phase0Obj defines an object with clusterv1.Conditions. +// +kubebuilder:object:root=true +type Phase0Obj struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec Phase0ObjSpec `json:"spec,omitempty"` + Status Phase0ObjStatus `json:"status,omitempty"` +} + +// Phase0ObjSpec defines the spec of a Phase0Obj. +type Phase0ObjSpec struct { + Foo string `json:"foo,omitempty"` +} + +// Phase0ObjStatus defines the status of a Phase0Obj. +type Phase0ObjStatus struct { + Bar string `json:"bar,omitempty"` + Conditions clusterv1.Conditions `json:"conditions,omitempty"` +} + +// GetConditions returns the set of conditions for this object. +func (o *Phase0Obj) GetConditions() clusterv1.Conditions { + return o.Status.Conditions +} + +// SetConditions sets the conditions on this object. +func (o *Phase0Obj) SetConditions(conditions clusterv1.Conditions) { + o.Status.Conditions = conditions +} + +func phase0ObjCRD(gvk schema.GroupVersionKind) *apiextensionsv1.CustomResourceDefinition { + return generateCRD(gvk, map[string]apiextensionsv1.JSONSchemaProps{ + "metadata": { + // NOTE: in CRD there is only a partial definition of metadata schema. + // Ref https://github.com/kubernetes-sigs/controller-tools/blob/59485af1c1f6a664655dad49543c474bb4a0d2a2/pkg/crd/gen.go#L185 + Type: "object", + }, + "spec": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "foo": {Type: "string"}, + }, + }, + "status": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "bar": {Type: "string"}, + "conditions": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "type": {Type: "string"}, + "status": {Type: "string"}, + "severity": {Type: "string"}, + "reason": {Type: "string"}, + "message": {Type: "string"}, + "lastTransitionTime": {Type: "string"}, + }, + Required: []string{"type", "status"}, + }, + }, + }, + }, + }, + }) +} + +// Phase1ObjList is a list of Phase1Obj. +// +kubebuilder:object:root=true +type Phase1ObjList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Phase1Obj `json:"items"` +} + +// Phase1Obj defines an object with conditions and experimental conditions. +// +kubebuilder:object:root=true +type Phase1Obj struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec Phase1ObjSpec `json:"spec,omitempty"` + Status Phase1ObjStatus `json:"status,omitempty"` +} + +// Phase1ObjSpec defines the spec of a Phase1Obj. +type Phase1ObjSpec struct { + Foo string `json:"foo,omitempty"` +} + +// Phase1ObjStatus defines the status of a Phase1Obj. +type Phase1ObjStatus struct { + Bar string `json:"bar,omitempty"` + Conditions clusterv1.Conditions `json:"conditions,omitempty"` + ExperimentalConditions []metav1.Condition `json:"experimentalConditions,omitempty"` +} + +// GetConditions returns the set of conditions for this object. +func (o *Phase1Obj) GetConditions() clusterv1.Conditions { + return o.Status.Conditions +} + +// SetConditions sets the conditions on this object. +func (o *Phase1Obj) SetConditions(conditions clusterv1.Conditions) { + o.Status.Conditions = conditions +} + +func phase1ObjCRD(gvk schema.GroupVersionKind) *apiextensionsv1.CustomResourceDefinition { + return generateCRD(gvk, map[string]apiextensionsv1.JSONSchemaProps{ + "metadata": { + // NOTE: in CRD there is only a partial definition of metadata schema. + // Ref https://github.com/kubernetes-sigs/controller-tools/blob/59485af1c1f6a664655dad49543c474bb4a0d2a2/pkg/crd/gen.go#L185 + Type: "object", + }, + "spec": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "foo": {Type: "string"}, + }, + }, + "status": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "bar": {Type: "string"}, + "conditions": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "type": {Type: "string"}, + "status": {Type: "string"}, + "severity": {Type: "string"}, + "reason": {Type: "string"}, + "message": {Type: "string"}, + "lastTransitionTime": {Type: "string"}, + }, + Required: []string{"type", "status"}, + }, + }, + }, + "experimentalConditions": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "type": {Type: "string"}, + "status": {Type: "string"}, + "observedGeneration": {Type: "integer"}, + "reason": {Type: "string"}, + "message": {Type: "string"}, + "lastTransitionTime": {Type: "string"}, + }, + Required: []string{"type", "status"}, + }, + }, + }, + }, + }, + }) +} + +// Phase2ObjList is a list of Phase2Obj. +// +kubebuilder:object:root=true +type Phase2ObjList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Phase2Obj `json:"items"` +} + +// Phase2Obj defines an object with conditions and back compatibility conditions. +// +kubebuilder:object:root=true +type Phase2Obj struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec Phase2ObjSpec `json:"spec,omitempty"` + Status Phase2ObjStatus `json:"status,omitempty"` +} + +// Phase2ObjSpec defines the spec of a Phase2Obj. +type Phase2ObjSpec struct { + Foo string `json:"foo,omitempty"` +} + +// Phase2ObjStatus defines the status of a Phase2Obj. +type Phase2ObjStatus struct { + Bar string `json:"bar,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` + BackCompatibility Phase2ObjStatusBackCompatibility `json:"backCompatibility,omitempty"` +} + +// Phase2ObjStatusBackCompatibility defines the status.BackCompatibility of a Phase2Obj. +type Phase2ObjStatusBackCompatibility struct { + Conditions clusterv1.Conditions `json:"conditions,omitempty"` +} + +// GetConditions returns the set of conditions for this object. +func (o *Phase2Obj) GetConditions() clusterv1.Conditions { + return o.Status.BackCompatibility.Conditions +} + +// SetConditions sets the conditions on this object. +func (o *Phase2Obj) SetConditions(conditions clusterv1.Conditions) { + o.Status.BackCompatibility.Conditions = conditions +} + +func phase2ObjCRD(gvk schema.GroupVersionKind) *apiextensionsv1.CustomResourceDefinition { + return generateCRD(gvk, map[string]apiextensionsv1.JSONSchemaProps{ + "metadata": { + // NOTE: in CRD there is only a partial definition of metadata schema. + // Ref https://github.com/kubernetes-sigs/controller-tools/blob/59485af1c1f6a664655dad49543c474bb4a0d2a2/pkg/crd/gen.go#L185 + Type: "object", + }, + "spec": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "foo": {Type: "string"}, + }, + }, + "status": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "bar": {Type: "string"}, + "conditions": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "type": {Type: "string"}, + "status": {Type: "string"}, + "observedGeneration": {Type: "integer"}, + "reason": {Type: "string"}, + "message": {Type: "string"}, + "lastTransitionTime": {Type: "string"}, + }, + Required: []string{"type", "status"}, + }, + }, + }, + "backCompatibility": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "conditions": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "type": {Type: "string"}, + "status": {Type: "string"}, + "severity": {Type: "string"}, + "reason": {Type: "string"}, + "message": {Type: "string"}, + "lastTransitionTime": {Type: "string"}, + }, + Required: []string{"type", "status"}, + }, + }, + }, + }, + }, + }, + }, + }) +} + +// Phase3ObjList is a list of Phase3Obj. +// +kubebuilder:object:root=true +type Phase3ObjList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Phase3Obj `json:"items"` +} + +// Phase3Obj defines an object with metav1.conditions. +// +kubebuilder:object:root=true +type Phase3Obj struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec Phase3ObjSpec `json:"spec,omitempty"` + Status Phase3ObjStatus `json:"status,omitempty"` +} + +// Phase3ObjSpec defines the spec of a Phase3Obj. +type Phase3ObjSpec struct { + Foo string `json:"foo,omitempty"` +} + +// Phase3ObjStatus defines the status of a Phase3Obj. +type Phase3ObjStatus struct { + Bar string `json:"bar,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +func phase3ObjCRD(gvk schema.GroupVersionKind) *apiextensionsv1.CustomResourceDefinition { + return generateCRD(gvk, map[string]apiextensionsv1.JSONSchemaProps{ + "metadata": { + // NOTE: in CRD there is only a partial definition of metadata schema. + // Ref https://github.com/kubernetes-sigs/controller-tools/blob/59485af1c1f6a664655dad49543c474bb4a0d2a2/pkg/crd/gen.go#L185 + Type: "object", + }, + "spec": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "foo": {Type: "string"}, + }, + }, + "status": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "bar": {Type: "string"}, + "conditions": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "type": {Type: "string"}, + "status": {Type: "string"}, + "observedGeneration": {Type: "integer"}, + "reason": {Type: "string"}, + "message": {Type: "string"}, + "lastTransitionTime": {Type: "string"}, + }, + Required: []string{"type", "status"}, + }, + }, + }, + }, + }, + }) +} diff --git a/internal/test/builder/zz_generated.deepcopy.go b/internal/test/builder/zz_generated.deepcopy.go index fc215627ce28..bf87beff060a 100644 --- a/internal/test/builder/zz_generated.deepcopy.go +++ b/internal/test/builder/zz_generated.deepcopy.go @@ -22,6 +22,7 @@ package builder import ( "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" "sigs.k8s.io/cluster-api/api/v1beta1" apiv1beta1 "sigs.k8s.io/cluster-api/exp/api/v1beta1" @@ -844,6 +845,420 @@ func (in *NodeBuilder) DeepCopy() *NodeBuilder { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Phase0Obj) DeepCopyInto(out *Phase0Obj) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Phase0Obj. +func (in *Phase0Obj) DeepCopy() *Phase0Obj { + if in == nil { + return nil + } + out := new(Phase0Obj) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Phase0Obj) 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 *Phase0ObjList) DeepCopyInto(out *Phase0ObjList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Phase0Obj, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Phase0ObjList. +func (in *Phase0ObjList) DeepCopy() *Phase0ObjList { + if in == nil { + return nil + } + out := new(Phase0ObjList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Phase0ObjList) 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 *Phase0ObjSpec) DeepCopyInto(out *Phase0ObjSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Phase0ObjSpec. +func (in *Phase0ObjSpec) DeepCopy() *Phase0ObjSpec { + if in == nil { + return nil + } + out := new(Phase0ObjSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Phase0ObjStatus) DeepCopyInto(out *Phase0ObjStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(v1beta1.Conditions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Phase0ObjStatus. +func (in *Phase0ObjStatus) DeepCopy() *Phase0ObjStatus { + if in == nil { + return nil + } + out := new(Phase0ObjStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Phase1Obj) DeepCopyInto(out *Phase1Obj) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Phase1Obj. +func (in *Phase1Obj) DeepCopy() *Phase1Obj { + if in == nil { + return nil + } + out := new(Phase1Obj) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Phase1Obj) 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 *Phase1ObjList) DeepCopyInto(out *Phase1ObjList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Phase1Obj, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Phase1ObjList. +func (in *Phase1ObjList) DeepCopy() *Phase1ObjList { + if in == nil { + return nil + } + out := new(Phase1ObjList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Phase1ObjList) 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 *Phase1ObjSpec) DeepCopyInto(out *Phase1ObjSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Phase1ObjSpec. +func (in *Phase1ObjSpec) DeepCopy() *Phase1ObjSpec { + if in == nil { + return nil + } + out := new(Phase1ObjSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Phase1ObjStatus) DeepCopyInto(out *Phase1ObjStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(v1beta1.Conditions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ExperimentalConditions != nil { + in, out := &in.ExperimentalConditions, &out.ExperimentalConditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Phase1ObjStatus. +func (in *Phase1ObjStatus) DeepCopy() *Phase1ObjStatus { + if in == nil { + return nil + } + out := new(Phase1ObjStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Phase2Obj) DeepCopyInto(out *Phase2Obj) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Phase2Obj. +func (in *Phase2Obj) DeepCopy() *Phase2Obj { + if in == nil { + return nil + } + out := new(Phase2Obj) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Phase2Obj) 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 *Phase2ObjList) DeepCopyInto(out *Phase2ObjList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Phase2Obj, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Phase2ObjList. +func (in *Phase2ObjList) DeepCopy() *Phase2ObjList { + if in == nil { + return nil + } + out := new(Phase2ObjList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Phase2ObjList) 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 *Phase2ObjSpec) DeepCopyInto(out *Phase2ObjSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Phase2ObjSpec. +func (in *Phase2ObjSpec) DeepCopy() *Phase2ObjSpec { + if in == nil { + return nil + } + out := new(Phase2ObjSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Phase2ObjStatus) DeepCopyInto(out *Phase2ObjStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.BackCompatibility.DeepCopyInto(&out.BackCompatibility) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Phase2ObjStatus. +func (in *Phase2ObjStatus) DeepCopy() *Phase2ObjStatus { + if in == nil { + return nil + } + out := new(Phase2ObjStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Phase2ObjStatusBackCompatibility) DeepCopyInto(out *Phase2ObjStatusBackCompatibility) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(v1beta1.Conditions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Phase2ObjStatusBackCompatibility. +func (in *Phase2ObjStatusBackCompatibility) DeepCopy() *Phase2ObjStatusBackCompatibility { + if in == nil { + return nil + } + out := new(Phase2ObjStatusBackCompatibility) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Phase3Obj) DeepCopyInto(out *Phase3Obj) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Phase3Obj. +func (in *Phase3Obj) DeepCopy() *Phase3Obj { + if in == nil { + return nil + } + out := new(Phase3Obj) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Phase3Obj) 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 *Phase3ObjList) DeepCopyInto(out *Phase3ObjList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Phase3Obj, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Phase3ObjList. +func (in *Phase3ObjList) DeepCopy() *Phase3ObjList { + if in == nil { + return nil + } + out := new(Phase3ObjList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Phase3ObjList) 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 *Phase3ObjSpec) DeepCopyInto(out *Phase3ObjSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Phase3ObjSpec. +func (in *Phase3ObjSpec) DeepCopy() *Phase3ObjSpec { + if in == nil { + return nil + } + out := new(Phase3ObjSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Phase3ObjStatus) DeepCopyInto(out *Phase3ObjStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Phase3ObjStatus. +func (in *Phase3ObjStatus) DeepCopy() *Phase3ObjStatus { + if in == nil { + return nil + } + out := new(Phase3ObjStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TestBootstrapConfigBuilder) DeepCopyInto(out *TestBootstrapConfigBuilder) { *out = *in diff --git a/internal/test/envtest/environment.go b/internal/test/envtest/environment.go index 2727ff1f7b68..04bf1808504d 100644 --- a/internal/test/envtest/environment.go +++ b/internal/test/envtest/environment.go @@ -90,6 +90,7 @@ func init() { utilruntime.Must(admissionv1.AddToScheme(scheme.Scheme)) utilruntime.Must(runtimev1.AddToScheme(scheme.Scheme)) utilruntime.Must(ipamv1.AddToScheme(scheme.Scheme)) + utilruntime.Must(builder.AddTransitionV1beta2ToScheme(scheme.Scheme)) } // RunInput is the input for Run. @@ -230,6 +231,10 @@ func newEnvironment(uncachedObjs ...client.Object) *Environment { builder.TestBootstrapConfigCRD.DeepCopy(), builder.TestControlPlaneTemplateCRD.DeepCopy(), builder.TestControlPlaneCRD.DeepCopy(), + builder.Phase0ObjCRD.DeepCopy(), + builder.Phase1ObjCRD.DeepCopy(), + builder.Phase2ObjCRD.DeepCopy(), + builder.Phase3ObjCRD.DeepCopy(), }, // initialize webhook here to be able to test the envtest install via webhookOptions // This should set LocalServingCertDir and LocalServingPort that are used below. diff --git a/util/conditions/experimental/aggregate_test.go b/util/conditions/experimental/aggregate_test.go index 18c7dc01fa27..908d118f48e4 100644 --- a/util/conditions/experimental/aggregate_test.go +++ b/util/conditions/experimental/aggregate_test.go @@ -23,6 +23,8 @@ import ( . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + + "sigs.k8s.io/cluster-api/internal/test/builder" ) func TestAggregate(t *testing.T) { @@ -79,9 +81,9 @@ func TestAggregate(t *testing.T) { options: []AggregateOption{}, want: &metav1.Condition{ Type: AvailableCondition, - Status: metav1.ConditionFalse, // False because there is one issue - Reason: ManyIssuesReason, // Using a generic reason - Message: "(False): Message-1 from default/obj0, default/obj1, default/obj2 and 2 other V1Beta2ResourceWithConditions", // messages from all the issues & unknown conditions (info dropped) + Status: metav1.ConditionFalse, // False because there is one issue + Reason: ManyIssuesReason, // Using a generic reason + Message: "(False): Message-1 from default/obj0, default/obj1, default/obj2 and 2 other Phase3Objs", // messages from all the issues & unknown conditions (info dropped) }, }, { @@ -119,9 +121,9 @@ func TestAggregate(t *testing.T) { options: []AggregateOption{}, want: &metav1.Condition{ Type: AvailableCondition, - Status: metav1.ConditionFalse, // False because there is one issue - Reason: ManyIssuesReason, // Using a generic reason - Message: "(False): Message-1 from default/obj0, default/obj4; (False): Message-2 from default/obj1; (False): Message-3 from default/obj5; other 2 V1Beta2ResourceWithConditions with issues", // messages from all the issues & unknown conditions (info dropped) + Status: metav1.ConditionFalse, // False because there is one issue + Reason: ManyIssuesReason, // Using a generic reason + Message: "(False): Message-1 from default/obj0, default/obj4; (False): Message-2 from default/obj1; (False): Message-3 from default/obj5; other 2 Phase3Objs with issues", // messages from all the issues & unknown conditions (info dropped) }, }, { @@ -154,9 +156,9 @@ func TestAggregate(t *testing.T) { options: []AggregateOption{}, want: &metav1.Condition{ Type: AvailableCondition, - Status: metav1.ConditionFalse, // False because there is one issue - Reason: ManyIssuesReason, // Using a generic reason - Message: "(False): Message-1 from default/obj0; (False): Message-2 from default/obj1; (False): Message-4 from default/obj3; other 1 V1Beta2ResourceWithConditions unknown", // messages from all the issues & unknown conditions (info dropped) + Status: metav1.ConditionFalse, // False because there is one issue + Reason: ManyIssuesReason, // Using a generic reason + Message: "(False): Message-1 from default/obj0; (False): Message-2 from default/obj1; (False): Message-4 from default/obj3; other 1 Phase3Objs unknown", // messages from all the issues & unknown conditions (info dropped) }, }, { @@ -174,9 +176,9 @@ func TestAggregate(t *testing.T) { options: []AggregateOption{}, want: &metav1.Condition{ Type: AvailableCondition, - Status: metav1.ConditionUnknown, // False because there is one issue - Reason: ManyUnknownsReason, // Using a generic reason - Message: "(Unknown): Message-1 from default/obj0, default/obj4; (Unknown): Message-2 from default/obj1; (Unknown): Message-3 from default/obj5; other 2 V1Beta2ResourceWithConditions unknown", // messages from all the issues & unknown conditions (info dropped) + Status: metav1.ConditionUnknown, // False because there is one issue + Reason: ManyUnknownsReason, // Using a generic reason + Message: "(Unknown): Message-1 from default/obj0, default/obj4; (Unknown): Message-2 from default/obj1; (Unknown): Message-3 from default/obj5; other 2 Phase3Objs unknown", // messages from all the issues & unknown conditions (info dropped) }, }, { @@ -193,9 +195,9 @@ func TestAggregate(t *testing.T) { options: []AggregateOption{}, want: &metav1.Condition{ Type: AvailableCondition, - Status: metav1.ConditionTrue, // False because there is one issue - Reason: ManyInfoReason, // Using a generic reason - Message: "(True): Message-1 from default/obj0, default/obj4; (True): Message-2 from default/obj1; (True): Message-3 from default/obj5; other 1 V1Beta2ResourceWithConditions with info messages", // messages from all the issues & unknown conditions (info dropped) + Status: metav1.ConditionTrue, // False because there is one issue + Reason: ManyInfoReason, // Using a generic reason + Message: "(True): Message-1 from default/obj0, default/obj4; (True): Message-2 from default/obj1; (True): Message-3 from default/obj5; other 1 Phase3Objs with info messages", // messages from all the issues & unknown conditions (info dropped) }, }, } @@ -206,12 +208,12 @@ func TestAggregate(t *testing.T) { objs := make([]runtime.Object, 0, len(tt.conditions)) for i := range tt.conditions { - objs = append(objs, &V1Beta2ResourceWithConditions{ + objs = append(objs, &builder.Phase3Obj{ ObjectMeta: metav1.ObjectMeta{ Namespace: metav1.NamespaceDefault, Name: fmt.Sprintf("obj%d", i), }, - Status: struct{ Conditions []metav1.Condition }{ + Status: builder.Phase3ObjStatus{ Conditions: tt.conditions[i], }, }) diff --git a/util/conditions/experimental/getter_test.go b/util/conditions/experimental/getter_test.go index ce189d07db7d..53e1d47ca449 100644 --- a/util/conditions/experimental/getter_test.go +++ b/util/conditions/experimental/getter_test.go @@ -27,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/cluster-api/internal/test/builder" ) type ObjWithoutStatus struct { @@ -74,58 +75,6 @@ func (f *ObjWithWrongExperimentalConditionType) DeepCopyObject() runtime.Object panic("implement me") } -type V1Beta1ResourceWithLegacyConditions struct { - metav1.TypeMeta - metav1.ObjectMeta - Status struct { - Conditions clusterv1.Conditions - } -} - -func (f *V1Beta1ResourceWithLegacyConditions) DeepCopyObject() runtime.Object { - panic("implement me") -} - -type V1Beta1ResourceWithLegacyAndExperimentalConditionsV1 struct { - metav1.TypeMeta - metav1.ObjectMeta - Status struct { - Conditions clusterv1.Conditions - ExperimentalConditions []metav1.Condition - } -} - -func (f *V1Beta1ResourceWithLegacyAndExperimentalConditionsV1) DeepCopyObject() runtime.Object { - panic("implement me") -} - -type V1Beta2ResourceWithConditionsAndBackwardCompatibleConditions struct { - metav1.TypeMeta - metav1.ObjectMeta - Status struct { - Conditions []metav1.Condition - BackwardCompatibility struct { - Conditions clusterv1.Conditions - } - } -} - -func (f *V1Beta2ResourceWithConditionsAndBackwardCompatibleConditions) DeepCopyObject() runtime.Object { - panic("implement me") -} - -type V1Beta2ResourceWithConditions struct { - metav1.TypeMeta - metav1.ObjectMeta - Status struct { - Conditions []metav1.Condition - } -} - -func (f *V1Beta2ResourceWithConditions) DeepCopyObject() runtime.Object { - panic("implement me") -} - func TestGetAll(t *testing.T) { now := metav1.Now().Rfc3339Copy() @@ -138,7 +87,7 @@ func TestGetAll(t *testing.T) { t.Run("fails for nil object", func(t *testing.T) { g := NewWithT(t) - var foo *V1Beta1ResourceWithLegacyConditions + var foo *builder.Phase0Obj _, err := GetAll(foo) g.Expect(err).To(HaveOccurred()) @@ -199,21 +148,23 @@ func TestGetAll(t *testing.T) { t.Run("v1beta object with legacy conditions", func(t *testing.T) { g := NewWithT(t) - foo := &V1Beta1ResourceWithLegacyConditions{ - Status: struct{ Conditions clusterv1.Conditions }{Conditions: clusterv1.Conditions{ - { - Type: "fooCondition", - Status: corev1.ConditionTrue, - LastTransitionTime: now, - }, - { - Type: "fooCondition", - Status: corev1.ConditionFalse, - LastTransitionTime: now, - Reason: "FooReason", - Message: "FooMessage", + foo := &builder.Phase0Obj{ + Status: builder.Phase0ObjStatus{ + Conditions: clusterv1.Conditions{ + { + Type: "fooCondition", + Status: corev1.ConditionTrue, + LastTransitionTime: now, + }, + { + Type: "fooCondition", + Status: corev1.ConditionFalse, + LastTransitionTime: now, + Reason: "FooReason", + Message: "FooMessage", + }, }, - }}, + }, } expect := []metav1.Condition{ @@ -245,11 +196,8 @@ func TestGetAll(t *testing.T) { t.Run("v1beta1 object with both legacy and experimental conditions", func(t *testing.T) { g := NewWithT(t) - foo := &V1Beta1ResourceWithLegacyAndExperimentalConditionsV1{ - Status: struct { - Conditions clusterv1.Conditions - ExperimentalConditions []metav1.Condition - }{ + foo := &builder.Phase1Obj{ + Status: builder.Phase1ObjStatus{ Conditions: clusterv1.Conditions{ { Type: "barCondition", @@ -295,11 +243,8 @@ func TestGetAll(t *testing.T) { t.Run("v1beta2 object with conditions and backward compatible conditions", func(t *testing.T) { g := NewWithT(t) - foo := &V1Beta2ResourceWithConditionsAndBackwardCompatibleConditions{ - Status: struct { - Conditions []metav1.Condition - BackwardCompatibility struct{ Conditions clusterv1.Conditions } - }{ + foo := &builder.Phase2Obj{ + Status: builder.Phase2ObjStatus{ Conditions: []metav1.Condition{ { Type: "fooCondition", @@ -307,7 +252,7 @@ func TestGetAll(t *testing.T) { LastTransitionTime: now, }, }, - BackwardCompatibility: struct{ Conditions clusterv1.Conditions }{ + BackCompatibility: builder.Phase2ObjStatusBackCompatibility{ Conditions: clusterv1.Conditions{ { Type: "barCondition", @@ -341,10 +286,8 @@ func TestGetAll(t *testing.T) { t.Run("v1beta2 object with conditions (end state)", func(t *testing.T) { g := NewWithT(t) - foo := &V1Beta2ResourceWithConditions{ - Status: struct { - Conditions []metav1.Condition - }{ + foo := &builder.Phase3Obj{ + Status: builder.Phase3ObjStatus{ Conditions: []metav1.Condition{ { Type: "fooCondition", diff --git a/util/conditions/experimental/matcher.go b/util/conditions/experimental/matcher.go new file mode 100644 index 000000000000..72a9dd5db883 --- /dev/null +++ b/util/conditions/experimental/matcher.go @@ -0,0 +1,102 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package experimental + +import ( + "fmt" + + "github.com/onsi/gomega" + "github.com/onsi/gomega/types" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// MatchConditions returns a custom matcher to check equality of clusterv1.Conditions. +func MatchConditions(expected []metav1.Condition) types.GomegaMatcher { + return &matchConditions{ + expected: expected, + } +} + +type matchConditions struct { + expected []metav1.Condition +} + +func (m matchConditions) Match(actual interface{}) (success bool, err error) { + elems := []interface{}{} + for _, condition := range m.expected { + elems = append(elems, MatchCondition(condition)) + } + + return gomega.ConsistOf(elems...).Match(actual) +} + +func (m matchConditions) FailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("expected\n\t%#v\nto match\n\t%#v\n", actual, m.expected) +} + +func (m matchConditions) NegatedFailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("expected\n\t%#v\nto not match\n\t%#v\n", actual, m.expected) +} + +// MatchCondition returns a custom matcher to check equality of clusterv1.Condition. +func MatchCondition(expected metav1.Condition) types.GomegaMatcher { + return &matchCondition{ + expected: expected, + } +} + +type matchCondition struct { + expected metav1.Condition +} + +func (m matchCondition) Match(actual interface{}) (success bool, err error) { + actualCondition, ok := actual.(metav1.Condition) + if !ok { + return false, fmt.Errorf("actual should be of type metav1.Condition") + } + + ok, err = gomega.Equal(m.expected.Type).Match(actualCondition.Type) + if !ok { + return ok, err + } + ok, err = gomega.Equal(m.expected.Status).Match(actualCondition.Status) + if !ok { + return ok, err + } + ok, err = gomega.Equal(m.expected.ObservedGeneration).Match(actualCondition.ObservedGeneration) + if !ok { + return ok, err + } + ok, err = gomega.Equal(m.expected.Reason).Match(actualCondition.Reason) + if !ok { + return ok, err + } + ok, err = gomega.Equal(m.expected.Message).Match(actualCondition.Message) + if !ok { + return ok, err + } + + return ok, err +} + +func (m matchCondition) FailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("expected\n\t%#v\nto match\n\t%#v\n", actual, m.expected) +} + +func (m matchCondition) NegatedFailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("expected\n\t%#v\nto not match\n\t%#v\n", actual, m.expected) +} diff --git a/util/conditions/experimental/matcher_test.go b/util/conditions/experimental/matcher_test.go new file mode 100644 index 000000000000..cd45ecc0b4d5 --- /dev/null +++ b/util/conditions/experimental/matcher_test.go @@ -0,0 +1,293 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package experimental + +import ( + "testing" + + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestMatchConditions(t *testing.T) { + testCases := []struct { + name string + actual interface{} + expected []metav1.Condition + expectMatch bool + }{ + { + name: "with an empty conditions", + actual: []metav1.Condition{}, + expected: []metav1.Condition{}, + expectMatch: true, + }, + { + name: "with matching conditions", + actual: []metav1.Condition{ + { + Type: "type", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "reason", + Message: "message", + }, + }, + expected: []metav1.Condition{ + { + Type: "type", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "reason", + Message: "message", + }, + }, + expectMatch: true, + }, + { + name: "with non-matching conditions", + actual: []metav1.Condition{ + { + Type: "type", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "reason", + Message: "message", + }, + { + Type: "type", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "reason", + Message: "message", + }, + }, + expected: []metav1.Condition{ + { + Type: "type", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "reason", + Message: "message", + }, + { + Type: "different", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "different", + Message: "different", + }, + }, + expectMatch: false, + }, + { + name: "with a different number of conditions", + actual: []metav1.Condition{ + { + Type: "type", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "reason", + Message: "message", + }, + { + Type: "type", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "reason", + Message: "message", + }, + }, + expected: []metav1.Condition{ + { + Type: "type", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "reason", + Message: "message", + }, + }, + expectMatch: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + if tc.expectMatch { + g.Expect(tc.actual).To(MatchConditions(tc.expected)) + } else { + g.Expect(tc.actual).ToNot(MatchConditions(tc.expected)) + } + }) + } +} + +func TestMatchCondition(t *testing.T) { + testCases := []struct { + name string + actual interface{} + expected metav1.Condition + expectMatch bool + }{ + { + name: "with an empty condition", + actual: metav1.Condition{}, + expected: metav1.Condition{}, + expectMatch: true, + }, + { + name: "with a matching condition", + actual: metav1.Condition{ + Type: "type", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Time{}, + Reason: "reason", + Message: "message", + }, + expected: metav1.Condition{ + Type: "type", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Time{}, + Reason: "reason", + Message: "message", + }, + expectMatch: true, + }, + { + name: "with a different LastTransitionTime", + actual: metav1.Condition{ + Type: "type", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "reason", + Message: "message", + }, + expected: metav1.Condition{ + Type: "type", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Time{}, + Reason: "reason", + Message: "message", + }, + expectMatch: true, + }, + { + name: "with a different type", + actual: metav1.Condition{ + Type: "type", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "reason", + Message: "message", + }, + expected: metav1.Condition{ + Type: "different", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "reason", + Message: "message", + }, + expectMatch: false, + }, + { + name: "with a different status", + actual: metav1.Condition{ + Type: "type", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "reason", + Message: "message", + }, + expected: metav1.Condition{ + Type: "type", + Status: metav1.ConditionFalse, + LastTransitionTime: metav1.Now(), + Reason: "reason", + Message: "message", + }, + expectMatch: false, + }, + { + name: "with a different ObservedGeneration", + actual: metav1.Condition{ + Type: "type", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "reason", + Message: "message", + ObservedGeneration: 1, + }, + expected: metav1.Condition{ + Type: "type", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "reason", + Message: "message", + ObservedGeneration: 2, + }, + expectMatch: false, + }, + { + name: "with a different reason", + actual: metav1.Condition{ + Type: "type", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "reason", + Message: "message", + }, + expected: metav1.Condition{ + Type: "type", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "different", + Message: "message", + }, + expectMatch: false, + }, + { + name: "with a different message", + actual: metav1.Condition{ + Type: "type", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "reason", + Message: "message", + }, + expected: metav1.Condition{ + Type: "type", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "reason", + Message: "different", + }, + expectMatch: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + if tc.expectMatch { + g.Expect(tc.actual).To(MatchCondition(tc.expected)) + } else { + g.Expect(tc.actual).ToNot(MatchCondition(tc.expected)) + } + }) + } +} diff --git a/util/conditions/experimental/mirror_test.go b/util/conditions/experimental/mirror_test.go index 6c3827394bb2..466d1a71eebd 100644 --- a/util/conditions/experimental/mirror_test.go +++ b/util/conditions/experimental/mirror_test.go @@ -21,6 +21,8 @@ import ( . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "sigs.k8s.io/cluster-api/internal/test/builder" ) func TestMirrorStatusCondition(t *testing.T) { @@ -39,7 +41,7 @@ func TestMirrorStatusCondition(t *testing.T) { }, conditionType: "Ready", options: []MirrorOption{}, - want: metav1.Condition{Type: "Ready", Status: metav1.ConditionTrue, Reason: "AllGood!", Message: "We are good! (from V1Beta2ResourceWithConditions default/SourceObject)", ObservedGeneration: 10, LastTransitionTime: now}, + want: metav1.Condition{Type: "Ready", Status: metav1.ConditionTrue, Reason: "AllGood!", Message: "We are good! (from Phase3Obj default/SourceObject)", ObservedGeneration: 10, LastTransitionTime: now}, }, { name: "Mirror a condition with override type", @@ -48,7 +50,7 @@ func TestMirrorStatusCondition(t *testing.T) { }, conditionType: "Ready", options: []MirrorOption{OverrideType("SomethingReady")}, - want: metav1.Condition{Type: "SomethingReady", Status: metav1.ConditionTrue, Reason: "AllGood!", Message: "We are good! (from V1Beta2ResourceWithConditions default/SourceObject)", ObservedGeneration: 10, LastTransitionTime: now}, + want: metav1.Condition{Type: "SomethingReady", Status: metav1.ConditionTrue, Reason: "AllGood!", Message: "We are good! (from Phase3Obj default/SourceObject)", ObservedGeneration: 10, LastTransitionTime: now}, }, { name: "Mirror a condition with empty message", @@ -57,21 +59,21 @@ func TestMirrorStatusCondition(t *testing.T) { }, conditionType: "Ready", options: []MirrorOption{}, - want: metav1.Condition{Type: "Ready", Status: metav1.ConditionTrue, Reason: "AllGood!", Message: "(from V1Beta2ResourceWithConditions default/SourceObject)", ObservedGeneration: 10, LastTransitionTime: now}, + want: metav1.Condition{Type: "Ready", Status: metav1.ConditionTrue, Reason: "AllGood!", Message: "(from Phase3Obj default/SourceObject)", ObservedGeneration: 10, LastTransitionTime: now}, }, { name: "Mirror a condition not yet reported", conditions: []metav1.Condition{}, conditionType: "Ready", options: []MirrorOption{}, - want: metav1.Condition{Type: "Ready", Status: metav1.ConditionUnknown, Reason: NotYetReportedReason, Message: "Condition Ready not yet reported from V1Beta2ResourceWithConditions default/SourceObject", LastTransitionTime: now}, + want: metav1.Condition{Type: "Ready", Status: metav1.ConditionUnknown, Reason: NotYetReportedReason, Message: "Condition Ready not yet reported from Phase3Obj default/SourceObject", LastTransitionTime: now}, }, { name: "Mirror a condition not yet reported with override type", conditions: []metav1.Condition{}, conditionType: "Ready", options: []MirrorOption{OverrideType("SomethingReady")}, - want: metav1.Condition{Type: "SomethingReady", Status: metav1.ConditionUnknown, Reason: NotYetReportedReason, Message: "Condition Ready not yet reported from V1Beta2ResourceWithConditions default/SourceObject", LastTransitionTime: now}, + want: metav1.Condition{Type: "SomethingReady", Status: metav1.ConditionUnknown, Reason: NotYetReportedReason, Message: "Condition Ready not yet reported from Phase3Obj default/SourceObject", LastTransitionTime: now}, }, } @@ -79,12 +81,12 @@ func TestMirrorStatusCondition(t *testing.T) { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) - obj := &V1Beta2ResourceWithConditions{ + obj := &builder.Phase3Obj{ ObjectMeta: metav1.ObjectMeta{ Namespace: metav1.NamespaceDefault, Name: "SourceObject", }, - Status: struct{ Conditions []metav1.Condition }{ + Status: builder.Phase3ObjStatus{ Conditions: tt.conditions, }, } diff --git a/util/conditions/experimental/setter.go b/util/conditions/experimental/setter.go index 772158501b2e..b8334b5317e8 100644 --- a/util/conditions/experimental/setter.go +++ b/util/conditions/experimental/setter.go @@ -17,7 +17,6 @@ limitations under the License. package experimental import ( - "fmt" "reflect" "sort" @@ -173,8 +172,6 @@ func setToTypedObject(obj runtime.Object, conditions []metav1.Condition) error { // The ExperimentalConditions branch should be dropped when v1beta1 API are removed. if conditionField := statusField.FieldByName("ExperimentalConditions"); conditionField != (reflect.Value{}) { - fmt.Println("Status.ExperimentalConditions is a", reflect.TypeOf(conditionField.Interface()).String()) - if conditionField.Type() != metav1ConditionsType { return errors.Errorf("cannot set conditions on Status.ExperimentalConditions field if it isn't %s: %s type detected", metav1ConditionsType.String(), reflect.TypeOf(conditionField.Interface()).String()) } @@ -184,8 +181,6 @@ func setToTypedObject(obj runtime.Object, conditions []metav1.Condition) error { } if conditionField := statusField.FieldByName("Conditions"); conditionField != (reflect.Value{}) { - fmt.Println("Status.Conditions is a", reflect.TypeOf(conditionField.Interface()).String()) - if conditionField.Type() != metav1ConditionsType { return errors.Errorf("cannot set conditions on Status.Conditions field if it isn't %s: %s type detected", metav1ConditionsType.String(), reflect.TypeOf(conditionField.Interface()).String()) } diff --git a/util/conditions/experimental/setter_test.go b/util/conditions/experimental/setter_test.go index c7cb98720a43..7c9543110a38 100644 --- a/util/conditions/experimental/setter_test.go +++ b/util/conditions/experimental/setter_test.go @@ -27,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/cluster-api/internal/test/builder" ) func TestSetAll(t *testing.T) { @@ -59,7 +60,7 @@ func TestSetAll(t *testing.T) { t.Run("fails for nil object", func(t *testing.T) { g := NewWithT(t) - var foo *V1Beta1ResourceWithLegacyConditions + var foo *builder.Phase0Obj conditions := cloneConditions() err := SetAll(foo, conditions) @@ -123,8 +124,8 @@ func TestSetAll(t *testing.T) { t.Run("v1beta object with legacy conditions", func(t *testing.T) { g := NewWithT(t) - foo := &V1Beta1ResourceWithLegacyConditions{ - Status: struct{ Conditions clusterv1.Conditions }{Conditions: nil}, + foo := &builder.Phase0Obj{ + Status: builder.Phase0ObjStatus{Conditions: nil}, } conditions := cloneConditions() @@ -134,11 +135,8 @@ func TestSetAll(t *testing.T) { t.Run("v1beta1 object with both legacy and experimental conditions", func(t *testing.T) { g := NewWithT(t) - foo := &V1Beta1ResourceWithLegacyAndExperimentalConditionsV1{ - Status: struct { - Conditions clusterv1.Conditions - ExperimentalConditions []metav1.Condition - }{ + foo := &builder.Phase1Obj{ + Status: builder.Phase1ObjStatus{ Conditions: clusterv1.Conditions{ { Type: "barCondition", @@ -158,11 +156,8 @@ func TestSetAll(t *testing.T) { t.Run("v1beta1 object with both legacy and experimental conditions / Unstructured", func(t *testing.T) { g := NewWithT(t) - foo := &V1Beta1ResourceWithLegacyAndExperimentalConditionsV1{ - Status: struct { - Conditions clusterv1.Conditions - ExperimentalConditions []metav1.Condition - }{ + foo := &builder.Phase1Obj{ + Status: builder.Phase1ObjStatus{ Conditions: clusterv1.Conditions{ { Type: "barCondition", @@ -182,7 +177,7 @@ func TestSetAll(t *testing.T) { err = SetAll(u, conditions, ConditionFields{"status", "experimentalConditions"}) g.Expect(err).NotTo(HaveOccurred()) - fooFromUnstructured := &V1Beta1ResourceWithLegacyAndExperimentalConditionsV1{} + fooFromUnstructured := &builder.Phase1Obj{} err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), fooFromUnstructured) g.Expect(err).NotTo(HaveOccurred()) @@ -193,13 +188,10 @@ func TestSetAll(t *testing.T) { t.Run("v1beta2 object with conditions and backward compatible conditions", func(t *testing.T) { g := NewWithT(t) - foo := &V1Beta2ResourceWithConditionsAndBackwardCompatibleConditions{ - Status: struct { - Conditions []metav1.Condition - BackwardCompatibility struct{ Conditions clusterv1.Conditions } - }{ + foo := &builder.Phase2Obj{ + Status: builder.Phase2ObjStatus{ Conditions: nil, - BackwardCompatibility: struct{ Conditions clusterv1.Conditions }{ + BackCompatibility: builder.Phase2ObjStatusBackCompatibility{ Conditions: clusterv1.Conditions{ { Type: "barCondition", @@ -219,13 +211,10 @@ func TestSetAll(t *testing.T) { t.Run("v1beta2 object with conditions and backward compatible conditions / Unstructured", func(t *testing.T) { g := NewWithT(t) - foo := &V1Beta2ResourceWithConditionsAndBackwardCompatibleConditions{ - Status: struct { - Conditions []metav1.Condition - BackwardCompatibility struct{ Conditions clusterv1.Conditions } - }{ + foo := &builder.Phase2Obj{ + Status: builder.Phase2ObjStatus{ Conditions: nil, - BackwardCompatibility: struct{ Conditions clusterv1.Conditions }{ + BackCompatibility: builder.Phase2ObjStatusBackCompatibility{ Conditions: clusterv1.Conditions{ { Type: "barCondition", @@ -245,7 +234,7 @@ func TestSetAll(t *testing.T) { err = SetAll(u, conditions, ConditionFields{"status", "conditions"}) g.Expect(err).NotTo(HaveOccurred()) - fooFromUnstructured := &V1Beta2ResourceWithConditionsAndBackwardCompatibleConditions{} + fooFromUnstructured := &builder.Phase2Obj{} err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), fooFromUnstructured) g.Expect(err).NotTo(HaveOccurred()) @@ -256,10 +245,8 @@ func TestSetAll(t *testing.T) { t.Run("v1beta2 object with conditions (end state)", func(t *testing.T) { g := NewWithT(t) - foo := &V1Beta2ResourceWithConditions{ - Status: struct { - Conditions []metav1.Condition - }{ + foo := &builder.Phase3Obj{ + Status: builder.Phase3ObjStatus{ Conditions: nil, }, } @@ -272,10 +259,8 @@ func TestSetAll(t *testing.T) { t.Run("v1beta2 object with conditions (end state) / Unstructured", func(t *testing.T) { g := NewWithT(t) - foo := &V1Beta2ResourceWithConditions{ - Status: struct { - Conditions []metav1.Condition - }{ + foo := &builder.Phase3Obj{ + Status: builder.Phase3ObjStatus{ Conditions: nil, }, } @@ -288,7 +273,7 @@ func TestSetAll(t *testing.T) { err = SetAll(u, conditions, ConditionFields{"status", "conditions"}) g.Expect(err).NotTo(HaveOccurred()) - fooFromUnstructured := &V1Beta2ResourceWithConditions{} + fooFromUnstructured := &builder.Phase3Obj{} err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), fooFromUnstructured) g.Expect(err).NotTo(HaveOccurred()) diff --git a/util/conditions/experimental/summary_test.go b/util/conditions/experimental/summary_test.go index 232f588a999c..e6f01aeb79f1 100644 --- a/util/conditions/experimental/summary_test.go +++ b/util/conditions/experimental/summary_test.go @@ -21,6 +21,8 @@ import ( . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "sigs.k8s.io/cluster-api/internal/test/builder" ) func TestSummary(t *testing.T) { @@ -183,12 +185,12 @@ func TestSummary(t *testing.T) { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) - obj := &V1Beta2ResourceWithConditions{ + obj := &builder.Phase3Obj{ ObjectMeta: metav1.ObjectMeta{ Namespace: metav1.NamespaceDefault, Name: "SourceObject", }, - Status: struct{ Conditions []metav1.Condition }{ + Status: builder.Phase3ObjStatus{ Conditions: tt.conditions, }, }