From 6bc8a6a3d82536c533e756a0b819f9abed4067f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cyrill=20N=C3=A4f?= Date: Mon, 8 Jul 2024 11:45:25 +0000 Subject: [PATCH 1/3] Adding composed.To() method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Cyrill Näf --- resource/composed/composed.go | 32 ++++++ resource/composed/composed_test.go | 165 +++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+) diff --git a/resource/composed/composed.go b/resource/composed/composed.go index 26e54a0..57495d8 100644 --- a/resource/composed/composed.go +++ b/resource/composed/composed.go @@ -42,6 +42,38 @@ func New() *Unstructured { return &Unstructured{unstructured.Unstructured{Object: make(map[string]any)}} } +// To converts a unstructured composed resource to the provided object. +func To(un *Unstructured, obj interface{}) error { + rt, ok := obj.(runtime.Object) + if !ok { + return errors.New("object is not a compatible runtime.Object") + } + + // Get known GVKs for the runtime object type + knownGVKs, _, err := Scheme.ObjectKinds(rt) + if err != nil { + return errors.Errorf("could not retrieve GVKs for the provided object: %v", err) + } + + // Check if GVK is known as we should not try to convert it if it doesn't match + gvkMatches := false + for _, knownGVK := range knownGVKs { + if knownGVK == un.GetObjectKind().GroupVersionKind() { + gvkMatches = true + } + } + + if !gvkMatches { + return errors.Errorf("GVK %v is not known by the scheme for the provided object type", un.GetObjectKind().GroupVersionKind()) + } + + err = runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, obj) + if err != nil { + return err + } + return nil +} + // From creates a new unstructured composed resource from the supplied object. func From(o runtime.Object) (*Unstructured, error) { // If the supplied object is already unstructured content, avoid a JSON diff --git a/resource/composed/composed_test.go b/resource/composed/composed_test.go index fd952d6..13c5ceb 100644 --- a/resource/composed/composed_test.go +++ b/resource/composed/composed_test.go @@ -17,6 +17,7 @@ limitations under the License. package composed import ( + "errors" "fmt" "testing" @@ -201,3 +202,167 @@ func TestFrom(t *testing.T) { }) } } + +func ExampleTo() { + // Add all v1beta1 types to the scheme so that From can automatically + // determine their apiVersion and kind. + v1beta1.AddToScheme(Scheme) + + // Create a unstructured object as we would receive by the function (observed/desired). + ub := &Unstructured{Unstructured: unstructured.Unstructured{Object: map[string]any{ + "apiVersion": v1beta1.CRDGroupVersion.String(), + "kind": v1beta1.Bucket_Kind, + "metadata": map[string]any{ + "name": "cool-bucket", + }, + "spec": map[string]any{ + "forProvider": map[string]any{ + "region": "us-east-2", + }, + }, + "status": map[string]any{ + "observedGeneration": float64(0), + }, + }}} + + // Create a strongly typed object from the unstructured object. + sb := &v1beta1.Bucket{} + err := To(ub, sb) + if err != nil { + panic(err) + } + // Now you have a strongly typed Bucket object. + objectLock := true + sb.Spec.ForProvider.ObjectLockEnabled = &objectLock +} + +// Test the To function +func TestTo(t *testing.T) { + v1beta1.AddToScheme(Scheme) + type args struct { + un *Unstructured + obj interface{} + } + type want struct { + obj interface{} + err error + } + + cases := map[string]struct { + reason string + args args + want want + }{ + "SuccessfulConversion": { + reason: "A valid unstructured object should convert to a structured object without errors", + args: args{ + un: &Unstructured{Unstructured: unstructured.Unstructured{Object: map[string]any{ + "apiVersion": v1beta1.CRDGroupVersion.String(), + "kind": v1beta1.Bucket_Kind, + "metadata": map[string]any{ + "name": "cool-bucket", + }, + "spec": map[string]any{ + "forProvider": map[string]any{ + "region": "us-east-2", + }, + }, + "status": map[string]any{ + "observedGeneration": float64(0), + }, + }}}, + obj: &v1beta1.Bucket{}, + }, + want: want{ + obj: &v1beta1.Bucket{ + TypeMeta: metav1.TypeMeta{ + Kind: v1beta1.Bucket_Kind, + APIVersion: v1beta1.CRDGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "cool-bucket", + }, + Spec: v1beta1.BucketSpec{ + ForProvider: v1beta1.BucketParameters{ + Region: ptr.To[string]("us-east-2"), + }, + }, + }, + err: nil, + }, + }, + "InvalidGVK": { + reason: "An unstructured object with mismatched GVK should result in an error", + args: args{ + un: &Unstructured{Unstructured: unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "test.example.io", + "kind": "Unknown", + "metadata": map[string]any{ + "name": "cool-bucket", + }, + "spec": map[string]any{ + "forProvider": map[string]any{ + "region": "us-east-2", + }, + }, + "status": map[string]any{ + "observedGeneration": float64(0), + }, + }}}, + obj: &v1beta1.Bucket{}, + }, + want: want{ + obj: &v1beta1.Bucket{}, + err: errors.New("GVK /test.example.io, Kind=Unknown is not known by the scheme for the provided object type"), + }, + }, + "NoRuntimeObject": { + reason: "Should only convert to a object if the object is a runtime.Object", + args: args{ + un: &Unstructured{Unstructured: unstructured.Unstructured{Object: map[string]any{ + "apiVersion": v1beta1.CRDGroupVersion.String(), + "kind": v1beta1.Bucket_Kind, + "metadata": map[string]any{ + "name": "cool-bucket", + }, + }}}, + obj: "not-a-runtime-object", + }, + want: want{ + obj: string("not-a-runtime-object"), + err: errors.New("object is not a compatible runtime.Object"), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + err := To(tc.args.un, tc.args.obj) + + // Compare the resulting object with the expected one + if diff := cmp.Diff(tc.want.obj, tc.args.obj); diff != "" { + t.Errorf("\n%s\nTo(...): -want, +got:\n%s", tc.reason, diff) + } + // Compare the error with the expected error + if diff := cmp.Diff(tc.want.err, err, EquateErrors()); diff != "" { + t.Errorf("\n%s\nTo(...): -want error, +got error:\n%s", tc.reason, diff) + } + }) + } +} + +// EquateErrors returns true if the supplied errors are of the same type and +// produce identical strings. This mirrors the error comparison behaviour of +// https://github.com/go-test/deep, +// +// This differs from cmpopts.EquateErrors, which does not test for error strings +// and instead returns whether one error 'is' (in the errors.Is sense) the +// other. +func EquateErrors() cmp.Option { + return cmp.Comparer(func(a, b error) bool { + if a == nil || b == nil { + return a == nil && b == nil + } + return a.Error() == b.Error() + }) +} From 92b9428a78b87c14ef4f21b785084074ae29211e Mon Sep 17 00:00:00 2001 From: bakito Date: Sun, 21 Jul 2024 14:54:51 +0200 Subject: [PATCH 2/3] use generics Signed-off-by: bakito --- resource/composed/composed.go | 8 ++------ resource/composed/composed_test.go | 19 +------------------ 2 files changed, 3 insertions(+), 24 deletions(-) diff --git a/resource/composed/composed.go b/resource/composed/composed.go index 57495d8..0cce9e1 100644 --- a/resource/composed/composed.go +++ b/resource/composed/composed.go @@ -43,14 +43,10 @@ func New() *Unstructured { } // To converts a unstructured composed resource to the provided object. -func To(un *Unstructured, obj interface{}) error { - rt, ok := obj.(runtime.Object) - if !ok { - return errors.New("object is not a compatible runtime.Object") - } +func To[T runtime.Object](un *Unstructured, obj T) error { // Get known GVKs for the runtime object type - knownGVKs, _, err := Scheme.ObjectKinds(rt) + knownGVKs, _, err := Scheme.ObjectKinds(obj) if err != nil { return errors.Errorf("could not retrieve GVKs for the provided object: %v", err) } diff --git a/resource/composed/composed_test.go b/resource/composed/composed_test.go index 13c5ceb..ebf6241 100644 --- a/resource/composed/composed_test.go +++ b/resource/composed/composed_test.go @@ -241,7 +241,7 @@ func TestTo(t *testing.T) { v1beta1.AddToScheme(Scheme) type args struct { un *Unstructured - obj interface{} + obj runtime.Object } type want struct { obj interface{} @@ -316,23 +316,6 @@ func TestTo(t *testing.T) { err: errors.New("GVK /test.example.io, Kind=Unknown is not known by the scheme for the provided object type"), }, }, - "NoRuntimeObject": { - reason: "Should only convert to a object if the object is a runtime.Object", - args: args{ - un: &Unstructured{Unstructured: unstructured.Unstructured{Object: map[string]any{ - "apiVersion": v1beta1.CRDGroupVersion.String(), - "kind": v1beta1.Bucket_Kind, - "metadata": map[string]any{ - "name": "cool-bucket", - }, - }}}, - obj: "not-a-runtime-object", - }, - want: want{ - obj: string("not-a-runtime-object"), - err: errors.New("object is not a compatible runtime.Object"), - }, - }, } for name, tc := range cases { From 90cd4161c3841e0811d3bda3ffdf36fbd47c6f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cyrill=20N=C3=A4f?= Date: Thu, 10 Oct 2024 07:37:26 +0200 Subject: [PATCH 3/3] Simplify error handling Simplify error handling by directly returning the error Co-authored-by: Nic Cope --- resource/composed/composed.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/resource/composed/composed.go b/resource/composed/composed.go index 0cce9e1..b0864d6 100644 --- a/resource/composed/composed.go +++ b/resource/composed/composed.go @@ -63,11 +63,7 @@ func To[T runtime.Object](un *Unstructured, obj T) error { return errors.Errorf("GVK %v is not known by the scheme for the provided object type", un.GetObjectKind().GroupVersionKind()) } - err = runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, obj) - if err != nil { - return err - } - return nil + return runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, obj) } // From creates a new unstructured composed resource from the supplied object.