github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/config/config.go (about) 1 // Copyright (c) 2015-2021 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 config 19 20 import ( 21 "bufio" 22 "fmt" 23 "io" 24 "regexp" 25 "sort" 26 "strings" 27 28 "github.com/minio/madmin-go/v3" 29 "github.com/minio/minio-go/v7/pkg/set" 30 "github.com/minio/minio/internal/auth" 31 "github.com/minio/pkg/v2/env" 32 ) 33 34 // ErrorConfig holds the config error types 35 type ErrorConfig interface { 36 ErrConfigGeneric | ErrConfigNotFound 37 } 38 39 // ErrConfigGeneric is a generic config type 40 type ErrConfigGeneric struct { 41 msg string 42 } 43 44 func (ge *ErrConfigGeneric) setMsg(msg string) { 45 ge.msg = msg 46 } 47 48 func (ge ErrConfigGeneric) Error() string { 49 return ge.msg 50 } 51 52 // ErrConfigNotFound is an error to indicate 53 // that a config parameter is not found 54 type ErrConfigNotFound struct { 55 ErrConfigGeneric 56 } 57 58 // Error creates an error message and wraps 59 // it with the error type specified in the type parameter 60 func Error[T ErrorConfig, PT interface { 61 *T 62 setMsg(string) 63 }](format string, vals ...interface{}, 64 ) T { 65 pt := PT(new(T)) 66 pt.setMsg(fmt.Sprintf(format, vals...)) 67 return *pt 68 } 69 70 // Errorf formats an error and returns it as a generic config error 71 func Errorf(format string, vals ...interface{}) ErrConfigGeneric { 72 return Error[ErrConfigGeneric](format, vals...) 73 } 74 75 // Default keys 76 const ( 77 Default = madmin.Default 78 Enable = madmin.EnableKey 79 Comment = madmin.CommentKey 80 81 EnvSeparator = "=" 82 83 // Enable values 84 EnableOn = madmin.EnableOn 85 EnableOff = madmin.EnableOff 86 87 RegionKey = "region" 88 NameKey = "name" 89 RegionName = "name" 90 AccessKey = "access_key" 91 SecretKey = "secret_key" 92 License = "license" // Deprecated Dec 2021 93 APIKey = "api_key" 94 Proxy = "proxy" 95 ) 96 97 // Top level config constants. 98 const ( 99 PolicyOPASubSys = madmin.PolicyOPASubSys 100 PolicyPluginSubSys = madmin.PolicyPluginSubSys 101 IdentityOpenIDSubSys = madmin.IdentityOpenIDSubSys 102 IdentityLDAPSubSys = madmin.IdentityLDAPSubSys 103 IdentityTLSSubSys = madmin.IdentityTLSSubSys 104 IdentityPluginSubSys = madmin.IdentityPluginSubSys 105 CacheSubSys = madmin.CacheSubSys 106 SiteSubSys = madmin.SiteSubSys 107 RegionSubSys = madmin.RegionSubSys 108 EtcdSubSys = madmin.EtcdSubSys 109 StorageClassSubSys = madmin.StorageClassSubSys 110 APISubSys = madmin.APISubSys 111 CompressionSubSys = madmin.CompressionSubSys 112 LoggerWebhookSubSys = madmin.LoggerWebhookSubSys 113 AuditWebhookSubSys = madmin.AuditWebhookSubSys 114 AuditKafkaSubSys = madmin.AuditKafkaSubSys 115 HealSubSys = madmin.HealSubSys 116 ScannerSubSys = madmin.ScannerSubSys 117 CrawlerSubSys = madmin.CrawlerSubSys 118 SubnetSubSys = madmin.SubnetSubSys 119 CallhomeSubSys = madmin.CallhomeSubSys 120 DriveSubSys = madmin.DriveSubSys 121 BatchSubSys = madmin.BatchSubSys 122 BrowserSubSys = madmin.BrowserSubSys 123 ILMSubSys = madmin.ILMSubsys 124 125 // Add new constants here (similar to above) if you add new fields to config. 126 ) 127 128 // Notification config constants. 129 const ( 130 NotifyKafkaSubSys = madmin.NotifyKafkaSubSys 131 NotifyMQTTSubSys = madmin.NotifyMQTTSubSys 132 NotifyMySQLSubSys = madmin.NotifyMySQLSubSys 133 NotifyNATSSubSys = madmin.NotifyNATSSubSys 134 NotifyNSQSubSys = madmin.NotifyNSQSubSys 135 NotifyESSubSys = madmin.NotifyESSubSys 136 NotifyAMQPSubSys = madmin.NotifyAMQPSubSys 137 NotifyPostgresSubSys = madmin.NotifyPostgresSubSys 138 NotifyRedisSubSys = madmin.NotifyRedisSubSys 139 NotifyWebhookSubSys = madmin.NotifyWebhookSubSys 140 141 // Add new constants here (similar to above) if you add new fields to config. 142 ) 143 144 // Lambda config constants. 145 const ( 146 LambdaWebhookSubSys = madmin.LambdaWebhookSubSys 147 ) 148 149 // NotifySubSystems - all notification sub-systems 150 var NotifySubSystems = set.CreateStringSet( 151 NotifyKafkaSubSys, 152 NotifyMQTTSubSys, 153 NotifyMySQLSubSys, 154 NotifyNATSSubSys, 155 NotifyNSQSubSys, 156 NotifyESSubSys, 157 NotifyAMQPSubSys, 158 NotifyPostgresSubSys, 159 NotifyRedisSubSys, 160 NotifyWebhookSubSys, 161 ) 162 163 // LambdaSubSystems - all lambda sub-systems 164 var LambdaSubSystems = set.CreateStringSet( 165 LambdaWebhookSubSys, 166 ) 167 168 // LoggerSubSystems - all sub-systems related to logger 169 var LoggerSubSystems = set.CreateStringSet( 170 LoggerWebhookSubSys, 171 AuditWebhookSubSys, 172 AuditKafkaSubSys, 173 ) 174 175 // SubSystems - all supported sub-systems 176 var SubSystems = madmin.SubSystems 177 178 // SubSystemsDynamic - all sub-systems that have dynamic config. 179 var SubSystemsDynamic = set.CreateStringSet( 180 APISubSys, 181 CompressionSubSys, 182 ScannerSubSys, 183 HealSubSys, 184 SubnetSubSys, 185 CallhomeSubSys, 186 DriveSubSys, 187 LoggerWebhookSubSys, 188 AuditWebhookSubSys, 189 AuditKafkaSubSys, 190 StorageClassSubSys, 191 CacheSubSys, 192 ILMSubSys, 193 BatchSubSys, 194 BrowserSubSys, 195 ) 196 197 // SubSystemsSingleTargets - subsystems which only support single target. 198 var SubSystemsSingleTargets = set.CreateStringSet( 199 SiteSubSys, 200 RegionSubSys, 201 EtcdSubSys, 202 CacheSubSys, 203 APISubSys, 204 StorageClassSubSys, 205 CompressionSubSys, 206 PolicyOPASubSys, 207 PolicyPluginSubSys, 208 IdentityLDAPSubSys, 209 IdentityTLSSubSys, 210 IdentityPluginSubSys, 211 HealSubSys, 212 ScannerSubSys, 213 SubnetSubSys, 214 CallhomeSubSys, 215 DriveSubSys, 216 ILMSubSys, 217 BatchSubSys, 218 BrowserSubSys, 219 ) 220 221 // Constant separators 222 const ( 223 SubSystemSeparator = madmin.SubSystemSeparator 224 KvSeparator = madmin.KvSeparator 225 KvSpaceSeparator = madmin.KvSpaceSeparator 226 KvComment = madmin.KvComment 227 KvNewline = madmin.KvNewline 228 KvDoubleQuote = madmin.KvDoubleQuote 229 KvSingleQuote = madmin.KvSingleQuote 230 231 // Env prefix used for all envs in MinIO 232 EnvPrefix = madmin.EnvPrefix 233 EnvWordDelimiter = madmin.EnvWordDelimiter 234 ) 235 236 // DefaultKVS - default kvs for all sub-systems 237 var DefaultKVS = map[string]KVS{} 238 239 // RegisterDefaultKVS - this function saves input kvsMap 240 // globally, this should be called only once preferably 241 // during `init()`. 242 func RegisterDefaultKVS(kvsMap map[string]KVS) { 243 for subSys, kvs := range kvsMap { 244 DefaultKVS[subSys] = kvs 245 } 246 } 247 248 // HelpSubSysMap - help for all individual KVS for each sub-systems 249 // also carries a special empty sub-system which dumps 250 // help for each sub-system key. 251 var HelpSubSysMap = map[string]HelpKVS{} 252 253 // RegisterHelpSubSys - this function saves 254 // input help KVS for each sub-system globally, 255 // this function should be called only once 256 // preferably in during `init()`. 257 func RegisterHelpSubSys(helpKVSMap map[string]HelpKVS) { 258 for subSys, hkvs := range helpKVSMap { 259 HelpSubSysMap[subSys] = hkvs 260 } 261 } 262 263 // HelpDeprecatedSubSysMap - help for all deprecated sub-systems, that may be 264 // removed in the future. 265 var HelpDeprecatedSubSysMap = map[string]HelpKV{} 266 267 // RegisterHelpDeprecatedSubSys - saves input help KVS for deprecated 268 // sub-systems globally. Should be called only once at init. 269 func RegisterHelpDeprecatedSubSys(helpDeprecatedKVMap map[string]HelpKV) { 270 for k, v := range helpDeprecatedKVMap { 271 HelpDeprecatedSubSysMap[k] = v 272 } 273 } 274 275 // KV - is a shorthand of each key value. 276 type KV struct { 277 Key string `json:"key"` 278 Value string `json:"value"` 279 280 HiddenIfEmpty bool `json:"-"` 281 } 282 283 func (kv KV) String() string { 284 var s strings.Builder 285 s.WriteString(kv.Key) 286 s.WriteString(KvSeparator) 287 spc := madmin.HasSpace(kv.Value) 288 if spc { 289 s.WriteString(KvDoubleQuote) 290 } 291 s.WriteString(kv.Value) 292 if spc { 293 s.WriteString(KvDoubleQuote) 294 } 295 return s.String() 296 } 297 298 // KVS - is a shorthand for some wrapper functions 299 // to operate on list of key values. 300 type KVS []KV 301 302 // Empty - return if kv is empty 303 func (kvs KVS) Empty() bool { 304 return len(kvs) == 0 305 } 306 307 // Clone - returns a copy of the KVS 308 func (kvs KVS) Clone() KVS { 309 return append(make(KVS, 0, len(kvs)), kvs...) 310 } 311 312 // GetWithDefault - returns default value if key not set 313 func (kvs KVS) GetWithDefault(key string, defaultKVS KVS) string { 314 v := kvs.Get(key) 315 if len(v) == 0 { 316 return defaultKVS.Get(key) 317 } 318 return v 319 } 320 321 // Keys returns the list of keys for the current KVS 322 func (kvs KVS) Keys() []string { 323 keys := make([]string, len(kvs)) 324 var foundComment bool 325 for i := range kvs { 326 if kvs[i].Key == madmin.CommentKey { 327 foundComment = true 328 } 329 keys[i] = kvs[i].Key 330 } 331 // Comment KV not found, add it explicitly. 332 if !foundComment { 333 keys = append(keys, madmin.CommentKey) 334 } 335 return keys 336 } 337 338 func (kvs KVS) String() string { 339 var s strings.Builder 340 for _, kv := range kvs { 341 s.WriteString(kv.String()) 342 s.WriteString(KvSpaceSeparator) 343 } 344 return s.String() 345 } 346 347 // Merge environment values with on disk KVS, environment values overrides 348 // anything on the disk. 349 func Merge(cfgKVS map[string]KVS, envname string, defaultKVS KVS) map[string]KVS { 350 newCfgKVS := make(map[string]KVS) 351 for _, e := range env.List(envname) { 352 tgt := strings.TrimPrefix(e, envname+Default) 353 if tgt == envname { 354 tgt = Default 355 } 356 newCfgKVS[tgt] = defaultKVS 357 } 358 for tgt, kv := range cfgKVS { 359 newCfgKVS[tgt] = kv 360 } 361 return newCfgKVS 362 } 363 364 // Set sets a value, if not sets a default value. 365 func (kvs *KVS) Set(key, value string) { 366 for i, kv := range *kvs { 367 if kv.Key == key { 368 (*kvs)[i] = KV{ 369 Key: key, 370 Value: value, 371 } 372 return 373 } 374 } 375 *kvs = append(*kvs, KV{ 376 Key: key, 377 Value: value, 378 }) 379 } 380 381 // Get - returns the value of a key, if not found returns empty. 382 func (kvs KVS) Get(key string) string { 383 v, ok := kvs.Lookup(key) 384 if ok { 385 return v 386 } 387 return "" 388 } 389 390 // Delete - deletes the key if present from the KV list. 391 func (kvs *KVS) Delete(key string) { 392 for i, kv := range *kvs { 393 if kv.Key == key { 394 *kvs = append((*kvs)[:i], (*kvs)[i+1:]...) 395 return 396 } 397 } 398 } 399 400 // LookupKV returns the KV by its key 401 func (kvs KVS) LookupKV(key string) (KV, bool) { 402 for _, kv := range kvs { 403 if kv.Key == key { 404 return kv, true 405 } 406 } 407 return KV{}, false 408 } 409 410 // Lookup - lookup a key in a list of KVS 411 func (kvs KVS) Lookup(key string) (string, bool) { 412 for _, kv := range kvs { 413 if kv.Key == key { 414 return kv.Value, true 415 } 416 } 417 return "", false 418 } 419 420 // Config - MinIO server config structure. 421 type Config map[string]map[string]KVS 422 423 // DelFrom - deletes all keys in the input reader. 424 func (c Config) DelFrom(r io.Reader) error { 425 scanner := bufio.NewScanner(r) 426 for scanner.Scan() { 427 // Skip any empty lines, or comment like characters 428 text := scanner.Text() 429 if text == "" || strings.HasPrefix(text, KvComment) { 430 continue 431 } 432 if err := c.DelKVS(text); err != nil { 433 return err 434 } 435 } 436 return scanner.Err() 437 } 438 439 // ContextKeyString is type(string) for contextKey 440 type ContextKeyString string 441 442 // ContextKeyForTargetFromConfig - key for context for target from config 443 const ContextKeyForTargetFromConfig = ContextKeyString("ContextKeyForTargetFromConfig") 444 445 // ParseConfigTargetID - read all targetIDs from reader 446 func ParseConfigTargetID(r io.Reader) (ids map[string]bool, err error) { 447 ids = make(map[string]bool) 448 scanner := bufio.NewScanner(r) 449 for scanner.Scan() { 450 // Skip any empty lines, or comment like characters 451 text := scanner.Text() 452 if text == "" || strings.HasPrefix(text, KvComment) { 453 continue 454 } 455 _, _, tgt, err := GetSubSys(text) 456 if err != nil { 457 return nil, err 458 } 459 ids[tgt] = true 460 } 461 if err := scanner.Err(); err != nil { 462 return nil, err 463 } 464 return 465 } 466 467 // ReadConfig - read content from input and write into c. 468 // Returns whether all parameters were dynamic. 469 func (c Config) ReadConfig(r io.Reader) (dynOnly bool, err error) { 470 var n int 471 scanner := bufio.NewScanner(r) 472 dynOnly = true 473 for scanner.Scan() { 474 // Skip any empty lines, or comment like characters 475 text := scanner.Text() 476 if text == "" || strings.HasPrefix(text, KvComment) { 477 continue 478 } 479 dynamic, err := c.SetKVS(text, DefaultKVS) 480 if err != nil { 481 return false, err 482 } 483 dynOnly = dynOnly && dynamic 484 n += len(text) 485 } 486 if err := scanner.Err(); err != nil { 487 return false, err 488 } 489 return dynOnly, nil 490 } 491 492 // RedactSensitiveInfo - removes sensitive information 493 // like urls and credentials from the configuration 494 func (c Config) RedactSensitiveInfo() Config { 495 nc := c.Clone() 496 497 for configName, configVals := range nc { 498 for _, helpKV := range HelpSubSysMap[configName] { 499 if helpKV.Sensitive { 500 for name, kvs := range configVals { 501 for i := range kvs { 502 if kvs[i].Key == helpKV.Key && len(kvs[i].Value) > 0 { 503 kvs[i].Value = "*redacted*" 504 } 505 } 506 configVals[name] = kvs 507 } 508 } 509 } 510 } 511 512 return nc 513 } 514 515 // Default KV configs for worm and region 516 var ( 517 DefaultCredentialKVS = KVS{ 518 KV{ 519 Key: AccessKey, 520 Value: auth.DefaultAccessKey, 521 }, 522 KV{ 523 Key: SecretKey, 524 Value: auth.DefaultSecretKey, 525 }, 526 } 527 528 DefaultSiteKVS = KVS{ 529 KV{ 530 Key: NameKey, 531 Value: "", 532 }, 533 KV{ 534 Key: RegionKey, 535 Value: "", 536 }, 537 } 538 539 DefaultRegionKVS = KVS{ 540 KV{ 541 Key: RegionName, 542 Value: "", 543 }, 544 } 545 ) 546 547 // Site - holds site info - name and region. 548 type Site struct { 549 Name string 550 Region string 551 } 552 553 var validRegionRegex = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9-_-]+$") 554 555 // validSiteNameRegex - allows lowercase letters, digits and '-', starts with 556 // letter. At least 2 characters long. 557 var validSiteNameRegex = regexp.MustCompile("^[a-z][a-z0-9-]+$") 558 559 // LookupSite - get site related configuration. Loads configuration from legacy 560 // region sub-system as well. 561 func LookupSite(siteKV KVS, regionKV KVS) (s Site, err error) { 562 if err = CheckValidKeys(SiteSubSys, siteKV, DefaultSiteKVS); err != nil { 563 return 564 } 565 region := env.Get(EnvRegion, "") 566 if region == "" { 567 env.Get(EnvRegionName, "") 568 } 569 if region == "" { 570 region = env.Get(EnvSiteRegion, siteKV.Get(RegionKey)) 571 } 572 if region == "" { 573 // No region config found in the site-subsystem. So lookup the legacy 574 // region sub-system. 575 if err = CheckValidKeys(RegionSubSys, regionKV, DefaultRegionKVS); err != nil { 576 // An invalid key was found in the region sub-system. 577 // Since the region sub-system cannot be (re)set as it 578 // is legacy, we return an error to tell the user to 579 // reset the region via the new command. 580 err = Errorf("could not load region from legacy configuration as it was invalid - use 'mc admin config set myminio site region=myregion name=myname' to set a region and name (%v)", err) 581 return 582 } 583 584 region = regionKV.Get(RegionName) 585 } 586 if region != "" { 587 if !validRegionRegex.MatchString(region) { 588 err = Errorf( 589 "region '%s' is invalid, expected simple characters such as [us-east-1, myregion...]", 590 region) 591 return 592 } 593 s.Region = region 594 } 595 596 name := env.Get(EnvSiteName, siteKV.Get(NameKey)) 597 if name != "" { 598 if !validSiteNameRegex.MatchString(name) { 599 err = Errorf( 600 "site name '%s' is invalid, expected simple characters such as [cal-rack0, myname...]", 601 name) 602 return 603 } 604 s.Name = name 605 } 606 return 607 } 608 609 // CheckValidKeys - checks if inputs KVS has the necessary keys, 610 // returns error if it find extra or superfluous keys. 611 func CheckValidKeys(subSys string, kv KVS, validKVS KVS, deprecatedKeys ...string) error { 612 nkv := KVS{} 613 for _, kv := range kv { 614 // Comment is a valid key, its also fully optional 615 // ignore it since it is a valid key for all 616 // sub-systems. 617 if kv.Key == Comment { 618 continue 619 } 620 var skip bool 621 for _, deprecatedKey := range deprecatedKeys { 622 if kv.Key == deprecatedKey { 623 skip = true 624 break 625 } 626 } 627 if skip { 628 continue 629 } 630 if _, ok := validKVS.Lookup(kv.Key); !ok { 631 nkv = append(nkv, kv) 632 } 633 } 634 if len(nkv) > 0 { 635 return Errorf( 636 "found invalid keys (%s) for '%s' sub-system, use 'mc admin config reset myminio %s' to fix invalid keys", nkv.String(), subSys, subSys) 637 } 638 return nil 639 } 640 641 // LookupWorm - check if worm is enabled 642 func LookupWorm() (bool, error) { 643 return ParseBool(env.Get(EnvWorm, EnableOff)) 644 } 645 646 // Carries all the renamed sub-systems from their 647 // previously known names 648 var renamedSubsys = map[string]string{ 649 CrawlerSubSys: ScannerSubSys, 650 // Add future sub-system renames 651 } 652 653 const ( // deprecated keys 654 apiReplicationWorkers = "replication_workers" 655 apiReplicationFailedWorkers = "replication_failed_workers" 656 ) 657 658 // map of subsystem to deleted keys 659 var deletedSubSysKeys = map[string][]string{ 660 APISubSys: {apiReplicationWorkers, apiReplicationFailedWorkers}, 661 // Add future sub-system deleted keys 662 } 663 664 // Merge - merges a new config with all the 665 // missing values for default configs, 666 // returns a config. 667 func (c Config) Merge() Config { 668 cp := New() 669 for subSys, tgtKV := range c { 670 for tgt := range tgtKV { 671 ckvs := c[subSys][tgt] 672 for _, kv := range cp[subSys][Default] { 673 _, ok := c[subSys][tgt].Lookup(kv.Key) 674 if !ok { 675 ckvs.Set(kv.Key, kv.Value) 676 } 677 } 678 if _, ok := cp[subSys]; !ok { 679 rnSubSys, ok := renamedSubsys[subSys] 680 if !ok { 681 // A config subsystem was removed or server was downgraded. 682 continue 683 } 684 // Copy over settings from previous sub-system 685 // to newly renamed sub-system 686 for _, kv := range cp[rnSubSys][Default] { 687 _, ok := c[subSys][tgt].Lookup(kv.Key) 688 if !ok { 689 ckvs.Set(kv.Key, kv.Value) 690 } 691 } 692 subSys = rnSubSys 693 } 694 // Delete deprecated keys for subsystem if any 695 if keys, ok := deletedSubSysKeys[subSys]; ok { 696 for _, key := range keys { 697 ckvs.Delete(key) 698 } 699 } 700 cp[subSys][tgt] = ckvs 701 } 702 } 703 704 return cp 705 } 706 707 // New - initialize a new server config. 708 func New() Config { 709 srvCfg := make(Config) 710 for _, k := range SubSystems.ToSlice() { 711 srvCfg[k] = map[string]KVS{} 712 srvCfg[k][Default] = DefaultKVS[k] 713 } 714 return srvCfg 715 } 716 717 // Target signifies an individual target 718 type Target struct { 719 SubSystem string 720 KVS KVS 721 } 722 723 // Targets sub-system targets 724 type Targets []Target 725 726 // GetKVS - get kvs from specific subsystem. 727 func (c Config) GetKVS(s string, defaultKVS map[string]KVS) (Targets, error) { 728 if len(s) == 0 { 729 return nil, Errorf("input cannot be empty") 730 } 731 inputs := strings.Fields(s) 732 if len(inputs) > 1 { 733 return nil, Errorf("invalid number of arguments %s", s) 734 } 735 subSystemValue := strings.SplitN(inputs[0], SubSystemSeparator, 2) 736 if len(subSystemValue) == 0 { 737 return nil, Errorf("invalid number of arguments %s", s) 738 } 739 found := SubSystems.Contains(subSystemValue[0]) 740 if !found { 741 // Check for sub-prefix only if the input value is only a 742 // single value, this rejects invalid inputs if any. 743 found = !SubSystems.FuncMatch(strings.HasPrefix, subSystemValue[0]).IsEmpty() && len(subSystemValue) == 1 744 } 745 if !found { 746 return nil, Errorf("unknown sub-system %s", s) 747 } 748 749 targets := Targets{} 750 subSysPrefix := subSystemValue[0] 751 if len(subSystemValue) == 2 { 752 if len(subSystemValue[1]) == 0 { 753 return nil, Errorf("sub-system target '%s' cannot be empty", s) 754 } 755 kvs, ok := c[subSysPrefix][subSystemValue[1]] 756 if !ok { 757 return nil, Errorf("sub-system target '%s' doesn't exist", s) 758 } 759 for _, kv := range defaultKVS[subSysPrefix] { 760 _, ok = kvs.Lookup(kv.Key) 761 if !ok { 762 kvs.Set(kv.Key, kv.Value) 763 } 764 } 765 targets = append(targets, Target{ 766 SubSystem: inputs[0], 767 KVS: kvs, 768 }) 769 } else { 770 // Use help for sub-system to preserve the order. Add deprecated 771 // keys at the end (in some order). 772 kvsOrder := append([]HelpKV{}, HelpSubSysMap[""]...) 773 for _, v := range HelpDeprecatedSubSysMap { 774 kvsOrder = append(kvsOrder, v) 775 } 776 777 for _, hkv := range kvsOrder { 778 if !strings.HasPrefix(hkv.Key, subSysPrefix) { 779 continue 780 } 781 if c[hkv.Key][Default].Empty() { 782 targets = append(targets, Target{ 783 SubSystem: hkv.Key, 784 KVS: defaultKVS[hkv.Key], 785 }) 786 } 787 for k, kvs := range c[hkv.Key] { 788 for _, dkv := range defaultKVS[hkv.Key] { 789 _, ok := kvs.Lookup(dkv.Key) 790 if !ok { 791 kvs.Set(dkv.Key, dkv.Value) 792 } 793 } 794 if k != Default { 795 targets = append(targets, Target{ 796 SubSystem: hkv.Key + SubSystemSeparator + k, 797 KVS: kvs, 798 }) 799 } else { 800 targets = append(targets, Target{ 801 SubSystem: hkv.Key, 802 KVS: kvs, 803 }) 804 } 805 } 806 } 807 } 808 return targets, nil 809 } 810 811 // DelKVS - delete a specific key. 812 func (c Config) DelKVS(s string) error { 813 subSys, inputs, tgt, err := GetSubSys(s) 814 if err != nil { 815 if !SubSystems.Contains(subSys) && len(inputs) == 1 { 816 // Unknown sub-system found try to remove it anyways. 817 delete(c, subSys) 818 return nil 819 } 820 return err 821 } 822 823 ck, ok := c[subSys][tgt] 824 if !ok { 825 return Error[ErrConfigNotFound]("sub-system %s:%s already deleted or does not exist", subSys, tgt) 826 } 827 828 if len(inputs) == 2 { 829 currKVS := ck.Clone() 830 defKVS := DefaultKVS[subSys] 831 for _, delKey := range strings.Fields(inputs[1]) { 832 _, ok := currKVS.Lookup(delKey) 833 if !ok { 834 return Error[ErrConfigNotFound]("key %s doesn't exist", delKey) 835 } 836 defVal, isDef := defKVS.Lookup(delKey) 837 if isDef { 838 currKVS.Set(delKey, defVal) 839 } else { 840 currKVS.Delete(delKey) 841 } 842 } 843 c[subSys][tgt] = currKVS 844 } else { 845 delete(c[subSys], tgt) 846 } 847 return nil 848 } 849 850 // Clone - clones a config map entirely. 851 func (c Config) Clone() Config { 852 cp := New() 853 for subSys, tgtKV := range c { 854 cp[subSys] = make(map[string]KVS) 855 for tgt, kv := range tgtKV { 856 cp[subSys][tgt] = append(cp[subSys][tgt], kv...) 857 } 858 } 859 return cp 860 } 861 862 // GetSubSys - extracts subssystem info from given config string 863 func GetSubSys(s string) (subSys string, inputs []string, tgt string, e error) { 864 tgt = Default 865 if len(s) == 0 { 866 return subSys, inputs, tgt, Errorf("input arguments cannot be empty") 867 } 868 inputs = strings.SplitN(s, KvSpaceSeparator, 2) 869 870 subSystemValue := strings.SplitN(inputs[0], SubSystemSeparator, 2) 871 subSys = subSystemValue[0] 872 if !SubSystems.Contains(subSys) { 873 return subSys, inputs, tgt, Errorf("unknown sub-system %s", s) 874 } 875 876 if SubSystemsSingleTargets.Contains(subSystemValue[0]) && len(subSystemValue) == 2 { 877 return subSys, inputs, tgt, Errorf("sub-system '%s' only supports single target", subSystemValue[0]) 878 } 879 880 if len(subSystemValue) == 2 { 881 tgt = subSystemValue[1] 882 } 883 884 return subSys, inputs, tgt, e 885 } 886 887 // kvFields - converts an input string of form "k1=v1 k2=v2" into 888 // fields of ["k1=v1", "k2=v2"], the tokenization of each `k=v` 889 // happens with the right number of input keys, if keys 890 // input is empty returned value is empty slice as well. 891 func kvFields(input string, keys []string) []string { 892 valueIndexes := make([]int, 0, len(keys)) 893 for _, key := range keys { 894 i := strings.Index(input, key+KvSeparator) 895 if i == -1 { 896 continue 897 } 898 valueIndexes = append(valueIndexes, i) 899 } 900 901 sort.Ints(valueIndexes) 902 fields := make([]string, len(valueIndexes)) 903 for i := range valueIndexes { 904 j := i + 1 905 if j < len(valueIndexes) { 906 fields[i] = strings.TrimSpace(input[valueIndexes[i]:valueIndexes[j]]) 907 } else { 908 fields[i] = strings.TrimSpace(input[valueIndexes[i]:]) 909 } 910 } 911 return fields 912 } 913 914 // SetKVS - set specific key values per sub-system. 915 func (c Config) SetKVS(s string, defaultKVS map[string]KVS) (dynamic bool, err error) { 916 subSys, inputs, tgt, err := GetSubSys(s) 917 if err != nil { 918 return false, err 919 } 920 921 dynamic = SubSystemsDynamic.Contains(subSys) 922 923 fields := kvFields(inputs[1], defaultKVS[subSys].Keys()) 924 if len(fields) == 0 { 925 return false, Errorf("sub-system '%s' cannot have empty keys", subSys) 926 } 927 928 kvs := KVS{} 929 var prevK string 930 for _, v := range fields { 931 kv := strings.SplitN(v, KvSeparator, 2) 932 if len(kv) == 0 { 933 continue 934 } 935 if len(kv) == 1 && prevK != "" { 936 value := strings.Join([]string{ 937 kvs.Get(prevK), 938 madmin.SanitizeValue(kv[0]), 939 }, KvSpaceSeparator) 940 kvs.Set(prevK, value) 941 continue 942 } 943 if len(kv) == 2 { 944 prevK = kv[0] 945 kvs.Set(prevK, madmin.SanitizeValue(kv[1])) 946 continue 947 } 948 return false, Errorf("key '%s', cannot have empty value", kv[0]) 949 } 950 951 _, ok := kvs.Lookup(Enable) 952 // Check if state is required 953 _, enableRequired := defaultKVS[subSys].Lookup(Enable) 954 if !ok && enableRequired { 955 // implicit state "on" if not specified. 956 kvs.Set(Enable, EnableOn) 957 } 958 959 var currKVS KVS 960 ck, ok := c[subSys][tgt] 961 if !ok { 962 currKVS = defaultKVS[subSys].Clone() 963 } else { 964 currKVS = ck.Clone() 965 for _, kv := range defaultKVS[subSys] { 966 if _, ok = currKVS.Lookup(kv.Key); !ok { 967 currKVS.Set(kv.Key, kv.Value) 968 } 969 } 970 } 971 972 for _, kv := range kvs { 973 if kv.Key == Comment { 974 // Skip comment and add it later. 975 continue 976 } 977 currKVS.Set(kv.Key, kv.Value) 978 } 979 980 v, ok := kvs.Lookup(Comment) 981 if ok { 982 currKVS.Set(Comment, v) 983 } 984 985 hkvs := HelpSubSysMap[subSys] 986 for _, hkv := range hkvs { 987 var enabled bool 988 if enableRequired { 989 enabled = currKVS.Get(Enable) == EnableOn 990 } else { 991 // when enable arg is not required 992 // then it is implicit on for the sub-system. 993 enabled = true 994 } 995 v, _ := currKVS.Lookup(hkv.Key) 996 if v == "" && !hkv.Optional && enabled { 997 // Return error only if the 998 // key is enabled, for state=off 999 // let it be empty. 1000 return false, Errorf( 1001 "'%s' is not optional for '%s' sub-system, please check '%s' documentation", 1002 hkv.Key, subSys, subSys) 1003 } 1004 } 1005 c[subSys][tgt] = currKVS 1006 return dynamic, nil 1007 } 1008 1009 // CheckValidKeys - checks if the config parameters for the given subsystem and 1010 // target are valid. It checks both the configuration store as well as 1011 // environment variables. 1012 func (c Config) CheckValidKeys(subSys string, deprecatedKeys []string) error { 1013 defKVS, ok := DefaultKVS[subSys] 1014 if !ok { 1015 return Errorf("Subsystem %s does not exist", subSys) 1016 } 1017 1018 // Make a list of valid keys for the subsystem including the `comment` 1019 // key. 1020 validKeys := make([]string, 0, len(defKVS)+1) 1021 for _, param := range defKVS { 1022 validKeys = append(validKeys, param.Key) 1023 } 1024 validKeys = append(validKeys, Comment) 1025 1026 subSysEnvVars := env.List(fmt.Sprintf("%s%s", EnvPrefix, strings.ToUpper(subSys))) 1027 1028 // Set of env vars for the sub-system to validate. 1029 candidates := set.CreateStringSet(subSysEnvVars...) 1030 1031 // Remove all default target env vars from the candidates set (as they 1032 // are valid). 1033 for _, param := range validKeys { 1034 paramEnvName := getEnvVarName(subSys, Default, param) 1035 candidates.Remove(paramEnvName) 1036 } 1037 1038 isSingleTarget := SubSystemsSingleTargets.Contains(subSys) 1039 if isSingleTarget && len(candidates) > 0 { 1040 return Errorf("The following environment variables are unknown: %s", 1041 strings.Join(candidates.ToSlice(), ", ")) 1042 } 1043 1044 if !isSingleTarget { 1045 // Validate other env vars for all targets. 1046 envVars := candidates.ToSlice() 1047 for _, envVar := range envVars { 1048 for _, param := range validKeys { 1049 pEnvName := getEnvVarName(subSys, Default, param) + Default 1050 if len(envVar) > len(pEnvName) && strings.HasPrefix(envVar, pEnvName) { 1051 // This envVar is valid - it has a 1052 // non-empty target. 1053 candidates.Remove(envVar) 1054 } 1055 } 1056 } 1057 1058 // Whatever remains are invalid env vars - return an error. 1059 if len(candidates) > 0 { 1060 return Errorf("The following environment variables are unknown: %s", 1061 strings.Join(candidates.ToSlice(), ", ")) 1062 } 1063 } 1064 1065 validKeysSet := set.CreateStringSet(validKeys...) 1066 validKeysSet = validKeysSet.Difference(set.CreateStringSet(deprecatedKeys...)) 1067 kvsMap := c[subSys] 1068 for tgt, kvs := range kvsMap { 1069 invalidKV := KVS{} 1070 for _, kv := range kvs { 1071 if !validKeysSet.Contains(kv.Key) { 1072 invalidKV = append(invalidKV, kv) 1073 } 1074 } 1075 if len(invalidKV) > 0 { 1076 return Errorf( 1077 "found invalid keys (%s) for '%s:%s' sub-system, use 'mc admin config reset myminio %s:%s' to fix invalid keys", 1078 invalidKV.String(), subSys, tgt, subSys, tgt) 1079 } 1080 } 1081 return nil 1082 } 1083 1084 // GetAvailableTargets - returns a list of targets configured for the given 1085 // subsystem (whether they are enabled or not). A target could be configured via 1086 // environment variables or via the configuration store. The default target is 1087 // `_` and is always returned. The result is sorted so that the default target 1088 // is the first one and the remaining entries are sorted in ascending order. 1089 func (c Config) GetAvailableTargets(subSys string) ([]string, error) { 1090 if SubSystemsSingleTargets.Contains(subSys) { 1091 return []string{Default}, nil 1092 } 1093 1094 defKVS, ok := DefaultKVS[subSys] 1095 if !ok { 1096 return nil, Errorf("Subsystem %s does not exist", subSys) 1097 } 1098 1099 kvsMap := c[subSys] 1100 seen := set.NewStringSet() 1101 1102 // Add all targets that are configured in the config store. 1103 for k := range kvsMap { 1104 seen.Add(k) 1105 } 1106 1107 // env:prefix 1108 filterMap := map[string]string{} 1109 // Add targets that are configured via environment variables. 1110 for _, param := range defKVS { 1111 envVarPrefix := getEnvVarName(subSys, Default, param.Key) + Default 1112 envsWithPrefix := env.List(envVarPrefix) 1113 for _, k := range envsWithPrefix { 1114 tgtName := strings.TrimPrefix(k, envVarPrefix) 1115 if tgtName != "" { 1116 if v, ok := filterMap[k]; ok { 1117 if strings.HasPrefix(envVarPrefix, v) { 1118 filterMap[k] = envVarPrefix 1119 } 1120 } else { 1121 filterMap[k] = envVarPrefix 1122 } 1123 } 1124 } 1125 } 1126 1127 for k, v := range filterMap { 1128 seen.Add(strings.TrimPrefix(k, v)) 1129 } 1130 1131 seen.Remove(Default) 1132 targets := seen.ToSlice() 1133 sort.Strings(targets) 1134 targets = append([]string{Default}, targets...) 1135 1136 return targets, nil 1137 } 1138 1139 func getEnvVarName(subSys, target, param string) string { 1140 if target == Default { 1141 return fmt.Sprintf("%s%s%s%s", EnvPrefix, strings.ToUpper(subSys), Default, strings.ToUpper(param)) 1142 } 1143 1144 return fmt.Sprintf("%s%s%s%s%s%s", EnvPrefix, strings.ToUpper(subSys), Default, strings.ToUpper(param), 1145 Default, target) 1146 } 1147 1148 var resolvableSubsystems = set.CreateStringSet(IdentityOpenIDSubSys, IdentityLDAPSubSys, PolicyPluginSubSys) 1149 1150 // ValueSource represents the source of a config parameter value. 1151 type ValueSource uint8 1152 1153 // Constants for ValueSource 1154 const ( 1155 ValueSourceAbsent ValueSource = iota // this is an error case 1156 ValueSourceDef 1157 ValueSourceCfg 1158 ValueSourceEnv 1159 ) 1160 1161 // ResolveConfigParam returns the effective value of a configuration parameter, 1162 // within a subsystem and subsystem target. The effective value is, in order of 1163 // decreasing precedence: 1164 // 1165 // 1. the value of the corresponding environment variable if set, 1166 // 2. the value of the parameter in the config store if set, 1167 // 3. the default value, 1168 // 1169 // This function only works for a subset of sub-systems, others return 1170 // `ValueSourceAbsent`. FIXME: some parameters have custom environment 1171 // variables for which support needs to be added. 1172 // 1173 // When redactSecrets is true, the returned value is empty if the configuration 1174 // parameter is a secret, and the returned isRedacted flag is set. 1175 func (c Config) ResolveConfigParam(subSys, target, cfgParam string, redactSecrets bool, 1176 ) (value string, cs ValueSource, isRedacted bool) { 1177 // cs = ValueSourceAbsent initially as it is iota by default. 1178 1179 // Initially only support OpenID 1180 if !resolvableSubsystems.Contains(subSys) { 1181 return 1182 } 1183 1184 // Check if config param requested is valid. 1185 defKVS, ok := DefaultKVS[subSys] 1186 if !ok { 1187 return 1188 } 1189 1190 defValue, isFound := defKVS.Lookup(cfgParam) 1191 // Comments usually are absent from `defKVS`, so we handle it specially. 1192 if !isFound && cfgParam == Comment { 1193 defValue, isFound = "", true 1194 } 1195 if !isFound { 1196 return 1197 } 1198 1199 if target == "" { 1200 target = Default 1201 } 1202 1203 if redactSecrets { 1204 // If the configuration parameter is a secret, make sure to redact it when 1205 // we return. 1206 helpKV, _ := HelpSubSysMap[subSys].Lookup(cfgParam) 1207 if helpKV.Secret { 1208 defer func() { 1209 value = "" 1210 isRedacted = true 1211 }() 1212 } 1213 } 1214 1215 envVar := getEnvVarName(subSys, target, cfgParam) 1216 1217 // Lookup Env var. 1218 value = env.Get(envVar, "") 1219 if value != "" { 1220 cs = ValueSourceEnv 1221 return 1222 } 1223 1224 // Lookup config store. 1225 if subSysStore, ok := c[subSys]; ok { 1226 if kvs, ok2 := subSysStore[target]; ok2 { 1227 var ok3 bool 1228 value, ok3 = kvs.Lookup(cfgParam) 1229 if ok3 { 1230 cs = ValueSourceCfg 1231 return 1232 } 1233 } 1234 } 1235 1236 // Return the default value. 1237 value = defValue 1238 cs = ValueSourceDef 1239 return 1240 } 1241 1242 // KVSrc represents a configuration parameter key and value along with the 1243 // source of the value. 1244 type KVSrc struct { 1245 Key string 1246 Value string 1247 Src ValueSource 1248 } 1249 1250 // GetResolvedConfigParams returns all applicable config parameters with their 1251 // value sources. 1252 func (c Config) GetResolvedConfigParams(subSys, target string, redactSecrets bool) ([]KVSrc, error) { 1253 if !resolvableSubsystems.Contains(subSys) { 1254 return nil, Errorf("unsupported subsystem: %s", subSys) 1255 } 1256 1257 // Check if config param requested is valid. 1258 defKVS, ok := DefaultKVS[subSys] 1259 if !ok { 1260 return nil, Errorf("unknown subsystem: %s", subSys) 1261 } 1262 1263 r := make([]KVSrc, 0, len(defKVS)+1) 1264 for _, kv := range defKVS { 1265 v, vs, isRedacted := c.ResolveConfigParam(subSys, target, kv.Key, redactSecrets) 1266 1267 // Fix `vs` when default. 1268 if v == kv.Value { 1269 vs = ValueSourceDef 1270 } 1271 1272 if redactSecrets && isRedacted { 1273 // Skip adding redacted secrets to the output. 1274 continue 1275 } 1276 1277 r = append(r, KVSrc{ 1278 Key: kv.Key, 1279 Value: v, 1280 Src: vs, 1281 }) 1282 } 1283 1284 // Add the comment key as well if non-empty (and comments are never 1285 // redacted). 1286 v, vs, _ := c.ResolveConfigParam(subSys, target, Comment, redactSecrets) 1287 if vs != ValueSourceDef { 1288 r = append(r, KVSrc{ 1289 Key: Comment, 1290 Value: v, 1291 Src: vs, 1292 }) 1293 } 1294 1295 return r, nil 1296 } 1297 1298 // getTargetKVS returns configuration KVs for the given subsystem and target. It 1299 // does not return any secrets in the configuration values when `redactSecrets` 1300 // is set. 1301 func (c Config) getTargetKVS(subSys, target string, redactSecrets bool) KVS { 1302 store, ok := c[subSys] 1303 if !ok { 1304 return nil 1305 } 1306 1307 // Lookup will succeed, because this function only works with valid subSys 1308 // values. 1309 resultKVS := make([]KV, 0, len(store[target])) 1310 hkvs := HelpSubSysMap[subSys] 1311 for _, kv := range store[target] { 1312 hkv, _ := hkvs.Lookup(kv.Key) 1313 if hkv.Secret && redactSecrets && kv.Value != "" { 1314 // Skip returning secrets. 1315 continue 1316 // clonedKV := kv 1317 // clonedKV.Value = redactedSecret 1318 // resultKVS = append(resultKVS, clonedKV) 1319 } 1320 resultKVS = append(resultKVS, kv) 1321 } 1322 1323 return resultKVS 1324 } 1325 1326 // getTargetEnvs returns configured environment variable settings for the given 1327 // subsystem and target. 1328 func (c Config) getTargetEnvs(subSys, target string, defKVS KVS, redactSecrets bool) map[string]EnvPair { 1329 hkvs := HelpSubSysMap[subSys] 1330 envMap := make(map[string]EnvPair) 1331 1332 // Add all env vars that are set. 1333 for _, kv := range defKVS { 1334 envName := getEnvVarName(subSys, target, kv.Key) 1335 envPair := EnvPair{ 1336 Name: envName, 1337 Value: env.Get(envName, ""), 1338 } 1339 if envPair.Value != "" { 1340 hkv, _ := hkvs.Lookup(kv.Key) 1341 if hkv.Secret && redactSecrets { 1342 // Skip adding any secret to the returned value. 1343 continue 1344 // envPair.Value = redactedSecret 1345 } 1346 envMap[kv.Key] = envPair 1347 } 1348 } 1349 return envMap 1350 } 1351 1352 // EnvPair represents an environment variable and its value. 1353 type EnvPair struct { 1354 Name, Value string 1355 } 1356 1357 // SubsysInfo holds config info for a subsystem target. 1358 type SubsysInfo struct { 1359 SubSys, Target string 1360 Defaults KVS 1361 Config KVS 1362 1363 // map of config parameter name to EnvPair. 1364 EnvMap map[string]EnvPair 1365 } 1366 1367 // GetSubsysInfo returns `SubsysInfo`s for all targets for the subsystem, when 1368 // target is empty. Otherwise returns `SubsysInfo` for the desired target only. 1369 // To request the default target only, target must be set to `Default`. 1370 func (c Config) GetSubsysInfo(subSys, target string, redactSecrets bool) ([]SubsysInfo, error) { 1371 // Check if config param requested is valid. 1372 defKVS1, ok := DefaultKVS[subSys] 1373 if !ok { 1374 return nil, Errorf("unknown subsystem: %s", subSys) 1375 } 1376 1377 targets, err := c.GetAvailableTargets(subSys) 1378 if err != nil { 1379 return nil, err 1380 } 1381 1382 if target != "" { 1383 found := false 1384 for _, t := range targets { 1385 if t == target { 1386 found = true 1387 break 1388 } 1389 } 1390 if !found { 1391 return nil, Errorf("there is no target `%s` for subsystem `%s`", target, subSys) 1392 } 1393 targets = []string{target} 1394 } 1395 1396 // The `Comment` configuration variable is optional but is available to be 1397 // set for all sub-systems. It is not present in the `DefaultKVS` map's 1398 // values. To enable fetching a configured comment value from the 1399 // environment we add it to the list of default keys for the subsystem. 1400 defKVS := make([]KV, len(defKVS1), len(defKVS1)+1) 1401 copy(defKVS, defKVS1) 1402 defKVS = append(defKVS, KV{Key: Comment}) 1403 1404 r := make([]SubsysInfo, 0, len(targets)) 1405 for _, target := range targets { 1406 r = append(r, SubsysInfo{ 1407 SubSys: subSys, 1408 Target: target, 1409 Defaults: defKVS, 1410 Config: c.getTargetKVS(subSys, target, redactSecrets), 1411 EnvMap: c.getTargetEnvs(subSys, target, defKVS, redactSecrets), 1412 }) 1413 } 1414 1415 return r, nil 1416 } 1417 1418 // AddEnvString adds env vars to the given string builder. 1419 func (cs *SubsysInfo) AddEnvString(b *strings.Builder) { 1420 for _, v := range cs.Defaults { 1421 if ep, ok := cs.EnvMap[v.Key]; ok { 1422 b.WriteString(KvComment) 1423 b.WriteString(KvSpaceSeparator) 1424 b.WriteString(ep.Name) 1425 b.WriteString(EnvSeparator) 1426 b.WriteString(ep.Value) 1427 b.WriteString(KvNewline) 1428 } 1429 } 1430 } 1431 1432 // WriteTo writes the string representation of the configuration to the given 1433 // builder. When off is true, adds a comment character before the config system 1434 // output. It also ignores values when empty and deprecated. 1435 func (cs *SubsysInfo) WriteTo(b *strings.Builder, off bool) { 1436 cs.AddEnvString(b) 1437 if off { 1438 b.WriteString(KvComment) 1439 b.WriteString(KvSpaceSeparator) 1440 } 1441 b.WriteString(cs.SubSys) 1442 if cs.Target != Default { 1443 b.WriteString(SubSystemSeparator) 1444 b.WriteString(cs.Target) 1445 } 1446 b.WriteString(KvSpaceSeparator) 1447 for _, kv := range cs.Config { 1448 dkv, ok := cs.Defaults.LookupKV(kv.Key) 1449 if !ok { 1450 continue 1451 } 1452 // Ignore empty and deprecated values 1453 if dkv.HiddenIfEmpty && kv.Value == "" { 1454 continue 1455 } 1456 // Do not need to print if state is on 1457 if kv.Key == Enable && kv.Value == EnableOn { 1458 continue 1459 } 1460 b.WriteString(kv.String()) 1461 b.WriteString(KvSpaceSeparator) 1462 } 1463 1464 b.WriteString(KvNewline) 1465 }