Skip to content

Commit 6bc8a6a

Browse files
committed
Adding composed.To() method
Signed-off-by: Cyrill Näf <[email protected]>
1 parent bc16c87 commit 6bc8a6a

File tree

2 files changed

+197
-0
lines changed

2 files changed

+197
-0
lines changed

resource/composed/composed.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,38 @@ func New() *Unstructured {
4242
return &Unstructured{unstructured.Unstructured{Object: make(map[string]any)}}
4343
}
4444

45+
// To converts a unstructured composed resource to the provided object.
46+
func To(un *Unstructured, obj interface{}) error {
47+
rt, ok := obj.(runtime.Object)
48+
if !ok {
49+
return errors.New("object is not a compatible runtime.Object")
50+
}
51+
52+
// Get known GVKs for the runtime object type
53+
knownGVKs, _, err := Scheme.ObjectKinds(rt)
54+
if err != nil {
55+
return errors.Errorf("could not retrieve GVKs for the provided object: %v", err)
56+
}
57+
58+
// Check if GVK is known as we should not try to convert it if it doesn't match
59+
gvkMatches := false
60+
for _, knownGVK := range knownGVKs {
61+
if knownGVK == un.GetObjectKind().GroupVersionKind() {
62+
gvkMatches = true
63+
}
64+
}
65+
66+
if !gvkMatches {
67+
return errors.Errorf("GVK %v is not known by the scheme for the provided object type", un.GetObjectKind().GroupVersionKind())
68+
}
69+
70+
err = runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, obj)
71+
if err != nil {
72+
return err
73+
}
74+
return nil
75+
}
76+
4577
// From creates a new unstructured composed resource from the supplied object.
4678
func From(o runtime.Object) (*Unstructured, error) {
4779
// If the supplied object is already unstructured content, avoid a JSON

resource/composed/composed_test.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package composed
1818

1919
import (
20+
"errors"
2021
"fmt"
2122
"testing"
2223

@@ -201,3 +202,167 @@ func TestFrom(t *testing.T) {
201202
})
202203
}
203204
}
205+
206+
func ExampleTo() {
207+
// Add all v1beta1 types to the scheme so that From can automatically
208+
// determine their apiVersion and kind.
209+
v1beta1.AddToScheme(Scheme)
210+
211+
// Create a unstructured object as we would receive by the function (observed/desired).
212+
ub := &Unstructured{Unstructured: unstructured.Unstructured{Object: map[string]any{
213+
"apiVersion": v1beta1.CRDGroupVersion.String(),
214+
"kind": v1beta1.Bucket_Kind,
215+
"metadata": map[string]any{
216+
"name": "cool-bucket",
217+
},
218+
"spec": map[string]any{
219+
"forProvider": map[string]any{
220+
"region": "us-east-2",
221+
},
222+
},
223+
"status": map[string]any{
224+
"observedGeneration": float64(0),
225+
},
226+
}}}
227+
228+
// Create a strongly typed object from the unstructured object.
229+
sb := &v1beta1.Bucket{}
230+
err := To(ub, sb)
231+
if err != nil {
232+
panic(err)
233+
}
234+
// Now you have a strongly typed Bucket object.
235+
objectLock := true
236+
sb.Spec.ForProvider.ObjectLockEnabled = &objectLock
237+
}
238+
239+
// Test the To function
240+
func TestTo(t *testing.T) {
241+
v1beta1.AddToScheme(Scheme)
242+
type args struct {
243+
un *Unstructured
244+
obj interface{}
245+
}
246+
type want struct {
247+
obj interface{}
248+
err error
249+
}
250+
251+
cases := map[string]struct {
252+
reason string
253+
args args
254+
want want
255+
}{
256+
"SuccessfulConversion": {
257+
reason: "A valid unstructured object should convert to a structured object without errors",
258+
args: args{
259+
un: &Unstructured{Unstructured: unstructured.Unstructured{Object: map[string]any{
260+
"apiVersion": v1beta1.CRDGroupVersion.String(),
261+
"kind": v1beta1.Bucket_Kind,
262+
"metadata": map[string]any{
263+
"name": "cool-bucket",
264+
},
265+
"spec": map[string]any{
266+
"forProvider": map[string]any{
267+
"region": "us-east-2",
268+
},
269+
},
270+
"status": map[string]any{
271+
"observedGeneration": float64(0),
272+
},
273+
}}},
274+
obj: &v1beta1.Bucket{},
275+
},
276+
want: want{
277+
obj: &v1beta1.Bucket{
278+
TypeMeta: metav1.TypeMeta{
279+
Kind: v1beta1.Bucket_Kind,
280+
APIVersion: v1beta1.CRDGroupVersion.String(),
281+
},
282+
ObjectMeta: metav1.ObjectMeta{
283+
Name: "cool-bucket",
284+
},
285+
Spec: v1beta1.BucketSpec{
286+
ForProvider: v1beta1.BucketParameters{
287+
Region: ptr.To[string]("us-east-2"),
288+
},
289+
},
290+
},
291+
err: nil,
292+
},
293+
},
294+
"InvalidGVK": {
295+
reason: "An unstructured object with mismatched GVK should result in an error",
296+
args: args{
297+
un: &Unstructured{Unstructured: unstructured.Unstructured{Object: map[string]any{
298+
"apiVersion": "test.example.io",
299+
"kind": "Unknown",
300+
"metadata": map[string]any{
301+
"name": "cool-bucket",
302+
},
303+
"spec": map[string]any{
304+
"forProvider": map[string]any{
305+
"region": "us-east-2",
306+
},
307+
},
308+
"status": map[string]any{
309+
"observedGeneration": float64(0),
310+
},
311+
}}},
312+
obj: &v1beta1.Bucket{},
313+
},
314+
want: want{
315+
obj: &v1beta1.Bucket{},
316+
err: errors.New("GVK /test.example.io, Kind=Unknown is not known by the scheme for the provided object type"),
317+
},
318+
},
319+
"NoRuntimeObject": {
320+
reason: "Should only convert to a object if the object is a runtime.Object",
321+
args: args{
322+
un: &Unstructured{Unstructured: unstructured.Unstructured{Object: map[string]any{
323+
"apiVersion": v1beta1.CRDGroupVersion.String(),
324+
"kind": v1beta1.Bucket_Kind,
325+
"metadata": map[string]any{
326+
"name": "cool-bucket",
327+
},
328+
}}},
329+
obj: "not-a-runtime-object",
330+
},
331+
want: want{
332+
obj: string("not-a-runtime-object"),
333+
err: errors.New("object is not a compatible runtime.Object"),
334+
},
335+
},
336+
}
337+
338+
for name, tc := range cases {
339+
t.Run(name, func(t *testing.T) {
340+
err := To(tc.args.un, tc.args.obj)
341+
342+
// Compare the resulting object with the expected one
343+
if diff := cmp.Diff(tc.want.obj, tc.args.obj); diff != "" {
344+
t.Errorf("\n%s\nTo(...): -want, +got:\n%s", tc.reason, diff)
345+
}
346+
// Compare the error with the expected error
347+
if diff := cmp.Diff(tc.want.err, err, EquateErrors()); diff != "" {
348+
t.Errorf("\n%s\nTo(...): -want error, +got error:\n%s", tc.reason, diff)
349+
}
350+
})
351+
}
352+
}
353+
354+
// EquateErrors returns true if the supplied errors are of the same type and
355+
// produce identical strings. This mirrors the error comparison behaviour of
356+
// https://github.com/go-test/deep,
357+
//
358+
// This differs from cmpopts.EquateErrors, which does not test for error strings
359+
// and instead returns whether one error 'is' (in the errors.Is sense) the
360+
// other.
361+
func EquateErrors() cmp.Option {
362+
return cmp.Comparer(func(a, b error) bool {
363+
if a == nil || b == nil {
364+
return a == nil && b == nil
365+
}
366+
return a.Error() == b.Error()
367+
})
368+
}

0 commit comments

Comments
 (0)