diff --git a/cmd/admin-handlers-config-kv.go b/cmd/admin-handlers-config-kv.go index 5b6e7d499..8afe3a536 100644 --- a/cmd/admin-handlers-config-kv.go +++ b/cmd/admin-handlers-config-kv.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2021 MinIO, Inc. +// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // @@ -117,6 +117,26 @@ func applyDynamic(ctx context.Context, objectAPI ObjectLayer, cfg config.Config, w.Header().Set(madmin.ConfigAppliedHeader, madmin.ConfigAppliedTrue) } +type badConfigErr struct { + Err error +} + +// Error - return the error message +func (bce badConfigErr) Error() string { + return bce.Err.Error() +} + +// Unwrap the error to its underlying error. +func (bce badConfigErr) Unwrap() error { + return bce.Err +} + +type setConfigResult struct { + Cfg config.Config + SubSys string + Dynamic bool +} + // SetConfigKVHandler - PUT /minio/admin/v3/set-config-kv func (a adminAPIHandlers) SetConfigKVHandler(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "SetConfigKV") @@ -142,46 +162,53 @@ func (a adminAPIHandlers) SetConfigKVHandler(w http.ResponseWriter, r *http.Requ return } - cfg, err := readServerConfig(ctx, objectAPI, nil) + result, err := setConfigKV(ctx, objectAPI, kvBytes) if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + switch err.(type) { + case badConfigErr: + writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL) + default: + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + } return } - dynamic, err := cfg.ReadConfig(bytes.NewReader(kvBytes)) + if result.Dynamic { + applyDynamic(ctx, objectAPI, result.Cfg, result.SubSys, r, w) + } + + writeSuccessResponseHeadersOnly(w) +} + +func setConfigKV(ctx context.Context, objectAPI ObjectLayer, kvBytes []byte) (result setConfigResult, err error) { + result.Cfg, err = readServerConfig(ctx, objectAPI, nil) if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } - subSys, _, _, err := config.GetSubSys(string(kvBytes)) + result.Dynamic, err = result.Cfg.ReadConfig(bytes.NewReader(kvBytes)) if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } - if err = validateConfig(cfg, subSys); err != nil { - writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL) + result.SubSys, _, _, err = config.GetSubSys(string(kvBytes)) + if err != nil { + return + } + + if verr := validateConfig(result.Cfg, result.SubSys); verr != nil { + err = badConfigErr{Err: verr} return } // Update the actual server config on disk. - if err = saveServerConfig(ctx, objectAPI, cfg); err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + if err = saveServerConfig(ctx, objectAPI, result.Cfg); err != nil { return } - // Write to the config input KV to history. - if err = saveServerConfigHistory(ctx, objectAPI, kvBytes); err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - if dynamic { - applyDynamic(ctx, objectAPI, cfg, subSys, r, w) - } - - writeSuccessResponseHeadersOnly(w) + // Write the config input KV to history. + err = saveServerConfigHistory(ctx, objectAPI, kvBytes) + return } // GetConfigKVHandler - GET /minio/admin/v3/get-config-kv?key={key} diff --git a/cmd/license-update.go b/cmd/license-update.go new file mode 100644 index 000000000..b97b6c445 --- /dev/null +++ b/cmd/license-update.go @@ -0,0 +1,122 @@ +// Copyright (c) 2015-2023 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "math/rand" + "time" + + "github.com/minio/minio/internal/logger" + "github.com/tidwall/gjson" +) + +const ( + licUpdateCycle = 24 * time.Hour * 30 + licRenewURL = "https://subnet.min.io/api/cluster/renew-license" + licRenewURLDev = "http://localhost:9000/api/cluster/renew-license" +) + +// initlicenseUpdateJob start the periodic license update job in the background. +func initLicenseUpdateJob(ctx context.Context, objAPI ObjectLayer) { + go func() { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + // Leader node (that successfully acquires the lock inside licenceUpdaterLoop) + // will keep performing the license update. If the leader goes down for some + // reason, the lock will be released and another node will acquire it and + // take over because of this loop. + for { + licenceUpdaterLoop(ctx, objAPI) + + // license update stopped for some reason. + // sleep for some time and try again. + duration := time.Duration(r.Float64() * float64(time.Hour)) + if duration < time.Second { + // Make sure to sleep atleast a second to avoid high CPU ticks. + duration = time.Second + } + time.Sleep(duration) + } + }() +} + +func licenceUpdaterLoop(ctx context.Context, objAPI ObjectLayer) { + ctx, cancel := globalLeaderLock.GetLock(ctx) + defer cancel() + + licenseUpdateTimer := time.NewTimer(licUpdateCycle) + defer licenseUpdateTimer.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-licenseUpdateTimer.C: + + if globalSubnetConfig.Registered() { + performLicenseUpdate(ctx, objAPI) + } + + // Reset the timer for next cycle. + licenseUpdateTimer.Reset(licUpdateCycle) + } + } +} + +func performLicenseUpdate(ctx context.Context, objectAPI ObjectLayer) { + // the subnet license renewal api renews the license only + // if required e.g. when it is expiring soon + url := licRenewURL + if globalIsCICD { + url = licRenewURLDev + } + + resp, err := globalSubnetConfig.Post(url, nil) + if err != nil { + logger.LogIf(ctx, fmt.Errorf("error from %s: %w", url, err)) + return + } + + r := gjson.Parse(resp).Get("license") + if r.Index == 0 { + logger.LogIf(ctx, fmt.Errorf("license not found in response from %s", url)) + return + } + + lic := r.String() + if lic == globalSubnetConfig.License { + // license hasn't changed. + return + } + + kv := "subnet license=" + lic + result, err := setConfigKV(ctx, objectAPI, []byte(kv)) + if err != nil { + logger.LogIf(ctx, fmt.Errorf("error setting subnet license config: %w", err)) + return + } + + if result.Dynamic { + if err := applyDynamicConfigForSubSys(GlobalContext, objectAPI, result.Cfg, result.SubSys); err != nil { + logger.LogIf(ctx, fmt.Errorf("error applying subnet dynamic config: %w", err)) + return + } + globalNotificationSys.SignalConfigReload(result.SubSys) + } +} diff --git a/cmd/server-main.go b/cmd/server-main.go index e7ac1d1f5..ca24809db 100644 --- a/cmd/server-main.go +++ b/cmd/server-main.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2022 MinIO, Inc. +// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // @@ -720,6 +720,9 @@ func serverMain(ctx *cli.Context) { setCacheObjectLayer(cacheAPI) } + // Initialize the license update job + initLicenseUpdateJob(GlobalContext, newObject) + // Prints the formatted startup message, if err is not nil then it prints additional information as well. printStartupMessage(getAPIEndpoints(), err)