sigs.k8s.io/cluster-api@v1.7.1/exp/runtime/internal/controllers/warmup.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  	"time"
    22  
    23  	"github.com/pkg/errors"
    24  	kerrors "k8s.io/apimachinery/pkg/util/errors"
    25  	"k8s.io/apimachinery/pkg/util/wait"
    26  	"k8s.io/klog/v2"
    27  	ctrl "sigs.k8s.io/controller-runtime"
    28  	"sigs.k8s.io/controller-runtime/pkg/client"
    29  	"sigs.k8s.io/controller-runtime/pkg/manager"
    30  
    31  	runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1"
    32  	runtimeclient "sigs.k8s.io/cluster-api/internal/runtime/client"
    33  )
    34  
    35  const (
    36  	defaultWarmupTimeout  = 60 * time.Second
    37  	defaultWarmupInterval = 2 * time.Second
    38  )
    39  
    40  var _ manager.LeaderElectionRunnable = &warmupRunnable{}
    41  
    42  // warmupRunnable is a controller runtime LeaderElectionRunnable. It warms up the registry on controller start.
    43  type warmupRunnable struct {
    44  	Client         client.Client
    45  	APIReader      client.Reader
    46  	RuntimeClient  runtimeclient.Client
    47  	warmupTimeout  time.Duration
    48  	warmupInterval time.Duration
    49  }
    50  
    51  // NeedLeaderElection satisfies the controller runtime LeaderElectionRunnable interface.
    52  // This ensures we warm up the RuntimeSDK registry only after the controller became leader.
    53  // Note: Only after the warmupRunnable is completed the registry becomes ready and thus
    54  // all controllers using the runtime client or registry will wait until warmup is completed.
    55  func (r *warmupRunnable) NeedLeaderElection() bool {
    56  	return true
    57  }
    58  
    59  // Start attempts to warm up the registry. It will retry for 60 seconds before returning an error. An error on Start will
    60  // cause the CAPI controller manager to fail.
    61  // We are retrying for 60 seconds to mitigate failures when the CAPI controller manager and RuntimeExtensions
    62  // are started at the same time. After 60 seconds we crash the entire controller to surface the
    63  // issue to users in a timely fashion as it would block reconciliation of all Clusters.
    64  func (r *warmupRunnable) Start(ctx context.Context) error {
    65  	log := ctrl.LoggerFrom(ctx)
    66  	if r.warmupInterval == 0 {
    67  		r.warmupInterval = defaultWarmupInterval
    68  	}
    69  	if r.warmupTimeout == 0 {
    70  		r.warmupTimeout = defaultWarmupTimeout
    71  	}
    72  	ctx, cancel := context.WithTimeout(ctx, r.warmupTimeout)
    73  	defer cancel()
    74  
    75  	err := wait.PollUntilContextTimeout(ctx, r.warmupInterval, r.warmupTimeout, true, func(ctx context.Context) (done bool, err error) {
    76  		if err = warmupRegistry(ctx, r.Client, r.APIReader, r.RuntimeClient); err != nil {
    77  			log.Error(err, "ExtensionConfig registry warmup failed")
    78  			return false, nil
    79  		}
    80  		return true, nil
    81  	})
    82  
    83  	if err != nil {
    84  		return errors.Wrapf(err, "ExtensionConfig registry warmup timed out after %s", r.warmupTimeout.String())
    85  	}
    86  
    87  	return nil
    88  }
    89  
    90  // warmupRegistry attempts to discover all existing ExtensionConfigs and patch their status with discovered Handlers.
    91  // It warms up the registry by passing it the up-to-date list of ExtensionConfigs.
    92  func warmupRegistry(ctx context.Context, client client.Client, reader client.Reader, runtimeClient runtimeclient.Client) error {
    93  	log := ctrl.LoggerFrom(ctx)
    94  
    95  	var errs []error
    96  
    97  	extensionConfigList := runtimev1.ExtensionConfigList{}
    98  	if err := reader.List(ctx, &extensionConfigList); err != nil {
    99  		return errors.Wrapf(err, "failed to list ExtensionConfigs")
   100  	}
   101  
   102  	for i := range extensionConfigList.Items {
   103  		extensionConfig := &extensionConfigList.Items[i]
   104  		original := extensionConfig.DeepCopy()
   105  
   106  		log := log.WithValues("ExtensionConfig", klog.KObj(extensionConfig))
   107  		ctx := ctrl.LoggerInto(ctx, log)
   108  
   109  		// Inject CABundle from secret if annotation is set. Otherwise https calls may fail.
   110  		if err := reconcileCABundle(ctx, client, extensionConfig); err != nil {
   111  			errs = append(errs, err)
   112  			// Note: we continue here because if reconcileCABundle doesn't work discovery will fail as well.
   113  			continue
   114  		}
   115  
   116  		extensionConfig, err := discoverExtensionConfig(ctx, runtimeClient, extensionConfig)
   117  		if err != nil {
   118  			errs = append(errs, err)
   119  		}
   120  
   121  		// Always patch the ExtensionConfig as it may contain updates in conditions or clientConfig.caBundle.
   122  		if err = patchExtensionConfig(ctx, client, original, extensionConfig); err != nil {
   123  			errs = append(errs, err)
   124  		}
   125  		extensionConfigList.Items[i] = *extensionConfig
   126  	}
   127  
   128  	// If there was some error in discovery or patching return before committing to the Registry.
   129  	if len(errs) != 0 {
   130  		return kerrors.NewAggregate(errs)
   131  	}
   132  
   133  	if err := runtimeClient.WarmUp(&extensionConfigList); err != nil {
   134  		return err
   135  	}
   136  
   137  	log.Info("The extension registry is warmed up")
   138  
   139  	return nil
   140  }