github.com/simonferquel/app@v0.6.1-0.20181012141724-68b7cccf26ac/pkg/yatee/yatee.go (about)

     1  package yatee
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"github.com/docker/app/internal/yaml"
    11  	yml "gopkg.in/yaml.v2"
    12  )
    13  
    14  const (
    15  	// OptionErrOnMissingKey if set will make rendering fail if a non-existing variable is used
    16  	OptionErrOnMissingKey = "ErrOnMissingKey"
    17  )
    18  
    19  type options struct {
    20  	errOnMissingKey bool
    21  }
    22  
    23  // flatten flattens a structure: foo.bar.baz -> 'foo.bar.baz'
    24  func flatten(in map[string]interface{}, out map[string]interface{}, prefix string) {
    25  	for k, v := range in {
    26  		switch vv := v.(type) {
    27  		case string:
    28  			out[prefix+k] = vv
    29  		case map[string]interface{}:
    30  			flatten(vv, out, prefix+k+".")
    31  		case []interface{}:
    32  			values := []string{}
    33  			for _, i := range vv {
    34  				values = append(values, fmt.Sprintf("%v", i))
    35  			}
    36  			out[prefix+k] = strings.Join(values, " ")
    37  		default:
    38  			out[prefix+k] = v
    39  		}
    40  	}
    41  }
    42  
    43  func merge(res map[string]interface{}, src map[interface{}]interface{}) {
    44  	for k, v := range src {
    45  		kk, ok := k.(string)
    46  		if !ok {
    47  			panic(fmt.Sprintf("fatal error, key %v in %#v is not a string", k, src))
    48  		}
    49  		eval, ok := res[kk]
    50  		switch vv := v.(type) {
    51  		case map[interface{}]interface{}:
    52  			if !ok {
    53  				res[kk] = make(map[string]interface{})
    54  			} else {
    55  				if _, ok2 := eval.(map[string]interface{}); !ok2 {
    56  					res[kk] = make(map[string]interface{})
    57  				}
    58  			}
    59  			merge(res[kk].(map[string]interface{}), vv)
    60  		default:
    61  			res[kk] = vv
    62  		}
    63  	}
    64  }
    65  
    66  // LoadSettings loads a set of settings file and produce a property dictionary
    67  func LoadSettings(files []string) (map[string]interface{}, error) {
    68  	res := make(map[string]interface{})
    69  	for _, f := range files {
    70  		data, err := ioutil.ReadFile(f)
    71  		if err != nil {
    72  			return nil, err
    73  		}
    74  		s := make(map[interface{}]interface{})
    75  		err = yaml.Unmarshal(data, &s)
    76  		if err != nil {
    77  			return nil, err
    78  		}
    79  		merge(res, s)
    80  	}
    81  	return res, nil
    82  }
    83  
    84  func isIdentNumChar(r byte) bool {
    85  	return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') ||
    86  		r == '.' || r == '_'
    87  }
    88  
    89  // extract extracts an expression from a string
    90  // nolint: gocyclo
    91  func extract(expr string) (string, error) {
    92  	if expr == "" {
    93  		return "", nil
    94  	}
    95  	if expr[0] == '{' {
    96  		closing := strings.Index(expr, "}")
    97  		if closing == -1 {
    98  			return "", fmt.Errorf("Missing '}' at end of expression")
    99  		}
   100  		return expr[0 : closing+1], nil
   101  	}
   102  	if expr[0] == '(' {
   103  		indent := 1
   104  		i := 1
   105  		for ; i < len(expr); i++ {
   106  			if expr[i] == '(' {
   107  				indent++
   108  			}
   109  			if expr[i] == ')' {
   110  				indent--
   111  			}
   112  			if indent == 0 {
   113  				break
   114  			}
   115  		}
   116  		if indent != 0 {
   117  			return "", fmt.Errorf("Missing ')' at end of expression")
   118  		}
   119  		return expr[0 : i+1], nil
   120  	}
   121  	i := 0
   122  	for ; i < len(expr); i++ {
   123  		if !((expr[i] >= 'a' && expr[i] <= 'z') || (expr[i] >= 'A' && expr[i] <= 'Z') ||
   124  			expr[i] == '.' || expr[i] == '_') {
   125  			break
   126  		}
   127  	}
   128  	return expr[0:i], nil
   129  }
   130  
   131  func tokenize(expr string) ([]string, error) {
   132  	var tokens []string
   133  	p := 0
   134  	for p < len(expr) {
   135  		if isIdentNumChar(expr[p]) {
   136  			pp := p + 1
   137  			for ; pp < len(expr) && isIdentNumChar(expr[pp]); pp++ {
   138  			}
   139  			tokens = append(tokens, expr[p:pp])
   140  			p = pp
   141  		} else {
   142  			if expr[p] != ' ' {
   143  				tokens = append(tokens, expr[p:p+1])
   144  			}
   145  			p++
   146  		}
   147  	}
   148  	return tokens, nil
   149  }
   150  
   151  func evalValue(comps []string, i int) (int64, int, error) {
   152  	c := comps[i]
   153  	if c == "(" {
   154  		value, ni, error := evalSub(comps, i+1)
   155  		if error != nil {
   156  			return 0, 0, error
   157  		}
   158  		return value, ni, nil
   159  	}
   160  	v, err := strconv.ParseInt(c, 0, 64)
   161  	return v, i + 1, err
   162  }
   163  
   164  func evalSub(comps []string, i int) (int64, int, error) {
   165  	current, next, err := evalValue(comps, i)
   166  	if err != nil {
   167  		return 0, 0, err
   168  	}
   169  	i = next
   170  	for i < len(comps) {
   171  		c := comps[i]
   172  		if c == ")" {
   173  			return current, i + 1, nil
   174  		}
   175  		if c == "*" || c == "+" || c == "-" || c == "/" || c == "%" {
   176  			rhs, next, err := evalValue(comps, i+1)
   177  			if err != nil {
   178  				return 0, 0, err
   179  			}
   180  			switch c {
   181  			case "+":
   182  				current += rhs
   183  			case "-":
   184  				current -= rhs
   185  			case "/":
   186  				current /= rhs
   187  			case "*":
   188  				current *= rhs
   189  			case "%":
   190  				current %= rhs
   191  			}
   192  			i = next
   193  		} else {
   194  			return 0, 0, fmt.Errorf("expected operator")
   195  		}
   196  	}
   197  	return current, i, nil
   198  }
   199  
   200  // resolves an arithmetic expression
   201  func evalExpr(expr string) (int64, error) {
   202  	comps, err := tokenize(expr)
   203  	if err != nil {
   204  		return 0, err
   205  	}
   206  	v, _, err := evalSub(comps, 0)
   207  	return v, err
   208  }
   209  
   210  // resolves and evaluate all ${foo.bar}, $foo.bar and $(expr) in epr
   211  // nolint: gocyclo
   212  func eval(expr string, flattened map[string]interface{}, o options) (interface{}, error) {
   213  	// Since we go from right to left to support nesting, handling $$ escape is
   214  	// painful, so just hide them and restore them at the end
   215  	expr = strings.Replace(expr, "$$", "\x00", -1)
   216  	end := len(expr)
   217  	// If evaluation resolves to a single value, return the type value, not a string
   218  	var bypass interface{}
   219  	iteration := 0
   220  	for {
   221  		iteration++
   222  		if iteration > 100 {
   223  			return "", fmt.Errorf("eval loop detected")
   224  		}
   225  		i := strings.LastIndex(expr[0:end], "$")
   226  		if i == -1 {
   227  			break
   228  		}
   229  		bypass = nil
   230  		comp, err := extract(expr[i+1:])
   231  		if err != nil {
   232  			return "", err
   233  		}
   234  		var val interface{}
   235  		if len(comp) != 0 && comp[0] == '(' {
   236  			var err error
   237  			val, err = evalExpr(comp[1 : len(comp)-1])
   238  			if err != nil {
   239  				return "", err
   240  			}
   241  		} else {
   242  			var ok bool
   243  			if len(comp) != 0 && comp[0] == '{' {
   244  				content := comp[1 : len(comp)-1]
   245  				q := strings.Index(content, "?")
   246  				if q != -1 {
   247  					s := strings.Index(content, ":")
   248  					if s == -1 {
   249  						return "", fmt.Errorf("parse error in ternary '%s', missing ':'", content)
   250  					}
   251  					variable := content[0:q]
   252  					val, ok = flattened[variable]
   253  					if isTrue(fmt.Sprintf("%v", val)) {
   254  						val = content[q+1 : s]
   255  					} else {
   256  						val = content[s+1:]
   257  					}
   258  				} else {
   259  					val, ok = flattened[comp[1:len(comp)-1]]
   260  				}
   261  			} else {
   262  				val, ok = flattened[comp]
   263  			}
   264  			if !ok {
   265  				if o.errOnMissingKey {
   266  					return "", fmt.Errorf("variable '%s' not set", comp)
   267  				}
   268  				fmt.Fprintf(os.Stderr, "variable '%s' not set, expanding to empty string", comp)
   269  			}
   270  		}
   271  		valstr := fmt.Sprintf("%v", val)
   272  		expr = expr[0:i] + valstr + expr[i+1+len(comp):]
   273  		if strings.Trim(expr, " ") == valstr {
   274  			bypass = val
   275  		}
   276  		end = len(expr)
   277  	}
   278  	if bypass != nil {
   279  		return bypass, nil
   280  	}
   281  	expr = strings.Replace(expr, "\x00", "$", -1)
   282  	return expr, nil
   283  }
   284  
   285  func isTrue(cond string) bool {
   286  	ct := strings.TrimLeft(cond, " ")
   287  	reverse := len(cond) != 0 && cond[0] == '!'
   288  	if reverse {
   289  		cond = ct[1:]
   290  	}
   291  	cond = strings.Trim(cond, " ")
   292  	return (cond != "" && cond != "false" && cond != "0") != reverse
   293  }
   294  
   295  func recurseList(input []interface{}, settings map[string]interface{}, flattened map[string]interface{}, o options) ([]interface{}, error) {
   296  	var res []interface{}
   297  	for _, v := range input {
   298  		switch vv := v.(type) {
   299  		case yml.MapSlice:
   300  			newv, err := recurse(vv, settings, flattened, o)
   301  			if err != nil {
   302  				return nil, err
   303  			}
   304  			res = append(res, newv)
   305  		case []interface{}:
   306  			newv, err := recurseList(vv, settings, flattened, o)
   307  			if err != nil {
   308  				return nil, err
   309  			}
   310  			res = append(res, newv)
   311  		case string:
   312  			vvv, err := eval(vv, flattened, o)
   313  			if err != nil {
   314  				return nil, err
   315  			}
   316  			if vvvs, ok := vvv.(string); ok {
   317  				trimed := strings.TrimLeft(vvvs, " ")
   318  				if strings.HasPrefix(trimed, "@if") {
   319  					be := strings.Index(trimed, "(")
   320  					ee := strings.Index(trimed, ")")
   321  					if be == -1 || ee == -1 || be > ee {
   322  						return nil, fmt.Errorf("parse error looking for if condition in '%s'", vvv)
   323  					}
   324  					cond := trimed[be+1 : ee]
   325  					if isTrue(cond) {
   326  						res = append(res, strings.Trim(trimed[ee+1:], " "))
   327  					}
   328  					continue
   329  				}
   330  			}
   331  			res = append(res, vvv)
   332  		default:
   333  			res = append(res, v)
   334  		}
   335  	}
   336  	return res, nil
   337  }
   338  
   339  // FIXME complexity on this is 47… get it lower than 16
   340  // nolint: gocyclo
   341  func recurse(input yml.MapSlice, settings map[string]interface{}, flattened map[string]interface{}, o options) (yml.MapSlice, error) {
   342  	res := yml.MapSlice{}
   343  	for _, kvp := range input {
   344  		k := kvp.Key
   345  		v := kvp.Value
   346  		rk := k
   347  		kstr, isks := k.(string)
   348  		if isks {
   349  			trimed := strings.TrimLeft(kstr, " ")
   350  			if strings.HasPrefix(trimed, "@switch ") {
   351  				mii, ok := v.(yml.MapSlice)
   352  				if !ok {
   353  					return nil, fmt.Errorf("@switch value must be a mapping")
   354  				}
   355  				key, err := eval(strings.TrimPrefix(trimed, "@switch "), flattened, o)
   356  				if err != nil {
   357  					return nil, err
   358  				}
   359  				var defaultValue interface{}
   360  				hit := false
   361  				for _, sval := range mii {
   362  					sk := sval.Key
   363  					sv := sval.Value
   364  					ssk, ok := sk.(string)
   365  					if !ok {
   366  						return nil, fmt.Errorf("@switch entry key must be a string")
   367  					}
   368  					if ssk == "default" {
   369  						defaultValue = sv
   370  					}
   371  					if ssk == key {
   372  						hit = true
   373  						svv, ok := sv.(yml.MapSlice)
   374  						if !ok {
   375  							return nil, fmt.Errorf("@switch entry must be a mapping")
   376  						}
   377  						for _, vval := range svv {
   378  							res = append(res, yml.MapItem{Key: vval.Key, Value: vval.Value})
   379  						}
   380  					}
   381  				}
   382  				if !hit && defaultValue != nil {
   383  					svv, ok := defaultValue.(yml.MapSlice)
   384  					if !ok {
   385  						return nil, fmt.Errorf("@switch entry must be a mapping")
   386  					}
   387  					for _, vval := range svv {
   388  						res = append(res, yml.MapItem{Key: vval.Key, Value: vval.Value})
   389  					}
   390  				}
   391  				continue
   392  			}
   393  			if strings.HasPrefix(trimed, "@for ") {
   394  				mii, ok := v.(yml.MapSlice)
   395  				if !ok {
   396  					return nil, fmt.Errorf("@for value must be a mapping")
   397  				}
   398  				comps := strings.SplitN(trimed, " ", 4)
   399  				varname := comps[1]
   400  				varrangeraw, err := eval(comps[3], flattened, o)
   401  				if err != nil {
   402  					return nil, err
   403  				}
   404  				varrange, ok := varrangeraw.(string)
   405  				if !ok {
   406  					return nil, fmt.Errorf("@for argument must be a string")
   407  				}
   408  				mayberange := strings.Split(varrange, "..")
   409  				if len(mayberange) == 2 {
   410  					rangestart, err := strconv.ParseInt(mayberange[0], 0, 64)
   411  					if err != nil {
   412  						return nil, err
   413  					}
   414  					rangeend, err := strconv.ParseInt(mayberange[1], 0, 64)
   415  					if err != nil {
   416  						return nil, err
   417  					}
   418  					for i := rangestart; i < rangeend; i++ {
   419  						flattened[varname] = fmt.Sprintf("%v", i)
   420  						val, err := recurse(mii, settings, flattened, o)
   421  						if err != nil {
   422  							return nil, err
   423  						}
   424  						for _, vval := range val {
   425  							res = append(res, yml.MapItem{Key: vval.Key, Value: vval.Value})
   426  						}
   427  					}
   428  				} else {
   429  					// treat range as a list
   430  					rangevalues := strings.Split(varrange, " ")
   431  					for _, i := range rangevalues {
   432  						flattened[varname] = i
   433  						val, err := recurse(mii, settings, flattened, o)
   434  						if err != nil {
   435  							return nil, err
   436  						}
   437  						for _, vval := range val {
   438  							res = append(res, yml.MapItem{Key: vval.Key, Value: vval.Value})
   439  						}
   440  					}
   441  				}
   442  				continue
   443  			}
   444  			if strings.HasPrefix(trimed, "@if ") {
   445  				cond, err := eval(strings.TrimPrefix(trimed, "@if "), flattened, o)
   446  				if err != nil {
   447  					return nil, err
   448  				}
   449  				mii, ok := v.(yml.MapSlice)
   450  				if !ok {
   451  					return nil, fmt.Errorf("@if value must be a mapping")
   452  				}
   453  				if isTrue(fmt.Sprintf("%v", cond)) {
   454  					val, err := recurse(mii, settings, flattened, o)
   455  					if err != nil {
   456  						return nil, err
   457  					}
   458  					for _, vval := range val {
   459  						if vval.Key != "@else" {
   460  							res = append(res, yml.MapItem{Key: vval.Key, Value: vval.Value})
   461  						}
   462  					}
   463  				} else {
   464  					var elseClause interface{}
   465  					for _, miiv := range mii {
   466  						if miiv.Key == "@else" {
   467  							elseClause = miiv.Value
   468  							break
   469  						}
   470  					}
   471  					if elseClause != nil {
   472  						elseDict, ok := elseClause.(yml.MapSlice)
   473  						if !ok {
   474  							return nil, fmt.Errorf("@else value must be a mapping")
   475  						}
   476  						for _, vval := range elseDict {
   477  							res = append(res, yml.MapItem{Key: vval.Key, Value: vval.Value})
   478  						}
   479  					}
   480  				}
   481  				continue
   482  			}
   483  			rstr, err := eval(kstr, flattened, o)
   484  			if err != nil {
   485  				return nil, err
   486  			}
   487  			rk = rstr
   488  		}
   489  		switch vv := v.(type) {
   490  		case yml.MapSlice:
   491  			newv, err := recurse(vv, settings, flattened, o)
   492  			if err != nil {
   493  				return nil, err
   494  			}
   495  			res = append(res, yml.MapItem{Key: rk, Value: newv})
   496  		case []interface{}:
   497  			newv, err := recurseList(vv, settings, flattened, o)
   498  			if err != nil {
   499  				return nil, err
   500  			}
   501  			res = append(res, yml.MapItem{Key: rk, Value: newv})
   502  		case string:
   503  			vvv, err := eval(vv, flattened, o)
   504  			if err != nil {
   505  				return nil, err
   506  			}
   507  			res = append(res, yml.MapItem{Key: rk, Value: vvv})
   508  		default:
   509  			res = append(res, yml.MapItem{Key: rk, Value: v})
   510  		}
   511  	}
   512  	return res, nil
   513  }
   514  
   515  // ProcessStrings resolves input templated yaml using values in settings yaml
   516  func ProcessStrings(input, settings string) (string, error) {
   517  	ps := make(map[interface{}]interface{})
   518  	err := yaml.Unmarshal([]byte(settings), ps)
   519  	if err != nil {
   520  		return "", err
   521  	}
   522  	s := make(map[string]interface{})
   523  	merge(s, ps)
   524  	res, err := Process(input, s)
   525  	if err != nil {
   526  		return "", err
   527  	}
   528  	sres, err := yaml.Marshal(res)
   529  	if err != nil {
   530  		return "", err
   531  	}
   532  	return string(sres), nil
   533  }
   534  
   535  // ProcessWithOrder resolves input templated yaml using values given in settings, returning a MapSlice with order preserved
   536  func ProcessWithOrder(inputString string, settings map[string]interface{}, opts ...string) (yml.MapSlice, error) {
   537  	var o options
   538  	for _, v := range opts {
   539  		switch v {
   540  		case OptionErrOnMissingKey:
   541  			o.errOnMissingKey = true
   542  		default:
   543  			return nil, fmt.Errorf("unknown option %q", v)
   544  		}
   545  	}
   546  	var input yml.MapSlice
   547  	err := yaml.Unmarshal([]byte(inputString), &input)
   548  	if err != nil {
   549  		return nil, err
   550  	}
   551  	flattened := make(map[string]interface{})
   552  	flatten(settings, flattened, "")
   553  	return recurse(input, settings, flattened, o)
   554  }
   555  
   556  // Process resolves input templated yaml using values given in settings, returning a map
   557  func Process(inputString string, settings map[string]interface{}, opts ...string) (map[interface{}]interface{}, error) {
   558  	mapSlice, err := ProcessWithOrder(inputString, settings, opts...)
   559  	if err != nil {
   560  		return nil, err
   561  	}
   562  
   563  	res, err := convert(mapSlice)
   564  	if err != nil {
   565  		return nil, err
   566  	}
   567  	return res, nil
   568  }
   569  
   570  func convert(mapSlice yml.MapSlice) (map[interface{}]interface{}, error) {
   571  	res := make(map[interface{}]interface{})
   572  	for _, kv := range mapSlice {
   573  		v := kv.Value
   574  		castValue, ok := v.(yml.MapSlice)
   575  		if !ok {
   576  			res[kv.Key] = kv.Value
   577  		} else {
   578  			recursed, err := convert(castValue)
   579  			if err != nil {
   580  				return nil, err
   581  			}
   582  			res[kv.Key] = recursed
   583  		}
   584  	}
   585  	return res, nil
   586  }