github.com/grafana/tanka@v0.26.1-0.20240506093700-c22cfc35c21a/pkg/jsonnet/eval.go (about)

     1  package jsonnet
     2  
     3  import (
     4  	"os"
     5  	"regexp"
     6  	"time"
     7  
     8  	jsonnet "github.com/google/go-jsonnet"
     9  	"github.com/pkg/errors"
    10  	"github.com/rs/zerolog/log"
    11  
    12  	"github.com/grafana/tanka/pkg/jsonnet/implementations/goimpl"
    13  	"github.com/grafana/tanka/pkg/jsonnet/implementations/types"
    14  	"github.com/grafana/tanka/pkg/jsonnet/jpath"
    15  )
    16  
    17  // Modifier allows to set optional parameters on the Jsonnet VM.
    18  // See jsonnet.With* for this.
    19  type Modifier func(vm *jsonnet.VM) error
    20  
    21  // InjectedCode holds data that is "late-bound" into the VM
    22  type InjectedCode map[string]string
    23  
    24  // Set allows to set values on an InjectedCode, even when it is nil
    25  func (i *InjectedCode) Set(key, value string) {
    26  	if *i == nil {
    27  		*i = make(InjectedCode)
    28  	}
    29  
    30  	(*i)[key] = value
    31  }
    32  
    33  // Opts are additional properties for the Jsonnet VM
    34  type Opts struct {
    35  	MaxStack    int
    36  	ExtCode     InjectedCode
    37  	TLACode     InjectedCode
    38  	ImportPaths []string
    39  	EvalScript  string
    40  	CachePath   string
    41  
    42  	CachePathRegexes []*regexp.Regexp
    43  }
    44  
    45  // PathIsCached determines if a given path is matched by any of the configured cached path regexes
    46  // If no path regexes are defined, all paths are matched
    47  func (o Opts) PathIsCached(path string) bool {
    48  	for _, regex := range o.CachePathRegexes {
    49  		if regex.MatchString(path) {
    50  			return true
    51  		}
    52  	}
    53  	return len(o.CachePathRegexes) == 0
    54  }
    55  
    56  // Clone returns a deep copy of Opts
    57  func (o Opts) Clone() Opts {
    58  	extCode, tlaCode := InjectedCode{}, InjectedCode{}
    59  
    60  	for k, v := range o.ExtCode {
    61  		extCode[k] = v
    62  	}
    63  
    64  	for k, v := range o.TLACode {
    65  		tlaCode[k] = v
    66  	}
    67  
    68  	return Opts{
    69  		TLACode:     tlaCode,
    70  		ExtCode:     extCode,
    71  		ImportPaths: append([]string{}, o.ImportPaths...),
    72  		EvalScript:  o.EvalScript,
    73  
    74  		CachePath:        o.CachePath,
    75  		CachePathRegexes: o.CachePathRegexes,
    76  	}
    77  }
    78  
    79  // EvaluateFile evaluates the Jsonnet code in the given file and returns the
    80  // result in JSON form. It disregards opts.ImportPaths in favor of automatically
    81  // resolving these according to the specified file.
    82  func EvaluateFile(impl types.JsonnetImplementation, jsonnetFile string, opts Opts) (string, error) {
    83  	evalFunc := func(evaluator types.JsonnetEvaluator) (string, error) {
    84  		return evaluator.EvaluateFile(jsonnetFile)
    85  	}
    86  	data, err := os.ReadFile(jsonnetFile)
    87  	if err != nil {
    88  		return "", err
    89  	}
    90  	return evaluateSnippet(impl, evalFunc, jsonnetFile, string(data), opts)
    91  }
    92  
    93  // Evaluate renders the given jsonnet into a string
    94  // If cache options are given, a hash from the data will be computed and
    95  // the resulting string will be cached for future retrieval
    96  func Evaluate(path string, impl types.JsonnetImplementation, data string, opts Opts) (string, error) {
    97  	evalFunc := func(evaluator types.JsonnetEvaluator) (string, error) {
    98  		return evaluator.EvaluateAnonymousSnippet(data)
    99  	}
   100  	return evaluateSnippet(impl, evalFunc, path, data, opts)
   101  }
   102  
   103  type evalFunc func(evaluator types.JsonnetEvaluator) (string, error)
   104  
   105  func evaluateSnippet(jsonnetImpl types.JsonnetImplementation, evalFunc evalFunc, path, data string, opts Opts) (string, error) {
   106  	var cache *FileEvalCache
   107  	if opts.CachePath != "" && opts.PathIsCached(path) {
   108  		cache = NewFileEvalCache(opts.CachePath)
   109  	}
   110  
   111  	jpath, _, _, err := jpath.Resolve(path, false)
   112  	if err != nil {
   113  		return "", errors.Wrap(err, "resolving import paths")
   114  	}
   115  	opts.ImportPaths = jpath
   116  	evaluator := jsonnetImpl.MakeEvaluator(opts.ImportPaths, opts.ExtCode, opts.TLACode, opts.MaxStack)
   117  	// We're using the go implementation to deal with imports because we're not evaluating, we're reading the AST
   118  	importVM := goimpl.MakeRawVM(opts.ImportPaths, opts.ExtCode, opts.TLACode, opts.MaxStack)
   119  
   120  	var hash string
   121  	if cache != nil {
   122  		startTime := time.Now()
   123  		if hash, err = getSnippetHash(importVM, path, data); err != nil {
   124  			return "", err
   125  		}
   126  		cacheLog := log.Debug().Str("path", path).Str("hash", hash).Dur("duration_ms", time.Since(startTime))
   127  		if v, err := cache.Get(hash); err != nil {
   128  			return "", err
   129  		} else if v != "" {
   130  			cacheLog.Bool("cache_hit", true).Msg("computed snippet hash")
   131  			return v, nil
   132  		}
   133  		cacheLog.Bool("cache_hit", false).Msg("computed snippet hash")
   134  	}
   135  
   136  	content, err := evalFunc(evaluator)
   137  	if err != nil {
   138  		return "", err
   139  	}
   140  
   141  	if cache != nil {
   142  		return content, cache.Store(hash, content)
   143  	}
   144  
   145  	return content, nil
   146  }