@@ -17,19 +17,29 @@ limitations under the License.
17
17
package unstructured_test
18
18
19
19
import (
20
+ "bytes"
21
+ "math/big"
20
22
"math/rand"
23
+ "os"
21
24
"reflect"
25
+ "strconv"
26
+ "strings"
22
27
"testing"
28
+ "time"
23
29
24
30
"github.com/google/go-cmp/cmp"
31
+ fuzz "github.com/google/gofuzz"
25
32
"github.com/stretchr/testify/assert"
33
+
26
34
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
27
35
"k8s.io/apimachinery/pkg/api/equality"
28
36
metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer"
29
37
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30
38
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
31
39
"k8s.io/apimachinery/pkg/runtime"
32
40
"k8s.io/apimachinery/pkg/runtime/serializer"
41
+ cborserializer "k8s.io/apimachinery/pkg/runtime/serializer/cbor"
42
+ jsonserializer "k8s.io/apimachinery/pkg/runtime/serializer/json"
33
43
)
34
44
35
45
func TestNilUnstructuredContent (t * testing.T ) {
@@ -118,6 +128,18 @@ func TestUnstructuredMetadataOmitempty(t *testing.T) {
118
128
}
119
129
}
120
130
131
+ // TestRoundTripJSONCBORUnstructured performs fuzz testing for roundtrip for
132
+ // unstructured object between JSON and CBOR
133
+ func TestRoundTripJSONCBORUnstructured (t * testing.T ) {
134
+ roundtripType [* unstructured.Unstructured ](t )
135
+ }
136
+
137
+ // TestRoundTripJSONCBORUnstructuredList performs fuzz testing for roundtrip for
138
+ // unstructuredList object between JSON and CBOR
139
+ func TestRoundTripJSONCBORUnstructuredList (t * testing.T ) {
140
+ roundtripType [* unstructured.UnstructuredList ](t )
141
+ }
142
+
121
143
func setObjectMeta (u * unstructured.Unstructured , objectMeta * metav1.ObjectMeta ) error {
122
144
if objectMeta == nil {
123
145
unstructured .RemoveNestedField (u .UnstructuredContent (), "metadata" )
@@ -148,3 +170,307 @@ func setObjectMetaUsingAccessors(u, uCopy *unstructured.Unstructured) {
148
170
uCopy .SetFinalizers (u .GetFinalizers ())
149
171
uCopy .SetManagedFields (u .GetManagedFields ())
150
172
}
173
+
174
+ // roundtripType performs fuzz testing for roundtrip conversion for
175
+ // unstructured or unstructuredList object between two formats (A and B) in forward
176
+ // and backward directions
177
+ // Original and final unstructured/list are compared along with all intermediate ones
178
+ func roundtripType [U runtime.Unstructured ](t * testing.T ) {
179
+ scheme := runtime .NewScheme ()
180
+ fuzzer := fuzzer .FuzzerFor (fuzzer .MergeFuzzerFuncs (metafuzzer .Funcs , unstructuredFuzzerFuncs ), rand .NewSource (getSeed (t )), serializer .NewCodecFactory (scheme ))
181
+
182
+ jS := jsonserializer .NewSerializerWithOptions (jsonserializer .DefaultMetaFactory , scheme , scheme , jsonserializer.SerializerOptions {})
183
+ cS := cborserializer .NewSerializer (scheme , scheme )
184
+
185
+ for i := 0 ; i < 50 ; i ++ {
186
+ original := reflect .New (reflect .TypeFor [U ]().Elem ()).Interface ().(runtime.Unstructured )
187
+ fuzzer .Fuzz (original )
188
+ // unstructured -> JSON > unstructured > CBOR -> unstructured -> JSON -> unstructured
189
+ roundtrip (t , original , jS , cS )
190
+ // unstructured -> CBOR > unstructured > JSON -> unstructured -> CBOR -> unstructured
191
+ roundtrip (t , original , cS , jS )
192
+ }
193
+ }
194
+
195
+ // roundtrip tests that an Unstructured object roundtrips faithfully along the
196
+ // sequence Unstructured -> A -> Unstructured -> B -> Unstructured -> A -> Unstructured,
197
+ // given serializers for two encodings A and B. The final object and both intermediate
198
+ // objects must all be equal to the original.
199
+ func roundtrip (t * testing.T , original runtime.Unstructured , a , b runtime.Serializer ) {
200
+ var buf bytes.Buffer
201
+
202
+ buf .Reset ()
203
+ // (original) Unstructured -> A
204
+ if err := a .Encode (original , & buf ); err != nil {
205
+ t .Fatalf ("error encoding original unstructured to A: %v" , err )
206
+ }
207
+ // A -> intermediate unstructured
208
+ uA := reflect .New (reflect .TypeOf (original ).Elem ()).Interface ().(runtime.Object )
209
+ uA , _ , err := a .Decode (buf .Bytes (), nil , uA )
210
+ if err != nil {
211
+ t .Fatalf ("error decoding A to unstructured: %v" , err )
212
+ }
213
+
214
+ // Compare original unstructured vs intermediate unstructured
215
+ tmp , ok := uA .(runtime.Unstructured )
216
+ if ! ok {
217
+ t .Fatalf ("unexpected type %T for unstructured" , tmp )
218
+ }
219
+ if ! unstructuredEqual (t , original , uA .(runtime.Unstructured )) {
220
+ t .Fatalf ("original unstructured differed from unstructured via A: %v" , cmp .Diff (original , uA ))
221
+ }
222
+
223
+ buf .Reset ()
224
+ // intermediate unstructured -> B
225
+ if err := b .Encode (uA , & buf ); err != nil {
226
+ t .Fatalf ("error encoding unstructured to B: %v" , err )
227
+ }
228
+ // B -> intermediate unstructured
229
+ uB := reflect .New (reflect .TypeOf (original ).Elem ()).Interface ().(runtime.Object )
230
+ uB , _ , err = b .Decode (buf .Bytes (), nil , uB )
231
+ if err != nil {
232
+ t .Fatalf ("error decoding B to unstructured: %v" , err )
233
+ }
234
+
235
+ // compare original vs intermediate unstructured
236
+ tmp , ok = uB .(runtime.Unstructured )
237
+ if ! ok {
238
+ t .Fatalf ("unexpected type %T for unstructured" , tmp )
239
+ }
240
+ if ! unstructuredEqual (t , original , uB .(runtime.Unstructured )) {
241
+ t .Fatalf ("unstructured via A differed from unstructured via B: %v" , cmp .Diff (original , uB ))
242
+ }
243
+
244
+ // intermediate unstructured -> A
245
+ buf .Reset ()
246
+ if err := a .Encode (uB , & buf ); err != nil {
247
+ t .Fatalf ("error encoding unstructured to A: %v" , err )
248
+ }
249
+ // A -> final unstructured
250
+ final := reflect .New (reflect .TypeOf (original ).Elem ()).Interface ().(runtime.Object )
251
+ final , _ , err = a .Decode (buf .Bytes (), nil , final )
252
+ if err != nil {
253
+ t .Fatalf ("error decoding A to unstructured: %v" , err )
254
+ }
255
+
256
+ // Compare original unstructured vs final unstructured
257
+ tmp , ok = final .(runtime.Unstructured )
258
+ if ! ok {
259
+ t .Fatalf ("unexpected type %T for unstructured" , tmp )
260
+ }
261
+ if ! unstructuredEqual (t , original , final .(runtime.Unstructured )) {
262
+ t .Errorf ("object changed during unstructured->A->unstructured->B->unstructured roundtrip, diff: %s" , cmp .Diff (original , final ))
263
+ }
264
+ }
265
+
266
+ func getSeed (t * testing.T ) int64 {
267
+ seed := int64 (time .Now ().Nanosecond ())
268
+ if override := os .Getenv ("TEST_RAND_SEED" ); len (override ) > 0 {
269
+ overrideSeed , err := strconv .ParseInt (override , 10 , 64 )
270
+ if err != nil {
271
+ t .Fatal (err )
272
+ }
273
+ seed = overrideSeed
274
+ t .Logf ("using overridden seed: %d" , seed )
275
+ } else {
276
+ t .Logf ("seed (override with TEST_RAND_SEED if desired): %d" , seed )
277
+ }
278
+ return seed
279
+ }
280
+
281
+ const (
282
+ maxUnstructuredDepth = 64
283
+ maxUnstructuredFanOut = 5
284
+ )
285
+
286
+ func unstructuredFuzzerFuncs (codecs serializer.CodecFactory ) []interface {} {
287
+ return []interface {}{
288
+ func (u * unstructured.Unstructured , c fuzz.Continue ) {
289
+ obj := make (map [string ]interface {})
290
+ obj ["apiVersion" ] = generateValidAPIVersionString (c )
291
+ obj ["kind" ] = generateNonEmptyString (c )
292
+ for j := c .Intn (maxUnstructuredFanOut ); j >= 0 ; j -- {
293
+ obj [c .RandString ()] = generateRandomTypeValue (maxUnstructuredDepth , c )
294
+ }
295
+ u .Object = obj
296
+ },
297
+ func (ul * unstructured.UnstructuredList , c fuzz.Continue ) {
298
+ obj := make (map [string ]interface {})
299
+ obj ["apiVersion" ] = generateValidAPIVersionString (c )
300
+ obj ["kind" ] = generateNonEmptyString (c )
301
+ for j := c .Intn (maxUnstructuredFanOut ); j >= 0 ; j -- {
302
+ obj [c .RandString ()] = generateRandomTypeValue (maxUnstructuredDepth , c )
303
+ }
304
+ for j := c .Intn (maxUnstructuredFanOut ); j >= 0 ; j -- {
305
+ var item = unstructured.Unstructured {}
306
+ c .Fuzz (& item )
307
+ ul .Items = append (ul .Items , item )
308
+ }
309
+ ul .Object = obj
310
+ },
311
+ }
312
+ }
313
+
314
+ func generateNonEmptyString (c fuzz.Continue ) string {
315
+ temp := c .RandString ()
316
+ for len (temp ) == 0 {
317
+ temp = c .RandString ()
318
+ }
319
+ return temp
320
+ }
321
+
322
+ // generateNonEmptyNoSlashString generates a non-empty string without any slashes
323
+ func generateNonEmptyNoSlashString (c fuzz.Continue ) string {
324
+ temp := strings .ReplaceAll (generateNonEmptyString (c ), "/" , "" )
325
+ for len (temp ) == 0 {
326
+ temp = strings .ReplaceAll (generateNonEmptyString (c ), "/" , "" )
327
+ }
328
+ return temp
329
+ }
330
+
331
+ // generateValidAPIVersionString generates valid apiVersion string with formats:
332
+ // <string>/<string> or <string>
333
+ func generateValidAPIVersionString (c fuzz.Continue ) string {
334
+ if c .RandBool () {
335
+ return generateNonEmptyNoSlashString (c ) + "/" + generateNonEmptyNoSlashString (c )
336
+ } else {
337
+ return generateNonEmptyNoSlashString (c )
338
+ }
339
+ }
340
+
341
+ // generateRandomTypeValue generates fuzzed valid JSON data types:
342
+ // 1. numbers (float64, int64)
343
+ // 2. string (utf-8 encodings)
344
+ // 3. boolean
345
+ // 4. array ([]interface{})
346
+ // 5. object (map[string]interface{})
347
+ // 6. null
348
+ // Decoding into unstructured can only produce a nil interface{} value or the
349
+ // concrete types map[string]interface{}, []interface{}, int64, float64, string, and bool
350
+ // If a value of other types is put into an unstructured, it will roundtrip
351
+ // to one of the above list of supported types. For example, if Time type is used,
352
+ // it will be encoded into a RFC 3339 format string such as "2001-02-03T12:34:56Z"
353
+ // and when decoding into Unstructured, there is no information to indicate
354
+ // that this string was originally produced by encoding a metav1.Time.
355
+ // All external-versioned builtin types are exercised through RoundtripToUnstructured
356
+ // in apitesting package. Types like metav1.Time are implicitly being exercised
357
+ // because they appear as fields in those types.
358
+ func generateRandomTypeValue (depth int , c fuzz.Continue ) interface {} {
359
+ t := c .Rand .Intn (120 )
360
+ // If the max depth for unstructured is reached, only add non-recursive types
361
+ // which is 20+ in range
362
+ if depth == 0 {
363
+ t = 20 + c .Rand .Intn (120 - 20 )
364
+ }
365
+
366
+ switch {
367
+ case t < 10 :
368
+ item := make ([]interface {}, c .Intn (maxUnstructuredFanOut ))
369
+ for k := range item {
370
+ item [k ] = generateRandomTypeValue (depth - 1 , c )
371
+ }
372
+ return item
373
+ case t < 20 :
374
+ item := map [string ]interface {}{}
375
+ for j := c .Intn (maxUnstructuredFanOut ); j >= 0 ; j -- {
376
+ item [c .RandString ()] = generateRandomTypeValue (depth - 1 , c )
377
+ }
378
+ return item
379
+ case t < 40 :
380
+ // Only valid UTF-8 encodings
381
+ var item string
382
+ c .Fuzz (& item )
383
+ return item
384
+ case t < 60 :
385
+ var item int64
386
+ c .Fuzz (& item )
387
+ return item
388
+ case t < 80 :
389
+ var item bool
390
+ c .Fuzz (& item )
391
+ return item
392
+ case t < 100 :
393
+ return c .Rand .NormFloat64 ()
394
+ case t < 120 :
395
+ return nil
396
+ default :
397
+ panic ("invalid case" )
398
+ }
399
+ }
400
+
401
+ func unstructuredEqual (t * testing.T , a , b runtime.Unstructured ) bool {
402
+ return anyEqual (t , a .UnstructuredContent (), b .UnstructuredContent ())
403
+ }
404
+
405
+ // numberEqual asserts equality of two numbers which one is int64 and one is float64
406
+ // In JSON, a non-decimal float64 is converted to int64 automatically in case the
407
+ // float64 fits into int64 range. Otherwise, the non-decimal float64 remains a float.
408
+ // As a result, this func does an int64 to float64 conversion using math/big package
409
+ // to ensure the conversion is lossless before comparison.
410
+ func numberEqual (a int64 , b float64 ) bool {
411
+ // Ensure roundtrip int64 to float64 conversion is lossless
412
+ f , accuracy := big .NewInt (a ).Float64 ()
413
+ if accuracy == big .Exact {
414
+ // Distinction between int64 and float64 is not preserved during JSON roundtrip for all numbers.
415
+ return f == b
416
+ }
417
+ return false
418
+ }
419
+
420
+ func anyEqual (t * testing.T , a , b interface {}) bool {
421
+ switch b .(type ) {
422
+ case nil , bool , string , int64 , float64 , []interface {}, map [string ]interface {}:
423
+ default :
424
+ t .Fatalf ("unexpected value %v of type %T" , b , b )
425
+ }
426
+
427
+ switch ac := a .(type ) {
428
+ case nil , bool , string :
429
+ return ac == b
430
+ case int64 :
431
+ if bc , ok := b .(float64 ); ok {
432
+ return numberEqual (ac , bc )
433
+ }
434
+ return ac == b
435
+ case float64 :
436
+ if bc , ok := b .(int64 ); ok {
437
+ return numberEqual (bc , ac )
438
+ }
439
+ return ac == b
440
+ case []interface {}:
441
+ bc , ok := b .([]interface {})
442
+ if ! ok {
443
+ return false
444
+ }
445
+ if len (ac ) != len (bc ) {
446
+ return false
447
+ }
448
+ for i , aa := range ac {
449
+ if ! anyEqual (t , aa , bc [i ]) {
450
+ return false
451
+ }
452
+ }
453
+ return true
454
+ case map [string ]interface {}:
455
+ bc , ok := b .(map [string ]interface {})
456
+ if ! ok {
457
+ return false
458
+ }
459
+ if len (ac ) != len (bc ) {
460
+ return false
461
+ }
462
+ for k , aa := range ac {
463
+ bb , ok := bc [k ]
464
+ if ! ok {
465
+ return false
466
+ }
467
+ if ! anyEqual (t , aa , bb ) {
468
+ return false
469
+ }
470
+ }
471
+ return true
472
+ default :
473
+ t .Fatalf ("unexpected value %v of type %T" , a , a )
474
+ }
475
+ return true
476
+ }
0 commit comments