github.com/lyraproj/hiera@v1.0.0-rc4/hiera/hiera.go (about)

     1  // Package hiera contains the Lookup functions to use when using Hiera as a library.
     2  package hiera
     3  
     4  import (
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"os"
    10  	"regexp"
    11  	"strings"
    12  
    13  	"github.com/tada/catch"
    14  
    15  	"github.com/lyraproj/dgo/dgo"
    16  	"github.com/lyraproj/dgo/typ"
    17  	"github.com/lyraproj/dgo/vf"
    18  	"github.com/lyraproj/dgoyaml/yaml"
    19  	"github.com/lyraproj/hiera/api"
    20  	"github.com/lyraproj/hiera/explain"
    21  	"github.com/lyraproj/hiera/session"
    22  	"github.com/lyraproj/hierasdk/hiera"
    23  )
    24  
    25  // A CommandOptions contains the options given by to the CLI lookup command or a REST invocation.
    26  type CommandOptions struct {
    27  	// Type is a  Type string such as "string" or "[]int" used for assertion of the
    28  	// found value.
    29  	Type string
    30  
    31  	// Merge is the name of a merge strategy
    32  	Merge string
    33  
    34  	// Default is a pointer to the string representation of a default value or nil if no default value exists
    35  	Default *string
    36  
    37  	// FactPaths are an optional paths to a files containing extra variables to add to the lookup scope
    38  	// and as a copy under the lookup scope "facts" key.
    39  	FactPaths []string
    40  
    41  	// VarPaths are an optional paths to a files containing extra variables to add to the lookup scope
    42  	VarPaths []string
    43  
    44  	// Variables are an optional paths to a files containing extra variables to add to the lookup scope
    45  	Variables []string
    46  
    47  	// RenderAs is the name of the desired rendering
    48  	RenderAs string
    49  
    50  	// ExplainData should be set to true to explain the progress of a lookup
    51  	ExplainData bool
    52  
    53  	// ExplainOptions should be set to true to explain how lookup options were found for the lookup
    54  	ExplainOptions bool
    55  }
    56  
    57  // Lookup performs a lookup using the given parameters.
    58  //
    59  // ic - The lookup invocation
    60  //
    61  // name - The name to lookup
    62  //
    63  // defaultValue - Optional value to use as default when no value is found
    64  //
    65  // options - Optional map with merge strategy and options
    66  func Lookup(ic api.Invocation, name string, defaultValue dgo.Value, options interface{}) dgo.Value {
    67  	return Lookup2(ic, []string{name}, typ.Any, defaultValue, nil, nil, api.ToMap(`lookup options`, options), nil)
    68  }
    69  
    70  // Lookup2 performs a lookup using the given parameters.
    71  //
    72  // ic - The lookup invocation
    73  //
    74  // names[] - The name or names to lookup
    75  //
    76  // valueType - Optional expected type of the found value
    77  //
    78  // defaultValue - Optional value to use as default when no value is found
    79  //
    80  // override - Optional map to use as override. Values found here are returned immediately (no merge)
    81  //
    82  // defaultValuesHash - Optional map to use as the last resort (but before defaultValue)
    83  //
    84  // options - Optional map with merge strategy and options
    85  //
    86  // defaultFunc - Optional function to produce a default value
    87  func Lookup2(
    88  	ic api.Invocation,
    89  	names []string,
    90  	valueType dgo.Type,
    91  	defaultValue dgo.Value,
    92  	override dgo.Map,
    93  	defaultValuesHash dgo.Map,
    94  	options dgo.Map,
    95  	defaultFunc dgo.Producer) dgo.Value {
    96  	if v := lookupInMap(names, override); v != nil {
    97  		return ensureType(valueType, v)
    98  	}
    99  	for _, name := range names {
   100  		if v := ic.Lookup(api.NewKey(name), options); v != nil {
   101  			return ensureType(valueType, v)
   102  		}
   103  	}
   104  	if v := lookupInMap(names, defaultValuesHash); v != nil {
   105  		return ensureType(valueType, v)
   106  	}
   107  	if defaultValue != nil {
   108  		return ensureType(valueType, defaultValue)
   109  	}
   110  	if defaultFunc != nil {
   111  		return ensureType(valueType, defaultFunc())
   112  	}
   113  	return nil
   114  }
   115  
   116  func lookupInMap(names []string, m dgo.Map) dgo.Value {
   117  	if m != nil && m.Len() > 0 {
   118  		for _, name := range names {
   119  			if dv := m.Get(name); dv != nil {
   120  				return dv
   121  			}
   122  		}
   123  	}
   124  	return nil
   125  }
   126  
   127  func ensureType(t dgo.Type, v dgo.Value) dgo.Value {
   128  	if t == nil || t.Instance(v) {
   129  		return v
   130  	}
   131  	return vf.New(t, v)
   132  }
   133  
   134  // TryWithParent initializes a lookup context with global options and a top-level lookup key function and then calls
   135  // the given consumer function with that context. If the given function panics, the panic will be recovered and returned
   136  // as an error.
   137  func TryWithParent(parent context.Context, tp hiera.LookupKey, options interface{}, consumer func(api.Session) error) error {
   138  	return catch.Do(func() {
   139  		s := session.New(parent, tp, options, nil)
   140  		defer s.KillPlugins()
   141  		err := consumer(s)
   142  		if err != nil {
   143  			panic(err)
   144  		}
   145  	})
   146  }
   147  
   148  // DoWithParent initializes a lookup context with global options and a top-level lookup key function and then calls
   149  // the given consumer function with that context.
   150  func DoWithParent(parent context.Context, tp hiera.LookupKey, options interface{}, consumer func(api.Session)) {
   151  	s := session.New(parent, tp, options, nil)
   152  	defer s.KillPlugins()
   153  	consumer(s)
   154  }
   155  
   156  // varSplit splits on either ':' or '=' but not on '::', ':=', '=:' or '=='
   157  var varSplit = regexp.MustCompile(`\A(.*?[^:=])[:=]([^:=].*)\z`)
   158  var needParsePrefix = []string{`{`, `[`, `"`, `'`}
   159  
   160  // LookupAndRender performs a lookup using the given command options and arguments and renders the result on the given
   161  // io.Writer in accordance with the `RenderAs` option.
   162  func LookupAndRender(c api.Session, opts *CommandOptions, args []string, out io.Writer) bool {
   163  	tp := typ.Any
   164  	dl := c.Dialect()
   165  	if opts.Type != `` {
   166  		tp = dl.ParseType(nil, vf.String(opts.Type))
   167  	}
   168  
   169  	var options dgo.Map
   170  	if !(opts.Merge == `` || opts.Merge == `first`) {
   171  		options = vf.Map(`merge`, opts.Merge)
   172  	}
   173  
   174  	var dv dgo.Value
   175  	if opts.Default != nil {
   176  		s := *opts.Default
   177  		if s == `` {
   178  			dv = vf.String(``)
   179  		} else {
   180  			dv = parseCommandLineValue(c, s)
   181  		}
   182  	}
   183  
   184  	var explainer api.Explainer
   185  	if opts.ExplainData || opts.ExplainOptions {
   186  		explainer = explain.NewExplainer(opts.ExplainOptions, opts.ExplainOptions && !opts.ExplainData)
   187  	}
   188  
   189  	found := Lookup2(c.Invocation(createScope(c, opts), explainer), args, tp, dv, nil, nil, options, nil)
   190  	if explainer != nil {
   191  		renderAs := Text
   192  		if opts.RenderAs != `` {
   193  			renderAs = RenderName(opts.RenderAs)
   194  		}
   195  		Render(c, renderAs, explainer, out)
   196  		return found != nil
   197  	}
   198  
   199  	if found == nil {
   200  		return false
   201  	}
   202  
   203  	renderAs := YAML
   204  	if opts.RenderAs != `` {
   205  		renderAs = RenderName(opts.RenderAs)
   206  	}
   207  	Render(c, renderAs, found, out)
   208  	return true
   209  }
   210  
   211  func parseCommandLineValue(c api.Session, vs string) dgo.Value {
   212  	vs = strings.TrimSpace(vs)
   213  	for _, pfx := range needParsePrefix {
   214  		if strings.HasPrefix(vs, pfx) {
   215  			var v dgo.Value
   216  			c.AliasMap().Collect(func(aa dgo.AliasAdder) {
   217  				v = c.Dialect().ParseType(aa, vf.String(vs))
   218  			})
   219  			return v
   220  		}
   221  	}
   222  	return vf.String(vs)
   223  }
   224  
   225  func createScope(c api.Session, opts *CommandOptions) dgo.Map {
   226  	scope := vf.MutableMap()
   227  	if vl := len(opts.Variables); vl > 0 {
   228  		for _, e := range opts.Variables {
   229  			if m := varSplit.FindStringSubmatch(e); m != nil {
   230  				key := strings.TrimSpace(m[1])
   231  				scope.Put(key, parseCommandLineValue(c, m[2]))
   232  			} else {
   233  				panic(catch.Error("unable to parse variable '%s'", e))
   234  			}
   235  		}
   236  	}
   237  
   238  	addVarPaths(opts.VarPaths, scope)
   239  	if len(opts.FactPaths) > 0 {
   240  		facts := vf.MutableMap()
   241  		addVarPaths(opts.FactPaths, facts)
   242  		scope.PutAll(facts)
   243  		scope.Put(`facts`, facts)
   244  	}
   245  	return scope
   246  }
   247  
   248  func addVarPaths(varPaths []string, m dgo.Map) {
   249  	for _, vars := range varPaths {
   250  		var bs []byte
   251  		var err error
   252  		if vars == `-` {
   253  			bs, err = ioutil.ReadAll(os.Stdin)
   254  		} else {
   255  			bs, err = ioutil.ReadFile(vars)
   256  		}
   257  		if err == nil && len(bs) > 0 {
   258  			var yv dgo.Value
   259  			if yv, err = yaml.Unmarshal(bs); err == nil {
   260  				if data, ok := yv.(dgo.Map); ok {
   261  					m.PutAll(data)
   262  				} else {
   263  					err = fmt.Errorf(`file '%s' does not contain a YAML hash`, vars)
   264  				}
   265  			}
   266  		}
   267  		if err != nil {
   268  			panic(err)
   269  		}
   270  	}
   271  }