k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/controller/certificates/rootcacertpublisher/publisher.go (about)

     1  /*
     2  Copyright 2018 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 rootcacertpublisher
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"reflect"
    23  	"time"
    24  
    25  	v1 "k8s.io/api/core/v1"
    26  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    29  	"k8s.io/apimachinery/pkg/util/wait"
    30  	coreinformers "k8s.io/client-go/informers/core/v1"
    31  	clientset "k8s.io/client-go/kubernetes"
    32  	corelisters "k8s.io/client-go/listers/core/v1"
    33  	"k8s.io/client-go/tools/cache"
    34  	"k8s.io/client-go/util/workqueue"
    35  	"k8s.io/klog/v2"
    36  )
    37  
    38  // RootCACertConfigMapName is name of the configmap which stores certificates
    39  // to access api-server
    40  const (
    41  	RootCACertConfigMapName = "kube-root-ca.crt"
    42  	DescriptionAnnotation   = "kubernetes.io/description"
    43  	Description             = "Contains a CA bundle that can be used to verify the kube-apiserver when using internal endpoints such as the internal service IP or kubernetes.default.svc. " +
    44  		"No other usage is guaranteed across distributions of Kubernetes clusters."
    45  )
    46  
    47  func init() {
    48  	registerMetrics()
    49  }
    50  
    51  // NewPublisher construct a new controller which would manage the configmap
    52  // which stores certificates in each namespace. It will make sure certificate
    53  // configmap exists in each namespace.
    54  func NewPublisher(cmInformer coreinformers.ConfigMapInformer, nsInformer coreinformers.NamespaceInformer, cl clientset.Interface, rootCA []byte) (*Publisher, error) {
    55  	e := &Publisher{
    56  		client: cl,
    57  		rootCA: rootCA,
    58  		queue: workqueue.NewTypedRateLimitingQueueWithConfig(
    59  			workqueue.DefaultTypedControllerRateLimiter[string](),
    60  			workqueue.TypedRateLimitingQueueConfig[string]{
    61  				Name: "root_ca_cert_publisher",
    62  			},
    63  		),
    64  	}
    65  
    66  	cmInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
    67  		DeleteFunc: e.configMapDeleted,
    68  		UpdateFunc: e.configMapUpdated,
    69  	})
    70  	e.cmLister = cmInformer.Lister()
    71  	e.cmListerSynced = cmInformer.Informer().HasSynced
    72  
    73  	nsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
    74  		AddFunc:    e.namespaceAdded,
    75  		UpdateFunc: e.namespaceUpdated,
    76  	})
    77  	e.nsListerSynced = nsInformer.Informer().HasSynced
    78  
    79  	e.syncHandler = e.syncNamespace
    80  
    81  	return e, nil
    82  
    83  }
    84  
    85  // Publisher manages certificate ConfigMap objects inside Namespaces
    86  type Publisher struct {
    87  	client clientset.Interface
    88  	rootCA []byte
    89  
    90  	// To allow injection for testing.
    91  	syncHandler func(ctx context.Context, key string) error
    92  
    93  	cmLister       corelisters.ConfigMapLister
    94  	cmListerSynced cache.InformerSynced
    95  
    96  	nsListerSynced cache.InformerSynced
    97  
    98  	queue workqueue.TypedRateLimitingInterface[string]
    99  }
   100  
   101  // Run starts process
   102  func (c *Publisher) Run(ctx context.Context, workers int) {
   103  	defer utilruntime.HandleCrash()
   104  	defer c.queue.ShutDown()
   105  
   106  	logger := klog.FromContext(ctx)
   107  	logger.Info("Starting root CA cert publisher controller")
   108  	defer logger.Info("Shutting down root CA cert publisher controller")
   109  
   110  	if !cache.WaitForNamedCacheSync("crt configmap", ctx.Done(), c.cmListerSynced) {
   111  		return
   112  	}
   113  
   114  	for i := 0; i < workers; i++ {
   115  		go wait.UntilWithContext(ctx, c.runWorker, time.Second)
   116  	}
   117  
   118  	<-ctx.Done()
   119  }
   120  
   121  func (c *Publisher) configMapDeleted(obj interface{}) {
   122  	cm, err := convertToCM(obj)
   123  	if err != nil {
   124  		utilruntime.HandleError(err)
   125  		return
   126  	}
   127  	if cm.Name != RootCACertConfigMapName {
   128  		return
   129  	}
   130  	c.queue.Add(cm.Namespace)
   131  }
   132  
   133  func (c *Publisher) configMapUpdated(_, newObj interface{}) {
   134  	cm, err := convertToCM(newObj)
   135  	if err != nil {
   136  		utilruntime.HandleError(err)
   137  		return
   138  	}
   139  	if cm.Name != RootCACertConfigMapName {
   140  		return
   141  	}
   142  	c.queue.Add(cm.Namespace)
   143  }
   144  
   145  func (c *Publisher) namespaceAdded(obj interface{}) {
   146  	namespace := obj.(*v1.Namespace)
   147  	c.queue.Add(namespace.Name)
   148  }
   149  
   150  func (c *Publisher) namespaceUpdated(oldObj interface{}, newObj interface{}) {
   151  	newNamespace := newObj.(*v1.Namespace)
   152  	if newNamespace.Status.Phase != v1.NamespaceActive {
   153  		return
   154  	}
   155  	c.queue.Add(newNamespace.Name)
   156  }
   157  
   158  func (c *Publisher) runWorker(ctx context.Context) {
   159  	for c.processNextWorkItem(ctx) {
   160  	}
   161  }
   162  
   163  // processNextWorkItem deals with one key off the queue. It returns false when
   164  // it's time to quit.
   165  func (c *Publisher) processNextWorkItem(ctx context.Context) bool {
   166  	key, quit := c.queue.Get()
   167  	if quit {
   168  		return false
   169  	}
   170  	defer c.queue.Done(key)
   171  
   172  	if err := c.syncHandler(ctx, key); err != nil {
   173  		utilruntime.HandleError(fmt.Errorf("syncing %q failed: %v", key, err))
   174  		c.queue.AddRateLimited(key)
   175  		return true
   176  	}
   177  
   178  	c.queue.Forget(key)
   179  	return true
   180  }
   181  
   182  func (c *Publisher) syncNamespace(ctx context.Context, ns string) (err error) {
   183  	startTime := time.Now()
   184  	defer func() {
   185  		recordMetrics(startTime, err)
   186  		klog.FromContext(ctx).V(4).Info("Finished syncing namespace", "namespace", ns, "elapsedTime", time.Since(startTime))
   187  	}()
   188  
   189  	cm, err := c.cmLister.ConfigMaps(ns).Get(RootCACertConfigMapName)
   190  	switch {
   191  	case apierrors.IsNotFound(err):
   192  		_, err = c.client.CoreV1().ConfigMaps(ns).Create(ctx, &v1.ConfigMap{
   193  			ObjectMeta: metav1.ObjectMeta{
   194  				Name:        RootCACertConfigMapName,
   195  				Annotations: map[string]string{DescriptionAnnotation: Description},
   196  			},
   197  			Data: map[string]string{
   198  				"ca.crt": string(c.rootCA),
   199  			},
   200  		}, metav1.CreateOptions{})
   201  		// don't retry a create if the namespace doesn't exist or is terminating
   202  		if apierrors.IsNotFound(err) || apierrors.HasStatusCause(err, v1.NamespaceTerminatingCause) {
   203  			return nil
   204  		}
   205  		return err
   206  	case err != nil:
   207  		return err
   208  	}
   209  
   210  	data := map[string]string{
   211  		"ca.crt": string(c.rootCA),
   212  	}
   213  
   214  	// ensure the data and the one annotation describing usage of this configmap match.
   215  	if reflect.DeepEqual(cm.Data, data) && len(cm.Annotations[DescriptionAnnotation]) > 0 {
   216  		return nil
   217  	}
   218  
   219  	// copy so we don't modify the cache's instance of the configmap
   220  	cm = cm.DeepCopy()
   221  	cm.Data = data
   222  	if cm.Annotations == nil {
   223  		cm.Annotations = map[string]string{}
   224  	}
   225  	cm.Annotations[DescriptionAnnotation] = Description
   226  
   227  	_, err = c.client.CoreV1().ConfigMaps(ns).Update(ctx, cm, metav1.UpdateOptions{})
   228  	return err
   229  }
   230  
   231  func convertToCM(obj interface{}) (*v1.ConfigMap, error) {
   232  	cm, ok := obj.(*v1.ConfigMap)
   233  	if !ok {
   234  		tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
   235  		if !ok {
   236  			return nil, fmt.Errorf("couldn't get object from tombstone %#v", obj)
   237  		}
   238  		cm, ok = tombstone.Obj.(*v1.ConfigMap)
   239  		if !ok {
   240  			return nil, fmt.Errorf("tombstone contained object that is not a ConfigMap %#v", obj)
   241  		}
   242  	}
   243  	return cm, nil
   244  }