github.com/fabianvf/ocp-release-operator-sdk@v0.0.0-20190426141702-57620ee2f090/pkg/ansible/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  	"encoding/json"
    20  	"errors"
    21  	"fmt"
    22  	"math/rand"
    23  	"os"
    24  	"strconv"
    25  	"strings"
    26  	"time"
    27  
    28  	ansiblestatus "github.com/operator-framework/operator-sdk/pkg/ansible/controller/status"
    29  	"github.com/operator-framework/operator-sdk/pkg/ansible/events"
    30  	"github.com/operator-framework/operator-sdk/pkg/ansible/proxy/kubeconfig"
    31  	"github.com/operator-framework/operator-sdk/pkg/ansible/runner"
    32  	"github.com/operator-framework/operator-sdk/pkg/ansible/runner/eventapi"
    33  
    34  	v1 "k8s.io/api/core/v1"
    35  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    36  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    37  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    38  	"k8s.io/apimachinery/pkg/runtime/schema"
    39  	"k8s.io/apimachinery/pkg/types"
    40  	"sigs.k8s.io/controller-runtime/pkg/client"
    41  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    42  	logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
    43  )
    44  
    45  const (
    46  	// ReconcilePeriodAnnotation - annotation used by a user to specify the reconciliation interval for the CR.
    47  	// To use create a CR with an annotation "ansible.operator-sdk/reconcile-period: 30s" or some other valid
    48  	// Duration. This will override the operators/or controllers reconcile period for that particular CR.
    49  	ReconcilePeriodAnnotation = "ansible.operator-sdk/reconcile-period"
    50  )
    51  
    52  // AnsibleOperatorReconciler - object to reconcile runner requests
    53  type AnsibleOperatorReconciler struct {
    54  	GVK             schema.GroupVersionKind
    55  	Runner          runner.Runner
    56  	Client          client.Client
    57  	APIReader       client.Reader
    58  	EventHandlers   []events.EventHandler
    59  	ReconcilePeriod time.Duration
    60  	ManageStatus    bool
    61  }
    62  
    63  // Reconcile - handle the event.
    64  func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconcile.Result, error) {
    65  	u := &unstructured.Unstructured{}
    66  	u.SetGroupVersionKind(r.GVK)
    67  	err := r.Client.Get(context.TODO(), request.NamespacedName, u)
    68  	if apierrors.IsNotFound(err) {
    69  		return reconcile.Result{}, nil
    70  	}
    71  	if err != nil {
    72  		return reconcile.Result{}, err
    73  	}
    74  
    75  	ident := strconv.Itoa(rand.Int())
    76  	logger := logf.Log.WithName("reconciler").WithValues(
    77  		"job", ident,
    78  		"name", u.GetName(),
    79  		"namespace", u.GetNamespace(),
    80  	)
    81  
    82  	reconcileResult := reconcile.Result{RequeueAfter: r.ReconcilePeriod}
    83  	if ds, ok := u.GetAnnotations()[ReconcilePeriodAnnotation]; ok {
    84  		duration, err := time.ParseDuration(ds)
    85  		if err != nil {
    86  			// Should attempt to update to a failed condition
    87  			r.markError(u, request.NamespacedName, fmt.Sprintf("Unable to parse reconcile period annotation: %v", err))
    88  			logger.Error(err, "Unable to parse reconcile period annotation")
    89  			return reconcileResult, err
    90  		}
    91  		reconcileResult.RequeueAfter = duration
    92  	}
    93  
    94  	deleted := u.GetDeletionTimestamp() != nil
    95  	finalizer, finalizerExists := r.Runner.GetFinalizer()
    96  	pendingFinalizers := u.GetFinalizers()
    97  	// If the resource is being deleted we don't want to add the finalizer again
    98  	if finalizerExists && !deleted && !contains(pendingFinalizers, finalizer) {
    99  		logger.V(1).Info("Adding finalizer to resource", "Finalizer", finalizer)
   100  		finalizers := append(pendingFinalizers, finalizer)
   101  		u.SetFinalizers(finalizers)
   102  		err := r.Client.Update(context.TODO(), u)
   103  		if err != nil {
   104  			logger.Error(err, "Unable to update cr with finalizer")
   105  			return reconcileResult, err
   106  		}
   107  	}
   108  	if !contains(pendingFinalizers, finalizer) && deleted {
   109  		logger.Info("Resource is terminated, skipping reconciliation")
   110  		return reconcile.Result{}, nil
   111  	}
   112  
   113  	spec := u.Object["spec"]
   114  	_, ok := spec.(map[string]interface{})
   115  	// Need to handle cases where there is no spec.
   116  	// We can add the spec to the object, which will allow
   117  	// everything to work, and will not get updated.
   118  	// Therefore we can now deal with the case of secrets and configmaps.
   119  	if !ok {
   120  		logger.V(1).Info("Spec was not found")
   121  		u.Object["spec"] = map[string]interface{}{}
   122  	}
   123  
   124  	if r.ManageStatus {
   125  		err = r.markRunning(u, request.NamespacedName)
   126  		if err != nil {
   127  			logger.Error(err, "Unable to update the status to mark cr as running")
   128  			return reconcileResult, err
   129  		}
   130  	}
   131  
   132  	ownerRef := metav1.OwnerReference{
   133  		APIVersion: u.GetAPIVersion(),
   134  		Kind:       u.GetKind(),
   135  		Name:       u.GetName(),
   136  		UID:        u.GetUID(),
   137  	}
   138  
   139  	kc, err := kubeconfig.Create(ownerRef, "http://localhost:8888", u.GetNamespace())
   140  	if err != nil {
   141  		r.markError(u, request.NamespacedName, "Unable to run reconciliation")
   142  		logger.Error(err, "Unable to generate kubeconfig")
   143  		return reconcileResult, err
   144  	}
   145  	defer func() {
   146  		if err := os.Remove(kc.Name()); err != nil {
   147  			logger.Error(err, "Failed to remove generated kubeconfig file")
   148  		}
   149  	}()
   150  	result, err := r.Runner.Run(ident, u, kc.Name())
   151  	if err != nil {
   152  		r.markError(u, request.NamespacedName, "Unable to run reconciliation")
   153  		logger.Error(err, "Unable to run ansible runner")
   154  		return reconcileResult, err
   155  	}
   156  
   157  	// iterate events from ansible, looking for the final one
   158  	statusEvent := eventapi.StatusJobEvent{}
   159  	failureMessages := eventapi.FailureMessages{}
   160  	for event := range result.Events() {
   161  		for _, eHandler := range r.EventHandlers {
   162  			go eHandler.Handle(ident, u, event)
   163  		}
   164  		if event.Event == eventapi.EventPlaybookOnStats {
   165  			// convert to StatusJobEvent; would love a better way to do this
   166  			data, err := json.Marshal(event)
   167  			if err != nil {
   168  				return reconcile.Result{}, err
   169  			}
   170  			err = json.Unmarshal(data, &statusEvent)
   171  			if err != nil {
   172  				return reconcile.Result{}, err
   173  			}
   174  		}
   175  		if event.Event == eventapi.EventRunnerOnFailed && !event.IgnoreError() {
   176  			failureMessages = append(failureMessages, event.GetFailedPlaybookMessage())
   177  		}
   178  	}
   179  	if statusEvent.Event == "" {
   180  		eventErr := errors.New("did not receive playbook_on_stats event")
   181  		stdout, err := result.Stdout()
   182  		if err != nil {
   183  			logger.Error(err, "Failed to get ansible-runner stdout")
   184  			return reconcileResult, err
   185  		}
   186  		logger.Error(eventErr, stdout)
   187  		return reconcileResult, eventErr
   188  	}
   189  
   190  	err = r.APIReader.Get(context.TODO(), request.NamespacedName, u)
   191  	if err != nil {
   192  		log.Error(err, "Unable to get updated object from api")
   193  		return reconcile.Result{}, err
   194  	}
   195  
   196  	// We only want to update the CustomResource once, so we'll track changes and do it at the end
   197  	runSuccessful := len(failureMessages) == 0
   198  	// The finalizer has run successfully, time to remove it
   199  	if deleted && finalizerExists && runSuccessful {
   200  		finalizers := []string{}
   201  		for _, pendingFinalizer := range pendingFinalizers {
   202  			if pendingFinalizer != finalizer {
   203  				finalizers = append(finalizers, pendingFinalizer)
   204  			}
   205  		}
   206  		u.SetFinalizers(finalizers)
   207  		err := r.Client.Update(context.TODO(), u)
   208  		if err != nil {
   209  			logger.Error(err, "Failed to remove finalizer")
   210  			return reconcileResult, err
   211  		}
   212  	}
   213  	if r.ManageStatus {
   214  		err = r.markDone(u, request.NamespacedName, statusEvent, failureMessages)
   215  		if exit, err := determineReturn(err); exit {
   216  			return reconcileResult, err
   217  		}
   218  
   219  	}
   220  	return reconcileResult, err
   221  }
   222  
   223  func (r *AnsibleOperatorReconciler) markRunning(u *unstructured.Unstructured, namespacedName types.NamespacedName) error {
   224  	// Get the latest resource to prevent updating a stale status
   225  	statusInterface := u.Object["status"]
   226  	statusMap, _ := statusInterface.(map[string]interface{})
   227  	crStatus := ansiblestatus.CreateFromMap(statusMap)
   228  
   229  	// If there is no current status add that we are working on this resource.
   230  	errCond := ansiblestatus.GetCondition(crStatus, ansiblestatus.FailureConditionType)
   231  
   232  	if errCond != nil {
   233  		errCond.Status = v1.ConditionFalse
   234  		ansiblestatus.SetCondition(&crStatus, *errCond)
   235  	}
   236  	// If the condition is currently running, making sure that the values are correct.
   237  	// If they are the same a no-op, if they are different then it is a good thing we
   238  	// are updating it.
   239  	c := ansiblestatus.NewCondition(
   240  		ansiblestatus.RunningConditionType,
   241  		v1.ConditionTrue,
   242  		nil,
   243  		ansiblestatus.RunningReason,
   244  		ansiblestatus.RunningMessage,
   245  	)
   246  	ansiblestatus.SetCondition(&crStatus, *c)
   247  	u.Object["status"] = crStatus.GetJSONMap()
   248  	err := r.Client.Status().Update(context.TODO(), u)
   249  	if err != nil {
   250  		return err
   251  	}
   252  	return nil
   253  }
   254  
   255  // markError - used to alert the user to the issues during the validation of a reconcile run.
   256  // i.e Annotations that could be incorrect
   257  func (r *AnsibleOperatorReconciler) markError(u *unstructured.Unstructured, namespacedName types.NamespacedName, failureMessage string) error {
   258  	logger := logf.Log.WithName("markError")
   259  	// Get the latest resource to prevent updating a stale status
   260  	err := r.Client.Get(context.TODO(), namespacedName, u)
   261  	if apierrors.IsNotFound(err) {
   262  		logger.Info("Resource not found, assuming it was deleted", err)
   263  		return nil
   264  	}
   265  	if err != nil {
   266  		return err
   267  	}
   268  	statusInterface := u.Object["status"]
   269  	statusMap, ok := statusInterface.(map[string]interface{})
   270  	// If the map is not available create one.
   271  	if !ok {
   272  		statusMap = map[string]interface{}{}
   273  	}
   274  	crStatus := ansiblestatus.CreateFromMap(statusMap)
   275  
   276  	sc := ansiblestatus.GetCondition(crStatus, ansiblestatus.RunningConditionType)
   277  	if sc != nil {
   278  		sc.Status = v1.ConditionFalse
   279  		ansiblestatus.SetCondition(&crStatus, *sc)
   280  	}
   281  
   282  	c := ansiblestatus.NewCondition(
   283  		ansiblestatus.FailureConditionType,
   284  		v1.ConditionTrue,
   285  		nil,
   286  		ansiblestatus.FailedReason,
   287  		failureMessage,
   288  	)
   289  	ansiblestatus.SetCondition(&crStatus, *c)
   290  	// This needs the status subresource to be enabled by default.
   291  	u.Object["status"] = crStatus.GetJSONMap()
   292  
   293  	return r.Client.Status().Update(context.TODO(), u)
   294  }
   295  
   296  func (r *AnsibleOperatorReconciler) markDone(u *unstructured.Unstructured, namespacedName types.NamespacedName, statusEvent eventapi.StatusJobEvent, failureMessages eventapi.FailureMessages) error {
   297  	statusInterface := u.Object["status"]
   298  	statusMap, _ := statusInterface.(map[string]interface{})
   299  	crStatus := ansiblestatus.CreateFromMap(statusMap)
   300  
   301  	runSuccessful := len(failureMessages) == 0
   302  	ansibleStatus := ansiblestatus.NewAnsibleResultFromStatusJobEvent(statusEvent)
   303  
   304  	if !runSuccessful {
   305  		sc := ansiblestatus.GetCondition(crStatus, ansiblestatus.RunningConditionType)
   306  		sc.Status = v1.ConditionFalse
   307  		ansiblestatus.SetCondition(&crStatus, *sc)
   308  		c := ansiblestatus.NewCondition(
   309  			ansiblestatus.FailureConditionType,
   310  			v1.ConditionTrue,
   311  			ansibleStatus,
   312  			ansiblestatus.FailedReason,
   313  			strings.Join(failureMessages, "\n"),
   314  		)
   315  		ansiblestatus.SetCondition(&crStatus, *c)
   316  	} else {
   317  		c := ansiblestatus.NewCondition(
   318  			ansiblestatus.RunningConditionType,
   319  			v1.ConditionTrue,
   320  			ansibleStatus,
   321  			ansiblestatus.SuccessfulReason,
   322  			ansiblestatus.SuccessfulMessage,
   323  		)
   324  		// Remove the failure condition if set, because this completed successfully.
   325  		ansiblestatus.RemoveCondition(&crStatus, ansiblestatus.FailureConditionType)
   326  		ansiblestatus.SetCondition(&crStatus, *c)
   327  	}
   328  	// This needs the status subresource to be enabled by default.
   329  	u.Object["status"] = crStatus.GetJSONMap()
   330  
   331  	return r.Client.Status().Update(context.TODO(), u)
   332  }
   333  
   334  func contains(l []string, s string) bool {
   335  	for _, elem := range l {
   336  		if elem == s {
   337  			return true
   338  		}
   339  	}
   340  	return false
   341  }
   342  
   343  // determineReturn - if the object was updated outside of our controller
   344  // this means that the current reconcilation is over and we should use the
   345  // latest version. To do this, we just exit without error because the
   346  // latest version should be queued for update.
   347  func determineReturn(err error) (bool, error) {
   348  	exit := false
   349  	if err == nil {
   350  		return exit, err
   351  	}
   352  	exit = true
   353  
   354  	if apierrors.IsConflict(err) {
   355  		log.V(1).Info("Conflict found during an update; re-running reconcilation")
   356  		return exit, nil
   357  	}
   358  	return exit, err
   359  }