github.com/minio/madmin-go@v1.7.5/parse-config.go (about)

     1  //
     2  // MinIO Object Storage (c) 2022 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  package madmin
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"strings"
    23  	"unicode"
    24  
    25  	"github.com/minio/minio-go/v7/pkg/set"
    26  )
    27  
    28  // Top level configuration key constants.
    29  const (
    30  	CredentialsSubSys    = "credentials"
    31  	PolicyOPASubSys      = "policy_opa"
    32  	PolicyPluginSubSys   = "policy_plugin"
    33  	IdentityOpenIDSubSys = "identity_openid"
    34  	IdentityLDAPSubSys   = "identity_ldap"
    35  	IdentityTLSSubSys    = "identity_tls"
    36  	IdentityPluginSubSys = "identity_plugin"
    37  	CacheSubSys          = "cache"
    38  	SiteSubSys           = "site"
    39  	RegionSubSys         = "region"
    40  	EtcdSubSys           = "etcd"
    41  	StorageClassSubSys   = "storage_class"
    42  	APISubSys            = "api"
    43  	CompressionSubSys    = "compression"
    44  	LoggerWebhookSubSys  = "logger_webhook"
    45  	AuditWebhookSubSys   = "audit_webhook"
    46  	AuditKafkaSubSys     = "audit_kafka"
    47  	HealSubSys           = "heal"
    48  	ScannerSubSys        = "scanner"
    49  	CrawlerSubSys        = "crawler"
    50  	SubnetSubSys         = "subnet"
    51  	CallhomeSubSys       = "callhome"
    52  
    53  	NotifyKafkaSubSys    = "notify_kafka"
    54  	NotifyMQTTSubSys     = "notify_mqtt"
    55  	NotifyMySQLSubSys    = "notify_mysql"
    56  	NotifyNATSSubSys     = "notify_nats"
    57  	NotifyNSQSubSys      = "notify_nsq"
    58  	NotifyESSubSys       = "notify_elasticsearch"
    59  	NotifyAMQPSubSys     = "notify_amqp"
    60  	NotifyPostgresSubSys = "notify_postgres"
    61  	NotifyRedisSubSys    = "notify_redis"
    62  	NotifyWebhookSubSys  = "notify_webhook"
    63  )
    64  
    65  // SubSystems - list of all subsystems in MinIO
    66  var SubSystems = set.CreateStringSet(
    67  	CredentialsSubSys,
    68  	PolicyOPASubSys,
    69  	PolicyPluginSubSys,
    70  	IdentityOpenIDSubSys,
    71  	IdentityLDAPSubSys,
    72  	IdentityTLSSubSys,
    73  	IdentityPluginSubSys,
    74  	CacheSubSys,
    75  	SiteSubSys,
    76  	RegionSubSys,
    77  	EtcdSubSys,
    78  	StorageClassSubSys,
    79  	APISubSys,
    80  	CompressionSubSys,
    81  	LoggerWebhookSubSys,
    82  	AuditWebhookSubSys,
    83  	AuditKafkaSubSys,
    84  	HealSubSys,
    85  	ScannerSubSys,
    86  	CrawlerSubSys,
    87  	SubnetSubSys,
    88  	CallhomeSubSys,
    89  	NotifyKafkaSubSys,
    90  	NotifyMQTTSubSys,
    91  	NotifyMySQLSubSys,
    92  	NotifyNATSSubSys,
    93  	NotifyNSQSubSys,
    94  	NotifyESSubSys,
    95  	NotifyAMQPSubSys,
    96  	NotifyPostgresSubSys,
    97  	NotifyRedisSubSys,
    98  	NotifyWebhookSubSys,
    99  )
   100  
   101  // Standard config keys and values.
   102  const (
   103  	EnableKey  = "enable"
   104  	CommentKey = "comment"
   105  
   106  	// Enable values
   107  	EnableOn  = "on"
   108  	EnableOff = "off"
   109  )
   110  
   111  // HasSpace - returns if given string has space.
   112  func HasSpace(s string) bool {
   113  	for _, r := range s {
   114  		if unicode.IsSpace(r) {
   115  			return true
   116  		}
   117  	}
   118  	return false
   119  }
   120  
   121  // Constant separators
   122  const (
   123  	SubSystemSeparator = `:`
   124  	KvSeparator        = `=`
   125  	KvComment          = `#`
   126  	KvSpaceSeparator   = ` `
   127  	KvNewline          = "\n"
   128  	KvDoubleQuote      = `"`
   129  	KvSingleQuote      = `'`
   130  
   131  	Default = `_`
   132  
   133  	EnvPrefix        = "MINIO_"
   134  	EnvWordDelimiter = `_`
   135  
   136  	EnvLinePrefix = KvComment + KvSpaceSeparator + EnvPrefix
   137  )
   138  
   139  // SanitizeValue - this function is needed, to trim off single or double quotes, creeping into the values.
   140  func SanitizeValue(v string) string {
   141  	v = strings.TrimSuffix(strings.TrimPrefix(strings.TrimSpace(v), KvDoubleQuote), KvDoubleQuote)
   142  	return strings.TrimSuffix(strings.TrimPrefix(v, KvSingleQuote), KvSingleQuote)
   143  }
   144  
   145  // EnvOverride contains the name of the environment variable and its value.
   146  type EnvOverride struct {
   147  	Name  string `json:"name"`
   148  	Value string `json:"value"`
   149  }
   150  
   151  // ConfigKV represents a configuration key and value, along with any environment
   152  // override if present.
   153  type ConfigKV struct {
   154  	Key         string       `json:"key"`
   155  	Value       string       `json:"value"`
   156  	EnvOverride *EnvOverride `json:"envOverride,omitempty"`
   157  }
   158  
   159  // SubsysConfig represents the configuration for a particular subsytem and
   160  // target.
   161  type SubsysConfig struct {
   162  	SubSystem string `json:"subSystem"`
   163  	Target    string `json:"target,omitempty"`
   164  
   165  	// WARNING: Use AddConfigKV() to mutate this.
   166  	KV []ConfigKV `json:"kv"`
   167  
   168  	kvIndexMap map[string]int
   169  }
   170  
   171  // AddConfigKV - adds a config parameter to the subsystem.
   172  func (c *SubsysConfig) AddConfigKV(ckv ConfigKV) {
   173  	if c.kvIndexMap == nil {
   174  		c.kvIndexMap = make(map[string]int)
   175  	}
   176  	idx, ok := c.kvIndexMap[ckv.Key]
   177  	if ok {
   178  		c.KV[idx] = ckv
   179  	} else {
   180  		c.KV = append(c.KV, ckv)
   181  		c.kvIndexMap[ckv.Key] = len(c.KV) - 1
   182  	}
   183  }
   184  
   185  // Lookup resolves the value of a config parameter. If an env variable is
   186  // specified on the server for the parameter, it is returned.
   187  func (c *SubsysConfig) Lookup(key string) (val string, present bool) {
   188  	if c.kvIndexMap == nil {
   189  		return "", false
   190  	}
   191  
   192  	idx, ok := c.kvIndexMap[key]
   193  	if !ok {
   194  		return "", false
   195  	}
   196  	if c.KV[idx].EnvOverride != nil {
   197  		return c.KV[idx].EnvOverride.Value, true
   198  	}
   199  	return c.KV[idx].Value, true
   200  }
   201  
   202  var (
   203  	ErrInvalidEnvVarLine = errors.New("expected env var line of the form `# MINIO_...=...`")
   204  	ErrInvalidConfigKV   = errors.New("expected config value in the format `key=value`")
   205  )
   206  
   207  func parseEnvVarLine(s, subSystem, target string) (val ConfigKV, err error) {
   208  	s = strings.TrimPrefix(s, KvComment+KvSpaceSeparator)
   209  	ps := strings.SplitN(s, KvSeparator, 2)
   210  	if len(ps) != 2 {
   211  		err = ErrInvalidEnvVarLine
   212  		return
   213  	}
   214  
   215  	val.EnvOverride = &EnvOverride{
   216  		Name:  ps[0],
   217  		Value: ps[1],
   218  	}
   219  
   220  	envVar := val.EnvOverride.Name
   221  	envPrefix := EnvPrefix + strings.ToUpper(subSystem) + EnvWordDelimiter
   222  	if !strings.HasPrefix(envVar, envPrefix) {
   223  		err = fmt.Errorf("expected env %v to have prefix %v", envVar, envPrefix)
   224  		return
   225  	}
   226  	configVar := strings.TrimPrefix(envVar, envPrefix)
   227  	if target != Default {
   228  		configVar = strings.TrimSuffix(configVar, EnvWordDelimiter+target)
   229  	}
   230  	val.Key = strings.ToLower(configVar)
   231  	return
   232  }
   233  
   234  // Takes "k1=v1 k2=v2 ..." and returns key=k1 and rem="v1 k2=v2 ..." on success.
   235  func parseConfigKey(text string) (key, rem string, err error) {
   236  	// Split to first `=`
   237  	ts := strings.SplitN(text, KvSeparator, 2)
   238  
   239  	key = strings.TrimSpace(ts[0])
   240  	if len(key) == 0 {
   241  		err = ErrInvalidConfigKV
   242  		return
   243  	}
   244  
   245  	if len(ts) == 1 {
   246  		err = ErrInvalidConfigKV
   247  		return
   248  	}
   249  
   250  	return key, ts[1], nil
   251  }
   252  
   253  func parseConfigValue(text string) (v, rem string, err error) {
   254  	// Value may be double quoted.
   255  	if strings.HasPrefix(text, KvDoubleQuote) {
   256  		text = strings.TrimPrefix(text, KvDoubleQuote)
   257  		ts := strings.SplitN(text, KvDoubleQuote, 2)
   258  		v = ts[0]
   259  		if len(ts) == 1 {
   260  			err = ErrInvalidConfigKV
   261  			return
   262  		}
   263  		rem = strings.TrimSpace(ts[1])
   264  	} else {
   265  		ts := strings.SplitN(text, KvSpaceSeparator, 2)
   266  		v = ts[0]
   267  		if len(ts) == 2 {
   268  			rem = strings.TrimSpace(ts[1])
   269  		} else {
   270  			rem = ""
   271  		}
   272  	}
   273  	return
   274  }
   275  
   276  func parseConfigLine(s string) (c SubsysConfig, err error) {
   277  	ps := strings.SplitN(s, KvSpaceSeparator, 2)
   278  
   279  	ws := strings.SplitN(ps[0], SubSystemSeparator, 2)
   280  	c.SubSystem = ws[0]
   281  	if len(ws) == 2 {
   282  		c.Target = ws[1]
   283  	}
   284  
   285  	if len(ps) == 1 {
   286  		// No config KVs present.
   287  		return
   288  	}
   289  
   290  	// Parse keys and values
   291  	text := strings.TrimSpace(ps[1])
   292  	for len(text) > 0 {
   293  
   294  		kv := ConfigKV{}
   295  		kv.Key, text, err = parseConfigKey(text)
   296  		if err != nil {
   297  			return
   298  		}
   299  
   300  		kv.Value, text, err = parseConfigValue(text)
   301  		if err != nil {
   302  			return
   303  		}
   304  
   305  		c.AddConfigKV(kv)
   306  	}
   307  	return
   308  }
   309  
   310  func isEnvLine(s string) bool {
   311  	return strings.HasPrefix(s, EnvLinePrefix)
   312  }
   313  
   314  func isCommentLine(s string) bool {
   315  	return strings.HasPrefix(s, KvComment)
   316  }
   317  
   318  func getConfigLineSubSystemAndTarget(s string) (subSys, target string) {
   319  	words := strings.SplitN(s, KvSpaceSeparator, 2)
   320  	pieces := strings.SplitN(words[0], SubSystemSeparator, 2)
   321  	if len(pieces) == 2 {
   322  		return pieces[0], pieces[1]
   323  	}
   324  	// If no target is present, it is the default target.
   325  	return pieces[0], Default
   326  }
   327  
   328  // ParseServerConfigOutput - takes a server config output and returns a slice of
   329  // configs. Depending on the server config get API request, this may return
   330  // configuration info for one or more configuration sub-systems.
   331  //
   332  // A configuration subsystem in the server may have one or more subsystem
   333  // targets (named instances of the sub-system, for example `notify_postres`,
   334  // `logger_webhook` or `identity_openid`). For every subsystem and target
   335  // returned in `serverConfigOutput`, this function returns a separate
   336  // `SubsysConfig` value in the output slice. The default target is returned as
   337  // "" (empty string) by this function.
   338  //
   339  // Use the `Lookup()` function on the `SubsysConfig` type to query a
   340  // subsystem-target pair for a configuration parameter. This returns the
   341  // effective value (i.e. possibly overridden by an environment variable) of the
   342  // configuration parameter on the server.
   343  func ParseServerConfigOutput(serverConfigOutput string) ([]SubsysConfig, error) {
   344  	lines := strings.Split(serverConfigOutput, "\n")
   345  
   346  	// Clean up config lines
   347  	var configLines []string
   348  	for _, line := range lines {
   349  		line = strings.TrimSpace(line)
   350  		if line != "" {
   351  			configLines = append(configLines, line)
   352  		}
   353  	}
   354  
   355  	// Parse out config lines into groups corresponding to a single subsystem
   356  	// and target.
   357  	//
   358  	// How does it work? The server output is a list of lines, where each line
   359  	// may be one of:
   360  	//
   361  	//   1. A config line for a single subsystem (and optional target). For
   362  	//   example, "site region=us-east-1" or "identity_openid:okta k1=v1 k2=v2".
   363  	//
   364  	//   2. A comment line showing an environment variable set on the server.
   365  	//   For example "# MINIO_SITE_NAME=my-cluster".
   366  	//
   367  	//   3. Comment lines with other content. These will not start with `#
   368  	//   MINIO_`.
   369  	//
   370  	// For the structured JSON representation, only lines of type 1 and 2 are
   371  	// required as they correspond to configuration specified by an
   372  	// administrator.
   373  	//
   374  	// Additionally, after ignoring lines of type 3 above:
   375  	//
   376  	//   1. environment variable lines for a subsystem (and target if present)
   377  	//   appear consecutively.
   378  	//
   379  	//   2. exactly one config line for a subsystem and target immediately
   380  	//   follows the env var lines for the same subsystem and target.
   381  	//
   382  	// The parsing logic below classifies each line and groups them by
   383  	// subsystem and target.
   384  	var configGroups [][]string
   385  	var subSystems []string
   386  	var targets []string
   387  	var currGroup []string
   388  	for _, line := range configLines {
   389  		if isEnvLine(line) {
   390  			currGroup = append(currGroup, line)
   391  		} else if isCommentLine(line) {
   392  			continue
   393  		} else {
   394  			subSys, target := getConfigLineSubSystemAndTarget(line)
   395  			currGroup = append(currGroup, line)
   396  			configGroups = append(configGroups, currGroup)
   397  			subSystems = append(subSystems, subSys)
   398  			targets = append(targets, target)
   399  
   400  			// Reset currGroup to collect lines for the next group.
   401  			currGroup = nil
   402  		}
   403  	}
   404  
   405  	res := make([]SubsysConfig, 0, len(configGroups))
   406  	for i, group := range configGroups {
   407  		sc := SubsysConfig{
   408  			SubSystem: subSystems[i],
   409  		}
   410  		if targets[i] != Default {
   411  			sc.Target = targets[i]
   412  		}
   413  
   414  		for _, line := range group {
   415  			if isEnvLine(line) {
   416  				ckv, err := parseEnvVarLine(line, subSystems[i], targets[i])
   417  				if err != nil {
   418  					return nil, err
   419  				}
   420  				// Since all env lines have distinct env vars, we can append
   421  				// here without risk of introducing any duplicates.
   422  				sc.AddConfigKV(ckv)
   423  				continue
   424  			}
   425  
   426  			// At this point all env vars for this subsys and target are already
   427  			// in `sc.KV`, so we fill in values if a ConfigKV entry for the
   428  			// config parameter is already present.
   429  			lineCfg, err := parseConfigLine(line)
   430  			if err != nil {
   431  				return nil, err
   432  			}
   433  			for _, kv := range lineCfg.KV {
   434  				idx, ok := sc.kvIndexMap[kv.Key]
   435  				if ok {
   436  					sc.KV[idx].Value = kv.Value
   437  				} else {
   438  					sc.AddConfigKV(kv)
   439  				}
   440  			}
   441  		}
   442  
   443  		res = append(res, sc)
   444  	}
   445  
   446  	return res, nil
   447  }