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 }