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  }