/
watcher.go
223 lines (199 loc) · 6.23 KB
/
watcher.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 Authors of Cilium
package certloader
import (
"sync"
"time"
"github.com/sirupsen/logrus"
"github.com/cilium/cilium/pkg/crypto/certloader/fswatcher"
"github.com/cilium/cilium/pkg/inctimer"
"github.com/cilium/cilium/pkg/logging/logfields"
)
const watcherEventCoalesceWindow = 100 * time.Millisecond
// Watcher is a set of TLS configuration files including CA files, and a
// certificate along with its private key. The files are watched for change and
// reloaded automatically.
type Watcher struct {
*FileReloader
log logrus.FieldLogger
fswatcher *fswatcher.Watcher
stop chan struct{}
}
// NewWatcher returns a Watcher that watch over the given file
// paths. The given files are expected to already exists when this function is
// called. On success, the returned Watcher is ready to use.
func NewWatcher(log logrus.FieldLogger, caFiles []string, certFile, privkeyFile string) (*Watcher, error) {
r, err := NewFileReloaderReady(caFiles, certFile, privkeyFile)
if err != nil {
return nil, err
}
// An error here would be unexpected as we were able to create a
// FileReloader having read the files, so the files should exist and be
// "watchable".
fswatcher, err := newFsWatcher(caFiles, certFile, privkeyFile)
if err != nil {
return nil, err
}
w := &Watcher{
FileReloader: r,
log: log,
fswatcher: fswatcher,
stop: make(chan struct{}),
}
w.Watch()
return w, nil
}
// FutureWatcher returns a channel where exactly one Watcher will be sent once
// the given files are ready and loaded. This can be useful when the file paths
// are well-known, but the files themselves don't exist yet. Note that the
// requirement is that the file directories must exists.
func FutureWatcher(log logrus.FieldLogger, caFiles []string, certFile, privkeyFile string) (<-chan *Watcher, error) {
r, err := NewFileReloader(caFiles, certFile, privkeyFile)
if err != nil {
return nil, err
}
fswatcher, err := newFsWatcher(caFiles, certFile, privkeyFile)
if err != nil {
return nil, err
}
w := &Watcher{
FileReloader: r,
log: log,
fswatcher: fswatcher,
stop: make(chan struct{}),
}
res := make(chan *Watcher)
go func(res chan<- *Watcher) {
defer close(res)
// Attempt a reload without having received any fs notification in case
// all the files are already there. Note that the keypair and CA are
// reloaded separately as a "partial update" is still useful: If the
// FileReloader is "half-ready" (e.g. has loaded the keypair but failed
// to load the CA), we only need a successfully handled CA related fs
// notify event to become Ready (in other words, we don't need to
// receive a fs event for the keypair in that case to become ready).
_, keypairErr := w.ReloadKeypair()
_, caErr := w.ReloadCA()
ready := w.Watch()
if keypairErr == nil && caErr == nil {
log.Debug("TLS configuration ready")
res <- w
return
}
log.Debug("Waiting on fsnotify update to be ready")
select {
case <-ready:
log.Debug("TLS configuration ready")
res <- w
case <-w.stop:
}
}(res)
return res, nil
}
// Watch initialize the files watcher and update goroutine. It returns a ready
// channel that will be closed once an update made the underlying FileReloader
// ready.
func (w *Watcher) Watch() <-chan struct{} {
// prepare the ready channel to be returned. We will close it exactly once.
var once sync.Once
ready := make(chan struct{})
markReady := func() {
once.Do(func() {
close(ready)
})
}
// build maps for the CA files and keypair files to help detecting what has
// changed in order to reload only the appropriate certificates.
keypairMap := make(map[string]struct{})
caMap := make(map[string]struct{})
if w.FileReloader.certFile != "" {
keypairMap[w.FileReloader.certFile] = struct{}{}
}
if w.FileReloader.privkeyFile != "" {
keypairMap[w.FileReloader.privkeyFile] = struct{}{}
}
for _, path := range w.FileReloader.caFiles {
caMap[path] = struct{}{}
}
// used to coalesce fswatcher events that arrive within the same time window
var keypairReload, caReload <-chan time.Time
go func() {
defer w.fswatcher.Close()
for {
select {
case event := <-w.fswatcher.Events:
path := event.Name
log := w.log.WithFields(logrus.Fields{
logfields.Path: path,
"operation": event.Op,
})
log.Debug("Received fswatcher event")
_, keypairUpdated := keypairMap[path]
_, caUpdated := caMap[path]
if keypairUpdated {
if keypairReload == nil {
keypairReload = inctimer.After(watcherEventCoalesceWindow)
}
} else if caUpdated {
if caReload == nil {
caReload = inctimer.After(watcherEventCoalesceWindow)
}
} else {
// fswatcher should never send events for unknown files
log.Warn("Unknown file, ignoring.")
continue
}
case <-keypairReload:
keypairReload = nil
keypair, err := w.ReloadKeypair()
if err != nil {
w.log.WithError(err).Warn("Keypair update failed")
continue
}
id := keypairId(keypair)
w.log.WithField("keypair-sn", id).Info("Keypair updated")
if w.Ready() {
markReady()
}
case <-caReload:
caReload = nil
if _, err := w.ReloadCA(); err != nil {
w.log.WithError(err).Warn("Certificate authority update failed")
continue
}
w.log.Info("Certificate authority updated")
if w.Ready() {
markReady()
}
case err := <-w.fswatcher.Errors:
w.log.WithError(err).Warn("fswatcher error")
case <-w.stop:
w.log.Info("Stopping fswatcher")
return
}
}
}()
return ready
}
// Stop watching the files.
func (w *Watcher) Stop() {
close(w.stop)
}
// newFsWatcher returns a fswatcher.Watcher watching over the given files.
// The fswatcher.Watcher supports watching over files which do not exist yet.
// A create event will be emitted once the file is added.
func newFsWatcher(caFiles []string, certFile, privkeyFile string) (*fswatcher.Watcher, error) {
trackFiles := []string{}
if certFile != "" {
trackFiles = append(trackFiles, certFile)
}
if privkeyFile != "" {
trackFiles = append(trackFiles, privkeyFile)
}
for _, path := range caFiles {
if path != "" {
trackFiles = append(trackFiles, path)
}
}
return fswatcher.New(trackFiles)
}