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 }