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 }