github.com/operator-framework/operator-lifecycle-manager@v0.30.0/test/e2e/magic_catalog.go (about) 1 package e2e 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 8 operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" 9 corev1 "k8s.io/api/core/v1" 10 k8serror "k8s.io/apimachinery/pkg/api/errors" 11 "k8s.io/apimachinery/pkg/api/resource" 12 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 utilerrors "k8s.io/apimachinery/pkg/util/errors" 14 "k8s.io/apimachinery/pkg/util/intstr" 15 "k8s.io/utils/ptr" 16 k8scontrollerclient "sigs.k8s.io/controller-runtime/pkg/client" 17 ) 18 19 const ( 20 olmCatalogLabel string = "olm.catalogSource" 21 catalogMountPath string = "/opt/olm" 22 catalogServicePort int32 = 50051 23 catalogReadyState string = "READY" 24 ) 25 26 type MagicCatalog struct { 27 fileBasedCatalog FileBasedCatalogProvider 28 kubeClient k8scontrollerclient.Client 29 name string 30 namespace string 31 configMapName string 32 serviceName string 33 podName string 34 } 35 36 // NewMagicCatalog creates an object that can deploy an arbitrary file-based catalog given by the FileBasedCatalogProvider 37 // Keep in mind that there are limits to the configMaps. So, the catalogs need to be relatively simple 38 func NewMagicCatalog(kubeClient k8scontrollerclient.Client, namespace string, catalogName string, provider FileBasedCatalogProvider) *MagicCatalog { 39 return &MagicCatalog{ 40 fileBasedCatalog: provider, 41 kubeClient: kubeClient, 42 namespace: namespace, 43 name: catalogName, 44 configMapName: catalogName + "-configmap", 45 serviceName: catalogName + "-svc", 46 podName: catalogName + "-pod", 47 } 48 } 49 50 func NewMagicCatalogFromFile(kubeClient k8scontrollerclient.Client, namespace string, catalogName string, fbcFilePath string) (*MagicCatalog, error) { 51 provider, err := NewFileBasedFiledBasedCatalogProvider(fbcFilePath) 52 if err != nil { 53 return nil, err 54 } 55 catalog := NewMagicCatalog(kubeClient, namespace, catalogName, provider) 56 return catalog, nil 57 } 58 59 func (c *MagicCatalog) GetName() string { 60 return c.name 61 } 62 63 func (c *MagicCatalog) GetNamespace() string { 64 return c.namespace 65 } 66 67 func (c *MagicCatalog) DeployCatalog(ctx context.Context) error { 68 resourcesInOrderOfDeployment := []k8scontrollerclient.Object{ 69 c.makeConfigMap(), 70 c.makeCatalogSourcePod(), 71 c.makeCatalogService(), 72 c.makeCatalogSource(), 73 } 74 if err := c.deployCatalog(ctx, resourcesInOrderOfDeployment); err != nil { 75 return err 76 } 77 if err := c.catalogSourceIsReady(ctx); err != nil { 78 return c.cleanUpAfterError(ctx, err) 79 } 80 81 return nil 82 } 83 84 func (c *MagicCatalog) UpdateCatalog(ctx context.Context, provider FileBasedCatalogProvider) error { 85 resourcesInOrderOfDeletion := []k8scontrollerclient.Object{ 86 c.makeCatalogSourcePod(), 87 c.makeConfigMap(), 88 } 89 errors := c.undeployCatalog(ctx, resourcesInOrderOfDeletion) 90 if len(errors) != 0 { 91 return utilerrors.NewAggregate(errors) 92 } 93 94 // TODO(tflannag): Create a pod watcher struct and setup an underlying watch 95 // and block until ctx.Done()? 96 err := waitFor(func() (bool, error) { 97 pod := &corev1.Pod{} 98 err := c.kubeClient.Get(ctx, k8scontrollerclient.ObjectKey{ 99 Name: c.podName, 100 Namespace: c.namespace, 101 }, pod) 102 if k8serror.IsNotFound(err) { 103 return true, nil 104 } 105 return false, err 106 }) 107 if err != nil { 108 return fmt.Errorf("failed to successfully update the catalog deployment: %v", err) 109 } 110 111 c.fileBasedCatalog = provider 112 resourcesInOrderOfCreation := []k8scontrollerclient.Object{ 113 c.makeConfigMap(), 114 c.makeCatalogSourcePod(), 115 } 116 if err := c.deployCatalog(ctx, resourcesInOrderOfCreation); err != nil { 117 return err 118 } 119 if err := c.catalogSourceIsReady(ctx); err != nil { 120 return c.cleanUpAfterError(ctx, err) 121 } 122 123 return nil 124 } 125 126 func (c *MagicCatalog) UndeployCatalog(ctx context.Context) []error { 127 resourcesInOrderOfDeletion := []k8scontrollerclient.Object{ 128 c.makeCatalogSource(), 129 c.makeCatalogService(), 130 c.makeCatalogSourcePod(), 131 c.makeConfigMap(), 132 } 133 return c.undeployCatalog(ctx, resourcesInOrderOfDeletion) 134 } 135 136 func (c *MagicCatalog) catalogSourceIsReady(ctx context.Context) error { 137 // wait for catalog source to become ready 138 key := k8scontrollerclient.ObjectKey{ 139 Name: c.name, 140 Namespace: c.namespace, 141 } 142 143 return waitFor(func() (bool, error) { 144 catalogSource := &operatorsv1alpha1.CatalogSource{} 145 err := c.kubeClient.Get(ctx, key, catalogSource) 146 if err != nil || catalogSource.Status.GRPCConnectionState == nil { 147 return false, err 148 } 149 state := catalogSource.Status.GRPCConnectionState.LastObservedState 150 if state != catalogReadyState { 151 return false, nil 152 } 153 return true, nil 154 }) 155 } 156 157 func (c *MagicCatalog) deployCatalog(ctx context.Context, resources []k8scontrollerclient.Object) error { 158 for _, res := range resources { 159 err := c.kubeClient.Create(ctx, res) 160 if err != nil { 161 return c.cleanUpAfterError(ctx, err) 162 } 163 } 164 return nil 165 } 166 167 func (c *MagicCatalog) undeployCatalog(ctx context.Context, resources []k8scontrollerclient.Object) []error { 168 var errors []error 169 // try to delete all resourcesInOrderOfDeletion even if errors are 170 // encountered through deletion. 171 for _, res := range resources { 172 err := c.kubeClient.Delete(ctx, res) 173 174 // ignore not found errors 175 if err != nil && !k8serror.IsNotFound(err) { 176 if errors == nil { 177 errors = make([]error, 0) 178 } 179 errors = append(errors, err) 180 } 181 } 182 return errors 183 } 184 185 func (c *MagicCatalog) cleanUpAfterError(ctx context.Context, err error) error { 186 cleanupErr := c.UndeployCatalog(ctx) 187 if cleanupErr != nil { 188 return fmt.Errorf("the following cleanup errors occurred: '%s' after an error deploying the configmap: '%s' ", cleanupErr, err) 189 } 190 return err 191 } 192 193 func (c *MagicCatalog) makeCatalogService() *corev1.Service { 194 return &corev1.Service{ 195 ObjectMeta: metav1.ObjectMeta{ 196 Name: c.serviceName, 197 Namespace: c.namespace, 198 }, 199 Spec: corev1.ServiceSpec{ 200 Ports: []corev1.ServicePort{ 201 { 202 Name: "grpc", 203 Port: catalogServicePort, 204 Protocol: "TCP", 205 TargetPort: intstr.FromInt(int(catalogServicePort)), 206 }, 207 }, 208 Selector: c.makeCatalogSourcePodLabels(), 209 }, 210 } 211 } 212 213 func (c *MagicCatalog) makeConfigMap() *corev1.ConfigMap { 214 isImmutable := true 215 return &corev1.ConfigMap{ 216 ObjectMeta: metav1.ObjectMeta{ 217 Name: c.configMapName, 218 Namespace: c.namespace, 219 }, 220 Immutable: &isImmutable, 221 Data: map[string]string{ 222 "catalog.json": c.fileBasedCatalog.GetCatalog(), 223 // due to the way files get mounted to pods from configMaps 224 // it is important to add _this_ .indexignore 225 // 226 // The mount folder will look something like this: 227 // /opt/olm 228 // |--> ..2021_12_15_02_01_11.729011450 229 // |--> catalog.json 230 // |--> .indexignore 231 // |--> ..data -> ..2021_12_15_02_01_11.729011450 232 // |--> catalog.json -> ..data/catalog.json 233 // |--> .indexignore -> ..data/.indexignore 234 // Adding '**/..*' to the .indexignore ensures the 235 // '..2021_12_15_02_01_11.729011450' and ' ..data' directories are ignored. 236 // Otherwise, opm will pick up on both catalog.json files and fail with a conflicts (duplicate packages) 237 ".indexignore": "**/\\.\\.*\n", 238 }, 239 } 240 } 241 242 func (c *MagicCatalog) makeCatalogSource() *operatorsv1alpha1.CatalogSource { 243 return &operatorsv1alpha1.CatalogSource{ 244 ObjectMeta: metav1.ObjectMeta{ 245 Name: c.name, 246 Namespace: c.namespace, 247 }, 248 Spec: operatorsv1alpha1.CatalogSourceSpec{ 249 SourceType: operatorsv1alpha1.SourceTypeGrpc, 250 Address: fmt.Sprintf("%s.%s.svc:50051", c.serviceName, c.namespace), 251 GrpcPodConfig: &operatorsv1alpha1.GrpcPodConfig{ 252 SecurityContextConfig: operatorsv1alpha1.Restricted, 253 }, 254 }, 255 } 256 } 257 258 func (c *MagicCatalog) makeCatalogSourcePod() *corev1.Pod { 259 260 const ( 261 readinessDelay int32 = 5 262 livenessDelay int32 = 10 263 volumeMountName string = "fbc-catalog" 264 ) 265 266 var image = "quay.io/operator-framework/opm" 267 if os.Getenv("OPERATOR_REGISTRY_TAG") != "" { 268 image = fmt.Sprintf("quay.io/operator-framework/opm:%s", os.Getenv("OPERATOR_REGISTRY_TAG")) 269 } 270 271 return &corev1.Pod{ 272 ObjectMeta: metav1.ObjectMeta{ 273 Name: c.podName, 274 Namespace: c.namespace, 275 Labels: c.makeCatalogSourcePodLabels(), 276 }, 277 Spec: corev1.PodSpec{ 278 SecurityContext: &corev1.PodSecurityContext{ 279 SeccompProfile: &corev1.SeccompProfile{ 280 Type: corev1.SeccompProfileTypeRuntimeDefault, 281 }, 282 }, 283 Containers: []corev1.Container{ 284 { 285 Name: "catalog", 286 Image: image, 287 Command: []string{"opm", "serve", catalogMountPath}, 288 Ports: []corev1.ContainerPort{ 289 { 290 Name: "grpc", 291 ContainerPort: 50051, 292 }, 293 }, 294 ReadinessProbe: &corev1.Probe{ 295 ProbeHandler: corev1.ProbeHandler{ 296 Exec: &corev1.ExecAction{ 297 Command: []string{"grpc_health_probe", "-addr=:50051"}, 298 }, 299 }, 300 InitialDelaySeconds: readinessDelay, 301 TimeoutSeconds: 5, 302 }, 303 LivenessProbe: &corev1.Probe{ 304 ProbeHandler: corev1.ProbeHandler{ 305 Exec: &corev1.ExecAction{ 306 Command: []string{"grpc_health_probe", "-addr=:50051"}, 307 }, 308 }, 309 InitialDelaySeconds: livenessDelay, 310 TimeoutSeconds: 5, 311 }, 312 Resources: corev1.ResourceRequirements{ 313 Requests: corev1.ResourceList{ 314 corev1.ResourceCPU: resource.MustParse("10m"), 315 corev1.ResourceMemory: resource.MustParse("50Mi"), 316 }, 317 }, 318 SecurityContext: &corev1.SecurityContext{ 319 ReadOnlyRootFilesystem: ptr.To(bool(false)), 320 AllowPrivilegeEscalation: ptr.To(bool(false)), 321 Capabilities: &corev1.Capabilities{ 322 Drop: []corev1.Capability{"ALL"}, 323 }, 324 RunAsNonRoot: ptr.To(bool(true)), 325 RunAsUser: ptr.To(int64(1001)), 326 }, 327 ImagePullPolicy: corev1.PullAlways, 328 TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, 329 VolumeMounts: []corev1.VolumeMount{ 330 { 331 Name: volumeMountName, 332 MountPath: catalogMountPath, 333 ReadOnly: true, 334 }, 335 }, 336 }, 337 }, 338 Volumes: []corev1.Volume{ 339 { 340 Name: volumeMountName, 341 VolumeSource: corev1.VolumeSource{ 342 ConfigMap: &corev1.ConfigMapVolumeSource{ 343 LocalObjectReference: corev1.LocalObjectReference{ 344 Name: c.configMapName, 345 }, 346 }, 347 }, 348 }, 349 }, 350 }, 351 } 352 } 353 354 func (c *MagicCatalog) makeCatalogSourcePodLabels() map[string]string { 355 return map[string]string{ 356 olmCatalogLabel: c.name, 357 } 358 }