@@ -6,9 +6,13 @@ import (
6
6
"reflect"
7
7
"testing"
8
8
9
+ "github.com/google/go-cmp/cmp"
10
+ "github.com/google/go-cmp/cmp/cmpopts"
9
11
"github.com/stretchr/testify/require"
10
12
appsv1 "k8s.io/api/apps/v1"
11
13
corev1 "k8s.io/api/core/v1"
14
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
15
+ "k8s.io/apimachinery/pkg/runtime/schema"
12
16
"sigs.k8s.io/controller-runtime/pkg/client"
13
17
14
18
"github.com/operator-framework/api/pkg/operators/v1alpha1"
@@ -267,3 +271,182 @@ func Test_BundleValidatorCallsAllValidationFnsInOrder(t *testing.T) {
267
271
require .NoError (t , val .Validate (nil ))
268
272
require .Equal (t , "hi" , actual )
269
273
}
274
+
275
+ // Test_Render_ValidatesOutputForAllInstallModes ensures that the BundleRenderer
276
+ // correctly generates and returns the exact list of client.Objects produced by the
277
+ // ResourceGenerators, across all supported install modes (AllNamespaces, SingleNamespace, OwnNamespace).
278
+ func Test_Render_ValidatesOutputForAllInstallModes (t * testing.T ) {
279
+ testCases := []struct {
280
+ name string
281
+ installNamespace string
282
+ watchNamespace string
283
+ installModes []v1alpha1.InstallMode
284
+ expectedNS string
285
+ }{
286
+ {
287
+ name : "AllNamespaces" ,
288
+ installNamespace : "mock-system" ,
289
+ watchNamespace : "" ,
290
+ installModes : []v1alpha1.InstallMode {
291
+ {Type : v1alpha1 .InstallModeTypeAllNamespaces , Supported : true },
292
+ },
293
+ expectedNS : "mock-system" ,
294
+ },
295
+ {
296
+ name : "SingleNamespace" ,
297
+ installNamespace : "mock-system" ,
298
+ watchNamespace : "mock-watch" ,
299
+ installModes : []v1alpha1.InstallMode {
300
+ {Type : v1alpha1 .InstallModeTypeSingleNamespace , Supported : true },
301
+ },
302
+ expectedNS : "mock-watch" ,
303
+ },
304
+ {
305
+ name : "OwnNamespace" ,
306
+ installNamespace : "mock-system" ,
307
+ watchNamespace : "mock-system" ,
308
+ installModes : []v1alpha1.InstallMode {
309
+ {Type : v1alpha1 .InstallModeTypeOwnNamespace , Supported : true },
310
+ },
311
+ expectedNS : "mock-system" ,
312
+ },
313
+ }
314
+
315
+ for _ , tc := range testCases {
316
+ t .Run (tc .name , func (t * testing.T ) {
317
+ expectedObjects := []client.Object {
318
+ fakeUnstructured ("ClusterRole" , "" , "mock-clusterrole" ),
319
+ fakeUnstructured ("ClusterRoleBinding" , "" , "mock-clusterrolebinding" ),
320
+ fakeUnstructured ("Role" , tc .expectedNS , "mock-role" ),
321
+ fakeUnstructured ("RoleBinding" , tc .expectedNS , "mock-rolebinding" ),
322
+ fakeUnstructured ("ConfigMap" , tc .expectedNS , "mock-config" ),
323
+ fakeUnstructured ("Secret" , tc .expectedNS , "mock-secret" ),
324
+ fakeUnstructured ("Service" , tc .expectedNS , "mock-service" ),
325
+ fakeUnstructured ("Deployment" , tc .expectedNS , "mock-deployment" ),
326
+ fakeUnstructured ("ServiceAccount" , tc .expectedNS , "mock-sa" ),
327
+ fakeUnstructured ("NetworkPolicy" , tc .expectedNS , "mock-netpol" ),
328
+ }
329
+
330
+ mockGen := render .ResourceGenerator (func (_ * bundle.RegistryV1 , _ render.Options ) ([]client.Object , error ) {
331
+ return expectedObjects , nil
332
+ })
333
+
334
+ mockBundle := bundle.RegistryV1 {
335
+ CSV : v1alpha1.ClusterServiceVersion {
336
+ Spec : v1alpha1.ClusterServiceVersionSpec {
337
+ InstallModes : tc .installModes ,
338
+ },
339
+ },
340
+ }
341
+
342
+ renderer := render.BundleRenderer {
343
+ BundleValidator : render.BundleValidator {
344
+ func (_ * bundle.RegistryV1 ) []error { return nil },
345
+ },
346
+ ResourceGenerators : []render.ResourceGenerator {mockGen },
347
+ }
348
+
349
+ opts := []render.Option {
350
+ render .WithTargetNamespaces (tc .watchNamespace ),
351
+ render .WithUniqueNameGenerator (render .DefaultUniqueNameGenerator ),
352
+ }
353
+
354
+ objs , err := renderer .Render (mockBundle , tc .installNamespace , opts ... )
355
+ require .NoError (t , err )
356
+ require .Len (t , objs , len (expectedObjects ))
357
+
358
+ // Compare expected vs actual objects
359
+ gotMap := make (map [string ]client.Object )
360
+ for _ , obj := range objs {
361
+ gotMap [objectKey (obj )] = obj
362
+ }
363
+
364
+ for _ , exp := range expectedObjects {
365
+ key := objectKey (exp )
366
+ got , exists := gotMap [key ]
367
+ require .True (t , exists , "missing expected object: %s" , key )
368
+
369
+ expObj := exp .(* unstructured.Unstructured )
370
+ gotObj := got .(* unstructured.Unstructured )
371
+
372
+ if diff := cmp .Diff (expObj .Object , gotObj .Object , cmpopts .EquateEmpty ()); diff != "" {
373
+ t .Errorf ("object content mismatch for %s (-want +got):\n %s" , key , diff )
374
+ }
375
+ }
376
+ })
377
+ }
378
+ }
379
+
380
+ // fakeUnstructured creates a fake unstructured client.Object with the specified kind, namespace, and name.
381
+ func fakeUnstructured (kind , namespace , name string ) client.Object {
382
+ obj := & unstructured.Unstructured {}
383
+ obj .Object = make (map [string ]interface {})
384
+
385
+ group := ""
386
+ version := "v1"
387
+
388
+ switch kind {
389
+ case "NetworkPolicy" :
390
+ err := unstructured .SetNestedField (obj .Object , map [string ]interface {}{
391
+ "podSelector" : map [string ]interface {}{
392
+ "matchLabels" : map [string ]interface {}{"app" : "my-app" },
393
+ },
394
+ "policyTypes" : []interface {}{"Ingress" },
395
+ }, "spec" )
396
+ if err != nil {
397
+ panic (fmt .Sprintf ("failed to set spec for NetworkPolicy: %v" , err ))
398
+ }
399
+ case "Service" :
400
+ _ = unstructured .SetNestedField (obj .Object , map [string ]interface {}{
401
+ "ports" : []interface {}{
402
+ map [string ]interface {}{
403
+ "port" : int64 (8080 ),
404
+ "targetPort" : "http" ,
405
+ },
406
+ },
407
+ "selector" : map [string ]interface {}{
408
+ "app" : "mock-app" ,
409
+ },
410
+ }, "spec" )
411
+ case "Deployment" :
412
+ _ = unstructured .SetNestedField (obj .Object , map [string ]interface {}{
413
+ "replicas" : int64 (1 ),
414
+ "selector" : map [string ]interface {}{
415
+ "matchLabels" : map [string ]interface {}{"app" : "mock-app" },
416
+ },
417
+ "template" : map [string ]interface {}{
418
+ "metadata" : map [string ]interface {}{
419
+ "labels" : map [string ]interface {}{"app" : "mock-app" },
420
+ },
421
+ "spec" : map [string ]interface {}{
422
+ "containers" : []interface {}{
423
+ map [string ]interface {}{
424
+ "name" : "controller" ,
425
+ "image" : "mock-controller:latest" ,
426
+ },
427
+ },
428
+ },
429
+ },
430
+ }, "spec" )
431
+ case "ConfigMap" :
432
+ _ = unstructured .SetNestedField (obj .Object , map [string ]interface {}{
433
+ "controller" : "enabled" ,
434
+ }, "data" )
435
+ }
436
+
437
+ obj .SetGroupVersionKind (schema.GroupVersionKind {
438
+ Group : group ,
439
+ Version : version ,
440
+ Kind : kind ,
441
+ })
442
+ obj .SetNamespace (namespace )
443
+ obj .SetName (name )
444
+
445
+ return obj
446
+ }
447
+
448
+ // objectKey returns a unique key for a Kubernetes object based on Kind/Namespace/Name.
449
+ func objectKey (obj client.Object ) string {
450
+ gvk := obj .GetObjectKind ().GroupVersionKind ()
451
+ return fmt .Sprintf ("%s/%s/%s" , gvk .Kind , obj .GetNamespace (), obj .GetName ())
452
+ }
0 commit comments