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

     1  package install
     2  
     3  import (
     4  	"fmt"
     5  	"time"
     6  
     7  	log "github.com/sirupsen/logrus"
     8  	appsv1 "k8s.io/api/apps/v1"
     9  	corev1 "k8s.io/api/core/v1"
    10  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    11  	k8slabels "k8s.io/apimachinery/pkg/labels"
    12  	"k8s.io/utils/ptr"
    13  
    14  	"github.com/operator-framework/api/pkg/operators/v1alpha1"
    15  	"github.com/operator-framework/operator-lifecycle-manager/pkg/api/wrappers"
    16  	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/olm/overrides/inject"
    17  	hashutil "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/kubernetes/pkg/util/hash"
    18  	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/ownerutil"
    19  )
    20  
    21  const DeploymentSpecHashLabelKey = "olm.deployment-spec-hash"
    22  
    23  type StrategyDeploymentInstaller struct {
    24  	strategyClient            wrappers.InstallStrategyDeploymentInterface
    25  	owner                     ownerutil.Owner
    26  	previousStrategy          Strategy
    27  	templateAnnotations       map[string]string
    28  	initializers              DeploymentInitializerFuncChain
    29  	apiServiceDescriptions    []certResource
    30  	webhookDescriptions       []certResource
    31  	certificateExpirationTime time.Time
    32  	certificatesRotated       bool
    33  }
    34  
    35  var _ Strategy = &v1alpha1.StrategyDetailsDeployment{}
    36  var _ StrategyInstaller = &StrategyDeploymentInstaller{}
    37  
    38  // DeploymentInitializerFunc takes a deployment object and appropriately
    39  // initializes it for install.
    40  //
    41  // Before a deployment is created on the cluster, we can run a series of
    42  // overrides functions that will properly initialize the deployment object.
    43  type DeploymentInitializerFunc func(deployment *appsv1.Deployment) error
    44  
    45  // DeploymentInitializerFuncChain defines a chain of DeploymentInitializerFunc.
    46  type DeploymentInitializerFuncChain []DeploymentInitializerFunc
    47  
    48  // Apply runs series of overrides functions that will properly initialize
    49  // the deployment object.
    50  func (c DeploymentInitializerFuncChain) Apply(deployment *appsv1.Deployment) (err error) {
    51  	for _, initializer := range c {
    52  		if initializer == nil {
    53  			continue
    54  		}
    55  
    56  		if initializationErr := initializer(deployment); initializationErr != nil {
    57  			err = initializationErr
    58  			break
    59  		}
    60  	}
    61  
    62  	return
    63  }
    64  
    65  // DeploymentInitializerBuilderFunc returns a DeploymentInitializerFunc based on
    66  // the given context.
    67  type DeploymentInitializerBuilderFunc func(owner ownerutil.Owner) DeploymentInitializerFunc
    68  
    69  func NewStrategyDeploymentInstaller(strategyClient wrappers.InstallStrategyDeploymentInterface, templateAnnotations map[string]string, owner ownerutil.Owner, previousStrategy Strategy, initializers DeploymentInitializerFuncChain, apiServiceDescriptions []v1alpha1.APIServiceDescription, webhookDescriptions []v1alpha1.WebhookDescription) StrategyInstaller {
    70  	apiDescs := make([]certResource, len(apiServiceDescriptions))
    71  	for i := range apiServiceDescriptions {
    72  		apiDescs[i] = &apiServiceDescriptionsWithCAPEM{apiServiceDescriptions[i], []byte{}}
    73  	}
    74  
    75  	webhookDescs := make([]certResource, len(webhookDescriptions))
    76  	for i := range webhookDescs {
    77  		webhookDescs[i] = &webhookDescriptionWithCAPEM{webhookDescriptions[i], []byte{}}
    78  	}
    79  
    80  	return &StrategyDeploymentInstaller{
    81  		strategyClient:            strategyClient,
    82  		owner:                     owner,
    83  		previousStrategy:          previousStrategy,
    84  		templateAnnotations:       templateAnnotations,
    85  		initializers:              initializers,
    86  		apiServiceDescriptions:    apiDescs,
    87  		webhookDescriptions:       webhookDescs,
    88  		certificatesRotated:       false,
    89  		certificateExpirationTime: time.Time{},
    90  	}
    91  }
    92  
    93  func (i *StrategyDeploymentInstaller) installDeployments(deps []v1alpha1.StrategyDeploymentSpec) error {
    94  	for _, d := range deps {
    95  		deployment, _, err := i.deploymentForSpec(d.Name, d.Spec, d.Label)
    96  		if err != nil {
    97  			return err
    98  		}
    99  
   100  		if _, err := i.strategyClient.CreateOrUpdateDeployment(deployment); err != nil {
   101  			return err
   102  		}
   103  
   104  		if err := i.createOrUpdateCertResourcesForDeployment(); err != nil {
   105  			return err
   106  		}
   107  	}
   108  	return nil
   109  }
   110  
   111  func (i *StrategyDeploymentInstaller) createOrUpdateCertResourcesForDeployment() error {
   112  	for _, desc := range i.getCertResources() {
   113  		switch d := desc.(type) {
   114  		case *apiServiceDescriptionsWithCAPEM:
   115  			err := i.createOrUpdateAPIService(d.caPEM, d.apiServiceDescription)
   116  			if err != nil {
   117  				return err
   118  			}
   119  
   120  			// Cleanup legacy APIService resources
   121  			err = i.deleteLegacyAPIServiceResources(*d)
   122  			if err != nil {
   123  				return err
   124  			}
   125  		case *webhookDescriptionWithCAPEM:
   126  			err := i.createOrUpdateWebhook(d.caPEM, d.webhookDescription)
   127  			if err != nil {
   128  				return err
   129  			}
   130  		default:
   131  			return fmt.Errorf("unsupported CA Resource")
   132  		}
   133  	}
   134  	return nil
   135  }
   136  
   137  func (i *StrategyDeploymentInstaller) deploymentForSpec(name string, spec appsv1.DeploymentSpec, specLabels k8slabels.Set) (deployment *appsv1.Deployment, hash string, err error) {
   138  	dep := &appsv1.Deployment{Spec: spec}
   139  	dep.SetName(name)
   140  	dep.SetNamespace(i.owner.GetNamespace())
   141  
   142  	// Merge annotations (to avoid losing info from pod template)
   143  	annotations := map[string]string{}
   144  	for k, v := range dep.Spec.Template.GetAnnotations() {
   145  		annotations[k] = v
   146  	}
   147  	for k, v := range i.templateAnnotations { // templateAnnotations comes from CSV.Annotations
   148  		annotations[k] = v
   149  	}
   150  	dep.Spec.Template.SetAnnotations(annotations)
   151  
   152  	// Set custom labels before CSV owner labels
   153  	dep.SetLabels(specLabels)
   154  	if dep.Labels == nil {
   155  		dep.Labels = map[string]string{}
   156  	}
   157  	dep.Labels[OLMManagedLabelKey] = OLMManagedLabelValue
   158  
   159  	ownerutil.AddNonBlockingOwner(dep, i.owner)
   160  	ownerutil.AddOwnerLabelsForKind(dep, i.owner, v1alpha1.ClusterServiceVersionKind)
   161  
   162  	// Any admin-provided config (Subscription.Spec.Config) gets injected here.
   163  	if applyErr := i.initializers.Apply(dep); applyErr != nil {
   164  		err = applyErr
   165  		return
   166  	}
   167  
   168  	podSpec := &dep.Spec.Template.Spec
   169  	if injectErr := inject.InjectEnvIntoDeployment(podSpec, []corev1.EnvVar{{
   170  		Name:  "OPERATOR_CONDITION_NAME",
   171  		Value: i.owner.GetName(),
   172  	}}); injectErr != nil {
   173  		err = injectErr
   174  		return
   175  	}
   176  
   177  	// OLM does not support Rollbacks.
   178  	// By default, each deployment created by OLM could spawn up to 10 replicaSets.
   179  	// By setting the deployments revisionHistoryLimit to 1, OLM will only create up
   180  	// to 2 ReplicaSets per deployment it manages, saving memory.
   181  	dep.Spec.RevisionHistoryLimit = ptr.To(int32(1))
   182  
   183  	hash, err = hashutil.DeepHashObject(&dep.Spec)
   184  	if err != nil {
   185  		return nil, "", err
   186  	}
   187  	dep.Labels[DeploymentSpecHashLabelKey] = hash
   188  
   189  	deployment = dep
   190  	return
   191  }
   192  
   193  func (i *StrategyDeploymentInstaller) Install(s Strategy) error {
   194  	strategy, ok := s.(*v1alpha1.StrategyDetailsDeployment)
   195  	if !ok {
   196  		return fmt.Errorf("attempted to install %s strategy with deployment installer", strategy.GetStrategyName())
   197  	}
   198  
   199  	// Install owned APIServices and update strategy with serving cert data
   200  	updatedStrategy, err := i.installCertRequirements(strategy)
   201  	if err != nil {
   202  		return err
   203  	}
   204  
   205  	if err := i.installDeployments(updatedStrategy.DeploymentSpecs); err != nil {
   206  		if apierrors.IsForbidden(err) {
   207  			return StrategyError{Reason: StrategyErrInsufficientPermissions, Message: fmt.Sprintf("install strategy failed: %s", err)}
   208  		}
   209  		return err
   210  	}
   211  
   212  	// Clean up orphaned deployments
   213  	return i.cleanupOrphanedDeployments(updatedStrategy.DeploymentSpecs)
   214  }
   215  
   216  // CheckInstalled can return nil (installed), or errors
   217  // Errors can indicate: some component missing (keep installing), unable to query (check again later), or unrecoverable (failed in a way we know we can't recover from)
   218  func (i *StrategyDeploymentInstaller) CheckInstalled(s Strategy) (installed bool, err error) {
   219  	strategy, ok := s.(*v1alpha1.StrategyDetailsDeployment)
   220  	if !ok {
   221  		return false, StrategyError{Reason: StrategyErrReasonInvalidStrategy, Message: fmt.Sprintf("attempted to check %s strategy with deployment installer", strategy.GetStrategyName())}
   222  	}
   223  
   224  	// Check deployments
   225  	if err := i.checkForDeployments(strategy.DeploymentSpecs); err != nil {
   226  		return false, err
   227  	}
   228  	return true, nil
   229  }
   230  
   231  func (i *StrategyDeploymentInstaller) checkForDeployments(deploymentSpecs []v1alpha1.StrategyDeploymentSpec) error {
   232  	// Check the owner is a CSV
   233  	csv, ok := i.owner.(*v1alpha1.ClusterServiceVersion)
   234  	if !ok {
   235  		return StrategyError{Reason: StrategyErrReasonComponentMissing, Message: fmt.Sprintf("owner %s is not a CSV", i.owner.GetName())}
   236  	}
   237  
   238  	existingDeployments, err := i.strategyClient.FindAnyDeploymentsMatchingLabels(ownerutil.CSVOwnerSelector(csv))
   239  	if err != nil {
   240  		return StrategyError{Reason: StrategyErrReasonComponentMissing, Message: fmt.Sprintf("error querying existing deployments for CSV %s: %s", csv.GetName(), err)}
   241  	}
   242  
   243  	// compare deployments to see if any need to be created/updated
   244  	existingMap := map[string]*appsv1.Deployment{}
   245  	for _, d := range existingDeployments {
   246  		existingMap[d.GetName()] = d.DeepCopy()
   247  	}
   248  	for _, spec := range deploymentSpecs {
   249  		dep, exists := existingMap[spec.Name]
   250  		if !exists {
   251  			log.Debugf("missing deployment with name=%s", spec.Name)
   252  			return StrategyError{Reason: StrategyErrReasonComponentMissing, Message: fmt.Sprintf("missing deployment with name=%s", spec.Name)}
   253  		}
   254  		reason, ready, err := DeploymentStatus(dep)
   255  		if err != nil {
   256  			log.Debugf("deployment %s not ready before timeout: %s", dep.Name, err.Error())
   257  			return StrategyError{Reason: StrategyErrReasonTimeout, Message: fmt.Sprintf("deployment %s not ready before timeout: %s", dep.Name, err.Error())}
   258  		}
   259  		if !ready {
   260  			return StrategyError{Reason: StrategyErrReasonWaiting, Message: fmt.Sprintf("waiting for deployment %s to become ready: %s", dep.Name, reason)}
   261  		}
   262  
   263  		// check annotations
   264  		if len(i.templateAnnotations) > 0 && dep.Spec.Template.Annotations == nil {
   265  			return StrategyError{Reason: StrategyErrReasonAnnotationsMissing, Message: "no annotations found on deployment"}
   266  		}
   267  		for key, value := range i.templateAnnotations {
   268  			if actualValue, ok := dep.Spec.Template.Annotations[key]; !ok {
   269  				return StrategyError{Reason: StrategyErrReasonAnnotationsMissing, Message: fmt.Sprintf("annotations on deployment does not contain expected key: %s", key)}
   270  			} else if dep.Spec.Template.Annotations[key] != value {
   271  				return StrategyError{Reason: StrategyErrReasonAnnotationsMissing, Message: fmt.Sprintf("unexpected annotation on deployment. Expected %s:%s, found %s:%s", key, value, key, actualValue)}
   272  			}
   273  		}
   274  
   275  		// check that the deployment spec hasn't changed since it was created
   276  		labels := dep.GetLabels()
   277  		if len(labels) == 0 {
   278  			return StrategyError{Reason: StrategyErrDeploymentUpdated, Message: fmt.Sprintf("deployment %s doesn't have a spec hash, update it", dep.Name)}
   279  		}
   280  		existingDeploymentSpecHash, ok := labels[DeploymentSpecHashLabelKey]
   281  		if !ok {
   282  			return StrategyError{Reason: StrategyErrDeploymentUpdated, Message: fmt.Sprintf("deployment %s doesn't have a spec hash, update it", dep.Name)}
   283  		}
   284  
   285  		_, calculatedDeploymentHash, err := i.deploymentForSpec(spec.Name, spec.Spec, labels)
   286  		if err != nil {
   287  			return StrategyError{Reason: StrategyErrDeploymentUpdated, Message: fmt.Sprintf("couldn't calculate deployment spec hash: %v", err)}
   288  		}
   289  
   290  		if existingDeploymentSpecHash != calculatedDeploymentHash {
   291  			return StrategyError{Reason: StrategyErrDeploymentUpdated, Message: fmt.Sprintf("deployment changed old hash=%s, new hash=%s", existingDeploymentSpecHash, calculatedDeploymentHash)}
   292  		}
   293  	}
   294  	return nil
   295  }
   296  
   297  // Clean up orphaned deployments after reinstalling deployments process
   298  func (i *StrategyDeploymentInstaller) cleanupOrphanedDeployments(deploymentSpecs []v1alpha1.StrategyDeploymentSpec) error {
   299  	// Map of deployments
   300  	depNames := map[string]string{}
   301  	for _, dep := range deploymentSpecs {
   302  		depNames[dep.Name] = dep.Name
   303  	}
   304  
   305  	// Check the owner is a CSV
   306  	csv, ok := i.owner.(*v1alpha1.ClusterServiceVersion)
   307  	if !ok {
   308  		return fmt.Errorf("owner %s is not a CSV", i.owner.GetName())
   309  	}
   310  
   311  	// Get existing deployments in CSV's namespace and owned by CSV
   312  	existingDeployments, err := i.strategyClient.FindAnyDeploymentsMatchingLabels(ownerutil.CSVOwnerSelector(csv))
   313  	if err != nil {
   314  		return err
   315  	}
   316  
   317  	// compare existing deployments to deployments in CSV's spec to see if any need to be deleted
   318  	for _, d := range existingDeployments {
   319  		if _, exists := depNames[d.GetName()]; !exists {
   320  			if ownerutil.IsOwnedBy(d, i.owner) {
   321  				log.Infof("found an orphaned deployment %s in namespace %s", d.GetName(), i.owner.GetNamespace())
   322  				if err := i.strategyClient.DeleteDeployment(d.GetName()); err != nil {
   323  					log.Warnf("error cleaning up deployment %s", d.GetName())
   324  					return err
   325  				}
   326  			}
   327  		}
   328  	}
   329  
   330  	return nil
   331  }