@@ -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,182 @@ 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 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