@@ -17,61 +17,62 @@ 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
-
31
27
"sigs.k8s.io/controller-runtime/pkg/certwatcher/metrics"
32
28
logf "sigs.k8s.io/controller-runtime/pkg/internal/log"
33
29
)
34
30
35
31
var log = logf .RuntimeLog .WithName ("certwatcher" )
36
32
37
- // CertWatcher watches certificate and key files for changes. When either file
38
- // changes, it reads and parses both and calls an optional callback with the new
39
- // 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.
40
39
type CertWatcher struct {
41
40
sync.RWMutex
42
41
43
42
currentCert * tls.Certificate
44
- watcher * fsnotify. Watcher
43
+ interval time. Duration
45
44
46
45
certPath string
47
46
keyPath string
48
47
48
+ cachedKeyPEMBlock []byte
49
+
49
50
// callback is a function to be invoked when the certificate changes.
50
51
callback func (tls.Certificate )
51
52
}
52
53
53
54
// New returns a new CertWatcher watching the given certificate and key.
54
55
func New (certPath , keyPath string ) (* CertWatcher , error ) {
55
- var err error
56
-
57
56
cw := & CertWatcher {
58
57
certPath : certPath ,
59
58
keyPath : keyPath ,
59
+ interval : defaultWatchInterval ,
60
60
}
61
61
62
62
// Initial read of certificate and key.
63
63
if err := cw .ReadCertificate (); err != nil {
64
64
return nil , err
65
65
}
66
66
67
- cw .watcher , err = fsnotify .NewWatcher ()
68
- if err != nil {
69
- return nil , err
70
- }
71
-
72
67
return cw , nil
73
68
}
74
69
70
+ // WithWatchInterval sets the watch interval and returns the CertWatcher pointer
71
+ func (cw * CertWatcher ) WithWatchInterval (interval time.Duration ) * CertWatcher {
72
+ cw .interval = interval
73
+ return cw
74
+ }
75
+
75
76
// RegisterCallback registers a callback to be invoked when the certificate changes.
76
77
func (cw * CertWatcher ) RegisterCallback (callback func (tls.Certificate )) {
77
78
cw .Lock ()
@@ -92,97 +93,64 @@ func (cw *CertWatcher) GetCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate,
92
93
93
94
// Start starts the watch on the certificate and key files.
94
95
func (cw * CertWatcher ) Start (ctx context.Context ) error {
95
- files := sets .New (cw .certPath , cw .keyPath )
96
-
97
- {
98
- var watchErr error
99
- if err := wait .PollUntilContextTimeout (ctx , 1 * time .Second , 10 * time .Second , true , func (ctx context.Context ) (done bool , err error ) {
100
- for _ , f := range files .UnsortedList () {
101
- if err := cw .watcher .Add (f ); err != nil {
102
- watchErr = err
103
- return false , nil //nolint:nilerr // We want to keep trying.
104
- }
105
- // We've added the watch, remove it from the set.
106
- files .Delete (f )
107
- }
108
- return true , nil
109
- }); err != nil {
110
- return fmt .Errorf ("failed to add watches: %w" , kerrors .NewAggregate ([]error {err , watchErr }))
111
- }
112
- }
113
-
114
- go cw .Watch ()
96
+ ticker := time .NewTicker (cw .interval )
97
+ defer ticker .Stop ()
115
98
116
99
log .Info ("Starting certificate watcher" )
117
-
118
- // Block until the context is done.
119
- <- ctx .Done ()
120
-
121
- return cw .watcher .Close ()
122
- }
123
-
124
- func (cw * CertWatcher ) ensureAllFilesAreWatched () {
125
- watchList := sets .New (cw .watcher .WatchList ()... )
126
- difference := sets .New (cw .certPath , cw .keyPath ).Difference (watchList )
127
- if difference .Len () == 0 {
128
- return
129
- }
130
-
131
- for _ , missingWatchPath := range difference .UnsortedList () {
132
- log .V (1 ).Info ("re-adding missing watch" , "path" , missingWatchPath )
133
- if err := cw .watcher .Add (missingWatchPath ); err != nil {
134
- log .Error (err , "failed to add watch" , "path" , missingWatchPath )
135
- return
136
- }
137
- }
138
-
139
- log .V (1 ).Info ("all files are watched again" , "list" , cw .watcher .WatchList ())
140
-
141
- if err := cw .ReadCertificate (); err != nil {
142
- log .Error (err , "error re-reading certificate" )
143
- }
144
- }
145
-
146
- // Watch reads events from the watcher's channel and reacts to changes.
147
- func (cw * CertWatcher ) Watch () {
148
- watcherHealthTimer := time .NewTicker (time .Second )
149
100
for {
150
101
select {
151
- case event , ok := <- cw .watcher .Events :
152
- // Channel is closed.
153
- if ! ok {
154
- return
102
+ case <- ctx .Done ():
103
+ return nil
104
+ case <- ticker .C :
105
+ if err := cw .ReadCertificate (); err != nil {
106
+ log .Error (err , "failed read certificate" )
155
107
}
108
+ }
109
+ }
110
+ }
156
111
157
- cw .handleEvent (event )
158
-
159
- case err , ok := <- cw .watcher .Errors :
160
- // Channel is closed.
161
- if ! ok {
162
- return
163
- }
112
+ // updateCachedCertificate checks if the new certificate differs from the cache,
113
+ // updates it and returns the result if it was updated or not
114
+ func (cw * CertWatcher ) updateCachedCertificate (cert * tls.Certificate , keyPEMBlock []byte ) bool {
115
+ cw .Lock ()
116
+ defer cw .Unlock ()
164
117
165
- log .Error (err , "certificate watch error" )
166
- case <- watcherHealthTimer .C :
167
- cw .ensureAllFilesAreWatched ()
168
- }
118
+ if cw .currentCert != nil &&
119
+ bytes .Equal (cw .currentCert .Certificate [0 ], cert .Certificate [0 ]) &&
120
+ bytes .Equal (cw .cachedKeyPEMBlock , keyPEMBlock ) {
121
+ log .V (7 ).Info ("certificate already cached" )
122
+ return false
169
123
}
124
+ cw .currentCert = cert
125
+ cw .cachedKeyPEMBlock = keyPEMBlock
126
+ return true
170
127
}
171
128
172
129
// ReadCertificate reads the certificate and key files from disk, parses them,
173
- // and updates the current certificate on the watcher. If a callback is set, it
130
+ // and updates the current certificate on the watcher if updated. If a callback is set, it
174
131
// is invoked with the new certificate.
175
132
func (cw * CertWatcher ) ReadCertificate () error {
176
133
metrics .ReadCertificateTotal .Inc ()
177
- cert , err := tls .LoadX509KeyPair (cw .certPath , cw .keyPath )
134
+ certPEMBlock , err := os .ReadFile (cw .certPath )
135
+ if err != nil {
136
+ metrics .ReadCertificateErrors .Inc ()
137
+ return err
138
+ }
139
+ keyPEMBlock , err := os .ReadFile (cw .keyPath )
178
140
if err != nil {
179
141
metrics .ReadCertificateErrors .Inc ()
180
142
return err
181
143
}
182
144
183
- cw .Lock ()
184
- cw .currentCert = & cert
185
- cw .Unlock ()
145
+ cert , err := tls .X509KeyPair (certPEMBlock , keyPEMBlock )
146
+ if err != nil {
147
+ metrics .ReadCertificateErrors .Inc ()
148
+ return err
149
+ }
150
+
151
+ if ! cw .updateCachedCertificate (& cert , keyPEMBlock ) {
152
+ return nil
153
+ }
186
154
187
155
log .Info ("Updated current TLS certificate" )
188
156
@@ -196,39 +164,3 @@ func (cw *CertWatcher) ReadCertificate() error {
196
164
}
197
165
return nil
198
166
}
199
-
200
- func (cw * CertWatcher ) handleEvent (event fsnotify.Event ) {
201
- // Only care about events which may modify the contents of the file.
202
- if ! (isWrite (event ) || isRemove (event ) || isCreate (event ) || isChmod (event )) {
203
- return
204
- }
205
-
206
- log .V (1 ).Info ("certificate event" , "event" , event )
207
-
208
- // If the file was removed or renamed, re-add the watch to the previous name
209
- if isRemove (event ) || isChmod (event ) {
210
- if err := cw .watcher .Add (event .Name ); err != nil {
211
- log .Error (err , "error re-watching file" )
212
- }
213
- }
214
-
215
- if err := cw .ReadCertificate (); err != nil {
216
- log .Error (err , "error re-reading certificate" )
217
- }
218
- }
219
-
220
- func isWrite (event fsnotify.Event ) bool {
221
- return event .Op .Has (fsnotify .Write )
222
- }
223
-
224
- func isCreate (event fsnotify.Event ) bool {
225
- return event .Op .Has (fsnotify .Create )
226
- }
227
-
228
- func isRemove (event fsnotify.Event ) bool {
229
- return event .Op .Has (fsnotify .Remove )
230
- }
231
-
232
- func isChmod (event fsnotify.Event ) bool {
233
- return event .Op .Has (fsnotify .Chmod )
234
- }
0 commit comments