diff --git a/cmd/diff.go b/cmd/diff.go index def0ff548ef28b993fcd0f49fcc887143f46293d..93dcec1ddecc9998ecbf6e4cdacb7e0a98f48058 100644 --- a/cmd/diff.go +++ b/cmd/diff.go @@ -315,7 +315,8 @@ func expandEnvObjs(cmd *cobra.Command, env string, manager metadata.Manager) ([] return nil, err } - libPath, vendorPath, envLibPath, envComponentPath, envParamsPath := manager.LibPaths(env) + libPath, vendorPath := manager.LibPaths() + metadataPath, mainPath, paramsPath, specPath := manager.EnvPaths(env) componentPaths, err := manager.ComponentPaths() if err != nil { return nil, err @@ -325,12 +326,13 @@ func expandEnvObjs(cmd *cobra.Command, env string, manager metadata.Manager) ([] if err != nil { return nil, err } - params := importParams(string(envParamsPath)) + params := importParams(string(paramsPath)) + spec := importEnv(string(specPath)) - expander.FlagJpath = append([]string{string(libPath), string(vendorPath), string(envLibPath)}, expander.FlagJpath...) - expander.ExtCodes = append([]string{baseObj, params}, expander.ExtCodes...) + expander.FlagJpath = append([]string{string(libPath), string(vendorPath), string(metadataPath)}, expander.FlagJpath...) + expander.ExtCodes = append([]string{baseObj, params, spec}, expander.ExtCodes...) - envFiles := []string{string(envComponentPath)} + envFiles := []string{string(mainPath)} return expander.Expand(envFiles) } diff --git a/cmd/prototype.go b/cmd/prototype.go index ad2f909e9db525a6ba1e656697e3ecc75e619e34..5df4520bb1d4e111d750fc0a525b5e8463c8656c 100644 --- a/cmd/prototype.go +++ b/cmd/prototype.go @@ -449,7 +449,7 @@ expand prototypes into Jsonnet files. ks prototype use io.ksonnet.pkg.single-port-deployment nginx-depl \ --image=nginx - If the optional ` + "`--name`" + ` tag is not specified, all Kubernetes API resources + If the optional ` + "`--name`" + ` tag is not specified, all Kubernetes API resources declared by this prototype use this argument as their own ` + "`metadata.name`" + ` 3. Prototypes can be further customized by passing in **parameters** via additional @@ -503,7 +503,10 @@ func expandPrototype(proto *prototype.SpecificationSchema, templateType prototyp if !utils.IsASCIIIdentifier(componentName) { componentsText = fmt.Sprintf(`components["%s"]`, componentName) } - template = append([]string{`local params = std.extVar("` + metadata.ParamsExtCodeKey + `").` + componentsText + ";"}, template...) + template = append([]string{ + `local env = std.extVar("` + metadata.EnvExtCodeKey + `");`, + `local params = std.extVar("` + metadata.ParamsExtCodeKey + `").` + componentsText + ";"}, + template...) return jsonnet.Parse(componentName, strings.Join(template, "\n")) } diff --git a/cmd/root.go b/cmd/root.go index db6403c2cf7a276ef36d90aa239c35edc0249026..947b07e4bc139fad3034a55541828828e27eab64 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -399,8 +399,10 @@ func expandEnvCmdObjs(cmd *cobra.Command, env string, components []string, cwd m return nil, err } - libPath, vendorPath, envLibPath, envComponentPath, envParamsPath := manager.LibPaths(env) - expander.FlagJpath = append([]string{string(libPath), string(vendorPath), string(envLibPath)}, expander.FlagJpath...) + libPath, vendorPath := manager.LibPaths() + metadataPath, mainPath, paramsPath, specPath := manager.EnvPaths(env) + + expander.FlagJpath = append([]string{string(libPath), string(vendorPath), string(metadataPath)}, expander.FlagJpath...) componentPaths, err := manager.ComponentPaths() if err != nil { @@ -411,14 +413,20 @@ func expandEnvCmdObjs(cmd *cobra.Command, env string, components []string, cwd m if err != nil { return nil, err } - params := importParams(string(envParamsPath)) - expander.ExtCodes = append([]string{baseObj, params}, expander.ExtCodes...) + + // + // Set up ExtCodes to resolve runtime variables such as the environment namespace. + // + + params := importParams(string(paramsPath)) + spec := importEnv(string(specPath)) + expander.ExtCodes = append([]string{baseObj, params, spec}, expander.ExtCodes...) // // Expand the ksonnet app as rendered for environment `env`. // - return expander.Expand([]string{string(envComponentPath)}) + return expander.Expand([]string{string(mainPath)}) } // constructBaseObj constructs the base Jsonnet object that represents k-v @@ -502,3 +510,7 @@ func constructBaseObj(componentPaths, componentNames []string) (string, error) { func importParams(path string) string { return fmt.Sprintf(`%s=import "%s"`, metadata.ParamsExtCodeKey, path) } + +func importEnv(path string) string { + return fmt.Sprintf(`%s=import "%s"`, metadata.EnvExtCodeKey, path) +} diff --git a/metadata/interface.go b/metadata/interface.go index e406a5715e3990d0b3b2927e71573dbd7c9d4d93..90c1b7461da28c417dfb1452c3f60b37d3881cc2 100644 --- a/metadata/interface.go +++ b/metadata/interface.go @@ -45,7 +45,8 @@ type AbsPaths []string // libraries; and other non-core-application tasks. type Manager interface { Root() AbsPath - LibPaths(envName string) (libPath, vendorPath, envLibPath, envComponentPath, envParamsPath AbsPath) + LibPaths() (libPath, vendorPath AbsPath) + EnvPaths(env string) (metadataPath, mainPath, paramsPath, specPath AbsPath) // Components API. ComponentPaths() (AbsPaths, error) diff --git a/metadata/manager.go b/metadata/manager.go index 4aab3ffeff6dc5e9c25673a2ee52132107ac8ebb..d477a1b49d90a649a0c44089507486737fefa14c 100644 --- a/metadata/manager.go +++ b/metadata/manager.go @@ -51,7 +51,9 @@ const ( // ComponentsExtCodeKey is the ExtCode key for component imports ComponentsExtCodeKey = "__ksonnet/components" - // ParamsExtCodeKey is the ExtCode key for importing environment parameters + // EnvExtCodeKey is the ExtCode key for importing environment metadata + EnvExtCodeKey = "__ksonnet/environments" + // ParamsExtCodeKey is the ExtCode key for importing component parameters ParamsExtCodeKey = "__ksonnet/params" // User-level ksonnet directories. @@ -197,10 +199,23 @@ func (m *manager) Root() AbsPath { return m.rootPath } -func (m *manager) LibPaths(envName string) (libPath, vendorPath, envLibPath, envComponentPath, envParamsPath AbsPath) { - envPath := appendToAbsPath(m.environmentsPath, envName) - return m.libPath, m.vendorPath, appendToAbsPath(envPath, metadataDirName), - appendToAbsPath(envPath, envFileName), appendToAbsPath(envPath, componentParamsFile) +func (m *manager) LibPaths() (libPath, vendorPath AbsPath) { + return m.libPath, m.vendorPath +} + +func (m *manager) EnvPaths(env string) (metadataPath, mainPath, paramsPath, specPath AbsPath) { + envPath := appendToAbsPath(m.environmentsPath, env) + + // .metadata directory + metadataPath = appendToAbsPath(envPath, metadataDirName) + // main.jsonnet file + mainPath = appendToAbsPath(envPath, envFileName) + // params.libsonnet file + paramsPath = appendToAbsPath(envPath, componentParamsFile) + // spec.json file + specPath = appendToAbsPath(envPath, specFilename) + + return } func (m *manager) GetComponentParams(component string) (param.Params, error) { diff --git a/metadata/manager_test.go b/metadata/manager_test.go index 72cb32d2f43aac834fd0e2b6e04f40bb817bba0d..9c4cf463005d0c697b6c00f40be286ebd0b21fef 100644 --- a/metadata/manager_test.go +++ b/metadata/manager_test.go @@ -210,26 +210,38 @@ func TestLibPaths(t *testing.T) { appName := "test-lib-paths" expectedVendorPath := path.Join(appName, vendorDir) expectedLibPath := path.Join(appName, libDir) - expectedEnvLibPath := path.Join(appName, environmentsDir, mockEnvName, metadataDirName) - expectedEnvComponentPath := path.Join(appName, environmentsDir, mockEnvName, envFileName) - expectedEnvParamsPath := path.Join(appName, environmentsDir, mockEnvName, paramsFileName) m := mockEnvironments(t, appName) - libPath, vendorPath, envLibPath, envComponentPath, envParamsPath := m.LibPaths(mockEnvName) + libPath, vendorPath := m.LibPaths() if string(libPath) != expectedLibPath { t.Fatalf("Expected lib path to be:\n '%s'\n, got:\n '%s'", expectedLibPath, libPath) } if string(vendorPath) != expectedVendorPath { t.Fatalf("Expected vendor lib path to be:\n '%s'\n, got:\n '%s'", expectedVendorPath, vendorPath) } - if string(envLibPath) != expectedEnvLibPath { - t.Fatalf("Expected environment lib path to be:\n '%s'\n, got:\n '%s'", expectedEnvLibPath, envLibPath) +} + +func TestEnvPaths(t *testing.T) { + appName := "test-env-paths" + expectedMetadataPath := path.Join(appName, environmentsDir, mockEnvName, metadataDirName) + expectedMainPath := path.Join(appName, environmentsDir, mockEnvName, envFileName) + expectedParamsPath := path.Join(appName, environmentsDir, mockEnvName, paramsFileName) + expectedSpecPath := path.Join(appName, environmentsDir, mockEnvName, specFilename) + m := mockEnvironments(t, appName) + + metadataPath, mainPath, paramsPath, specPath := m.EnvPaths(mockEnvName) + + if string(metadataPath) != expectedMetadataPath { + t.Fatalf("Expected environment metadata dir path to be:\n '%s'\n, got:\n '%s'", expectedMetadataPath, metadataPath) + } + if string(mainPath) != expectedMainPath { + t.Fatalf("Expected environment main path to be:\n '%s'\n, got:\n '%s'", expectedMainPath, mainPath) } - if string(envComponentPath) != expectedEnvComponentPath { - t.Fatalf("Expected environment component path to be:\n '%s'\n, got:\n '%s'", expectedEnvComponentPath, envComponentPath) + if string(paramsPath) != expectedParamsPath { + t.Fatalf("Expected environment params path to be:\n '%s'\n, got:\n '%s'", expectedParamsPath, paramsPath) } - if string(envParamsPath) != expectedEnvParamsPath { - t.Fatalf("Expected environment params path to be:\n '%s'\n, got:\n '%s'", expectedEnvParamsPath, envParamsPath) + if string(specPath) != expectedSpecPath { + t.Fatalf("Expected environment spec path to be:\n '%s'\n, got:\n '%s'", expectedSpecPath, specPath) } } diff --git a/prototype/snippet/jsonnet/snippet.go b/prototype/snippet/jsonnet/snippet.go index ffde16ed3e0ec6f03f2d27fa3b1e5904b0c6c2f5..7324bfb33b0061922e52726768512bfcb503ee0a 100644 --- a/prototype/snippet/jsonnet/snippet.go +++ b/prototype/snippet/jsonnet/snippet.go @@ -17,6 +17,7 @@ package jsonnet import ( "errors" + "fmt" "sort" "strings" @@ -27,6 +28,9 @@ import ( const ( paramPrefix = "param://" paramReplacementPrefix = "params." + + envPrefix = "env://" + envReplacementPrefix = "env." ) // Parse rewrites the imports in a Jsonnet file before returning the snippet. @@ -52,14 +56,14 @@ func parse(fn string, jsonnet string) (string, error) { var imports []ast.Import - // Gather all parameter imports + // Gather all parameter or environment imports err = visit(root, &imports) if err != nil { return "", err } - // Replace all parameter imports - return replace(jsonnet, imports), nil + // Replace all parameter or environment imports + return replace(jsonnet, imports) } // --------------------------------------------------------------------------- @@ -67,11 +71,17 @@ func parse(fn string, jsonnet string) (string, error) { func visit(node ast.Node, imports *[]ast.Import) error { switch n := node.(type) { case *ast.Import: - // Add parameter-type imports to the list of replacements. + // Add parameter/environment type imports to the list of replacements. if strings.HasPrefix(n.File.Value, paramPrefix) { param := strings.TrimPrefix(n.File.Value, paramPrefix) if len(param) < 1 { - return errors.New("There must be a parameter following import param://") + return fmt.Errorf("There must be a parameter following import %s", paramPrefix) + } + *imports = append(*imports, *n) + } else if strings.HasPrefix(n.File.Value, envPrefix) { + env := strings.TrimPrefix(n.File.Value, envPrefix) + if len(env) < 1 { + return fmt.Errorf("There must be a attribute following import %s", envPrefix) } *imports = append(*imports, *n) } @@ -297,8 +307,10 @@ func visitLocalBind(node ast.LocalBind, imports *[]ast.Import) error { // --------------------------------------------------------------------------- // replace converts all parameters in the passed Jsonnet of form -// `import 'param://port'` into `params.port`. -func replace(jsonnet string, imports []ast.Import) string { +// +// 1. `import 'param://port'` into `params.port`. +// 2. `import 'env://namespace'` into `env.namespace`. +func replace(jsonnet string, imports []ast.Import) (string, error) { lines := strings.Split(jsonnet, "\n") // Imports must be sorted by reverse location to avoid indexing problems @@ -311,28 +323,35 @@ func replace(jsonnet string, imports []ast.Import) string { }) for _, im := range imports { - param := paramReplacementPrefix + strings.TrimPrefix(im.File.Value, paramPrefix) + var replacement string + if strings.HasPrefix(im.File.Value, paramPrefix) { + replacement = paramReplacementPrefix + strings.TrimPrefix(im.File.Value, paramPrefix) + } else if strings.HasPrefix(im.File.Value, envPrefix) { + replacement = envReplacementPrefix + strings.TrimPrefix(im.File.Value, envPrefix) + } else { + return "", fmt.Errorf("Found unsupported import prefix in %s", im.File.Value) + } lineStart := im.Loc().Begin.Line lineEnd := im.Loc().End.Line colStart := im.Loc().Begin.Column colEnd := im.Loc().End.Column - // Case where import param is split over multiple strings. + // Case where import is split over multiple strings. if lineEnd != lineStart { // Replace all intermediate lines with the empty string. for i := lineStart; i < lineEnd-1; i++ { lines[i] = "" } - // Remove import param related logic from the last line. + // Remove import related logic from the last line. lines[lineEnd-1] = lines[lineEnd-1][colEnd:len(lines[lineEnd-1])] - // Perform replacement in the first line of import param occurance. - lines[lineStart-1] = lines[lineStart-1][:colStart-1] + param + // Perform replacement in the first line of import occurance. + lines[lineStart-1] = lines[lineStart-1][:colStart-1] + replacement } else { line := lines[lineStart-1] - lines[lineStart-1] = line[:colStart-1] + param + line[colEnd:len(line)] + lines[lineStart-1] = line[:colStart-1] + replacement + line[colEnd:len(line)] } } - return strings.Join(lines, "\n") + return strings.Join(lines, "\n"), nil } diff --git a/prototype/snippet/jsonnet/snippet_test.go b/prototype/snippet/jsonnet/snippet_test.go index d2f60bbc349df67b2fde5ec6daf0a325f81fef89..e95b066b0dd56bf6402f51c68d8f8c8cc920800c 100644 --- a/prototype/snippet/jsonnet/snippet_test.go +++ b/prototype/snippet/jsonnet/snippet_test.go @@ -135,6 +135,32 @@ func TestParse(t *testing.T) { `local f = f; { foo: f, }`, }, + // Test where there are multiple import types. + { + ` + local k = import 'ksonnet.beta.2/k.libsonnet'; + + local service = k.core.v1.service; + local servicePort = k.core.v1.service.mixin.spec.portsType; + local port = servicePort.new((import 'param://port'), (import 'param://portName')); + + local namespace = import 'env://namespace'; + + local name = import 'param://name'; + k.core.v1.service.new('%s-service' % [name], {app: name}, port)`, + + ` + local k = import 'ksonnet.beta.2/k.libsonnet'; + + local service = k.core.v1.service; + local servicePort = k.core.v1.service.mixin.spec.portsType; + local port = servicePort.new((params.port), (params.portName)); + + local namespace = env.namespace; + + local name = params.name; + k.core.v1.service.new('%s-service' % [name], {app: name}, port)`, + }, } errors := []string{