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 }