github.com/minio/madmin-go/v2@v2.2.1/parse-config.go (about)

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