github.com/operator-framework/operator-lifecycle-manager@v0.30.0/pkg/controller/registry/reconciler/configmap.go (about) 1 //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -o ../../../fakes/fake_reconciler.go . RegistryReconciler 2 package reconciler 3 4 import ( 5 "context" 6 "errors" 7 "fmt" 8 9 "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install" 10 hashutil "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/kubernetes/pkg/util/hash" 11 pkgerrors "github.com/pkg/errors" 12 "github.com/sirupsen/logrus" 13 corev1 "k8s.io/api/core/v1" 14 rbacv1 "k8s.io/api/rbac/v1" 15 apierrors "k8s.io/apimachinery/pkg/api/errors" 16 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 "k8s.io/apimachinery/pkg/labels" 18 "k8s.io/apimachinery/pkg/util/intstr" 19 "k8s.io/utils/ptr" 20 21 "github.com/operator-framework/api/pkg/operators/v1alpha1" 22 "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" 23 "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorlister" 24 "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/ownerutil" 25 ) 26 27 // configMapCatalogSourceDecorator wraps CatalogSource to add additional methods 28 type configMapCatalogSourceDecorator struct { 29 *v1alpha1.CatalogSource 30 runAsUser int64 31 } 32 33 const ( 34 // ConfigMapServerPostfix is a postfix appended to the names of resources generated for a ConfigMap server. 35 ConfigMapServerPostfix string = "-configmap-server" 36 ) 37 38 func (s *configMapCatalogSourceDecorator) serviceAccountName() string { 39 return s.GetName() + ConfigMapServerPostfix 40 } 41 42 func (s *configMapCatalogSourceDecorator) roleName() string { 43 return s.GetName() + "-configmap-reader" 44 } 45 46 func (s *configMapCatalogSourceDecorator) Selector() map[string]string { 47 return map[string]string{ 48 CatalogSourceLabelKey: s.GetName(), 49 } 50 } 51 52 const ( 53 // ConfigMapRVLabelKey is the key for a label used to track the resource version of a related ConfigMap. 54 ConfigMapRVLabelKey string = "olm.configMapResourceVersion" 55 ) 56 57 func (s *configMapCatalogSourceDecorator) Labels() map[string]string { 58 labels := map[string]string{ 59 CatalogSourceLabelKey: s.GetName(), 60 install.OLMManagedLabelKey: install.OLMManagedLabelValue, 61 } 62 if s.Spec.SourceType == v1alpha1.SourceTypeInternal || s.Spec.SourceType == v1alpha1.SourceTypeConfigmap { 63 labels[ConfigMapRVLabelKey] = s.Status.ConfigMapResource.ResourceVersion 64 } 65 return labels 66 } 67 68 func (s *configMapCatalogSourceDecorator) Annotations() map[string]string { 69 // TODO: Maybe something better than just a copy of all annotations would be to have a specific 'podMetadata' section in the CatalogSource? 70 return s.GetAnnotations() 71 } 72 73 func (s *configMapCatalogSourceDecorator) ConfigMapChanges(configMap *corev1.ConfigMap) bool { 74 if s.Status.ConfigMapResource == nil { 75 return true 76 } 77 if s.Status.ConfigMapResource.ResourceVersion == configMap.GetResourceVersion() { 78 return false 79 } 80 return true 81 } 82 83 func (s *configMapCatalogSourceDecorator) Service() (*corev1.Service, error) { 84 svc := &corev1.Service{ 85 ObjectMeta: metav1.ObjectMeta{ 86 Name: s.GetName(), 87 Namespace: s.GetNamespace(), 88 }, 89 Spec: corev1.ServiceSpec{ 90 Ports: []corev1.ServicePort{ 91 { 92 Name: "grpc", 93 Port: 50051, 94 TargetPort: intstr.FromInt(50051), 95 }, 96 }, 97 Selector: s.Selector(), 98 }, 99 } 100 101 labels := map[string]string{ 102 install.OLMManagedLabelKey: install.OLMManagedLabelValue, 103 } 104 hash, err := hashutil.DeepHashObject(&svc.Spec) 105 if err != nil { 106 return nil, err 107 } 108 labels[ServiceHashLabelKey] = hash 109 svc.SetLabels(labels) 110 ownerutil.AddOwner(svc, s.CatalogSource, false, false) 111 return svc, nil 112 } 113 114 func (s *configMapCatalogSourceDecorator) Pod(image string, defaultPodSecurityConfig v1alpha1.SecurityConfig) (*corev1.Pod, error) { 115 pod, err := Pod(s.CatalogSource, "configmap-registry-server", "", "", image, nil, s.Labels(), s.Annotations(), 5, 5, s.runAsUser, defaultPodSecurityConfig) 116 if err != nil { 117 return nil, err 118 } 119 pod.Spec.ServiceAccountName = s.GetName() + ConfigMapServerPostfix 120 pod.Spec.Containers[0].Command = []string{"configmap-server", "-c", s.Spec.ConfigMap, "-n", s.GetNamespace()} 121 ownerutil.AddOwner(pod, s.CatalogSource, false, true) 122 return pod, nil 123 } 124 125 func (s *configMapCatalogSourceDecorator) ServiceAccount() *corev1.ServiceAccount { 126 sa := &corev1.ServiceAccount{ 127 ObjectMeta: metav1.ObjectMeta{ 128 Name: s.serviceAccountName(), 129 Namespace: s.GetNamespace(), 130 Labels: map[string]string{ 131 install.OLMManagedLabelKey: install.OLMManagedLabelValue, 132 }, 133 }, 134 } 135 ownerutil.AddOwner(sa, s.CatalogSource, false, false) 136 return sa 137 } 138 139 func (s *configMapCatalogSourceDecorator) Role() *rbacv1.Role { 140 role := &rbacv1.Role{ 141 ObjectMeta: metav1.ObjectMeta{ 142 Name: s.roleName(), 143 Namespace: s.GetNamespace(), 144 Labels: map[string]string{ 145 install.OLMManagedLabelKey: install.OLMManagedLabelValue, 146 }, 147 }, 148 Rules: []rbacv1.PolicyRule{ 149 { 150 Verbs: []string{"get"}, 151 APIGroups: []string{""}, 152 Resources: []string{"configmaps"}, 153 ResourceNames: []string{s.Spec.ConfigMap}, 154 }, 155 }, 156 } 157 ownerutil.AddOwner(role, s.CatalogSource, false, false) 158 return role 159 } 160 161 func (s *configMapCatalogSourceDecorator) RoleBinding() *rbacv1.RoleBinding { 162 rb := &rbacv1.RoleBinding{ 163 ObjectMeta: metav1.ObjectMeta{ 164 Name: s.GetName() + "-server-configmap-reader", 165 Namespace: s.GetNamespace(), 166 Labels: map[string]string{ 167 install.OLMManagedLabelKey: install.OLMManagedLabelValue, 168 }, 169 }, 170 Subjects: []rbacv1.Subject{ 171 { 172 Kind: "ServiceAccount", 173 Name: s.serviceAccountName(), 174 Namespace: s.GetNamespace(), 175 }, 176 }, 177 RoleRef: rbacv1.RoleRef{ 178 APIGroup: "rbac.authorization.k8s.io", 179 Kind: "Role", 180 Name: s.roleName(), 181 }, 182 } 183 ownerutil.AddOwner(rb, s.CatalogSource, false, false) 184 return rb 185 } 186 187 type ConfigMapRegistryReconciler struct { 188 now nowFunc 189 Lister operatorlister.OperatorLister 190 OpClient operatorclient.ClientInterface 191 Image string 192 createPodAsUser int64 193 } 194 195 var _ RegistryEnsurer = &ConfigMapRegistryReconciler{} 196 var _ RegistryChecker = &ConfigMapRegistryReconciler{} 197 var _ RegistryReconciler = &ConfigMapRegistryReconciler{} 198 199 func (c *ConfigMapRegistryReconciler) currentService(source configMapCatalogSourceDecorator) (*corev1.Service, error) { 200 protoService, err := source.Service() 201 if err != nil { 202 return nil, err 203 } 204 serviceName := protoService.GetName() 205 service, err := c.Lister.CoreV1().ServiceLister().Services(source.GetNamespace()).Get(serviceName) 206 if err != nil { 207 logrus.WithField("service", serviceName).Debug("couldn't find service in cache") 208 return nil, nil 209 } 210 return service, nil 211 } 212 213 func (c *ConfigMapRegistryReconciler) currentServiceAccount(source configMapCatalogSourceDecorator) *corev1.ServiceAccount { 214 serviceAccountName := source.ServiceAccount().GetName() 215 serviceAccount, err := c.Lister.CoreV1().ServiceAccountLister().ServiceAccounts(source.GetNamespace()).Get(serviceAccountName) 216 if err != nil { 217 logrus.WithField("serviceAccouint", serviceAccountName).WithError(err).Debug("couldn't find service account in cache") 218 return nil 219 } 220 return serviceAccount 221 } 222 223 func (c *ConfigMapRegistryReconciler) currentRole(source configMapCatalogSourceDecorator) *rbacv1.Role { 224 roleName := source.Role().GetName() 225 role, err := c.Lister.RbacV1().RoleLister().Roles(source.GetNamespace()).Get(roleName) 226 if err != nil { 227 logrus.WithField("role", roleName).WithError(err).Debug("couldn't find role in cache") 228 return nil 229 } 230 return role 231 } 232 233 func (c *ConfigMapRegistryReconciler) currentRoleBinding(source configMapCatalogSourceDecorator) *rbacv1.RoleBinding { 234 roleBindingName := source.RoleBinding().GetName() 235 roleBinding, err := c.Lister.RbacV1().RoleBindingLister().RoleBindings(source.GetNamespace()).Get(roleBindingName) 236 if err != nil { 237 logrus.WithField("roleBinding", roleBindingName).WithError(err).Debug("couldn't find role binding in cache") 238 return nil 239 } 240 return roleBinding 241 } 242 243 func (c *ConfigMapRegistryReconciler) currentPods(source configMapCatalogSourceDecorator, image string, defaultPodSecurityConfig v1alpha1.SecurityConfig) ([]*corev1.Pod, error) { 244 protoPod, err := source.Pod(image, defaultPodSecurityConfig) 245 if err != nil { 246 return nil, err 247 } 248 podName := protoPod.GetName() 249 pods, err := c.Lister.CoreV1().PodLister().Pods(source.GetNamespace()).List(labels.SelectorFromSet(source.Selector())) 250 if err != nil { 251 logrus.WithField("pod", podName).WithError(err).Debug("couldn't find pod in cache") 252 return nil, nil 253 } 254 if len(pods) > 1 { 255 logrus.WithField("selector", source.Selector()).Debug("multiple pods found for selector") 256 } 257 return pods, nil 258 } 259 260 func (c *ConfigMapRegistryReconciler) currentPodsWithCorrectResourceVersion(source configMapCatalogSourceDecorator, image string, defaultPodSecurityConfig v1alpha1.SecurityConfig) ([]*corev1.Pod, error) { 261 protoPod, err := source.Pod(image, defaultPodSecurityConfig) 262 if err != nil { 263 return nil, err 264 } 265 podName := protoPod.GetName() 266 pods, err := c.Lister.CoreV1().PodLister().Pods(source.GetNamespace()).List(labels.SelectorFromValidatedSet(source.Labels())) 267 if err != nil { 268 logrus.WithField("pod", podName).WithError(err).Debug("couldn't find pod in cache") 269 return nil, nil 270 } 271 if len(pods) > 1 { 272 logrus.WithField("selector", source.Labels()).Debug("multiple pods found for selector") 273 } 274 return pods, nil 275 } 276 277 // EnsureRegistryServer ensures that all components of registry server are up to date. 278 func (c *ConfigMapRegistryReconciler) EnsureRegistryServer(logger *logrus.Entry, catalogSource *v1alpha1.CatalogSource) error { 279 source := configMapCatalogSourceDecorator{catalogSource, c.createPodAsUser} 280 281 image := c.Image 282 if source.Spec.SourceType == "grpc" { 283 image = source.Spec.Image 284 } 285 if image == "" { 286 return fmt.Errorf("no image for registry") 287 } 288 289 // if service status is nil, we force create every object to ensure they're created the first time 290 overwrite := source.Status.RegistryServiceStatus == nil 291 overwritePod := overwrite 292 293 defaultPodSecurityConfig, err := getDefaultPodContextConfig(c.OpClient, catalogSource.GetNamespace()) 294 if err != nil { 295 return err 296 } 297 298 if source.Spec.SourceType == v1alpha1.SourceTypeConfigmap || source.Spec.SourceType == v1alpha1.SourceTypeInternal { 299 // fetch configmap first, exit early if we can't find it 300 // we use the live client here instead of a lister since our listers are scoped to objects with the olm.managed label, 301 // and this configmap is a user-provided input to the catalog source and will not have that label 302 configMap, err := c.OpClient.KubernetesInterface().CoreV1().ConfigMaps(source.GetNamespace()).Get(context.TODO(), source.Spec.ConfigMap, metav1.GetOptions{}) 303 if err != nil { 304 return fmt.Errorf("unable to find configmap %s/%s: %w", source.GetNamespace(), source.Spec.ConfigMap, err) 305 } 306 307 if source.ConfigMapChanges(configMap) { 308 catalogSource.Status.ConfigMapResource = &v1alpha1.ConfigMapResourceReference{ 309 Name: configMap.GetName(), 310 Namespace: configMap.GetNamespace(), 311 UID: configMap.GetUID(), 312 ResourceVersion: configMap.GetResourceVersion(), 313 LastUpdateTime: c.now(), 314 } 315 316 // recreate the pod if there are configmap changes; this causes the db to be rebuilt 317 overwritePod = true 318 } 319 320 // recreate the pod if no existing pod is serving the latest image 321 current, err := c.currentPodsWithCorrectResourceVersion(source, image, defaultPodSecurityConfig) 322 if err != nil { 323 return err 324 } 325 if len(current) == 0 { 326 overwritePod = true 327 } 328 } 329 330 //TODO: if any of these error out, we should write a status back (possibly set RegistryServiceStatus to nil so they get recreated) 331 if err := c.ensureServiceAccount(source, overwrite); err != nil { 332 return pkgerrors.Wrapf(err, "error ensuring service account: %s", source.serviceAccountName()) 333 } 334 if err := c.ensureRole(source, overwrite); err != nil { 335 return pkgerrors.Wrapf(err, "error ensuring role: %s", source.roleName()) 336 } 337 if err := c.ensureRoleBinding(source, overwrite); err != nil { 338 return pkgerrors.Wrapf(err, "error ensuring rolebinding: %s", source.RoleBinding().GetName()) 339 } 340 pod, err := source.Pod(image, defaultPodSecurityConfig) 341 if err != nil { 342 return err 343 } 344 if err := c.ensurePod(source, defaultPodSecurityConfig, overwritePod); err != nil { 345 return pkgerrors.Wrapf(err, "error ensuring pod: %s", pod.GetName()) 346 } 347 service, err := source.Service() 348 if err != nil { 349 return err 350 } 351 if err := c.ensureService(source, overwrite); err != nil { 352 return pkgerrors.Wrapf(err, "error ensuring service: %s", service.GetName()) 353 } 354 355 if overwritePod { 356 now := c.now() 357 catalogSource.Status.RegistryServiceStatus = &v1alpha1.RegistryServiceStatus{ 358 CreatedAt: now, 359 Protocol: "grpc", 360 ServiceName: service.GetName(), 361 ServiceNamespace: source.GetNamespace(), 362 Port: fmt.Sprintf("%d", service.Spec.Ports[0].Port), 363 } 364 } 365 return nil 366 } 367 368 func (c *ConfigMapRegistryReconciler) ensureServiceAccount(source configMapCatalogSourceDecorator, overwrite bool) error { 369 serviceAccount := source.ServiceAccount() 370 if c.currentServiceAccount(source) != nil { 371 if !overwrite { 372 return nil 373 } 374 if err := c.OpClient.DeleteServiceAccount(serviceAccount.GetNamespace(), serviceAccount.GetName(), metav1.NewDeleteOptions(0)); err != nil && !apierrors.IsNotFound(err) { 375 return err 376 } 377 } 378 _, err := c.OpClient.CreateServiceAccount(serviceAccount) 379 return err 380 } 381 382 func (c *ConfigMapRegistryReconciler) ensureRole(source configMapCatalogSourceDecorator, overwrite bool) error { 383 role := source.Role() 384 if c.currentRole(source) != nil { 385 if !overwrite { 386 return nil 387 } 388 if err := c.OpClient.DeleteRole(role.GetNamespace(), role.GetName(), metav1.NewDeleteOptions(0)); err != nil && !apierrors.IsNotFound(err) { 389 return err 390 } 391 } 392 _, err := c.OpClient.CreateRole(role) 393 return err 394 } 395 396 func (c *ConfigMapRegistryReconciler) ensureRoleBinding(source configMapCatalogSourceDecorator, overwrite bool) error { 397 roleBinding := source.RoleBinding() 398 if c.currentRoleBinding(source) != nil { 399 if !overwrite { 400 return nil 401 } 402 if err := c.OpClient.DeleteRoleBinding(roleBinding.GetNamespace(), roleBinding.GetName(), metav1.NewDeleteOptions(0)); err != nil && !apierrors.IsNotFound(err) { 403 return err 404 } 405 } 406 _, err := c.OpClient.CreateRoleBinding(roleBinding) 407 return err 408 } 409 410 func (c *ConfigMapRegistryReconciler) ensurePod(source configMapCatalogSourceDecorator, defaultPodSecurityConfig v1alpha1.SecurityConfig, overwrite bool) error { 411 pod, err := source.Pod(c.Image, defaultPodSecurityConfig) 412 if err != nil { 413 return err 414 } 415 currentPods, err := c.currentPods(source, c.Image, defaultPodSecurityConfig) 416 if err != nil { 417 return err 418 } 419 if len(currentPods) > 0 { 420 if !overwrite { 421 return nil 422 } 423 for _, p := range currentPods { 424 if err := c.OpClient.KubernetesInterface().CoreV1().Pods(pod.GetNamespace()).Delete(context.TODO(), p.GetName(), *metav1.NewDeleteOptions(1)); err != nil && !apierrors.IsNotFound(err) { 425 return pkgerrors.Wrapf(err, "error deleting old pod: %s", p.GetName()) 426 } 427 } 428 } 429 _, err = c.OpClient.KubernetesInterface().CoreV1().Pods(pod.GetNamespace()).Create(context.TODO(), pod, metav1.CreateOptions{}) 430 if err == nil { 431 return nil 432 } 433 return pkgerrors.Wrapf(err, "error creating new pod: %s", pod.GetGenerateName()) 434 } 435 436 func (c *ConfigMapRegistryReconciler) ensureService(source configMapCatalogSourceDecorator, overwrite bool) error { 437 service, err := source.Service() 438 if err != nil { 439 return err 440 } 441 svc, err := c.currentService(source) 442 if err != nil { 443 return err 444 } 445 if svc != nil { 446 if !overwrite && ServiceHashMatch(svc, service) { 447 return nil 448 } 449 if err := c.OpClient.DeleteService(service.GetNamespace(), service.GetName(), metav1.NewDeleteOptions(0)); err != nil && !apierrors.IsNotFound(err) { 450 return err 451 } 452 } 453 _, err = c.OpClient.CreateService(service) 454 return err 455 } 456 457 // CheckRegistryServer returns true if the given CatalogSource is considered healthy; false otherwise. 458 func (c *ConfigMapRegistryReconciler) CheckRegistryServer(logger *logrus.Entry, catalogSource *v1alpha1.CatalogSource) (healthy bool, err error) { 459 source := configMapCatalogSourceDecorator{catalogSource, c.createPodAsUser} 460 461 image := c.Image 462 if source.Spec.SourceType == "grpc" { 463 image = source.Spec.Image 464 } 465 if image == "" { 466 err = fmt.Errorf("no image for registry") 467 return 468 } 469 470 defaultPodSecurityConfig, err := getDefaultPodContextConfig(c.OpClient, catalogSource.GetNamespace()) 471 if err != nil { 472 return false, err 473 } 474 475 if source.Spec.SourceType == v1alpha1.SourceTypeConfigmap || source.Spec.SourceType == v1alpha1.SourceTypeInternal { 476 // we use the live client here instead of a lister since our listers are scoped to objects with the olm.managed label, 477 // and this configmap is a user-provided input to the catalog source and will not have that label 478 configMap, err := c.OpClient.KubernetesInterface().CoreV1().ConfigMaps(source.GetNamespace()).Get(context.TODO(), source.Spec.ConfigMap, metav1.GetOptions{}) 479 if err != nil { 480 return false, fmt.Errorf("unable to find configmap %s/%s: %w", source.GetNamespace(), source.Spec.ConfigMap, err) 481 } 482 483 if source.ConfigMapChanges(configMap) { 484 return false, nil 485 } 486 487 // recreate the pod if no existing pod is serving the latest image 488 current, err := c.currentPodsWithCorrectResourceVersion(source, image, defaultPodSecurityConfig) 489 if err != nil { 490 return false, err 491 } 492 if len(current) == 0 { 493 return false, nil 494 } 495 } 496 497 // Check on registry resources 498 // TODO: more complex checks for resources 499 // TODO: add gRPC health check 500 service, err := c.currentService(source) 501 if err != nil { 502 return false, err 503 } 504 pods, err := c.currentPods(source, c.Image, defaultPodSecurityConfig) 505 if err != nil { 506 return false, err 507 } 508 if c.currentServiceAccount(source) == nil || 509 c.currentRole(source) == nil || 510 c.currentRoleBinding(source) == nil || 511 service == nil || 512 len(pods) < 1 { 513 healthy = false 514 return 515 } 516 517 podsAreLive, e := detectAndDeleteDeadPods(logger, c.OpClient, pods, source.GetNamespace()) 518 if e != nil { 519 return false, fmt.Errorf("error deleting dead pods: %v", e) 520 } 521 return podsAreLive, nil 522 } 523 524 // detectAndDeleteDeadPods determines if there are registry client pods that are in the deleted state 525 // but have not been removed by GC (eg the node goes down before GC can remove them), and attempts to 526 // force delete the pods. If there are live registry pods remaining, it returns true, otherwise returns false. 527 func detectAndDeleteDeadPods(logger *logrus.Entry, client operatorclient.ClientInterface, pods []*corev1.Pod, sourceNamespace string) (bool, error) { 528 var forceDeletionErrs []error 529 livePodFound := false 530 for _, pod := range pods { 531 if !isPodDead(pod) { 532 livePodFound = true 533 logger.WithFields(logrus.Fields{"pod.namespace": sourceNamespace, "pod.name": pod.GetName()}).Debug("pod is alive") 534 continue 535 } 536 logger.WithFields(logrus.Fields{"pod.namespace": sourceNamespace, "pod.name": pod.GetName()}).Info("force deleting dead pod") 537 if err := client.KubernetesInterface().CoreV1().Pods(sourceNamespace).Delete(context.TODO(), pod.GetName(), metav1.DeleteOptions{ 538 GracePeriodSeconds: ptr.To[int64](0), 539 }); err != nil && !apierrors.IsNotFound(err) { 540 forceDeletionErrs = append(forceDeletionErrs, err) 541 } 542 } 543 if len(forceDeletionErrs) > 0 { 544 return false, errors.Join(forceDeletionErrs...) 545 } 546 return livePodFound, nil 547 }