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  }