Skip to content

Commit ac86d8e

Browse files
fix(controller): separate proxy from controller on leader-election (#747)
* fix(controller): separate proxy from controller on leader-election Signed-off-by: Oliver Bähler <[email protected]> * fix(controller): separate proxy from controller on leader-election Signed-off-by: Oliver Bähler <[email protected]> --------- Signed-off-by: Oliver Bähler <[email protected]>
1 parent 9473576 commit ac86d8e

File tree

8 files changed

+154
-122
lines changed

8 files changed

+154
-122
lines changed

charts/capsule-proxy/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ If you only need to make minor customizations, you can specify them on the comma
183183
| options.extraArgs | list | `[]` | A list of extra arguments to add to the capsule-proxy. |
184184
| options.generateCertificates | bool | `true` | Specify if capsule-proxy will generate self-signed SSL certificates |
185185
| options.ignoredUserGroups | list | `[]` | Define which groups must be ignored while proxying requests |
186+
| options.leaderElection | bool | `false` | Set leader election to true if you are running n-replicas |
186187
| options.listeningPort | int | `9001` | Set the listening port of the capsule-proxy |
187188
| options.logLevel | string | `"4"` | Set the log verbosity of the capsule-proxy with a value from 1 to 10 |
188189
| options.oidcUsernameClaim | string | `"preferred_username"` | Specify if capsule-proxy will use SSL |

charts/capsule-proxy/ci/deploy-values.yaml

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ rbac:
66
extra: annotation
77
labels:
88
extra: label
9-
kind: DaemonSet
9+
kind: Deployment
1010
imagePullSecrets: []
1111
certManager:
1212
generateCertificates: true
@@ -19,7 +19,6 @@ certManager:
1919
name: "" # Name of the ClusterIssuer
2020
replicaCount: 1
2121
podAnnotations:
22-
scheduler.alpha.kubernetes.io/critical-pod: ''
2322
extra: annotation
2423
podLabels:
2524
extra: label
@@ -44,7 +43,7 @@ autoscaling:
4443
example: annotation
4544
labels:
4645
example: label
47-
minReplicas: 1
46+
minReplicas: 3
4847
maxReplicas: 5
4948
targetCPUUtilizationPercentage: 80
5049
metrics:
@@ -64,19 +63,11 @@ autoscaling:
6463
- type: Percent
6564
value: 10
6665
periodSeconds: 60
67-
nodeSelector:
68-
node-role.kubernetes.io/master: ""
69-
tolerations:
70-
- key: CriticalAddonsOnly
71-
operator: Exists
72-
- effect: NoSchedule
73-
key: node-role.kubernetes.io/master
7466
service:
7567
annotations:
7668
example: annotation
7769
labels:
7870
example: label
79-
# Ingress
8071
ingress:
8172
enabled: true
8273
ingressClassName: "nginx"

charts/capsule-proxy/ci/ds-values.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ certManager:
1717
name: "" # Name of the ClusterIssuer
1818
replicaCount: 1
1919
podAnnotations:
20-
scheduler.alpha.kubernetes.io/critical-pod: ''
2120
extra: annotation
2221
podLabels:
2322
extra: label

charts/capsule-proxy/templates/_pod.tpl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ spec:
7070
- --client-connection-qps={{ .Values.options.clientConnectionQPS }}
7171
- --client-connection-burst={{ .Values.options.clientConnectionBurst }}
7272
- --enable-pprof={{ .Values.options.pprof }}
73+
- --enable-leader-election={{ .Values.options.leaderElection }}
7374
{{- if .Values.webhooks.enabled }}
7475
{{- if .Values.webhooks.watchdog.enabled }}
7576
- --webhooks=watchdog
@@ -78,8 +79,12 @@ spec:
7879
{{- with .Values.options.extraArgs }}
7980
{{- toYaml . | nindent 4 }}
8081
{{- end }}
82+
env:
83+
- name: NAMESPACE
84+
valueFrom:
85+
fieldRef:
86+
fieldPath: metadata.namespace
8187
{{- with .Values.env }}
82-
env:
8388
{{- toYaml . | nindent 4 }}
8489
{{- end }}
8590
ports:

charts/capsule-proxy/values.schema.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -487,9 +487,6 @@
487487
"properties": {
488488
"extra": {
489489
"type": "string"
490-
},
491-
"scheduler.alpha.kubernetes.io/critical-pod": {
492-
"type": "string"
493490
}
494491
},
495492
"type": "object"

charts/capsule-proxy/values.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ volumeMounts: []
240240
options:
241241
# -- Set the listening port of the capsule-proxy
242242
listeningPort: 9001
243+
# -- Set leader election to true if you are running n-replicas
244+
leaderElection: false
243245
# -- Set the log verbosity of the capsule-proxy with a value from 1 to 10
244246
logLevel: '4'
245247
# -- Name of the CapsuleConfiguration custom resource used by Capsule, required to identify the user groups

internal/webserver/webserver.go

Lines changed: 98 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,14 @@ import (
5959
"github.com/projectcapsule/capsule-proxy/internal/webserver/middleware"
6060
)
6161

62-
func NewKubeFilter(opts options.ListenerOpts, srv options.ServerOptions, gates featuregate.FeatureGate, rbReflector *controllers.RoleBindingReflector, clientOverride client.Reader, client client.Client) (Filter, error) {
62+
func NewKubeFilter(
63+
opts options.ListenerOpts,
64+
srv options.ServerOptions,
65+
gates featuregate.FeatureGate,
66+
rbReflector *controllers.RoleBindingReflector,
67+
clientOverride client.Reader,
68+
mgr ctrl.Manager,
69+
) (Filter, error) {
6370
reverseProxy := httputil.NewSingleHostReverseProxy(opts.KubernetesControlPlaneURL())
6471
reverseProxy.FlushInterval = time.Millisecond * 100
6572

@@ -71,10 +78,11 @@ func NewKubeFilter(opts options.ListenerOpts, srv options.ServerOptions, gates f
7178
reverseProxy.Transport = reverseProxyTransport
7279

7380
return &kubeFilter{
81+
mgr: mgr,
7482
gates: gates,
7583
reader: clientOverride,
76-
writer: client,
77-
managerReader: client,
84+
writer: mgr.GetClient(),
85+
managerReader: mgr.GetClient(),
7886
allowedPaths: sets.New("/api", "/apis", "/version"),
7987
authTypes: opts.AuthTypes(),
8088
ignoredUserGroups: sets.New(opts.IgnoredGroupNames()...),
@@ -93,6 +101,7 @@ func NewKubeFilter(opts options.ListenerOpts, srv options.ServerOptions, gates f
93101
}
94102

95103
type kubeFilter struct {
104+
mgr ctrl.Manager
96105
allowedPaths sets.Set[string]
97106
authTypes []req.AuthType
98107
ignoredUserGroups sets.Set[string]
@@ -113,11 +122,86 @@ type kubeFilter struct {
113122
writer client.Writer
114123
}
115124

125+
//nolint:funlen
126+
func (n *kubeFilter) Start(ctx context.Context) error {
127+
r := mux.NewRouter()
128+
r.Use(handlers.RecoveryHandler())
129+
130+
r.Path("/_healthz").Subrouter().HandleFunc("", func(writer http.ResponseWriter, _ *http.Request) {
131+
writer.WriteHeader(http.StatusOK)
132+
_, _ = writer.Write([]byte("ok"))
133+
})
134+
135+
root := r.PathPrefix("").Subrouter()
136+
n.registerModules(ctx, root)
137+
root.Use(
138+
n.reverseProxyMiddleware,
139+
middleware.CheckPaths(n.log, n.allowedPaths, n.impersonateHandler),
140+
middleware.CheckJWTMiddleware(n.writer),
141+
)
142+
root.PathPrefix("/").HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
143+
n.impersonateHandler(writer, request)
144+
})
145+
146+
var srv *http.Server
147+
148+
go func() {
149+
var err error
150+
151+
addr := fmt.Sprintf("0.0.0.0:%d", n.serverOptions.ListeningPort())
152+
153+
if n.serverOptions.IsListeningTLS() {
154+
tlsConfig := &tls.Config{
155+
MinVersion: tls.VersionTLS12,
156+
ClientCAs: n.serverOptions.GetCertificateAuthorityPool(),
157+
}
158+
159+
for _, authType := range n.authTypes {
160+
if authType == req.TLSCertificate {
161+
tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven
162+
163+
break
164+
}
165+
}
166+
167+
srv = &http.Server{
168+
Handler: r,
169+
Addr: addr,
170+
TLSConfig: tlsConfig,
171+
ReadHeaderTimeout: 5 * time.Second,
172+
}
173+
err = srv.ListenAndServeTLS(n.serverOptions.TLSCertificatePath(), n.serverOptions.TLSCertificateKeyPath())
174+
} else {
175+
srv = &http.Server{
176+
Handler: r,
177+
Addr: addr,
178+
ReadHeaderTimeout: 5 * time.Second,
179+
}
180+
err = srv.ListenAndServe()
181+
}
182+
183+
if err != nil {
184+
panic(err)
185+
}
186+
}()
187+
188+
<-ctx.Done()
189+
190+
return srv.Shutdown(ctx)
191+
}
192+
116193
func (n *kubeFilter) LivenessProbe(*http.Request) error {
117194
return nil
118195
}
119196

120197
func (n *kubeFilter) ReadinessProbe(req *http.Request) (err error) {
198+
select {
199+
case <-n.mgr.Elected():
200+
// OK, we're the leader..
201+
default:
202+
return nil
203+
}
204+
121205
scheme := "http"
122206
clt := &http.Client{}
123207

@@ -158,6 +242,17 @@ func (n *kubeFilter) ReadinessProbe(req *http.Request) (err error) {
158242
return nil
159243
}
160244

245+
func (n *kubeFilter) BearerToken() string {
246+
if time.Now().After(n.bearerTokenExpirationTime) {
247+
n.log.V(5).Info("Token expired. Reading new token from file", "token", n.bearerToken, "token file", n.bearerTokenFile)
248+
token, _ := os.ReadFile(n.bearerTokenFile)
249+
n.bearerToken = string(token)
250+
n.bearerTokenExpirationTime = bearerExpirationTime(string(token))
251+
}
252+
253+
return n.bearerToken
254+
}
255+
161256
func (n *kubeFilter) reverseProxyMiddleware(next http.Handler) http.Handler {
162257
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
163258
next.ServeHTTP(writer, request)
@@ -346,74 +441,6 @@ func (n *kubeFilter) registerModules(ctx context.Context, root *mux.Router) {
346441
}
347442
}
348443

349-
//nolint:funlen
350-
func (n *kubeFilter) Start(ctx context.Context) error {
351-
r := mux.NewRouter()
352-
r.Use(handlers.RecoveryHandler())
353-
354-
r.Path("/_healthz").Subrouter().HandleFunc("", func(writer http.ResponseWriter, _ *http.Request) {
355-
writer.WriteHeader(http.StatusOK)
356-
_, _ = writer.Write([]byte("ok"))
357-
})
358-
359-
root := r.PathPrefix("").Subrouter()
360-
n.registerModules(ctx, root)
361-
root.Use(
362-
n.reverseProxyMiddleware,
363-
middleware.CheckPaths(n.log, n.allowedPaths, n.impersonateHandler),
364-
middleware.CheckJWTMiddleware(n.writer),
365-
)
366-
root.PathPrefix("/").HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
367-
n.impersonateHandler(writer, request)
368-
})
369-
370-
var srv *http.Server
371-
372-
go func() {
373-
var err error
374-
375-
addr := fmt.Sprintf("0.0.0.0:%d", n.serverOptions.ListeningPort())
376-
377-
if n.serverOptions.IsListeningTLS() {
378-
tlsConfig := &tls.Config{
379-
MinVersion: tls.VersionTLS12,
380-
ClientCAs: n.serverOptions.GetCertificateAuthorityPool(),
381-
}
382-
383-
for _, authType := range n.authTypes {
384-
if authType == req.TLSCertificate {
385-
tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven
386-
387-
break
388-
}
389-
}
390-
391-
srv = &http.Server{
392-
Handler: r,
393-
Addr: addr,
394-
TLSConfig: tlsConfig,
395-
ReadHeaderTimeout: 5 * time.Second,
396-
}
397-
err = srv.ListenAndServeTLS(n.serverOptions.TLSCertificatePath(), n.serverOptions.TLSCertificateKeyPath())
398-
} else {
399-
srv = &http.Server{
400-
Handler: r,
401-
Addr: addr,
402-
ReadHeaderTimeout: 5 * time.Second,
403-
}
404-
err = srv.ListenAndServe()
405-
}
406-
407-
if err != nil {
408-
panic(err)
409-
}
410-
}()
411-
412-
<-ctx.Done()
413-
414-
return srv.Shutdown(ctx)
415-
}
416-
417444
func (n *kubeFilter) getTenantsForOwner(ctx context.Context, username string, groups []string) (proxyTenants []*tenant.ProxyTenant, err error) {
418445
if strings.HasPrefix(username, serviceaccount.ServiceAccountUsernamePrefix) {
419446
proxyTenants, err = n.getProxyTenantsForOwnerKind(ctx, capsulev1beta2.ServiceAccountOwner, username)
@@ -547,17 +574,6 @@ func (n *kubeFilter) removingHopByHopHeaders(request *http.Request) {
547574
request.Header.Del(connectionHeaderName)
548575
}
549576

550-
func (n *kubeFilter) BearerToken() string {
551-
if time.Now().After(n.bearerTokenExpirationTime) {
552-
n.log.V(5).Info("Token expired. Reading new token from file", "token", n.bearerToken, "token file", n.bearerTokenFile)
553-
token, _ := os.ReadFile(n.bearerTokenFile)
554-
n.bearerToken = string(token)
555-
n.bearerTokenExpirationTime = bearerExpirationTime(string(token))
556-
}
557-
558-
return n.bearerToken
559-
}
560-
561577
func bearerExpirationTime(tokenString string) time.Time {
562578
token, _, _ := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{})
563579
claims, _ := token.Claims.(jwt.MapClaims)

0 commit comments

Comments
 (0)