Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decouple system config from user overrides in ipfs/conf #9000

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions cmd/ipfs/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,28 +248,26 @@ func daemonFunc(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment
if initialize && !fsrepo.IsInitialized(cctx.ConfigRoot) {
cfgLocation, _ := req.Options[initConfigOptionKwd].(string)
profiles, _ := req.Options[initProfileOptionKwd].(string)
var conf *config.Config
var conf config.UserConfigOverrides

if cfgLocation != "" {
if conf, err = cserial.Load(cfgLocation); err != nil {
return err
}
}

if conf == nil {
} else {
identity, err := config.CreateIdentity(os.Stdout, []options.KeyGenerateOption{
options.Key.Type(algorithmDefault),
})
if err != nil {
return err
}
conf, err = config.InitWithIdentity(identity)
conf, err = config.NewUserConfigOverridesWithProfiles(identity, profiles)
if err != nil {
return err
}
}

if err = doInit(os.Stdout, cctx.ConfigRoot, false, profiles, conf); err != nil {
if err = doInit(os.Stdout, cctx.ConfigRoot, false, conf); err != nil {
return err
}
}
Expand Down
56 changes: 22 additions & 34 deletions cmd/ipfs/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,19 @@ package main

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"

assets "github.com/ipfs/go-ipfs/assets"
oldcmds "github.com/ipfs/go-ipfs/commands"
core "github.com/ipfs/go-ipfs/core"
"github.com/ipfs/go-ipfs/core/commands"
"github.com/ipfs/go-ipfs/repo/common"
fsrepo "github.com/ipfs/go-ipfs/repo/fsrepo"
path "github.com/ipfs/go-path"
unixfs "github.com/ipfs/go-unixfs"
"io"
"os"
"path/filepath"

cmds "github.com/ipfs/go-ipfs-cmds"
files "github.com/ipfs/go-ipfs-files"
Expand Down Expand Up @@ -77,7 +75,7 @@ environment variable:
algorithm, _ := req.Options[algorithmOptionName].(string)
nBitsForKeypair, nBitsGiven := req.Options[bitsOptionName].(int)

var conf *config.Config
var conf config.UserConfigOverrides

f := req.Files
if f != nil {
Expand All @@ -93,10 +91,11 @@ environment variable:
return fmt.Errorf("expected a regular file")
}

conf = &config.Config{}
if err := json.NewDecoder(file).Decode(conf); err != nil {
decoded, err := config.DecodeUserConfigOverrides(file)
if err != nil {
return err
}
conf = decoded
}

if conf == nil {
Expand All @@ -115,36 +114,29 @@ environment variable:
if err != nil {
return err
}
conf, err = config.InitWithIdentity(identity)
conf, err = config.NewUserConfigOverrides(identity)
if err != nil {
return err
}
}

profiles, _ := req.Options[profileOptionName].(string)
return doInit(os.Stdout, cctx.ConfigRoot, empty, profiles, conf)
},
}

func applyProfiles(conf *config.Config, profiles string) error {
if profiles == "" {
return nil
}

for _, profile := range strings.Split(profiles, ",") {
transformer, ok := config.Profiles[profile]
if !ok {
return fmt.Errorf("invalid configuration profile: %s", profile)
if profiles, ok := req.Options[profileOptionName].(string); ok {
// Replace old config's profiles with the ones passed as arguments.
err := config.CheckProfiles(profiles)
if err != nil {
return err
}
err = common.MapSetKV(conf, "Profiles", profiles)
if err != nil {
return err
}
}

if err := transformer.Transform(conf); err != nil {
return err
}
}
return nil
return doInit(os.Stdout, cctx.ConfigRoot, empty, conf)
},
}

