diff --git a/cmd/root.go b/cmd/root.go index 21bba34b394aef78d07e2cdfdf5ebf333e86b95b..372f8875ea8647e2fa26b36e16b315929d4678e6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -59,8 +59,6 @@ const ( // environment or the -f flag. flagFile = "file" flagFileShort = "f" - - componentsExtCodeKey = "__ksonnet/components" ) var clientConfig clientcmd.ClientConfig @@ -372,16 +370,17 @@ func expandEnvCmdObjs(cmd *cobra.Command, envSpec *envSpec, cwd metadata.AbsPath return nil, err } - libPath, envLibPath := manager.LibPaths(*envSpec.env) + libPath, envLibPath, envComponentPath := manager.LibPaths(*envSpec.env) expander.FlagJpath = append([]string{string(libPath), string(envLibPath)}, expander.FlagJpath...) if !filesPresent { - fileNames, err = manager.ComponentPaths() + componentPaths, err := manager.ComponentPaths() if err != nil { return nil, err } - baseObjExtCode := fmt.Sprintf("%s=%s", componentsExtCodeKey, constructBaseObj(fileNames)) - expander.ExtCodes = append([]string{baseObjExtCode}) + baseObjExtCode := fmt.Sprintf("%s=%s", metadata.ComponentsExtCodeKey, constructBaseObj(componentPaths)) + expander.ExtCodes = append([]string{baseObjExtCode}, expander.ExtCodes...) + fileNames = []string{string(envComponentPath)} } } diff --git a/metadata/environment.go b/metadata/environment.go index e9243996e26e66b3f79b357f9d8272e0410f9c0d..850cbc3bfb384f44c773e190878b44d8018deca8 100644 --- a/metadata/environment.go +++ b/metadata/environment.go @@ -16,9 +16,11 @@ package metadata import ( + "bytes" "encoding/json" "fmt" "os" + "path" "path/filepath" "strings" @@ -30,7 +32,8 @@ import ( ) const ( - defaultEnvName = "default" + defaultEnvName = "default" + metadataDirName = ".metadata" schemaFilename = "swagger.json" extensionsLibFilename = "k.libsonnet" @@ -87,11 +90,17 @@ func (m *manager) createEnvironment(name, uri string, extensionsLibData, k8sLibD return err } + metadataPath := appendToAbsPath(envPath, metadataDirName) + err = m.appFS.MkdirAll(string(metadataPath), defaultFolderPermissions) + if err != nil { + return err + } + log.Infof("Generating environment metadata at path '%s'", envPath) // Generate the schema file. log.Debugf("Generating '%s', length: %d", schemaFilename, len(specData)) - schemaPath := appendToAbsPath(envPath, schemaFilename) + schemaPath := appendToAbsPath(metadataPath, schemaFilename) err = afero.WriteFile(m.appFS, string(schemaPath), specData, defaultFilePermissions) if err != nil { log.Debugf("Failed to write '%s'", schemaFilename) @@ -99,7 +108,7 @@ func (m *manager) createEnvironment(name, uri string, extensionsLibData, k8sLibD } log.Debugf("Generating '%s', length: %d", k8sLibFilename, len(k8sLibData)) - k8sLibPath := appendToAbsPath(envPath, k8sLibFilename) + k8sLibPath := appendToAbsPath(metadataPath, k8sLibFilename) err = afero.WriteFile(m.appFS, string(k8sLibPath), k8sLibData, defaultFilePermissions) if err != nil { log.Debugf("Failed to write '%s'", k8sLibFilename) @@ -107,13 +116,24 @@ func (m *manager) createEnvironment(name, uri string, extensionsLibData, k8sLibD } log.Debugf("Generating '%s', length: %d", extensionsLibFilename, len(extensionsLibData)) - extensionsLibPath := appendToAbsPath(envPath, extensionsLibFilename) + extensionsLibPath := appendToAbsPath(metadataPath, extensionsLibFilename) err = afero.WriteFile(m.appFS, string(extensionsLibPath), extensionsLibData, defaultFilePermissions) if err != nil { log.Debugf("Failed to write '%s'", extensionsLibFilename) return err } + // Generate the environment .jsonnet file + overrideFileName := path.Base(name) + ".jsonnet" + overrideData := m.generateOverrideData() + log.Debugf("Generating '%s', length: %d", overrideFileName, len(overrideData)) + overrideLibPath := appendToAbsPath(envPath, overrideFileName) + err = afero.WriteFile(m.appFS, string(overrideLibPath), overrideData, defaultFilePermissions) + if err != nil { + log.Debugf("Failed to write '%s'", overrideFileName) + return err + } + // Generate the environment spec file. envSpecData, err := generateSpecData(uri) if err != nil { @@ -327,6 +347,17 @@ func (m *manager) generateKsonnetLibData(spec ClusterSpec) ([]byte, []byte, []by return extensionsLibData, k8sLibData, text, err } +func (m *manager) generateOverrideData() []byte { + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf("local base = import \"%s\";\n", m.baseLibsonnetPath)) + buf.WriteString(fmt.Sprintf("local k = import \"%s\";\n\n", extensionsLibFilename)) + buf.WriteString("base + {\n") + buf.WriteString(" // Insert user-specified overrides here. For example if a component is named \"nginx-deployment\", you might have something like:\n") + buf.WriteString(" // \"nginx-deployment\"+: k.deployment.mixin.metadata.labels({foo: \"bar\"})\n") + buf.WriteString("}\n") + return buf.Bytes() +} + func generateSpecData(uri string) ([]byte, error) { // Format the spec json and return; preface keys with 2 space idents. return json.MarshalIndent(EnvironmentSpec{URI: uri}, "", " ") diff --git a/metadata/environment_test.go b/metadata/environment_test.go index 2d9d2d73925cf48b3a4dcc1758662fabce4fe242..e4350417444bb9338d35d40217a2b8c906a27041 100644 --- a/metadata/environment_test.go +++ b/metadata/environment_test.go @@ -180,3 +180,21 @@ func TestSetEnvironment(t *testing.T) { t.Fatalf("Expected set URI to be \"%s\", got:\n %s", set.URI, envSpec.URI) } } + +func TestGenerateOverrideData(t *testing.T) { + m := mockEnvironments(t, "test-gen-override-data") + + expected := `local base = import "test-gen-override-data/environments/base.libsonnet"; +local k = import "k.libsonnet"; + +base + { + // Insert user-specified overrides here. For example if a component is named "nginx-deployment", you might have something like: + // "nginx-deployment"+: k.deployment.mixin.metadata.labels({foo: "bar"}) +} +` + result := m.generateOverrideData() + + if string(result) != expected { + t.Fatalf("Expected to generate override file with data:\n%s\n,got:\n%s", expected, result) + } +} diff --git a/metadata/interface.go b/metadata/interface.go index afdb46dfefe106e48243a9a02a5819a77c01f42d..5c22265ecbdcda1b86777f9dd50db32916e5ce9e 100644 --- a/metadata/interface.go +++ b/metadata/interface.go @@ -43,7 +43,7 @@ type Manager interface { Root() AbsPath ComponentPaths() (AbsPaths, error) CreateComponent(name string, text string, templateType prototype.TemplateType) error - LibPaths(envName string) (libPath, envLibPath AbsPath) + LibPaths(envName string) (libPath, envLibPath, envComponentPath AbsPath) CreateEnvironment(name, uri string, spec ClusterSpec) error DeleteEnvironment(name string) error GetEnvironments() ([]*Environment, error) diff --git a/metadata/manager.go b/metadata/manager.go index 55022151069a269c71fcacc720ce6c90be24172e..2792d4051854417282f48b97f2d434e07b43c6e1 100644 --- a/metadata/manager.go +++ b/metadata/manager.go @@ -38,6 +38,11 @@ const ( componentsDir = "components" environmentsDir = "environments" vendorDir = "vendor" + + baseLibsonnetFile = "base.libsonnet" + + // ComponentsExtCodeKey is the ExtCode key for component imports + ComponentsExtCodeKey = "__ksonnet/components" ) type manager struct { @@ -49,6 +54,8 @@ type manager struct { componentsPath AbsPath environmentsPath AbsPath vendorDir AbsPath + + baseLibsonnetPath AbsPath } func findManager(abs AbsPath, appFS afero.Fs) (*manager, error) { @@ -115,6 +122,8 @@ func newManager(rootPath AbsPath, appFS afero.Fs) *manager { componentsPath: appendToAbsPath(rootPath, componentsDir), environmentsPath: appendToAbsPath(rootPath, environmentsDir), vendorDir: appendToAbsPath(rootPath, vendorDir), + + baseLibsonnetPath: appendToAbsPath(rootPath, environmentsDir, baseLibsonnetFile), } } @@ -169,8 +178,9 @@ func (m *manager) CreateComponent(name string, text string, templateType prototy return afero.WriteFile(m.appFS, componentPath, []byte(text), defaultFilePermissions) } -func (m *manager) LibPaths(envName string) (libPath, envLibPath AbsPath) { - return m.libPath, appendToAbsPath(m.environmentsPath, envName) +func (m *manager) LibPaths(envName string) (libPath, envLibPath, envComponentPath AbsPath) { + envPath := appendToAbsPath(m.environmentsPath, envName) + return m.libPath, appendToAbsPath(envPath, metadataDirName), appendToAbsPath(envPath, path.Base(envName)+".jsonnet") } func (m *manager) createAppDirTree() error { @@ -181,7 +191,7 @@ func (m *manager) createAppDirTree() error { return fmt.Errorf("Could not create app; directory '%s' already exists", m.rootPath) } - paths := []AbsPath{ + dirPaths := []AbsPath{ m.rootPath, m.ksonnetPath, m.libPath, @@ -190,11 +200,19 @@ func (m *manager) createAppDirTree() error { m.vendorDir, } - for _, p := range paths { + for _, p := range dirPaths { if err := m.appFS.MkdirAll(string(p), defaultFolderPermissions); err != nil { return err } } - return nil + return afero.WriteFile(m.appFS, string(m.baseLibsonnetPath), genBaseLibsonnetContent(), defaultFilePermissions) +} + +func genBaseLibsonnetContent() []byte { + return []byte(`local components = std.extVar("` + ComponentsExtCodeKey + `"); +components + { + // Insert user-specified overrides here. +} +`) } diff --git a/metadata/manager_test.go b/metadata/manager_test.go index 66c8f2d76bae5a6908c3930039166e5e9487aaf7..c3f40ad2c73ba47220044bc07b0ac75ab4958a1a 100644 --- a/metadata/manager_test.go +++ b/metadata/manager_test.go @@ -17,6 +17,7 @@ package metadata import ( "fmt" "os" + "path" "sort" "testing" @@ -84,8 +85,10 @@ func TestInitSuccess(t *testing.T) { } } - envPath := appendToAbsPath(appPath, string(defaultEnvDir)) - schemaPath := appendToAbsPath(envPath, schemaFilename) + envPath := appendToAbsPath(appPath, string(environmentsDir)) + metadataPath := appendToAbsPath(appPath, string(defaultEnvDir), string(metadataDirName)) + + schemaPath := appendToAbsPath(metadataPath, schemaFilename) bytes, err := afero.ReadFile(testFS, string(schemaPath)) if err != nil { t.Fatalf("Failed to read swagger file at '%s':\n%v", schemaPath, err) @@ -93,7 +96,7 @@ func TestInitSuccess(t *testing.T) { t.Fatalf("Expected swagger file at '%s' to have value: '%s', got: '%s'", schemaPath, blankSwaggerData, actualSwagger) } - k8sLibPath := appendToAbsPath(envPath, k8sLibFilename) + k8sLibPath := appendToAbsPath(metadataPath, k8sLibFilename) k8sLibBytes, err := afero.ReadFile(testFS, string(k8sLibPath)) if err != nil { t.Fatalf("Failed to read ksonnet-lib file at '%s':\n%v", k8sLibPath, err) @@ -101,13 +104,21 @@ func TestInitSuccess(t *testing.T) { t.Fatalf("Expected swagger file at '%s' to have value: '%s', got: '%s'", k8sLibPath, blankK8sLib, actualK8sLib) } - extensionsLibPath := appendToAbsPath(envPath, extensionsLibFilename) + extensionsLibPath := appendToAbsPath(metadataPath, extensionsLibFilename) extensionsLibBytes, err := afero.ReadFile(testFS, string(extensionsLibPath)) if err != nil { t.Fatalf("Failed to read ksonnet-lib file at '%s':\n%v", extensionsLibPath, err) } else if string(extensionsLibBytes) == "" { t.Fatalf("Expected extension library file at '%s' to be non-empty", extensionsLibPath) } + + baseLibsonnetPath := appendToAbsPath(envPath, baseLibsonnetFile) + baseLibsonnetBytes, err := afero.ReadFile(testFS, string(baseLibsonnetPath)) + if err != nil { + t.Fatalf("Failed to read base.libsonnet file at '%s':\n%v", baseLibsonnetPath, err) + } else if len(baseLibsonnetBytes) == 0 { + t.Fatalf("Expected base.libsonnet at '%s' to be non-empty", baseLibsonnetPath) + } } func TestFindSuccess(t *testing.T) { @@ -200,6 +211,25 @@ func TestComponentPaths(t *testing.T) { } } +func TestLibPaths(t *testing.T) { + appName := "test-lib-paths" + expectedLibPath := path.Join(appName, libDir) + expectedEnvLibPath := path.Join(appName, environmentsDir, mockEnvName, metadataDirName) + expectedEnvComponentPath := path.Join(appName, environmentsDir, mockEnvName, path.Base(mockEnvName)+".jsonnet") + m := mockEnvironments(t, appName) + + libPath, envLibPath, envComponentPath := m.LibPaths(mockEnvName) + if string(libPath) != expectedLibPath { + t.Fatalf("Expected lib path to be:\n '%s'\n, got:\n '%s'", expectedLibPath, libPath) + } + if string(envLibPath) != expectedEnvLibPath { + t.Fatalf("Expected environment lib path to be:\n '%s'\n, got:\n '%s'", expectedEnvLibPath, envLibPath) + } + if string(envComponentPath) != expectedEnvComponentPath { + t.Fatalf("Expected environment component path to be:\n '%s'\n, got:\n '%s'", expectedEnvComponentPath, envComponentPath) + } +} + func TestFindFailure(t *testing.T) { findFailure := func(t *testing.T, currDir AbsPath) { _, err := findManager(currDir, testFS)