Skip to content

Commit 5700ef5

Browse files
committed
feat(jsonnet): add support for importing all files in a directory
Add native function `importFiles` which reads and evaluates all jsonnet files in a directory. Returns an object of with key/value pairs of `<file name>: <evaluated object>` Directory path is found using a `calledFrom` option, similar to `helmTemplate`. Includes support for: - Choosing files by extension using the `extension` option (default is `.libsonnet`) - Excluding files using the `exclude` option, which takes an array of file names to skip
1 parent 9f92ae2 commit 5700ef5

File tree

2 files changed

+88
-0
lines changed

2 files changed

+88
-0
lines changed

pkg/jsonnet/implementations/goimpl/vm.go

+4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ func MakeRawVM(importPaths []string, extCode map[string]string, tlaCode map[stri
2525
vm.NativeFunction(nf)
2626
}
2727

28+
for _, nvf := range native.VMFuncs(vm) {
29+
vm.NativeFunction(nvf)
30+
}
31+
2832
if maxStack > 0 {
2933
vm.MaxStack = maxStack
3034
}

pkg/jsonnet/native/funcs.go

+84
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@ import (
66
"encoding/json"
77
"fmt"
88
"io"
9+
"os"
10+
"path/filepath"
911
"regexp"
12+
"slices"
1013
"strings"
1114

1215
jsonnet "github.com/google/go-jsonnet"
1316
"github.com/google/go-jsonnet/ast"
1417
"github.com/grafana/tanka/pkg/helm"
1518
"github.com/grafana/tanka/pkg/kustomize"
1619
"github.com/pkg/errors"
20+
"github.com/rs/zerolog/log"
1721
yaml "gopkg.in/yaml.v3"
1822
)
1923

@@ -42,6 +46,14 @@ func Funcs() []*jsonnet.NativeFunction {
4246
}
4347
}
4448

49+
// VMFuncs returns a slice of functions similar to Funcs but are passed the jsonnet VM
50+
// for in-line evaluation
51+
func VMFuncs(vm *jsonnet.VM) []*jsonnet.NativeFunction {
52+
return []*jsonnet.NativeFunction{
53+
importFiles(vm),
54+
}
55+
}
56+
4557
// parseJSON wraps `json.Unmarshal` to convert a json string into a dict
4658
func parseJSON() *jsonnet.NativeFunction {
4759
return &jsonnet.NativeFunction{
@@ -178,3 +190,75 @@ func regexSubst() *jsonnet.NativeFunction {
178190
},
179191
}
180192
}
193+
194+
type importFilesOpts struct {
195+
CalledFrom string `json:"calledFrom"`
196+
Exclude []string `json:"exclude"`
197+
Extension string `json:"extension"`
198+
}
199+
200+
func parseImportOpts(data interface{}) (*importFilesOpts, error) {
201+
c, err := json.Marshal(data)
202+
if err != nil {
203+
return nil, err
204+
}
205+
206+
// default extension to `.libsonnet`
207+
opts := importFilesOpts{
208+
Extension: ".libsonnet",
209+
}
210+
if err := json.Unmarshal(c, &opts); err != nil {
211+
return nil, err
212+
}
213+
if opts.CalledFrom == "" {
214+
return nil, fmt.Errorf("importFiles: `opts.calledFrom` is unset or empty\nTanka needs this to find your directory.")
215+
}
216+
return &opts, nil
217+
}
218+
219+
// importFiles imports and evaluates all matching jsonnet files in the given relative directory
220+
func importFiles(vm *jsonnet.VM) *jsonnet.NativeFunction {
221+
return &jsonnet.NativeFunction{
222+
Name: "importFiles",
223+
Params: ast.Identifiers{"directory", "opts"},
224+
Func: func(data []interface{}) (interface{}, error) {
225+
dir, ok := data[0].(string)
226+
if !ok {
227+
return nil, fmt.Errorf("first argument 'directory' must be of 'string' type, got '%T' instead", data[0])
228+
}
229+
opts, err := parseImportOpts(data[1])
230+
if err != nil {
231+
return nil, err
232+
}
233+
dirPath := filepath.Join(filepath.Dir(opts.CalledFrom), dir)
234+
imports := make(map[string]interface{})
235+
err = filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
236+
if err != nil {
237+
return err
238+
}
239+
if info.IsDir() || !strings.HasSuffix(info.Name(), opts.Extension) {
240+
return nil
241+
}
242+
if slices.Contains(opts.Exclude, info.Name()) {
243+
return nil
244+
}
245+
log.Debug().Msgf("importFiles: parsing file %s", info.Name())
246+
resultStr, err := vm.EvaluateFile(path)
247+
if err != nil {
248+
return fmt.Errorf("importFiles: failed to evaluate %s: %s", path, err)
249+
}
250+
var result interface{}
251+
err = json.Unmarshal([]byte(resultStr), &result)
252+
if err != nil {
253+
return err
254+
}
255+
imports[info.Name()] = result
256+
return nil
257+
})
258+
if err != nil {
259+
return nil, err
260+
}
261+
return imports, nil
262+
},
263+
}
264+
}

0 commit comments

Comments
 (0)