github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/cmd/reloader/tools/main.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 main
    21  
    22  import (
    23  	"bufio"
    24  	"bytes"
    25  	"flag"
    26  	"fmt"
    27  	"io"
    28  	"os"
    29  	"reflect"
    30  	"regexp"
    31  	"strconv"
    32  	"strings"
    33  
    34  	cfgcore "github.com/1aal/kubeblocks/pkg/configuration/core"
    35  )
    36  
    37  const (
    38  	FileNotExist  = -1
    39  	FileReadError = -2
    40  
    41  	RecordFieldCount = 8
    42  	RecordMinField   = 7
    43  
    44  	NameField          = 0
    45  	DefaultValueField  = 1
    46  	ValueRestrictField = 2
    47  	ImmutableField     = 3
    48  	ValueTypeField     = 4
    49  	ChangeTypeField    = 5
    50  	DocField           = 6
    51  )
    52  
    53  var (
    54  	prefixString        = ""
    55  	filePath            = ""
    56  	typeName            = "MyParameter"
    57  	ignoreStringDefault = true
    58  	booleanPromotion    = false
    59  )
    60  
    61  type ValueType string
    62  
    63  const (
    64  	BooleanType = "boolean"
    65  	IntegerType = "integer"
    66  	FloatType   = "float"
    67  	StringType  = "string"
    68  	ListType    = "list" // for string
    69  )
    70  
    71  type ValueParser func(s string) (interface{}, error)
    72  
    73  func EmptyParser(s string) (interface{}, error) {
    74  	return s, nil
    75  }
    76  
    77  var numberRegex = regexp.MustCompile(`^\d+$`)
    78  
    79  var ValueTypeParserMap = map[ValueType]ValueParser{
    80  	BooleanType: func(s string) (interface{}, error) {
    81  		if booleanPromotion && numberRegex.MatchString(s) {
    82  			return nil, cfgcore.MakeError("boolean parser failed")
    83  		}
    84  		return strconv.ParseBool(s)
    85  	},
    86  	IntegerType: func(s string) (interface{}, error) {
    87  		if v, err := strconv.ParseInt(s, 10, 64); err == nil {
    88  			return v, nil
    89  		}
    90  		return strconv.ParseUint(s, 10, 64)
    91  	},
    92  	FloatType: func(s string) (interface{}, error) {
    93  		return strconv.ParseFloat(s, 64)
    94  	},
    95  	StringType: EmptyParser,
    96  	ListType:   EmptyParser,
    97  }
    98  
    99  func main() {
   100  	// The source file format is one line per parameter, the fields are separated by tabs, and the fields are as follows:
   101  	// parameter name | default value | value restriction |  is immutable(true/false) | value type(boolean/integer/string) | change type(static/dynamic) | description
   102  	// file format example:
   103  	// default_authentication_plugin\tmysql_native_password\tmysql_native_password, sha256_password, caching_sha2_password\tfalse\string\tstatic\tThe default authentication plugin
   104  
   105  	flag.StringVar(&filePath, "file-path", "", "The source file path for generating cue template.")
   106  	flag.StringVar(&prefixString, "output-prefix", prefixString, "prefix, default: \"\"")
   107  	flag.StringVar(&typeName, "type-name", typeName, "cue parameter type name.")
   108  	flag.BoolVar(&ignoreStringDefault, "ignore-string-default", ignoreStringDefault, "ignore string default. ")
   109  	flag.BoolVar(&booleanPromotion, "boolean-promotion", booleanPromotion, "enable using OFF or ON. ")
   110  	flag.Parse()
   111  
   112  	f, err := os.Open(filePath)
   113  	if err != nil {
   114  		fmt.Printf("open file[%s] failed. error: %v", filePath, err)
   115  		os.Exit(FileNotExist)
   116  	}
   117  
   118  	writer := os.Stdout
   119  	scanner := bufio.NewScanner(f)
   120  	wrapOutputTypeDefineBegin(typeName, writer)
   121  	for scanner.Scan() {
   122  		if err := scanner.Err(); err != nil {
   123  			fmt.Printf("readline failed. error: %v", err)
   124  			os.Exit(FileReadError)
   125  		}
   126  		fields := strings.SplitN(scanner.Text(), "\t", RecordFieldCount)
   127  		if len(fields) < RecordMinField {
   128  			continue
   129  		}
   130  		wrapOutputCueLang(ConstructParameterType(fields), writer)
   131  	}
   132  	wrapOutputTypeDefineEnd(writer)
   133  }
   134  
   135  func wrapOutputTypeDefineEnd(writer io.Writer) int {
   136  	r, _ := writer.Write([]byte(fmt.Sprintf("\n  %s...\n%s}", prefixString, prefixString)))
   137  	return r
   138  }
   139  
   140  func wrapOutputTypeDefineBegin(typeName string, writer io.Writer) int {
   141  	r, _ := writer.Write([]byte(fmt.Sprintf("%s#%s: {\n\n", prefixString, typeName)))
   142  	return r
   143  }
   144  
   145  func wrapOutputCueLang(parameter *ParameterType, writer io.Writer) {
   146  	if !validateParameter(parameter) {
   147  		return
   148  	}
   149  
   150  	wrapper := CueWrapper{
   151  		writer:        writer,
   152  		ParameterType: parameter,
   153  	}
   154  
   155  	wrapper.output()
   156  }
   157  
   158  func validateParameter(parameter *ParameterType) bool {
   159  	return parameter != nil &&
   160  		parameter.Name != "" &&
   161  		parameter.Type != ""
   162  }
   163  
   164  func ConstructParameterType(fields []string) *ParameterType {
   165  	// type promotion
   166  	types := []ValueType{BooleanType, IntegerType, StringType}
   167  	param := ParameterType{
   168  		Name:         fields[NameField],
   169  		Type:         ValueType(fields[ValueTypeField]),
   170  		DefaultValue: strings.TrimSpace(fields[DefaultValueField]),
   171  		IsStatic:     true,
   172  		Immutable:    false,
   173  	}
   174  
   175  	if len(fields) > RecordMinField {
   176  		param.Document = fields[DocField]
   177  	}
   178  	if r, err := strconv.ParseBool(fields[ImmutableField]); err == nil {
   179  		param.Immutable = r
   180  	}
   181  	if fields[ChangeTypeField] == "dynamic" {
   182  		param.IsStatic = false
   183  	}
   184  	if param.Type == ListType {
   185  		param.Type = StringType
   186  	}
   187  	checkAndUpdateDefaultValue(&param)
   188  
   189  	if param.Type != BooleanType {
   190  		pr, _ := parseParameterRestrict(fields[ValueRestrictField], param.Type)
   191  		param.ParameterRestrict = pr
   192  		return &param
   193  	}
   194  
   195  	for _, vt := range types {
   196  		pr, err := parseParameterRestrict(fields[ValueRestrictField], vt)
   197  		if err != nil {
   198  			continue
   199  		}
   200  		if vt == IntegerType && booleanPromotion && pr.isEnum {
   201  			fields[ValueRestrictField] += ", OFF, ON"
   202  			continue
   203  		}
   204  		param.Type = vt
   205  		param.ParameterRestrict = pr
   206  		break
   207  	}
   208  	return &param
   209  }
   210  
   211  func checkAndUpdateDefaultValue(param *ParameterType) {
   212  	var (
   213  		defaultValue = param.DefaultValue
   214  		valueType    = param.Type
   215  	)
   216  
   217  	formatString := func(v interface{}) string {
   218  		return fmt.Sprintf("%v", v)
   219  	}
   220  
   221  	if defaultValue == "" {
   222  		return
   223  	}
   224  	if defaultValue[0] == '{' {
   225  		param.DefaultValue = ""
   226  	}
   227  
   228  	switch valueType {
   229  	case BooleanType:
   230  		checkAndUpdateBoolDefaultValue(param, formatString)
   231  	case StringType:
   232  		if ignoreStringDefault {
   233  			param.DefaultValue = ""
   234  		}
   235  	case IntegerType, FloatType:
   236  		if v, err := ValueTypeParserMap[param.Type](param.DefaultValue); err != nil || formatString(v) != defaultValue {
   237  			param.DefaultValue = ""
   238  		}
   239  	}
   240  }
   241  
   242  func checkAndUpdateBoolDefaultValue(param *ParameterType, formatString func(v interface{}) string) {
   243  	if booleanPromotion {
   244  		return
   245  	}
   246  
   247  	v, err := ValueTypeParserMap[BooleanType](param.DefaultValue)
   248  	if err != nil {
   249  		param.DefaultValue = ""
   250  		return
   251  	}
   252  	param.DefaultValue = formatString(v)
   253  }
   254  
   255  type ParameterRestrict struct {
   256  	isEnum bool
   257  
   258  	Min interface{}
   259  	Max interface{}
   260  
   261  	EnumList []interface{}
   262  }
   263  
   264  type ParameterType struct {
   265  	Name         string
   266  	Type         ValueType
   267  	DefaultValue string
   268  	IsStatic     bool
   269  
   270  	Immutable         bool
   271  	ParameterRestrict *ParameterRestrict
   272  	Document          string
   273  }
   274  
   275  func generateRestrictParam(buffer *bytes.Buffer, restrict *ParameterRestrict, valueType ValueType) {
   276  	buffer.WriteString(" & ")
   277  	if restrict.isEnum {
   278  		for i := 0; i < len(restrict.EnumList); i++ {
   279  			if i > 0 {
   280  				buffer.WriteString(" | ")
   281  			}
   282  			generateElemValue(buffer, restrict.EnumList[i], valueType)
   283  		}
   284  	} else {
   285  		buffer.WriteString(">= ")
   286  		generateElemValue(buffer, restrict.Min, valueType)
   287  		buffer.WriteString(" & <= ")
   288  		generateElemValue(buffer, restrict.Max, valueType)
   289  	}
   290  }
   291  
   292  func generateElemValue(buffer *bytes.Buffer, value interface{}, valueType ValueType) {
   293  	if valueType == StringType {
   294  		buffer.WriteString("\"")
   295  	}
   296  	buffer.WriteString(fmt.Sprintf("%v", value))
   297  	if valueType == StringType {
   298  		buffer.WriteString("\"")
   299  	}
   300  }
   301  
   302  func parseValue(s string, valueType ValueType) (interface{}, error) {
   303  	v, err := ValueTypeParserMap[valueType](s)
   304  	if err != nil {
   305  		return nil, cfgcore.MakeError("parse type[%s] value[%s] failed!", valueType, s)
   306  	}
   307  	return v, nil
   308  }
   309  
   310  func parseParameterRestrict(s string, valueType ValueType) (*ParameterRestrict, error) {
   311  	var (
   312  		IntegerRangeRegex = regexp.MustCompile(`([\+\-]?\d+)-([\+\-]?\d+)`)
   313  		// support format: 0-1.79769e+308
   314  		FloatRangeRegex = regexp.MustCompile(`([\+\-]?\d+(\.\d*(e[\+\-]\d+))?)-([\+\-]?\d+(\.\d*(e[\+\-]\d+))?)`)
   315  
   316  		pr  *ParameterRestrict
   317  		err error
   318  	)
   319  
   320  	setValueHelper := func(rv reflect.Value, s string, valueType ValueType) error {
   321  		if rv.Kind() != reflect.Pointer || rv.IsNil() {
   322  			return cfgcore.MakeError("invalid return type")
   323  		}
   324  
   325  		value, err := parseValue(s, valueType)
   326  		if err != nil {
   327  			return err
   328  		}
   329  		reflect.Indirect(rv).Set(reflect.Indirect(reflect.ValueOf(value)))
   330  		return nil
   331  	}
   332  	integerTypeHandle := func(s string) (*ParameterRestrict, error) {
   333  		r := IntegerRangeRegex.FindStringSubmatch(s)
   334  		if len(r) == 0 {
   335  			return nil, nil
   336  		}
   337  		t := &ParameterRestrict{isEnum: false}
   338  		if err := setValueHelper(reflect.ValueOf(&t.Min), r[1], valueType); err != nil {
   339  			return nil, err
   340  		}
   341  		if err := setValueHelper(reflect.ValueOf(&t.Max), r[2], valueType); err != nil {
   342  			return nil, err
   343  		}
   344  		return t, nil
   345  	}
   346  	floatTypeHandle := func(s string) (*ParameterRestrict, error) {
   347  		r := FloatRangeRegex.FindStringSubmatch(s)
   348  		if len(r) == 0 {
   349  			return nil, nil
   350  		}
   351  		t := &ParameterRestrict{isEnum: false}
   352  		if err := setValueHelper(reflect.ValueOf(&t.Min), r[1], valueType); err != nil {
   353  			return nil, err
   354  		}
   355  		if err := setValueHelper(reflect.ValueOf(&t.Max), r[4], valueType); err != nil {
   356  			return nil, err
   357  		}
   358  		return t, nil
   359  	}
   360  
   361  	if s == "" {
   362  		return nil, nil
   363  	}
   364  
   365  	switch valueType {
   366  	case IntegerType:
   367  		pr, err = integerTypeHandle(s)
   368  	case FloatType:
   369  		pr, err = floatTypeHandle(s)
   370  	}
   371  
   372  	if err != nil {
   373  		return nil, err
   374  	}
   375  	if pr != nil {
   376  		return pr, nil
   377  	}
   378  
   379  	return parseListParameter(s, valueType)
   380  }
   381  
   382  func parseListParameter(s string, valueType ValueType) (*ParameterRestrict, error) {
   383  	values := strings.Split(s, ",")
   384  	if len(values) == 0 {
   385  		return nil, nil
   386  	}
   387  
   388  	p := &ParameterRestrict{isEnum: true}
   389  	for _, v := range values {
   390  		v = strings.TrimSpace(v)
   391  		if len(v) == 0 {
   392  			continue
   393  		}
   394  		if typeValue, err := ValueTypeParserMap[valueType](v); err != nil {
   395  			return nil, cfgcore.WrapError(err, "parse failed: [%s] [%s] [%s]\n", s, v, valueType)
   396  		} else {
   397  			AddRestrictValue(p, typeValue)
   398  		}
   399  	}
   400  	if len(p.EnumList) == 0 {
   401  		return nil, nil
   402  	}
   403  	return p, nil
   404  }
   405  
   406  func AddRestrictValue(p *ParameterRestrict, value interface{}) {
   407  	if p.EnumList == nil {
   408  		p.EnumList = make([]interface{}, 0)
   409  	}
   410  
   411  	p.EnumList = append(p.EnumList, value)
   412  }
   413  
   414  type CueWrapper struct {
   415  	writer io.Writer
   416  	*ParameterType
   417  }
   418  
   419  func (w *CueWrapper) output() int {
   420  	var buffer bytes.Buffer
   421  	w.generateCueDocument(&buffer)
   422  	w.generateCueTypeParameter(&buffer)
   423  	w.generateCueRestrict(&buffer)
   424  	w.generateCueDefaultValue(&buffer)
   425  
   426  	_ = buffer.WriteByte('\n')
   427  	_ = buffer.WriteByte('\n')
   428  	b, _ := w.writer.Write(buffer.Bytes())
   429  	return b
   430  }
   431  
   432  func (w *CueWrapper) generateCueDefaultValue(buffer *bytes.Buffer) {
   433  	if w.DefaultValue == "" {
   434  		return
   435  	}
   436  	buffer.WriteString(" | *")
   437  	generateElemValue(buffer, w.DefaultValue, w.Type)
   438  }
   439  
   440  func (w *CueWrapper) generateCueRestrict(buffer *bytes.Buffer) {
   441  	if w.ParameterRestrict != nil {
   442  		generateRestrictParam(buffer, w.ParameterRestrict, w.Type)
   443  	}
   444  }
   445  
   446  func (w *CueWrapper) generateCueTypeParameter(buffer *bytes.Buffer) {
   447  	buffer.WriteString(prefixString + "  ")
   448  	if strings.ContainsAny(w.Name, "-.") {
   449  		buffer.WriteByte('"')
   450  		buffer.WriteString(w.Name)
   451  		buffer.WriteByte('"')
   452  	} else {
   453  		buffer.WriteString(w.Name)
   454  	}
   455  
   456  	if w.DefaultValue == "" {
   457  		buffer.WriteByte('?')
   458  	}
   459  	buffer.WriteString(": ")
   460  	switch w.Type {
   461  	case IntegerType:
   462  		buffer.WriteString("int")
   463  	case BooleanType:
   464  		buffer.WriteString("bool")
   465  	default:
   466  		buffer.WriteString(string(w.Type))
   467  	}
   468  }
   469  
   470  func (w *CueWrapper) generateCueDocument(buffer *bytes.Buffer) {
   471  	if w.Document != "" {
   472  		buffer.WriteString(prefixString + "  ")
   473  		buffer.WriteString("// ")
   474  		buffer.WriteString(w.Document)
   475  		buffer.WriteByte('\n')
   476  	}
   477  }