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 }