diff --git a/pkg/spec/v1alpha1/config.go b/pkg/spec/v1alpha1/config.go index 77ce0296b..db8c90c06 100644 --- a/pkg/spec/v1alpha1/config.go +++ b/pkg/spec/v1alpha1/config.go @@ -21,10 +21,11 @@ func New() *Config { // Config holds the configuration variables for config version v1alpha1 // ApiVersion and Kind are currently unused, this may change in the future. type Config struct { - APIVersion string `json:"apiVersion"` - Kind string `json:"kind"` - Metadata Metadata `json:"metadata"` - Spec Spec `json:"spec"` + APIVersion string `json:"apiVersion"` + Kind string `json:"kind"` + Metadata Metadata `json:"metadata"` + Spec Spec `json:"spec"` + Data interface{} `json:"data"` } // Metadata is meant for humans and not parsed diff --git a/pkg/tanka/parse.go b/pkg/tanka/parse.go index f408d9ab3..09cd31bdf 100644 --- a/pkg/tanka/parse.go +++ b/pkg/tanka/parse.go @@ -64,16 +64,20 @@ func (p *loaded) connect() (*kubernetes.Kubernetes, error) { // load runs all processing stages described at the Processed type func load(path string, opts Opts) (*loaded, error) { - raw, env, err := eval(path, opts.JsonnetOpts) + _, env, err := eval(path, opts.JsonnetOpts) if err != nil { return nil, err } + if env == nil { + return nil, fmt.Errorf("no Tanka environment found") + } + if err := checkVersion(env.Spec.ExpectVersions.Tanka); err != nil { return nil, err } - rec, err := process.Process(raw, *env, opts.Filters) + rec, err := process.Process(env.Data, *env, opts.Filters) if err != nil { return nil, err } @@ -87,12 +91,7 @@ func load(path 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(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(path, env, opts) + raw, env, err = evalJsonnet(path, opts) if err != nil { return nil, nil, errors.Wrap(err, "evaluating jsonnet") } @@ -119,7 +118,7 @@ func parseSpec(path string) (*v1alpha1.Config, error) { log.Println(err) // spec.json missing. we can still work with the default value case spec.ErrNoSpec: - return config, nil + return config, err // some other error default: return nil, errors.Wrap(err, "reading spec.json") @@ -130,39 +129,83 @@ func parseSpec(path string) (*v1alpha1.Config, 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) +func evalJsonnet(path string, opts jsonnet.Opts) (interface{}, *v1alpha1.Config, error) { + var hasSpec bool + specEnv, err := parseSpec(path) if err != nil { - return nil, errors.Wrap(err, "marshalling environment config") + switch err.(type) { + case spec.ErrNoSpec: + hasSpec = false + 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) + if err != nil { + return nil, nil, errors.Wrap(err, "marshalling environment config") + } + opts.ExtCode.Set(spec.APIGroup+"/environment", string(jsonEnv)) } - opts.ExtCode.Set(spec.APIGroup+"/environment", string(jsonEnv)) - // evaluate Jsonnet - var raw string entrypoint, err := jpath.Entrypoint(path) if err != nil { - return nil, err + 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, err + return nil, nil, err } } else { raw, err = jsonnet.EvaluateFile(entrypoint, opts) if err != nil { - return nil, err + return nil, nil, err } } - // parse result + var data interface{} if err := json.Unmarshal([]byte(raw), &data); err != nil { - return nil, err + return nil, nil, err + } + + if opts.EvalPattern != "" { + // EvalPattern has no affinity with an environment, behave as jsonnet interpreter + return data, nil, err + } + + var env *v1alpha1.Config + switch data.(type) { + case []interface{}: + env = &v1alpha1.Config{} + // data is array, do not try to unmarshal, + // multiple envs currently unsupported + default: + if err := json.Unmarshal([]byte(raw), &env); err != nil { + return nil, nil, err + } + } + + // env is not a v1alpha1.Config, fallback to original behavior + if env.Kind != "Environment" { + if hasSpec { + specEnv.Data = data + // return env from spec.json + return data, specEnv, nil + } else { + // No spec.json found, behave as jsonnet interpreter + return data, nil, nil + } } - return data, nil + // return env AS IS + return *env, env, nil } func checkVersion(constraint string) error { diff --git a/pkg/tanka/parse_test.go b/pkg/tanka/parse_test.go index 2715e0e08..c8b8cdbd3 100644 --- a/pkg/tanka/parse_test.go +++ b/pkg/tanka/parse_test.go @@ -12,6 +12,7 @@ func TestEvalJsonnet(t *testing.T) { cases := []struct { baseDir string expected interface{} + env *v1alpha1.Config }{ { baseDir: "./testdata/cases/array/", @@ -25,18 +26,94 @@ func TestEvalJsonnet(t *testing.T) { map[string]interface{}{"testCase": "nestedArray[1][1]"}, }, }, + env: nil, }, { baseDir: "./testdata/cases/object/", expected: map[string]interface{}{ "testCase": "object", }, + env: nil, + }, + { + baseDir: "./testdata/cases/withspecjson/", + expected: map[string]interface{}{ + "testCase": "object", + }, + env: &v1alpha1.Config{ + APIVersion: v1alpha1.New().APIVersion, + Kind: v1alpha1.New().Kind, + Metadata: v1alpha1.Metadata{ + Name: "cases/withspecjson", + Labels: v1alpha1.New().Metadata.Labels, + }, + Spec: v1alpha1.Spec{ + APIServer: "https://localhost", + Namespace: "withspec", + }, + Data: map[string]interface{}{ + "testCase": "object", + }, + }, + }, + { + baseDir: "./testdata/cases/withspecjson/main.jsonnet", + expected: map[string]interface{}{ + "testCase": "object", + }, + env: &v1alpha1.Config{ + APIVersion: v1alpha1.New().APIVersion, + Kind: v1alpha1.New().Kind, + Metadata: v1alpha1.Metadata{ + Name: "cases/withspecjson", + Labels: v1alpha1.New().Metadata.Labels, + }, + Spec: v1alpha1.Spec{ + APIServer: "https://localhost", + Namespace: "withspec", + }, + Data: map[string]interface{}{ + "testCase": "object", + }, + }, + }, + { + baseDir: "./testdata/cases/withenv/main.jsonnet", + expected: v1alpha1.Config{ + APIVersion: v1alpha1.New().APIVersion, + Kind: v1alpha1.New().Kind, + Metadata: v1alpha1.Metadata{ + Name: "withenv", + }, + Spec: v1alpha1.Spec{ + APIServer: "https://localhost", + Namespace: "withenv", + }, + Data: map[string]interface{}{ + "testCase": "object", + }, + }, + env: &v1alpha1.Config{ + APIVersion: v1alpha1.New().APIVersion, + Kind: v1alpha1.New().Kind, + Metadata: v1alpha1.Metadata{ + Name: "withenv", + }, + Spec: v1alpha1.Spec{ + APIServer: "https://localhost", + Namespace: "withenv", + }, + Data: map[string]interface{}{ + "testCase": "object", + }, + }, }, } for _, test := range cases { - data, e := evalJsonnet(test.baseDir, v1alpha1.New(), jsonnet.Opts{}) + data, env, e := evalJsonnet(test.baseDir, jsonnet.Opts{}) assert.NoError(t, e) assert.Equal(t, test.expected, data) + assert.Equal(t, test.env, env) } } diff --git a/pkg/tanka/testdata/cases/withenv/main.jsonnet b/pkg/tanka/testdata/cases/withenv/main.jsonnet new file mode 100644 index 000000000..d6f4b7c2a --- /dev/null +++ b/pkg/tanka/testdata/cases/withenv/main.jsonnet @@ -0,0 +1,14 @@ +{ + apiVersion: 'tanka.dev/v1alpha1', + kind: 'Environment', + metadata: { + name: 'withenv', + }, + spec: { + apiServer: 'https://localhost', + namespace: 'withenv', + }, + data: { + testCase: 'object', + }, +} diff --git a/pkg/tanka/testdata/cases/withspecjson/main.jsonnet b/pkg/tanka/testdata/cases/withspecjson/main.jsonnet new file mode 100644 index 000000000..157b04a42 --- /dev/null +++ b/pkg/tanka/testdata/cases/withspecjson/main.jsonnet @@ -0,0 +1,3 @@ +{ + testCase: 'object', +} diff --git a/pkg/tanka/testdata/cases/withspecjson/spec.json b/pkg/tanka/testdata/cases/withspecjson/spec.json new file mode 100644 index 000000000..9c70d6b21 --- /dev/null +++ b/pkg/tanka/testdata/cases/withspecjson/spec.json @@ -0,0 +1,11 @@ +{ + "apiVersion": "tanka.dev/v1alpha1", + "kind": "Environment", + "metadata": { + "name": "withspec" + }, + "spec": { + "apiServer": "https://localhost", + "namespace": "withspec" + } +}