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 }