github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/config/config.go (about)

     1  // Package config encapsulates qri configuration options & details. configuration is generally stored
     2  // as a .yaml file, or provided at CLI runtime via command a line argument
     3  package config
     4  
     5  import (
     6  	"context"
     7  	"encoding/json"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"path/filepath"
    11  	"reflect"
    12  
    13  	"github.com/ghodss/yaml"
    14  	"github.com/qri-io/jsonschema"
    15  	"github.com/qri-io/qfs"
    16  	"github.com/qri-io/qri/base/fill"
    17  )
    18  
    19  // CurrentConfigRevision is the latest configuration revision configurations
    20  // that don't match this revision number should be migrated up
    21  const CurrentConfigRevision = 4
    22  
    23  // Config encapsulates all configuration details for qri
    24  type Config struct {
    25  	path string
    26  
    27  	Revision    int
    28  	Profile     *ProfilePod
    29  	Repo        *Repo
    30  	Filesystems []qfs.Config
    31  	P2P         *P2P
    32  	Automation  *Automation
    33  	Stats       *Stats
    34  
    35  	Registry     *Registry
    36  	Remotes      *Remotes
    37  	RemoteServer *RemoteServer
    38  
    39  	CLI     *CLI
    40  	API     *API
    41  	Logging *Logging
    42  }
    43  
    44  // SetArbitrary is an interface implementation of base/fill/struct in order to safely
    45  // consume config files that have definitions beyond those specified in the struct.
    46  // This simply ignores all additional fields at read time.
    47  func (cfg *Config) SetArbitrary(key string, val interface{}) error {
    48  	return nil
    49  }
    50  
    51  // NOTE: The configuration returned by DefaultConfig is insufficient, as is, to run a functional
    52  // qri node. In particular, it lacks cryptographic keys and a peerID, which are necessary to
    53  // join the p2p network. However, these are very expensive to create, so they shouldn't be added
    54  // to the DefaultConfig, which only does the bare minimum necessary to construct the object. In
    55  // real use, the only places a Config object comes from are the cmd/setup command, which builds
    56  // upon DefaultConfig by adding p2p data, and LoadConfig, which parses a serialized config file
    57  // from the user's repo.
    58  
    59  // DefaultConfig gives a new configuration with simple, default settings
    60  func DefaultConfig() *Config {
    61  	return &Config{
    62  		Revision:    CurrentConfigRevision,
    63  		Profile:     DefaultProfile(),
    64  		Repo:        DefaultRepo(),
    65  		Filesystems: DefaultFilesystems(),
    66  		P2P:         DefaultP2P(),
    67  		Automation:  DefaultAutomation(),
    68  		Stats:       DefaultStats(),
    69  
    70  		Registry: DefaultRegistry(),
    71  		// default to no configured remotes
    72  
    73  		CLI:     DefaultCLI(),
    74  		API:     DefaultAPI(),
    75  		Logging: DefaultLogging(),
    76  	}
    77  }
    78  
    79  // SummaryString creates a pretty string summarizing the
    80  // configuration, useful for log output
    81  // TODO (b5): this summary string doesn't confirm these services are actually
    82  // running. we should move this elsewhere
    83  func (cfg Config) SummaryString() (summary string) {
    84  	summary = "\n"
    85  	if cfg.Profile != nil {
    86  		summary += fmt.Sprintf("peername:\t%s\nprofileID:\t%s\n", cfg.Profile.Peername, cfg.Profile.ID)
    87  	}
    88  
    89  	if cfg.API != nil && cfg.API.Enabled {
    90  		summary += fmt.Sprintf("API address:\t%s\n", cfg.API.Address)
    91  	}
    92  
    93  	return summary
    94  }
    95  
    96  // ReadFromFile reads a YAML configuration file from path
    97  func ReadFromFile(path string) (*Config, error) {
    98  	data, err := ioutil.ReadFile(path)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  
   103  	fields := make(map[string]interface{})
   104  	if err = yaml.Unmarshal(data, &fields); err != nil {
   105  		return nil, err
   106  	}
   107  
   108  	cfg := &Config{path: path}
   109  
   110  	if rev, ok := fields["revision"]; ok {
   111  		cfg.Revision = (int)(rev.(float64))
   112  	}
   113  	if err = fill.Struct(fields, cfg); err != nil {
   114  		return cfg, err
   115  	}
   116  
   117  	return cfg, nil
   118  }
   119  
   120  // SetPath assigns unexported filepath to write config to
   121  func (cfg *Config) SetPath(path string) {
   122  	cfg.path = path
   123  }
   124  
   125  // Path gives the unexported filepath for a config
   126  func (cfg Config) Path() string {
   127  	return cfg.path
   128  }
   129  
   130  // WriteToFile encodes a configration to YAML and writes it to path
   131  func (cfg Config) WriteToFile(path string) error {
   132  	// Never serialize the address mapping to the configuration file.
   133  	prev := cfg.Profile.PeerIDs
   134  	cfg.Profile.NetworkAddrs = nil
   135  	cfg.Profile.Online = false
   136  	cfg.Profile.PeerIDs = nil
   137  	defer func() { cfg.Profile.PeerIDs = prev }()
   138  
   139  	data, err := yaml.Marshal(cfg)
   140  	if err != nil {
   141  		return err
   142  	}
   143  
   144  	return ioutil.WriteFile(path, data, 0644)
   145  }
   146  
   147  // Get a config value with case.insensitive.dot.separated.paths
   148  func (cfg Config) Get(path string) (interface{}, error) {
   149  	return fill.GetPathValue(path, cfg)
   150  }
   151  
   152  // Set a config value with case.insensitive.dot.separated.paths
   153  func (cfg *Config) Set(path string, value interface{}) error {
   154  	return fill.SetPathValue(path, value, cfg)
   155  }
   156  
   157  // ImmutablePaths returns a map of paths that should never be modified
   158  func ImmutablePaths() map[string]bool {
   159  	return map[string]bool{
   160  		"p2p.peerid":      true,
   161  		"p2p.privkey":     true,
   162  		"profile.id":      true,
   163  		"profile.privkey": true,
   164  		"profile.created": true,
   165  		"profile.updated": true,
   166  	}
   167  }
   168  
   169  // valiate is a helper function that wraps json.Marshal an ValidateBytes
   170  // it is used by each struct that is in a Config field (eg API, Profile, etc)
   171  func validate(rs *jsonschema.Schema, s interface{}) error {
   172  	ctx := context.Background()
   173  	strct, err := json.Marshal(s)
   174  	if err != nil {
   175  		return fmt.Errorf("error marshaling profile to json: %s", err)
   176  	}
   177  	if errors, err := rs.ValidateBytes(ctx, strct); len(errors) > 0 {
   178  		return fmt.Errorf("%s", errors[0])
   179  	} else if err != nil {
   180  		return err
   181  	}
   182  	return nil
   183  }
   184  
   185  type validator interface {
   186  	Validate() error
   187  }
   188  
   189  // Validate validates each section of the config struct,
   190  // returning the first error
   191  func (cfg Config) Validate() error {
   192  	schema := jsonschema.Must(`{
   193      "$schema": "http://json-schema.org/draft-06/schema#",
   194      "title": "config",
   195      "description": "qri configuration",
   196      "type": "object",
   197      "required": ["Profile", "Repo", "Filesystems", "P2P", "CLI", "API", "Automation"],
   198      "properties" : {
   199  			"Profile" : { "type":"object" },
   200  			"Repo" : { "type":"object" },
   201  			"Filesystems" : { "type":"array" },
   202  			"P2P" : { "type":"object" },
   203  			"CLI" : { "type":"object" },
   204  			"API" : { "type":"object" }
   205      }
   206    }`)
   207  	if err := validate(schema, &cfg); err != nil {
   208  		return fmt.Errorf("config validation error: %s", err)
   209  	}
   210  
   211  	validators := []validator{
   212  		cfg.Profile,
   213  		cfg.Repo,
   214  		cfg.P2P,
   215  		cfg.CLI,
   216  		cfg.API,
   217  		cfg.Logging,
   218  		cfg.Automation,
   219  	}
   220  	for _, val := range validators {
   221  		// we need to check here because we're potentially calling methods on nil
   222  		// values that don't handle a nil receiver gracefully.
   223  		// https://tour.golang.org/methods/12
   224  		// https://groups.google.com/forum/#!topic/golang-nuts/wnH302gBa4I/discussion
   225  		// TODO (b5) - make validate methods handle being nil
   226  		if !reflect.ValueOf(val).IsNil() {
   227  			if err := val.Validate(); err != nil {
   228  				return err
   229  			}
   230  		}
   231  	}
   232  
   233  	return nil
   234  }
   235  
   236  // Copy returns a deep copy of the Config struct
   237  func (cfg *Config) Copy() *Config {
   238  	res := &Config{
   239  		Revision: cfg.Revision,
   240  	}
   241  	if cfg.path != "" {
   242  		res.path = cfg.path
   243  	}
   244  	if cfg.Profile != nil {
   245  		res.Profile = cfg.Profile.Copy()
   246  	}
   247  	if cfg.Repo != nil {
   248  		res.Repo = cfg.Repo.Copy()
   249  	}
   250  	if cfg.P2P != nil {
   251  		res.P2P = cfg.P2P.Copy()
   252  	}
   253  	if cfg.Registry != nil {
   254  		res.Registry = cfg.Registry.Copy()
   255  	}
   256  	if cfg.CLI != nil {
   257  		res.CLI = cfg.CLI.Copy()
   258  	}
   259  	if cfg.API != nil {
   260  		res.API = cfg.API.Copy()
   261  	}
   262  	if cfg.Remotes != nil {
   263  		res.Remotes = cfg.Remotes.Copy()
   264  	}
   265  	if cfg.RemoteServer != nil {
   266  		res.RemoteServer = cfg.RemoteServer.Copy()
   267  	}
   268  	if cfg.Logging != nil {
   269  		res.Logging = cfg.Logging.Copy()
   270  	}
   271  	if cfg.Stats != nil {
   272  		res.Stats = cfg.Stats.Copy()
   273  	}
   274  	if cfg.Automation != nil {
   275  		res.Automation = cfg.Automation.Copy()
   276  	}
   277  	if cfg.Filesystems != nil {
   278  		for _, fs := range cfg.Filesystems {
   279  			res.Filesystems = append(res.Filesystems, fs)
   280  		}
   281  	}
   282  
   283  	return res
   284  }
   285  
   286  // WithoutPrivateValues returns a deep copy of the receiver with the private values removed
   287  func (cfg *Config) WithoutPrivateValues() *Config {
   288  	res := cfg.Copy()
   289  
   290  	res.Profile.PrivKey = ""
   291  	res.P2P.PrivKey = ""
   292  
   293  	return res
   294  }
   295  
   296  // WithPrivateValues returns a deep copy of the receiver with the private values from
   297  // the *Config passed in from the params
   298  func (cfg *Config) WithPrivateValues(p *Config) *Config {
   299  	res := cfg.Copy()
   300  
   301  	res.Profile.PrivKey = p.Profile.PrivKey
   302  	res.P2P.PrivKey = p.P2P.PrivKey
   303  
   304  	return res
   305  }
   306  
   307  // DefaultFilesystems is the default filesystem stack
   308  func DefaultFilesystems() []qfs.Config {
   309  	return []qfs.Config{
   310  		{
   311  			Type: "ipfs",
   312  			Config: map[string]interface{}{
   313  				"path": filepath.Join(".", "ipfs"),
   314  			},
   315  		},
   316  		{Type: "local"},
   317  		{Type: "http"},
   318  	}
   319  }