github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/tier.go (about)

     1  // Copyright (c) 2015-2024 MinIO, Inc
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"bytes"
    22  	"context"
    23  	"encoding/base64"
    24  	"encoding/binary"
    25  	"fmt"
    26  	"math/rand"
    27  	"net/http"
    28  	"path"
    29  	"strings"
    30  	"sync"
    31  	"time"
    32  
    33  	"github.com/minio/madmin-go/v3"
    34  	"github.com/minio/minio/internal/crypto"
    35  	"github.com/minio/minio/internal/hash"
    36  	"github.com/minio/minio/internal/kms"
    37  	"github.com/minio/minio/internal/logger"
    38  	"github.com/prometheus/client_golang/prometheus"
    39  )
    40  
    41  //go:generate msgp -file $GOFILE
    42  
    43  var (
    44  	errTierMissingCredentials = AdminError{
    45  		Code:       "XMinioAdminTierMissingCredentials",
    46  		Message:    "Specified remote credentials are empty",
    47  		StatusCode: http.StatusForbidden,
    48  	}
    49  
    50  	errTierBackendInUse = AdminError{
    51  		Code:       "XMinioAdminTierBackendInUse",
    52  		Message:    "Specified remote tier is already in use",
    53  		StatusCode: http.StatusConflict,
    54  	}
    55  
    56  	errTierTypeUnsupported = AdminError{
    57  		Code:       "XMinioAdminTierTypeUnsupported",
    58  		Message:    "Specified tier type is unsupported",
    59  		StatusCode: http.StatusBadRequest,
    60  	}
    61  
    62  	errTierBackendNotEmpty = AdminError{
    63  		Code:       "XMinioAdminTierBackendNotEmpty",
    64  		Message:    "Specified remote backend is not empty",
    65  		StatusCode: http.StatusBadRequest,
    66  	}
    67  )
    68  
    69  const (
    70  	tierConfigFile    = "tier-config.bin"
    71  	tierConfigFormat  = 1
    72  	tierConfigV1      = 1
    73  	tierConfigVersion = 2
    74  )
    75  
    76  // tierConfigPath refers to remote tier config object name
    77  var tierConfigPath = path.Join(minioConfigPrefix, tierConfigFile)
    78  
    79  const tierCfgRefreshAtHdr = "X-MinIO-TierCfg-RefreshedAt"
    80  
    81  // TierConfigMgr holds the collection of remote tiers configured in this deployment.
    82  type TierConfigMgr struct {
    83  	sync.RWMutex `msg:"-"`
    84  	drivercache  map[string]WarmBackend `msg:"-"`
    85  
    86  	Tiers           map[string]madmin.TierConfig `json:"tiers"`
    87  	lastRefreshedAt time.Time                    `msg:"-"`
    88  }
    89  
    90  type tierMetrics struct {
    91  	sync.RWMutex  // protects requestsCount only
    92  	requestsCount map[string]struct {
    93  		success int64
    94  		failure int64
    95  	}
    96  	histogram *prometheus.HistogramVec
    97  }
    98  
    99  var globalTierMetrics = tierMetrics{
   100  	requestsCount: make(map[string]struct {
   101  		success int64
   102  		failure int64
   103  	}),
   104  	histogram: prometheus.NewHistogramVec(prometheus.HistogramOpts{
   105  		Name:    "tier_ttlb_seconds",
   106  		Help:    "Time taken by requests served by warm tier",
   107  		Buckets: []float64{0.01, 0.1, 1, 2, 5, 10, 60, 5 * 60, 15 * 60, 30 * 60},
   108  	}, []string{"tier"}),
   109  }
   110  
   111  func (t *tierMetrics) Observe(tier string, dur time.Duration) {
   112  	t.histogram.With(prometheus.Labels{"tier": tier}).Observe(dur.Seconds())
   113  }
   114  
   115  func (t *tierMetrics) logSuccess(tier string) {
   116  	t.Lock()
   117  	defer t.Unlock()
   118  
   119  	stat := t.requestsCount[tier]
   120  	stat.success++
   121  	t.requestsCount[tier] = stat
   122  }
   123  
   124  func (t *tierMetrics) logFailure(tier string) {
   125  	t.Lock()
   126  	defer t.Unlock()
   127  
   128  	stat := t.requestsCount[tier]
   129  	stat.failure++
   130  	t.requestsCount[tier] = stat
   131  }
   132  
   133  var (
   134  	// {minio_node}_{tier}_{ttlb_seconds_distribution}
   135  	tierTTLBMD = MetricDescription{
   136  		Namespace: nodeMetricNamespace,
   137  		Subsystem: tierSubsystem,
   138  		Name:      ttlbDistribution,
   139  		Help:      "Distribution of time to last byte for objects downloaded from warm tier",
   140  		Type:      gaugeMetric,
   141  	}
   142  
   143  	// {minio_node}_{tier}_{requests_success}
   144  	tierRequestsSuccessMD = MetricDescription{
   145  		Namespace: nodeMetricNamespace,
   146  		Subsystem: tierSubsystem,
   147  		Name:      tierRequestsSuccess,
   148  		Help:      "Number of requests to download object from warm tier that were successful",
   149  		Type:      counterMetric,
   150  	}
   151  	// {minio_node}_{tier}_{requests_failure}
   152  	tierRequestsFailureMD = MetricDescription{
   153  		Namespace: nodeMetricNamespace,
   154  		Subsystem: tierSubsystem,
   155  		Name:      tierRequestsFailure,
   156  		Help:      "Number of requests to download object from warm tier that failed",
   157  		Type:      counterMetric,
   158  	}
   159  )
   160  
   161  func (t *tierMetrics) Report() []MetricV2 {
   162  	metrics := getHistogramMetrics(t.histogram, tierTTLBMD, true)
   163  	t.RLock()
   164  	defer t.RUnlock()
   165  	for tier, stat := range t.requestsCount {
   166  		metrics = append(metrics, MetricV2{
   167  			Description:    tierRequestsSuccessMD,
   168  			Value:          float64(stat.success),
   169  			VariableLabels: map[string]string{"tier": tier},
   170  		})
   171  		metrics = append(metrics, MetricV2{
   172  			Description:    tierRequestsFailureMD,
   173  			Value:          float64(stat.failure),
   174  			VariableLabels: map[string]string{"tier": tier},
   175  		})
   176  	}
   177  	return metrics
   178  }
   179  
   180  func (config *TierConfigMgr) refreshedAt() time.Time {
   181  	config.RLock()
   182  	defer config.RUnlock()
   183  	return config.lastRefreshedAt
   184  }
   185  
   186  // IsTierValid returns true if there exists a remote tier by name tierName,
   187  // otherwise returns false.
   188  func (config *TierConfigMgr) IsTierValid(tierName string) bool {
   189  	config.RLock()
   190  	defer config.RUnlock()
   191  	_, valid := config.isTierNameInUse(tierName)
   192  	return valid
   193  }
   194  
   195  // isTierNameInUse returns tier type and true if there exists a remote tier by
   196  // name tierName, otherwise returns madmin.Unsupported and false. N B this
   197  // function is meant for internal use, where the caller is expected to take
   198  // appropriate locks.
   199  func (config *TierConfigMgr) isTierNameInUse(tierName string) (madmin.TierType, bool) {
   200  	if t, ok := config.Tiers[tierName]; ok {
   201  		return t.Type, true
   202  	}
   203  	return madmin.Unsupported, false
   204  }
   205  
   206  // Add adds tier to config if it passes all validations.
   207  func (config *TierConfigMgr) Add(ctx context.Context, tier madmin.TierConfig, ignoreInUse bool) error {
   208  	config.Lock()
   209  	defer config.Unlock()
   210  
   211  	// check if tier name is in all caps
   212  	tierName := tier.Name
   213  	if tierName != strings.ToUpper(tierName) {
   214  		return errTierNameNotUppercase
   215  	}
   216  
   217  	// check if tier name already in use
   218  	if _, exists := config.isTierNameInUse(tierName); exists {
   219  		return errTierAlreadyExists
   220  	}
   221  
   222  	d, err := newWarmBackend(ctx, tier)
   223  	if err != nil {
   224  		return err
   225  	}
   226  
   227  	if !ignoreInUse {
   228  		// Check if warmbackend is in use by other MinIO tenants
   229  		inUse, err := d.InUse(ctx)
   230  		if err != nil {
   231  			return err
   232  		}
   233  		if inUse {
   234  			return errTierBackendInUse
   235  		}
   236  	}
   237  
   238  	config.Tiers[tierName] = tier
   239  	config.drivercache[tierName] = d
   240  
   241  	return nil
   242  }
   243  
   244  // Remove removes tier if it is empty.
   245  func (config *TierConfigMgr) Remove(ctx context.Context, tier string) error {
   246  	d, err := config.getDriver(tier)
   247  	if err != nil {
   248  		return err
   249  	}
   250  	if inuse, err := d.InUse(ctx); err != nil {
   251  		return err
   252  	} else if inuse {
   253  		return errTierBackendNotEmpty
   254  	}
   255  	config.Lock()
   256  	delete(config.Tiers, tier)
   257  	delete(config.drivercache, tier)
   258  	config.Unlock()
   259  	return nil
   260  }
   261  
   262  // Verify verifies if tier's config is valid by performing all supported
   263  // operations on the corresponding warmbackend.
   264  func (config *TierConfigMgr) Verify(ctx context.Context, tier string) error {
   265  	d, err := config.getDriver(tier)
   266  	if err != nil {
   267  		return err
   268  	}
   269  	return checkWarmBackend(ctx, d)
   270  }
   271  
   272  // Empty returns if tier targets are empty
   273  func (config *TierConfigMgr) Empty() bool {
   274  	if config == nil {
   275  		return true
   276  	}
   277  	return len(config.ListTiers()) == 0
   278  }
   279  
   280  // TierType returns the type of tier
   281  func (config *TierConfigMgr) TierType(name string) string {
   282  	config.RLock()
   283  	defer config.RUnlock()
   284  
   285  	cfg, ok := config.Tiers[name]
   286  	if !ok {
   287  		return "internal"
   288  	}
   289  	return cfg.Type.String()
   290  }
   291  
   292  // ListTiers lists remote tiers configured in this deployment.
   293  func (config *TierConfigMgr) ListTiers() []madmin.TierConfig {
   294  	if config == nil {
   295  		return nil
   296  	}
   297  
   298  	config.RLock()
   299  	defer config.RUnlock()
   300  
   301  	var tierCfgs []madmin.TierConfig
   302  	for _, tier := range config.Tiers {
   303  		// This makes a local copy of tier config before
   304  		// passing a reference to it.
   305  		tier := tier.Clone()
   306  		tierCfgs = append(tierCfgs, tier)
   307  	}
   308  	return tierCfgs
   309  }
   310  
   311  // Edit replaces the credentials of the remote tier specified by tierName with creds.
   312  func (config *TierConfigMgr) Edit(ctx context.Context, tierName string, creds madmin.TierCreds) error {
   313  	config.Lock()
   314  	defer config.Unlock()
   315  
   316  	// check if tier by this name exists
   317  	tierType, exists := config.isTierNameInUse(tierName)
   318  	if !exists {
   319  		return errTierNotFound
   320  	}
   321  
   322  	cfg := config.Tiers[tierName]
   323  	switch tierType {
   324  	case madmin.S3:
   325  		if creds.AWSRole {
   326  			cfg.S3.AWSRole = true
   327  		}
   328  		if creds.AWSRoleWebIdentityTokenFile != "" && creds.AWSRoleARN != "" {
   329  			cfg.S3.AWSRoleARN = creds.AWSRoleARN
   330  			cfg.S3.AWSRoleWebIdentityTokenFile = creds.AWSRoleWebIdentityTokenFile
   331  		}
   332  		if creds.AccessKey != "" && creds.SecretKey != "" {
   333  			cfg.S3.AccessKey = creds.AccessKey
   334  			cfg.S3.SecretKey = creds.SecretKey
   335  		}
   336  	case madmin.Azure:
   337  		if creds.SecretKey != "" {
   338  			cfg.Azure.AccountKey = creds.SecretKey
   339  		}
   340  		if creds.AzSP.TenantID != "" {
   341  			cfg.Azure.SPAuth.TenantID = creds.AzSP.TenantID
   342  		}
   343  		if creds.AzSP.ClientID != "" {
   344  			cfg.Azure.SPAuth.ClientID = creds.AzSP.ClientID
   345  		}
   346  		if creds.AzSP.ClientSecret != "" {
   347  			cfg.Azure.SPAuth.ClientSecret = creds.AzSP.ClientSecret
   348  		}
   349  	case madmin.GCS:
   350  		if creds.CredsJSON == nil {
   351  			return errTierMissingCredentials
   352  		}
   353  		cfg.GCS.Creds = base64.URLEncoding.EncodeToString(creds.CredsJSON)
   354  	case madmin.MinIO:
   355  		if creds.AccessKey == "" || creds.SecretKey == "" {
   356  			return errTierMissingCredentials
   357  		}
   358  		cfg.MinIO.AccessKey = creds.AccessKey
   359  		cfg.MinIO.SecretKey = creds.SecretKey
   360  	}
   361  
   362  	d, err := newWarmBackend(ctx, cfg)
   363  	if err != nil {
   364  		return err
   365  	}
   366  	config.Tiers[tierName] = cfg
   367  	config.drivercache[tierName] = d
   368  	return nil
   369  }
   370  
   371  // Bytes returns msgpack encoded config with format and version headers.
   372  func (config *TierConfigMgr) Bytes() ([]byte, error) {
   373  	config.RLock()
   374  	defer config.RUnlock()
   375  	data := make([]byte, 4, config.Msgsize()+4)
   376  
   377  	// Initialize the header.
   378  	binary.LittleEndian.PutUint16(data[0:2], tierConfigFormat)
   379  	binary.LittleEndian.PutUint16(data[2:4], tierConfigVersion)
   380  
   381  	// Marshal the tier config
   382  	return config.MarshalMsg(data)
   383  }
   384  
   385  // getDriver returns a warmBackend interface object initialized with remote tier config matching tierName
   386  func (config *TierConfigMgr) getDriver(tierName string) (d WarmBackend, err error) {
   387  	config.Lock()
   388  	defer config.Unlock()
   389  
   390  	var ok bool
   391  	// Lookup in-memory drivercache
   392  	d, ok = config.drivercache[tierName]
   393  	if ok {
   394  		return d, nil
   395  	}
   396  
   397  	// Initialize driver from tier config matching tierName
   398  	t, ok := config.Tiers[tierName]
   399  	if !ok {
   400  		return nil, errTierNotFound
   401  	}
   402  	d, err = newWarmBackend(context.TODO(), t)
   403  	if err != nil {
   404  		return nil, err
   405  	}
   406  	config.drivercache[tierName] = d
   407  	return d, nil
   408  }
   409  
   410  // configReader returns a PutObjReader and ObjectOptions needed to save config
   411  // using a PutObject API. PutObjReader encrypts json encoded tier configurations
   412  // if KMS is enabled, otherwise simply yields the json encoded bytes as is.
   413  // Similarly, ObjectOptions value depends on KMS' status.
   414  func (config *TierConfigMgr) configReader(ctx context.Context) (*PutObjReader, *ObjectOptions, error) {
   415  	b, err := config.Bytes()
   416  	if err != nil {
   417  		return nil, nil, err
   418  	}
   419  
   420  	payloadSize := int64(len(b))
   421  	br := bytes.NewReader(b)
   422  	hr, err := hash.NewReader(ctx, br, payloadSize, "", "", payloadSize)
   423  	if err != nil {
   424  		return nil, nil, err
   425  	}
   426  	if GlobalKMS == nil {
   427  		return NewPutObjReader(hr), &ObjectOptions{MaxParity: true}, nil
   428  	}
   429  
   430  	// Note: Local variables with names ek, oek, etc are named inline with
   431  	// acronyms defined here -
   432  	// https://github.com/minio/minio/blob/master/docs/security/README.md#acronyms
   433  
   434  	// Encrypt json encoded tier configurations
   435  	metadata := make(map[string]string)
   436  	encBr, oek, err := newEncryptReader(context.Background(), hr, crypto.S3, "", nil, minioMetaBucket, tierConfigPath, metadata, kms.Context{})
   437  	if err != nil {
   438  		return nil, nil, err
   439  	}
   440  
   441  	info := ObjectInfo{
   442  		Size: payloadSize,
   443  	}
   444  	encSize := info.EncryptedSize()
   445  	encHr, err := hash.NewReader(ctx, encBr, encSize, "", "", encSize)
   446  	if err != nil {
   447  		return nil, nil, err
   448  	}
   449  
   450  	pReader, err := NewPutObjReader(hr).WithEncryption(encHr, &oek)
   451  	if err != nil {
   452  		return nil, nil, err
   453  	}
   454  	opts := &ObjectOptions{
   455  		UserDefined: metadata,
   456  		MTime:       UTCNow(),
   457  		MaxParity:   true,
   458  	}
   459  
   460  	return pReader, opts, nil
   461  }
   462  
   463  // Reload updates config by reloading remote tier config from config store.
   464  func (config *TierConfigMgr) Reload(ctx context.Context, objAPI ObjectLayer) error {
   465  	newConfig, err := loadTierConfig(ctx, objAPI)
   466  	switch err {
   467  	case nil:
   468  		break
   469  	case errConfigNotFound: // nothing to reload
   470  		// To maintain the invariance that lastRefreshedAt records the
   471  		// timestamp of last successful refresh
   472  		config.lastRefreshedAt = UTCNow()
   473  		return nil
   474  	default:
   475  		return err
   476  	}
   477  
   478  	config.Lock()
   479  	defer config.Unlock()
   480  	// Reset drivercache built using current config
   481  	for k := range config.drivercache {
   482  		delete(config.drivercache, k)
   483  	}
   484  	// Remove existing tier configs
   485  	for k := range config.Tiers {
   486  		delete(config.Tiers, k)
   487  	}
   488  	// Copy over the new tier configs
   489  	for tier, cfg := range newConfig.Tiers {
   490  		config.Tiers[tier] = cfg
   491  	}
   492  	config.lastRefreshedAt = UTCNow()
   493  	return nil
   494  }
   495  
   496  // Save saves tier configuration onto objAPI
   497  func (config *TierConfigMgr) Save(ctx context.Context, objAPI ObjectLayer) error {
   498  	if objAPI == nil {
   499  		return errServerNotInitialized
   500  	}
   501  
   502  	pr, opts, err := globalTierConfigMgr.configReader(ctx)
   503  	if err != nil {
   504  		return err
   505  	}
   506  
   507  	_, err = objAPI.PutObject(ctx, minioMetaBucket, tierConfigPath, pr, *opts)
   508  	return err
   509  }
   510  
   511  // NewTierConfigMgr - creates new tier configuration manager,
   512  func NewTierConfigMgr() *TierConfigMgr {
   513  	return &TierConfigMgr{
   514  		drivercache: make(map[string]WarmBackend),
   515  		Tiers:       make(map[string]madmin.TierConfig),
   516  	}
   517  }
   518  
   519  func (config *TierConfigMgr) refreshTierConfig(ctx context.Context, objAPI ObjectLayer) {
   520  	const tierCfgRefresh = 15 * time.Minute
   521  	r := rand.New(rand.NewSource(time.Now().UnixNano()))
   522  	randInterval := func() time.Duration {
   523  		return time.Duration(r.Float64() * 5 * float64(time.Second))
   524  	}
   525  
   526  	// To avoid all MinIO nodes reading the tier config object at the same
   527  	// time.
   528  	t := time.NewTimer(tierCfgRefresh + randInterval())
   529  	defer t.Stop()
   530  	for {
   531  		select {
   532  		case <-ctx.Done():
   533  			return
   534  		case <-t.C:
   535  			err := config.Reload(ctx, objAPI)
   536  			if err != nil {
   537  				logger.LogIf(ctx, err)
   538  			}
   539  		}
   540  		t.Reset(tierCfgRefresh + randInterval())
   541  	}
   542  }
   543  
   544  // loadTierConfig loads remote tier configuration from objAPI.
   545  func loadTierConfig(ctx context.Context, objAPI ObjectLayer) (*TierConfigMgr, error) {
   546  	if objAPI == nil {
   547  		return nil, errServerNotInitialized
   548  	}
   549  
   550  	data, err := readConfig(ctx, objAPI, tierConfigPath)
   551  	if err != nil {
   552  		return nil, err
   553  	}
   554  
   555  	if len(data) <= 4 {
   556  		return nil, fmt.Errorf("tierConfigInit: no data")
   557  	}
   558  
   559  	// Read header
   560  	switch format := binary.LittleEndian.Uint16(data[0:2]); format {
   561  	case tierConfigFormat:
   562  	default:
   563  		return nil, fmt.Errorf("tierConfigInit: unknown format: %d", format)
   564  	}
   565  
   566  	cfg := NewTierConfigMgr()
   567  	switch version := binary.LittleEndian.Uint16(data[2:4]); version {
   568  	case tierConfigV1, tierConfigVersion:
   569  		if _, decErr := cfg.UnmarshalMsg(data[4:]); decErr != nil {
   570  			return nil, decErr
   571  		}
   572  	default:
   573  		return nil, fmt.Errorf("tierConfigInit: unknown version: %d", version)
   574  	}
   575  
   576  	return cfg, nil
   577  }
   578  
   579  // Init initializes tier configuration reading from objAPI
   580  func (config *TierConfigMgr) Init(ctx context.Context, objAPI ObjectLayer) error {
   581  	err := config.Reload(ctx, objAPI)
   582  	if globalIsDistErasure {
   583  		go config.refreshTierConfig(ctx, objAPI)
   584  	}
   585  	return err
   586  }