depot/packages/networking/ipfs-cluster/config/config.go

630 lines
16 KiB
Go

// Package config provides interfaces and utilities for different Cluster
// components to register, read, write and validate configuration sections
// stored in a central configuration file.
package config
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"sync"
"time"
logging "github.com/ipfs/go-log/v2"
)
var logger = logging.Logger("config")
var (
// Error when downloading a Source-based configuration
errFetchingSource = errors.New("could not fetch configuration from source")
// Error when remote source points to another remote-source
errSourceRedirect = errors.New("a sourced configuration cannot point to another source")
)
// IsErrFetchingSource reports whether this error happened when trying to
// fetch a remote configuration source (as opposed to an error parsing the
// config).
func IsErrFetchingSource(err error) bool {
return errors.Is(err, errFetchingSource)
}
// ConfigSaveInterval specifies how often to save the configuration file if
// it needs saving.
var ConfigSaveInterval = time.Second
// The ComponentConfig interface allows components to define configurations
// which can be managed as part of the ipfs-cluster configuration file by the
// Manager.
type ComponentConfig interface {
// Returns a string identifying the section name for this configuration
ConfigKey() string
// Parses a JSON representation of this configuration
LoadJSON([]byte) error
// Provides a JSON representation of this configuration
ToJSON() ([]byte, error)
// Sets default working values
Default() error
// Sets values from environment variables
ApplyEnvVars() error
// Allows this component to work under a subfolder
SetBaseDir(string)
// Checks that the configuration is valid
Validate() error
// Provides a channel to signal the Manager that the configuration
// should be persisted.
SaveCh() <-chan struct{}
// ToDisplayJSON returns a string representing the config excluding hidden fields.
ToDisplayJSON() ([]byte, error)
}
// These are the component configuration types
// supported by the Manager.
const (
Cluster SectionType = iota
Consensus
API
IPFSConn
State
PinTracker
Monitor
Allocator
Informer
Observations
Datastore
endTypes // keep this at the end
)
// SectionType specifies to which section a component configuration belongs.
type SectionType int
// SectionTypes returns the list of supported SectionTypes
func SectionTypes() []SectionType {
var l []SectionType
for i := Cluster; i < endTypes; i++ {
l = append(l, i)
}
return l
}
// Section is a section of which stores
// component-specific configurations.
type Section map[string]ComponentConfig
// jsonSection stores component specific
// configurations. Component configurations depend on
// components themselves.
type jsonSection map[string]*json.RawMessage
// Manager represents an ipfs-cluster configuration which bundles
// different ComponentConfigs object together.
// Use RegisterComponent() to add a component configurations to the
// object. Once registered, configurations will be parsed from the
// central configuration file when doing LoadJSON(), and saved to it
// when doing SaveJSON().
type Manager struct {
ctx context.Context
cancel func()
wg sync.WaitGroup
// The Cluster configuration has a top-level
// special section.
clusterConfig ComponentConfig
// Holds configuration objects for components.
sections map[SectionType]Section
// store originally parsed jsonConfig
jsonCfg *jsonConfig
// stores original source if any
Source string
sourceRedirs int // used avoid recursive source load
// map of components which has empty configuration
// in JSON file
undefinedComps map[SectionType]map[string]bool
// if a config has been loaded from disk, track the path
// so it can be saved to the same place.
path string
saveMux sync.Mutex
}
// NewManager returns a correctly initialized Manager
// which is ready to accept component configurations.
func NewManager() *Manager {
ctx, cancel := context.WithCancel(context.Background())
return &Manager{
ctx: ctx,
cancel: cancel,
undefinedComps: make(map[SectionType]map[string]bool),
sections: make(map[SectionType]Section),
}
}
// Shutdown makes sure all configuration save operations are finished
// before returning.
func (cfg *Manager) Shutdown() {
cfg.cancel()
cfg.wg.Wait()
}
// this watches a save channel which is used to signal that
// we need to store changes in the configuration.
// because saving can be called too much, we will only
// save at intervals of 1 save/second at most.
func (cfg *Manager) watchSave(save <-chan struct{}) {
defer cfg.wg.Done()
// Save once per second mostly
ticker := time.NewTicker(ConfigSaveInterval)
defer ticker.Stop()
thingsToSave := false
for {
select {
case <-save:
thingsToSave = true
case <-ticker.C:
if thingsToSave {
err := cfg.SaveJSON("")
if err != nil {
logger.Error(err)
}
thingsToSave = false
}
// Exit if we have to
select {
case <-cfg.ctx.Done():
return
default:
}
}
}
}
// jsonConfig represents a Cluster configuration as it will look when it is
// saved using json. Most configuration keys are converted into simple types
// like strings, and key names aim to be self-explanatory for the user.
type jsonConfig struct {
Source string `json:"source,omitempty"`
Cluster *json.RawMessage `json:"cluster,omitempty"`
Consensus jsonSection `json:"consensus,omitempty"`
API jsonSection `json:"api,omitempty"`
IPFSConn jsonSection `json:"ipfs_connector,omitempty"`
State jsonSection `json:"state,omitempty"`
PinTracker jsonSection `json:"pin_tracker,omitempty"`
Monitor jsonSection `json:"monitor,omitempty"`
Allocator jsonSection `json:"allocator,omitempty"`
Informer jsonSection `json:"informer,omitempty"`
Observations jsonSection `json:"observations,omitempty"`
Datastore jsonSection `json:"datastore,omitempty"`
}
func (jcfg *jsonConfig) getSection(i SectionType) *jsonSection {
switch i {
case Consensus:
return &jcfg.Consensus
case API:
return &jcfg.API
case IPFSConn:
return &jcfg.IPFSConn
case State:
return &jcfg.State
case PinTracker:
return &jcfg.PinTracker
case Monitor:
return &jcfg.Monitor
case Allocator:
return &jcfg.Allocator
case Informer:
return &jcfg.Informer
case Observations:
return &jcfg.Observations
case Datastore:
return &jcfg.Datastore
default:
return nil
}
}
// Default generates a default configuration by generating defaults for all
// registered components.
func (cfg *Manager) Default() error {
for _, section := range cfg.sections {
for k, compcfg := range section {
logger.Debugf("generating default conf for %s", k)
err := compcfg.Default()
if err != nil {
return err
}
}
}
if cfg.clusterConfig != nil {
logger.Debug("generating default conf for cluster")
err := cfg.clusterConfig.Default()
if err != nil {
return err
}
}
return nil
}
// ApplyEnvVars overrides configuration fields with any values found
// in environment variables.
func (cfg *Manager) ApplyEnvVars() error {
for _, section := range cfg.sections {
for k, compcfg := range section {
logger.Debugf("applying environment variables conf for %s", k)
err := compcfg.ApplyEnvVars()
if err != nil {
return err
}
}
}
if cfg.clusterConfig != nil {
logger.Debugf("applying environment variables conf for cluster")
err := cfg.clusterConfig.ApplyEnvVars()
if err != nil {
return err
}
}
return nil
}
// RegisterComponent lets the Manager load and save component configurations
func (cfg *Manager) RegisterComponent(t SectionType, ccfg ComponentConfig) {
cfg.wg.Add(1)
go cfg.watchSave(ccfg.SaveCh())
if t == Cluster {
cfg.clusterConfig = ccfg
return
}
if cfg.sections == nil {
cfg.sections = make(map[SectionType]Section)
}
_, ok := cfg.sections[t]
if !ok {
cfg.sections[t] = make(Section)
}
cfg.sections[t][ccfg.ConfigKey()] = ccfg
_, ok = cfg.undefinedComps[t]
if !ok {
cfg.undefinedComps[t] = make(map[string]bool)
}
}
// Validate checks that all the registered components in this
// Manager have valid configurations. It also makes sure that
// the main Cluster compoenent exists.
func (cfg *Manager) Validate() error {
if cfg.clusterConfig == nil {
return errors.New("no registered cluster section")
}
if cfg.sections == nil {
return errors.New("no registered components")
}
err := cfg.clusterConfig.Validate()
if err != nil {
return fmt.Errorf("cluster section failed to validate: %s", err)
}
for t, section := range cfg.sections {
if section == nil {
return fmt.Errorf("section %d is nil", t)
}
for k, compCfg := range section {
if compCfg == nil {
return fmt.Errorf("%s entry for section %d is nil", k, t)
}
err := compCfg.Validate()
if err != nil {
return fmt.Errorf("%s failed to validate: %s", k, err)
}
}
}
return nil
}
// LoadJSONFromFile reads a Configuration file from disk and parses
// it. See LoadJSON too.
func (cfg *Manager) LoadJSONFromFile(path string) error {
cfg.path = path
file, err := os.ReadFile(path)
if err != nil {
logger.Error("error reading the configuration file: ", err)
return err
}
return cfg.LoadJSON(file)
}
// LoadJSONFromHTTPSource reads a Configuration file from a URL and parses it.
func (cfg *Manager) LoadJSONFromHTTPSource(url string) error {
logger.Infof("loading configuration from %s", url)
cfg.Source = url
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("%w: %s", errFetchingSource, url)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode >= 300 {
return fmt.Errorf("unsuccessful request (%d): %s", resp.StatusCode, body)
}
// Avoid recursively loading remote sources
if cfg.sourceRedirs > 0 {
return errSourceRedirect
}
cfg.sourceRedirs++
// make sure the counter is always reset when function done
defer func() { cfg.sourceRedirs = 0 }()
err = cfg.LoadJSON(body)
if err != nil {
return err
}
return nil
}
// LoadJSONFileAndEnv calls LoadJSONFromFile followed by ApplyEnvVars,
// reading and parsing a Configuration file and then overriding fields
// with any values found in environment variables.
func (cfg *Manager) LoadJSONFileAndEnv(path string) error {
if err := cfg.LoadJSONFromFile(path); err != nil {
return err
}
return cfg.ApplyEnvVars()
}
// LoadJSON parses configurations for all registered components,
// In order to work, component configurations must have been registered
// beforehand with RegisterComponent.
func (cfg *Manager) LoadJSON(bs []byte) error {
dir := filepath.Dir(cfg.path)
jcfg := &jsonConfig{}
err := json.Unmarshal(bs, jcfg)
if err != nil {
logger.Error("error parsing JSON: ", err)
return err
}
cfg.jsonCfg = jcfg
// Handle remote source
if jcfg.Source != "" {
return cfg.LoadJSONFromHTTPSource(jcfg.Source)
}
// Load Cluster section. Needs to have been registered
if cfg.clusterConfig != nil && jcfg.Cluster != nil {
cfg.clusterConfig.SetBaseDir(dir)
err = cfg.clusterConfig.LoadJSON([]byte(*jcfg.Cluster))
if err != nil {
return err
}
}
loadCompJSON := func(name string, component ComponentConfig, jsonSection jsonSection, t SectionType) error {
component.SetBaseDir(dir)
raw, ok := jsonSection[name]
if ok {
err := component.LoadJSON([]byte(*raw))
if err != nil {
return err
}
logger.Debugf("%s component configuration loaded", name)
} else {
cfg.undefinedComps[t][name] = true
logger.Debugf("%s component is empty, generating default", name)
component.Default()
}
return nil
}
// Helper function to load json from each section in the json config
loadSectionJSON := func(section Section, jsonSection jsonSection, t SectionType) error {
for name, component := range section {
err := loadCompJSON(name, component, jsonSection, t)
if err != nil {
logger.Error(err)
return err
}
}
return nil
}
sections := cfg.sections
for _, t := range SectionTypes() {
if t == Cluster {
continue
}
err := loadSectionJSON(sections[t], *jcfg.getSection(t), t)
if err != nil {
return err
}
}
return cfg.Validate()
}
// SaveJSON saves the JSON representation of the Config to
// the given path.
func (cfg *Manager) SaveJSON(path string) error {
cfg.saveMux.Lock()
defer cfg.saveMux.Unlock()
logger.Info("Saving configuration")
if path != "" {
cfg.path = path
}
bs, err := cfg.ToJSON()
if err != nil {
return err
}
return os.WriteFile(cfg.path, bs, 0600)
}
// ToJSON provides a JSON representation of the configuration by
// generating JSON for all componenents registered.
func (cfg *Manager) ToJSON() ([]byte, error) {
dir := filepath.Dir(cfg.path)
err := cfg.Validate()
if err != nil {
return nil, err
}
if cfg.Source != "" {
return DefaultJSONMarshal(&jsonConfig{Source: cfg.Source})
}
jcfg := cfg.jsonCfg
if jcfg == nil {
jcfg = &jsonConfig{}
}
if cfg.clusterConfig != nil {
cfg.clusterConfig.SetBaseDir(dir)
raw, err := cfg.clusterConfig.ToJSON()
if err != nil {
return nil, err
}
jcfg.Cluster = new(json.RawMessage)
*jcfg.Cluster = raw
logger.Debug("writing changes for cluster section")
}
// Given a Section and a *jsonSection, it updates the
// component-configurations in the latter.
updateJSONConfigs := func(section Section, dest *jsonSection) error {
for k, v := range section {
v.SetBaseDir(dir)
logger.Debugf("writing changes for %s section", k)
j, err := v.ToJSON()
if err != nil {
return err
}
if *dest == nil {
*dest = make(jsonSection)
}
jsonSection := *dest
jsonSection[k] = new(json.RawMessage)
*jsonSection[k] = j
}
return nil
}
err = cfg.applyUpdateJSONConfigs(jcfg, updateJSONConfigs)
if err != nil {
return nil, err
}
return DefaultJSONMarshal(jcfg)
}
// ToDisplayJSON returns a printable cluster configuration.
func (cfg *Manager) ToDisplayJSON() ([]byte, error) {
jcfg := &jsonConfig{}
if cfg.clusterConfig != nil {
raw, err := cfg.clusterConfig.ToDisplayJSON()
if err != nil {
return nil, err
}
jcfg.Cluster = new(json.RawMessage)
*jcfg.Cluster = raw
}
updateJSONConfigs := func(section Section, dest *jsonSection) error {
for k, v := range section {
j, err := v.ToDisplayJSON()
if err != nil {
return err
}
if *dest == nil {
*dest = make(jsonSection)
}
jsonSection := *dest
jsonSection[k] = new(json.RawMessage)
*jsonSection[k] = j
}
return nil
}
err := cfg.applyUpdateJSONConfigs(jcfg, updateJSONConfigs)
if err != nil {
return nil, err
}
return DefaultJSONMarshal(jcfg)
}
func (cfg *Manager) applyUpdateJSONConfigs(jcfg *jsonConfig, updateJSONConfigs func(section Section, dest *jsonSection) error) error {
for _, t := range SectionTypes() {
if t == Cluster {
continue
}
jsection := jcfg.getSection(t)
err := updateJSONConfigs(cfg.sections[t], jsection)
if err != nil {
return err
}
}
return nil
}
// IsLoadedFromJSON tells whether the given component belonging to
// the given section type is present in the cluster JSON
// config or not.
func (cfg *Manager) IsLoadedFromJSON(t SectionType, name string) bool {
return !cfg.undefinedComps[t][name]
}
// GetClusterConfig extracts cluster config from the configuration file
// and returns bytes of it
func GetClusterConfig(configPath string) ([]byte, error) {
file, err := os.ReadFile(configPath)
if err != nil {
logger.Error("error reading the configuration file: ", err)
return nil, err
}
jcfg := &jsonConfig{}
err = json.Unmarshal(file, jcfg)
if err != nil {
logger.Error("error parsing JSON: ", err)
return nil, err
}
return []byte(*jcfg.Cluster), nil
}