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