github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/configuration/core/config.go (about) 1 /* 2 Copyright (C) 2022-2023 ApeCloud Co., Ltd 3 4 This file is part of KubeBlocks project 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 published by 8 the Free Software Foundation, either version 3 of the License, or 9 (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 core 21 22 import ( 23 "encoding/json" 24 "path" 25 "strings" 26 27 "github.com/StudioSol/set" 28 "github.com/spf13/cast" 29 30 appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1" 31 "github.com/1aal/kubeblocks/pkg/configuration/util" 32 "github.com/1aal/kubeblocks/pkg/unstructured" 33 ) 34 35 type ConfigLoaderProvider func(option CfgOption) (*cfgWrapper, error) 36 37 // ReconfiguringProgress defines the progress percentage. 38 // range: 0~100 39 // Unconfirmed(-1) describes an uncertain progress, e.g: fsm is failed. 40 // +enum 41 type ReconfiguringProgress int32 42 43 type PolicyExecStatus struct { 44 PolicyName string 45 ExecStatus string 46 Status string 47 48 SucceedCount int32 49 ExpectedCount int32 50 } 51 52 const ( 53 Unconfirmed int32 = -1 54 NotStarted int32 = 0 55 ) 56 57 const emptyJSON = "{}" 58 59 var ( 60 loaderProvider = map[ConfigType]ConfigLoaderProvider{} 61 ) 62 63 func init() { 64 // For RAW 65 loaderProvider[CfgRawType] = func(option CfgOption) (*cfgWrapper, error) { 66 if len(option.RawData) == 0 { 67 return nil, MakeError("rawdata not empty! [%v]", option) 68 } 69 70 meta := cfgWrapper{ 71 name: "raw", 72 fileCount: 0, 73 v: make([]unstructured.ConfigObject, 1), 74 indexer: make(map[string]unstructured.ConfigObject, 1), 75 } 76 77 v, err := unstructured.LoadConfig(meta.name, string(option.RawData), option.CfgType) 78 if err != nil { 79 option.Log.Error(err, "failed to parse config!", "context", option.RawData) 80 return nil, err 81 } 82 83 meta.v[0] = v 84 meta.indexer[meta.name] = v 85 return &meta, nil 86 } 87 88 // For CM/TPL 89 loaderProvider[CfgCmType] = func(option CfgOption) (*cfgWrapper, error) { 90 if option.ConfigResource == nil { 91 return nil, MakeError("invalid k8s resource[%v]", option) 92 } 93 94 ctx := option.ConfigResource 95 if ctx.ConfigData == nil && ctx.ResourceReader != nil { 96 configs, err := ctx.ResourceReader(ctx.CfgKey) 97 if err != nil { 98 return nil, WrapError(err, "failed to get cm, cm key: [%v]", ctx.CfgKey) 99 } 100 ctx.ConfigData = configs 101 } 102 103 fileCount := len(ctx.ConfigData) 104 meta := cfgWrapper{ 105 name: path.Base(ctx.CfgKey.Name), 106 fileCount: fileCount, 107 v: make([]unstructured.ConfigObject, fileCount), 108 indexer: make(map[string]unstructured.ConfigObject, 1), 109 } 110 111 var err error 112 var index = 0 113 var v unstructured.ConfigObject 114 for fileName, content := range ctx.ConfigData { 115 if ctx.CMKeys != nil && !ctx.CMKeys.InArray(fileName) { 116 continue 117 } 118 if v, err = unstructured.LoadConfig(fileName, content, option.CfgType); err != nil { 119 return nil, WrapError(err, "failed to load config: filename[%s], type[%s]", fileName, option.CfgType) 120 } 121 meta.indexer[fileName] = v 122 meta.v[index] = v 123 index++ 124 } 125 return &meta, nil 126 } 127 128 // For TPL 129 loaderProvider[CfgTplType] = loaderProvider[CfgCmType] 130 } 131 132 type cfgWrapper struct { 133 // name is config name 134 name string 135 // volumeName string 136 137 // fileCount 138 fileCount int 139 // indexer map[string]*viper.Viper 140 indexer map[string]unstructured.ConfigObject 141 v []unstructured.ConfigObject 142 } 143 144 type dataConfig struct { 145 // Option is config for 146 Option CfgOption 147 148 // cfgWrapper references configuration template or configmap 149 *cfgWrapper 150 } 151 152 func NewConfigLoader(option CfgOption) (*dataConfig, error) { 153 loader, ok := loaderProvider[option.Type] 154 if !ok { 155 return nil, MakeError("not supported config type: %s", option.Type) 156 } 157 158 meta, err := loader(option) 159 if err != nil { 160 return nil, err 161 } 162 163 return &dataConfig{ 164 Option: option, 165 cfgWrapper: meta, 166 }, nil 167 } 168 169 // Option for operator 170 type Option func(ctx *CfgOpOption) 171 172 func (c *cfgWrapper) MergeFrom(params map[string]interface{}, option CfgOpOption) error { 173 var err error 174 var cfg unstructured.ConfigObject 175 176 if cfg = c.getConfigObject(option); cfg == nil { 177 return MakeError("not found the config file:[%s]", option.FileName) 178 } 179 for paramKey, paramValue := range params { 180 if paramValue != nil { 181 err = cfg.Update(c.generateKey(paramKey, option), paramValue) 182 } else { 183 err = cfg.RemoveKey(c.generateKey(paramKey, option)) 184 } 185 if err != nil { 186 return err 187 } 188 } 189 return nil 190 } 191 192 func (c *cfgWrapper) ToCfgContent() (map[string]string, error) { 193 fileContents := make(map[string]string, c.fileCount) 194 for fileName, v := range c.indexer { 195 content, err := v.Marshal() 196 if err != nil { 197 return nil, err 198 } 199 fileContents[fileName] = content 200 } 201 return fileContents, nil 202 } 203 204 type ConfigPatchInfo struct { 205 IsModify bool 206 // new config 207 AddConfig map[string]interface{} 208 209 // delete config 210 DeleteConfig map[string]interface{} 211 212 // update config 213 // patch json 214 UpdateConfig map[string][]byte 215 216 Target *cfgWrapper 217 LastVersion *cfgWrapper 218 } 219 220 func NewCfgOptions(filename string, options ...Option) CfgOpOption { 221 context := CfgOpOption{ 222 FileName: filename, 223 } 224 225 for _, op := range options { 226 op(&context) 227 } 228 229 return context 230 } 231 232 func WithFormatterConfig(formatConfig *appsv1alpha1.FormatterConfig) Option { 233 return func(ctx *CfgOpOption) { 234 if formatConfig.Format == appsv1alpha1.Ini && formatConfig.IniConfig != nil { 235 ctx.IniContext = &IniContext{ 236 SectionName: formatConfig.IniConfig.SectionName, 237 } 238 } 239 } 240 } 241 242 func NestedPrefixField(formatConfig *appsv1alpha1.FormatterConfig) string { 243 if formatConfig != nil && formatConfig.Format == appsv1alpha1.Ini && formatConfig.IniConfig != nil { 244 return formatConfig.IniConfig.SectionName 245 } 246 return "" 247 } 248 249 func (c *cfgWrapper) Query(jsonpath string, option CfgOpOption) ([]byte, error) { 250 if option.AllSearch && c.fileCount > 1 { 251 return c.queryAllCfg(jsonpath, option) 252 } 253 254 cfg := c.getConfigObject(option) 255 if cfg == nil { 256 return nil, MakeError("not found the config file:[%s]", option.FileName) 257 } 258 259 iniContext := option.IniContext 260 if iniContext != nil && len(iniContext.SectionName) > 0 { 261 cfg = cfg.SubConfig(iniContext.SectionName) 262 if cfg == nil { 263 return nil, MakeError("the section[%s] does not exist in the config file", iniContext.SectionName) 264 } 265 } 266 267 return util.RetrievalWithJSONPath(cfg.GetAllParameters(), jsonpath) 268 } 269 270 func (c *cfgWrapper) queryAllCfg(jsonpath string, option CfgOpOption) ([]byte, error) { 271 tops := make(map[string]interface{}, c.fileCount) 272 273 for filename, v := range c.indexer { 274 tops[filename] = v.GetAllParameters() 275 } 276 return util.RetrievalWithJSONPath(tops, jsonpath) 277 } 278 279 func (c cfgWrapper) getConfigObject(option CfgOpOption) unstructured.ConfigObject { 280 if len(c.v) == 0 { 281 return nil 282 } 283 284 if len(option.FileName) == 0 { 285 return c.v[0] 286 } else { 287 return c.indexer[option.FileName] 288 } 289 } 290 291 func (c *cfgWrapper) generateKey(paramKey string, option CfgOpOption) string { 292 if option.IniContext != nil && len(option.IniContext.SectionName) > 0 { 293 return strings.Join([]string{option.IniContext.SectionName, paramKey}, unstructured.DelimiterDot) 294 } 295 296 return paramKey 297 } 298 299 func FromCMKeysSelector(keys []string) *set.LinkedHashSetString { 300 var cmKeySet *set.LinkedHashSetString 301 if len(keys) > 0 { 302 cmKeySet = set.NewLinkedHashSetString(keys...) 303 } 304 return cmKeySet 305 } 306 307 func GenerateVisualizedParamsList(configPatch *ConfigPatchInfo, formatConfig *appsv1alpha1.FormatterConfig, sets *set.LinkedHashSetString) []VisualizedParam { 308 if !configPatch.IsModify { 309 return nil 310 } 311 312 var trimPrefix = NestedPrefixField(formatConfig) 313 314 r := make([]VisualizedParam, 0) 315 r = append(r, generateUpdateParam(configPatch.UpdateConfig, trimPrefix, sets)...) 316 r = append(r, generateUpdateKeyParam(configPatch.AddConfig, trimPrefix, AddedType, sets)...) 317 r = append(r, generateUpdateKeyParam(configPatch.DeleteConfig, trimPrefix, DeletedType, sets)...) 318 return r 319 } 320 321 func generateUpdateParam(updatedParams map[string][]byte, trimPrefix string, sets *set.LinkedHashSetString) []VisualizedParam { 322 r := make([]VisualizedParam, 0, len(updatedParams)) 323 324 for key, b := range updatedParams { 325 // TODO support keys 326 if sets != nil && sets.Length() > 0 && !sets.InArray(key) { 327 continue 328 } 329 var v any 330 if err := json.Unmarshal(b, &v); err != nil { 331 return nil 332 } 333 if params := checkAndFlattenMap(v, trimPrefix); params != nil { 334 r = append(r, VisualizedParam{ 335 Key: key, 336 Parameters: params, 337 UpdateType: UpdatedType, 338 }) 339 } 340 } 341 return r 342 } 343 344 func checkAndFlattenMap(v any, trim string) []ParameterPair { 345 m := cast.ToStringMap(v) 346 if m != nil && trim != "" { 347 m = cast.ToStringMap(m[trim]) 348 } 349 if m != nil { 350 return flattenMap(m, "") 351 } 352 return nil 353 } 354 355 func flattenMap(m map[string]interface{}, prefix string) []ParameterPair { 356 if prefix != "" { 357 prefix += unstructured.DelimiterDot 358 } 359 360 r := make([]ParameterPair, 0) 361 for k, val := range m { 362 fullKey := prefix + k 363 switch m2 := val.(type) { 364 case map[string]interface{}: 365 r = append(r, flattenMap(m2, fullKey)...) 366 default: 367 var v *string = nil 368 if val != nil { 369 v = util.ToPointer(cast.ToString(val)) 370 } 371 r = append(r, ParameterPair{ 372 Key: fullKey, 373 Value: v, 374 }) 375 } 376 } 377 return r 378 } 379 380 func generateUpdateKeyParam(files map[string]interface{}, trimPrefix string, updatedType ParameterUpdateType, sets *set.LinkedHashSetString) []VisualizedParam { 381 r := make([]VisualizedParam, 0, len(files)) 382 383 for key, params := range files { 384 if sets != nil && sets.Length() > 0 && !sets.InArray(key) { 385 continue 386 } 387 if params := checkAndFlattenMap(params, trimPrefix); params != nil { 388 r = append(r, VisualizedParam{ 389 Key: key, 390 Parameters: params, 391 UpdateType: updatedType, 392 }) 393 } 394 } 395 return r 396 }