Skip to content

Commit e7e6e38

Browse files
committed
Add certwatcher to webhook server
Signed-off-by: Grant Griffiths <[email protected]>
1 parent 271a825 commit e7e6e38

24 files changed

+2774
-11
lines changed

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.15
44

55
require (
66
github.com/container-storage-interface/spec v1.2.0
7+
github.com/fsnotify/fsnotify v1.4.9
78
github.com/golang/mock v1.4.3
89
github.com/golang/protobuf v1.4.2
910
github.com/google/gofuzz v1.1.0

pkg/validation-webhook/certwatcher.go

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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 webhook
18+
19+
import (
20+
"context"
21+
"crypto/tls"
22+
"sync"
23+
24+
"github.com/fsnotify/fsnotify"
25+
"k8s.io/klog"
26+
)
27+
28+
// This file originated from github.com/kubernetes-sigs/controller-runtime/pkg/webhook/internal/certwatcher.
29+
// We cannot import this package as it's an internal one. In addition, we cannot yet easily integrate
30+
// with controller-runtime/pkg/webhook directly, as it would require extensive rework:
31+
// https://github.com/kubernetes-csi/external-snapshotter/issues/422
32+
33+
// CertWatcher watches certificate and key files for changes. When either file
34+
// changes, it reads and parses both and calls an optional callback with the new
35+
// certificate.
36+
type CertWatcher struct {
37+
sync.Mutex
38+
39+
currentCert *tls.Certificate
40+
watcher *fsnotify.Watcher
41+
42+
certPath string
43+
keyPath string
44+
}
45+
46+
// NewCertWatcher returns a new CertWatcher watching the given certificate and key.
47+
func NewCertWatcher(certPath, keyPath string) (*CertWatcher, error) {
48+
var err error
49+
50+
cw := &CertWatcher{
51+
certPath: certPath,
52+
keyPath: keyPath,
53+
}
54+
55+
// Initial read of certificate and key.
56+
if err := cw.ReadCertificate(); err != nil {
57+
return nil, err
58+
}
59+
60+
cw.watcher, err = fsnotify.NewWatcher()
61+
if err != nil {
62+
return nil, err
63+
}
64+
65+
return cw, nil
66+
}
67+
68+
// GetCertificate fetches the currently loaded certificate, which may be nil.
69+
func (cw *CertWatcher) GetCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
70+
cw.Lock()
71+
defer cw.Unlock()
72+
return cw.currentCert, nil
73+
}
74+
75+
// Start starts the watch on the certificate and key files.
76+
func (cw *CertWatcher) Start(ctx context.Context) error {
77+
files := []string{cw.certPath, cw.keyPath}
78+
79+
for _, f := range files {
80+
if err := cw.watcher.Add(f); err != nil {
81+
return err
82+
}
83+
}
84+
85+
go cw.Watch()
86+
87+
// Block until the context is done.
88+
<-ctx.Done()
89+
90+
return cw.watcher.Close()
91+
}
92+
93+
// Watch reads events from the watcher's channel and reacts to changes.
94+
func (cw *CertWatcher) Watch() {
95+
for {
96+
select {
97+
case event, ok := <-cw.watcher.Events:
98+
// Channel is closed.
99+
if !ok {
100+
return
101+
}
102+
103+
cw.handleEvent(event)
104+
105+
case err, ok := <-cw.watcher.Errors:
106+
// Channel is closed.
107+
if !ok {
108+
return
109+
}
110+
111+
klog.Error(err, "certificate watch error")
112+
}
113+
}
114+
}
115+
116+
// ReadCertificate reads the certificate and key files from disk, parses them,
117+
// and updates the current certificate on the watcher. If a callback is set, it
118+
// is invoked with the new certificate.
119+
func (cw *CertWatcher) ReadCertificate() error {
120+
cert, err := tls.LoadX509KeyPair(cw.certPath, cw.keyPath)
121+
if err != nil {
122+
return err
123+
}
124+
125+
cw.Lock()
126+
cw.currentCert = &cert
127+
cw.Unlock()
128+
129+
klog.Info("Updated current TLS certificate")
130+
131+
return nil
132+
}
133+
134+
func (cw *CertWatcher) handleEvent(event fsnotify.Event) {
135+
// Only care about events which may modify the contents of the file.
136+
if !(isWrite(event) || isRemove(event) || isCreate(event)) {
137+
return
138+
}
139+
140+
klog.V(1).Info("certificate event", "event", event)
141+
142+
// If the file was removed, re-add the watch.
143+
if isRemove(event) {
144+
if err := cw.watcher.Add(event.Name); err != nil {
145+
klog.Error(err, "error re-watching file")
146+
}
147+
}
148+
149+
if err := cw.ReadCertificate(); err != nil {
150+
klog.Error(err, "error re-reading certificate")
151+
}
152+
}
153+
154+
func isWrite(event fsnotify.Event) bool {
155+
return event.Op&fsnotify.Write == fsnotify.Write
156+
}
157+
158+
func isCreate(event fsnotify.Event) bool {
159+
return event.Op&fsnotify.Create == fsnotify.Create
160+
}
161+
162+
func isRemove(event fsnotify.Event) bool {
163+
return event.Op&fsnotify.Remove == fsnotify.Remove
164+
}

