-
Notifications
You must be signed in to change notification settings - Fork 694
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dynamically reload cert/keys if changed on disk #1595
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
package rtls | ||
|
||
import ( | ||
"crypto/tls" | ||
"log" | ||
"os" | ||
"sync/atomic" | ||
"time" | ||
) | ||
|
||
// monitorFiles watches the given files for changes, calling the supplied callback when a | ||
// change is detected. The callback is passed a slice of the files that changed. The loop | ||
// never terminates, so it's expected the caller will invoke this in a goroutine. | ||
func monitorFiles(files []string, cb func([]string)) { | ||
mtimes := make([]time.Time, len(files)) | ||
for { | ||
var changed []string | ||
for n, file := range files { | ||
info, err := os.Stat(file) | ||
if err != nil { | ||
continue | ||
} | ||
if !mtimes[n].IsZero() && info.ModTime() != mtimes[n] { | ||
changed = append(changed, file) | ||
} | ||
mtimes[n] = info.ModTime() | ||
} | ||
if changed != nil { | ||
cb(changed) | ||
} | ||
time.Sleep(2 * time.Second) | ||
} | ||
} | ||
|
||
// monitoredCertificate watches the given certificate and key files for changes, reloading | ||
// the certificate if a change is detected. | ||
type monitoredCertificate struct { | ||
certFile string | ||
keyFile string | ||
// This holds a *tls.Certificate which can be updated from another goroutine, so use | ||
// an atomic value to synchronize access | ||
cert atomic.Value | ||
} | ||
|
||
// newMonitoredCertificate loads the certificate and key and spawns a goroutine to watch | ||
// the files for changes. | ||
func newMonitoredCertificate(certFile, keyFile string) (*monitoredCertificate, error) { | ||
mc := &monitoredCertificate{ | ||
certFile: certFile, | ||
keyFile: keyFile, | ||
} | ||
// Prime the cached certificate and induce any errors immediately | ||
if err := mc.load(); err != nil { | ||
return nil, err | ||
} | ||
// Now start watching for changes | ||
go monitorFiles([]string{certFile, keyFile}, func(changed []string) { | ||
if err := mc.load(); err != nil { | ||
log.Printf("error reloading certificate %s, not replacing: %s", certFile, err) | ||
} else { | ||
log.Printf("reloaded certificate %s", certFile) | ||
} | ||
}) | ||
return mc, nil | ||
} | ||
|
||
// load reads the certificate and key files from disk and caches the result for Get() | ||
func (mc *monitoredCertificate) load() error { | ||
cert, err := tls.LoadX509KeyPair(mc.certFile, mc.keyFile) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question, does this safely fails if one file changed but the other still didn't? For example, what happens if the cert file changes and there is some random slowness until the key file gets also written? Before loading them it could be wise to have a delay, unless There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Exactly. If the key doesn't match the cert, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did this similar feature in a lib https://github.com/LuKks/simple-hosting/blob/main/index.js#L228-L255 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh great then, I should find the equivalent for |
||
if err != nil { | ||
return err | ||
} | ||
mc.cert.Store(&cert) | ||
return nil | ||
} | ||
|
||
// Get returns the certificate. | ||
// | ||
// This will never return nil provided the monitoredCertificate object was created via | ||
// newMonitoredCertificate(), | ||
func (mc *monitoredCertificate) Get() *tls.Certificate { | ||
return mc.cert.Load().(*tls.Certificate) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not idiomatic Go. You should use the ticker pattern.
https://gobyexample.com/tickers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I can adapt it easily enough. Although it's not clear to me what value using a ticker and channel will provide over sleep and callback in this case. A ticker makes sense when you want to select on multiple things (most notably a context), but in this case it was literally just a "do a thing every interval T."
It uses an atomic.Value to hold a scalar (a pointer to tls.Certificate).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is it that will signal doneness?
In this case there's no cleanup activity for the goroutine to do, it's expected to run continuously until program exit, and it doesn't impede rqlited's ability to terminate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be clear, I'm not objecting to making the changes, just asking to be administered the cluebat a little bit more. :)