...

Source file src/github.com/cert-manager/issuer-lib/controllers/request_controller.go

Documentation: github.com/cert-manager/issuer-lib/controllers

     1  /*
     2  Copyright 2023 The cert-manager Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package controllers
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"time"
    24  
    25  	cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
    26  	cmmeta "github.com/cert-manager/cert-manager/pkg/apis/meta/v1"
    27  	"github.com/go-logr/logr"
    28  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/apimachinery/pkg/runtime/schema"
    32  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    33  	"k8s.io/client-go/tools/record"
    34  	"k8s.io/utils/clock"
    35  	"k8s.io/utils/ptr"
    36  	ctrl "sigs.k8s.io/controller-runtime"
    37  	"sigs.k8s.io/controller-runtime/pkg/builder"
    38  	"sigs.k8s.io/controller-runtime/pkg/client"
    39  	"sigs.k8s.io/controller-runtime/pkg/controller"
    40  	"sigs.k8s.io/controller-runtime/pkg/log"
    41  	"sigs.k8s.io/controller-runtime/pkg/predicate"
    42  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    43  
    44  	v1alpha1 "github.com/cert-manager/issuer-lib/api/v1alpha1"
    45  	"github.com/cert-manager/issuer-lib/conditions"
    46  	"github.com/cert-manager/issuer-lib/controllers/signer"
    47  	"github.com/cert-manager/issuer-lib/internal/kubeutil"
    48  )
    49  
    50  // RequestController reconciles a "request" object.
    51  // A request object implementation can be provided using the requestObjectHelperCreator
    52  // function. This function is responsible for creating a RequestObjectHelper that
    53  // is used to interact with the request object.
    54  // Currently, we support cert-manager CertificateRequests and Kubernetes CertificateSigningRequests.
    55  type RequestController struct {
    56  	IssuerTypes        []v1alpha1.Issuer
    57  	ClusterIssuerTypes []v1alpha1.Issuer
    58  
    59  	FieldOwner       string
    60  	MaxRetryDuration time.Duration
    61  	EventSource      kubeutil.EventSource
    62  
    63  	// Client is a controller-runtime client used to get and set K8S API resources
    64  	client.Client
    65  	// Sign connects to a CA and returns a signed certificate for the supplied Request.
    66  	signer.Sign
    67  	// IgnoreCertificateRequest is an optional function that can prevent the Request
    68  	// and Kubernetes CSR controllers from reconciling a Request resource.
    69  	signer.IgnoreCertificateRequest
    70  
    71  	// EventRecorder is used for creating Kubernetes events on resources.
    72  	EventRecorder record.EventRecorder
    73  
    74  	// Clock is used to mock condition transition times in tests.
    75  	Clock clock.PassiveClock
    76  
    77  	// PreSetupWithManager is an optional function that can be used to perform
    78  	// additional setup before the controller is built and registered with the
    79  	// manager.
    80  	PreSetupWithManager func(context.Context, schema.GroupVersionKind, ctrl.Manager, *builder.Builder) error
    81  
    82  	// PostSetupWithManager is an optional function that can be used to perform
    83  	// additional setup after the controller is built and registered with the
    84  	// manager.
    85  	PostSetupWithManager func(context.Context, schema.GroupVersionKind, ctrl.Manager, controller.Controller) error
    86  
    87  	allIssuerTypes []IssuerType
    88  
    89  	initialised                bool
    90  	requestType                client.Object
    91  	requestPredicate           predicate.Predicate
    92  	matchIssuerType            MatchIssuerType
    93  	requestObjectHelperCreator RequestObjectHelperCreator
    94  }
    95  
    96  type MatchIssuerType func(client.Object) (v1alpha1.Issuer, client.ObjectKey, error)
    97  type RequestObjectHelperCreator func(client.Object) RequestObjectHelper
    98  
    99  type IssuerType struct {
   100  	Type         v1alpha1.Issuer
   101  	IsNamespaced bool
   102  }
   103  
   104  func (r *RequestController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
   105  	logger := log.FromContext(ctx).WithName("Reconcile")
   106  
   107  	logger.V(2).Info("Starting reconcile loop", "name", req.Name, "namespace", req.Namespace)
   108  
   109  	// The error returned by `reconcileStatusPatch` is meant for controller-runtime,
   110  	// not for us. That's why we aren't checking `reconcileError != nil` .
   111  	result, statusPatch, reconcileError := r.reconcileStatusPatch(logger, ctx, req)
   112  
   113  	if statusPatch != nil {
   114  		obj, patch, err := statusPatch.Patch()
   115  		if err != nil {
   116  			return ctrl.Result{}, utilerrors.NewAggregate([]error{err, reconcileError}) // requeue with backoff
   117  		}
   118  
   119  		logger.V(2).Info("Got StatusPatch result", "result", result, "error", reconcileError, "patch", patch)
   120  
   121  		if err := r.Client.Status().Patch(ctx, obj, patch, &client.SubResourcePatchOptions{
   122  			PatchOptions: client.PatchOptions{
   123  				FieldManager: r.FieldOwner,
   124  				Force:        ptr.To(true),
   125  			},
   126  		}); err != nil {
   127  			if !apierrors.IsNotFound(err) {
   128  				return ctrl.Result{}, utilerrors.NewAggregate([]error{err, reconcileError}) // requeue with backoff
   129  			}
   130  
   131  			logger.V(1).Info("Request not found. Ignoring.")
   132  		}
   133  	} else {
   134  		logger.V(2).Info("Got nil StatusPatch result", "result", result, "error", reconcileError)
   135  	}
   136  
   137  	return result, reconcileError
   138  }
   139  
   140  // reconcileStatusPatch is responsible for reconciling the request resource (cert-manager
   141  // CertificateRequest or Kubernetes CertificateSigningRequest). It will return the
   142  // result and reconcileError to be returned by the Reconcile function. It also returns
   143  // a statusPatch that the Reconcile function will apply to the request resource's status.
   144  // This function is split out from the Reconcile function to allow for easier testing.
   145  //
   146  // The error returned by `reconcileStatusPatch` is meant for controller-runtime,
   147  // not for the caller. The caller must not check the error (i.e., they must not
   148  // do `if err != nil...`).
   149  func (r *RequestController) reconcileStatusPatch(
   150  	logger logr.Logger,
   151  	ctx context.Context,
   152  	req ctrl.Request,
   153  ) (result ctrl.Result, _ RequestPatch, reconcileError error) {
   154  	requestObject := r.requestType.DeepCopyObject().(client.Object)
   155  
   156  	if err := r.Client.Get(ctx, req.NamespacedName, requestObject); err != nil && apierrors.IsNotFound(err) {
   157  		logger.V(1).Info("Request not found. Ignoring.")
   158  		return result, nil, nil // done
   159  	} else if err != nil {
   160  		return result, nil, fmt.Errorf("unexpected get error: %v", err) // requeue with backoff
   161  	}
   162  
   163  	// Select first matching issuer type and construct an issuerObject and issuerName
   164  	issuerObject, issuerName, err := r.matchIssuerType(requestObject)
   165  	// Ignore Request if issuerRef doesn't match one of our issuer Types
   166  	if err != nil {
   167  		logger.V(1).Info("Request has a foreign issuer. Ignoring.", "error", err)
   168  		return result, nil, nil // done
   169  	}
   170  	issuerGvk := issuerObject.GetObjectKind().GroupVersionKind()
   171  
   172  	// Create a helper for the requestObject
   173  	requestObjectHelper := r.requestObjectHelperCreator(requestObject)
   174  
   175  	// Ignore Request if it has not yet been assigned an approval
   176  	// status condition by an approval controller.
   177  	if !requestObjectHelper.IsApproved() && !requestObjectHelper.IsDenied() {
   178  		logger.V(1).Info("Request has not been approved or denied. Ignoring.")
   179  		return result, nil, nil // done
   180  	}
   181  
   182  	// Ignore Request if it is already Ready
   183  	if requestObjectHelper.IsReady() {
   184  		logger.V(1).Info("Request is Ready. Ignoring.")
   185  		return result, nil, nil // done
   186  	}
   187  
   188  	// Ignore Request if it is already Failed
   189  	if requestObjectHelper.IsFailed() {
   190  		logger.V(1).Info("Request is Failed. Ignoring.")
   191  		return result, nil, nil // done
   192  	}
   193  
   194  	// Ignore Request if it is already Denied
   195  	if requestObjectHelper.IsDenied() {
   196  		logger.V(1).Info("Request is Denied. Ignoring.")
   197  		return result, nil, nil // done
   198  	}
   199  
   200  	if r.IgnoreCertificateRequest != nil {
   201  		ignore, err := r.IgnoreCertificateRequest(
   202  			ctx,
   203  			requestObjectHelper.RequestObject(),
   204  			issuerGvk,
   205  			issuerName,
   206  		)
   207  		if err != nil {
   208  			logger.V(1).Error(err, "Unexpected error while checking if Request should be ignored")
   209  			return result, nil, fmt.Errorf("failed to check if Request should be ignored: %v", err) // requeue with backoff
   210  		}
   211  
   212  		if ignore {
   213  			logger.V(1).Info("Ignoring Request")
   214  			return result, nil, nil // done
   215  		}
   216  	}
   217  
   218  	// We now have a Request that belongs to us so we are responsible
   219  	// for updating its Status.
   220  	statusPatch := requestObjectHelper.NewPatch(
   221  		r.Clock,
   222  		r.FieldOwner,
   223  		r.EventRecorder,
   224  	)
   225  
   226  	// Add a Ready condition if one does not already exist. Set initial Status
   227  	// to Unknown.
   228  	if statusPatch.SetInitializing() {
   229  		logger.V(1).Info("Initialised Ready condition")
   230  
   231  		// To continue reconciling this Request, we must re-run the reconcile loop
   232  		// after adding the Unknown Ready condition. This update will trigger a
   233  		// new reconcile loop, so we don't need to requeue here.
   234  		return result, statusPatch, nil // apply patch, done
   235  	}
   236  
   237  	if err := r.Client.Get(ctx, issuerName, issuerObject); err != nil && apierrors.IsNotFound(err) {
   238  		logger.V(1).Info("Issuer not found. Waiting for it to be created")
   239  		statusPatch.SetWaitingForIssuerExist(err)
   240  
   241  		return result, statusPatch, nil // apply patch, done
   242  	} else if err != nil {
   243  		logger.V(1).Error(err, "Unexpected error while getting Issuer")
   244  		statusPatch.SetUnexpectedError(err)
   245  
   246  		return result, nil, fmt.Errorf("unexpected get error: %v", err) // requeue with backoff
   247  	}
   248  
   249  	readyCondition := conditions.GetIssuerStatusCondition(
   250  		issuerObject.GetStatus().Conditions,
   251  		cmapi.IssuerConditionReady,
   252  	)
   253  	if readyCondition == nil {
   254  		logger.V(1).Info("Issuer is not Ready yet (no ready condition). Waiting for it to become ready.")
   255  		statusPatch.SetWaitingForIssuerReadyNoCondition()
   256  
   257  		return result, statusPatch, nil // apply patch, done
   258  	}
   259  	if readyCondition.ObservedGeneration < issuerObject.GetGeneration() {
   260  		logger.V(1).Info("Issuer is not Ready yet (ready condition out-of-date). Waiting for it to become ready.", "issuer ready condition", readyCondition)
   261  		statusPatch.SetWaitingForIssuerReadyOutdated()
   262  
   263  		return result, statusPatch, nil // apply patch, done
   264  	}
   265  	if readyCondition.Status != cmmeta.ConditionTrue {
   266  		logger.V(1).Info("Issuer is not Ready yet (status == false). Waiting for it to become ready.", "issuer ready condition", readyCondition)
   267  		statusPatch.SetWaitingForIssuerReadyNotReady(readyCondition)
   268  
   269  		return result, statusPatch, nil // apply patch, done
   270  	}
   271  
   272  	signedCertificate, err := r.Sign(log.IntoContext(ctx, logger), requestObjectHelper.RequestObject(), issuerObject)
   273  	if err == nil {
   274  		logger.V(1).Info("Successfully finished the reconciliation.")
   275  		statusPatch.SetIssued(signedCertificate)
   276  
   277  		return result, statusPatch, nil // apply patch, done
   278  	}
   279  
   280  	// An error in the issuer part of the operator should trigger a reconcile
   281  	// of the issuer's state.
   282  	if issuerError := new(signer.IssuerError); errors.As(err, issuerError) {
   283  		if reportError := r.EventSource.ReportError(
   284  			issuerGvk, client.ObjectKeyFromObject(issuerObject),
   285  			issuerError.Err,
   286  		); reportError != nil {
   287  			return result, nil, fmt.Errorf("unexpected ReportError error: %v", reportError) // requeue with backoff
   288  		}
   289  
   290  		logger.V(1).Info("Issuer is not Ready yet (ready condition out-of-date). Waiting for it to become ready.", "issuer-error", issuerError)
   291  		statusPatch.SetWaitingForIssuerReadyOutdated()
   292  
   293  		return result, statusPatch, nil // apply patch, done
   294  	}
   295  
   296  	didCustomConditionTransition := false
   297  	if targetCustom := new(signer.SetCertificateRequestConditionError); errors.As(err, targetCustom) {
   298  		logger.V(1).Info("Set RequestCondition error. Setting condition.", "error", err)
   299  		didCustomConditionTransition = statusPatch.SetCustomCondition(
   300  			string(targetCustom.ConditionType),
   301  			metav1.ConditionStatus(targetCustom.Status),
   302  			targetCustom.Reason,
   303  			targetCustom.Error(),
   304  		)
   305  	}
   306  
   307  	// Check if we have still time to requeue & retry
   308  	isPending := errors.As(err, &signer.PendingError{})
   309  	isPermanentError := errors.As(err, &signer.PermanentError{})
   310  	pastMaxRetryDuration := r.Clock.Now().After(requestObject.GetCreationTimestamp().Add(r.MaxRetryDuration))
   311  	switch {
   312  	case isPending:
   313  		// Signing is pending, wait more.
   314  		//
   315  		// The PendingError has a misleading name: although it is an error,
   316  		// it isn't an error. It just means that we should poll again later.
   317  		// Its message gives the reason why the signing process is still in
   318  		// progress. Thus, we don't log any error.
   319  		logger.V(1).WithValues("reason", err.Error()).Info("Signing in progress.")
   320  		statusPatch.SetPending(fmt.Sprintf("Signing still in progress. Reason: %s", err))
   321  
   322  		// Let's not trigger an unnecessary reconciliation when we know that the
   323  		// user-defined condition was changed and will trigger a reconciliation.
   324  		if didCustomConditionTransition {
   325  			return result, statusPatch, nil // apply patch, done
   326  		} else {
   327  			result.Requeue = true
   328  			return result, statusPatch, nil // apply patch, requeue with backoff
   329  		}
   330  	case isPermanentError:
   331  		logger.V(1).Error(err, "Permanent Request error. Marking as failed.")
   332  		statusPatch.SetPermanentError(err)
   333  		return result, statusPatch, reconcile.TerminalError(err) // apply patch, done
   334  	case pastMaxRetryDuration:
   335  		logger.V(1).Error(err, "Request has been retried for too long. Marking as failed.")
   336  		statusPatch.SetPermanentError(err)
   337  		return result, statusPatch, reconcile.TerminalError(err) // apply patch, done
   338  	default:
   339  		// We consider all the other errors as being retryable.
   340  		logger.V(1).Error(err, "Got an error, will be retried.")
   341  		statusPatch.SetRetryableError(err)
   342  
   343  		// Let's not trigger an unnecessary reconciliation when we know that the
   344  		// user-defined condition was changed and will trigger a reconciliation.
   345  		if didCustomConditionTransition {
   346  			return result, statusPatch, reconcile.TerminalError(err) // apply patch, done
   347  		} else {
   348  			return result, statusPatch, err // apply patch, requeue with backoff
   349  		}
   350  	}
   351  }
   352  
   353  func (r *RequestController) setAllIssuerTypesWithGroupVersionKind(scheme *runtime.Scheme) error {
   354  	issuers := make([]IssuerType, 0, len(r.IssuerTypes)+len(r.ClusterIssuerTypes))
   355  	for _, issuer := range r.IssuerTypes {
   356  		issuers = append(issuers, IssuerType{
   357  			Type:         issuer,
   358  			IsNamespaced: true,
   359  		})
   360  
   361  	}
   362  	for _, issuer := range r.ClusterIssuerTypes {
   363  		issuers = append(issuers, IssuerType{
   364  			Type:         issuer,
   365  			IsNamespaced: false,
   366  		})
   367  	}
   368  
   369  	for _, issuer := range issuers {
   370  		if err := kubeutil.SetGroupVersionKind(scheme, issuer.Type); err != nil {
   371  			return err
   372  		}
   373  	}
   374  
   375  	r.allIssuerTypes = issuers
   376  
   377  	return nil
   378  }
   379  
   380  func (r *RequestController) AllIssuerTypes() []IssuerType {
   381  	return r.allIssuerTypes
   382  }
   383  
   384  func (r *RequestController) Init(
   385  	requestType client.Object,
   386  	requestPredicate predicate.Predicate,
   387  	matchIssuerType MatchIssuerType,
   388  	requestObjectHelperCreator RequestObjectHelperCreator,
   389  ) *RequestController {
   390  	r.requestType = requestType
   391  	r.requestPredicate = requestPredicate
   392  	r.matchIssuerType = matchIssuerType
   393  	r.requestObjectHelperCreator = requestObjectHelperCreator
   394  
   395  	r.initialised = true
   396  
   397  	return r
   398  }
   399  
   400  // SetupWithManager sets up the controller with the Manager.
   401  func (r *RequestController) SetupWithManager(
   402  	ctx context.Context,
   403  	mgr ctrl.Manager,
   404  ) error {
   405  	if !r.initialised {
   406  		return fmt.Errorf("must call Init(...) before calling SetupWithManager(...)")
   407  	}
   408  
   409  	if err := kubeutil.SetGroupVersionKind(mgr.GetScheme(), r.requestType); err != nil {
   410  		return err
   411  	}
   412  
   413  	if err := r.setAllIssuerTypesWithGroupVersionKind(mgr.GetScheme()); err != nil {
   414  		return err
   415  	}
   416  
   417  	build := ctrl.
   418  		NewControllerManagedBy(mgr).
   419  		For(
   420  			r.requestType,
   421  			// We are only interested in changes to the non-ready conditions of the
   422  			// certificaterequest, this also prevents us to get in fast reconcile loop
   423  			// when setting the status to Pending causing the resource to update, while
   424  			// we only want to re-reconcile with backoff/ when a resource becomes available.
   425  			builder.WithPredicates(
   426  				predicate.ResourceVersionChangedPredicate{},
   427  				r.requestPredicate,
   428  			),
   429  		)
   430  
   431  	// We watch all the issuer types. When an issuer receives a watch event, we
   432  	// reconcile all the certificate requests that reference that issuer. This
   433  	// is useful when the certificate request undergoes long backoff retry
   434  	// periods and wouldn't react quickly to a fix in the issuer configuration.
   435  	for _, issuerType := range r.AllIssuerTypes() {
   436  		issuerType := issuerType
   437  		gvk := issuerType.Type.GetObjectKind().GroupVersionKind()
   438  
   439  		// This context is passed through to the client-go informer factory and the
   440  		// timeout dictates how long to wait for the informer to sync with the K8S
   441  		// API server. See:
   442  		// * https://github.com/kubernetes-sigs/controller-runtime/issues/562
   443  		// * https://github.com/kubernetes-sigs/controller-runtime/issues/1219
   444  		//
   445  		// The defaulting logic is based on:
   446  		// https://github.com/kubernetes-sigs/controller-runtime/blob/30eae58f1b984c1b8139dd9b9f68dd2d530ed429/pkg/controller/controller.go#L138-L144
   447  		timeout := mgr.GetControllerOptions().CacheSyncTimeout
   448  		if timeout == 0 {
   449  			timeout = 2 * time.Minute
   450  		}
   451  		cacheSyncCtx, cancel := context.WithTimeout(ctx, timeout)
   452  		defer cancel()
   453  
   454  		resourceHandler, err := kubeutil.NewLinkedResourceHandler(
   455  			cacheSyncCtx,
   456  			mgr.GetLogger(),
   457  			mgr.GetScheme(),
   458  			mgr.GetCache(),
   459  			r.requestType,
   460  			func(rawObj client.Object) []string {
   461  				issuerObject, issuerName, err := r.matchIssuerType(rawObj)
   462  				if err != nil || issuerObject.GetObjectKind().GroupVersionKind() != gvk {
   463  					return nil
   464  				}
   465  
   466  				return []string{fmt.Sprintf("%s/%s", issuerName.Namespace, issuerName.Name)}
   467  			},
   468  			nil,
   469  		)
   470  		if err != nil {
   471  			return err
   472  		}
   473  
   474  		build = build.Watches(
   475  			issuerType.Type,
   476  			resourceHandler,
   477  			builder.WithPredicates(
   478  				predicate.ResourceVersionChangedPredicate{},
   479  				LinkedIssuerPredicate{},
   480  			),
   481  		)
   482  	}
   483  
   484  	if r.PreSetupWithManager != nil {
   485  		err := r.PreSetupWithManager(ctx, r.requestType.GetObjectKind().GroupVersionKind(), mgr, build)
   486  		r.PreSetupWithManager = nil // free setup function
   487  		if err != nil {
   488  			return err
   489  		}
   490  	}
   491  
   492  	if controller, err := build.Build(r); err != nil {
   493  		return err
   494  	} else if r.PostSetupWithManager != nil {
   495  		err := r.PostSetupWithManager(ctx, r.requestType.GetObjectKind().GroupVersionKind(), mgr, controller)
   496  		r.PostSetupWithManager = nil // free setup function
   497  		return err
   498  	}
   499  	return nil
   500  }
   501  

View as plain text