func (s *Store) SetConfigData(ctx context.Context, tx *sql.Tx, data []byte) (int, error) {
	err := permission.LimitCheckAny(ctx, permission.System, permission.Admin)
	if err != nil {
		return 0, err
	}
	var cfg Config
	err = json.Unmarshal(data, &cfg)
	if err != nil {
		return 0, errors.Wrap(err, "validate config")
	}
	data, err = s.keys.Encrypt("CONFIG", data)
	if err != nil {
		return 0, errors.Wrap(err, "encrypt config")
	}
	var id int
	err = wrapTx(ctx, tx, s.setConfig).QueryRowContext(ctx, SchemaVersion, data).Scan(&id)
	if err != nil {
		return 0, err
	}
	return id, nil
}
SetConfigData will replace the current DB config with data.
package config
import (
	"context"
	cryptoRand "crypto/rand"
	"database/sql"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"io"
	"math/rand"
	"net/http"
	"sync"
	"time"
	"github.com/target/goalert/keyring"
	"github.com/target/goalert/permission"
	"github.com/target/goalert/util"
	"github.com/target/goalert/util/errutil"
	"github.com/target/goalert/util/jsonutil"
	"github.com/target/goalert/util/log"
	"github.com/pkg/errors"
)
type Store struct {
	rawCfg       Config
	cfgVers      int
	fallbackURL  string
	mx           sync.RWMutex
	db           *sql.DB
	keys         keyring.Keys
	latestConfig *sql.Stmt
	setConfig    *sql.Stmt
	lock         *sql.Stmt
	closeCh chan struct{}
}
func NewStore(ctx context.Context, db *sql.DB, keys keyring.Keys, fallbackURL string) (*Store, error) {
	p := util.Prepare{Ctx: ctx, DB: db}
	s := &Store{
		db:           db,
		fallbackURL:  fallbackURL,
		latestConfig: p.P(`select id, data, schema from config where schema <= $1 order by id desc limit 1`),
		setConfig:    p.P(`insert into config (id, schema, data) values (DEFAULT, $1, $2) returning (id)`),
		lock:         p.P(`lock config in exclusive mode`),
		keys:         keys,
		closeCh:      make(chan struct{}),
	}
	if p.Err != nil {
		return nil, p.Err
	}
	var err error
	permission.SudoContext(ctx, func(ctx context.Context) {
		err = s.Reload(ctx)
	})
	if err != nil {
		return nil, err
	}
	var seed int64
	err = binary.Read(cryptoRand.Reader, binary.BigEndian, &seed)
	if err != nil {
		return nil, err
	}
	src := rand.New(rand.NewSource(
		seed,
	))
	go func() {
		randDelay := func() time.Duration {
			return 30*time.Second + time.Duration(src.Int63n(int64(30*time.Second)))
		}
		t := time.NewTimer(randDelay())
		for {
			select {
			case <-t.C:
				t.Reset(randDelay())
				permission.SudoContext(context.Background(), func(ctx context.Context) {
					err := s.Reload(ctx)
					if err != nil {
						log.Log(ctx, errors.Wrap(err, "config auto-reload"))
					}
				})
			case s.closeCh <- struct{}{}:
				close(s.closeCh)
				return
			}
		}
	}()
	return s, nil
}
func (s *Store) Shutdown(ctx context.Context) error {
	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-s.closeCh:
	}
	return nil
}
func wrapTx(ctx context.Context, tx *sql.Tx, stmt *sql.Stmt) *sql.Stmt {
	if tx == nil {
		return stmt
	}
	return tx.StmtContext(ctx, stmt)
}
func (s *Store) Reload(ctx context.Context) error {
	cfg, id, err := s.reloadTx(ctx, nil)
	if err != nil {
		return err
	}
	rawCfg := *cfg
	rawCfg.fallbackURL = s.fallbackURL
	err = cfg.Validate()
	if err != nil {
		log.Log(ctx, errors.Wrap(err, "validate config"))
	}
	s.mx.Lock()
	oldVers := s.cfgVers
	s.cfgVers = id
	s.rawCfg = rawCfg
	s.mx.Unlock()
	if oldVers != id {
		log.Logf(ctx, "Loaded config version %d ", id)
	}
	return nil
}
func (s *Store) ServeConfig(w http.ResponseWriter, req *http.Request) {
	ctx := req.Context()
	switch req.Method {
	case "GET":
		id, _, data, err := s.ConfigData(ctx, nil)
		if errutil.HTTPError(ctx, w, err) {
			return
		}
		w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="goalert-config.%d.json"`, id))
		_, err = w.Write(data)
		if errutil.HTTPError(ctx, w, err) {
			return
		}
	case "PUT":
		data, err := io.ReadAll(req.Body)
		if errutil.HTTPError(ctx, w, err) {
			return
		}
		id, err := s.SetConfigData(ctx, nil, data)
		if errutil.HTTPError(ctx, w, err) {
			return
		}
		log.Logf(ctx, "Set configuration to version %d (schema version %d)", id, SchemaVersion)
		err = s.Reload(ctx)
		if errutil.HTTPError(ctx, w, err) {
			return
		}
		w.WriteHeader(204)
	default:
		http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
	}
}
func (s *Store) ConfigData(ctx context.Context, tx *sql.Tx) (id, schemaVersion int, data []byte, err error) {
	err = permission.LimitCheckAny(ctx, permission.System, permission.Admin)
	if err != nil {
		return 0, 0, nil, err
	}
	err = wrapTx(ctx, tx, s.latestConfig).QueryRowContext(ctx, SchemaVersion).Scan(&id, &data, &schemaVersion)
	if errors.Is(err, sql.ErrNoRows) {
		return 0, SchemaVersion, []byte("{}"), nil
	}
	if err != nil {
		return 0, 0, nil, err
	}
	data, _, err = s.keys.Decrypt(data)
	if err != nil {
		return 0, 0, nil, errors.Wrap(err, "decrypt config")
	}
	return id, schemaVersion, data, nil
}