github.com/splunk/dan1-qbec@v0.7.3/internal/vm/vm.go (about) 1 /* 2 Copyright 2019 Splunk Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // Package vm allows flexible creation of a Jsonnet VM. 18 package vm 19 20 import ( 21 "bufio" 22 "bytes" 23 "fmt" 24 "io/ioutil" 25 "os" 26 "strings" 27 28 "github.com/google/go-jsonnet" 29 "github.com/pkg/errors" 30 "github.com/spf13/cobra" 31 ) 32 33 // Config is the desired configuration of the Jsonnet VM. 34 type Config struct { 35 vars map[string]string // string variables keyed by name 36 codeVars map[string]string // code variables keyed by name 37 topLevelVars map[string]string // TLA string vars keyed by name 38 topLevelCodeVars map[string]string // TLA code vars keyed by name 39 importer jsonnet.Importer // optional custom importer - default is the filesystem importer 40 libPaths []string // library paths in filesystem, ignored when a custom importer is specified 41 } 42 43 func copyArray(in []string) []string { 44 return append([]string{}, in...) 45 } 46 47 func copyMap(m map[string]string) map[string]string { 48 ret := map[string]string{} 49 if m == nil { 50 return nil 51 } 52 for k, v := range m { 53 ret[k] = v 54 } 55 return ret 56 } 57 58 func copyMapNonNil(m map[string]string) map[string]string { 59 ret := copyMap(m) 60 if ret == nil { 61 ret = map[string]string{} 62 } 63 return ret 64 } 65 66 // Clone creates a clone of this config. 67 func (c Config) clone() Config { 68 ret := Config{ 69 importer: c.importer, 70 } 71 ret.vars = copyMap(c.vars) 72 ret.codeVars = copyMap(c.codeVars) 73 ret.topLevelVars = copyMap(c.topLevelVars) 74 ret.topLevelCodeVars = copyMap(c.topLevelCodeVars) 75 ret.libPaths = copyArray(c.libPaths) 76 return ret 77 } 78 79 // Vars returns the string external variables defined for this config. 80 func (c Config) Vars() map[string]string { 81 return copyMapNonNil(c.vars) 82 } 83 84 // CodeVars returns the code external variables defined for this config. 85 func (c Config) CodeVars() map[string]string { 86 return copyMapNonNil(c.codeVars) 87 } 88 89 // TopLevelVars returns the string top-level variables defined for this config. 90 func (c Config) TopLevelVars() map[string]string { 91 return copyMapNonNil(c.topLevelVars) 92 } 93 94 // TopLevelCodeVars returns the code top-level variables defined for this config. 95 func (c Config) TopLevelCodeVars() map[string]string { 96 return copyMapNonNil(c.topLevelCodeVars) 97 } 98 99 // LibPaths returns the library paths for this config. 100 func (c Config) LibPaths() []string { 101 return copyArray(c.libPaths) 102 } 103 104 func keyExists(m map[string]string, key string) bool { 105 if m == nil { 106 return false 107 } 108 _, ok := m[key] 109 return ok 110 } 111 112 // HasVar returns true if the specified external variable is defined. 113 func (c Config) HasVar(name string) bool { 114 return keyExists(c.vars, name) || keyExists(c.codeVars, name) 115 } 116 117 // HasTopLevelVar returns true if the specified TLA variable is defined. 118 func (c Config) HasTopLevelVar(name string) bool { 119 return keyExists(c.topLevelVars, name) || keyExists(c.topLevelCodeVars, name) 120 } 121 122 // WithoutTopLevel returns a config that does not have any top level variables set. 123 func (c Config) WithoutTopLevel() Config { 124 if len(c.topLevelCodeVars) == 0 && len(c.topLevelVars) == 0 { 125 return c 126 } 127 clone := c.clone() 128 clone.topLevelVars = nil 129 clone.topLevelCodeVars = nil 130 return clone 131 } 132 133 // WithCodeVars returns a config with additional code variables in its environment. 134 func (c Config) WithCodeVars(add map[string]string) Config { 135 if len(add) == 0 { 136 return c 137 } 138 clone := c.clone() 139 if clone.codeVars == nil { 140 clone.codeVars = map[string]string{} 141 } 142 for k, v := range add { 143 clone.codeVars[k] = v 144 } 145 return clone 146 } 147 148 // WithTopLevelCodeVars returns a config with additional top-level code variables in its environment. 149 func (c Config) WithTopLevelCodeVars(add map[string]string) Config { 150 if len(add) == 0 { 151 return c 152 } 153 clone := c.clone() 154 if clone.topLevelCodeVars == nil { 155 clone.topLevelCodeVars = map[string]string{} 156 } 157 for k, v := range add { 158 clone.topLevelCodeVars[k] = v 159 } 160 return clone 161 } 162 163 // WithVars returns a config with additional string variables in its environment. 164 func (c Config) WithVars(add map[string]string) Config { 165 if len(add) == 0 { 166 return c 167 } 168 clone := c.clone() 169 if clone.vars == nil { 170 clone.vars = map[string]string{} 171 } 172 for k, v := range add { 173 clone.vars[k] = v 174 } 175 return clone 176 } 177 178 // WithTopLevelVars returns a config with additional top-level string variables in its environment. 179 func (c Config) WithTopLevelVars(add map[string]string) Config { 180 if len(add) == 0 { 181 return c 182 } 183 clone := c.clone() 184 if clone.topLevelVars == nil { 185 clone.topLevelVars = map[string]string{} 186 } 187 for k, v := range add { 188 clone.topLevelVars[k] = v 189 } 190 return clone 191 } 192 193 // WithLibPaths returns a config with additional library paths. 194 func (c Config) WithLibPaths(paths []string) Config { 195 if len(paths) == 0 { 196 return c 197 } 198 clone := c.clone() 199 clone.libPaths = append(clone.libPaths, paths...) 200 return clone 201 } 202 203 // WithImporter returns a config with the supplied importer. 204 func (c Config) WithImporter(importer jsonnet.Importer) Config { 205 clone := c.clone() 206 clone.importer = importer 207 return clone 208 } 209 210 type strFiles struct { 211 strings []string 212 files []string 213 lists []string 214 } 215 216 func getValues(name string, s strFiles) (map[string]string, error) { 217 ret := map[string]string{} 218 219 processStr := func(s string, ctx string) error { 220 parts := strings.SplitN(s, "=", 2) 221 if len(parts) == 2 { 222 ret[parts[0]] = parts[1] 223 return nil 224 } 225 v, ok := os.LookupEnv(s) 226 if !ok { 227 return fmt.Errorf("%sno value found from environment for %s", ctx, s) 228 } 229 ret[s] = v 230 return nil 231 } 232 processFile := func(s string) error { 233 parts := strings.SplitN(s, "=", 2) 234 if len(parts) == 1 { 235 return fmt.Errorf("%s-file no filename specified for %s", name, s) 236 } 237 b, err := ioutil.ReadFile(parts[1]) 238 if err != nil { 239 return err 240 } 241 ret[parts[0]] = string(b) 242 return nil 243 } 244 processList := func(l string) error { 245 b, err := ioutil.ReadFile(l) 246 if err != nil { 247 return err 248 } 249 scanner := bufio.NewScanner(bytes.NewReader(b)) 250 num := 0 251 for scanner.Scan() { 252 num++ 253 line := scanner.Text() 254 if line != "" { 255 err := processStr(line, "") 256 if err != nil { 257 return errors.Wrap(err, fmt.Sprintf("process list %s, line %d", l, num)) 258 } 259 } 260 } 261 if err := scanner.Err(); err != nil { 262 return errors.Wrap(err, fmt.Sprintf("process list %s", l)) 263 } 264 return nil 265 } 266 for _, s := range s.lists { 267 if err := processList(s); err != nil { 268 return nil, err 269 } 270 } 271 for _, s := range s.strings { 272 if err := processStr(s, name+" "); err != nil { 273 return nil, err 274 } 275 } 276 for _, s := range s.files { 277 if err := processFile(s); err != nil { 278 return nil, err 279 } 280 } 281 return ret, nil 282 } 283 284 // ConfigFromCommandParams attaches VM related flags to the specified command and returns 285 // a function that provides the config based on command line flags. 286 func ConfigFromCommandParams(cmd *cobra.Command, prefix string, addShortcuts bool) func() (Config, error) { 287 var ( 288 extStrings strFiles 289 extCodes strFiles 290 tlaStrings strFiles 291 tlaCodes strFiles 292 paths []string 293 ) 294 fs := cmd.PersistentFlags() 295 if addShortcuts { 296 fs.StringArrayVarP(&extStrings.strings, prefix+"ext-str", "V", nil, "external string: <var>=[val], if <val> is omitted, get from environment var <var>") 297 } else { 298 fs.StringArrayVar(&extStrings.strings, prefix+"ext-str", nil, "external string: <var>=[val], if <val> is omitted, get from environment var <var>") 299 } 300 fs.StringArrayVar(&extStrings.files, prefix+"ext-str-file", nil, "external string from file: <var>=<filename>") 301 fs.StringArrayVar(&extStrings.lists, prefix+"ext-str-list", nil, "file containing lines of the form <var>[=<val>]") 302 fs.StringArrayVar(&extCodes.strings, prefix+"ext-code", nil, "external code: <var>=[val], if <val> is omitted, get from environment var <var>") 303 fs.StringArrayVar(&extCodes.files, prefix+"ext-code-file", nil, "external code from file: <var>=<filename>") 304 if addShortcuts { 305 fs.StringArrayVarP(&tlaStrings.strings, prefix+"tla-str", "A", nil, "top-level string: <var>=[val], if <val> is omitted, get from environment var <var>") 306 } else { 307 fs.StringArrayVar(&tlaStrings.strings, prefix+"tla-str", nil, "top-level string: <var>=[val], if <val> is omitted, get from environment var <var>") 308 } 309 fs.StringArrayVar(&tlaStrings.files, prefix+"tla-str-file", nil, "top-level string from file: <var>=<filename>") 310 fs.StringArrayVar(&tlaStrings.lists, prefix+"tla-str-list", nil, "file containing lines of the form <var>[=<val>]") 311 fs.StringArrayVar(&tlaCodes.strings, prefix+"tla-code", nil, "top-level code: <var>=[val], if <val> is omitted, get from environment var <var>") 312 fs.StringArrayVar(&tlaCodes.files, prefix+"tla-code-file", nil, "top-level code from file: <var>=<filename>") 313 fs.StringArrayVar(&paths, prefix+"jpath", nil, "additional jsonnet library path") 314 315 return func() (c Config, err error) { 316 if c.vars, err = getValues("ext-str", extStrings); err != nil { 317 return 318 } 319 if c.codeVars, err = getValues("ext-code", extCodes); err != nil { 320 return 321 } 322 if c.topLevelVars, err = getValues("tla-str", tlaStrings); err != nil { 323 return 324 } 325 if c.topLevelCodeVars, err = getValues("tla-code", tlaCodes); err != nil { 326 return 327 } 328 c.libPaths = paths 329 return 330 } 331 } 332 333 // VM wraps a jsonnet VM and provides some additional methods to create new 334 // VMs using the same base configuration and additional tweaks. 335 type VM struct { 336 *jsonnet.VM 337 config Config 338 } 339 340 // New constructs a new VM based on the supplied config. 341 func New(config Config) *VM { 342 vm := jsonnet.MakeVM() 343 registerNativeFuncs(vm) 344 registerVars := func(m map[string]string, registrar func(k, v string)) { 345 for k, v := range m { 346 registrar(k, v) 347 } 348 } 349 registerVars(config.vars, vm.ExtVar) 350 registerVars(config.codeVars, vm.ExtCode) 351 registerVars(config.topLevelVars, vm.TLAVar) 352 registerVars(config.topLevelCodeVars, vm.TLACode) 353 if config.importer != nil { 354 vm.Importer(config.importer) 355 } else { 356 vm.Importer(&jsonnet.FileImporter{ 357 JPaths: config.libPaths, 358 }) 359 } 360 return &VM{VM: vm, config: config} 361 } 362 363 // Config returns the current VM config. 364 func (v *VM) Config() Config { 365 return v.config 366 }