sigs.k8s.io/cluster-api@v1.6.3/exp/runtime/internal/controllers/extensionconfig_controller.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes 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  	"fmt"
    22  	"strings"
    23  
    24  	"github.com/pkg/errors"
    25  	corev1 "k8s.io/api/core/v1"
    26  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    27  	"k8s.io/apimachinery/pkg/types"
    28  	kerrors "k8s.io/apimachinery/pkg/util/errors"
    29  	ctrl "sigs.k8s.io/controller-runtime"
    30  	"sigs.k8s.io/controller-runtime/pkg/client"
    31  	"sigs.k8s.io/controller-runtime/pkg/controller"
    32  	"sigs.k8s.io/controller-runtime/pkg/handler"
    33  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    34  
    35  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    36  	runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1"
    37  	tlog "sigs.k8s.io/cluster-api/internal/log"
    38  	runtimeclient "sigs.k8s.io/cluster-api/internal/runtime/client"
    39  	"sigs.k8s.io/cluster-api/util/annotations"
    40  	"sigs.k8s.io/cluster-api/util/conditions"
    41  	"sigs.k8s.io/cluster-api/util/patch"
    42  	"sigs.k8s.io/cluster-api/util/predicates"
    43  )
    44  
    45  const (
    46  	// tlsCAKey is used as a data key in Secret resources to store a CA certificate.
    47  	tlsCAKey = "ca.crt"
    48  )
    49  
    50  // +kubebuilder:rbac:groups=runtime.cluster.x-k8s.io,resources=extensionconfigs;extensionconfigs/status,verbs=get;list;watch;patch;update
    51  // +kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch
    52  
    53  // Reconciler reconciles an ExtensionConfig object.
    54  type Reconciler struct {
    55  	Client        client.Client
    56  	APIReader     client.Reader
    57  	RuntimeClient runtimeclient.Client
    58  	// WatchFilterValue is the label value used to filter events prior to reconciliation.
    59  	WatchFilterValue string
    60  }
    61  
    62  func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
    63  	err := ctrl.NewControllerManagedBy(mgr).
    64  		For(&runtimev1.ExtensionConfig{}).
    65  		WatchesMetadata(
    66  			&corev1.Secret{},
    67  			handler.EnqueueRequestsFromMapFunc(r.secretToExtensionConfig),
    68  		).
    69  		WithOptions(options).
    70  		WithEventFilter(predicates.ResourceNotPausedAndHasFilterLabel(ctrl.LoggerFrom(ctx), r.WatchFilterValue)).
    71  		Complete(r)
    72  	if err != nil {
    73  		return errors.Wrap(err, "failed setting up with a controller manager")
    74  	}
    75  
    76  	if err := indexByExtensionInjectCAFromSecretName(ctx, mgr); err != nil {
    77  		return errors.Wrap(err, "failed setting up with a controller manager")
    78  	}
    79  
    80  	// warmupRunnable will attempt to sync the RuntimeSDK registry with existing ExtensionConfig objects to ensure extensions
    81  	// are discovered before controllers begin reconciling.
    82  	err = mgr.Add(&warmupRunnable{
    83  		Client:        r.Client,
    84  		APIReader:     r.APIReader,
    85  		RuntimeClient: r.RuntimeClient,
    86  	})
    87  	if err != nil {
    88  		return errors.Wrap(err, "failed adding warmupRunnable to controller manager")
    89  	}
    90  	return nil
    91  }
    92  
    93  func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    94  	var errs []error
    95  	log := ctrl.LoggerFrom(ctx)
    96  
    97  	// Requeue events when the registry is not ready.
    98  	// The registry will become ready after it is 'warmed up' by warmupRunnable.
    99  	if !r.RuntimeClient.IsReady() {
   100  		return ctrl.Result{Requeue: true}, nil
   101  	}
   102  
   103  	extensionConfig := &runtimev1.ExtensionConfig{}
   104  	err := r.Client.Get(ctx, req.NamespacedName, extensionConfig)
   105  	if err != nil {
   106  		if apierrors.IsNotFound(err) {
   107  			// ExtensionConfig not found. Remove from registry.
   108  			// First we need to add Namespace/Name to empty ExtensionConfig object.
   109  			extensionConfig.Name = req.Name
   110  			extensionConfig.Namespace = req.Namespace
   111  			return r.reconcileDelete(ctx, extensionConfig)
   112  		}
   113  		// Error reading the object - requeue the request.
   114  		return ctrl.Result{}, err
   115  	}
   116  
   117  	// Return early if the ExtensionConfig is paused.
   118  	if annotations.HasPaused(extensionConfig) {
   119  		log.Info("Reconciliation is paused for this object")
   120  		return ctrl.Result{}, nil
   121  	}
   122  
   123  	// Handle deletion reconciliation loop.
   124  	if !extensionConfig.ObjectMeta.DeletionTimestamp.IsZero() {
   125  		return r.reconcileDelete(ctx, extensionConfig)
   126  	}
   127  
   128  	// Copy to avoid modifying the original extensionConfig.
   129  	original := extensionConfig.DeepCopy()
   130  
   131  	// Inject CABundle from secret if annotation is set. Otherwise https calls may fail.
   132  	if err := reconcileCABundle(ctx, r.Client, extensionConfig); err != nil {
   133  		return ctrl.Result{}, err
   134  	}
   135  
   136  	// discoverExtensionConfig will return a discovered ExtensionConfig with the appropriate conditions.
   137  	discoveredExtensionConfig, err := discoverExtensionConfig(ctx, r.RuntimeClient, extensionConfig)
   138  	if err != nil {
   139  		errs = append(errs, err)
   140  	}
   141  
   142  	// Always patch the ExtensionConfig as it may contain updates in conditions or clientConfig.caBundle.
   143  	if err = patchExtensionConfig(ctx, r.Client, original, discoveredExtensionConfig); err != nil {
   144  		errs = append(errs, err)
   145  	}
   146  
   147  	if len(errs) != 0 {
   148  		return ctrl.Result{}, kerrors.NewAggregate(errs)
   149  	}
   150  
   151  	// Register the ExtensionConfig if it was found and patched without error.
   152  	log.Info("Registering ExtensionConfig information into registry")
   153  	if err = r.RuntimeClient.Register(discoveredExtensionConfig); err != nil {
   154  		return ctrl.Result{}, errors.Wrapf(err, "failed to register ExtensionConfig %s/%s", extensionConfig.Namespace, extensionConfig.Name)
   155  	}
   156  	return ctrl.Result{}, nil
   157  }
   158  
   159  func patchExtensionConfig(ctx context.Context, client client.Client, original, modified *runtimev1.ExtensionConfig, options ...patch.Option) error {
   160  	patchHelper, err := patch.NewHelper(original, client)
   161  	if err != nil {
   162  		return errors.Wrapf(err, "failed to create patch helper for %s", tlog.KObj{Obj: modified})
   163  	}
   164  
   165  	options = append(options, patch.WithOwnedConditions{Conditions: []clusterv1.ConditionType{
   166  		runtimev1.RuntimeExtensionDiscoveredCondition,
   167  	}})
   168  	err = patchHelper.Patch(ctx, modified, options...)
   169  	if err != nil {
   170  		return errors.Wrapf(err, "failed to patch %s", tlog.KObj{Obj: modified})
   171  	}
   172  	return nil
   173  }
   174  
   175  // reconcileDelete will remove the ExtensionConfig from the registry on deletion of the object. Note this is a best
   176  // effort deletion that may not catch all cases.
   177  func (r *Reconciler) reconcileDelete(ctx context.Context, extensionConfig *runtimev1.ExtensionConfig) (ctrl.Result, error) {
   178  	log := ctrl.LoggerFrom(ctx)
   179  	log.Info("Unregistering ExtensionConfig information from registry")
   180  	if err := r.RuntimeClient.Unregister(extensionConfig); err != nil {
   181  		return ctrl.Result{}, errors.Wrapf(err, "failed to unregister %s", tlog.KObj{Obj: extensionConfig})
   182  	}
   183  	return ctrl.Result{}, nil
   184  }
   185  
   186  // secretToExtensionConfig maps a secret to ExtensionConfigs with the corresponding InjectCAFromSecretAnnotation
   187  // to reconcile them on updates of the secrets.
   188  func (r *Reconciler) secretToExtensionConfig(ctx context.Context, secret client.Object) []reconcile.Request {
   189  	result := []ctrl.Request{}
   190  
   191  	extensionConfigs := runtimev1.ExtensionConfigList{}
   192  	indexKey := secret.GetNamespace() + "/" + secret.GetName()
   193  
   194  	if err := r.Client.List(
   195  		ctx,
   196  		&extensionConfigs,
   197  		client.MatchingFields{injectCAFromSecretAnnotationField: indexKey},
   198  	); err != nil {
   199  		return nil
   200  	}
   201  
   202  	for _, ext := range extensionConfigs.Items {
   203  		result = append(result, ctrl.Request{NamespacedName: client.ObjectKey{Name: ext.Name}})
   204  	}
   205  
   206  	return result
   207  }
   208  
   209  // discoverExtensionConfig attempts to discover the Handlers for an ExtensionConfig.
   210  // If discovery succeeds it returns the ExtensionConfig with Handlers updated in Status and an updated Condition.
   211  // If discovery fails it returns the ExtensionConfig with no update to Handlers and a Failed Condition.
   212  func discoverExtensionConfig(ctx context.Context, runtimeClient runtimeclient.Client, extensionConfig *runtimev1.ExtensionConfig) (*runtimev1.ExtensionConfig, error) {
   213  	discoveredExtension, err := runtimeClient.Discover(ctx, extensionConfig.DeepCopy())
   214  	if err != nil {
   215  		modifiedExtensionConfig := extensionConfig.DeepCopy()
   216  		conditions.MarkFalse(modifiedExtensionConfig, runtimev1.RuntimeExtensionDiscoveredCondition, runtimev1.DiscoveryFailedReason, clusterv1.ConditionSeverityError, "error in discovery: %v", err)
   217  		return modifiedExtensionConfig, errors.Wrapf(err, "failed to discover %s", tlog.KObj{Obj: extensionConfig})
   218  	}
   219  
   220  	conditions.MarkTrue(discoveredExtension, runtimev1.RuntimeExtensionDiscoveredCondition)
   221  	return discoveredExtension, nil
   222  }
   223  
   224  // reconcileCABundle reconciles the CA bundle for the ExtensionConfig.
   225  // Note: This was implemented to behave similar to the cert-manager cainjector.
   226  // We couldn't use the cert-manager cainjector because it doesn't work with CustomResources.
   227  func reconcileCABundle(ctx context.Context, client client.Client, config *runtimev1.ExtensionConfig) error {
   228  	log := ctrl.LoggerFrom(ctx)
   229  
   230  	secretNameRaw, ok := config.Annotations[runtimev1.InjectCAFromSecretAnnotation]
   231  	if !ok {
   232  		return nil
   233  	}
   234  	secretName := splitNamespacedName(secretNameRaw)
   235  
   236  	log.Info(fmt.Sprintf("Injecting CA Bundle into ExtensionConfig from secret %q", secretNameRaw))
   237  
   238  	if secretName.Namespace == "" || secretName.Name == "" {
   239  		return errors.Errorf("failed to reconcile caBundle: secret name %q must be in the form <namespace>/<name>", secretNameRaw)
   240  	}
   241  
   242  	var secret corev1.Secret
   243  	// Note: this is an expensive API call because secrets are explicitly not cached.
   244  	if err := client.Get(ctx, secretName, &secret); err != nil {
   245  		return errors.Wrapf(err, "failed to reconcile caBundle: failed to get secret %q", secretNameRaw)
   246  	}
   247  
   248  	caData, hasCAData := secret.Data[tlsCAKey]
   249  	if !hasCAData {
   250  		return errors.Errorf("failed to reconcile caBundle: secret %s does not contain a %q entry", secretNameRaw, tlsCAKey)
   251  	}
   252  
   253  	config.Spec.ClientConfig.CABundle = caData
   254  	return nil
   255  }
   256  
   257  // splitNamespacedName turns the string form of a namespaced name
   258  // (<namespace>/<name>) into a types.NamespacedName.
   259  func splitNamespacedName(nameStr string) types.NamespacedName {
   260  	splitPoint := strings.IndexRune(nameStr, types.Separator)
   261  	if splitPoint == -1 {
   262  		return types.NamespacedName{Name: nameStr}
   263  	}
   264  	return types.NamespacedName{Namespace: nameStr[:splitPoint], Name: nameStr[splitPoint+1:]}
   265  }