storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/config/config.go (about) 1 /* 2 * MinIO Cloud Storage, (C) 2019 MinIO, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 */ 17 18 package config 19 20 import ( 21 "bufio" 22 "fmt" 23 "io" 24 "regexp" 25 "strings" 26 27 "github.com/minio/minio-go/v7/pkg/set" 28 29 "storj.io/minio/pkg/auth" 30 "storj.io/minio/pkg/env" 31 "storj.io/minio/pkg/madmin" 32 ) 33 34 // Error config error type 35 type Error struct { 36 Err string 37 } 38 39 // Errorf - formats according to a format specifier and returns 40 // the string as a value that satisfies error of type config.Error 41 func Errorf(format string, a ...interface{}) error { 42 return Error{Err: fmt.Sprintf(format, a...)} 43 } 44 45 func (e Error) Error() string { 46 return e.Err 47 } 48 49 // Default keys 50 const ( 51 Default = madmin.Default 52 Enable = madmin.EnableKey 53 Comment = madmin.CommentKey 54 55 // Enable values 56 EnableOn = madmin.EnableOn 57 EnableOff = madmin.EnableOff 58 59 RegionName = "name" 60 AccessKey = "access_key" 61 SecretKey = "secret_key" 62 ) 63 64 // Top level config constants. 65 const ( 66 CredentialsSubSys = "credentials" 67 PolicyOPASubSys = "policy_opa" 68 IdentityOpenIDSubSys = "identity_openid" 69 IdentityLDAPSubSys = "identity_ldap" 70 CacheSubSys = "cache" 71 RegionSubSys = "region" 72 EtcdSubSys = "etcd" 73 StorageClassSubSys = "storage_class" 74 APISubSys = "api" 75 CompressionSubSys = "compression" 76 KmsVaultSubSys = "kms_vault" 77 KmsKesSubSys = "kms_kes" 78 LoggerWebhookSubSys = "logger_webhook" 79 AuditWebhookSubSys = "audit_webhook" 80 HealSubSys = "heal" 81 ScannerSubSys = "scanner" 82 CrawlerSubSys = "crawler" 83 84 // Add new constants here if you add new fields to config. 85 ) 86 87 // Notification config constants. 88 const ( 89 NotifyKafkaSubSys = "notify_kafka" 90 NotifyMQTTSubSys = "notify_mqtt" 91 NotifyMySQLSubSys = "notify_mysql" 92 NotifyNATSSubSys = "notify_nats" 93 NotifyNSQSubSys = "notify_nsq" 94 NotifyESSubSys = "notify_elasticsearch" 95 NotifyAMQPSubSys = "notify_amqp" 96 NotifyPostgresSubSys = "notify_postgres" 97 NotifyRedisSubSys = "notify_redis" 98 NotifyWebhookSubSys = "notify_webhook" 99 100 // Add new constants here if you add new fields to config. 101 ) 102 103 // SubSystems - all supported sub-systems 104 var SubSystems = set.CreateStringSet( 105 CredentialsSubSys, 106 RegionSubSys, 107 EtcdSubSys, 108 CacheSubSys, 109 APISubSys, 110 StorageClassSubSys, 111 CompressionSubSys, 112 KmsVaultSubSys, 113 KmsKesSubSys, 114 LoggerWebhookSubSys, 115 AuditWebhookSubSys, 116 PolicyOPASubSys, 117 IdentityLDAPSubSys, 118 IdentityOpenIDSubSys, 119 ScannerSubSys, 120 HealSubSys, 121 NotifyAMQPSubSys, 122 NotifyESSubSys, 123 NotifyKafkaSubSys, 124 NotifyMQTTSubSys, 125 NotifyMySQLSubSys, 126 NotifyNATSSubSys, 127 NotifyNSQSubSys, 128 NotifyPostgresSubSys, 129 NotifyRedisSubSys, 130 NotifyWebhookSubSys, 131 ) 132 133 // SubSystemsDynamic - all sub-systems that have dynamic config. 134 var SubSystemsDynamic = set.CreateStringSet( 135 APISubSys, 136 CompressionSubSys, 137 ScannerSubSys, 138 HealSubSys, 139 ) 140 141 // SubSystemsSingleTargets - subsystems which only support single target. 142 var SubSystemsSingleTargets = set.CreateStringSet([]string{ 143 CredentialsSubSys, 144 RegionSubSys, 145 EtcdSubSys, 146 CacheSubSys, 147 APISubSys, 148 StorageClassSubSys, 149 CompressionSubSys, 150 KmsVaultSubSys, 151 KmsKesSubSys, 152 PolicyOPASubSys, 153 IdentityLDAPSubSys, 154 IdentityOpenIDSubSys, 155 HealSubSys, 156 ScannerSubSys, 157 }...) 158 159 // Constant separators 160 const ( 161 SubSystemSeparator = madmin.SubSystemSeparator 162 KvSeparator = madmin.KvSeparator 163 KvSpaceSeparator = madmin.KvSpaceSeparator 164 KvComment = madmin.KvComment 165 KvNewline = madmin.KvNewline 166 KvDoubleQuote = madmin.KvDoubleQuote 167 KvSingleQuote = madmin.KvSingleQuote 168 169 // Env prefix used for all envs in MinIO 170 EnvPrefix = "MINIO_" 171 EnvWordDelimiter = `_` 172 ) 173 174 // DefaultKVS - default kvs for all sub-systems 175 var DefaultKVS map[string]KVS 176 177 // RegisterDefaultKVS - this function saves input kvsMap 178 // globally, this should be called only once preferably 179 // during `init()`. 180 func RegisterDefaultKVS(kvsMap map[string]KVS) { 181 DefaultKVS = map[string]KVS{} 182 for subSys, kvs := range kvsMap { 183 DefaultKVS[subSys] = kvs 184 } 185 } 186 187 // HelpSubSysMap - help for all individual KVS for each sub-systems 188 // also carries a special empty sub-system which dumps 189 // help for each sub-system key. 190 var HelpSubSysMap map[string]HelpKVS 191 192 // RegisterHelpSubSys - this function saves 193 // input help KVS for each sub-system globally, 194 // this function should be called only once 195 // preferably in during `init()`. 196 func RegisterHelpSubSys(helpKVSMap map[string]HelpKVS) { 197 HelpSubSysMap = map[string]HelpKVS{} 198 for subSys, hkvs := range helpKVSMap { 199 HelpSubSysMap[subSys] = hkvs 200 } 201 } 202 203 // KV - is a shorthand of each key value. 204 type KV struct { 205 Key string `json:"key"` 206 Value string `json:"value"` 207 } 208 209 // KVS - is a shorthand for some wrapper functions 210 // to operate on list of key values. 211 type KVS []KV 212 213 // Empty - return if kv is empty 214 func (kvs KVS) Empty() bool { 215 return len(kvs) == 0 216 } 217 218 // Keys returns the list of keys for the current KVS 219 func (kvs KVS) Keys() []string { 220 var keys = make([]string, len(kvs)) 221 var foundComment bool 222 for i := range kvs { 223 if kvs[i].Key == madmin.CommentKey { 224 foundComment = true 225 } 226 keys[i] = kvs[i].Key 227 } 228 // Comment KV not found, add it explicitly. 229 if !foundComment { 230 keys = append(keys, madmin.CommentKey) 231 } 232 return keys 233 } 234 235 func (kvs KVS) String() string { 236 var s strings.Builder 237 for _, kv := range kvs { 238 // Do not need to print if state is on 239 if kv.Key == Enable && kv.Value == EnableOn { 240 continue 241 } 242 s.WriteString(kv.Key) 243 s.WriteString(KvSeparator) 244 spc := madmin.HasSpace(kv.Value) 245 if spc { 246 s.WriteString(KvDoubleQuote) 247 } 248 s.WriteString(kv.Value) 249 if spc { 250 s.WriteString(KvDoubleQuote) 251 } 252 s.WriteString(KvSpaceSeparator) 253 } 254 return s.String() 255 } 256 257 // Set sets a value, if not sets a default value. 258 func (kvs *KVS) Set(key, value string) { 259 for i, kv := range *kvs { 260 if kv.Key == key { 261 (*kvs)[i] = KV{ 262 Key: key, 263 Value: value, 264 } 265 return 266 } 267 } 268 *kvs = append(*kvs, KV{ 269 Key: key, 270 Value: value, 271 }) 272 } 273 274 // Get - returns the value of a key, if not found returns empty. 275 func (kvs KVS) Get(key string) string { 276 v, ok := kvs.Lookup(key) 277 if ok { 278 return v 279 } 280 return "" 281 } 282 283 // Delete - deletes the key if present from the KV list. 284 func (kvs *KVS) Delete(key string) { 285 for i, kv := range *kvs { 286 if kv.Key == key { 287 *kvs = append((*kvs)[:i], (*kvs)[i+1:]...) 288 return 289 } 290 } 291 } 292 293 // Lookup - lookup a key in a list of KVS 294 func (kvs KVS) Lookup(key string) (string, bool) { 295 for _, kv := range kvs { 296 if kv.Key == key { 297 return kv.Value, true 298 } 299 } 300 return "", false 301 } 302 303 // Config - MinIO server config structure. 304 type Config map[string]map[string]KVS 305 306 // DelFrom - deletes all keys in the input reader. 307 func (c Config) DelFrom(r io.Reader) error { 308 scanner := bufio.NewScanner(r) 309 for scanner.Scan() { 310 // Skip any empty lines, or comment like characters 311 text := scanner.Text() 312 if text == "" || strings.HasPrefix(text, KvComment) { 313 continue 314 } 315 if err := c.DelKVS(text); err != nil { 316 return err 317 } 318 } 319 if err := scanner.Err(); err != nil { 320 return err 321 } 322 return nil 323 } 324 325 // ReadConfig - read content from input and write into c. 326 // Returns whether all parameters were dynamic. 327 func (c Config) ReadConfig(r io.Reader) (dynOnly bool, err error) { 328 var n int 329 scanner := bufio.NewScanner(r) 330 dynOnly = true 331 for scanner.Scan() { 332 // Skip any empty lines, or comment like characters 333 text := scanner.Text() 334 if text == "" || strings.HasPrefix(text, KvComment) { 335 continue 336 } 337 dynamic, err := c.SetKVS(text, DefaultKVS) 338 if err != nil { 339 return false, err 340 } 341 dynOnly = dynOnly && dynamic 342 n += len(text) 343 } 344 if err := scanner.Err(); err != nil { 345 return false, err 346 } 347 return dynOnly, nil 348 } 349 350 type configWriteTo struct { 351 Config 352 filterByKey string 353 } 354 355 // NewConfigWriteTo - returns a struct which 356 // allows for serializing the config/kv struct 357 // to a io.WriterTo 358 func NewConfigWriteTo(cfg Config, key string) io.WriterTo { 359 return &configWriteTo{Config: cfg, filterByKey: key} 360 } 361 362 // WriteTo - implements io.WriterTo interface implementation for config. 363 func (c *configWriteTo) WriteTo(w io.Writer) (int64, error) { 364 kvsTargets, err := c.GetKVS(c.filterByKey, DefaultKVS) 365 if err != nil { 366 return 0, err 367 } 368 var n int 369 for _, target := range kvsTargets { 370 m1, _ := w.Write([]byte(target.SubSystem)) 371 m2, _ := w.Write([]byte(KvSpaceSeparator)) 372 m3, _ := w.Write([]byte(target.KVS.String())) 373 if len(kvsTargets) > 1 { 374 m4, _ := w.Write([]byte(KvNewline)) 375 n += m1 + m2 + m3 + m4 376 } else { 377 n += m1 + m2 + m3 378 } 379 } 380 return int64(n), nil 381 } 382 383 // Default KV configs for worm and region 384 var ( 385 DefaultCredentialKVS = KVS{ 386 KV{ 387 Key: AccessKey, 388 Value: auth.DefaultAccessKey, 389 }, 390 KV{ 391 Key: SecretKey, 392 Value: auth.DefaultSecretKey, 393 }, 394 } 395 396 DefaultRegionKVS = KVS{ 397 KV{ 398 Key: RegionName, 399 Value: "", 400 }, 401 } 402 ) 403 404 // LookupCreds - lookup credentials from config. 405 func LookupCreds(kv KVS) (auth.Credentials, error) { 406 if err := CheckValidKeys(CredentialsSubSys, kv, DefaultCredentialKVS); err != nil { 407 return auth.Credentials{}, err 408 } 409 accessKey := kv.Get(AccessKey) 410 secretKey := kv.Get(SecretKey) 411 if accessKey == "" || secretKey == "" { 412 accessKey = auth.DefaultAccessKey 413 secretKey = auth.DefaultSecretKey 414 } 415 return auth.CreateCredentials(accessKey, secretKey) 416 } 417 418 var validRegionRegex = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9-_-]+$") 419 420 // LookupRegion - get current region. 421 func LookupRegion(kv KVS) (string, error) { 422 if err := CheckValidKeys(RegionSubSys, kv, DefaultRegionKVS); err != nil { 423 return "", err 424 } 425 region := env.Get(EnvRegion, "") 426 if region == "" { 427 region = env.Get(EnvRegionName, kv.Get(RegionName)) 428 } 429 if region != "" { 430 if validRegionRegex.MatchString(region) { 431 return region, nil 432 } 433 return "", Errorf( 434 "region '%s' is invalid, expected simple characters such as [us-east-1, myregion...]", 435 region) 436 } 437 return "", nil 438 } 439 440 // CheckValidKeys - checks if inputs KVS has the necessary keys, 441 // returns error if it find extra or superflous keys. 442 func CheckValidKeys(subSys string, kv KVS, validKVS KVS) error { 443 nkv := KVS{} 444 for _, kv := range kv { 445 // Comment is a valid key, its also fully optional 446 // ignore it since it is a valid key for all 447 // sub-systems. 448 if kv.Key == Comment { 449 continue 450 } 451 if _, ok := validKVS.Lookup(kv.Key); !ok { 452 nkv = append(nkv, kv) 453 } 454 } 455 if len(nkv) > 0 { 456 return Errorf( 457 "found invalid keys (%s) for '%s' sub-system, use 'mc admin config reset myminio %s' to fix invalid keys", nkv.String(), subSys, subSys) 458 } 459 return nil 460 } 461 462 // LookupWorm - check if worm is enabled 463 func LookupWorm() (bool, error) { 464 return ParseBool(env.Get(EnvWorm, EnableOff)) 465 } 466 467 // Carries all the renamed sub-systems from their 468 // previously known names 469 var renamedSubsys = map[string]string{ 470 CrawlerSubSys: ScannerSubSys, 471 // Add future sub-system renames 472 } 473 474 // Merge - merges a new config with all the 475 // missing values for default configs, 476 // returns a config. 477 func (c Config) Merge() Config { 478 cp := New() 479 for subSys, tgtKV := range c { 480 for tgt := range tgtKV { 481 ckvs := c[subSys][tgt] 482 for _, kv := range cp[subSys][Default] { 483 _, ok := c[subSys][tgt].Lookup(kv.Key) 484 if !ok { 485 ckvs.Set(kv.Key, kv.Value) 486 } 487 } 488 if _, ok := cp[subSys]; !ok { 489 rnSubSys, ok := renamedSubsys[subSys] 490 if !ok { 491 // A config subsystem was removed or server was downgraded. 492 Logger.Info("config: ignoring unknown subsystem config %q\n", subSys) 493 continue 494 } 495 // Copy over settings from previous sub-system 496 // to newly renamed sub-system 497 for _, kv := range cp[rnSubSys][Default] { 498 _, ok := c[subSys][tgt].Lookup(kv.Key) 499 if !ok { 500 ckvs.Set(kv.Key, kv.Value) 501 } 502 } 503 subSys = rnSubSys 504 } 505 cp[subSys][tgt] = ckvs 506 } 507 } 508 return cp 509 } 510 511 // New - initialize a new server config. 512 func New() Config { 513 srvCfg := make(Config) 514 for _, k := range SubSystems.ToSlice() { 515 srvCfg[k] = map[string]KVS{} 516 srvCfg[k][Default] = DefaultKVS[k] 517 } 518 return srvCfg 519 } 520 521 // Target signifies an individual target 522 type Target struct { 523 SubSystem string 524 KVS KVS 525 } 526 527 // Targets sub-system targets 528 type Targets []Target 529 530 // GetKVS - get kvs from specific subsystem. 531 func (c Config) GetKVS(s string, defaultKVS map[string]KVS) (Targets, error) { 532 if len(s) == 0 { 533 return nil, Errorf("input cannot be empty") 534 } 535 inputs := strings.Fields(s) 536 if len(inputs) > 1 { 537 return nil, Errorf("invalid number of arguments %s", s) 538 } 539 subSystemValue := strings.SplitN(inputs[0], SubSystemSeparator, 2) 540 if len(subSystemValue) == 0 { 541 return nil, Errorf("invalid number of arguments %s", s) 542 } 543 found := SubSystems.Contains(subSystemValue[0]) 544 if !found { 545 // Check for sub-prefix only if the input value is only a 546 // single value, this rejects invalid inputs if any. 547 found = !SubSystems.FuncMatch(strings.HasPrefix, subSystemValue[0]).IsEmpty() && len(subSystemValue) == 1 548 } 549 if !found { 550 return nil, Errorf("unknown sub-system %s", s) 551 } 552 553 targets := Targets{} 554 subSysPrefix := subSystemValue[0] 555 if len(subSystemValue) == 2 { 556 if len(subSystemValue[1]) == 0 { 557 return nil, Errorf("sub-system target '%s' cannot be empty", s) 558 } 559 kvs, ok := c[subSysPrefix][subSystemValue[1]] 560 if !ok { 561 return nil, Errorf("sub-system target '%s' doesn't exist", s) 562 } 563 for _, kv := range defaultKVS[subSysPrefix] { 564 _, ok = kvs.Lookup(kv.Key) 565 if !ok { 566 kvs.Set(kv.Key, kv.Value) 567 } 568 } 569 targets = append(targets, Target{ 570 SubSystem: inputs[0], 571 KVS: kvs, 572 }) 573 } else { 574 hkvs := HelpSubSysMap[""] 575 // Use help for sub-system to preserve the order. 576 for _, hkv := range hkvs { 577 if !strings.HasPrefix(hkv.Key, subSysPrefix) { 578 continue 579 } 580 if c[hkv.Key][Default].Empty() { 581 targets = append(targets, Target{ 582 SubSystem: hkv.Key, 583 KVS: defaultKVS[hkv.Key], 584 }) 585 } 586 for k, kvs := range c[hkv.Key] { 587 for _, dkv := range defaultKVS[hkv.Key] { 588 _, ok := kvs.Lookup(dkv.Key) 589 if !ok { 590 kvs.Set(dkv.Key, dkv.Value) 591 } 592 } 593 if k != Default { 594 targets = append(targets, Target{ 595 SubSystem: hkv.Key + SubSystemSeparator + k, 596 KVS: kvs, 597 }) 598 } else { 599 targets = append(targets, Target{ 600 SubSystem: hkv.Key, 601 KVS: kvs, 602 }) 603 } 604 } 605 } 606 } 607 return targets, nil 608 } 609 610 // DelKVS - delete a specific key. 611 func (c Config) DelKVS(s string) error { 612 if len(s) == 0 { 613 return Errorf("input arguments cannot be empty") 614 } 615 inputs := strings.Fields(s) 616 if len(inputs) > 1 { 617 return Errorf("invalid number of arguments %s", s) 618 } 619 subSystemValue := strings.SplitN(inputs[0], SubSystemSeparator, 2) 620 if len(subSystemValue) == 0 { 621 return Errorf("invalid number of arguments %s", s) 622 } 623 if !SubSystems.Contains(subSystemValue[0]) { 624 // Unknown sub-system found try to remove it anyways. 625 delete(c, subSystemValue[0]) 626 return nil 627 } 628 tgt := Default 629 subSys := subSystemValue[0] 630 if len(subSystemValue) == 2 { 631 if len(subSystemValue[1]) == 0 { 632 return Errorf("sub-system target '%s' cannot be empty", s) 633 } 634 tgt = subSystemValue[1] 635 } 636 _, ok := c[subSys][tgt] 637 if !ok { 638 return Errorf("sub-system %s already deleted", s) 639 } 640 delete(c[subSys], tgt) 641 return nil 642 } 643 644 // Clone - clones a config map entirely. 645 func (c Config) Clone() Config { 646 cp := New() 647 for subSys, tgtKV := range c { 648 cp[subSys] = make(map[string]KVS) 649 for tgt, kv := range tgtKV { 650 cp[subSys][tgt] = append(cp[subSys][tgt], kv...) 651 } 652 } 653 return cp 654 } 655 656 // SetKVS - set specific key values per sub-system. 657 func (c Config) SetKVS(s string, defaultKVS map[string]KVS) (dynamic bool, err error) { 658 if len(s) == 0 { 659 return false, Errorf("input arguments cannot be empty") 660 } 661 inputs := strings.SplitN(s, KvSpaceSeparator, 2) 662 if len(inputs) <= 1 { 663 return false, Errorf("invalid number of arguments '%s'", s) 664 } 665 subSystemValue := strings.SplitN(inputs[0], SubSystemSeparator, 2) 666 if len(subSystemValue) == 0 { 667 return false, Errorf("invalid number of arguments %s", s) 668 } 669 670 if !SubSystems.Contains(subSystemValue[0]) { 671 return false, Errorf("unknown sub-system %s", s) 672 } 673 674 if SubSystemsSingleTargets.Contains(subSystemValue[0]) && len(subSystemValue) == 2 { 675 return false, Errorf("sub-system '%s' only supports single target", subSystemValue[0]) 676 } 677 dynamic = SubSystemsDynamic.Contains(subSystemValue[0]) 678 679 tgt := Default 680 subSys := subSystemValue[0] 681 if len(subSystemValue) == 2 { 682 tgt = subSystemValue[1] 683 } 684 685 fields := madmin.KvFields(inputs[1], defaultKVS[subSys].Keys()) 686 if len(fields) == 0 { 687 return false, Errorf("sub-system '%s' cannot have empty keys", subSys) 688 } 689 690 var kvs = KVS{} 691 var prevK string 692 for _, v := range fields { 693 kv := strings.SplitN(v, KvSeparator, 2) 694 if len(kv) == 0 { 695 continue 696 } 697 if len(kv) == 1 && prevK != "" { 698 value := strings.Join([]string{ 699 kvs.Get(prevK), 700 madmin.SanitizeValue(kv[0]), 701 }, KvSpaceSeparator) 702 kvs.Set(prevK, value) 703 continue 704 } 705 if len(kv) == 2 { 706 prevK = kv[0] 707 kvs.Set(prevK, madmin.SanitizeValue(kv[1])) 708 continue 709 } 710 return false, Errorf("key '%s', cannot have empty value", kv[0]) 711 } 712 713 _, ok := kvs.Lookup(Enable) 714 // Check if state is required 715 _, enableRequired := defaultKVS[subSys].Lookup(Enable) 716 if !ok && enableRequired { 717 // implicit state "on" if not specified. 718 kvs.Set(Enable, EnableOn) 719 } 720 721 currKVS, ok := c[subSys][tgt] 722 if !ok { 723 currKVS = defaultKVS[subSys] 724 } else { 725 for _, kv := range defaultKVS[subSys] { 726 if _, ok = currKVS.Lookup(kv.Key); !ok { 727 currKVS.Set(kv.Key, kv.Value) 728 } 729 } 730 } 731 732 for _, kv := range kvs { 733 if kv.Key == Comment { 734 // Skip comment and add it later. 735 continue 736 } 737 currKVS.Set(kv.Key, kv.Value) 738 } 739 740 v, ok := kvs.Lookup(Comment) 741 if ok { 742 currKVS.Set(Comment, v) 743 } 744 745 hkvs := HelpSubSysMap[subSys] 746 for _, hkv := range hkvs { 747 var enabled bool 748 if enableRequired { 749 enabled = currKVS.Get(Enable) == EnableOn 750 } else { 751 // when enable arg is not required 752 // then it is implicit on for the sub-system. 753 enabled = true 754 } 755 v, _ := currKVS.Lookup(hkv.Key) 756 if v == "" && !hkv.Optional && enabled { 757 // Return error only if the 758 // key is enabled, for state=off 759 // let it be empty. 760 return false, Errorf( 761 "'%s' is not optional for '%s' sub-system, please check '%s' documentation", 762 hkv.Key, subSys, subSys) 763 } 764 } 765 c[subSys][tgt] = currKVS 766 return dynamic, nil 767 }