func doInit(out io.Writer, repoRoot string, empty bool, confProfiles string, conf *config.Config) error {
func doInit(out io.Writer, repoRoot string, empty bool, conf config.UserConfigOverrides) error {
if _, err := fmt.Fprintf(out, "initializing IPFS node at %s\n", repoRoot); err != nil {
return err
}
Expand All @@ -157,10 +149,6 @@ func doInit(out io.Writer, repoRoot string, empty bool, confProfiles string, con
return errRepoExists
}

if err := applyProfiles(conf, confProfiles); err != nil {
return err
}

if err := fsrepo.Init(repoRoot, conf); err != nil {
return err
}
Expand Down
177 changes: 159 additions & 18 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,24 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/ipfs/go-ipfs/repo/common"
"io"
"os"
"path/filepath"
"strings"

"github.com/mitchellh/go-homedir"
)

// Config is used to load ipfs config files.
// Config is the configuration used by an IPFS node.
// NOTE: It is a system-defined configuration with internal defaults that can be
// overridden by a user-defined JSON file (which is *not* the configuration itself).
// FIXME: Currently internal to the Repo interface, it should have its own place
// in the IpfsNode structure to decouple it from the user file. Similarly,
// everything here related to the user overrides should its own file.
type Config struct {
Identity Identity // local node's peer identity
Profiles string // profile from the user overrides to apply to defaults
Datastore Datastore // local node's storage
Addresses Addresses // local node's addresses
Mounts Mounts // local node's mount points
Expand All @@ -40,6 +48,11 @@ type Config struct {
Internal Internal // experimental/unstable options
}

// UserConfigOverrides is the Go map representing the loaded JSON file
// with user-defined configuration overrides. It is guaranteed on load
// that its structure matches a subset of the encoded Config struct.
type UserConfigOverrides map[string]interface{}

const (
// DefaultPathName is the default config dir name
DefaultPathName = ".ipfs"
Expand Down Expand Up @@ -111,41 +124,169 @@ func Marshal(value interface{}) ([]byte, error) {
}

func FromMap(v map[string]interface{}) (*Config, error) {
buf := new(bytes.Buffer)
if err := json.NewEncoder(buf).Encode(v); err != nil {
buf, err := JsonEncode(v)
if err != nil {
return nil, err
}
var conf Config
if err := json.NewDecoder(buf).Decode(&conf); err != nil {
return nil, fmt.Errorf("failure to decode config: %s", err)
}
return &conf, nil
return jsonDecodeConfig(buf)
}

func ToMap(conf *Config) (map[string]interface{}, error) {
buf := new(bytes.Buffer)
if err := json.NewEncoder(buf).Encode(conf); err != nil {
buf, err := JsonEncode(conf)
if err != nil {
return nil, err
}
return jsonDecodeMap(buf)
}

// Clone copies the config. Use when updating.
// FIXME: This can't error. Refactor API.
func (c *Config) Clone() (*Config, error) {
buf, err := JsonEncode(c)
if err != nil {
return nil, err
}
return jsonDecodeConfig(buf)
}

func JsonEncode(v interface{}) (*bytes.Buffer, error) {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(v); err != nil {
return nil, fmt.Errorf("failure to encode config: %s", err)
}
return &buf, nil
}

func jsonDecodeConfig(buf *bytes.Buffer) (*Config, error) {
var c Config
if err := json.NewDecoder(buf).Decode(&c); err != nil {
return nil, fmt.Errorf("failure to decode config: %s", err)
}
return &c, nil
}

func jsonDecodeMap(buf *bytes.Buffer) (map[string]interface{}, error) {
var m map[string]interface{}
if err := json.NewDecoder(buf).Decode(&m); err != nil {
return nil, fmt.Errorf("failure to decode config: %s", err)
}
return m, nil
}

// Clone copies the config. Use when updating.
func (c *Config) Clone() (*Config, error) {
var newConfig Config
func NewUserConfigOverrides(identity Identity) (UserConfigOverrides, error) {
// FIXME: Is there an easier way to encode the Identity in a map?
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(identity); err != nil {
return nil, fmt.Errorf("failure to encode identity: %s", err)
}
var m map[string]interface{}
if err := json.NewDecoder(&buf).Decode(&m); err != nil {
return nil, fmt.Errorf("failure to decode identity: %s", err)
}

if err := json.NewEncoder(&buf).Encode(c); err != nil {
return nil, fmt.Errorf("failure to encode config: %s", err)
return map[string]interface{}{
"Identity": m,
}, nil
}

func NewUserConfigOverridesWithProfiles(identity Identity, profiles string) (UserConfigOverrides, error) {
overrides, err := NewUserConfigOverrides(identity)
if err != nil {
return nil, err
}
err = CheckProfiles(profiles)
if err != nil {
return nil, err
}
overrides["Profiles"] = profiles
return overrides, nil
}

if err := json.NewDecoder(&buf).Decode(&newConfig); err != nil {
return nil, fmt.Errorf("failure to decode config: %s", err)
// OverrideMap replaces keys in left map with the right map, recursively traversing
// child maps until a non-map value is found.
// NOTE: Used for the JSON Config-to-map conversions: maps are expected to have
// same types, otherwise this will panic.
func OverrideMap(left, right map[string]interface{}) {
for key, rightVal := range right {
leftVal, found := left[key]
if !found {
// FIXME: For now nonexistent values in the left will be accepted
// and created from right. This is because JSON-decoded default config, left,
// still has a lot of `json:",omitempty"` that won't be present. In the future
// this should be removed.
left[key] = rightVal
continue
}
leftMap, ok := leftVal.(map[string]interface{})
if !ok {
left[key] = rightVal
continue
}
if rightVal == nil {
return // FIXME: Do we want to clear config values?
// If override is empty we should error when loading the user override
// config file.
}
OverrideMap(leftMap, rightVal.(map[string]interface{}))
}
}

// openConfig returns an error if the config file is not present.
func GetConfig(configFilePath string) (*Config, error) {
overrides, err := ReadUserConfigOverrides(configFilePath)
if err != nil {
return nil, err
}
var profiles string
p, err := common.MapGetKV(overrides, "Profiles")
if err == nil {
if profString, ok := p.(string); ok {
profiles = profString
}
}

defaultConfig, err := DefaultConfig(profiles)
if err != nil {
return nil, err
}

return &newConfig, nil
configMap, err := ToMap(defaultConfig)
if err != nil {
return nil, err
}
// This shoudln't be neccessary but just in case remove Identity, we'll never
// use a default one here.
delete(configMap, "Identity")

OverrideMap(configMap, overrides)
config, err := FromMap(configMap)
if err != nil {
return nil, err
}

return config, nil
}

func ReadUserConfigOverrides(filename string) (UserConfigOverrides, error) {
f, err := os.Open(filename)
if err != nil {
//if os.IsNotExist(err) {
// err = ErrNotInitialized
//}
return nil, err
}
defer f.Close()

return DecodeUserConfigOverrides(f)
}

func DecodeUserConfigOverrides(r io.Reader) (UserConfigOverrides, error) {
var overrides UserConfigOverrides
dec := json.NewDecoder(r)
// FIXME: Check that this matches the contents of the Config struct.
dec.DisallowUnknownFields()
if err := dec.Decode(&overrides); err != nil {
return nil, fmt.Errorf("failure to decode user config: %s", err)
}
return overrides, nil
}
Loading