Skip to content

Commit d87f38c

Browse files
authored
Merge pull request kubernetes#90548 from p0lyn0mial/wire-ctrl-dynamic-request-header-auth-provider
provides DynamicRequestHeaderController for dynamically filling RequestHeaderConfig struct
2 parents 8caddda + f83f4a8 commit d87f38c

File tree

6 files changed

+741
-53
lines changed

6 files changed

+741
-53
lines changed

staging/src/k8s.io/apiserver/pkg/authentication/request/headerrequest/BUILD

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,48 @@ load(
88

99
go_test(
1010
name = "go_default_test",
11-
srcs = ["requestheader_test.go"],
11+
srcs = [
12+
"requestheader_controller_test.go",
13+
"requestheader_test.go",
14+
],
1215
embed = [":go_default_library"],
13-
deps = ["//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library"],
16+
deps = [
17+
"//staging/src/k8s.io/api/core/v1:go_default_library",
18+
"//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library",
19+
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
20+
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
21+
"//staging/src/k8s.io/client-go/kubernetes/fake:go_default_library",
22+
"//staging/src/k8s.io/client-go/listers/core/v1:go_default_library",
23+
"//staging/src/k8s.io/client-go/tools/cache:go_default_library",
24+
],
1425
)
1526

1627
go_library(
1728
name = "go_default_library",
18-
srcs = ["requestheader.go"],
29+
srcs = [
30+
"requestheader.go",
31+
"requestheader_controller.go",
32+
],
1933
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/authentication/request/headerrequest",
2034
importpath = "k8s.io/apiserver/pkg/authentication/request/headerrequest",
2135
deps = [
36+
"//staging/src/k8s.io/api/core/v1:go_default_library",
37+
"//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library",
38+
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
39+
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
40+
"//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library",
41+
"//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
42+
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
2243
"//staging/src/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library",
2344
"//staging/src/k8s.io/apiserver/pkg/authentication/request/x509:go_default_library",
2445
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
46+
"//staging/src/k8s.io/client-go/informers/core/v1:go_default_library",
47+
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
48+
"//staging/src/k8s.io/client-go/listers/core/v1:go_default_library",
49+
"//staging/src/k8s.io/client-go/tools/cache:go_default_library",
2550
"//staging/src/k8s.io/client-go/util/cert:go_default_library",
51+
"//staging/src/k8s.io/client-go/util/workqueue:go_default_library",
52+
"//vendor/k8s.io/klog:go_default_library",
2653
],
2754
)
2855

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

0 commit comments

Comments
 (0)