k8s.io/apiserver@v0.31.1/pkg/server/dynamiccertificates/configmap_cafile_content.go (about)

     1  /*
     2  Copyright 2019 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 dynamiccertificates
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"crypto/x509"
    23  	"fmt"
    24  	"sync/atomic"
    25  	"time"
    26  
    27  	corev1 "k8s.io/api/core/v1"
    28  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/fields"
    30  	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    31  	"k8s.io/apimachinery/pkg/util/wait"
    32  	corev1informers "k8s.io/client-go/informers/core/v1"
    33  	"k8s.io/client-go/kubernetes"
    34  	corev1listers "k8s.io/client-go/listers/core/v1"
    35  	"k8s.io/client-go/tools/cache"
    36  	"k8s.io/client-go/util/workqueue"
    37  	"k8s.io/klog/v2"
    38  )
    39  
    40  // ConfigMapCAController provies a CAContentProvider that can dynamically react to configmap changes
    41  // It also fulfills the authenticator interface to provide verifyoptions
    42  type ConfigMapCAController struct {
    43  	name string
    44  
    45  	configmapLister    corev1listers.ConfigMapLister
    46  	configmapNamespace string
    47  	configmapName      string
    48  	configmapKey       string
    49  	// configMapInformer is tracked so that we can start these on Run
    50  	configMapInformer cache.SharedIndexInformer
    51  
    52  	// caBundle is a caBundleAndVerifier that contains the last read, non-zero length content of the file
    53  	caBundle atomic.Value
    54  
    55  	listeners []Listener
    56  
    57  	queue workqueue.TypedRateLimitingInterface[string]
    58  	// preRunCaches are the caches to sync before starting the work of this control loop
    59  	preRunCaches []cache.InformerSynced
    60  }
    61  
    62  var _ CAContentProvider = &ConfigMapCAController{}
    63  var _ ControllerRunner = &ConfigMapCAController{}
    64  
    65  // NewDynamicCAFromConfigMapController returns a CAContentProvider based on a configmap that automatically reloads content.
    66  // It is near-realtime via an informer.
    67  func NewDynamicCAFromConfigMapController(purpose, namespace, name, key string, kubeClient kubernetes.Interface) (*ConfigMapCAController, error) {
    68  	if len(purpose) == 0 {
    69  		return nil, fmt.Errorf("missing purpose for ca bundle")
    70  	}
    71  	if len(namespace) == 0 {
    72  		return nil, fmt.Errorf("missing namespace for ca bundle")
    73  	}
    74  	if len(name) == 0 {
    75  		return nil, fmt.Errorf("missing name for ca bundle")
    76  	}
    77  	if len(key) == 0 {
    78  		return nil, fmt.Errorf("missing key for ca bundle")
    79  	}
    80  	caContentName := fmt.Sprintf("%s::%s::%s::%s", purpose, namespace, name, key)
    81  
    82  	// we construct our own informer because we need such a small subset of the information available.  Just one namespace.
    83  	uncastConfigmapInformer := corev1informers.NewFilteredConfigMapInformer(kubeClient, namespace, 12*time.Hour, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, func(listOptions *v1.ListOptions) {
    84  		listOptions.FieldSelector = fields.OneTermEqualSelector("metadata.name", name).String()
    85  	})
    86  
    87  	configmapLister := corev1listers.NewConfigMapLister(uncastConfigmapInformer.GetIndexer())
    88  
    89  	c := &ConfigMapCAController{
    90  		name:               caContentName,
    91  		configmapNamespace: namespace,
    92  		configmapName:      name,
    93  		configmapKey:       key,
    94  		configmapLister:    configmapLister,
    95  		configMapInformer:  uncastConfigmapInformer,
    96  
    97  		queue: workqueue.NewTypedRateLimitingQueueWithConfig(
    98  			workqueue.DefaultTypedControllerRateLimiter[string](),
    99  			workqueue.TypedRateLimitingQueueConfig[string]{Name: fmt.Sprintf("DynamicConfigMapCABundle-%s", purpose)},
   100  		),
   101  		preRunCaches: []cache.InformerSynced{uncastConfigmapInformer.HasSynced},
   102  	}
   103  
   104  	uncastConfigmapInformer.AddEventHandler(cache.FilteringResourceEventHandler{
   105  		FilterFunc: func(obj interface{}) bool {
   106  			if cast, ok := obj.(*corev1.ConfigMap); ok {
   107  				return cast.Name == c.configmapName && cast.Namespace == c.configmapNamespace
   108  			}
   109  			if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok {
   110  				if cast, ok := tombstone.Obj.(*corev1.ConfigMap); ok {
   111  					return cast.Name == c.configmapName && cast.Namespace == c.configmapNamespace
   112  				}
   113  			}
   114  			return true // always return true just in case.  The checks are fairly cheap
   115  		},
   116  		Handler: cache.ResourceEventHandlerFuncs{
   117  			// we have a filter, so any time we're called, we may as well queue. We only ever check one configmap
   118  			// so we don't have to be choosy about our key.
   119  			AddFunc: func(obj interface{}) {
   120  				c.queue.Add(c.keyFn())
   121  			},
   122  			UpdateFunc: func(oldObj, newObj interface{}) {
   123  				c.queue.Add(c.keyFn())
   124  			},
   125  			DeleteFunc: func(obj interface{}) {
   126  				c.queue.Add(c.keyFn())
   127  			},
   128  		},
   129  	})
   130  
   131  	return c, nil
   132  }
   133  
   134  func (c *ConfigMapCAController) keyFn() string {
   135  	// this format matches DeletionHandlingMetaNamespaceKeyFunc for our single key
   136  	return c.configmapNamespace + "/" + c.configmapName
   137  }
   138  
   139  // AddListener adds a listener to be notified when the CA content changes.
   140  func (c *ConfigMapCAController) AddListener(listener Listener) {
   141  	c.listeners = append(c.listeners, listener)
   142  }
   143  
   144  // loadCABundle determines the next set of content for the file.
   145  func (c *ConfigMapCAController) loadCABundle() error {
   146  	configMap, err := c.configmapLister.ConfigMaps(c.configmapNamespace).Get(c.configmapName)
   147  	if err != nil {
   148  		return err
   149  	}
   150  	caBundle := configMap.Data[c.configmapKey]
   151  	if len(caBundle) == 0 {
   152  		return fmt.Errorf("missing content for CA bundle %q", c.Name())
   153  	}
   154  
   155  	// check to see if we have a change. If the values are the same, do nothing.
   156  	if !c.hasCAChanged([]byte(caBundle)) {
   157  		return nil
   158  	}
   159  
   160  	caBundleAndVerifier, err := newCABundleAndVerifier(c.Name(), []byte(caBundle))
   161  	if err != nil {
   162  		return err
   163  	}
   164  	c.caBundle.Store(caBundleAndVerifier)
   165  
   166  	for _, listener := range c.listeners {
   167  		listener.Enqueue()
   168  	}
   169  
   170  	return nil
   171  }
   172  
   173  // hasCAChanged returns true if the caBundle is different than the current.
   174  func (c *ConfigMapCAController) hasCAChanged(caBundle []byte) bool {
   175  	uncastExisting := c.caBundle.Load()
   176  	if uncastExisting == nil {
   177  		return true
   178  	}
   179  
   180  	// check to see if we have a change. If the values are the same, do nothing.
   181  	existing, ok := uncastExisting.(*caBundleAndVerifier)
   182  	if !ok {
   183  		return true
   184  	}
   185  	if !bytes.Equal(existing.caBundle, caBundle) {
   186  		return true
   187  	}
   188  
   189  	return false
   190  }
   191  
   192  // RunOnce runs a single sync loop
   193  func (c *ConfigMapCAController) RunOnce(ctx context.Context) error {
   194  	// Ignore the error when running once because when using a dynamically loaded ca file, because we think it's better to have nothing for
   195  	// a brief time than completely crash.  If crashing is necessary, higher order logic like a healthcheck and cause failures.
   196  	_ = c.loadCABundle()
   197  	return nil
   198  }
   199  
   200  // Run starts the kube-apiserver and blocks until stopCh is closed.
   201  func (c *ConfigMapCAController) Run(ctx context.Context, workers int) {
   202  	defer utilruntime.HandleCrash()
   203  	defer c.queue.ShutDown()
   204  
   205  	klog.InfoS("Starting controller", "name", c.name)
   206  	defer klog.InfoS("Shutting down controller", "name", c.name)
   207  
   208  	// we have a personal informer that is narrowly scoped, start it.
   209  	go c.configMapInformer.Run(ctx.Done())
   210  
   211  	// wait for your secondary caches to fill before starting your work
   212  	if !cache.WaitForNamedCacheSync(c.name, ctx.Done(), c.preRunCaches...) {
   213  		return
   214  	}
   215  
   216  	// doesn't matter what workers say, only start one.
   217  	go wait.Until(c.runWorker, time.Second, ctx.Done())
   218  
   219  	// start timer that rechecks every minute, just in case.  this also serves to prime the controller quickly.
   220  	go wait.PollImmediateUntil(FileRefreshDuration, func() (bool, error) {
   221  		c.queue.Add(workItemKey)
   222  		return false, nil
   223  	}, ctx.Done())
   224  
   225  	<-ctx.Done()
   226  }
   227  
   228  func (c *ConfigMapCAController) runWorker() {
   229  	for c.processNextWorkItem() {
   230  	}
   231  }
   232  
   233  func (c *ConfigMapCAController) processNextWorkItem() bool {
   234  	dsKey, quit := c.queue.Get()
   235  	if quit {
   236  		return false
   237  	}
   238  	defer c.queue.Done(dsKey)
   239  
   240  	err := c.loadCABundle()
   241  	if err == nil {
   242  		c.queue.Forget(dsKey)
   243  		return true
   244  	}
   245  
   246  	utilruntime.HandleError(fmt.Errorf("%v failed with : %v", dsKey, err))
   247  	c.queue.AddRateLimited(dsKey)
   248  
   249  	return true
   250  }
   251  
   252  // Name is just an identifier
   253  func (c *ConfigMapCAController) Name() string {
   254  	return c.name
   255  }
   256  
   257  // CurrentCABundleContent provides ca bundle byte content
   258  func (c *ConfigMapCAController) CurrentCABundleContent() []byte {
   259  	uncastObj := c.caBundle.Load()
   260  	if uncastObj == nil {
   261  		return nil // this can happen if we've been unable load data from the apiserver for some reason
   262  	}
   263  
   264  	return c.caBundle.Load().(*caBundleAndVerifier).caBundle
   265  }
   266  
   267  // VerifyOptions provides verifyoptions compatible with authenticators
   268  func (c *ConfigMapCAController) VerifyOptions() (x509.VerifyOptions, bool) {
   269  	uncastObj := c.caBundle.Load()
   270  	if uncastObj == nil {
   271  		// This can happen if we've been unable load data from the apiserver for some reason.
   272  		// In this case, we should not accept any connections on the basis of this ca bundle.
   273  		return x509.VerifyOptions{}, false
   274  	}
   275  
   276  	return uncastObj.(*caBundleAndVerifier).verifyOptions, true
   277  }