M mconsul/globals.go => mconsul/globals.go +0 -3
@@ 5,9 5,6 @@ package mconsul
// gets put in URLs.
var GlobalAddress string
-// GlobalToken is the token to pass to the Consul server in an HTTP header.
-var GlobalToken string
-
// GlobalRandomSelection holds whether to return a random address of a service,
// rather than the nearest one to the Consul agent. Make sure to seed the random
// number generator before setting this.
M mconsul/kv.go => mconsul/kv.go +5 -2
@@ 25,9 25,12 @@ func FetchValueFromKvStore(key string) ([]byte, error) {
return nil, err
}
- if GlobalToken != "" {
- req.Header.Set("X-Consul-Token", GlobalToken)
+ // Read token secret (possibly waiting for a lock)
+ consulToken := getConsulToken()
+ if consulToken != "" {
+ req.Header.Set("X-Consul-Token", consulToken)
}
+ consulToken = ""
// Make the request and wait for a response
client := &http.Client{}
M mconsul/services.go => mconsul/services.go +5 -2
@@ 29,9 29,12 @@ func FetchServiceAddress(serviceName string) (*FoundService, error) {
return nil, err
}
- if GlobalToken != "" {
- req.Header.Set("X-Consul-Token", GlobalToken)
+ // Read token secret (possibly waiting for a lock)
+ consulToken := getConsulToken()
+ if consulToken != "" {
+ req.Header.Set("X-Consul-Token", consulToken)
}
+ consulToken = ""
// Make the request and wait for a response
client := &http.Client{}
A mconsul/token.go => mconsul/token.go +37 -0
@@ 0,0 1,37 @@
+package mconsul
+
+import (
+ "log"
+ "sync"
+)
+
+
+// The current Consul token.
+var storedConsulToken string
+
+// The mutex that synchronises access to storedConsulToken.
+var globalTokenMutex sync.RWMutex
+
+// getConsulToken returns the current Consul token through the mutex.
+func getConsulToken() string {
+ globalTokenMutex.RLock()
+ token := storedConsulToken
+ globalTokenMutex.RUnlock()
+ return token
+}
+
+// SetConsulToken updates the current Consul token to the new one provided,
+// write-locking the mutex and printing a message out depending on whether the
+// token changed or not.
+func SetConsulToken(newToken string) {
+ globalTokenMutex.Lock()
+ updated := storedConsulToken != newToken
+ storedConsulToken = newToken
+ globalTokenMutex.Unlock()
+
+ if updated {
+ log.Println("Updated Consul token")
+ } else {
+ log.Println("Updated Consul token (to the same as the last one?)")
+ }
+}
A msecrets/secrets.go => msecrets/secrets.go +112 -0
@@ 0,0 1,112 @@
+package msecrets
+
+import (
+ "bufio"
+ "log"
+ "os"
+ "os/signal"
+ "strings"
+ "syscall"
+
+ "git.sr.ht/~ogham/doom/mpath"
+)
+
+
+// Secrets does not actually keep any secrets itself; it’s just a wrapper around
+// a file path and a series of loading functions, and, when called, it reads
+// values from the file, line-by-line, and calls the relevant applicator. It
+// basically enables dynamically-reloadable configuration.
+//
+// It should be used over environment variables, not because of the risk of
+// information leakage from the entire environment being sent or logged
+// somewhere, but because they are static: they cannot be updated after the
+// program starts. This makes dealing with dynamic secrets annoying. This Secrets
+// wrapper, on the other hand, allows them to be updated upon receiving a signal.
+type Secrets struct {
+ path mpath.Path
+ appliers map[string]func(string)
+}
+
+// New returns a new Secrets handler with the given path. New does not read the
+// secrets when called by itself.
+func New(path mpath.Path) *Secrets {
+ return &Secrets{path, make(map[string]func(string))}
+}
+
+// Register registers a new secret applier, pairing a secret name with a function
+// that determines what to do with the value.
+//
+// When loading secrets, each applier function is called in serial, and we only
+// make one loading run at a time, so only one instance of each applier function
+// should be running at any given moment, so you should not have to worry about
+// making them thread-safe for THIS reason; however, you will almost certainly
+// want to synchronise access to the memory that holds each secret behind a
+// sync.Mutex or sync.RWMutex to prevent it from being read while being updated.
+func (s *Secrets) Register(secretName string, applier func(secretValue string)) {
+ s.appliers[secretName] = applier
+}
+
+// Read opens the secrets file and applies every secret contained therein,
+// logging as it goes.
+func (s *Secrets) Read() {
+ file, err := os.Open(s.path.Deref())
+ if err != nil {
+ log.Printf("Failed to open secrets file: %v", err)
+ return
+ }
+
+ defer func(file *os.File) {
+ err := file.Close()
+ if err != nil {
+ log.Printf("Failed to close secrets file: %v", err)
+ }
+ }(file)
+
+ scanner := bufio.NewScanner(file)
+ linum := 0
+ secretsApplied := 0
+ for scanner.Scan() {
+ line := scanner.Text()
+ linum += 1
+
+ secretName, secretValue, found := strings.Cut(line, "=")
+ if ! found {
+ log.Printf("Weird line (#%d) in secrets file", linum)
+ continue
+ }
+
+ if applier, matches := s.appliers[secretName]; matches {
+ applier(secretValue)
+ secretsApplied++
+ } else {
+ log.Printf("Unknown secret %q (line #%d) in secrets file", secretName, linum)
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ log.Printf("Failed to read from secrets file: %v", err)
+ } else if secretsApplied == 0 {
+ log.Println("No secrets applied.")
+ } else if secretsApplied == 1 {
+ log.Println("1 secret applied.")
+ } else {
+ log.Printf("%d secrets applied.", secretsApplied)
+ }
+}
+
+const SIGUSR1 = syscall.SIGUSR1
+
+// ListenForReload sets up the signal listener for the OS signal with the given
+// value, wherein upon receiving a signal, runs Read.
+func (s *Secrets) ListenForReload(signalToListenFor os.Signal) {
+ sigs := make(chan os.Signal, 1)
+ signal.Notify(sigs, signalToListenFor)
+
+ go func() {
+ for {
+ sig := <-sigs
+ log.Println("Received signal for secrets reload:", sig.String())
+ s.Read()
+ }
+ }()
+}