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 }