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

     1  package bundle
     2  
     3  import (
     4  	"context"
     5  	"crypto/sha256"
     6  	"fmt"
     7  	"sort"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/operator-framework/operator-registry/pkg/api"
    12  	"github.com/operator-framework/operator-registry/pkg/configmap"
    13  	"github.com/sirupsen/logrus"
    14  	batchv1 "k8s.io/api/batch/v1"
    15  	corev1 "k8s.io/api/core/v1"
    16  	rbacv1 "k8s.io/api/rbac/v1"
    17  	"k8s.io/apimachinery/pkg/api/equality"
    18  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    19  	"k8s.io/apimachinery/pkg/api/resource"
    20  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    21  	k8slabels "k8s.io/apimachinery/pkg/labels"
    22  	"k8s.io/apiserver/pkg/storage/names"
    23  	"k8s.io/client-go/kubernetes"
    24  	listersbatchv1 "k8s.io/client-go/listers/batch/v1"
    25  	listerscorev1 "k8s.io/client-go/listers/core/v1"
    26  	listersrbacv1 "k8s.io/client-go/listers/rbac/v1"
    27  	"k8s.io/utils/ptr"
    28  
    29  	"github.com/operator-framework/api/pkg/operators/reference"
    30  	operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
    31  	v1listers "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/listers/operators/v1"
    32  	listersoperatorsv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/listers/operators/v1alpha1"
    33  	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install"
    34  	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/projection"
    35  	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/image"
    36  )
    37  
    38  const (
    39  	// TODO: This can be a spec field
    40  	// BundleUnpackTimeoutAnnotationKey allows setting a bundle unpack timeout per OperatorGroup
    41  	// and overrides the default specified by the --bundle-unpack-timeout flag
    42  	// The time duration should be in the same format as accepted by time.ParseDuration()
    43  	// e.g 1m30s
    44  	BundleUnpackTimeoutAnnotationKey = "operatorframework.io/bundle-unpack-timeout"
    45  	BundleUnpackPodLabel             = "job-name"
    46  
    47  	// BundleUnpackRetryMinimumIntervalAnnotationKey sets a minimum interval to wait before
    48  	// attempting to recreate a failed unpack job for a bundle.
    49  	BundleUnpackRetryMinimumIntervalAnnotationKey = "operatorframework.io/bundle-unpack-min-retry-interval"
    50  
    51  	// bundleUnpackRefLabel is used to filter for all unpack jobs for a specific bundle.
    52  	bundleUnpackRefLabel = "operatorframework.io/bundle-unpack-ref"
    53  )
    54  
    55  type BundleUnpackResult struct {
    56  	*operatorsv1alpha1.BundleLookup
    57  
    58  	bundle *api.Bundle
    59  	name   string
    60  }
    61  
    62  func (b *BundleUnpackResult) Bundle() *api.Bundle {
    63  	return b.bundle
    64  }
    65  
    66  func (b *BundleUnpackResult) Name() string {
    67  	return b.name
    68  }
    69  
    70  // SetCondition replaces the existing BundleLookupCondition of the same type, or adds it if it was not found.
    71  func (b *BundleUnpackResult) SetCondition(cond operatorsv1alpha1.BundleLookupCondition) operatorsv1alpha1.BundleLookupCondition {
    72  	for i, existing := range b.Conditions {
    73  		if existing.Type != cond.Type {
    74  			continue
    75  		}
    76  		if existing.Status == cond.Status && existing.Reason == cond.Reason {
    77  			cond.LastTransitionTime = existing.LastTransitionTime
    78  		}
    79  		b.Conditions[i] = cond
    80  		return cond
    81  	}
    82  	b.Conditions = append(b.Conditions, cond)
    83  
    84  	return cond
    85  }
    86  
    87  var catalogSourceGVK = operatorsv1alpha1.SchemeGroupVersion.WithKind(operatorsv1alpha1.CatalogSourceKind)
    88  
    89  func newBundleUnpackResult(lookup *operatorsv1alpha1.BundleLookup) *BundleUnpackResult {
    90  	return &BundleUnpackResult{
    91  		BundleLookup: lookup.DeepCopy(),
    92  		name:         hash(lookup.Path),
    93  	}
    94  }
    95  
    96  func (c *ConfigMapUnpacker) job(cmRef *corev1.ObjectReference, bundlePath string, secrets []corev1.LocalObjectReference, annotationUnpackTimeout time.Duration) *batchv1.Job {
    97  	job := &batchv1.Job{
    98  		ObjectMeta: metav1.ObjectMeta{
    99  			Labels: map[string]string{
   100  				install.OLMManagedLabelKey: install.OLMManagedLabelValue,
   101  				bundleUnpackRefLabel:       cmRef.Name,
   102  			},
   103  		},
   104  		Spec: batchv1.JobSpec{
   105  			//ttlSecondsAfterFinished: 0 // can use in the future to not have to clean up job
   106  			Template: corev1.PodTemplateSpec{
   107  				ObjectMeta: metav1.ObjectMeta{
   108  					Name: cmRef.Name,
   109  					Labels: map[string]string{
   110  						install.OLMManagedLabelKey: install.OLMManagedLabelValue,
   111  					},
   112  				},
   113  				Spec: corev1.PodSpec{
   114  					// With restartPolicy = "OnFailure" when the spec.backoffLimit is reached, the job controller will delete all
   115  					// the job's pods to stop them from crashlooping forever.
   116  					// By setting restartPolicy = "Never" the pods don't get cleaned up since they're not running after a failure.
   117  					// Keeping the pods around after failures helps in inspecting the logs of a failed bundle unpack job.
   118  					// See: https://kubernetes.io/docs/concepts/workloads/controllers/job/#pod-backoff-failure-policy
   119  					RestartPolicy:    corev1.RestartPolicyNever,
   120  					ImagePullSecrets: secrets,
   121  					SecurityContext: &corev1.PodSecurityContext{
   122  						SeccompProfile: &corev1.SeccompProfile{
   123  							Type: corev1.SeccompProfileTypeRuntimeDefault,
   124  						},
   125  					},
   126  					Containers: []corev1.Container{
   127  						{
   128  							Name:  "extract",
   129  							Image: c.opmImage,
   130  							Command: []string{"opm", "alpha", "bundle", "extract",
   131  								"-m", "/bundle/",
   132  								"-n", cmRef.Namespace,
   133  								"-c", cmRef.Name,
   134  								"-z",
   135  							},
   136  							Env: []corev1.EnvVar{
   137  								{
   138  									Name:  configmap.EnvContainerImage,
   139  									Value: bundlePath,
   140  								},
   141  							},
   142  							VolumeMounts: []corev1.VolumeMount{
   143  								{
   144  									Name:      "bundle", // Expected bundle content mount
   145  									MountPath: "/bundle",
   146  								},
   147  							},
   148  							Resources: corev1.ResourceRequirements{
   149  								Requests: corev1.ResourceList{
   150  									corev1.ResourceCPU:    resource.MustParse("10m"),
   151  									corev1.ResourceMemory: resource.MustParse("50Mi"),
   152  								},
   153  							},
   154  							SecurityContext: &corev1.SecurityContext{
   155  								AllowPrivilegeEscalation: ptr.To(bool(false)),
   156  								Capabilities: &corev1.Capabilities{
   157  									Drop: []corev1.Capability{"ALL"},
   158  								},
   159  							},
   160  							TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
   161  						},
   162  					},
   163  					InitContainers: []corev1.Container{
   164  						{
   165  							Name:    "util",
   166  							Image:   c.utilImage,
   167  							Command: []string{"/bin/cp", "-Rv", "/bin/cpb", "/util/cpb"}, // Copy tooling for the bundle container to use
   168  							VolumeMounts: []corev1.VolumeMount{
   169  								{
   170  									Name:      "util",
   171  									MountPath: "/util",
   172  								},
   173  							},
   174  							Resources: corev1.ResourceRequirements{
   175  								Requests: corev1.ResourceList{
   176  									corev1.ResourceCPU:    resource.MustParse("10m"),
   177  									corev1.ResourceMemory: resource.MustParse("50Mi"),
   178  								},
   179  							},
   180  							SecurityContext: &corev1.SecurityContext{
   181  								AllowPrivilegeEscalation: ptr.To(bool(false)),
   182  								Capabilities: &corev1.Capabilities{
   183  									Drop: []corev1.Capability{"ALL"},
   184  								},
   185  							},
   186  							TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
   187  						},
   188  						{
   189  							Name:            "pull",
   190  							Image:           bundlePath,
   191  							ImagePullPolicy: image.InferImagePullPolicy(bundlePath),
   192  							Command:         []string{"/util/cpb", "/bundle"}, // Copy bundle content to its mount
   193  							VolumeMounts: []corev1.VolumeMount{
   194  								{
   195  									Name:      "bundle",
   196  									MountPath: "/bundle",
   197  								},
   198  								{
   199  									Name:      "util",
   200  									MountPath: "/util",
   201  								},
   202  							},
   203  							Resources: corev1.ResourceRequirements{
   204  								Requests: corev1.ResourceList{
   205  									corev1.ResourceCPU:    resource.MustParse("10m"),
   206  									corev1.ResourceMemory: resource.MustParse("50Mi"),
   207  								},
   208  							},
   209  							SecurityContext: &corev1.SecurityContext{
   210  								AllowPrivilegeEscalation: ptr.To(bool(false)),
   211  								Capabilities: &corev1.Capabilities{
   212  									Drop: []corev1.Capability{"ALL"},
   213  								},
   214  							},
   215  							TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
   216  						},
   217  					},
   218  					Volumes: []corev1.Volume{
   219  						{
   220  							Name: "bundle", // Used to share bundle content
   221  							VolumeSource: corev1.VolumeSource{
   222  								EmptyDir: &corev1.EmptyDirVolumeSource{},
   223  							},
   224  						},
   225  						{
   226  							Name: "util", // Used to share utils
   227  							VolumeSource: corev1.VolumeSource{
   228  								EmptyDir: &corev1.EmptyDirVolumeSource{},
   229  							},
   230  						},
   231  					},
   232  					NodeSelector: map[string]string{
   233  						"kubernetes.io/os": "linux",
   234  					},
   235  					Tolerations: []corev1.Toleration{
   236  						{
   237  							Key:      "kubernetes.io/arch",
   238  							Value:    "amd64",
   239  							Operator: "Equal",
   240  						},
   241  						{
   242  							Key:      "kubernetes.io/arch",
   243  							Value:    "arm64",
   244  							Operator: "Equal",
   245  						},
   246  						{
   247  							Key:      "kubernetes.io/arch",
   248  							Value:    "ppc64le",
   249  							Operator: "Equal",
   250  						},
   251  						{
   252  							Key:      "kubernetes.io/arch",
   253  							Value:    "s390x",
   254  							Operator: "Equal",
   255  						},
   256  					},
   257  				},
   258  			},
   259  		},
   260  	}
   261  	job.SetNamespace(cmRef.Namespace)
   262  	job.SetName(cmRef.Name)
   263  	job.SetOwnerReferences([]metav1.OwnerReference{ownerRef(cmRef)})
   264  	if c.runAsUser > 0 {
   265  		job.Spec.Template.Spec.SecurityContext.RunAsUser = &c.runAsUser
   266  		job.Spec.Template.Spec.SecurityContext.RunAsNonRoot = ptr.To(bool(true))
   267  	}
   268  	// By default the BackoffLimit is set to 6 which with exponential backoff 10s + 20s + 40s ...
   269  	// translates to ~10m of waiting time.
   270  	// We want to fail faster than that when we have repeated failures from the bundle unpack pod
   271  	// so we set it to 3 which is ~1m of waiting time
   272  	// See: https://kubernetes.io/docs/concepts/workloads/controllers/job/#pod-backoff-failure-policy
   273  	backOffLimit := int32(3)
   274  	job.Spec.BackoffLimit = &backOffLimit
   275  
   276  	// Set ActiveDeadlineSeconds as the unpack timeout
   277  	// Don't set a timeout if it is 0
   278  	if c.unpackTimeout != time.Duration(0) {
   279  		t := int64(c.unpackTimeout.Seconds())
   280  		job.Spec.ActiveDeadlineSeconds = &t
   281  	}
   282  
   283  	// Check annotationUnpackTimeout which is the annotation override for the default unpack timeout
   284  	// A negative timeout means the annotation was unset or malformed so we ignore it
   285  	if annotationUnpackTimeout < time.Duration(0) {
   286  		return job
   287  	}
   288  	// // 0 means no timeout so we unset ActiveDeadlineSeconds
   289  	if annotationUnpackTimeout == time.Duration(0) {
   290  		job.Spec.ActiveDeadlineSeconds = nil
   291  		return job
   292  	}
   293  
   294  	timeoutSeconds := int64(annotationUnpackTimeout.Seconds())
   295  	job.Spec.ActiveDeadlineSeconds = &timeoutSeconds
   296  
   297  	return job
   298  }
   299  
   300  //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 . Unpacker
   301  
   302  type Unpacker interface {
   303  	UnpackBundle(lookup *operatorsv1alpha1.BundleLookup, timeout, retryInterval time.Duration) (result *BundleUnpackResult, err error)
   304  }
   305  
   306  type ConfigMapUnpacker struct {
   307  	logger        *logrus.Logger
   308  	opmImage      string
   309  	utilImage     string
   310  	client        kubernetes.Interface
   311  	csLister      listersoperatorsv1alpha1.CatalogSourceLister
   312  	cmLister      listerscorev1.ConfigMapLister
   313  	jobLister     listersbatchv1.JobLister
   314  	podLister     listerscorev1.PodLister
   315  	roleLister    listersrbacv1.RoleLister
   316  	rbLister      listersrbacv1.RoleBindingLister
   317  	loader        *configmap.BundleLoader
   318  	now           func() metav1.Time
   319  	unpackTimeout time.Duration
   320  	runAsUser     int64
   321  }
   322  
   323  type ConfigMapUnpackerOption func(*ConfigMapUnpacker)
   324  
   325  func NewConfigmapUnpacker(options ...ConfigMapUnpackerOption) (*ConfigMapUnpacker, error) {
   326  	unpacker := &ConfigMapUnpacker{
   327  		loader: configmap.NewBundleLoader(),
   328  	}
   329  
   330  	unpacker.apply(options...)
   331  	if err := unpacker.validate(); err != nil {
   332  		return nil, err
   333  	}
   334  
   335  	return unpacker, nil
   336  }
   337  
   338  func WithUnpackTimeout(timeout time.Duration) ConfigMapUnpackerOption {
   339  	return func(unpacker *ConfigMapUnpacker) {
   340  		unpacker.unpackTimeout = timeout
   341  	}
   342  }
   343  
   344  func WithOPMImage(opmImage string) ConfigMapUnpackerOption {
   345  	return func(unpacker *ConfigMapUnpacker) {
   346  		unpacker.opmImage = opmImage
   347  	}
   348  }
   349  
   350  func WithUtilImage(utilImage string) ConfigMapUnpackerOption {
   351  	return func(unpacker *ConfigMapUnpacker) {
   352  		unpacker.utilImage = utilImage
   353  	}
   354  }
   355  
   356  func WithLogger(logger *logrus.Logger) ConfigMapUnpackerOption {
   357  	return func(unpacker *ConfigMapUnpacker) {
   358  		unpacker.logger = logger
   359  	}
   360  }
   361  
   362  func WithClient(client kubernetes.Interface) ConfigMapUnpackerOption {
   363  	return func(unpacker *ConfigMapUnpacker) {
   364  		unpacker.client = client
   365  	}
   366  }
   367  
   368  func WithCatalogSourceLister(csLister listersoperatorsv1alpha1.CatalogSourceLister) ConfigMapUnpackerOption {
   369  	return func(unpacker *ConfigMapUnpacker) {
   370  		unpacker.csLister = csLister
   371  	}
   372  }
   373  
   374  func WithConfigMapLister(cmLister listerscorev1.ConfigMapLister) ConfigMapUnpackerOption {
   375  	return func(unpacker *ConfigMapUnpacker) {
   376  		unpacker.cmLister = cmLister
   377  	}
   378  }
   379  
   380  func WithJobLister(jobLister listersbatchv1.JobLister) ConfigMapUnpackerOption {
   381  	return func(unpacker *ConfigMapUnpacker) {
   382  		unpacker.jobLister = jobLister
   383  	}
   384  }
   385  
   386  func WithPodLister(podLister listerscorev1.PodLister) ConfigMapUnpackerOption {
   387  	return func(unpacker *ConfigMapUnpacker) {
   388  		unpacker.podLister = podLister
   389  	}
   390  }
   391  
   392  func WithRoleLister(roleLister listersrbacv1.RoleLister) ConfigMapUnpackerOption {
   393  	return func(unpacker *ConfigMapUnpacker) {
   394  		unpacker.roleLister = roleLister
   395  	}
   396  }
   397  
   398  func WithRoleBindingLister(rbLister listersrbacv1.RoleBindingLister) ConfigMapUnpackerOption {
   399  	return func(unpacker *ConfigMapUnpacker) {
   400  		unpacker.rbLister = rbLister
   401  	}
   402  }
   403  
   404  func WithNow(now func() metav1.Time) ConfigMapUnpackerOption {
   405  	return func(unpacker *ConfigMapUnpacker) {
   406  		unpacker.now = now
   407  	}
   408  }
   409  
   410  func WithUserID(id int64) ConfigMapUnpackerOption {
   411  	return func(unpacker *ConfigMapUnpacker) {
   412  		unpacker.runAsUser = id
   413  	}
   414  }
   415  
   416  func (c *ConfigMapUnpacker) apply(options ...ConfigMapUnpackerOption) {
   417  	for _, option := range options {
   418  		option(c)
   419  	}
   420  }
   421  
   422  func (c *ConfigMapUnpacker) validate() (err error) {
   423  	switch {
   424  	case c.opmImage == "":
   425  		err = fmt.Errorf("no opm image given")
   426  	case c.utilImage == "":
   427  		err = fmt.Errorf("no util image given")
   428  	case c.client == nil:
   429  		err = fmt.Errorf("client is nil")
   430  	case c.csLister == nil:
   431  		err = fmt.Errorf("catalogsource lister is nil")
   432  	case c.cmLister == nil:
   433  		err = fmt.Errorf("configmap lister is nil")
   434  	case c.jobLister == nil:
   435  		err = fmt.Errorf("job lister is nil")
   436  	case c.podLister == nil:
   437  		err = fmt.Errorf("pod lister is nil")
   438  	case c.roleLister == nil:
   439  		err = fmt.Errorf("role lister is nil")
   440  	case c.rbLister == nil:
   441  		err = fmt.Errorf("rolebinding lister is nil")
   442  	case c.loader == nil:
   443  		err = fmt.Errorf("bundle loader is nil")
   444  	case c.now == nil:
   445  		err = fmt.Errorf("now func is nil")
   446  	}
   447  
   448  	return
   449  }
   450  
   451  const (
   452  	CatalogSourceMissingReason  = "CatalogSourceMissing"
   453  	CatalogSourceMissingMessage = "referenced catalogsource not found"
   454  	JobFailedReason             = "JobFailed"
   455  	JobFailedMessage            = "unpack job has failed"
   456  	JobIncompleteReason         = "JobIncomplete"
   457  	JobIncompleteMessage        = "unpack job not completed"
   458  	JobNotStartedReason         = "JobNotStarted"
   459  	JobNotStartedMessage        = "unpack job not yet started"
   460  	NotUnpackedReason           = "BundleNotUnpacked"
   461  	NotUnpackedMessage          = "bundle contents have not yet been persisted to installplan status"
   462  )
   463  
   464  func (c *ConfigMapUnpacker) UnpackBundle(lookup *operatorsv1alpha1.BundleLookup, timeout, retryInterval time.Duration) (result *BundleUnpackResult, err error) {
   465  	result = newBundleUnpackResult(lookup)
   466  
   467  	// if bundle lookup failed condition already present, then there is nothing more to do
   468  	failedCond := result.GetCondition(operatorsv1alpha1.BundleLookupFailed)
   469  	if failedCond.Status == corev1.ConditionTrue {
   470  		return result, nil
   471  	}
   472  
   473  	// if pending condition is not true then bundle has already been unpacked(unknown)
   474  	pendingCond := result.GetCondition(operatorsv1alpha1.BundleLookupPending)
   475  	if pendingCond.Status != corev1.ConditionTrue {
   476  		return result, nil
   477  	}
   478  
   479  	now := c.now()
   480  
   481  	var cs *operatorsv1alpha1.CatalogSource
   482  	if cs, err = c.csLister.CatalogSources(result.CatalogSourceRef.Namespace).Get(result.CatalogSourceRef.Name); err != nil {
   483  		if apierrors.IsNotFound(err) && pendingCond.Reason != CatalogSourceMissingReason {
   484  			pendingCond.Status = corev1.ConditionTrue
   485  			pendingCond.Reason = CatalogSourceMissingReason
   486  			pendingCond.Message = CatalogSourceMissingMessage
   487  			pendingCond.LastTransitionTime = &now
   488  			result.SetCondition(pendingCond)
   489  			err = nil
   490  		}
   491  
   492  		return
   493  	}
   494  
   495  	// Add missing info to the object reference
   496  	csRef := result.CatalogSourceRef.DeepCopy()
   497  	csRef.SetGroupVersionKind(catalogSourceGVK)
   498  	csRef.UID = cs.GetUID()
   499  
   500  	cm, err := c.ensureConfigmap(csRef, result.name)
   501  	if err != nil {
   502  		return
   503  	}
   504  
   505  	var cmRef *corev1.ObjectReference
   506  	cmRef, err = reference.GetReference(cm)
   507  	if err != nil {
   508  		return
   509  	}
   510  
   511  	_, err = c.ensureRole(cmRef)
   512  	if err != nil {
   513  		return
   514  	}
   515  
   516  	_, err = c.ensureRoleBinding(cmRef)
   517  	if err != nil {
   518  		return
   519  	}
   520  
   521  	secrets := make([]corev1.LocalObjectReference, 0)
   522  	for _, secretName := range cs.Spec.Secrets {
   523  		secrets = append(secrets, corev1.LocalObjectReference{Name: secretName})
   524  	}
   525  	var job *batchv1.Job
   526  	job, err = c.ensureJob(cmRef, result.Path, secrets, timeout, retryInterval)
   527  	if err != nil || job == nil {
   528  		// ensureJob can return nil if the job present does not match the expected job (spec and ownerefs)
   529  		// The current job is deleted in that case so UnpackBundle needs to be retried
   530  		return
   531  	}
   532  
   533  	// Check if bundle unpack job has failed due a timeout
   534  	// Return a BundleJobError so we can mark the InstallPlan as Failed
   535  	if jobCond, isFailed := getCondition(job, batchv1.JobFailed); isFailed {
   536  		// Add the BundleLookupFailed condition with the message and reason from the job failure
   537  		failedCond.Status = corev1.ConditionTrue
   538  		failedCond.Reason = jobCond.Reason
   539  		failedCond.Message = jobCond.Message
   540  		failedCond.LastTransitionTime = &now
   541  		result.SetCondition(failedCond)
   542  
   543  		return
   544  	}
   545  
   546  	if _, isComplete := getCondition(job, batchv1.JobComplete); !isComplete {
   547  		// In the case of an image pull failure for a non-existent image the bundle unpack job
   548  		// can stay pending until the ActiveDeadlineSeconds timeout ~10m
   549  		// To indicate why it's pending we inspect the container statuses of the
   550  		// unpack Job pods to surface that information on the bundle lookup conditions
   551  		pendingMessage := JobIncompleteMessage
   552  		var pendingContainerStatusMsgs string
   553  		pendingContainerStatusMsgs, err = c.pendingContainerStatusMessages(job)
   554  		if err != nil {
   555  			return
   556  		}
   557  
   558  		if pendingContainerStatusMsgs != "" {
   559  			pendingMessage = pendingMessage + ": " + pendingContainerStatusMsgs
   560  		}
   561  
   562  		// Update BundleLookupPending condition if there are any changes
   563  		if pendingCond.Status != corev1.ConditionTrue || pendingCond.Reason != JobIncompleteReason || pendingCond.Message != pendingMessage {
   564  			pendingCond.Status = corev1.ConditionTrue
   565  			pendingCond.Reason = JobIncompleteReason
   566  			pendingCond.Message = pendingMessage
   567  			pendingCond.LastTransitionTime = &now
   568  			result.SetCondition(pendingCond)
   569  		}
   570  
   571  		return
   572  	}
   573  
   574  	result.bundle, err = c.loader.Load(cm)
   575  	if err != nil {
   576  		return
   577  	}
   578  
   579  	if result.Bundle() == nil || len(result.Bundle().GetObject()) == 0 {
   580  		return
   581  	}
   582  
   583  	if result.BundleLookup.Properties != "" {
   584  		props, err := projection.PropertyListFromPropertiesAnnotation(lookup.Properties)
   585  		if err != nil {
   586  			return nil, fmt.Errorf("failed to load bundle properties for %q: %w", lookup.Identifier, err)
   587  		}
   588  		result.bundle.Properties = props
   589  	}
   590  
   591  	// A successful load should remove the pending condition
   592  	result.RemoveCondition(operatorsv1alpha1.BundleLookupPending)
   593  
   594  	return
   595  }
   596  
   597  func (c *ConfigMapUnpacker) pendingContainerStatusMessages(job *batchv1.Job) (string, error) {
   598  	containerStatusMessages := []string{}
   599  	// List pods for unpack job
   600  	podLabel := map[string]string{BundleUnpackPodLabel: job.GetName()}
   601  	pods, listErr := c.podLister.Pods(job.GetNamespace()).List(k8slabels.SelectorFromValidatedSet(podLabel))
   602  	if listErr != nil {
   603  		c.logger.Errorf("failed to list pods for job(%s): %v", job.GetName(), listErr)
   604  		return "", fmt.Errorf("failed to list pods for job(%s): %v", job.GetName(), listErr)
   605  	}
   606  
   607  	// Ideally there should be just 1 pod running but inspect all pods in the pending phase
   608  	// to see if any are stuck on an ImagePullBackOff or ErrImagePull error
   609  	for _, pod := range pods {
   610  		if pod.Status.Phase != corev1.PodPending {
   611  			// skip status check for non-pending pods
   612  			continue
   613  		}
   614  
   615  		for _, ic := range pod.Status.InitContainerStatuses {
   616  			if ic.Ready {
   617  				// only check non-ready containers for their waiting reasons
   618  				continue
   619  			}
   620  
   621  			msg := fmt.Sprintf("Unpack pod(%s/%s) container(%s) is pending", pod.Namespace, pod.Name, ic.Name)
   622  			waiting := ic.State.Waiting
   623  			if waiting != nil {
   624  				msg = fmt.Sprintf("Unpack pod(%s/%s) container(%s) is pending. Reason: %s, Message: %s",
   625  					pod.Namespace, pod.Name, ic.Name, waiting.Reason, waiting.Message)
   626  			}
   627  
   628  			// Aggregate the wait reasons for all pending containers
   629  			containerStatusMessages = append(containerStatusMessages, msg)
   630  		}
   631  	}
   632  
   633  	return strings.Join(containerStatusMessages, " | "), nil
   634  }
   635  
   636  func (c *ConfigMapUnpacker) ensureConfigmap(csRef *corev1.ObjectReference, name string) (cm *corev1.ConfigMap, err error) {
   637  	fresh := &corev1.ConfigMap{}
   638  	fresh.SetNamespace(csRef.Namespace)
   639  	fresh.SetName(name)
   640  	fresh.SetOwnerReferences([]metav1.OwnerReference{ownerRef(csRef)})
   641  	fresh.SetLabels(map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue})
   642  
   643  	cm, err = c.cmLister.ConfigMaps(fresh.GetNamespace()).Get(fresh.GetName())
   644  	if apierrors.IsNotFound(err) {
   645  		cm, err = c.client.CoreV1().ConfigMaps(fresh.GetNamespace()).Create(context.TODO(), fresh, metav1.CreateOptions{})
   646  		// CM already exists in cluster but not in cache, then add the label
   647  		if err != nil && apierrors.IsAlreadyExists(err) {
   648  			cm, err = c.client.CoreV1().ConfigMaps(fresh.GetNamespace()).Get(context.TODO(), fresh.GetName(), metav1.GetOptions{})
   649  			if err != nil {
   650  				return nil, fmt.Errorf("failed to retrieve configmap %s: %v", fresh.GetName(), err)
   651  			}
   652  			cm.SetLabels(map[string]string{
   653  				install.OLMManagedLabelKey: install.OLMManagedLabelValue,
   654  			})
   655  			cm, err = c.client.CoreV1().ConfigMaps(cm.GetNamespace()).Update(context.TODO(), cm, metav1.UpdateOptions{})
   656  			if err != nil {
   657  				return nil, fmt.Errorf("failed to update configmap %s: %v", cm.GetName(), err)
   658  			}
   659  		}
   660  	}
   661  
   662  	return
   663  }
   664  
   665  func (c *ConfigMapUnpacker) ensureJob(cmRef *corev1.ObjectReference, bundlePath string, secrets []corev1.LocalObjectReference, timeout time.Duration, unpackRetryInterval time.Duration) (job *batchv1.Job, err error) {
   666  	fresh := c.job(cmRef, bundlePath, secrets, timeout)
   667  	var jobs, toDelete []*batchv1.Job
   668  	jobs, err = c.jobLister.Jobs(fresh.GetNamespace()).List(k8slabels.ValidatedSetSelector{bundleUnpackRefLabel: cmRef.Name})
   669  	if err != nil {
   670  		return
   671  	}
   672  
   673  	// This is to ensure that we account for any existing unpack jobs that may be missing the label
   674  	jobWithoutLabel, err := c.jobLister.Jobs(fresh.GetNamespace()).Get(cmRef.Name)
   675  	if err != nil && !apierrors.IsNotFound(err) {
   676  		return
   677  	}
   678  	if jobWithoutLabel != nil {
   679  		_, labelExists := jobWithoutLabel.Labels[bundleUnpackRefLabel]
   680  		if !labelExists {
   681  			jobs = append(jobs, jobWithoutLabel)
   682  		}
   683  	}
   684  
   685  	if len(jobs) == 0 {
   686  		job, err = c.client.BatchV1().Jobs(fresh.GetNamespace()).Create(context.TODO(), fresh, metav1.CreateOptions{})
   687  		return
   688  	}
   689  
   690  	maxRetainedJobs := 5                                  // TODO: make this configurable
   691  	job, toDelete = sortUnpackJobs(jobs, maxRetainedJobs) // choose latest or on-failed job attempt
   692  
   693  	// only check for retries if an unpackRetryInterval is specified
   694  	if unpackRetryInterval > 0 {
   695  		if _, isFailed := getCondition(job, batchv1.JobFailed); isFailed {
   696  			// Look for other unpack jobs for the same bundle
   697  			if cond, failed := getCondition(job, batchv1.JobFailed); failed {
   698  				if time.Now().After(cond.LastTransitionTime.Time.Add(unpackRetryInterval)) {
   699  					fresh.SetName(names.SimpleNameGenerator.GenerateName(fresh.GetName()))
   700  					job, err = c.client.BatchV1().Jobs(fresh.GetNamespace()).Create(context.TODO(), fresh, metav1.CreateOptions{})
   701  				}
   702  			}
   703  
   704  			// cleanup old failed jobs, but don't clean up successful jobs to avoid repeat unpacking
   705  			for _, j := range toDelete {
   706  				_ = c.client.BatchV1().Jobs(j.GetNamespace()).Delete(context.TODO(), j.GetName(), metav1.DeleteOptions{})
   707  			}
   708  			return
   709  		}
   710  	}
   711  
   712  	if equality.Semantic.DeepDerivative(fresh.GetOwnerReferences(), job.GetOwnerReferences()) && equality.Semantic.DeepDerivative(fresh.Spec, job.Spec) {
   713  		return
   714  	}
   715  
   716  	// TODO: Decide when to fail-out instead of deleting the job
   717  	err = c.client.BatchV1().Jobs(job.GetNamespace()).Delete(context.TODO(), job.GetName(), metav1.DeleteOptions{})
   718  	job = nil
   719  	return
   720  }
   721  
   722  func (c *ConfigMapUnpacker) ensureRole(cmRef *corev1.ObjectReference) (role *rbacv1.Role, err error) {
   723  	if cmRef == nil {
   724  		return nil, fmt.Errorf("configmap reference is nil")
   725  	}
   726  
   727  	rule := rbacv1.PolicyRule{
   728  		APIGroups: []string{
   729  			"",
   730  		},
   731  		Verbs: []string{
   732  			"create", "get", "update",
   733  		},
   734  		Resources: []string{
   735  			"configmaps",
   736  		},
   737  		ResourceNames: []string{
   738  			cmRef.Name,
   739  		},
   740  	}
   741  	fresh := &rbacv1.Role{
   742  		Rules: []rbacv1.PolicyRule{rule},
   743  	}
   744  	fresh.SetNamespace(cmRef.Namespace)
   745  	fresh.SetName(cmRef.Name)
   746  	fresh.SetOwnerReferences([]metav1.OwnerReference{ownerRef(cmRef)})
   747  	fresh.SetLabels(map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue})
   748  
   749  	role, err = c.roleLister.Roles(fresh.GetNamespace()).Get(fresh.GetName())
   750  	if err != nil {
   751  		if apierrors.IsNotFound(err) {
   752  			role, err = c.client.RbacV1().Roles(fresh.GetNamespace()).Create(context.TODO(), fresh, metav1.CreateOptions{})
   753  			if apierrors.IsAlreadyExists(err) {
   754  				role, err = c.client.RbacV1().Roles(fresh.GetNamespace()).Update(context.TODO(), fresh, metav1.UpdateOptions{})
   755  			}
   756  		}
   757  
   758  		return
   759  	}
   760  
   761  	// Add the policy rule if necessary
   762  	for _, existing := range role.Rules {
   763  		if equality.Semantic.DeepDerivative(rule, existing) {
   764  			return
   765  		}
   766  	}
   767  	role = role.DeepCopy()
   768  	role.Rules = append(role.Rules, rule)
   769  
   770  	role, err = c.client.RbacV1().Roles(role.GetNamespace()).Update(context.TODO(), role, metav1.UpdateOptions{})
   771  
   772  	return
   773  }
   774  
   775  func (c *ConfigMapUnpacker) ensureRoleBinding(cmRef *corev1.ObjectReference) (roleBinding *rbacv1.RoleBinding, err error) {
   776  	fresh := &rbacv1.RoleBinding{
   777  		Subjects: []rbacv1.Subject{
   778  			{
   779  				Kind:      "ServiceAccount",
   780  				APIGroup:  "",
   781  				Name:      "default",
   782  				Namespace: cmRef.Namespace,
   783  			},
   784  		},
   785  		RoleRef: rbacv1.RoleRef{
   786  			APIGroup: "rbac.authorization.k8s.io",
   787  			Kind:     "Role",
   788  			Name:     cmRef.Name,
   789  		},
   790  	}
   791  	fresh.SetNamespace(cmRef.Namespace)
   792  	fresh.SetName(cmRef.Name)
   793  	fresh.SetOwnerReferences([]metav1.OwnerReference{ownerRef(cmRef)})
   794  	fresh.SetLabels(map[string]string{install.OLMManagedLabelKey: install.OLMManagedLabelValue})
   795  
   796  	roleBinding, err = c.rbLister.RoleBindings(fresh.GetNamespace()).Get(fresh.GetName())
   797  	if err != nil {
   798  		if apierrors.IsNotFound(err) {
   799  			roleBinding, err = c.client.RbacV1().RoleBindings(fresh.GetNamespace()).Create(context.TODO(), fresh, metav1.CreateOptions{})
   800  			if apierrors.IsAlreadyExists(err) {
   801  				roleBinding, err = c.client.RbacV1().RoleBindings(fresh.GetNamespace()).Update(context.TODO(), fresh, metav1.UpdateOptions{})
   802  			}
   803  		}
   804  
   805  		return
   806  	}
   807  
   808  	if equality.Semantic.DeepDerivative(fresh.Subjects, roleBinding.Subjects) && equality.Semantic.DeepDerivative(fresh.RoleRef, roleBinding.RoleRef) {
   809  		return
   810  	}
   811  
   812  	// TODO: Decide when to fail-out instead of deleting the rbac
   813  	err = c.client.RbacV1().RoleBindings(roleBinding.GetNamespace()).Delete(context.TODO(), roleBinding.GetName(), metav1.DeleteOptions{})
   814  	roleBinding = nil
   815  
   816  	return
   817  }
   818  
   819  // hash hashes data with sha256 and returns the hex string.
   820  func hash(data string) string {
   821  	// A SHA256 hash is 64 characters, which is within the 253 character limit for kube resource names
   822  	h := fmt.Sprintf("%x", sha256.Sum256([]byte(data)))
   823  
   824  	// Make the hash 63 characters instead to comply with the 63 character limit for labels
   825  	return h[:len(h)-1]
   826  }
   827  
   828  var blockOwnerDeletion = false
   829  
   830  // ownerRef converts an ObjectReference to an OwnerReference.
   831  func ownerRef(ref *corev1.ObjectReference) metav1.OwnerReference {
   832  	return metav1.OwnerReference{
   833  		APIVersion:         ref.APIVersion,
   834  		Kind:               ref.Kind,
   835  		Name:               ref.Name,
   836  		UID:                ref.UID,
   837  		Controller:         &blockOwnerDeletion,
   838  		BlockOwnerDeletion: &blockOwnerDeletion,
   839  	}
   840  }
   841  
   842  // getCondition returns true if the given job has the given condition with the given condition type true, and returns false otherwise.
   843  // Also returns the condition if true
   844  func getCondition(job *batchv1.Job, conditionType batchv1.JobConditionType) (condition *batchv1.JobCondition, isTrue bool) {
   845  	if job == nil {
   846  		return
   847  	}
   848  
   849  	for _, cond := range job.Status.Conditions {
   850  		if cond.Type == conditionType && cond.Status == corev1.ConditionTrue {
   851  			condition = &cond
   852  			isTrue = true
   853  			return
   854  		}
   855  	}
   856  	return
   857  }
   858  
   859  func sortUnpackJobs(jobs []*batchv1.Job, maxRetainedJobs int) (latest *batchv1.Job, toDelete []*batchv1.Job) {
   860  	if len(jobs) == 0 {
   861  		return
   862  	}
   863  	// sort jobs so that latest job is first
   864  	// with preference for non-failed jobs
   865  	sort.Slice(jobs, func(i, j int) bool {
   866  		if jobs[i] == nil || jobs[j] == nil {
   867  			return jobs[i] != nil
   868  		}
   869  		condI, failedI := getCondition(jobs[i], batchv1.JobFailed)
   870  		condJ, failedJ := getCondition(jobs[j], batchv1.JobFailed)
   871  		if failedI != failedJ {
   872  			return !failedI // non-failed job goes first
   873  		}
   874  		return condI.LastTransitionTime.After(condJ.LastTransitionTime.Time)
   875  	})
   876  	if jobs[0] == nil {
   877  		// all nil jobs
   878  		return
   879  	}
   880  	latest = jobs[0]
   881  	nilJobsIndex := len(jobs) - 1
   882  	for ; nilJobsIndex >= 0 && jobs[nilJobsIndex] == nil; nilJobsIndex-- {
   883  	}
   884  
   885  	jobs = jobs[:nilJobsIndex+1] // exclude nil jobs from list of jobs to delete
   886  	if len(jobs) <= maxRetainedJobs {
   887  		return
   888  	}
   889  	if maxRetainedJobs == 0 {
   890  		toDelete = jobs[1:]
   891  		return
   892  	}
   893  
   894  	// cleanup old failed jobs, n-1 recent jobs and the oldest job
   895  	for i := 0; i < maxRetainedJobs && i+maxRetainedJobs < len(jobs)-1; i++ {
   896  		toDelete = append(toDelete, jobs[maxRetainedJobs+i])
   897  	}
   898  
   899  	return
   900  }
   901  
   902  // OperatorGroupBundleUnpackTimeout returns bundle timeout from annotation if specified.
   903  // If the timeout annotation is not set, return timeout < 0 which is subsequently ignored.
   904  // This is to overrides the --bundle-unpack-timeout flag value on per-OperatorGroup basis.
   905  func OperatorGroupBundleUnpackTimeout(ogLister v1listers.OperatorGroupNamespaceLister) (time.Duration, error) {
   906  	ignoreTimeout := -1 * time.Minute
   907  
   908  	ogs, err := ogLister.List(k8slabels.Everything())
   909  	if err != nil {
   910  		return ignoreTimeout, err
   911  	}
   912  	if len(ogs) != 1 {
   913  		return ignoreTimeout, fmt.Errorf("found %d operatorGroups, expected 1", len(ogs))
   914  	}
   915  
   916  	timeoutStr, ok := ogs[0].GetAnnotations()[BundleUnpackTimeoutAnnotationKey]
   917  	if !ok {
   918  		return ignoreTimeout, nil
   919  	}
   920  
   921  	d, err := time.ParseDuration(timeoutStr)
   922  	if err != nil {
   923  		return ignoreTimeout, fmt.Errorf("failed to parse unpack timeout annotation(%s: %s): %w", BundleUnpackTimeoutAnnotationKey, timeoutStr, err)
   924  	}
   925  
   926  	return d, nil
   927  }
   928  
   929  // OperatorGroupBundleUnpackRetryInterval returns bundle unpack retry interval from annotation if specified.
   930  // If the retry annotation is not set, return retry = 0 which is subsequently ignored. This interval, if > 0,
   931  // determines the minimum interval between recreating a failed unpack job.
   932  func OperatorGroupBundleUnpackRetryInterval(ogLister v1listers.OperatorGroupNamespaceLister) (time.Duration, error) {
   933  	ogs, err := ogLister.List(k8slabels.Everything())
   934  	if err != nil {
   935  		return 0, err
   936  	}
   937  	if len(ogs) != 1 {
   938  		return 0, fmt.Errorf("found %d operatorGroups, expected 1", len(ogs))
   939  	}
   940  
   941  	timeoutStr, ok := ogs[0].GetAnnotations()[BundleUnpackRetryMinimumIntervalAnnotationKey]
   942  	if !ok {
   943  		return 0, nil
   944  	}
   945  
   946  	d, err := time.ParseDuration(timeoutStr)
   947  	if err != nil {
   948  		return 0, fmt.Errorf("failed to parse unpack retry annotation(%s: %s): %w", BundleUnpackRetryMinimumIntervalAnnotationKey, timeoutStr, err)
   949  	}
   950  
   951  	return d, nil
   952  }