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 }