mirror of https://github.com/minio/minio.git
1319 lines
33 KiB
Go
1319 lines
33 KiB
Go
// Copyright (c) 2015-2021 MinIO, Inc.
|
|
//
|
|
// This file is part of MinIO Object Storage stack
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package config
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/minio/madmin-go"
|
|
"github.com/minio/minio-go/v7/pkg/set"
|
|
"github.com/minio/minio/internal/auth"
|
|
"github.com/minio/pkg/env"
|
|
)
|
|
|
|
// Error config error type
|
|
type Error struct {
|
|
Err string
|
|
}
|
|
|
|
// Errorf - formats according to a format specifier and returns
|
|
// the string as a value that satisfies error of type config.Error
|
|
func Errorf(format string, a ...interface{}) error {
|
|
return Error{Err: fmt.Sprintf(format, a...)}
|
|
}
|
|
|
|
func (e Error) Error() string {
|
|
return e.Err
|
|
}
|
|
|
|
// Default keys
|
|
const (
|
|
Default = madmin.Default
|
|
Enable = madmin.EnableKey
|
|
Comment = madmin.CommentKey
|
|
|
|
EnvSeparator = "="
|
|
|
|
// Enable values
|
|
EnableOn = madmin.EnableOn
|
|
EnableOff = madmin.EnableOff
|
|
|
|
RegionKey = "region"
|
|
NameKey = "name"
|
|
RegionName = "name"
|
|
AccessKey = "access_key"
|
|
SecretKey = "secret_key"
|
|
License = "license" // Deprecated Dec 2021
|
|
APIKey = "api_key"
|
|
Proxy = "proxy"
|
|
)
|
|
|
|
// Top level config constants.
|
|
const (
|
|
CredentialsSubSys = madmin.CredentialsSubSys
|
|
PolicyOPASubSys = madmin.PolicyOPASubSys
|
|
PolicyPluginSubSys = madmin.PolicyPluginSubSys
|
|
IdentityOpenIDSubSys = madmin.IdentityOpenIDSubSys
|
|
IdentityLDAPSubSys = madmin.IdentityLDAPSubSys
|
|
IdentityTLSSubSys = madmin.IdentityTLSSubSys
|
|
IdentityPluginSubSys = madmin.IdentityPluginSubSys
|
|
CacheSubSys = madmin.CacheSubSys
|
|
SiteSubSys = madmin.SiteSubSys
|
|
RegionSubSys = madmin.RegionSubSys
|
|
EtcdSubSys = madmin.EtcdSubSys
|
|
StorageClassSubSys = madmin.StorageClassSubSys
|
|
APISubSys = madmin.APISubSys
|
|
CompressionSubSys = madmin.CompressionSubSys
|
|
LoggerWebhookSubSys = madmin.LoggerWebhookSubSys
|
|
AuditWebhookSubSys = madmin.AuditWebhookSubSys
|
|
AuditKafkaSubSys = madmin.AuditKafkaSubSys
|
|
HealSubSys = madmin.HealSubSys
|
|
ScannerSubSys = madmin.ScannerSubSys
|
|
CrawlerSubSys = madmin.CrawlerSubSys
|
|
SubnetSubSys = madmin.SubnetSubSys
|
|
CallhomeSubSys = madmin.CallhomeSubSys
|
|
|
|
// Add new constants here (similar to above) if you add new fields to config.
|
|
)
|
|
|
|
// Notification config constants.
|
|
const (
|
|
NotifyKafkaSubSys = madmin.NotifyKafkaSubSys
|
|
NotifyMQTTSubSys = madmin.NotifyMQTTSubSys
|
|
NotifyMySQLSubSys = madmin.NotifyMySQLSubSys
|
|
NotifyNATSSubSys = madmin.NotifyNATSSubSys
|
|
NotifyNSQSubSys = madmin.NotifyNSQSubSys
|
|
NotifyESSubSys = madmin.NotifyESSubSys
|
|
NotifyAMQPSubSys = madmin.NotifyAMQPSubSys
|
|
NotifyPostgresSubSys = madmin.NotifyPostgresSubSys
|
|
NotifyRedisSubSys = madmin.NotifyRedisSubSys
|
|
NotifyWebhookSubSys = madmin.NotifyWebhookSubSys
|
|
|
|
// Add new constants here (similar to above) if you add new fields to config.
|
|
)
|
|
|
|
// NotifySubSystems - all notification sub-systems
|
|
var NotifySubSystems = set.CreateStringSet(
|
|
NotifyKafkaSubSys,
|
|
NotifyMQTTSubSys,
|
|
NotifyMySQLSubSys,
|
|
NotifyNATSSubSys,
|
|
NotifyNSQSubSys,
|
|
NotifyESSubSys,
|
|
NotifyAMQPSubSys,
|
|
NotifyPostgresSubSys,
|
|
NotifyRedisSubSys,
|
|
NotifyWebhookSubSys,
|
|
)
|
|
|
|
// LoggerSubSystems - all sub-systems related to logger
|
|
var LoggerSubSystems = set.CreateStringSet(
|
|
LoggerWebhookSubSys,
|
|
AuditWebhookSubSys,
|
|
AuditKafkaSubSys,
|
|
)
|
|
|
|
// SubSystems - all supported sub-systems
|
|
var SubSystems = madmin.SubSystems
|
|
|
|
// SubSystemsDynamic - all sub-systems that have dynamic config.
|
|
var SubSystemsDynamic = set.CreateStringSet(
|
|
APISubSys,
|
|
CompressionSubSys,
|
|
ScannerSubSys,
|
|
HealSubSys,
|
|
SubnetSubSys,
|
|
CallhomeSubSys,
|
|
LoggerWebhookSubSys,
|
|
AuditWebhookSubSys,
|
|
AuditKafkaSubSys,
|
|
StorageClassSubSys,
|
|
)
|
|
|
|
// SubSystemsSingleTargets - subsystems which only support single target.
|
|
var SubSystemsSingleTargets = set.CreateStringSet(
|
|
CredentialsSubSys,
|
|
SiteSubSys,
|
|
RegionSubSys,
|
|
EtcdSubSys,
|
|
CacheSubSys,
|
|
APISubSys,
|
|
StorageClassSubSys,
|
|
CompressionSubSys,
|
|
PolicyOPASubSys,
|
|
PolicyPluginSubSys,
|
|
IdentityLDAPSubSys,
|
|
IdentityTLSSubSys,
|
|
IdentityPluginSubSys,
|
|
HealSubSys,
|
|
ScannerSubSys,
|
|
SubnetSubSys,
|
|
CallhomeSubSys,
|
|
)
|
|
|
|
// Constant separators
|
|
const (
|
|
SubSystemSeparator = madmin.SubSystemSeparator
|
|
KvSeparator = madmin.KvSeparator
|
|
KvSpaceSeparator = madmin.KvSpaceSeparator
|
|
KvComment = madmin.KvComment
|
|
KvNewline = madmin.KvNewline
|
|
KvDoubleQuote = madmin.KvDoubleQuote
|
|
KvSingleQuote = madmin.KvSingleQuote
|
|
|
|
// Env prefix used for all envs in MinIO
|
|
EnvPrefix = madmin.EnvPrefix
|
|
EnvWordDelimiter = madmin.EnvWordDelimiter
|
|
)
|
|
|
|
// DefaultKVS - default kvs for all sub-systems
|
|
var DefaultKVS map[string]KVS
|
|
|
|
// RegisterDefaultKVS - this function saves input kvsMap
|
|
// globally, this should be called only once preferably
|
|
// during `init()`.
|
|
func RegisterDefaultKVS(kvsMap map[string]KVS) {
|
|
DefaultKVS = map[string]KVS{}
|
|
for subSys, kvs := range kvsMap {
|
|
DefaultKVS[subSys] = kvs
|
|
}
|
|
}
|
|
|
|
// HelpSubSysMap - help for all individual KVS for each sub-systems
|
|
// also carries a special empty sub-system which dumps
|
|
// help for each sub-system key.
|
|
var HelpSubSysMap map[string]HelpKVS
|
|
|
|
// RegisterHelpSubSys - this function saves
|
|
// input help KVS for each sub-system globally,
|
|
// this function should be called only once
|
|
// preferably in during `init()`.
|
|
func RegisterHelpSubSys(helpKVSMap map[string]HelpKVS) {
|
|
HelpSubSysMap = map[string]HelpKVS{}
|
|
for subSys, hkvs := range helpKVSMap {
|
|
HelpSubSysMap[subSys] = hkvs
|
|
}
|
|
}
|
|
|
|
// HelpDeprecatedSubSysMap - help for all deprecated sub-systems, that may be
|
|
// removed in the future.
|
|
var HelpDeprecatedSubSysMap map[string]HelpKV
|
|
|
|
// RegisterHelpDeprecatedSubSys - saves input help KVS for deprecated
|
|
// sub-systems globally. Should be called only once at init.
|
|
func RegisterHelpDeprecatedSubSys(helpDeprecatedKVMap map[string]HelpKV) {
|
|
HelpDeprecatedSubSysMap = map[string]HelpKV{}
|
|
for k, v := range helpDeprecatedKVMap {
|
|
HelpDeprecatedSubSysMap[k] = v
|
|
}
|
|
}
|
|
|
|
// KV - is a shorthand of each key value.
|
|
type KV struct {
|
|
Key string `json:"key"`
|
|
Value string `json:"value"`
|
|
|
|
Deprecated bool `json:"-"`
|
|
}
|
|
|
|
func (kv KV) String() string {
|
|
var s strings.Builder
|
|
s.WriteString(kv.Key)
|
|
s.WriteString(KvSeparator)
|
|
spc := madmin.HasSpace(kv.Value)
|
|
if spc {
|
|
s.WriteString(KvDoubleQuote)
|
|
}
|
|
s.WriteString(kv.Value)
|
|
if spc {
|
|
s.WriteString(KvDoubleQuote)
|
|
}
|
|
return s.String()
|
|
}
|
|
|
|
// KVS - is a shorthand for some wrapper functions
|
|
// to operate on list of key values.
|
|
type KVS []KV
|
|
|
|
// Empty - return if kv is empty
|
|
func (kvs KVS) Empty() bool {
|
|
return len(kvs) == 0
|
|
}
|
|
|
|
// Clone - returns a copy of the KVS
|
|
func (kvs KVS) Clone() KVS {
|
|
return append(make(KVS, 0, len(kvs)), kvs...)
|
|
}
|
|
|
|
// GetWithDefault - returns default value if key not set
|
|
func (kvs KVS) GetWithDefault(key string, defaultKVS KVS) string {
|
|
v := kvs.Get(key)
|
|
if len(v) == 0 {
|
|
return defaultKVS.Get(key)
|
|
}
|
|
return v
|
|
}
|
|
|
|
// Keys returns the list of keys for the current KVS
|
|
func (kvs KVS) Keys() []string {
|
|
keys := make([]string, len(kvs))
|
|
var foundComment bool
|
|
for i := range kvs {
|
|
if kvs[i].Key == madmin.CommentKey {
|
|
foundComment = true
|
|
}
|
|
keys[i] = kvs[i].Key
|
|
}
|
|
// Comment KV not found, add it explicitly.
|
|
if !foundComment {
|
|
keys = append(keys, madmin.CommentKey)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
func (kvs KVS) String() string {
|
|
var s strings.Builder
|
|
for _, kv := range kvs {
|
|
s.WriteString(kv.String())
|
|
s.WriteString(KvSpaceSeparator)
|
|
}
|
|
return s.String()
|
|
}
|
|
|
|
// Merge environment values with on disk KVS, environment values overrides
|
|
// anything on the disk.
|
|
func Merge(cfgKVS map[string]KVS, envname string, defaultKVS KVS) map[string]KVS {
|
|
newCfgKVS := make(map[string]KVS)
|
|
for _, e := range env.List(envname) {
|
|
tgt := strings.TrimPrefix(e, envname+Default)
|
|
if tgt == envname {
|
|
tgt = Default
|
|
}
|
|
newCfgKVS[tgt] = defaultKVS
|
|
}
|
|
for tgt, kv := range cfgKVS {
|
|
newCfgKVS[tgt] = kv
|
|
}
|
|
return newCfgKVS
|
|
}
|
|
|
|
// Set sets a value, if not sets a default value.
|
|
func (kvs *KVS) Set(key, value string) {
|
|
for i, kv := range *kvs {
|
|
if kv.Key == key {
|
|
(*kvs)[i] = KV{
|
|
Key: key,
|
|
Value: value,
|
|
}
|
|
return
|
|
}
|
|
}
|
|
*kvs = append(*kvs, KV{
|
|
Key: key,
|
|
Value: value,
|
|
})
|
|
}
|
|
|
|
// Get - returns the value of a key, if not found returns empty.
|
|
func (kvs KVS) Get(key string) string {
|
|
v, ok := kvs.Lookup(key)
|
|
if ok {
|
|
return v
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Delete - deletes the key if present from the KV list.
|
|
func (kvs *KVS) Delete(key string) {
|
|
for i, kv := range *kvs {
|
|
if kv.Key == key {
|
|
*kvs = append((*kvs)[:i], (*kvs)[i+1:]...)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// LookupKV returns the KV by its key
|
|
func (kvs KVS) LookupKV(key string) (KV, bool) {
|
|
for _, kv := range kvs {
|
|
if kv.Key == key {
|
|
return kv, true
|
|
}
|
|
}
|
|
return KV{}, false
|
|
}
|
|
|
|
// Lookup - lookup a key in a list of KVS
|
|
func (kvs KVS) Lookup(key string) (string, bool) {
|
|
for _, kv := range kvs {
|
|
if kv.Key == key {
|
|
return kv.Value, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// Config - MinIO server config structure.
|
|
type Config map[string]map[string]KVS
|
|
|
|
// DelFrom - deletes all keys in the input reader.
|
|
func (c Config) DelFrom(r io.Reader) error {
|
|
scanner := bufio.NewScanner(r)
|
|
for scanner.Scan() {
|
|
// Skip any empty lines, or comment like characters
|
|
text := scanner.Text()
|
|
if text == "" || strings.HasPrefix(text, KvComment) {
|
|
continue
|
|
}
|
|
if err := c.DelKVS(text); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return scanner.Err()
|
|
}
|
|
|
|
// ReadConfig - read content from input and write into c.
|
|
// Returns whether all parameters were dynamic.
|
|
func (c Config) ReadConfig(r io.Reader) (dynOnly bool, err error) {
|
|
var n int
|
|
scanner := bufio.NewScanner(r)
|
|
dynOnly = true
|
|
for scanner.Scan() {
|
|
// Skip any empty lines, or comment like characters
|
|
text := scanner.Text()
|
|
if text == "" || strings.HasPrefix(text, KvComment) {
|
|
continue
|
|
}
|
|
dynamic, err := c.SetKVS(text, DefaultKVS)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
dynOnly = dynOnly && dynamic
|
|
n += len(text)
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return false, err
|
|
}
|
|
return dynOnly, nil
|
|
}
|
|
|
|
// RedactSensitiveInfo - removes sensitive information
|
|
// like urls and credentials from the configuration
|
|
func (c Config) RedactSensitiveInfo() Config {
|
|
nc := c.Clone()
|
|
|
|
for configName, configVals := range nc {
|
|
for _, helpKV := range HelpSubSysMap[configName] {
|
|
if helpKV.Sensitive {
|
|
for name, kvs := range configVals {
|
|
for i := range kvs {
|
|
if kvs[i].Key == helpKV.Key && len(kvs[i].Value) > 0 {
|
|
kvs[i].Value = "*redacted*"
|
|
}
|
|
}
|
|
configVals[name] = kvs
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove the server credentials altogether
|
|
nc.DelKVS(CredentialsSubSys)
|
|
|
|
return nc
|
|
}
|
|
|
|
// Default KV configs for worm and region
|
|
var (
|
|
DefaultCredentialKVS = KVS{
|
|
KV{
|
|
Key: AccessKey,
|
|
Value: auth.DefaultAccessKey,
|
|
},
|
|
KV{
|
|
Key: SecretKey,
|
|
Value: auth.DefaultSecretKey,
|
|
},
|
|
}
|
|
|
|
DefaultSiteKVS = KVS{
|
|
KV{
|
|
Key: NameKey,
|
|
Value: "",
|
|
},
|
|
KV{
|
|
Key: RegionKey,
|
|
Value: "",
|
|
},
|
|
}
|
|
|
|
DefaultRegionKVS = KVS{
|
|
KV{
|
|
Key: RegionName,
|
|
Value: "",
|
|
},
|
|
}
|
|
)
|
|
|
|
// LookupCreds - lookup credentials from config.
|
|
func LookupCreds(kv KVS) (auth.Credentials, error) {
|
|
if err := CheckValidKeys(CredentialsSubSys, kv, DefaultCredentialKVS); err != nil {
|
|
return auth.Credentials{}, err
|
|
}
|
|
accessKey := kv.Get(AccessKey)
|
|
secretKey := kv.Get(SecretKey)
|
|
if accessKey == "" || secretKey == "" {
|
|
accessKey = auth.DefaultAccessKey
|
|
secretKey = auth.DefaultSecretKey
|
|
}
|
|
return auth.CreateCredentials(accessKey, secretKey)
|
|
}
|
|
|
|
// Site - holds site info - name and region.
|
|
type Site struct {
|
|
Name string
|
|
Region string
|
|
}
|
|
|
|
var validRegionRegex = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9-_-]+$")
|
|
|
|
// validSiteNameRegex - allows lowercase letters, digits and '-', starts with
|
|
// letter. At least 2 characters long.
|
|
var validSiteNameRegex = regexp.MustCompile("^[a-z][a-z0-9-]+$")
|
|
|
|
// LookupSite - get site related configuration. Loads configuration from legacy
|
|
// region sub-system as well.
|
|
func LookupSite(siteKV KVS, regionKV KVS) (s Site, err error) {
|
|
if err = CheckValidKeys(SiteSubSys, siteKV, DefaultSiteKVS); err != nil {
|
|
return
|
|
}
|
|
region := env.Get(EnvRegion, "")
|
|
if region == "" {
|
|
env.Get(EnvRegionName, "")
|
|
}
|
|
if region == "" {
|
|
region = env.Get(EnvSiteRegion, siteKV.Get(RegionKey))
|
|
}
|
|
if region == "" {
|
|
// No region config found in the site-subsystem. So lookup the legacy
|
|
// region sub-system.
|
|
if err = CheckValidKeys(RegionSubSys, regionKV, DefaultRegionKVS); err != nil {
|
|
// An invalid key was found in the region sub-system.
|
|
// Since the region sub-system cannot be (re)set as it
|
|
// is legacy, we return an error to tell the user to
|
|
// reset the region via the new command.
|
|
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)
|
|
return
|
|
}
|
|
|
|
region = regionKV.Get(RegionName)
|
|
}
|
|
if region != "" {
|
|
if !validRegionRegex.MatchString(region) {
|
|
err = Errorf(
|
|
"region '%s' is invalid, expected simple characters such as [us-east-1, myregion...]",
|
|
region)
|
|
return
|
|
}
|
|
s.Region = region
|
|
}
|
|
|
|
name := env.Get(EnvSiteName, siteKV.Get(NameKey))
|
|
if name != "" {
|
|
if !validSiteNameRegex.MatchString(name) {
|
|
err = Errorf(
|
|
"site name '%s' is invalid, expected simple characters such as [cal-rack0, myname...]",
|
|
name)
|
|
return
|
|
}
|
|
s.Name = name
|
|
}
|
|
return
|
|
}
|
|
|
|
// CheckValidKeys - checks if inputs KVS has the necessary keys,
|
|
// returns error if it find extra or superflous keys.
|
|
func CheckValidKeys(subSys string, kv KVS, validKVS KVS) error {
|
|
nkv := KVS{}
|
|
for _, kv := range kv {
|
|
// Comment is a valid key, its also fully optional
|
|
// ignore it since it is a valid key for all
|
|
// sub-systems.
|
|
if kv.Key == Comment {
|
|
continue
|
|
}
|
|
if _, ok := validKVS.Lookup(kv.Key); !ok {
|
|
nkv = append(nkv, kv)
|
|
}
|
|
}
|
|
if len(nkv) > 0 {
|
|
return Errorf(
|
|
"found invalid keys (%s) for '%s' sub-system, use 'mc admin config reset myminio %s' to fix invalid keys", nkv.String(), subSys, subSys)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LookupWorm - check if worm is enabled
|
|
func LookupWorm() (bool, error) {
|
|
return ParseBool(env.Get(EnvWorm, EnableOff))
|
|
}
|
|
|
|
// Carries all the renamed sub-systems from their
|
|
// previously known names
|
|
var renamedSubsys = map[string]string{
|
|
CrawlerSubSys: ScannerSubSys,
|
|
// Add future sub-system renames
|
|
}
|
|
|
|
// Merge - merges a new config with all the
|
|
// missing values for default configs,
|
|
// returns a config.
|
|
func (c Config) Merge() Config {
|
|
cp := New()
|
|
for subSys, tgtKV := range c {
|
|
for tgt := range tgtKV {
|
|
ckvs := c[subSys][tgt]
|
|
for _, kv := range cp[subSys][Default] {
|
|
_, ok := c[subSys][tgt].Lookup(kv.Key)
|
|
if !ok {
|
|
ckvs.Set(kv.Key, kv.Value)
|
|
}
|
|
}
|
|
if _, ok := cp[subSys]; !ok {
|
|
rnSubSys, ok := renamedSubsys[subSys]
|
|
if !ok {
|
|
// A config subsystem was removed or server was downgraded.
|
|
continue
|
|
}
|
|
// Copy over settings from previous sub-system
|
|
// to newly renamed sub-system
|
|
for _, kv := range cp[rnSubSys][Default] {
|
|
_, ok := c[subSys][tgt].Lookup(kv.Key)
|
|
if !ok {
|
|
ckvs.Set(kv.Key, kv.Value)
|
|
}
|
|
}
|
|
subSys = rnSubSys
|
|
}
|
|
cp[subSys][tgt] = ckvs
|
|
}
|
|
}
|
|
return cp
|
|
}
|
|
|
|
// New - initialize a new server config.
|
|
func New() Config {
|
|
srvCfg := make(Config)
|
|
for _, k := range SubSystems.ToSlice() {
|
|
srvCfg[k] = map[string]KVS{}
|
|
srvCfg[k][Default] = DefaultKVS[k]
|
|
}
|
|
return srvCfg
|
|
}
|
|
|
|
// Target signifies an individual target
|
|
type Target struct {
|
|
SubSystem string
|
|
KVS KVS
|
|
}
|
|
|
|
// Targets sub-system targets
|
|
type Targets []Target
|
|
|
|
// GetKVS - get kvs from specific subsystem.
|
|
func (c Config) GetKVS(s string, defaultKVS map[string]KVS) (Targets, error) {
|
|
if len(s) == 0 {
|
|
return nil, Errorf("input cannot be empty")
|
|
}
|
|
inputs := strings.Fields(s)
|
|
if len(inputs) > 1 {
|
|
return nil, Errorf("invalid number of arguments %s", s)
|
|
}
|
|
subSystemValue := strings.SplitN(inputs[0], SubSystemSeparator, 2)
|
|
if len(subSystemValue) == 0 {
|
|
return nil, Errorf("invalid number of arguments %s", s)
|
|
}
|
|
found := SubSystems.Contains(subSystemValue[0])
|
|
if !found {
|
|
// Check for sub-prefix only if the input value is only a
|
|
// single value, this rejects invalid inputs if any.
|
|
found = !SubSystems.FuncMatch(strings.HasPrefix, subSystemValue[0]).IsEmpty() && len(subSystemValue) == 1
|
|
}
|
|
if !found {
|
|
return nil, Errorf("unknown sub-system %s", s)
|
|
}
|
|
|
|
targets := Targets{}
|
|
subSysPrefix := subSystemValue[0]
|
|
if len(subSystemValue) == 2 {
|
|
if len(subSystemValue[1]) == 0 {
|
|
return nil, Errorf("sub-system target '%s' cannot be empty", s)
|
|
}
|
|
kvs, ok := c[subSysPrefix][subSystemValue[1]]
|
|
if !ok {
|
|
return nil, Errorf("sub-system target '%s' doesn't exist", s)
|
|
}
|
|
for _, kv := range defaultKVS[subSysPrefix] {
|
|
_, ok = kvs.Lookup(kv.Key)
|
|
if !ok {
|
|
kvs.Set(kv.Key, kv.Value)
|
|
}
|
|
}
|
|
targets = append(targets, Target{
|
|
SubSystem: inputs[0],
|
|
KVS: kvs,
|
|
})
|
|
} else {
|
|
// Use help for sub-system to preserve the order. Add deprecated
|
|
// keys at the end (in some order).
|
|
kvsOrder := append([]HelpKV{}, HelpSubSysMap[""]...)
|
|
for _, v := range HelpDeprecatedSubSysMap {
|
|
kvsOrder = append(kvsOrder, v)
|
|
}
|
|
|
|
for _, hkv := range kvsOrder {
|
|
if !strings.HasPrefix(hkv.Key, subSysPrefix) {
|
|
continue
|
|
}
|
|
if c[hkv.Key][Default].Empty() {
|
|
targets = append(targets, Target{
|
|
SubSystem: hkv.Key,
|
|
KVS: defaultKVS[hkv.Key],
|
|
})
|
|
}
|
|
for k, kvs := range c[hkv.Key] {
|
|
for _, dkv := range defaultKVS[hkv.Key] {
|
|
_, ok := kvs.Lookup(dkv.Key)
|
|
if !ok {
|
|
kvs.Set(dkv.Key, dkv.Value)
|
|
}
|
|
}
|
|
if k != Default {
|
|
targets = append(targets, Target{
|
|
SubSystem: hkv.Key + SubSystemSeparator + k,
|
|
KVS: kvs,
|
|
})
|
|
} else {
|
|
targets = append(targets, Target{
|
|
SubSystem: hkv.Key,
|
|
KVS: kvs,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return targets, nil
|
|
}
|
|
|
|
// DelKVS - delete a specific key.
|
|
func (c Config) DelKVS(s string) error {
|
|
subSys, inputs, tgt, err := GetSubSys(s)
|
|
if err != nil {
|
|
if !SubSystems.Contains(subSys) && len(inputs) == 1 {
|
|
// Unknown sub-system found try to remove it anyways.
|
|
delete(c, subSys)
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
ck, ok := c[subSys][tgt]
|
|
if !ok {
|
|
return Errorf("sub-system %s:%s already deleted or does not exist", subSys, tgt)
|
|
}
|
|
|
|
if len(inputs) == 2 {
|
|
currKVS := ck.Clone()
|
|
defKVS := DefaultKVS[subSys]
|
|
for _, delKey := range strings.Fields(inputs[1]) {
|
|
_, ok := currKVS.Lookup(delKey)
|
|
if !ok {
|
|
return Errorf("key %s doesn't exist", delKey)
|
|
}
|
|
defVal, isDef := defKVS.Lookup(delKey)
|
|
if isDef {
|
|
currKVS.Set(delKey, defVal)
|
|
} else {
|
|
currKVS.Delete(delKey)
|
|
}
|
|
}
|
|
c[subSys][tgt] = currKVS
|
|
} else {
|
|
delete(c[subSys], tgt)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Clone - clones a config map entirely.
|
|
func (c Config) Clone() Config {
|
|
cp := New()
|
|
for subSys, tgtKV := range c {
|
|
cp[subSys] = make(map[string]KVS)
|
|
for tgt, kv := range tgtKV {
|
|
cp[subSys][tgt] = append(cp[subSys][tgt], kv...)
|
|
}
|
|
}
|
|
return cp
|
|
}
|
|
|
|
// GetSubSys - extracts subssystem info from given config string
|
|
func GetSubSys(s string) (subSys string, inputs []string, tgt string, e error) {
|
|
tgt = Default
|
|
if len(s) == 0 {
|
|
return subSys, inputs, tgt, Errorf("input arguments cannot be empty")
|
|
}
|
|
inputs = strings.SplitN(s, KvSpaceSeparator, 2)
|
|
|
|
subSystemValue := strings.SplitN(inputs[0], SubSystemSeparator, 2)
|
|
subSys = subSystemValue[0]
|
|
if !SubSystems.Contains(subSys) {
|
|
return subSys, inputs, tgt, Errorf("unknown sub-system %s", s)
|
|
}
|
|
|
|
if SubSystemsSingleTargets.Contains(subSystemValue[0]) && len(subSystemValue) == 2 {
|
|
return subSys, inputs, tgt, Errorf("sub-system '%s' only supports single target", subSystemValue[0])
|
|
}
|
|
|
|
if len(subSystemValue) == 2 {
|
|
tgt = subSystemValue[1]
|
|
}
|
|
|
|
return subSys, inputs, tgt, e
|
|
}
|
|
|
|
// kvFields - converts an input string of form "k1=v1 k2=v2" into
|
|
// fields of ["k1=v1", "k2=v2"], the tokenization of each `k=v`
|
|
// happens with the right number of input keys, if keys
|
|
// input is empty returned value is empty slice as well.
|
|
func kvFields(input string, keys []string) []string {
|
|
valueIndexes := make([]int, 0, len(keys))
|
|
for _, key := range keys {
|
|
i := strings.Index(input, key+KvSeparator)
|
|
if i == -1 {
|
|
continue
|
|
}
|
|
valueIndexes = append(valueIndexes, i)
|
|
}
|
|
|
|
sort.Ints(valueIndexes)
|
|
fields := make([]string, len(valueIndexes))
|
|
for i := range valueIndexes {
|
|
j := i + 1
|
|
if j < len(valueIndexes) {
|
|
fields[i] = strings.TrimSpace(input[valueIndexes[i]:valueIndexes[j]])
|
|
} else {
|
|
fields[i] = strings.TrimSpace(input[valueIndexes[i]:])
|
|
}
|
|
}
|
|
return fields
|
|
}
|
|
|
|
// SetKVS - set specific key values per sub-system.
|
|
func (c Config) SetKVS(s string, defaultKVS map[string]KVS) (dynamic bool, err error) {
|
|
subSys, inputs, tgt, err := GetSubSys(s)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
dynamic = SubSystemsDynamic.Contains(subSys)
|
|
|
|
fields := kvFields(inputs[1], defaultKVS[subSys].Keys())
|
|
if len(fields) == 0 {
|
|
return false, Errorf("sub-system '%s' cannot have empty keys", subSys)
|
|
}
|
|
|
|
kvs := KVS{}
|
|
var prevK string
|
|
for _, v := range fields {
|
|
kv := strings.SplitN(v, KvSeparator, 2)
|
|
if len(kv) == 0 {
|
|
continue
|
|
}
|
|
if len(kv) == 1 && prevK != "" {
|
|
value := strings.Join([]string{
|
|
kvs.Get(prevK),
|
|
madmin.SanitizeValue(kv[0]),
|
|
}, KvSpaceSeparator)
|
|
kvs.Set(prevK, value)
|
|
continue
|
|
}
|
|
if len(kv) == 2 {
|
|
prevK = kv[0]
|
|
kvs.Set(prevK, madmin.SanitizeValue(kv[1]))
|
|
continue
|
|
}
|
|
return false, Errorf("key '%s', cannot have empty value", kv[0])
|
|
}
|
|
|
|
_, ok := kvs.Lookup(Enable)
|
|
// Check if state is required
|
|
_, enableRequired := defaultKVS[subSys].Lookup(Enable)
|
|
if !ok && enableRequired {
|
|
// implicit state "on" if not specified.
|
|
kvs.Set(Enable, EnableOn)
|
|
}
|
|
|
|
var currKVS KVS
|
|
ck, ok := c[subSys][tgt]
|
|
if !ok {
|
|
currKVS = defaultKVS[subSys].Clone()
|
|
} else {
|
|
currKVS = ck.Clone()
|
|
for _, kv := range defaultKVS[subSys] {
|
|
if _, ok = currKVS.Lookup(kv.Key); !ok {
|
|
currKVS.Set(kv.Key, kv.Value)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, kv := range kvs {
|
|
if kv.Key == Comment {
|
|
// Skip comment and add it later.
|
|
continue
|
|
}
|
|
currKVS.Set(kv.Key, kv.Value)
|
|
}
|
|
|
|
v, ok := kvs.Lookup(Comment)
|
|
if ok {
|
|
currKVS.Set(Comment, v)
|
|
}
|
|
|
|
hkvs := HelpSubSysMap[subSys]
|
|
for _, hkv := range hkvs {
|
|
var enabled bool
|
|
if enableRequired {
|
|
enabled = currKVS.Get(Enable) == EnableOn
|
|
} else {
|
|
// when enable arg is not required
|
|
// then it is implicit on for the sub-system.
|
|
enabled = true
|
|
}
|
|
v, _ := currKVS.Lookup(hkv.Key)
|
|
if v == "" && !hkv.Optional && enabled {
|
|
// Return error only if the
|
|
// key is enabled, for state=off
|
|
// let it be empty.
|
|
return false, Errorf(
|
|
"'%s' is not optional for '%s' sub-system, please check '%s' documentation",
|
|
hkv.Key, subSys, subSys)
|
|
}
|
|
}
|
|
c[subSys][tgt] = currKVS
|
|
return dynamic, nil
|
|
}
|
|
|
|
// CheckValidKeys - checks if the config parameters for the given subsystem and
|
|
// target are valid. It checks both the configuration store as well as
|
|
// environment variables.
|
|
func (c Config) CheckValidKeys(subSys string, deprecatedKeys []string) error {
|
|
defKVS, ok := DefaultKVS[subSys]
|
|
if !ok {
|
|
return Errorf("Subsystem %s does not exist", subSys)
|
|
}
|
|
|
|
// Make a list of valid keys for the subsystem including the `comment`
|
|
// key.
|
|
validKeys := make([]string, 0, len(defKVS)+1)
|
|
for _, param := range defKVS {
|
|
validKeys = append(validKeys, param.Key)
|
|
}
|
|
validKeys = append(validKeys, Comment)
|
|
|
|
subSysEnvVars := env.List(fmt.Sprintf("%s%s", EnvPrefix, strings.ToUpper(subSys)))
|
|
|
|
// Set of env vars for the sub-system to validate.
|
|
candidates := set.CreateStringSet(subSysEnvVars...)
|
|
|
|
// Remove all default target env vars from the candidates set (as they
|
|
// are valid).
|
|
for _, param := range validKeys {
|
|
paramEnvName := getEnvVarName(subSys, Default, param)
|
|
candidates.Remove(paramEnvName)
|
|
}
|
|
|
|
isSingleTarget := SubSystemsSingleTargets.Contains(subSys)
|
|
if isSingleTarget && len(candidates) > 0 {
|
|
return Errorf("The following environment variables are unknown: %s",
|
|
strings.Join(candidates.ToSlice(), ", "))
|
|
}
|
|
|
|
if !isSingleTarget {
|
|
// Validate other env vars for all targets.
|
|
envVars := candidates.ToSlice()
|
|
for _, envVar := range envVars {
|
|
for _, param := range validKeys {
|
|
pEnvName := getEnvVarName(subSys, Default, param) + Default
|
|
if len(envVar) > len(pEnvName) && strings.HasPrefix(envVar, pEnvName) {
|
|
// This envVar is valid - it has a
|
|
// non-empty target.
|
|
candidates.Remove(envVar)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Whatever remains are invalid env vars - return an error.
|
|
if len(candidates) > 0 {
|
|
return Errorf("The following environment variables are unknown: %s",
|
|
strings.Join(candidates.ToSlice(), ", "))
|
|
}
|
|
}
|
|
|
|
validKeysSet := set.CreateStringSet(validKeys...)
|
|
validKeysSet = validKeysSet.Difference(set.CreateStringSet(deprecatedKeys...))
|
|
kvsMap := c[subSys]
|
|
for tgt, kvs := range kvsMap {
|
|
invalidKV := KVS{}
|
|
for _, kv := range kvs {
|
|
if !validKeysSet.Contains(kv.Key) {
|
|
invalidKV = append(invalidKV, kv)
|
|
}
|
|
}
|
|
if len(invalidKV) > 0 {
|
|
return Errorf(
|
|
"found invalid keys (%s) for '%s:%s' sub-system, use 'mc admin config reset myminio %s:%s' to fix invalid keys",
|
|
invalidKV.String(), subSys, tgt, subSys, tgt)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetAvailableTargets - returns a list of targets configured for the given
|
|
// subsystem (whether they are enabled or not). A target could be configured via
|
|
// environment variables or via the configuration store. The default target is
|
|
// `_` and is always returned. The result is sorted so that the default target
|
|
// is the first one and the remaining entries are sorted in ascending order.
|
|
func (c Config) GetAvailableTargets(subSys string) ([]string, error) {
|
|
if SubSystemsSingleTargets.Contains(subSys) {
|
|
return []string{Default}, nil
|
|
}
|
|
|
|
defKVS, ok := DefaultKVS[subSys]
|
|
if !ok {
|
|
return nil, Errorf("Subsystem %s does not exist", subSys)
|
|
}
|
|
|
|
kvsMap := c[subSys]
|
|
seen := set.NewStringSet()
|
|
|
|
// Add all targets that are configured in the config store.
|
|
for k := range kvsMap {
|
|
seen.Add(k)
|
|
}
|
|
|
|
// Add targets that are configured via environment variables.
|
|
for _, param := range defKVS {
|
|
envVarPrefix := getEnvVarName(subSys, Default, param.Key) + Default
|
|
envsWithPrefix := env.List(envVarPrefix)
|
|
for _, k := range envsWithPrefix {
|
|
tgtName := strings.TrimPrefix(k, envVarPrefix)
|
|
if tgtName != "" {
|
|
seen.Add(tgtName)
|
|
}
|
|
}
|
|
}
|
|
|
|
seen.Remove(Default)
|
|
targets := seen.ToSlice()
|
|
sort.Strings(targets)
|
|
targets = append([]string{Default}, targets...)
|
|
|
|
return targets, nil
|
|
}
|
|
|
|
func getEnvVarName(subSys, target, param string) string {
|
|
if target == Default {
|
|
return fmt.Sprintf("%s%s%s%s", EnvPrefix, strings.ToUpper(subSys), Default, strings.ToUpper(param))
|
|
}
|
|
|
|
return fmt.Sprintf("%s%s%s%s%s%s", EnvPrefix, strings.ToUpper(subSys), Default, strings.ToUpper(param),
|
|
Default, target)
|
|
}
|
|
|
|
var resolvableSubsystems = set.CreateStringSet(IdentityOpenIDSubSys, IdentityLDAPSubSys)
|
|
|
|
// ValueSource represents the source of a config parameter value.
|
|
type ValueSource uint8
|
|
|
|
// Constants for ValueSource
|
|
const (
|
|
ValueSourceAbsent ValueSource = iota // this is an error case
|
|
ValueSourceDef
|
|
ValueSourceCfg
|
|
ValueSourceEnv
|
|
)
|
|
|
|
// ResolveConfigParam returns the effective value of a configuration parameter,
|
|
// within a subsystem and subsystem target. The effective value is, in order of
|
|
// decreasing precedence:
|
|
//
|
|
// 1. the value of the corresponding environment variable if set,
|
|
// 2. the value of the parameter in the config store if set,
|
|
// 3. the default value,
|
|
//
|
|
// This function only works for a subset of sub-systems, others return
|
|
// `ValueSourceAbsent`. FIXME: some parameters have custom environment
|
|
// variables for which support needs to be added.
|
|
func (c Config) ResolveConfigParam(subSys, target, cfgParam string) (value string, cs ValueSource) {
|
|
// cs = ValueSourceAbsent initially as it is iota by default.
|
|
|
|
// Initially only support OpenID
|
|
if !resolvableSubsystems.Contains(subSys) {
|
|
return
|
|
}
|
|
|
|
// Check if config param requested is valid.
|
|
defKVS, ok := DefaultKVS[subSys]
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
defValue, isFound := defKVS.Lookup(cfgParam)
|
|
// Comments usually are absent from `defKVS`, so we handle it specially.
|
|
if !isFound && cfgParam == Comment {
|
|
defValue, isFound = "", true
|
|
}
|
|
if !isFound {
|
|
return
|
|
}
|
|
|
|
if target == "" {
|
|
target = Default
|
|
}
|
|
|
|
envVar := getEnvVarName(subSys, target, cfgParam)
|
|
|
|
// Lookup Env var.
|
|
value = env.Get(envVar, "")
|
|
if value != "" {
|
|
cs = ValueSourceEnv
|
|
return
|
|
}
|
|
|
|
// Lookup config store.
|
|
if subSysStore, ok := c[subSys]; ok {
|
|
if kvs, ok2 := subSysStore[target]; ok2 {
|
|
var ok3 bool
|
|
value, ok3 = kvs.Lookup(cfgParam)
|
|
if ok3 {
|
|
cs = ValueSourceCfg
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return the default value.
|
|
value = defValue
|
|
cs = ValueSourceDef
|
|
return
|
|
}
|
|
|
|
// KVSrc represents a configuration parameter key and value along with the
|
|
// source of the value.
|
|
type KVSrc struct {
|
|
Key string
|
|
Value string
|
|
Src ValueSource
|
|
}
|
|
|
|
// GetResolvedConfigParams returns all applicable config parameters with their
|
|
// value sources.
|
|
func (c Config) GetResolvedConfigParams(subSys, target string) ([]KVSrc, error) {
|
|
// Initially only support OpenID
|
|
if !resolvableSubsystems.Contains(subSys) {
|
|
return nil, Errorf("unsupported subsystem: %s", subSys)
|
|
}
|
|
|
|
// Check if config param requested is valid.
|
|
defKVS, ok := DefaultKVS[subSys]
|
|
if !ok {
|
|
return nil, Errorf("unknown subsystem: %s", subSys)
|
|
}
|
|
|
|
r := make([]KVSrc, 0, len(defKVS)+1)
|
|
for _, kv := range defKVS {
|
|
v, vs := c.ResolveConfigParam(subSys, target, kv.Key)
|
|
|
|
// Fix `vs` when default.
|
|
if v == kv.Value {
|
|
vs = ValueSourceDef
|
|
}
|
|
|
|
r = append(r, KVSrc{
|
|
Key: kv.Key,
|
|
Value: v,
|
|
Src: vs,
|
|
})
|
|
}
|
|
|
|
// Add the comment key as well if non-empty.
|
|
v, vs := c.ResolveConfigParam(subSys, target, Comment)
|
|
if vs != ValueSourceDef {
|
|
r = append(r, KVSrc{
|
|
Key: Comment,
|
|
Value: v,
|
|
Src: vs,
|
|
})
|
|
}
|
|
|
|
return r, nil
|
|
}
|
|
|
|
func (c Config) getTargetKVS(subSys, target string) KVS {
|
|
store, ok := c[subSys]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return store[target]
|
|
}
|
|
|
|
// EnvPair represents an environment variable and its value.
|
|
type EnvPair struct {
|
|
Name, Value string
|
|
}
|
|
|
|
// SubsysInfo holds config info for a subsystem target.
|
|
type SubsysInfo struct {
|
|
SubSys, Target string
|
|
Defaults KVS
|
|
Config KVS
|
|
|
|
// map of config parameter name to EnvPair.
|
|
EnvMap map[string]EnvPair
|
|
}
|
|
|
|
// GetSubsysInfo returns `SubsysInfo`s for all targets for the subsystem, when
|
|
// target is empty. Otherwise returns `SubsysInfo` for the desired target only.
|
|
// To request the default target only, target must be set to `Default`.
|
|
func (c Config) GetSubsysInfo(subSys, target string) ([]SubsysInfo, error) {
|
|
// Check if config param requested is valid.
|
|
defKVS1, ok := DefaultKVS[subSys]
|
|
if !ok {
|
|
return nil, Errorf("unknown subsystem: %s", subSys)
|
|
}
|
|
|
|
targets, err := c.GetAvailableTargets(subSys)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if target != "" {
|
|
found := false
|
|
for _, t := range targets {
|
|
if t == target {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return nil, Errorf("there is no target `%s` for subsystem `%s`", target, subSys)
|
|
}
|
|
targets = []string{target}
|
|
}
|
|
|
|
// The `Comment` configuration variable is optional but is available to be
|
|
// set for all sub-systems. It is not present in the `DefaultKVS` map's
|
|
// values. To enable fetching a configured comment value from the
|
|
// environment we add it to the list of default keys for the subsystem.
|
|
defKVS := make([]KV, len(defKVS1), len(defKVS1)+1)
|
|
copy(defKVS, defKVS1)
|
|
defKVS = append(defKVS, KV{Key: Comment})
|
|
|
|
r := make([]SubsysInfo, 0, len(targets))
|
|
for _, target := range targets {
|
|
kvs := c.getTargetKVS(subSys, target)
|
|
cs := SubsysInfo{
|
|
SubSys: subSys,
|
|
Target: target,
|
|
Defaults: defKVS,
|
|
Config: kvs,
|
|
EnvMap: make(map[string]EnvPair),
|
|
}
|
|
|
|
// Add all env vars that are set.
|
|
for _, kv := range defKVS {
|
|
envName := getEnvVarName(subSys, target, kv.Key)
|
|
envPair := EnvPair{
|
|
Name: envName,
|
|
Value: env.Get(envName, ""),
|
|
}
|
|
if envPair.Value != "" {
|
|
cs.EnvMap[kv.Key] = envPair
|
|
}
|
|
}
|
|
|
|
r = append(r, cs)
|
|
}
|
|
|
|
return r, nil
|
|
}
|
|
|
|
// AddEnvString adds env vars to the given string builder.
|
|
func (cs *SubsysInfo) AddEnvString(b *strings.Builder) {
|
|
for _, v := range cs.Defaults {
|
|
if ep, ok := cs.EnvMap[v.Key]; ok {
|
|
b.WriteString(KvComment)
|
|
b.WriteString(KvSpaceSeparator)
|
|
b.WriteString(ep.Name)
|
|
b.WriteString(EnvSeparator)
|
|
b.WriteString(ep.Value)
|
|
b.WriteString(KvNewline)
|
|
}
|
|
}
|
|
}
|
|
|
|
// WriteTo writes the string representation of the configuration to the given
|
|
// builder. When off is true, adds a comment character before the config system
|
|
// output. It also ignores values when empty and deprecated.
|
|
func (cs *SubsysInfo) WriteTo(b *strings.Builder, off bool) {
|
|
cs.AddEnvString(b)
|
|
if off {
|
|
b.WriteString(KvComment)
|
|
b.WriteString(KvSpaceSeparator)
|
|
}
|
|
b.WriteString(cs.SubSys)
|
|
if cs.Target != Default {
|
|
b.WriteString(SubSystemSeparator)
|
|
b.WriteString(cs.Target)
|
|
}
|
|
b.WriteString(KvSpaceSeparator)
|
|
for _, kv := range cs.Config {
|
|
dkv, ok := cs.Defaults.LookupKV(kv.Key)
|
|
if !ok {
|
|
continue
|
|
}
|
|
// Ignore empty and deprecated values
|
|
if dkv.Deprecated && kv.Value == "" {
|
|
continue
|
|
}
|
|
// Do not need to print if state is on
|
|
if kv.Key == Enable && kv.Value == EnableOn {
|
|
continue
|
|
}
|
|
b.WriteString(kv.String())
|
|
b.WriteString(KvSpaceSeparator)
|
|
}
|
|
|
|
b.WriteString(KvNewline)
|
|
}
|