mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-28 18:06:34 +02:00
This also replaces instances where we manually write "return ctx.Err()" with "return context.Cause(ctx)" which is functionally identical, but will also correctly propagate cause errors if present.
316 lines
7.6 KiB
Go
316 lines
7.6 KiB
Go
package config
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sync"
|
|
|
|
"github.com/cespare/xxhash/v2"
|
|
"github.com/google/uuid"
|
|
"github.com/rs/zerolog"
|
|
|
|
"github.com/pomerium/pomerium/internal/events"
|
|
"github.com/pomerium/pomerium/internal/fileutil"
|
|
"github.com/pomerium/pomerium/internal/log"
|
|
"github.com/pomerium/pomerium/internal/telemetry/metrics"
|
|
"github.com/pomerium/pomerium/pkg/netutil"
|
|
"github.com/pomerium/pomerium/pkg/slices"
|
|
)
|
|
|
|
// A ChangeListener is called when configuration changes.
|
|
type ChangeListener = func(context.Context, *Config)
|
|
|
|
type changeDispatcherEvent struct {
|
|
cfg *Config
|
|
}
|
|
|
|
// A ChangeDispatcher manages listeners on config changes.
|
|
type ChangeDispatcher struct {
|
|
target events.Target[changeDispatcherEvent]
|
|
}
|
|
|
|
// Trigger triggers a change.
|
|
func (dispatcher *ChangeDispatcher) Trigger(ctx context.Context, cfg *Config) {
|
|
dispatcher.target.Dispatch(ctx, changeDispatcherEvent{
|
|
cfg: cfg,
|
|
})
|
|
}
|
|
|
|
// OnConfigChange adds a listener.
|
|
func (dispatcher *ChangeDispatcher) OnConfigChange(_ context.Context, li ChangeListener) {
|
|
dispatcher.target.AddListener(func(ctx context.Context, evt changeDispatcherEvent) {
|
|
li(ctx, evt.cfg)
|
|
})
|
|
}
|
|
|
|
// A Source gets configuration.
|
|
type Source interface {
|
|
GetConfig() *Config
|
|
OnConfigChange(context.Context, 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(ctx context.Context, cfg *Config) {
|
|
src.mu.Lock()
|
|
defer src.mu.Unlock()
|
|
|
|
src.cfg = cfg
|
|
for _, li := range src.lis {
|
|
li(ctx, cfg)
|
|
}
|
|
}
|
|
|
|
// OnConfigChange is ignored for the StaticSource.
|
|
func (src *StaticSource) OnConfigChange(_ context.Context, 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
|
|
watcher *fileutil.Watcher
|
|
|
|
mu sync.RWMutex
|
|
config *Config
|
|
|
|
ChangeDispatcher
|
|
}
|
|
|
|
// NewFileOrEnvironmentSource creates a new FileOrEnvironmentSource.
|
|
func NewFileOrEnvironmentSource(
|
|
ctx context.Context,
|
|
configFile, envoyVersion string,
|
|
) (*FileOrEnvironmentSource, error) {
|
|
ctx = log.WithContext(ctx, func(c zerolog.Context) zerolog.Context {
|
|
return c.Str("config_file_source", configFile)
|
|
})
|
|
|
|
options, err := newOptionsFromConfig(configFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cfg := &Config{
|
|
Options: options,
|
|
EnvoyVersion: envoyVersion,
|
|
}
|
|
|
|
ports, err := netutil.AllocatePorts(6)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("allocating ports: %w", err)
|
|
}
|
|
|
|
cfg.AllocatePorts(*(*[6]string)(ports))
|
|
|
|
metrics.SetConfigInfo(ctx, cfg.Options.Services, "local", cfg.Checksum(), true)
|
|
|
|
src := &FileOrEnvironmentSource{
|
|
configFile: configFile,
|
|
watcher: fileutil.NewWatcher(),
|
|
config: cfg,
|
|
}
|
|
if configFile != "" {
|
|
if cfg.Options.IsRuntimeFlagSet(RuntimeFlagConfigHotReload) {
|
|
src.watcher.Watch(ctx, []string{configFile})
|
|
} else {
|
|
log.Ctx(ctx).Info().Msg("hot reload disabled")
|
|
src.watcher.Watch(ctx, nil)
|
|
}
|
|
}
|
|
ch := src.watcher.Bind()
|
|
go func() {
|
|
for range ch {
|
|
src.check(ctx)
|
|
}
|
|
}()
|
|
|
|
return src, nil
|
|
}
|
|
|
|
func (src *FileOrEnvironmentSource) check(ctx context.Context) {
|
|
ctx = log.WithContext(ctx, func(c zerolog.Context) zerolog.Context {
|
|
return c.Str("config_change_id", uuid.New().String())
|
|
})
|
|
log.Ctx(ctx).Info().Msg("config: file updated, reconfiguring...")
|
|
src.mu.Lock()
|
|
cfg := src.config
|
|
options, err := newOptionsFromConfig(src.configFile)
|
|
if err == nil {
|
|
cfg = cfg.Clone()
|
|
cfg.Options = options
|
|
metrics.SetConfigInfo(ctx, cfg.Options.Services, "local", cfg.Checksum(), true)
|
|
} else {
|
|
log.Ctx(ctx).Error().Err(err).Msg("config: error updating config")
|
|
metrics.SetConfigInfo(ctx, cfg.Options.Services, "local", cfg.Checksum(), false)
|
|
}
|
|
src.config = cfg
|
|
src.mu.Unlock()
|
|
|
|
log.Ctx(ctx).Info().Msg("config: loaded configuration")
|
|
|
|
src.Trigger(ctx, 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
|
|
hash uint64
|
|
cfg *Config
|
|
|
|
ChangeDispatcher
|
|
}
|
|
|
|
// NewFileWatcherSource creates a new FileWatcherSource
|
|
func NewFileWatcherSource(ctx context.Context, underlying Source) *FileWatcherSource {
|
|
cfg := underlying.GetConfig()
|
|
src := &FileWatcherSource{
|
|
underlying: underlying,
|
|
watcher: fileutil.NewWatcher(),
|
|
cfg: cfg,
|
|
}
|
|
|
|
ch := src.watcher.Bind()
|
|
go func() {
|
|
for range ch {
|
|
src.onFileChange(ctx)
|
|
}
|
|
}()
|
|
underlying.OnConfigChange(ctx, func(ctx context.Context, cfg *Config) {
|
|
src.onConfigChange(ctx, cfg)
|
|
})
|
|
src.onConfigChange(ctx, cfg)
|
|
|
|
return src
|
|
}
|
|
|
|
// GetConfig gets the underlying config.
|
|
func (src *FileWatcherSource) GetConfig() *Config {
|
|
src.mu.RLock()
|
|
defer src.mu.RUnlock()
|
|
|
|
return src.cfg
|
|
}
|
|
|
|
func (src *FileWatcherSource) onConfigChange(ctx context.Context, cfg *Config) {
|
|
// update the file watcher with paths from the config
|
|
if cfg.Options.IsRuntimeFlagSet(RuntimeFlagConfigHotReload) {
|
|
src.watcher.Watch(ctx, getAllConfigFilePaths(cfg))
|
|
} else {
|
|
src.watcher.Watch(ctx, nil)
|
|
}
|
|
|
|
src.mu.Lock()
|
|
defer src.mu.Unlock()
|
|
|
|
// store the config and trigger an update
|
|
src.cfg = cfg.Clone()
|
|
src.hash = getAllConfigFilePathsHash(src.cfg)
|
|
log.Ctx(ctx).Info().Uint64("hash", src.hash).Msg("config/filewatchersource: underlying config change, triggering update")
|
|
src.Trigger(ctx, src.cfg)
|
|
}
|
|
|
|
func (src *FileWatcherSource) onFileChange(ctx context.Context) {
|
|
src.mu.Lock()
|
|
defer src.mu.Unlock()
|
|
|
|
hash := getAllConfigFilePathsHash(src.cfg)
|
|
|
|
if hash == src.hash {
|
|
log.Ctx(ctx).Info().Uint64("hash", src.hash).Msg("config/filewatchersource: no change detected")
|
|
} else {
|
|
// if the hash changed, trigger an update
|
|
// the actual config will be identical
|
|
src.hash = hash
|
|
log.Ctx(ctx).Info().Uint64("hash", src.hash).Msg("config/filewatchersource: change detected, triggering update")
|
|
src.Trigger(ctx, src.cfg)
|
|
}
|
|
}
|
|
|
|
func getAllConfigFilePathsHash(cfg *Config) uint64 {
|
|
// read all the config files and build a hash from their contents
|
|
h := xxhash.New()
|
|
for _, f := range getAllConfigFilePaths(cfg) {
|
|
_, _ = h.Write([]byte{0})
|
|
f, err := os.Open(f)
|
|
if err == nil {
|
|
_, _ = io.Copy(h, f)
|
|
_ = f.Close()
|
|
}
|
|
}
|
|
return h.Sum64()
|
|
}
|
|
|
|
func getAllConfigFilePaths(cfg *Config) []string {
|
|
fs := []string{
|
|
cfg.Options.CAFile,
|
|
cfg.Options.CertFile,
|
|
cfg.Options.ClientSecretFile,
|
|
cfg.Options.CookieSecretFile,
|
|
cfg.Options.DataBrokerStorageConnectionStringFile,
|
|
cfg.Options.DownstreamMTLS.CAFile,
|
|
cfg.Options.DownstreamMTLS.CRLFile,
|
|
cfg.Options.KeyFile,
|
|
cfg.Options.MetricsCertificateFile,
|
|
cfg.Options.MetricsCertificateKeyFile,
|
|
cfg.Options.MetricsClientCAFile,
|
|
cfg.Options.PolicyFile,
|
|
cfg.Options.SharedSecretFile,
|
|
cfg.Options.SigningKeyFile,
|
|
}
|
|
|
|
for _, pair := range cfg.Options.CertificateFiles {
|
|
fs = append(fs, pair.CertFile, pair.KeyFile)
|
|
}
|
|
|
|
for _, policy := range cfg.Options.Policies {
|
|
fs = append(fs,
|
|
policy.KubernetesServiceAccountTokenFile,
|
|
policy.TLSClientCertFile,
|
|
policy.TLSClientKeyFile,
|
|
policy.TLSCustomCAFile,
|
|
policy.TLSDownstreamClientCAFile,
|
|
)
|
|
}
|
|
|
|
fs = slices.Filter(fs, func(s string) bool {
|
|
return s != ""
|
|
})
|
|
|
|
return fs
|
|
}
|