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 ®istryReconcilerFactory{ 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 }