github.com/banzaicloud/operator-tools@v0.28.10/pkg/reconciler/native.go (about) 1 // Copyright © 2020 Banzai Cloud 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package reconciler 16 17 import ( 18 "context" 19 "strings" 20 21 "emperror.dev/errors" 22 "github.com/go-logr/logr" 23 corev1 "k8s.io/api/core/v1" 24 crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 25 crdv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 26 k8serrors "k8s.io/apimachinery/pkg/api/errors" 27 "k8s.io/apimachinery/pkg/api/meta" 28 apimeta "k8s.io/apimachinery/pkg/api/meta" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 31 "k8s.io/apimachinery/pkg/runtime" 32 "k8s.io/apimachinery/pkg/runtime/schema" 33 clientgoscheme "k8s.io/client-go/kubernetes/scheme" 34 "k8s.io/client-go/util/retry" 35 "sigs.k8s.io/controller-runtime/pkg/builder" 36 "sigs.k8s.io/controller-runtime/pkg/client" 37 "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 38 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 39 "sigs.k8s.io/controller-runtime/pkg/reconcile" 40 41 "github.com/banzaicloud/operator-tools/pkg/resources" 42 "github.com/banzaicloud/operator-tools/pkg/types" 43 "github.com/banzaicloud/operator-tools/pkg/utils" 44 "github.com/banzaicloud/operator-tools/pkg/wait" 45 ) 46 47 type ResourceOwner interface { 48 // to be aware of metadata 49 metav1.Object 50 // to be aware of the owner's type 51 runtime.Object 52 } 53 54 type ResourceOwnerWithControlNamespace interface { 55 ResourceOwner 56 // control namespace dictates where namespaced objects should belong to 57 GetControlNamespace() string 58 } 59 60 type ( 61 ResourceBuilders func(parent ResourceOwner, object interface{}) []ResourceBuilder 62 ResourceBuilder func() (runtime.Object, DesiredState, error) 63 ResourceTranslate func(runtime.Object) (parent ResourceOwner, config interface{}) 64 PurgeTypesFunc func() []schema.GroupVersionKind 65 ) 66 67 func GetResourceBuildersFromObjects(objects []runtime.Object, state DesiredState, modifierFuncs ...resources.ObjectModifierFunc) ([]ResourceBuilder, error) { 68 resources := []ResourceBuilder{} 69 70 utils.RuntimeObjects(objects).Sort(utils.InstallResourceOrder) 71 72 for _, o := range objects { 73 o := o 74 for _, modifierFunc := range modifierFuncs { 75 var err error 76 o, err = modifierFunc(o) 77 if err != nil { 78 return nil, err 79 } 80 } 81 resources = append(resources, func() (runtime.Object, DesiredState, error) { 82 ds := DynamicDesiredState{ 83 DesiredState: state, 84 } 85 ds.BeforeUpdateFunc = func(current, desired runtime.Object) error { 86 for _, f := range []func(current, desired runtime.Object) error{ 87 ServiceIPModifier, 88 KeepLabelsAndAnnotationsModifer, 89 KeepServiceAccountTokenReferences, 90 } { 91 err := f(current, desired) 92 if err != nil { 93 return err 94 } 95 } 96 return nil 97 } 98 99 return o, ds, nil 100 }) 101 } 102 103 return resources, nil 104 } 105 106 type NativeReconciledComponent interface { 107 ResourceBuilders(parent ResourceOwner, object interface{}) []ResourceBuilder 108 RegisterWatches(*builder.Builder) 109 PurgeTypes() []schema.GroupVersionKind 110 } 111 112 type DefaultReconciledComponent struct { 113 builders ResourceBuilders 114 watches func(b *builder.Builder) 115 purgeTypes func() []schema.GroupVersionKind 116 } 117 118 func NewReconciledComponent(b ResourceBuilders, w func(b *builder.Builder), p func() []schema.GroupVersionKind) NativeReconciledComponent { 119 if p == nil { 120 p = func() []schema.GroupVersionKind { 121 return nil 122 } 123 } 124 if w == nil { 125 w = func(*builder.Builder) {} 126 } 127 return &DefaultReconciledComponent{ 128 builders: b, 129 watches: w, 130 purgeTypes: p, 131 } 132 } 133 134 func (d *DefaultReconciledComponent) ResourceBuilders(parent ResourceOwner, object interface{}) []ResourceBuilder { 135 return d.builders(parent, object) 136 } 137 138 func (d *DefaultReconciledComponent) RegisterWatches(b *builder.Builder) { 139 if d.watches != nil { 140 d.watches(b) 141 } 142 } 143 144 func (d *DefaultReconciledComponent) PurgeTypes() []schema.GroupVersionKind { 145 return d.purgeTypes() 146 } 147 148 type NativeReconciler struct { 149 *GenericResourceReconciler 150 client.Client 151 scheme *runtime.Scheme 152 restMapper meta.RESTMapper 153 reconciledComponent NativeReconciledComponent 154 configTranslate ResourceTranslate 155 componentName string 156 setControllerRef bool 157 reconciledObjectStates map[reconciledObjectState][]runtime.Object 158 waitBackoff *wait.Backoff 159 retryBackoff wait.Backoff 160 retriableErrorFunc func(error) bool 161 objectModifiers []resources.ObjectModifierWithParentFunc 162 } 163 164 type NativeReconcilerOpt func(*NativeReconciler) 165 166 func NativeReconcilerWithScheme(scheme *runtime.Scheme) NativeReconcilerOpt { 167 return func(r *NativeReconciler) { 168 r.scheme = scheme 169 } 170 } 171 172 func NativeReconcilerSetControllerRef() NativeReconcilerOpt { 173 return func(r *NativeReconciler) { 174 r.setControllerRef = true 175 } 176 } 177 178 func NativeReconcilerSetRESTMapper(mapper meta.RESTMapper) NativeReconcilerOpt { 179 return func(r *NativeReconciler) { 180 r.restMapper = mapper 181 } 182 } 183 184 func NativeReconcilerWithWait(backoff *wait.Backoff) NativeReconcilerOpt { 185 return func(r *NativeReconciler) { 186 r.waitBackoff = backoff 187 } 188 } 189 190 func NativeReconcilerWithModifier(modifierFunc resources.ObjectModifierWithParentFunc) NativeReconcilerOpt { 191 return func(r *NativeReconciler) { 192 r.objectModifiers = append(r.objectModifiers, modifierFunc) 193 } 194 } 195 196 func NativeReconcilerWithRetryBackoff(backoff wait.Backoff) NativeReconcilerOpt { 197 return func(r *NativeReconciler) { 198 r.retryBackoff = backoff 199 } 200 } 201 202 func NativeReconcilerWithRetriableErrorFunc(retriableErrorFunc func(error) bool) NativeReconcilerOpt { 203 return func(r *NativeReconciler) { 204 r.retriableErrorFunc = retriableErrorFunc 205 } 206 } 207 208 func NewNativeReconcilerWithDefaults( 209 component string, 210 client client.Client, 211 scheme *runtime.Scheme, 212 logger logr.Logger, 213 resourceBuilders ResourceBuilders, 214 purgeTypes PurgeTypesFunc, 215 resourceTranslate ResourceTranslate, 216 opts ...NativeReconcilerOpt, 217 ) *NativeReconciler { 218 reconcilerOpts := &ReconcilerOpts{ 219 EnableRecreateWorkloadOnImmutableFieldChange: true, 220 Scheme: scheme, 221 } 222 223 return NewNativeReconciler( 224 component, 225 NewGenericReconciler( 226 client, 227 logger, 228 *reconcilerOpts, 229 ), 230 client, 231 NewReconciledComponent( 232 resourceBuilders, 233 nil, 234 purgeTypes, 235 ), 236 resourceTranslate, 237 opts..., 238 ) 239 } 240 241 func NewNativeReconciler( 242 componentName string, 243 rec *GenericResourceReconciler, 244 client client.Client, 245 reconciledComponent NativeReconciledComponent, 246 resourceTranslate func(runtime.Object) (parent ResourceOwner, config interface{}), 247 opts ...NativeReconcilerOpt) *NativeReconciler { 248 reconciler := &NativeReconciler{ 249 GenericResourceReconciler: rec, 250 Client: client, 251 reconciledComponent: reconciledComponent, 252 configTranslate: resourceTranslate, 253 componentName: componentName, 254 255 // do not retry on errors by default 256 retriableErrorFunc: func(error) bool { return false }, 257 retryBackoff: retry.DefaultRetry, 258 } 259 260 reconciler.initReconciledObjectStates() 261 262 for _, opt := range opts { 263 opt(reconciler) 264 } 265 266 if reconciler.scheme == nil { 267 reconciler.scheme = runtime.NewScheme() 268 _ = clientgoscheme.AddToScheme(reconciler.scheme) 269 } 270 271 return reconciler 272 } 273 274 func (rec *NativeReconciler) Reconcile(owner runtime.Object) (*reconcile.Result, error) { 275 if rec.componentName == "" { 276 return nil, errors.New("component name cannot be empty") 277 } 278 279 componentID, ownerMeta, err := rec.generateComponentID(owner) 280 if err != nil { 281 return nil, err 282 } 283 // visited objects wont be purged 284 excludeFromPurge := map[string]bool{} 285 combinedResult := &CombinedResult{} 286 LOOP: 287 for _, r := range rec.reconciledComponent.ResourceBuilders(rec.configTranslate(owner)) { 288 o, state, err := r() 289 if err != nil { 290 combinedResult.CombineErr(err) 291 } else if o == nil || state == nil { 292 rec.Log.Info("skipping resource builder reconciliation due to object or desired state was nil") 293 continue 294 } else { 295 var objectMeta metav1.Object 296 objectMeta, err = rec.addComponentIDAnnotation(o, componentID) 297 if err != nil { 298 combinedResult.CombineErr(err) 299 continue 300 } 301 rec.addRelatedToAnnotation(objectMeta, ownerMeta) 302 if rec.setControllerRef { 303 skipControllerRef := false 304 switch o.(type) { 305 case *crdv1.CustomResourceDefinition: 306 skipControllerRef = true 307 case *crdv1beta1.CustomResourceDefinition: 308 skipControllerRef = true 309 case *corev1.Namespace: 310 skipControllerRef = true 311 } 312 if !skipControllerRef { 313 // namespaced resource can only own resources in the same namespace 314 if ownerMeta.GetNamespace() == "" || ownerMeta.GetNamespace() == objectMeta.GetNamespace() { 315 if err := controllerutil.SetControllerReference(ownerMeta, objectMeta, rec.scheme); err != nil { 316 combinedResult.CombineErr(err) 317 continue 318 } 319 } 320 } 321 } 322 323 if len(rec.objectModifiers) > 0 { 324 for _, om := range rec.objectModifiers { 325 o, err = om(o, owner) 326 if err != nil { 327 combinedResult.CombineErr(errors.WrapIf(err, "unable to apply object modifier")) 328 continue LOOP 329 } 330 } 331 } 332 333 // desired state can be overriden to create-only by an annotation 334 if _, ok := objectMeta.GetAnnotations()[types.BanzaiCloudDesiredStateCreated]; ok { 335 if ds, ok := state.(DynamicDesiredState); ok && ds.DesiredState == StatePresent || state == StatePresent { 336 state = StateCreated 337 } 338 } 339 340 var result *reconcile.Result 341 err = retry.OnError(rec.retryBackoff, rec.retriableErrorFunc, func() error { 342 var err error 343 result, err = rec.ReconcileResource(o, state) 344 return err 345 }) 346 if err == nil { 347 resourceID, err := rec.generateResourceIDForPurge(o) 348 if err != nil { 349 combinedResult.CombineErr(err) 350 continue 351 } 352 excludeFromPurge[resourceID] = true 353 354 s := ReconciledObjectStatePresent 355 if state == StateAbsent { 356 s = ReconciledObjectStateAbsent 357 } 358 rec.addReconciledObjectState(s, o.DeepCopyObject()) 359 } 360 combinedResult.Combine(result, err) 361 } 362 } 363 if combinedResult.Err == nil { 364 if err := rec.purge(excludeFromPurge, componentID); err != nil { 365 combinedResult.CombineErr(err) 366 } 367 } else { 368 rec.Log.Error(combinedResult.Err, "skip purging results due to previous errors") 369 } 370 if rec.waitBackoff != nil { 371 if err := rec.waitForResources(*rec.waitBackoff); err != nil { 372 combinedResult.CombineErr(err) 373 } 374 } 375 return &combinedResult.Result, combinedResult.Err 376 } 377 378 func (rec *NativeReconciler) generateComponentID(owner runtime.Object) (string, metav1.Object, error) { 379 ownerMeta, err := meta.Accessor(owner) 380 if err != nil { 381 return "", nil, errors.WrapIf(err, "failed to access owner object meta") 382 } 383 384 // generated componentId will be used to purge unwanted objects 385 identifiers := []string{} 386 if ownerMeta.GetName() == "" { 387 return "", nil, errors.New("unable to generate component id for resource without a name") 388 } 389 identifiers = append(identifiers, ownerMeta.GetName()) 390 391 if ownerMeta.GetNamespace() != "" { 392 identifiers = append(identifiers, ownerMeta.GetNamespace()) 393 } 394 395 if rec.componentName == "" { 396 return "", nil, errors.New("unable to generate component id without a component name") 397 } 398 identifiers = append(identifiers, rec.componentName) 399 400 gvk, err := apiutil.GVKForObject(owner, rec.scheme) 401 if err != nil { 402 return "", nil, errors.WrapIf(err, "") 403 } 404 apiVersion, kind := gvk.ToAPIVersionAndKind() 405 identifiers = append(identifiers, apiVersion, strings.ToLower(kind)) 406 407 return strings.Join(identifiers, "-"), ownerMeta, nil 408 } 409 410 func (rec *NativeReconciler) generateResourceIDForPurge(resource runtime.Object) (string, error) { 411 resourceMeta, err := meta.Accessor(resource) 412 if err != nil { 413 return "", errors.WrapIf(err, "failed to access owner object meta") 414 } 415 416 // generated componentId will be used to purge unwanted objects 417 identifiers := []string{} 418 if resourceMeta.GetName() == "" { 419 return "", errors.New("unable to generate component id for resource without a name") 420 } 421 identifiers = append(identifiers, resourceMeta.GetName()) 422 423 if resourceMeta.GetNamespace() != "" { 424 identifiers = append(identifiers, resourceMeta.GetNamespace()) 425 } 426 427 gvk, err := apiutil.GVKForObject(resource, rec.scheme) 428 if err != nil { 429 return "", errors.WrapIf(err, "") 430 } 431 identifiers = append(identifiers, strings.ToLower(gvk.GroupKind().String())) 432 433 return strings.Join(identifiers, "-"), nil 434 } 435 436 func (rec *NativeReconciler) gvkExists(gvk schema.GroupVersionKind) bool { 437 if rec.restMapper == nil { 438 return true 439 } 440 441 mappings, err := rec.restMapper.RESTMappings(gvk.GroupKind(), gvk.Version) 442 if apimeta.IsNoMatchError(err) { 443 return false 444 } 445 if err != nil { 446 return true 447 } 448 449 for _, m := range mappings { 450 if gvk == m.GroupVersionKind { 451 return true 452 } 453 } 454 455 return false 456 } 457 458 func (rec *NativeReconciler) purge(excluded map[string]bool, componentId string) error { 459 var allErr error 460 var purgeObjects []runtime.Object 461 for _, gvk := range rec.reconciledComponent.PurgeTypes() { 462 rec.Log.V(2).Info("purging GVK", "gvk", gvk) 463 if !rec.gvkExists(gvk) { 464 continue 465 } 466 objects := &unstructured.UnstructuredList{} 467 objects.SetGroupVersionKind(gvk) 468 err := rec.List(context.TODO(), objects) 469 if apimeta.IsNoMatchError(err) { 470 // skip unknown GVKs 471 continue 472 } 473 if err != nil { 474 rec.Log.Error(err, "failed list objects to prune", 475 "groupversion", gvk.GroupVersion().String(), 476 "kind", gvk.Kind) 477 continue 478 } 479 for _, o := range objects.Items { 480 objectMeta, err := meta.Accessor(&o) 481 if err != nil { 482 allErr = errors.Combine(allErr, errors.WrapIf(err, "failed to get object metadata")) 483 continue 484 } 485 resourceID, err := rec.generateResourceIDForPurge(&o) 486 if err != nil { 487 allErr = errors.Combine(allErr, err) 488 continue 489 } 490 if excluded[resourceID] { 491 continue 492 } 493 if objectMeta.GetAnnotations()[types.BanzaiCloudManagedComponent] == componentId { 494 rec.Log.Info("will prune unmmanaged resource", 495 "name", objectMeta.GetName(), 496 "namespace", objectMeta.GetNamespace(), 497 "group", gvk.Group, 498 "version", gvk.Version, 499 "listKind", gvk.Kind) 500 purgeObjects = append(purgeObjects, o.DeepCopyObject()) 501 } 502 } 503 } 504 505 utils.RuntimeObjects(purgeObjects).Sort(utils.UninstallResourceOrder) 506 for _, o := range purgeObjects { 507 if err := rec.Client.Delete(context.TODO(), o.(client.Object)); err != nil && !k8serrors.IsNotFound(err) { 508 allErr = errors.Combine(allErr, err) 509 } else { 510 rec.addReconciledObjectState(ReconciledObjectStatePurged, o.DeepCopyObject()) 511 } 512 } 513 return allErr 514 } 515 516 type reconciledObjectState string 517 518 const ( 519 ReconciledObjectStateAbsent reconciledObjectState = "Absent" 520 ReconciledObjectStatePresent reconciledObjectState = "Present" 521 ReconciledObjectStatePurged reconciledObjectState = "Purged" 522 ) 523 524 func (rec *NativeReconciler) initReconciledObjectStates() { 525 rec.reconciledObjectStates = make(map[reconciledObjectState][]runtime.Object) 526 } 527 528 func (rec *NativeReconciler) addReconciledObjectState(state reconciledObjectState, o runtime.Object) { 529 rec.reconciledObjectStates[state] = append(rec.reconciledObjectStates[state], o) 530 } 531 532 func (rec *NativeReconciler) GetReconciledObjectWithState(state reconciledObjectState) []runtime.Object { 533 return rec.reconciledObjectStates[state] 534 } 535 536 func (rec *NativeReconciler) addRelatedToAnnotation(objectMeta, ownerMeta metav1.Object) { 537 annotations := objectMeta.GetAnnotations() 538 if annotations == nil { 539 annotations = make(map[string]string) 540 } 541 annotations[types.BanzaiCloudRelatedTo] = utils.ObjectKeyFromObjectMeta(ownerMeta).String() 542 objectMeta.SetAnnotations(annotations) 543 } 544 545 func (rec *NativeReconciler) addComponentIDAnnotation(o runtime.Object, componentId string) (metav1.Object, error) { 546 objectMeta, err := meta.Accessor(o) 547 if err != nil { 548 return nil, errors.Wrapf(err, "failed to access object metadata") 549 } 550 annotations := objectMeta.GetAnnotations() 551 if annotations == nil { 552 annotations = make(map[string]string) 553 } 554 if currentComponentId, ok := annotations[types.BanzaiCloudManagedComponent]; ok { 555 if currentComponentId != componentId { 556 return nil, errors.Errorf( 557 "object actual component id `%s` is different from the one defined by the component `%s`", 558 currentComponentId, componentId) 559 } 560 } else { 561 annotations[types.BanzaiCloudManagedComponent] = componentId 562 objectMeta.SetAnnotations(annotations) 563 } 564 return objectMeta, nil 565 } 566 567 func (rec *NativeReconciler) RegisterWatches(b *builder.Builder) { 568 rec.reconciledComponent.RegisterWatches(b) 569 } 570 571 func (rec *NativeReconciler) waitForResources(backoff wait.Backoff) error { 572 rcc := wait.NewResourceConditionChecks(rec.Client, backoff, rec.Log, rec.scheme) 573 574 presentObjects := rec.GetReconciledObjectWithState(ReconciledObjectStatePresent) 575 576 err := rcc.WaitForResources("readiness", presentObjects, wait.ExistsConditionCheck, wait.ReadyReplicasConditionCheck) 577 if err != nil { 578 return err 579 } 580 581 absentObjects := append(rec.GetReconciledObjectWithState(ReconciledObjectStateAbsent), rec.GetReconciledObjectWithState(ReconciledObjectStatePurged)...) 582 err = rcc.WaitForResources("removal", absentObjects, wait.NonExistsConditionCheck) 583 if err != nil { 584 return err 585 } 586 587 return nil 588 }