| 
 | 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 | +}  | 
0 commit comments