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

     1  //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -o ../../../fakes/fake_reconciler_factory.go . RegistryReconcilerFactory
     2  package reconciler
     3  
     4  import (
     5  	"context"
     6  	"fmt"
     7  	"path/filepath"
     8  
     9  	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install"
    10  	"github.com/sirupsen/logrus"
    11  	corev1 "k8s.io/api/core/v1"
    12  	"k8s.io/apimachinery/pkg/api/resource"
    13  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  	"k8s.io/utils/ptr"
    15  
    16  	operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
    17  	controllerclient "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/controller-runtime/client"
    18  	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/image"
    19  	hashutil "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/kubernetes/pkg/util/hash"
    20  	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient"
    21  	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorlister"
    22  )
    23  
    24  type nowFunc func() metav1.Time
    25  
    26  const (
    27  	// CatalogSourceLabelKey is the key for a label containing a CatalogSource name.
    28  	CatalogSourceLabelKey string = "olm.catalogSource"
    29  	// CatalogPriorityClassKey is the key of an annotation in default catalogsources
    30  	CatalogPriorityClassKey string = "operatorframework.io/priorityclass"
    31  	// PodHashLabelKey is the key of a label for podspec hash information
    32  	PodHashLabelKey = "olm.pod-spec-hash"
    33  	//ClusterAutoscalingAnnotationKey is the annotation that enables the cluster autoscaler to evict catalog pods
    34  	ClusterAutoscalingAnnotationKey string = "cluster-autoscaler.kubernetes.io/safe-to-evict"
    35  )
    36  
    37  // RegistryEnsurer describes methods for ensuring a registry exists.
    38  type RegistryEnsurer interface {
    39  	// EnsureRegistryServer ensures a registry server exists for the given CatalogSource.
    40  	EnsureRegistryServer(logger *logrus.Entry, catalogSource *operatorsv1alpha1.CatalogSource) error
    41  }
    42  
    43  // RegistryChecker describes methods for checking a registry.
    44  type RegistryChecker interface {
    45  	// CheckRegistryServer returns true if the given CatalogSource is considered healthy; false otherwise.
    46  	CheckRegistryServer(logger *logrus.Entry, catalogSource *operatorsv1alpha1.CatalogSource) (healthy bool, err error)
    47  }
    48  
    49  // RegistryReconciler knows how to reconcile a registry.
    50  type RegistryReconciler interface {
    51  	RegistryChecker
    52  	RegistryEnsurer
    53  }
    54  
    55  // RegistryReconcilerFactory describes factory methods for RegistryReconcilers.
    56  type RegistryReconcilerFactory interface {
    57  	ReconcilerForSource(source *operatorsv1alpha1.CatalogSource) RegistryReconciler
    58  }
    59  
    60  // RegistryReconcilerFactory is a factory for RegistryReconcilers.
    61  type registryReconcilerFactory struct {
    62  	now                  nowFunc
    63  	Lister               operatorlister.OperatorLister
    64  	OpClient             operatorclient.ClientInterface
    65  	ConfigMapServerImage string
    66  	SSAClient            *controllerclient.ServerSideApplier
    67  	createPodAsUser      int64
    68  	opmImage             string
    69  	utilImage            string
    70  }
    71  
    72  // ReconcilerForSource returns a RegistryReconciler based on the configuration of the given CatalogSource.
    73  func (r *registryReconcilerFactory) ReconcilerForSource(source *operatorsv1alpha1.CatalogSource) RegistryReconciler {
    74  	// TODO: add memoization by source type
    75  	switch source.Spec.SourceType {
    76  	case operatorsv1alpha1.SourceTypeInternal, operatorsv1alpha1.SourceTypeConfigmap:
    77  		return &ConfigMapRegistryReconciler{
    78  			now:             r.now,
    79  			Lister:          r.Lister,
    80  			OpClient:        r.OpClient,
    81  			Image:           r.ConfigMapServerImage,
    82  			createPodAsUser: r.createPodAsUser,
    83  		}
    84  	case operatorsv1alpha1.SourceTypeGrpc:
    85  		if source.Spec.Image != "" {
    86  			return &GrpcRegistryReconciler{
    87  				now:             r.now,
    88  				Lister:          r.Lister,
    89  				OpClient:        r.OpClient,
    90  				SSAClient:       r.SSAClient,
    91  				createPodAsUser: r.createPodAsUser,
    92  				opmImage:        r.opmImage,
    93  				utilImage:       r.utilImage,
    94  			}
    95  		} else if source.Spec.Address != "" {
    96  			return &GrpcAddressRegistryReconciler{
    97  				now: r.now,
    98  			}
    99  		}
   100  	}
   101  	return nil
   102  }
   103  
   104  // NewRegistryReconcilerFactory returns an initialized RegistryReconcilerFactory.
   105  func NewRegistryReconcilerFactory(lister operatorlister.OperatorLister, opClient operatorclient.ClientInterface, configMapServerImage string, now nowFunc, ssaClient *controllerclient.ServerSideApplier, createPodAsUser int64, opmImage, utilImage string) RegistryReconcilerFactory {
   106  	return &registryReconcilerFactory{
   107  		now:                  now,
   108  		Lister:               lister,
   109  		OpClient:             opClient,
   110  		ConfigMapServerImage: configMapServerImage,
   111  		SSAClient:            ssaClient,
   112  		createPodAsUser:      createPodAsUser,
   113  		opmImage:             opmImage,
   114  		utilImage:            utilImage,
   115  	}
   116  }
   117  
   118  func Pod(source *operatorsv1alpha1.CatalogSource, name, opmImg, utilImage, img string, serviceAccount *corev1.ServiceAccount, labels, annotations map[string]string, readinessDelay, livenessDelay int32, runAsUser int64, defaultSecurityConfig operatorsv1alpha1.SecurityConfig) (*corev1.Pod, error) {
   119  	// make a copy of the labels and annotations to avoid mutating the input parameters
   120  	podLabels := make(map[string]string)
   121  	podAnnotations := make(map[string]string)
   122  
   123  	for key, value := range labels {
   124  		podLabels[key] = value
   125  	}
   126  	podLabels[install.OLMManagedLabelKey] = install.OLMManagedLabelValue
   127  
   128  	for key, value := range annotations {
   129  		podAnnotations[key] = value
   130  	}
   131  
   132  	// Default case for nil serviceAccount
   133  	var saName string
   134  	var saImagePullSecrets []corev1.LocalObjectReference
   135  	// If the serviceAccount is not nil, set the fields that should appear on the pod
   136  	if serviceAccount != nil {
   137  		saName = serviceAccount.GetName()
   138  		saImagePullSecrets = serviceAccount.ImagePullSecrets
   139  	}
   140  
   141  	pod := &corev1.Pod{
   142  		ObjectMeta: metav1.ObjectMeta{
   143  			GenerateName: source.GetName() + "-",
   144  			Namespace:    source.GetNamespace(),
   145  			Labels:       podLabels,
   146  			Annotations:  podAnnotations,
   147  		},
   148  		Spec: corev1.PodSpec{
   149  			Containers: []corev1.Container{
   150  				{
   151  					Name:  name,
   152  					Image: img,
   153  					Ports: []corev1.ContainerPort{
   154  						{
   155  							Name:          "grpc",
   156  							ContainerPort: 50051,
   157  						},
   158  					},
   159  					ReadinessProbe: &corev1.Probe{
   160  						ProbeHandler: corev1.ProbeHandler{
   161  							Exec: &corev1.ExecAction{
   162  								Command: []string{"grpc_health_probe", "-addr=:50051"},
   163  							},
   164  						},
   165  						InitialDelaySeconds: readinessDelay,
   166  						TimeoutSeconds:      5,
   167  					},
   168  					LivenessProbe: &corev1.Probe{
   169  						ProbeHandler: corev1.ProbeHandler{
   170  							Exec: &corev1.ExecAction{
   171  								Command: []string{"grpc_health_probe", "-addr=:50051"},
   172  							},
   173  						},
   174  						InitialDelaySeconds: livenessDelay,
   175  						TimeoutSeconds:      5,
   176  					},
   177  					StartupProbe: &corev1.Probe{
   178  						ProbeHandler: corev1.ProbeHandler{
   179  							Exec: &corev1.ExecAction{
   180  								Command: []string{"grpc_health_probe", "-addr=:50051"},
   181  							},
   182  						},
   183  						FailureThreshold: 10,
   184  						PeriodSeconds:    10,
   185  						TimeoutSeconds:   5,
   186  					},
   187  					Resources: corev1.ResourceRequirements{
   188  						Requests: corev1.ResourceList{
   189  							corev1.ResourceCPU:    resource.MustParse("10m"),
   190  							corev1.ResourceMemory: resource.MustParse("50Mi"),
   191  						},
   192  					},
   193  					SecurityContext: &corev1.SecurityContext{
   194  						ReadOnlyRootFilesystem: ptr.To(false),
   195  					},
   196  					ImagePullPolicy:          image.InferImagePullPolicy(img),
   197  					TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
   198  				},
   199  			},
   200  			NodeSelector: map[string]string{
   201  				"kubernetes.io/os": "linux",
   202  			},
   203  			ServiceAccountName: saName,
   204  			// If this field is not set, there is a chance that pod will be created without the imagePullSecret
   205  			// defined by the serviceAccount
   206  			ImagePullSecrets: saImagePullSecrets,
   207  		},
   208  	}
   209  
   210  	// Override scheduling options if specified
   211  	if source.Spec.GrpcPodConfig != nil {
   212  		grpcPodConfig := source.Spec.GrpcPodConfig
   213  
   214  		// Override node selector
   215  		if grpcPodConfig.NodeSelector != nil {
   216  			pod.Spec.NodeSelector = make(map[string]string, len(grpcPodConfig.NodeSelector))
   217  			for key, value := range grpcPodConfig.NodeSelector {
   218  				pod.Spec.NodeSelector[key] = value
   219  			}
   220  		}
   221  
   222  		// Override priority class name
   223  		if grpcPodConfig.PriorityClassName != nil {
   224  			pod.Spec.PriorityClassName = *grpcPodConfig.PriorityClassName
   225  		}
   226  
   227  		// Override tolerations
   228  		if grpcPodConfig.Tolerations != nil {
   229  			pod.Spec.Tolerations = make([]corev1.Toleration, len(grpcPodConfig.Tolerations))
   230  			for index, toleration := range grpcPodConfig.Tolerations {
   231  				pod.Spec.Tolerations[index] = *toleration.DeepCopy()
   232  			}
   233  		}
   234  
   235  		// Override affinity
   236  		if grpcPodConfig.Affinity != nil {
   237  			pod.Spec.Affinity = grpcPodConfig.Affinity.DeepCopy()
   238  		}
   239  
   240  		// Add memory targets
   241  		if grpcPodConfig.MemoryTarget != nil {
   242  			pod.Spec.Containers[0].Resources.Requests[corev1.ResourceMemory] = *grpcPodConfig.MemoryTarget
   243  
   244  			if pod.Spec.Containers[0].Resources.Limits == nil {
   245  				pod.Spec.Containers[0].Resources.Limits = map[corev1.ResourceName]resource.Quantity{}
   246  			}
   247  
   248  			grpcPodConfig.MemoryTarget.Format = resource.BinarySI
   249  			pod.Spec.Containers[0].Env = append(pod.Spec.Containers[0].Env, corev1.EnvVar{
   250  				Name:  "GOMEMLIMIT",
   251  				Value: grpcPodConfig.MemoryTarget.String() + "B", // k8s resources use Mi, GOMEMLIMIT wants MiB
   252  			})
   253  		}
   254  
   255  		// Reconfigure pod to extract content
   256  		if grpcPodConfig.ExtractContent != nil {
   257  			pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{
   258  				Name: "utilities",
   259  				VolumeSource: corev1.VolumeSource{
   260  					EmptyDir: &corev1.EmptyDirVolumeSource{},
   261  				},
   262  			}, corev1.Volume{
   263  				Name: "catalog-content",
   264  				VolumeSource: corev1.VolumeSource{
   265  					EmptyDir: &corev1.EmptyDirVolumeSource{},
   266  				},
   267  			})
   268  			const utilitiesPath = "/utilities"
   269  			utilitiesVolumeMount := corev1.VolumeMount{
   270  				Name:      "utilities",
   271  				MountPath: utilitiesPath,
   272  			}
   273  			const catalogPath = "/extracted-catalog"
   274  			contentVolumeMount := corev1.VolumeMount{
   275  				Name:      "catalog-content",
   276  				MountPath: catalogPath,
   277  			}
   278  			pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{
   279  				Name:                     "extract-utilities",
   280  				Image:                    utilImage,
   281  				Command:                  []string{"cp"},
   282  				Args:                     []string{"/bin/copy-content", fmt.Sprintf("%s/copy-content", utilitiesPath)},
   283  				VolumeMounts:             []corev1.VolumeMount{utilitiesVolumeMount},
   284  				TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
   285  			}, corev1.Container{
   286  				Name:            "extract-content",
   287  				Image:           img,
   288  				ImagePullPolicy: image.InferImagePullPolicy(img),
   289  				Command:         []string{utilitiesPath + "/copy-content"},
   290  				Args: []string{
   291  					"--catalog.from=" + grpcPodConfig.ExtractContent.CatalogDir,
   292  					"--catalog.to=" + fmt.Sprintf("%s/catalog", catalogPath),
   293  					"--cache.from=" + grpcPodConfig.ExtractContent.CacheDir,
   294  					"--cache.to=" + fmt.Sprintf("%s/cache", catalogPath),
   295  				},
   296  				VolumeMounts:             []corev1.VolumeMount{utilitiesVolumeMount, contentVolumeMount},
   297  				TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
   298  			})
   299  
   300  			pod.Spec.Containers[0].Image = opmImg
   301  			pod.Spec.Containers[0].Command = []string{"/bin/opm"}
   302  			pod.Spec.Containers[0].Args = []string{
   303  				"serve",
   304  				filepath.Join(catalogPath, "catalog"),
   305  				"--cache-dir=" + filepath.Join(catalogPath, "cache"),
   306  			}
   307  			pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, contentVolumeMount)
   308  		}
   309  	}
   310  
   311  	// Determine the security context configuration
   312  	var securityContextConfig operatorsv1alpha1.SecurityConfig
   313  
   314  	// Use the user-provided security context config if it is defined
   315  	if source.Spec.GrpcPodConfig != nil && source.Spec.GrpcPodConfig.SecurityContextConfig != "" {
   316  		securityContextConfig = source.Spec.GrpcPodConfig.SecurityContextConfig
   317  	} else {
   318  		// Default to the defaultNamespace based and provided security context config
   319  		securityContextConfig = defaultSecurityConfig
   320  	}
   321  
   322  	// Apply the appropriate security context configuration
   323  	if securityContextConfig == operatorsv1alpha1.Restricted {
   324  		// Apply 'restricted' security settings
   325  		addSecurityContext(pod, runAsUser)
   326  	}
   327  
   328  	// Set priorityclass if its annotation exists
   329  	if prio, ok := podAnnotations[CatalogPriorityClassKey]; ok && prio != "" {
   330  		pod.Spec.PriorityClassName = prio
   331  	}
   332  
   333  	// Add PodSpec hash
   334  	// This hash info will be used to detect PodSpec changes
   335  	hash, err := hashutil.DeepHashObject(&pod.Spec)
   336  	if err != nil {
   337  		return nil, err
   338  	}
   339  	podLabels[PodHashLabelKey] = hash
   340  
   341  	// add eviction annotation to enable the cluster autoscaler to evict the pod in order to drain the node
   342  	// since catalog pods are not backed by a controller, they cannot be evicted by default
   343  	podAnnotations[ClusterAutoscalingAnnotationKey] = "true"
   344  
   345  	return pod, nil
   346  }
   347  
   348  func addSecurityContext(pod *corev1.Pod, runAsUser int64) {
   349  	for i := range pod.Spec.InitContainers {
   350  		if pod.Spec.InitContainers[i].SecurityContext == nil {
   351  			pod.Spec.InitContainers[i].SecurityContext = &corev1.SecurityContext{}
   352  		}
   353  		pod.Spec.InitContainers[i].SecurityContext.AllowPrivilegeEscalation = ptr.To(false)
   354  		pod.Spec.InitContainers[i].SecurityContext.Capabilities = &corev1.Capabilities{
   355  			Drop: []corev1.Capability{"ALL"},
   356  		}
   357  	}
   358  	for i := range pod.Spec.Containers {
   359  		if pod.Spec.Containers[i].SecurityContext == nil {
   360  			pod.Spec.Containers[i].SecurityContext = &corev1.SecurityContext{}
   361  		}
   362  		pod.Spec.Containers[i].SecurityContext.AllowPrivilegeEscalation = ptr.To(false)
   363  		pod.Spec.Containers[i].SecurityContext.Capabilities = &corev1.Capabilities{
   364  			Drop: []corev1.Capability{"ALL"},
   365  		}
   366  	}
   367  
   368  	pod.Spec.SecurityContext = &corev1.PodSecurityContext{
   369  		SeccompProfile: &corev1.SeccompProfile{
   370  			Type: corev1.SeccompProfileTypeRuntimeDefault,
   371  		},
   372  	}
   373  	if runAsUser > 0 {
   374  		pod.Spec.SecurityContext.RunAsUser = &runAsUser
   375  		pod.Spec.SecurityContext.RunAsNonRoot = ptr.To(true)
   376  	}
   377  }
   378  
   379  // getDefaultPodContextConfig returns Restricted if the defaultNamespace has the 'pod-security.kubernetes.io/enforce' label	set to 'restricted',
   380  // otherwise it returns Legacy. This is used to help determine the security context of the registry pod when it is not already defined by the user
   381  func getDefaultPodContextConfig(client operatorclient.ClientInterface, namespace string) (operatorsv1alpha1.SecurityConfig, error) {
   382  	ns, err := client.KubernetesInterface().CoreV1().Namespaces().Get(context.TODO(), namespace, metav1.GetOptions{})
   383  	if err != nil {
   384  		return "", fmt.Errorf("error fetching defaultNamespace: %v", err)
   385  	}
   386  	// 'pod-security.kubernetes.io/enforce' is the label used for enforcing defaultNamespace level security,
   387  	// and 'restricted' is the value indicating a restricted security policy.
   388  	if val, exists := ns.Labels["pod-security.kubernetes.io/enforce"]; exists && val == "restricted" {
   389  		return operatorsv1alpha1.Restricted, nil
   390  	}
   391  
   392  	return operatorsv1alpha1.Legacy, nil
   393  }