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  }