@@ -17,58 +17,55 @@ limitations under the License.
17
17
package certwatcher
18
18
19
19
import (
20
+ "bytes"
20
21
"context"
21
22
"crypto/tls"
22
- "fmt "
23
+ "os "
23
24
"sync"
24
25
"time"
25
26
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"
30
27
"sigs.k8s.io/controller-runtime/pkg/certwatcher/metrics"
31
28
logf "sigs.k8s.io/controller-runtime/pkg/internal/log"
32
29
)
33
30
34
31
var log = logf .RuntimeLog .WithName ("certwatcher" )
35
32
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.
39
39
type CertWatcher struct {
40
40
sync.RWMutex
41
41
42
42
currentCert * tls.Certificate
43
- watcher * fsnotify. Watcher
43
+ interval time. Duration
44
44
45
45
certPath string
46
46
keyPath string
47
47
48
+ cachedKeyPEMBlock []byte
49
+
48
50
// callback is a function to be invoked when the certificate changes.
49
51
callback func (tls.Certificate )
50
52
}
51
53
52
54
// New returns a new CertWatcher watching the given certificate and key.
53
55
func New (certPath , keyPath string ) (* CertWatcher , error ) {
54
- var err error
55
-
56
56
cw := & CertWatcher {
57
57
certPath : certPath ,
58
58
keyPath : keyPath ,
59
+ interval : defaultWatchInterval ,
59
60
}
60
61
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
+ }
70
64
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
72
69
}
73
70
74
71
// RegisterCallback registers a callback to be invoked when the certificate changes.
@@ -91,72 +88,71 @@ func (cw *CertWatcher) GetCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate,
91
88
92
89
// Start starts the watch on the certificate and key files.
93
90
func (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 ()
114
93
115
94
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 () {
125
95
for {
126
96
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" )
131
102
}
103
+ }
104
+ }
105
+ }
132
106
133
- cw .handleEvent (event )
107
+ // Watch used to read events from the watcher's channel and reacts to changes,
108
+ // it has currently no function and it's left here for backward compatibility until a future release.
109
+ //
110
+ // Deprecated: fsnotify has been removed and Start() is now polling instead.
111
+ func (cw * CertWatcher ) Watch () {
112
+ }
134
113
135
- case err , ok := <- cw . watcher . Errors :
136
- // Channel is closed.
137
- if ! ok {
138
- return
139
- }
114
+ // updateCachedCertificate checks if the new certificate differs from the cache,
115
+ // updates it and returns the result if it was updated or not
116
+ func ( cw * CertWatcher ) updateCachedCertificate ( cert * tls. Certificate , keyPEMBlock [] byte ) bool {
117
+ cw . Lock ()
118
+ defer cw . Unlock ()
140
119
141
- log .Error (err , "certificate watch error" )
142
- }
120
+ if cw .currentCert != nil &&
121
+ bytes .Equal (cw .currentCert .Certificate [0 ], cert .Certificate [0 ]) &&
122
+ bytes .Equal (cw .cachedKeyPEMBlock , keyPEMBlock ) {
123
+ log .V (7 ).Info ("certificate already cached" )
124
+ return false
143
125
}
126
+ cw .currentCert = cert
127
+ cw .cachedKeyPEMBlock = keyPEMBlock
128
+ return true
144
129
}
145
130
146
131
// 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
132
+ // and updates the current certificate on the watcher if updated. If a callback is set, it
148
133
// is invoked with the new certificate.
149
134
func (cw * CertWatcher ) ReadCertificate () error {
150
135
metrics .ReadCertificateTotal .Inc ()
151
- cert , err := tls .LoadX509KeyPair (cw .certPath , cw .keyPath )
136
+ certPEMBlock , err := os .ReadFile (cw .certPath )
137
+ if err != nil {
138
+ metrics .ReadCertificateErrors .Inc ()
139
+ return err
140
+ }
141
+ keyPEMBlock , err := os .ReadFile (cw .keyPath )
152
142
if err != nil {
153
143
metrics .ReadCertificateErrors .Inc ()
154
144
return err
155
145
}
156
146
157
- cw .Lock ()
158
- cw .currentCert = & cert
159
- cw .Unlock ()
147
+ cert , err := tls .X509KeyPair (certPEMBlock , keyPEMBlock )
148
+ if err != nil {
149
+ metrics .ReadCertificateErrors .Inc ()
150
+ return err
151
+ }
152
+
153
+ if ! cw .updateCachedCertificate (& cert , keyPEMBlock ) {
154
+ return nil
155
+ }
160
156
161
157
log .Info ("Updated current TLS certificate" )
162
158
@@ -170,39 +166,3 @@ func (cw *CertWatcher) ReadCertificate() error {
170
166
}
171
167
return nil
172
168
}
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