pomerium/config/config_source.go
Caleb Doxsey a825b06014
metrics: add TLS options (#1939)
* move metrics listener to envoy

* add metrics tls options

* add test

* update docs

* update config proto

* add function to validate metric addr

* fix validation
2021-02-24 09:42:53 -07:00

213 lines
4.6 KiB
Go

package config
import (
"crypto/sha256"
"io/ioutil"
"sync"
"github.com/fsnotify/fsnotify"
"github.com/pomerium/pomerium/internal/fileutil"
)
// A ChangeListener is called when configuration changes.
type ChangeListener = func(*Config)
// A ChangeDispatcher manages listeners on config changes.
type ChangeDispatcher struct {
sync.Mutex
onConfigChangeListeners []ChangeListener
}
// Trigger triggers a change.
func (dispatcher *ChangeDispatcher) Trigger(cfg *Config) {
dispatcher.Lock()
defer dispatcher.Unlock()
for _, li := range dispatcher.onConfigChangeListeners {
li(cfg)
}
}
// OnConfigChange adds a listener.
func (dispatcher *ChangeDispatcher) OnConfigChange(li ChangeListener) {
dispatcher.Lock()
defer dispatcher.Unlock()
dispatcher.onConfigChangeListeners = append(dispatcher.onConfigChangeListeners, li)
}
// A Source gets configuration.
type Source interface {
GetConfig() *Config
OnConfigChange(ChangeListener)
}
// A StaticSource always returns the same config. Useful for testing.
type StaticSource struct {
mu sync.Mutex
cfg *Config
lis []ChangeListener
}
// NewStaticSource creates a new StaticSource.
func NewStaticSource(cfg *Config) *StaticSource {
return &StaticSource{cfg: cfg}
}
// GetConfig gets the config.
func (src *StaticSource) GetConfig() *Config {
src.mu.Lock()
defer src.mu.Unlock()
return src.cfg
}
// SetConfig sets the config.
func (src *StaticSource) SetConfig(cfg *Config) {
src.mu.Lock()
defer src.mu.Unlock()
src.cfg = cfg
for _, li := range src.lis {
li(cfg)
}
}
// OnConfigChange is ignored for the StaticSource.
func (src *StaticSource) OnConfigChange(li ChangeListener) {
src.mu.Lock()
defer src.mu.Unlock()
src.lis = append(src.lis, li)
}
// A FileOrEnvironmentSource retrieves config options from a file or the environment.
type FileOrEnvironmentSource struct {
configFile string
mu sync.RWMutex
config *Config
ChangeDispatcher
}
// NewFileOrEnvironmentSource creates a new FileOrEnvironmentSource.
func NewFileOrEnvironmentSource(configFile string) (*FileOrEnvironmentSource, error) {
options, err := newOptionsFromConfig(configFile)
if err != nil {
return nil, err
}
src := &FileOrEnvironmentSource{
configFile: configFile,
config: &Config{Options: options},
}
options.viper.OnConfigChange(src.onConfigChange)
go options.viper.WatchConfig()
return src, nil
}
func (src *FileOrEnvironmentSource) onConfigChange(evt fsnotify.Event) {
src.mu.Lock()
newOptions := handleConfigUpdate(src.configFile, src.config.Options)
cfg := &Config{Options: newOptions}
src.config = cfg
src.mu.Unlock()
src.Trigger(cfg)
}
// GetConfig gets the config.
func (src *FileOrEnvironmentSource) GetConfig() *Config {
src.mu.RLock()
defer src.mu.RUnlock()
return src.config
}
// FileWatcherSource is a config source which triggers a change any time a file in the options changes.
type FileWatcherSource struct {
underlying Source
watcher *fileutil.Watcher
mu sync.RWMutex
computedConfig *Config
ChangeDispatcher
}
// NewFileWatcherSource creates a new FileWatcherSource.
func NewFileWatcherSource(underlying Source) *FileWatcherSource {
src := &FileWatcherSource{
underlying: underlying,
watcher: fileutil.NewWatcher(),
}
ch := src.watcher.Bind()
go func() {
for range ch {
src.check(underlying.GetConfig())
}
}()
underlying.OnConfigChange(func(cfg *Config) {
src.check(cfg)
})
src.check(underlying.GetConfig())
return src
}
// GetConfig gets the underlying config.
func (src *FileWatcherSource) GetConfig() *Config {
src.mu.RLock()
defer src.mu.RUnlock()
return src.computedConfig
}
func (src *FileWatcherSource) check(cfg *Config) {
if cfg == nil || cfg.Options == nil {
return
}
src.mu.Lock()
defer src.mu.Unlock()
src.watcher.Clear()
h := sha256.New()
fs := []string{
cfg.Options.CAFile,
cfg.Options.CertFile,
cfg.Options.ClientCAFile,
cfg.Options.DataBrokerStorageCAFile,
cfg.Options.DataBrokerStorageCertFile,
cfg.Options.DataBrokerStorageCertKeyFile,
cfg.Options.KeyFile,
cfg.Options.PolicyFile,
cfg.Options.MetricsClientCAFile,
cfg.Options.MetricsCertificateFile,
cfg.Options.MetricsCertificateKeyFile,
}
for _, pair := range cfg.Options.CertificateFiles {
fs = append(fs, pair.CertFile, pair.KeyFile)
}
for _, f := range fs {
_, _ = h.Write([]byte{0})
bs, err := ioutil.ReadFile(f)
if err == nil {
src.watcher.Add(f)
_, _ = h.Write(bs)
}
}
// update the computed config
src.computedConfig = cfg.Clone()
src.computedConfig.Options.Certificates = nil
_ = src.computedConfig.Options.Validate()
// trigger a change
src.Trigger(src.computedConfig)
}