github.com/banzaicloud/operator-tools@v0.28.10/pkg/helm/templatereconciler/reconciler.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 templatereconciler
    16  
    17  import (
    18  	"context"
    19  	"net/http"
    20  	"time"
    21  
    22  	"emperror.dev/errors"
    23  	"github.com/go-logr/logr"
    24  	"helm.sh/helm/v3/pkg/action"
    25  	"helm.sh/helm/v3/pkg/chartutil"
    26  	v1 "k8s.io/api/core/v1"
    27  	"k8s.io/apimachinery/pkg/api/meta"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	"k8s.io/client-go/discovery"
    31  	controllerruntime "sigs.k8s.io/controller-runtime"
    32  	"sigs.k8s.io/controller-runtime/pkg/client"
    33  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    34  
    35  	"github.com/banzaicloud/operator-tools/pkg/inventory"
    36  	"github.com/banzaicloud/operator-tools/pkg/logger"
    37  	"github.com/banzaicloud/operator-tools/pkg/reconciler"
    38  	"github.com/banzaicloud/operator-tools/pkg/resources"
    39  	"github.com/banzaicloud/operator-tools/pkg/types"
    40  )
    41  
    42  type ReleaseData struct {
    43  	Chart       http.FileSystem
    44  	Values      map[string]interface{}
    45  	Namespace   string
    46  	ChartName   string
    47  	ReleaseName string
    48  	// Layers can be embedded into CRDs directly to provide flexible override mechanisms
    49  	Layers []resources.K8SResourceOverlay
    50  	// Modifiers can be used from client code to modify resources before being applied
    51  	Modifiers []resources.ObjectModifierFunc
    52  	// DesiredStateOverrides can be used to override desired states of certain objects
    53  	DesiredStateOverrides map[reconciler.ObjectKeyWithGVK]reconciler.DesiredState
    54  }
    55  
    56  type Component interface {
    57  	Name() string
    58  	Skipped(runtime.Object) bool
    59  	Enabled(runtime.Object) bool
    60  	PreChecks(runtime.Object) error
    61  	ReleaseData(runtime.Object) (*ReleaseData, error)
    62  	UpdateStatus(object runtime.Object, status types.ReconcileStatus, message string) error
    63  }
    64  
    65  type HelmReconciler struct {
    66  	client                client.Client
    67  	scheme                *runtime.Scheme
    68  	logger                logr.Logger
    69  	inventory             *inventory.Inventory
    70  	nativeReconcilerOpts  []reconciler.NativeReconcilerOpt
    71  	genericReconcilerOpts []reconciler.ResourceReconcilerOption
    72  	objectParser          *resources.ObjectParser
    73  	discovery             discovery.DiscoveryInterface
    74  	manageNamespace       bool
    75  }
    76  
    77  type preConditionsFatalErr struct {
    78  	error
    79  }
    80  
    81  func NewPreConditionsFatalErr(err error) error {
    82  	return &preConditionsFatalErr{err}
    83  }
    84  
    85  type HelmReconcilerOpt func(*HelmReconciler)
    86  
    87  func WithGenericReconcilerOptions(opts ...reconciler.ResourceReconcilerOption) HelmReconcilerOpt {
    88  	return func(r *HelmReconciler) {
    89  		if r.genericReconcilerOpts == nil {
    90  			r.genericReconcilerOpts = make([]reconciler.ResourceReconcilerOption, 0)
    91  		}
    92  		r.genericReconcilerOpts = append(r.genericReconcilerOpts, opts...)
    93  	}
    94  }
    95  
    96  func WithNativeReconcilerOptions(opts ...reconciler.NativeReconcilerOpt) HelmReconcilerOpt {
    97  	return func(r *HelmReconciler) {
    98  		if r.nativeReconcilerOpts == nil {
    99  			r.nativeReconcilerOpts = make([]reconciler.NativeReconcilerOpt, 0)
   100  		}
   101  		r.nativeReconcilerOpts = append(r.nativeReconcilerOpts, opts...)
   102  	}
   103  }
   104  
   105  func ManageNamespace(manageNamespace bool) HelmReconcilerOpt {
   106  	return func(r *HelmReconciler) {
   107  		r.manageNamespace = manageNamespace
   108  	}
   109  }
   110  
   111  func NewHelmReconciler(
   112  	client client.Client,
   113  	scheme *runtime.Scheme,
   114  	logger logr.Logger,
   115  	discovery discovery.DiscoveryInterface,
   116  	nativeReconcilerOpts []reconciler.NativeReconcilerOpt,
   117  ) *HelmReconciler {
   118  	return NewHelmReconcilerWith(client, scheme, logger, discovery, WithNativeReconcilerOptions(nativeReconcilerOpts...))
   119  }
   120  
   121  func NewHelmReconcilerWith(
   122  	client client.Client,
   123  	scheme *runtime.Scheme,
   124  	logger logr.Logger,
   125  	discovery discovery.DiscoveryInterface,
   126  	opts ...HelmReconcilerOpt,
   127  ) *HelmReconciler {
   128  	r := &HelmReconciler{
   129  		client:                client,
   130  		scheme:                scheme,
   131  		logger:                logger,
   132  		inventory:             inventory.NewDiscoveryInventory(client, logger, discovery),
   133  		discovery:             discovery,
   134  		objectParser:          resources.NewObjectParser(scheme),
   135  		nativeReconcilerOpts:  make([]reconciler.NativeReconcilerOpt, 0),
   136  		genericReconcilerOpts: make([]reconciler.ResourceReconcilerOption, 0),
   137  		manageNamespace:       true,
   138  	}
   139  
   140  	for _, opt := range opts {
   141  		opt(r)
   142  	}
   143  
   144  	if len(r.genericReconcilerOpts) == 0 {
   145  		r.genericReconcilerOpts = append(r.genericReconcilerOpts, reconciler.WithEnableRecreateWorkload())
   146  	}
   147  
   148  	return r
   149  }
   150  
   151  func (rec *HelmReconciler) GetClient() client.Client {
   152  	return rec.client
   153  }
   154  
   155  func (rec *HelmReconciler) Reconcile(object runtime.Object, component Component) (*reconcile.Result, error) {
   156  	var ok bool
   157  	var parent reconciler.ResourceOwner
   158  	if parent, ok = object.(reconciler.ResourceOwner); !ok {
   159  		return nil, errors.New("cannot convert object to ResourceOwner interface")
   160  	}
   161  
   162  	if component.Skipped(object) {
   163  		return &reconcile.Result{}, component.UpdateStatus(object, types.ReconcileStatusUnmanaged, "")
   164  	}
   165  
   166  	if err := component.UpdateStatus(object, types.ReconcileStatusReconciling, ""); err != nil {
   167  		rec.logger.Error(err, "status update failed")
   168  	}
   169  	rec.logger.Info("reconciling")
   170  
   171  	if component.Enabled(object) {
   172  		if err := component.PreChecks(object); err != nil {
   173  			if preCondErr, ok := err.(*preConditionsFatalErr); ok {
   174  				if err := component.UpdateStatus(object, types.ReconcileStatusFailed, preCondErr.Error()); err != nil {
   175  					rec.logger.Error(err, "status update failed")
   176  				}
   177  
   178  				return nil, preCondErr
   179  			}
   180  			if err := component.UpdateStatus(object, types.ReconcileStatusReconciling, "waiting for precondition checks to pass"); err != nil {
   181  				rec.logger.Error(err, "status update failed")
   182  			}
   183  			rec.logger.Error(err, "precondition checks failed")
   184  			return &reconcile.Result{
   185  				RequeueAfter: time.Second * 5,
   186  			}, nil
   187  
   188  		}
   189  	}
   190  
   191  	defer logger.EnableGroupSession(rec.logger)()
   192  
   193  	rec.logger.Info("syncing resources")
   194  
   195  	releaseData, err := component.ReleaseData(object)
   196  	if err != nil {
   197  		return nil, errors.WrapIf(err, "failed to get release data")
   198  	}
   199  
   200  	result, err := rec.reconcile(parent, component, releaseData)
   201  	if err != nil {
   202  		uerr := component.UpdateStatus(object, types.ReconcileStatusFailed, err.Error())
   203  		if uerr != nil {
   204  			rec.logger.Error(uerr, "status update failed")
   205  		}
   206  		return result, err
   207  	} else {
   208  		if component.Skipped(object) {
   209  			err = component.UpdateStatus(object, types.ReconcileStatusUnmanaged, "")
   210  			if err != nil {
   211  				return result, err
   212  			}
   213  		} else if component.Enabled(object) {
   214  			err = component.UpdateStatus(object, types.ReconcileStatusAvailable, "")
   215  			if err != nil {
   216  				return result, err
   217  			}
   218  		} else {
   219  			err = component.UpdateStatus(object, types.ReconcileStatusRemoved, "")
   220  			if err != nil {
   221  				return result, err
   222  			}
   223  		}
   224  	}
   225  
   226  	return result, err
   227  }
   228  
   229  func (rec *HelmReconciler) GetResourceBuilders(parent reconciler.ResourceOwner, component Component, releaseData *ReleaseData, doInventory bool) ([]reconciler.ResourceBuilder, error) {
   230  	var err error
   231  	resourceBuilders := make([]reconciler.ResourceBuilder, 0)
   232  
   233  	if rec.manageNamespace {
   234  		resourceBuilders, err = reconciler.GetResourceBuildersFromObjects([]runtime.Object{
   235  			&v1.Namespace{
   236  				TypeMeta: metav1.TypeMeta{
   237  					Kind:       "Namespace",
   238  					APIVersion: "v1",
   239  				},
   240  				ObjectMeta: metav1.ObjectMeta{
   241  					Name: releaseData.Namespace,
   242  				},
   243  			},
   244  		}, reconciler.StateCreated)
   245  		if err != nil {
   246  			return nil, err
   247  		}
   248  	}
   249  
   250  	if component.Enabled(parent) {
   251  		serverVersion, err := rec.discovery.ServerVersion()
   252  		if err != nil {
   253  			return nil, errors.Wrapf(err, "unable to detect server version")
   254  		}
   255  
   256  		apiVersions, err := action.GetVersionSet(rec.discovery)
   257  		if err != nil {
   258  			return nil, errors.Wrapf(err, "unable to detect supported API versions")
   259  		}
   260  
   261  		capabilities := chartutil.Capabilities{
   262  			KubeVersion: chartutil.KubeVersion{
   263  				Version: serverVersion.GitVersion,
   264  				Major:   serverVersion.Major,
   265  				Minor:   serverVersion.Minor,
   266  			},
   267  			APIVersions: apiVersions,
   268  		}
   269  
   270  		objects, state, err := orderedChartObjectsWithState(releaseData, rec.scheme, capabilities)
   271  		if err != nil {
   272  			return nil, err
   273  		}
   274  
   275  		modifiers := releaseData.Modifiers
   276  
   277  		for _, layer := range releaseData.Layers {
   278  			modifier, err := resources.PatchYAMLModifier(layer, rec.objectParser)
   279  			if err != nil {
   280  				return nil, errors.WrapIf(err, "failed to create modifier from layer")
   281  			}
   282  			modifiers = append(modifiers, modifier)
   283  		}
   284  
   285  		chartResourceBuilders, err := reconciler.GetResourceBuildersFromObjects(objects, state, modifiers...)
   286  		if err != nil {
   287  			return nil, err
   288  		}
   289  
   290  		resourceBuilders = append(resourceBuilders, chartResourceBuilders...)
   291  		if doInventory {
   292  			if resourceBuilders, err = rec.inventory.Append(releaseData.Namespace, releaseData.ReleaseName, parent, resourceBuilders); err != nil {
   293  				return nil, err
   294  			}
   295  		}
   296  	} else if doInventory {
   297  		if resourceBuilders, err = rec.inventory.Append(releaseData.Namespace, releaseData.ReleaseName, parent, resourceBuilders); err != nil {
   298  			return nil, err
   299  		}
   300  	}
   301  
   302  	return rec.setDesiredStateOverrides(resourceBuilders, releaseData), nil
   303  }
   304  
   305  func (rec *HelmReconciler) reconcile(parent reconciler.ResourceOwner, component Component, releaseData *ReleaseData) (*reconcile.Result, error) {
   306  	resourceBuilders, err := rec.GetResourceBuilders(parent, component, releaseData, true)
   307  	if err != nil {
   308  		return nil, err
   309  	}
   310  
   311  	r := reconciler.NewNativeReconciler(
   312  		component.Name(),
   313  		reconciler.NewReconcilerWith(
   314  			rec.client,
   315  			append(rec.genericReconcilerOpts, reconciler.WithLog(rec.logger), reconciler.WithScheme(rec.scheme))...,
   316  		).(*reconciler.GenericResourceReconciler),
   317  		rec.client,
   318  		reconciler.NewReconciledComponent(
   319  			func(_ reconciler.ResourceOwner, _ interface{}) []reconciler.ResourceBuilder {
   320  				return resourceBuilders
   321  			},
   322  			nil,
   323  			rec.inventory.TypesToPurge,
   324  		),
   325  		func(_ runtime.Object) (reconciler.ResourceOwner, interface{}) {
   326  			return nil, nil
   327  		},
   328  		append(rec.nativeReconcilerOpts, reconciler.NativeReconcilerWithScheme(rec.scheme))...,
   329  	)
   330  
   331  	result, err := r.Reconcile(parent)
   332  	if err != nil {
   333  		return result, err
   334  	}
   335  
   336  	if !component.Enabled(parent) {
   337  		// cleanup orphaned pods left from removed jobs
   338  		if err := rec.client.DeleteAllOf(context.TODO(), &v1.Pod{},
   339  			client.MatchingLabels{"release": releaseData.ReleaseName},
   340  			client.HasLabels{"job-name"},
   341  			client.InNamespace(releaseData.Namespace),
   342  		); err != nil {
   343  			return result, errors.WrapIf(err, "failed to remove pods left from the release")
   344  		}
   345  	}
   346  
   347  	rec.logger.Info("reconciled")
   348  
   349  	return result, nil
   350  }
   351  
   352  func (rec *HelmReconciler) setDesiredStateOverrides(resourceBuilders []reconciler.ResourceBuilder, releaseData *ReleaseData) []reconciler.ResourceBuilder {
   353  	resources := []reconciler.ResourceBuilder{}
   354  
   355  	for _, rb := range resourceBuilders {
   356  		rb := rb
   357  		resources = append(resources, func() (runtime.Object, reconciler.DesiredState, error) {
   358  			o, state, err := rb()
   359  			if err != nil {
   360  				return nil, nil, err
   361  			}
   362  
   363  			om, err := meta.Accessor(o)
   364  			if err != nil {
   365  				return nil, nil, err
   366  			}
   367  
   368  			var overrideState reconciler.DesiredState
   369  
   370  			gvk := o.GetObjectKind().GroupVersionKind()
   371  
   372  			if ds, ok := releaseData.DesiredStateOverrides[reconciler.ObjectKeyWithGVK{
   373  				GVK: gvk,
   374  			}]; ok {
   375  				rec.logger.V(2).Info("override object desired state by gvk", "gvk", gvk.String(), "namespace", om.GetNamespace(), "name", om.GetName())
   376  				overrideState = ds
   377  			}
   378  
   379  			if ds, ok := releaseData.DesiredStateOverrides[reconciler.ObjectKeyWithGVK{
   380  				ObjectKey: client.ObjectKey{
   381  					Name:      om.GetName(),
   382  					Namespace: om.GetNamespace(),
   383  				},
   384  				GVK: gvk,
   385  			}]; ok {
   386  				rec.logger.V(2).Info("override object desired state by gvk and object key", "gvk", gvk.String(), "namespace", om.GetNamespace(), "name", om.GetName())
   387  				overrideState = ds
   388  			}
   389  
   390  			if overrideState != nil {
   391  				state = reconciler.MultipleDesiredStates{
   392  					state, overrideState,
   393  				}
   394  			}
   395  
   396  			return o, state, nil
   397  		})
   398  	}
   399  
   400  	return resources
   401  }
   402  
   403  func (rec HelmReconciler) RegisterWatches(_ *controllerruntime.Builder) {}