@@ -17,58 +17,55 @@ limitations under the License.
1717package certwatcher
1818
1919import (
20+ "bytes"
2021 "context"
2122 "crypto/tls"
22- "fmt "
23+ "os "
2324 "sync"
2425 "time"
2526
26- "github.com/fsnotify/fsnotify"
27- kerrors "k8s.io/apimachinery/pkg/util/errors"
28- "k8s.io/apimachinery/pkg/util/sets"
29- "k8s.io/apimachinery/pkg/util/wait"
3027 "sigs.k8s.io/controller-runtime/pkg/certwatcher/metrics"
3128 logf "sigs.k8s.io/controller-runtime/pkg/internal/log"
3229)
3330
3431var log = logf .RuntimeLog .WithName ("certwatcher" )
3532
36- // CertWatcher watches certificate and key files for changes. When either file
37- // changes, it reads and parses both and calls an optional callback with the new
38- // certificate.
33+ const defaultWatchInterval = 10 * time .Second
34+
35+ // CertWatcher watches certificate and key files for changes.
36+ // It always returns the cached version,
37+ // but periodically reads and parses certificate and key for changes
38+ // and calls an optional callback with the new certificate.
3939type CertWatcher struct {
4040 sync.RWMutex
4141
4242 currentCert * tls.Certificate
43- watcher * fsnotify. Watcher
43+ interval time. Duration
4444
4545 certPath string
4646 keyPath string
4747
48+ cachedKeyPEMBlock []byte
49+
4850 // callback is a function to be invoked when the certificate changes.
4951 callback func (tls.Certificate )
5052}
5153
5254// New returns a new CertWatcher watching the given certificate and key.
5355func New (certPath , keyPath string ) (* CertWatcher , error ) {
54- var err error
55-
5656 cw := & CertWatcher {
5757 certPath : certPath ,
5858 keyPath : keyPath ,
59+ interval : defaultWatchInterval ,
5960 }
6061
61- // Initial read of certificate and key.
62- if err := cw .ReadCertificate (); err != nil {
63- return nil , err
64- }
65-
66- cw .watcher , err = fsnotify .NewWatcher ()
67- if err != nil {
68- return nil , err
69- }
62+ return cw , cw .ReadCertificate ()
63+ }
7064
71- return cw , nil
65+ // WithWatchInterval sets the watch interval and returns the CertWatcher pointer
66+ func (cw * CertWatcher ) WithWatchInterval (interval time.Duration ) * CertWatcher {
67+ cw .interval = interval
68+ return cw
7269}
7370
7471// RegisterCallback registers a callback to be invoked when the certificate changes.
@@ -91,72 +88,64 @@ func (cw *CertWatcher) GetCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate,
9188
9289// Start starts the watch on the certificate and key files.
9390func (cw * CertWatcher ) Start (ctx context.Context ) error {
94- files := sets .New (cw .certPath , cw .keyPath )
95-
96- {
97- var watchErr error
98- if err := wait .PollUntilContextTimeout (ctx , 1 * time .Second , 10 * time .Second , true , func (ctx context.Context ) (done bool , err error ) {
99- for _ , f := range files .UnsortedList () {
100- if err := cw .watcher .Add (f ); err != nil {
101- watchErr = err
102- return false , nil //nolint:nilerr // We want to keep trying.
103- }
104- // We've added the watch, remove it from the set.
105- files .Delete (f )
106- }
107- return true , nil
108- }); err != nil {
109- return fmt .Errorf ("failed to add watches: %w" , kerrors .NewAggregate ([]error {err , watchErr }))
110- }
111- }
112-
113- go cw .Watch ()
91+ ticker := time .NewTicker (cw .interval )
92+ defer ticker .Stop ()
11493
11594 log .Info ("Starting certificate watcher" )
116-
117- // Block until the context is done.
118- <- ctx .Done ()
119-
120- return cw .watcher .Close ()
121- }
122-
123- // Watch reads events from the watcher's channel and reacts to changes.
124- func (cw * CertWatcher ) Watch () {
12595 for {
12696 select {
127- case event , ok := <- cw .watcher .Events :
128- // Channel is closed.
129- if ! ok {
130- return
97+ case <- ctx .Done ():
98+ return nil
99+ case <- ticker .C :
100+ if err := cw .ReadCertificate (); err != nil {
101+ log .Error (err , "failed read certificate" )
131102 }
103+ }
104+ }
105+ }
132106
133- cw .handleEvent (event )
134-
135- case err , ok := <- cw .watcher .Errors :
136- // Channel is closed.
137- if ! ok {
138- return
139- }
107+ // updateCachedCertificate checks if the new certificate differs from the cache,
108+ // updates it and returns the result if it was updated or not
109+ func (cw * CertWatcher ) updateCachedCertificate (cert * tls.Certificate , keyPEMBlock []byte ) bool {
110+ cw .Lock ()
111+ defer cw .Unlock ()
140112
141- log .Error (err , "certificate watch error" )
142- }
113+ if cw .currentCert != nil &&
114+ bytes .Equal (cw .currentCert .Certificate [0 ], cert .Certificate [0 ]) &&
115+ bytes .Equal (cw .cachedKeyPEMBlock , keyPEMBlock ) {
116+ log .V (7 ).Info ("certificate already cached" )
117+ return false
143118 }
119+ cw .currentCert = cert
120+ cw .cachedKeyPEMBlock = keyPEMBlock
121+ return true
144122}
145123
146124// ReadCertificate reads the certificate and key files from disk, parses them,
147- // and updates the current certificate on the watcher. If a callback is set, it
125+ // and updates the current certificate on the watcher if updated. If a callback is set, it
148126// is invoked with the new certificate.
149127func (cw * CertWatcher ) ReadCertificate () error {
150128 metrics .ReadCertificateTotal .Inc ()
151- cert , err := tls .LoadX509KeyPair (cw .certPath , cw .keyPath )
129+ certPEMBlock , err := os .ReadFile (cw .certPath )
130+ if err != nil {
131+ metrics .ReadCertificateErrors .Inc ()
132+ return err
133+ }
134+ keyPEMBlock , err := os .ReadFile (cw .keyPath )
152135 if err != nil {
153136 metrics .ReadCertificateErrors .Inc ()
154137 return err
155138 }
156139
157- cw .Lock ()
158- cw .currentCert = & cert
159- cw .Unlock ()
140+ cert , err := tls .X509KeyPair (certPEMBlock , keyPEMBlock )
141+ if err != nil {
142+ metrics .ReadCertificateErrors .Inc ()
143+ return err
144+ }
145+
146+ if ! cw .updateCachedCertificate (& cert , keyPEMBlock ) {
147+ return nil
148+ }
160149
161150 log .Info ("Updated current TLS certificate" )
162151
@@ -170,39 +159,3 @@ func (cw *CertWatcher) ReadCertificate() error {
170159 }
171160 return nil
172161}
173-
174- func (cw * CertWatcher ) handleEvent (event fsnotify.Event ) {
175- // Only care about events which may modify the contents of the file.
176- if ! (isWrite (event ) || isRemove (event ) || isCreate (event ) || isChmod (event )) {
177- return
178- }
179-
180- log .V (1 ).Info ("certificate event" , "event" , event )
181-
182- // If the file was removed or renamed, re-add the watch to the previous name
183- if isRemove (event ) || isChmod (event ) {
184- if err := cw .watcher .Add (event .Name ); err != nil {
185- log .Error (err , "error re-watching file" )
186- }
187- }
188-
189- if err := cw .ReadCertificate (); err != nil {
190- log .Error (err , "error re-reading certificate" )
191- }
192- }
193-
194- func isWrite (event fsnotify.Event ) bool {
195- return event .Op .Has (fsnotify .Write )
196- }
197-
198- func isCreate (event fsnotify.Event ) bool {
199- return event .Op .Has (fsnotify .Create )
200- }
201-
202- func isRemove (event fsnotify.Event ) bool {
203- return event .Op .Has (fsnotify .Remove )
204- }
205-
206- func isChmod (event fsnotify.Event ) bool {
207- return event .Op .Has (fsnotify .Chmod )
208- }
0 commit comments