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  }