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 }