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