diff --git a/cmd/admin-handlers-config-kv.go b/cmd/admin-handlers-config-kv.go index 3fa4eaf5a..2ec085ac1 100644 --- a/cmd/admin-handlers-config-kv.go +++ b/cmd/admin-handlers-config-kv.go @@ -33,7 +33,7 @@ import ( func validateAdminReqConfigKV(ctx context.Context, w http.ResponseWriter, r *http.Request) ObjectLayer { // Get current object layer instance. - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) return nil @@ -136,8 +136,15 @@ func (a adminAPIHandlers) SetConfigKVHandler(w http.ResponseWriter, r *http.Requ } cfg, err := readServerConfig(ctx, objectAPI) if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return + // Config not found for some reason, allow things to continue + // by initializing a new fresh config in safe mode. + if err == errConfigNotFound && globalSafeMode { + cfg = newServerConfig() + err = nil + } else { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } } defaultKVS := configDefaultKVS() @@ -174,6 +181,9 @@ func (a adminAPIHandlers) SetConfigKVHandler(w http.ResponseWriter, r *http.Requ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } + + // Make sure to write backend is encrypted + saveConfig(context.Background(), objectAPI, backendEncryptedFile, backendEncryptedMigrationComplete) } // GetConfigKVHandler - GET /minio/admin/v2/get-config-kv?key={key} @@ -280,8 +290,15 @@ func (a adminAPIHandlers) RestoreConfigHistoryKVHandler(w http.ResponseWriter, r cfg, err := readServerConfig(ctx, objectAPI) if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return + // Config not found for some reason, allow things to continue + // by initializing a new fresh config in safe mode. + if err == errConfigNotFound && globalSafeMode { + cfg = newServerConfig() + err = nil + } else { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } } defaultKVS := configDefaultKVS() @@ -425,6 +442,9 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques return } + // Make sure to write backend is encrypted + saveConfig(context.Background(), objectAPI, backendEncryptedFile, backendEncryptedMigrationComplete) + // Reply to the client before restarting minio server. writeSuccessResponseHeadersOnly(w) } diff --git a/cmd/admin-handlers-users.go b/cmd/admin-handlers-users.go new file mode 100644 index 000000000..4f2c5d3fc --- /dev/null +++ b/cmd/admin-handlers-users.go @@ -0,0 +1,529 @@ +/* + * MinIO Cloud Storage, (C) 2019 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 cmd + +import ( + "context" + "encoding/json" + "io" + "io/ioutil" + "net/http" + + "github.com/gorilla/mux" + "github.com/minio/minio/cmd/logger" + iampolicy "github.com/minio/minio/pkg/iam/policy" + "github.com/minio/minio/pkg/madmin" +) + +func validateAdminUsersReq(ctx context.Context, w http.ResponseWriter, r *http.Request) ObjectLayer { + // Get current object layer instance. + objectAPI := newObjectLayerWithoutSafeModeFn() + if objectAPI == nil || globalNotificationSys == nil || globalIAMSys == nil { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) + return nil + } + + // Validate request signature. + adminAPIErr := checkAdminRequestAuthType(ctx, r, "") + if adminAPIErr != ErrNone { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL) + return nil + } + + return objectAPI +} + +// RemoveUser - DELETE /minio/admin/v2/remove-user?accessKey= +func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "RemoveUser") + + objectAPI := validateAdminUsersReq(ctx, w, r) + if objectAPI == nil { + return + } + + // Deny if WORM is enabled + if globalWORMEnabled { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL) + return + } + + vars := mux.Vars(r) + accessKey := vars["accessKey"] + + if err := globalIAMSys.DeleteUser(accessKey); err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + // Notify all other MinIO peers to delete user. + for _, nerr := range globalNotificationSys.DeleteUser(accessKey) { + if nerr.Err != nil { + logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String()) + logger.LogIf(ctx, nerr.Err) + } + } +} + +// ListUsers - GET /minio/admin/v2/list-users +func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "ListUsers") + + objectAPI := validateAdminUsersReq(ctx, w, r) + if objectAPI == nil { + return + } + + allCredentials, err := globalIAMSys.ListUsers() + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + data, err := json.Marshal(allCredentials) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + password := globalActiveCred.SecretKey + econfigData, err := madmin.EncryptData(password, data) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + writeSuccessResponseJSON(w, econfigData) +} + +// GetUserInfo - GET /minio/admin/v2/user-info +func (a adminAPIHandlers) GetUserInfo(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "GetUserInfo") + + objectAPI := validateAdminUsersReq(ctx, w, r) + if objectAPI == nil { + return + } + + vars := mux.Vars(r) + name := vars["accessKey"] + + userInfo, err := globalIAMSys.GetUserInfo(name) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + data, err := json.Marshal(userInfo) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + writeSuccessResponseJSON(w, data) +} + +// UpdateGroupMembers - PUT /minio/admin/v2/update-group-members +func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "UpdateGroupMembers") + + objectAPI := validateAdminUsersReq(ctx, w, r) + if objectAPI == nil { + return + } + + defer r.Body.Close() + data, err := ioutil.ReadAll(r.Body) + if err != nil { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) + return + } + + var updReq madmin.GroupAddRemove + err = json.Unmarshal(data, &updReq) + if err != nil { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) + return + } + + if updReq.IsRemove { + err = globalIAMSys.RemoveUsersFromGroup(updReq.Group, updReq.Members) + } else { + err = globalIAMSys.AddUsersToGroup(updReq.Group, updReq.Members) + } + + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + // Notify all other MinIO peers to load group. + for _, nerr := range globalNotificationSys.LoadGroup(updReq.Group) { + if nerr.Err != nil { + logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String()) + logger.LogIf(ctx, nerr.Err) + } + } +} + +// GetGroup - /minio/admin/v2/group?group=mygroup1 +func (a adminAPIHandlers) GetGroup(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "GetGroup") + + objectAPI := validateAdminUsersReq(ctx, w, r) + if objectAPI == nil { + return + } + + vars := mux.Vars(r) + group := vars["group"] + + gdesc, err := globalIAMSys.GetGroupDescription(group) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + body, err := json.Marshal(gdesc) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + writeSuccessResponseJSON(w, body) +} + +// ListGroups - GET /minio/admin/v2/groups +func (a adminAPIHandlers) ListGroups(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "ListGroups") + + objectAPI := validateAdminUsersReq(ctx, w, r) + if objectAPI == nil { + return + } + + groups, err := globalIAMSys.ListGroups() + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + body, err := json.Marshal(groups) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + writeSuccessResponseJSON(w, body) +} + +// SetGroupStatus - PUT /minio/admin/v2/set-group-status?group=mygroup1&status=enabled +func (a adminAPIHandlers) SetGroupStatus(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "SetGroupStatus") + + objectAPI := validateAdminUsersReq(ctx, w, r) + if objectAPI == nil { + return + } + + vars := mux.Vars(r) + group := vars["group"] + status := vars["status"] + + var err error + if status == statusEnabled { + err = globalIAMSys.SetGroupStatus(group, true) + } else if status == statusDisabled { + err = globalIAMSys.SetGroupStatus(group, false) + } else { + err = errInvalidArgument + } + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + // Notify all other MinIO peers to reload user. + for _, nerr := range globalNotificationSys.LoadGroup(group) { + if nerr.Err != nil { + logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String()) + logger.LogIf(ctx, nerr.Err) + } + } +} + +// SetUserStatus - PUT /minio/admin/v2/set-user-status?accessKey=&status=[enabled|disabled] +func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "SetUserStatus") + + objectAPI := validateAdminUsersReq(ctx, w, r) + if objectAPI == nil { + return + } + + // Deny if WORM is enabled + if globalWORMEnabled { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL) + return + } + + vars := mux.Vars(r) + accessKey := vars["accessKey"] + status := vars["status"] + + // Custom IAM policies not allowed for admin user. + if accessKey == globalActiveCred.AccessKey { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) + return + } + + if err := globalIAMSys.SetUserStatus(accessKey, madmin.AccountStatus(status)); err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + // Notify all other MinIO peers to reload user. + for _, nerr := range globalNotificationSys.LoadUser(accessKey, false) { + if nerr.Err != nil { + logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String()) + logger.LogIf(ctx, nerr.Err) + } + } +} + +// AddUser - PUT /minio/admin/v2/add-user?accessKey= +func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "AddUser") + + objectAPI := validateAdminUsersReq(ctx, w, r) + if objectAPI == nil { + return + } + + // Deny if WORM is enabled + if globalWORMEnabled { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL) + return + } + + vars := mux.Vars(r) + accessKey := vars["accessKey"] + + // Custom IAM policies not allowed for admin user. + if accessKey == globalActiveCred.AccessKey { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL) + return + } + + if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 { + // More than maxConfigSize bytes were available + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL) + return + } + + password := globalActiveCred.SecretKey + configBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength)) + if err != nil { + logger.LogIf(ctx, err) + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL) + return + } + + var uinfo madmin.UserInfo + if err = json.Unmarshal(configBytes, &uinfo); err != nil { + logger.LogIf(ctx, err) + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL) + return + } + + if err = globalIAMSys.SetUser(accessKey, uinfo); err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + // Notify all other Minio peers to reload user + for _, nerr := range globalNotificationSys.LoadUser(accessKey, false) { + if nerr.Err != nil { + logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String()) + logger.LogIf(ctx, nerr.Err) + } + } +} + +// InfoCannedPolicy - GET /minio/admin/v2/info-canned-policy?name={policyName} +func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "InfoCannedPolicy") + + objectAPI := validateAdminUsersReq(ctx, w, r) + if objectAPI == nil { + return + } + + data, err := globalIAMSys.InfoPolicy(mux.Vars(r)["name"]) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + w.Write(data) + w.(http.Flusher).Flush() +} + +// ListCannedPolicies - GET /minio/admin/v2/list-canned-policies +func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "ListCannedPolicies") + + objectAPI := validateAdminUsersReq(ctx, w, r) + if objectAPI == nil { + return + } + + policies, err := globalIAMSys.ListPolicies() + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + if err = json.NewEncoder(w).Encode(policies); err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + w.(http.Flusher).Flush() +} + +// RemoveCannedPolicy - DELETE /minio/admin/v2/remove-canned-policy?name= +func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "RemoveCannedPolicy") + + objectAPI := validateAdminUsersReq(ctx, w, r) + if objectAPI == nil { + return + } + + vars := mux.Vars(r) + policyName := vars["name"] + + // Deny if WORM is enabled + if globalWORMEnabled { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL) + return + } + + if err := globalIAMSys.DeletePolicy(policyName); err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + // Notify all other MinIO peers to delete policy + for _, nerr := range globalNotificationSys.DeletePolicy(policyName) { + if nerr.Err != nil { + logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String()) + logger.LogIf(ctx, nerr.Err) + } + } +} + +// AddCannedPolicy - PUT /minio/admin/v2/add-canned-policy?name= +func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "AddCannedPolicy") + + objectAPI := validateAdminUsersReq(ctx, w, r) + if objectAPI == nil { + return + } + + vars := mux.Vars(r) + policyName := vars["name"] + + // Deny if WORM is enabled + if globalWORMEnabled { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL) + return + } + + // Error out if Content-Length is missing. + if r.ContentLength <= 0 { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL) + return + } + + // Error out if Content-Length is beyond allowed size. + if r.ContentLength > maxBucketPolicySize { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrEntityTooLarge), r.URL) + return + } + + iamPolicy, err := iampolicy.ParseConfig(io.LimitReader(r.Body, r.ContentLength)) + if err != nil { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMalformedPolicy), r.URL) + return + } + + // Version in policy must not be empty + if iamPolicy.Version == "" { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMalformedPolicy), r.URL) + return + } + + if err = globalIAMSys.SetPolicy(policyName, *iamPolicy); err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + // Notify all other MinIO peers to reload policy + for _, nerr := range globalNotificationSys.LoadPolicy(policyName) { + if nerr.Err != nil { + logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String()) + logger.LogIf(ctx, nerr.Err) + } + } +} + +// SetPolicyForUserOrGroup - PUT /minio/admin/v2/set-policy?policy=xxx&user-or-group=?[&is-group] +func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "SetPolicyForUserOrGroup") + + objectAPI := validateAdminUsersReq(ctx, w, r) + if objectAPI == nil { + return + } + + vars := mux.Vars(r) + policyName := vars["policyName"] + entityName := vars["userOrGroup"] + isGroup := vars["isGroup"] == "true" + + // Deny if WORM is enabled + if globalWORMEnabled { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL) + return + } + + if err := globalIAMSys.PolicyDBSet(entityName, policyName, isGroup); err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + // Notify all other MinIO peers to reload policy + for _, nerr := range globalNotificationSys.LoadPolicyMapping(entityName, isGroup) { + if nerr.Err != nil { + logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String()) + logger.LogIf(ctx, nerr.Err) + } + } +} diff --git a/cmd/admin-handlers.go b/cmd/admin-handlers.go index 228222743..ee7303d24 100644 --- a/cmd/admin-handlers.go +++ b/cmd/admin-handlers.go @@ -22,7 +22,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "net/http" "net/url" "os" @@ -42,7 +41,6 @@ import ( "github.com/minio/minio/cmd/logger" "github.com/minio/minio/pkg/cpu" "github.com/minio/minio/pkg/handlers" - iampolicy "github.com/minio/minio/pkg/iam/policy" "github.com/minio/minio/pkg/madmin" "github.com/minio/minio/pkg/mem" xnet "github.com/minio/minio/pkg/net" @@ -94,7 +92,7 @@ func updateServer(updateURL, sha256Hex string, latestReleaseTime time.Time) (us return us, nil } -// ServerUpdateHandler - POST /minio/admin/v1/update?updateURL={updateURL} +// ServerUpdateHandler - POST /minio/admin/v2/update?updateURL={updateURL} // ---------- // updates all minio servers and restarts them gracefully. func (a adminAPIHandlers) ServerUpdateHandler(w http.ResponseWriter, r *http.Request) { @@ -173,7 +171,7 @@ func (a adminAPIHandlers) ServerUpdateHandler(w http.ResponseWriter, r *http.Req } } -// ServiceActionHandler - POST /minio/admin/v1/service?action={action} +// ServiceActionHandler - POST /minio/admin/v2/service?action={action} // ---------- // restarts/stops minio server gracefully. In a distributed setup, func (a adminAPIHandlers) ServiceActionHandler(w http.ResponseWriter, r *http.Request) { @@ -262,7 +260,7 @@ type ServerInfo struct { Data *ServerInfoData `json:"data"` } -// ServerInfoHandler - GET /minio/admin/v1/info +// ServerInfoHandler - GET /minio/admin/v2/info // ---------- // Get server information func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Request) { @@ -303,7 +301,7 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque writeSuccessResponseJSON(w, jsonBytes) } -// ServerInfoHandler - GET /minio/admin/v1/storageinfo +// ServerInfoHandler - GET /minio/admin/v2/storageinfo // ---------- // Get server information func (a adminAPIHandlers) StorageInfoHandler(w http.ResponseWriter, r *http.Request) { @@ -355,7 +353,7 @@ type ServerNetReadPerfInfo struct { Error string `json:"error,omitempty"` } -// PerfInfoHandler - GET /minio/admin/v1/performance?perfType={perfType} +// PerfInfoHandler - GET /minio/admin/v2/performance?perfType={perfType} // ---------- // Get all performance information based on input type // Supported types = drive @@ -561,7 +559,7 @@ type StartProfilingResult struct { Error string `json:"error"` } -// StartProfilingHandler - POST /minio/admin/v1/profiling/start?profilerType={profilerType} +// StartProfilingHandler - POST /minio/admin/v2/profiling/start?profilerType={profilerType} // ---------- // Enable server profiling func (a adminAPIHandlers) StartProfilingHandler(w http.ResponseWriter, r *http.Request) { @@ -643,7 +641,7 @@ func (f dummyFileInfo) ModTime() time.Time { return f.modTime } func (f dummyFileInfo) IsDir() bool { return f.isDir } func (f dummyFileInfo) Sys() interface{} { return f.sys } -// DownloadProfilingHandler - POST /minio/admin/v1/profiling/download +// DownloadProfilingHandler - POST /minio/admin/v2/profiling/download // ---------- // Download profiling information of all nodes in a zip format func (a adminAPIHandlers) DownloadProfilingHandler(w http.ResponseWriter, r *http.Request) { @@ -725,7 +723,7 @@ func extractHealInitParams(vars map[string]string, qParms url.Values, r io.Reade return } -// HealHandler - POST /minio/admin/v1/heal/ +// HealHandler - POST /minio/admin/v2/heal/ // ----------- // Start heal processing and return heal status items. // @@ -927,8 +925,8 @@ func (a adminAPIHandlers) BackgroundHealStatusHandler(w http.ResponseWriter, r * func validateAdminReq(ctx context.Context, w http.ResponseWriter, r *http.Request) ObjectLayer { // Get current object layer instance. - objectAPI := globalObjectAPI - if objectAPI == nil || globalNotificationSys == nil || globalIAMSys == nil { + objectAPI := newObjectLayerWithoutSafeModeFn() + if objectAPI == nil || globalNotificationSys == nil { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) return nil } @@ -992,492 +990,19 @@ func toAdminAPIErr(ctx context.Context, err error) APIError { HTTPStatusCode: e.StatusCode, } default: - apiErr = errorCodes.ToAPIErr(toAdminAPIErrCode(ctx, err)) + if err == errConfigNotFound { + apiErr = APIError{ + Code: "XMinioConfigError", + Description: err.Error(), + HTTPStatusCode: http.StatusNotFound, + } + } else { + apiErr = errorCodes.ToAPIErr(toAdminAPIErrCode(ctx, err)) + } } return apiErr } -// RemoveUser - DELETE /minio/admin/v1/remove-user?accessKey= -func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "RemoveUser") - - objectAPI := validateAdminReq(ctx, w, r) - if objectAPI == nil { - return - } - - // Deny if WORM is enabled - if globalWORMEnabled { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL) - return - } - - vars := mux.Vars(r) - accessKey := vars["accessKey"] - - if err := globalIAMSys.DeleteUser(accessKey); err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - // Notify all other MinIO peers to delete user. - for _, nerr := range globalNotificationSys.DeleteUser(accessKey) { - if nerr.Err != nil { - logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String()) - logger.LogIf(ctx, nerr.Err) - } - } -} - -// ListUsers - GET /minio/admin/v1/list-users -func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "ListUsers") - - objectAPI := validateAdminReq(ctx, w, r) - if objectAPI == nil { - return - } - - allCredentials, err := globalIAMSys.ListUsers() - if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - data, err := json.Marshal(allCredentials) - if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - password := globalActiveCred.SecretKey - econfigData, err := madmin.EncryptData(password, data) - if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - writeSuccessResponseJSON(w, econfigData) -} - -// GetUserInfo - GET /minio/admin/v1/user-info -func (a adminAPIHandlers) GetUserInfo(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "GetUserInfo") - - objectAPI := validateAdminReq(ctx, w, r) - if objectAPI == nil { - return - } - - vars := mux.Vars(r) - name := vars["accessKey"] - - userInfo, err := globalIAMSys.GetUserInfo(name) - if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - data, err := json.Marshal(userInfo) - if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - writeSuccessResponseJSON(w, data) -} - -// UpdateGroupMembers - PUT /minio/admin/v1/update-group-members -func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "UpdateGroupMembers") - - objectAPI := validateAdminReq(ctx, w, r) - if objectAPI == nil { - return - } - - defer r.Body.Close() - data, err := ioutil.ReadAll(r.Body) - if err != nil { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) - return - } - - var updReq madmin.GroupAddRemove - err = json.Unmarshal(data, &updReq) - if err != nil { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) - return - } - - if updReq.IsRemove { - err = globalIAMSys.RemoveUsersFromGroup(updReq.Group, updReq.Members) - } else { - err = globalIAMSys.AddUsersToGroup(updReq.Group, updReq.Members) - } - - if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - // Notify all other MinIO peers to load group. - for _, nerr := range globalNotificationSys.LoadGroup(updReq.Group) { - if nerr.Err != nil { - logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String()) - logger.LogIf(ctx, nerr.Err) - } - } -} - -// GetGroup - /minio/admin/v1/group?group=mygroup1 -func (a adminAPIHandlers) GetGroup(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "GetGroup") - - objectAPI := validateAdminReq(ctx, w, r) - if objectAPI == nil { - return - } - - vars := mux.Vars(r) - group := vars["group"] - - gdesc, err := globalIAMSys.GetGroupDescription(group) - if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - body, err := json.Marshal(gdesc) - if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - writeSuccessResponseJSON(w, body) -} - -// ListGroups - GET /minio/admin/v1/groups -func (a adminAPIHandlers) ListGroups(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "ListGroups") - - objectAPI := validateAdminReq(ctx, w, r) - if objectAPI == nil { - return - } - - groups, err := globalIAMSys.ListGroups() - if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - body, err := json.Marshal(groups) - if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - writeSuccessResponseJSON(w, body) -} - -// SetGroupStatus - PUT /minio/admin/v1/set-group-status?group=mygroup1&status=enabled -func (a adminAPIHandlers) SetGroupStatus(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "SetGroupStatus") - - objectAPI := validateAdminReq(ctx, w, r) - if objectAPI == nil { - return - } - - vars := mux.Vars(r) - group := vars["group"] - status := vars["status"] - - var err error - if status == statusEnabled { - err = globalIAMSys.SetGroupStatus(group, true) - } else if status == statusDisabled { - err = globalIAMSys.SetGroupStatus(group, false) - } else { - err = errInvalidArgument - } - if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - // Notify all other MinIO peers to reload user. - for _, nerr := range globalNotificationSys.LoadGroup(group) { - if nerr.Err != nil { - logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String()) - logger.LogIf(ctx, nerr.Err) - } - } -} - -// SetUserStatus - PUT /minio/admin/v1/set-user-status?accessKey=&status=[enabled|disabled] -func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "SetUserStatus") - - objectAPI := validateAdminReq(ctx, w, r) - if objectAPI == nil { - return - } - - // Deny if WORM is enabled - if globalWORMEnabled { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL) - return - } - - vars := mux.Vars(r) - accessKey := vars["accessKey"] - status := vars["status"] - - // Custom IAM policies not allowed for admin user. - if accessKey == globalActiveCred.AccessKey { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) - return - } - - if err := globalIAMSys.SetUserStatus(accessKey, madmin.AccountStatus(status)); err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - // Notify all other MinIO peers to reload user. - for _, nerr := range globalNotificationSys.LoadUser(accessKey, false) { - if nerr.Err != nil { - logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String()) - logger.LogIf(ctx, nerr.Err) - } - } -} - -// AddUser - PUT /minio/admin/v1/add-user?accessKey= -func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "AddUser") - - objectAPI := validateAdminReq(ctx, w, r) - if objectAPI == nil { - return - } - - // Deny if WORM is enabled - if globalWORMEnabled { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL) - return - } - - vars := mux.Vars(r) - accessKey := vars["accessKey"] - - // Custom IAM policies not allowed for admin user. - if accessKey == globalActiveCred.AccessKey { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL) - return - } - - if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 { - // More than maxConfigSize bytes were available - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL) - return - } - - password := globalActiveCred.SecretKey - configBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength)) - if err != nil { - logger.LogIf(ctx, err) - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL) - return - } - - var uinfo madmin.UserInfo - if err = json.Unmarshal(configBytes, &uinfo); err != nil { - logger.LogIf(ctx, err) - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL) - return - } - - if err = globalIAMSys.SetUser(accessKey, uinfo); err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - // Notify all other Minio peers to reload user - for _, nerr := range globalNotificationSys.LoadUser(accessKey, false) { - if nerr.Err != nil { - logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String()) - logger.LogIf(ctx, nerr.Err) - } - } -} - -// InfoCannedPolicy - GET /minio/admin/v1/info-canned-policy?name={policyName} -func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "InfoCannedPolicy") - - objectAPI := validateAdminReq(ctx, w, r) - if objectAPI == nil { - return - } - - data, err := globalIAMSys.InfoPolicy(mux.Vars(r)["name"]) - if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - w.Write(data) - w.(http.Flusher).Flush() -} - -// ListCannedPolicies - GET /minio/admin/v1/list-canned-policies -func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "ListCannedPolicies") - - objectAPI := validateAdminReq(ctx, w, r) - if objectAPI == nil { - return - } - - policies, err := globalIAMSys.ListPolicies() - if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - if err = json.NewEncoder(w).Encode(policies); err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - w.(http.Flusher).Flush() -} - -// RemoveCannedPolicy - DELETE /minio/admin/v1/remove-canned-policy?name= -func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "RemoveCannedPolicy") - - objectAPI := validateAdminReq(ctx, w, r) - if objectAPI == nil { - return - } - - vars := mux.Vars(r) - policyName := vars["name"] - - // Deny if WORM is enabled - if globalWORMEnabled { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL) - return - } - - if err := globalIAMSys.DeletePolicy(policyName); err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - // Notify all other MinIO peers to delete policy - for _, nerr := range globalNotificationSys.DeletePolicy(policyName) { - if nerr.Err != nil { - logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String()) - logger.LogIf(ctx, nerr.Err) - } - } -} - -// AddCannedPolicy - PUT /minio/admin/v1/add-canned-policy?name= -func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "AddCannedPolicy") - - objectAPI := validateAdminReq(ctx, w, r) - if objectAPI == nil { - return - } - - vars := mux.Vars(r) - policyName := vars["name"] - - // Deny if WORM is enabled - if globalWORMEnabled { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL) - return - } - - // Error out if Content-Length is missing. - if r.ContentLength <= 0 { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL) - return - } - - // Error out if Content-Length is beyond allowed size. - if r.ContentLength > maxBucketPolicySize { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrEntityTooLarge), r.URL) - return - } - - iamPolicy, err := iampolicy.ParseConfig(io.LimitReader(r.Body, r.ContentLength)) - if err != nil { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMalformedPolicy), r.URL) - return - } - - // Version in policy must not be empty - if iamPolicy.Version == "" { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMalformedPolicy), r.URL) - return - } - - if err = globalIAMSys.SetPolicy(policyName, *iamPolicy); err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - // Notify all other MinIO peers to reload policy - for _, nerr := range globalNotificationSys.LoadPolicy(policyName) { - if nerr.Err != nil { - logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String()) - logger.LogIf(ctx, nerr.Err) - } - } -} - -// SetPolicyForUserOrGroup - PUT /minio/admin/v1/set-policy?policy=xxx&user-or-group=?[&is-group] -func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "SetPolicyForUserOrGroup") - - objectAPI := validateAdminReq(ctx, w, r) - if objectAPI == nil { - return - } - - vars := mux.Vars(r) - policyName := vars["policyName"] - entityName := vars["userOrGroup"] - isGroup := vars["isGroup"] == "true" - - // Deny if WORM is enabled - if globalWORMEnabled { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL) - return - } - - if err := globalIAMSys.PolicyDBSet(entityName, policyName, isGroup); err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - // Notify all other MinIO peers to reload policy - for _, nerr := range globalNotificationSys.LoadPolicyMapping(entityName, isGroup) { - if nerr.Err != nil { - logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String()) - logger.LogIf(ctx, nerr.Err) - } - } -} - // Returns true if the trace.Info should be traced, // false if certain conditions are not met. // - input entry is not of the type *trace.Info* @@ -1495,7 +1020,7 @@ func mustTrace(entry interface{}, trcAll, errOnly bool) bool { return trace } -// TraceHandler - POST /minio/admin/v1/trace +// TraceHandler - POST /minio/admin/v2/trace // ---------- // The handler sends http trace to the connected HTTP client. func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) { @@ -1519,13 +1044,16 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) { // Use buffered channel to take care of burst sends or slow w.Write() traceCh := make(chan interface{}, 4000) - peers := getRestClients(getRemoteHosts(globalEndpoints)) + peers := getRestClients(globalEndpoints) globalHTTPTrace.Subscribe(traceCh, doneCh, func(entry interface{}) bool { return mustTrace(entry, trcAll, trcErr) }) for _, peer := range peers { + if peer == nil { + continue + } peer.Trace(traceCh, doneCh, trcAll, trcErr) } @@ -1583,12 +1111,14 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque defer close(doneCh) logCh := make(chan interface{}, 4000) - remoteHosts := getRemoteHosts(globalEndpoints) - peers := getRestClients(remoteHosts) + peers := getRestClients(globalEndpoints) globalConsoleSys.Subscribe(logCh, doneCh, node, limitLines, logKind, nil) for _, peer := range peers { + if peer == nil { + continue + } if node == "" || strings.EqualFold(peer.host.Name, node) { peer.ConsoleLog(logCh, doneCh) } @@ -1620,7 +1150,7 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque } } -// KMSKeyStatusHandler - GET /minio/admin/v1/kms/key/status?key-id= +// KMSKeyStatusHandler - GET /minio/admin/v2/kms/key/status?key-id= func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "KMSKeyStatusHandler") @@ -1702,7 +1232,7 @@ func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Req writeSuccessResponseJSON(w, resp) } -// ServerHardwareInfoHandler - GET /minio/admin/v1/hardwareinfo?Type={hwType} +// ServerHardwareInfoHandler - GET /minio/admin/v2/hardwareinfo?Type={hwType} // ---------- // Get all hardware information based on input type // Supported types = cpu diff --git a/cmd/admin-handlers_test.go b/cmd/admin-handlers_test.go index b1d7f372a..9547eb7ea 100644 --- a/cmd/admin-handlers_test.go +++ b/cmd/admin-handlers_test.go @@ -90,11 +90,7 @@ func prepareAdminXLTestBed() (*adminXLTestBed, error) { globalPolicySys = NewPolicySys() globalPolicySys.Init(buckets, objLayer) - globalNotificationSys, err = NewNotificationSys(globalServerConfig, globalEndpoints) - if err != nil { - return nil, err - } - + globalNotificationSys = NewNotificationSys(globalEndpoints) globalNotificationSys.Init(buckets, objLayer) // Setup admin mgmt REST API handlers. diff --git a/cmd/admin-heal-ops.go b/cmd/admin-heal-ops.go index 3f5b15471..74d58be3d 100644 --- a/cmd/admin-heal-ops.go +++ b/cmd/admin-heal-ops.go @@ -660,7 +660,7 @@ func (h *healSequence) traverseAndHeal() { func (h *healSequence) healMinioSysMeta(metaPrefix string) func() error { return func() error { // Get current object layer instance. - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return errServerNotInitialized } @@ -692,7 +692,7 @@ func (h *healSequence) healDiskFormat() error { } // Get current object layer instance. - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return errServerNotInitialized } @@ -712,7 +712,7 @@ func (h *healSequence) healBuckets() error { } // Get current object layer instance. - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return errServerNotInitialized } @@ -734,7 +734,7 @@ func (h *healSequence) healBuckets() error { // healBucket - traverses and heals given bucket func (h *healSequence) healBucket(bucket string) error { // Get current object layer instance. - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return errServerNotInitialized } @@ -771,7 +771,7 @@ func (h *healSequence) healObject(bucket, object string) error { } // Get current object layer instance. - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return errServerNotInitialized } diff --git a/cmd/api-router.go b/cmd/api-router.go index 541c242bc..a566ac899 100644 --- a/cmd/api-router.go +++ b/cmd/api-router.go @@ -23,6 +23,31 @@ import ( xhttp "github.com/minio/minio/cmd/http" ) +func newObjectLayerWithoutSafeModeFn() ObjectLayer { + globalObjLayerMutex.Lock() + defer globalObjLayerMutex.Unlock() + return globalObjectAPI +} + +func newObjectLayerFn() ObjectLayer { + globalObjLayerMutex.Lock() + defer globalObjLayerMutex.Unlock() + if globalSafeMode { + return nil + } + return globalObjectAPI +} + +func newCachedObjectLayerFn() CacheObjectLayer { + globalObjLayerMutex.Lock() + defer globalObjLayerMutex.Unlock() + + if globalSafeMode { + return nil + } + return globalCacheObjectAPI +} + // objectAPIHandler implements and provides http handlers for S3 API. type objectAPIHandlers struct { ObjectAPI func() ObjectLayer @@ -37,18 +62,8 @@ type objectAPIHandlers struct { func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool) { // Initialize API. api := objectAPIHandlers{ - ObjectAPI: func() ObjectLayer { - if !globalSafeMode { - return globalObjectAPI - } - return nil - }, - CacheAPI: func() CacheObjectLayer { - if !globalSafeMode { - return globalCacheObjectAPI - } - return nil - }, + ObjectAPI: newObjectLayerFn, + CacheAPI: newCachedObjectLayerFn, EncryptionEnabled: func() bool { return encryptionEnabled }, diff --git a/cmd/background-heal-ops.go b/cmd/background-heal-ops.go index b0c335174..d1f1d862a 100644 --- a/cmd/background-heal-ops.go +++ b/cmd/background-heal-ops.go @@ -107,7 +107,7 @@ func startBackgroundHealing() { var objAPI ObjectLayer for { - objAPI = globalObjectAPI + objAPI = newObjectLayerWithoutSafeModeFn() if objAPI == nil { time.Sleep(time.Second) continue @@ -135,7 +135,7 @@ func initBackgroundHealing() { // failure error occurred. func bgHealDiskFormat(ctx context.Context, opts madmin.HealOpts) (madmin.HealResultItem, error) { // Get current object layer instance. - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return madmin.HealResultItem{}, errServerNotInitialized } @@ -165,7 +165,7 @@ func bgHealDiskFormat(ctx context.Context, opts madmin.HealOpts) (madmin.HealRes // bghealBucket - traverses and heals given bucket func bgHealBucket(ctx context.Context, bucket string, opts madmin.HealOpts) (madmin.HealResultItem, error) { // Get current object layer instance. - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return madmin.HealResultItem{}, errServerNotInitialized } @@ -176,7 +176,7 @@ func bgHealBucket(ctx context.Context, bucket string, opts madmin.HealOpts) (mad // bgHealObject - heal the given object and record result func bgHealObject(ctx context.Context, bucket, object string, opts madmin.HealOpts) (madmin.HealResultItem, error) { // Get current object layer instance. - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return madmin.HealResultItem{}, errServerNotInitialized } diff --git a/cmd/background-newdisks-heal-ops.go b/cmd/background-newdisks-heal-ops.go index cca358c74..a856a2624 100644 --- a/cmd/background-newdisks-heal-ops.go +++ b/cmd/background-newdisks-heal-ops.go @@ -36,7 +36,7 @@ func monitorLocalDisksAndHeal() { // Wait until the object layer is ready var objAPI ObjectLayer for { - objAPI = globalObjectAPI + objAPI = newObjectLayerWithoutSafeModeFn() if objAPI == nil { time.Sleep(time.Second) continue diff --git a/cmd/bucket-handlers.go b/cmd/bucket-handlers.go index 5d31b7ed4..8e2e1c879 100644 --- a/cmd/bucket-handlers.go +++ b/cmd/bucket-handlers.go @@ -51,11 +51,8 @@ import ( // -- If no, make an entry // -- If yes, check if the IP of entry matches local IP. This means entry is for this instance. // -- If IP of the entry doesn't match, this means entry is for another instance. Log an error to console. -func initFederatorBackend(objLayer ObjectLayer) { - // Get buckets in the backend - b, err := objLayer.ListBuckets(context.Background()) - if err != nil { - logger.LogIf(context.Background(), err) +func initFederatorBackend(buckets []BucketInfo, objLayer ObjectLayer) { + if len(buckets) == 0 { return } @@ -69,21 +66,21 @@ func initFederatorBackend(objLayer ObjectLayer) { bucketSet := set.NewStringSet() // Add buckets that are not registered with the DNS - g := errgroup.WithNErrs(len(b)) - for index := range b { - bucketSet.Add(b[index].Name) + g := errgroup.WithNErrs(len(buckets)) + for index := range buckets { + bucketSet.Add(buckets[index].Name) index := index g.Go(func() error { - r, gerr := globalDNSConfig.Get(b[index].Name) + r, gerr := globalDNSConfig.Get(buckets[index].Name) if gerr != nil { if gerr == dns.ErrNoEntriesFound { - return globalDNSConfig.Put(b[index].Name) + return globalDNSConfig.Put(buckets[index].Name) } return gerr } if globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(r)...)).IsEmpty() { // There is already an entry for this bucket, with all IP addresses different. This indicates a bucket name collision. Log an error and continue. - return fmt.Errorf("Unable to add bucket DNS entry for bucket %s, an entry exists for the same bucket. Use one of these IP addresses %v to access the bucket", b[index].Name, globalDomainIPs.ToSlice()) + return fmt.Errorf("Unable to add bucket DNS entry for bucket %s, an entry exists for the same bucket. Use one of these IP addresses %v to access the bucket", buckets[index].Name, globalDomainIPs.ToSlice()) } return nil }, index) diff --git a/cmd/config-current.go b/cmd/config-current.go index 54b40d357..47e38c580 100644 --- a/cmd/config-current.go +++ b/cmd/config-current.go @@ -48,51 +48,84 @@ func validateConfig(s config.Config) error { // Disable merging env values with config for validation. env.SetEnvOff() - // Enable env values upon return. + // Enable env values to validate KMS. defer env.SetEnvOn() if _, err := config.LookupCreds(s[config.CredentialsSubSys][config.Default]); err != nil { return err } + if _, err := config.LookupRegion(s[config.RegionSubSys][config.Default]); err != nil { return err } + if _, err := config.LookupWorm(s[config.WormSubSys][config.Default]); err != nil { return err } + if globalIsXL { if _, err := storageclass.LookupConfig(s[config.StorageClassSubSys][config.Default], globalXLSetDriveCount); err != nil { return err } } - if _, err := etcd.LookupConfig(s[config.EtcdSubSys][config.Default], globalRootCAs); err != nil { - return err - } + if _, err := cache.LookupConfig(s[config.CacheSubSys][config.Default]); err != nil { return err } - if _, err := crypto.LookupConfig(s[config.KmsVaultSubSys][config.Default]); err != nil { - return err - } + if _, err := compress.LookupConfig(s[config.CompressionSubSys][config.Default]); err != nil { return err } + + { + etcdCfg, err := etcd.LookupConfig(s[config.EtcdSubSys][config.Default], globalRootCAs) + if err != nil { + return err + } + if etcdCfg.Enabled { + etcdClnt, err := etcd.New(etcdCfg) + if err != nil { + return err + } + etcdClnt.Close() + } + } + { + kmsCfg, err := crypto.LookupConfig(s[config.KmsVaultSubSys][config.Default]) + if err != nil { + return err + } + if kmsCfg.Vault.Enabled { + // Set env to enable master key validation. + // this is needed only for KMS. + env.SetEnvOn() + + if _, err = crypto.NewKMS(kmsCfg); err != nil { + return err + } + } + } + if _, err := openid.LookupConfig(s[config.IdentityOpenIDSubSys][config.Default], NewCustomHTTPTransport(), xhttp.DrainBody); err != nil { return err } + if _, err := xldap.Lookup(s[config.IdentityLDAPSubSys][config.Default], globalRootCAs); err != nil { return err } + if _, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default], NewCustomHTTPTransport(), xhttp.DrainBody); err != nil { return err } + if _, err := logger.LookupConfig(s); err != nil { return err } + return notify.TestNotificationTargets(s, GlobalServiceDoneCh, globalRootCAs) } diff --git a/cmd/config-encrypted.go b/cmd/config-encrypted.go index f3a4a1e93..47f24b6b3 100644 --- a/cmd/config-encrypted.go +++ b/cmd/config-encrypted.go @@ -19,9 +19,11 @@ package cmd import ( "bytes" "context" + "os" "strings" etcd "github.com/coreos/etcd/clientv3" + jsoniter "github.com/json-iterator/go" "github.com/minio/minio/cmd/config" "github.com/minio/minio/cmd/logger" "github.com/minio/minio/pkg/auth" @@ -97,6 +99,8 @@ func handleEncryptedConfigBackend(objAPI ObjectLayer, server bool) error { if err != nil { return err } + os.Unsetenv(config.EnvAccessKeyOld) + os.Unsetenv(config.EnvSecretKeyOld) } // Migrating Config backend needs a retry mechanism for @@ -124,7 +128,8 @@ const ( ) var ( - backendEncryptedFileValue = []byte("encrypted") + backendEncryptedMigrationIncomplete = []byte("incomplete") + backendEncryptedMigrationComplete = []byte("encrypted") ) func checkBackendEtcdEncrypted(ctx context.Context, client *etcd.Client) (bool, error) { @@ -132,7 +137,7 @@ func checkBackendEtcdEncrypted(ctx context.Context, client *etcd.Client) (bool, if err != nil && err != errConfigNotFound { return false, err } - return err == nil && bytes.Equal(data, backendEncryptedFileValue), nil + return err == nil && bytes.Equal(data, backendEncryptedMigrationComplete), nil } func checkBackendEncrypted(objAPI ObjectLayer) (bool, error) { @@ -140,7 +145,7 @@ func checkBackendEncrypted(objAPI ObjectLayer) (bool, error) { if err != nil && err != errConfigNotFound { return false, err } - return err == nil && bytes.Equal(data, backendEncryptedFileValue), nil + return err == nil && bytes.Equal(data, backendEncryptedMigrationComplete), nil } func migrateIAMConfigsEtcdToEncrypted(client *etcd.Client) error { @@ -178,6 +183,9 @@ func migrateIAMConfigsEtcdToEncrypted(client *etcd.Client) error { if err != nil { return err } + // Once we have obtained the rotating creds + os.Unsetenv(config.EnvAccessKeyOld) + os.Unsetenv(config.EnvSecretKeyOld) } if encrypted { @@ -191,18 +199,21 @@ func migrateIAMConfigsEtcdToEncrypted(client *etcd.Client) error { if activeCredOld.Equal(globalActiveCred) { return nil } - } - if !activeCredOld.IsValid() { - logger.Info("Attempting a one time encrypt of all IAM users and policies on etcd") + logger.Info("Attempting rotation of encrypted IAM users and policies on etcd with newly supplied credentials") } else { - logger.Info("Attempting a rotation of encrypted IAM users and policies on etcd with newly supplied credentials") + logger.Info("Attempting encryption of all IAM users and policies on etcd") } r, err := client.Get(ctx, minioConfigPrefix, etcd.WithPrefix(), etcd.WithKeysOnly()) if err != nil { return err } + + if err = saveKeyEtcd(ctx, client, backendEncryptedFile, backendEncryptedMigrationIncomplete); err != nil { + return err + } + for _, kv := range r.Kvs { var ( cdata []byte @@ -217,18 +228,32 @@ func migrateIAMConfigsEtcdToEncrypted(client *etcd.Client) error { } return err } + + var data []byte // Is rotating of creds requested? if activeCredOld.IsValid() { - cdata, err = madmin.DecryptData(activeCredOld.String(), bytes.NewReader(cdata)) + data, err = madmin.DecryptData(activeCredOld.String(), bytes.NewReader(cdata)) if err != nil { if err == madmin.ErrMaliciousData { - return config.ErrInvalidRotatingCredentialsBackendEncrypted(nil) + data, err = madmin.DecryptData(globalActiveCred.String(), + bytes.NewReader(cdata)) + if err != nil { + return config.ErrInvalidRotatingCredentialsBackendEncrypted(nil) + } + } else { + return err } - return err } } - cencdata, err = madmin.EncryptData(globalActiveCred.String(), cdata) + // Attempt to unmarshal JSON content + var dummy map[string]interface{} + var json = jsoniter.ConfigCompatibleWithStandardLibrary + if err = json.Unmarshal(data, &dummy); err != nil { + return err + } + + cencdata, err = madmin.EncryptData(globalActiveCred.String(), data) if err != nil { return err } @@ -237,7 +262,10 @@ func migrateIAMConfigsEtcdToEncrypted(client *etcd.Client) error { return err } } - return saveKeyEtcd(ctx, client, backendEncryptedFile, backendEncryptedFileValue) + if encrypted && globalActiveCred.IsValid() { + logger.Info("Rotation complete, please make sure to unset MINIO_ACCESS_KEY_OLD and MINIO_SECRET_KEY_OLD envs") + } + return saveKeyEtcd(ctx, client, backendEncryptedFile, backendEncryptedMigrationComplete) } func migrateConfigPrefixToEncrypted(objAPI ObjectLayer, activeCredOld auth.Credentials, encrypted bool) error { @@ -252,12 +280,14 @@ func migrateConfigPrefixToEncrypted(objAPI ObjectLayer, activeCredOld auth.Crede if activeCredOld.Equal(globalActiveCred) { return nil } + logger.Info("Attempting rotation of encrypted config, IAM users and policies on MinIO with newly supplied credentials") + } else { + logger.Info("Attempting encryption of all config, IAM users and policies on MinIO backend") } - if !activeCredOld.IsValid() { - logger.Info("Attempting a one time encrypt of all config, IAM users and policies on MinIO backend") - } else { - logger.Info("Attempting a rotation of encrypted config, IAM users and policies on MinIO with newly supplied credentials") + err := saveConfig(context.Background(), objAPI, backendEncryptedFile, backendEncryptedMigrationIncomplete) + if err != nil { + return err } var marker string @@ -277,18 +307,31 @@ func migrateConfigPrefixToEncrypted(objAPI ObjectLayer, activeCredOld auth.Crede return err } + var data []byte // Is rotating of creds requested? if activeCredOld.IsValid() { - cdata, err = madmin.DecryptData(activeCredOld.String(), bytes.NewReader(cdata)) + data, err = madmin.DecryptData(activeCredOld.String(), bytes.NewReader(cdata)) if err != nil { if err == madmin.ErrMaliciousData { - return config.ErrInvalidRotatingCredentialsBackendEncrypted(nil) + data, err = madmin.DecryptData(globalActiveCred.String(), + bytes.NewReader(cdata)) + if err != nil { + return config.ErrInvalidRotatingCredentialsBackendEncrypted(nil) + } + } else { + return err } - return err } } - cencdata, err = madmin.EncryptData(globalActiveCred.String(), cdata) + // Attempt to unmarshal JSON content + var dummy map[string]interface{} + var json = jsoniter.ConfigCompatibleWithStandardLibrary + if err = json.Unmarshal(data, &dummy); err != nil { + return err + } + + cencdata, err = madmin.EncryptData(globalActiveCred.String(), data) if err != nil { return err } @@ -305,5 +348,9 @@ func migrateConfigPrefixToEncrypted(objAPI ObjectLayer, activeCredOld auth.Crede marker = res.NextMarker } - return saveConfig(context.Background(), objAPI, backendEncryptedFile, backendEncryptedFileValue) + if encrypted && globalActiveCred.IsValid() { + logger.Info("Rotation complete, please make sure to unset MINIO_ACCESS_KEY_OLD and MINIO_SECRET_KEY_OLD envs") + } + + return saveConfig(context.Background(), objAPI, backendEncryptedFile, backendEncryptedMigrationComplete) } diff --git a/cmd/config/config.go b/cmd/config/config.go index 6b1ac7c30..fd26eb9b2 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -386,11 +386,6 @@ func (c Config) SetKVS(s string, defaultKVS map[string]KVS) error { return Error(fmt.Sprintf("invalid number of arguments %s", s)) } - if subSystemValue[0] == CredentialsSubSys { - return Error(fmt.Sprintf("changing '%s' sub-system values is not allowed, use ENVs instead", - subSystemValue[0])) - } - if !SubSystems.Contains(subSystemValue[0]) { return Error(fmt.Sprintf("unknown sub-system %s", s)) } diff --git a/cmd/config/etcd/etcd.go b/cmd/config/etcd/etcd.go index 4f8d036ed..d28789e10 100644 --- a/cmd/config/etcd/etcd.go +++ b/cmd/config/etcd/etcd.go @@ -31,7 +31,7 @@ import ( const ( // Default values used while communicating with etcd. - defaultDialTimeout = 30 * time.Second + defaultDialTimeout = 5 * time.Second defaultDialKeepAlive = 30 * time.Second ) @@ -112,7 +112,7 @@ func LookupConfig(kv config.KVS, rootCAs *x509.CertPool) (Config, error) { return cfg, config.Error("'endpoints' key cannot be empty if you wish to enable etcd") } - if len(endpoints) == 0 { + if len(endpoints) == 0 && !stateBool { return cfg, nil } diff --git a/cmd/crypto/config.go b/cmd/crypto/config.go index 393a3617e..519b61633 100644 --- a/cmd/crypto/config.go +++ b/cmd/crypto/config.go @@ -17,6 +17,7 @@ package crypto import ( "errors" "fmt" + "reflect" "strconv" "github.com/minio/minio/cmd/config" @@ -112,6 +113,12 @@ const ( EnvKMSVaultNamespace = "MINIO_KMS_VAULT_NAMESPACE" ) +var defaultCfg = VaultConfig{ + Auth: VaultAuth{ + Type: "approle", + }, +} + // LookupConfig extracts the KMS configuration provided by environment // variables and merge them with the provided KMS configuration. The // merging follows the following rules: @@ -139,7 +146,7 @@ func LookupConfig(kvs config.KVS) (KMSConfig, error) { return kmsCfg, err } } - if !kmsCfg.Vault.IsEmpty() { + if kmsCfg.Vault.Enabled { return kmsCfg, nil } stateBool, err := config.ParseBool(env.Get(EnvKMSVaultState, kvs.Get(config.State))) @@ -166,23 +173,30 @@ func LookupConfig(kvs config.KVS) (KMSConfig, error) { vcfg.Endpoint = endpointStr vcfg.CAPath = env.Get(EnvKMSVaultCAPath, kvs.Get(KMSVaultCAPath)) vcfg.Auth.Type = env.Get(EnvKMSVaultAuthType, kvs.Get(KMSVaultAuthType)) + if vcfg.Auth.Type == "" { + vcfg.Auth.Type = "approle" + } vcfg.Auth.AppRole.ID = env.Get(EnvKMSVaultAppRoleID, kvs.Get(KMSVaultAppRoleID)) vcfg.Auth.AppRole.Secret = env.Get(EnvKMSVaultAppSecretID, kvs.Get(KMSVaultAppRoleSecret)) vcfg.Key.Name = env.Get(EnvKMSVaultKeyName, kvs.Get(KMSVaultKeyName)) vcfg.Namespace = env.Get(EnvKMSVaultNamespace, kvs.Get(KMSVaultNamespace)) - keyVersion := env.Get(EnvKMSVaultKeyVersion, kvs.Get(KMSVaultKeyVersion)) - - if keyVersion != "" { + if keyVersion := env.Get(EnvKMSVaultKeyVersion, kvs.Get(KMSVaultKeyVersion)); keyVersion != "" { vcfg.Key.Version, err = strconv.Atoi(keyVersion) if err != nil { return kmsCfg, fmt.Errorf("Unable to parse VaultKeyVersion value (`%s`)", keyVersion) } } + if reflect.DeepEqual(vcfg, defaultCfg) { + return kmsCfg, nil + } + + // Verify all the proper settings. if err = vcfg.Verify(); err != nil { return kmsCfg, err } + vcfg.Enabled = true kmsCfg.Vault = vcfg return kmsCfg, nil } @@ -191,7 +205,7 @@ func LookupConfig(kvs config.KVS) (KMSConfig, error) { func NewKMS(cfg KMSConfig) (kms KMS, err error) { // Lookup KMS master keys - only available through ENV. if masterKeyLegacy := env.Get(EnvKMSMasterKeyLegacy, ""); len(masterKeyLegacy) != 0 { - if !cfg.Vault.IsEmpty() { // Vault and KMS master key provided + if cfg.Vault.Enabled { // Vault and KMS master key provided return kms, errors.New("Ambiguous KMS configuration: vault configuration and a master key are provided at the same time") } kms, err = ParseMasterKey(masterKeyLegacy) @@ -199,15 +213,14 @@ func NewKMS(cfg KMSConfig) (kms KMS, err error) { return kms, err } } else if masterKey := env.Get(EnvKMSMasterKey, ""); len(masterKey) != 0 { - if !cfg.Vault.IsEmpty() { // Vault and KMS master key provided + if cfg.Vault.Enabled { // Vault and KMS master key provided return kms, errors.New("Ambiguous KMS configuration: vault configuration and a master key are provided at the same time") } kms, err = ParseMasterKey(masterKey) if err != nil { return kms, err } - } - if !cfg.Vault.IsEmpty() { + } else if cfg.Vault.Enabled { kms, err = NewVault(cfg.Vault) if err != nil { return kms, err diff --git a/cmd/crypto/legacy.go b/cmd/crypto/legacy.go index 9963b691a..387995463 100644 --- a/cmd/crypto/legacy.go +++ b/cmd/crypto/legacy.go @@ -18,6 +18,7 @@ package crypto import ( "fmt" + "reflect" "strconv" "github.com/minio/minio/cmd/config" @@ -40,40 +41,40 @@ const ( ) const ( - // EnvVaultEndpoint is the environment variable used to specify + // EnvLegacyVaultEndpoint is the environment variable used to specify // the vault HTTPS endpoint. - EnvVaultEndpoint = "MINIO_SSE_VAULT_ENDPOINT" + EnvLegacyVaultEndpoint = "MINIO_SSE_VAULT_ENDPOINT" - // EnvVaultAuthType is the environment variable used to specify + // EnvLegacyVaultAuthType is the environment variable used to specify // the authentication type for vault. - EnvVaultAuthType = "MINIO_SSE_VAULT_AUTH_TYPE" + EnvLegacyVaultAuthType = "MINIO_SSE_VAULT_AUTH_TYPE" - // EnvVaultAppRoleID is the environment variable used to specify + // EnvLegacyVaultAppRoleID is the environment variable used to specify // the vault AppRole ID. - EnvVaultAppRoleID = "MINIO_SSE_VAULT_APPROLE_ID" + EnvLegacyVaultAppRoleID = "MINIO_SSE_VAULT_APPROLE_ID" - // EnvVaultAppSecretID is the environment variable used to specify + // EnvLegacyVaultAppSecretID is the environment variable used to specify // the vault AppRole secret corresponding to the AppRole ID. - EnvVaultAppSecretID = "MINIO_SSE_VAULT_APPROLE_SECRET" + EnvLegacyVaultAppSecretID = "MINIO_SSE_VAULT_APPROLE_SECRET" - // EnvVaultKeyVersion is the environment variable used to specify + // EnvLegacyVaultKeyVersion is the environment variable used to specify // the vault key version. - EnvVaultKeyVersion = "MINIO_SSE_VAULT_KEY_VERSION" + EnvLegacyVaultKeyVersion = "MINIO_SSE_VAULT_KEY_VERSION" - // EnvVaultKeyName is the environment variable used to specify + // EnvLegacyVaultKeyName is the environment variable used to specify // the vault named key-ring. In the S3 context it's referred as // customer master key ID (CMK-ID). - EnvVaultKeyName = "MINIO_SSE_VAULT_KEY_NAME" + EnvLegacyVaultKeyName = "MINIO_SSE_VAULT_KEY_NAME" - // EnvVaultCAPath is the environment variable used to specify the + // EnvLegacyVaultCAPath is the environment variable used to specify the // path to a directory of PEM-encoded CA cert files. These CA cert // files are used to authenticate MinIO to Vault over mTLS. - EnvVaultCAPath = "MINIO_SSE_VAULT_CAPATH" + EnvLegacyVaultCAPath = "MINIO_SSE_VAULT_CAPATH" - // EnvVaultNamespace is the environment variable used to specify + // EnvLegacyVaultNamespace is the environment variable used to specify // vault namespace. The vault namespace is used if the enterprise // version of Hashicorp Vault is used. - EnvVaultNamespace = "MINIO_SSE_VAULT_NAMESPACE" + EnvLegacyVaultNamespace = "MINIO_SSE_VAULT_NAMESPACE" ) // SetKMSConfig helper to migrate from older KMSConfig to new KV. @@ -93,7 +94,7 @@ func SetKMSConfig(s config.Config, cfg KMSConfig) { KMSVaultKeyVersion: strconv.Itoa(cfg.Vault.Key.Version), KMSVaultNamespace: cfg.Vault.Namespace, config.State: func() string { - if !cfg.Vault.IsEmpty() { + if cfg.Vault.Endpoint != "" { return config.StateOn } return config.StateOff @@ -141,7 +142,7 @@ func lookupConfigLegacy(kvs config.KVS) (KMSConfig, error) { return cfg, nil } - endpointStr := env.Get(EnvKMSVaultEndpoint, kvs.Get(KMSVaultEndpoint)) + endpointStr := env.Get(EnvLegacyVaultEndpoint, "") if endpointStr != "" { // Lookup Hashicorp-Vault configuration & overwrite config entry if ENV var is present endpoint, err := xnet.ParseHTTPURL(endpointStr) @@ -152,25 +153,31 @@ func lookupConfigLegacy(kvs config.KVS) (KMSConfig, error) { } cfg.Vault.Endpoint = endpointStr - cfg.Vault.CAPath = env.Get(EnvVaultCAPath, kvs.Get(KMSVaultCAPath)) - cfg.Vault.Auth.Type = env.Get(EnvVaultAuthType, kvs.Get(KMSVaultAuthType)) - cfg.Vault.Auth.AppRole.ID = env.Get(EnvVaultAppRoleID, kvs.Get(KMSVaultAppRoleID)) - cfg.Vault.Auth.AppRole.Secret = env.Get(EnvVaultAppSecretID, kvs.Get(KMSVaultAppRoleSecret)) - cfg.Vault.Key.Name = env.Get(EnvVaultKeyName, kvs.Get(KMSVaultKeyName)) - cfg.Vault.Namespace = env.Get(EnvVaultNamespace, kvs.Get(KMSVaultNamespace)) - keyVersion := env.Get(EnvVaultKeyVersion, kvs.Get(KMSVaultKeyVersion)) - - if keyVersion != "" { + cfg.Vault.CAPath = env.Get(EnvLegacyVaultCAPath, "") + cfg.Vault.Auth.Type = env.Get(EnvLegacyVaultAuthType, "") + if cfg.Vault.Auth.Type == "" { + cfg.Vault.Auth.Type = "approle" + } + cfg.Vault.Auth.AppRole.ID = env.Get(EnvLegacyVaultAppRoleID, "") + cfg.Vault.Auth.AppRole.Secret = env.Get(EnvLegacyVaultAppSecretID, "") + cfg.Vault.Key.Name = env.Get(EnvLegacyVaultKeyName, "") + cfg.Vault.Namespace = env.Get(EnvLegacyVaultNamespace, "") + if keyVersion := env.Get(EnvLegacyVaultKeyVersion, ""); keyVersion != "" { cfg.Vault.Key.Version, err = strconv.Atoi(keyVersion) if err != nil { return cfg, fmt.Errorf("Invalid ENV variable: Unable to parse %s value (`%s`)", - EnvVaultKeyVersion, keyVersion) + EnvLegacyVaultKeyVersion, keyVersion) } } + if reflect.DeepEqual(cfg.Vault, defaultCfg) { + return cfg, nil + } + if err = cfg.Vault.Verify(); err != nil { return cfg, err } + cfg.Vault.Enabled = true return cfg, nil } diff --git a/cmd/crypto/vault.go b/cmd/crypto/vault.go index 1c461d741..10bff73c5 100644 --- a/cmd/crypto/vault.go +++ b/cmd/crypto/vault.go @@ -51,6 +51,7 @@ type VaultAppRole struct { // VaultConfig represents vault configuration. type VaultConfig struct { + Enabled bool `json:"-"` Endpoint string `json:"endpoint"` // The vault API endpoint as URL CAPath string `json:"-"` // The path to PEM-encoded certificate files used for mTLS. Currently not used in config file. Auth VaultAuth `json:"auth"` // The vault authentication configuration @@ -68,24 +69,10 @@ type vaultService struct { var _ KMS = (*vaultService)(nil) // compiler check that *vaultService implements KMS -// empty/default vault configuration used to check whether a particular is empty. -var emptyVaultConfig = VaultConfig{ - Auth: VaultAuth{ - Type: "approle", - }, -} - -// IsEmpty returns true if the vault config struct is an -// empty configuration. -func (v *VaultConfig) IsEmpty() bool { return *v == emptyVaultConfig } - // Verify returns a nil error if the vault configuration // is valid. A valid configuration is either empty or // contains valid non-default values. func (v *VaultConfig) Verify() (err error) { - if v.IsEmpty() { - return // an empty configuration is valid - } switch { case v.Endpoint == "": err = errors.New("crypto: missing hashicorp vault endpoint") @@ -107,8 +94,8 @@ func (v *VaultConfig) Verify() (err error) { // to Vault with the credentials in config and gets a client // token for future api calls. func NewVault(config VaultConfig) (KMS, error) { - if config.IsEmpty() { - return nil, errors.New("crypto: the hashicorp vault configuration must not be empty") + if !config.Enabled { + return nil, nil } if err := config.Verify(); err != nil { return nil, err diff --git a/cmd/crypto/vault_test.go b/cmd/crypto/vault_test.go index 397bc2ae1..c83cb8f3a 100644 --- a/cmd/crypto/vault_test.go +++ b/cmd/crypto/vault_test.go @@ -15,7 +15,6 @@ package crypto import ( - "fmt" "testing" ) @@ -23,17 +22,17 @@ var verifyVaultConfigTests = []struct { Config VaultConfig ShouldFail bool }{ - { - ShouldFail: false, // 0 - Config: emptyVaultConfig, - }, { ShouldFail: true, - Config: VaultConfig{Endpoint: "https://127.0.0.1:8080"}, + Config: VaultConfig{ + Endpoint: "https://127.0.0.1:8080", + Enabled: true, + }, }, { ShouldFail: true, // 1 Config: VaultConfig{ + Enabled: true, Endpoint: "https://127.0.0.1:8080", Auth: VaultAuth{Type: "unsupported"}, }, @@ -41,6 +40,7 @@ var verifyVaultConfigTests = []struct { { ShouldFail: true, // 2 Config: VaultConfig{ + Enabled: true, Endpoint: "https://127.0.0.1:8080", Auth: VaultAuth{ Type: "approle", @@ -51,6 +51,7 @@ var verifyVaultConfigTests = []struct { { ShouldFail: true, // 3 Config: VaultConfig{ + Enabled: true, Endpoint: "https://127.0.0.1:8080", Auth: VaultAuth{ Type: "approle", @@ -61,6 +62,7 @@ var verifyVaultConfigTests = []struct { { ShouldFail: true, // 4 Config: VaultConfig{ + Enabled: true, Endpoint: "https://127.0.0.1:8080", Auth: VaultAuth{ Type: "approle", @@ -71,6 +73,7 @@ var verifyVaultConfigTests = []struct { { ShouldFail: true, // 5 Config: VaultConfig{ + Enabled: true, Endpoint: "https://127.0.0.1:8080", Auth: VaultAuth{ Type: "approle", @@ -82,9 +85,9 @@ var verifyVaultConfigTests = []struct { } func TestVerifyVaultConfig(t *testing.T) { - for i, test := range verifyVaultConfigTests { + for _, test := range verifyVaultConfigTests { test := test - t.Run(fmt.Sprintf("Test-%d", i), func(t *testing.T) { + t.Run(test.Config.Endpoint, func(t *testing.T) { err := test.Config.Verify() if test.ShouldFail && err == nil { t.Errorf("Verify should fail but returned 'err == nil'") diff --git a/cmd/daily-lifecycle-ops.go b/cmd/daily-lifecycle-ops.go index 2afa65be4..563189af1 100644 --- a/cmd/daily-lifecycle-ops.go +++ b/cmd/daily-lifecycle-ops.go @@ -56,7 +56,7 @@ func startDailyLifecycle() { // Wait until the object API is ready for { - objAPI = globalObjectAPI + objAPI = newObjectLayerWithoutSafeModeFn() if objAPI == nil { time.Sleep(time.Second) continue diff --git a/cmd/disk-cache.go b/cmd/disk-cache.go index 75531658e..659e0224c 100644 --- a/cmd/disk-cache.go +++ b/cmd/disk-cache.go @@ -553,23 +553,23 @@ func newServerCacheObjects(ctx context.Context, config cache.Config) (CacheObjec migrating: migrateSw, migMutex: sync.Mutex{}, GetObjectInfoFn: func(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) { - return globalObjectAPI.GetObjectInfo(ctx, bucket, object, opts) + return newObjectLayerFn().GetObjectInfo(ctx, bucket, object, opts) }, GetObjectNInfoFn: func(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) { - return globalObjectAPI.GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts) + return newObjectLayerFn().GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts) }, DeleteObjectFn: func(ctx context.Context, bucket, object string) error { - return globalObjectAPI.DeleteObject(ctx, bucket, object) + return newObjectLayerFn().DeleteObject(ctx, bucket, object) }, DeleteObjectsFn: func(ctx context.Context, bucket string, objects []string) ([]error, error) { errs := make([]error, len(objects)) for idx, object := range objects { - errs[idx] = globalObjectAPI.DeleteObject(ctx, bucket, object) + errs[idx] = newObjectLayerFn().DeleteObject(ctx, bucket, object) } return errs, nil }, PutObjectFn: func(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) { - return globalObjectAPI.PutObject(ctx, bucket, object, data, opts) + return newObjectLayerFn().PutObject(ctx, bucket, object, data, opts) }, } if migrateSw { diff --git a/cmd/gateway-main.go b/cmd/gateway-main.go index ff47be7dd..dc8331cab 100644 --- a/cmd/gateway-main.go +++ b/cmd/gateway-main.go @@ -222,9 +222,22 @@ func StartGateway(ctx *cli.Context, gw Gateway) { logger.FatalIf(err, "Unable to initialize gateway backend") } + // Re-enable logging + logger.Disable = false + + // Once endpoints are finalized, initialize the new object api in safe mode. + globalObjLayerMutex.Lock() + globalSafeMode = true + globalObjectAPI = newObject + globalObjLayerMutex.Unlock() + // Populate existing buckets to the etcd backend if globalDNSConfig != nil { - initFederatorBackend(newObject) + buckets, err := newObject.ListBuckets(context.Background()) + if err != nil { + logger.Fatal(err, "Unable to list buckets") + } + initFederatorBackend(buckets, newObject) } // Migrate all backend configs to encrypted backend, also handles rotation as well. @@ -233,16 +246,15 @@ func StartGateway(ctx *cli.Context, gw Gateway) { logger.FatalIf(handleEncryptedConfigBackend(newObject, enableConfigOps), "Unable to handle encrypted backend for config, iam and policies") + // Calls all New() for all sub-systems. + newAllSubsystems() + // **** WARNING **** // Migrating to encrypted backend should happen before initialization of any // sub-systems, make sure that we do not move the above codeblock elsewhere. if enableConfigOps { - // Create a new config system. - globalConfigSys = NewConfigSys() - - // Load globalServerConfig from disk - logger.LogIf(context.Background(), globalConfigSys.Init(newObject)) + logger.FatalIf(globalConfigSys.Init(newObject), "Unable to initialize config system") // Start watching disk for reloading config, this // is only enabled for "NAS" gateway. @@ -261,32 +273,20 @@ func StartGateway(ctx *cli.Context, gw Gateway) { "Unable to handle encrypted backend for iam and policies") } - if globalCacheConfig.Enabled { - // initialize the new disk cache objects. - globalCacheObjectAPI, err = newServerCacheObjects(context.Background(), globalCacheConfig) - logger.FatalIf(err, "Unable to initialize disk caching") - } - - // Re-enable logging - logger.Disable = false - - // Create new IAM system. - globalIAMSys = NewIAMSys() if enableIAMOps { // Initialize IAM sys. - logger.LogIf(context.Background(), globalIAMSys.Init(newObject)) + logger.FatalIf(globalIAMSys.Init(newObject), "Unable to initialize IAM system") } - // Create new policy system. - globalPolicySys = NewPolicySys() + if globalCacheConfig.Enabled { + // initialize the new disk cache objects. + var cacheAPI CacheObjectLayer + cacheAPI, err = newServerCacheObjects(context.Background(), globalCacheConfig) + logger.FatalIf(err, "Unable to initialize disk caching") - // Create new lifecycle system. - globalLifecycleSys = NewLifecycleSys() - - // Create new notification system. - globalNotificationSys, err = NewNotificationSys(globalServerConfig, globalEndpoints) - if err != nil { - logger.FatalIf(err, "Unable to initialize notification system") + globalObjLayerMutex.Lock() + globalCacheObjectAPI = cacheAPI + globalObjLayerMutex.Unlock() } // Verify if object layer supports @@ -294,11 +294,6 @@ func StartGateway(ctx *cli.Context, gw Gateway) { // - compression verifyObjectLayerFeatures("gateway "+gatewayName, newObject) - // Once endpoints are finalized, initialize the new object api. - globalObjLayerMutex.Lock() - globalObjectAPI = newObject - globalObjLayerMutex.Unlock() - // Prints the formatted startup message once object layer is initialized. if !globalCLIContext.Quiet { mode := globalMinioModeGatewayPrefix + gatewayName @@ -314,6 +309,11 @@ func StartGateway(ctx *cli.Context, gw Gateway) { printGatewayStartupMessage(getAPIEndpoints(), gatewayName) } + // Disable safe mode operation, after all initialization is over. + globalObjLayerMutex.Lock() + globalSafeMode = false + globalObjLayerMutex.Unlock() + // Set uptime time after object layer has initialized. globalBootTime = UTCNow() diff --git a/cmd/gateway-startup-msg.go b/cmd/gateway-startup-msg.go index 3e12ed68e..193bc34c5 100644 --- a/cmd/gateway-startup-msg.go +++ b/cmd/gateway-startup-msg.go @@ -28,8 +28,9 @@ import ( func printGatewayStartupMessage(apiEndPoints []string, backendType string) { strippedAPIEndpoints := stripStandardPorts(apiEndPoints) // If cache layer is enabled, print cache capacity. - if globalCacheObjectAPI != nil { - printCacheStorageInfo(globalCacheObjectAPI.StorageInfo(context.Background())) + cacheAPI := newCachedObjectLayerFn() + if cacheAPI != nil { + printCacheStorageInfo(cacheAPI.StorageInfo(context.Background())) } // Prints credential. printGatewayCommonMsg(strippedAPIEndpoints) diff --git a/cmd/global-heal.go b/cmd/global-heal.go index e52922300..534207671 100644 --- a/cmd/global-heal.go +++ b/cmd/global-heal.go @@ -156,7 +156,7 @@ func execLeaderTasks(sets *xlSets) { func startGlobalHeal() { var objAPI ObjectLayer for { - objAPI = globalObjectAPI + objAPI = newObjectLayerWithoutSafeModeFn() if objAPI == nil { time.Sleep(time.Second) continue diff --git a/cmd/healthcheck-handler.go b/cmd/healthcheck-handler.go index 081f328fb..842d25959 100644 --- a/cmd/healthcheck-handler.go +++ b/cmd/healthcheck-handler.go @@ -52,9 +52,9 @@ func ReadinessCheckHandler(w http.ResponseWriter, r *http.Request) { func LivenessCheckHandler(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "LivenessCheckHandler") - objLayer := globalObjectAPI + objLayer := newObjectLayerFn() // Service not initialized yet - if objLayer == nil || globalSafeMode { + if objLayer == nil { // Respond with 200 OK while server initializes to ensure a distributed cluster // is able to start on orchestration platforms like Docker Swarm. // Refer https://github.com/minio/minio/issues/8140 for more details. diff --git a/cmd/iam-object-store.go b/cmd/iam-object-store.go index 2d1fa4a67..5d18844d3 100644 --- a/cmd/iam-object-store.go +++ b/cmd/iam-object-store.go @@ -49,7 +49,7 @@ func (iamOS *IAMObjectStore) getObjectAPI() ObjectLayer { if iamOS.objAPI != nil { return iamOS.objAPI } - return globalObjectAPI + return newObjectLayerWithoutSafeModeFn() } func (iamOS *IAMObjectStore) setObjectAPI(objAPI ObjectLayer) { diff --git a/cmd/iam.go b/cmd/iam.go index 1e15fdd1d..ce5b7df11 100644 --- a/cmd/iam.go +++ b/cmd/iam.go @@ -406,7 +406,7 @@ func (sys *IAMSys) Init(objAPI ObjectLayer) error { // DeletePolicy - deletes a canned policy from backend or etcd. func (sys *IAMSys) DeletePolicy(policyName string) error { - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return errServerNotInitialized } @@ -431,7 +431,7 @@ func (sys *IAMSys) DeletePolicy(policyName string) error { // InfoPolicy - expands the canned policy into its JSON structure. func (sys *IAMSys) InfoPolicy(policyName string) ([]byte, error) { - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return nil, errServerNotInitialized } @@ -448,7 +448,7 @@ func (sys *IAMSys) InfoPolicy(policyName string) ([]byte, error) { // ListPolicies - lists all canned policies. func (sys *IAMSys) ListPolicies() (map[string][]byte, error) { - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return nil, errServerNotInitialized } @@ -471,7 +471,7 @@ func (sys *IAMSys) ListPolicies() (map[string][]byte, error) { // SetPolicy - sets a new name policy. func (sys *IAMSys) SetPolicy(policyName string, p iampolicy.Policy) error { - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return errServerNotInitialized } @@ -492,7 +492,7 @@ func (sys *IAMSys) SetPolicy(policyName string, p iampolicy.Policy) error { // DeleteUser - delete user (only for long-term users not STS users). func (sys *IAMSys) DeleteUser(accessKey string) error { - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return errServerNotInitialized } @@ -521,7 +521,7 @@ func (sys *IAMSys) DeleteUser(accessKey string) error { // SetTempUser - set temporary user credentials, these credentials have an expiry. func (sys *IAMSys) SetTempUser(accessKey string, cred auth.Credentials, policyName string) error { - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return errServerNotInitialized } @@ -561,7 +561,7 @@ func (sys *IAMSys) SetTempUser(accessKey string, cred auth.Credentials, policyNa // ListUsers - list all users. func (sys *IAMSys) ListUsers() (map[string]madmin.UserInfo, error) { - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return nil, errServerNotInitialized } @@ -587,7 +587,7 @@ func (sys *IAMSys) ListUsers() (map[string]madmin.UserInfo, error) { // GetUserInfo - get info on a user. func (sys *IAMSys) GetUserInfo(name string) (u madmin.UserInfo, err error) { - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return u, errServerNotInitialized } @@ -617,7 +617,7 @@ func (sys *IAMSys) GetUserInfo(name string) (u madmin.UserInfo, err error) { // SetUserStatus - sets current user status, supports disabled or enabled. func (sys *IAMSys) SetUserStatus(accessKey string, status madmin.AccountStatus) error { - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return errServerNotInitialized } @@ -653,7 +653,7 @@ func (sys *IAMSys) SetUserStatus(accessKey string, status madmin.AccountStatus) // SetUser - set user credentials and policy. func (sys *IAMSys) SetUser(accessKey string, uinfo madmin.UserInfo) error { - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return errServerNotInitialized } @@ -685,7 +685,7 @@ func (sys *IAMSys) SetUser(accessKey string, uinfo madmin.UserInfo) error { // SetUserSecretKey - sets user secret key func (sys *IAMSys) SetUserSecretKey(accessKey string, secretKey string) error { - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return errServerNotInitialized } @@ -724,7 +724,7 @@ func (sys *IAMSys) GetUser(accessKey string) (cred auth.Credentials, ok bool) { // AddUsersToGroup - adds users to a group, creating the group if // needed. No error if user(s) already are in the group. func (sys *IAMSys) AddUsersToGroup(group string, members []string) error { - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return errServerNotInitialized } @@ -782,7 +782,7 @@ func (sys *IAMSys) AddUsersToGroup(group string, members []string) error { // RemoveUsersFromGroup - remove users from group. If no users are // given, and the group is empty, deletes the group as well. func (sys *IAMSys) RemoveUsersFromGroup(group string, members []string) error { - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return errServerNotInitialized } @@ -863,7 +863,7 @@ func (sys *IAMSys) RemoveUsersFromGroup(group string, members []string) error { // SetGroupStatus - enable/disabled a group func (sys *IAMSys) SetGroupStatus(group string, enabled bool) error { - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return errServerNotInitialized } @@ -952,7 +952,7 @@ func (sys *IAMSys) ListGroups() (r []string, err error) { // PolicyDB. This function applies only long-term users. For STS // users, policy is set directly by called sys.policyDBSet(). func (sys *IAMSys) PolicyDBSet(name, policy string, isGroup bool) error { - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return errServerNotInitialized } @@ -1007,7 +1007,7 @@ func (sys *IAMSys) PolicyDBGet(name string, isGroup bool) ([]string, error) { return nil, errInvalidArgument } - objectAPI := globalObjectAPI + objectAPI := newObjectLayerWithoutSafeModeFn() if objectAPI == nil { return nil, errServerNotInitialized } diff --git a/cmd/lifecycle.go b/cmd/lifecycle.go index aa39ac217..b680c49e2 100644 --- a/cmd/lifecycle.go +++ b/cmd/lifecycle.go @@ -59,7 +59,7 @@ func (sys *LifecycleSys) Get(bucketName string) (lifecycle lifecycle.Lifecycle, if globalIsGateway { // When gateway is enabled, no cached value // is used to validate life cycle policies. - objAPI := globalObjectAPI + objAPI := newObjectLayerWithoutSafeModeFn() if objAPI == nil { return } diff --git a/cmd/metrics.go b/cmd/metrics.go index 2e27747d8..4be5b05cb 100644 --- a/cmd/metrics.go +++ b/cmd/metrics.go @@ -83,9 +83,9 @@ func (c *minioCollector) Collect(ch chan<- prometheus.Metric) { minioVersionInfo.WithLabelValues(Version, CommitID).Set(float64(1.0)) // Fetch disk space info - objLayer := globalObjectAPI + objLayer := newObjectLayerFn() // Service not initialized yet - if objLayer == nil || globalSafeMode { + if objLayer == nil { return } diff --git a/cmd/notification.go b/cmd/notification.go index 2bb0621ef..3754fb14c 100644 --- a/cmd/notification.go +++ b/cmd/notification.go @@ -628,6 +628,20 @@ func (sys *NotificationSys) ListenBucketNotification(ctx context.Context, bucket }() } +// AddNotificationTargetsFromConfig - adds notification targets from server config. +func (sys *NotificationSys) AddNotificationTargetsFromConfig(cfg config.Config) error { + targetList, err := notify.GetNotificationTargets(cfg, GlobalServiceDoneCh, globalRootCAs) + if err != nil { + return err + } + for _, target := range targetList.Targets() { + if err = sys.targetList.Add(target); err != nil { + return err + } + } + return nil +} + // AddRemoteTarget - adds event rules map, HTTP/PeerRPC client target to bucket name. func (sys *NotificationSys) AddRemoteTarget(bucketName string, target event.Target, rulesMap event.RulesMap) error { if err := sys.targetList.Add(target); err != nil { @@ -684,7 +698,7 @@ func (sys *NotificationSys) initListeners(ctx context.Context, objAPI ObjectLaye // Construct path to listener.json for the given bucket. configFile := path.Join(bucketConfigPrefix, bucketName, bucketListenerConfig) - transactionConfigFile := configFile + ".transaction" + transactionConfigFile := configFile + "/transaction.lock" // As object layer's GetObject() and PutObject() take respective lock on minioMetaBucket // and configFile, take a transaction lock to avoid data race between readConfig() @@ -1098,22 +1112,14 @@ func (sys *NotificationSys) NetworkInfo() []madmin.ServerNetworkHardwareInfo { } // NewNotificationSys - creates new notification system object. -func NewNotificationSys(cfg config.Config, endpoints EndpointList) (*NotificationSys, error) { - targetList, err := notify.GetNotificationTargets(cfg, GlobalServiceDoneCh, globalRootCAs) - if err != nil { - return nil, config.Errorf("Unable to start notification sub system: %s", err) - } - - remoteHosts := getRemoteHosts(endpoints) - remoteClients := getRestClients(remoteHosts) - +func NewNotificationSys(endpoints EndpointList) *NotificationSys { // bucketRulesMap/bucketRemoteTargetRulesMap are initialized by NotificationSys.Init() return &NotificationSys{ - targetList: targetList, + targetList: event.NewTargetList(), bucketRulesMap: make(map[string]event.RulesMap), bucketRemoteTargetRulesMap: make(map[string]map[event.TargetID]event.RulesMap), - peerClients: remoteClients, - }, nil + peerClients: getRestClients(endpoints), + } } type eventArgs struct { @@ -1264,7 +1270,7 @@ func SaveListener(objAPI ObjectLayer, bucketName string, eventNames []event.Name // Construct path to listener.json for the given bucket. configFile := path.Join(bucketConfigPrefix, bucketName, bucketListenerConfig) - transactionConfigFile := configFile + ".transaction" + transactionConfigFile := configFile + "/transaction.lock" // As object layer's GetObject() and PutObject() take respective lock on minioMetaBucket // and configFile, take a transaction lock to avoid data race between readConfig() @@ -1315,7 +1321,7 @@ func RemoveListener(objAPI ObjectLayer, bucketName string, targetID event.Target // Construct path to listener.json for the given bucket. configFile := path.Join(bucketConfigPrefix, bucketName, bucketListenerConfig) - transactionConfigFile := configFile + ".transaction" + transactionConfigFile := configFile + "/transaction.lock" // As object layer's GetObject() and PutObject() take respective lock on minioMetaBucket // and configFile, take a transaction lock to avoid data race between readConfig() diff --git a/cmd/peer-rest-client.go b/cmd/peer-rest-client.go index 6dc2b9a0e..3ad9b528d 100644 --- a/cmd/peer-rest-client.go +++ b/cmd/peer-rest-client.go @@ -693,19 +693,24 @@ func getRemoteHosts(endpoints EndpointList) []*xnet.Host { var remoteHosts []*xnet.Host for _, hostStr := range GetRemotePeers(endpoints) { host, err := xnet.ParseHost(hostStr) - logger.FatalIf(err, "Unable to parse peer Host") + if err != nil { + logger.LogIf(context.Background(), err) + continue + } remoteHosts = append(remoteHosts, host) } return remoteHosts } -func getRestClients(peerHosts []*xnet.Host) []*peerRESTClient { +func getRestClients(endpoints EndpointList) []*peerRESTClient { + peerHosts := getRemoteHosts(endpoints) restClients := make([]*peerRESTClient, len(peerHosts)) for i, host := range peerHosts { client, err := newPeerRESTClient(host) if err != nil { logger.LogIf(context.Background(), err) + continue } restClients[i] = client } diff --git a/cmd/peer-rest-server.go b/cmd/peer-rest-server.go index 5d0981951..560b1218c 100644 --- a/cmd/peer-rest-server.go +++ b/cmd/peer-rest-server.go @@ -42,12 +42,8 @@ type peerRESTServer struct { } func getServerInfo() (*ServerInfoData, error) { - if globalBootTime.IsZero() { - return nil, errServerNotInitialized - } - - objLayer := globalObjectAPI - if objLayer == nil || globalSafeMode { + objLayer := newObjectLayerWithoutSafeModeFn() + if objLayer == nil { return nil, errServerNotInitialized } @@ -166,7 +162,7 @@ func (s *peerRESTServer) DeletePolicyHandler(w http.ResponseWriter, r *http.Requ return } - objAPI := globalObjectAPI + objAPI := newObjectLayerWithoutSafeModeFn() if objAPI == nil { s.writeErrorResponse(w, errServerNotInitialized) return @@ -194,7 +190,7 @@ func (s *peerRESTServer) LoadPolicyHandler(w http.ResponseWriter, r *http.Reques return } - objAPI := globalObjectAPI + objAPI := newObjectLayerWithoutSafeModeFn() if objAPI == nil { s.writeErrorResponse(w, errServerNotInitialized) return @@ -222,7 +218,7 @@ func (s *peerRESTServer) LoadPolicyMappingHandler(w http.ResponseWriter, r *http return } - objAPI := globalObjectAPI + objAPI := newObjectLayerWithoutSafeModeFn() if objAPI == nil { s.writeErrorResponse(w, errServerNotInitialized) return @@ -251,7 +247,7 @@ func (s *peerRESTServer) DeleteUserHandler(w http.ResponseWriter, r *http.Reques return } - objAPI := globalObjectAPI + objAPI := newObjectLayerWithoutSafeModeFn() if objAPI == nil { s.writeErrorResponse(w, errServerNotInitialized) return @@ -279,7 +275,7 @@ func (s *peerRESTServer) LoadUserHandler(w http.ResponseWriter, r *http.Request) return } - objAPI := globalObjectAPI + objAPI := newObjectLayerWithoutSafeModeFn() if objAPI == nil { s.writeErrorResponse(w, errServerNotInitialized) return @@ -329,7 +325,7 @@ func (s *peerRESTServer) LoadGroupHandler(w http.ResponseWriter, r *http.Request return } - objAPI := globalObjectAPI + objAPI := newObjectLayerWithoutSafeModeFn() if objAPI == nil { s.writeErrorResponse(w, errServerNotInitialized) return @@ -539,7 +535,7 @@ func (s *peerRESTServer) ReloadFormatHandler(w http.ResponseWriter, r *http.Requ return } - objAPI := globalObjectAPI + objAPI := newObjectLayerWithoutSafeModeFn() if objAPI == nil { s.writeErrorResponse(w, errServerNotInitialized) return diff --git a/cmd/policy.go b/cmd/policy.go index 89c987d1c..7e1e5ba7c 100644 --- a/cmd/policy.go +++ b/cmd/policy.go @@ -69,7 +69,7 @@ func (sys *PolicySys) IsAllowed(args policy.Args) bool { if globalIsGateway { // When gateway is enabled, no cached value // is used to validate bucket policies. - objAPI := globalObjectAPI + objAPI := newObjectLayerFn() if objAPI != nil { config, err := objAPI.GetBucketPolicy(context.Background(), args.BucketName) if err == nil { diff --git a/cmd/server-main.go b/cmd/server-main.go index 5d9309033..5fcfa8b29 100644 --- a/cmd/server-main.go +++ b/cmd/server-main.go @@ -180,53 +180,99 @@ func serverHandleEnvVars() { handleCommonEnvVars() } -func initAllSubsystems(newObject ObjectLayer) { +func newAllSubsystems() { + // Create new notification system and initialize notification targets + globalNotificationSys = NewNotificationSys(globalEndpoints) + // Create a new config system. globalConfigSys = NewConfigSys() - // Initialize config system. - if err := globalConfigSys.Init(newObject); err != nil { - logger.Fatal(err, "Unable to initialize config system") - } - // Create new IAM system. globalIAMSys = NewIAMSys() - if err := globalIAMSys.Init(newObject); err != nil { - logger.Fatal(err, "Unable to initialize IAM system") + // Create new policy system. + globalPolicySys = NewPolicySys() + + // Create new lifecycle system. + globalLifecycleSys = NewLifecycleSys() +} + +func initSafeModeInit(buckets []BucketInfo) (err error) { + defer func() { + if err != nil { + // Enable logger + logger.Disable = false + + // Prints the formatted startup message in safe mode operation. + printStartupSafeModeMessage(getAPIEndpoints(), err) + + // Initialization returned error reaching safe mode and + // not proceeding waiting for admin action. + handleSignals() + } + }() + + newObject := newObjectLayerWithoutSafeModeFn() + + // Calls New() for all sub-systems. + newAllSubsystems() + + // Migrate all backend configs to encrypted backend, also handles rotation as well. + if err = handleEncryptedConfigBackend(newObject, true); err != nil { + return fmt.Errorf("Unable to handle encrypted backend for config, iam and policies: %v", err) } - buckets, err := newObject.ListBuckets(context.Background()) - if err != nil { - logger.Fatal(err, "Unable to list buckets") + // **** WARNING **** + // Migrating to encrypted backend should happen before initialization of any + // sub-systems, make sure that we do not move the above codeblock elsewhere. + + // Validate and initialize all subsystems. + if err = initAllSubsystems(buckets, newObject); err != nil { + return err } - // Create new notification system and initialize notification targets - globalNotificationSys, err = NewNotificationSys(globalServerConfig, globalEndpoints) - if err != nil { - logger.Fatal(err, "Unable to initialize notification targets") + if globalEtcdClient != nil { + // **** WARNING **** + // Migrating to encrypted backend on etcd should happen before initialization of + // IAM sub-systems, make sure that we do not move the above codeblock elsewhere. + if err = migrateIAMConfigsEtcdToEncrypted(globalEtcdClient); err != nil { + return fmt.Errorf("Unable to handle encrypted backend for iam and policies: %v", err) + } + } + + return nil +} + +func initAllSubsystems(buckets []BucketInfo, newObject ObjectLayer) (err error) { + // Initialize config system. + if err = globalConfigSys.Init(newObject); err != nil { + return fmt.Errorf("Unable to initialize config system: %v", err) + } + + if err = globalNotificationSys.AddNotificationTargetsFromConfig(globalServerConfig); err != nil { + return fmt.Errorf("Unable to initialize notification target(s) from config: %v", err) + } + + if err = globalIAMSys.Init(newObject); err != nil { + return fmt.Errorf("Unable to initialize IAM system: %v", err) } // Initialize notification system. if err = globalNotificationSys.Init(buckets, newObject); err != nil { - logger.Fatal(err, "Unable to initialize notification system") + return fmt.Errorf("Unable to initialize notification system: %v", err) } - // Create new policy system. - globalPolicySys = NewPolicySys() - // Initialize policy system. if err = globalPolicySys.Init(buckets, newObject); err != nil { - logger.Fatal(err, "Unable to initialize policy system") + return fmt.Errorf("Unable to initialize policy system; %v", err) } - // Create new lifecycle system. - globalLifecycleSys = NewLifecycleSys() - // Initialize lifecycle system. if err = globalLifecycleSys.Init(buckets, newObject); err != nil { - logger.Fatal(err, "Unable to initialize lifecycle system") + return fmt.Errorf("Unable to initialize lifecycle system: %v", err) } + + return nil } // serverMain handler called for 'minio server' command. @@ -327,42 +373,33 @@ func serverMain(ctx *cli.Context) { globalTLSCerts.Stop() globalHTTPServer.Shutdown() - logger.FatalIf(err, "Unable to initialize backend") - } - - // Populate existing buckets to the etcd backend - if globalDNSConfig != nil { - initFederatorBackend(newObject) + logger.Fatal(err, "Unable to initialize backend") } // Re-enable logging logger.Disable = false - // Migrate all backend configs to encrypted backend, also handles rotation as well. - logger.FatalIf(handleEncryptedConfigBackend(newObject, true), - "Unable to handle encrypted backend for config, iam and policies") + // Once endpoints are finalized, initialize the new object api in safe mode. + globalObjLayerMutex.Lock() + globalSafeMode = true + globalObjectAPI = newObject + globalObjLayerMutex.Unlock() - // **** WARNING **** - // Migrating to encrypted backend should happen before initialization of any - // sub-systems, make sure that we do not move the above codeblock elsewhere. - - // Validate and initialize all subsystems. - initAllSubsystems(newObject) - - if globalEtcdClient != nil { - // **** WARNING **** - // Migrating to encrypted backend on etcd should happen before initialization of - // IAM sub-systems, make sure that we do not move the above codeblock elsewhere. - logger.FatalIf(migrateIAMConfigsEtcdToEncrypted(globalEtcdClient), - "Unable to handle encrypted backend for iam and policies") + buckets, err := newObject.ListBuckets(context.Background()) + if err != nil { + logger.Fatal(err, "Unable to list buckets") } - if globalCacheConfig.Enabled { - logger.StartupMessage(color.Red(color.Bold("Disk caching is recommended only for gateway deployments"))) + // Populate existing buckets to the etcd backend + if globalDNSConfig != nil { + initFederatorBackend(buckets, newObject) + } - // initialize the new disk cache objects. - globalCacheObjectAPI, err = newServerCacheObjects(context.Background(), globalCacheConfig) - logger.FatalIf(err, "Unable to initialize disk caching") + initSafeModeInit(buckets) + + if globalCacheConfig.Enabled { + msg := color.RedBold("Disk caching is disabled in 'server' mode, 'caching' is only supported in gateway deployments") + logger.StartupMessage(msg) } initDailyLifecycle() @@ -373,14 +410,17 @@ func serverMain(ctx *cli.Context) { initGlobalHeal() } - globalObjectAPI = newObject + // Disable safe mode operation, after all initialization is over. + globalObjLayerMutex.Lock() + globalSafeMode = false + globalObjLayerMutex.Unlock() // Prints the formatted startup message once object layer is initialized. printStartupMessage(getAPIEndpoints()) if globalActiveCred.Equal(auth.DefaultCredentials) { msg := fmt.Sprintf("Detected default credentials '%s', please change the credentials immediately using 'MINIO_ACCESS_KEY' and 'MINIO_SECRET_KEY'", globalActiveCred) - logger.StartupMessage(color.Red(color.Bold(msg))) + logger.StartupMessage(color.RedBold(msg)) } // Set uptime time after object layer has initialized. diff --git a/cmd/server-startup-msg.go b/cmd/server-startup-msg.go index b2f813cfe..7b7b5dcd9 100644 --- a/cmd/server-startup-msg.go +++ b/cmd/server-startup-msg.go @@ -31,12 +31,13 @@ import ( // Documentation links, these are part of message printing code. const ( - mcQuickStartGuide = "https://docs.min.io/docs/minio-client-quickstart-guide" - goQuickStartGuide = "https://docs.min.io/docs/golang-client-quickstart-guide" - jsQuickStartGuide = "https://docs.min.io/docs/javascript-client-quickstart-guide" - javaQuickStartGuide = "https://docs.min.io/docs/java-client-quickstart-guide" - pyQuickStartGuide = "https://docs.min.io/docs/python-client-quickstart-guide" - dotnetQuickStartGuide = "https://docs.min.io/docs/dotnet-client-quickstart-guide" + mcQuickStartGuide = "https://docs.min.io/docs/minio-client-quickstart-guide" + mcAdminQuickStartGuide = "https://docs.min.io/docs/minio-admin-complete-guide.html" + goQuickStartGuide = "https://docs.min.io/docs/golang-client-quickstart-guide" + jsQuickStartGuide = "https://docs.min.io/docs/javascript-client-quickstart-guide" + javaQuickStartGuide = "https://docs.min.io/docs/java-client-quickstart-guide" + pyQuickStartGuide = "https://docs.min.io/docs/python-client-quickstart-guide" + dotnetQuickStartGuide = "https://docs.min.io/docs/dotnet-client-quickstart-guide" ) // generates format string depending on the string length and padding. @@ -45,16 +46,75 @@ func getFormatStr(strLen int, padding int) string { return "%" + formatStr } -// Prints the formatted startup message. -func printStartupMessage(apiEndPoints []string) { +func printStartupSafeModeMessage(apiEndpoints []string, err error) { + logStartupMessage(color.RedBold("Server startup failed with '%v'", err)) + logStartupMessage(color.RedBold("Server switching to safe mode")) + logStartupMessage(color.RedBold("Please use 'mc admin' commands to fix this issue")) - strippedAPIEndpoints := stripStandardPorts(apiEndPoints) - // If cache layer is enabled, print cache capacity. - if globalCacheObjectAPI != nil { - printCacheStorageInfo(globalCacheObjectAPI.StorageInfo(context.Background())) + // Object layer is initialized then print StorageInfo in safe mode. + objAPI := newObjectLayerWithoutSafeModeFn() + if objAPI != nil { + if msg := getStorageInfoMsgSafeMode(objAPI.StorageInfo(context.Background())); msg != "" { + logStartupMessage(msg) + } } + + // Get saved credentials. + cred := globalActiveCred + + // Get saved region. + region := globalServerRegion + + strippedAPIEndpoints := stripStandardPorts(apiEndpoints) + + apiEndpointStr := strings.Join(strippedAPIEndpoints, " ") + + // Colorize the message and print. + logStartupMessage(color.Red("Endpoint: ") + color.Bold(fmt.Sprintf(getFormatStr(len(apiEndpointStr), 1), apiEndpointStr))) + if color.IsTerminal() && !globalCLIContext.Anonymous { + logStartupMessage(color.Red("AccessKey: ") + color.Bold(fmt.Sprintf("%s ", cred.AccessKey))) + logStartupMessage(color.Red("SecretKey: ") + color.Bold(fmt.Sprintf("%s ", cred.SecretKey))) + if region != "" { + logStartupMessage(color.Red("Region: ") + color.Bold(fmt.Sprintf(getFormatStr(len(region), 3), region))) + } + } + + // Prints `mc` cli configuration message chooses + // first endpoint as default. + alias := "myminio" + endPoint := strippedAPIEndpoints[0] + + // Configure 'mc', following block prints platform specific information for minio client admin commands. + if color.IsTerminal() { + logStartupMessage(color.RedBold("\nCommand-line Access: ") + mcAdminQuickStartGuide) + if runtime.GOOS == globalWindowsOSName { + mcMessage := fmt.Sprintf("> mc.exe config host add %s %s %s %s --api s3v4", alias, + endPoint, cred.AccessKey, cred.SecretKey) + logStartupMessage(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage)) + mcMessage = fmt.Sprintf("> mc.exe admin --help") + logStartupMessage(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage)) + } else { + mcMessage := fmt.Sprintf("$ mc config host add %s %s %s %s --api s3v4", alias, + endPoint, cred.AccessKey, cred.SecretKey) + logStartupMessage(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage)) + mcMessage = fmt.Sprintf("$ mc admin --help") + logStartupMessage(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage)) + } + } +} + +// Prints the formatted startup message. +func printStartupMessage(apiEndpoints []string) { + + strippedAPIEndpoints := stripStandardPorts(apiEndpoints) + // If cache layer is enabled, print cache capacity. + cachedObjAPI := newCachedObjectLayerFn() + if cachedObjAPI != nil { + printCacheStorageInfo(cachedObjAPI.StorageInfo(context.Background())) + } + // Object layer is initialized then print StorageInfo. - objAPI := globalObjectAPI + objAPI := newObjectLayerFn() if objAPI != nil { printStorageInfo(objAPI.StorageInfo(context.Background())) } @@ -183,6 +243,16 @@ func printObjectAPIMsg() { logStartupMessage(color.Blue(" .NET: ") + fmt.Sprintf(getFormatStr(len(dotnetQuickStartGuide), 6), dotnetQuickStartGuide)) } +// Get formatted disk/storage info message. +func getStorageInfoMsgSafeMode(storageInfo StorageInfo) string { + var msg string + if storageInfo.Backend.Type == BackendErasure { + diskInfo := fmt.Sprintf(" %d Online, %d Offline. ", storageInfo.Backend.OnlineDisks.Sum(), storageInfo.Backend.OfflineDisks.Sum()) + msg += color.Red("Status:") + fmt.Sprintf(getFormatStr(len(diskInfo), 8), diskInfo) + } + return msg +} + // Get formatted disk/storage info message. func getStorageInfoMsg(storageInfo StorageInfo) string { var msg string diff --git a/cmd/signals.go b/cmd/signals.go index d5eb72600..6107e6286 100644 --- a/cmd/signals.go +++ b/cmd/signals.go @@ -55,7 +55,7 @@ func handleSignals() { // send signal to various go-routines that they need to quit. close(GlobalServiceDoneCh) - if objAPI := globalObjectAPI; objAPI != nil { + if objAPI := newObjectLayerWithoutSafeModeFn(); objAPI != nil { oerr = objAPI.Shutdown(context.Background()) logger.LogIf(context.Background(), oerr) } @@ -66,7 +66,7 @@ func handleSignals() { for { select { case err := <-globalHTTPServerErrorCh: - if objAPI := globalObjectAPI; objAPI != nil { + if objAPI := newObjectLayerWithoutSafeModeFn(); objAPI != nil { objAPI.Shutdown(context.Background()) } if err != nil { diff --git a/cmd/test-utils_test.go b/cmd/test-utils_test.go index 8ac50a77c..aa5efc07e 100644 --- a/cmd/test-utils_test.go +++ b/cmd/test-utils_test.go @@ -370,11 +370,7 @@ func UnstartedTestServer(t TestErrHandler, instanceType string) TestServer { globalPolicySys = NewPolicySys() globalPolicySys.Init(buckets, objLayer) - globalNotificationSys, err = NewNotificationSys(globalServerConfig, testServer.Disks) - if err != nil { - t.Fatalf("Unable to initialize notification system %s", err) - } - + globalNotificationSys = NewNotificationSys(testServer.Disks) globalNotificationSys.Init(buckets, objLayer) globalLifecycleSys = NewLifecycleSys() @@ -1640,10 +1636,10 @@ func newTestObjectLayer(endpoints EndpointList) (newObject ObjectLayer, err erro globalIAMSys.Init(xl) globalPolicySys = NewPolicySys() - globalNotificationSys, err = NewNotificationSys(globalServerConfig, endpoints) - if err != nil { - return xl, err - } + globalPolicySys.Init(nil, xl) + + globalNotificationSys = NewNotificationSys(endpoints) + globalNotificationSys.Init(nil, xl) return xl, nil } diff --git a/cmd/web-router.go b/cmd/web-router.go index f8362ce16..6e94246c4 100644 --- a/cmd/web-router.go +++ b/cmd/web-router.go @@ -62,18 +62,8 @@ const specialAssets = "index_bundle.*.js|loader.css|logo.svg|firefox.png|safari. func registerWebRouter(router *mux.Router) error { // Initialize Web. web := &webAPIHandlers{ - ObjectAPI: func() ObjectLayer { - if !globalSafeMode { - return globalObjectAPI - } - return nil - }, - CacheAPI: func() CacheObjectLayer { - if !globalSafeMode { - return globalCacheObjectAPI - } - return nil - }, + ObjectAPI: newObjectLayerFn, + CacheAPI: newCachedObjectLayerFn, } // Initialize a new json2 codec. diff --git a/cmd/xl-v1-multipart.go b/cmd/xl-v1-multipart.go index f24603454..2ba3e8b14 100644 --- a/cmd/xl-v1-multipart.go +++ b/cmd/xl-v1-multipart.go @@ -839,12 +839,12 @@ func (xl xlObjects) AbortMultipartUpload(ctx context.Context, bucket, object, up // get Quorum for this object _, writeQuorum, err := objectQuorumFromMeta(ctx, xl, partsMetadata, errs) if err != nil { - return toObjectErr(err, bucket, object) + return toObjectErr(err, bucket, object, uploadID) } // Cleanup all uploaded parts. if err = xl.deleteObject(ctx, minioMetaMultipartBucket, uploadIDPath, writeQuorum, false); err != nil { - return toObjectErr(err, bucket, object) + return toObjectErr(err, bucket, object, uploadID) } // Successfully purged. diff --git a/cmd/xl-v1-utils.go b/cmd/xl-v1-utils.go index 5e20a407d..511f1455c 100644 --- a/cmd/xl-v1-utils.go +++ b/cmd/xl-v1-utils.go @@ -18,11 +18,11 @@ package cmd import ( "context" + "encoding/json" "errors" "hash/crc32" "path" - jsoniter "github.com/json-iterator/go" "github.com/minio/minio/cmd/logger" "github.com/minio/minio/pkg/sync/errgroup" ) @@ -117,7 +117,6 @@ func hashOrder(key string, cardinality int) []int { // Constructs xlMetaV1 using `jsoniter` lib. func xlMetaV1UnmarshalJSON(ctx context.Context, xlMetaBuf []byte) (xlMeta xlMetaV1, err error) { - var json = jsoniter.ConfigCompatibleWithStandardLibrary err = json.Unmarshal(xlMetaBuf, &xlMeta) return xlMeta, err } diff --git a/docs/config/README.md b/docs/config/README.md index 939ee3472..fb7d18c9c 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -54,7 +54,9 @@ export MINIO_SECRET_KEY_OLD=minio123 minio server /data ``` -Once the migration is complete and server has started successfully remove `MINIO_ACCESS_KEY_OLD` and `MINIO_SECRET_KEY_OLD` environment variables, restart the server. +Once the migration is complete, server will automatically unset the `MINIO_ACCESS_KEY_OLD` and `MINIO_SECRET_KEY_OLD` with in the process namespace. + +> **NOTE: Make sure to remove `MINIO_ACCESS_KEY_OLD` and `MINIO_SECRET_KEY_OLD` in scripts or service files before next service restarts of the server to avoid double encryption of your existing contents.** #### Region | Field | Type | Description | diff --git a/pkg/color/color.go b/pkg/color/color.go index 94a2a8ba9..f5353a91c 100644 --- a/pkg/color/color.go +++ b/pkg/color/color.go @@ -37,6 +37,13 @@ var ( return fmt.Sprint }() + RedBold = func() func(format string, a ...interface{}) string { + if IsTerminal() { + return color.New(color.FgRed, color.Bold).SprintfFunc() + } + return fmt.Sprintf + }() + Red = func() func(format string, a ...interface{}) string { if IsTerminal() { return color.New(color.FgRed).SprintfFunc() diff --git a/pkg/event/targetlist.go b/pkg/event/targetlist.go index c2082c386..2912ba199 100644 --- a/pkg/event/targetlist.go +++ b/pkg/event/targetlist.go @@ -36,15 +36,17 @@ type TargetList struct { } // Add - adds unique target to target list. -func (list *TargetList) Add(target Target) error { +func (list *TargetList) Add(targets ...Target) error { list.Lock() defer list.Unlock() - if _, ok := list.targets[target.ID()]; ok { - return fmt.Errorf("target %v already exists", target.ID()) + for _, target := range targets { + if _, ok := list.targets[target.ID()]; ok { + return fmt.Errorf("target %v already exists", target.ID()) + } + list.targets[target.ID()] = target } - list.targets[target.ID()] = target return nil } @@ -102,6 +104,19 @@ func (list *TargetList) Remove(targetids ...TargetID) <-chan TargetIDErr { return errCh } +// Targets - list all targets +func (list *TargetList) Targets() []Target { + list.RLock() + defer list.RUnlock() + + targets := []Target{} + for _, tgt := range list.targets { + targets = append(targets, tgt) + } + + return targets +} + // List - returns available target IDs. func (list *TargetList) List() []TargetID { list.RLock()