629 lines
16 KiB
Go
629 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
|
|
}
|