diff --git a/cmd/admin-handlers.go b/cmd/admin-handlers.go index 2f6333c80..525d308d5 100644 --- a/cmd/admin-handlers.go +++ b/cmd/admin-handlers.go @@ -1,5 +1,5 @@ /* - * Minio Cloud Storage, (C) 2016, 2017 Minio, Inc. + * Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,10 +19,13 @@ package cmd import ( "bytes" "context" + "encoding/base64" "encoding/json" "errors" "io" "net/http" + "strconv" + "strings" "sync" "time" @@ -32,6 +35,8 @@ import ( "github.com/minio/minio/pkg/handlers" "github.com/minio/minio/pkg/madmin" "github.com/minio/minio/pkg/quick" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" ) const ( @@ -460,7 +465,7 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques return } - configData, err := json.Marshal(config) + configData, err := json.MarshalIndent(config, "", "\t") if err != nil { logger.LogIf(ctx, err) writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) @@ -478,6 +483,90 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques writeSuccessResponseJSON(w, econfigData) } +// Disable tidwall json array notation in JSON key path so +// users can set json with a key as a number. +// In tidwall json, notify.webhook.0 = val means { "notify" : { "webhook" : [val] }} +// In Minio, notify.webhook.0 = val means { "notify" : { "webhook" : {"0" : val}}} +func normalizeJSONKey(input string) (key string) { + subKeys := strings.Split(input, ".") + for i, k := range subKeys { + if i > 0 { + key += "." + } + if _, err := strconv.Atoi(k); err == nil { + key += ":" + k + } else { + key += k + } + } + return +} + +// GetConfigHandler - GET /minio/admin/v1/config-keys +// Get some keys in config.json of this minio setup. +func (a adminAPIHandlers) GetConfigKeysHandler(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "GetConfigKeysHandler") + + // Get current object layer instance. + objectAPI := newObjectLayerFn() + if objectAPI == nil { + writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL) + return + } + + // Validate request signature. + adminAPIErr := checkAdminRequestAuthType(r, "") + if adminAPIErr != ErrNone { + writeErrorResponseJSON(w, adminAPIErr, r.URL) + return + } + + var keys []string + queries := r.URL.Query() + + for k := range queries { + keys = append(keys, k) + } + + config, err := readServerConfig(ctx, objectAPI) + if err != nil { + writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) + return + } + + configData, err := json.Marshal(config) + if err != nil { + logger.LogIf(ctx, err) + writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) + return + } + + configStr := string(configData) + newConfigStr := `{}` + + for _, key := range keys { + // sjson.Set does not return an error if key is empty + // we should check by ourselves here + if key == "" { + continue + } + val := gjson.Get(configStr, key) + if j, err := sjson.Set(newConfigStr, normalizeJSONKey(key), val.Value()); err == nil { + newConfigStr = j + } + } + + password := config.GetCredential().SecretKey + econfigData, err := madmin.EncryptServerConfigData(password, []byte(newConfigStr)) + if err != nil { + logger.LogIf(ctx, err) + writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) + return + } + + writeSuccessResponseJSON(w, []byte(econfigData)) +} + // toAdminAPIErrCode - converts errXLWriteQuorum error to admin API // specific error. func toAdminAPIErrCode(err error) APIErrorCode { @@ -507,6 +596,12 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques return } + // Deny if WORM is enabled + if globalWORMEnabled { + writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL) + return + } + // Read configuration bytes from request body. configBuf := make([]byte, maxConfigJSONSize+1) n, err := io.ReadFull(r.Body, configBuf) @@ -561,7 +656,7 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques return } - if err = saveServerConfig(objectAPI, &config); err != nil { + if err = saveServerConfig(ctx, objectAPI, &config); err != nil { writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) return } @@ -572,6 +667,139 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques sendServiceCmd(globalAdminPeers, serviceRestart) } +func convertValueType(elem []byte, jsonType gjson.Type) (interface{}, error) { + str := string(elem) + switch jsonType { + case gjson.False, gjson.True: + return strconv.ParseBool(str) + case gjson.JSON: + return gjson.Parse(str).Value(), nil + case gjson.String: + return str, nil + case gjson.Number: + return strconv.ParseFloat(str, 64) + default: + return nil, nil + } +} + +// SetConfigKeysHandler - PUT /minio/admin/v1/config-keys +func (a adminAPIHandlers) SetConfigKeysHandler(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "SetConfigKeysHandler") + + // Get current object layer instance. + objectAPI := newObjectLayerFn() + if objectAPI == nil { + writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL) + return + } + + // Deny if WORM is enabled + if globalWORMEnabled { + writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL) + return + } + + // Validate request signature. + adminAPIErr := checkAdminRequestAuthType(r, "") + if adminAPIErr != ErrNone { + writeErrorResponseJSON(w, adminAPIErr, r.URL) + return + } + + // Load config + configStruct, err := readServerConfig(ctx, objectAPI) + if err != nil { + writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) + return + } + + // Convert config to json bytes + configBytes, err := json.Marshal(configStruct) + if err != nil { + logger.LogIf(ctx, err) + writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) + return + } + + configStr := string(configBytes) + + queries := r.URL.Query() + password := globalServerConfig.GetCredential().SecretKey + + // Set key values in the JSON config + for k := range queries { + // Decode encrypted data associated to the current key + encryptedElem, dErr := base64.StdEncoding.DecodeString(queries.Get(k)) + if dErr != nil { + reqInfo := (&logger.ReqInfo{}).AppendTags("key", k) + ctx = logger.SetReqInfo(ctx, reqInfo) + logger.LogIf(ctx, dErr) + writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL) + return + } + elem, dErr := madmin.DecryptServerConfigData(password, bytes.NewBuffer([]byte(encryptedElem))) + if dErr != nil { + logger.LogIf(ctx, dErr) + writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL) + return + } + // Calculate the type of the current key from the + // original config json + jsonFieldType := gjson.Get(configStr, k).Type + // Convert passed value to json filed type + val, cErr := convertValueType(elem, jsonFieldType) + if cErr != nil { + writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, cErr.Error(), r.URL) + return + } + // Set the key/value in the new json document + if s, sErr := sjson.Set(configStr, normalizeJSONKey(k), val); sErr == nil { + configStr = s + } + } + + configBytes = []byte(configStr) + + // Validate config + var config serverConfig + if err = json.Unmarshal(configBytes, &config); err != nil { + writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL) + return + } + + if err = config.Validate(); err != nil { + writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL) + return + } + + if err = config.TestNotificationTargets(); err != nil { + writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL) + return + } + + // If credentials for the server are provided via environment, + // then credentials in the provided configuration must match. + if globalIsEnvCreds { + creds := globalServerConfig.GetCredential() + if config.Credential.AccessKey != creds.AccessKey || + config.Credential.SecretKey != creds.SecretKey { + writeErrorResponseJSON(w, ErrAdminCredentialsMismatch, r.URL) + return + } + } + + if err = saveServerConfig(ctx, objectAPI, &config); err != nil { + writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) + return + } + + // Send success response + writeSuccessResponseHeadersOnly(w) + + sendServiceCmd(globalAdminPeers, serviceRestart) +} + // UpdateCredsHandler - POST /minio/admin/v1/config/credential // ---------- // Update credentials in a minio server. In a distributed setup, @@ -645,16 +873,17 @@ func (a adminAPIHandlers) UpdateCredentialsHandler(w http.ResponseWriter, // Update local credentials in memory. globalServerConfig.SetCredential(creds) - if err = saveServerConfig(objectAPI, globalServerConfig); err != nil { + if err = saveServerConfig(ctx, objectAPI, globalServerConfig); err != nil { writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) return } // Notify all other Minio peers to update credentials for host, err := range globalNotificationSys.LoadCredentials() { - reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", host.String()) - ctx := logger.SetReqInfo(ctx, reqInfo) - logger.LogIf(ctx, err) + if err != nil { + logger.GetReqInfo(ctx).SetTags("peerAddress", host.String()) + logger.LogIf(ctx, err) + } } // Reply to the client before restarting minio server. diff --git a/cmd/admin-router.go b/cmd/admin-router.go index 1f1d04c82..f5a209a26 100644 --- a/cmd/admin-router.go +++ b/cmd/admin-router.go @@ -68,4 +68,9 @@ func registerAdminRouter(router *mux.Router) { adminV1Router.Methods(http.MethodGet).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigHandler)) // Set config adminV1Router.Methods(http.MethodPut).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigHandler)) + + // Get config keys/values + adminV1Router.Methods(http.MethodGet).Path("/config-keys").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigKeysHandler)) + // Set config keys/values + adminV1Router.Methods(http.MethodPut).Path("/config-keys").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigKeysHandler)) } diff --git a/cmd/api-errors.go b/cmd/api-errors.go index fab4497d5..c0bd4436e 100644 --- a/cmd/api-errors.go +++ b/cmd/api-errors.go @@ -290,6 +290,7 @@ const ( ErrEvaluatorBindingDoesNotExist ErrInvalidColumnIndex ErrMissingHeaders + ErrAdminConfigNotificationTargetsFailed ) // error code to APIError structure, these fields carry respective @@ -886,6 +887,11 @@ var errorCodeResponse = map[APIErrorCode]APIError{ Description: "JSON configuration provided has objects with duplicate keys", HTTPStatusCode: http.StatusBadRequest, }, + ErrAdminConfigNotificationTargetsFailed: { + Code: "XMinioAdminNotificationTargetsTestFailed", + Description: "Configuration update failed due an unsuccessful attempt to connect to one or more notification servers", + HTTPStatusCode: http.StatusBadRequest, + }, ErrAdminCredentialsMismatch: { Code: "XMinioAdminCredentialsMismatch", Description: "Credentials in config mismatch with server environment variables", @@ -1443,7 +1449,7 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) { apiErr = ErrKMSNotConfigured case crypto.ErrKMSAuthLogin: apiErr = ErrKMSAuthFailure - case context.Canceled, context.DeadlineExceeded: + case errOperationTimedOut, context.Canceled, context.DeadlineExceeded: apiErr = ErrOperationTimedOut } switch err { diff --git a/cmd/config-current.go b/cmd/config-current.go index 92c852aa2..1dbacc9b4 100644 --- a/cmd/config-current.go +++ b/cmd/config-current.go @@ -288,6 +288,102 @@ func (s *serverConfig) loadFromEnvs() { } } +// TestNotificationTargets tries to establish connections to all notification +// targets when enabled. This is a good way to make sure all configurations +// set by the user can work. +func (s *serverConfig) TestNotificationTargets() error { + for k, v := range s.Notify.AMQP { + if !v.Enable { + continue + } + t, err := target.NewAMQPTarget(k, v) + if err != nil { + return fmt.Errorf("amqp(%s): %s", k, err.Error()) + } + t.Close() + } + + for k, v := range s.Notify.Elasticsearch { + if !v.Enable { + continue + } + t, err := target.NewElasticsearchTarget(k, v) + if err != nil { + return fmt.Errorf("elasticsearch(%s): %s", k, err.Error()) + } + t.Close() + } + + for k, v := range s.Notify.Kafka { + if !v.Enable { + continue + } + t, err := target.NewKafkaTarget(k, v) + if err != nil { + return fmt.Errorf("kafka(%s): %s", k, err.Error()) + } + t.Close() + } + + for k, v := range s.Notify.MQTT { + if !v.Enable { + continue + } + t, err := target.NewMQTTTarget(k, v) + if err != nil { + return fmt.Errorf("mqtt(%s): %s", k, err.Error()) + } + t.Close() + } + + for k, v := range s.Notify.MySQL { + if !v.Enable { + continue + } + t, err := target.NewMySQLTarget(k, v) + if err != nil { + return fmt.Errorf("mysql(%s): %s", k, err.Error()) + } + t.Close() + } + + for k, v := range s.Notify.NATS { + if !v.Enable { + continue + } + t, err := target.NewNATSTarget(k, v) + if err != nil { + return fmt.Errorf("nats(%s): %s", k, err.Error()) + } + t.Close() + } + + for k, v := range s.Notify.PostgreSQL { + if !v.Enable { + continue + } + t, err := target.NewPostgreSQLTarget(k, v) + if err != nil { + return fmt.Errorf("postgreSQL(%s): %s", k, err.Error()) + } + t.Close() + } + + for k, v := range s.Notify.Redis { + if !v.Enable { + continue + } + t, err := target.NewRedisTarget(k, v) + if err != nil { + return fmt.Errorf("redis(%s): %s", k, err.Error()) + } + t.Close() + + } + + return nil +} + // Returns the string describing a difference with the given // configuration object. If the given configuration object is // identical, an empty string is returned. @@ -448,7 +544,7 @@ func newConfig(objAPI ObjectLayer) error { globalServerConfigMu.Unlock() // Save config into file. - return saveServerConfig(objAPI, globalServerConfig) + return saveServerConfig(context.Background(), objAPI, globalServerConfig) } // getValidConfig - returns valid server configuration diff --git a/cmd/config-current_test.go b/cmd/config-current_test.go index 3db9c017f..2b456d126 100644 --- a/cmd/config-current_test.go +++ b/cmd/config-current_test.go @@ -17,6 +17,7 @@ package cmd import ( + "context" "os" "path" "testing" @@ -51,7 +52,7 @@ func TestServerConfig(t *testing.T) { t.Errorf("Expecting version %s found %s", globalServerConfig.GetVersion(), serverConfigVersion) } - if err := saveServerConfig(objLayer, globalServerConfig); err != nil { + if err := saveServerConfig(context.Background(), objLayer, globalServerConfig); err != nil { t.Fatalf("Unable to save updated config file %s", err) } diff --git a/cmd/config-migrate.go b/cmd/config-migrate.go index 796cfaf5e..626bf77ec 100644 --- a/cmd/config-migrate.go +++ b/cmd/config-migrate.go @@ -2428,7 +2428,7 @@ func migrateV27ToV28MinioSys(objAPI ObjectLayer) error { srvConfig.Version = "28" srvConfig.KMS = crypto.KMSConfig{} - if err = saveServerConfig(objAPI, srvConfig); err != nil { + if err = saveServerConfig(context.Background(), objAPI, srvConfig); err != nil { return fmt.Errorf("Failed to migrate config from ‘27’ to ‘28’. %v", err) } diff --git a/cmd/config.go b/cmd/config.go index f7805f0ac..2cf6ffc2e 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -37,26 +37,49 @@ const ( // Minio configuration file. minioConfigFile = "config.json" + + // Minio backup file + minioConfigBackupFile = minioConfigFile + ".backup" ) -func saveServerConfig(objAPI ObjectLayer, config *serverConfig) error { +func saveServerConfig(ctx context.Context, objAPI ObjectLayer, config *serverConfig) error { if err := quick.CheckData(config); err != nil { return err } - data, err := json.Marshal(config) + data, err := json.MarshalIndent(config, "", "\t") if err != nil { return err } configFile := path.Join(minioConfigPrefix, minioConfigFile) if globalEtcdClient != nil { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - _, err := globalEtcdClient.Put(ctx, configFile, string(data)) + timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Minute) + _, err := globalEtcdClient.Put(timeoutCtx, configFile, string(data)) defer cancel() return err } + // Create a backup of the current config + reader, err := readConfig(ctx, objAPI, configFile) + if err == nil { + var oldData []byte + oldData, err = ioutil.ReadAll(reader) + if err != nil { + return err + } + backupConfigFile := path.Join(minioConfigPrefix, minioConfigBackupFile) + err = saveConfig(objAPI, backupConfigFile, oldData) + if err != nil { + return err + } + } else { + if err != errConfigNotFound { + return err + } + } + + // Save the new config in the std config path return saveConfig(objAPI, configFile, data) } @@ -214,7 +237,7 @@ func migrateConfigToMinioSys(objAPI ObjectLayer) error { return err } - return saveServerConfig(objAPI, config) + return saveServerConfig(context.Background(), objAPI, config) } // Initialize and load config from remote etcd or local config directory diff --git a/cmd/logger/reqinfo.go b/cmd/logger/reqinfo.go index 863309fe8..616b295c2 100644 --- a/cmd/logger/reqinfo.go +++ b/cmd/logger/reqinfo.go @@ -68,6 +68,29 @@ func (r *ReqInfo) AppendTags(key string, val string) *ReqInfo { return r } +// SetTags - sets key/val to ReqInfo.tags +func (r *ReqInfo) SetTags(key string, val string) *ReqInfo { + if r == nil { + return nil + } + r.Lock() + defer r.Unlock() + // Search of tag key already exists in tags + var updated bool + for _, tag := range r.tags { + if tag.Key == key { + tag.Val = val + updated = true + break + } + } + if !updated { + // Append to the end of tags list + r.tags = append(r.tags, KeyVal{key, val}) + } + return r +} + // GetTags - returns the user defined tags func (r *ReqInfo) GetTags() []KeyVal { if r == nil { diff --git a/cmd/test-utils_test.go b/cmd/test-utils_test.go index 3befb18c6..ca29b0a60 100644 --- a/cmd/test-utils_test.go +++ b/cmd/test-utils_test.go @@ -502,7 +502,7 @@ func newTestConfig(bucketLocation string, obj ObjectLayer) (err error) { globalServerConfig.SetRegion(bucketLocation) // Save config. - return saveServerConfig(obj, globalServerConfig) + return saveServerConfig(context.Background(), obj, globalServerConfig) } // Deleting the temporary backend and stopping the server. diff --git a/cmd/typed-errors.go b/cmd/typed-errors.go index f261f2ffc..be00ddca6 100644 --- a/cmd/typed-errors.go +++ b/cmd/typed-errors.go @@ -50,6 +50,9 @@ var errRPCAPIVersionUnsupported = errors.New("Unsupported rpc API version") // errServerTimeMismatch - server times are too far apart. var errServerTimeMismatch = errors.New("Server times are too far apart") +// errOperationTimedOut +var errOperationTimedOut = errors.New("Operation timed out") + // errInvalidBucketName - bucket name is reserved for Minio, usually // returned for 'minio', '.minio.sys', buckets with capital letters. var errInvalidBucketName = errors.New("The specified bucket is not valid") diff --git a/cmd/web-handlers.go b/cmd/web-handlers.go index aedf659bc..296d441e5 100644 --- a/cmd/web-handlers.go +++ b/cmd/web-handlers.go @@ -534,7 +534,7 @@ func (web *webAPIHandlers) SetAuth(r *http.Request, args *SetAuthArgs, reply *Se prevCred := globalServerConfig.SetCredential(creds) // Persist updated credentials. - if err = saveServerConfig(newObjectLayerFn(), globalServerConfig); err != nil { + if err = saveServerConfig(context.Background(), newObjectLayerFn(), globalServerConfig); err != nil { // Save the current creds when failed to update. globalServerConfig.SetCredential(prevCred) logger.LogIf(context.Background(), err) diff --git a/pkg/madmin/API.md b/pkg/madmin/API.md index bd0de0435..93064eb6f 100644 --- a/pkg/madmin/API.md +++ b/pkg/madmin/API.md @@ -40,6 +40,8 @@ func main() { |:----------------------------|:----------------------------|:--------------------------------------|:--------------------------|:------------------------------------| | [`ServiceStatus`](#ServiceStatus) | [`ServerInfo`](#ServerInfo) | [`Heal`](#Heal) | [`GetConfig`](#GetConfig) | [`SetCredentials`](#SetCredentials) | | [`ServiceSendAction`](#ServiceSendAction) | | | [`SetConfig`](#SetConfig) | | +| | | | [`GetConfigKeys`](#GetConfigKeys) | | +| | | | [`SetConfigKeys`](#SetConfigKeys) | | ## 1. Constructor @@ -326,6 +328,47 @@ __Example__ log.Println("SetConfig: ", string(buf.Bytes())) ``` + +### GetConfigKeys(keys []string) ([]byte, error) +Get a json document which contains a set of keys and their values from config.json. + +__Example__ + +``` go + configBytes, err := madmClnt.GetConfigKeys([]string{"version", "notify.amqp.1"}) + if err != nil { + log.Fatalf("failed due to: %v", err) + } + + // Pretty-print config received as json. + var buf bytes.Buffer + err = json.Indent(buf, configBytes, "", "\t") + if err != nil { + log.Fatalf("failed due to: %v", err) + } + + log.Println("config received successfully: ", string(buf.Bytes())) +``` + + + +### SetConfigKeys(params map[string]string) error +Set a set of keys and values for Minio server or distributed setup and restart the Minio +server for the new configuration changes to take effect. + +__Example__ + +``` go + err := madmClnt.SetConfigKeys(map[string]string{"notify.webhook.1": "{\"enable\": true, \"endpoint\": \"http://example.com/api\"}"}) + if err != nil { + log.Fatalf("failed due to: %v", err) + } + + log.Println("New configuration successfully set") +``` + + + ## 8. Misc operations diff --git a/pkg/madmin/config-commands.go b/pkg/madmin/config-commands.go index 429a7aaf0..4dc10bb02 100644 --- a/pkg/madmin/config-commands.go +++ b/pkg/madmin/config-commands.go @@ -20,12 +20,14 @@ package madmin import ( "bytes" "crypto/rand" + "encoding/base64" "encoding/json" "errors" "fmt" "io" "io/ioutil" "net/http" + "net/url" "github.com/minio/minio/pkg/quick" "github.com/minio/sio" @@ -90,6 +92,36 @@ func (adm *AdminClient) GetConfig() ([]byte, error) { return DecryptServerConfigData(adm.secretAccessKey, resp.Body) } +// GetConfigKeys - returns partial json or json value from config.json of a minio setup. +func (adm *AdminClient) GetConfigKeys(keys []string) ([]byte, error) { + // No TLS? + if !adm.secure { + // return nil, fmt.Errorf("credentials/configuration cannot be retrieved over an insecure connection") + } + + queryVals := make(url.Values) + for _, k := range keys { + queryVals.Add(k, "") + } + + // Execute GET on /minio/admin/v1/config-keys to get config of a setup. + resp, err := adm.executeMethod("GET", + requestData{ + relPath: "/v1/config-keys", + queryValues: queryVals, + }) + defer closeResponse(resp) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, httpRespToErrorResponse(resp) + } + + return DecryptServerConfigData(adm.secretAccessKey, resp.Body) +} + // SetConfig - set config supplied as config.json for the setup. func (adm *AdminClient) SetConfig(config io.Reader) (err error) { const maxConfigJSONSize = 256 * 1024 // 256KiB @@ -148,3 +180,35 @@ func (adm *AdminClient) SetConfig(config io.Reader) (err error) { return nil } + +// SetConfigKeys - set config keys supplied as config.json for the setup. +func (adm *AdminClient) SetConfigKeys(params map[string]string) error { + queryVals := make(url.Values) + for k, v := range params { + encryptedVal, err := EncryptServerConfigData(adm.secretAccessKey, []byte(v)) + if err != nil { + return err + } + encodedVal := base64.StdEncoding.EncodeToString(encryptedVal) + queryVals.Add(k, string(encodedVal)) + } + + reqData := requestData{ + relPath: "/v1/config-keys", + queryValues: queryVals, + } + + // Execute PUT on /minio/admin/v1/config-keys to set config. + resp, err := adm.executeMethod("PUT", reqData) + + defer closeResponse(resp) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return httpRespToErrorResponse(resp) + } + + return nil +} diff --git a/pkg/madmin/examples/get-config-keys.go b/pkg/madmin/examples/get-config-keys.go new file mode 100644 index 000000000..e64d1c818 --- /dev/null +++ b/pkg/madmin/examples/get-config-keys.go @@ -0,0 +1,54 @@ +// +build ignore + +/* + * Minio Cloud Storage, (C) 2017 Minio, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package main + +import ( + "bytes" + "encoding/json" + "log" + + "github.com/minio/minio/pkg/madmin" +) + +func main() { + // Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY are + // dummy values, please replace them with original values. + + // API requests are secure (HTTPS) if secure=true and insecure (HTTPS) otherwise. + // New returns an Minio Admin client object. + madmClnt, err := madmin.New("your-minio.example.com:9000", "YOUR-ACCESSKEYID", "YOUR-SECRETACCESSKEY", true) + if err != nil { + log.Fatalln(err) + } + + configBytes, err := madmClnt.GetConfigKeys([]string{"notify.amqp.1", "version"}) + if err != nil { + log.Fatalf("failed due to: %v", err) + } + + // Pretty-print config received as json. + var buf bytes.Buffer + err = json.Indent(&buf, configBytes, "", "\t") + if err != nil { + log.Fatalf("failed due to: %v", err) + } + + log.Println("config received successfully: ", string(buf.Bytes())) +} diff --git a/pkg/madmin/examples/get-config.go b/pkg/madmin/examples/get-config.go index 87e6790a0..3bf6e0f22 100644 --- a/pkg/madmin/examples/get-config.go +++ b/pkg/madmin/examples/get-config.go @@ -1,4 +1,6 @@ -/* +build ignore +// +build ignore + +/* * Minio Cloud Storage, (C) 2017 Minio, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/pkg/madmin/examples/set-config-keys.go b/pkg/madmin/examples/set-config-keys.go new file mode 100644 index 000000000..9db933ac0 --- /dev/null +++ b/pkg/madmin/examples/set-config-keys.go @@ -0,0 +1,53 @@ +// +build ignore + +/* + * Minio Cloud Storage, (C) 2017 Minio, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package main + +import ( + "fmt" + "log" + + "github.com/minio/minio/pkg/madmin" +) + +func main() { + // Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY are + // dummy values, please replace them with original values. + + // Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY are + // dummy values, please replace them with original values. + + // API requests are secure (HTTPS) if secure=true and insecure (HTTPS) otherwise. + // New returns an Minio Admin client object. + madmClnt, err := madmin.New("your-minio.example.com:9000", "YOUR-ACCESSKEYID", "YOUR-SECRETACCESSKEY", true) + if err != nil { + log.Fatalln(err) + } + + err = madmClnt.SetConfigKeys(map[string]string{ + "domain": "example.com", + "notify.webhook.1": "{\"enable\": true, \"endpoint\": \"http://example.com/api/object-notifications\"}", + }) + + if err != nil { + log.Fatalln(err) + } + + fmt.Println("Setting new configuration successfully executed.") +} diff --git a/vendor/github.com/tidwall/sjson/LICENSE b/vendor/github.com/tidwall/sjson/LICENSE new file mode 100644 index 000000000..89593c7c8 --- /dev/null +++ b/vendor/github.com/tidwall/sjson/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Josh Baker + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/vendor/github.com/tidwall/sjson/README.md b/vendor/github.com/tidwall/sjson/README.md new file mode 100644 index 000000000..1a7c5c420 --- /dev/null +++ b/vendor/github.com/tidwall/sjson/README.md @@ -0,0 +1,278 @@ +

+SJSON +
+Build Status +GoDoc +

+ +

set a json value quickly

+ +SJSON is a Go package that provides a [very fast](#performance) and simple way to set a value in a json document. The purpose for this library is to provide efficient json updating for the [SummitDB](https://github.com/tidwall/summitdb) project. +For quickly retrieving json values check out [GJSON](https://github.com/tidwall/gjson). + +For a command line interface check out [JSONed](https://github.com/tidwall/jsoned). + +Getting Started +=============== + +Installing +---------- + +To start using SJSON, install Go and run `go get`: + +```sh +$ go get -u github.com/tidwall/sjson +``` + +This will retrieve the library. + +Set a value +----------- +Set sets the value for the specified path. +A path is in dot syntax, such as "name.last" or "age". +This function expects that the json is well-formed and validated. +Invalid json will not panic, but it may return back unexpected results. +Invalid paths may return an error. + +```go +package main + +import "github.com/tidwall/sjson" + +const json = `{"name":{"first":"Janet","last":"Prichard"},"age":47}` + +func main() { + value, _ := sjson.Set(json, "name.last", "Anderson") + println(value) +} +``` + +This will print: + +```json +{"name":{"first":"Janet","last":"Anderson"},"age":47} +``` + +Path syntax +----------- + +A path is a series of keys separated by a dot. +The dot and colon characters can be escaped with '\'. + +```json +{ + "name": {"first": "Tom", "last": "Anderson"}, + "age":37, + "children": ["Sara","Alex","Jack"], + "fav.movie": "Deer Hunter", + "friends": [ + {"first": "James", "last": "Murphy"}, + {"first": "Roger", "last": "Craig"} + ] +} +``` +``` +"name.last" >> "Anderson" +"age" >> 37 +"children.1" >> "Alex" +"friends.1.last" >> "Craig" +``` + +The `-1` key can be used to append a value to an existing array: + +``` +"children.-1" >> appends a new value to the end of the children array +``` + +Normally number keys are used to modify arrays, but it's possible to force a numeric object key by using the colon character: + +```json +{ + "users":{ + "2313":{"name":"Sara"}, + "7839":{"name":"Andy"} + } +} +``` + +A colon path would look like: + +``` +"users.:2313.name" >> "Sara" +``` + +Supported types +--------------- + +Pretty much any type is supported: + +```go +sjson.Set(`{"key":true}`, "key", nil) +sjson.Set(`{"key":true}`, "key", false) +sjson.Set(`{"key":true}`, "key", 1) +sjson.Set(`{"key":true}`, "key", 10.5) +sjson.Set(`{"key":true}`, "key", "hello") +sjson.Set(`{"key":true}`, "key", map[string]interface{}{"hello":"world"}) +``` + +When a type is not recognized, SJSON will fallback to the `encoding/json` Marshaller. + + +Examples +-------- + +Set a value from empty document: +```go +value, _ := sjson.Set("", "name", "Tom") +println(value) + +// Output: +// {"name":"Tom"} +``` + +Set a nested value from empty document: +```go +value, _ := sjson.Set("", "name.last", "Anderson") +println(value) + +// Output: +// {"name":{"last":"Anderson"}} +``` + +Set a new value: +```go +value, _ := sjson.Set(`{"name":{"last":"Anderson"}}`, "name.first", "Sara") +println(value) + +// Output: +// {"name":{"first":"Sara","last":"Anderson"}} +``` + +Update an existing value: +```go +value, _ := sjson.Set(`{"name":{"last":"Anderson"}}`, "name.last", "Smith") +println(value) + +// Output: +// {"name":{"last":"Smith"}} +``` + +Set a new array value: +```go +value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.2", "Sara") +println(value) + +// Output: +// {"friends":["Andy","Carol","Sara"] +``` + +Append an array value by using the `-1` key in a path: +```go +value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.-1", "Sara") +println(value) + +// Output: +// {"friends":["Andy","Carol","Sara"] +``` + +Append an array value that is past the end: +```go +value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.4", "Sara") +println(value) + +// Output: +// {"friends":["Andy","Carol",null,null,"Sara"] +``` + +Delete a value: +```go +value, _ := sjson.Delete(`{"name":{"first":"Sara","last":"Anderson"}}`, "name.first") +println(value) + +// Output: +// {"name":{"last":"Anderson"}} +``` + +Delete an array value: +```go +value, _ := sjson.Delete(`{"friends":["Andy","Carol"]}`, "friends.1") +println(value) + +// Output: +// {"friends":["Andy"]} +``` + +Delete the last array value: +```go +value, _ := sjson.Delete(`{"friends":["Andy","Carol"]}`, "friends.-1") +println(value) + +// Output: +// {"friends":["Andy"]} +``` + +## Performance + +Benchmarks of SJSON alongside [encoding/json](https://golang.org/pkg/encoding/json/), +[ffjson](https://github.com/pquerna/ffjson), +[EasyJSON](https://github.com/mailru/easyjson), +and [Gabs](https://github.com/Jeffail/gabs) + +``` +Benchmark_SJSON-8 3000000 805 ns/op 1077 B/op 3 allocs/op +Benchmark_SJSON_ReplaceInPlace-8 3000000 449 ns/op 0 B/op 0 allocs/op +Benchmark_JSON_Map-8 300000 21236 ns/op 6392 B/op 150 allocs/op +Benchmark_JSON_Struct-8 300000 14691 ns/op 1789 B/op 24 allocs/op +Benchmark_Gabs-8 300000 21311 ns/op 6752 B/op 150 allocs/op +Benchmark_FFJSON-8 300000 17673 ns/op 3589 B/op 47 allocs/op +Benchmark_EasyJSON-8 1500000 3119 ns/op 1061 B/op 13 allocs/op +``` + +JSON document used: + +```json +{ + "widget": { + "debug": "on", + "window": { + "title": "Sample Konfabulator Widget", + "name": "main_window", + "width": 500, + "height": 500 + }, + "image": { + "src": "Images/Sun.png", + "hOffset": 250, + "vOffset": 250, + "alignment": "center" + }, + "text": { + "data": "Click Here", + "size": 36, + "style": "bold", + "vOffset": 100, + "alignment": "center", + "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;" + } + } +} +``` + +Each operation was rotated though one of the following search paths: + +``` +widget.window.name +widget.image.hOffset +widget.text.onMouseUp +``` + +*These benchmarks were run on a MacBook Pro 15" 2.8 GHz Intel Core i7 using Go 1.7.* + +## Contact +Josh Baker [@tidwall](http://twitter.com/tidwall) + +## License + +SJSON source code is available under the MIT [License](/LICENSE). diff --git a/vendor/github.com/tidwall/sjson/logo.png b/vendor/github.com/tidwall/sjson/logo.png new file mode 100644 index 000000000..b5aa257b6 Binary files /dev/null and b/vendor/github.com/tidwall/sjson/logo.png differ diff --git a/vendor/github.com/tidwall/sjson/sjson.go b/vendor/github.com/tidwall/sjson/sjson.go new file mode 100644 index 000000000..7f1d3588c --- /dev/null +++ b/vendor/github.com/tidwall/sjson/sjson.go @@ -0,0 +1,653 @@ +// Package sjson provides setting json values. +package sjson + +import ( + jsongo "encoding/json" + "reflect" + "strconv" + "unsafe" + + "github.com/tidwall/gjson" +) + +type errorType struct { + msg string +} + +func (err *errorType) Error() string { + return err.msg +} + +// Options represents additional options for the Set and Delete functions. +type Options struct { + // Optimistic is a hint that the value likely exists which + // allows for the sjson to perform a fast-track search and replace. + Optimistic bool + // ReplaceInPlace is a hint to replace the input json rather than + // allocate a new json byte slice. When this field is specified + // the input json will not longer be valid and it should not be used + // In the case when the destination slice doesn't have enough free + // bytes to replace the data in place, a new bytes slice will be + // created under the hood. + // The Optimistic flag must be set to true and the input must be a + // byte slice in order to use this field. + ReplaceInPlace bool +} + +type pathResult struct { + part string // current key part + path string // remaining path + force bool // force a string key + more bool // there is more path to parse +} + +func parsePath(path string) (pathResult, error) { + var r pathResult + if len(path) > 0 && path[0] == ':' { + r.force = true + path = path[1:] + } + for i := 0; i < len(path); i++ { + if path[i] == '.' { + r.part = path[:i] + r.path = path[i+1:] + r.more = true + return r, nil + } + if path[i] == '*' || path[i] == '?' { + return r, &errorType{"wildcard characters not allowed in path"} + } else if path[i] == '#' { + return r, &errorType{"array access character not allowed in path"} + } + if path[i] == '\\' { + // go into escape mode. this is a slower path that + // strips off the escape character from the part. + epart := []byte(path[:i]) + i++ + if i < len(path) { + epart = append(epart, path[i]) + i++ + for ; i < len(path); i++ { + if path[i] == '\\' { + i++ + if i < len(path) { + epart = append(epart, path[i]) + } + continue + } else if path[i] == '.' { + r.part = string(epart) + r.path = path[i+1:] + r.more = true + return r, nil + } else if path[i] == '*' || path[i] == '?' { + return r, &errorType{ + "wildcard characters not allowed in path"} + } else if path[i] == '#' { + return r, &errorType{ + "array access character not allowed in path"} + } + epart = append(epart, path[i]) + } + } + // append the last part + r.part = string(epart) + return r, nil + } + } + r.part = path + return r, nil +} + +func mustMarshalString(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] < ' ' || s[i] > 0x7f || s[i] == '"' { + return true + } + } + return false +} + +// appendStringify makes a json string and appends to buf. +func appendStringify(buf []byte, s string) []byte { + if mustMarshalString(s) { + b, _ := jsongo.Marshal(s) + return append(buf, b...) + } + buf = append(buf, '"') + buf = append(buf, s...) + buf = append(buf, '"') + return buf +} + +// appendBuild builds a json block from a json path. +func appendBuild(buf []byte, array bool, paths []pathResult, raw string, + stringify bool) []byte { + if !array { + buf = appendStringify(buf, paths[0].part) + buf = append(buf, ':') + } + if len(paths) > 1 { + n, numeric := atoui(paths[1]) + if numeric || (!paths[1].force && paths[1].part == "-1") { + buf = append(buf, '[') + buf = appendRepeat(buf, "null,", n) + buf = appendBuild(buf, true, paths[1:], raw, stringify) + buf = append(buf, ']') + } else { + buf = append(buf, '{') + buf = appendBuild(buf, false, paths[1:], raw, stringify) + buf = append(buf, '}') + } + } else { + if stringify { + buf = appendStringify(buf, raw) + } else { + buf = append(buf, raw...) + } + } + return buf +} + +// atoui does a rip conversion of string -> unigned int. +func atoui(r pathResult) (n int, ok bool) { + if r.force { + return 0, false + } + for i := 0; i < len(r.part); i++ { + if r.part[i] < '0' || r.part[i] > '9' { + return 0, false + } + n = n*10 + int(r.part[i]-'0') + } + return n, true +} + +// appendRepeat repeats string "n" times and appends to buf. +func appendRepeat(buf []byte, s string, n int) []byte { + for i := 0; i < n; i++ { + buf = append(buf, s...) + } + return buf +} + +// trim does a rip trim +func trim(s string) string { + for len(s) > 0 { + if s[0] <= ' ' { + s = s[1:] + continue + } + break + } + for len(s) > 0 { + if s[len(s)-1] <= ' ' { + s = s[:len(s)-1] + continue + } + break + } + return s +} + +// deleteTailItem deletes the previous key or comma. +func deleteTailItem(buf []byte) ([]byte, bool) { +loop: + for i := len(buf) - 1; i >= 0; i-- { + // look for either a ',',':','[' + switch buf[i] { + case '[': + return buf, true + case ',': + return buf[:i], false + case ':': + // delete tail string + i-- + for ; i >= 0; i-- { + if buf[i] == '"' { + i-- + for ; i >= 0; i-- { + if buf[i] == '"' { + i-- + if i >= 0 && i == '\\' { + i-- + continue + } + for ; i >= 0; i-- { + // look for either a ',','{' + switch buf[i] { + case '{': + return buf[:i+1], true + case ',': + return buf[:i], false + } + } + } + } + break + } + } + break loop + } + } + return buf, false +} + +var errNoChange = &errorType{"no change"} + +func appendRawPaths(buf []byte, jstr string, paths []pathResult, raw string, + stringify, del bool) ([]byte, error) { + var err error + var res gjson.Result + var found bool + if del { + if paths[0].part == "-1" && !paths[0].force { + res = gjson.Get(jstr, "#") + if res.Int() > 0 { + res = gjson.Get(jstr, strconv.FormatInt(int64(res.Int()-1), 10)) + found = true + } + } + } + if !found { + res = gjson.Get(jstr, paths[0].part) + } + if res.Index > 0 { + if len(paths) > 1 { + buf = append(buf, jstr[:res.Index]...) + buf, err = appendRawPaths(buf, res.Raw, paths[1:], raw, + stringify, del) + if err != nil { + return nil, err + } + buf = append(buf, jstr[res.Index+len(res.Raw):]...) + return buf, nil + } + buf = append(buf, jstr[:res.Index]...) + var exidx int // additional forward stripping + if del { + var delNextComma bool + buf, delNextComma = deleteTailItem(buf) + if delNextComma { + i, j := res.Index+len(res.Raw), 0 + for ; i < len(jstr); i, j = i+1, j+1 { + if jstr[i] <= ' ' { + continue + } + if jstr[i] == ',' { + exidx = j + 1 + } + break + } + } + } else { + if stringify { + buf = appendStringify(buf, raw) + } else { + buf = append(buf, raw...) + } + } + buf = append(buf, jstr[res.Index+len(res.Raw)+exidx:]...) + return buf, nil + } + if del { + return nil, errNoChange + } + n, numeric := atoui(paths[0]) + isempty := true + for i := 0; i < len(jstr); i++ { + if jstr[i] > ' ' { + isempty = false + break + } + } + if isempty { + if numeric { + jstr = "[]" + } else { + jstr = "{}" + } + } + jsres := gjson.Parse(jstr) + if jsres.Type != gjson.JSON { + if numeric { + jstr = "[]" + } else { + jstr = "{}" + } + jsres = gjson.Parse(jstr) + } + var comma bool + for i := 1; i < len(jsres.Raw); i++ { + if jsres.Raw[i] <= ' ' { + continue + } + if jsres.Raw[i] == '}' || jsres.Raw[i] == ']' { + break + } + comma = true + break + } + switch jsres.Raw[0] { + default: + return nil, &errorType{"json must be an object or array"} + case '{': + buf = append(buf, '{') + buf = appendBuild(buf, false, paths, raw, stringify) + if comma { + buf = append(buf, ',') + } + buf = append(buf, jsres.Raw[1:]...) + return buf, nil + case '[': + var appendit bool + if !numeric { + if paths[0].part == "-1" && !paths[0].force { + appendit = true + } else { + return nil, &errorType{ + "cannot set array element for non-numeric key '" + + paths[0].part + "'"} + } + } + if appendit { + njson := trim(jsres.Raw) + if njson[len(njson)-1] == ']' { + njson = njson[:len(njson)-1] + } + buf = append(buf, njson...) + if comma { + buf = append(buf, ',') + } + + buf = appendBuild(buf, true, paths, raw, stringify) + buf = append(buf, ']') + return buf, nil + } + buf = append(buf, '[') + ress := jsres.Array() + for i := 0; i < len(ress); i++ { + if i > 0 { + buf = append(buf, ',') + } + buf = append(buf, ress[i].Raw...) + } + if len(ress) == 0 { + buf = appendRepeat(buf, "null,", n-len(ress)) + } else { + buf = appendRepeat(buf, ",null", n-len(ress)) + if comma { + buf = append(buf, ',') + } + } + buf = appendBuild(buf, true, paths, raw, stringify) + buf = append(buf, ']') + return buf, nil + } +} + +func isOptimisticPath(path string) bool { + for i := 0; i < len(path); i++ { + if path[i] < '.' || path[i] > 'z' { + return false + } + if path[i] > '9' && path[i] < 'A' { + return false + } + if path[i] > 'z' { + return false + } + } + return true +} + +func set(jstr, path, raw string, + stringify, del, optimistic, inplace bool) ([]byte, error) { + if path == "" { + return nil, &errorType{"path cannot be empty"} + } + if !del && optimistic && isOptimisticPath(path) { + res := gjson.Get(jstr, path) + if res.Exists() && res.Index > 0 { + sz := len(jstr) - len(res.Raw) + len(raw) + if stringify { + sz += 2 + } + if inplace && sz <= len(jstr) { + if !stringify || !mustMarshalString(raw) { + jsonh := *(*reflect.StringHeader)(unsafe.Pointer(&jstr)) + jsonbh := reflect.SliceHeader{ + Data: jsonh.Data, Len: jsonh.Len, Cap: jsonh.Len} + jbytes := *(*[]byte)(unsafe.Pointer(&jsonbh)) + if stringify { + jbytes[res.Index] = '"' + copy(jbytes[res.Index+1:], []byte(raw)) + jbytes[res.Index+1+len(raw)] = '"' + copy(jbytes[res.Index+1+len(raw)+1:], + jbytes[res.Index+len(res.Raw):]) + } else { + copy(jbytes[res.Index:], []byte(raw)) + copy(jbytes[res.Index+len(raw):], + jbytes[res.Index+len(res.Raw):]) + } + return jbytes[:sz], nil + } + return nil, nil + } + buf := make([]byte, 0, sz) + buf = append(buf, jstr[:res.Index]...) + if stringify { + buf = appendStringify(buf, raw) + } else { + buf = append(buf, raw...) + } + buf = append(buf, jstr[res.Index+len(res.Raw):]...) + return buf, nil + } + } + // parse the path, make sure that it does not contain invalid characters + // such as '#', '?', '*' + paths := make([]pathResult, 0, 4) + r, err := parsePath(path) + if err != nil { + return nil, err + } + paths = append(paths, r) + for r.more { + if r, err = parsePath(r.path); err != nil { + return nil, err + } + paths = append(paths, r) + } + + njson, err := appendRawPaths(nil, jstr, paths, raw, stringify, del) + if err != nil { + return nil, err + } + return njson, nil +} + +// Set sets a json value for the specified path. +// A path is in dot syntax, such as "name.last" or "age". +// This function expects that the json is well-formed, and does not validate. +// Invalid json will not panic, but it may return back unexpected results. +// An error is returned if the path is not valid. +// +// A path is a series of keys separated by a dot. +// +// { +// "name": {"first": "Tom", "last": "Anderson"}, +// "age":37, +// "children": ["Sara","Alex","Jack"], +// "friends": [ +// {"first": "James", "last": "Murphy"}, +// {"first": "Roger", "last": "Craig"} +// ] +// } +// "name.last" >> "Anderson" +// "age" >> 37 +// "children.1" >> "Alex" +// +func Set(json, path string, value interface{}) (string, error) { + return SetOptions(json, path, value, nil) +} + +// SetOptions sets a json value for the specified path with options. +// A path is in dot syntax, such as "name.last" or "age". +// This function expects that the json is well-formed, and does not validate. +// Invalid json will not panic, but it may return back unexpected results. +// An error is returned if the path is not valid. +func SetOptions(json, path string, value interface{}, + opts *Options) (string, error) { + if opts != nil { + if opts.ReplaceInPlace { + // it's not safe to replace bytes in-place for strings + // copy the Options and set options.ReplaceInPlace to false. + nopts := *opts + opts = &nopts + opts.ReplaceInPlace = false + } + } + jsonh := *(*reflect.StringHeader)(unsafe.Pointer(&json)) + jsonbh := reflect.SliceHeader{Data: jsonh.Data, Len: jsonh.Len} + jsonb := *(*[]byte)(unsafe.Pointer(&jsonbh)) + res, err := SetBytesOptions(jsonb, path, value, opts) + return string(res), err +} + +// SetBytes sets a json value for the specified path. +// If working with bytes, this method preferred over +// Set(string(data), path, value) +func SetBytes(json []byte, path string, value interface{}) ([]byte, error) { + return SetBytesOptions(json, path, value, nil) +} + +// SetBytesOptions sets a json value for the specified path with options. +// If working with bytes, this method preferred over +// SetOptions(string(data), path, value) +func SetBytesOptions(json []byte, path string, value interface{}, + opts *Options) ([]byte, error) { + var optimistic, inplace bool + if opts != nil { + optimistic = opts.Optimistic + inplace = opts.ReplaceInPlace + } + jstr := *(*string)(unsafe.Pointer(&json)) + var res []byte + var err error + switch v := value.(type) { + default: + b, err := jsongo.Marshal(value) + if err != nil { + return nil, err + } + raw := *(*string)(unsafe.Pointer(&b)) + res, err = set(jstr, path, raw, false, false, optimistic, inplace) + case dtype: + res, err = set(jstr, path, "", false, true, optimistic, inplace) + case string: + res, err = set(jstr, path, v, true, false, optimistic, inplace) + case []byte: + raw := *(*string)(unsafe.Pointer(&v)) + res, err = set(jstr, path, raw, true, false, optimistic, inplace) + case bool: + if v { + res, err = set(jstr, path, "true", false, false, optimistic, inplace) + } else { + res, err = set(jstr, path, "false", false, false, optimistic, inplace) + } + case int8: + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) + case int16: + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) + case int32: + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) + case int64: + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) + case uint8: + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) + case uint16: + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) + case uint32: + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) + case uint64: + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) + case float32: + res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64), + false, false, optimistic, inplace) + case float64: + res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64), + false, false, optimistic, inplace) + } + if err == errNoChange { + return json, nil + } + return res, err +} + +// SetRaw sets a raw json value for the specified path. +// This function works the same as Set except that the value is set as a +// raw block of json. This allows for setting premarshalled json objects. +func SetRaw(json, path, value string) (string, error) { + return SetRawOptions(json, path, value, nil) +} + +// SetRawOptions sets a raw json value for the specified path with options. +// This furnction works the same as SetOptions except that the value is set +// as a raw block of json. This allows for setting premarshalled json objects. +func SetRawOptions(json, path, value string, opts *Options) (string, error) { + var optimistic bool + if opts != nil { + optimistic = opts.Optimistic + } + res, err := set(json, path, value, false, false, optimistic, false) + if err == errNoChange { + return json, nil + } + return string(res), err +} + +// SetRawBytes sets a raw json value for the specified path. +// If working with bytes, this method preferred over +// SetRaw(string(data), path, value) +func SetRawBytes(json []byte, path string, value []byte) ([]byte, error) { + return SetRawBytesOptions(json, path, value, nil) +} + +// SetRawBytesOptions sets a raw json value for the specified path with options. +// If working with bytes, this method preferred over +// SetRawOptions(string(data), path, value, opts) +func SetRawBytesOptions(json []byte, path string, value []byte, + opts *Options) ([]byte, error) { + jstr := *(*string)(unsafe.Pointer(&json)) + vstr := *(*string)(unsafe.Pointer(&value)) + var optimistic, inplace bool + if opts != nil { + optimistic = opts.Optimistic + inplace = opts.ReplaceInPlace + } + res, err := set(jstr, path, vstr, false, false, optimistic, inplace) + if err == errNoChange { + return json, nil + } + return res, err +} + +type dtype struct{} + +// Delete deletes a value from json for the specified path. +func Delete(json, path string) (string, error) { + return Set(json, path, dtype{}) +} + +// DeleteBytes deletes a value from json for the specified path. +func DeleteBytes(json []byte, path string) ([]byte, error) { + return SetBytes(json, path, dtype{}) +} diff --git a/vendor/vendor.json b/vendor/vendor.json index ff84819ad..747f18bf5 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -888,6 +888,12 @@ "revision": "173748da739a410c5b0b813b956f89ff94730b4c", "revisionTime": "2016-08-30T17:39:30Z" }, + { + "checksumSHA1": "j1wNJXkZyuFKjYFpPawESOaXYxk=", + "path": "github.com/tidwall/sjson", + "revision": "6a22caf2fd45d5e2119bfc3717e984f15a7eb7ee", + "revisionTime": "2016-12-12T16:53:56Z" + }, { "checksumSHA1": "MWqyOvDMkW+XYe2RJ5mplvut+aE=", "path": "github.com/ugorji/go/codec",