Generalize loadConfig method to avoid reading from disk (#5819)

As we move to multiple config backends like local disk and etcd,
config file should not be read from the disk, instead the quick
package should load and verify for duplicate entries.
This commit is contained in:
Nitish Tiwari 2018-04-14 03:44:19 +05:30 committed by Harshavardhana
parent 57b8db2088
commit 638f01f9e4
6 changed files with 97 additions and 96 deletions

View File

@ -33,6 +33,7 @@ import (
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/handlers" "github.com/minio/minio/pkg/handlers"
"github.com/minio/minio/pkg/madmin" "github.com/minio/minio/pkg/madmin"
"github.com/minio/minio/pkg/quick"
) )
const ( const (
@ -693,7 +694,7 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
// Validate JSON provided in the request body: check the // Validate JSON provided in the request body: check the
// client has not sent JSON objects with duplicate keys. // client has not sent JSON objects with duplicate keys.
if err = checkDupJSONKeys(string(configBytes)); err != nil { if err = quick.CheckDuplicateKeys(string(configBytes)); err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
writeErrorResponse(w, ErrAdminConfigBadJSON, r.URL) writeErrorResponse(w, ErrAdminConfigBadJSON, r.URL)
return return

View File

@ -19,7 +19,6 @@ package cmd
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"reflect" "reflect"
"sync" "sync"
@ -27,7 +26,6 @@ import (
"github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/event"
"github.com/minio/minio/pkg/event/target" "github.com/minio/minio/pkg/event/target"
"github.com/minio/minio/pkg/quick" "github.com/minio/minio/pkg/quick"
"github.com/tidwall/gjson"
) )
// Steps to move from version N to version N+1 // Steps to move from version N to version N+1
@ -254,57 +252,6 @@ func newConfig() error {
return globalServerConfig.Save() return globalServerConfig.Save()
} }
// doCheckDupJSONKeys recursively detects duplicate json keys
func doCheckDupJSONKeys(key, value gjson.Result) error {
// Key occurrences map of the current scope to count
// if there is any duplicated json key.
keysOcc := make(map[string]int)
// Holds the found error
var checkErr error
// Iterate over keys in the current json scope
value.ForEach(func(k, v gjson.Result) bool {
// If current key is not null, check if its
// value contains some duplicated keys.
if k.Type != gjson.Null {
keysOcc[k.String()]++
checkErr = doCheckDupJSONKeys(k, v)
}
return checkErr == nil
})
// Check found err
if checkErr != nil {
return errors.New(key.String() + " => " + checkErr.Error())
}
// Check for duplicated keys
for k, v := range keysOcc {
if v > 1 {
return errors.New(key.String() + " => `" + k + "` entry is duplicated")
}
}
return nil
}
// Check recursively if a key is duplicated in the same json scope
// e.g.:
// `{ "key" : { "key" ..` is accepted
// `{ "key" : { "subkey" : "val1", "subkey": "val2" ..` throws subkey duplicated error
func checkDupJSONKeys(json string) error {
// Parse config with gjson library
config := gjson.Parse(json)
// Create a fake rootKey since root json doesn't seem to have representation
// in gjson library.
rootKey := gjson.Result{Type: gjson.String, Str: minioConfigFile}
// Check if loaded json contains any duplicated keys
return doCheckDupJSONKeys(rootKey, config)
}
// getValidConfig - returns valid server configuration // getValidConfig - returns valid server configuration
func getValidConfig() (*serverConfig, error) { func getValidConfig() (*serverConfig, error) {
srvCfg := &serverConfig{ srvCfg := &serverConfig{
@ -312,8 +259,7 @@ func getValidConfig() (*serverConfig, error) {
Browser: true, Browser: true,
} }
configFile := getConfigFile() if _, err := quick.Load(getConfigFile(), srvCfg); err != nil {
if _, err := quick.Load(configFile, srvCfg); err != nil {
return nil, err return nil, err
} }
@ -321,21 +267,11 @@ func getValidConfig() (*serverConfig, error) {
return nil, fmt.Errorf("configuration version mismatch. Expected: %s, Got: %s", serverConfigVersion, srvCfg.Version) return nil, fmt.Errorf("configuration version mismatch. Expected: %s, Got: %s", serverConfigVersion, srvCfg.Version)
} }
// Load config file json and check for duplication json keys
jsonBytes, err := ioutil.ReadFile(configFile)
if err != nil {
return nil, err
}
if err = checkDupJSONKeys(string(jsonBytes)); err != nil {
return nil, err
}
// Validate credential fields only when // Validate credential fields only when
// they are not set via the environment // they are not set via the environment
// Error out if global is env credential is not set and config has invalid credential // Error out if global is env credential is not set and config has invalid credential
if !globalIsEnvCreds && !srvCfg.Credential.IsValid() { if !globalIsEnvCreds && !srvCfg.Credential.IsValid() {
return nil, errors.New("invalid credential in config file " + configFile) return nil, errors.New("invalid credential in config file " + getConfigFile())
} }
return srvCfg, nil return srvCfg, nil

View File

@ -24,7 +24,6 @@ import (
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/event/target" "github.com/minio/minio/pkg/event/target"
"github.com/tidwall/gjson"
) )
func TestServerConfig(t *testing.T) { func TestServerConfig(t *testing.T) {
@ -126,34 +125,6 @@ func TestServerConfigWithEnvs(t *testing.T) {
} }
} }
func TestCheckDupJSONKeys(t *testing.T) {
testCases := []struct {
json string
shouldPass bool
}{
{`{}`, true},
{`{"version" : "13"}`, true},
{`{"version" : "13", "version": "14"}`, false},
{`{"version" : "13", "credential": {"accessKey": "12345"}}`, true},
{`{"version" : "13", "credential": {"accessKey": "12345", "accessKey":"12345"}}`, false},
{`{"version" : "13", "notify": {"amqp": {"1"}, "webhook":{"3"}}}`, true},
{`{"version" : "13", "notify": {"amqp": {"1"}, "amqp":{"3"}}}`, false},
{`{"version" : "13", "notify": {"amqp": {"1":{}, "2":{}}}}`, true},
{`{"version" : "13", "notify": {"amqp": {"1":{}, "1":{}}}}`, false},
}
for i, testCase := range testCases {
err := doCheckDupJSONKeys(gjson.Result{}, gjson.Parse(testCase.json))
if testCase.shouldPass && err != nil {
t.Errorf("Test %d, should pass but it failed with err = %v", i+1, err)
}
if !testCase.shouldPass && err == nil {
t.Errorf("Test %d, should fail but it succeed.", i+1)
}
}
}
// Tests config validator.. // Tests config validator..
func TestValidateConfig(t *testing.T) { func TestValidateConfig(t *testing.T) {
rootPath, err := newTestConfig(globalMinioDefaultRegion) rootPath, err := newTestConfig(globalMinioDefaultRegion)

View File

@ -136,6 +136,11 @@ func loadFileConfig(filename string, v interface{}) error {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
fileData = []byte(strings.Replace(string(fileData), "\r\n", "\n", -1)) fileData = []byte(strings.Replace(string(fileData), "\r\n", "\n", -1))
} }
if err = checkDupJSONKeys(string(fileData)); err != nil {
return err
}
// Unmarshal file's content // Unmarshal file's content
return toUnmarshaller(filepath.Ext(filename))(fileData, v) return toUnmarshaller(filepath.Ext(filename))(fileData, v)
} }

