Skip to content

Commit

Permalink
feat(jsonnet): allow other entrypoints (#389)
Browse files Browse the repository at this point in the history
So far, Tanka always evaluates `main.jsonnet` whatever directory it receives from the user. Going on, this is still supported and the default behavior.

When passed a plain file instead of a directory instead however, Tanka evaluates that file now, choosing the enclosing directory as the environments directory. 

This makes `tk show` and `tk eval` more lightweight, requiring only a single file, not a whole directory.
  • Loading branch information
Duologic authored Oct 24, 2020
1 parent 7aad7b9 commit 59a5f5f
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 38 deletions.
7 changes: 5 additions & 2 deletions cmd/tk/tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,11 @@ func jpathCmd() *cli.Command {
if err != nil {
return fmt.Errorf("Resolving JPATH: %s", err)
}

fmt.Println("main:", filepath.Join(base, "main.jsonnet"))
entrypoint, err := jpath.Entrypoint(base)
if err != nil {
return fmt.Errorf("Resolving JPATH: %s", err)
}
fmt.Println("main:", entrypoint)
fmt.Println("rootDir:", root)
fmt.Println("baseDir:", base)
fmt.Println("jpath:", path)
Expand Down
10 changes: 4 additions & 6 deletions pkg/jsonnet/eval.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package jsonnet

import (
"path/filepath"

jsonnet "github.com/google/go-jsonnet"
"github.com/pkg/errors"

Expand Down Expand Up @@ -60,7 +58,7 @@ func MakeVM(opts Opts) *jsonnet.VM {
// result in JSON form. It disregards opts.ImportPaths in favor of automatically
// resolving these according to the specified file.
func EvaluateFile(jsonnetFile string, opts Opts) (string, error) {
jpath, _, _, err := jpath.Resolve(filepath.Dir(jsonnetFile))
jpath, _, _, err := jpath.Resolve(jsonnetFile)
if err != nil {
return "", errors.Wrap(err, "resolving import paths")
}
Expand All @@ -71,12 +69,12 @@ func EvaluateFile(jsonnetFile string, opts Opts) (string, error) {
}

// Evaluate renders the given jsonnet into a string
func Evaluate(filename, data string, opts Opts) (string, error) {
jpath, _, _, err := jpath.Resolve(filepath.Dir(filename))
func Evaluate(path, data string, opts Opts) (string, error) {
jpath, _, _, err := jpath.Resolve(path)
if err != nil {
return "", errors.Wrap(err, "resolving import paths")
}
opts.ImportPaths = jpath
vm := MakeVM(opts)
return vm.EvaluateAnonymousSnippet(filename, data)
return vm.EvaluateAnonymousSnippet(path, data)
}
13 changes: 8 additions & 5 deletions pkg/jsonnet/imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ func TransitiveImports(dir string) ([]string, error) {
return nil, err
}

mainFile := filepath.Join(dir, "main.jsonnet")
entrypoint, err := jpath.Entrypoint(dir)
if err != nil {
return nil, err
}

sonnet, err := ioutil.ReadFile(mainFile)
sonnet, err := ioutil.ReadFile(entrypoint)
if err != nil {
return nil, errors.Wrap(err, "opening file")
}
Expand All @@ -44,13 +47,13 @@ func TransitiveImports(dir string) ([]string, error) {
vm.NativeFunction(nf)
}

node, err := jsonnet.SnippetToAST("main.jsonnet", string(sonnet))
node, err := jsonnet.SnippetToAST(filepath.Base(entrypoint), string(sonnet))
if err != nil {
return nil, errors.Wrap(err, "creating Jsonnet AST")
}

imports := make(map[string]bool)
if err = importRecursive(imports, vm, node, "main.jsonnet"); err != nil {
if err = importRecursive(imports, vm, node, filepath.Base(entrypoint)); err != nil {
return nil, err
}

Expand All @@ -65,7 +68,7 @@ func TransitiveImports(dir string) ([]string, error) {
paths = append(paths, p)

}
paths = append(paths, mainFile)
paths = append(paths, entrypoint)

for i := range paths {
paths[i], _ = filepath.Rel(rootDir, paths[i])
Expand Down
35 changes: 28 additions & 7 deletions pkg/jsonnet/jpath/jpath.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import (
"runtime"
)

const DEFAULT_ENTRYPOINT = "main.jsonnet"

var (
// ErrorNoRoot means no rootDir was found in the parents
ErrorNoRoot = errors.New("could not locate a tkrc.yaml or jsonnetfile.json in the parent directories, which is required to identify the project root.\nRefer to https://tanka.dev/directory-structure for more information")

// ErrorNoBase means no baseDir was found in the parents
ErrorNoBase = errors.New("could not locate a main.jsonnet in the parent directories, which is required as the entrypoint for the evaluation.\nRefer to https://tanka.dev/directory-structure for more information")
ErrorNoBase = errors.New("could not locate entrypoint (usually main.jsonnet) in the parent directories, which is required as the entrypoint for the evaluation.\nRefer to https://tanka.dev/directory-structure for more information")
)

// ErrorFileNotFound means that the searched file was not found
Expand All @@ -25,25 +27,25 @@ func (e ErrorFileNotFound) Error() string {
return e.filename + " not found"
}

// Resolve the given directory and resolves the jPath around it. This means it:
// Resolve the given path and resolves the jPath around it. This means it:
// - figures out the project root (the one with .jsonnetfile, vendor/ and lib/)
// - figures out the environments base directory (the one with the main.jsonnet)
// - figures out the environments base directory (usually the main.jsonnet)
//
// It then constructs a jPath with the base directory, vendor/ and lib/.
// This results in predictable imports, as it doesn't matter whether the user called
// called the command further down tree or not. A little bit like git.
func Resolve(workdir string) (path []string, base, root string, err error) {
workdir, err = filepath.Abs(workdir)
func Resolve(path string) (jpath []string, base, root string, err error) {
entrypoint, err := Entrypoint(path)
if err != nil {
return nil, "", "", err
}

root, err = FindRoot(workdir)
root, err = FindRoot(filepath.Dir(entrypoint))
if err != nil {
return nil, "", "", err
}

base, err = FindParentFile("main.jsonnet", workdir, root)
base, err = FindParentFile(filepath.Base(entrypoint), filepath.Dir(entrypoint), root)
if err != nil {
if _, ok := err.(ErrorFileNotFound); ok {
return nil, "", "", ErrorNoBase
Expand Down Expand Up @@ -113,3 +115,22 @@ func dirContainsFile(files []os.FileInfo, filename string) bool {
}
return false
}

func Entrypoint(path string) (string, error) {
filename := DEFAULT_ENTRYPOINT

entrypoint, err := filepath.Abs(path)
if err != nil {
return "", err
}

stat, err := os.Stat(entrypoint)
if err != nil {
return "", err
}
if !stat.IsDir() {
return entrypoint, nil
}

return filepath.Join(entrypoint, filename), nil
}
39 changes: 21 additions & 18 deletions pkg/tanka/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ func (p *loaded) connect() (*kubernetes.Kubernetes, error) {
}

// load runs all processing stages described at the Processed type
func load(dir string, opts Opts) (*loaded, error) {
raw, env, err := eval(dir, opts.JsonnetOpts)
func load(path string, opts Opts) (*loaded, error) {
raw, env, err := eval(path, opts.JsonnetOpts)
if err != nil {
return nil, err
}
Expand All @@ -86,18 +86,13 @@ func load(dir string, opts Opts) (*loaded, error) {

// eval runs all processing stages describe at the Processed type apart from
// post-processing, thus returning the raw Jsonnet result.
func eval(dir string, opts jsonnet.Opts) (raw interface{}, env *v1alpha1.Config, err error) {
_, baseDir, rootDir, err := jpath.Resolve(dir)
if err != nil {
return nil, nil, errors.Wrap(err, "resolving jpath")
}

env, err = parseSpec(baseDir, rootDir)
func eval(path string, opts jsonnet.Opts) (raw interface{}, env *v1alpha1.Config, err error) {
env, err = parseSpec(path)
if err != nil {
return nil, nil, err
}

raw, err = evalJsonnet(baseDir, env, opts)
raw, err = evalJsonnet(path, env, opts)
if err != nil {
return nil, nil, errors.Wrap(err, "evaluating jsonnet")
}
Expand All @@ -107,7 +102,12 @@ func eval(dir string, opts jsonnet.Opts) (raw interface{}, env *v1alpha1.Config,

// parseEnv parses the `spec.json` of the environment and returns a
// *kubernetes.Kubernetes from it
func parseSpec(baseDir, rootDir string) (*v1alpha1.Config, error) {
func parseSpec(path string) (*v1alpha1.Config, error) {
_, baseDir, rootDir, err := jpath.Resolve(path)
if err != nil {
return nil, errors.Wrap(err, "resolving jpath")
}

// name of the environment: relative path from rootDir
name, _ := filepath.Rel(rootDir, baseDir)

Expand All @@ -129,9 +129,8 @@ func parseSpec(baseDir, rootDir string) (*v1alpha1.Config, error) {
return config, nil
}

// evalJsonnet evaluates the jsonnet environment at the given directory starting with
// `main.jsonnet`
func evalJsonnet(baseDir string, env *v1alpha1.Config, opts jsonnet.Opts) (interface{}, error) {
// evalJsonnet evaluates the jsonnet environment at the given path
func evalJsonnet(path string, env *v1alpha1.Config, opts jsonnet.Opts) (interface{}, error) {
// make env spec accessible from Jsonnet
jsonEnv, err := json.Marshal(env)
if err != nil {
Expand All @@ -141,15 +140,19 @@ func evalJsonnet(baseDir string, env *v1alpha1.Config, opts jsonnet.Opts) (inter

// evaluate Jsonnet
var raw string
mainFile := filepath.Join(baseDir, "main.jsonnet")
entrypoint, err := jpath.Entrypoint(path)
if err != nil {
return nil, err
}

if opts.EvalPattern != "" {
evalScript := fmt.Sprintf("(import '%s').%s", mainFile, opts.EvalPattern)
raw, err = jsonnet.Evaluate(mainFile, evalScript, opts)
evalScript := fmt.Sprintf("(import '%s').%s", entrypoint, opts.EvalPattern)
raw, err = jsonnet.Evaluate(entrypoint, evalScript, opts)
if err != nil {
return nil, err
}
} else {
raw, err = jsonnet.EvaluateFile(mainFile, opts)
raw, err = jsonnet.EvaluateFile(entrypoint, opts)
if err != nil {
return nil, err
}
Expand Down

0 comments on commit 59a5f5f

Please sign in to comment.