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 }