sigs.k8s.io/cluster-api@v1.7.1/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 err
   163  	}
   164  
   165  	options = append(options, patch.WithOwnedConditions{Conditions: []clusterv1.ConditionType{
   166  		runtimev1.RuntimeExtensionDiscoveredCondition,
   167  	}})
   168  	return patchHelper.Patch(ctx, modified, options...)
   169  }
   170  
   171  // reconcileDelete will remove the ExtensionConfig from the registry on deletion of the object. Note this is a best
   172  // effort deletion that may not catch all cases.
   173  func (r *Reconciler) reconcileDelete(ctx context.Context, extensionConfig *runtimev1.ExtensionConfig) (ctrl.Result, error) {
   174  	log := ctrl.LoggerFrom(ctx)
   175  	log.Info("Unregistering ExtensionConfig information from registry")
   176  	if err := r.RuntimeClient.Unregister(extensionConfig); err != nil {
   177  		return ctrl.Result{}, errors.Wrapf(err, "failed to unregister %s", tlog.KObj{Obj: extensionConfig})
   178  	}
   179  	return ctrl.Result{}, nil
   180  }
   181  
   182  // secretToExtensionConfig maps a secret to ExtensionConfigs with the corresponding InjectCAFromSecretAnnotation
   183  // to reconcile them on updates of the secrets.
   184  func (r *Reconciler) secretToExtensionConfig(ctx context.Context, secret client.Object) []reconcile.Request {
   185  	result := []ctrl.Request{}
   186  
   187  	extensionConfigs := runtimev1.ExtensionConfigList{}
   188  	indexKey := secret.GetNamespace() + "/" + secret.GetName()
   189  
   190  	if err := r.Client.List(
   191  		ctx,
   192  		&extensionConfigs,
   193  		client.MatchingFields{injectCAFromSecretAnnotationField: indexKey},
   194  	); err != nil {
   195  		return nil
   196  	}
   197  
   198  	for _, ext := range extensionConfigs.Items {
   199  		result = append(result, ctrl.Request{NamespacedName: client.ObjectKey{Name: ext.Name}})
   200  	}
   201  
   202  	return result
   203  }
   204  
   205  // discoverExtensionConfig attempts to discover the Handlers for an ExtensionConfig.
   206  // If discovery succeeds it returns the ExtensionConfig with Handlers updated in Status and an updated Condition.
   207  // If discovery fails it returns the ExtensionConfig with no update to Handlers and a Failed Condition.
   208  func discoverExtensionConfig(ctx context.Context, runtimeClient runtimeclient.Client, extensionConfig *runtimev1.ExtensionConfig) (*runtimev1.ExtensionConfig, error) {
   209  	discoveredExtension, err := runtimeClient.Discover(ctx, extensionConfig.DeepCopy())
   210  	if err != nil {
   211  		modifiedExtensionConfig := extensionConfig.DeepCopy()
   212  		conditions.MarkFalse(modifiedExtensionConfig, runtimev1.RuntimeExtensionDiscoveredCondition, runtimev1.DiscoveryFailedReason, clusterv1.ConditionSeverityError, "error in discovery: %v", err)
   213  		return modifiedExtensionConfig, errors.Wrapf(err, "failed to discover %s", tlog.KObj{Obj: extensionConfig})
   214  	}
   215  
   216  	conditions.MarkTrue(discoveredExtension, runtimev1.RuntimeExtensionDiscoveredCondition)
   217  	return discoveredExtension, nil
   218  }
   219  
   220  // reconcileCABundle reconciles the CA bundle for the ExtensionConfig.
   221  // Note: This was implemented to behave similar to the cert-manager cainjector.
   222  // We couldn't use the cert-manager cainjector because it doesn't work with CustomResources.
   223  func reconcileCABundle(ctx context.Context, client client.Client, config *runtimev1.ExtensionConfig) error {
   224  	log := ctrl.LoggerFrom(ctx)
   225  
   226  	secretNameRaw, ok := config.Annotations[runtimev1.InjectCAFromSecretAnnotation]
   227  	if !ok {
   228  		return nil
   229  	}
   230  	secretName := splitNamespacedName(secretNameRaw)
   231  
   232  	log.Info(fmt.Sprintf("Injecting CA Bundle into ExtensionConfig from secret %q", secretNameRaw))
   233  
   234  	if secretName.Namespace == "" || secretName.Name == "" {
   235  		return errors.Errorf("failed to reconcile caBundle: secret name %q must be in the form <namespace>/<name>", secretNameRaw)
   236  	}
   237  
   238  	var secret corev1.Secret
   239  	// Note: this is an expensive API call because secrets are explicitly not cached.
   240  	if err := client.Get(ctx, secretName, &secret); err != nil {
   241  		return errors.Wrapf(err, "failed to reconcile caBundle: failed to get secret %q", secretNameRaw)
   242  	}
   243  
   244  	caData, hasCAData := secret.Data[tlsCAKey]
   245  	if !hasCAData {
   246  		return errors.Errorf("failed to reconcile caBundle: secret %s does not contain a %q entry", secretNameRaw, tlsCAKey)
   247  	}
   248  
   249  	config.Spec.ClientConfig.CABundle = caData
   250  	return nil
   251  }
   252  
   253  // splitNamespacedName turns the string form of a namespaced name
   254  // (<namespace>/<name>) into a types.NamespacedName.
   255  func splitNamespacedName(nameStr string) types.NamespacedName {
   256  	splitPoint := strings.IndexRune(nameStr, types.Separator)
   257  	if splitPoint == -1 {
   258  		return types.NamespacedName{Name: nameStr}
   259  	}
   260  	return types.NamespacedName{Namespace: nameStr[:splitPoint], Name: nameStr[splitPoint+1:]}
   261  }