Skip to content

Commit b1265b2

Browse files
authored
Merge pull request #7454 from maxcao13/reload-ca
Allow VPA admission controller to reload the caBundle certificate and patch the webhook
2 parents 47f07e5 + e76466f commit b1265b2

File tree

8 files changed

+332
-20
lines changed

8 files changed

+332
-20
lines changed

vertical-pod-autoscaler/deploy/vpa-rbac.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ rules:
291291
- delete
292292
- get
293293
- list
294+
- patch
294295
- apiGroups:
295296
- "poc.autoscaling.k8s.io"
296297
resources:

vertical-pod-autoscaler/e2e/v1/admission_controller.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -853,19 +853,20 @@ var _ = AdmissionControllerE2eDescribe("Admission-controller", func() {
853853
gomega.Expect(err2.Error()).To(gomega.MatchRegexp(`.*admission webhook .*vpa.* denied the request: .*`))
854854
})
855855

856-
ginkgo.It("reloads the webhook certificate", func(ctx ginkgo.SpecContext) {
857-
ginkgo.By("Retrieving alternative certificate")
856+
ginkgo.It("reloads the webhook leaf and CA certificate", func(ctx ginkgo.SpecContext) {
857+
ginkgo.By("Retrieving alternative certificates")
858858
c := f.ClientSet
859859
e2eCertsSecret, err := c.CoreV1().Secrets(metav1.NamespaceSystem).Get(ctx, "vpa-e2e-certs", metav1.GetOptions{})
860860
gomega.Expect(err).To(gomega.Succeed(), "Failed to get vpa-e2e-certs secret")
861861
actualCertsSecret, err := c.CoreV1().Secrets(metav1.NamespaceSystem).Get(ctx, "vpa-tls-certs", metav1.GetOptions{})
862862
gomega.Expect(err).To(gomega.Succeed(), "Failed to get vpa-tls-certs secret")
863863
actualCertsSecret.Data["serverKey.pem"] = e2eCertsSecret.Data["e2eKey.pem"]
864864
actualCertsSecret.Data["serverCert.pem"] = e2eCertsSecret.Data["e2eCert.pem"]
865+
actualCertsSecret.Data["caCert.pem"] = e2eCertsSecret.Data["e2eCaCert.pem"]
865866
_, err = c.CoreV1().Secrets(metav1.NamespaceSystem).Update(ctx, actualCertsSecret, metav1.UpdateOptions{})
866867
gomega.Expect(err).To(gomega.Succeed(), "Failed to update vpa-tls-certs secret with e2e rotation certs")
867868

868-
ginkgo.By("Waiting for certificate reload")
869+
ginkgo.By("Waiting for certificate reloads")
869870
pods, err := c.CoreV1().Pods(metav1.NamespaceSystem).List(ctx, metav1.ListOptions{})
870871
gomega.Expect(err).To(gomega.Succeed())
871872

@@ -883,7 +884,7 @@ var _ = AdmissionControllerE2eDescribe("Admission-controller", func() {
883884
logs, err := io.ReadAll(reader)
884885
g.Expect(err).To(gomega.Succeed())
885886
return string(logs)
886-
}).Should(gomega.ContainSubstring("New certificate found, reloading"))
887+
}).Should(gomega.And(gomega.ContainSubstring("New certificate found, reloading"), gomega.ContainSubstring("New client CA found, reloading and patching webhook"), gomega.ContainSubstring("Successfully patched webhook with new client CA")))
887888

888889
ginkgo.By("Setting up invalid VPA object")
889890
// there is an invalid "requests" field.

vertical-pod-autoscaler/pkg/admission-controller/certs.go

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,18 @@ limitations under the License.
1717
package main
1818

1919
import (
20+
"context"
2021
"crypto/tls"
22+
"encoding/base64"
23+
"fmt"
2124
"os"
22-
"path"
2325
"sync"
2426

27+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
"k8s.io/apimachinery/pkg/types"
29+
2530
"github.com/fsnotify/fsnotify"
31+
admissionregistrationv1 "k8s.io/client-go/kubernetes/typed/admissionregistration/v1"
2632
"k8s.io/klog/v2"
2733
)
2834

@@ -42,10 +48,12 @@ func readFile(filePath string) []byte {
4248
}
4349

4450
type certReloader struct {
45-
tlsCertPath string
46-
tlsKeyPath string
47-
cert *tls.Certificate
48-
mu sync.RWMutex
51+
tlsCertPath string
52+
tlsKeyPath string
53+
clientCaPath string
54+
cert *tls.Certificate
55+
mu sync.RWMutex
56+
mutatingWebhookClient admissionregistrationv1.MutatingWebhookConfigurationInterface
4957
}
5058

5159
func (cr *certReloader) start(stop <-chan struct{}) error {
@@ -54,22 +62,43 @@ func (cr *certReloader) start(stop <-chan struct{}) error {
5462
return err
5563
}
5664

57-
if err = watcher.Add(path.Dir(cr.tlsCertPath)); err != nil {
65+
if err = watcher.Add(cr.tlsCertPath); err != nil {
66+
return err
67+
}
68+
if err = watcher.Add(cr.tlsKeyPath); err != nil {
5869
return err
5970
}
60-
if err = watcher.Add(path.Dir(cr.tlsKeyPath)); err != nil {
71+
if err = watcher.Add(cr.clientCaPath); err != nil {
6172
return err
6273
}
74+
6375
go func() {
6476
defer watcher.Close()
6577
for {
6678
select {
6779
case event := <-watcher.Events:
68-
if event.Has(fsnotify.Create) || event.Has(fsnotify.Write) {
80+
// we need to watch "Remove" events because Kubernetes uses symbolic links to point to ConfigMaps/Secrets volumes
81+
if !event.Has(fsnotify.Remove) && !event.Has(fsnotify.Create) && !event.Has(fsnotify.Write) {
82+
continue
83+
}
84+
switch event.Name {
85+
case cr.tlsCertPath, cr.tlsKeyPath:
6986
klog.V(2).InfoS("New certificate found, reloading")
7087
if err := cr.load(); err != nil {
7188
klog.ErrorS(err, "Failed to reload certificate")
7289
}
90+
case cr.clientCaPath:
91+
if err := cr.reloadWebhookCA(); err != nil {
92+
klog.ErrorS(err, "Failed to reload client CA")
93+
}
94+
default:
95+
continue
96+
}
97+
// watches get removed along with the symlinks, so we need to add them back
98+
if event.Has(fsnotify.Remove) {
99+
if err := watcher.Add(event.Name); err != nil {
100+
klog.ErrorS(err, "Failed to add watcher for file", "filename", event.Name)
101+
}
73102
}
74103
case err := <-watcher.Errors:
75104
klog.Warningf("Error watching certificate files: %s", err)
@@ -92,6 +121,36 @@ func (cr *certReloader) load() error {
92121
return nil
93122
}
94123

124+
func (cr *certReloader) reloadWebhookCA() error {
125+
client := cr.mutatingWebhookClient
126+
webhook, err := client.Get(context.TODO(), webhookConfigName, metav1.GetOptions{})
127+
if err != nil {
128+
return err
129+
}
130+
if webhook == nil {
131+
return fmt.Errorf("webhook not found")
132+
}
133+
if len(webhook.Webhooks) == 0 {
134+
return fmt.Errorf("webhook configuration has no webhooks")
135+
}
136+
currentBundle := webhook.Webhooks[0].ClientConfig.CABundle[:]
137+
base64CurrentBundle := base64.StdEncoding.EncodeToString(currentBundle)
138+
newBundle := readFile(cr.clientCaPath)
139+
base64NewBundle := base64.StdEncoding.EncodeToString(newBundle)
140+
// make sure clientCA actually changed
141+
if base64CurrentBundle == base64NewBundle {
142+
klog.V(2).InfoS("Client CA did not change, skipping patch")
143+
return nil
144+
}
145+
klog.V(2).InfoS("New client CA found, reloading and patching webhook")
146+
patch := []byte(fmt.Sprintf(`{"webhooks":[{"name":"%s","clientConfig":{"caBundle":"%s"}}]}`, webhookName, base64NewBundle))
147+
_, err = client.Patch(context.TODO(), webhookConfigName, types.StrategicMergePatchType, patch, metav1.PatchOptions{})
148+
if err == nil {
149+
klog.V(2).InfoS("Successfully patched webhook with new client CA")
150+
}
151+
return err
152+
}
153+
95154
func (cr *certReloader) getCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
96155
cr.mu.RLock()
97156
defer cr.mu.RUnlock()

0 commit comments

Comments
 (0)