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 }