Skip to content

Commit

Permalink
refactor(api): Loader interface (#459)
Browse files Browse the repository at this point in the history
Introduces an abstraction over environment loading, for clearly handling
inline and static loading
  • Loading branch information
sh0rez committed Jan 4, 2021
1 parent 3ab42ec commit 21bdb9e
Show file tree
Hide file tree
Showing 21 changed files with 426 additions and 396 deletions.
38 changes: 11 additions & 27 deletions cmd/tk/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (

"github.com/grafana/tanka/pkg/jsonnet/jpath"
"github.com/grafana/tanka/pkg/kubernetes/client"
"github.com/grafana/tanka/pkg/spec"
"github.com/grafana/tanka/pkg/spec/v1alpha1"
"github.com/grafana/tanka/pkg/tanka"
"github.com/grafana/tanka/pkg/term"
Expand Down Expand Up @@ -43,32 +42,6 @@ var kubectlContexts = cli.PredictFunc(
},
)

func setupConfiguration(baseDir string) *v1alpha1.Environment {
_, baseDir, rootDir, err := jpath.Resolve(baseDir)
if err != nil {
log.Fatalln("Resolving jpath:", err)
}

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

config, err := spec.ParseDir(baseDir, name)
if err != nil {
switch err.(type) {
// the config includes deprecated fields
case spec.ErrDeprecated:
if verbose {
fmt.Print(err)
}
// some other error
default:
log.Fatalf("Reading spec.json: %s", err)
}
}

return config
}

func envSetCmd() *cli.Command {
cmd := &cli.Command{
Use: "set",
Expand Down Expand Up @@ -303,3 +276,14 @@ func envListCmd() *cli.Command {
}
return cmd
}

func setupConfiguration(baseDir string) *v1alpha1.Environment {
env, err := tanka.Load(baseDir, tanka.Opts{
JsonnetOpts: tanka.JsonnetOpts{EvalScript: tanka.EnvsOnlyEvalScript},
})
if err != nil {
log.Fatalln(err)
}

return env.Env
}
5 changes: 4 additions & 1 deletion cmd/tk/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ func exportCmd() *cli.Command {

var paths []string
for _, path := range args[1:] {
// find possible environments
if *recursive {
rootDir, err := jpath.FindRoot(path)
if err != nil {
Expand All @@ -73,15 +74,17 @@ func exportCmd() *cli.Command {
continue
}

// validate environment
jsonnetOpts := opts.ParseParallelOpts.JsonnetOpts
jsonnetOpts.EvalScript = tanka.EnvsOnlyEvalScript
_, _, err := tanka.ParseEnv(path, jsonnetOpts)
_, err := tanka.Load(path, tanka.Opts{JsonnetOpts: jsonnetOpts})
if err != nil {
return err
}
paths = append(paths, path)
}

// export them
return tanka.ExportEnvironments(paths, args[0], &opts)
}
return cmd
Expand Down
1 change: 1 addition & 0 deletions pkg/jsonnet/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ func EvaluateFile(jsonnetFile string, opts Opts) (string, error) {
}

// Evaluate renders the given jsonnet into a string
// TODO: don't resolve jpath, this is ANONYMOUS AFTER ALL
func Evaluate(path, data string, opts Opts) (string, error) {
jpath, _, _, err := jpath.Resolve(path)
if err != nil {
Expand Down
15 changes: 15 additions & 0 deletions pkg/jsonnet/jpath/dirs.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ import (
"runtime"
)

// Dirs returns the project-root (root) and environment directory (base)
func Dirs(path string) (root string, base string, err error) {
root, err = FindRoot(path)
if err != nil {
return "", "", err
}

base, err = FindBase(path, root)
if err != nil {
return root, "", err
}

return root, base, err
}

// FindRoot returns the absolute path of the project root, being the directory
// that directly holds `tkrc.yaml` if it exists, otherwise the directory that
// directly holds `jsonnetfile.json`
Expand Down
8 changes: 7 additions & 1 deletion pkg/process/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ const (
// - tanka.dev/** labels
// - filtering
// - best-effort sorting
func Process(raw interface{}, cfg v1alpha1.Environment, exprs Matchers) (manifest.List, error) {
func Process(cfg v1alpha1.Environment, exprs Matchers) (manifest.List, error) {
raw := cfg.Data

if raw == nil {
return manifest.List{}, nil
}

// Scan for everything that looks like a Kubernetes object
extracted, err := Extract(raw)
if err != nil {
Expand Down
18 changes: 11 additions & 7 deletions pkg/process/process_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,18 +109,19 @@ func TestProcess(t *testing.T) {

for _, c := range tests {
t.Run(c.name, func(t *testing.T) {
config := v1alpha1.New()
config.Metadata.Name = "testdata"
config.Spec = c.spec
env := v1alpha1.New()
env.Metadata.Name = "testdata"
env.Spec = c.spec
env.Data = c.deep.(map[string]interface{})

if config.Spec.InjectLabels {
if env.Spec.InjectLabels {
for i, m := range c.flat {
m.Metadata().Labels()[LabelEnvironment] = config.Metadata.NameLabel()
m.Metadata().Labels()[LabelEnvironment] = env.Metadata.NameLabel()
c.flat[i] = m
}
}

got, err := Process(c.deep.(map[string]interface{}), *config, c.targets)
got, err := Process(*env, c.targets)
require.Equal(t, c.err, err)

Sort(c.flat)
Expand All @@ -142,7 +143,10 @@ func mapToList(ms map[string]manifest.Manifest) manifest.List {
func TestProcessOrder(t *testing.T) {
got := make([]manifest.List, 10)
for i := 0; i < 10; i++ {
r, err := Process(testDataDeep().Deep.(map[string]interface{}), *v1alpha1.New(), nil)
env := v1alpha1.New()
env.Data = testDataDeep().Deep.(map[string]interface{})

r, err := Process(*env, nil)
require.NoError(t, err)
got[i] = r
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/spec/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func ParseDir(baseDir, path string) (*v1alpha1.Environment, error) {
}

// Parse parses the json `data` into a `v1alpha1.Environment` object.
func Parse(data []byte, path string) (*v1alpha1.Environment, error) {
func Parse(data []byte, namespace string) (*v1alpha1.Environment, error) {
config := v1alpha1.New()
if err := json.Unmarshal(data, config); err != nil {
return nil, errors.Wrap(err, "parsing spec.json")
Expand All @@ -65,7 +65,7 @@ func Parse(data []byte, path string) (*v1alpha1.Environment, error) {
config.Spec.APIServer = "https://" + config.Spec.APIServer
}

config.Metadata.Namespace = path
config.Metadata.Namespace = namespace

return config, nil
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/tanka/environments.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,12 @@ type parseJob struct {

func parseWorker(envsChan <-chan parseJob) (errs []error) {
for req := range envsChan {
_, env, err := ParseEnv(req.path, req.opts)
loaded, err := Load(req.path, Opts{JsonnetOpts: req.opts})
if err != nil {
errs = append(errs, fmt.Errorf("%s:\n %w", req.path, err))
continue
}
*req.env = *env
*req.env = *loaded.Env
}
if len(errs) != 0 {
return errs
Expand Down
13 changes: 7 additions & 6 deletions pkg/tanka/evaluators.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@ func EvalJsonnet(path string, opts jsonnet.Opts) (raw string, err error) {
// evaluate Jsonnet
if opts.EvalScript != "" {
evalScript := fmt.Sprintf(opts.EvalScript, entrypoint)
raw, err = jsonnet.Evaluate(entrypoint, evalScript, opts)
if err != nil {
return "", errors.Wrap(err, "evaluating jsonnet")
}
} else {
raw, err = jsonnet.EvaluateFile(entrypoint, opts)
raw, err = jsonnet.Evaluate(path, evalScript, opts)
if err != nil {
return "", errors.Wrap(err, "evaluating jsonnet")
}
return raw, nil
}

raw, err = jsonnet.EvaluateFile(entrypoint, opts)
if err != nil {
return "", errors.Wrap(err, "evaluating jsonnet")
}
return raw, nil
}
Expand Down
15 changes: 7 additions & 8 deletions pkg/tanka/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,25 +54,24 @@ func ExportEnvironments(paths []string, to string, opts *ExportEnvOpts) error {
return fmt.Errorf("Output dir `%s` not empty. Pass --merge to ignore this", to)
}

// get all environments for paths
envs, err := ParseParallel(paths, opts.ParseParallelOpts)
if err != nil {
return err
}

for _, env := range envs {
for _, path := range paths {
// select targets to export
filter, err := process.StrExps(opts.Targets...)
if err != nil {
return err
}

// get the manifests
res, err := LoadManifests(env, filter)
loaded, err := Load(path, Opts{
Filters: filter,
})
if err != nil {
return err
}

env := loaded.Env
res := loaded.Resources

// create raw manifest version of env for templating
env.Data = nil
raw, err := json.Marshal(env)
Expand Down
92 changes: 92 additions & 0 deletions pkg/tanka/inline.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package tanka

import (
"encoding/json"
"fmt"
"path/filepath"

"github.com/grafana/tanka/pkg/jsonnet/jpath"
"github.com/grafana/tanka/pkg/kubernetes/manifest"
"github.com/grafana/tanka/pkg/process"
"github.com/grafana/tanka/pkg/spec"
"github.com/grafana/tanka/pkg/spec/v1alpha1"
)

// InlineLoader loads an environment that is specified inline from within
// Jsonnet. The Jsonnet output is expected to hold a tanka.dev/Environment type,
// Kubernetes resources are expected at the `data` key of this very type
type InlineLoader struct{}

func (i *InlineLoader) Load(path string, opts JsonnetOpts) (*v1alpha1.Environment, error) {
raw, err := EvalJsonnet(path, opts)
if err != nil {
return nil, err
}

var data interface{}
if err := json.Unmarshal([]byte(raw), &data); err != nil {
return nil, err
}

envs, err := extractEnvs(data)
if err != nil {
return nil, err
}

if len(envs) > 1 {
names := make([]string, 0, len(envs))
for _, e := range envs {
names = append(names, e.Metadata().Name())
}
return nil, ErrMultipleEnvs{path, names}
}

if len(envs) == 0 {
return nil, fmt.Errorf("Found no environments in '%s'", path)
}

root, base, err := jpath.Dirs(path)
if err != nil {
return nil, err
}

name, err := filepath.Rel(root, base)
if err != nil {
return nil, err
}

// TODO: Re-serializing the entire env here. This is horribly inefficient
envData, err := json.Marshal(envs[0])
if err != nil {
return nil, err
}

env, err := spec.Parse(envData, name)
if err != nil {
return nil, err
}

return env, nil
}

// extractEnvs filters out any Environment manifests
func extractEnvs(data interface{}) (manifest.List, error) {
// Scan for everything that looks like a Kubernetes object
extracted, err := process.Extract(data)
if err != nil {
return nil, err
}

// Unwrap *List types
if err := process.Unwrap(extracted); err != nil {
return nil, err
}

out := make(manifest.List, 0, len(extracted))
for _, m := range extracted {
out = append(out, m)
}

// Extract only object of Kind: Environment
return process.Filter(out, process.MustStrExps("Environment/.*")), nil
}
Loading

0 comments on commit 21bdb9e

Please sign in to comment.