pomerium/internal/zero/bootstrap/bootstrap.go
2023-08-07 22:16:32 -04:00

151 lines
4 KiB
Go

package bootstrap
/*
* Initial configuration for Pomerium start-up is obtained from the cloud.
* Some parameters are derived from the cluster token.
*
* The expectation is that if the user wishes to survive a cloud outage,
* it should be sufficient to set up Pomerium to use a durable database (Postgres)
* and receive cloud configuration once.
*
*/
import (
"context"
"crypto/cipher"
"fmt"
"time"
"golang.org/x/sync/errgroup"
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/log"
cluster_api "github.com/pomerium/zero-sdk/cluster"
connect_mux "github.com/pomerium/zero-sdk/connect-mux"
)
const (
// DefaultCheckForUpdateInterval is the default interval to check for updates
// if there is no connection to the update service
DefaultCheckForUpdateInterval = 5 * time.Minute
// DefaultCheckForUpdateIntervalWhenConnected is the default interval to check for updates
// if there is a connection to the update service
DefaultCheckForUpdateIntervalWhenConnected = 2 * time.Hour
)
// Run initializes the bootstrap config source
func (svc *BootstrapConfigSource) Run(ctx context.Context) error {
svc.tryLoadInitial(ctx)
eg, ctx := errgroup.WithContext(ctx)
eg.Go(func() error { return svc.watchUpdates(ctx) })
eg.Go(func() error { return svc.updateLoop(ctx) })
return eg.Wait()
}
func (svc *BootstrapConfigSource) watchUpdates(ctx context.Context) error {
return svc.connectMux.Watch(ctx,
connect_mux.WithOnConnected(func(_ context.Context) {
svc.triggerUpdate(DefaultCheckForUpdateIntervalWhenConnected)
}),
connect_mux.WithOnDisconnected(func(_ context.Context) {
svc.triggerUpdate(DefaultCheckForUpdateInterval)
}),
connect_mux.WithOnBootstrapConfigUpdated(func(_ context.Context) {
svc.triggerUpdate(DefaultCheckForUpdateIntervalWhenConnected)
}),
)
}
func (svc *BootstrapConfigSource) updateLoop(ctx context.Context) error {
ticker := time.NewTicker(svc.updateInterval.Load())
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-svc.checkForUpdate:
case <-ticker.C:
}
ticker.Reset(svc.updateInterval.Load())
err := svc.update(ctx)
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("failed to update bootstrap config")
}
}
}
// triggerUpdate triggers an update of the bootstrap config
// and sets the interval for the next update
func (svc *BootstrapConfigSource) triggerUpdate(newUpdateInterval time.Duration) {
svc.updateInterval.Store(newUpdateInterval)
select {
case svc.checkForUpdate <- struct{}{}:
default:
}
}
func (svc *BootstrapConfigSource) update(ctx context.Context) error {
current := svc.GetConfig()
cfg := current.Clone()
err := tryUpdateAndSave(ctx, cfg.Options, svc.clusterAPI, svc.fileCachePath, svc.fileCipher)
if err != nil {
return err
}
_ = svc.SetConfig(ctx, cfg)
return nil
}
func tryUpdateAndSave(
ctx context.Context,
dst *config.Options,
clusterAPI cluster_api.ClientWithResponsesInterface,
fileCachePath string,
fileCipher cipher.AEAD,
) error {
err := LoadBootstrapConfigFromAPI(ctx, dst, clusterAPI)
if err != nil {
return fmt.Errorf("load bootstrap config from API: %w", err)
}
err = SaveBootstrapConfigToFile(dst, fileCachePath, fileCipher)
if err != nil {
log.Ctx(ctx).Error().Err(err).
Msg("failed to save bootstrap config to file, note it may prevent Pomerium from starting up in case of connectivity issues")
}
return nil
}
func (src *BootstrapConfigSource) tryLoadInitial(ctx context.Context) {
dst := src.GetConfig()
err := tryUpdateAndSave(ctx, dst.Options, src.clusterAPI, src.fileCachePath, src.fileCipher)
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("failed to load bootstrap config")
src.tryLoadFromFile(ctx)
return
}
src.SetConfig(ctx, dst)
}
func (src *BootstrapConfigSource) tryLoadFromFile(ctx context.Context) {
dst := src.GetConfig()
err := LoadBootstrapConfigFromFile(dst.Options, src.fileCachePath, src.fileCipher)
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("failed to load bootstrap config from file")
return
}
src.SetConfig(ctx, dst)
}