github.com/operator-framework/operator-lifecycle-manager@v0.30.0/pkg/controller/bundle/bundle_unpacker_test.go (about)

     1  package bundle
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/stretchr/testify/require"
    12  	batchv1 "k8s.io/api/batch/v1"
    13  	corev1 "k8s.io/api/core/v1"
    14  	rbacv1 "k8s.io/api/rbac/v1"
    15  	"k8s.io/apimachinery/pkg/api/resource"
    16  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    17  	"k8s.io/apimachinery/pkg/runtime"
    18  	"k8s.io/client-go/informers"
    19  	k8sfake "k8s.io/client-go/kubernetes/fake"
    20  	"k8s.io/client-go/tools/cache"
    21  	"k8s.io/utils/ptr"
    22  
    23  	operatorsv1 "github.com/operator-framework/api/pkg/operators/v1"
    24  	operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
    25  	crfake "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned/fake"
    26  	crinformers "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/informers/externalversions"
    27  	v1listers "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/listers/operators/v1"
    28  	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install"
    29  	"github.com/operator-framework/operator-registry/pkg/api"
    30  	"github.com/operator-framework/operator-registry/pkg/configmap"
    31  )
    32  
    33  const (
    34  	csvJSON     = "{\"apiVersion\":\"operators.coreos.com/v1alpha1\",\"kind\":\"ClusterServiceVersion\",\"metadata\":{\"annotations\":{\"olm.skipRange\":\"\\u003c 0.6.0\",\"tectonic-visibility\":\"ocs\"},\"name\":\"etcdoperator.v0.9.2\",\"namespace\":\"placeholder\"},\"spec\":{\"customresourcedefinitions\":{\"owned\":[{\"description\":\"Represents a cluster of etcd nodes.\",\"displayName\":\"etcd Cluster\",\"kind\":\"EtcdCluster\",\"name\":\"etcdclusters.etcd.database.coreos.com\",\"resources\":[{\"kind\":\"Service\",\"version\":\"v1\"},{\"kind\":\"Pod\",\"version\":\"v1\"}],\"specDescriptors\":[{\"description\":\"The desired number of member Pods for the etcd cluster.\",\"displayName\":\"Size\",\"path\":\"size\",\"x-descriptors\":[\"urn:alm:descriptor:com.tectonic.ui:podCount\"]},{\"description\":\"Limits describes the minimum/maximum amount of compute resources required/allowed\",\"displayName\":\"Resource Requirements\",\"path\":\"pod.resources\",\"x-descriptors\":[\"urn:alm:descriptor:com.tectonic.ui:resourceRequirements\"]}],\"statusDescriptors\":[{\"description\":\"The status of each of the member Pods for the etcd cluster.\",\"displayName\":\"Member Status\",\"path\":\"members\",\"x-descriptors\":[\"urn:alm:descriptor:com.tectonic.ui:podStatuses\"]},{\"description\":\"The service at which the running etcd cluster can be accessed.\",\"displayName\":\"Service\",\"path\":\"serviceName\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes:Service\"]},{\"description\":\"The current size of the etcd cluster.\",\"displayName\":\"Cluster Size\",\"path\":\"size\"},{\"description\":\"The current version of the etcd cluster.\",\"displayName\":\"Current Version\",\"path\":\"currentVersion\"},{\"description\":\"The target version of the etcd cluster, after upgrading.\",\"displayName\":\"Target Version\",\"path\":\"targetVersion\"},{\"description\":\"The current status of the etcd cluster.\",\"displayName\":\"Status\",\"path\":\"phase\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes.phase\"]},{\"description\":\"Explanation for the current status of the cluster.\",\"displayName\":\"Status Details\",\"path\":\"reason\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes.phase:reason\"]}],\"version\":\"v1beta2\"},{\"description\":\"Represents the intent to backup an etcd cluster.\",\"displayName\":\"etcd Backup\",\"kind\":\"EtcdBackup\",\"name\":\"etcdbackups.etcd.database.coreos.com\",\"specDescriptors\":[{\"description\":\"Specifies the endpoints of an etcd cluster.\",\"displayName\":\"etcd Endpoint(s)\",\"path\":\"etcdEndpoints\",\"x-descriptors\":[\"urn:alm:descriptor:etcd:endpoint\"]},{\"description\":\"The full AWS S3 path where the backup is saved.\",\"displayName\":\"S3 Path\",\"path\":\"s3.path\",\"x-descriptors\":[\"urn:alm:descriptor:aws:s3:path\"]},{\"description\":\"The name of the secret object that stores the AWS credential and config files.\",\"displayName\":\"AWS Secret\",\"path\":\"s3.awsSecret\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes:Secret\"]}],\"statusDescriptors\":[{\"description\":\"Indicates if the backup was successful.\",\"displayName\":\"Succeeded\",\"path\":\"succeeded\",\"x-descriptors\":[\"urn:alm:descriptor:text\"]},{\"description\":\"Indicates the reason for any backup related failures.\",\"displayName\":\"Reason\",\"path\":\"reason\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes.phase:reason\"]}],\"version\":\"v1beta2\"},{\"description\":\"Represents the intent to restore an etcd cluster from a backup.\",\"displayName\":\"etcd Restore\",\"kind\":\"EtcdRestore\",\"name\":\"etcdrestores.etcd.database.coreos.com\",\"specDescriptors\":[{\"description\":\"References the EtcdCluster which should be restored,\",\"displayName\":\"etcd Cluster\",\"path\":\"etcdCluster.name\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes:EtcdCluster\",\"urn:alm:descriptor:text\"]},{\"description\":\"The full AWS S3 path where the backup is saved.\",\"displayName\":\"S3 Path\",\"path\":\"s3.path\",\"x-descriptors\":[\"urn:alm:descriptor:aws:s3:path\"]},{\"description\":\"The name of the secret object that stores the AWS credential and config files.\",\"displayName\":\"AWS Secret\",\"path\":\"s3.awsSecret\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes:Secret\"]}],\"statusDescriptors\":[{\"description\":\"Indicates if the restore was successful.\",\"displayName\":\"Succeeded\",\"path\":\"succeeded\",\"x-descriptors\":[\"urn:alm:descriptor:text\"]},{\"description\":\"Indicates the reason for any restore related failures.\",\"displayName\":\"Reason\",\"path\":\"reason\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes.phase:reason\"]}],\"version\":\"v1beta2\"}],\"required\":[{\"description\":\"Represents a cluster of etcd nodes.\",\"displayName\":\"etcd Cluster\",\"kind\":\"EtcdCluster\",\"name\":\"etcdclusters.etcd.database.coreos.com\",\"resources\":[{\"kind\":\"Service\",\"version\":\"v1\"},{\"kind\":\"Pod\",\"version\":\"v1\"}],\"specDescriptors\":[{\"description\":\"The desired number of member Pods for the etcd cluster.\",\"displayName\":\"Size\",\"path\":\"size\",\"x-descriptors\":[\"urn:alm:descriptor:com.tectonic.ui:podCount\"]}],\"version\":\"v1beta2\"}]},\"description\":\"etcd is a distributed key value store that provides a reliable way to store data across a cluster of machines. It’s open-source and available on GitHub. etcd gracefully handles leader elections during network partitions and will tolerate machine failure, including the leader. Your applications can read and write data into etcd.\\nA simple use-case is to store database connection details or feature flags within etcd as key value pairs. These values can be watched, allowing your app to reconfigure itself when they change. Advanced uses take advantage of the consistency guarantees to implement database leader elections or do distributed locking across a cluster of workers.\\n\\n_The etcd Open Cloud Service is Public Alpha. The goal before Beta is to fully implement backup features._\\n\\n### Reading and writing to etcd\\n\\nCommunicate with etcd though its command line utility `etcdctl` or with the API using the automatically generated Kubernetes Service.\\n\\n[Read the complete guide to using the etcd Open Cloud Service](https://coreos.com/tectonic/docs/latest/alm/etcd-ocs.html)\\n\\n### Supported Features\\n\\n\\n**High availability**\\n\\n\\nMultiple instances of etcd are networked together and secured. Individual failures or networking issues are transparently handled to keep your cluster up and running.\\n\\n\\n**Automated updates**\\n\\n\\nRolling out a new etcd version works like all Kubernetes rolling updates. Simply declare the desired version, and the etcd service starts a safe rolling update to the new version automatically.\\n\\n\\n**Backups included**\\n\\n\\nComing soon, the ability to schedule backups to happen on or off cluster.\\n\",\"displayName\":\"etcd\",\"install\":{\"spec\":{\"deployments\":[{\"name\":\"etcd-operator\",\"spec\":{\"replicas\":1,\"selector\":{\"matchLabels\":{\"name\":\"etcd-operator-alm-owned\"}},\"template\":{\"metadata\":{\"labels\":{\"name\":\"etcd-operator-alm-owned\"},\"name\":\"etcd-operator-alm-owned\"},\"spec\":{\"containers\":[{\"command\":[\"etcd-operator\",\"--create-crd=false\"],\"env\":[{\"name\":\"MY_POD_NAMESPACE\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.namespace\"}}},{\"name\":\"MY_POD_NAME\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.name\"}}}],\"image\":\"quay.io/coreos/etcd-operator@sha256:c0301e4686c3ed4206e370b42de5a3bd2229b9fb4906cf85f3f30650424abec2\",\"name\":\"etcd-operator\"},{\"command\":[\"etcd-backup-operator\",\"--create-crd=false\"],\"env\":[{\"name\":\"MY_POD_NAMESPACE\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.namespace\"}}},{\"name\":\"MY_POD_NAME\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.name\"}}}],\"image\":\"quay.io/coreos/etcd-operator@sha256:c0301e4686c3ed4206e370b42de5a3bd2229b9fb4906cf85f3f30650424abec2\",\"name\":\"etcd-backup-operator\"},{\"command\":[\"etcd-restore-operator\",\"--create-crd=false\"],\"env\":[{\"name\":\"MY_POD_NAMESPACE\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.namespace\"}}},{\"name\":\"MY_POD_NAME\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.name\"}}}],\"image\":\"quay.io/coreos/etcd-operator@sha256:c0301e4686c3ed4206e370b42de5a3bd2229b9fb4906cf85f3f30650424abec2\",\"name\":\"etcd-restore-operator\"}],\"serviceAccountName\":\"etcd-operator\"}}}}],\"permissions\":[{\"rules\":[{\"apiGroups\":[\"etcd.database.coreos.com\"],\"resources\":[\"etcdclusters\",\"etcdbackups\",\"etcdrestores\"],\"verbs\":[\"*\"]},{\"apiGroups\":[\"\"],\"resources\":[\"pods\",\"services\",\"endpoints\",\"persistentvolumeclaims\",\"events\"],\"verbs\":[\"*\"]},{\"apiGroups\":[\"apps\"],\"resources\":[\"deployments\"],\"verbs\":[\"*\"]},{\"apiGroups\":[\"\"],\"resources\":[\"secrets\"],\"verbs\":[\"get\"]}],\"serviceAccountName\":\"etcd-operator\"}]},\"strategy\":\"deployment\"},\"keywords\":[\"etcd\",\"key value\",\"database\",\"coreos\",\"open source\"],\"labels\":{\"alm-owner-etcd\":\"etcdoperator\",\"operated-by\":\"etcdoperator\"},\"links\":[{\"name\":\"Blog\",\"url\":\"https://coreos.com/etcd\"},{\"name\":\"Documentation\",\"url\":\"https://coreos.com/operators/etcd/docs/latest/\"},{\"name\":\"etcd Operator Source Code\",\"url\":\"https://github.com/coreos/etcd-operator\"}],\"maintainers\":[{\"email\":\"support@coreos.com\",\"name\":\"CoreOS, Inc\"}],\"maturity\":\"alpha\",\"provider\":{\"name\":\"CoreOS, Inc\"},\"relatedImages\":[{\"image\":\"quay.io/coreos/etcd@sha256:3816b6daf9b66d6ced6f0f966314e2d4f894982c6b1493061502f8c2bf86ac84\",\"name\":\"etcd-v3.4.0\"},{\"image\":\"quay.io/coreos/etcd@sha256:49d3d4a81e0d030d3f689e7167f23e120abf955f7d08dbedf3ea246485acee9f\",\"name\":\"etcd-3.4.1\"}],\"replaces\":\"etcdoperator.v0.9.0\",\"selector\":{\"matchLabels\":{\"alm-owner-etcd\":\"etcdoperator\",\"operated-by\":\"etcdoperator\"}},\"skips\":[\"etcdoperator.v0.9.1\"],\"version\":\"0.9.2\"}}"
    35  	etcdBackup  = "{\"apiVersion\":\"apiextensions.k8s.io/v1beta1\",\"kind\":\"CustomResourceDefinition\",\"metadata\":{\"name\":\"etcdbackups.etcd.database.coreos.com\"},\"spec\":{\"group\":\"etcd.database.coreos.com\",\"names\":{\"kind\":\"EtcdBackup\",\"listKind\":\"EtcdBackupList\",\"plural\":\"etcdbackups\",\"singular\":\"etcdbackup\"},\"scope\":\"Namespaced\",\"version\":\"v1beta2\"}}"
    36  	etcdCluster = "{\"apiVersion\":\"apiextensions.k8s.io/v1beta1\",\"kind\":\"CustomResourceDefinition\",\"metadata\":{\"name\":\"etcdclusters.etcd.database.coreos.com\"},\"spec\":{\"group\":\"etcd.database.coreos.com\",\"names\":{\"kind\":\"EtcdCluster\",\"listKind\":\"EtcdClusterList\",\"plural\":\"etcdclusters\",\"shortNames\":[\"etcdclus\",\"etcd\"],\"singular\":\"etcdcluster\"},\"scope\":\"Namespaced\",\"version\":\"v1beta2\"}}"
    37  	etcdRestore = "{\"apiVersion\":\"apiextensions.k8s.io/v1beta1\",\"kind\":\"CustomResourceDefinition\",\"metadata\":{\"name\":\"etcdrestores.etcd.database.coreos.com\"},\"spec\":{\"group\":\"etcd.database.coreos.com\",\"names\":{\"kind\":\"EtcdRestore\",\"listKind\":\"EtcdRestoreList\",\"plural\":\"etcdrestores\",\"singular\":\"etcdrestore\"},\"scope\":\"Namespaced\",\"version\":\"v1beta2\"}}"
    38  	opmImage    = "opm-image"
    39  	utilImage   = "util-image"
    40  	bundlePath  = "bundle-path"
    41  	digestPath  = "bundle-path@sha256:54d626e08c1c802b305dad30b7e54a82f102390cc92c7d4db112048935236e9c"
    42  	runAsUser   = 1001
    43  )
    44  
    45  func TestConfigMapUnpacker(t *testing.T) {
    46  	pathHash := hash(bundlePath)
    47  	digestHash := hash(digestPath)
    48  	start := metav1.Now()
    49  	now := func() metav1.Time {
    50  		return start
    51  	}
    52  	backoffLimit := int32(3)
    53  	// Used to set the default value for job.spec.ActiveDeadlineSeconds
    54  	// that would normally be passed from the cmdline flag
    55  	defaultUnpackDuration := 10 * time.Minute
    56  	defaultUnpackTimeoutSeconds := int64(defaultUnpackDuration.Seconds())
    57  
    58  	// Custom timeout to override the default cmdline flag ActiveDeadlineSeconds value
    59  	customAnnotationDuration := 2 * time.Minute
    60  	customAnnotationTimeoutSeconds := int64(customAnnotationDuration.Seconds())
    61  
    62  	type fields struct {
    63  		objs []runtime.Object
    64  		crs  []runtime.Object
    65  	}
    66  	type args struct {
    67  		lookup *operatorsv1alpha1.BundleLookup
    68  		// A negative timeout duration arg means it will be ignored and the default flag timeout will be used
    69  		annotationTimeout time.Duration
    70  	}
    71  	type expected struct {
    72  		res          *BundleUnpackResult
    73  		err          error
    74  		configMaps   []*corev1.ConfigMap
    75  		jobs         []*batchv1.Job
    76  		roles        []*rbacv1.Role
    77  		roleBindings []*rbacv1.RoleBinding
    78  	}
    79  
    80  	tests := []struct {
    81  		description string
    82  		fields      fields
    83  		args        args
    84  		expected    expected
    85  	}{
    86  		{
    87  			description: "NoCatalogSource/NoConfigMap/NoJob/NotCreated/Pending",
    88  			fields:      fields{},
    89  			args: args{
    90  				annotationTimeout: -1 * time.Minute,
    91  				lookup: &operatorsv1alpha1.BundleLookup{
    92  					Path:     bundlePath,
    93  					Replaces: "",
    94  					CatalogSourceRef: &corev1.ObjectReference{
    95  						Namespace: "ns-a",
    96  						Name:      "src-a",
    97  					},
    98  					Conditions: []operatorsv1alpha1.BundleLookupCondition{
    99  						{
   100  							Type:    operatorsv1alpha1.BundleLookupPending,
   101  							Status:  corev1.ConditionTrue,
   102  							Reason:  JobNotStartedReason,
   103  							Message: JobNotStartedMessage,
   104  						},
   105  					},
   106  				},
   107  			},
   108  			expected: expected{
   109  				res: &BundleUnpackResult{
   110  					BundleLookup: &operatorsv1alpha1.BundleLookup{
   111  						Path:     bundlePath,
   112  						Replaces: "",
   113  						CatalogSourceRef: &corev1.ObjectReference{
   114  							Namespace: "ns-a",
   115  							Name:      "src-a",
   116  						},
   117  						Conditions: []operatorsv1alpha1.BundleLookupCondition{
   118  							{
   119  								Type:               operatorsv1alpha1.BundleLookupPending,
   120  								Status:             corev1.ConditionTrue,
   121  								Reason:             CatalogSourceMissingReason,
   122  								Message:            CatalogSourceMissingMessage,
   123  								LastTransitionTime: &start,
   124  							},
   125  						},
   126  					},
   127  					name: pathHash,
   128  				},
   129  			},
   130  		},
   131  		{
   132  			description: "CatalogSourcePresent/NoConfigMap/NoJob/JobCreated/Pending/WithCustomTimeout",
   133  			fields: fields{
   134  				crs: []runtime.Object{
   135  					&operatorsv1alpha1.CatalogSource{
   136  						ObjectMeta: metav1.ObjectMeta{
   137  							Namespace: "ns-a",
   138  							Name:      "src-a",
   139  						},
   140  						Spec: operatorsv1alpha1.CatalogSourceSpec{
   141  							Secrets: []string{"my-secret"},
   142  						},
   143  					},
   144  				},
   145  			},
   146  			args: args{
   147  				// We override the default timeout and expect to see the job created with
   148  				// the custom annotation based timeout value
   149  				annotationTimeout: customAnnotationDuration,
   150  				lookup: &operatorsv1alpha1.BundleLookup{
   151  					Path:     bundlePath,
   152  					Replaces: "",
   153  					CatalogSourceRef: &corev1.ObjectReference{
   154  						Namespace: "ns-a",
   155  						Name:      "src-a",
   156  					},
   157  					Conditions: []operatorsv1alpha1.BundleLookupCondition{
   158  						{
   159  							Type:    operatorsv1alpha1.BundleLookupPending,
   160  							Status:  corev1.ConditionTrue,
   161  							Reason:  JobNotStartedReason,
   162  							Message: JobNotStartedMessage,
   163  						},
   164  					},
   165  				},
   166  			},
   167  			expected: expected{
   168  				res: &BundleUnpackResult{
   169  					BundleLookup: &operatorsv1alpha1.BundleLookup{
   170  						Path:     bundlePath,
   171  						Replaces: "",
   172  						CatalogSourceRef: &corev1.ObjectReference{
   173  							Namespace: "ns-a",
   174  							Name:      "src-a",
   175  						},
   176  						Conditions: []operatorsv1alpha1.BundleLookupCondition{
   177  							{
   178  								Type:               operatorsv1alpha1.BundleLookupPending,
   179  								Status:             corev1.ConditionTrue,
   180  								Reason:             JobIncompleteReason,
   181  								Message:            JobIncompleteMessage,
   182  								LastTransitionTime: &start,
   183  							},
   184  						},
   185  					},
   186  					name: pathHash,
   187  				},
   188  				configMaps: []*corev1.ConfigMap{
   189  					{
   190  						ObjectMeta: metav1.ObjectMeta{
   191  							Name:      pathHash,
   192  							Namespace: "ns-a",
   193  							Labels:    map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue},
   194  							OwnerReferences: []metav1.OwnerReference{
   195  								{
   196  									APIVersion:         "operators.coreos.com/v1alpha1",
   197  									Kind:               "CatalogSource",
   198  									Name:               "src-a",
   199  									Controller:         &blockOwnerDeletion,
   200  									BlockOwnerDeletion: &blockOwnerDeletion,
   201  								},
   202  							},
   203  						},
   204  					},
   205  				},
   206  				jobs: []*batchv1.Job{
   207  					{
   208  						ObjectMeta: metav1.ObjectMeta{
   209  							Name:      pathHash,
   210  							Namespace: "ns-a",
   211  							Labels:    map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue, bundleUnpackRefLabel: pathHash},
   212  							OwnerReferences: []metav1.OwnerReference{
   213  								{
   214  									APIVersion:         "v1",
   215  									Kind:               "ConfigMap",
   216  									Name:               pathHash,
   217  									Controller:         &blockOwnerDeletion,
   218  									BlockOwnerDeletion: &blockOwnerDeletion,
   219  								},
   220  							},
   221  						},
   222  						Spec: batchv1.JobSpec{
   223  							// The expected job's timeout should be set to the custom annotation timeout
   224  							ActiveDeadlineSeconds: &customAnnotationTimeoutSeconds,
   225  							BackoffLimit:          &backoffLimit,
   226  							Template: corev1.PodTemplateSpec{
   227  								ObjectMeta: metav1.ObjectMeta{
   228  									Name:   pathHash,
   229  									Labels: map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue},
   230  								},
   231  								Spec: corev1.PodSpec{
   232  									RestartPolicy:    corev1.RestartPolicyNever,
   233  									ImagePullSecrets: []corev1.LocalObjectReference{{Name: "my-secret"}},
   234  									SecurityContext: &corev1.PodSecurityContext{
   235  										RunAsNonRoot: ptr.To(bool(true)),
   236  										RunAsUser:    ptr.To(int64(runAsUser)),
   237  										SeccompProfile: &corev1.SeccompProfile{
   238  											Type: corev1.SeccompProfileTypeRuntimeDefault,
   239  										},
   240  									},
   241  									Containers: []corev1.Container{
   242  										{
   243  											Name:    "extract",
   244  											Image:   opmImage,
   245  											Command: []string{"opm", "alpha", "bundle", "extract", "-m", "/bundle/", "-n", "ns-a", "-c", pathHash, "-z"},
   246  											Env: []corev1.EnvVar{
   247  												{
   248  													Name:  configmap.EnvContainerImage,
   249  													Value: bundlePath,
   250  												},
   251  											},
   252  											VolumeMounts: []corev1.VolumeMount{
   253  												{
   254  													Name:      "bundle",
   255  													MountPath: "/bundle",
   256  												},
   257  											},
   258  											Resources: corev1.ResourceRequirements{
   259  												Requests: corev1.ResourceList{
   260  													corev1.ResourceCPU:    resource.MustParse("10m"),
   261  													corev1.ResourceMemory: resource.MustParse("50Mi"),
   262  												},
   263  											},
   264  											SecurityContext: &corev1.SecurityContext{
   265  												AllowPrivilegeEscalation: ptr.To(bool(false)),
   266  												Capabilities: &corev1.Capabilities{
   267  													Drop: []corev1.Capability{"ALL"},
   268  												},
   269  											},
   270  											TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
   271  										},
   272  									},
   273  									InitContainers: []corev1.Container{
   274  										{
   275  											Name:    "util",
   276  											Image:   utilImage,
   277  											Command: []string{"/bin/cp", "-Rv", "/bin/cpb", "/util/cpb"}, // Copy tooling for the bundle container to use
   278  											VolumeMounts: []corev1.VolumeMount{
   279  												{
   280  													Name:      "util",
   281  													MountPath: "/util",
   282  												},
   283  											},
   284  											Resources: corev1.ResourceRequirements{
   285  												Requests: corev1.ResourceList{
   286  													corev1.ResourceCPU:    resource.MustParse("10m"),
   287  													corev1.ResourceMemory: resource.MustParse("50Mi"),
   288  												},
   289  											},
   290  											SecurityContext: &corev1.SecurityContext{
   291  												AllowPrivilegeEscalation: ptr.To(bool(false)),
   292  												Capabilities: &corev1.Capabilities{
   293  													Drop: []corev1.Capability{"ALL"},
   294  												},
   295  											},
   296  											TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
   297  										},
   298  										{
   299  											Name:            "pull",
   300  											Image:           bundlePath,
   301  											ImagePullPolicy: "Always",
   302  											Command:         []string{"/util/cpb", "/bundle"}, // Copy bundle content to its mount
   303  											VolumeMounts: []corev1.VolumeMount{
   304  												{
   305  													Name:      "bundle",
   306  													MountPath: "/bundle",
   307  												},
   308  												{
   309  													Name:      "util",
   310  													MountPath: "/util",
   311  												},
   312  											},
   313  											Resources: corev1.ResourceRequirements{
   314  												Requests: corev1.ResourceList{
   315  													corev1.ResourceCPU:    resource.MustParse("10m"),
   316  													corev1.ResourceMemory: resource.MustParse("50Mi"),
   317  												},
   318  											},
   319  											SecurityContext: &corev1.SecurityContext{
   320  												AllowPrivilegeEscalation: ptr.To(bool(false)),
   321  												Capabilities: &corev1.Capabilities{
   322  													Drop: []corev1.Capability{"ALL"},
   323  												},
   324  											},
   325  											TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
   326  										},
   327  									},
   328  									Volumes: []corev1.Volume{
   329  										{
   330  											Name: "bundle",
   331  											VolumeSource: corev1.VolumeSource{
   332  												EmptyDir: &corev1.EmptyDirVolumeSource{},
   333  											},
   334  										},
   335  										{
   336  											Name: "util",
   337  											VolumeSource: corev1.VolumeSource{
   338  												EmptyDir: &corev1.EmptyDirVolumeSource{},
   339  											},
   340  										},
   341  									},
   342  									NodeSelector: map[string]string{
   343  										"kubernetes.io/os": "linux",
   344  									},
   345  									Tolerations: []corev1.Toleration{
   346  										{
   347  											Key:      "kubernetes.io/arch",
   348  											Value:    "amd64",
   349  											Operator: "Equal",
   350  										},
   351  										{
   352  											Key:      "kubernetes.io/arch",
   353  											Value:    "arm64",
   354  											Operator: "Equal",
   355  										},
   356  										{
   357  											Key:      "kubernetes.io/arch",
   358  											Value:    "ppc64le",
   359  											Operator: "Equal",
   360  										},
   361  										{
   362  											Key:      "kubernetes.io/arch",
   363  											Value:    "s390x",
   364  											Operator: "Equal",
   365  										},
   366  									},
   367  								},
   368  							},
   369  						},
   370  					},
   371  				},
   372  				roles: []*rbacv1.Role{
   373  					{
   374  						ObjectMeta: metav1.ObjectMeta{
   375  							Name:      pathHash,
   376  							Namespace: "ns-a",
   377  							Labels:    map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue},
   378  							OwnerReferences: []metav1.OwnerReference{
   379  								{
   380  									APIVersion:         "v1",
   381  									Kind:               "ConfigMap",
   382  									Name:               pathHash,
   383  									Controller:         &blockOwnerDeletion,
   384  									BlockOwnerDeletion: &blockOwnerDeletion,
   385  								},
   386  							},
   387  						},
   388  						Rules: []rbacv1.PolicyRule{
   389  							{
   390  								APIGroups: []string{
   391  									"",
   392  								},
   393  								Verbs: []string{
   394  									"create", "get", "update",
   395  								},
   396  								Resources: []string{
   397  									"configmaps",
   398  								},
   399  								ResourceNames: []string{
   400  									pathHash,
   401  								},
   402  							},
   403  						},
   404  					},
   405  				},
   406  				roleBindings: []*rbacv1.RoleBinding{
   407  					{
   408  						ObjectMeta: metav1.ObjectMeta{
   409  							Name:      pathHash,
   410  							Namespace: "ns-a",
   411  							Labels:    map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue},
   412  							OwnerReferences: []metav1.OwnerReference{
   413  								{
   414  									APIVersion:         "v1",
   415  									Kind:               "ConfigMap",
   416  									Name:               pathHash,
   417  									Controller:         &blockOwnerDeletion,
   418  									BlockOwnerDeletion: &blockOwnerDeletion,
   419  								},
   420  							},
   421  						},
   422  						Subjects: []rbacv1.Subject{
   423  							{
   424  								Kind:      "ServiceAccount",
   425  								APIGroup:  "",
   426  								Name:      "default",
   427  								Namespace: "ns-a",
   428  							},
   429  						},
   430  						RoleRef: rbacv1.RoleRef{
   431  							APIGroup: "rbac.authorization.k8s.io",
   432  							Kind:     "Role",
   433  							Name:     pathHash,
   434  						},
   435  					},
   436  				},
   437  			},
   438  		},
   439  		{
   440  			description: "CatalogSourcePresent/ConfigMapPresent/JobPresent/DigestImage/Unpacked",
   441  			fields: fields{
   442  				objs: []runtime.Object{
   443  					&batchv1.Job{
   444  						ObjectMeta: metav1.ObjectMeta{
   445  							Name:      digestHash,
   446  							Namespace: "ns-a",
   447  							Labels:    map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue, bundleUnpackRefLabel: digestHash},
   448  							OwnerReferences: []metav1.OwnerReference{
   449  								{
   450  									APIVersion:         "v1",
   451  									Kind:               "ConfigMap",
   452  									Name:               digestHash,
   453  									Controller:         &blockOwnerDeletion,
   454  									BlockOwnerDeletion: &blockOwnerDeletion,
   455  								},
   456  							},
   457  						},
   458  						Spec: batchv1.JobSpec{
   459  							ActiveDeadlineSeconds: &defaultUnpackTimeoutSeconds,
   460  							BackoffLimit:          &backoffLimit,
   461  							Template: corev1.PodTemplateSpec{
   462  								ObjectMeta: metav1.ObjectMeta{
   463  									Name:   digestHash,
   464  									Labels: map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue},
   465  								},
   466  								Spec: corev1.PodSpec{
   467  									RestartPolicy: corev1.RestartPolicyNever,
   468  									SecurityContext: &corev1.PodSecurityContext{
   469  										RunAsNonRoot: ptr.To(bool(true)),
   470  										RunAsUser:    ptr.To(int64(runAsUser)),
   471  										SeccompProfile: &corev1.SeccompProfile{
   472  											Type: corev1.SeccompProfileTypeRuntimeDefault,
   473  										},
   474  									},
   475  									Containers: []corev1.Container{
   476  										{
   477  											Name:    "extract",
   478  											Image:   opmImage,
   479  											Command: []string{"opm", "alpha", "bundle", "extract", "-m", "/bundle/", "-n", "ns-a", "-c", digestHash, "-z"},
   480  											Env: []corev1.EnvVar{
   481  												{
   482  													Name:  configmap.EnvContainerImage,
   483  													Value: digestPath,
   484  												},
   485  											},
   486  											VolumeMounts: []corev1.VolumeMount{
   487  												{
   488  													Name:      "bundle",
   489  													MountPath: "/bundle",
   490  												},
   491  											},
   492  											Resources: corev1.ResourceRequirements{
   493  												Requests: corev1.ResourceList{
   494  													corev1.ResourceCPU:    resource.MustParse("10m"),
   495  													corev1.ResourceMemory: resource.MustParse("50Mi"),
   496  												},
   497  											},
   498  											SecurityContext: &corev1.SecurityContext{
   499  												AllowPrivilegeEscalation: ptr.To(bool(false)),
   500  												Capabilities: &corev1.Capabilities{
   501  													Drop: []corev1.Capability{"ALL"},
   502  												},
   503  											},
   504  											TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
   505  										},
   506  									},
   507  									InitContainers: []corev1.Container{
   508  										{
   509  											Name:    "util",
   510  											Image:   utilImage,
   511  											Command: []string{"/bin/cp", "-Rv", "/bin/cpb", "/util/cpb"}, // Copy tooling for the bundle container to use
   512  											VolumeMounts: []corev1.VolumeMount{
   513  												{
   514  													Name:      "util",
   515  													MountPath: "/util",
   516  												},
   517  											},
   518  											Resources: corev1.ResourceRequirements{
   519  												Requests: corev1.ResourceList{
   520  													corev1.ResourceCPU:    resource.MustParse("10m"),
   521  													corev1.ResourceMemory: resource.MustParse("50Mi"),
   522  												},
   523  											},
   524  											SecurityContext: &corev1.SecurityContext{
   525  												AllowPrivilegeEscalation: ptr.To(bool(false)),
   526  												Capabilities: &corev1.Capabilities{
   527  													Drop: []corev1.Capability{"ALL"},
   528  												},
   529  											},
   530  											TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
   531  										},
   532  										{
   533  											Name:            "pull",
   534  											Image:           digestPath,
   535  											ImagePullPolicy: "IfNotPresent",
   536  											Command:         []string{"/util/cpb", "/bundle"}, // Copy bundle content to its mount
   537  											VolumeMounts: []corev1.VolumeMount{
   538  												{
   539  													Name:      "bundle",
   540  													MountPath: "/bundle",
   541  												},
   542  												{
   543  													Name:      "util",
   544  													MountPath: "/util",
   545  												},
   546  											},
   547  											Resources: corev1.ResourceRequirements{
   548  												Requests: corev1.ResourceList{
   549  													corev1.ResourceCPU:    resource.MustParse("10m"),
   550  													corev1.ResourceMemory: resource.MustParse("50Mi"),
   551  												},
   552  											},
   553  											SecurityContext: &corev1.SecurityContext{
   554  												AllowPrivilegeEscalation: ptr.To(bool(false)),
   555  												Capabilities: &corev1.Capabilities{
   556  													Drop: []corev1.Capability{"ALL"},
   557  												},
   558  											},
   559  											TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
   560  										},
   561  									},
   562  									Volumes: []corev1.Volume{
   563  										{
   564  											Name: "bundle",
   565  											VolumeSource: corev1.VolumeSource{
   566  												EmptyDir: &corev1.EmptyDirVolumeSource{},
   567  											},
   568  										},
   569  										{
   570  											Name: "util",
   571  											VolumeSource: corev1.VolumeSource{
   572  												EmptyDir: &corev1.EmptyDirVolumeSource{},
   573  											},
   574  										},
   575  									},
   576  									NodeSelector: map[string]string{
   577  										"kubernetes.io/os": "linux",
   578  									},
   579  									Tolerations: []corev1.Toleration{
   580  										{
   581  											Key:      "kubernetes.io/arch",
   582  											Value:    "amd64",
   583  											Operator: "Equal",
   584  										},
   585  										{
   586  											Key:      "kubernetes.io/arch",
   587  											Value:    "arm64",
   588  											Operator: "Equal",
   589  										},
   590  										{
   591  											Key:      "kubernetes.io/arch",
   592  											Value:    "ppc64le",
   593  											Operator: "Equal",
   594  										},
   595  										{
   596  											Key:      "kubernetes.io/arch",
   597  											Value:    "s390x",
   598  											Operator: "Equal",
   599  										},
   600  									},
   601  								},
   602  							},
   603  						},
   604  						Status: batchv1.JobStatus{
   605  							Succeeded:      1,
   606  							StartTime:      &start,
   607  							CompletionTime: &start,
   608  							Conditions: []batchv1.JobCondition{
   609  								{
   610  									LastProbeTime:      start,
   611  									LastTransitionTime: start,
   612  									Status:             corev1.ConditionTrue,
   613  									Type:               batchv1.JobComplete,
   614  								},
   615  							},
   616  						},
   617  					},
   618  					&corev1.ConfigMap{
   619  						ObjectMeta: metav1.ObjectMeta{
   620  							Name:      digestHash,
   621  							Namespace: "ns-a",
   622  							Labels:    map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue},
   623  							OwnerReferences: []metav1.OwnerReference{
   624  								{
   625  									APIVersion:         "operators.coreos.com/v1alpha1",
   626  									Kind:               "CatalogSource",
   627  									Name:               "src-a",
   628  									Controller:         &blockOwnerDeletion,
   629  									BlockOwnerDeletion: &blockOwnerDeletion,
   630  								},
   631  							},
   632  						},
   633  						Data: map[string]string{
   634  							"etcdbackups.crd.json":  etcdBackup,
   635  							"etcdclusters.crd.json": etcdCluster,
   636  							"csv.json":              csvJSON,
   637  							"etcdrestores.crd.json": etcdRestore,
   638  						},
   639  					},
   640  				},
   641  				crs: []runtime.Object{
   642  					&operatorsv1alpha1.CatalogSource{
   643  						ObjectMeta: metav1.ObjectMeta{
   644  							Namespace: "ns-a",
   645  							Name:      "src-a",
   646  						},
   647  					},
   648  				},
   649  			},
   650  			args: args{
   651  				annotationTimeout: -1 * time.Minute,
   652  				lookup: &operatorsv1alpha1.BundleLookup{
   653  					Path:     digestPath,
   654  					Replaces: "",
   655  					CatalogSourceRef: &corev1.ObjectReference{
   656  						Namespace: "ns-a",
   657  						Name:      "src-a",
   658  					},
   659  					Conditions: []operatorsv1alpha1.BundleLookupCondition{
   660  						{
   661  							Type:               operatorsv1alpha1.BundleLookupPending,
   662  							Status:             corev1.ConditionTrue,
   663  							Reason:             JobIncompleteReason,
   664  							Message:            JobIncompleteMessage,
   665  							LastTransitionTime: &start,
   666  						},
   667  					},
   668  				},
   669  			},
   670  			expected: expected{
   671  				res: &BundleUnpackResult{
   672  					BundleLookup: &operatorsv1alpha1.BundleLookup{
   673  						Path:     digestPath,
   674  						Replaces: "",
   675  						CatalogSourceRef: &corev1.ObjectReference{
   676  							Namespace: "ns-a",
   677  							Name:      "src-a",
   678  						},
   679  					},
   680  					name: digestHash,
   681  					bundle: &api.Bundle{
   682  						CsvName: "etcdoperator.v0.9.2",
   683  						CsvJson: csvJSON + "\n",
   684  						Object: []string{
   685  							etcdBackup,
   686  							etcdCluster,
   687  							csvJSON,
   688  							etcdRestore,
   689  						},
   690  					},
   691  				},
   692  				configMaps: []*corev1.ConfigMap{
   693  					{
   694  						ObjectMeta: metav1.ObjectMeta{
   695  							Name:      digestHash,
   696  							Namespace: "ns-a",
   697  							Labels:    map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue},
   698  							OwnerReferences: []metav1.OwnerReference{
   699  								{
   700  									APIVersion:         "operators.coreos.com/v1alpha1",
   701  									Kind:               "CatalogSource",
   702  									Name:               "src-a",
   703  									Controller:         &blockOwnerDeletion,
   704  									BlockOwnerDeletion: &blockOwnerDeletion,
   705  								},
   706  							},
   707  						},
   708  						Data: map[string]string{
   709  							"etcdbackups.crd.json":  etcdBackup,
   710  							"etcdclusters.crd.json": etcdCluster,
   711  							"csv.json":              csvJSON,
   712  							"etcdrestores.crd.json": etcdRestore,
   713  						},
   714  					},
   715  				},
   716  				jobs: []*batchv1.Job{
   717  					{
   718  						ObjectMeta: metav1.ObjectMeta{
   719  							Name:      digestHash,
   720  							Namespace: "ns-a",
   721  							Labels:    map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue, bundleUnpackRefLabel: digestHash},
   722  							OwnerReferences: []metav1.OwnerReference{
   723  								{
   724  									APIVersion:         "v1",
   725  									Kind:               "ConfigMap",
   726  									Name:               digestHash,
   727  									Controller:         &blockOwnerDeletion,
   728  									BlockOwnerDeletion: &blockOwnerDeletion,
   729  								},
   730  							},
   731  						},
   732  						Spec: batchv1.JobSpec{
   733  							ActiveDeadlineSeconds: &defaultUnpackTimeoutSeconds,
   734  							BackoffLimit:          &backoffLimit,
   735  							Template: corev1.PodTemplateSpec{
   736  								ObjectMeta: metav1.ObjectMeta{
   737  									Name:   digestHash,
   738  									Labels: map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue},
   739  								},
   740  								Spec: corev1.PodSpec{
   741  									RestartPolicy: corev1.RestartPolicyNever,
   742  									SecurityContext: &corev1.PodSecurityContext{
   743  										RunAsNonRoot: ptr.To(bool(true)),
   744  										RunAsUser:    ptr.To(int64(runAsUser)),
   745  										SeccompProfile: &corev1.SeccompProfile{
   746  											Type: corev1.SeccompProfileTypeRuntimeDefault,
   747  										},
   748  									},
   749  									Containers: []corev1.Container{
   750  										{
   751  											Name:    "extract",
   752  											Image:   opmImage,
   753  											Command: []string{"opm", "alpha", "bundle", "extract", "-m", "/bundle/", "-n", "ns-a", "-c", digestHash, "-z"},
   754  											Env: []corev1.EnvVar{
   755  												{
   756  													Name:  configmap.EnvContainerImage,
   757  													Value: digestPath,
   758  												},
   759  											},
   760  											VolumeMounts: []corev1.VolumeMount{
   761  												{
   762  													Name:      "bundle",
   763  													MountPath: "/bundle",
   764  												},
   765  											},
   766  											Resources: corev1.ResourceRequirements{
   767  												Requests: corev1.ResourceList{
   768  													corev1.ResourceCPU:    resource.MustParse("10m"),
   769  													corev1.ResourceMemory: resource.MustParse("50Mi"),
   770  												},
   771  											},
   772  											SecurityContext: &corev1.SecurityContext{
   773  												AllowPrivilegeEscalation: ptr.To(bool(false)),
   774  												Capabilities: &corev1.Capabilities{
   775  													Drop: []corev1.Capability{"ALL"},
   776  												},
   777  											},
   778  											TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
   779  										},
   780  									},
   781  									InitContainers: []corev1.Container{
   782  										{
   783  											Name:    "util",
   784  											Image:   utilImage,
   785  											Command: []string{"/bin/cp", "-Rv", "/bin/cpb", "/util/cpb"}, // Copy tooling for the bundle container to use
   786  											VolumeMounts: []corev1.VolumeMount{
   787  												{
   788  													Name:      "util",
   789  													MountPath: "/util",
   790  												},
   791  											},
   792  											Resources: corev1.ResourceRequirements{
   793  												Requests: corev1.ResourceList{
   794  													corev1.ResourceCPU:    resource.MustParse("10m"),
   795  													corev1.ResourceMemory: resource.MustParse("50Mi"),
   796  												},
   797  											},
   798  											SecurityContext: &corev1.SecurityContext{
   799  												AllowPrivilegeEscalation: ptr.To(bool(false)),
   800  												Capabilities: &corev1.Capabilities{
   801  													Drop: []corev1.Capability{"ALL"},
   802  												},
   803  											},
   804  											TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
   805  										},
   806  										{
   807  											Name:            "pull",
   808  											Image:           digestPath,
   809  											ImagePullPolicy: "IfNotPresent",
   810  											Command:         []string{"/util/cpb", "/bundle"}, // Copy bundle content to its mount
   811  											VolumeMounts: []corev1.VolumeMount{
   812  												{
   813  													Name:      "bundle",
   814  													MountPath: "/bundle",
   815  												},
   816  												{
   817  													Name:      "util",
   818  													MountPath: "/util",
   819  												},
   820  											},
   821  											Resources: corev1.ResourceRequirements{
   822  												Requests: corev1.ResourceList{
   823  													corev1.ResourceCPU:    resource.MustParse("10m"),
   824  													corev1.ResourceMemory: resource.MustParse("50Mi"),
   825  												},
   826  											},
   827  											SecurityContext: &corev1.SecurityContext{
   828  												AllowPrivilegeEscalation: ptr.To(bool(false)),
   829  												Capabilities: &corev1.Capabilities{
   830  													Drop: []corev1.Capability{"ALL"},
   831  												},
   832  											},
   833  											TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
   834  										},
   835  									},
   836  									Volumes: []corev1.Volume{
   837  										{
   838  											Name: "bundle",
   839  											VolumeSource: corev1.VolumeSource{
   840  												EmptyDir: &corev1.EmptyDirVolumeSource{},
   841  											},
   842  										},
   843  										{
   844  											Name: "util",
   845  											VolumeSource: corev1.VolumeSource{
   846  												EmptyDir: &corev1.EmptyDirVolumeSource{},
   847  											},
   848  										},
   849  									},
   850  									NodeSelector: map[string]string{
   851  										"kubernetes.io/os": "linux",
   852  									},
   853  									Tolerations: []corev1.Toleration{
   854  										{
   855  											Key:      "kubernetes.io/arch",
   856  											Value:    "amd64",
   857  											Operator: "Equal",
   858  										},
   859  										{
   860  											Key:      "kubernetes.io/arch",
   861  											Value:    "arm64",
   862  											Operator: "Equal",
   863  										},
   864  										{
   865  											Key:      "kubernetes.io/arch",
   866  											Value:    "ppc64le",
   867  											Operator: "Equal",
   868  										},
   869  										{
   870  											Key:      "kubernetes.io/arch",
   871  											Value:    "s390x",
   872  											Operator: "Equal",
   873  										},
   874  									},
   875  								},
   876  							},
   877  						},
   878  						Status: batchv1.JobStatus{
   879  							Succeeded:      1,
   880  							StartTime:      &start,
   881  							CompletionTime: &start,
   882  							Conditions: []batchv1.JobCondition{
   883  								{
   884  									LastProbeTime:      start,
   885  									LastTransitionTime: start,
   886  									Status:             corev1.ConditionTrue,
   887  									Type:               batchv1.JobComplete,
   888  								},
   889  							},
   890  						},
   891  					},
   892  				},
   893  				roles: []*rbacv1.Role{
   894  					{
   895  						ObjectMeta: metav1.ObjectMeta{
   896  							Name:      digestHash,
   897  							Namespace: "ns-a",
   898  							Labels:    map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue},
   899  							OwnerReferences: []metav1.OwnerReference{
   900  								{
   901  									APIVersion:         "v1",
   902  									Kind:               "ConfigMap",
   903  									Name:               digestHash,
   904  									Controller:         &blockOwnerDeletion,
   905  									BlockOwnerDeletion: &blockOwnerDeletion,
   906  								},
   907  							},
   908  						},
   909  						Rules: []rbacv1.PolicyRule{
   910  							{
   911  								APIGroups: []string{
   912  									"",
   913  								},
   914  								Verbs: []string{
   915  									"create", "get", "update",
   916  								},
   917  								Resources: []string{
   918  									"configmaps",
   919  								},
   920  								ResourceNames: []string{
   921  									digestHash,
   922  								},
   923  							},
   924  						},
   925  					},
   926  				},
   927  				roleBindings: []*rbacv1.RoleBinding{
   928  					{
   929  						ObjectMeta: metav1.ObjectMeta{
   930  							Name:      digestHash,
   931  							Namespace: "ns-a",
   932  							Labels:    map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue},
   933  							OwnerReferences: []metav1.OwnerReference{
   934  								{
   935  									APIVersion:         "v1",
   936  									Kind:               "ConfigMap",
   937  									Name:               digestHash,
   938  									Controller:         &blockOwnerDeletion,
   939  									BlockOwnerDeletion: &blockOwnerDeletion,
   940  								},
   941  							},
   942  						},
   943  						Subjects: []rbacv1.Subject{
   944  							{
   945  								Kind:      "ServiceAccount",
   946  								APIGroup:  "",
   947  								Name:      "default",
   948  								Namespace: "ns-a",
   949  							},
   950  						},
   951  						RoleRef: rbacv1.RoleRef{
   952  							APIGroup: "rbac.authorization.k8s.io",
   953  							Kind:     "Role",
   954  							Name:     digestHash,
   955  						},
   956  					},
   957  				},
   958  			},
   959  		},
   960  		{
   961  			description: "CatalogSourcePresent/JobPending/PodPending/BundlePending/WithContainerStatus",
   962  			fields: fields{
   963  				objs: []runtime.Object{
   964  					&corev1.Pod{
   965  						ObjectMeta: metav1.ObjectMeta{
   966  							Name:      pathHash + "-pod",
   967  							Namespace: "ns-a",
   968  							Labels:    map[string]string{"job-name": pathHash},
   969  						},
   970  						Status: corev1.PodStatus{
   971  							Phase: corev1.PodPending,
   972  							InitContainerStatuses: []corev1.ContainerStatus{
   973  								{
   974  									Name:  "pull",
   975  									Ready: false,
   976  									State: corev1.ContainerState{
   977  										Waiting: &corev1.ContainerStateWaiting{
   978  											Reason:  "ErrImagePull",
   979  											Message: "pod pending for some reason",
   980  										},
   981  									},
   982  								},
   983  							},
   984  						},
   985  					},
   986  					&batchv1.Job{
   987  						ObjectMeta: metav1.ObjectMeta{
   988  							Name:      pathHash,
   989  							Namespace: "ns-a",
   990  							Labels:    map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue, bundleUnpackRefLabel: pathHash},
   991  							OwnerReferences: []metav1.OwnerReference{
   992  								{
   993  									APIVersion:         "v1",
   994  									Kind:               "ConfigMap",
   995  									Name:               pathHash,
   996  									Controller:         &blockOwnerDeletion,
   997  									BlockOwnerDeletion: &blockOwnerDeletion,
   998  								},
   999  							},
  1000  						},
  1001  						Spec: batchv1.JobSpec{
  1002  							ActiveDeadlineSeconds: &defaultUnpackTimeoutSeconds,
  1003  							BackoffLimit:          &backoffLimit,
  1004  							Template: corev1.PodTemplateSpec{
  1005  								ObjectMeta: metav1.ObjectMeta{
  1006  									Name:   pathHash,
  1007  									Labels: map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue},
  1008  								},
  1009  								Spec: corev1.PodSpec{
  1010  									RestartPolicy: corev1.RestartPolicyNever,
  1011  									SecurityContext: &corev1.PodSecurityContext{
  1012  										RunAsNonRoot: ptr.To(bool(true)),
  1013  										RunAsUser:    ptr.To(int64(runAsUser)),
  1014  										SeccompProfile: &corev1.SeccompProfile{
  1015  											Type: corev1.SeccompProfileTypeRuntimeDefault,
  1016  										},
  1017  									},
  1018  									Containers: []corev1.Container{
  1019  										{
  1020  											Name:    "extract",
  1021  											Image:   opmImage,
  1022  											Command: []string{"opm", "alpha", "bundle", "extract", "-m", "/bundle/", "-n", "ns-a", "-c", pathHash, "-z"},
  1023  											Env: []corev1.EnvVar{
  1024  												{
  1025  													Name:  configmap.EnvContainerImage,
  1026  													Value: bundlePath,
  1027  												},
  1028  											},
  1029  											VolumeMounts: []corev1.VolumeMount{
  1030  												{
  1031  													Name:      "bundle",
  1032  													MountPath: "/bundle",
  1033  												},
  1034  											},
  1035  											Resources: corev1.ResourceRequirements{
  1036  												Requests: corev1.ResourceList{
  1037  													corev1.ResourceCPU:    resource.MustParse("10m"),
  1038  													corev1.ResourceMemory: resource.MustParse("50Mi"),
  1039  												},
  1040  											},
  1041  											SecurityContext: &corev1.SecurityContext{
  1042  												AllowPrivilegeEscalation: ptr.To(bool(false)),
  1043  												Capabilities: &corev1.Capabilities{
  1044  													Drop: []corev1.Capability{"ALL"},
  1045  												},
  1046  											},
  1047  											TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
  1048  										},
  1049  									},
  1050  									InitContainers: []corev1.Container{
  1051  										{
  1052  											Name:    "util",
  1053  											Image:   utilImage,
  1054  											Command: []string{"/bin/cp", "-Rv", "/bin/cpb", "/util/cpb"}, // Copy tooling for the bundle container to use
  1055  											VolumeMounts: []corev1.VolumeMount{
  1056  												{
  1057  													Name:      "util",
  1058  													MountPath: "/util",
  1059  												},
  1060  											},
  1061  											Resources: corev1.ResourceRequirements{
  1062  												Requests: corev1.ResourceList{
  1063  													corev1.ResourceCPU:    resource.MustParse("10m"),
  1064  													corev1.ResourceMemory: resource.MustParse("50Mi"),
  1065  												},
  1066  											},
  1067  											SecurityContext: &corev1.SecurityContext{
  1068  												AllowPrivilegeEscalation: ptr.To(bool(false)),
  1069  												Capabilities: &corev1.Capabilities{
  1070  													Drop: []corev1.Capability{"ALL"},
  1071  												},
  1072  											},
  1073  											TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
  1074  										},
  1075  										{
  1076  											Name:            "pull",
  1077  											Image:           bundlePath,
  1078  											ImagePullPolicy: "Always",
  1079  											Command:         []string{"/util/cpb", "/bundle"}, // Copy bundle content to its mount
  1080  											VolumeMounts: []corev1.VolumeMount{
  1081  												{
  1082  													Name:      "bundle",
  1083  													MountPath: "/bundle",
  1084  												},
  1085  												{
  1086  													Name:      "util",
  1087  													MountPath: "/util",
  1088  												},
  1089  											},
  1090  											Resources: corev1.ResourceRequirements{
  1091  												Requests: corev1.ResourceList{
  1092  													corev1.ResourceCPU:    resource.MustParse("10m"),
  1093  													corev1.ResourceMemory: resource.MustParse("50Mi"),
  1094  												},
  1095  											},
  1096  											SecurityContext: &corev1.SecurityContext{
  1097  												AllowPrivilegeEscalation: ptr.To(bool(false)),
  1098  												Capabilities: &corev1.Capabilities{
  1099  													Drop: []corev1.Capability{"ALL"},
  1100  												},
  1101  											},
  1102  											TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
  1103  										},
  1104  									},
  1105  									Volumes: []corev1.Volume{
  1106  										{
  1107  											Name: "bundle",
  1108  											VolumeSource: corev1.VolumeSource{
  1109  												EmptyDir: &corev1.EmptyDirVolumeSource{},
  1110  											},
  1111  										},
  1112  										{
  1113  											Name: "util",
  1114  											VolumeSource: corev1.VolumeSource{
  1115  												EmptyDir: &corev1.EmptyDirVolumeSource{},
  1116  											},
  1117  										},
  1118  									},
  1119  									NodeSelector: map[string]string{
  1120  										"kubernetes.io/os": "linux",
  1121  									},
  1122  									Tolerations: []corev1.Toleration{
  1123  										{
  1124  											Key:      "kubernetes.io/arch",
  1125  											Value:    "amd64",
  1126  											Operator: "Equal",
  1127  										},
  1128  										{
  1129  											Key:      "kubernetes.io/arch",
  1130  											Value:    "arm64",
  1131  											Operator: "Equal",
  1132  										},
  1133  										{
  1134  											Key:      "kubernetes.io/arch",
  1135  											Value:    "ppc64le",
  1136  											Operator: "Equal",
  1137  										},
  1138  										{
  1139  											Key:      "kubernetes.io/arch",
  1140  											Value:    "s390x",
  1141  											Operator: "Equal",
  1142  										},
  1143  									},
  1144  								},
  1145  							},
  1146  						},
  1147  					},
  1148  					&corev1.ConfigMap{
  1149  						ObjectMeta: metav1.ObjectMeta{
  1150  							Name:      pathHash,
  1151  							Namespace: "ns-a",
  1152  							Labels:    map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue},
  1153  							OwnerReferences: []metav1.OwnerReference{
  1154  								{
  1155  									APIVersion:         "operators.coreos.com/v1alpha1",
  1156  									Kind:               "CatalogSource",
  1157  									Name:               "src-a",
  1158  									Controller:         &blockOwnerDeletion,
  1159  									BlockOwnerDeletion: &blockOwnerDeletion,
  1160  								},
  1161  							},
  1162  						},
  1163  					},
  1164  				},
  1165  				crs: []runtime.Object{
  1166  					&operatorsv1alpha1.CatalogSource{
  1167  						ObjectMeta: metav1.ObjectMeta{
  1168  							Namespace: "ns-a",
  1169  							Name:      "src-a",
  1170  						},
  1171  					},
  1172  				},
  1173  			},
  1174  
  1175  			args: args{
  1176  				annotationTimeout: -1 * time.Minute,
  1177  				lookup: &operatorsv1alpha1.BundleLookup{
  1178  					Path:     bundlePath,
  1179  					Replaces: "",
  1180  					CatalogSourceRef: &corev1.ObjectReference{
  1181  						Namespace: "ns-a",
  1182  						Name:      "src-a",
  1183  					},
  1184  					Conditions: []operatorsv1alpha1.BundleLookupCondition{
  1185  						{
  1186  							Type:               operatorsv1alpha1.BundleLookupPending,
  1187  							Status:             corev1.ConditionTrue,
  1188  							Reason:             JobIncompleteReason,
  1189  							Message:            JobIncompleteMessage,
  1190  							LastTransitionTime: &start,
  1191  						},
  1192  					},
  1193  				},
  1194  			},
  1195  
  1196  			expected: expected{
  1197  				res: &BundleUnpackResult{
  1198  					name: pathHash,
  1199  					BundleLookup: &operatorsv1alpha1.BundleLookup{
  1200  						Path:     bundlePath,
  1201  						Replaces: "",
  1202  						CatalogSourceRef: &corev1.ObjectReference{
  1203  							Namespace: "ns-a",
  1204  							Name:      "src-a",
  1205  						},
  1206  						Conditions: []operatorsv1alpha1.BundleLookupCondition{
  1207  							{
  1208  								Type:   operatorsv1alpha1.BundleLookupPending,
  1209  								Status: corev1.ConditionTrue,
  1210  								Reason: JobIncompleteReason,
  1211  								Message: fmt.Sprintf("%s: Unpack pod(ns-a/%s) container(pull) is pending. Reason: ErrImagePull, Message: pod pending for some reason",
  1212  									JobIncompleteMessage, pathHash+"-pod"),
  1213  								LastTransitionTime: &start,
  1214  							},
  1215  						},
  1216  					},
  1217  				},
  1218  			},
  1219  		},
  1220  		{
  1221  			description: "CatalogSourcePresent/JobFailed/BundleLookupFailed/WithJobFailReasonNoLabel",
  1222  			fields: fields{
  1223  				objs: []runtime.Object{
  1224  					&batchv1.Job{
  1225  						ObjectMeta: metav1.ObjectMeta{
  1226  							Name:      pathHash,
  1227  							Namespace: "ns-a",
  1228  							//omit the "operatorframework.io/bundle-unpack-ref" label
  1229  							Labels: map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue},
  1230  							OwnerReferences: []metav1.OwnerReference{
  1231  								{
  1232  									APIVersion:         "v1",
  1233  									Kind:               "ConfigMap",
  1234  									Name:               pathHash,
  1235  									Controller:         &blockOwnerDeletion,
  1236  									BlockOwnerDeletion: &blockOwnerDeletion,
  1237  								},
  1238  							},
  1239  						},
  1240  						Spec: batchv1.JobSpec{
  1241  							ActiveDeadlineSeconds: &defaultUnpackTimeoutSeconds,
  1242  							BackoffLimit:          &backoffLimit,
  1243  							Template: corev1.PodTemplateSpec{
  1244  								ObjectMeta: metav1.ObjectMeta{
  1245  									Name:   pathHash,
  1246  									Labels: map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue},
  1247  								},
  1248  								Spec: corev1.PodSpec{
  1249  									RestartPolicy: corev1.RestartPolicyNever,
  1250  									SecurityContext: &corev1.PodSecurityContext{
  1251  										RunAsNonRoot: ptr.To(bool(true)),
  1252  										RunAsUser:    ptr.To(int64(runAsUser)),
  1253  										SeccompProfile: &corev1.SeccompProfile{
  1254  											Type: corev1.SeccompProfileTypeRuntimeDefault,
  1255  										},
  1256  									},
  1257  									Containers: []corev1.Container{
  1258  										{
  1259  											Name:    "extract",
  1260  											Image:   opmImage,
  1261  											Command: []string{"opm", "alpha", "bundle", "extract", "-m", "/bundle/", "-n", "ns-a", "-c", pathHash, "-z"},
  1262  											Env: []corev1.EnvVar{
  1263  												{
  1264  													Name:  configmap.EnvContainerImage,
  1265  													Value: bundlePath,
  1266  												},
  1267  											},
  1268  											VolumeMounts: []corev1.VolumeMount{
  1269  												{
  1270  													Name:      "bundle",
  1271  													MountPath: "/bundle",
  1272  												},
  1273  											},
  1274  											Resources: corev1.ResourceRequirements{
  1275  												Requests: corev1.ResourceList{
  1276  													corev1.ResourceCPU:    resource.MustParse("10m"),
  1277  													corev1.ResourceMemory: resource.MustParse("50Mi"),
  1278  												},
  1279  											},
  1280  											SecurityContext: &corev1.SecurityContext{
  1281  												AllowPrivilegeEscalation: ptr.To(bool(false)),
  1282  												Capabilities: &corev1.Capabilities{
  1283  													Drop: []corev1.Capability{"ALL"},
  1284  												},
  1285  											},
  1286  											TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
  1287  										},
  1288  									},
  1289  									InitContainers: []corev1.Container{
  1290  										{
  1291  											Name:    "util",
  1292  											Image:   utilImage,
  1293  											Command: []string{"/bin/cp", "-Rv", "/bin/cpb", "/util/cpb"}, // Copy tooling for the bundle container to use
  1294  											VolumeMounts: []corev1.VolumeMount{
  1295  												{
  1296  													Name:      "util",
  1297  													MountPath: "/util",
  1298  												},
  1299  											},
  1300  											Resources: corev1.ResourceRequirements{
  1301  												Requests: corev1.ResourceList{
  1302  													corev1.ResourceCPU:    resource.MustParse("10m"),
  1303  													corev1.ResourceMemory: resource.MustParse("50Mi"),
  1304  												},
  1305  											},
  1306  											SecurityContext: &corev1.SecurityContext{
  1307  												AllowPrivilegeEscalation: ptr.To(bool(false)),
  1308  												Capabilities: &corev1.Capabilities{
  1309  													Drop: []corev1.Capability{"ALL"},
  1310  												},
  1311  											},
  1312  											TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
  1313  										},
  1314  										{
  1315  											Name:            "pull",
  1316  											Image:           bundlePath,
  1317  											ImagePullPolicy: "Always",
  1318  											Command:         []string{"/util/cpb", "/bundle"}, // Copy bundle content to its mount
  1319  											VolumeMounts: []corev1.VolumeMount{
  1320  												{
  1321  													Name:      "bundle",
  1322  													MountPath: "/bundle",
  1323  												},
  1324  												{
  1325  													Name:      "util",
  1326  													MountPath: "/util",
  1327  												},
  1328  											},
  1329  											Resources: corev1.ResourceRequirements{
  1330  												Requests: corev1.ResourceList{
  1331  													corev1.ResourceCPU:    resource.MustParse("10m"),
  1332  													corev1.ResourceMemory: resource.MustParse("50Mi"),
  1333  												},
  1334  											},
  1335  											SecurityContext: &corev1.SecurityContext{
  1336  												AllowPrivilegeEscalation: ptr.To(bool(false)),
  1337  												Capabilities: &corev1.Capabilities{
  1338  													Drop: []corev1.Capability{"ALL"},
  1339  												},
  1340  											},
  1341  											TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
  1342  										},
  1343  									},
  1344  									Volumes: []corev1.Volume{
  1345  										{
  1346  											Name: "bundle",
  1347  											VolumeSource: corev1.VolumeSource{
  1348  												EmptyDir: &corev1.EmptyDirVolumeSource{},
  1349  											},
  1350  										},
  1351  										{
  1352  											Name: "util",
  1353  											VolumeSource: corev1.VolumeSource{
  1354  												EmptyDir: &corev1.EmptyDirVolumeSource{},
  1355  											},
  1356  										},
  1357  									},
  1358  									NodeSelector: map[string]string{
  1359  										"kubernetes.io/os": "linux",
  1360  									},
  1361  									Tolerations: []corev1.Toleration{
  1362  										{
  1363  											Key:      "kubernetes.io/arch",
  1364  											Value:    "amd64",
  1365  											Operator: "Equal",
  1366  										},
  1367  										{
  1368  											Key:      "kubernetes.io/arch",
  1369  											Value:    "arm64",
  1370  											Operator: "Equal",
  1371  										},
  1372  										{
  1373  											Key:      "kubernetes.io/arch",
  1374  											Value:    "ppc64le",
  1375  											Operator: "Equal",
  1376  										},
  1377  										{
  1378  											Key:      "kubernetes.io/arch",
  1379  											Value:    "s390x",
  1380  											Operator: "Equal",
  1381  										},
  1382  									},
  1383  								},
  1384  							},
  1385  						},
  1386  						Status: batchv1.JobStatus{
  1387  							Failed: 1,
  1388  							Conditions: []batchv1.JobCondition{
  1389  								{
  1390  
  1391  									Type:    batchv1.JobFailed,
  1392  									Status:  corev1.ConditionTrue,
  1393  									Message: "Job was active longer than specified deadline",
  1394  									Reason:  "DeadlineExceeded",
  1395  								},
  1396  							},
  1397  						},
  1398  					},
  1399  					&corev1.ConfigMap{
  1400  						ObjectMeta: metav1.ObjectMeta{
  1401  							Name:      pathHash,
  1402  							Namespace: "ns-a",
  1403  							Labels:    map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue},
  1404  							OwnerReferences: []metav1.OwnerReference{
  1405  								{
  1406  									APIVersion:         "operators.coreos.com/v1alpha1",
  1407  									Kind:               "CatalogSource",
  1408  									Name:               "src-a",
  1409  									Controller:         &blockOwnerDeletion,
  1410  									BlockOwnerDeletion: &blockOwnerDeletion,
  1411  								},
  1412  							},
  1413  						},
  1414  					},
  1415  				},
  1416  				crs: []runtime.Object{
  1417  					&operatorsv1alpha1.CatalogSource{
  1418  						ObjectMeta: metav1.ObjectMeta{
  1419  							Namespace: "ns-a",
  1420  							Name:      "src-a",
  1421  						},
  1422  					},
  1423  				},
  1424  			},
  1425  			args: args{
  1426  				annotationTimeout: -1 * time.Minute,
  1427  				lookup: &operatorsv1alpha1.BundleLookup{
  1428  					Path:     bundlePath,
  1429  					Replaces: "",
  1430  					CatalogSourceRef: &corev1.ObjectReference{
  1431  						Namespace: "ns-a",
  1432  						Name:      "src-a",
  1433  					},
  1434  					Conditions: []operatorsv1alpha1.BundleLookupCondition{
  1435  						{
  1436  							Type:               operatorsv1alpha1.BundleLookupPending,
  1437  							Status:             corev1.ConditionTrue,
  1438  							Reason:             JobIncompleteReason,
  1439  							Message:            JobIncompleteMessage,
  1440  							LastTransitionTime: &start,
  1441  						},
  1442  					},
  1443  				},
  1444  			},
  1445  			expected: expected{
  1446  				// If job is not found due to missing "operatorframework.io/bundle-unpack-ref" label,
  1447  				// we will get an 'AlreadyExists' error in this test when the new job is created
  1448  				err: nil,
  1449  				res: &BundleUnpackResult{
  1450  					name: pathHash,
  1451  					BundleLookup: &operatorsv1alpha1.BundleLookup{
  1452  						Path:     bundlePath,
  1453  						Replaces: "",
  1454  						CatalogSourceRef: &corev1.ObjectReference{
  1455  							Namespace: "ns-a",
  1456  							Name:      "src-a",
  1457  						},
  1458  						Conditions: []operatorsv1alpha1.BundleLookupCondition{
  1459  							{
  1460  								Type:               operatorsv1alpha1.BundleLookupPending,
  1461  								Status:             corev1.ConditionTrue,
  1462  								Reason:             JobIncompleteReason,
  1463  								Message:            JobIncompleteMessage,
  1464  								LastTransitionTime: &start,
  1465  							},
  1466  							{
  1467  								Type:               operatorsv1alpha1.BundleLookupFailed,
  1468  								Status:             corev1.ConditionTrue,
  1469  								Reason:             "DeadlineExceeded",
  1470  								Message:            "Job was active longer than specified deadline",
  1471  								LastTransitionTime: &start,
  1472  							},
  1473  						},
  1474  					},
  1475  				},
  1476  				jobs: []*batchv1.Job{
  1477  					{
  1478  						ObjectMeta: metav1.ObjectMeta{
  1479  							Name:      pathHash,
  1480  							Namespace: "ns-a",
  1481  							Labels:    map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue},
  1482  							OwnerReferences: []metav1.OwnerReference{
  1483  								{
  1484  									APIVersion:         "v1",
  1485  									Kind:               "ConfigMap",
  1486  									Name:               pathHash,
  1487  									Controller:         &blockOwnerDeletion,
  1488  									BlockOwnerDeletion: &blockOwnerDeletion,
  1489  								},
  1490  							},
  1491  						},
  1492  						Spec: batchv1.JobSpec{
  1493  							ActiveDeadlineSeconds: &defaultUnpackTimeoutSeconds,
  1494  							BackoffLimit:          &backoffLimit,
  1495  							Template: corev1.PodTemplateSpec{
  1496  								ObjectMeta: metav1.ObjectMeta{
  1497  									Name:   pathHash,
  1498  									Labels: map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue},
  1499  								},
  1500  								Spec: corev1.PodSpec{
  1501  									RestartPolicy: corev1.RestartPolicyNever,
  1502  									SecurityContext: &corev1.PodSecurityContext{
  1503  										RunAsNonRoot: ptr.To(bool(true)),
  1504  										RunAsUser:    ptr.To(int64(runAsUser)),
  1505  										SeccompProfile: &corev1.SeccompProfile{
  1506  											Type: corev1.SeccompProfileTypeRuntimeDefault,
  1507  										},
  1508  									},
  1509  									Containers: []corev1.Container{
  1510  										{
  1511  											Name:    "extract",
  1512  											Image:   opmImage,
  1513  											Command: []string{"opm", "alpha", "bundle", "extract", "-m", "/bundle/", "-n", "ns-a", "-c", pathHash, "-z"},
  1514  											Env: []corev1.EnvVar{
  1515  												{
  1516  													Name:  configmap.EnvContainerImage,
  1517  													Value: bundlePath,
  1518  												},
  1519  											},
  1520  											VolumeMounts: []corev1.VolumeMount{
  1521  												{
  1522  													Name:      "bundle",
  1523  													MountPath: "/bundle",
  1524  												},
  1525  											},
  1526  											Resources: corev1.ResourceRequirements{
  1527  												Requests: corev1.ResourceList{
  1528  													corev1.ResourceCPU:    resource.MustParse("10m"),
  1529  													corev1.ResourceMemory: resource.MustParse("50Mi"),
  1530  												},
  1531  											},
  1532  											SecurityContext: &corev1.SecurityContext{
  1533  												AllowPrivilegeEscalation: ptr.To(bool(false)),
  1534  												Capabilities: &corev1.Capabilities{
  1535  													Drop: []corev1.Capability{"ALL"},
  1536  												},
  1537  											},
  1538  											TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
  1539  										},
  1540  									},
  1541  									InitContainers: []corev1.Container{
  1542  										{
  1543  											Name:    "util",
  1544  											Image:   utilImage,
  1545  											Command: []string{"/bin/cp", "-Rv", "/bin/cpb", "/util/cpb"}, // Copy tooling for the bundle container to use
  1546  											VolumeMounts: []corev1.VolumeMount{
  1547  												{
  1548  													Name:      "util",
  1549  													MountPath: "/util",
  1550  												},
  1551  											},
  1552  											Resources: corev1.ResourceRequirements{
  1553  												Requests: corev1.ResourceList{
  1554  													corev1.ResourceCPU:    resource.MustParse("10m"),
  1555  													corev1.ResourceMemory: resource.MustParse("50Mi"),
  1556  												},
  1557  											},
  1558  											SecurityContext: &corev1.SecurityContext{
  1559  												AllowPrivilegeEscalation: ptr.To(bool(false)),
  1560  												Capabilities: &corev1.Capabilities{
  1561  													Drop: []corev1.Capability{"ALL"},
  1562  												},
  1563  											},
  1564  											TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
  1565  										},
  1566  										{
  1567  											Name:            "pull",
  1568  											Image:           bundlePath,
  1569  											ImagePullPolicy: "Always",
  1570  											Command:         []string{"/util/cpb", "/bundle"}, // Copy bundle content to its mount
  1571  											VolumeMounts: []corev1.VolumeMount{
  1572  												{
  1573  													Name:      "bundle",
  1574  													MountPath: "/bundle",
  1575  												},
  1576  												{
  1577  													Name:      "util",
  1578  													MountPath: "/util",
  1579  												},
  1580  											},
  1581  											Resources: corev1.ResourceRequirements{
  1582  												Requests: corev1.ResourceList{
  1583  													corev1.ResourceCPU:    resource.MustParse("10m"),
  1584  													corev1.ResourceMemory: resource.MustParse("50Mi"),
  1585  												},
  1586  											},
  1587  											SecurityContext: &corev1.SecurityContext{
  1588  												AllowPrivilegeEscalation: ptr.To(bool(false)),
  1589  												Capabilities: &corev1.Capabilities{
  1590  													Drop: []corev1.Capability{"ALL"},
  1591  												},
  1592  											},
  1593  											TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
  1594  										},
  1595  									},
  1596  									Volumes: []corev1.Volume{
  1597  										{
  1598  											Name: "bundle",
  1599  											VolumeSource: corev1.VolumeSource{
  1600  												EmptyDir: &corev1.EmptyDirVolumeSource{},
  1601  											},
  1602  										},
  1603  										{
  1604  											Name: "util",
  1605  											VolumeSource: corev1.VolumeSource{
  1606  												EmptyDir: &corev1.EmptyDirVolumeSource{},
  1607  											},
  1608  										},
  1609  									},
  1610  									NodeSelector: map[string]string{
  1611  										"kubernetes.io/os": "linux",
  1612  									},
  1613  									Tolerations: []corev1.Toleration{
  1614  										{
  1615  											Key:      "kubernetes.io/arch",
  1616  											Value:    "amd64",
  1617  											Operator: "Equal",
  1618  										},
  1619  										{
  1620  											Key:      "kubernetes.io/arch",
  1621  											Value:    "arm64",
  1622  											Operator: "Equal",
  1623  										},
  1624  										{
  1625  											Key:      "kubernetes.io/arch",
  1626  											Value:    "ppc64le",
  1627  											Operator: "Equal",
  1628  										},
  1629  										{
  1630  											Key:      "kubernetes.io/arch",
  1631  											Value:    "s390x",
  1632  											Operator: "Equal",
  1633  										},
  1634  									},
  1635  								},
  1636  							},
  1637  						},
  1638  						Status: batchv1.JobStatus{
  1639  							Failed: 1,
  1640  							Conditions: []batchv1.JobCondition{
  1641  								{
  1642  									Type:    batchv1.JobFailed,
  1643  									Status:  corev1.ConditionTrue,
  1644  									Message: "Job was active longer than specified deadline",
  1645  									Reason:  "DeadlineExceeded",
  1646  								},
  1647  							},
  1648  						},
  1649  					},
  1650  				},
  1651  			},
  1652  		},
  1653  	}
  1654  
  1655  	for _, tt := range tests {
  1656  		t.Run(tt.description, func(t *testing.T) {
  1657  			client := k8sfake.NewSimpleClientset(tt.fields.objs...)
  1658  
  1659  			period := 5 * time.Minute
  1660  			factory := informers.NewSharedInformerFactory(client, period)
  1661  			configMapInformer := informers.NewSharedInformerFactoryWithOptions(client, period, informers.WithTweakListOptions(func(options *metav1.ListOptions) {
  1662  				options.LabelSelector = install.OLMManagedLabelKey
  1663  			})).Core().V1().ConfigMaps()
  1664  			cmLister := configMapInformer.Lister()
  1665  			jobLister := factory.Batch().V1().Jobs().Lister()
  1666  			podLister := factory.Core().V1().Pods().Lister()
  1667  			roleLister := factory.Rbac().V1().Roles().Lister()
  1668  			rbLister := factory.Rbac().V1().RoleBindings().Lister()
  1669  
  1670  			stop := make(chan struct{})
  1671  			defer close(stop)
  1672  
  1673  			factory.Start(stop)
  1674  			factory.WaitForCacheSync(context.Background().Done())
  1675  
  1676  			crClient := crfake.NewSimpleClientset(tt.fields.crs...)
  1677  			crFactory := crinformers.NewSharedInformerFactory(crClient, period)
  1678  			csLister := crFactory.Operators().V1alpha1().CatalogSources().Lister()
  1679  			crFactory.Start(stop)
  1680  			crFactory.WaitForCacheSync(context.Background().Done())
  1681  
  1682  			unpacker, err := NewConfigmapUnpacker(
  1683  				WithClient(client),
  1684  				WithCatalogSourceLister(csLister),
  1685  				WithConfigMapLister(cmLister),
  1686  				WithJobLister(jobLister),
  1687  				WithPodLister(podLister),
  1688  				WithRoleLister(roleLister),
  1689  				WithRoleBindingLister(rbLister),
  1690  				WithOPMImage(opmImage),
  1691  				WithUtilImage(utilImage),
  1692  				WithNow(now),
  1693  				WithUnpackTimeout(defaultUnpackDuration),
  1694  				WithUserID(int64(runAsUser)),
  1695  			)
  1696  			require.NoError(t, err)
  1697  
  1698  			res, err := unpacker.UnpackBundle(tt.args.lookup, tt.args.annotationTimeout, 0)
  1699  			require.Equal(t, tt.expected.err, err)
  1700  
  1701  			if tt.expected.res == nil {
  1702  				require.Nil(t, res)
  1703  			} else {
  1704  				if tt.expected.res.bundle == nil {
  1705  					require.Nil(t, res.bundle)
  1706  				} else {
  1707  					require.NotNil(t, res.bundle)
  1708  					require.Equal(t, tt.expected.res.bundle.CsvJson, res.bundle.CsvJson)
  1709  					require.Equal(t, tt.expected.res.bundle.CsvName, res.bundle.CsvName)
  1710  					require.Equal(t, tt.expected.res.bundle.Version, res.bundle.Version)
  1711  					require.Equal(t, tt.expected.res.bundle.SkipRange, res.bundle.SkipRange)
  1712  					require.Equal(t, tt.expected.res.bundle.ProvidedApis, res.bundle.ProvidedApis)
  1713  					require.Equal(t, tt.expected.res.bundle.RequiredApis, res.bundle.RequiredApis)
  1714  					require.Equal(t, tt.expected.res.bundle.PackageName, res.bundle.PackageName)
  1715  					require.Equal(t, tt.expected.res.bundle.ChannelName, res.bundle.ChannelName)
  1716  
  1717  					// Object order is not stable, so perform a set based assertion
  1718  					require.ElementsMatch(t, tt.expected.res.bundle.Object, res.bundle.Object)
  1719  				}
  1720  				require.Equal(t, tt.expected.res.Path, res.Path)
  1721  				require.Equal(t, tt.expected.res.Replaces, res.Replaces)
  1722  				require.Equal(t, tt.expected.res.CatalogSourceRef, res.CatalogSourceRef)
  1723  				require.ElementsMatch(t, tt.expected.res.Conditions, res.Conditions)
  1724  			}
  1725  
  1726  			opts := metav1.GetOptions{}
  1727  			for _, job := range tt.expected.jobs {
  1728  				stored, err := client.BatchV1().Jobs(job.GetNamespace()).Get(context.TODO(), job.GetName(), opts)
  1729  				require.NoError(t, err)
  1730  				require.Equal(t, job, stored)
  1731  			}
  1732  
  1733  			for _, cm := range tt.expected.configMaps {
  1734  				stored, err := client.CoreV1().ConfigMaps(cm.GetNamespace()).Get(context.TODO(), cm.GetName(), opts)
  1735  				require.NoError(t, err)
  1736  				require.Equal(t, cm, stored)
  1737  			}
  1738  
  1739  			for _, role := range tt.expected.roles {
  1740  				stored, err := client.RbacV1().Roles(role.GetNamespace()).Get(context.TODO(), role.GetName(), opts)
  1741  				require.NoError(t, err)
  1742  				require.Equal(t, role, stored)
  1743  			}
  1744  
  1745  			for _, rb := range tt.expected.roleBindings {
  1746  				stored, err := client.RbacV1().RoleBindings(rb.GetNamespace()).Get(context.TODO(), rb.GetName(), opts)
  1747  				require.NoError(t, err)
  1748  				require.Equal(t, rb, stored)
  1749  			}
  1750  		})
  1751  	}
  1752  }
  1753  
  1754  func TestOperatorGroupBundleUnpackTimeout(t *testing.T) {
  1755  	nsName := "fake-ns"
  1756  
  1757  	for _, tc := range []struct {
  1758  		name            string
  1759  		operatorGroups  []*operatorsv1.OperatorGroup
  1760  		expectedTimeout time.Duration
  1761  		expectedError   error
  1762  	}{
  1763  		{
  1764  			name:            "No operator groups exist",
  1765  			expectedTimeout: -1 * time.Minute,
  1766  			expectedError:   errors.New("found 0 operatorGroups, expected 1"),
  1767  		},
  1768  		{
  1769  			name: "Multiple operator groups exist",
  1770  			operatorGroups: []*operatorsv1.OperatorGroup{
  1771  				{
  1772  					TypeMeta: metav1.TypeMeta{
  1773  						Kind:       operatorsv1.OperatorGroupKind,
  1774  						APIVersion: operatorsv1.GroupVersion.String(),
  1775  					},
  1776  					ObjectMeta: metav1.ObjectMeta{
  1777  						Name:      "og1",
  1778  						Namespace: nsName,
  1779  					},
  1780  				},
  1781  				{
  1782  					TypeMeta: metav1.TypeMeta{
  1783  						Kind:       operatorsv1.OperatorGroupKind,
  1784  						APIVersion: operatorsv1.GroupVersion.String(),
  1785  					},
  1786  					ObjectMeta: metav1.ObjectMeta{
  1787  						Name:      "og2",
  1788  						Namespace: nsName,
  1789  					},
  1790  				},
  1791  			},
  1792  			expectedTimeout: -1 * time.Minute,
  1793  			expectedError:   errors.New("found 2 operatorGroups, expected 1"),
  1794  		},
  1795  		{
  1796  			name: "One operator group exists with valid timeout annotation",
  1797  			operatorGroups: []*operatorsv1.OperatorGroup{
  1798  				{
  1799  					TypeMeta: metav1.TypeMeta{
  1800  						Kind:       operatorsv1.OperatorGroupKind,
  1801  						APIVersion: operatorsv1.GroupVersion.String(),
  1802  					},
  1803  					ObjectMeta: metav1.ObjectMeta{
  1804  						Name:        "og",
  1805  						Namespace:   nsName,
  1806  						Annotations: map[string]string{BundleUnpackTimeoutAnnotationKey: "1m"},
  1807  					},
  1808  				},
  1809  			},
  1810  			expectedTimeout: 1 * time.Minute,
  1811  			expectedError:   nil,
  1812  		},
  1813  		{
  1814  			name: "One operator group exists with no timeout annotation",
  1815  			operatorGroups: []*operatorsv1.OperatorGroup{
  1816  				{
  1817  					TypeMeta: metav1.TypeMeta{
  1818  						Kind:       operatorsv1.OperatorGroupKind,
  1819  						APIVersion: operatorsv1.GroupVersion.String(),
  1820  					},
  1821  					ObjectMeta: metav1.ObjectMeta{
  1822  						Name:      "og",
  1823  						Namespace: nsName,
  1824  					},
  1825  				},
  1826  			},
  1827  			expectedTimeout: -1 * time.Minute,
  1828  		},
  1829  		{
  1830  			name: "One operator group exists with invalid timeout annotation",
  1831  			operatorGroups: []*operatorsv1.OperatorGroup{
  1832  				{
  1833  					TypeMeta: metav1.TypeMeta{
  1834  						Kind:       operatorsv1.OperatorGroupKind,
  1835  						APIVersion: operatorsv1.GroupVersion.String(),
  1836  					},
  1837  					ObjectMeta: metav1.ObjectMeta{
  1838  						Name:        "og",
  1839  						Namespace:   nsName,
  1840  						Annotations: map[string]string{BundleUnpackTimeoutAnnotationKey: "invalid"},
  1841  					},
  1842  				},
  1843  			},
  1844  			expectedTimeout: -1 * time.Minute,
  1845  			expectedError:   fmt.Errorf("failed to parse unpack timeout annotation(operatorframework.io/bundle-unpack-timeout: invalid): %w", errors.New("time: invalid duration \"invalid\"")),
  1846  		},
  1847  	} {
  1848  		t.Run(tc.name, func(t *testing.T) {
  1849  			ogIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{})
  1850  			ogLister := v1listers.NewOperatorGroupLister(ogIndexer).OperatorGroups(nsName)
  1851  
  1852  			for _, og := range tc.operatorGroups {
  1853  				err := ogIndexer.Add(og)
  1854  				assert.NoError(t, err)
  1855  			}
  1856  
  1857  			timeout, err := OperatorGroupBundleUnpackTimeout(ogLister)
  1858  
  1859  			assert.Equal(t, tc.expectedTimeout, timeout)
  1860  			assert.Equal(t, tc.expectedError, err)
  1861  		})
  1862  	}
  1863  }
  1864  
  1865  func TestOperatorGroupBundleUnpackRetryInterval(t *testing.T) {
  1866  	nsName := "fake-ns"
  1867  
  1868  	for _, tc := range []struct {
  1869  		name            string
  1870  		operatorGroups  []*operatorsv1.OperatorGroup
  1871  		expectedTimeout time.Duration
  1872  		expectedError   error
  1873  	}{
  1874  		{
  1875  			name:            "No operator groups exist",
  1876  			expectedTimeout: 0,
  1877  			expectedError:   errors.New("found 0 operatorGroups, expected 1"),
  1878  		},
  1879  		{
  1880  			name: "Multiple operator groups exist",
  1881  			operatorGroups: []*operatorsv1.OperatorGroup{
  1882  				{
  1883  					TypeMeta: metav1.TypeMeta{
  1884  						Kind:       operatorsv1.OperatorGroupKind,
  1885  						APIVersion: operatorsv1.GroupVersion.String(),
  1886  					},
  1887  					ObjectMeta: metav1.ObjectMeta{
  1888  						Name:      "og1",
  1889  						Namespace: nsName,
  1890  					},
  1891  				},
  1892  				{
  1893  					TypeMeta: metav1.TypeMeta{
  1894  						Kind:       operatorsv1.OperatorGroupKind,
  1895  						APIVersion: operatorsv1.GroupVersion.String(),
  1896  					},
  1897  					ObjectMeta: metav1.ObjectMeta{
  1898  						Name:      "og2",
  1899  						Namespace: nsName,
  1900  					},
  1901  				},
  1902  			},
  1903  			expectedTimeout: 0,
  1904  			expectedError:   errors.New("found 2 operatorGroups, expected 1"),
  1905  		},
  1906  		{
  1907  			name: "One operator group exists with valid unpack retry annotation",
  1908  			operatorGroups: []*operatorsv1.OperatorGroup{
  1909  				{
  1910  					TypeMeta: metav1.TypeMeta{
  1911  						Kind:       operatorsv1.OperatorGroupKind,
  1912  						APIVersion: operatorsv1.GroupVersion.String(),
  1913  					},
  1914  					ObjectMeta: metav1.ObjectMeta{
  1915  						Name:        "og",
  1916  						Namespace:   nsName,
  1917  						Annotations: map[string]string{BundleUnpackRetryMinimumIntervalAnnotationKey: "1m"},
  1918  					},
  1919  				},
  1920  			},
  1921  			expectedTimeout: 1 * time.Minute,
  1922  			expectedError:   nil,
  1923  		},
  1924  		{
  1925  			name: "One operator group exists with no unpack retry annotation",
  1926  			operatorGroups: []*operatorsv1.OperatorGroup{
  1927  				{
  1928  					TypeMeta: metav1.TypeMeta{
  1929  						Kind:       operatorsv1.OperatorGroupKind,
  1930  						APIVersion: operatorsv1.GroupVersion.String(),
  1931  					},
  1932  					ObjectMeta: metav1.ObjectMeta{
  1933  						Name:      "og",
  1934  						Namespace: nsName,
  1935  					},
  1936  				},
  1937  			},
  1938  			expectedTimeout: 0,
  1939  			expectedError:   nil,
  1940  		},
  1941  		{
  1942  			name: "One operator group exists with invalid unpack retry annotation",
  1943  			operatorGroups: []*operatorsv1.OperatorGroup{
  1944  				{
  1945  					TypeMeta: metav1.TypeMeta{
  1946  						Kind:       operatorsv1.OperatorGroupKind,
  1947  						APIVersion: operatorsv1.GroupVersion.String(),
  1948  					},
  1949  					ObjectMeta: metav1.ObjectMeta{
  1950  						Name:        "og",
  1951  						Namespace:   nsName,
  1952  						Annotations: map[string]string{BundleUnpackRetryMinimumIntervalAnnotationKey: "invalid"},
  1953  					},
  1954  				},
  1955  			},
  1956  			expectedTimeout: 0,
  1957  			expectedError:   fmt.Errorf("failed to parse unpack retry annotation(operatorframework.io/bundle-unpack-min-retry-interval: invalid): %w", errors.New("time: invalid duration \"invalid\"")),
  1958  		},
  1959  	} {
  1960  		t.Run(tc.name, func(t *testing.T) {
  1961  			ogIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{})
  1962  			ogLister := v1listers.NewOperatorGroupLister(ogIndexer).OperatorGroups(nsName)
  1963  
  1964  			for _, og := range tc.operatorGroups {
  1965  				err := ogIndexer.Add(og)
  1966  				assert.NoError(t, err)
  1967  			}
  1968  
  1969  			timeout, err := OperatorGroupBundleUnpackRetryInterval(ogLister)
  1970  
  1971  			assert.Equal(t, tc.expectedTimeout, timeout)
  1972  			assert.Equal(t, tc.expectedError, err)
  1973  		})
  1974  	}
  1975  }
  1976  
  1977  func TestSortUnpackJobs(t *testing.T) {
  1978  	// if there is a non-failed job, it should be first
  1979  	// otherwise, the latest job should be first
  1980  	//first n-1 jobs and oldest job are preserved
  1981  	testJob := func(name string, failed bool, ts int64) *batchv1.Job {
  1982  		conditions := []batchv1.JobCondition{}
  1983  		if failed {
  1984  			conditions = append(conditions, batchv1.JobCondition{
  1985  				Type:               batchv1.JobFailed,
  1986  				Status:             corev1.ConditionTrue,
  1987  				LastTransitionTime: metav1.Time{Time: time.Unix(ts, 0)},
  1988  			})
  1989  		}
  1990  		return &batchv1.Job{
  1991  			ObjectMeta: metav1.ObjectMeta{
  1992  				Name:   name,
  1993  				Labels: map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue, bundleUnpackRefLabel: "test"},
  1994  			},
  1995  			Status: batchv1.JobStatus{
  1996  				Conditions: conditions,
  1997  			},
  1998  		}
  1999  	}
  2000  	nilConditionJob := &batchv1.Job{
  2001  		ObjectMeta: metav1.ObjectMeta{
  2002  			Name:   "nc",
  2003  			Labels: map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue, bundleUnpackRefLabel: "test"},
  2004  		},
  2005  		Status: batchv1.JobStatus{
  2006  			Conditions: nil,
  2007  		},
  2008  	}
  2009  	failedJobs := []*batchv1.Job{
  2010  		testJob("f-1", true, 1),
  2011  		testJob("f-2", true, 2),
  2012  		testJob("f-3", true, 3),
  2013  		testJob("f-4", true, 4),
  2014  		testJob("f-5", true, 5),
  2015  	}
  2016  	nonFailedJob := testJob("s-1", false, 1)
  2017  	for _, tc := range []struct {
  2018  		name             string
  2019  		jobs             []*batchv1.Job
  2020  		maxRetained      int
  2021  		expectedLatest   *batchv1.Job
  2022  		expectedToDelete []*batchv1.Job
  2023  	}{
  2024  		{
  2025  			name:        "no job history",
  2026  			maxRetained: 0,
  2027  			jobs: []*batchv1.Job{
  2028  				failedJobs[1],
  2029  				failedJobs[2],
  2030  				failedJobs[0],
  2031  			},
  2032  			expectedLatest: failedJobs[2],
  2033  			expectedToDelete: []*batchv1.Job{
  2034  				failedJobs[1],
  2035  				failedJobs[0],
  2036  			},
  2037  		}, {
  2038  			name:        "empty job list",
  2039  			maxRetained: 1,
  2040  		}, {
  2041  			name:        "nil job in list",
  2042  			maxRetained: 1,
  2043  			jobs: []*batchv1.Job{
  2044  				failedJobs[2],
  2045  				nil,
  2046  				failedJobs[1],
  2047  			},
  2048  			expectedLatest: failedJobs[2],
  2049  		}, {
  2050  			name:        "nil condition",
  2051  			maxRetained: 3,
  2052  			jobs: []*batchv1.Job{
  2053  				failedJobs[2],
  2054  				nilConditionJob,
  2055  				failedJobs[1],
  2056  			},
  2057  			expectedLatest: nilConditionJob,
  2058  		}, {
  2059  			name:        "retain oldest",
  2060  			maxRetained: 1,
  2061  			jobs: []*batchv1.Job{
  2062  				failedJobs[2],
  2063  				failedJobs[0],
  2064  				failedJobs[1],
  2065  			},
  2066  			expectedToDelete: []*batchv1.Job{
  2067  				failedJobs[1],
  2068  			},
  2069  			expectedLatest: failedJobs[2],
  2070  		}, {
  2071  			name:        "multiple old jobs",
  2072  			maxRetained: 2,
  2073  			jobs: []*batchv1.Job{
  2074  				failedJobs[1],
  2075  				failedJobs[0],
  2076  				failedJobs[2],
  2077  				failedJobs[3],
  2078  				failedJobs[4],
  2079  			},
  2080  			expectedLatest: failedJobs[4],
  2081  			expectedToDelete: []*batchv1.Job{
  2082  				failedJobs[1],
  2083  				failedJobs[2],
  2084  			},
  2085  		}, {
  2086  			name:        "select non-failed as latest",
  2087  			maxRetained: 3,
  2088  			jobs: []*batchv1.Job{
  2089  				failedJobs[0],
  2090  				failedJobs[1],
  2091  				nonFailedJob,
  2092  			},
  2093  			expectedLatest: nonFailedJob,
  2094  		},
  2095  	} {
  2096  		latest, toDelete := sortUnpackJobs(tc.jobs, tc.maxRetained)
  2097  		assert.Equal(t, tc.expectedLatest, latest)
  2098  		assert.ElementsMatch(t, tc.expectedToDelete, toDelete)
  2099  	}
  2100  }