Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: inline Environment #403

Merged
merged 16 commits into from
Nov 26, 2020
Merged
Prev Previous commit
Next Next commit
fix(cli): tk env list for inline envs
  • Loading branch information
Duologic committed Nov 6, 2020
commit 2ee1696d3b875a5785a16ae419598329d62c4f40
13 changes: 13 additions & 0 deletions cmd/tk/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/go-clix/cli"

"github.com/grafana/tanka/pkg/jsonnet"
"github.com/grafana/tanka/pkg/jsonnet/jpath"
"github.com/grafana/tanka/pkg/spec"
"github.com/grafana/tanka/pkg/spec/v1alpha1"
Expand Down Expand Up @@ -77,6 +78,18 @@ func setupConfiguration(baseDir string) *v1alpha1.Config {
if verbose {
fmt.Print(err)
}
// no spec.json is found, try parsing main.jsonnet
case spec.ErrNoSpec:
config, err := tanka.EvalEnvs(baseDir, jsonnet.Opts{})
if err != nil {
switch err.(type) {
case tanka.ErrNoEnv:
return nil
default:
log.Fatalf("Reading main.jsonnet: %s", err)
}
}
return config
// some other error
default:
log.Fatalf("Reading spec.json: %s", err)
Expand Down
2 changes: 1 addition & 1 deletion cmd/tk/other.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func findBaseDirs() (dirs []string) {
}

if err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
requiredFiles := []string{"main.jsonnet", "spec.json"}
requiredFiles := []string{"main.jsonnet"}
for _, name := range requiredFiles {
if _, err := os.Stat(filepath.Join(path, name)); err != nil {
// missing file, not a valid environment directory
Expand Down
22 changes: 22 additions & 0 deletions pkg/tanka/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package tanka

import "fmt"

// ErrNoEnv means that the given jsonnet has no Environment object
// This must not be fatal, some operations work without
type ErrNoEnv struct {
path string
}

func (e ErrNoEnv) Error() string {
return fmt.Sprintf("unable to find an Environment in '%s'", e.path)
}

// ErrMultipleEnvs means that the given jsonnet has multiple Environment objects
type ErrMultipleEnvs struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this error message include the names of the environments? Makes it far easier to debug then

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

path string
}

func (e ErrMultipleEnvs) Error() string {
return fmt.Sprintf("found multiple Environments in '%s'", e.path)
}
129 changes: 98 additions & 31 deletions pkg/tanka/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,36 @@ func load(path string, opts Opts) (*loaded, error) {
}, nil
}

// eval evaluates the jsonnet environment at the given path
func eval(path string, opts jsonnet.Opts) (interface{}, *v1alpha1.Config, error) {
return parseEnv(
path,
opts,
func(path string, opts jsonnet.Opts) (string, error) {
entrypoint, err := jpath.Entrypoint(path)
if err != nil {
return "", err
}

// evaluate Jsonnet
var raw string
if opts.EvalPattern != "" {
evalScript := fmt.Sprintf("(import '%s').%s", entrypoint, opts.EvalPattern)
raw, err = jsonnet.Evaluate(entrypoint, evalScript, opts)
if err != nil {
return "", errors.Wrap(err, "evaluating jsonnet")
}
} else {
raw, err = jsonnet.EvaluateFile(entrypoint, opts)
if err != nil {
return "", errors.Wrap(err, "evaluating jsonnet")
}
}
return raw, nil
},
)
}

// parseSpec parses the `spec.json` of the environment and returns a
// *kubernetes.Kubernetes from it
func parseSpec(path string) (*v1alpha1.Config, error) {
Expand Down Expand Up @@ -118,20 +148,19 @@ func parseSpec(path string) (*v1alpha1.Config, error) {
return config, nil
}

// eval evaluates the jsonnet environment at the given path
func eval(path string, opts jsonnet.Opts) (interface{}, *v1alpha1.Config, error) {
var hasSpec bool
type evaluateFunc func(path string, opts jsonnet.Opts) (string, error)

// parseEnv finds the Environment object at the given path
Duologic marked this conversation as resolved.
Show resolved Hide resolved
func parseEnv(path string, opts jsonnet.Opts, evalFn evaluateFunc) (interface{}, *v1alpha1.Config, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Injecting big amounts of logic as a anonymous function is very hard to debug (behavior no longer obvious from function body). I will work with @Duologic to find a better way here. I wish to hold this PR until that happened, if that's okay (@malcolmholmes)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy with the functionality of this PR as it is. I agree with the code being a bit more complex than it needs to be, but am also happy to see that addressed in a subsequent PR.

specEnv, err := parseSpec(path)
if err != nil {
switch err.(type) {
case spec.ErrNoSpec:
hasSpec = false
specEnv = nil
default:
return nil, nil, errors.Wrap(err, "reading spec.json")
}
} else {
hasSpec = true

// original behavior, if env has spec.json
// then make env spec accessible through extCode
jsonEnv, err := json.Marshal(specEnv)
Expand All @@ -141,26 +170,11 @@ func eval(path string, opts jsonnet.Opts) (interface{}, *v1alpha1.Config, error)
opts.ExtCode.Set(spec.APIGroup+"/environment", string(jsonEnv))
}

entrypoint, err := jpath.Entrypoint(path)
raw, err := evalFn(path, opts)
if err != nil {
return nil, nil, err
}

// evaluate Jsonnet
var raw string
if opts.EvalPattern != "" {
evalScript := fmt.Sprintf("(import '%s').%s", entrypoint, opts.EvalPattern)
raw, err = jsonnet.Evaluate(entrypoint, evalScript, opts)
if err != nil {
return nil, nil, errors.Wrap(err, "evaluating jsonnet")
}
} else {
raw, err = jsonnet.EvaluateFile(entrypoint, opts)
if err != nil {
return nil, nil, errors.Wrap(err, "evaluating jsonnet")
}
}

var data interface{}
if err := json.Unmarshal([]byte(raw), &data); err != nil {
return nil, nil, errors.Wrap(err, "unmarshalling data")
Expand All @@ -173,7 +187,7 @@ func eval(path string, opts jsonnet.Opts) (interface{}, *v1alpha1.Config, error)

extract, err := extractEnvironments(data)
Duologic marked this conversation as resolved.
Show resolved Hide resolved
if _, ok := err.(process.ErrorPrimitiveReached); ok {
if !hasSpec {
if specEnv == nil {
// if no environments or spec found, behave as jsonnet interpreter
return data, nil, err
}
Expand All @@ -184,25 +198,24 @@ func eval(path string, opts jsonnet.Opts) (interface{}, *v1alpha1.Config, error)
var env *v1alpha1.Config

if len(extract) > 1 {
return nil, nil, fmt.Errorf("more than 1 environments found")
return data, nil, ErrMultipleEnvs{path}
} else if len(extract) == 1 {
data, err := json.Marshal(extract[0])
marshalled, err := json.Marshal(extract[0])
if err != nil {
return nil, nil, err
}
env, err = spec.Parse(data)
env, err = spec.Parse(marshalled)
if err != nil {
return nil, nil, err
}
} else if hasSpec {
return data, env, nil
} else if specEnv != nil {
// if no environments found, fallback to original behavior
specEnv.Data = data
return data, specEnv, nil
} else {
// if no environments or spec found, behave as jsonnet interpreter
return data, nil, fmt.Errorf("no environments found")
}
return data, env, nil
// if no environments or spec found, behave as jsonnet interpreter
return data, nil, ErrNoEnv{path}
}

func checkVersion(constraint string) error {
Expand Down Expand Up @@ -251,3 +264,57 @@ func extractEnvironments(data interface{}) (manifest.List, error) {
// Extract only object of Kind: Environment
return process.Filter(out, process.MustStrExps("Environment/.*")), nil
}

// EvalEnvs finds the Environment object (without its .data object) at the given path
// intended for use by the `tk env` command
func EvalEnvs(path string, opts jsonnet.Opts) (*v1alpha1.Config, error) {
_, env, err := parseEnv(
path,
opts,
func(path string, opts jsonnet.Opts) (string, error) {
entrypoint, err := jpath.Entrypoint(path)
if err != nil {
return "", err
}

// Snippet to find all Environment objects and remove the .data object for faster evaluation
noData := `
local noDataEnv(object) =
if std.isObject(object)
then
if std.objectHas(object, 'apiVersion')
&& std.objectHas(object, 'kind')
then
if object.kind == 'Environment'
then object { data:: {} }
else {}
else
std.mapWithKey(
function(key, obj)
noDataEnv(obj),
object
)
else if std.isArray(object)
then
std.map(
function(obj)
noDataEnv(obj),
object
)
else {};

noDataEnv(import '%s')
`

// evaluate Jsonnet with noData snippet
var raw string
evalScript := fmt.Sprintf(noData, entrypoint)
raw, err = jsonnet.Evaluate(entrypoint, evalScript, opts)
if err != nil {
return "", errors.Wrap(err, "evaluating jsonnet")
}
return raw, nil
},
)
return env, err
}