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 }