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