Skip to content

Commit 6e0211b

Browse files
committed
provides RequestHeaderAuthRequestController for dynamically filling RequestHeaderConfig struct
1 parent d3d870f commit 6e0211b

File tree

2 files changed

+603
-0
lines changed

2 files changed

+603
-0
lines changed
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
/*
2+
Copyright 2020 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 headerrequest
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"fmt"
23+
"time"
24+
25+
corev1 "k8s.io/api/core/v1"
26+
"k8s.io/apimachinery/pkg/api/equality"
27+
"k8s.io/apimachinery/pkg/api/errors"
28+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
30+
"k8s.io/apimachinery/pkg/util/wait"
31+
coreinformers "k8s.io/client-go/informers/core/v1"
32+
"k8s.io/client-go/kubernetes"
33+
corev1listers "k8s.io/client-go/listers/core/v1"
34+
"k8s.io/client-go/tools/cache"
35+
"k8s.io/client-go/util/workqueue"
36+
"k8s.io/klog"
37+
"sync/atomic"
38+
)
39+
40+
const (
41+
authenticationRoleName = "extension-apiserver-authentication-reader"
42+
)
43+
44+
type requestHeaderBundle struct {
45+
UsernameHeaders []string
46+
GroupHeaders []string
47+
ExtraHeaderPrefixes []string
48+
AllowedClientNames []string
49+
}
50+
51+
// RequestHeaderAuthRequestController a controller that exposes a set of methods for dynamically filling RequestHeaderConfig struct.
52+
// The methods are sourced from the config map which is being monitored by this controller.
53+
// The controller is primed from the server at the construction time for components that don't want to dynamically react to changes
54+
// in the config map.
55+
type RequestHeaderAuthRequestController struct {
56+
name string
57+
58+
configmapName string
59+
configmapNamespace string
60+
61+
configmapLister corev1listers.ConfigMapNamespaceLister
62+
configmapInformerSynced cache.InformerSynced
63+
64+
queue workqueue.RateLimitingInterface
65+
66+
// exportedRequestHeaderBundle is a requestHeaderBundle that contains the last read, non-zero length content of the configmap
67+
exportedRequestHeaderBundle atomic.Value
68+
69+
usernameHeadersKey string
70+
groupHeadersKey string
71+
extraHeaderPrefixesKey string
72+
allowedClientNamesKey string
73+
}
74+
75+
// NewRequestHeaderAuthRequestController creates a new controller that implements RequestHeaderAuthRequestController
76+
func NewRequestHeaderAuthRequestController(
77+
cmName string,
78+
cmNamespace string,
79+
client kubernetes.Interface,
80+
cmInformer coreinformers.ConfigMapInformer,
81+
usernameHeadersKey, groupHeadersKey, extraHeaderPrefixesKey, allowedClientNamesKey string) (*RequestHeaderAuthRequestController, error) {
82+
c := &RequestHeaderAuthRequestController{
83+
name: "RequestHeaderAuthRequestController",
84+
85+
configmapName: cmName,
86+
configmapNamespace: cmNamespace,
87+
88+
usernameHeadersKey: usernameHeadersKey,
89+
groupHeadersKey: groupHeadersKey,
90+
extraHeaderPrefixesKey: extraHeaderPrefixesKey,
91+
allowedClientNamesKey: allowedClientNamesKey,
92+
93+
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "RequestHeaderAuthRequestController"),
94+
}
95+
96+
// use the live client to prime the controller
97+
if err := c.syncOnce(client); err != nil {
98+
return nil, err
99+
}
100+
101+
cmInformer.Informer().AddEventHandler(cache.FilteringResourceEventHandler{
102+
FilterFunc: func(obj interface{}) bool {
103+
if cast, ok := obj.(*corev1.ConfigMap); ok {
104+
return cast.Name == c.configmapName && cast.Namespace == c.configmapNamespace
105+
}
106+
if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok {
107+
if cast, ok := tombstone.Obj.(*corev1.ConfigMap); ok {
108+
return cast.Name == c.configmapName && cast.Namespace == c.configmapNamespace
109+
}
110+
}
111+
return true // always return true just in case. The checks are fairly cheap
112+
},
113+
Handler: cache.ResourceEventHandlerFuncs{
114+
// we have a filter, so any time we're called, we may as well queue. We only ever check one configmap
115+
// so we don't have to be choosy about our key.
116+
AddFunc: func(obj interface{}) {
117+
c.queue.Add(c.keyFn())
118+
},
119+
UpdateFunc: func(oldObj, newObj interface{}) {
120+
c.queue.Add(c.keyFn())
121+
},
122+
DeleteFunc: func(obj interface{}) {
123+
c.queue.Add(c.keyFn())
124+
},
125+
},
126+
})
127+
128+
c.configmapLister = cmInformer.Lister().ConfigMaps(c.configmapNamespace)
129+
c.configmapInformerSynced = cmInformer.Informer().HasSynced
130+
131+
return c, nil
132+
}
133+
134+
func (c *RequestHeaderAuthRequestController) UsernameHeaders() []string {
135+
return c.loadRequestHeaderFor(c.usernameHeadersKey)
136+
}
137+
138+
func (c *RequestHeaderAuthRequestController) GroupHeaders() []string {
139+
return c.loadRequestHeaderFor(c.groupHeadersKey)
140+
}
141+
142+
func (c *RequestHeaderAuthRequestController) ExtraHeaderPrefixes() []string {
143+
return c.loadRequestHeaderFor(c.extraHeaderPrefixesKey)
144+
}
145+
146+
func (c *RequestHeaderAuthRequestController) AllowedClientNames() []string {
147+
return c.loadRequestHeaderFor(c.allowedClientNamesKey)
148+
}
149+
150+
// Run starts RequestHeaderAuthRequestController controller and blocks until stopCh is closed.
151+
func (c *RequestHeaderAuthRequestController) Run(workers int, stopCh <-chan struct{}) {
152+
defer utilruntime.HandleCrash()
153+
defer c.queue.ShutDown()
154+
155+
klog.Infof("Starting %s", c.name)
156+
defer klog.Infof("Shutting down %s", c.name)
157+
158+
// wait for caches to fill before starting your work
159+
if !cache.WaitForNamedCacheSync(c.name, stopCh, c.configmapInformerSynced) {
160+
return
161+
}
162+
163+
// doesn't matter what workers say, only start one.
164+
go wait.Until(c.runWorker, time.Second, stopCh)
165+
166+
<-stopCh
167+
}
168+
169+
func (c *RequestHeaderAuthRequestController) runWorker() {
170+
for c.processNextWorkItem() {
171+
}
172+
}
173+
174+
func (c *RequestHeaderAuthRequestController) processNextWorkItem() bool {
175+
dsKey, quit := c.queue.Get()
176+
if quit {
177+
return false
178+
}
179+
defer c.queue.Done(dsKey)
180+
181+
err := c.sync()
182+
if err == nil {
183+
c.queue.Forget(dsKey)
184+
return true
185+
}
186+
187+
utilruntime.HandleError(fmt.Errorf("%v failed with : %v", dsKey, err))
188+
c.queue.AddRateLimited(dsKey)
189+
190+
return true
191+
}
192+
193+
func (c *RequestHeaderAuthRequestController) syncOnce(client kubernetes.Interface) error {
194+
configMap, err := client.CoreV1().ConfigMaps(c.configmapNamespace).Get(context.TODO(), c.configmapName, metav1.GetOptions{})
195+
switch {
196+
case errors.IsNotFound(err):
197+
// ignore, authConfigMap is nil now
198+
return nil
199+
case errors.IsForbidden(err):
200+
klog.Warningf("Unable to get configmap/%s in %s. Usually fixed by "+
201+
"'kubectl create rolebinding -n %s ROLEBINDING_NAME --role=%s --serviceaccount=YOUR_NS:YOUR_SA'",
202+
c.configmapName, c.configmapNamespace, c.configmapNamespace, authenticationRoleName)
203+
return err
204+
case err != nil:
205+
return err
206+
}
207+
return c.syncConfigMap(configMap)
208+
}
209+
210+
// sync reads the config and propagates the changes to exportedRequestHeaderBundle
211+
// which is exposed by the set of methods that are used to fill RequestHeaderConfig struct
212+
func (c *RequestHeaderAuthRequestController) sync() error {
213+
configMap, err := c.configmapLister.Get(c.configmapName)
214+
if err != nil {
215+
return err
216+
}
217+
return c.syncConfigMap(configMap)
218+
}
219+
220+
func (c *RequestHeaderAuthRequestController) syncConfigMap(configMap *corev1.ConfigMap) error {
221+
hasChanged, newRequestHeaderBundle, err := c.hasRequestHeaderBundleChanged(configMap)
222+
if err != nil {
223+
return err
224+
}
225+
if hasChanged {
226+
c.exportedRequestHeaderBundle.Store(newRequestHeaderBundle)
227+
}
228+
return nil
229+
}
230+
231+
func (c *RequestHeaderAuthRequestController) hasRequestHeaderBundleChanged(cm *corev1.ConfigMap) (bool, *requestHeaderBundle, error) {
232+
currentHeadersBundle, err := c.getRequestHeaderBundleFromConfigMap(cm)
233+
if err != nil {
234+
return false, nil, err
235+
}
236+
237+
rawHeaderBundle := c.exportedRequestHeaderBundle.Load()
238+
if rawHeaderBundle == nil {
239+
return true, currentHeadersBundle, nil
240+
}
241+
242+
// check to see if we have a change. If the values are the same, do nothing.
243+
loadedHeadersBundle, ok := rawHeaderBundle.(*requestHeaderBundle)
244+
if !ok {
245+
return true, currentHeadersBundle, nil
246+
}
247+
248+
if !equality.Semantic.DeepEqual(loadedHeadersBundle, currentHeadersBundle) {
249+
return true, currentHeadersBundle, nil
250+
}
251+
return false, nil, nil
252+
}
253+
254+
func (c *RequestHeaderAuthRequestController) getRequestHeaderBundleFromConfigMap(cm *corev1.ConfigMap) (*requestHeaderBundle, error) {
255+
usernameHeaderCurrentValue, err := deserializeStrings(cm.Data[c.usernameHeadersKey])
256+
if err != nil {
257+
return nil, err
258+
}
259+
260+
groupHeadersCurrentValue, err := deserializeStrings(cm.Data[c.groupHeadersKey])
261+
if err != nil {
262+
return nil, err
263+
}
264+
265+
extraHeaderPrefixesCurrentValue, err := deserializeStrings(cm.Data[c.extraHeaderPrefixesKey])
266+
if err != nil {
267+
return nil, err
268+
269+
}
270+
271+
allowedClientNamesCurrentValue, err := deserializeStrings(cm.Data[c.allowedClientNamesKey])
272+
if err != nil {
273+
return nil, err
274+
}
275+
276+
return &requestHeaderBundle{
277+
UsernameHeaders: usernameHeaderCurrentValue,
278+
GroupHeaders: groupHeadersCurrentValue,
279+
ExtraHeaderPrefixes: extraHeaderPrefixesCurrentValue,
280+
AllowedClientNames: allowedClientNamesCurrentValue,
281+
}, nil
282+
}
283+
284+
func (c *RequestHeaderAuthRequestController) loadRequestHeaderFor(key string) []string {
285+
rawHeaderBundle := c.exportedRequestHeaderBundle.Load()
286+
if rawHeaderBundle == nil {
287+
return nil // this can happen if we've been unable load data from the apiserver for some reason
288+
}
289+
headerBundle := rawHeaderBundle.(*requestHeaderBundle)
290+
291+
switch key {
292+
case c.usernameHeadersKey:
293+
return headerBundle.UsernameHeaders
294+
case c.groupHeadersKey:
295+
return headerBundle.GroupHeaders
296+
case c.extraHeaderPrefixesKey:
297+
return headerBundle.ExtraHeaderPrefixes
298+
case c.allowedClientNamesKey:
299+
return headerBundle.AllowedClientNames
300+
default:
301+
return nil
302+
}
303+
}
304+
305+
func (c *RequestHeaderAuthRequestController) keyFn() string {
306+
// this format matches DeletionHandlingMetaNamespaceKeyFunc for our single key
307+
return c.configmapNamespace + "/" + c.configmapName
308+
}
309+
310+
func deserializeStrings(in string) ([]string, error) {
311+
if len(in) == 0 {
312+
return nil, nil
313+
}
314+
var ret []string
315+
if err := json.Unmarshal([]byte(in), &ret); err != nil {
316+
return nil, err
317+
}
318+
return ret, nil
319+
}

0 commit comments

Comments
 (0)