From a69fa810d0dee32fbcb137aa29b5ba3d3282453a Mon Sep 17 00:00:00 2001 From: Erik Godding Boye Date: Sat, 14 Oct 2023 15:17:43 +0200 Subject: [PATCH] chore: convert v2alpha1 SubNamespace status to kstatus-compatible subresource --- Makefile | 2 +- api/v1/conversion_test.go | 34 +++++ api/v1/subnamespace_conversion.go | 108 ++++++++++++++++ api/v1/subnamespace_conversion_test.go | 57 +++++++++ api/v2alpha1/subnamespace_conversion.go | 4 + api/v2alpha1/subnamespace_types.go | 17 ++- api/v2alpha1/zz_generated.deepcopy.go | 24 ++++ .../accurate.cybozu.com_subnamespaces.yaml | 59 ++++++++- .../accurate.cybozu.com_subnamespaces.yaml | 71 +++++++++- go.mod | 5 +- go.sum | 2 + internal/util/conversion/conversion.go | 121 ++++++++++++++++++ pkg/constants/meta.go | 8 ++ 13 files changed, 495 insertions(+), 17 deletions(-) create mode 100644 api/v1/conversion_test.go create mode 100644 api/v1/subnamespace_conversion.go create mode 100644 api/v1/subnamespace_conversion_test.go create mode 100644 api/v2alpha1/subnamespace_conversion.go create mode 100644 internal/util/conversion/conversion.go diff --git a/Makefile b/Makefile index 7513d62..002e15e 100644 --- a/Makefile +++ b/Makefile @@ -77,7 +77,7 @@ envtest: setup-envtest .PHONY: test test: test-tools - go test -v -count 1 -race ./pkg/... + go test -v -count 1 -race ./api/... ./pkg/... go install ./... go vet ./... test -z $$(gofmt -s -l . | tee /dev/stderr) diff --git a/api/v1/conversion_test.go b/api/v1/conversion_test.go new file mode 100644 index 0000000..7596a9a --- /dev/null +++ b/api/v1/conversion_test.go @@ -0,0 +1,34 @@ +package v1 + +import ( + "testing" + + accuratev2alpha1 "github.com/cybozu-go/accurate/api/v2alpha1" + utilconversion "github.com/cybozu-go/accurate/internal/util/conversion" + fuzz "github.com/google/gofuzz" + "k8s.io/apimachinery/pkg/api/apitesting/fuzzer" + runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" +) + +func TestFuzzyConversion(t *testing.T) { + t.Run("for SubNamespace", utilconversion.FuzzTestFunc(utilconversion.FuzzTestFuncInput{ + Hub: &accuratev2alpha1.SubNamespace{}, + Spoke: &SubNamespace{}, + FuzzerFuncs: []fuzzer.FuzzerFuncs{SubNamespaceStatusFuzzFunc}, + })) +} + +func SubNamespaceStatusFuzzFunc(_ runtimeserializer.CodecFactory) []interface{} { + return []interface{}{ + SubNamespaceStatusFuzzer, + } +} + +func SubNamespaceStatusFuzzer(in *SubNamespace, c fuzz.Continue) { + c.FuzzNoCustom(in) + + // The status is just a string in v1, and the controller is the sole actor updating status. + // As long as we make the controller reconcile v2alpha1, and also makes it the stored version, + // we will never need to convert status from v1 to v2alpha1. + in.Status = "" +} diff --git a/api/v1/subnamespace_conversion.go b/api/v1/subnamespace_conversion.go new file mode 100644 index 0000000..76780d6 --- /dev/null +++ b/api/v1/subnamespace_conversion.go @@ -0,0 +1,108 @@ +package v1 + +import ( + "encoding/json" + "fmt" + "strconv" + + accuratev2alpha1 "github.com/cybozu-go/accurate/api/v2alpha1" + "github.com/cybozu-go/accurate/pkg/constants" + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/api/meta" + kstatus "sigs.k8s.io/cli-utils/pkg/kstatus/status" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/conversion" +) + +// ConvertTo converts this SubNamespace to the Hub version (v2alpha1). +func (src *SubNamespace) ConvertTo(dstRaw conversion.Hub) error { + dst := dstRaw.(*accuratev2alpha1.SubNamespace) + + logger := getConversionLogger(src).WithValues( + "source", GroupVersion.Version, + "destination", GroupVersion.Version, + ) + logger.V(5).Info("converting") + + dst.ObjectMeta = src.ObjectMeta + dst.Spec.Annotations = src.Spec.Annotations + dst.Spec.Labels = src.Spec.Labels + + // Restore info from annotations to ensure conversions are lossy-less. + // Delete annotation after processing it to avoid polluting converted resource. + if v, ok := dst.Annotations[constants.AnnObservedGeneration]; ok { + obsGen, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return fmt.Errorf("error converting %q to int64 from annotation %s", v, constants.AnnObservedGeneration) + } + dst.Status.ObservedGeneration = obsGen + + delete(dst.Annotations, constants.AnnObservedGeneration) + } + if conds, ok := dst.Annotations[constants.AnnConditions]; ok { + err := json.Unmarshal([]byte(conds), &dst.Status.Conditions) + if err != nil { + return fmt.Errorf("error unmarshalling JSON from annotation %s", constants.AnnConditions) + } + + delete(dst.Annotations, constants.AnnConditions) + } + + return nil +} + +// ConvertFrom converts from the Hub version (v2alpha1) to this version. +func (dst *SubNamespace) ConvertFrom(srcRaw conversion.Hub) error { + src := srcRaw.(*accuratev2alpha1.SubNamespace) + + logger := getConversionLogger(src).WithValues( + "source", GroupVersion.Version, + "destination", GroupVersion.Version, + ) + logger.V(5).Info("converting") + + dst.ObjectMeta = src.ObjectMeta + dst.Spec.Annotations = src.Spec.Annotations + dst.Spec.Labels = src.Spec.Labels + + switch { + case meta.IsStatusConditionTrue(src.Status.Conditions, string(kstatus.ConditionStalled)): + dst.Status = SubNamespaceConflict + case src.Status.ObservedGeneration == 0: + // SubNamespace has never been reconciled. + case src.Status.ObservedGeneration == src.Generation && len(src.Status.Conditions) == 0: + dst.Status = SubNamespaceOK + default: + // SubNamespace is in some transitional state, not possible to represent in v1 status. + // An unset value is probably our best option. + } + + // Store info in annotations to ensure conversions are lossy-less. + if dst.Annotations == nil { + dst.Annotations = make(map[string]string) + } + if src.Status.ObservedGeneration != 0 { + dst.Annotations[constants.AnnObservedGeneration] = strconv.FormatInt(src.Status.ObservedGeneration, 10) + } + if len(src.Status.Conditions) > 0 { + buf, err := json.Marshal(src.Status.Conditions) + if err != nil { + return fmt.Errorf("error marshalling conditions to JSON") + } + dst.Annotations[constants.AnnConditions] = string(buf) + } + if len(dst.Annotations) == 0 { + dst.Annotations = nil + } + + return nil +} + +func getConversionLogger(obj client.Object) logr.Logger { + return ctrl.Log.WithName("conversion").WithValues( + "kind", "SubNamespace", + "namespace", obj.GetNamespace(), + "name", obj.GetName(), + ) +} diff --git a/api/v1/subnamespace_conversion_test.go b/api/v1/subnamespace_conversion_test.go new file mode 100644 index 0000000..4158500 --- /dev/null +++ b/api/v1/subnamespace_conversion_test.go @@ -0,0 +1,57 @@ +package v1 + +import ( + accuratev2alpha1 "github.com/cybozu-go/accurate/api/v2alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kstatus "sigs.k8s.io/cli-utils/pkg/kstatus/status" + "testing" +) + +func TestSubNamespace_ConvertFrom(t *testing.T) { + tests := map[string]struct { + src *accuratev2alpha1.SubNamespace + expStatus SubNamespaceStatus + wantErr bool + }{ + "if SubNamespace has never been reconciled, status should have zero-value": { + src: newSubNamespaceWithStatus(0, 0), + }, + "if SubNamespace spec is updated, but not yet reconciled, status should have zero-value": { + src: newSubNamespaceWithStatus(2, 1), + }, + "if SubNamespace is reconciled successfully, status should be ok": { + src: newSubNamespaceWithStatus(1, 1), + expStatus: SubNamespaceOK, + }, + "if SubNamespace is reconciled with errors, status should be conflict": { + src: newSubNamespaceWithStatus(1, 1, newStalledCondition()), + expStatus: SubNamespaceConflict, + }, + } + for n, tt := range tests { + t.Run(n, func(t *testing.T) { + dst := &SubNamespace{} + if err := dst.ConvertFrom(tt.src); (err != nil) != tt.wantErr { + t.Errorf("ConvertFrom() error = %v, wantErr %v", err, tt.wantErr) + } + if dst.Status != tt.expStatus { + t.Errorf("ConvertFrom() status = %q, expStatus %q", dst.Status, tt.expStatus) + } + }) + } +} + +func newSubNamespaceWithStatus(gen, obsGen int, conds ...metav1.Condition) *accuratev2alpha1.SubNamespace { + subNS := &accuratev2alpha1.SubNamespace{} + subNS.Generation = int64(gen) + subNS.Status.ObservedGeneration = int64(obsGen) + subNS.Status.Conditions = conds + return subNS +} + +func newStalledCondition() metav1.Condition { + return metav1.Condition{ + Type: string(kstatus.ConditionStalled), + Status: metav1.ConditionTrue, + } +} diff --git a/api/v2alpha1/subnamespace_conversion.go b/api/v2alpha1/subnamespace_conversion.go new file mode 100644 index 0000000..28560c8 --- /dev/null +++ b/api/v2alpha1/subnamespace_conversion.go @@ -0,0 +1,4 @@ +package v2alpha1 + +// Hub marks this SubNamespace version as a conversion hub. +func (*SubNamespace) Hub() {} diff --git a/api/v2alpha1/subnamespace_types.go b/api/v2alpha1/subnamespace_types.go index bbbdcb0..e899a2e 100644 --- a/api/v2alpha1/subnamespace_types.go +++ b/api/v2alpha1/subnamespace_types.go @@ -8,13 +8,17 @@ import ( // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. // SubNamespaceStatus defines the observed state of SubNamespace -// +kubebuilder:validation:Enum=ok;conflict -type SubNamespaceStatus string +type SubNamespaceStatus struct { + // The generation observed by the object controller. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` -const ( - SubNamespaceOK = SubNamespaceStatus("ok") - SubNamespaceConflict = SubNamespaceStatus("conflict") -) + // Conditions represent the latest available observations of an object's state + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} // SubNamespaceSpec defines the desired state of SubNamespace type SubNamespaceSpec struct { @@ -30,6 +34,7 @@ type SubNamespaceSpec struct { // Keeping this version un-served for now //+kubebuilder:unservedversion //+kubebuilder:object:root=true +//+kubebuilder:subresource:status // SubNamespace is the Schema for the subnamespaces API type SubNamespace struct { diff --git a/api/v2alpha1/zz_generated.deepcopy.go b/api/v2alpha1/zz_generated.deepcopy.go index 593fd3a..fa1b1cb 100644 --- a/api/v2alpha1/zz_generated.deepcopy.go +++ b/api/v2alpha1/zz_generated.deepcopy.go @@ -6,6 +6,7 @@ package v2alpha1 import ( + "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -15,6 +16,7 @@ func (in *SubNamespace) DeepCopyInto(out *SubNamespace) { 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 SubNamespace. @@ -95,3 +97,25 @@ func (in *SubNamespaceSpec) DeepCopy() *SubNamespaceSpec { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SubNamespaceStatus) DeepCopyInto(out *SubNamespaceStatus) { + *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 SubNamespaceStatus. +func (in *SubNamespaceStatus) DeepCopy() *SubNamespaceStatus { + if in == nil { + return nil + } + out := new(SubNamespaceStatus) + in.DeepCopyInto(out) + return out +} diff --git a/charts/accurate/crds/accurate.cybozu.com_subnamespaces.yaml b/charts/accurate/crds/accurate.cybozu.com_subnamespaces.yaml index 7537c1e..5a72b20 100644 --- a/charts/accurate/crds/accurate.cybozu.com_subnamespaces.yaml +++ b/charts/accurate/crds/accurate.cybozu.com_subnamespaces.yaml @@ -80,10 +80,61 @@ spec: type: object status: description: Status is the status of SubNamespace. - enum: - - ok - - conflict - type: string + properties: + conditions: + description: Conditions represent the latest available observations of an object's state + items: + description: Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. + format: date-time + type: string + message: + description: message is a human readable message indicating details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x]. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating the reason for the condition's last transition. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + observedGeneration: + description: The generation observed by the object controller. + format: int64 + type: integer + type: object type: object served: false storage: false + subresources: + status: {} diff --git a/config/crd/bases/accurate.cybozu.com_subnamespaces.yaml b/config/crd/bases/accurate.cybozu.com_subnamespaces.yaml index c29e953..34dfb01 100644 --- a/config/crd/bases/accurate.cybozu.com_subnamespaces.yaml +++ b/config/crd/bases/accurate.cybozu.com_subnamespaces.yaml @@ -89,10 +89,73 @@ spec: type: object status: description: Status is the status of SubNamespace. - enum: - - ok - - conflict - type: string + properties: + conditions: + description: Conditions represent the latest available observations + of an object's state + items: + description: Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x]. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + observedGeneration: + description: The generation observed by the object controller. + format: int64 + type: integer + type: object type: object served: false storage: false + subresources: + status: {} diff --git a/go.mod b/go.mod index cad343e..af32fde 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,9 @@ module github.com/cybozu-go/accurate go 1.21 require ( + github.com/go-logr/logr v1.2.4 github.com/google/go-cmp v0.5.9 + github.com/google/gofuzz v1.2.0 github.com/onsi/ginkgo/v2 v2.11.0 github.com/onsi/gomega v1.27.10 github.com/spf13/cobra v1.7.0 @@ -15,6 +17,7 @@ require ( k8s.io/client-go v0.28.2 k8s.io/component-base v0.28.2 k8s.io/klog/v2 v2.100.1 + sigs.k8s.io/cli-utils v0.35.0 sigs.k8s.io/controller-runtime v0.16.2 sigs.k8s.io/yaml v1.3.0 ) @@ -29,7 +32,6 @@ require ( github.com/evanphx/json-patch/v5 v5.7.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-errors/errors v1.4.2 // indirect - github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/zapr v1.2.4 // indirect github.com/go-openapi/jsonpointer v0.20.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect @@ -40,7 +42,6 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.3.1 // indirect diff --git a/go.sum b/go.sum index 25329af..652ad2f 100644 --- a/go.sum +++ b/go.sum @@ -324,6 +324,8 @@ k8s.io/kube-openapi v0.0.0-20230918164632-68afd615200d h1:/CFeJBjBrZvHX09rObS2+2 k8s.io/kube-openapi v0.0.0-20230918164632-68afd615200d/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/cli-utils v0.35.0 h1:dfSJaF1W0frW74PtjwiyoB4cwdRygbHnC7qe7HF0g/Y= +sigs.k8s.io/cli-utils v0.35.0/go.mod h1:ITitykCJxP1vaj1Cew/FZEaVJ2YsTN9Q71m02jebkoE= sigs.k8s.io/controller-runtime v0.16.2 h1:mwXAVuEk3EQf478PQwQ48zGOXvW27UJc8NHktQVuIPU= sigs.k8s.io/controller-runtime v0.16.2/go.mod h1:vpMu3LpI5sYWtujJOa2uPK61nB5rbwlN7BAB8aSLvGU= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= diff --git a/internal/util/conversion/conversion.go b/internal/util/conversion/conversion.go new file mode 100644 index 0000000..6c6a80c --- /dev/null +++ b/internal/util/conversion/conversion.go @@ -0,0 +1,121 @@ +package conversion + +import ( + "math/rand" + "testing" + + "github.com/google/go-cmp/cmp" + fuzz "github.com/google/gofuzz" + "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/apitesting/fuzzer" + apiequality "k8s.io/apimachinery/pkg/api/equality" + metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/conversion" +) + +// GetFuzzer returns a new fuzzer to be used for testing. +func GetFuzzer(scheme *runtime.Scheme, funcs ...fuzzer.FuzzerFuncs) *fuzz.Fuzzer { + funcs = append([]fuzzer.FuzzerFuncs{ + metafuzzer.Funcs, + func(_ runtimeserializer.CodecFactory) []interface{} { + return []interface{}{ + // Custom fuzzer for metav1.Time pointers which weren't + // fuzzed and always resulted in `nil` values. + // This implementation is somewhat similar to the one provided + // in the metafuzzer.Funcs. + func(input *metav1.Time, c fuzz.Continue) { + if input != nil { + var sec, nsec uint32 + c.Fuzz(&sec) + c.Fuzz(&nsec) + fuzzed := metav1.Unix(int64(sec), int64(nsec)).Rfc3339Copy() + input.Time = fuzzed.Time + } + }, + } + }, + }, funcs...) + return fuzzer.FuzzerFor( + fuzzer.MergeFuzzerFuncs(funcs...), + rand.NewSource(rand.Int63()), //nolint:gosec + runtimeserializer.NewCodecFactory(scheme), + ) +} + +// FuzzTestFuncInput contains input parameters +// for the FuzzTestFunc function. +type FuzzTestFuncInput struct { + Scheme *runtime.Scheme + + Hub conversion.Hub + HubAfterMutation func(conversion.Hub) + + Spoke conversion.Convertible + SpokeAfterMutation func(convertible conversion.Convertible) + + FuzzerFuncs []fuzzer.FuzzerFuncs +} + +// FuzzTestFunc returns a new testing function to be used in tests to make sure conversions between +// the Hub version of an object and an older version aren't lossy. +func FuzzTestFunc(input FuzzTestFuncInput) func(*testing.T) { + if input.Scheme == nil { + input.Scheme = scheme.Scheme + } + + return func(t *testing.T) { + t.Helper() + t.Run("spoke-hub-spoke", func(t *testing.T) { + g := gomega.NewWithT(t) + fzr := GetFuzzer(input.Scheme, input.FuzzerFuncs...) + + for i := 0; i < 10000; i++ { + // Create the spoke and fuzz it + spokeBefore := input.Spoke.DeepCopyObject().(conversion.Convertible) + fzr.Fuzz(spokeBefore) + + // First convert spoke to hub + hubCopy := input.Hub.DeepCopyObject().(conversion.Hub) + g.Expect(spokeBefore.ConvertTo(hubCopy)).To(gomega.Succeed()) + + // Convert hub back to spoke and check if the resulting spoke is equal to the spoke before the round trip + spokeAfter := input.Spoke.DeepCopyObject().(conversion.Convertible) + g.Expect(spokeAfter.ConvertFrom(hubCopy)).To(gomega.Succeed()) + + if input.SpokeAfterMutation != nil { + input.SpokeAfterMutation(spokeAfter) + } + + g.Expect(apiequality.Semantic.DeepEqual(spokeBefore, spokeAfter)).To(gomega.BeTrue(), cmp.Diff(spokeBefore, spokeAfter)) + } + }) + t.Run("hub-spoke-hub", func(t *testing.T) { + g := gomega.NewWithT(t) + fzr := GetFuzzer(input.Scheme, input.FuzzerFuncs...) + + for i := 0; i < 10000; i++ { + // Create the hub and fuzz it + hubBefore := input.Hub.DeepCopyObject().(conversion.Hub) + fzr.Fuzz(hubBefore) + + // First convert hub to spoke + dstCopy := input.Spoke.DeepCopyObject().(conversion.Convertible) + g.Expect(dstCopy.ConvertFrom(hubBefore)).To(gomega.Succeed()) + + // Convert spoke back to hub and check if the resulting hub is equal to the hub before the round trip + hubAfter := input.Hub.DeepCopyObject().(conversion.Hub) + g.Expect(dstCopy.ConvertTo(hubAfter)).To(gomega.Succeed()) + + if input.HubAfterMutation != nil { + input.HubAfterMutation(hubAfter) + } + + g.Expect(apiequality.Semantic.DeepEqual(hubBefore, hubAfter)).To(gomega.BeTrue(), cmp.Diff(hubBefore, hubAfter)) + } + }) + } +} diff --git a/pkg/constants/meta.go b/pkg/constants/meta.go index 5d21887..34a9a1e 100644 --- a/pkg/constants/meta.go +++ b/pkg/constants/meta.go @@ -33,3 +33,11 @@ const ( PropagateUpdate = "update" PropagateAny = "any" // defined as an in-memory index value ) + +// InternalMetaPrefix is the MetaPrefix for internal (not user-facing) annotations of Accurate. +const InternalMetaPrefix = "internal.accurate.cybozu.com/" + +const ( + AnnObservedGeneration = InternalMetaPrefix + "observed-generation" + AnnConditions = InternalMetaPrefix + "conditions" +)