pkg/validation-webhook/webhook.go

+37-11
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ limitations under the License.
1717
package webhook
1818

1919
import (
20+
"context"
21+
"crypto/tls"
2022
"encoding/json"
2123
"fmt"
2224
"io/ioutil"
@@ -175,21 +177,45 @@ func serveSnapshotRequest(w http.ResponseWriter, r *http.Request) {
175177
serve(w, r, newDelegateToV1AdmitHandler(admitSnapshot))
176178
}
177179

178-
func main(cmd *cobra.Command, args []string) {
180+
func startServer(ctx context.Context, tlsConfig *tls.Config, cw *CertWatcher) error {
181+
go func() {
182+
klog.Info("Starting certificate watcher")
183+
if err := cw.Start(ctx); err != nil {
184+
klog.Errorf("certificate watcher error: %v", err)
185+
}
186+
}()
187+
179188
fmt.Println("Starting webhook server")
180-
config := Config{
181-
CertFile: certFile,
182-
KeyFile: keyFile,
189+
mux := http.NewServeMux()
190+
mux.HandleFunc("/volumesnapshot", serveSnapshotRequest)
191+
mux.HandleFunc("/readyz", func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("ok")) })
192+
srv := &http.Server{
193+
Handler: mux,
194+
TLSConfig: tlsConfig,
183195
}
184196

185-
http.HandleFunc("/volumesnapshot", serveSnapshotRequest)
186-
http.HandleFunc("/readyz", func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("ok")) })
187-
server := &http.Server{
188-
Addr: fmt.Sprintf(":%d", port),
189-
TLSConfig: configTLS(config),
197+
// listener is always closed by srv.Serve
198+
listener, err := tls.Listen("tcp", fmt.Sprintf(":%d", port), tlsConfig)
199+
if err != nil {
200+
return err
190201
}
191-
err := server.ListenAndServeTLS("", "")
202+
203+
return srv.Serve(listener)
204+
}
205+
206+
func main(cmd *cobra.Command, args []string) {
207+
// Create new cert watcher
208+
ctx, cancel := context.WithCancel(cmd.Context())
209+
defer cancel() // stops certwatcher
210+
cw, err := NewCertWatcher(certFile, keyFile)
192211
if err != nil {
193-
panic(err)
212+
klog.Fatalf("failed to initialize new cert watcher: %v", err)
213+
}
214+
tlsConfig := &tls.Config{
215+
GetCertificate: cw.GetCertificate,
216+
}
217+
218+
if err := startServer(ctx, tlsConfig, cw); err != nil {
219+
klog.Fatalf("server stopped: %v", err)
194220
}
195221
}

0 commit comments

Comments
 (0)