~ogham/doom

9ce268b9ff1c8c8240c8e127f3a713e0b1f7b417 — Benjamin Sago 1 year, 11 months ago 2840443
S E C R E T S
5 files changed, 159 insertions(+), 7 deletions(-)

M mconsul/globals.go
M mconsul/kv.go
M mconsul/services.go
A mconsul/token.go
A msecrets/secrets.go
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()
        }
    }()
}