github.com/operator-framework/operator-lifecycle-manager@v0.30.0/pkg/controller/operators/catalog/step.go (about)

     1  package catalog
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  
     7  	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install"
     8  	"github.com/pkg/errors"
     9  	"github.com/sirupsen/logrus"
    10  	corev1 "k8s.io/api/core/v1"
    11  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    12  	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    13  	apiextensionsv1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
    14  	apiextensionsv1beta1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1"
    15  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    16  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    17  	"k8s.io/client-go/dynamic"
    18  	"k8s.io/client-go/tools/record"
    19  	"k8s.io/client-go/util/retry"
    20  
    21  	"github.com/operator-framework/api/pkg/operators/v1alpha1"
    22  	listersv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/listers/operators/v1alpha1"
    23  	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/internal/alongside"
    24  	crdlib "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/crd"
    25  	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient"
    26  )
    27  
    28  // Stepper manages cluster interactions based on the step.
    29  type Stepper interface {
    30  	Status() (v1alpha1.StepStatus, error)
    31  }
    32  
    33  // StepperFunc fulfills the Stepper interface.
    34  type StepperFunc func() (v1alpha1.StepStatus, error)
    35  
    36  func (s StepperFunc) Status() (v1alpha1.StepStatus, error) {
    37  	return s()
    38  }
    39  
    40  // Builder holds clients and data structures required for the StepBuilder to work
    41  // Builder attributes are not to meant to be accessed outside the StepBuilder method
    42  type builder struct {
    43  	plan             *v1alpha1.InstallPlan
    44  	csvLister        listersv1alpha1.ClusterServiceVersionLister
    45  	opclient         operatorclient.ClientInterface
    46  	dynamicClient    dynamic.Interface
    47  	manifestResolver ManifestResolver
    48  	logger           logrus.FieldLogger
    49  	eventRecorder    record.EventRecorder
    50  
    51  	annotator alongside.Annotator
    52  }
    53  
    54  func newBuilder(plan *v1alpha1.InstallPlan, csvLister listersv1alpha1.ClusterServiceVersionLister, opclient operatorclient.ClientInterface, dynamicClient dynamic.Interface, manifestResolver ManifestResolver, logger logrus.FieldLogger, er record.EventRecorder) *builder {
    55  	return &builder{
    56  		plan:             plan,
    57  		csvLister:        csvLister,
    58  		opclient:         opclient,
    59  		dynamicClient:    dynamicClient,
    60  		manifestResolver: manifestResolver,
    61  		logger:           logger,
    62  		eventRecorder:    er,
    63  	}
    64  }
    65  
    66  type notSupportedStepperErr struct {
    67  	message string
    68  }
    69  
    70  func (n notSupportedStepperErr) Error() string {
    71  	return n.message
    72  }
    73  
    74  // step is a factory that creates StepperFuncs based on the install plan step Kind.
    75  func (b *builder) create(step v1alpha1.Step) (Stepper, error) {
    76  	manifest, err := b.manifestResolver.ManifestForStep(&step)
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  
    81  	switch step.Resource.Kind {
    82  	case crdKind:
    83  		version, err := crdlib.Version(&manifest)
    84  		if err != nil {
    85  			return nil, err
    86  		}
    87  
    88  		switch version {
    89  		case crdlib.V1Version:
    90  			return b.NewCRDV1Step(b.opclient.ApiextensionsInterface().ApiextensionsV1(), &step, manifest), nil
    91  		case crdlib.V1Beta1Version:
    92  			return b.NewCRDV1Beta1Step(b.opclient.ApiextensionsInterface().ApiextensionsV1beta1(), &step, manifest), nil
    93  		}
    94  	}
    95  	return nil, notSupportedStepperErr{fmt.Sprintf("stepper interface does not support %s", step.Resource.Kind)}
    96  }
    97  
    98  func (b *builder) NewCRDV1Step(client apiextensionsv1client.ApiextensionsV1Interface, step *v1alpha1.Step, manifest string) StepperFunc {
    99  	return func() (v1alpha1.StepStatus, error) {
   100  		switch step.Status {
   101  		case v1alpha1.StepStatusPresent:
   102  			return v1alpha1.StepStatusPresent, nil
   103  		case v1alpha1.StepStatusCreated:
   104  			return v1alpha1.StepStatusCreated, nil
   105  		case v1alpha1.StepStatusWaitingForAPI:
   106  			crd, err := client.CustomResourceDefinitions().Get(context.TODO(), step.Resource.Name, metav1.GetOptions{})
   107  			if err != nil {
   108  				if apierrors.IsNotFound(err) {
   109  					return v1alpha1.StepStatusNotPresent, nil
   110  				}
   111  				return v1alpha1.StepStatusNotPresent, errors.Wrapf(err, "error finding the %s CRD", crd.Name)
   112  			}
   113  			established, namesAccepted := false, false
   114  			for _, cdt := range crd.Status.Conditions {
   115  				switch cdt.Type {
   116  				case apiextensionsv1.Established:
   117  					if cdt.Status == apiextensionsv1.ConditionTrue {
   118  						established = true
   119  					}
   120  				case apiextensionsv1.NamesAccepted:
   121  					if cdt.Status == apiextensionsv1.ConditionTrue {
   122  						namesAccepted = true
   123  					}
   124  				}
   125  			}
   126  			if established && namesAccepted {
   127  				return v1alpha1.StepStatusCreated, nil
   128  			}
   129  		case v1alpha1.StepStatusUnknown, v1alpha1.StepStatusNotPresent:
   130  			crd, err := crdlib.UnmarshalV1(manifest)
   131  			if err != nil {
   132  				return v1alpha1.StepStatusUnknown, err
   133  			}
   134  
   135  			setInstalledAlongsideAnnotation(b.annotator, crd, b.plan.GetNamespace(), step.Resolving, b.csvLister, crd)
   136  			if crd.Labels == nil {
   137  				crd.Labels = map[string]string{}
   138  			}
   139  			crd.Labels[install.OLMManagedLabelKey] = install.OLMManagedLabelValue
   140  
   141  			_, createError := client.CustomResourceDefinitions().Create(context.TODO(), crd, metav1.CreateOptions{})
   142  			if apierrors.IsAlreadyExists(createError) {
   143  				err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
   144  					currentCRD, _ := client.CustomResourceDefinitions().Get(context.TODO(), crd.GetName(), metav1.GetOptions{})
   145  					crd.SetResourceVersion(currentCRD.GetResourceVersion())
   146  					if err = validateV1CRDCompatibility(b.dynamicClient, currentCRD, crd); err != nil {
   147  						vErr := &validationError{}
   148  						// if the conversion strategy in the new CRD is not "Webhook" OR the error is not a ValidationError
   149  						// return an error. This will catch and return any errors that occur unrelated to actual validation.
   150  						// For example, the API server returning an error when performing a list operation
   151  						if crd.Spec.Conversion == nil || crd.Spec.Conversion.Strategy != apiextensionsv1.WebhookConverter || !errors.As(err, vErr) {
   152  							return fmt.Errorf("error validating existing CRs against new CRD's schema for %q: %w", step.Resource.Name, err)
   153  						}
   154  						// If the conversion strategy in the new CRD is "Webhook" and the error that occurred
   155  						// is an error related to validation, warn that validation failed but that we are trusting
   156  						// that the conversion strategy specified by the author will successfully convert to a format
   157  						// that passes validation and allow the upgrade to continue
   158  						warnTempl := `Validation of existing CRs against the new CRD's schema failed, but a webhook conversion strategy was specified in the new CRD.
   159  The new webhook will only start after the bundle is upgraded, so we must assume that it will successfully convert existing CRs to a format that would have passed validation.
   160  
   161  CRD: %q
   162  Validation Error: %s
   163  `
   164  						warnString := fmt.Sprintf(warnTempl, step.Resource.Name, err.Error())
   165  						b.logger.Warn(warnString)
   166  						b.eventRecorder.Event(b.plan, corev1.EventTypeWarning, "CRDValidation", warnString)
   167  					}
   168  
   169  					// check to see if stored versions changed and whether the upgrade could cause potential data loss
   170  					safe, err := crdlib.SafeStorageVersionUpgrade(currentCRD, crd)
   171  					if !safe {
   172  						b.logger.Errorf("risk of data loss updating %q: %s", step.Resource.Name, err)
   173  						return fmt.Errorf("risk of data loss updating %q: %w", step.Resource.Name, err)
   174  					}
   175  					if err != nil {
   176  						return fmt.Errorf("checking CRD for potential data loss updating %q: %w", step.Resource.Name, err)
   177  					}
   178  
   179  					// Update CRD to new version
   180  					setInstalledAlongsideAnnotation(b.annotator, crd, b.plan.GetNamespace(), step.Resolving, b.csvLister, crd, currentCRD)
   181  					_, err = client.CustomResourceDefinitions().Update(context.TODO(), crd, metav1.UpdateOptions{})
   182  					if err != nil {
   183  						return fmt.Errorf("error updating CRD %q: %w", step.Resource.Name, err)
   184  					}
   185  					return nil
   186  				})
   187  				if err != nil {
   188  					return v1alpha1.StepStatusUnknown, err
   189  				}
   190  				// If it already existed, mark the step as Present.
   191  				// they were equal - mark CRD as present
   192  				return v1alpha1.StepStatusPresent, nil
   193  			} else if createError != nil {
   194  				// Unexpected error creating the CRD.
   195  				return v1alpha1.StepStatusUnknown, createError
   196  			}
   197  			// If no error occurred, make sure to wait for the API to become available.
   198  			return v1alpha1.StepStatusWaitingForAPI, nil
   199  		}
   200  		return v1alpha1.StepStatusUnknown, nil
   201  	}
   202  }
   203  
   204  func (b *builder) NewCRDV1Beta1Step(client apiextensionsv1beta1client.ApiextensionsV1beta1Interface, step *v1alpha1.Step, manifest string) StepperFunc {
   205  	return func() (v1alpha1.StepStatus, error) {
   206  		switch step.Status {
   207  		case v1alpha1.StepStatusPresent:
   208  			return v1alpha1.StepStatusPresent, nil
   209  		case v1alpha1.StepStatusCreated:
   210  			return v1alpha1.StepStatusCreated, nil
   211  		case v1alpha1.StepStatusWaitingForAPI:
   212  			crd, err := client.CustomResourceDefinitions().Get(context.TODO(), step.Resource.Name, metav1.GetOptions{})
   213  			if err != nil {
   214  				if apierrors.IsNotFound(err) {
   215  					return v1alpha1.StepStatusNotPresent, nil
   216  				}
   217  				return v1alpha1.StepStatusNotPresent, fmt.Errorf("error finding the %q CRD: %w", crd.Name, err)
   218  			}
   219  			established, namesAccepted := false, false
   220  			for _, cdt := range crd.Status.Conditions {
   221  				switch cdt.Type {
   222  				case apiextensionsv1beta1.Established:
   223  					if cdt.Status == apiextensionsv1beta1.ConditionTrue {
   224  						established = true
   225  					}
   226  				case apiextensionsv1beta1.NamesAccepted:
   227  					if cdt.Status == apiextensionsv1beta1.ConditionTrue {
   228  						namesAccepted = true
   229  					}
   230  				}
   231  			}
   232  			if established && namesAccepted {
   233  				return v1alpha1.StepStatusCreated, nil
   234  			}
   235  		case v1alpha1.StepStatusUnknown, v1alpha1.StepStatusNotPresent:
   236  			crd, err := crdlib.UnmarshalV1Beta1(manifest)
   237  			if err != nil {
   238  				return v1alpha1.StepStatusUnknown, err
   239  			}
   240  
   241  			setInstalledAlongsideAnnotation(b.annotator, crd, b.plan.GetNamespace(), step.Resolving, b.csvLister, crd)
   242  			if crd.Labels == nil {
   243  				crd.Labels = map[string]string{}
   244  			}
   245  			crd.Labels[install.OLMManagedLabelKey] = install.OLMManagedLabelValue
   246  
   247  			_, createError := client.CustomResourceDefinitions().Create(context.TODO(), crd, metav1.CreateOptions{})
   248  			if apierrors.IsAlreadyExists(createError) {
   249  				err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
   250  					currentCRD, _ := client.CustomResourceDefinitions().Get(context.TODO(), crd.GetName(), metav1.GetOptions{})
   251  					crd.SetResourceVersion(currentCRD.GetResourceVersion())
   252  
   253  					if err = validateV1Beta1CRDCompatibility(b.dynamicClient, currentCRD, crd); err != nil {
   254  						return fmt.Errorf("error validating existing CRs against new CRD's schema for %q: %w", step.Resource.Name, err)
   255  					}
   256  
   257  					// check to see if stored versions changed and whether the upgrade could cause potential data loss
   258  					safe, err := crdlib.SafeStorageVersionUpgrade(currentCRD, crd)
   259  					if !safe {
   260  						b.logger.Errorf("risk of data loss updating %q: %s", step.Resource.Name, err)
   261  						return fmt.Errorf("risk of data loss updating %q: %w", step.Resource.Name, err)
   262  					}
   263  					if err != nil {
   264  						return fmt.Errorf("checking CRD for potential data loss updating %q: %w", step.Resource.Name, err)
   265  					}
   266  
   267  					// Update CRD to new version
   268  					setInstalledAlongsideAnnotation(b.annotator, crd, b.plan.GetNamespace(), step.Resolving, b.csvLister, crd, currentCRD)
   269  					_, err = client.CustomResourceDefinitions().Update(context.TODO(), crd, metav1.UpdateOptions{})
   270  					if err != nil {
   271  						return fmt.Errorf("error updating CRD %q: %w", step.Resource.Name, err)
   272  					}
   273  					return nil
   274  				})
   275  				if err != nil {
   276  					return v1alpha1.StepStatusUnknown, err
   277  				}
   278  				// If it already existed, mark the step as Present.
   279  				// they were equal - mark CRD as present
   280  				return v1alpha1.StepStatusPresent, nil
   281  			} else if createError != nil {
   282  				// Unexpected error creating the CRD.
   283  				return v1alpha1.StepStatusUnknown, createError
   284  			}
   285  			// If no error occurred, make sure to wait for the API to become available.
   286  			return v1alpha1.StepStatusWaitingForAPI, nil
   287  		}
   288  		return v1alpha1.StepStatusUnknown, nil
   289  	}
   290  }
   291  
   292  func setInstalledAlongsideAnnotation(a alongside.Annotator, dst metav1.Object, namespace string, name string, lister listersv1alpha1.ClusterServiceVersionLister, srcs ...metav1.Object) {
   293  	var nns []alongside.NamespacedName
   294  
   295  	// Only keep references to existing and non-copied CSVs to
   296  	// avoid unbounded growth.
   297  	for _, src := range srcs {
   298  		for _, nn := range a.FromObject(src) {
   299  			if nn.Namespace == namespace && nn.Name == name {
   300  				continue
   301  			}
   302  
   303  			if csv, err := lister.ClusterServiceVersions(nn.Namespace).Get(nn.Name); apierrors.IsNotFound(err) {
   304  				continue
   305  			} else if err == nil && csv.IsCopied() {
   306  				continue
   307  			}
   308  			// CSV exists and is not copied OR err is non-nil, but
   309  			// not 404. Safer to assume it exists in unhandled
   310  			// error cases and try again next time.
   311  			nns = append(nns, nn)
   312  		}
   313  	}
   314  
   315  	if namespace != "" && name != "" {
   316  		nns = append(nns, alongside.NamespacedName{Namespace: namespace, Name: name})
   317  	}
   318  
   319  	a.ToObject(dst, nns)
   320  }