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