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