github.com/lablabs/operator-sdk@v0.8.2/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/metrics"
    31  	"github.com/operator-framework/operator-sdk/pkg/ansible/proxy/kubeconfig"
    32  	"github.com/operator-framework/operator-sdk/pkg/ansible/runner"
    33  	"github.com/operator-framework/operator-sdk/pkg/ansible/runner/eventapi"
    34  
    35  	v1 "k8s.io/api/core/v1"
    36  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    37  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    38  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    39  	"k8s.io/apimachinery/pkg/runtime/schema"
    40  	"k8s.io/apimachinery/pkg/types"
    41  	"sigs.k8s.io/controller-runtime/pkg/client"
    42  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    43  	logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
    44  )
    45  
    46  const (
    47  	// ReconcilePeriodAnnotation - annotation used by a user to specify the reconciliation interval for the CR.
    48  	// To use create a CR with an annotation "ansible.operator-sdk/reconcile-period: 30s" or some other valid
    49  	// Duration. This will override the operators/or controllers reconcile period for that particular CR.
    50  	ReconcilePeriodAnnotation = "ansible.operator-sdk/reconcile-period"
    51  )
    52  
    53  // AnsibleOperatorReconciler - object to reconcile runner requests
    54  type AnsibleOperatorReconciler struct {
    55  	GVK             schema.GroupVersionKind
    56  	Runner          runner.Runner
    57  	Client          client.Client
    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  	// Need to get the unstructured object after ansible
   191  	// this needs to hit the API
   192  	err = r.Client.Get(context.TODO(), request.NamespacedName, u)
   193  	if apierrors.IsNotFound(err) {
   194  		return reconcile.Result{}, nil
   195  	}
   196  	if err != nil {
   197  		return reconcile.Result{}, err
   198  	}
   199  
   200  	// try to get the updated finalizers
   201  	pendingFinalizers = u.GetFinalizers()
   202  
   203  	// We only want to update the CustomResource once, so we'll track changes
   204  	// and do it at the end
   205  	runSuccessful := len(failureMessages) == 0
   206  	// The finalizer has run successfully, time to remove it
   207  	if deleted && finalizerExists && runSuccessful {
   208  		finalizers := []string{}
   209  		for _, pendingFinalizer := range pendingFinalizers {
   210  			if pendingFinalizer != finalizer {
   211  				finalizers = append(finalizers, pendingFinalizer)
   212  			}
   213  		}
   214  		u.SetFinalizers(finalizers)
   215  		err := r.Client.Update(context.TODO(), u)
   216  		if err != nil {
   217  			logger.Error(err, "Failed to remove finalizer")
   218  			return reconcileResult, err
   219  		}
   220  	}
   221  	if r.ManageStatus {
   222  		err = r.markDone(u, request.NamespacedName, statusEvent, failureMessages)
   223  		if err != nil {
   224  			logger.Error(err, "Failed to mark status done")
   225  		}
   226  	}
   227  	return reconcileResult, err
   228  }
   229  
   230  func (r *AnsibleOperatorReconciler) markRunning(u *unstructured.Unstructured, namespacedName types.NamespacedName) error {
   231  	// Get the latest resource to prevent updating a stale status
   232  	err := r.Client.Get(context.TODO(), namespacedName, u)
   233  	if err != nil {
   234  		return err
   235  	}
   236  	statusInterface := u.Object["status"]
   237  	statusMap, _ := statusInterface.(map[string]interface{})
   238  	crStatus := ansiblestatus.CreateFromMap(statusMap)
   239  
   240  	// If there is no current status add that we are working on this resource.
   241  	errCond := ansiblestatus.GetCondition(crStatus, ansiblestatus.FailureConditionType)
   242  
   243  	if errCond != nil {
   244  		errCond.Status = v1.ConditionFalse
   245  		ansiblestatus.SetCondition(&crStatus, *errCond)
   246  	}
   247  	// If the condition is currently running, making sure that the values are correct.
   248  	// If they are the same a no-op, if they are different then it is a good thing we
   249  	// are updating it.
   250  	c := ansiblestatus.NewCondition(
   251  		ansiblestatus.RunningConditionType,
   252  		v1.ConditionTrue,
   253  		nil,
   254  		ansiblestatus.RunningReason,
   255  		ansiblestatus.RunningMessage,
   256  	)
   257  	ansiblestatus.SetCondition(&crStatus, *c)
   258  	u.Object["status"] = crStatus.GetJSONMap()
   259  	err = r.Client.Status().Update(context.TODO(), u)
   260  	if err != nil {
   261  		return err
   262  	}
   263  	return nil
   264  }
   265  
   266  // markError - used to alert the user to the issues during the validation of a reconcile run.
   267  // i.e Annotations that could be incorrect
   268  func (r *AnsibleOperatorReconciler) markError(u *unstructured.Unstructured, namespacedName types.NamespacedName, failureMessage string) error {
   269  	logger := logf.Log.WithName("markError")
   270  	metrics.ReconcileFailed(r.GVK.String())
   271  	// Get the latest resource to prevent updating a stale status
   272  	err := r.Client.Get(context.TODO(), namespacedName, u)
   273  	if apierrors.IsNotFound(err) {
   274  		logger.Info("Resource not found, assuming it was deleted", err)
   275  		return nil
   276  	}
   277  	if err != nil {
   278  		return err
   279  	}
   280  	statusInterface := u.Object["status"]
   281  	statusMap, ok := statusInterface.(map[string]interface{})
   282  	// If the map is not available create one.
   283  	if !ok {
   284  		statusMap = map[string]interface{}{}
   285  	}
   286  	crStatus := ansiblestatus.CreateFromMap(statusMap)
   287  
   288  	sc := ansiblestatus.GetCondition(crStatus, ansiblestatus.RunningConditionType)
   289  	if sc != nil {
   290  		sc.Status = v1.ConditionFalse
   291  		ansiblestatus.SetCondition(&crStatus, *sc)
   292  	}
   293  
   294  	c := ansiblestatus.NewCondition(
   295  		ansiblestatus.FailureConditionType,
   296  		v1.ConditionTrue,
   297  		nil,
   298  		ansiblestatus.FailedReason,
   299  		failureMessage,
   300  	)
   301  	ansiblestatus.SetCondition(&crStatus, *c)
   302  	// This needs the status subresource to be enabled by default.
   303  	u.Object["status"] = crStatus.GetJSONMap()
   304  
   305  	return r.Client.Status().Update(context.TODO(), u)
   306  }
   307  
   308  func (r *AnsibleOperatorReconciler) markDone(u *unstructured.Unstructured, namespacedName types.NamespacedName, statusEvent eventapi.StatusJobEvent, failureMessages eventapi.FailureMessages) error {
   309  	logger := logf.Log.WithName("markDone")
   310  	// Get the latest resource to prevent updating a stale status
   311  	err := r.Client.Get(context.TODO(), namespacedName, u)
   312  	if apierrors.IsNotFound(err) {
   313  		logger.Info("Resource not found, assuming it was deleted", err)
   314  		return nil
   315  	}
   316  	if err != nil {
   317  		return err
   318  	}
   319  	statusInterface := u.Object["status"]
   320  	statusMap, _ := statusInterface.(map[string]interface{})
   321  	crStatus := ansiblestatus.CreateFromMap(statusMap)
   322  
   323  	runSuccessful := len(failureMessages) == 0
   324  	ansibleStatus := ansiblestatus.NewAnsibleResultFromStatusJobEvent(statusEvent)
   325  
   326  	if !runSuccessful {
   327  		metrics.ReconcileFailed(r.GVK.String())
   328  		sc := ansiblestatus.GetCondition(crStatus, ansiblestatus.RunningConditionType)
   329  		sc.Status = v1.ConditionFalse
   330  		ansiblestatus.SetCondition(&crStatus, *sc)
   331  		c := ansiblestatus.NewCondition(
   332  			ansiblestatus.FailureConditionType,
   333  			v1.ConditionTrue,
   334  			ansibleStatus,
   335  			ansiblestatus.FailedReason,
   336  			strings.Join(failureMessages, "\n"),
   337  		)
   338  		ansiblestatus.SetCondition(&crStatus, *c)
   339  	} else {
   340  		metrics.ReconcileSucceeded(r.GVK.String())
   341  		c := ansiblestatus.NewCondition(
   342  			ansiblestatus.RunningConditionType,
   343  			v1.ConditionTrue,
   344  			ansibleStatus,
   345  			ansiblestatus.SuccessfulReason,
   346  			ansiblestatus.SuccessfulMessage,
   347  		)
   348  		// Remove the failure condition if set, because this completed successfully.
   349  		ansiblestatus.RemoveCondition(&crStatus, ansiblestatus.FailureConditionType)
   350  		ansiblestatus.SetCondition(&crStatus, *c)
   351  	}
   352  	// This needs the status subresource to be enabled by default.
   353  	u.Object["status"] = crStatus.GetJSONMap()
   354  
   355  	return r.Client.Status().Update(context.TODO(), u)
   356  }
   357  
   358  func contains(l []string, s string) bool {
   359  	for _, elem := range l {
   360  		if elem == s {
   361  			return true
   362  		}
   363  	}
   364  	return false
   365  }