github.xiaoq7.com/operator-framework/operator-sdk@v0.8.2/pkg/helm/controller/reconcile.go (about)

     1  // Copyright 2018 The Operator-SDK Authors
     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 controller
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"time"
    21  
    22  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    23  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    24  	"k8s.io/apimachinery/pkg/runtime/schema"
    25  	rpb "k8s.io/helm/pkg/proto/hapi/release"
    26  	"sigs.k8s.io/controller-runtime/pkg/client"
    27  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    28  
    29  	"github.com/operator-framework/operator-sdk/internal/util/diffutil"
    30  	"github.com/operator-framework/operator-sdk/pkg/helm/internal/types"
    31  	"github.com/operator-framework/operator-sdk/pkg/helm/release"
    32  )
    33  
    34  // blank assignment to verify that HelmOperatorReconciler implements reconcile.Reconciler
    35  var _ reconcile.Reconciler = &HelmOperatorReconciler{}
    36  
    37  // ReleaseHookFunc defines a function signature for release hooks.
    38  type ReleaseHookFunc func(*rpb.Release) error
    39  
    40  // HelmOperatorReconciler reconciles custom resources as Helm releases.
    41  type HelmOperatorReconciler struct {
    42  	Client          client.Client
    43  	GVK             schema.GroupVersionKind
    44  	ManagerFactory  release.ManagerFactory
    45  	ReconcilePeriod time.Duration
    46  	releaseHook     ReleaseHookFunc
    47  }
    48  
    49  const (
    50  	finalizer = "uninstall-helm-release"
    51  )
    52  
    53  // Reconcile reconciles the requested resource by installing, updating, or
    54  // uninstalling a Helm release based on the resource's current state. If no
    55  // release changes are necessary, Reconcile will create or patch the underlying
    56  // resources to match the expected release manifest.
    57  func (r HelmOperatorReconciler) Reconcile(request reconcile.Request) (reconcile.Result, error) {
    58  	o := &unstructured.Unstructured{}
    59  	o.SetGroupVersionKind(r.GVK)
    60  	o.SetNamespace(request.Namespace)
    61  	o.SetName(request.Name)
    62  	log := log.WithValues(
    63  		"namespace", o.GetNamespace(),
    64  		"name", o.GetName(),
    65  		"apiVersion", o.GetAPIVersion(),
    66  		"kind", o.GetKind(),
    67  	)
    68  	log.V(1).Info("Reconciling")
    69  
    70  	err := r.Client.Get(context.TODO(), request.NamespacedName, o)
    71  	if apierrors.IsNotFound(err) {
    72  		return reconcile.Result{}, nil
    73  	}
    74  	if err != nil {
    75  		log.Error(err, "Failed to lookup resource")
    76  		return reconcile.Result{}, err
    77  	}
    78  
    79  	manager, err := r.ManagerFactory.NewManager(o)
    80  	if err != nil {
    81  		log.Error(err, "Failed to get release manager")
    82  		return reconcile.Result{}, err
    83  	}
    84  
    85  	status := types.StatusFor(o)
    86  	log = log.WithValues("release", manager.ReleaseName())
    87  
    88  	deleted := o.GetDeletionTimestamp() != nil
    89  	pendingFinalizers := o.GetFinalizers()
    90  	if !deleted && !contains(pendingFinalizers, finalizer) {
    91  		log.V(1).Info("Adding finalizer", "finalizer", finalizer)
    92  		finalizers := append(pendingFinalizers, finalizer)
    93  		o.SetFinalizers(finalizers)
    94  		err = r.updateResource(o)
    95  
    96  		// Need to requeue because finalizer update does not change metadata.generation
    97  		return reconcile.Result{Requeue: true}, err
    98  	}
    99  
   100  	status.SetCondition(types.HelmAppCondition{
   101  		Type:   types.ConditionInitialized,
   102  		Status: types.StatusTrue,
   103  	})
   104  
   105  	if err := manager.Sync(context.TODO()); err != nil {
   106  		log.Error(err, "Failed to sync release")
   107  		status.SetCondition(types.HelmAppCondition{
   108  			Type:    types.ConditionIrreconcilable,
   109  			Status:  types.StatusTrue,
   110  			Reason:  types.ReasonReconcileError,
   111  			Message: err.Error(),
   112  		})
   113  		_ = r.updateResourceStatus(o, status)
   114  		return reconcile.Result{}, err
   115  	}
   116  	status.RemoveCondition(types.ConditionIrreconcilable)
   117  
   118  	if deleted {
   119  		if !contains(pendingFinalizers, finalizer) {
   120  			log.Info("Resource is terminated, skipping reconciliation")
   121  			return reconcile.Result{}, nil
   122  		}
   123  
   124  		uninstalledRelease, err := manager.UninstallRelease(context.TODO())
   125  		if err != nil && err != release.ErrNotFound {
   126  			log.Error(err, "Failed to uninstall release")
   127  			status.SetCondition(types.HelmAppCondition{
   128  				Type:    types.ConditionReleaseFailed,
   129  				Status:  types.StatusTrue,
   130  				Reason:  types.ReasonUninstallError,
   131  				Message: err.Error(),
   132  			})
   133  			_ = r.updateResourceStatus(o, status)
   134  			return reconcile.Result{}, err
   135  		}
   136  		status.RemoveCondition(types.ConditionReleaseFailed)
   137  
   138  		if err == release.ErrNotFound {
   139  			log.Info("Release not found, removing finalizer")
   140  		} else {
   141  			log.Info("Uninstalled release")
   142  			if log.Enabled() {
   143  				fmt.Println(diffutil.Diff(uninstalledRelease.GetManifest(), ""))
   144  			}
   145  			status.SetCondition(types.HelmAppCondition{
   146  				Type:   types.ConditionDeployed,
   147  				Status: types.StatusFalse,
   148  				Reason: types.ReasonUninstallSuccessful,
   149  			})
   150  			status.DeployedRelease = nil
   151  		}
   152  		if err := r.updateResourceStatus(o, status); err != nil {
   153  			return reconcile.Result{}, err
   154  		}
   155  
   156  		finalizers := []string{}
   157  		for _, pendingFinalizer := range pendingFinalizers {
   158  			if pendingFinalizer != finalizer {
   159  				finalizers = append(finalizers, pendingFinalizer)
   160  			}
   161  		}
   162  		o.SetFinalizers(finalizers)
   163  		err = r.updateResource(o)
   164  
   165  		// Need to requeue because finalizer update does not change metadata.generation
   166  		return reconcile.Result{Requeue: true}, err
   167  	}
   168  
   169  	if !manager.IsInstalled() {
   170  		installedRelease, err := manager.InstallRelease(context.TODO())
   171  		if err != nil {
   172  			log.Error(err, "Failed to install release")
   173  			status.SetCondition(types.HelmAppCondition{
   174  				Type:    types.ConditionReleaseFailed,
   175  				Status:  types.StatusTrue,
   176  				Reason:  types.ReasonInstallError,
   177  				Message: err.Error(),
   178  			})
   179  			_ = r.updateResourceStatus(o, status)
   180  			return reconcile.Result{}, err
   181  		}
   182  		status.RemoveCondition(types.ConditionReleaseFailed)
   183  
   184  		if r.releaseHook != nil {
   185  			if err := r.releaseHook(installedRelease); err != nil {
   186  				log.Error(err, "Failed to run release hook")
   187  				return reconcile.Result{}, err
   188  			}
   189  		}
   190  
   191  		log.Info("Installed release")
   192  		if log.Enabled() {
   193  			fmt.Println(diffutil.Diff("", installedRelease.GetManifest()))
   194  		}
   195  		log.V(1).Info("Config values", "values", installedRelease.GetConfig())
   196  		status.SetCondition(types.HelmAppCondition{
   197  			Type:    types.ConditionDeployed,
   198  			Status:  types.StatusTrue,
   199  			Reason:  types.ReasonInstallSuccessful,
   200  			Message: installedRelease.GetInfo().GetStatus().GetNotes(),
   201  		})
   202  		status.DeployedRelease = &types.HelmAppRelease{
   203  			Name:     installedRelease.Name,
   204  			Manifest: installedRelease.Manifest,
   205  		}
   206  		err = r.updateResourceStatus(o, status)
   207  		return reconcile.Result{RequeueAfter: r.ReconcilePeriod}, err
   208  	}
   209  
   210  	if manager.IsUpdateRequired() {
   211  		previousRelease, updatedRelease, err := manager.UpdateRelease(context.TODO())
   212  		if err != nil {
   213  			log.Error(err, "Failed to update release")
   214  			status.SetCondition(types.HelmAppCondition{
   215  				Type:    types.ConditionReleaseFailed,
   216  				Status:  types.StatusTrue,
   217  				Reason:  types.ReasonUpdateError,
   218  				Message: err.Error(),
   219  			})
   220  			_ = r.updateResourceStatus(o, status)
   221  			return reconcile.Result{}, err
   222  		}
   223  		status.RemoveCondition(types.ConditionReleaseFailed)
   224  
   225  		if r.releaseHook != nil {
   226  			if err := r.releaseHook(updatedRelease); err != nil {
   227  				log.Error(err, "Failed to run release hook")
   228  				return reconcile.Result{}, err
   229  			}
   230  		}
   231  
   232  		log.Info("Updated release")
   233  		if log.Enabled() {
   234  			fmt.Println(diffutil.Diff(previousRelease.GetManifest(), updatedRelease.GetManifest()))
   235  		}
   236  		log.V(1).Info("Config values", "values", updatedRelease.GetConfig())
   237  		status.SetCondition(types.HelmAppCondition{
   238  			Type:    types.ConditionDeployed,
   239  			Status:  types.StatusTrue,
   240  			Reason:  types.ReasonUpdateSuccessful,
   241  			Message: updatedRelease.GetInfo().GetStatus().GetNotes(),
   242  		})
   243  		status.DeployedRelease = &types.HelmAppRelease{
   244  			Name:     updatedRelease.Name,
   245  			Manifest: updatedRelease.Manifest,
   246  		}
   247  		err = r.updateResourceStatus(o, status)
   248  		return reconcile.Result{RequeueAfter: r.ReconcilePeriod}, err
   249  	}
   250  
   251  	// If a change is made to the CR spec that causes a release failure, a
   252  	// ConditionReleaseFailed is added to the status conditions. If that change
   253  	// is then reverted to its previous state, the operator will stop
   254  	// attempting the release and will resume reconciling. In this case, we
   255  	// need to remove the ConditionReleaseFailed because the failing release is
   256  	// no longer being attempted.
   257  	status.RemoveCondition(types.ConditionReleaseFailed)
   258  
   259  	expectedRelease, err := manager.ReconcileRelease(context.TODO())
   260  	if err != nil {
   261  		log.Error(err, "Failed to reconcile release")
   262  		status.SetCondition(types.HelmAppCondition{
   263  			Type:    types.ConditionIrreconcilable,
   264  			Status:  types.StatusTrue,
   265  			Reason:  types.ReasonReconcileError,
   266  			Message: err.Error(),
   267  		})
   268  		_ = r.updateResourceStatus(o, status)
   269  		return reconcile.Result{}, err
   270  	}
   271  	status.RemoveCondition(types.ConditionIrreconcilable)
   272  
   273  	if r.releaseHook != nil {
   274  		if err := r.releaseHook(expectedRelease); err != nil {
   275  			log.Error(err, "Failed to run release hook")
   276  			return reconcile.Result{}, err
   277  		}
   278  	}
   279  
   280  	log.Info("Reconciled release")
   281  	status.DeployedRelease = &types.HelmAppRelease{
   282  		Name:     expectedRelease.Name,
   283  		Manifest: expectedRelease.Manifest,
   284  	}
   285  	err = r.updateResourceStatus(o, status)
   286  	return reconcile.Result{RequeueAfter: r.ReconcilePeriod}, err
   287  }
   288  
   289  func (r HelmOperatorReconciler) updateResource(o *unstructured.Unstructured) error {
   290  	return r.Client.Update(context.TODO(), o)
   291  }
   292  
   293  func (r HelmOperatorReconciler) updateResourceStatus(o *unstructured.Unstructured, status *types.HelmAppStatus) error {
   294  	o.Object["status"] = status
   295  	return r.Client.Status().Update(context.TODO(), o)
   296  }
   297  
   298  func contains(l []string, s string) bool {
   299  	for _, elem := range l {
   300  		if elem == s {
   301  			return true
   302  		}
   303  	}
   304  	return false
   305  }