View File

@ -21,10 +21,12 @@ package quick
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"errors"
"fmt" "fmt"
"io" "io"
"github.com/cheggaaa/pb" "github.com/cheggaaa/pb"
"github.com/tidwall/gjson"
) )
const errorFmt = "%5d: %s <<<<" const errorFmt = "%5d: %s <<<<"
@ -81,3 +83,59 @@ func FormatJSONSyntaxError(data io.Reader, offset int64) (highlight string) {
return fmt.Sprintf(errorFmt, errLine, readLine.String()[idx:]) return fmt.Sprintf(errorFmt, errLine, readLine.String()[idx:])
} }
// doCheckDupJSONKeys recursively detects duplicate json keys
func doCheckDupJSONKeys(key, value gjson.Result) error {
// Key occurrences map of the current scope to count
// if there is any duplicated json key.
keysOcc := make(map[string]int)
// Holds the found error
var checkErr error
// Iterate over keys in the current json scope
value.ForEach(func(k, v gjson.Result) bool {
// If current key is not null, check if its
// value contains some duplicated keys.
if k.Type != gjson.Null {
keysOcc[k.String()]++
checkErr = doCheckDupJSONKeys(k, v)
}
return checkErr == nil
})
// Check found err
if checkErr != nil {
return errors.New(key.String() + " => " + checkErr.Error())
}
// Check for duplicated keys
for k, v := range keysOcc {
if v > 1 {
return errors.New(key.String() + " => `" + k + "` entry is duplicated")
}
}
return nil
}
// Check recursively if a key is duplicated in the same json scope
// e.g.:
// `{ "key" : { "key" ..` is accepted
// `{ "key" : { "subkey" : "val1", "subkey": "val2" ..` throws subkey duplicated error
func checkDupJSONKeys(json string) error {
// Parse config with gjson library
config := gjson.Parse(json)
// Create a fake rootKey since root json doesn't seem to have representation
// in gjson library.
rootKey := gjson.Result{Type: gjson.String, Str: "config.json"}
// Check if loaded json contains any duplicated keys
return doCheckDupJSONKeys(rootKey, config)
}
// CheckDuplicateKeys - checks for duplicate entries in a JSON file
func CheckDuplicateKeys(json string) error {
return checkDupJSONKeys(json)
}

View File

@ -27,6 +27,8 @@ import (
"runtime" "runtime"
"strings" "strings"
"testing" "testing"
"github.com/tidwall/gjson"
) )
func TestReadVersion(t *testing.T) { func TestReadVersion(t *testing.T) {
@ -504,3 +506,31 @@ func TestDeepDiff(t *testing.T) {
// fmt.Printf("DeepDiff[%d]: %s=%v\n", i, field.Name(), field.Value()) // fmt.Printf("DeepDiff[%d]: %s=%v\n", i, field.Name(), field.Value())
// } // }
} }
func TestCheckDupJSONKeys(t *testing.T) {
testCases := []struct {
json string
shouldPass bool
}{
{`{}`, true},
{`{"version" : "13"}`, true},
{`{"version" : "13", "version": "14"}`, false},
{`{"version" : "13", "credential": {"accessKey": "12345"}}`, true},
{`{"version" : "13", "credential": {"accessKey": "12345", "accessKey":"12345"}}`, false},
{`{"version" : "13", "notify": {"amqp": {"1"}, "webhook":{"3"}}}`, true},
{`{"version" : "13", "notify": {"amqp": {"1"}, "amqp":{"3"}}}`, false},
{`{"version" : "13", "notify": {"amqp": {"1":{}, "2":{}}}}`, true},
{`{"version" : "13", "notify": {"amqp": {"1":{}, "1":{}}}}`, false},
}
for i, testCase := range testCases {
err := doCheckDupJSONKeys(gjson.Result{}, gjson.Parse(testCase.json))
if testCase.shouldPass && err != nil {
t.Errorf("Test %d, should pass but it failed with err = %v", i+1, err)
}
if !testCase.shouldPass && err == nil {
t.Errorf("Test %d, should fail but it succeed.", i+1)
}
}
}